指针详解1

运行环境以Dev-C++、Visual Studio 2022、MacOS的命令行和Xcode为主

1.指针的概念

  • 1.1 内存

    • 内存是CPU和硬盘之间交换数据的缓冲区,属于存储设备,断电后数据会丢失动态运行着的程序会加载到内存中,如正在玩的游戏、正在听的歌、正在编辑的课件、正在浏览的网页等

    • 计算机CPU处理数据时,通常从内存中读取数据,处理后的数据也会写到内存中。内存被划分为多个内存单元,每个内存单元的大小为1个字节Byte,即8个比特bit

    • 买手机时的 16GB + 256GB,16 表示运行内存(运存)为 16GB

    image

  • 1.2 地址

    • 计算机的每个内存单元都会有个编号(相当于门牌号),CPU通过该编号可以快速定位到内存空间

    • 在计算机中将内存单元的编号称为地址,C语言又赋予这个地址一个新名称——指针

    image

    • 计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成。硬件之间通过“线”传递的信息协同工作,比如CPU和内存之间有大量数据交互,两者通过“线”连接起来。这些“线”共有三类

      • 地址总线

        • 定位数据的“导航系统”

        • 传递CPU要访问的内存单元或外设端口的地址信息,用于确定数据的来源或目的地。CPU读写数据前,先通过地址总线指定要操作的内存地址,内存根据地址定位对应的存储单元,存储单元中的数据通过数据总线传入CPU寄存器

        • 32位机器有32根地址总线,寻址范围为4GB;64位机器有64根地址总线,寻址范围为18EB

      • 数据总线

        • 传输主句的“主干道”

        • CPU与内存、外设之间双向传输实际的数据。如指令、运算结果、输入输出数据等

        • 数据既可从CPU发送到外部(如写入内存),也可从外部传输到CPU(如读取内存数据)

      • 控制总线

        • 协调操作的“指挥系统”

        • 传输各种控制信号和状态信号,协调 CPU 与内存、外设之间的操作时序和同步

      image

  • 1.3 指针

    • 含义与特征

      • 指针也就是内存地址,指针变量是用来存放这些内存地址的变量

      • 不同类型的指针占用的存储空间长度相同,因为它们都是地址,地址的长度与操作系统有关,与数据类型无关

    • 定义变量的实质

      • 编译器根据变量的数据类型分配相应长度的存储空间,该存储空间内存储的是变量的值,而存储空间的地址就是变量的地址,即指针

      • 用户仅通过变量名就可访问存储空间的方式称为“直接访问方式”。类似地,通过变量的地址解引用获取变量的值,这种访问方式称为“间接访问方式”

      image

