强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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. 字符串安全

传统字符串函数(strcpystrcat)不检查缓冲区长度,容易导致缓冲区溢出。

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 系统提供 strlcpystrlcat,但它们不是 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;
}

13. 扩展阅读