【万字长文】一文搞定C语言指针

新晋码农一枚,小编会定期整理一些写的比较好的代码和知识点,作为自己的学习笔记,试着做一下批注和补充,转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!本章内容较多,可点击文章目录进行跳转!

小编整理和学习了C语言的相关知识,可作为扫盲使用,后续也会更新一些技术类的文章,大家共同交流学习!

您的点赞、关注、收藏就是对小编最大的动力!

目录

一、指针的本质与内存模型

二、指针的核心操作与类型系统

三、指针与数组的深度关联

四、指针在函数中的高级应用

五、动态内存管理:malloc与free

六、结构体与指针的深度结合

七、const限定符与指针的复杂交互

八、指针的常见问题与调试技巧

九、指针的高级主题与扩展应用

十、门牌号一样大,但门牌号代表的面积不一样大

十一、指针的大小与系统相关,而指针的类型决定了如何解释它指向的内存内容

十二、总结与学习建议


一、指针的本质与内存模型

1.1 内存的物理与逻辑结构
计算机内存由物理存储单元组成,每个单元有唯一地址(通常以十六进制表示,如0x7ffd3a4c)。操作系统通过虚拟内存机制将物理地址映射为逻辑地址,为每个进程提供独立的地址空间。C语言指针直接操作这些逻辑地址,实现数据的间接访问。

1.2 指针的定义与二重性

  • 定义:指针是存储内存地址的变量,其类型决定了访问内存的“步长”和解释方式。
    #include <stdio.h>
    int main() {
        
        int num = 43;
        /*野指针风险:若省略& num(如int* p; 后直接解引用* p = 43; ),p成为未初始化的野指针,解引用将引发段错误(Segmentation Fault)*/
        int* p = &num; // p存储num的地址
        //  指针声明:int *p定义一个指向整型的指针变量p。
        //  *表示p是指针类型,int限定其指向的数据类型为整型。
        /*  指针大小:32位系统占4字节,64位系统占8字节,与int大小无关。
            取地址操作:&num 获取num的内存地址(如0x7ffd3a4c),并赋值给p。
            此时p存储该地址,p与& num等价。*/
    
            /*空指针保护:可初始化为NULL(如int* p = NULL; ),使用前需检查:*/
        if (p == NULL) {
            *p = 100; // 安全操作
        }
    
    
        printf("num的值: %d\n", num);        // 输出43
        printf("num的地址: %p\n", &num);     // 输出地址(如0x7ffd3a4c)
        printf("p的值: %p\n", p);           // 输出与&num相同
        printf("*p的值: %d\n", *p);         // 输出43
    
        *p = 100; // 修改p指向的值
        printf("修改后的num: %d\n", num);   // 输出100
        /* 解引用操作:通过* p访问p指向的内存数据。
                       * p等价于num,值为43。
                       修改* p(如* p = 100; )会同步改变num的值 
           类型安全:  int *p确保解引用时按int类型解释内存数据(4字节对齐,小端序/大端序依赖系统)。
                       错误类型匹配(如char *p = &num;)会导致数据解析错误 */
    
        return 0;
    }
  • 二重性
    • 地址属性:指针本身是变量,存储地址值。
    • 类型属性:指针类型(如int*char*)决定解引用时的行为(如读取4字节或1字节)。

1.3 指针的大小与系统架构

  • 32位系统:指针占4字节(地址范围0x00000000~0xFFFFFFFF)。
  • 64位系统:指针占8字节(地址范围0x0000000000000000~0xFFFFFFFFFFFFFFFF)。
  • 验证方法
    #include <stdio.h>
    //这是一个预处理指令,用于引入标准输入输出库(stdio.h)
    //因为程序中使用了printf函数(用于输出内容到控制台),而printf的声明就包含在stdio.h中,所以必须通过该指令引入这个库才能正常使用printf
    int main() {
        printf("Pointer size: %zu bytes\n", sizeof(int*));
        return 0;
    }
    //main函数是 C 程序的入口点,程序从main函数开始执行。
    //int表示main函数的返回值类型为整数,通常用于表示程序的执行状态(return 0表示程序正常结束)
    //sizeof(int*):sizeof是 C 语言的一个运算符,用于计算括号中数据类型或变量所占用的字节数。这里int*表示 “指向 int 类型的指针”,sizeof(int*)即计算这种指针在当前系统中占用的内存字节数
    //printf输出:printf函数用于将内容打印到控制台。格式字符串"Pointer size: %zu bytes\n"中,%zu是格式化占位符,用于匹配sizeof的返回值(size_t类型,无符号整数),最终会被替换为int*指针的字节数。
    //例如,在 32 位系统中,指针通常占用 4 字节;在 64 位系统中,指针通常占用 8 字节,因此程序输出可能是Pointer size: 8 bytes(取决于运行环境)

