一、内存和编码
指针理解的2个要点:
- 指针是内存中一个最小单元的编号,也就是地址
- 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量。
1. 内存
先看一个⽣活中的案例:
假设有⼀栋宿舍楼,把你放在楼⾥,楼上有100个房间,但是房间没有编号,你的⼀个朋友来找你玩, 如果想找到你,就得挨个房⼦去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:
cpp//⼀楼:101,102,103... //⼆楼:201,202,203... //...
有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。
如果把上⾯的例⼦对照到计算机中,⼜是怎么样呢?
- 计算机内存的角色:计算机的 CPU 处理数据时,从内存读取数据,处理后的数据也存回内存。常见内存容量有 8GB、16GB、32GB 等。
- 内存单元划分:为高效管理内存,将其划分为一个个内存单元,每个内存单元大小通常为 1 个字节。
- 计算机存储单位 :
- bit(比特位):计算机最小信息单位。
- Byte(字节):1Byte = 8bit 。
- 其他单位换算:1KB = 1024Byte,1MB = 1024KB,1GB = 1024MB,1TB = 1024GB,1PB = 1024TB 。
2. 编码
- 编址的必要性:CPU 访问内存字节空间,需明确其位置。因内存字节众多,所以要对内存编址。
- 编址的实现方式:计算机编址依靠硬件设计,而非记录每个字节地址。CPU 与内存间的地址总线发挥关键作用。
- 地址总线原理 :以 32 位机器为例,它有 32 根地址总线,每根线有 0、1 两态(类似电脉冲有无)。一根线表示 2 种含义,两根线表示 = 4 种含义,32 根线可表示种含义,每种含义对应一个地址。地址信息经地址总线下达给内存,内存找到对应数据,再通过数据总线传入 CPU 内寄存器。
二、指针和指针类型
指针是什么?
指针理解的2个要点:
- 指针是内存中一个最小单元的编号,也就是地址
- 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量。
1. 取地址操作符
在 C 语言中,创建变量意味着向内存申请空间。
当我们定义 int a = 10; 时,会在内存中申请 4 个字节来存放整数 10,每个字节都有其对应的地址。如这 4 个字节的地址可能分别为
- 0x006FFD70
- 0x006FFD71
- 0x006FFD72
- 0x006FFD73
要获取变量 a 的地址,我们使用 & 操作符。通过以下代码:
cpp#include <stdio.h> int main() { int a = 10; &a;//取出a的地址 printf("%p\n", &a); return 0; }
打印获得:
cpp006FFD70
详细过程:
&a取出a所占4个字节中地址较⼩的字节的地址
虽然整型变量占用 4 个字节,但只要知道第一个字节的地址,就可以顺藤摸瓜访问到全部 4 个字节的数据。
2. 指针变量(存储地址的容器)
通过 & 获取的地址是数值,⽐如:0x006FFD70,需存储以便后续使用,指针变量就是专门存放地址的变量。例如:
cpp#include <stdio.h> int main() { int a = 10; int * pa = &a;//取出a的地址并存储到指针变量pa中 return 0; }
指针变量中存储的值被视为地址。
3. 指针变量类型
**指针变量类型由所指向对象类型和 * 构成,**例如:
cppint a = 10; int * pa = &a;
int pa, 表明 pa 是指针变量,int 表示它指向整型对象,即存储何种类型对象的地址,指针变量类型就是:**对象类型 + ***。
4. 解引用操作符(通过地址访问对象)
获取地址(指针)后,使用解引用操作符
(
*)
能找到指针指向的对象。例如:
cpp#include <stdio.h> int main() { int a = 100; int* pa = &a; *pa = 0; printf("%d", a); return 0; }
这里 *pa 借助 pa 中的地址找到对应空间,实际 *pa 就是变量 a,所以 *pa = 0 会将 a 的值改为 0。
5. 指针变量的大小
指针变量大小取决于地址大小。
- 32 位机器有 32 根地址总线,一个地址由 32 个 bit 位组成,需 4 字节存储,所以指针变量大小为 4 字节。
- 64 位机器有 64 根地址线,一个地址由 64 个二进制位组成,需 8 字节存储,指针变量大小为 8 字节。
例如:
cpp#include <stdio.h> int main() { printf("%zd\n", sizeof(char *)); printf("%zd\n", sizeof(short *)); printf("%zd\n", sizeof(int *)); printf("%zd\n", sizeof(double *)); return 0; }
64位情况下 :
32位情况下:
结论:
- 32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
- 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
- 注意指针变量的⼤⼩和类型⽆关,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的
6. void* 指针
特性与限制 :void* 是特殊指针类型,可理解为无具体类型或泛型指针 ,能接受任意类型地址。但它不能直接进行指针的 ± 整数和解引用运算。例如:
cpp#include <stdio.h> int main() { int a = 10; void* pa = &a; *pa = 10; return 0; }
应用场景 :void* 指针常用于函数参数 ,接收不同类型数据地址,实现泛型编程,使一个函数能处理多种类型数据。
7. const修饰指针
(1)const 在 * 左边
const 在 * 左边,修饰指针指向的内容,保证该内容不能通过指针改变 ,但指针变量本身内容可变。例如在 test2 函数中:
cppvoid test2() { int n = 10; int m = 20; const int* p = &n; *p = 20; // 报错 p = &m; // 允许 }
(2)const 在 * 右边
const 在 * 右边 :修饰指针变量本身,保证指针变量内容不能修改,但指针指向的内容可通过指针改变。例如在 test3 函数中:
cppvoid test3() { int n = 10; int m = 20; int * const p = &n; *p = 20; // 允许 p = &m; // 报错 }
(3)两边都有 const
两边都有 const :指针指向的内容和指针变量本身都不能修改。例如在 test4 函数中:
cppvoid test4() { int n = 10; int m = 20; int const * const p = &n; *p = 20; // 报错 p = &m; // 报错 }
const修饰指针变量时:
- const如果放在 * 的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。
- const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
三、指针类型的意义
- 指针的类型决定了指针向前或者向后走一步有多大(距离)。
- 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
1. 指针±整数
指针类型决定指针前后移动的距离。
cpp#include <stdio.h> int main() { int n = 10; char* pc = (char*)&n; int* pi = &n; printf("%p\n", &n); printf("%p\n", pc); printf("%p\n", pc + 1); printf("%p\n", pi); printf("%p\n", pi + 1); return 0; }
char* 指针 +1 跳过 1 字节,int* 指针 +1 跳过 4 字节。指针 +1 实际跳过 1 个指针指向的元素,指针也可 -1.
2. 指针的解引用
指针类型决定解引用时的权限,即一次能操作的字节数。
例如以下两段代码:
cpp#include <stdio.h> int main() { int n = 0x11223344; int *pi = &n; *pi = 0; return 0; }
cpp#include <stdio.h> int main() { int n = 0x11223344; char *pc = (char *)&n; *pc = 0; return 0; }
第一段代码会将 n 的4个字节全部改为0,但是第二段代码却不行。
char 的指针解引⽤就只能访问⼀个字节,⽽ int 的指针的解引⽤就能访问四个字节。**
四、指针运算
指针的基本运算有三种,分别是:
- 指针±整数
- 指针-指针
- 指针的关系运算
1. 指针±整数
原理 :由于数组在内存中是连续存放的,只要知道第一个元素的地址,通过指针加减整数可以方便地找到后续元素。
示例代码:
cpp#include <stdio.h> //指针+- 整数 int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &arr[0]; int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); for(i=0; i<sz; i++) { printf("%d ", *(p+i));//p+i 这里就是指针+整数 } return 0; }
p 是指向数组 arr 第一个元素的指针。通过 p + i 可以将指针移动到数组的第 i 个元素的位置,再使用 *(p + i) 进行解引用,就能访问该元素。在 for 循环中,我们遍历整个数组,依次输出元素。
2. 指针 - 指针
原理 :当两个指针都指向同一块内存空间时,可以进行指针相减运算,其结果表示两个指针之间元素的数量。
示例代码:
cpp#include <stdio.h> int my_strlen(char *s) { char *p = s; while(*p!= '\0' ) p++; return p - s; } int main() { printf("%d\n", my_strlen("abc")); return 0; }
在 my_strlen 函数中,s 指向字符串的起始位置,p 从 s 开始向后移动,直到遇到 '\0' 终止符。p - s 的结果就是字符串的长度。
3. 指针的关系运算
原理 :指针本质是地址,可视为一组二进制数(通常以十六进制显示),有大小之分,即低地址和高地址。可以对指针进行大小比较等关系运算。
示例代码:
cpp#include <stdio.h> int main() { 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; }
在上述代码中,arr + sz 指向数组最后一个元素之后的位置。通过 p < arr + sz 的关系运算,可确保 p 在遍历数组元素时不会越界。在循环中,使用
*p
输出元素,并将 p 指针向后移动。五、野指针
1. 野指针的概念
野指针是指指针指向的位置不可知(随机、不正确、无明确限制)。
2. 野指针的成因
(1)指针未初始化
cpp#include <stdio.h> int main() { int *p;//局部变量指针未初始化,默认为随机值 *p = 20; return 0; }
这里 p 作为局部变量未初始化,其值是随机的,对 *p 赋值会导致未定义行为,因为不知道 p 指向何处。
(2)指针越界访问
cpp#include <stdio.h> int main() { int arr[10] = {0}; int *p = &arr[0]; int i = 0; for(i=0; i<=11; i++) { //当指针指向的范围超出数组arr的范围时,p就是野指针 *(p++) = i; } return 0; }
在 for 循环中,当 i 大于等于 10 时,p 超出了数组 arr 的范围,导致越界,p 成为野指针。
(3)指针指向的空间释放
cpp#include <stdio.h> int* test() { int n = 100; return &n;//函数栈帧使用完销毁 } int main() { int*p = test();//但p还能找到这块空间 printf("%d\n", *p); return 0; }
test 函数返回局部变量 n 的地址,函数调用结束后栈帧销毁,但 p 仍指向原位置,此时 p 为野指针,访问 *p 会导致问题。
3. 如何规避野指针
(1)指针初始化
原理 :明确指针指向时直接赋值地址,不知指针应指向何处时赋值 NULL。
示例代码:
cpp#include <stdio.h> int main() { int num = 10; int*p1 = # int*p2 = NULL; return 0; }
p1 指向 num 的地址,而 p2 被初始化为 NULL,表示不指向任何可用地址,访问 NULL 会报错,从而避免意外操作。
(2)注意指针越界
原理:程序只能访问已申请的内存空间,超出范围即为越界。
示例代码:
cppint main() { 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; }
使用 p 遍历数组后将其置为 NULL,后续使用前检查 p 是否为 NULL,避免使用野指针。
(3)避免返回局部变量的地址
原理:局部变量在函数结束时销毁,其地址不再有效。
cpp#include <stdio.h> int* test() { int n = 100; return &n; } int main() { int* p = test(); printf("%d\n", *p); return 0; }
不要返回局部变量的地址,以防止产生野指针。
(4)assert 断言
原理 :assert.h 头文件中的 assert() 宏可在运行时确保程序符合指定条件,不符合时报错终止运行
示例代码:
cpp#include <assert.h> int main() { int *p = NULL; assert(p!= NULL); return 0; }
如果 p 为 NULL,程序运行到 assert(p!= NULL) 会终止,并给出报错信息,包括文件名和行号。通过定义 #define NDEBUG 可关闭 assert() 宏,在 Debug 阶段使用可方便排查问题,在 Release 版本可选择禁用,避免影响性能。
六、传值调用和传址调用
1. 传值调用
原理 :函数调用时,形参是实参的一份临时拷贝 ,改变形参不影响实参。
代码示例:
cpp#include <stdio.h> void Swap1(int x, int y) { int tmp = x; x = y; y = tmp; } int main() { 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; }
调用 Swap1 函数,由于是传值调用,x 和 y 只是 a 和 b 的副本,交换 x 和 y 的值不影响a 和 b 的值。
2. 传址调用
原理 :通过指针传递地址 ,可在被调函数中修改主调函数的变量。
示例代码:
cpp#include <stdio.h> void Swap2(int*px, int*py) { int tmp = 0; tmp = *px; *px = *py; *py = tmp; } int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b); Swap2(&a, &b); printf("交换后:a=%d b=%d\n", a, b); return 0; }
调用 Swap2 函数,将 a 和 b 的地址传递给 px 和 py,在函数内部通过解引用修改指针所指变量的值,实现了 a 和 b 的交换。
总结
- 传址调用能让被调函数和主调函数建立真正联系,当需要修改主调函数中的变量时使用。
- 若仅使用主调函数的变量值进行计算,可采用传值调用。
指针之矢:C 语言内存幽境的精准飞梭
阳光开朗大男孩 = ̄ω ̄=2024-12-29 11:54
相关推荐
WeeJot嵌入式8 小时前
C语言----词法符号是lethe先生10 小时前
数据结构部分题目(c语言版本)仟濹11 小时前
【算法笔记】洛谷 - 贪心算法 - P1208 [USACO1.3] 混合牛奶 Mixing MilkErik_LinX12 小时前
线索二叉树的实现(c语言)Galeoto15 小时前
fortran access pointer returned from c凭君语未可16 小时前
详解C语言字符串操作函数TPCloud16 小时前
详解下c语言中struct和union的对齐规则柒月的猫16 小时前
走方格(蓝桥杯2020年试题H)极客代码17 小时前
C语言性能优化:从基础到高级的全面指南鹿屿二向箔17 小时前
编写一个简单的引导加载程序(bootloader)