内存
举个栗子,如果把计算机比作是一所大学,那么内存就相当于一栋一栋的学生宿舍楼,楼里的每个宿舍都有自己的房间号,假设宿舍是一个8人间,把宿舍里的每个学生比作成一个比特位(bit),那么每个房间就是一个字节(Byte),而房间号,当你想要“串门”的时候,通过房间号就可以快速找到该房间。
bit --- 比特位
Byte --- 字节 1Byte = 8bit
KB 1KB = 1024Byte
MB 1MB = 1024KB
.........
可以想象:内存是一个巨大的空间,这个空间被均匀地划分成相同大小的内存单元,每个内存单元都有独属于自己的地址编号,称为地址,每个内存单元的大小是1个字节,每个字节能存放8个比特位。
在计算机中,我们把内存单元的地址编号叫做地址,在C语言中,我们给地址起了一个新名字——指针。所以我们可以这样理解:
内存单元的编号 == 地址 == 指针
指针变量和地址
取地址操作符(&)
在学习指针变量之前,先认识一个新的操作符:&(取地址操作符)。当我们想要获取A的地址时,只需要在A的前面加上" & "即可。比如:
int a = 0;
&a;//这样就能获取a的地址了
printf("%p",&a);//打印地址用" %p "
指针变量
在C语言中,我们如何创建一个整型变量or字符型变量呢?很简单,是这样的:
int a = 0;
char ch = 'c';
同样的方法,我们可以这样创建:
int* ptr;//" * "表示ptr是指针变量,int则表示ptr指向的是int类型的对象
char* ptr_ch;//同理
double* ptr_dl;//同理
(注:int* pa和int *pa是一样的,看个人书写习惯)
指针变量前的int* 、char* 、double* 有什么意义呢?我们稍后再提。
学会指针变量的创建之后,我们就可以用指针来存放地址了。例如:
int a = 10;
&a;
int* pa = &a;//pa是指针的名字,想叫啥都行
我们把地址存入指针之后该怎样使用呢?这时就需要学习一个新的操作符“ * ”
解引用操作符(*)
解引用操作符(*)就像一把钥匙,能够打开房间门,从而找到房间里的东西。也就是说,“ * ”能够获取指针所存放的地址里面的值(有种套娃的感觉,一定要花时间思考)
int a = 10;//假设a的地址编号为0x0012ff40
int* pa = &a;//假设pa的地址为0x00126650,此时pa里放的就是a的地址
*pa;//对pa解引用就能打开a的地址,从而获得a的值
printf("%d\n",a); //输出结果为10
printf("%d\n",*pa);//输出结果也为10
你也可以通过地址来改变变量的值,例如:
int a = 123;
int* pa = &a;
printf("%d\n",a);//输出为123
*pa = 456;//通过地址改变a的值
printf("%d\n",a);//输出为456
指针变量的大小
在32位机器中,指针变量的大小是固定的,为4个字节。在64位机器中,指针变量的大小也是固定的,为8个字节。不论这个指针变量的类型是int*、char*还是double*,它大小都是固定的。
指针变量类型的意义
现在回到我们之前没有解决的问题:指针变量前的int* 、char* 、double* 有什么意义呢?
我们前面提到,一个指针变量,比如:
int a = 10;
int* pa = &a;
" * " 前面的 int 意思是pa指向的是int类型的对象。
现在学习一个新的知识点:
当指针在访问内存空间时,它有一个访问权限,而这个访问权限的大小就与指针变量的类型有关。
先说结论:指针变量的类型决定它一次性能访问多少个字节(或者说一次性能跳过多少个字节)。比如,int*的指针能一次性访问4个字节,char*的指针能一次性访问1个字节,double*的指针能一次性访问8个字节
直接上代码:
int a = 123;
int* pi = &a;
char* pc = (char*)&a;//把a的地址强制类型转换为char*
printf("a的地址是:%p\n",&a );
printf("pi = %p\n", &a);
printf("pi+1 = %p\n", pi+1);
printf("pc = %p\n", &a);
printf("pc+1 = %p\n", pc+1);
运行结果:
我们可以发现,int*类型的指针pi在+1后从E4变成了E8(跳过了4个字节),而char*类型的指针pc在+1后从E4变成了E5(跳过了1个字节)。
除此之外,还有一种void*类型的指针,留到以后再讲。
const修饰指针
const是什么呢?
const是C语言中的一个关键字,若A被const修饰了,那么A就具有了常属性(简单理解为不能被修改)。
const修饰指针的三种形式:
1、const放在“ * ”的左边
int a = 0;
const int *pa = &a;//常量指针
你需要理解:
这种叫作常量指针,我们可以认为 const 修饰的是 *pa ,而 *pa 表示的就是a存放的值0
当我们尝试通过*pa来修改a的值时:
我们会发现编译器报错了,因此,对于这种修饰,我们无法通过指针来改变指针所指对象的值
再看一组代码:
指针ptr一开始指向的是a的地址,当我们改变指针ptr的指向时,程序成功运行。
因此,我们可以得出结论:
常量指针能改变指针的指向,但不能改变指针所指对象的值
2、const放在“ * ”的右边
int a = 0;
int * const ptr = &a;
这种叫作指针常量,我们可以认为 const 修饰的是指针ptr,而ptr表示的就是一个存放a的地址的指针
当我们尝试改变指针ptr的指向时:
我们发现编译器报错了,因此,我们能够知道:这种修饰方式不能改变指针的指向
接下来再看一组代码:
指针始终指向的是a的地址,当我们尝试通过*ptr来改变a的值时,程序成功运行。
因此,我们能够得出结论:
指针常量能改变指针所指对象的值,但不能改变指针的指向
不难发现,常量指针和指针常量的作用是相反的
3、const放在“ * ”的两边
int a = 0;
const int * const pa = &a;
有了前两个例子的铺垫,我们能够很容易的知道:这种修饰方式使得指针的指向和指针所指对象的值都不能改变
指针的运算
指针的+、-
指针也是能进行加减法的,接下来我用数组举例:
int i = 0;
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* ptr = arr;//数组名就是数组的首元素地址(即arr和&arr[0]的地址是一样的)
//用指针遍历数组
for (i = 0; i < 10; i++)
{
printf("%d ", *ptr);
ptr++;
}
由于ptr是int*类型的,arr是int类型的,所以ptr++就会使指针向后移动4个字节,也就是向后移动一个元素(减法同理,但要小心不要访问不属于自己的内存空间)
指针加减指针
同样以数组为例:
int i = 0;
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* left = &arr[2];//数组名就是数组的首元素地址(即arr和&arr[0]的地址是一样的)
int* right = &arr[8];
int ret = right - left;
printf("%d\n", ret);
下标: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
元素: | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
运行结果:
发现结果就是left和right之间的元素个数
指针的关系运算
就是比较地址的大小(不想写了偷个懒)
野指针
概念
野指针就是指针指向的地址是不可知的(随机的、不正确的、没有明确限制的)
野指针的成因
1、指针未初始化
int* ptr;//未初始化
*ptr = 10;
指针未初始化意思是指针在创建时没有给它一个地址。此时指针就会胡乱的指向任意的内存空间,我们无法去访问该空间,这是一种非法的(错误的)行为
2、指针访问数组时发生了越界
int arr[3] = { 0 };
int* ptr = &arr[0];
int i = 0;
for (i = 0; i < 3; i++)
{
*(ptr++) = i;//这一步把 i的值赋给*ptr,然后再+1使ptr向后移动到下一个元素的位置
}
i= 0 时,arr[0] = 0;i = 1 时,arr[1] = 1;i = 2时,arr[2] = 2 ,此时 i 赋值给*ptr后,ptr还要完成+1的操作循环才会结束,+1向后移动4个字节循环结束,此时的ptr指向的是arr[2]后的空间,超过了数组最后一个元素的空间,使得ptr指向了不属于数组的地址,这就是越界。
3、指针指向的内存空间被释放了
int* test(int n)
{
return &n;//n作为函数test的形参,会向系统在栈区申请一块空间来存放n的值
} //当test一结束,系统就会收回n申请的空间,此时n的地址对于这个程序就是非法的
int main()
{
int* p = test(123);
int* ptr = *p;//此时ptr接收的是一个非法的地址
printf("%d", *ptr);
return 0;
}
如何避免野指针
当我们创建好一个指针变量后,可以将其置为NULL。
NULL是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错。