这里补充解释一下范围0-F:

快速查阅表

十六进制十进制二进制(4位表示)
000000
110001
220010
330011
440100
550101
660110
770111
881000
991001
A101010
B111011
C121100
D131101
E141110
F151111

二、指针的核心操作与类型系统

2.1 指针的声明与初始化

  • 声明语法
    int *p;    // 声明指向int的指针
    char *c;   // 声明指向char的指针
  • 初始化规则
    • 直接初始化
      int num = 10;
      int *p = &num; // 合法:p指向num
    • 动态初始化
      #include <stdio.h>
      #include <stdlib.h> // 包含 malloc 和 exit
      
      int main() {
          int* p = malloc(sizeof(int));
          if (p == NULL) {
              fprintf(stderr, "错误:内存分配失败!\n");
              return 1;
          }
      
          *p = 42; // 写入数据
          printf("分配的内存地址: %p\n", (void*)p);
          printf("存储的值: %d\n", *p);
      
          free(p); // 释放内存
          p = NULL; // 避免悬垂指针
          return 0;
      }

      禁止行为
    • int *p;       // 未初始化
      *p = 42;      // 错误:p是野指针,解引用导致未定义行为

2.2 指针的类型系统

  • 类型决定步长
    int arr[3] = {1, 2, 3};
    int *p = arr;
    printf("%d\n", *(p + 1)); // 输出2(p+1移动4字节)
  • 类型匹配原则
    • 指针类型必须与指向数据类型一致,否则解引用可能读取错误数据。
    • 显式类型转换(谨慎使用):
      double d = 3.14;
      int *p = (int*)&d; // 危险:按int解释double的内存
      //指针类型决定了指针在被解引用的时候访问几个字节
      //如果是int* 的指针,解引用访问4个字节
      //如果是char* 的指针,解引用访问1个字节
      //推广到其他类型
数据类型32位系统64位系统说明
char11存储单个字符(如'A')
short22短整型(范围:-32768~32767)
int44整型(常用,范围约±21亿)
long48(Linux)/4(Windows)长整型
long long88超长整型(范围极大)
float44单精度浮点数(约7位有效数字)
double88双精度浮点数(约15位有效数字)
pointer(指针)48存储内存地址(32位/64位系统差异大)

2.3 空指针与野指针

  • 空指针(NULL)
    • 定义为(void*)0,表示不指向任何有效内存。
    • 使用前需检查:
      if (p != NULL) { *p = 42; }
  • 野指针
    • 产生原因:未初始化、越界访问、释放后继续使用。
    • 示例:
      int *p;
      free(p); // p成为野指针
      *p = 10; // 未定义行为

三、指针与数组的深度关联

3.1 数组名与指针的等价性

  • 数组名退化:在多数表达式中,数组名等价于首元素地址。
    int arr[5] = {1, 2, 3, 4, 5};
    int *p = arr; // 等价于 p = &arr[0]
  • 例外情况
    • sizeof(arr)返回整个数组大小(如5 * sizeof(int))。
    • &arr返回指向数组的指针(类型为int (*)[5])。

3.2 指针遍历数组

  • 下标法arr[i]等价于*(arr + i)
  • 指针法
    for (int *p = arr; p < arr + 5; p++) {
        printf("%d ", *p); // 输出1 2 3 4 5
    }
  • 性能对比:指针遍历通常比下标法更快(避免重复计算地址)。

