第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 + i与a[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)
-
- 1.输出数组
-
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; }



















浙公网安备 33010602011771号