4. 数组与字符串:一维数组、二维数组、字符数组与C风格字符串、C++ string类、常用字符串操作

数组和字符串,说白了就是程序里最基础的数据容器。我刚开始学C++那会儿,觉得数组不就是个列表嘛,有啥好学的?结果后来写项目时,因为数组越界查了三天bug……嗯,从那以后我再也不敢小看它了。

这一章,咱们就把数组和字符串彻底讲透。从一维数组到二维数组,从C风格字符串到C++的string类,我会把我在项目中踩过的坑、积累的经验都告诉你。

数组与字符串知识体系 一维数组 二维数组 字符数组 声明、初始化、访问、遍历 行优先存储、矩阵操作 C风格字符串、'\0'结尾 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风格字符串。最头疼的问题就是缓冲区溢出——strcpystrcat不会检查目标缓冲区大小。如果源字符串比目标缓冲区大,就会发生溢出。后来我全部改成了strncpystrncat,指定最大复制长度,才彻底解决了这个问题。

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 数组与字符串的常见陷阱

嗯,这里我要重点说几个我踩过的坑,你一定要注意:

  1. 数组作为函数参数会退化为指针——在函数内部用sizeof获取数组大小,得到的是指针大小,不是数组大小。我建议传数组时同时传大小参数。
  2. string的c_str()返回的指针可能失效——如果你修改了string对象,之前通过c_str()获取的指针就变成了野指针。我曾经因为这个在Qt中调试了半天。
  3. 不要用==比较C风格字符串——==比较的是地址,不是内容。要用strcmp。而std::string==比较的是内容,放心用。
  4. 二维数组作为函数参数时,必须指定第二维的大小——比如void func(int arr[][4]),因为编译器需要知道每行有多少个元素才能正确计算偏移量。

总结一下:

  • 一维数组:连续内存,下标从0开始,注意越界
  • 二维数组:行优先存储,遍历时注意顺序
  • C风格字符串:以'\0'结尾,手动管理内存,容易出bug
  • std::string:自动管理内存,功能丰富,推荐使用
  • 常用操作:查找、子串、替换、比较、转换、分割

数组和字符串是C++编程的基础,也是面试中经常被问到的知识点。我个人建议你多写代码练习,尤其是字符串处理——因为在实际项目中,字符串操作几乎无处不在。等你把这一章的内容吃透了,后面的指针、动态内存分配学起来就会轻松很多。


公众号:蓝海资料掘金营,微信deep3321