在 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*
指针广泛用于内存操作函数和通用接口设计:
-
内存操作 :
malloc
、memcpy
等函数的参数和返回值均为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; // 通过指针修改原结构体
}