3.3 多维数组与指针

  • 二维数组的指针表示
    int matrix[3][3] = {{1,2,3}, {4,5,6}, {7,8,9}};
    int (*p)[3] = matrix; // p指向包含3个int的数组
    printf("%d\n", p[1][2]); // 输出6(等价于matrix[1][2])
  • 指针数组
    int *rows[3]; // 存储3个int指针的数组
    for (int i = 0; i < 3; i++) {
        rows[i] = matrix[i]; // 每个rows[i]指向一行
    }

四、指针在函数中的高级应用

4.1 地址传递与参数修改

  • 基础示例
    void swap(int *a, int *b) {
        int temp = *a;
        *a = *b;
        *b = temp;
    }
    int x = 1, y = 2;
    swap(&x, &y); // x和y的值被交换
  • 数组参数传递
    void print_array(int *arr, int size) {
        for (int i = 0; i < size; i++) {
            printf("%d ", arr[i]); // arr退化为指针
        }
    }

4.2 函数指针与回调机制

  • 函数指针声明
    int add(int a, int b) { return a + b; }
    int (*func_ptr)(int, int) = add; // func_ptr指向add函数
  • 回调应用
    #include <stdio.h>
    void process(int (*operation)(int, int), int x, int y) {
        printf("Result: %d\n", operation(x, y));
    }
    int main() {
        process(add, 3, 4); // 输出7
        return 0;
    }

4.3 返回指针的函数

  • 安全实践
    • 避免返回局部变量地址(局部变量在函数返回后失效)。
    • 返回动态分配内存或全局变量地址:
      int *create_array(int size) {
          int *arr = malloc(size * sizeof(int));
          if (arr == NULL) return NULL;
          for (int i = 0; i < size; i++) arr[i] = i;
          return arr; // 调用者需负责释放
      }

五、动态内存管理:malloc与free

5.1 动态分配函数对比

函数原型行为
mallocvoid* malloc(size_t size)分配未初始化内存
callocvoid* calloc(size_t num, size_t size)分配并初始化为0
reallocvoid* realloc(void* ptr, size_t size)调整已分配内存大小

5.2 内存泄漏的常见场景

  • 场景1:分配后未释放
    void leak() {
        int *p = malloc(sizeof(int));
        // 忘记free(p)
    }
  • 场景2:异常路径导致泄漏
    void risky() {
        int *p = malloc(sizeof(int));
        if (error_condition) return; // 直接返回导致泄漏
        free(p);
    }

5.3 内存管理最佳实践

  • 成对使用:每个malloc必须有对应的free
  • 释放后置NULL
    free(p);
    p = NULL; // 避免悬垂指针
  • 使用工具检测
    • Valgrind:检测内存泄漏和非法访问。
    • AddressSanitizer(GCC/Clang):编译时插入内存检查代码。

六、结构体与指针的深度结合

6.1 结构体指针与成员访问

  • 箭头运算符
    struct Student {
        int age;
        char name[20];
    };
    struct Student s = {20, "Alice"};
    struct Student *p = &s;
    printf("%s\n", p->name); // 等价于 (*p).name

6.2 自引用结构与链表

  • 链表节点定义
    struct Node {
        int data;
        struct Node *next; // 自引用指针
    };
  • 链表遍历示例
    void print_list(struct Node *head) {
        for (struct Node *p = head; p != NULL; p = p->next) {
            printf("%d ", p->data);
        }
    }

6.3 动态结构体数组

  • 分配与释放
    struct Student *students = malloc(3 * sizeof(struct Student));
    if (students == NULL) { /* 处理错误 */ }
    students[0].age = 20; // 通过数组下标访问
    free(students); // 释放整个数组

七、const限定符与指针的复杂交互

7.1 const指针的分类

语法含义示例
const int *p指向常量,不可通过p修改数据const int *p = &num;
int *const p指针常量,不可修改指向地址int num = 10; int *const p = &num;
const int *const p双重常量,地址和数据均不可变const int num = 10; const int *const p = &num;

