思维导图:
目录
1.指针与内存
如果我们把内存想像成一座大楼,那指针就表示这座大楼里的一个房间的门牌号。而门牌号总是会在你收快递时填在收件地址那一栏,所以指针又被叫作地址,也叫一个内存单元。
1.1内存中的门牌号(地址)是如何编号的
其实计算机的地址都是由地址线随即划分的。计算机中的地址线是一条条物理线,着一条条物理线会通电,电的种类由正负两种,正负两种电信号在计算机中分别用零、一这两个二进制数表示。当计算机表示的二进制序列不同就可以表示出不同的地址了。当你用的是32位的计算机就可以表示2^32个地址,当你用的是64位的计算机时就可以表示2^64个地址。
1.2一个指针的大小(在32位平台上)
求不同类型指针的大小可以用以下代码:
#include<stdio.h>
int main() {
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(short*));
printf("%d\n", sizeof(long*));
printf("%d\n", sizeof(long long*));
printf("%d\n", sizeof(float*));
printf("%d\n", sizeof(double*));
}
结果:
4
4
4
4
4
4
在这里可以看到不同类型的指针的大小都是一样的,无论你是什么类型的.因为我们知道整形(int)的sizeof(int)打印的结果是4,单位是字节(byte)所以我们又可以知道可以知道一个内存单元的大小是由byte(字节)来组成的。
1.3内存总空间大小
一个内存单元的大小是一个字节,所以总空间大小=2^32byte=4,294,967,296byte=4,194,304kb=4096mb=2gb.
2.指针的使用
int*pa
2.1指针的构成:
指针的组成大概就是由这三部分构成,一个星号加一个变量名便表示这是一个指针变量。
但是,在文章的前面讲过指针的大小在一个平台上是一样大的。那我们可能就会可能想指针加一个类型有什么作用呢?
2.2指针类型的作用
1.决定指针访问的权限的大小
举个例子:
#include<stdio.h>
int main(){
int a = 0x11223344;
printf("%d\n", a);
int* pa = &a;
*pa = 0;
printf("%d", a);
}
现在我用的是整形的指针int*pa,并且将整形a的值赋值为0x11223344这个16进制位。因为一个16进制位需要八个二进制位来组成。所以一个十六进制数字就代表一个字节的空间大小。a就有八个字节的空间,也就是32个比特位。现在来看看修改的结果:
287454020
0//修改后的a值
可以看到a的值被改为了零。再来打开调试窗口看看内存里的变化:
在这里可以看到在用一个整形类型的指针对a进行值的修改时可以对a内存里的值全部进行修改。
现在将整形指针变量改为char型指针变量看看会发生什么:
#include<stdio.h>
int main(){
int a = 0x11223344;
printf("%d\n", a);
char* pa = &a;
*pa = 0;
printf("%d", a);
}
看看结果:
287454020
287453952
可以看到a的值完全就跟零一点都不沾边了。这是怎么回事呢?
好吧再来看看内存的情况:
从图中可以看到 如果用char型指针变量来修改一个整型变量的值的话是不能改到你想要的值的。因为char型只能改一个字节内存里的值。
小结:从这两个例子的对比中可以看出指针的不同类型决定了指针变量能访问,改变的内存的大小。
2.决定指针走一步(指针变量加一)可以走多远
举个例子:
#include<stdio.h>
int main() {
int arr[10] = { 0 };
int* pa = &arr;
char* pc = &arr;
printf("arr的地址:%p\n", &arr);
printf("pa的指向:%p\n", pa);
printf("pc的指向:%p\n", pc);
}
//%p即为打印地址的专用占位符。p是point的缩写
看看结果:
arr的地址:0115FE44
pa的指向:0115FE44
pc的指向:0115FE44
从上面代码的结果可以看出指针pa与指针pc指向的地址是一样的都是数组arr的地址。
现在摘来看看这个代码的结果:
#include<stdio.h>
int main() {
int arr[10] = { 0 };
int* pa = &arr;
char* pc = &arr;
printf("arr的地址:%p\n", &arr);
printf("pa的指向:%p\n", pa);
printf("pc的指向:%p\n", pc);
printf("pa+1的结果:%p\n", pa+1);
printf("pc+1的结果:%p\n", pc+1);
}
结果如下:
arr的地址:004FF6E8
pa的指向:004FF6E8
pc的指向:004FF6E8
pa+1的结果:004FF6EC
pc+1的结果:004FF6E9
从结果可以看出pa+1与pc+1的结果是不一样的。pa+1这个操作使pa的地址的显示值加了4,pc+1的操作只让pc的地址的显示值加了1。
总结:所以在这里可以知道指针类型的第二个作用便是决定指针的步长即一步走多远。
3.野指针的问题及其规避方法
3.1野指针的产生
如以下代码:
#include<stdio.h>
int main(){
int* p;
* p = 20;
printf("%d",*p);
return 0;
}
运行结果:
这里就会出现编译错误,错误的点是:使用未初始化的指针变量p。
再看这个代码:
#include<stdio.h>
int main() {
int arr[10] = {0};
int* p = arr;
int i = 0;
for (i = 0;i <= 10;i++){
*p = i;
p++;
}//指针最终加11次,到arr[11]这是一个不属于arr数组的内存空间。
return 0;
}
再来看看结果:
结果是报了个警告,还有一个我不认识的英文单词(corrupted),拿字典查一下可以知道这是溢出,越界的意思。所以第二个代码的指针有越界访问的问题,从而变成了野指针。
再到第三个代码:
#include<stdio.h>
int* test(){
int a = 10;
return &a;
}
int main(){
int* p = 20;
*p = 20;
return 0;
}
看看结果:是可以运行的。
但是这个指针p也是一个野指针。因为函数返回的是a的地址,*p接收的便是a的地址。但是当a出了函数以后,a这个变量就被销毁了,a的地址也没有了。当指针p接收a的地址时,指针p接收的地址是不确定的,所以指针p就变成了一个野指针。在这里就揭示了产生野指针的第三个问题----动态局部变量被销毁后产生野指针。
总结:产生野指针的三个问题:1.指针变量未初始化 2.指针越界访问 3.动态变量被销毁产生野指针。
3.1规避方法:
1.提前初始化
2.小心越界
3.释放后及时置空
4.检查有效性
4.指针的运算
指针的运算有三种1.指针+/-整数,2.指针的关系运算,3.指针减指针
指针加减整数以及指针的关系运算相信大家已经比较熟悉了。现在来重点看看指针减指针这个内容。猜猜下面代码的答案吧:
#include<stdio.h>
int main(){
int arr[10] = {0};
int* p = &arr[0];
int* pa = &arr[9];
printf("%d", pa - p);
return 0;
}
答案是什么呢?
现在揭晓答案:
9
9?9是个什么东西啊?9是不是arr[0]到arr[9]之间的元数个数啊?好像是的。
所以在这里我们就可以知道如果两个指针指向同一个数组,那这两个指针相减得到的差值就是这两个指针对应数组之间的元素个数。
那这有什么用呢?
用处:
假如我们要求一个字符串的长度时可以用strlen()函数,比如这样:
#include<string.h>
#include<stdio.h>
int main() {
int len = strlen("abc");//千万要记得用的是双引号。
printf("%d", len);
return 0;
}
结果:
3
也可以自定义一个函数来求,比如这样:
#include<string.h>
#include<stdio.h>
int my_strlen(char* str) {
int i = 0;
while (*str != '\0') {
i++;
str++;
}
return i;
}
int main() {
int len = my_strlen("abc");
printf("%d", len);
return 0;
}
答案也是和上面一样是3。
我们学了指针减指针是两者之间的元素个数以后又有第三种写法了:
#include<stdio.h>
int my_strlen(char* str) {
int len=strlen(str);
char*start = str;
while(*str!='\0){
str++;}
return str-start;
}
int main() {
int len = my_strlen("abc");
printf("%d", len);
return 0;
}
这个代码的答案也是3。
5.数组和指针
我们先来看一个例子:
#include<stdio.h>
int main() {
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
在这里看看结果:
在这里可以得到一个结论:数组名就是首元素的地址。
既然是地址的话,那数组内的元素就可以用指针来访问了。
比如:
#include<stdio.h>
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
for (int i = 0;i < 10;i++) {
printf("%d ", arr[i]);
}
return 0;
}
结果:
现在用指针来写:
#include<stdio.h>
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int* p = arr;
for (int i = 0;i < 10;i++) {
printf("%d ", *(p + i));
}
return 0;
}
结果:
6.二级指针
二级指针的意义其实显而易见,通过学习我们都知道一级指针存地址的。
比如:
#include<stdio.h>
int main() {
int a = 2;
int* p1 = &a;//用int型的指针存int型的变量地址
char b = 2;
char* p2 = &b;//用char型变量的指针存char型变量的地址
float c = 1.0;
float *p3= &c;//用float型的变量存float型变量的地址
return 0;
}
从这里我们看到我们要存怎样的变量就要用怎样的指针。
所以在我们要存一个指针型变量的地址时我们就要用到二级指针了。
比如:
#include<stdio.h>
int main() {
int a = 0;
int* p = &a;
int** ppa = &p;//用一个int*型的指针接收指针变量p的地址
return 0;
}
总结一下就是:
二级指针是因为有要存一级指针的地址的需要才出现的。
7.指针数组
数组其实可以理解为是一系列相同类型的数的集合。
比如:
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };//十个整型数字的集合
所以在我们要用数组存一系列的地址时就要用到指针数组了。
比如:
#include<stdio.h>
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int* p[10];
for (int i = 0;i < 10;i++) {
p[i] = &arr[i];
}
return 0;
}
可以打印出来看一下:
结语:
好了,小牛儿的分享就到这里了,如果有错误的话请各位看官多多指正。