目录
1. 内存和地址
1.1内存
(1)内存是电脑中一个重要的存储器,计算机中所有的程序都在内存中运行的,内存的性能对计算机的影响非常大。
(2)内存是计算机与CPU进行沟通的桥梁,计算机会把程序由硬盘加载到内存中后再去使用。
(3)为了有效的使用内存,把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。
为了能够有效的访问即快速找到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。
一般运行一个程序主要涉及计算机的三个设备:内存、CPU、硬盘。
1.1.1内存和CPU的关系
CPU是负责数据的运算和处理,内存是用来交换数据,如果没有内存则CPU就无法接收到数据。
1.1.2内存和硬盘的关系
eg:C语言中定义变量,都是在程序运行起来后在内存中开辟空间。
1.2地址
1.2.1概念
(1)为了有效的使用内存,把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。
(2)为了能够有效的访问即快速找到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。
1.2.2地址编号
在32位平台下,内存单元的地址是由32位的二进制数(即8位十六进制数)依次编号。所以内存单元共有2^32个,而每一个内存单元保存1个字节的内容。(具体见上图)
ps:
计算机访问内存的基本单位是字节。
一般计算机有32位和64位机器。
以32位机器为例,则能安装的最大内存为(2^32)*(1字节)=4GB
ps:4GB计算来源: (2^32)*(1字节)=(2*10)*(2*10)*(2*10)*(2*2)*(1字节)=4GB
(1)
常见的存储单位:bit(位)、B(字节)、KB(千字节)、MB(兆字节)、GB(千兆字节)。B 是 Byte 的缩写。换算关系:
1B=8bit
1KB=1024B
1MB=1024KB
1GB=1024MB2^10=1024
(2)
为了有效的使用内存,把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。
为了能够有效的访问即快速找到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。32位计算机是指能同时处理32位二进制数,内存单元的地址是由32位的二进制(即8位十六进制)数依次编号,所以内存单元共有2^32个,而每一个内存单元是1个字节。
所以有:
(2^32)*(1字节)=(2*10)*(2*10)*(2*10)*(2*2)*(1字节)=1024*1024*1024*4*(1字节)=4GB
1.2.3关于编址
(1)计算机为什么对每个字节进行编址?
提高查找效率。计算机内存中编址从最低的地址,依次向上进行编址,有了地址就可以对计算机中每个字节进行快速寻址,方便计算机进行快速查找。
(2)所有的编制都是连续编址的。
(3)编址是由硬件电路自动完成的。
CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节 很多,所以需要给内存进⾏编址(就如同宿舍很 多,需要给宿舍编号⼀样)。
计算机中的编址,并不是把每个字节的地址记录 下来,而是通过硬件设计完成的。
钢琴、吉他上面没有写上“都瑞咪发嗦啦”这样 的信息,但演奏者照样能够准确找到每⼀个琴弦的每⼀个位置,这是为何?因为制造商已经在乐器硬件层⾯上设计好了,并且所有的演奏者都知道。本质是⼀种约定出来的共识!
⾸先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要互相协同⼯作的。所谓的协 同,至少相互之间要能够进⾏数据传递。 但是硬件与硬件之间是互相独立的,那么如何通信呢?答案很简单,用"线"连起来。 ⽽CPU和内存之间也是有大量的数据交互的,所以,两者必须也用线连起来。 不过,我们今天关心⼀组线,叫做地址总线。
我们可以简单理解,32位机器有32根地址总线, 每根线只有两态,表示0,1【电脉冲有无】,那么 ⼀根线,就能表示2种含义,2根线就能表⽰4种含 义,依次类推。32根地址线,就能表示2^32种含义,每⼀种含义都代表⼀个地址。 地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器。
2. 指针变量和地址
2.1 取地址操作符(&)
理解了内存和地址的关系,我们再回到C语⾔,在C语⾔中创建变量其实就是向内存申请空间,⽐如:
比如,上述的代码就是创建了整型变量a,内存中 申请4个字节,⽤于存放整数10,其中每个字节都 有地址,上图中4个字节的地址分别是:
0x000000442BDAFAA4 0a
0x000000442BDAFAA5 00
0x000000442BDAFAA6 00
0x000000442BDAFAA7 00
那我们如何能得到a的地址呢?
这⾥就得学习⼀个操作符(&)-取地址操作符
#include <stdio.h>
int main()
{
int a = 10;
&a;//取出a的地址
printf("%p\n", &a);
return 0;
}
按照我上图的例子,会打印处理:0x000000442BDAFAA4 &a取出的是a所占4个字节中地址较小的字节的地址。
虽然整型变量占⽤4个字节,我们只要知道了第⼀个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。
2.2 指针变量和解引用操作符(*)
2.2.1 指针变量
那我们通过取地址操作符(&)拿到的地址是⼀个数值,比如:0x000000442BDAFAA4,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存放在哪⾥呢?答案是:指针变量中。
比如:
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;//取出a的地址并存储到指针变量pa中
return 0;
}
指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。
2.2.2 如何拆解指针类型
我们看到pa的类型是 int* ,我们该如何理解指针的类型呢?
这⾥pa左边写的是 int* , * 是在说明pa是指针变量,而前⾯的 int 是在说明pa指向的是整型(int) 类型的对象。
那如果有⼀个char类型的变量ch,ch的地址,要放在什么类型的指针变量中呢?
char *
2.2.3 解引用操作符
我们将地址保存起来,未来是要使⽤的,那怎么使用呢?
在现实生活中,我们使用地址要找到⼀个房间,在房间里可以拿去或者存放物品。
C语⾔中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针) 指向的对象,这里必须学习⼀个操作符叫解引用操作符(*)。
#include <stdio.h>
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;
return 0;
}
上⾯代码中第7⾏就使⽤了解引⽤操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间, *pa其实就是a变量了;所以*pa = 0,这个操作符是把a改成了0。
有人肯定在想,这里如果目的就是把a改成0的话,写成 a = 0; 不就完了,为啥非要使用指针呢? 其实这里是把a的修改交给了pa来操作,这样对a的修改,就多了⼀种的途径,写代码就会更加灵活, 后期慢慢就能理解了。
2.3 指针变量的大小
前⾯的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。
如果指针变量是用来存放地址的,那么指针变的大小就得是4个字节的空间才可以。
同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变的大小就是8个字节。
#include <stdio.h>
//指针变量的⼤⼩取决于地址的⼤⼩
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
return 0;
}
结论:
• 32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
• 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
• 注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。
3. 指针变量类型的意义
指针变量的大小和类型无关,只要是指针变量,在同⼀个平台下,大小都是⼀样的,为什么还要有各种各样的指针类型呢?
其实指针类型是有特殊意义的,我们接下来继续学习。
3.1 指针的解引用
对比,下面2段代码,主要在调试时观察内存的变化。
调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0。
结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。 比如: char* 的指针解引用就只能访问⼀个字节,而int* 的指针的解引⽤就能访问四个字节。
3.2 指针+-整数
先看⼀段代码,调试观察地址的变化。
我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化。
结论:指针的类型决定了指针向前或者向后走一步有多⼤(距离)。
3.3 void* 指针
在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为无具体类型的指针(或者叫泛型指 针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进行指针的+-整数和解引用的运算。
举例:
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
return 0;
}
在上面的代码中,将⼀个int类型的变量的地址赋值给⼀个char*类型的指针变量。编译器给出了⼀个警告(如下图),是因为类型不兼容。⽽使⽤void*类型就不会有这样的问题。
使⽤void*类型的指针接收地址:
#include <stdio.h>
int main()
{
int a = 10;
void* pa = &a;
void* pc = &a;
*pa = 10;
*pc = 0;
return 0;
}
VS编译代码的结果:
这里我们可以看到, void* 类型的指针可以接收不同类型的地址,但是无法直接进行指针运算。
那么 void* 类型的指针到底有什么用呢?
⼀般 void* 类型的指针是使⽤在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以 实现泛型编程的效果。使得⼀个函数来处理多种类型的数据。
4. const修饰指针
4.1 const修饰变量
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。 但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。
#include <stdio.h>
int main()
{
int m = 0;
m = 20;//m是可以修改的
const int n = 0;
n = 20;//n是不能被修改的
return 0;
}
上述代码中n是不能被修改的,其实n本质是变量,只不过被const修饰后,在语法上加了限制,只要我们在代码中对n就⾏修改,就不符合语法规则,就报错,致使没法直接修改n。
但是如果我们绕过n,使⽤n的地址,去修改n就能做到了,虽然这样做是在打破语法规则。
#include <stdio.h>
int main()
{
const int n = 0;
printf("n = %d\n", n);
int* p = &n;
*p = 20;
printf("n = %d\n", n);
return 0;
}
输出结果:
我们可以看到这⾥⼀个确实修改了,但是我们还是要思考⼀下,为什么n要被const修饰呢?就是为了不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,所以应该让p拿到n的地址也不能修改n,那接下来怎么做呢?
4.2 const修饰指针变量