7.2 const指针的应用场景

  • 保护函数参数
    void print_string(const char *s) {
        // 防止函数内部修改s指向的字符串
        while (*s) putchar(*s++);
    }
  • 常量字符串字面量
    const char *msg = "Hello"; // msg指向只读内存
    // msg[0] = 'h'; // 错误:试图修改只读内存

八、指针的常见问题与调试技巧

8.1 野指针与悬垂指针

  • 野指针:未初始化的指针,解引用导致段错误(Segmentation Fault)。
  • 悬垂指针:释放内存后未置NULL,继续解引用。
  • 调试方法
    • 使用gdb定位崩溃点:
      gcc -g program.c -o program
      gdb ./program
      (gdb) run
      (gdb) backtrace # 查看调用栈

8.2 内存越界访问

  • 数组越界
    int arr[3] = {1, 2, 3};
    int *p = arr;
    printf("%d\n", p[3]); // 越界访问,行为未定义
  • 缓冲区溢出
    char buf[10];
    strcpy(buf, "This string is too long!"); // 溢出

8.3 类型不匹配问题

  • 错误示例
    double d = 3.14;
    int *p = &d; // 警告:类型不匹配
    *p = 42;     // 可能覆盖相邻内存
  • 解决方案:显式类型转换(需确保逻辑正确):
    int *p = (int*)&d; // 仅在明确知道内存布局时使用

这里补充一点:指针的类型决定了指针+/-操作的时候跳过几个字节,决定了指针的步长

int main() {
	int a = 0x11223344;
	int* pa = &a;
	char* pc = (char*)&a;
	printf("pa=%p\n", pa);
	printf("pa+1 =%p\n", pa + 1);
	printf("pc=%p\n", pc);
	printf("pc+1 =%p\n", pc + 1);
	//指针的类型决定了指针+/-操作的时候跳过几个字节,决定了指针的步长
}

这里的int跳过了四个字节,char跳过了1个字节,跟int和char本身所占字节有关。

接着这个问题,继续提出猜想:int和float的字节数一样,那这两者可以通用吗?

答案是  不能

当存储的数值改为int型的100时:

int main() {
	int a = 0;
	int* pi = &a;
	float* pf = &a;
	*pi = 100;
	return 0;

}

可以看到输入a的地址时,内存中存储的数为64 00 00 00 cc........

 字节序(Endianness)的影响

  • 小端序(Little-Endian):低位字节存储在低地址。
    • 例如,1000x0064)存储为 64 00
  • 大端序(Big-Endian):高位字节存储在低地址。
    • 例如,100 存储为 00 64
  • 图中内存从 0x0000063FD6FFCB4 开始显示 64 00...,符合小端序下 100 的存储方式

当存储的数值改为float型的100.0时:变化为00 00 c8 42 cc......

特性整数 100int浮点数 100.0float
数据类型整型(直接二进制表示)浮点型(IEEE 754编码)
存储示例64 00 00 00(小端序)00 00 C8 42(小端序)
解析方式直接按权展开符号位 + 指数位 + 尾数位

根本原因:整数和浮点数的二进制编码规则完全不同,即使数值相同(如 100 和 100.0),内存中的字节表示也会不同。

所以,不能int 和float不通用,即使字节数一样!

九、指针的高级主题与扩展应用

9.1 柔性数组(C99特性)

  • 定义:结构体末尾的未指定大小数组,用于动态扩展。
    struct FlexArray {
        int size;
        int data[]; // 柔性数组
    };
  • 使用示例
    struct FlexArray *fa = malloc(sizeof(struct FlexArray) + 5 * sizeof(int));
    fa->size = 5;
    for (int i = 0; i < 5; i++) fa->data[i] = i;
    free(fa);

9.2 指针与位操作

  • 直接操作内存
    int num = 0x12345678;
    char *p = (char*)&num; // 按字节访问
    printf("%02x\n", *p); // 输出78(小端序)
  • 应用场景:协议解析、硬件寄存器操作。

