前言
"指针是C语言的灵魂,也是C语言的诅咒。"这句话道出了程序员对指针的复杂情感。理解指针,意味着你真正理解了C语言;畏惧指针,则说明你还没有完全掌握它。
指针就像现实生活中的地址:知道一个房子的地址,你可以找到房子;修改地址,你可以让信寄到不同的地方。掌握指针,你就掌握了C语言的"地址簿"。
一、指针基础:地址与值
1.1 什么是指针?
指针是一个变量,其值为另一个变量的内存地址。
int num = 10; // 普通整型变量
int *p = # // 指针变量,存储num的地址
printf("num的值: %d\n", num); // 输出: 10
printf("num的地址: %p\n", &num); // 输出: 0x16fdfee4c
printf("p的值: %p\n", p); // 输出: 0x16fdfee4c
printf("p指向的值: %d\n", *p); // 输出: 10
1.2 指针运算符
在上面的代码中,我们用到了&、*,他们的含义如下:
| 运算符 | 名称 | 作用 | 示例 |
|---|---|---|---|
& |
取地址符 | 获取变量地址 | &num |
* |
解引用符 | 获取指针指向的值 | *p |
* |
指针声明 | 声明指针变量 | int *p |
int a = 5;
int *ptr = &a; // ptr指向a的地址
// 通过指针修改变量值
*ptr = 20; // 等价于 a = 20
printf("a = %d\n", a); // 输出: 20
二、指针的声明与初始化
2.1 正确的声明方式
// 正确的方式
int *p; // 声明一个整型指针
char *c; // 声明一个字符指针
float *f; // 声明一个浮点指针
double *d; // 声明一个双精度指针
// 常见错误
int* p1, p2; // 错误!p1是指针,p2是int,不是指针!
int *p3, *p4; // 正确,p3和p4都是指针
2.2 初始化的重要性
// 危险:未初始化的指针(野指针)
int *danger; // 指向随机内存地址
// *danger = 10; // 可能导致程序崩溃!
// 安全初始化
int safe = 0;
int *safe_ptr = &safe; // 指向已知变量
int *null_ptr = NULL; // 初始化为空指针
int *malloc_ptr = (int*)malloc(sizeof(int)); // 动态分配
三、指针与数组
3.1 数组名是指针常量
C语言中,数组名城本身就是数组首地址,因此我们有以下几种方式访问数组:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // arr本身就是数组首地址
// 以下等价访问方式
printf("arr[0] = %d\n", arr[0]); // 下标访问
printf("*arr = %d\n", *arr); // 指针访问
printf("*p = %d\n", *p); // 通过指针访问
printf("p[0] = %d\n", p[0]); // 指针的下标访问
3.2 指针算术运算
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
printf("p指向: %d\n", *p); // 输出: 10
printf("p+1指向: %d\n", *(p+1)); // 输出: 20
printf("p+2指向: %d\n", *(p+2)); // 输出: 30
// 指针加减的含义
p = p + 1; // 指向下一个int(地址增加sizeof(int)字节)
p = p - 1; // 指向上一个int
3.3 遍历数组的多种方式
int arr[5] = {1, 2, 3, 4, 5};
int *p;
// 方式1:下标法
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 方式2:指针法
for (p = arr; p < arr + 5; p++) {
printf("%d ", *p);
}
printf("\n");
// 方式3:指针偏移法
for (int i = 0; i < 5; i++) {
printf("%d ", *(arr + i));
}
printf("\n");
四、多级指针
4.1 二级指针
int num = 100;
int *p1 = # // 一级指针,指向int
int **p2 = &p1; // 二级指针,指向int*
printf("num: %d\n", num); // 100
printf("*p1: %d\n", *p1); // 100
printf("**p2: %d\n", **p2); // 100
printf("&num: %p\n", &num); // num的地址
printf("p1: %p\n", p1); // 同上
printf("*p2: %p\n", *p2); // 同上
printf("&p1: %p\n", &p1); // p1的地址
printf("p2: %p\n", p2); // 同上
4.2 指针数组 vs 数组指针
// 指针数组:数组的元素是指针
int a = 1, b = 2, c = 3;
int *arr1[3] = {&a, &b, &c}; // 存放3个int指针的数组
// 数组指针:指向数组的指针
int arr2[3] = {4, 5, 6};
int (*p_arr)[3] = &arr2; // 指向包含3个int的数组的指针
// 访问方式
printf("*arr1[0] = %d\n", *arr1[0]); // 输出: 1
printf("(*p_arr)[0] = %d\n", (*p_arr)[0]); // 输出: 4
五、指针与函数
5.1 指针作为函数参数
// 值传递
void swapByValue(int a,int b){
int temp = a;
a = b;
b = temp;
}
// 指针传递
void swapByPointer(int * a,int * b){
int temp = * a;
* a = * b;
* b = temp;
}
int main(int argc, const char * argv[]) {
int a = 10,b = 20;
printf("交换之前:a=\t%d,b = %d\t\n",a,b);
swapByValue(a, b);
printf("值传递交换之后:a=\t%d,b = %d\t\n",a,b);
swapByPointer(&a, &b);
printf("指针传递交换之后:a=\t%d,b = %d\t\n",a,b);
printf("\n");
return EXIT_SUCCESS;
}
控制台打印日志如下:
交换之前:a= 10,b = 20
值传递交换之后:a= 10,b = 20
指针传递交换之后:a= 20,b = 10
5.2 函数指针
// 普通函数
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() {
// 声明函数指针
int (*operation)(int, int);
// 指向不同函数
operation = add;
printf("10 + 5 = %d\n", operation(10, 5)); // 输出: 15
operation = subtract;
printf("10 - 5 = %d\n", operation(10, 5)); // 输出: 5
operation = multiply;
printf("10 * 5 = %d\n", operation(10, 5)); // 输出: 50
return 0;
}
5.3 函数指针数组
// 回调函数示例
void process_array(int *arr, int size, int (*callback)(int)) {
for (int i = 0; i < size; i++) {
arr[i] = callback(arr[i]);
}
}
int square(int x) { return x * x; }
int double_value(int x) { return x * 2; }
int main() {
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
process_array(arr, size, square);
// 现在 arr = {1, 4, 9, 16, 25}
process_array(arr, size, double_value);
// 现在 arr = {2, 8, 18, 32, 50}
return 0;
}
六、动态内存管理
6.1 malloc、calloc、realloc、free
int main(int argc, const char * argv[]) {
// malloc: 分配内存,不初始化
int *p1 = (int*)malloc(5 * sizeof(int));
if (p1 == NULL) {
printf("内存分配失败!\n");
return 1;
}
printf("p1:%p\n",p1);
// calloc: 分配内存并初始化为0
int *p2 = (int*)calloc(5, sizeof(int)); // 5个int,全部为0
printf("p1:%p\n",p2);
// realloc: 重新分配内存
p1 = (int*)realloc(p1, 10 * sizeof(int)); // 扩展到10个int
printf("p1:%p\n",p1);
// free: 释放内存
free(p1);
free(p2);
// 避免悬空指针
p1 = NULL;
p2 = NULL;
return EXIT_SUCCESS;
}
控制台打印信息如下:
p1:0x1007a2620
p1:0x10079c520
p1:0x1007a27a0
6.2 动态二维数组
int main() {
int rows = 3, cols = 4;
// 分配行指针数组
int **matrix = (int**)malloc(rows * sizeof(int*));
// 为每一行分配列数组
for (int i = 0; i < rows; i++) {
matrix[i] = (int*)malloc(cols * sizeof(int));
}
// 初始化
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix[i][j] = i * cols + j;
}
}
// 释放内存(顺序重要!)
for (int i = 0; i < rows; i++) {
free(matrix[i]);
}
free(matrix);
return 0;
}
七、指针与字符串
7.1 字符串的两种表示
// 字符数组(栈上分配)
char str1[] = "Hello"; // 可修改
str1[0] = 'h'; // 允许
// 字符串字面量(常量区,只读)
char *str2 = "World"; // 不可修改
// str2[0] = 'w'; // 运行时错误!
// 正确修改方式
char *str3 = (char*)malloc(6 * sizeof(char));
strcpy(str3, "Hello");
str3[0] = 'h'; // 允许
free(str3);
7.2 常用字符串函数
#include <string.h>
#include <stdio.h>
int main() {
char src[] = "Hello World";
char dest[20];
// 字符串长度
int len = strlen(src); // 11
// 字符串复制
strcpy(dest, src); // dest = "Hello World"
// 字符串连接
strcat(dest, "!"); // dest = "Hello World!"
// 字符串比较
int cmp = strcmp("abc", "abd"); // 负值(abc < abd)
// 字符串查找
char *found = strchr(src, 'W'); // 指向"World"
return 0;
}
八、复杂指针声明解析
8.1 右左法则
用右左法则解析复杂指针声明:
-
从变量名开始
-
向右看,直到遇到
) -
向左看,直到遇到
( -
重复2-3,直到完全解析
int *ptr; // ptr是指向int的指针
int *arr[5]; // arr是数组,元素是int指针
int (*arr)[5]; // arr是指针,指向int数组[5]
int (func)(); // func是指针,指向返回int的函数
int ((*func[5])())(); // func是数组[5],元素是指针,指向返回函数指针的函数
8.2 typedef简化
// 复杂声明
int (*(*func)(int))[5];
// 用typedef简化
typedef int (*FuncReturningArrayPtr)(int);
typedef int (*Array5Ptr)[5];
Array5Ptr func_simplified(int);
// 等价于 int (*(*func_simplified)(int))[5];
九、指针的常见陷阱
9.1 悬空指针
int *create_dangling_pointer() {
int local = 10; // 局部变量
return &local; // 错误!返回局部变量地址
} // 函数结束,local被销毁
int main() {
int *p = create_dangling_pointer();
printf("%d\n", *p); // 未定义行为!
return 0;
}
9.2 内存泄漏
void memory_leak() {
int *p = (int*)malloc(sizeof(int) * 100);
// 使用p...
// 忘记free(p)! ← 内存泄漏
return;
}
9.3 缓冲区溢出
void buffer_overflow() {
char buffer[10];
char *p = buffer;
for (int i = 0; i <= 10; i++) { // 多写了一个字节!
p[i] = 'A';
} // 缓冲区溢出!
}
十、指针的最佳实践
10.1 防御性编程
// 良好的指针使用习惯
int safe_pointer_usage() {
int *ptr = NULL; // 总是初始化为NULL
ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) {
// 检查分配是否成功
return -1;
}
*ptr = 100;
// 使用前检查
if (ptr != NULL) {
printf("Value: %d\n", *ptr);
}
// 使用后清理
free(ptr);
ptr = NULL; // 避免悬空指针
return 0;
}
10.2 常量指针 vs 指针常量
int num = 10;
int other = 20;
// 常量指针:指针指向的值不能修改
const int *p1 = #
// *p1 = 20; // 错误!不能修改指向的值
p1 = &other; // 允许!可以指向其他变量
// 指针常量:指针本身不能修改
int *const p2 = #
*p2 = 20; // 允许!可以修改指向的值
// p2 = &other; // 错误!不能指向其他变量
// 既是指针常量又是常量指针
const int *const p3 = #
// *p3 = 20; // 错误!
// p3 = &other; // 错误!
十一、现代C语言中的指针
11.1 restrict关键字
// restrict表示指针是访问数据的唯一方式
void copy_array(int n, int *restrict dest, const int *restrict src) {
for (int i = 0; i < n; i++) {
dest[i] = src[i]; // 编译器可以优化
}
}
11.2 原子指针
#include <stdatomic.h>
atomic_int *atomic_ptr;
// 线程安全的指针操作
十二、实战:实现动态数组
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int *data; // 数据指针
int size; // 当前大小
int capacity; // 容量
} DynamicArray;
// 初始化动态数组
DynamicArray* da_create(int initial_capacity) {
DynamicArray *da = (DynamicArray*)malloc(sizeof(DynamicArray));
da->data = (int*)malloc(initial_capacity * sizeof(int));
da->size = 0;
da->capacity = initial_capacity;
return da;
}
// 添加元素
void da_append(DynamicArray *da, int value) {
if (da->size >= da->capacity) {
// 扩容
da->capacity *= 2;
da->data = (int*)realloc(da->data, da->capacity * sizeof(int));
}
da->data[da->size++] = value;
}
// 获取元素
int da_get(DynamicArray *da, int index) {
if (index < 0 || index >= da->size) {
printf("索引越界!\n");
return 0;
}
return da->data[index];
}
// 释放内存
void da_free(DynamicArray *da) {
free(da->data);
free(da);
}