C语言基础教程:复合数据类型

今天这次咱们就初步看看 C 语言的一些复合数据类型。

想象一下,如果你要搬运一本书,你可以直接用手拿着走,这就像是我们之前学过的 intchar,一次处理一个数据,简单直接。但如果你要搬运一百本书呢?你肯定不会一本一本地跑一百趟,而是会找个纸箱子,把书整整齐齐地装进去,然后一次性搬走。

在 C 语言中,复合数据类型就是这个“箱子”。它允许我们把多个数据打包在一起,从而更方便地管理和处理复杂信息。今天,我们先来聊聊最基础、也是最常用的两种“箱子”:数组(包括一维和二维)以及特殊的数组——字符串

C 语言数组与 Python 列表(list)虽然功能相似,但在底层实现逻辑上存在本质区别。C 语言数组严格遵循同构原则,要求所有元素必须属于同一数据类型,而 Python 列表则允许容纳不同类型的数据。在内存管理方面,C 数组通常在编译时分配固定大小的连续内存空间,这种静态特性虽然缺乏 Python 列表自动扩容的灵活性,却换来了更高效的访问速度。此外,C 语言不提供类似 Python 中 append 或 slice 等内置高级方法,对数据的操作需要依靠循环、索引或指针手动完成。这种显式的管理方式虽然增加了编码复杂度,却能提供对底层内存更精细的控制能力。

一、数组(Array)

先说数组。数组是 C 语言中最基础的复合数据类型,你可以把它想象成一排连续的储物柜。每个柜子上都有编号(索引),柜子里放的东西必须是一模一样的(比如全是整数,或者全是字符)。

如何定义一个数组?

定义数组时,我们需要告诉编译器三件事:存什么?叫什么?有多少?

语法格式:类型 数组名[数量];

例如 int arr[10];,这句话的意思就是:“帮我在内存里划出一块地盘,要能挨个儿放下 10 个整型数字。”

这里有两个关键点: 1. 同构性:这 10 个格子必须全都是 int,不能混进去一个 double。 2. 连续性:这 10 个格子在内存里是肩并肩、紧挨着的,中间没有空隙。

怎么知道数组有多大?

C 语言比较“硬核”,它不像 Python 那样可以直接用 len() 函数来问数组有多长。在 C 语言里,我们需要用 sizeof 运算符来自己动手“量”一下:

  • sizeof(arr):量出整个数组一共占了多少字节(比如 10 个 int,每个 4 字节,总共就是 40 字节)。
  • sizeof(arr[0]):量出其中一个元素占多少字节(一个 int 是 4 字节)。
  • 元素个数 = 总大小 / 单个元素大小。即 sizeof(arr) / sizeof(arr[0])。这是 C 语言程序员必须掌握的“心法口诀”。

示例 1:一维数组基本操作

下面的代码展示了数组的定义、初始化、长度计算以及如何像切蛋糕一样访问其中的一部分数据。

#include <stdio.h>

int main() {
    // 定义并初始化一个包含10个整数的数组
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    printf("=== 一维数组操作演示 ===\n");
    printf("原始数组: ");
    for (int i = 0; i < 10; i++) printf("%d ", arr[i]);
    printf("\n");

    // 基本信息统计
    // sizeof(arr) 返回整个数组占用的字节数(40)
    // sizeof(arr[0]) 返回单个元素占用的字节数(4)
    printf("数组大小: %lu 字节\n", sizeof(arr));
    printf("元素个数: %lu\n", sizeof(arr) / sizeof(arr[0]));

    // 模拟“切片”操作(打印前5个元素)
    printf("\n前5个元素: ");
    for (int i = 0; i < 5; i++) printf("%d ", arr[i]);

    // 条件筛选(只打印偶数)
    printf("\n偶数元素: ");
    int count = 0;
    for (int i = 0; i < 10; i++) {
        if (arr[i] % 2 == 0) {
            printf("%d ", arr[i]);
            count++;
        }
    }
    printf("(共%d个)\n", count);
    return 0;
}

运行结果:

=== 一维数组操作演示 ===
原始数组: 1 2 3 4 5 6 7 8 9 10 
数组大小: 40 字节
元素个数: 10

前5个元素: 1 2 3 4 5 
偶数元素: 2 4 6 8 10 (共5个)

二、二维数组

生活中的数据往往不止一行。比如电影院的座位表、Excel 的电子表格,它们既有行又有列。为了表示这种数据,我们需要用到二维数组

你可以把二维数组看作是“数组的数组”——也就是一个大数组,里面的每一个元素本身又是一个小数组。

示例 2:二维数组定义与遍历

定义二维数组时,我们需要指定行数和列数,格式是 类型 数组名[行数][列数]

#include <stdio.h>

