第8章 指针(正在更新中......)

运行环境以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 指针

    • 含义与特征

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

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

    • 定义变量的实质

      • C语言通过变量来使用存储空间,内存中每个存储空间都有编号,称为地址

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

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

      image

2.指向简单变量的指针

  • 一般格式:类型标识符 *指针变量名类型标识符* 指针变量名类型标识符*指针变量名
int *p = NULL;    // 未初始化的指针指向的对象不确定,建议定义指针变量的同时初始化为NULL,即空
char *str = NULL;
  • 注意事项

    • 1.类似定义普通变量时初始化为0,定义指针变量时建议直接初始化为NULL

    • 2.定义指针变量时用的*是指针变量的标志,靠近 类型标识符 或 指针变量名 均可

    • 3.访问指针变量指向空间中的数据时用的*是指针运算符,也称解引用操作符优先级为2

    • 4.&也是指针运算符,即取地址运算符,优先级为2

    • 5.int型指针变量指向的对象是int类型,以此类推

    • 6.定义多个指针变量时,每个变量旁边都要加上*

    int *p1 = NULL, *p2 = NULL;  // 定义了两个整型指针变量 p1 和 p2
    char *s1 = NULL, s2 = 0, s3 = 0;  // 只有s1是字符型指针变量,s2 和 s3均为普通字符型变量
    
    • 7.同类型的指针变量可以相互赋值

    image

  • 案例分析

    • 1.通过指针变量访问变量
    #include <stdio.h>
    
    int main(int argc, const char * argv[]) {
        // insert code here...
        int a = 123, b = 45, *p1 = NULL, *p2 = NULL;
        float x = 1.25, y = -0.98, *q1 = NULL, *q2 = NULL;
        
        p1 = &a;
        p2 = &b;
        q1 = &x;
        q2 = q1;
        
        printf("a=%d, b=%d\n", a, b);
        printf("*p1=%d, *p2=%d\n", *p1, *p2);
        printf("\nx=%f, y=%f\n", x, y);
        printf("*q1=%f, *q2=%f\n", *q1, *q2);
        return 0;
    }
    

    image

    image

