9 从 “内存怎么存” 到 “指针怎么用”:计算机内存编址机制 + C 语言指针核心 + memory 模拟实现
1 内存和地址
1 .1理解编址
CPU访问内存中的某个字节空间,必须知道这个 字节空间在内存的什么位置,⽽因为内存中字节 很多,所以需要给内存进⾏编址(就如同宿舍很 多,需要给宿舍编号⼀样)。 计算机中的编址,并不是把每个字节的地址记录 下来,⽽是通过硬件设计完成的。 钢琴、吉他?上⾯没有写上“都瑞咪发嗦啦”这样 的信息,但演奏者照样能够准确找到每⼀个琴弦 的每⼀个位置,这是为何?因为制造商已经在乐 器硬件层⾯上设计好了,并且所有的演奏者都知 道。本质是⼀种约定出来的共识! 硬件编址也是如此。
⾸先,必须理解,计算机内是有很多的硬件单元,⽽硬件单元是要互相协同⼯作的。所谓的协同,⾄少相互之间要能够进⾏数据传递。但是硬件与硬件之间是互相独⽴的,那么如何通信呢?答案很简单,⽤"线"连起来。?⽽CPU和内存之间也是有⼤量的数据交互的,所以,两者必须也⽤线连起来。不过,我们今天关⼼⼀组线,叫做地址总线。我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表⽰0,1【电脉冲有⽆】,那么⼀根线,就能表⽰2种含义,2根线就能表⽰4种含义,依次类推。32根地址线,就能表⽰2^32种含义,每⼀种含义都代表⼀个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊CPU内寄存器。
1.2取地址&注意点
&a取出的是所占4字节中地址最小的字节的地址。比如int a,取出a的地址为006FFD70
1.3指针变量大小和类型问题
指针变量的⼤⼩
• 32位平台下地址(32根地址总线)是32个bit位,指针变量⼤⼩是4个字节。
• 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节。 **
• 注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。**
指针变量类型的意义
指针变量的⼤⼩和类型⽆关,只要是指针变量,在同⼀个平台下,⼤⼩都是⼀样的,为什么还要有各 种各样的指针类型呢?
指针变量类型的意义
指针变量的⼤⼩和类型⽆关,只要是指针变量,在同⼀个平台下,⼤⼩都是⼀样的,为什么还要有各 种各样的指针类型呢?
1 指针的解引⽤
指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。 ⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。
2 指针的解引⽤的实际应用--memcpy按字节拷贝数据
原理:取出任何一个类型的数据的地址任何强转成char* 类型的,然后解引用访问每一个字节的数据然后赋值给另一个同类型的变量的对应字节即可。
//模拟实现memcpy
/*不能自己拷贝到自己的内存中去*/
void* my_memcpy( void* Des, const void* Sou, size_t num)
{assert(Des&&Sou);void* st = Des;while (num--) {*(char*)Des = *(char*)Sou;Sou = (char*)Sou+1;Des = (char*)Des+1;}return st;
}
3 指针+-整数
char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化。 结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。
4 void* 指针注意点
在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指 针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进 ⾏指针的+-整数和解引⽤的运算,因为没有具体类型,解引用不知道访问几个字节。⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得⼀个函数来处理多种类型的数据。
2 const修饰指针注意点
2.1 const修饰变量
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。 但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。
#include <stdio.h>
//上述代码中n是不能被修改的,其实n本质是变量,只不过被const修饰后,在语法上加了限制,只要我
//们在代码中对n就⾏修改,就不符合语法规则,就报错,致使没法直接修改n。
//但是如果我们绕过n,使⽤n的地址,去修改n就能做到了,虽然这样做是在打破语法规则。
int main()
{int m = 0;m = 20; //m是可以修改的const int n = 0;n = 20; //n是不能被修改的int*p = &n;//取地址修改*p = 20;return 0;
}
2 const修饰指针变量
• const如果放在的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。
• const如果放在的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。
#include <stdio.h>
void test2()
{
//代码2
int n = 10;
int m = 20;
//const 在*左边修饰的是指针变量p指向的变量不能被改变
//但是p这个指针变量本身的值也就是地址是可以被改成其它的地址的
const int* p = &n;
*p = 20; //ok?
p = &m; //ok?
}void test3()
{
int n = 10;
int m = 20;
//const 在*右边修饰的是指针变量p的值即存储的地址不能被改变
//但是p这个指针指向的值是可以被改成其它的值的
int *const p = &n;
*p = 20; //ok?p = &m; //ok?
}
3 指针运算注意点
两个指针相减 (指针1 - 指针2
)
意义:计算两个指针之间的元素个数(而非字节数)。
计算规则:
元素数量 = (地址1 - 地址2) / sizeof(类型)。
要求:两个指针必须指向同一数组(或数组末尾之后)。
示例:
int arr[5] = {10, 20, 30, 40, 50}; int *p = arr; // p 指向 arr[0](地址0x1000) int *q = p + 2; // q 指向 arr[2](地址0x1000 + 2*sizeof(int) = 0x1008) int diff = q - p; // q = arr[2], p = arr[0] printf("%td", diff); // 输出2(相差2个int元素)
结果元素个数。
若
q
在p
之前(低地址),结果为负数。指针加指针没有意义。
• 指针的关系运算
指针就是用32个或者64比特位来表示的所以也是有数值大小的所以也是可以用来进行关系运算的,但是有以下注意点。
操作 | 意义 | 有效场景 |
---|---|---|
p == q | 是否指向同一内存地址 | 任意指针(含NULL ) |
p != q | 是否指向不同地址 | 任意指针 |
p < q | p 是否在q 之前(低地址) | 同一数组/内存块内 |
p > q | p 是否在q 之后(高地址) | 同一数组/内存块内 |
空指针的特殊性 :NULL
可与任何指针进行 ==
/ !=
比较。
核心价值:在操作连续内存结构(数组、字符串、缓冲区等)时,安全地导航和验证边界,防止非法内存访问。
4 野指针问题
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的),所以解引用的结果也是未知的。常常出现在定义指针变量没有初始化,数组访问越界等等。
规避野指针
1 如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量(宏定义),值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错。
2 ⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。
3 当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的 时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL。
5 assert函数断言
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报 错终⽌运⾏。这个宏常常被称为“断⾔”。 assert(p != NULL); 上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序 继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。 assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣ 任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误 流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。 assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和 出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问 题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个NDEBUG 。
#define NDEBUG
#include <assert.h>
然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移 除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语 句。assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。 ⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开 发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题, 在 Release 版本不影响⽤⼾使⽤时程序的效率。