指针
指针是 C 语言最核心、最强大的特性之一。指针允许直接操作内存地址,是理解 C 语言底层机制的关键。
指针基础
什么是指针
指针是一个变量,其值是另一个变量的内存地址。通过指针可以直接访问和修改内存中的数据。
每个变量在内存中都有一个地址,指针就是存储这个地址的变量。
int num = 10;
int* ptr = # // ptr 存储 num 的地址
printf("num 的值: %d\n", num); // 10
printf("num 的地址: %p\n", (void*)&num); // 0x7ffd12345678
printf("ptr 的值: %p\n", (void*)ptr); // 0x7ffd12345678(与 &num 相同)
printf("ptr 指向的值: %d\n", *ptr); // 10
指针的声明
int* ptr; // 指向 int 的指针
char* cptr; // 指向 char 的指针
double* dptr; // 指向 double 的指针
void* vptr; // 通用指针,可以指向任何类型
* 的位置是风格问题,以下写法等价:
int *ptr;
int * ptr;
int* ptr;
推荐:声明时 * 靠近变量名,避免多指针声明的混淆:
int *ptr1, *ptr2; // 两个指针
int* ptr3, ptr4; // ptr3 是指针,ptr4 是 int(容易混淆)
取地址与解引用
& 运算符获取变量的地址,* 运算符访问指针指向的值:
int num = 10;
int* ptr = # // & 取地址
printf("值: %d\n", *ptr); // * 解引用,获取指向的值
*ptr = 20; // 通过指针修改值
printf("num = %d\n", num); // 20
指针的初始化
指针应该始终初始化,未初始化的指针包含随机地址,使用它会导致未定义行为:
int* ptr1 = NULL; // 初始化为空指针
int* ptr2 = 0; // 同上,NULL 通常定义为 0
int value = 10;
int* ptr3 = &value; // 初始化为有效地址
int* ptr4; // 未初始化,危险!
现代 C(C23)推荐使用 nullptr:
int* ptr = nullptr; // C23 新增
空指针
空指针不指向任何有效地址,用于表示指针无效:
int* ptr = NULL;
if (ptr == NULL) {
printf("指针为空\n");
}
// 使用前检查
if (ptr != NULL) {
printf("%d\n", *ptr);
}
解引用空指针会导致程序崩溃(段错误):
int* ptr = NULL;
printf("%d\n", *ptr); // 错误!段错误
指针运算
指针加减整数
指针加减整数时,实际移动的字节数取决于指针类型:
int arr[] = {10, 20, 30, 40, 50};
int* ptr = arr; // 指向 arr[0]
printf("%d\n", *ptr); // 10
printf("%d\n", *(ptr + 1)); // 20(移动 sizeof(int) 字节)
printf("%d\n", *(ptr + 2)); // 30
ptr++; // ptr 现在指向 arr[1]
printf("%d\n", *ptr); // 20
指针运算以元素大小为单位:
char c_arr[] = {'a', 'b', 'c'};
char* c_ptr = c_arr;
c_ptr++; // 移动 1 字节
int i_arr[] = {1, 2, 3};
int* i_ptr = i_arr;
i_ptr++; // 移动 4 字节(假设 int 为 4 字节)
指针相减
两个指向同一数组的指针可以相减,结果是元素个数:
int arr[] = {10, 20, 30, 40, 50};
int* p1 = &arr[1];
int* p2 = &arr[4];
printf("元素个数: %td\n", p2 - p1); // 3
指针比较
指向同一数组的指针可以比较:
int arr[] = {10, 20, 30, 40, 50};
int* start = arr;
int* end = arr + 5;
while (start < end) {
printf("%d ", *start);
start++;
}
// 输出: 10 20 30 40 50
指针与数组
数组名与指针
数组名在大多数情况下会退化为指向首元素的指针:
int arr[] = {10, 20, 30};
int* ptr = arr; // arr 退化为 &arr[0]
printf("%d\n", *ptr); // 10
printf("%d\n", arr[0]); // 10
printf("%d\n", ptr[0]); // 10(指针可以用下标访问)
printf("%d\n", *(arr + 1)); // 20
printf("%d\n", *(ptr + 1)); // 20
数组名与指针的区别
数组名是常量指针,不能修改:
int arr[] = {10, 20, 30};
int* ptr = arr;
ptr++; // 正确:指针可以修改
// arr++; // 错误:数组名不能修改
sizeof(arr); // 返回整个数组的大小
sizeof(ptr); // 返回指针的大小(4 或 8 字节)
指针遍历数组
int arr[] = {10, 20, 30, 40, 50};
int* start = arr;
int* end = arr + 5;
for (int* p = start; p < end; p++) {
printf("%d ", *p);
}
数组作为函数参数
数组作为参数传递时退化为指针:
void print_array(int* arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
void print_array_alt(int arr[], int size) {
// 等价于 int* arr
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
指针与字符串
字符串可以用字符指针表示:
char str[] = "Hello"; // 字符数组,可修改
char* ptr = "Hello"; // 指向字符串字面量,不可修改
str[0] = 'h'; // 正确
// ptr[0] = 'h'; // 错误!字符串字面量是只读的
字符串字面量存储在只读区域,尝试修改会导致未定义行为。
安全的做法:
const char* ptr = "Hello"; // 使用 const 防止修改
字符串遍历:
const char* str = "Hello";
while (*str != '\0') {
printf("%c ", *str);
str++;
}
指针与 const
指向常量的指针
不能通过指针修改指向的值:
int value = 10;
const int* ptr = &value; // 或 int const* ptr
// *ptr = 20; // 错误:不能修改指向的值
value = 20; // 正确:可以直接修改原变量
int other = 30;
ptr = &other; // 正确:可以改变指针指向
常量指针
指针本身不能修改:
int value = 10;
int other = 20;
int* const ptr = &value;
*ptr = 20; // 正确:可以修改指向的值
// ptr = &other; // 错误:不能改变指针指向
指向常量的常量指针
两者都不能修改:
int value = 10;
const int* const ptr = &value;
// *ptr = 20; // 错误
// ptr = &other; // 错误
记忆技巧:const 修饰其左边的内容,如果左边没有则修饰右边。
const int* p; // const 修饰 *p,即 *p 不能修改
int* const p; // const 修饰 p,即 p 不能修改
指针数组与数组指针
指针数组
指针数组是元素为指针的数组:
int a = 1, b = 2, c = 3;
int* arr[3] = {&a, &b, &c}; // 包含 3 个 int 指针的数组
for (int i = 0; i < 3; i++) {
printf("%d ", *arr[i]);
}
字符串数组常用指针数组:
const char* fruits[] = {
"Apple",
"Banana",
"Orange"
};
for (int i = 0; i < 3; i++) {
printf("%s\n", fruits[i]);
}
数组指针
数组指针是指向数组的指针:
int arr[3] = {1, 2, 3};
int (*ptr)[3] = &arr; // 指向包含 3 个 int 的数组
printf("%d\n", (*ptr)[0]); // 1
printf("%d\n", (*ptr)[1]); // 2
用于二维数组:
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
int (*ptr)[3] = matrix; // 指向第一行
printf("%d\n", ptr[0][0]); // 1
printf("%d\n", ptr[1][2]); // 6
区分指针数组和数组指针
int* arr[3]; // 指针数组:3 个 int 指针
int (*ptr)[3]; // 数组指针:指向 int[3] 的指针
解析:[] 的优先级高于 *。
多级指针
二级指针
二级指针存储一级指针的地址:
int value = 10;
int* ptr = &value;
int** pptr = &ptr;
printf("value = %d\n", value); // 10
printf("*ptr = %d\n", *ptr); // 10
printf("**pptr = %d\n", **pptr); // 10
二级指针的应用
修改指针本身:
void allocate(int** pptr, int value) {
*pptr = malloc(sizeof(int));
if (*pptr != NULL) {
**pptr = value;
}
}
int main(void) {
int* ptr = NULL;
allocate(&ptr, 10);
if (ptr != NULL) {
printf("%d\n", *ptr); // 10
free(ptr);
}
return 0;
}
字符串数组作为参数:
void print_strings(char** strings, int count) {
for (int i = 0; i < count; i++) {
printf("%s\n", strings[i]);
}
}
int main(void) {
char* fruits[] = {"Apple", "Banana", "Orange"};
print_strings(fruits, 3);
return 0;
}
函数指针
函数指针是指向函数的指针,用于回调、策略模式等场景。
声明与使用
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int main(void) {
int (*operation)(int, int); // 声明函数指针
operation = add; // 指向 add 函数
printf("%d\n", operation(5, 3)); // 8
operation = subtract;
printf("%d\n", operation(5, 3)); // 2
return 0;
}
函数指针数组
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int main(void) {
int (*ops[])(int, int) = {add, subtract, multiply};
char* names[] = {"+", "-", "*"};
int a = 10, b = 5;
for (int i = 0; i < 3; i++) {
printf("%d %s %d = %d\n", a, names[i], b, ops[i](a, b));
}
return 0;
}
回调函数
typedef int (*CompareFunc)(const void*, const void*);
void bubble_sort(void* arr, size_t count, size_t size, CompareFunc compare) {
char* base = arr;
char temp[size];
for (size_t i = 0; i < count - 1; i++) {
for (size_t j = 0; j < count - 1 - i; j++) {
void* a = base + j * size;
void* b = base + (j + 1) * size;
if (compare(a, b) > 0) {
memcpy(temp, a, size);
memcpy(a, b, size);
memcpy(b, temp, size);
}
}
}
}
int compare_int(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
int main(void) {
int arr[] = {5, 2, 8, 1, 9};
int count = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, count, sizeof(int), compare_int);
for (int i = 0; i < count; i++) {
printf("%d ", arr[i]);
}
return 0;
}
使用 typedef 简化
typedef int (*Operation)(int, int);
int add(int a, int b) { return a + b; }
int main(void) {
Operation op = add;
printf("%d\n", op(3, 5));
return 0;
}
void 指针
void* 是通用指针,可以指向任何类型:
int num = 10;
double d = 3.14;
void* ptr;
ptr = #
printf("%d\n", *(int*)ptr); // 需要类型转换
ptr = &d;
printf("%f\n", *(double*)ptr);
void* 的用途:
- 通用函数参数(如
qsort的比较函数) - 动态内存分配(
malloc返回void*)
野指针与悬空指针
野指针
未初始化的指针,指向随机地址:
int* ptr; // 野指针,包含随机值
// printf("%d\n", *ptr); // 危险!未定义行为
解决方法:始终初始化指针为 NULL 或有效地址。
悬空指针
指向已释放内存的指针:
int* ptr = malloc(sizeof(int));
*ptr = 10;
free(ptr); // 释放内存
// ptr 仍然指向已释放的地址
// printf("%d\n", *ptr); // 危险!未定义行为
ptr = NULL; // 释放后置空
解决方法:释放内存后立即将指针置为 NULL。
指针与内存
内存区域
C 程序的内存布局:
| 区域 | 说明 |
|---|---|
| 代码段 | 存储程序代码,只读 |
| 数据段 | 存储全局变量和静态变量 |
| BSS 段 | 存储未初始化的全局变量 |
| 堆 | 动态分配的内存,向上增长 |
| 栈 | 局部变量和函数调用信息,向下增长 |
栈内存
局部变量存储在栈上,自动管理:
void func(void) {
int local = 10; // 栈上分配
int arr[100]; // 栈上分配
} // 函数返回时自动释放
栈空间有限,大数组应使用堆内存。
堆内存
动态分配的内存在堆上,需要手动管理:
int* ptr = malloc(100 * sizeof(int)); // 堆上分配
if (ptr != NULL) {
// 使用内存
free(ptr); // 手动释放
}
指针最佳实践
1. 始终初始化指针
int* ptr = NULL; // 好习惯
int* bad_ptr; // 危险
2. 使用前检查空指针
void process(int* ptr) {
if (ptr == NULL) {
return; // 或报错
}
// 使用 ptr
}
3. 释放后置空
free(ptr);
ptr = NULL;
4. 使用 const 保护数据
void print_string(const char* str) {
// str[0] = 'x'; // 编译错误
printf("%s\n", str);
}
5. 避免返回局部变量的地址
int* bad_func(void) {
int local = 10;
return &local; // 错误!返回局部变量地址
}
int* good_func(void) {
static int local = 10; // 静态变量
return &local;
}
int* better_func(void) {
int* ptr = malloc(sizeof(int));
if (ptr != NULL) {
*ptr = 10;
}
return ptr; // 调用者负责释放
}
小结
本章介绍了 C 语言的指针:
- 指针基础:声明、初始化、取地址、解引用
- 指针运算:加减、比较、相减
- 指针与数组:数组名与指针的关系
- 指针与字符串:字符串指针的使用
- 指针与 const:各种 const 组合
- 指针数组与数组指针:区分两者的语法
- 多级指针:二级指针的概念和应用
- 函数指针:声明、使用、回调
- void 指针:通用指针
- 野指针与悬空指针:避免常见错误
- 指针最佳实践
下一章将学习 结构体与联合体,了解如何创建自定义数据类型。