5、数组与字符串:一维数组、二维数组、字符数组、C风格字符串、string类、常用字符串操作
数组和字符串,说白了就是C++里最基础的数据容器。我刚开始学的时候,觉得这东西太简单了,不就是一堆数据排排坐嘛。结果真到项目里,踩过的坑一个接一个。今天咱们就把这块彻底捋清楚。
5.1 一维数组:最朴素的连续内存
一维数组,就是同一类型元素的线性集合。它在内存里是连续存放的,这一点非常关键。
int scores[5] = {88, 92, 76, 85, 90};
// 内存布局:scores[0] scores[1] ... scores[4] 紧挨着
声明与初始化
- 类型固定:所有元素类型必须一致
- 大小固定:编译时确定,运行时不能变
- 下标从0开始:这是C/C++的传统,也是新手最容易犯迷糊的地方
核心要点:数组名就是首元素的地址。scores 等价于 &scores[0]。这个特性在函数传参时特别重要。
我在项目中遇到过一个问题:用 sizeof 求数组长度,结果传参后就不灵了。为什么?因为数组作为参数传递时,会退化为指针,sizeof 只能拿到指针的大小,而不是整个数组的大小。
void printArray(int arr[]) {
// arr 已经退化为指针了
int len = sizeof(arr) / sizeof(arr[0]); // 错误!拿不到正确长度
}
int main() {
int arr[10];
int len = sizeof(arr) / sizeof(arr[0]); // 正确:10
printArray(arr);
}
避坑指南:我曾经在项目里用 sizeof 算数组长度,结果传参后长度变成了 2(64位系统下指针8字节,int4字节)。排查了半天才发现是数组退化的问题。后来我养成了习惯:要么传长度参数,要么用 std::array 或 std::vector。
5.2 二维数组:表格数据的天然选择
二维数组,你可以把它想象成一个表格——行和列。但内存里它仍然是连续存储的,按行优先排列。
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 访问:matrix[行][列]
内存布局:matrix[0][0], matrix[0][1], ..., matrix[0][3], matrix[1][0], ... 全部连续。
嗯,这里要注意:二维数组作为函数参数时,除了第一维可以省略,其他维度必须指定。否则编译器不知道每行有多少列,没法计算偏移量。
void printMatrix(int arr[][4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
cout << arr[i][j] << " ";
}
cout << endl;
}
}
小技巧:如果你需要动态大小的二维数组,别用 int arr[n][m](这是C99的VLA,C++不支持)。用 vector<vector<int>> 或者自己手动分配一维数组模拟二维。
5.3 字符数组与C风格字符串
C风格字符串,说白了就是以 '\0'(空字符)结尾的字符数组。这是C语言留下的遗产,C++里依然广泛使用。
char str1[] = "Hello"; // 自动添加 '\0',实际长度6
char str2[] = {'H','e','l','l','o','\0'}; // 手动加 '\0'
char str3[10] = "Hello"; // 预留空间,后面自动补 '\0'
为什么需要 '\0'? 因为C风格字符串没有长度信息,必须靠这个结束标记来知道字符串到哪里结束。你想想看,如果没有 '\0',像 strlen、strcpy 这些函数怎么知道字符串有多长?
避坑指南:我曾经在项目里犯过一个低级错误——声明字符数组时忘了给 '\0' 留位置。比如 char name[5] = "Alice"; 看起来没问题,但 "Alice" 实际需要6个字符(5个字母+1个'\0')。结果 strcpy 的时候直接越界,把相邻内存的数据给覆盖了。排查了整整一下午。
常用C风格字符串函数(需要 #include <cstring>)
| 函数 | 作用 | 注意事项 |
|---|---|---|
| strlen(s) | 返回字符串长度(不含'\0') | O(n)复杂度,不是常数时间 |
| strcpy(dest, src) | 复制字符串 | 确保 dest 空间足够 |
| strcat(dest, src) | 拼接字符串 | 确保 dest 空间足够 |
| strcmp(s1, s2) | 比较字符串 | 返回0表示相等 |
| strchr(s, c) | 查找字符 | 返回指针或NULL |
5.4 string类:C++的现代方案
说实话,C风格字符串用起来太痛苦了。手动管理内存、担心越界、拼接还要小心翼翼。C++的 string 类把这些麻烦全解决了。
#include <string>
using namespace std;
string s1 = "Hello";
string s2("World");
string s3 = s1 + " " + s2; // 直接拼接,不用操心内存
cout << s3 << endl; // "Hello World"
cout << s3.length() << endl; // 11
string 的优势
- 自动管理内存:不用手动分配释放
- 动态增长:可以随时追加字符
- 运算符重载:+、+=、==、< 等直接用
- 安全:不会出现缓冲区溢出
个人习惯:我现在的项目里,除非是跟C库接口对接,否则一律用 std::string。C风格字符串只作为底层交互的桥梁,业务逻辑里完全用 string。
5.5 常用字符串操作详解
不管是C风格还是 string,有些操作是绕不开的。咱们挑几个最常用的说说。
1. 查找子串
string text = "Hello, welcome to C++ world!";
size_t pos = text.find("welcome");
if (pos != string::npos) {
cout << "找到了,位置:" << pos << endl;
}
// C风格:strstr
const char* result = strstr(text.c_str(), "welcome");
2. 截取子串
string sub = text.substr(7, 7); // 从位置7开始,取7个字符
cout << sub << endl; // "welcome"
3. 插入与删除
string s = "I C++";
s.insert(2, "love "); // "I love C++"
s.erase(2, 5); // 又变回 "I C++"
4. 替换
string s = "I like Java";
s.replace(7, 4, "C++"); // "I like C++"
5. 转换大小写
string s = "Hello World";
for (char &c : s) {
c = toupper(c); // 或者 tolower
}
// s 变成 "HELLO WORLD"
小技巧:string 的 c_str() 方法可以返回C风格字符串,方便跟C库函数交互。但要注意,c_str() 返回的指针在 string 对象修改后可能失效。
5.6 知识体系总览
下面这张图把本章的核心知识点串起来了。你可以看到,数组是基础,字符数组是数组的特例,C风格字符串是字符数组的约定用法,而 string 类是对C风格字符串的封装和升级。
说白了,数组是C++的底层基石,理解它你才能理解指针和内存管理。而 string 类则是C++对字符串操作的现代化封装,让你写代码更高效、更安全。我个人建议:新项目里优先用 string,但一定要理解底层数组的原理——因为很多面试题和底层调试,最终还是绕不开数组和指针。
本章总结:一维数组是线性存储,二维数组是表格存储,字符数组是字符串的底层实现,C风格字符串靠 '\0' 结尾,string 类提供了安全便捷的操作接口。掌握这些,你就掌握了C++数据存储的半壁江山。