2.指针变量和地址

  • 2.1 取地址操作符&

    • 分析变量在内存中的存储地址
    #include <stdio.h>
    
    int main(int argc, const char * argv[]) {
        // insert code here...
        int a = 10;
        printf("%d\n", a);
    
        return 0;
    }
    
    // 上述代码创建整型变量 a 期间会向内存申请 4B,用于存放整数10,每个字节在内存中都有对应的地址
    // 变量 a 在内存中的地址开辟情况如下图
    

    image

    • 获取变量在内存中的存储地址
    #include <stdio.h>
    
    int main(int argc, const char * argv[]) {
        // insert code here...
        int a = 0;
        &a;    // 获取 a 的地址
        printf("%p\n", &a);
        
        return 0;
    }
    
    // &a 会取出变量 a 所占的 4 个字节中地址较小的字节地址
    // 只要知道了第 1 个字节的地址,就可以顺藤摸瓜访问 4 个字节的数据,这在指针访问数组时指针的移动体现地尤为明显
    
  • 2.2 指针变量

    • 通过取地址操作符&获取的地址是一个数值,比如0xffc018,有时候也需要将该值存储方便后期使用,像这样的地址值不能用普通变量存储,应当使用指针变量
    #include <stdio.h>
    
    int main(int argc, const char * argv[]) {
        // insert code here...
        int a = 10;
        int *pa = &a;   // 取出 a 的地址并存储到指针变量 pa 中
        printf("%p\n", &a);
        
        return 0;
    }
    
    // 指针变量也是一种变量,主要用来存放地址,存放在指针变量中的值均会被系统解读为地址
    
  • 2.3 拆解指针类型

    • pa左边写的是int **表示 pa是整型变量,int表示pa指向的是整型类型的对象

    • C 语言对*、类型和变量名之间的空格没有严格语法限制,建议在*左侧与类型名之间留一个空格

    int a = 10;
    int *pa = &a;
    int*pa = &a;    // 不建议采用这种方式,无空格,可读性差
    
    char ch = 'w';
    char *pc = &ch;
    

    image

  • 2.4 解引用操作符

    • C语言中只要获取了地址(指针),就可以通过该地址使用解引用操作符*找到它指向的对象

    • 借助指针修改代码中变量的值在一定程度上可提高写代码的灵活性,并提升程序的运行效率

    #include <stdio.h>
    int main() {
        int a = 100;
        int *pa = &a;
        *pa = 0;    // *pa 即通过 pa 中存放的地址找到指向的空间,空间中存储了变量 a 的值,由 10 改为 0
        printf("%d\n", a);
        return 0;
    }
    
  • 2.5 指针变量的大小

    • 假设 32 位机器有 32 根地址总线,每根地址线发出的电信号转为数字信号后是 1 或 0。将 32 根地址线产生的二进制序列当做一个地址,共 32 bit,需要 4 个字节才能存储

    • 类似地,假设 64 位机器共 64 根地址线,一个地址就是 64 个二进制位组成的二进制序列,需要 8 个字节才能存储

    • 既然指针用来存放地址,那么指针变量的大小也应该为 4 或 8 个字节,具体取决于系统是 32 位平台还是 64 位平台

    #include <stdio.h>
    
    int main(int argc, const char * argv[]) {
        // insert code here...
        printf("%zd\n", sizeof(char *));
        printf("%zd\n", sizeof(short *));
        printf("%zd\n", sizeof(int *));
        printf("%zd\n", sizeof(double *));
        
        return 0;
    }
    
    // 由运行结果可知:1.运行系统为 x_64 平台;2.指针变量的大小与类型无关。只要是指针类型的变量在同一平台下,大小均相同
    // 思考:既然指针变量的大小和类型无关,为什么还要有各种各样的指针类型呢?
    

    image

  • 2.6 指针变量类型的意义

    • 1.指针的解引用

      • 案例分析
        // 观察以下代码在调试时内存的变化
        // 代码1
        #include <stdio.h>
      
        int main(int argc, const char * argv[]) {
            // insert code here...
            int n = 0x11223344;
            int *pi = &n;
            *pi = 0;
            
            return 0;
        }
      
        // 代码2
        #include <stdio.h>
      
        int main(int argc, const char * argv[]) {
            // insert code here...
            int n = 0x11223344;
            char *pc = (char *)&n;
            *pc = 0;
        
        return 0;
        }
      
      • 代码1运行前后的内存数据修改情况

        • 将变量 n 的 4 个字节全部改为 0,4 个字节对应整型数据的长度

        • int *型指针的解引用可访问 4 个字节

      image

      image

      • 代码2运行前后的内存数据修改情况

        • 将变量 n 的第 1 个字节改为 0,1 个字节对应字符型数据的长度
        • char *型指针的解引用只能访问 1 个字节

      image

      image

    • 2.指针与整数的加减运算

      • 案例分析
      #include <stdio.h>
      
      int main(int argc, const char * argv[]) {
          // insert code here...
          int n = 10;
          char *pc = (char *)&n;
          int *pi = &n;
          
          printf("&n = %p\n", &n);
          printf("pc = %p\n", pc);
          printf("pc+1 = %p\n", pc + 1);    // char* 类型的指针变量加 1 跳过 1 个字节
          printf("pi = %p\n", pi);
          printf("pi+1 = %p\n", pi + 1);    // int* 类型的指针变量加 1 跳过 4 个字节
          
          return 0;
      }
      
      // 这就是指针变量的类型差异带来的变化,指针加 1 即跳过 1 个指针指向的元素
      // 指针可以加 1,也可以减 1,指针的类型决定了指针向前或向后走一步有多少距离
      

      image

    • 3.void *指针

      • 无具体类型的指针(泛型指针),可以用来接受任意类型地址

      • 泛型指针不能直接进行指针的加减整数和解引用运算

      • 泛型指针通常使用在函数参数中,用于接收不同类型数据的地址,从而实现泛型编程的效果

      image

      image

