跳到主要内容

指针

指针是 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 = &num;
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 指针:通用指针
  • 野指针与悬空指针:避免常见错误
  • 指针最佳实践

下一章将学习 结构体与联合体,了解如何创建自定义数据类型。