嵌入式 - C语言:指针与构造数据类型

在 C 语言中,指针与构造数据类型是实现复杂功能的核心工具。本文将从二维数组传参入手,深入解析指针数组传参、void * 指针特性,以及结构体等构造数据类型的核心知识点,并结合实际应用场景补充说明,帮助读者彻底掌握这些进阶内容。

一、二维数组的传参

二维数组在传参时存在一个容易被忽略的特性:数组名会退化为指向首元素的指针,但二维数组的首元素是一维数组,因此传参时需要特殊处理。

1. 传参本质

二维数组的数组名本质是指向第一行的数组指针 (即指向一个一维数组的指针)。例如int a[2][3]中,a的类型是int (*)[3](指向包含 3 个 int 元素的数组的指针)。

当二维数组作为函数参数时,直接传递数组名会导致维度信息丢失,因此必须在函数形参中明确第二维的大小,或使用数组指针接收。

2. 正确传参形式

文档中给出的示例为:

复制代码
int a[2][3] = {1, 2, 3, 4, 5};
int fun(int (*p)[3], int len); // 用数组指针接收二维数组
  • 这里int (*p)[3]明确了指针p指向的是包含 3 个 int 元素的数组,与a的类型匹配,保证了传参时类型一致。
  • 若省略第二维大小(如int fun(int p[][3], int len)),编译器也能正确识别,因为二维数组传参时第一维可以省略,第二维必须明确。

3. 应用场景

二维数组传参常用于矩阵运算、二维表格数据处理等场景。例如,一个计算二维数组元素和的函数:

复制代码
int sumArray(int (*arr)[3], int rows) {
    int sum = 0;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 3; j++) {
            sum += arr[i][j]; // 等价于*(*(arr+i)+j)
        }
    }
    return sum;
}

二、指针数组的传参

指针数组是 "存储指针的数组",其传参方式与普通数组不同,核心是通过二级指针接收。

1. 指针数组的特性

指针数组的每个元素都是指针,例如char *pstr[5]是一个包含 5 个char*指针的数组,常用于存储多个字符串(每个元素指向一个字符串的首地址)。

2. 传参形式

由于指针数组的数组名是指向首元素(即第一个指针)的指针,因此其类型是 "指针的指针"(二级指针)。文档中示例为:

复制代码
char *pstr[5] = {NULL};
int fun(char **ppstr, int len); // 用二级指针接收指针数组
  • char **ppstr表示ppstr是一个指向char*的指针,与pstr(类型为char*[5],退化后为char**)匹配。

3. 应用场景

指针数组传参常用于处理字符串数组,例如对多个字符串进行排序:

复制代码
// 比较两个字符串(用于qsort)
int cmpStr(const void *a, const void *b) {
    return strcmp(*(char**)a, *(char**)b);
}

// 对指针数组中的字符串排序
void sortStrArray(char **strs, int len) {
    qsort(strs, len, sizeof(char*), cmpStr);
}

三、void * 指针:通用地址容器

void*指针是一种特殊的指针类型,被称为 "无类型指针",其核心作用是作为通用的地址容器。

1. 核心特性

  • 存储地址void*指针可以保存任意类型变量的地址,例如int*char*double*等。
  • 转换规则
    • void*转换为其他类型指针(如char*int*)时,无需强制类型转换(编译器自动处理);
    • 其他类型指针转换为void*时,需要强制类型转换(如p = (void*)a)。

2. 应用场景

void*指针广泛用于内存操作函数和通用接口设计:

  • 内存操作mallocmemcpy等函数的参数和返回值均为void*,例如void *malloc(size_t size)返回一块未类型化的内存地址。

  • 通用函数 :实现跨类型的操作,如一个通用的交换函数:

    c

    复制代码
    void swap(void *a, void *b, size_t size) {
        char tmp[size]; // 临时缓冲区
        memcpy(tmp, a, size);
        memcpy(a, b, size);
        memcpy(b, tmp, size);
    }

3. 注意事项

void*指针不能直接解引用(*p)或进行算术运算,必须先转换为具体类型的指针才能操作。

四、构造数据类型:结构体详解

结构体是 C 语言中自定义复杂数据类型的核心工具,用于将多个不同类型的变量封装为一个整体。

1. 结构体定义与初始化

  • 定义 :使用struct关键字声明结构体类型,例如:

    复制代码
    struct student {
        char name[32]; // 姓名
        char sex;      // 性别('m'/'f')
        int age;       // 年龄
        int score;     // 成绩
    };
  • 初始化

    • 全部初始化:struct student stu = {"zhangsan", 'm', 18, 90};

    • 局部初始化(指定成员):

      复制代码
      struct student stu = {
          .name = "zhangsan", 
          .score = 90
      }; // 未指定的成员自动初始化为0

2. 成员访问

结构体成员的访问有两种方式:

  • 结构体变量 :使用.运算符,如stu.age = 19;
  • 结构体指针 :使用->运算符,如struct student *p = &stu; p->score = 95;

3. 结构体的存储:内存对齐

结构体的大小并非简单的成员大小之和,而是遵循内存对齐规则,以提高硬件访问效率:

  • 每个成员的地址必须是自身类型大小的整数倍(如int成员地址必须是 4 的倍数);
  • 结构体总大小必须是最大成员类型大小的整数倍。

例如,struct student的大小计算:

复制代码
struct student {
    char name[32]; // 32字节(已对齐)
    char sex;      // 1字节(下一个成员int需4字节对齐,因此补3字节)
    int age;       // 4字节(从36地址开始,36是4的倍数)
    int score;     // 4字节(从40地址开始)
}; 
// 总大小:32 + 1 + 3(补位) + 4 + 4 = 44字节(44是4的倍数,符合规则)

4. 结构体传参

结构体传参有两种方式,优先选择传地址:

  • 传值void fun(struct student tmp); 会拷贝整个结构体(若结构体较大,开销高);
  • 传地址void fun(struct student *ptmp); 仅拷贝 8 字节指针(效率高)。

示例:修改学生成绩的函数

复制代码
void updateScore(struct student *stu, int newScore) {
    stu->score = newScore; // 通过指针修改原结构体
}