第5章 数组与字符串
第5章 数组与字符串
- 如何声明和使用数组存储同类型数据
- 如何用字符数组表示和操作字符串
- 理解数组的内存布局和下标访问机制
想象一下你有一排整齐的储物柜,每个柜子都有编号,里面可以放东西。在C++里,数组就是这排储物柜,它让你能用一个名字管理多个同类型的变量。
数组是什么?
生活类比:一栋公寓楼里连续的房间,每个房间号(下标)对应一个住户(数据)。
技术定义:数组是一种数据结构,用于在连续的内存空间中存储固定数量的、相同数据类型的元素集合。
5.1 一维数组:你的第一个数据集合
一维数组是最简单的形式,就像一条直线上的点。它的声明语法是:数据类型 数组名[元素个数];。
// 声明一个能存放5个整数的数组
int scores[5];
// 声明并初始化数组
int primes[5] = {2, 3, 5, 7, 11};
// 可以省略数组大小,编译器会自动计算
int morePrimes[] = {13, 17, 19, 23};
数组元素通过下标访问,下标从0开始。这意味着第一个元素是array[0],最后一个元素是array[数组长度-1]。我在项目中踩过这个坑,一个新手工程师因为把scores[5]当成第五个成绩,导致程序访问了非法内存,直接崩溃。
⚠️ 常见误区:数组下标越界
这是新手最容易犯的错误,编译器通常不会报错,但会导致程序行为异常甚至崩溃。
- 错误1:访问
arr[10],而数组大小只有10。最后一个有效下标是arr[9]。 - 错误2:在循环中使用
for(int i=0; i<=10; i++)遍历大小为10的数组,i=10时越界。 - 错误3:使用负数下标,如
arr[-1],这完全是非法的。
5.2 数组在内存中如何排列
理解内存布局对调试和性能优化至关重要。数组元素在内存中是连续存储的。对于一个int arr[3] = {10, 20, 30};的数组,假设int占4字节,其内存布局如下:
| 元素 | 内存地址(示例) | 存储的值 | 字节占用 |
|---|---|---|---|
| arr[0] | 0x1000 | 10 | 0x1000 - 0x1003 |
| arr[1] | 0x1004 | 20 | 0x1004 - 0x1007 |
| arr[2] | 0x1008 | 30 | 0x1008 - 0x100B |
这种连续存储的特性使得数组的访问速度极快,因为CPU缓存可以高效地预加载相邻数据。实际开发中,对于需要频繁遍历的固定大小数据集,我们首选数组。
关键点:数组大小必须是编译期常量
在标准C++中,数组的大小必须在编译时确定。你不能写int n = 5; int arr[n];(虽然某些编译器扩展允许)。正确的做法是使用常量:const int SIZE = 5; int arr[SIZE];。如果需要运行时决定大小,你需要等待第9章介绍的动态内存分配。
5.3 字符数组与C风格字符串
在C++中,字符串最初是通过字符数组实现的,称为C风格字符串。它本质上是一个以空字符‘\0’(ASCII码为0)结尾的字符数组。
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
// 更简洁的初始化方式(编译器会自动添加'\0')
char greeting2[] = "Hello";
字符串是什么?
生活类比:一列火车,车头是第一个字符,车尾有一个特殊的“终点”标志(‘\0’)。
技术定义:字符串是由字符组成的序列,在C风格字符串中以空字符作为终止符,标识字符串的结束。
双引号“Hello”被称为字符串字面量。它会在内存中创建一个包含6个字符(5个字母+1个‘\0’)的常量数组。这里有个隐藏的坑:字符串字面量是不可修改的。尝试char* ptr = “Hello”; ptr[0] = ‘h’;会导致未定义行为(通常是程序崩溃)。
5.4 操作字符串:输入、输出与常用函数
对于C风格字符串,你不能直接用==比较,也不能用=直接赋值。必须使用C标准库中的函数。
#include
#include // 引入字符串操作函数
using namespace std;
int main() {
char name[20];
char copy[20];
// 输入字符串(遇到空格会停止)
cin >> name;
// 输入带空格的字符串(更安全,指定最大长度)
cin.getline(name, 20);
// 复制字符串
strcpy(copy, name);
// 连接字符串
strcat(name, "先生");
// 获取字符串长度(不包括'\0')
int len = strlen(name);
// 比较字符串(相等返回0)
if (strcmp(name, copy) == 0) {
cout << "字符串相同" << endl;
}
return 0;
}
🔧 工程师经验:strcpy的安全隐患
strcpy函数不会检查目标数组的大小。如果源字符串比目标数组长,会导致缓冲区溢出,这是严重的安全漏洞。实际开发中,我们使用更安全的strncpy,它可以指定最大复制字符数:strncpy(dest, src, dest_size - 1); dest[dest_size - 1] = ‘\0’;。在第22章,你会看到C++的std::string类完全避免了这些问题。
编者提示:本章学习的数组是静态的,大小固定。当你需要在程序运行时动态创建大小可变的数据集合时,请参考第9章《动态内存管理》和第18章《标准模板库(STL)容器》中的vector。
5.5 二维数组:表格数据的管理者
二维数组可以看作一个矩阵或表格,它有两个维度:行和列。声明语法:数据类型 数组名[行数][列数];。
// 一个3行4列的整数表格(例如:3个学生,4门成绩)
int gradebook[3][4] = {
{85, 90, 78, 92}, // 学生1的成绩
{88, 76, 95, 80}, // 学生2的成绩
{70, 82, 88, 75} // 学生3的成绩
};
// 访问第2个学生(行索引1)的第3门成绩(列索引2)
int score = gradebook[1][2]; // score = 95
在内存中,二维数组仍然是按行连续存储的。上面的gradebook在内存中的排列顺序是:85, 90, 78, 92, 88, 76, 95, 80, 70, 82, 88, 75。理解这一点对性能优化很重要:按行顺序遍历(外层循环行,内层循环列)比按列顺序遍历更快,因为它符合内存的连续性,缓存命中率高。
游戏地图
- 用二维数组
int map[10][10] - 每个元素代表一个格子类型(0=空地,1=墙,2=水源)
图像像素
- 黑白图像:
bool pixels[480][640] - RGB图像:
int pixels[480][640][3](三维数组)
矩阵计算
- 存储矩阵数据
float matrixA[3][3] - 用于图形变换、物理模拟等
数据表格
- 存储传感器读数
float sensorData[100][5] - 100个时间点,5个传感器通道
动手试一试
1. 数组反转:编写程序,声明一个包含6个整数的数组{1,2,3,4,5,6},在不创建新数组的情况下,将数组元素顺序反转,变成{6,5,4,3,2,1}。提示:使用两个下标,一个从头部开始,一个从尾部开始,交换它们指向的元素。
2. 单词统计:编写程序,使用cin.getline()读取一行英文句子(不超过100字符),统计其中有多少个单词(单词之间用空格分隔)。例如,输入“Hello C++ world”,输出3。
3. 查找最高分:使用二维数组存储3个学生4门课的成绩,计算并输出每个学生的平均分,以及找出所有成绩中的最高分是几分,属于哪个学生(行)的哪门课(列)。
检验你的理解
- 以下代码片段有什么问题?
char buffer[5]; strcpy(buffer, “HelloWorld”); - 数组
int arr[10];的有效下标范围是多少?arr[10]访问的是什么? - 如何初始化一个字符数组,使其内容为字符串
“C++”?写出两种方法。 - 如果
int matrix[2][3] = {{1,2,3}, {4,5,6}};,那么matrix[1][2]的值是多少? - 判断:字符串
“A”和字符‘A’在内存中占用相同的字节数。
本章小结
- 数组是连续内存中相同类型元素的集合,通过下标(从0开始)访问,大小必须在编译时确定。
- C风格字符串是以空字符‘\0’结尾的字符数组,使用
中的函数(如strcpy, strcat, strlen, strcmp)进行操作,需注意缓冲区溢出风险。 - 二维数组用于表示表格数据,内存中按行连续存储,按行遍历效率更高。
- 数组下标越界是常见且危险的错误,会导致未定义行为或安全漏洞。
- 字符串字面量(如
“text”)存储在只读内存区,试图修改它们会导致程序崩溃。
微信扫码关注
抖音扫码关注