指针详解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 = # 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的地址为0x0098fadc,b的地址为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]()
-

























浙公网安备 33010602011771号