int main() {
    int matrix[3][4];  // 定义一个 3 行 4 列的矩阵

    // 初始化并显示
    printf("=== 二维数组演示 ===\n");
    int value = 1;
    printf("初始化为递增序列:\n");

    // 外层循环控制行,内层循环控制列
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            matrix[i][j] = value++;
        }
    }

    // 格式化显示(打印成矩阵形状)
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%3d ", matrix[i][j]);
        }
        printf("\n"); // 每打印完一行后换行
    }

    // 内存布局信息
    printf("\n内存布局信息:\n");
    printf("总大小: %lu 字节\n", sizeof(matrix));
    printf("行数×列数×元素大小 = %lu×%lu×%lu = %lu\n", 
           sizeof(matrix)/sizeof(matrix[0]),        // 行数 = 总大小 / 一行的大小
           sizeof(matrix[0])/sizeof(matrix[0][0]),  // 列数 = 一行的大小 / 单个元素大小
           sizeof(matrix[0][0]),                    // 单个元素大小
           sizeof(matrix));                         // 总大小

    return 0;
}

运行结果:

=== 二维数组演示 ===
初始化为递增序列:
  1   2   3   4 
  5   6   7   8 
  9  10  11  12 

内存布局信息:
总大小: 48 字节
行数×列数×元素大小 = 3×4×4 = 48

2.2 为什么遍历顺序很重要?

这里有个非常关键的概念:行优先存储(Row-Major Order)

虽然我们在脑海里把二维数组想象成一个方方正正的表格,但在计算机的内存条里,并没有“表格”这种东西,内存是一条长长的直线。C 语言在存储二维数组时,是按行来存的:先把第一行的数据排好,紧接着排第二行,然后是第三行……

也就是说,matrix[0][3](第一行最后一个)和 matrix[1][0](第二行第一个)在内存里是紧紧挨着的邻居。

理解这一点对性能至关重要。CPU 读取内存时,不是只读一个数,而是会把相邻的一大块数据都读进高速缓存(Cache)。如果你顺着内存的顺序读(先读第一行,再读第二行),CPU 就会很高兴,因为数据已经在缓存里准备好了;反之,如果你跳着读(先读第一行的第一个,再读第二行的第一个),CPU 就得频繁地去内存里重新搬运数据,速度就会慢很多。

让我们用一个实验来验证一下:

示例 3:二维数组形状对缓存的影响

#include <stdio.h>
#include <time.h>

int main() {
    clock_t start, end;
    double time1, time2;

    printf("=== 二维数组访问效率对比 ===\n");

    // 情况一:3行1000列(扁长的数组)
    // 这种形状下,内层循环(列)很长,行数很少
    int a[3][1000];
    start = clock();
    // 重复多次以放大差异
    for (int k = 0; k < 10000; k++) {
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 1000; j++) {
                a[i][j] = i + j;
            }
        }
    }
    end = clock();
    time1 = ((double)(end - start)) / CLOCKS_PER_SEC;

    // 情况二:1000行3列(瘦高的数组)
    // 这种形状下,内层循环(列)很短,换行非常频繁
    int b[1000][3];
    start = clock();
    for (int k = 0; k < 10000; k++) {
        for (int i = 0; i < 1000; i++) {
            for (int j = 0; j < 3; j++) {
                b[i][j] = i + j;
            }
        }
    }
    end = clock();
    time2 = ((double)(end - start)) / CLOCKS_PER_SEC;

    // 结果对比
    printf("3×1000数组耗时: %.6f 秒\n", time1);
    printf("1000×3数组耗时: %.6f 秒\n", time2);

    if (time1 > 0)
        printf("效率差异: %.2f倍\n", time2/time1);
    else
        printf("耗时过短,无法计算倍数\n");

    return 0;
}

运行结果(示例):

=== 二维数组访问效率对比 ===
3×1000数组耗时: 0.025000 1000×3数组耗时: 0.057000 秒
效率差异: 2.28倍

这说明了什么?即使元素总数一样,“频繁换行”是有代价的

更夸张的情况是,如果你完全无视内存布局,故意颠倒循环顺序(先遍历列,再遍历行),性能损失会更严重:

示例 4:遍历顺序对性能的影响

#include <stdio.h>
#include <time.h>

int main() {
    clock_t start, end;
    double time_correct, time_wrong;

    printf("=== 访问顺序对性能的影响 ===\n");

    static int a[3][1000]; 

    // 正确:外层循环遍历行,内层循环遍历列
    // 这符合“行优先”的内存布局
    start = clock();
    for (int k = 0; k < 10000; k++) { 
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 1000; j++) {
                a[i][j] = i + j;
            }
        }
    }
    end = clock();
    time_correct = ((double)(end - start)) / CLOCKS_PER_SEC;

    // 错误:外层循环遍历列,内层循环遍历行
    // 这会导致内存访问在不同行之间反复跳跃
    static int a_wrong[3][1000];
    start = clock();
    for (int k = 0; k < 10000; k++) {
        for (int j = 0; j < 1000; j++) {
            for (int i = 0; i < 3; i++) {
                a_wrong[i][j] = i + j;
            }
        }
    }
    end = clock();
    time_wrong = ((double)(end - start)) / CLOCKS_PER_SEC;

    // 性能对比
    printf("行优先访问: %.6f 秒\n", time_correct);
    printf("列优先访问: %.6f 秒\n", time_wrong);

    if (time_correct > 0)
        printf("性能损失: %.2f倍\n", time_wrong/time_correct);

    printf("\n原因:行优先访问符合内存连续布局,缓存命中率更高\n");

    return 0;
}

