文章目录
前言
一:内存划分成内存单元,内存单元的编址
二~四:指针变量的创建,指针变量类型的意义,指针运算
五:const修饰变量和指针的作用
一、内存和地址
在讲内存和地址之前,我们想有个生活中的案例:
假设有⼀栋宿舍楼,你住在这栋楼里面,这栋楼有100个房间,但是房间没有编号,你的⼀个朋友来找你玩,如果想找到你,就得挨个房⼦去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:
一楼:101,102,103…
⼆楼:201,202,203…
…
有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。
1.内存中的编号(地址)
如果把上面的例子对照到计算机中,又是怎么样的呢?我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。我们买电脑的时候,电脑上内存是 8GB/16GB/32GB 等,那么这些内存空间的内部构造是怎样的呢?
其实内存是被划分为⼀个个的内存单元,每个内存单元的大小取1个字节。其中,每个内存单元(⼀个字节空间)里面能放8个比特位。
内存被划分成了数量庞大的内存单元,那么计算机里的CPU如何能根据要求高效精准地访问想要访问的内存单元呢?
其实每个内存单元都是有⼀个编号的(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间。
生活中我们把门牌号也叫做地址,在计算机中我们把内存单元的编号也称为地址。C语言中给地址起了新的名字叫:指针。 所以我们可以理解为:内存单元的编号 == 地址 == 指针
2.内存单元的编址方式
CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存中的内存单元进行编址(就如同宿舍很多,需要给宿舍编号⼀样)。
计算机中内存单元的编址是通过硬件设计完成的,每个内存单元对应的地址编号是在计算机生产的时候就已经在硬件层面上设计好了。
CPU与内存空间是互相独立的,那么它们是如何进行通信的呢?答案很简单,用"线"连接起来。
CPU与内存之间有三种总线,分别是地址总线、数据总线和控制总线。地址总线传输想要访问的内存单元的地址;数据总线传输数据;控制总线传输读/写命令。
不过,我们今天主要探讨地址总线:
我们可以简单理解,32位机器有32根地址总线,每根线只有两种状态,高电平(有脉冲信号)和低电平,用1和0来表示这两种状态,那么⼀根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每⼀种含义都代表⼀个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器。
二、指针变量
1.取地址操作符(&)
在C语⾔中创建变量其实就是向内存申请空间,⽐如:
#include <stdio.h>
int main()
{
int a = 51;//a是int类型,它在内存中会占用4字节空间,也就是4个内存单元
//a在内存中以二进制补码形式存储,正数的原、反、补码一致
//a的二进制:00000000 00000000 00000000 00110011
//转换成16进制是:00 00 00 33(编译器会把a在内存中的二进制形式转换为16进制展示)
return 0;
}
上述的代码就是创建了整型变量a,向内存中申请4个字节,⽤于存放整数51,其中每个字节都
有地址,4个字节的地址分别是:0x0088FD74、0x0088FD75、0x0088FD76、0x0088FD77。
那么我们如何能得到a的地址呢?
这里我们就得用到⼀个操作符(&)-取地址操作符
include <stdio.h>
int main()
{
int a = 51;
&a;//取出a的地址
printf("%p\n", &a);
return 0;
}
按照我画图的例⼦,会打印出:0088FD74,&a取出的是a所占4个字节中地址较小的字节的地址。 虽然整型变量占⽤4个字节,但因为这4个内存空间是连续的,我们只要知道了第⼀个字节地址,顺藤摸瓜就能访问到所有4个字节的数据。
2.指针变量的作用和大小
我们通过取地址操作符(&)拿到的地址是⼀个数值,比如:0x0088FD74,这个数值有时候也是需要
存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?答案是:指针变量中。
#include <stdio.h>
int main()
{
int a = 51;
int* pa = &a;//取出a的地址并存储到指针变量pa中
char b = '0';
char* pb = &b;
return 0;
}
指针变量是专门用来存放地址的变量,存放在指针变量中的值都会理解为地址。
我们如何理解指针的类型呢?
我们可以看到p的类型是 int*, * 是在说明pa是指针变量,而前面的 int 是在说明pa指向的是整型(int)类型的对象。
同理如果要存放⼀个char类型的变量b的地址时,b的地址要放到char*类型的指针变量中。
指针变量是专门用来存放地址的变量,那么在内存中创建一个指针变量需要申请几个字节的空间呢,指针变量的大小到底是多少呢?
从前面的内容我们了解到,32位机器有32根地址总线,每根地址线传输的电信号转换成数字信号后
是1或者0,那我们把32根地址线产生的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。因为指针变量就是用来存放地址的,所以指针变量的大小就是4个字节。
同理64位机器,有64根地址线,⼀个地址就是64个二进制位组成的二进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节。
注意指针变量的大小和类型是无关的,因为指针变量的大小只取决于地址的大小。32位平台下地址是32个bit位,指针变量大小固定是4个字节;64位平台下地址是64个bit位,指针变量大小固定是8个字节。所以只要是指针类型的变量,在相同的平台下,大小都是相同的。
3.解引用操作符(*)
我们用指针变量将地址保存起来后,未来是要使用的,那怎么使用呢?
在C语⾔中,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)所指向的对象,这里我们需要使用一个操作符叫解引用操作符(*)。
#include <stdio.h>
int main()
{
int a = 51;
int* pa = &a;
printf("%d\n", *pa);//*pa 的意思就是通过pa中存放的地址,找到指向的空间,所以*pa其实就是a变量
*pa = 100;//*pa = 100,其实是把a改成了100
printf("%d", a);
return 0;
}
三、指针变量类型的意义
指针变量的大小和类型⽆关,只要是指针变量,在同⼀个平台下,大小都是⼀样的,为什么还要有各种各样的指针变量类型呢?
其实指针类型是有特殊意义和特殊作用的,接下来展开叙述。
1.指针的解引用
对比下面2段代码,在调试时变量a中内容的变化。
代码1:
include <stdio.h>
int main()
{
int a = 0x11223344;
int *pa = &a; //取出a的地址并存储到int*类型的指针变量pa中
*pa = 0;
printf("%d",a);
return 0;
}
代码2:
#include <stdio.h>
int main()
{
int a = 0x11223344;
char* pa = (char*)&a;//取出a的地址先将其强制类型转换成char*类型,再存储到char*类型的指针变量pa中
*pa = 0;
printf("%d", a);
return 0;
}
通过调试我们可以看到,代码1会将a的4个字节全部改为0,但是代码2只是将a的第⼀个字节改为0。
结论:指针的类型决定了,对指针解引⽤的时候有多大的权限(⼀次能操作几个字节)。
比如: char* 的指针解引用只能访问⼀个字节,而 int* 的指针的解引用就能访问四个字节。
2.指针±整数
观察不同类型指针变量+1的效果:
#include <stdio.h>
int main()
{
int n = 10;
char* pa = (char*)&n;
int* pb = &n;
printf("%p\n", &n);//用%p格式打印地址(指针)
printf("%p\n", pa);
printf("%p\n", pa + 1);
printf("%p\n", pb);
printf("%p\n", pb + 1);
return 0;
}
我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。
这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。
结论:指针的类型决定了指针向前或者向后走⼀步有多大(距离)。
3.void*类型的指针
在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,void* 类型的指针不能直接进行指针的±整数和解引用的运算。 示例如下:
代码1:
#include <stdio.h>
int main()
{
int n = 10;
int* pa = &n;
char* pb = &n;//将⼀个int类型的变量的地址赋值给⼀个char*类型的指针变量,编译器给出了⼀个警告。
//要想消除警告,可以先将地址强制类型转换成char*类型,再赋给char*的指针变量,如:char* pb = (char*)&n;
void* pc = &n;
long m = 1000;
long* p1 = &m;
char* p2 = &m;//将⼀个long类型的变量的地址赋值给⼀个char*类型的指针变量,编译器给出了⼀个警告。
void* p3 = &m;
return 0;
}
在代码1中,将⼀个int类型的变量的地址赋值给⼀个char* 类型的指针变量,编译器给出了⼀个警告;同样将一个long类型的变量的地址赋值给⼀个char* 类型的指针变量,编译器也给出了⼀个警告。这都是因为类型不兼容。而使用void* 类型就不会有这样的问题,因为void*类型的指针变量可以用来接收任意类型的地址。
代码2:
#include <stdio.h>
int main()
{
int n = 10;
void* pa = &n;
*pa = 0;//对void*类型的指针解引用编译器会报错
printf("%p", pa + 1);//对void*类型的指针进行+-整数运算编译器会报错
return 0;
}
在代码2中,我们可以看到void* 类型的指针不能直接进行指针的±整数和解引用的运算。
这是因为void*类型指针是无具体类型指针,它可以用来接受任意类型地址,编译器在对它进行指针的解引用和±整数运算时,由于不能确定它的类型,也就不能确定对指针解引用的时候到底有多大的权限(⼀次能操作几个字节),也不能确定指针变量+1到底跳过多少个字节,所以就会报错了。
当我们拿到一个void* 类型的指针,它又不能直接进行指针的±整数和解引用的运算,那么它到底有什么⽤呢?
⼀般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果,使得⼀个函数可以处理多种类型的数据。
四、指针运算
1.指针±整数
#include <stdio.h>
int main()
{
int arr[8] = { 1,2,3,4,5,6,7,8 };
int* p = &arr[0];//把数组首元素的地址赋给指针变量p
//因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。
int i = 0;
for (i = 0; i < 8; i++)
{
printf("%d ", *(p + i));//p+i,这里就是指针+整数。
//指针+i,其实就是跳过i个指针指向的元素。所以p+1跳过1个int类型元素,它就指向数组中第2个元素,依次类推。
}
return 0;
}
2.指针- 指针
这种运算的前提条件是:必须是相同类型的指针,且两个指针得指向同一块空间(比如:同一个数组空间)
指针-指针得到的结果的绝对值是两个指针之间的元素个数
#include <stdio.h>
int main()
{
int arr[8] = { 1,2,3,4,5,6,7,8 };
int* p1 = &arr[0];//把数组首元素的地址赋给指针变量p1
int* p2 = &arr[7];//把数组中第8个元素的地址赋给指针变量p2
printf("%d\n", p2 - p1);//p2-p1,这就是指针-指针。
printf("%d", p1 - p2);
return 0;
}
五、const修饰指针
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量也可以对这个变量进行修改。
但是如果我们希望一个变量加上一些限制,不能被修改,该怎么做呢?这就是const的作用。
1.const修饰变量
#include <stdio.h>
int main()
{
int m = 0;
m = 20;//m是可以修改的
const int n = 0;
n = 20;//当我们试图修改变量n时,编译器会报错。因为被const修饰后的变量n是无法直接被修改的
return 0;
}
上述代码中n是不能被修改的,其实n本质是变量,只不过被const修饰后,在语法上加了限制,只要我们在代码中对n进行修改,就不符合语法规则,就报错,致使没法直接修改n。
但是如果我们不直接去修改n,而是使用n的地址去访问它,我们仍然可以对n进行修改。 示例如下:
#include <stdio.h>
int main()
{
const int n = 0;
int* p = &n;
*p = 20;
printf("n = %d\n", n);
return 0;
}
我们可以看到通过指针的方式确实将被const修饰的n给修改了,但是我们还是要思考⼀下,为什么n要被const修饰呢?就是为了n不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不太合理的,所以应该让p拿到n的地址也不能修改n,那接下来该怎么做呢?
2.const修饰指针变量
const修饰指针变量,可以放在 * 的左边,也可以放在 * 的右边,甚至可以放在 * 的两边,意义都是不⼀样的。
(1) int* p;//没有被const修饰的指针变量p,p所指向的内容可以通过p修改,p本身的内容也可以被修改。
(2) const int* p;
int const* p;//这两种书写方式都属于const放在*的左边,修饰的是p指向的内容,保证p指向的内容不能通过p来修改
(3) int* const p;//const放在*的右边,修饰的是指针变量p本⾝,保证了p本身的内容不能被修改
(4)const int* const p;//const放在*的两边,保证p指向的内容不能通过p来修改,也保证了p本身的内容不能被修改
我们看下面4个代码,来观察const修饰指针变量的具体效果。
代码1(测试无const修饰的情况):
#include <stdio.h>
int main()
{
int n = 10;
int m = 20;
int* p = &n;//无const修饰的指针变量p
*p = 20;//p所指向的内容可以通过p修改
p = &m; //p本身的内容也可以被修改
return 0;
}
代码2(测试const放在*左边的情况):
#include <stdio.h>
int main()
{
int n = 10;
int m = 20;
const int* p = &n;//const放在*的左边,修饰的是p指向的内容
*p = 20;//p指向的内容不能再通过p来修改。编译器会对这行代码报错
p = &m; //p本身的内容仍然可以被修改
return 0;
}
代码3(测试const放在*右边的情况):
#include <stdio.h>
int main()
{
int n = 10;
int m = 20;
int* const p = &n;//const放在*的右边,修饰的是指针变量p本⾝
*p = 20;//p所指向的内容可以通过p修改
p = &m; //p本身的内容不能再被修改。编译器会对这行代码报错
return 0;
}
代码4(测试*的左右两边都有const的情况):
#include <stdio.h>
int main()
{
int n = 10;
int m = 20;
const int* const p = &n;//const放在*的两边,p指向的内容和指针变量p本⾝都被const修饰
*p = 20;//p指向的内容不能再通过p来修改。编译器会对这行代码报错
p = &m; //p本身的内容不能再被修改。编译器会对这行代码报错
return 0;
}