指针进阶
内容简介
指针进阶关注的是工程里真正会用到的指针能力:数组接口、输出参数、动态内存、函数指针、const 指针、void* 通用接口,以及如何把“地址”变成可维护的函数边界。
本篇不追求展示指针的所有规则细节。数组退化例外、尾后指针、严格别名、对齐、&arr 和 arr 的类型差异、复杂声明阅读等内容,放在 C语言指针细节。那些内容对理解 C 很有帮助,但多数不应该成为日常工程代码的主要写法。
1. 指针参数的基本工程模型
函数参数使用指针,通常表达三种意图:
- 输入:函数只读取调用者传入的数据。
- 输出:函数通过指针把结果写回调用者。
- 输入输出:函数既读取旧值,也修改它。
输入参数:只读数据用 const
#include <stddef.h>
int sum_array(const int *data, size_t count)
{
int sum = 0;
if (data == NULL) {
return 0;
}
for (size_t i = 0; i < count; ++i) {
sum += data[i];
}
return sum;
}const int *data 表示函数不会通过 data 修改数组内容。这个约束很有价值:调用者能放心传入只读数据,编译器也能帮你发现误改。
输出参数:让函数写回一个结果
int parse_int(const char *text, int *out_value)
{
int value = 0;
if (text == NULL || out_value == NULL) {
return 0;
}
for (size_t i = 0; text[i] != '\0'; ++i) {
if (text[i] < '0' || text[i] > '9') {
return 0;
}
value = value * 10 + (text[i] - '0');
}
*out_value = value;
return 1;
}调用方式:
int value = 0;
if (parse_int("123", &value)) {
printf("%d\n", value);
}输出参数常和返回值配合使用:返回值表示成功或失败,输出参数保存真正的结果。
输入输出参数:修改调用者已有对象
void clamp(int *value, int low, int high)
{
if (value == NULL) {
return;
}
if (*value < low) {
*value = low;
} else if (*value > high) {
*value = high;
}
}这类函数要特别注意命名。const int * 通常暗示只读,int * 通常暗示可能修改。
2. 数组接口:指针加长度
C 函数拿不到数组本身,常规工程写法是显式传入“首元素指针 + 元素个数”。
void print_array(const int *data, size_t count)
{
if (data == NULL) {
return;
}
for (size_t i = 0; i < count; ++i) {
printf("%d\n", data[i]);
}
}调用:
int values[] = {10, 20, 30};
print_array(values, sizeof(values) / sizeof(values[0]));在函数内部不要用 sizeof(data) 计算数组长度。函数参数里的 data 是指针,sizeof(data) 得到的是指针大小,不是数组总大小。
修改数组内容
void fill_array(int *data, size_t count, int value)
{
if (data == NULL) {
return;
}
for (size_t i = 0; i < count; ++i) {
data[i] = value;
}
}只读数组用 const int *,可写数组用 int *。这是接口语义的一部分,不只是语法细节。
缓冲区接口:字节数要说清楚
处理内存缓冲区时,size 通常表示字节数,count 通常表示元素个数。不要混用。
#include <stddef.h>
void zero_buffer(void *buffer, size_t size)
{
unsigned char *bytes = buffer;
if (bytes == NULL) {
return;
}
for (size_t i = 0; i < size; ++i) {
bytes[i] = 0;
}
}推荐命名:
data, count:元素指针和元素个数。buffer, size:字节缓冲区和字节数。items, item_count:对象数组和对象数量。
3. 二级指针:修改调用者的指针
二级指针最常见的工程用途是“函数内部分配资源,然后把指针交给调用者”。
#include <stdlib.h>
int create_int(int **out)
{
int *p = NULL;
if (out == NULL) {
return 0;
}
p = malloc(sizeof(*p));
if (p == NULL) {
*out = NULL;
return 0;
}
*p = 10;
*out = p;
return 1;
}调用:
int *p = NULL;
if (create_int(&p)) {
printf("%d\n", *p);
free(p);
p = NULL;
}这里 out 是“调用者的指针变量的地址”。函数通过 *out = p; 修改调用者手里的指针。
用结构体降低二级指针的混乱
如果要返回的不只是一个指针,还包括长度、容量或状态,优先用结构体承载。
typedef struct IntBuffer {
int *data;
size_t count;
} IntBuffer;
int create_buffer(size_t count, IntBuffer *out)
{
if (out == NULL) {
return 0;
}
out->data = calloc(count, sizeof(out->data[0]));
if (out->data == NULL) {
out->count = 0;
return 0;
}
out->count = count;
return 1;
}
void destroy_buffer(IntBuffer *buffer)
{
if (buffer == NULL) {
return;
}
free(buffer->data);
buffer->data = NULL;
buffer->count = 0;
}这种写法比到处传 int ** 更容易维护。
4. 指针数组和数组指针
这两个名字相似,但工程含义完全不同。
指针数组:数组里放多个指针
const char *names[] = {"tom", "jerry", "alice"};
for (size_t i = 0; i < 3; ++i) {
printf("%s\n", names[i]);
}names 是数组,数组元素是 const char *。它常用于字符串列表、命令表、配置项表。
数组指针:指向整个数组
void print4(const int (*arr)[4])
{
if (arr == NULL) {
return;
}
for (size_t i = 0; i < 4; ++i) {
printf("%d\n", (*arr)[i]);
}
}
int values[4] = {1, 2, 3, 4};
print4(&values);int (*arr)[4] 表示“指向 4 个 int 的数组的指针”。这种写法适合表达固定长度约束,但日常接口更常用“指针 + 长度”。
5. 二维数组接口
普通二维数组不是 int **。如果数据是连续的二维数组,函数参数需要知道列数。
void print_matrix(size_t rows, size_t cols, const int matrix[rows][cols])
{
for (size_t r = 0; r < rows; ++r) {
for (size_t c = 0; c < cols; ++c) {
printf("%d ", matrix[r][c]);
}
puts("");
}
}调用:
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
print_matrix(2, 3, matrix);如果项目不使用 C99 变长数组参数,可以把二维数据摊平成一维数组:
int get_cell(const int *data, size_t cols, size_t row, size_t col)
{
return data[row * cols + col];
}这种写法在图像处理、矩阵计算、网络缓冲区解析里很常见。
6. 函数指针和回调
函数指针用于把“要执行的动作”作为参数传给另一个函数。
int greater_than(int a, int b)
{
return a > b;
}
int find_first(const int *data, size_t count, int (*pred)(int, int), int target)
{
if (data == NULL || pred == NULL) {
return -1;
}
for (size_t i = 0; i < count; ++i) {
if (pred(data[i], target)) {
return (int)i;
}
}
return -1;
}调用:
int values[] = {1, 3, 5, 7};
int index = find_first(values, 4, greater_than, 4);
printf("%d\n", index); // 2函数指针常用于:
- 排序比较函数。
- 事件回调。
- 驱动层操作表。
- 状态机处理函数表。
操作表示例
typedef int (*BinaryOp)(int, int);
typedef struct Command {
const char *name;
BinaryOp op;
} Command;
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
Command commands[] = {
{"add", add},
{"sub", sub}
};C++ 工程中,普通业务回调优先考虑 lambda、函数对象或 std::function。只有和 C 接口交互、需要固定 ABI 或追求极低开销时,函数指针才是首选。
7. void*:通用接口的代价
void* 可以保存任意对象地址,但它不保存类型、长度和所有权信息。使用它时必须额外传入这些信息。
void swap_bytes(void *lhs, void *rhs, size_t size)
{
unsigned char *a = lhs;
unsigned char *b = rhs;
if (a == NULL || b == NULL) {
return;
}
for (size_t i = 0; i < size; ++i) {
unsigned char tmp = a[i];
a[i] = b[i];
b[i] = tmp;
}
}调用:
int x = 10;
int y = 20;
swap_bytes(&x, &y, sizeof(x));void* 适合写底层通用工具,但会牺牲类型检查。业务代码里不要为了“通用”滥用 void*。
8. const 指针快速判断
int a = 10;
int b = 20;
const int *p1 = &a; // 不能通过 p1 改 a,p1 可以指向 b
p1 = &b;
int *const p2 = &a; // p2 不能改指向,可以通过 p2 改 a
*p2 = 30;
const int *const p3 = &a; // 既不能通过 p3 改值,也不能改 p3 指向阅读方法:从变量名开始向外读。p2 先遇到 const,所以指针本身不可改;p1 指向的是 const int,所以不能通过它改值。
详细规则见 CandCppconst。
最佳代码实践
- 指针参数只读时写成
const T *。 - 数组参数总是额外传长度,不在函数内部用
sizeof(pointer)猜长度。 - 输出参数先检查是否为
NULL,失败时尽量给输出参数写入安全状态。 - 动态资源用成对函数管理,例如
create_xxx/destroy_xxx。 - 复杂数据返回优先用结构体,不要滥用多层指针。
void*接口必须同时说明类型约定、元素大小、元素个数或字节数。- C++ 新代码优先用引用、
std::span、std::vector、智能指针和 RAII;裸指针主要用于非拥有关系、C 接口和底层边界。
常见错误
void clear(int *data)
{
memset(data, 0, sizeof(data)); // 错误:sizeof(data) 是指针大小
}int *bad(void)
{
int value = 10;
return &value; // 错误:返回局部变量地址
}void print_value(void *raw)
{
printf("%d\n", *raw); // 错误:void* 不能直接解引用
}void set_first(int **matrix)
{
matrix[0][0] = 1;
}
int matrix[3][4];
set_first(matrix); // 错误:普通二维数组不是 int**注意事项
- “能用指针写出来”不等于“应该用指针写出来”。
- 工程接口优先表达清楚所有权、长度、是否可写和失败行为。
- 指针是否为
NULL只是安全条件之一,生命周期、范围、类型兼容同样重要。 - 更细的 C 指针规则继续看 C语言指针细节;资源所有权继续看 C语言MallocFree 和 所有权模型。
学习路径
- 上一节:CandCpp指针
- 当前阶段:CandCPP基础
- 下一节:CandCppconst