3.const修饰指针

  • 3.1 const修饰变量

    • 变量可以修改,将变量的地址赋值给指针变量,就可以通过指针变量修改变量的值

    • const关键字可以限制变量的属性,使得变量的值不能被修改,即为常变量

    image

    image

  • 3.2 const修饰指针变量

    • const*的左边,修饰的是指针指向的内容,此内容不能通过指针修改。但指针变量本身的内容(指向)可变

    image

    • const*的右边,修饰的是指针变量本身,保证指针变量的内容(指向)不能修改,但指针指向的内容可变

    image

    image

4.指针运算

  • 4.1 指针加减整数

    • 数组的元素值在内存中连续存放,只要确定了首元素地址,就可以顺藤摸瓜找到后方所有元素
    #include <stdio.h>
    
    int main(int argc, const char * argv[]) {
        int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int *p = &arr[0];
        int sz = sizeof(arr) / sizeof(arr[0]);
        int i = 0;
        
        for (i = 0; i < sz; i++) {
            printf("%d ", *(p + i));    // p+i 就是指针加整数
        }
        return 0;
    }
    
    // 程序打印数组的所有内容
    
  • 4.2 指针减指针

    • 相减的前提是两个指针指向了同一块区间

    • 指针减指针的绝对值是两指针间元素的个数

    #include <stdio.h>
    
    size_t my_strlen(char *s) {
        char *p = s;
        while (*p != '\0') {
            p++;
        }
        return p - s;
    }
    
    int main(int argc, const char * argv[]) {
        printf("%zd\n", my_strlen("abcdef"));    // 6,即字符串的长度
    
        return 0;
    }
    
  • 4.3 指针的关系运算

    • 将指针和数组名、数组长度关联
    #include <stdio.h>
    
    int main(int argc, const char * argv[]) {
        int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int *p = &arr[0];
        int sz = sizeof(arr) / sizeof(arr[0]);
        
        while (p < arr + sz) {      // 指针的大小比较
            printf("%d ", *p);
            p++;
        }
        return 0;
    }
    
    // 程序打印数组的所有内容
    

