随着程序变得越来越复杂,把所有代码都堆在 main
函数里会变得难以管理和阅读。函数 (Function) 允许你将代码分解成逻辑上独立、可重用的块。这就像把一个大任务分解成几个小任务,每个小任务交给一个专门的“工人”(函数)来完成。
1. 函数的定义与调用
定义 (Definition): 创建一个函数,告诉编译器这个函数叫什么名字,它需要什么输入(参数),它会返回什么输出(返回值),以及它具体要做什么(函数体)。
C++// 语法:
// 返回值类型 函数名(参数列表) {
// // 函数体: 具体的代码逻辑
// return 返回值; // 如果返回值类型不是 void
// }
// 示例:一个简单的加法函数
int add(int num1, int num2) { // 返回 int, 函数名 add, 接收两个 int 参数
int sum = num1 + num2;
return sum; // 返回计算结果
}
// 示例:一个没有返回值的问候函数 (void)
void greet(std::string name) { // 返回 void (无返回值), 接收一个 string 参数
std::cout << "你好, " << name << "!\n";
// 没有 return 语句,或者可以写 return; 来提前结束函数
}
int
, double
, void
表示不返回任何值)。()
内声明,多个参数用逗号 ,
分隔。每个参数都需要指定类型和名称。如果函数不需要输入,括号内为空 (void myFunc()
)。{}
内的代码,是函数实际执行的操作。return
语句: 用于从函数返回值。一旦执行 return
,函数立即结束。void
函数没有返回值,可以不写 return
或使用 return;
提前退出。调用 (Call): 在程序的其他地方(比如 main
函数或其他函数中)使用函数名和必要的参数来执行函数定义的代码。
#include
#include
// (在此处或之后定义 add 和 greet 函数,或者使用下面的函数原型)
int add(int num1, int num2) { /* ... */ }
void greet(std::string name) { /* ... */ }
int main() {
int result = add(5, 3); // 调用 add 函数,传入 5 和 3 作为参数
// 函数返回的值 8 被存储在 result 变量中
std::cout << "5 + 3 = " << result << std::endl; // 输出 8
greet("Alice"); // 调用 greet 函数,传入 "Alice"
// 函数会直接输出 "你好, Alice!"
int x = 10, y = 20;
int sum_xy = add(x, y); // 也可以使用变量作为参数
std::cout << x << " + " << y << " = " << sum_xy << std::endl; // 输出 30
return 0;
}
2. 函数原型 (声明)
C++ 编译器是按从上到下的顺序读取代码的。如果你在调用一个函数之前还没有定义它,编译器就不知道这个函数长什么样(需要什么参数,返回什么类型),就会报错。
解决方法有两种:
将函数定义放在调用它的代码之前: 对于小程序可行,但项目变大后难以管理。
使用函数原型 (Function Prototype) 或声明 (Declaration): 在调用函数之前,先写一行函数的“签名”,告诉编译器这个函数的基本信息,但省略函数体。函数原型看起来就像函数定义的第一行,但后面跟的是分号 ;
而不是花括号 {}
。
#include
#include
// 函数原型 (声明)
int add(int num1, int num2); // 告诉编译器:有一个叫 add 的函数,接收两个 int,返回 int
void greet(std::string name); // 告诉编译器:有一个叫 greet 的函数,接收 string,返回 void
int main() {
// 现在可以在 main 函数中调用这些函数了,即使它们的定义在后面
int result = add(15, 7);
std::cout << "15 + 7 = " << result << std::endl;
greet("Bob");
return 0;
}
// 函数定义 (实现) - 可以放在 main 函数之后
int add(int num1, int num2) {
return num1 + num2;
}
void greet(std::string name) {
std::cout << "你好, " << name << "!\n";
}
最佳实践: 通常将函数原型放在源文件的开头(#include
之后)或者单独的头文件 (.h
或 .hpp
) 中,而将函数定义放在源文件 (.cpp
) 的后面或另一个 .cpp
文件中。这使得代码结构更清晰。
3. 作用域 (Scope)
变量并非在程序的任何地方都可用。作用域定义了变量可以被访问的区域。
局部变量 (Local Variables):
if
, for
, while
的花括号 {}
内)声明的变量。{}
内部有效。一旦程序执行离开这个代码块,局部变量就会被销毁,它们占用的内存会被释放。void myFunction() {
int localVar = 10; // localVar 是局部变量
std::cout << localVar << std::endl; // 在函数内可以访问
}
int main() {
int mainVar = 5; // mainVar 是 main 函数的局部变量
// std::cout << localVar << std::endl; // 错误!无法访问 myFunction 的局部变量 localVar
myFunction();
return 0;
}
全局变量 (Global Variables):
#include
int globalVar = 100; // 全局变量
void printGlobal() {
std::cout << "printGlobal: " << globalVar << std::endl; // 可以访问全局变量
globalVar = 200; // 也可以修改全局变量
}
int main() {
std::cout << "main (before): " << globalVar << std::endl; // 输出 100
printGlobal(); // 调用函数,修改了 globalVar
std::cout << "main (after): " << globalVar << std::endl; // 输出 200
return 0;
}
警告: 应尽量避免使用全局变量!
优先使用局部变量和函数参数/返回值来传递数据。
4. 传值调用 (Pass by Value) vs 传引用调用 (Pass by Reference, &
)
当我们将变量作为参数传递给函数时,有两种主要方式:
传值调用 (Pass by Value): 这是 C++ 的默认方式。
#include
void modifyValue(int val) { // val 是 num 的一个副本
val = val * 2;
std::cout << "Inside function, val = " << val << std::endl; // 输出 20
}
int main() {
int num = 10;
std::cout << "Before function call, num = " << num << std::endl; // 输出 10
modifyValue(num);
std::cout << "After function call, num = " << num << std::endl; // 仍然输出 10,原始 num 未改变
return 0;
}
传引用调用 (Pass by Reference): 通过在函数参数类型后加上 &
符号实现。
swap
函数)。#include
void modifyReference(int& ref) { // ref 是 num 的一个引用(别名)
ref = ref * 2;
std::cout << "Inside function, ref = " << ref << std::endl; // 输出 20
}
// 经典的 swap 函数示例
void swap(int& a, int& b) { // 接收两个 int 引用
int temp = a;
a = b;
b = temp;
}
int main() {
int num = 10;
std::cout << "Before function call, num = " << num << std::endl; // 输出 10
modifyReference(num);
std::cout << "After function call, num = " << num << std::endl; // 输出 20,原始 num 被改变了!
int x = 5, y = 9;
std::cout << "Before swap: x = " << x << ", y = " << y << std::endl; // 输出 5, 9
swap(x, y);
std::cout << "After swap: x = " << x << ", y = " << y << std::endl; // 输出 9, 5
return 0;
}
常量引用 (const &
): 如果你希望通过引用传递来避免复制开销,但又不希望函数修改原始数据,可以使用常量引用。
void printLargeData(const std::string& data) { // 使用常量引用传递字符串
std::cout << "Data: " << data << std::endl;
// data = "changed"; // 错误!不能通过常量引用修改数据
}
这是 C++ 中传递大型对象(如 string
, vector
)给函数时常用的高效且安全的方式。
5. 函数重载 (Function Overloading)
C++ 允许你定义多个同名的函数,只要它们的参数列表不同即可。参数列表的不同可以体现在:
注意: 函数的返回值类型不能作为区分重载函数的依据。
编译器会根据你调用函数时提供的参数类型和数量来自动选择匹配哪个版本的重载函数。
C++
#include
#include
// 重载 print 函数
void print(int value) {
std::cout << "Integer: " << value << std::endl;
}
void print(double value) {
std::cout << "Double: " << value << std::endl;
}
void print(std::string value) {
std::cout << "String: \"" << value << "\"" << std::endl;
}
int main() {
print(10); // 调用 print(int)
print(3.14); // 调用 print(double)
print("Hello"); // 调用 print(std::string) - C风格字符串字面量可以隐式转换为 std::string
return 0;
}
函数重载使得你可以为逻辑上相似但处理不同数据类型的操作使用同一个函数名,让代码更直观。
6. 实战练习
重构简易计算器:
calculator.cpp
(模块二的项目)。double add(double n1, double n2)
, double subtract(double n1, double n2)
, double multiply(double n1, double n2)
, double divide(double n1, double n2)
。main
函数中的加、减、乘、除计算逻辑分别移动到这四个函数中,并使用 return
返回结果。divide
函数内部处理除数为零的情况(例如,如果 n2
为 0,可以打印错误信息并返回一个特殊值,如 0.0
或 NaN
- 需要
)。main
函数,让它调用这些新定义的函数来获取计算结果并输出。记得在 main
函数之前添加函数原型或将函数定义放在 main
之前。为猜数字游戏添加输入验证函数:
int getValidIntInput(const std::string& prompt)
,它负责向用户显示提示信息 (prompt
),读取用户的整数输入,并确保用户输入的是一个有效的整数。如果输入无效,应提示用户重新输入,直到输入有效为止。while(true)
或 do-while
)。prompt
,然后使用 std::cin >> variable;
尝试读取。std::cin
的状态:
if (std::cin.fail())
: 如果读取失败(例如用户输入了字母),说明输入无效。
std::cin.clear();
// 清除 cin
的错误状态标志。std::cin.ignore(10000, '\n');
// 忽略掉输入缓冲区中错误的内容,直到遇到换行符或忽略了足够多的字符。continue;
跳过本次循环的剩余部分,重新提示输入。else
: 如果读取成功,说明输入有效,使用 break;
跳出循环。guessing_game.cpp
,用这个 getValidIntInput
函数来代替原来的 std::cin >> guess;
,让游戏更健壮。7. 实战项目 3: 增强版计算器
现在,我们将函数知识和之前的控制流知识结合起来,做一个功能更强、交互性更好的计算器。
%
、求幂 pow
等)。switch
或 if-else if-else
(处理菜单选择),循环 (while
或 do-while
控制程序主流程,直到用户退出),基本输入输出。#include
并使用 pow(base, exponent)
函数。注意处理除零、取模的除数为零等情况。void displayMenu()
函数,专门负责打印操作选项。main
逻辑:
do-while
或 while
循环来保持程序运行,直到用户选择退出。displayMenu()
显示菜单。int choice;
)。可以使用上面练习中写的 getValidIntInput
函数来确保输入是有效的整数。switch (choice)
或 if-else if-else
结构判断用户的选择:
case 1:
(加法)
num1
, num2
)。add(num1, num2)
函数。break;
case 2:
(减法) ... 以此类推。case 0:
(退出)
while(run)
,则设置 run = false;
)。break;
default:
(无效选项)
break;
return 0;
。这个项目能很好地锻炼你组织代码、使用函数进行模块化设计的能力。
目前我们处理的都是单个数据。但很多时候,我们需要处理一组数据,比如一个班级所有学生的分数,或者一个人的姓名(由多个字符组成)。这一模块将介绍如何处理这些数据集合。
1. 数组 (Array)
数组是最基本的数据集合,它可以在内存中连续存储固定数量的相同类型的元素。
声明: 数据类型 数组名[数组大小];
数组大小必须是一个常量表达式(在编译时就能确定)。
int scores[5]; // 声明一个可以存储 5 个 int 类型分数的数组
double prices[10]; // 声明一个可以存储 10 个 double 类型价格的数组
char grades[3]; // 声明一个可以存储 3 个 char 类型等级的数组
初始化: 可以在声明时使用花括号 {}
提供初始值。
int scores[5] = {85, 92, 78, 95, 88}; // 提供所有 5 个元素的初始值
double prices[10] = {9.9, 15.5, 8.0}; // 只提供前 3 个,其余元素会被自动初始化为 0
char grades[] = {'A', 'B', 'C'}; // 可以不指定大小,编译器会根据初始值数量自动推断 (大小为 3)
int counts[5] = {}; // 所有元素初始化为 0
访问元素: 通过索引 (index) 来访问数组中的特定元素。索引从 0 开始!对于大小为 N
的数组,有效的索引范围是 0
到 N-1
。使用方括号 []
进行访问。
int scores[5] = {85, 92, 78, 95, 88};
std::cout << "第一个分数: " << scores[0] << std::endl; // 输出 85 (索引 0)
std::cout << "第三个分数: " << scores[2] << std::endl; // 输出 78 (索引 2)
scores[0] = 90; // 修改第一个元素的值
std::cout << "修改后的第一个分数: " << scores[0] << std::endl; // 输出 90
// std::cout << scores[5] << std::endl; // 错误!索引越界 (有效的索引是 0 到 4)
// 访问越界是危险的,可能导致程序崩溃或不可预测的行为!
警告: C++ 不会自动检查数组索引是否越界。访问无效索引是常见的、危险的错误来源。
遍历数组: 通常使用 for
循环来遍历数组的所有元素。
const int NUM_SCORES = 5; // 使用常量表示数组大小是好习惯
int scores[NUM_SCORES] = {85, 92, 78, 95, 88};
double total = 0;
for (int i = 0; i < NUM_SCORES; ++i) { // i 从 0 循环到 NUM_SCORES - 1
std::cout << "分数 " << (i + 1) << ": " << scores[i] << std::endl;
total += scores[i]; // 累加分数
}
double average = total / NUM_SCORES;
std::cout << "平均分: " << average << std::endl;
数组作为函数参数:
#include
// 函数原型,接收一个 int 数组和它的大小
void printArray(int arr[], int size);
void modifyArray(int arr[], int size);
int main() {
const int ARRAY_SIZE = 3;
int numbers[ARRAY_SIZE] = {10, 20, 30};
std::cout << "原始数组: ";
printArray(numbers, ARRAY_SIZE); // 输出 10 20 30
modifyArray(numbers, ARRAY_SIZE);
std::cout << "修改后数组: ";
printArray(numbers, ARRAY_SIZE); // 输出 11 21 31 (原始数组被修改了)
return 0;
}
// 打印数组元素的函数
void printArray(int arr[], int size) { // arr 实际上是一个指向数组首元素的指针
for (int i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
// 修改数组元素的函数
void modifyArray(int arr[], int size) {
for (int i = 0; i < size; ++i) {
arr[i] = arr[i] + 1; // 直接修改了原始数组的元素
}
}
2. C 风格字符串 (简要介绍)
在 C++ 引入 std::string
之前,主要使用字符数组来表示字符串,并以一个特殊的空终止符 \0
(null terminator) 结尾来标记字符串的结束。
C++
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 需要手动添加 \0
char name[] = "Alice"; // 字符串字面量会自动在末尾添加 \0 (实际大小是 6)
你需要使用
(或
) 中的函数(如 strlen
计算长度 - 不含 \0
, strcpy
复制字符串, strcat
拼接字符串)来操作它们。
缺点:
\0
,并且像 strcpy
这样的函数如果不小心,很容易造成缓冲区溢出(写入的数据超出了数组分配的空间),这是一个严重的安全隐患。std::string
直观。结论: 了解 C 风格字符串有助于理解一些底层概念和旧代码,但在现代 C++ 编程中,强烈建议使用 std::string
。
3. std::string
(现代 C++ 字符串 - 重点)
std::string
是 C++ 标准库提供的一个类,专门用于方便、安全地处理字符串(文本)。
使用: 需要包含头文件 #include
。
创建与初始化:
C++#include
#include
int main() {
std::string s1; // 创建一个空字符串
std::string s2 = "Hello"; // 从 C 风格字符串字面量初始化
std::string s3 = s2; // 复制 s2 来创建 s3
std::string s4("World"); // 另一种初始化方式
std::string s5(5, 'c'); // 创建包含 5 个 'c' 的字符串 ("ccccc")
std::cout << "s2: " << s2 << std::endl;
std::cout << "s3: " << s3 << std::endl;
std::cout << "s4: " << s4 << std::endl;
std::cout << "s5: " << s5 << std::endl;
return 0;
}
赋值: 使用 =
运算符。
std::string message = "Initial message";
message = "New content"; // 赋值
拼接 (Concatenation): 使用 +
或 +=
运算符。
std::string firstName = "John";
std::string lastName = "Doe";
std::string fullName = firstName + " " + lastName; // 使用 +
std::cout << "Full Name: " << fullName << std::endl; // 输出 "John Doe"
std::string greeting = "Hi, ";
greeting += firstName; // 使用 +=
greeting += "!";
std::cout << greeting << std::endl; // 输出 "Hi, John!"
获取长度: 使用 .length()
或 .size()
成员函数 (两者功能相同)。
std::string text = "C++ is fun!";
std::cout << "Length: " << text.length() << std::endl; // 输出 11
输入:
std::cin >> myString;
: 从键盘读取字符串,遇到空白字符(空格、制表符、换行符)时停止读取。getline(std::cin, myString);
: 读取一整行输入,直到遇到换行符 \n
为止(换行符本身会被读取并丢弃)。当你需要读取包含空格的名字或句子时,应该使用 getline
。#include
#include
int main() {
std::string word;
std::cout << "Enter a word: ";
std::cin >> word; // 如果输入 "Hello World", word 只会得到 "Hello"
std::cout << "You entered the word: " << word << std::endl;
std::string line;
std::cout << "Enter a full line: ";
// *** 重要: 如果之前使用了 cin >> 读取,需要先忽略掉上次输入留下的换行符 ***
// std::cin.ignore(10000, '\n'); // 或者更简单的 std::ws
getline(std::cin >> std::ws, line); // std::ws 会跳过输入流开头的所有空白字符
// 如果输入 "Hello World", line 会得到 "Hello World"
std::cout << "You entered the line: \"" << line << "\"" << std::endl;
return 0;
}
注意 std::ws
或 cin.ignore()
! 在 cin >>
和 getline
混合使用时,cin >>
读取后会把换行符留在输入缓冲区,getline
看到这个换行符会立刻停止读取,导致似乎“跳过”了输入。使用 getline(std::cin >> std::ws, line)
或在 getline
前加 std::cin.ignore(...)
可以解决这个问题。
其他常用操作: std::string
还提供了很多有用的功能,如比较 (==
, !=
, <
, >
), 访问单个字符 ([]
或 .at()
), 查找子串 (.find()
), 提取子串 (.substr()
) 等。你可以在后续学习中探索。
std::string
的优势:
\0
。4. std::vector
(现代 C++ 动态数组 - 入门)
原始数组最大的限制是大小固定。如果你在写程序时不知道需要存储多少个元素(比如,用户要输入多少个分数),数组就不够灵活了。std::vector
就是解决这个问题的利器,它是一个大小可变的动态数组。
使用: 需要包含头文件 #include
。
创建: std::vector<数据类型> 变量名;
#include
#include
#include
int main() {
std::vector scores; // 创建一个空的 int 类型 vector
std::vector prices = {9.9, 15.5, 8.0}; // 初始化包含 3 个 double
std::vector names; // 创建一个空的 string 类型 vector
std::vector letters(5, 'a'); // 创建包含 5 个 'a' 的 vector
std::cout << "Initial size of scores: " << scores.size() << std::endl; // 输出 0
std::cout << "Initial size of prices: " << prices.size() << std::endl; // 输出 3
return 0;
}
添加元素: 使用 .push_back(元素值)
在 vector 的末尾添加一个元素。Vector 会自动管理内存,在需要时扩展容量。
std::vector numbers;
numbers.push_back(10); // numbers 现在是 {10}
numbers.push_back(20); // numbers 现在是 {10, 20}
numbers.push_back(30); // numbers 现在是 {10, 20, 30}
std::cout << "Size after push_back: " << numbers.size() << std::endl; // 输出 3
访问元素:
变量名[索引]
: 类似数组,使用方括号和从 0 开始的索引。不进行边界检查,如果索引越界,行为未定义(危险!)。变量名.at(索引)
: 也使用索引访问,但会进行边界检查。如果索引无效,它会抛出一个异常(使程序更安全,虽然异常处理我们还没学)。推荐使用 .at()
进行访问,尤其是在不确定索引是否有效时。std::vector data = {5, 10, 15};
std::cout << "Element at index 0: " << data[0] << std::endl; // 输出 5
std::cout << "Element at index 1: " << data.at(1) << std::endl; // 输出 10
data[0] = 7; // 修改元素
std::cout << "Modified element at index 0: " << data.at(0) << std::endl; // 输出 7
// std::cout << data[3] << std::endl; // 危险!索引越界
// std::cout << data.at(3) << std::endl; // 安全!会抛出异常,而不是访问无效内存
获取大小: 使用 .size()
成员函数,返回 vector 中当前元素的数量。
std::vector values = {1.1, 2.2};
std::cout << "Number of values: " << values.size() << std::endl; // 输出 2
values.push_back(3.3);
std::cout << "Number of values now: " << values.size() << std::endl; // 输出 3
遍历 Vector:
for
循环和索引: C++ std::vector fruits = {"Apple", "Banana", "Cherry"};
for (int i = 0; i < fruits.size(); ++i) { // 注意循环条件是 < fruits.size()
std::cout << "Fruit " << i << ": " << fruits.at(i) << std::endl; // 使用 .at() 更安全
}
for
循环 (Range-based for loop - C++11 及以后,推荐): 语法更简洁,不易出错。 C++ std::vector fruits = {"Apple", "Banana", "Cherry"};
std::cout << "Fruits using range-based for loop:\n";
// for (元素类型 元素变量名 : 容器名)
for (std::string fruit : fruits) { // 对 fruits 中的每个元素,将其复制到 fruit 变量
std::cout << "- " << fruit << std::endl;
}
// 如果需要在循环中修改元素,或避免复制大型元素,使用引用 (&)
std::vector nums = {1, 2, 3};
for (int& num : nums) { // num 是 vector 中元素的引用
num = num * 2; // 直接修改 vector 中的元素
}
// 现在 nums 是 {2, 4, 6}
// 如果只想读取元素且避免复制,使用常量引用 (const &)
for (const std::string& fruit : fruits) { // fruit 是元素的常量引用,高效且安全
std::cout << "- " << fruit << std::endl;
// fruit = "Orange"; // 错误!不能通过常量引用修改
}
// 使用 auto 自动推断类型,更简洁
for (auto& num : nums) { // 自动推断 num 为 int&
num += 1;
}
for (const auto& fruit : fruits) { // 自动推断 fruit 为 const std::string&
std::cout << "- " << fruit << std::endl;
}
范围 for
循环是遍历 vector
(以及许多其他容器) 的首选方式。std::vector
的优势:
new
和 delete
。.push_back()
, .size()
, .at()
等实用函数。.at()
提供边界检查。结论: 在现代 C++ 中,当你需要一个可变大小的数组时,std::vector
通常是比原始数组更好的选择。
5. 实战项目 4: 简单的学生成绩管理
这个项目将综合运用 std::vector
, std::string
以及函数知识。
std::vector
, std::vector
, 循环 (for
), 输入输出 (std::cin
, std::cout
, getline
, std::ws
), 函数(用于计算和查找)。#include
, #include
, #include
, #include
(可能需要用到数字极限值)。main
函数中声明两个 vector:std::vector studentNames;
和 std::vector studentScores;
。int numStudents;
。可以加入输入验证,确保输入的是正整数。for
循环,执行 numStudents
次:
i+1
个学生的名字。使用 getline(std::cin >> std::ws, name)
来读取可能包含空格的名字。name
添加到 studentNames
vector 中 (studentNames.push_back(name);
)。double score;
。可以加入输入验证,确保分数是有效的数字(比如在 0-100 之间)。score
添加到 studentScores
vector 中 (studentScores.push_back(score);
)。main
之前或之后,并在 main
之前提供原型):
double calculateAverage(const std::vector& scores)
:
scores
vector 的常量引用。scores.size()
。double findHighestScore(const std::vector& scores)
:
scores
vector 的常量引用。highest
为第一个分数 scores[0]
(或可能的最低分 std::numeric_limits::lowest()
)。score
大于 highest
,则更新 highest = score
。highest
。double findLowestScore(const std::vector& scores)
:
findHighestScore
,但比较 score < lowest
。初始化 lowest
为 scores[0]
(或可能的最高分 std::numeric_limits::max()
)。int findStudentIndex(const std::vector& scores, double targetScore)
:
scores
vector 和一个目标分数 targetScore
。scores
vector,找到第一个等于 targetScore
的元素的索引并返回。如果找不到,返回 -1。main
函数中调用函数并输出结果:
studentScores
是否为空。如果不为空:
calculateAverage(studentScores)
并输出平均分。findHighestScore(studentScores)
得到最高分 highScore
。findLowestScore(studentScores)
得到最低分 lowScore
。findStudentIndex
) 调用 findStudentIndex(studentScores, highScore)
得到最高分学生的索引 highIndex
。如果 highIndex != -1
,则输出最高分和对应的学生名字 studentNames[highIndex]
。这个项目会让你熟练掌握 vector
和 string
的基本操作,并进一步体会函数在组织代码中的作用。