C/C++ Linux 开发教程(GCC + CMake) / 05 — 数组与字符串
数组与字符串
1. 一维数组
1.1 声明与初始化
#include <stdio.h>
int main(void)
{
// 声明并初始化
int a[5] = {10, 20, 30, 40, 50};
// 部分初始化(剩余元素为 0)
int b[5] = {1, 2}; // {1, 2, 0, 0, 0}
// 全部初始化为 0
int c[5] = {0};
// 自动推断大小
int d[] = {10, 20, 30}; // 大小为 3
// C99 指定初始化器
int e[5] = {[0] = 1, [3] = 4}; // {1, 0, 0, 4, 0}
for (int i = 0; i < 5; i++) {
printf("a[%d] = %d\n", i, a[i]);
}
return 0;
}
1.2 遍历数组
#include <stdio.h>
int main(void)
{
int arr[] = {64, 25, 12, 22, 11};
int n = sizeof(arr) / sizeof(arr[0]);
// 基本遍历
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 计算总和与平均值
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i];
}
printf("总和: %d, 平均值: %.2f\n", sum, (double)sum / n);
// 查找最大值
int max = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > max) max = arr[i];
}
printf("最大值: %d\n", max);
return 0;
}
💡 提示:
sizeof(arr) / sizeof(arr[0])是计算数组元素个数的标准惯用法。
2. 二维数组
#include <stdio.h>
int main(void)
{
// 3×4 矩阵
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 遍历并打印
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%4d", matrix[i][j]);
}
printf("\n");
}
// 矩阵转置
int transposed[4][3];
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
transposed[j][i] = matrix[i][j];
}
}
printf("\n转置矩阵:\n");
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 3; j++) {
printf("%4d", transposed[i][j]);
}
printf("\n");
}
return 0;
}
2.1 矩阵乘法
#include <stdio.h>
#define M 2
#define K 3
#define N 2
int main(void)
{
int A[M][K] = {{1, 2, 3}, {4, 5, 6}};
int B[K][N] = {{7, 8}, {9, 10}, {11, 12}};
int C[M][N] = {0};
for (int i = 0; i < M; i++) {
for (int j = 0; j < N; j++) {
for (int k = 0; k < K; k++) {
C[i][j] += A[i][k] * B[k][j];
}
}
}
printf("结果矩阵:\n");
for (int i = 0; i < M; i++) {
for (int j = 0; j < N; j++) {
printf("%6d", C[i][j]);
}
printf("\n");
}
return 0;
}
3. 数组越界风险
⚠️ 注意: C 语言不检查数组边界。越界访问是未定义行为(UB),可能导致程序崩溃、数据损坏或安全漏洞。
#include <stdio.h>
int main(void)
{
int arr[5] = {1, 2, 3, 4, 5};
// 危险!arr[10] 越界,读取随机内存数据
// printf("%d\n", arr[10]); // 未定义行为
// 安全做法:始终检查边界
int index = 10;
if (index >= 0 && index < 5) {
printf("arr[%d] = %d\n", index, arr[index]);
} else {
printf("索引 %d 越界!\n", index);
}
return 0;
}
| 常见越界场景 | 后果 |
|---|---|
| 数组下标为负 | 读写栈上的其他数据 |
| 数组下标超过大小 | 可能覆盖相邻变量 |
| 缓冲区溢出 | 安全漏洞(栈溢出攻击) |
💡 提示: 使用 AddressSanitizer (
-fsanitize=address) 可以在运行时检测数组越界。
4. 字符数组与字符串
C 语言中,字符串是以 '\0'(空字符)结尾的字符数组。
#include <stdio.h>
int main(void)
{
// 字符数组 —— 手动加 '\0'
char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
// 字符串字面量初始化(自动加 '\0')
char str2[] = "Hello"; // 大小为 6(含 '\0')
char str3[10] = "Hello"; // 剩余 4 个元素为 '\0'
// 字符串指针
const char *str4 = "Hello"; // 指向字符串字面量(只读)
printf("str1: %s\n", str1);
printf("str2: %s\n", str2);
printf("str2 长度: %zu\n", sizeof(str2)); // 6
return 0;
}
| 声明方式 | 可修改 | 存储位置 | 大小 |
|---|---|---|---|
char s[] = "Hello" | ✅ 可以 | 栈 | 6 字节 |
char s[10] = "Hello" | ✅ 可以 | 栈 | 10 字节 |
const char *s = "Hello" | ❌ 不可 | 只读数据段 | 指针大小 |
⚠️ 注意: 修改字符串字面量(char *s = "Hello"; s[0] = 'h';)是未定义行为,通常会导致段错误。
5. 字符串函数(<string.h>)
#include <stdio.h>
#include <string.h>
int main(void)
{
char buf[64];
// strlen —— 获取字符串长度(不含 '\0')
const char *s = "Hello, World!";
printf("长度: %zu\n", strlen(s)); // 13
// strcpy —— 复制字符串
strcpy(buf, s);
printf("复制: %s\n", buf);
// strncpy —— 安全复制(指定最大长度)
strncpy(buf, "A very long string that exceeds buffer", sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0'; // 确保以 '\0' 结尾
printf("安全复制: %s\n", buf);
// strcat —— 追加字符串
char greeting[64] = "Hello";
strcat(greeting, ", ");
strcat(greeting, "World!");
printf("追加: %s\n", greeting);
// strcmp —— 比较字符串
int cmp = strcmp("abc", "abd");
if (cmp < 0) printf("\"abc\" < \"abd\"\n");
// strstr —— 查找子串
const char *pos = strstr("Hello, World!", "World");
if (pos) printf("找到子串: %s\n", pos);
// strchr —— 查找字符
const char *p = strchr("Hello", 'l');
if (p) printf("找到 'l' 位于索引 %ld\n", p - "Hello");
return 0;
}
字符串函数速查表
| 函数 | 说明 | 头文件 |
|---|---|---|
strlen(s) | 返回字符串长度 | <string.h> |
strcpy(dst, src) | 复制字符串 | <string.h> |
strncpy(dst, src, n) | 最多复制 n 字节 | <string.h> |
strcat(dst, src) | 追加字符串 | <string.h> |
strncat(dst, src, n) | 最多追加 n 字节 | <string.h> |
strcmp(s1, s2) | 比较字符串 | <string.h> |
strncmp(s1, s2, n) | 比较前 n 个字符 | <string.h> |
strstr(haystack, needle) | 查找子串 | <string.h> |
strchr(s, c) | 查找字符首次出现 | <string.h> |
strrchr(s, c) | 查找字符最后出现 | <string.h> |
6. 字符串安全
传统字符串函数(strcpy、strcat)不检查缓冲区长度,容易导致缓冲区溢出。
6.1 使用 snprintf 替代 sprintf
#include <stdio.h>
int main(void)
{
char buf[32];
// sprintf —— 危险,可能溢出
// sprintf(buf, "Name: %s Age: %d", "A Very Long Name", 25);
// snprintf —— 安全,限制写入长度
int written = snprintf(buf, sizeof(buf), "Name: %s Age: %d", "John", 25);
printf("%s (写了 %d 字符)\n", buf, written);
// 当输出被截断时,snprintf 返回"如果缓冲区足够大"应写入的字符数
snprintf(buf, 10, "Hello, World!");
printf("buf: %s\n", buf); // 被截断为 "Hello, Wo"
return 0;
}
6.2 安全复制的封装
#include <stdio.h>
#include <string.h>
// 安全复制:确保结果以 '\0' 结尾
size_t safe_strcpy(char *dst, const char *src, size_t dst_size)
{
if (dst_size == 0) return 0;
strncpy(dst, src, dst_size - 1);
dst[dst_size - 1] = '\0';
return strlen(dst);
}
int main(void)
{
char buf[8];
safe_strcpy(buf, "Hello, World!", sizeof(buf));
printf("buf: %s\n", buf); // "Hello, "(截断,但安全)
return 0;
}
💡 提示: BSD 系统提供
strlcpy和strlcat,但它们不是 C 标准的一部分。在 Linux 上可以自己封装。
7. sprintf / snprintf 详解
#include <stdio.h>
int main(void)
{
char buf[128];
// 格式化整数
snprintf(buf, sizeof(buf), "dec:%d hex:0x%x oct:%o", 255, 255, 255);
printf("%s\n"); // dec:255 hex:0xff oct:377
// 格式化浮点数
snprintf(buf, sizeof(buf), "PI = %.4f", 3.14159);
printf("%s\n", buf); // PI = 3.1416
// 对齐与填充
snprintf(buf, sizeof(buf), "|%-10s|%10s|%05d|", "left", "right", 42);
printf("%s\n", buf); // |left | right|00042|
// 拼接字符串
char result[256];
snprintf(result, sizeof(result), "%s %s, age %d", "Hello", "World", 25);
printf("%s\n", result);
return 0;
}
8. 字符串解析
8.1 strtok —— 分割字符串
#include <stdio.h>
#include <string.h>
int main(void)
{
char str[] = "apple,banana,cherry,date";
char *token;
// 第一次调用传入字符串,后续传入 NULL
token = strtok(str, ",");
while (token != NULL) {
printf("token: %s\n", token);
token = strtok(NULL, ",");
}
return 0;
}
输出:
token: apple
token: banana
token: cherry
token: date
⚠️ 注意: strtok 会修改原始字符串(用 '\0' 替换分隔符),且不是线程安全的。需要线程安全版本可使用 strtok_r。
8.2 strtol —— 字符串转整数
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(void)
{
const char *str = "42";
char *endptr;
errno = 0;
long val = strtol(str, &endptr, 10);
if (errno != 0) {
perror("strtol");
} else if (*endptr != '\0') {
printf("无效字符: '%c'\n", *endptr);
} else {
printf("结果: %ld\n", val);
}
// 不同进制
printf("十六进制 0xFF = %ld\n", strtol("0xFF", NULL, 16));
printf("二进制 1010 = %ld\n", strtol("1010", NULL, 2));
return 0;
}
| 函数 | 说明 |
|---|---|
strtol | 字符串转 long |
strtoul | 字符串转 unsigned long |
strtoll | 字符串转 long long |
strtod | 字符串转 double |
atoi / atof | 简单转换(不检查错误,不推荐) |
💡 提示: 始终使用
strtol而非atoi,因为前者有错误检测能力。
9. 数组与指针关系
数组名在大多数表达式中会退化为指向首元素的指针:
#include <stdio.h>
int main(void)
{
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // arr 等价于 &arr[0]
// 以下三种方式等价
printf("arr[2] = %d\n", arr[2]);
printf("*(arr+2) = %d\n", *(arr + 2));
printf("p[2] = %d\n", p[2]);
printf("*(p+2) = %d\n", *(p + 2));
// 指针遍历数组
for (int *q = arr; q < arr + 5; q++) {
printf("%d ", *q);
}
printf("\n");
// 但数组和指针不完全等价
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 20(整个数组)
printf("sizeof(p) = %zu\n", sizeof(p)); // 8(指针大小)
return 0;
}
| 表达式 | 含义 | 类型 |
|---|---|---|
arr | 数组首地址 | int *(退化后) |
&arr | 整个数组的地址 | int (*)[5] |
arr[0] / *arr | 第一个元素 | int |
arr + i / &arr[i] | 第 i 个元素的地址 | int * |
*(arr + i) / arr[i] | 第 i 个元素的值 | int |
⚠️ 注意: &arr + 1 跳过的是整个数组(5 个 int),而非一个元素。arr + 1 跳过一个元素。
10. 二维数组与函数
#include <stdio.h>
// 传递二维数组(必须指定列数)
void print_matrix(int rows, int cols, int matrix[rows][cols])
{
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%4d", matrix[i][j]);
}
printf("\n");
}
}
int main(void)
{
int m[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
print_matrix(3, 4, m);
return 0;
}
💡 提示: C99 的变长数组(VLA)允许函数参数中使用变量指定维度,这在传递多维数组时非常方便。
11. 实际场景:CSV 解析
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct {
char name[64];
int age;
char city[64];
} Person;
int parse_csv_line(const char *line, Person *p)
{
char buf[256];
strncpy(buf, line, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *token = strtok(buf, ",");
if (!token) return -1;
strncpy(p->name, token, sizeof(p->name) - 1);
token = strtok(NULL, ",");
if (!token) return -1;
p->age = atoi(token);
token = strtok(NULL, ",\n\r");
if (!token) return -1;
strncpy(p->city, token, sizeof(p->city) - 1);
return 0;
}
int main(void)
{
const char *csv =
"Alice,30,Beijing\n"
"Bob,25,Shanghai\n"
"Charlie,35,Guangzhou\n";
char line[256];
const char *p = csv;
while (*p) {
int i = 0;
while (*p && *p != '\n' && i < (int)sizeof(line) - 1) {
line[i++] = *p++;
}
line[i] = '\0';
if (*p == '\n') p++;
Person person;
if (parse_csv_line(line, &person) == 0) {
printf("姓名: %-10s 年龄: %-3d 城市: %s\n",
person.name, person.age, person.city);
}
}
return 0;
}
12. 实际场景:冒泡排序
#include <stdio.h>
void bubble_sort(int arr[], int n)
{
for (int i = 0; i < n - 1; i++) {
int swapped = 0;
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = 1;
}
}
if (!swapped) break; // 已有序,提前退出
}
}
int main(void)
{
int arr[] = {64, 25, 12, 22, 11};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
bubble_sort(arr, n);
printf("排序后: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
return 0;
}