5.野指针

  • 5.1 野指针的成因

    • 野指针就是指针指向的位置不可知,即随机的、不正确的、没有明确限制的。指针未初始化、指针越界访问、指针指向的空间没有释放都有可能产生野指针
    // 1.指针未初始化
    #include <stdio.h>
    
    int main(int argc, const char * argv[]) {
        int* p;    // 局部变量指针未初始化,默认为随机值
        *p = 20;
    
        return 0;
    }  
    
    // 2.指针越界访问
    #include <stdio.h>
    
    int main(int argc, const char * argv[]) {
        int arr[10] = {0};
        int* p = &arr[0];
        int i = 0;
        
        for (i = 0; i <= 11; i++) {
            *(p++) = i;     // 当指针指向的范围超出数组 arr 时,p就是野指针
        }
        return 0;
    } 
    
    // 3.指针指向的空间释放
    #include <stdio.h>
    
    int* test(void) {
        int n = 100;
        return &n;
    }
    // return &n 返回的是已经被释放的内存地址,main 函数中接收该地址的指针 p 就成了 野指针
    
    int main(int argc, const char * argv[]) {     
        int* p = test();    // n的空间被释放, p为野指针
        printf("%d\n", *p);    // 打印结果有可能是 100,但这不能说明程序正确
    
        return 0;
    }
    
    // test函数返回后,main函数紧接着访问 *p 以读取 n 原内存地址中的数据
    // 由于中间没有其他函数调用或内存操作,这块被释放的内存还没被新的数据覆盖,仍然保留着100这个值
    // 因此,printf 可能恰好读取到了残留的旧值,表现为 “运行正确”。
    

    image

  • 5.2 规避野指针的方法

    • 1.指针初始化

      • 若明确知道指针的指向地址就直接赋值,否则给指针赋值NULL
      • NULL是 C 语言中定义的一个标识符常量,值为 0。0 也是地址,该地址无法使用,读写该地址会报错
      #include <stdio.h>
      
      int main(int argc, const char * argv[]) {
          int num = 10;
          int* p1 = &num;
          int* p2 = NULL;    // 初始化为 NULL,避免生成野指针
      
          return 0;
      }
      
    • 2.避免指针越界

      • 一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问
      #include <stdio.h>
      
      int main(int argc, const char * argv[]) {
          int arr[3] = {10, 20, 30};
          int *p = arr;
          
          // 正常访问数组元素(索引 0-2)
          printf("正常访问:\n");
          for (int i = 0; i < 3; i++) {
              printf("arr[%d] = %d\n", i, *(p + i));
          }
          
          // 指针越界访问(索引3及以上,超出数组范围)
          printf("\n越界访问:\n");
          for (int i = 3; i < 6; i++) {
              // 此时p + i已经指向数组外的无效内存(野指针状态)
              printf("p + %d 指向的内存值:%d(地址:%p)\n", i, *(p + i), (void*)(p + i));
          }
          
          return 0;
      }
      

      image

    • 3.不再使用的指针变量及时置NULL,指针使用前检查有效性

      • 当指针变量指向一块区域时,可以通过指针访问该区域。后期不再使用这个指针访问空间时,可以将之置为NULL
      • 约定俗成的一个规则:只要是NULL就不去访问,使用指针之前可以判断指针是否为NULL
      #include <stdio.h>
      
      int main(int argc, const char * argv[]) {
          int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
          int *p = &arr[0];
          int i = 0;
          for (i = 0; i < 10; i++) {
              *(p++) = i;
          }
          
          // 此时p已经越界了,可以将p置为NULL
          p = NULL;
          
          // 下次使用的时候,判断p不为NULL的时候再使用
          p = &arr[0];    // 重新让p获得地址
          if (p != NULL) {
              // ...
          }
          
          return 0;
      }
      
    • 4.避免返回局部变量的地址

      • 理解函数栈帧的生命周期和野指针的特性。以 5.1 第 3 段代码为例,运行 “结果正确” 是因为野指针指向的内存数据尚未被覆盖,属于偶然现象,而非代码逻辑正确。正确的做法是避免返回局部变量的地址

      • 解决方法

        • 使用静态变量static int n = 100;

        • 动态分配内存int* n = malloc(sizeof(int)); *n=100; return n;

        • 由调用者传入指针参数void test(int* p) { *p = 100; }

