前言:从单个数据到批量数据
你好!欢迎回来。在前五篇中,我们学习了C++的核心基础:变量与数据类型、运算符、选择结构、循环结构。现在,你已经能够编写出包含复杂逻辑的程序了。但是,你会发现一个明显的限制:我们之前处理的数据都是单个的变量------一个整数、一个浮点数、一个字符。如果我们要处理成百上千个数据呢?比如一个班级50名学生的成绩、一个字符串中的每一个字符、一个图像中的每一个像素......难道要定义50个独立的变量吗?那显然不现实。
数组 正是为了解决这个问题而生的。它允许我们存储一组相同类型的数据,并通过索引来访问它们。而字符串则是字符的序列,是处理文本信息的基础。
在这一篇中,我们将深入学习:
-
数组的声明、初始化与访问
-
多维数组(尤其是二维数组)
-
C风格字符串(C-style strings)
-
std::string------现代C++的字符串处理方式 -
字符串的常用操作
-
数组与循环的完美配合
掌握这些内容后,你将能够编写出处理批量数据和文本信息的程序,这是迈向实用编程的重要一步。
第一章:数组------批量数据的容器
1.1 什么是数组?
数组是一组具有相同数据类型的元素的集合,这些元素在内存中连续存储,并通过一个共同的名称和下标(索引)来访问。
想象一下,你有一栋公寓楼,每个房间都有一个门牌号(索引),你可以通过门牌号找到对应的住户。数组也是类似的:每个元素都有一个编号(索引),从0开始。
1.2 数组的声明
声明数组需要指定三个要素:
-
元素的数据类型
-
数组的名称
-
数组的大小(元素个数)
基本语法:
cpp
数据类型 数组名[数组大小];
示例:
cpp
int scores[10]; // 声明一个包含10个整数的数组,索引0~9
double prices[5]; // 声明一个包含5个双精度浮点数的数组
char vowels[5]; // 声明一个包含5个字符的数组
注意:数组大小必须是编译期常量(C++11及以后可以使用变量作为大小,这是变长数组(VLA),但并非标准C++,不建议依赖)。在标准C++中,数组大小必须是常量表达式。
cpp
const int SIZE = 100;
int data[SIZE]; // 正确,SIZE是常量
int n;
std::cin >> n;
int arr[n]; // 某些编译器支持,但不是标准C++!避免使用
对于需要动态大小的数组,我们后续会学习std::vector。
1.3 数组的初始化
数组可以在声明时进行初始化。
1. 完全初始化
cpp
int scores[5] = {90, 85, 78, 92, 88};
2. 部分初始化:未显式初始化的元素会被自动初始化为0(对于基本类型)。
cpp
int scores[5] = {90, 85}; // 等价于 {90, 85, 0, 0, 0}
3. 省略大小:如果提供了初始化列表,可以省略数组大小,编译器会自动计算。
cpp
int scores[] = {90, 85, 78, 92, 88}; // 大小为5
4. 统一初始化(C++11)
cpp
int scores[5] {90, 85, 78, 92, 88};
5. 将所有元素初始化为0
cpp
int scores[5] = {0}; // 所有元素为0
int scores[5] {}; // C++11起,所有元素为0
1.4 访问数组元素
数组元素通过下标运算符 []来访问。下标从0开始,到大小-1结束。
cpp
int scores[5] = {90, 85, 78, 92, 88};
std::cout << scores[0] << std::endl; // 输出第一个元素:90
std::cout << scores[2] << std::endl; // 输出第三个元素:78
scores[3] = 95; // 修改第四个元素的值
1.5 数组的遍历
数组与循环是天作之合。通过循环变量作为下标,可以轻松遍历数组的所有元素。
cpp
#include <iostream>
int main() {
int scores[5] = {90, 85, 78, 92, 88};
// 使用for循环遍历
for (int i = 0; i < 5; i++) {
std::cout << "scores[" << i << "] = " << scores[i] << std::endl;
}
return 0;
}
1.6 数组的边界
数组的索引必须在有效范围内(0 到 size-1)。访问越界(如scores[5])是未定义行为,可能导致程序崩溃、数据损坏,或看似正常运行但隐藏bug。C++不进行自动边界检查,程序员必须自己保证索引有效。
cpp
int arr[3] = {1, 2, 3};
arr[3] = 4; // 越界!危险!
最佳实践:使用常量表示数组大小,避免硬编码数字。
cpp
const int SIZE = 5;
int arr[SIZE];
for (int i = 0; i < SIZE; i++) {
// ...
}
1.7 数组的sizeof操作
sizeof运算符可以获取数组占用的总字节数,以及单个元素的大小。
cpp
int scores[10];
std::cout << "数组总字节数: " << sizeof(scores) << std::endl; // 40 (int占4字节)
std::cout << "元素个数: " << sizeof(scores) / sizeof(scores[0]) << std::endl; // 10
这种技巧常用于获取数组长度,但仅适用于数组本身,不适用于指针(后面会讲)。
第二章:多维数组------表格数据
2.1 二维数组的概念
一维数组可以看作一行数据,而二维数组可以看作一张表格(行和列)。声明二维数组的语法:
cpp
数据类型 数组名[行数][列数];
例如,一个3行4列的整数矩阵:
cpp
int matrix[3][4];
逻辑上可以想象为:
text
matrix[0][0] matrix[0][1] matrix[0][2] matrix[0][3]
matrix[1][0] matrix[1][1] matrix[1][2] matrix[1][3]
matrix[2][0] matrix[2][1] matrix[2][2] matrix[2][3]
实际上,C++中的二维数组在内存中是按行连续存储的(行优先)。
2.2 二维数组的初始化
1. 嵌套花括号初始化
cpp
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
2. 省略内层花括号(按行顺序填充)
cpp
int matrix[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
3. 部分初始化(未指定的元素自动为0)
cpp
int matrix[3][4] = {
{1, 2}, // 第一行前两个为1,2,后两个为0
{5, 6, 7} // 第二行前三为5,6,7,最后一列为0
}; // 第三行全0
4. 省略第一维大小(第二维必须指定)
cpp
int matrix[][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8}
}; // 编译器自动推断第一维大小为2
2.3 二维数组的访问与遍历
使用两个下标访问元素。通常使用嵌套循环遍历。
cpp
#include <iostream>
int main() {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 输出矩阵
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
std::cout << matrix[i][j] << "\t";
}
std::cout << std::endl;
}
return 0;
}
2.4 二维数组的常见应用
-
矩阵运算:加法、乘法、转置等
-
游戏地图:用二维数组表示棋盘、网格世界
-
图像处理:像素的灰度值或RGB值
-
学生成绩表:行表示学生,列表示科目
第三章:C风格字符串------字符数组
在C++中,除了后面要讲的std::string,还有一种源自C语言的字符串表示方式:C风格字符串 。它本质上是一个字符数组,以空字符'\0'(ASCII码0)作为结束标志。
3.1 声明与初始化
cpp
char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 显式添加结束符
char str2[6] = "Hello"; // 字符串字面量自动添加'\0'
char str3[] = "Hello"; // 大小自动为6(包含'\0')
注意:字符串字面量"Hello"实际占用6个字符(H,e,l,l,o,\0)。所以数组大小必须至少比字符个数多1。
3.2 输入与输出
cpp
char name[50];
std::cout << "请输入你的名字:";
std::cin >> name; // 读取一个单词(以空白符分隔)
std::cout << "你好," << name << std::endl;
std::cin >> name会读取直到空白符,且不会检查缓冲区大小,可能导致缓冲区溢出。更安全的方法是使用std::cin.getline()。
cpp
char name[50];
std::cin.getline(name, 50); // 读取一行,最多49个字符+'\0'
3.3 常用C字符串函数
C++继承了C标准库中的字符串处理函数,需要包含<cstring>头文件。
| 函数 | 功能 | 示例 |
|---|---|---|
strlen(str) |
返回字符串长度(不含'\0') | int len = strlen("Hello"); // 5 |
strcpy(dest, src) |
复制字符串 | strcpy(dest, src); |
strncpy(dest, src, n) |
复制最多n个字符 | strncpy(dest, src, 10); |
strcat(dest, src) |
拼接字符串 | strcat(dest, src); |
strcmp(str1, str2) |
比较字符串(返回0表示相等) | if (strcmp(s1, s2) == 0) |
strchr(str, ch) |
查找字符第一次出现的位置 | char *p = strchr(str, 'a'); |
strstr(str, substr) |
查找子串第一次出现的位置 | char *p = strstr(str, "abc"); |
示例:
cpp
#include <iostream>
#include <cstring>
int main() {
char s1[20] = "Hello";
char s2[20] = "World";
std::cout << "长度: " << strlen(s1) << std::endl; // 5
strcat(s1, " "); // 拼接空格
strcat(s1, s2); // 拼接s2
std::cout << s1 << std::endl; // "Hello World"
if (strcmp(s1, "Hello World") == 0) {
std::cout << "相等" << std::endl;
}
return 0;
}
3.4 C风格字符串的缺点
-
不安全:容易缓冲区溢出
-
繁琐:需要手动管理内存和结束符
-
功能有限 :相比
std::string,操作复杂
因此,在现代C++编程中,除非有特殊原因(如与C代码交互),强烈推荐使用std::string。
第四章:std::string------现代C++字符串
std::string是C++标准库提供的字符串类,位于<string>头文件中。它封装了字符数组,提供了丰富的成员函数,使用起来安全、便捷。
4.1 基本使用
cpp
#include <iostream>
#include <string>
int main() {
std::string s1; // 空字符串
std::string s2 = "Hello"; // 初始化为"Hello"
std::string s3("World"); // 另一种初始化
std::string s4 = s2; // 拷贝构造
std::string s5(5, 'A'); // "AAAAA"
std::cout << s2 << " " << s3 << std::endl;
return 0;
}
4.2 输入与输出
cpp
std::string name;
std::cout << "请输入你的名字:";
std::cin >> name; // 读取一个单词
std::cout << "你好," << name << std::endl;
// 读取整行(包括空格)
std::string line;
std::getline(std::cin, line);
4.3 字符串拼接
使用+或+=运算符非常方便。
cpp
std::string s1 = "Hello";
std::string s2 = "World";
std::string s3 = s1 + " " + s2; // "Hello World"
s1 += " C++"; // s1变为"Hello C++"
4.4 访问字符
可以使用[]运算符或at()成员函数访问单个字符。at()会进行边界检查,越界时抛出异常,更安全。
cpp
std::string s = "Hello";
char ch1 = s[0]; // 'H'
char ch2 = s.at(1); // 'e'
s[0] = 'h'; // 修改为"hello"
4.5 字符串长度
cpp
std::string s = "Hello";
int len = s.length(); // 5
int size = s.size(); // 5(推荐,与容器统一)
4.6 常用成员函数
| 函数 | 功能 | 示例 |
|---|---|---|
size() / length() |
返回长度 | s.size() |
empty() |
判断是否为空 | if (s.empty()) |
clear() |
清空字符串 | s.clear(); |
append(str) |
追加字符串 | s.append(" World"); |
push_back(ch) |
追加字符 | s.push_back('!'); |
insert(pos, str) |
在位置pos插入 | s.insert(5, " C++"); |
erase(pos, len) |
删除从pos开始的len个字符 | s.erase(5, 3); |
replace(pos, len, str) |
替换子串 | s.replace(0, 5, "Hi"); |
substr(pos, len) |
提取子串 | std::string sub = s.substr(0, 5); |
find(str) |
查找子串第一次出现的位置 | size_t pos = s.find("lo"); |
rfind(str) |
从右侧查找 | size_t pos = s.rfind("l"); |
find_first_of(chars) |
查找任一字符第一次出现 | size_t pos = s.find_first_of("aeiou"); |
compare(str) |
比较字符串(返回0表示相等) | if (s.compare("Hello") == 0) |
c_str() |
返回C风格字符串(const char*) | const char* cstr = s.c_str(); |
示例:
cpp
#include <iostream>
#include <string>
int main() {
std::string s = "Hello, C++ World!";
// 查找子串
size_t pos = s.find("C++");
if (pos != std::string::npos) {
std::cout << "找到 C++ 在位置 " << pos << std::endl;
}
// 提取子串
std::string sub = s.substr(7, 3); // "C++"
std::cout << sub << std::endl;
// 替换
s.replace(7, 3, "Python");
std::cout << s << std::endl; // "Hello, Python World!"
// 插入
s.insert(13, "Programming ");
std::cout << s << std::endl; // "Hello, Python Programming World!"
// 删除
s.erase(13, 12);
std::cout << s << std::endl; // "Hello, Python World!"
return 0;
}
4.7 字符串与数值的转换
C++11提供了数值与字符串互转的函数,包含在<string>中。
-
std::to_string(val):将数值转换为字符串 -
std::stoi(str)、std::stol、std::stoll、std::stof、std::stod、std::stold:将字符串转换为数值
cpp
#include <string>
#include <iostream>
int main() {
int num = 123;
std::string str = std::to_string(num); // "123"
std::string s = "456.78";
double d = std::stod(s); // 456.78
int i = std::stoi(s); // 456(截断小数部分)
return 0;
}
4.8 范围for循环遍历字符串
C++11引入的范围for循环可以简洁地遍历字符串中的每个字符。
cpp
std::string s = "Hello";
for (char ch : s) {
std::cout << ch << " ";
}
// 输出:H e l l o
第五章:数组与字符串的综合应用
5.1 示例:学生成绩统计
假设有一个班级,有5名学生,每人有3门课的成绩。我们需要计算每个学生的平均分和每门课的平均分。
cpp
#include <iostream>
#include <iomanip>
int main() {
const int STUDENTS = 5;
const int SUBJECTS = 3;
double scores[STUDENTS][SUBJECTS] = {
{85.5, 90.0, 78.5},
{88.0, 76.5, 92.0},
{91.5, 89.0, 84.5},
{79.0, 85.5, 88.0},
{94.0, 87.5, 90.5}
};
// 计算每个学生的平均分
std::cout << "学生平均分:" << std::endl;
for (int i = 0; i < STUDENTS; i++) {
double sum = 0;
for (int j = 0; j < SUBJECTS; j++) {
sum += scores[i][j];
}
double avg = sum / SUBJECTS;
std::cout << "学生" << i+1 << ": " << std::fixed << std::setprecision(2) << avg << std::endl;
}
// 计算每门课的平均分
std::cout << "\n科目平均分:" << std::endl;
for (int j = 0; j < SUBJECTS; j++) {
double sum = 0;
for (int i = 0; i < STUDENTS; i++) {
sum += scores[i][j];
}
double avg = sum / STUDENTS;
std::cout << "科目" << j+1 << ": " << std::fixed << std::setprecision(2) << avg << std::endl;
}
return 0;
}
5.2 示例:字符串处理------统计单词数量
输入一行文本,统计其中单词的数量(假设单词由空格分隔)。
cpp
#include <iostream>
#include <string>
int main() {
std::string line;
std::cout << "请输入一行文本:";
std::getline(std::cin, line);
int wordCount = 0;
bool inWord = false;
for (char ch : line) {
if (ch == ' ' || ch == '\t' || ch == '\n') {
inWord = false;
} else {
if (!inWord) {
wordCount++;
inWord = true;
}
}
}
std::cout << "单词数量:" << wordCount << std::endl;
return 0;
}
5.3 示例:字符串反转
输入一个字符串,输出它的反转版本。
cpp
#include <iostream>
#include <string>
int main() {
std::string s;
std::cout << "请输入一个字符串:";
std::getline(std::cin, s);
int len = s.length();
for (int i = 0; i < len / 2; i++) {
char temp = s[i];
s[i] = s[len - 1 - i];
s[len - 1 - i] = temp;
}
std::cout << "反转后:" << s << std::endl;
return 0;
}
也可以使用标准库的std::reverse(包含<algorithm>):
cpp
#include <algorithm>
// ...
std::reverse(s.begin(), s.end());
第六章:常见错误与最佳实践
6.1 数组越界
错误:
cpp
int arr[5];
for (int i = 0; i <= 5; i++) { // 当i=5时越界
arr[i] = i;
}
修正 :使用<而不是<=。
6.2 数组名作为指针的陷阱
数组名在大多数情况下会退化为指向首元素的指针,这可能导致sizeof行为变化。例如:
cpp
int arr[10];
int *ptr = arr;
std::cout << sizeof(arr) << std::endl; // 40(10个int)
std::cout << sizeof(ptr) << std::endl; // 8(指针大小,取决于平台)
在函数参数中,数组会退化为指针,不能直接传递数组大小信息。通常需要额外传递大小参数。
6.3 使用未初始化的数组元素
局部数组如果不初始化,其元素的值是不确定的。务必在使用前初始化。
6.4 C风格字符串忘记空字符
cpp
char str[3] = {'a','b','c'}; // 没有'\0',不是合法字符串
std::cout << str; // 可能输出abc后继续输出垃圾,直到遇到'\0'
6.5 std::string的find返回值判断
find返回std::string::npos表示未找到,判断时用!=。
cpp
if (s.find("abc") != std::string::npos) {
// 找到
}
6.6 避免C风格字符串,优先使用std::string
除非必须与C API交互,否则尽量使用std::string,它更安全、更便捷。
第七章:实践练习------动手写代码!
练习一:数组元素统计
生成一个包含20个随机整数(范围1-100)的数组,然后计算并输出:
-
所有元素的平均值
-
最大值及其位置(下标)
-
最小值及其位置
-
有多少个元素大于平均值
练习二:数组排序
编写程序,输入10个整数,使用冒泡排序或选择排序将它们按升序排列,并输出排序后的结果。
练习三:二维数组------矩阵乘法
实现两个矩阵的乘法。给定矩阵A(m×n)和B(n×p),计算C = A × B。要求用户输入矩阵的行列数及元素值,输出乘积矩阵。
练习四:字符串判断------回文
输入一个字符串,判断它是否为回文(正读反读都一样),忽略空格和标点符号,不区分大小写。例如:"A man a plan a canal Panama" 是回文。
练习五:字符串统计------字符频率
输入一个字符串,统计每个字母(忽略大小写)出现的次数,并输出出现次数最多的字母及其次数。
练习六:字符串加密------凯撒密码
实现一个简单的凯撒密码加密/解密程序。输入一个字符串和一个位移值(整数),对每个字母进行位移(只处理字母,保持大小写),非字母保持不变。例如,输入"Hello, World!",位移3,输出"Khoor, Zruog!"。
练习七:学生成绩管理系统(综合)
设计一个简单的学生成绩管理系统:
-
最多存储50名学生,每名学生有学号(整数)、姓名(字符串)、三门课的成绩(整数)
-
提供菜单:
-
添加学生信息
-
显示所有学生信息及总分、平均分
-
按总分排序显示
-
按学号查找学生
-
退出
-
-
使用数组和字符串实现
练习八:螺旋矩阵
输入一个正整数n(1<=n<=10),生成一个n×n的螺旋矩阵。例如n=4时:
text
1 2 3 4
12 13 14 5
11 16 15 6
10 9 8 7
输出格式整齐。
练习九:字符串解析------计算器
输入一个简单的算术表达式,如"3 + 5 * 2",使用字符串解析提取数字和运算符,计算并输出结果。支持+、-、*、/,考虑运算符优先级。
练习十:单词统计
输入一段英文文本(多行,以空行结束),统计每个单词出现的次数,并按单词字母顺序输出结果(忽略大小写,忽略标点)。可以使用std::map(后续章节),或者用数组模拟。
第八章:常见问题与解答
Q1: 数组大小为什么必须用常量?
A: 在C++标准中,数组大小必须是编译期常量。这是因为数组在栈上分配,编译器需要在编译时知道分配多少内存。如果需要运行时确定大小,应使用动态内存分配(new/delete)或std::vector。
Q2: 如何获取数组的长度?
A: 如果数组是静态定义的(不是指针),可以使用sizeof(arr) / sizeof(arr[0])。但更安全的做法是使用std::size(arr)(C++17)或定义常量保存长度。对于函数参数中的数组(实际是指针),这种方法无效。
Q3: std::string和char[]有什么区别?
A: std::string是一个类,封装了字符数组,自动管理内存,提供丰富的成员函数,使用安全。char[]是原始字符数组,需要手动处理空字符、长度和边界,容易出错。现代C++推荐使用std::string。
Q4: std::string的c_str()有什么用?
A: 当需要将std::string传递给接受const char*的C函数时,使用c_str()获取底层字符数组的指针。注意返回的指针在std::string被修改或销毁后会失效。
Q5: 二维数组在内存中如何存储?
A: 二维数组按行优先存储:先存储第一行的所有元素,然后是第二行,依此类推。所有元素在内存中是连续的一段空间。
Q6: 如何将字符串转换为大写或小写?
A: 可以使用std::transform配合toupper或tolower(包含<algorithm>和<cctype>):
cpp
#include <algorithm>
#include <cctype>
#include <string>
std::string s = "Hello";
std::transform(s.begin(), s.end(), s.begin(), ::toupper);
Q7: 为什么std::string的find返回size_t而不是int?
A: size_t是无符号类型,通常与指针大小相同,能表示所有可能的索引。当未找到时,返回std::string::npos,其值为static_cast<size_t>(-1)。因此,判断是否找到应该用if (pos != std::string::npos)而不是if (pos >= 0)(因为size_t永远≥0)。
第九章:下一站预告
恭喜你完成了第六篇!现在你已经掌握了批量数据存储(数组)和文本处理(字符串)的核心知识。你能处理成百上千个数据,能解析和操作文本信息,你的程序能力又上了一个台阶。
在下一篇中,我们将进入C++的核心特性之一:函数。函数是模块化编程的基础,它让你能够将代码组织成可复用的单元,使程序结构更清晰、更易于维护。我们将学习:
-
函数的定义与调用
-
参数传递(值传递、引用传递)
-
返回值
-
函数重载
-
默认参数
-
内联函数
-
函数的作用域与生命周期
-
递归
掌握函数后,你将能够编写出结构清晰、高度复用的代码,这是从"写程序"到"设计程序"的飞跃。
结语:批量处理打开新世界
数组让我们能够处理大量数据,字符串让我们能够与用户进行有意义的文本交互。结合之前学习的循环和选择,你可以编写出很多实用程序了:成绩统计、文本分析、简单游戏......编程的世界正在你面前展开。
请务必完成本章的练习,尤其是综合性的学生成绩管理系统和螺旋矩阵等题目。它们能帮助你巩固数组和字符串的知识,并锻炼综合运用能力。
记住,编程不仅是学会语法,更重要的是学会如何用这些工具解决问题。每一次练习,都是一次思维训练。
现在,去写代码吧!让数组和字符串为你服务。我们下一篇见!