9.3 指针与多线程

  • 共享指针的同步
    #include <pthread.h>
    int shared_data = 0;
    int *p = &shared_data;
    void* thread_func(void* arg) {
        *p = 42; // 需要互斥锁保护
        return NULL;
    }
  • 线程安全实践:使用互斥锁(pthread_mutex_t)保护指针访问。

十、门牌号一样大,但门牌号代表的面积不一样大

我们可以通过一个门牌号与房屋面积的类比,来理解指针地址和存储面积的关系:

1. 门牌号(指针地址)

  • 作用:门牌号是房屋的唯一标识,用于定位具体位置(如“XX路123号”)。
  • 特点
    • 每个门牌号是唯一的,但不直接反映房屋大小
    • 例如,“XX路100号”可能是一个小公寓,也可能是一个大别墅。
  • 对应到内存
    • 指针地址是内存中某个位置的唯一标识(如 0x0000063FD6FFCB4)。
    • 地址本身不决定存储数据的大小,仅用于定位。

2. 房屋面积(存储面积)

  • 作用:房屋面积决定实际空间大小(如50㎡或200㎡)。
  • 特点
    • 面积由房屋设计决定,与门牌号无关。
    • 同一个门牌号(地址)可能指向不同大小的房屋(数据类型不同)。
  • 对应到内存
    • 存储面积由数据类型决定
      • char:1字节(小公寓)。
      • int:4字节(普通住宅)。
      • double:8字节(大别墅)。
    • 例如,地址 0x1000 可能存储:
      • 一个 char(1字节),或
      • 一个 int(4字节),或
      • 一个 double(8字节)。

3. 类比解释

场景1:门牌号相同,面积不同

  • 现实例子
    • “XX路100号”可能是一个:
      • 10㎡ 的快递柜(类似 char 类型),或
      • 100㎡ 的办公室(类似 int 类型),或
      • 500㎡ 的仓库(类似 double 数组)。
  • 内存例子
    • 地址 0x1000 可能存储:
      • 一个字符 'A'(1字节),或
      • 一个整数 100(4字节),或
      • 一个浮点数 3.14(4/8字节)。

场景2:门牌号不同,面积相同

  • 现实例子
    • “XX路100号”和“YY路200号”可能都是50㎡的公寓。
  • 内存例子
    • 地址 0x1000 和 0x2000 可能都存储 int 类型的 100(各占4字节)。

4. 指针与数据类型的关联

  • 指针的本质
    • 指针是一个变量,存储的是另一个变量的地址(门牌号)。
    • 指针的类型决定了如何解释该地址后的数据(即“房屋面积”)。
  • 示例
    int num = 100;
    int *p = &num;  // p指向一个4字节的int
    
    char c = 'A';
    char *q = &c;   // q指向一个1字节的char
    • p 和 q 都是地址(门牌号),但:
      • *p 会读取4字节(int 类型)。
      • *q 会读取1字节(char 类型)。

5. 为什么需要这种设计?

  • 灵活性
    • 同一个地址可以按需解释为不同类型(如通过类型转换)。
  • 效率
    • 指针仅存储地址(通常4/8字节),不占用额外空间。
  • 安全性
    • 程序员需明确指针类型,避免误读数据(如将 char* 当 int* 使用会导致错误)。

6. 总结类比表