运行结果(示例):

=== 访问顺序对性能的影响 ===
行优先访问: 0.024000 秒
列优先访问: 0.047000 秒
性能损失: 1.96倍

原因:行优先访问符合内存连续布局,缓存命中率更高

三、字符串

字符串在 C 语言里其实不是一种独立的“类型”,它本质上就是一个字符数组。但它有一个非常特殊的规定:必须以空字符 \0 结尾

3.1 为什么需要 \0

计算机很笨,它不知道你的字符串“hello”是到 'o' 结束,还是后面紧跟着的乱码也是字符串的一部分。所以 C 语言约定:只要遇到 \0(ASCII 码为 0 的字符),就表示字符串结束了。

这就像我们在写句子时,最后必须打个句号一样。如果忘了打句号(\0),程序打印字符串时就会一直往后打,直到遇到内存里随机出现的 0 为止,这往往会导致乱码甚至程序崩溃。

示例 5:字符串定义与操作

#include <stdio.h>
#include <string.h>

int main() {
    // 两种定义方式的对比
    // 方式一:直接用双引号。编译器会自动在末尾帮你加上 '\0'
    char str1[] = "hello"; 

    // 方式二:像定义普通数组一样,逐个字符赋值。
    // 注意:必须手动加上 '\0',否则它只是个字符数组,不是合法的字符串!
    char str2[6] = {'h', 'e', 'l', 'l', 'o', '\0'};

    printf("=== 字符串基本操作 ===\n");
    printf("str1内容: '%s'\n", str1);
    printf("str2内容: '%s'\n", str2);

    // strlen 计算的是有效字符的长度(不包含 \0)
    printf("长度比较: str1=%lu, str2=%lu\n", strlen(str1), strlen(str2));

    // sizeof 计算的是整个数组占用的内存大小(包含 \0)
    printf("内存大小: str1=%lu字节, str2=%lu字节\n", sizeof(str1), sizeof(str2));

    // 比较两个字符串的内容是否相同
    printf("内容相等: %s\n", strcmp(str1, str2) == 0 ? "是" : "否");

    // 字符级遍历展示:看看内存里到底存了什么
    printf("\n=== 字符级详细展示 ===\n");
    for (int i = 0; i < sizeof(str1); i++) {
        printf("str1[%d] = '%c' (ASCII: %d)", i, str1[i], str1[i]);
        if (str1[i] == '\0') printf(" <- 这里的 0 就是字符串的终止符");
        printf("\n");
    }

    return 0;
}

运行结果:

=== 字符串基本操作 ===
str1内容: 'hello'
str2内容: 'hello'
长度比较: str1=5, str2=5
内存大小: str1=6字节, str2=6字节
内容相等: 

=== 字符级详细展示 ===
str1[0] = 'h' (ASCII: 104)
str1[1] = 'e' (ASCII: 101)
str1[2] = 'l' (ASCII: 108)
str1[3] = 'l' (ASCII: 108)
str1[4] = 'o' (ASCII: 111)
str1[5] = ' ' (ASCII: 0) <- 这里的 0 就是字符串的终止符

总结

今天我们学习了 C 语言中最重要的“收纳工具”:

  1. 一维数组:像一排储物柜,大小固定,类型统一。记住公式 sizeof(arr)/sizeof(arr[0]) 来计算它的容量。
  2. 二维数组:像一个表格,但在内存里是按行“压扁”了存放的。所以,按行遍历(先读第一行,再读第二行)永远比按列遍历要快,这是编写高性能代码的第一步。
  3. 字符串:本质上是字符数组,但多了一个隐藏的尾巴 \0。千万别忘了这个尾巴,它是字符串结束的唯一标志。

理解了这些内存布局的细节,你不仅能写出正确的 C 代码,更能写出高效的 C 代码。这也是学习 C 语言最大的乐趣所在——能够让人通过它来窥探计算机底层的工作原理。

Category: C
Category
Tagcloud
Hack RTL-SDR Lens SKill Book GPT-OSS Discuss Poem OpenWebUI Mac Conda FckZhiHu Python Junck Translate Data Virtual Machine OpenCL Communicate Shit Kivy IDE Software VTK Tape Programming MayaVi Chat Memory Cursor Learning Remote VM Prompt Nvidia Photo Code Game Hardware Raspbian AI,Data Science Science Microscope GlumPy 耳机 LTO Story Qwen3 Geology LlamaFactory Code Generation Radio C Windows Camera HBase QGIS Tools Ollama Windows11 Agent Hadoop Linux FuckZhihu n8n Translation ML Ubuntu AI CUDA PVE Video GIS RaspberryPi Photography ChromeBook History Hackintosh Virtualization PyOpenCL AIGC Mount&Blade 音频 SandBox PHD VirtualMachine TUNA Pyenv Visualization Tool macOS NixOS FuckChunWan Scholar 蓝牙 Life LLM University QEMU LTFS