6.assert断言

  • 6.1 定义与运行原理

    • assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件。如果不符合,就报错终止运行,这个宏常被称为“断言”
    assert(p != NULL);
    
    • 程序运行到上述代码时,验证变量p是否等于NULL。如果确实不等于NULL,程序继续运行,否则就会终止运行,并给出报错信息提示

    • assert()宏接收一个表达式作为参数。若表达式值为真,assert()不会产生任何作用,程序继续运行。若表达式为假,assert()就会报错

  • 6.2 使用断言的优势

    • 对程序员非常友好,不仅能自动标识文件和出问题的行号,还有无需更改代码就能开启或关闭assert()的机制

    • 若确认程序没有问题,不需要再做断言,就在#include <stdio.h>前面定义一个宏NDEBUG,再重新编译程序,编译器会禁用文件中所有assert()语句

    #define NDEBUG
    #include <assert.h>
    
  • 6.3 使用断言的劣势

    • 引入了额外的检查,增加了程序的运行时间

    • 通常在Debug版本中使用,有利于程序员排查问题;Release版本中选择禁用断言,部分IDE会在Release版本中优化掉断言,不影响用户使用程序的效率

7.指针的使用和传址调用

  • 7.1 strlen的模拟实现

    • 库函数strlen的功能是求字符串长度,统计的是字符串中\0之前的字符个数

    • 函数原型为size_t strlen(const char *str),参数str接收一个字符串的起始地址,然后开始统计字符串中\0之前的字符个数

    • 模拟实现的过程为从起始地址开始逐个字符向后遍历,只要不是\0字符,计数器就加 1,直到\0就停止

    #include <stdio.h>
    #include <assert.h>
    
    size_t my_strlen(const char *str) {
        int count = 0;
        assert(str);
        
        while (*str) {
            count++;
            str++;
        }
        return count;
    }
    
    int main(int argc, const char * argv[]) {
        size_t len = my_strlen("abcdef");
        printf("%zd\n", len);
        return 0;
    }
    
  • 7.2 传值调用

    • 实质:实参的值传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参
    #include <stdio.h>
    
    void Swap1(int x, int y) {
        int temp = x;
        x = y;
        y = temp;
    }
    
    int main(int argc, const char * argv[]) {
        int a = 0;
        int b = 0;
        scanf("%d %d", &a, &b);
        printf("交换前: a = %d b = %d\n", a, b);
        Swap1(a, b);
        printf("交换后: a = %d b = %d\n", a, b);
        
        return 0;
    }
    

    image

    image

    • 运行结果分析

      • 1.在main()函数内部创建了a和b,a的地址为0x0098fadcb的地址为0x0098fad0。调用Swap1()函数时,将a和b传递给了Swap1()函数

      • 2.在Swap1()函数内部创建了形参x和y接收a和b的值,但x的地址是0x0098f9f8(不同于a的地址),y的地址是0x0098f9fc(不同于b的地址),即x和y是独立的空间

      • 3.因此在Swap1()函数内部交换x和y的值,不会影响a和b。当Swap1()函数调用结束后回到main()函数a和b并未交换

      • 4.Swap1()函数运行期间接收了来自main()函数的实参值,这种调用方式称为传值调用

      • 5.如何修改代码才能运行出正确的结果?其实只要保证运行Swap1()函数时内部操作的是a和b就可以,结合指针的知识,在main()函数中将a和b的地址传递给Swap1()函数,就可以通过地址间接操作main()函数中的a和b

  • 7.3 传址调用

    • 实质:使得其他函数与主函数之间建立真正的联系,在函数内部课修改主调函数中的变量

    • 使用情形:若函数中只是需要主调函数中的变量值,可采用传值调用;若函数内部要修改主调函数中的变量值,可以采用传址调用

    #include <stdio.h>
    
    void Swap2(int *px, int *py) {
        int temp = 0;
        temp = *px;
        *px = *py;
        *py = temp;
    }
    
    int main(int argc, const char * argv[]) {
        int a = 0;
        int b = 0;
        scanf("%d %d", &a, &b);
        printf("交换前: a = %d b = %d\n", a, b);
        Swap2(&a, &b);    // 将变量的地址传给了Swap2
        printf("交换后: a = %d b = %d\n", a, b);
        
        return 0;
    }
    

    image

posted @ 2025-08-09 16:56  pycoder_666  阅读(30)  评论(0)    收藏  举报