4. 数组与字符串:一维数组、二维数组、字符数组与C风格字符串、C++ string类、常用字符串操作
数组和字符串,说白了就是程序里最基础的数据容器。我刚开始学C++那会儿,觉得数组不就是个列表嘛,有啥好学的?结果后来写项目时,因为数组越界查了三天bug……嗯,从那以后我再也不敢小看它了。
这一章,咱们就把数组和字符串彻底讲透。从一维数组到二维数组,从C风格字符串到C++的string类,我会把我在项目中踩过的坑、积累的经验都告诉你。
4.1 一维数组:最基础的数据容器
一维数组,说白了就是一块连续的内存空间,里面存了一堆相同类型的数据。你想想看,如果你要存100个学生的成绩,难道要定义100个变量?那不得累死。数组就是来解决这个问题的。
核心要点:
- 数组下标从0开始,不是从1开始——这是C++的规矩,别问为什么,记住就行
- 声明时必须指定大小,或者直接初始化
- 数组名就是首地址,这个后面讲指针时会用到
// 声明和初始化
int scores[5]; // 声明一个包含5个int的数组,未初始化
int scores[5] = {90, 85, 78, 92, 88}; // 完全初始化
int scores[] = {90, 85, 78, 92, 88}; // 编译器自动推断大小
// 访问元素
scores[0] = 95; // 修改第一个元素
int first = scores[0]; // 读取第一个元素
// 遍历数组
for (int i = 0; i < 5; i++) {
cout << "scores[" << i << "] = " << scores[i] << endl;
}
⚠️ 避坑指南:我曾经在一个项目中,因为数组越界写入了相邻内存,导致程序在特定条件下崩溃。查了整整两天才发现是数组下标写成了 scores[5],而数组大小只有5(有效下标0-4)。记住:C++不会检查数组越界,越界是未定义行为,后果自负。
4.2 二维数组:表格数据的天然选择
二维数组,你可以把它想象成一个表格——有行有列。我个人习惯把二维数组看作「数组的数组」,这样理解起来更直观。
// 声明一个3行4列的二维数组
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 访问元素
int value = matrix[1][2]; // 第2行第3列,值为7
// 双重循环遍历
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
cout << matrix[i][j] << "\t";
}
cout << endl;
}
这里有个重要的概念——行优先存储。C++的二维数组在内存中是按行连续存储的。也就是说,matrix[0][0]后面紧跟着matrix[0][1],然后matrix[0][2]……直到第一行结束,才是matrix[1][0]。
💡 小技巧:遍历二维数组时,外层循环用行、内层循环用列,这样符合行优先存储,缓存命中率更高。我在做图像处理时,这个顺序优化能带来明显的性能提升。
4.3 字符数组与C风格字符串
C风格字符串,说白了就是一个以'\0'(空字符)结尾的字符数组。这是C语言留下的遗产,在C++中依然广泛使用,尤其是在与底层系统交互时。
// 字符数组的两种初始化方式
char str1[] = "Hello"; // 自动添加'\0',大小为6
char str2[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 手动添加'\0'
// 注意:下面这个不是字符串,只是字符数组
char str3[] = {'H', 'e', 'l', 'l', 'o'}; // 没有'\0',不是合法字符串
// 常用C风格字符串函数(需要 #include <cstring>)
char src[] = "World";
char dest[20];
strcpy(dest, src); // 复制字符串
strcat(dest, "!"); // 拼接字符串
int len = strlen(dest); // 获取长度(不包括'\0')
int cmp = strcmp(dest, src); // 比较字符串
⚠️ 避坑指南:我曾经接手过一个遗留项目,里面大量使用C风格字符串。最头疼的问题就是缓冲区溢出——strcpy和strcat不会检查目标缓冲区大小。如果源字符串比目标缓冲区大,就会发生溢出。后来我全部改成了strncpy和strncat,指定最大复制长度,才彻底解决了这个问题。
4.4 C++ string类:现代C++的正确选择
说实话,除非你在写嵌入式系统或者跟C库接口打交道,否则我个人建议你直接用std::string。它自动管理内存,不会溢出,而且提供了丰富的成员函数。
#include <string>
using namespace std;
// 声明和初始化
string s1; // 空字符串
string s2 = "Hello"; // 从C风格字符串初始化
string s3("World"); // 构造函数
string s4 = s2 + " " + s3; // 拼接,结果为 "Hello World"
string s5(5, 'A'); // "AAAAA"
// 基本操作
cout << s2.length(); // 5,获取长度
cout << s2[0]; // 'H',访问字符(不检查越界)
cout << s2.at(0); // 'H',访问字符(会检查越界,抛出异常)
// 修改
s2 += " C++"; // 追加
s2.push_back('!'); // 追加单个字符
s2.insert(5, " beautiful"); // 在位置5插入
s2.erase(5, 10); // 从位置5删除10个字符
s2.clear(); // 清空
string vs C风格字符串:
| 特性 | C风格字符串 | std::string |
|---|---|---|
| 内存管理 | 手动管理 | 自动管理 |
| 缓冲区溢出 | 容易发生 | 不会发生 |
| 长度获取 | strlen() O(n) | length() O(1) |
| 拼接 | strcat() 麻烦 | + 运算符 简单 |
| 与C接口兼容 | 直接兼容 | c_str() 转换 |
4.5 常用字符串操作实战
下面这些操作,是我在实际项目中几乎每天都会用到的。你想想看,处理用户输入、解析配置文件、生成日志……哪样离得开字符串操作?
#include <iostream>
#include <string>
#include <sstream> // 字符串流
using namespace std;
int main() {
string text = "Hello, C++ World!";
// 1. 查找
size_t pos = text.find("C++");
if (pos != string::npos) {
cout << "找到 'C++' 在位置: " << pos << endl;
}
// 2. 子串
string sub = text.substr(7, 3); // "C++"
// 3. 替换
text.replace(7, 3, "Qt"); // "Hello, Qt World!"
// 4. 比较
string a = "apple";
string b = "banana";
if (a < b) {
cout << a << " 小于 " << b << endl;
}
// 5. 数字转字符串
int num = 42;
string numStr = to_string(num); // "42"
// 6. 字符串转数字
string strNum = "3.14";
double val = stod(strNum); // 3.14
// 7. 分割字符串(使用字符串流)
string data = "apple,banana,orange";
stringstream ss(data);
string item;
while (getline(ss, item, ',')) {
cout << item << endl;
}
// 8. 大小写转换
string mixed = "Hello World";
for (char &c : mixed) {
c = toupper(c); // 转大写
}
// 或者 tolower() 转小写
return 0;
}
💡 个人经验:我在做Qt项目时,经常需要处理用户输入的字符串。Qt提供了QString,功能比std::string更强大,支持Unicode、正则表达式等。但如果你写的是纯C++后端,std::string完全够用。我的建议是:Qt项目用QString,其他项目用std::string。
4.6 数组与字符串的常见陷阱
嗯,这里我要重点说几个我踩过的坑,你一定要注意:
- 数组作为函数参数会退化为指针——在函数内部用
sizeof获取数组大小,得到的是指针大小,不是数组大小。我建议传数组时同时传大小参数。 - string的c_str()返回的指针可能失效——如果你修改了string对象,之前通过
c_str()获取的指针就变成了野指针。我曾经因为这个在Qt中调试了半天。 - 不要用
==比较C风格字符串——==比较的是地址,不是内容。要用strcmp。而std::string的==比较的是内容,放心用。 - 二维数组作为函数参数时,必须指定第二维的大小——比如
void func(int arr[][4]),因为编译器需要知道每行有多少个元素才能正确计算偏移量。
总结一下:
- 一维数组:连续内存,下标从0开始,注意越界
- 二维数组:行优先存储,遍历时注意顺序
- C风格字符串:以'\0'结尾,手动管理内存,容易出bug
- std::string:自动管理内存,功能丰富,推荐使用
- 常用操作:查找、子串、替换、比较、转换、分割
数组和字符串是C++编程的基础,也是面试中经常被问到的知识点。我个人建议你多写代码练习,尤其是字符串处理——因为在实际项目中,字符串操作几乎无处不在。等你把这一章的内容吃透了,后面的指针、动态内存分配学起来就会轻松很多。
公众号:蓝海资料掘金营,微信deep3321