内存和地址
众所周知,指针在c语言中是最重要的一个部分,它涉及到传参,访问等操作。说到这些就不得不提到一个概念内存和地址。
在讲内存和地址之前,我们先来讲一个生活中的例子。张三在大学宿舍里点了一份外卖,外卖员给他送外卖,到了地点后。外卖员要找他,宿舍里有500间房间,这个时候外卖员不可能挨个挨个敲门去找张三这样的话既费时又费力。这个时候就要用到门牌号了,门牌号可以帮助外卖员很快找到张三的房间并及时把外卖交给张三。
这里的门牌号就相当于电脑中地址。
在计算机中,CPU(中央处理器)在处理数据时,需要的数据是在内存中读取的,处理后的数据也会放回内存中。
我们在买电脑的时候电脑内存通常有8GB/16GB/32GB 等,那这些内存空间如何⾼效的管理呢?
其实内存里面是分为⼀个个的内存单元,每个内存单元的大小取1个字节。
其中,每个内存单元,相当于⼀间学⽣宿舍,⼀个字节空间⾥⾯能放8个比特位,就好⽐学生们住的八人间,每个⼈是⼀个⽐特位。每个内存单元也都有⼀个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间。生活中我们把⻔牌号也叫地址,在计算机中我们
把内存单元的编号也称为地址。C语⾔中给地址起 了新的名字叫:指针。
所以我们可以理解为:
内存单元的编号 == 地址 == 指针
在计算机中,电脑储存数据是2进制编码的形式,1个字节相当于8个比特位,一个比特位可以存一个二进制的0或1
常见的存储单位:
bit - 比特位
Byte - 字节 KB
MB
GB
TB
PB
1Byte = 8bit
1KB = 1024Byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB
1指针变量和地址
1.1取地址操作符(&)
#include <stdio.h>
int main()
{
int a = 0;
return 0;
}
在上面的代码中我们可以看见,当int a = 0;这条语句执行了后,系统向内存申请了4个字节来存放变量a的数据。每个字节都有自己的地址。
4个字节的地址为:
0x00B3FD28
0x00B3FD29
0x00B3FD2A
0x00B3FD2B
那么要如何才能获得这些地址呢,这时我们就要用到c语言中的取地址操作符&。
#include <stdio.h>
int main()
{
int a = 0;
printf("%p", &a); //&a取出a的地址
return 0;
}
运行上面代码后就可以打印出a在内存中的地址了,可以看到a此时的地址00B3FB6C;
00B3FB6C是变量a在内存中最小的地址,虽然a在内存中占4个字节,但是我们只需要知道它最小的地址后就行了,因为变量的地址在内存中是连续的,我们知道了它最小的地址后其它的地址就可以顺藤摸瓜的找出来了。
1.2指针变量和解引用操作符(*)
1.2.1指针变量
指针变量顾名思义就是存放指针的变量,也可以说存放地址的变量
#include <stdio.h>
int main()
{
int a = 0;
int* p = &a; //把a的地址存放在指针变量p中
printf("%p", p);
return 0;
}
如和理解 int* p = &a这条语句呢?
我们可以把 p 理解为一个变量,*表示p变量是指针是存放地址的,int表示存放的地址指向的是一个int类型,也可以直接把int*理解成一个类型,用来存放int类型的地址
同理,如果我们有一个字符变量ch,要存放它的地址我们可以这样写:
#include <stdio.h>
int main()
{
char ch = 'a';
char* p = &ch; //把a的地址存放在指针变量p中
printf("%p", p);
return 0;
}
1.2.2解引用操作符
那么我们把变量存起来是干什么呢?
当然是要使用啦!
在c语言中我们拿到了地址就能通过地址,对地址指向的变量进行修改
要修改就得用到c语言中的解引用操作符*
#include <stdio.h>
int main()
{
int a = 20;
int* p = &a;
printf("%d\n", a);
*p = 30;
printf("%d\n", a);
return 0;
}
运行代码后我们可以看见,在*p = 30 这条语句执行后,a的变量从原来的20 变成了30。
但有一点要切记,*p = 30 不要写出p = 30,如果写出p = 30,相当于把30这数字作为地址存放到指针变量p中去了,而a中的值并不会改变。
1.3指针变量的大小
在了解指针变量的大小之前我们先运行一段代码。
#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;
}
2指针变量类型的意义
2.1解引用
我们先来调试2段代码来观察一下,他们在内存中的变化
代码1:
#include <stdio.h>
int main()
{
int n = 0x55667788;
int* p = &n;
*p = 0;
return 0;
}
代码2:
#include <stdio.h>
int main()
{
int n = 0x55667788;
char* p = &n;
*p = 0;
return 0;
}
2.2指针+-整数
我们先来运行一段代码
#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后跳过了一个字节,而int*在+1后跳过了4个字节。指针变量+1跳过多少个字节也与他们的类型有关。
2.3void* 指针
在指针类型中有⼀种特殊的类型是 void* 类型,可以理解为无具体类型的指针或者叫泛型指针
这种指针可以接受任何类型的地址,但是不能进行+-整数和解引用操作。
3 const修饰指针
变量是可以修改的,也可以通过把地址传给指针变量,通过解引用来修改变量,如果我们想给变量加上一些限制,不能被修改,那要怎么做呢?
3.1const修饰变量
c语言中提供了一个关键字const可以修饰变量。
#include <stdio.h>
int main()
{
int a = 10;
a = 20; // 可修改变量
const int b = 10;
b = 20; //不可修改变量
return 0;
}
被const修饰后的变量属性就和常量一样不能修改了但本质上还是一种变量,只是在语法上添加了限制。
但是如果我们再创建一个指针变量,用来存放b的地址,通过解引用操作来修改b的值呢?
#include <stdio.h>
int main()
{
int a = 10;
a = 20;
const int b = 10;
int* p = &b;
*p = 20;
printf("a = %d,b = %d", a, b);
return 0;
}
我们发现这种方法可以打破语法规则,修改b里面的值。但是这样的话不就相当于打破了const的限制吗?那么怎么才能让p拿到b的地址去不能修改b里面的值呢?
3.2const修饰指针变量
const的位置可以放在*的左边也可以放在*的右边,但是意义是完全不一样的。
这里我们发现,const在左边的时候,是不能进行*p = 20也就是解引用修改操作;
但是在右边就可以进行解引用修改。
这里我们发现,const在左边的时候,可以给p重新取一个地址,但是cons在右边就不行。
综合以上结论,我们可以得出const在左边的时候是限制的*p但是p本身不受限制,如果const在右边限制的是p本身,*p不受限制。
当然我们也可以左右两边都加上const,这样的话指针变量p既不能重新赋值,也不能解引用修改值。
4.野指针
4.1野指针成因
#include <stdio.h>
int main()
{
int *p;
*p = 30;
return 0;
}
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
for (i = 0; i <= 11; i++)
{
*(p++) = i;
}
return 0;
}
#include <stdio.h>
int* test()
{
int n = 7;
return &n;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}