在C语言的世界里,指针就像一把瑞士军刀,功能强大但需要谨慎使用。上篇我们讨论了指针的基础知识,今天我们将深入探讨几个关键的高级话题:字符数组与字符串参数传递、函数指针与指针函数的区别,以及const指针的各种用法。
提示:理解指针的关键在于区分"指针本身"和"指针指向的内容"。就像快递单上的地址(指针)和实际包裹(内容)是两回事。
在C语言中,字符串本质上是字符数组,以空字符'\0'结尾。当我们将字符串传递给函数时,实际上传递的是数组首元素的地址(即指针)。这个看似简单的机制背后有几个重要细节需要注意。
当字符数组作为函数参数时,会发生"数组退化"现象——无论函数声明中写的是char[]还是char*,编译器都会将其视为char*。这不是bug,而是C语言的设计特性。
c复制// 以下三种声明完全等价
void printStr(char str[]);
void printStr(char* str);
void printStr(char str[100]); // 这里的100会被忽略
为什么这样设计?因为在函数调用时,如果传递整个数组,会导致大量数据拷贝,严重影响性能。传递指针(地址)则高效得多。
处理字符串时,我们常用指针来遍历字符,直到遇到'\0'。这是一个经典模板:
c复制void printString(const char* str) {
const char* p = str; // 使用const确保不修改字符串内容
while(*p != '\0') {
putchar(*p);
p++;
}
}
注意事项:
问题1:修改字符串字面量
c复制char* str = "hello"; // 字符串字面量存储在只读区
str[0] = 'H'; // 运行时错误:尝试修改只读内存
解决方案:
c复制char str[] = "hello"; // 创建可修改的副本
str[0] = 'H'; // 正确
问题2:缓冲区溢出
c复制void unsafeCopy(char* dest, const char* src) {
int i = 0;
while(src[i] != '\0') {
dest[i] = src[i]; // 可能越界
i++;
}
dest[i] = '\0';
}
解决方案:
c复制void safeCopy(char* dest, const char* src, size_t destSize) {
size_t i = 0;
while(src[i] != '\0' && i < destSize - 1) {
dest[i] = src[i];
i++;
}
dest[i] = '\0';
}
函数指针是C语言中最强大的特性之一,它允许我们将函数作为参数传递、存储在数组中,甚至实现类似面向对象的多态行为。
声明函数指针时,关键是匹配目标函数的签名(返回类型和参数类型)。例如,对于以下函数:
c复制int add(int a, int b) { return a + b; }
对应的函数指针声明为:
c复制int (*op)(int, int); // op是指向"返回int,接受两个int参数"函数的指针
使用技巧:
c复制typedef int (*Operation)(int, int);
Operation op = add;
c复制int result = op(3, 5); // 简洁写法
int result = (*op)(3, 5); // 传统写法
回调函数是函数指针最典型的应用场景。考虑一个排序函数,我们希望它能支持不同的比较逻辑:
c复制// 比较函数类型定义
typedef int (*CompareFunc)(const void*, const void*);
// 通用排序函数
void sort(int* array, size_t size, CompareFunc compare) {
for(size_t i = 0; i < size-1; i++) {
for(size_t j = 0; j < size-i-1; j++) {
if(compare(&array[j], &array[j+1]) > 0) {
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
// 升序比较
int ascending(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
// 降序比较
int descending(const void* a, const void* b) {
return *(int*)b - *(int*)a;
}
// 使用示例
int main() {
int nums[] = {3, 1, 4, 1, 5, 9};
size_t count = sizeof(nums)/sizeof(nums[0]);
sort(nums, count, ascending); // 升序排序
sort(nums, count, descending); // 降序排序
return 0;
}
函数指针数组可以替代复杂的switch-case结构,使代码更简洁:
c复制#include <stdio.h>
// 定义几个简单的数学运算函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return b != 0 ? a / b : 0; }
// 定义函数指针数组
int (*operations[])(int, int) = {add, sub, mul, div};
// 运算类型枚举
typedef enum { ADD, SUB, MUL, DIV } Operation;
int calculate(Operation op, int a, int b) {
if(op >= ADD && op <= DIV) {
return operations[op](a, b);
}
return 0;
}
int main() {
printf("3 + 5 = %d\n", calculate(ADD, 3, 5));
printf("3 * 5 = %d\n", calculate(MUL, 3, 5));
return 0;
}
指针函数是返回指针的函数,它提供了灵活性但也带来了风险,特别是当返回局部变量的地址时。
c复制int* createArray(size_t size) {
int* arr = malloc(size * sizeof(int));
if(arr != NULL) {
for(size_t i = 0; i < size; i++) {
arr[i] = i * i;
}
}
return arr; // 调用者必须记得free
}
c复制const char* getGreeting() {
static char greeting[] = "Hello, World!"; // static延长生命周期
return greeting;
}
c复制char* toUpperCase(char* str) {
for(char* p = str; *p; p++) {
if(*p >= 'a' && *p <= 'z') {
*p -= 32;
}
}
return str; // 返回传入的指针
}
错误示例:返回局部数组
c复制char* getTimeString() {
char timeStr[20]; // 局部数组,函数返回后失效
sprintf(timeStr, "%02d:%02d:%02d", hour, min, sec);
return timeStr; // 危险!返回栈内存指针
}
修正方案1:使用static
c复制char* getTimeString() {
static char timeStr[20]; // 静态存储期
sprintf(timeStr, "%02d:%02d:%02d", hour, min, sec);
return timeStr; // 安全但不可重入
}
修正方案2:动态分配
c复制char* getTimeString() {
char* timeStr = malloc(20);
if(timeStr != NULL) {
sprintf(timeStr, "%02d:%02d:%02d", hour, min, sec);
}
return timeStr; // 调用者必须free
}
当需要返回复杂数据结构时,返回结构体指针是常见做法:
c复制typedef struct {
int x;
int y;
} Point;
Point* createPoint(int x, int y) {
Point* p = malloc(sizeof(Point));
if(p != NULL) {
p->x = x;
p->y = y;
}
return p;
}
void freePoint(Point* p) {
free(p);
}
使用注意事项:
const关键字与指针结合使用可以创建多种保护级别,理解这些区别对编写健壮代码至关重要。
c复制const int* p; // 或 int const* p;
c复制int* const p = &some_var;
c复制const int* const p = &some_const_var;
c复制const int* const* pp; // 指向"指向常量的常量指针"的指针
案例1:字符串处理函数
c复制// 不良设计:可能意外修改输入字符串
void print(char* str);
// 良好设计:明确承诺不修改输入
void print(const char* str);
案例2:硬件寄存器访问
c复制volatile uint32_t* const reg = (uint32_t*)0x12345678;
// reg是常量指针,指向易变的硬件寄存器
// 可以:*reg = value; // 写入寄存器
// 不可以:reg = other_address; // 改变指针
案例3:配置结构体
c复制typedef struct {
int id;
char name[20];
} Config;
void processConfig(const Config* config) {
// 可以读取config->id和config->name
// 但不能修改它们
}
const保护可以被类型转换绕过,但这通常是糟糕的做法:
c复制const int x = 10;
int* p = (int*)&x; // 危险:去除了const限定
*p = 20; // 未定义行为:可能崩溃或静默失败
安全的方式是只有当你知道数据确实可变时才去除const:
c复制int y = 10;
const int* cp = &y; // 合法:添加const限定
int* p = (int*)cp; // 安全:原始对象是可变的
*p = 20; // 合法
让我们用一个综合案例展示指针高级用法的威力:实现一个基于栈的简单虚拟机。
c复制#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#define STACK_SIZE 256
#define PROGRAM_SIZE 1024
typedef enum {
PUSH,
POP,
ADD,
SUB,
HALT
} OpCode;
typedef struct {
OpCode op;
int operand;
} Instruction;
// 虚拟机状态
typedef struct {
int stack[STACK_SIZE];
int stack_ptr;
Instruction program[PROGRAM_SIZE];
int pc; // 程序计数器
} VM;
// 指令处理函数类型
typedef void (*InstructionHandler)(VM*, int);
// 指令处理函数
void push(VM* vm, int operand) {
if(vm->stack_ptr < STACK_SIZE) {
vm->stack[vm->stack_ptr++] = operand;
}
}
void pop(VM* vm, int _) {
if(vm->stack_ptr > 0) {
printf("Popped: %d\n", vm->stack[--vm->stack_ptr]);
}
}
void add(VM* vm, int _) {
if(vm->stack_ptr >= 2) {
int a = vm->stack[--vm->stack_ptr];
int b = vm->stack[--vm->stack_ptr];
vm->stack[vm->stack_ptr++] = a + b;
}
}
void sub(VM* vm, int _) {
if(vm->stack_ptr >= 2) {
int a = vm->stack[--vm->stack_ptr];
int b = vm->stack[--vm->stack_ptr];
vm->stack[vm->stack_ptr++] = b - a;
}
}
void halt(VM* vm, int _) {
vm->pc = -1; // 终止执行
}
// 指令处理函数查找表
InstructionHandler handlers[] = {push, pop, add, sub, halt};
// 执行程序
void run(VM* vm) {
while(vm->pc >= 0) {
Instruction instr = vm->program[vm->pc];
handlers[instr.op](vm, instr.operand);
vm->pc++;
}
}
int main() {
VM vm = {0};
// 简单程序:计算 (5 + 3) - 2
vm.program[0] = (Instruction){PUSH, 5};
vm.program[1] = (Instruction){PUSH, 3};
vm.program[2] = (Instruction){ADD, 0};
vm.program[3] = (Instruction){PUSH, 2};
vm.program[4] = (Instruction){SUB, 0};
vm.program[5] = (Instruction){HALT, 0};
run(&vm);
if(vm.stack_ptr > 0) {
printf("Result: %d\n", vm.stack[vm.stack_ptr-1]);
}
return 0;
}
这个案例展示了:
指针操作虽然强大,但也需要考虑性能影响。以下是一些关键点:
c复制void add(int* a, int* b, int* result) {
*result = *a + *b; // 如果result和a或b指向同一内存,会有别名问题
}
解决方案:使用restrict关键字(C99)
c复制void add(int* restrict a, int* restrict b, int* restrict result);
缓存友好性
指针追逐(通过指针访问分散的内存)会导致缓存命中率下降。相比之下,连续数组访问效率更高。
分支预测
函数指针调用可能干扰CPU的分支预测。对于性能关键代码,考虑用switch替代函数指针数组。
循环优化
c复制// 原始版本
for(int i = 0; i < n; i++) {
sum += array[i];
}
// 指针优化版本
int* p = array;
int* end = array + n;
while(p != end) {
sum += *p++;
}
现代编译器通常能自动优化数组索引为指针形式,手动优化可能适得其反。
指针相关的bug往往难以追踪,以下是一些实用技巧:
c复制void safeAccess(int* array, size_t size, size_t index) {
assert(index < size && "Index out of bounds");
// 安全访问array[index]
}
c复制typedef int* IntPtr;
typedef void (*Callback)(int, void*);
c复制#define DEBUG_PTR(p) printf("%s at %p points to %p\n", #p, (void*)&p, (void*)p)
int* ptr = malloc(sizeof(int));
DEBUG_PTR(ptr);
随着C标准的发展,一些新特性可以帮助我们更安全地使用指针:
c复制typedef struct {
void* ptr;
void (*deleter)(void*);
} SmartPtr;
SmartPtr makeSmartPtr(void* p, void (*d)(void*)) {
return (SmartPtr){p, d};
}
void releaseSmartPtr(SmartPtr* sp) {
if(sp->deleter && sp->ptr) {
sp->deleter(sp->ptr);
sp->ptr = NULL;
}
}
c复制#include <stdint.h>
uintptr_t ptr_value = (uintptr_t)pointer_var; // 指针到整型的可移植转换
_Generic可以改进NULL检测:c复制#define is_nullptr(x) _Generic((x), \
void*: !(x), \
default: 0)
c复制void foo(int* __attribute__((nonnull)) ptr);
指针在不同平台上可能有不同的表现:
sizeof(void*)检测c复制#include <stdalign.h>
alignas(16) int* aligned_ptr; // 16字节对齐
c复制uint32_t ntohl(uint32_t netlong); // 网络字节序转换
c复制struct SharedData {
size_t offset_to_data;
// ...
};
虽然本文聚焦C语言,但了解C++的指针发展有助于拓宽视野:
cpp复制int x = 10;
int& ref = x; // 比指针更安全的别名
cpp复制#include <memory>
std::unique_ptr<int> smartPtr(new int(42));
cpp复制std::vector<int> vec = {1, 2, 3};
for(auto it = vec.begin(); it != vec.end(); ++it) {
*it += 1; // 类似指针的解引用
}
cpp复制void foo(int* p); // C风格
void bar(std::span<int> data); // C++20更安全的方式
对于C程序员来说,理解这些概念有助于写出更现代的代码,即使是在纯C环境中。