C语言中的指针

前言

"指针是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 = &num;    // 一级指针,指向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 右左法则

右左法则解析复杂指针声明:

  1. 从变量名开始

  2. 向右看,直到遇到)

  3. 向左看,直到遇到(

  4. 重复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 = &num;
// *p1 = 20;  // 错误!不能修改指向的值
p1 = &other;  // 允许!可以指向其他变量

// 指针常量:指针本身不能修改
int *const p2 = &num;
*p2 = 20;     // 允许!可以修改指向的值
// p2 = &other;  // 错误!不能指向其他变量

// 既是指针常量又是常量指针
const int *const p3 = &num;
// *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);
}
相关推荐
查古穆2 小时前
栈-有效的括号
java·数据结构·算法
再一次等风来2 小时前
近场声全息(NAH)仿真实现:从阵列实值信号到波数域重建
算法·matlab·信号处理·近场声全息·nah
汀、人工智能2 小时前
16 - 高级特性
数据结构·算法·数据库架构·图论·16 - 高级特性
大熊背3 小时前
利用ISP离线模式进行分块LSC校正的方法
人工智能·算法·机器学习
XWalnut3 小时前
LeetCode刷题 day4
算法·leetcode·职场和发展
蒸汽求职3 小时前
机器人软件工程(Robotics SDE):特斯拉Optimus落地引发的嵌入式C++与感知算法人才抢夺战
大数据·c++·算法·职场和发展·机器人·求职招聘·ai-native
AI成长日志4 小时前
【笔面试算法学习专栏】双指针专题·简单难度两题精讲:167.两数之和II、283.移动零
学习·算法·面试
旖-旎4 小时前
分治(库存管理|||)(4)
c++·算法·leetcode·排序算法·快速选择算法
青稞社区.4 小时前
ICLR‘26 Oral | 当 LLM Agent 在多轮推理中迷失时:T3 如何让强化学习重新学会主动推理
人工智能·算法·agi