C++零基础实践教程 函数 数组、字符串与 Vector
模块四:函数 (代码复用与模块化)
随着程序变得越来越复杂,把所有代码都堆在 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; 来提前结束函数 }
- 返回值类型 (Return Type): 函数执行完毕后返回的数据类型(如
int
,double
,void
表示不返回任何值)。 - 函数名 (Function Name): 给函数起一个有意义的名字(遵循变量命名规则)。
- 参数列表 (Parameter List): 函数接收的输入数据,在圆括号
()
内声明,多个参数用逗号,
分隔。每个参数都需要指定类型和名称。如果函数不需要输入,括号内为空 (void myFunc()
)。 - 函数体 (Function Body): 花括号
{}
内的代码,是函数实际执行的操作。 return
语句: 用于从函数返回值。一旦执行return
,函数立即结束。void
函数没有返回值,可以不写return
或使用return;
提前退出。
- 返回值类型 (Return Type): 函数执行完毕后返回的数据类型(如
-
调用 (Call): 在程序的其他地方(比如
C++main
函数或其他函数中)使用函数名和必要的参数来执行函数定义的代码。#include <iostream> #include <string>// (在此处或之后定义 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; // 输出 8greet("Alice"); // 调用 greet 函数,传入 "Alice"// 函数会直接输出 "你好, Alice!"int x = 10, y = 20;int sum_xy = add(x, y); // 也可以使用变量作为参数std::cout << x << " + " << y << " = " << sum_xy << std::endl; // 输出 30return 0; }
- 参数 (Arguments): 调用函数时传递给函数的实际值,其数量和类型必须与函数定义中的参数列表匹配。
2. 函数原型 (声明)
C++ 编译器是按从上到下的顺序读取代码的。如果你在调用一个函数之前还没有定义它,编译器就不知道这个函数长什么样(需要什么参数,返回什么类型),就会报错。
解决方法有两种:
-
将函数定义放在调用它的代码之前: 对于小程序可行,但项目变大后难以管理。
-
使用函数原型 (Function Prototype) 或声明 (Declaration): 在调用函数之前,先写一行函数的“签名”,告诉编译器这个函数的基本信息,但省略函数体。函数原型看起来就像函数定义的第一行,但后面跟的是分号
C++;
而不是花括号{}
。#include <iostream> #include <string>// 函数原型 (声明) int add(int num1, int num2); // 告诉编译器:有一个叫 add 的函数,接收两个 int,返回 int void greet(std::string name); // 告诉编译器:有一个叫 greet 的函数,接收 string,返回 voidint 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 的局部变量 localVarmyFunction();return 0; }
- 在函数内部或某个代码块(如
-
全局变量 (Global Variables):
- 在所有函数之外声明的变量。
- 从声明点开始,到整个文件结束都有效,可以被该文件中它之后的所有函数访问。
#include <iostream>int globalVar = 100; // 全局变量void printGlobal() {std::cout << "printGlobal: " << globalVar << std::endl; // 可以访问全局变量globalVar = 200; // 也可以修改全局变量 }int main() {std::cout << "main (before): " << globalVar << std::endl; // 输出 100printGlobal(); // 调用函数,修改了 globalVarstd::cout << "main (after): " << globalVar << std::endl; // 输出 200return 0; }
警告: 应尽量避免使用全局变量!
- 难以追踪: 程序的任何地方都可能修改全局变量的值,使得调试和理解代码流程变得困难。
- 命名冲突: 如果不同文件或库定义了同名的全局变量,可能导致链接错误或意想不到的行为。
- 降低模块性: 函数依赖于全局变量,使得函数不够独立,难以复用。
优先使用局部变量和函数参数/返回值来传递数据。
4. 传值调用 (Pass by Value) vs 传引用调用 (Pass by Reference, &
)
当我们将变量作为参数传递给函数时,有两种主要方式:
-
传值调用 (Pass by Value): 这是 C++ 的默认方式。
- 函数接收到的是实参(调用时传入的值)的一个副本 (copy)。
- 在函数内部对这个副本参数的任何修改,不会影响到函数外部的原始实参变量。
- 优点: 安全,不会意外修改原始数据。
- 缺点: 如果传递的数据很大(比如大型结构体或对象),创建副本会有效率开销。
#include <iostream>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; // 输出 10modifyValue(num);std::cout << "After function call, num = " << num << std::endl; // 仍然输出 10,原始 num 未改变return 0; }
-
传引用调用 (Pass by Reference): 通过在函数参数类型后加上
&
符号实现。- 函数接收到的是原始实参变量的一个引用 (reference),可以理解为原始变量的别名。函数内部操作这个引用参数,实际上就是在操作原始变量。
- 在函数内部对引用参数的修改,会影响到函数外部的原始实参变量。
- 优点:
- 可以直接修改原始实参的值(例如
swap
函数)。 - 避免了创建副本的开销,对于传递大型数据结构更高效。
- 可以直接修改原始实参的值(例如
- 缺点: 可能会无意中修改原始数据,需要小心使用。
#include <iostream>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; // 输出 10modifyReference(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, 9swap(x, y);std::cout << "After swap: x = " << x << ", y = " << y << std::endl; // 输出 9, 5return 0; }
-
常量引用 (
C++const &
): 如果你希望通过引用传递来避免复制开销,但又不希望函数修改原始数据,可以使用常量引用。void printLargeData(const std::string& data) { // 使用常量引用传递字符串std::cout << "Data: " << data << std::endl;// data = "changed"; // 错误!不能通过常量引用修改数据 }
这是 C++ 中传递大型对象(如
string
,vector
)给函数时常用的高效且安全的方式。
5. 函数重载 (Function Overloading)
C++ 允许你定义多个同名的函数,只要它们的参数列表不同即可。参数列表的不同可以体现在:
- 参数的数量不同。
- 参数的类型不同。
- 参数的顺序不同(如果类型也不同)。
注意: 函数的返回值类型不能作为区分重载函数的依据。
编译器会根据你调用函数时提供的参数类型和数量来自动选择匹配哪个版本的重载函数。
C++
#include <iostream>
#include <string>// 重载 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::stringreturn 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
- 需要<cmath>
)。 - 修改
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
等)。 - 程序启动后显示一个菜单,列出可用的操作选项(例如:1. 加法, 2. 减法, ..., 0. 退出)。
- 用户通过输入数字选择操作。
- 程序根据用户的选择,提示用户输入操作数,然后调用相应的函数进行计算并显示结果。
- 完成一次计算后,再次显示菜单,让用户可以继续进行其他计算,直到用户选择退出为止。
- 使用函数实现各个计算操作(加、减、乘、除,可以额外添加取模
- 涉及知识点: 函数定义与调用 (核心),
switch
或if-else if-else
(处理菜单选择),循环 (while
或do-while
控制程序主流程,直到用户退出),基本输入输出。 - 建议步骤:
- 定义计算函数: 像练习 1 那样,为每个运算(加、减、乘、除、取模、求幂等)编写独立的函数。求幂可能需要
#include <cmath>
并使用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 那样,为每个运算(加、减、乘、除、取模、求幂等)编写独立的函数。求幂可能需要
这个项目能很好地锻炼你组织代码、使用函数进行模块化设计的能力。
模块五:数组、字符串与 Vector (处理数据集合)
目前我们处理的都是单个数据。但很多时候,我们需要处理一组数据,比如一个班级所有学生的分数,或者一个人的姓名(由多个字符组成)。这一模块将介绍如何处理这些数据集合。
1. 数组 (Array)
数组是最基本的数据集合,它可以在内存中连续存储固定数量的相同类型的元素。
-
声明:
C++数据类型 数组名[数组大小];
数组大小必须是一个常量表达式(在编译时就能确定)。int scores[5]; // 声明一个可以存储 5 个 int 类型分数的数组 double prices[10]; // 声明一个可以存储 10 个 double 类型价格的数组 char grades[3]; // 声明一个可以存储 3 个 char 类型等级的数组
-
初始化: 可以在声明时使用花括号
C++{}
提供初始值。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 开始!对于大小为
C++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++ 不会自动检查数组索引是否越界。访问无效索引是常见的、危险的错误来源。
-
遍历数组: 通常使用
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 - 1std::cout << "分数 " << (i + 1) << ": " << scores[i] << std::endl;total += scores[i]; // 累加分数 } double average = total / NUM_SCORES; std::cout << "平均分: " << average << std::endl;
-
数组作为函数参数:
- 当你将数组传递给函数时,实际上传递的是数组第一个元素的内存地址(一个指针)。函数不会创建整个数组的副本。
- 因此,在函数内部对数组参数的修改会影响原始数组。
- 因为传递的只是地址,函数本身不知道数组的大小。通常需要将数组大小作为另一个参数传递给函数。
#include <iostream>// 函数原型,接收一个 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)
你需要使用 <cstring>
(或 <string.h>
) 中的函数(如 strlen
计算长度 - 不含 \0
, strcpy
复制字符串, strcat
拼接字符串)来操作它们。
缺点:
- 大小固定: 数组大小在编译时确定,难以处理长度可变的文本。
- 容易出错: 必须手动管理
\0
,并且像strcpy
这样的函数如果不小心,很容易造成缓冲区溢出(写入的数据超出了数组分配的空间),这是一个严重的安全隐患。 - 操作不便: 拼接、比较等操作不如
std::string
直观。
结论: 了解 C 风格字符串有助于理解一些底层概念和旧代码,但在现代 C++ 编程中,强烈建议使用 std::string
。
3. std::string
(现代 C++ 字符串 - 重点)
std::string
是 C++ 标准库提供的一个类,专门用于方便、安全地处理字符串(文本)。
-
使用: 需要包含头文件
#include <string>
。 -
创建与初始化:
C++#include <string> #include <iostream>int main() {std::string s1; // 创建一个空字符串std::string s2 = "Hello"; // 从 C 风格字符串字面量初始化std::string s3 = s2; // 复制 s2 来创建 s3std::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; }
-
赋值: 使用
C++=
运算符。std::string message = "Initial message"; message = "New content"; // 赋值
-
拼接 (Concatenation): 使用
C+++
或+=
运算符。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!"
-
获取长度: 使用
C++.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 <string> #include <iostream>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::wsgetline(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 <vector>
。 -
创建:
C++std::vector<数据类型> 变量名;
#include <vector> #include <string> #include <iostream>int main() {std::vector<int> scores; // 创建一个空的 int 类型 vectorstd::vector<double> prices = {9.9, 15.5, 8.0}; // 初始化包含 3 个 doublestd::vector<std::string> names; // 创建一个空的 string 类型 vectorstd::vector<char> letters(5, 'a'); // 创建包含 5 个 'a' 的 vectorstd::cout << "Initial size of scores: " << scores.size() << std::endl; // 输出 0std::cout << "Initial size of prices: " << prices.size() << std::endl; // 输出 3return 0; }
-
添加元素: 使用
C++.push_back(元素值)
在 vector 的末尾添加一个元素。Vector 会自动管理内存,在需要时扩展容量。std::vector<int> 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<int> 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; // 输出 10data[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; // 安全!会抛出异常,而不是访问无效内存
-
获取大小: 使用
C++.size()
成员函数,返回 vector 中当前元素的数量。std::vector<double> 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<std::string> 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<std::string> 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<int> 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::string>
,std::vector<double>
, 循环 (for
), 输入输出 (std::cin
,std::cout
,getline
,std::ws
), 函数(用于计算和查找)。 - 建议步骤:
- 包含头文件:
#include <iostream>
,#include <vector>
,#include <string>
,#include <limits>
(可能需要用到数字极限值)。 - 声明 Vectors: 在
main
函数中声明两个 vector:std::vector<std::string> studentNames;
和std::vector<double> 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<double>& scores)
:- 接收一个
scores
vector 的常量引用。 - 检查 vector 是否为空,如果为空返回 0 或其他合适的值。
- 使用循环累加所有分数。
- 返回 总分数 /
scores.size()
。
- 接收一个
double findHighestScore(const std::vector<double>& scores)
:- 接收
scores
vector 的常量引用。 - 处理空 vector 的情况。
- 初始化一个变量
highest
为第一个分数scores[0]
(或可能的最低分std::numeric_limits<double>::lowest()
)。 - 使用循环遍历 vector,如果当前分数
score
大于highest
,则更新highest = score
。 - 返回
highest
。
- 接收
double findLowestScore(const std::vector<double>& scores)
:- 类似
findHighestScore
,但比较score < lowest
。初始化lowest
为scores[0]
(或可能的最高分std::numeric_limits<double>::max()
)。
- 类似
- (可选)
int findStudentIndex(const std::vector<double>& 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 为空,输出提示信息。
- 检查
- 包含头文件:
这个项目会让你熟练掌握 vector
和 string
的基本操作,并进一步体会函数在组织代码中的作用。