概念门牌号类比内存类比
地址门牌号(唯一标识)指针值(如 0x1000
数据大小房屋面积数据类型占用的字节数
指针类型房屋用途说明告诉编译器如何解释地址后的数据

通过这个类比,可以直观理解:指针地址是定位符,而存储面积由数据类型决定。就像门牌号不决定房屋大小一样,地址本身也不决定数据占用的内存空间。

十一、指针的大小与系统相关,而指针的类型决定了如何解释它指向的内存内容

实际上,指针 pa 的大小(占用的内存字节数)与它指向的变量 a 的大小无关,而是由系统的地址空间大小决定。而 pa 的类型决定了如何解释它指向的内存内容(即 a 的大小)。

1. 指针 pa 的大小(存储地址所需的字节数)

  • 指针的本质:指针是一个变量,用于存储另一个变量的地址。
  • 指针的大小
    • 在 32位系统 中,地址空间是 32 位,因此指针占 4 字节
    • 在 64位系统 中,地址空间是 64 位,因此指针占 8 字节
  • 与 a 的大小无关
    • 无论 a 是 char(1字节)、int(4字节)还是 double(8字节),指针 pa 的大小只取决于系统是 32 位还是 64 位。

2. 指针 pa 的类型(决定如何解释 a 的大小)

  • 指针的类型
    • 指针的类型(如 char*int*double*)告诉编译器如何解释指针指向的内存内容。
    • 例如:
      • int* pa = &a;pa 指向一个 int,编译器知道从 pa 开始的 4 字节 是 a 的值。
      • char* pa = &c;pa 指向一个 char,编译器知道从 pa 开始的 1 字节 是 c 的值。
  • 类型的作用
    • 决定指针算术运算的步长(如 pa++ 移动多少字节)。
    • 决定解引用(*pa)时读取多少字节。

3. 类比解释

  • 门牌号(指针地址)
    • 门牌号本身的大小(如写在纸上的数字长度)与它指向的房屋大小无关。
    • 无论门牌号指向的是小仓库还是大别墅,门牌号本身的存储空间是固定的(如用A4纸打印,总是占一张纸)。
  • 内存中的指针
    • 指针 pa 就像一张纸条,上面写着 a 的地址(如 0x1000)。
    • 在 32 位系统中,纸条上写 8 位十六进制数(4 字节);在 64 位系统中,写 16 位十六进制数(8 字节)。
    • 纸条的大小(指针的大小)与 a 的大小无关,只与系统有关。

4. 示例代码验证

#include <stdio.h>

int main() {
    char c = 'A';
    int a = 100;
    double d = 3.14;

    char* pc = &c;
    int* pa = &a;
    double* pd = &d;

    printf("指针 pc 的大小: %zu 字节\n", sizeof(pc));
    printf("指针 pa 的大小: %zu 字节\n", sizeof(pa));
    printf("指针 pd 的大小: %zu 字节\n", sizeof(pd));

    return 0;
}

输出结果(64位系统)

  • 结论
    • 无论 pcpa 还是 pd,它们的大小都是 8 字节(64 位系统)。
    • 它们的大小与指向的变量 cad 的大小无关。

5. 关键总结

特性解释
指针的大小由系统的地址空间大小决定(32位系统:4字节,64位系统:8字节)。
指针的类型决定如何解释指针指向的内存内容(即变量 a 的大小和类型)。
指针的用途类型用于指针算术和解引用,大小用于存储地址本身。

6. 为什么容易混淆?

  • 混淆点1:误以为指针的大小与指向的变量大小有关。
    • 实际:指针的大小是固定的,与系统相关。
  • 混淆点2:误以为 int* 和 char* 的指针大小不同。
    • 实际:所有指针类型的大小相同(在同一个系统中)。

的大小与系统相关,而指针的类型决定了如何解释它指向的内存内容

十二、总结与学习建议

12.1 指针的核心价值

  • 直接内存操作:绕过命名变量,直接访问任意地址。
  • 高效数据传递:函数间共享大数据无需复制。
  • 动态数据结构:支持链表、树、图等复杂结构。

12.2 学习路径建议

  1. 基础阶段:掌握指针声明、初始化、算术运算。
  2. 进阶阶段:理解指针与数组、结构体的关系,动态内存管理。
  3. 实战阶段:通过项目(如实现链表、解析二进制文件)巩固知识。
  4. 调试阶段:熟练使用gdbValgrind等工具排查问题。

12.3 经典书籍推荐

  • 《C程序设计语言》(K&R):指针章节为经典范本。
  • 《C和指针》:系统讲解指针的方方面面。
  • 《深度探索C++对象模型》:理解C++中指针的底层行为(进阶)。

通过系统学习与实践,指针将成为你驾驭C语言的利器,开启高效、灵活的系统编程之旅。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yvonne爱编码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值