第6章:数组与字符串——C++中最基础的数据容器
数组和字符串,说白了就是C++里最常用的两种数据组织方式。我做了这么多年C++开发,几乎每个项目都离不开它们。今天咱们就好好聊聊这两个基础但极其重要的概念。
6.1 一维数组:最简单的数据集合
一维数组就像一排连续的储物柜。每个柜子有编号,你可以直接通过编号存取东西。在C++里,数组的索引从0开始,这点我刚开始学的时候也经常搞混。
// 声明和初始化
int scores[5]; // 声明一个包含5个整数的数组
int scores[5] = {85, 92, 78, 90, 88}; // 声明并初始化
int scores[] = {85, 92, 78, 90, 88}; // 让编译器自动计算大小
// 访问元素
scores[0] = 95; // 修改第一个元素
int first = scores[0]; // 读取第一个元素
⚠️ 注意:数组越界是C++中最常见的bug之一。我曾经在一个金融交易系统里遇到过,就因为数组越界导致内存被意外修改,查了整整两天才定位到问题。C++不会检查数组边界,所以你得自己负责。
6.2 二维数组:表格数据的好帮手
二维数组其实就是「数组的数组」。我习惯把它想象成一个Excel表格——行和列。比如存储一个3×4的矩阵:
// 声明一个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
你想想看,二维数组在内存里其实是连续存储的。按行优先排列——先存完第一行所有列,再存第二行。这个特性在做图像处理时特别有用,我做过一个图像滤波的项目,就是利用这个特性做了缓存优化,性能提升了将近3倍。
6.3 数组与指针的关系
嗯,这里要重点说一下。数组名其实就是一个指向数组首元素的指针常量。这句话我当年理解了好久。
int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr; // 数组名arr就是指向第一个元素的指针
// 下面两种写法完全等价
cout << arr[2]; // 输出30
cout << *(ptr + 2); // 也输出30
为什么会这样?因为编译器会把 arr[i] 翻译成 *(arr + i)。说白了,数组下标操作本质上就是指针运算。我个人习惯在需要传递数组给函数时,明确使用指针参数,这样代码意图更清晰。
💡 小技巧:用 sizeof(arr) / sizeof(arr[0]) 可以计算数组元素个数。但注意,这只在数组定义的作用域内有效。一旦数组退化为指针,sizeof就只返回指针大小了。我踩过这个坑,所以现在都用 std::size()(C++17)或者模板函数来获取数组大小。
6.4 C风格字符串(char[])
C风格字符串其实就是以空字符 '\0' 结尾的字符数组。说实话,用起来挺麻烦的,但很多底层系统代码还在用。
char str1[] = "Hello"; // 自动添加'\0',实际占用6个字节
char str2[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 手动添加'\0'
// 常用操作
#include <cstring>
strlen(str1); // 获取长度(不包括'\0')
strcpy(dest, src); // 复制字符串
strcat(str1, str2); // 拼接字符串
strcmp(str1, str2); // 比较字符串
⚠️ 注意:C风格字符串最大的问题是缓冲区溢出。我曾经接手过一个遗留项目,里面大量使用 strcpy 和 sprintf,结果安全扫描报告了一堆高危漏洞。后来全部改成了 strncpy 和 snprintf。现在C++17已经不建议使用这些C风格函数了。
6.5 C++ string类(std::string)
终于说到 std::string 了。我个人强烈建议:能用string就别用char[]。string类封装了内存管理,自动处理扩容,还提供了丰富的成员函数。
#include <string>
std::string s1 = "Hello";
std::string s2("World");
std::string s3(5, 'A'); // "AAAAA"
// 基本操作
s1.length(); // 获取长度
s1.empty(); // 判断是否为空
s1[0]; // 访问字符(不检查边界)
s1.at(0); // 访问字符(检查边界,越界抛异常)
我习惯用 at() 而不是 operator[],虽然慢一点点,但至少不会出现静默越界。在调试阶段用 at(),发布版本如果性能敏感再换成 operator[]。
6.6 字符串常用操作
拼接
std::string s1 = "Hello";
std::string s2 = "World";
// 方法1:使用+运算符
std::string s3 = s1 + " " + s2; // "Hello World"
// 方法2:使用append()
s1.append(" ").append(s2); // 链式调用
// 方法3:使用+=运算符
s1 += " " + s2;
这三种方式里,我推荐用 += 或 append(),因为它们不会创建临时对象。用 + 运算符会产生中间临时字符串,在循环里大量拼接时性能会差很多。
查找
std::string text = "Hello World, Hello C++";
// 查找子串
size_t pos = text.find("World"); // 返回6,找不到返回std::string::npos
// 从指定位置开始查找
pos = text.find("Hello", 7); // 从索引7开始找,返回13
// 反向查找
pos = text.rfind("Hello"); // 从后往前找,返回13
// 查找字符集合中的任意字符
pos = text.find_first_of("aeiou"); // 返回1('e'的位置)
// 判断是否找到
if (pos != std::string::npos) {
std::cout << "找到了,位置:" << pos;
}
查找操作里有个细节:find 返回的是 size_t 类型,而 npos 的值是 -1 转换成 size_t 后的最大值。所以判断时一定要用 != npos,不要用 >= 0,因为 size_t 是无符号的。
替换
std::string text = "I like C++";
// 替换子串
text.replace(7, 3, "Python"); // 从索引7开始,替换3个字符为"Python"
// 结果:"I like Python"
// 替换所有匹配项(需要循环)
std::string target = "C++";
std::string replacement = "Java";
size_t pos = 0;
while ((pos = text.find(target, pos)) != std::string::npos) {
text.replace(pos, target.length(), replacement);
pos += replacement.length();
}
💡 经验之谈:如果你需要做大量的字符串替换操作,建议先用 std::stringstream 或者 std::ostringstream 构建新字符串,而不是在原字符串上反复 replace。每次 replace 都可能触发内存重分配,性能开销不小。
知识体系总览
下面这张图把本章的核心知识点串起来了,方便你理解它们之间的关系:
总结
数组和字符串是C++编程的基石。我个人建议你记住几点:
- 数组:固定大小,连续内存,下标从0开始。注意越界问题。
- 指针与数组:数组名是指向首元素的指针常量,但
sizeof行为不同。 - C风格字符串:以
'\0'结尾的字符数组,操作麻烦且不安全。 - std::string:现代C++的首选,自动管理内存,功能丰富。
- 字符串操作:拼接用
+=或append(),查找注意npos判断,替换考虑性能。
嗯,这些内容看起来基础,但实际项目中80%的字符串相关bug都出在这些细节上。多写多练,慢慢就熟练了。