3.指向数组的指针变量

  • 3.1 指向一维数组的指针变量

    • 一维数组与指针的关系

      • 一维数组的数组名表示数组的首地址,即第1个数组元素的地址

      • 将数组的起始地址或某元素的地址存放到一个指针变量中,该变量就可以指向整个数组单个数组元素

      int a[5] = {0};
      int *p = NULL;
      p = a;        // 指针变量p指向数组a的第1个元素a[0]
      p = &a[0];    // 指针变量p指向数组a的第1个元素a[0]
      
      • 无论一维数组元素占据多少空间,当指针p指向其中一个元素时,p + 1均指向它的下一个元素
      • a[i]、*(a + i)、*(p + i)等价

      image

    • 案例分析

      • 1.通过数组下标和指针变量输出数组元素
      #include <stdio.h>
      
      int main(int argc, const char * argv[]) {
          // insert code here...
          int a[5] = {5, 4, 3, 2, 1};
          int i = 0, *p = a;
          
          for (i = 0; i < 5; i++) {
              printf("%d, %d, %d\n", a[i], *(a+i), *(p+i));
          }
          return 0;
      }
      
      // 指针变量也是变量,值可以修改;数组名是一个地址常量,不能重新赋值
      

      image

      • 2.for循环中使用指针移动的方式输出数组元素
      #include <stdio.h>
      
      int main(int argc, const char * argv[]) {
          // insert code here...
          int a[5] = {0};
          int i = 0, *p = NULL;
          printf("输入5个整数: ");
          
          for (i = 0; i < 5; i++) {
              scanf("%d", a + i);
          }
          
          for (p = a; p < a + 5; p++) {
              printf("%-4d", *p);
          }
          
          return 0;
      }
      

      image

      • 3.输出数组元素,验证a[i] 和 *(p + i)不总是相等
      #include <stdio.h>
      
      int a[5] = {1, 2, 3, 4, 5};
      int main(int argc, const char * argv[]) {
          // insert code here...
          int *p = NULL, i = 0;
          
          i = 2;
          p = a + 1;
          printf("%d %d\n", a[i], *(p+i));    // 3 4
          
          return 0;
      }
      
      // p 最开始已经指向了 a[1],加上 i 后指向了 a[3]
      // 由于解引用运算符 * 的优先级高于 +,p + i 整体要放入括号中
      
  • 3.2 指向多维数组的指针变量

    • 二维数组与指针的关系

      • 二维数组是一种特殊的一维数组,即元素是一维数组的数组。如int a[4][3];是由4个元素a[0]、a[1]、a[2]、a[3]组成的一维数组,它们均有3个元素

      • 可以将a[0]、a[1]、a[2]、a[3]看做这些一维数组的数组名或地址,也可以用指针指向这些地址

      int a[4][3] = {0};
      
      a[0]    {a[0][0], a[0][1], a[0][2]}
      a[1]    {a[1][0], a[1][1], a[1][2]}
      a[2]    {a[2][0], a[2][1], a[2][2]}
      a[3]    {a[3][0], a[3][1], a[3][2]}
      
      // a 即 a + 0 是数组的首地址,即元素 a[0][0] 的地址
      // a + 1 是数组第 2 行的首地址,即元素 a[1][0] 的地址
      // a + 2 是数组第 3 行的首地址,即元素 a[2][0] 的地址
      // a + 3 是数组第 4 行的首地址,即元素 a[3][0] 的地址
      
      • 在二维数组中,a是数组名,数组名表示数组首元素的地址,这里的首元素应理解为二维数组的第一行元素,而不是第一个元素,即使两者的地址值相同

      • 在二维数组中,a + ia[i]均可表示数组中第i + 1行的首地址,但两者不完全等同

        • a + i的类型是int (*)[3],是指向包含 3 个整型元素的数组的指针(数组指针),它指向数组a的第i + 1行。又因为这一行第一个元素恰好是a[i][0],所以a + i&a[i][0]在数值上相等

        • a[i]的类型是int [3],是包含 3 个整型元素的数组,可以看做二维数组a的内层数组名。恰好因为它是数组名,而数组名又是数组首元素的地址,所以它被隐式转换为指向其首元素的指针,类型为int *,指向a[i][0],即&a[i][0]

        • 解引用操作符*用于取出指针指向空间的内容,因此*(a + i)就是取出数组a的第i + 1行,这一行恰好是是a[i],所以*(a + i)a[i]等价

      image

    • 案例分析

      • 1.输出数组int a[4][3]中第 3 行的所有元素
      #include <stdio.h>
      
      int main(int argc, const char * argv[]) {
          // insert code here...
          static int a[4][3] = { {1, 2, 3}, {4, 5 ,6}, {7, 8, 9}, {10, 11, 12}};
          int *p = NULL;
          
          // a[2] 是二维数组中下标为2的行,即一维数组 {7, 8, 9}。将 a[2] 看做一维数组的数组名,它表示数组中第一个元素的地址,即 7 的地址
          // a[2] + 3 以单个整型数据为增量,而不是以数组 a 的行长度为增量
          for (p = a[2]; p < a[2] + 3; p++) {
              printf("%d ", *p);    // 7 8 9
          }
          printf("\n");
          
          return 0;
      }
      
      #include <stdio.h>
      
      int main(int argc, const char * argv[]) {
          // insert code here...
          static int a[4][3] = { {1, 2, 3}, {4, 5 ,6}, {7, 8, 9}, {10, 11, 12}};
          int *p = NULL;
          
          // a[2] 是二维数组中下标为2的行的数组名,数组名也是数组首地址,即 &a[2][0]
          for (p = &a[2][0]; p < a[2] + 3; p++) {
              printf("%d ", *p);
          }
          printf("\n");
          
          return 0;
      }
      
      • 2.指针与数组首地址的关系
      #include <stdio.h>
      #define fa "%x, %x, %x\n"
      int a[4][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
      
      int main(int argc, const char * argv[]) {
          // a 是二维数组首元素的地址,即第一行的地址,本质上是数组指针,类型为int (*)[3](指向包含3个 int 的数组的指针)
          // p 是普通的整型指针,类型为 int *(指向一个 int 数据),两者类型不同导致IDE触发警告⚠️
          // 强行赋值之后 p 指向了数组 a 中的第 1 个元素,p + 1 的增量是单个整型元素的长度,而非数组 a 中一维数组的长度,这是由 p 的类型确定的
          int *p = a;
      
          // a 是二维数组第 1 行的地址,与 a + 0 相同。a[0] 恰好也是二维数组第一行的数组名,所以三者的值相等
          printf(fa, a, a+0, a[0]);
      
          // a + 1 是二维数组第二行的地址,也是一个数组指针,类型为int (*)[3]
          // a[1] 是二维数组的第 2 个元素,是包含 3 个 int 的一维数组,类型是 int [3]。作为一维数组的数组名会被隐式转换为“指向其首元素的指针”
          // *a[1] 中 [] 的优先级高于 *,即 *(a[1])。a[1] 指向了首元素,等价于 &a[1][0],再解引用即 a[1][0]
          printf(fa, a+1, a[1], *a[1]);
      
          // p 的值与 a 的值一致,也与 &a[0][0]的值一致,解引用后为 a[0][0]
          // 类似的,*(p+1) 为 a[0][1]
          printf(fa, p, *p, *(p+1));
          
          return 0;
      }
      

      image

      • 3.输出二维数组的各元素
      #include <stdio.h>
      
      int main(int argc, const char * argv[]) {
          // insert code here...
          static int a[][4] = { {3, 6, 5, 14}, {7, 4, 9, 12}, {45, 8, 21, 62} };
          int i = 0, j = 0;
          
          for (i = 0; i < 3; i++) {
              for (j = 0; j < 4; j++) {
                  printf("%d ", *(*(a+i) + j));
              }
              printf("\n");
          }
          
          return 0;
      }
      // a + i 是指向行标为 i 的数组指针,*(a + i) 即对指针解引用,得到指针指向的内容,即 a[i] 这个数组
      // a[i] 是一维数组,数组名表示首元素的地址,所以 a[i] 也是指针,指向了一个 int 数据 a[i][0]
      // a[i] + j 即 *(a + i) + j,意为将指针 a[i] 往后偏移 j 个单位,实质上是从 a[i][0] 开始往后数了 j 个 int 数据,指向了 a[i][j]
      // *(*(a + i) + j) 即获取指针 *(a + i) + j 指向的内容,即元素 a[i][j]。所以这里的 *(*(a + i) + j) 与 a[i][j] 等价
      

      image

      • 4.用指针输出二维数组的各元素
      #include <stdio.h>
      #define M 3
      #define N 4
      #define L M*N
      
      int main(int argc, const char * argv[]) {
          // insert code here...
          int a[M][N] = {0};
          int i = 0;
          int *p = NULL;
          
          printf("input array:\n");
          // a[0] 本质上是二维数组的第一行(一维数组)的数组名,指向了一维数组的第一个元素 a[0][0]
          // 恰好 p 是整形指针,此时不会像案例 2 报警⚠️
          // 由于 a[0] 是整型指针,它的移动以单个 int 数据为单位,数组共 L 个 int 数据,所以 p 最远移动到 a[0] + (L - 1) 的位置
          for (p = a[0]; p < a[0] + L; p++) {
              scanf("%d", p);
          }
          
          printf("output array:\n");
          for (i = 0; i < M; i++) {
              // 打印阶段,p 的初值指向了下标为 i 行的首元素,即 a[i][0],类型依然是整型指针,移动以单个 int 数据为单位,所以移动上限是每行的元素个数,即 a[i] + (N - 1)
              for (p = a[i]; p < a[i] + N; p++) {
                  printf("%-4d ", *p);
              }
              printf("\n");
          }
          
          return 0;
      }
      

      image

      • 5.现有 4 行 3 列的数组 a,分析 a、&a、a[0] 有何区别

        • C 语言中的数组名在大多数场景下会被隐式转换为 "指向数组首元素的指针",但 sizeof(数组名)、&数组名例外,这两种情况下的数组名表示整个数组

        • 对于二维数组a[4][3],数组名a表示数组首元素的地址,即第1行(一维数组)的地址。本质上是数组指针,类型为int (*)[3]

        • &a为指向整个数组的地址,&a + 1移动的字节数为4 * 3 * sizeof(int)

        • a[0]表示二维数组的第一行(一维数组),作为数组名,它指向了一维数组的第一个元素,即&a[0][0]。a[0] + 1移动的字节数为sizeof(int)

4.指向字符串的指针变量

  • 4.1 字符数组和字符指针

    • C语言中,字符串的存储可以使用字符数组或字符指针实现,后者更方便简洁

    • 字符数组名表述数组的首地址,是常量,无法修改其值

    • 字符指针是变量,可被初始化指向一个字符串常量,该值为字符串常量的地址

    char string[] = "this is a string!";
    char *pstr = "that is a book!";    // pstr 可改为指向另一个字符串数据
    
    • 字符指针与字符数组都可以表示字符串,可以在scanf() printf()函数中用%s格式对字符串整体输入或输出,但不能对数组整体赋值。以下代码❌
    char s[15];
    s = "fujiansheng";
    
    // 报错提示:Array type 'char[15]' is not assignable
    // 数组名是地址常量,不能被赋值。而指针变量可以改变值,因此指针变量更灵活
    
    • 字符数组与字符指针的区别归根结底是数组与指针变量的区别
  • 4.2 案例分析

    • 1.修改字符数组的内容,数组的首地址是否会发生变化
    #include <stdio.h>
    
    char mess[] = "I am a student";
    char *p = "China";
    
    int main(int argc, const char * argv[]) {
        // insert code here...
        int i = 0;
        printf("%x, %x\n", mess, p);
        
        p = "Fuzhou";
        while (mess[i] != '\0') {
            mess[i] = mess[i + 1];
            i++;
        }
        printf("%x, %x\n", mess, p);
        
        return 0;
    }
    
    // 虽然数组 mess 的内容发生了变化,但 mess 的值(字符数组首地址)不变
    // 指针 p 指向了另一个字符串,其值发生了变化
    

    image

    • 2.使用指针实现字符串复制函数strcpy(s1, s2)
    #include <stdio.h>
    
    char *my_strcpy(char *p, char *q) {
        char *start = p;
        
        if (p == NULL || q == NULL) {
            return NULL;
        }
        
        while ((*p++ = *q++) != '\0') {
            ;
        }
        return start;
    }
    
    int main(int argc, const char * argv[]) {
        // insert code here...
        char src[] = "hello world";
        char dest[20] = {0};
        
        char *ret = my_strcpy(dest, src);
        if (ret != NULL) {
            puts(dest);
        }
        else {
            printf("字符串复制失败!");
        }
        
        return 0;
    }
    
    • 3.改变数组指针的程序
    #include <stdio.h>
    
    int main(int argc, const char * argv[]) {
        char str[30] = "money order";
        char *p = str;
        
        p = p + 6;
        printf("%s\n", p);    // 打印: order
        
        return 0;
    }
    
    • 4.编程输出以下图形

    image

    #include <stdio.h>
    
    int main(int argc, const char * argv[]) {
        char *p = "12345";
        
        while (*p != '\0') {
            printf("%s\n", p);
            p++;
        }
        
        return 0;
    }
    
    // 字符指针变量开始指向字符串的第一个字符,以后不断加1,使首地址不断后移,直到'\0'结束
    
    • 5.编程输出以下图形

    image

    #include <stdio.h>
    
    int main(int argc, const char * argv[]) {
        char *p = "*****";
        char *q = p + 4;
        
        while (q >= p) {
            printf("%s\n", q);
            q--;
        }
        
        return 0;
    }
    
    // 字符指针变量 q 开始指向字符串的最后一个字符,以后不断减1,使首地址不断前移,直到 q < p 结束
    

5.指针作为函数参数

  • 5.1 应用场景

    • 想通过函数调用得到 n 个要改变的值,可以用 n 个指向这些值的指针变量(地址)作为实际参数传给调用函数的形参,借助形参指针变量间接访问的方式改变 n 个变量的值
  • 5.2 案例分析

    • 1.从键盘上输入2个整数,并将它们交换后输出
    #include <stdio.h>
    
    void swap(int *q1, int *q2) {
        int temp;
        temp = *q1;
        *q1 = *q2;
        *q2 = temp;
    }
    
    int main(int argc, const char * argv[]) {
        int a = 0, b = 0;
        int *p1 = &a, *p2 = &b;
        
        printf("输入2个整数:\n");
        scanf("%d %d", &a, &b);
        swap(p1, p2);
        printf("交换后的2个整数:\n");
        printf("%d %d\n", a, b);
        
        return 0;
    }
    
    // 指针变量 p1 中存储了变量 a 的地址,p2 中存储了变量 b 的地址,通过传参,指针变量 q1 也指向了变量 a,q2 也指向了变量 b
    // swap函数中操作 q1 和 q2 的解引用,本质上就是操作的变量 a 和 b
    

    image

6.指向结构体的指针变量

  • 6.1 应用场景

    • 结构体变量的指针指向结构体变量所占据内存段的起始地址,也可以用来指向结构体数组中的元素
  • 6.2 案例分析

    • 1.指向结构体指针变量的定义、输入和输出
    #include <stdio.h>
    
    // 定义结构体类型 struct student 和变量 s
    struct student {
        char stu_no[4];
        char stu_name[10];
        int score;
    }s = {"101", "张三", 98};
    
    int main(int argc, const char * argv[]) {
        // 定义指向 struct student 结构体类型的指针变量 p,并将 p 赋值为结构体变量 s 的起始地址
        struct student *p = &s;
    
        // p 是结构体指针,*p 取到了结构体变量,(*p).stu_name 取到了结构体变量成员
        // 解引用运算符 * 的优先级(2)低于初等运算符 .(1),*p 两侧的括号不可省略
        // 在还有指针的结构体成员访问中,推荐使用运算符 ->
        printf("学号: %s  姓名: %s  成绩: %d\n", s.stu_no, (*p).stu_name, p->score);
    
        return 0;
    }
    

7.指针应用实例

posted @ 2025-08-06 22:06  pycoder_666  阅读(18)  评论(0)    收藏  举报