1.内存和地址
内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的
所以为了有效的使用内存,就把内存划分成一个个小块,这每一个小块被称为内存单元,每个内存单元的大小是1个字节
为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址,地址在C语言中也被称为指针,所以以后就可以通过地址找到对应的内存单元
接下来创建一个变量a:int a = 4,int类型占四个字节,所以a在内存中占4个内存单元:
可以用&对a进行取地址:&a,a是一个整形变量,占用4个字节,每个字节都有自己的地址,这里取出的地址是第一个字节的地址(较小的地址)
所以这里&a取出值的是0x0012ff44
2.指针变量的大小
通过一下代码就能得出指针变量的大小
#include <stdio.h> int main() { printf("%d\n", sizeof(char *)); printf("%d\n", sizeof(short *)); printf("%d\n", sizeof(int *)); printf("%d\n", sizeof(double *)); return 0; }
结论:指针大小在32位平台是4个字节,64位平台是8个字节
以上结论是根据程序结果得到的,接下来我们也可以通过物理层面去得到指针变量大小:
对于32位机器,它有着32根地址线,每跟线在寻址的时候会产生高电压和低电压也就是1或者0,有着32根地址线,也就是地址有着32歌比特位,1字节等于8比特,所以32位机器中地址占4个字节
这里我们可以继续算下去:32根地址线,就有2的32次方个地址,2^32Byte == 2^32/1024KB ==2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB,所以在32位机器中,可以给4G的空间进行编址。
这里我们就明白:
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
总结:
指针变量是用来存放地址的,地址是唯一标示一个内存单元的
指针的大小在32位平台是4个字节,在64位平台是8个字节
3.指针类型的意义
指针有许多类型,如:int*,short*,char*,float*……
但是从前面得知指针的类型都是4或8字节大小,它们不会存在像如int和char不同类型有着不同占用空间的问题,既然指针类型大小一样,那么为什么不把指针设置成一个同一类型的指针并且可以接收不同的类型变量的地址呢?不同的指针类型都有什么意义呢?
意义1:指针访问权限的大小
首先看一下代码:
#include <stdio.h> int main() { int a = 0x11223344; int* pa = &a; *pa = 0; return 0; }
调试后进入内存窗口,找到a的地址后可以看见在内存中a中的存储的值为:44332211,这样“倒着存放”的原因是在Visual studio这个IDE中采取小端存储,如不懂大小端存储,👉可点击跳转
然后继续运行下一行代码,会发现*pa = 0使内存中4个字节内容变为0
接着再看一个代码:
#include <stdio.h> int main() { int a = 0x11223344; char* pa = &a; *pa = 0; return 0;
这个代码与上面代码不同的一点是这次pa指针变量是char类型的,上一个代码中的类型为int类型的
还是调试后进入内存窗口,找到a的地址后可以看见在内存中a中的存储的值为:44332211
接着运行,会发现,内存中只有44变为了00,也就是只有一个字节的内容发生了改变
这两个代码比较得出结论:
指针类型决定了,指针进行解引用操作的时候,一次性访问几个字节,访问权限的大小
int*类型的解引用会对4个字节内容进行操作
char*类型的解引用会对1个字节内容进行操作
float* 类型的解引用会对4个字节内容进行操作
double类型的解引用会对8个字节内容进行操作
所以,我们可以通过不同的指针变量按需求对不同字节大小内容进行操作
意义2:指针类型决定指针的步长
观看一下代码:
int main() { int a = 0x11223344; int* pa = &a; char* pc = &a; printf("%p\n", pa); printf("%p\n", pa+1); printf("%p\n", pc); printf("%p\n", pc+1); return 0; }
得出结果:
可以看出,前两个地址大小差4,后两个地址大小差2
pa的类型为int*,pa+1的地址增加了4,pc的类型为char*,pc+1的地址增加了1
得出结论:
指针类型决定指针的步长(指针+1到底跳过几个字节)
字符指针+1,跳过1个字节
整型指针+1,跳过4个字节指针初阶指针初阶
4.野指针
野指针概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针成因
指针未初始化
#include <stdio.h> int main() { int *p; *p = 20; return 0; }
这里的指针p在定义的时候没有初始化,默认为随机值,也就是指针指向位置不可知。
以随机值为地址,解引用时需要找到那个地址处,是很危险的
此程序在编译器里不会被编译成功,会警告:使用了未被初始化的内存p
指针越界访问
#include <stdio.h> int main() { int arr[10] = {0}; int *p = arr; int i = 0; for(i=0; i<=11; i++) { *(p++) = i; } return 0; }
在arr数组内有10个元素,循环时,到下标为10的时候,指针指针指向的范围超出数组arr的范围时,p就是野指针
3.指针指向的空间释放
int* test() { int a = 10; return &a; } int main() { int* p = test(); printf("%d\n", *p); return 0; } 1
在test函数中,返回int类型a的地址,在主函数中用p接受这个地址,但是当test运行完return语句后,在test函数中对于变量开辟的空间就会归还给系统,此时&a已经不存在了,所以p为野指针
如何规避野指针
1.指针初始化
2.小心指针越界
3.指针指向空间释放,及时置NULL p = NULL
4.避免返回局部变量的地址
5.指针使用之前检查有效性:if(p!=NULL)
5.指针的运算
指针加减整数
指针加减整数在前面指针类型意义里提到,这里不多说
这里看一个代码:
int main() { int values[5]; int * vp; for(vp = &values[0]; vp < &values[5];) { *vp++ = 0; } }
这个代码很简单,就是通过指针,逐步把数组中元素值赋值为0
这里有一点值得注意的:*vp++ = 0;,++优先级比*高,又由于++是后置,先用再加加。
vp的初始值为&values[0],所以才会从values[0]开始赋值,接着++,再对下一个位置赋值。
若改为前置++,则会先加加,再赋值,就会倒置values[0]位置处未被赋值。
指针减指针
指针-指针,前提是:两个指针要指向同一块空间,得到的结果的绝对值是两个指针之间元素个数
int main() { int arr[10] = { 0 }; printf("%d", &arr[9] - &arr[0]); return 0; }
输出结果:
这里我们可以利用指针减指针来模拟strlen函数
假设有一个字符串abcdefghi,在内存中实际存储的是a b c d e f g h i \0,所以只要找到首元素的地址和\0的地址,相减就能得到字符串的长度
代码如下:
int my_strlen(char* s) { char* begin = s; while (*s!='\0') { s++; } return s - begin; } int main() { char arr[] = "abcdefghi"; int len = my_strlen(arr); printf("%d\n", len); return 0; }
指针的比较运算
以数组为例,数组首元素地址是小于数组除首元素其他元素地址
但是标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与
指向第一个元素之前的那个内存位置的指针进行比较。
数组尾部的地址不是最后一个元素的地址,而是下标为数组长度处的地址
如上图:可以用&arr[6]和指针vpp与数组中其他地址进行比较,但是不可以用指针vp进行比较
通俗来讲就是:可以用数组后边的地址比较,但是不可以用数组前面的地址比较。
6.指针与数组的关系
指针与数组的关系主要在这篇文章里👉数组与地址,数组名到底是什么?
我们观察以下代码
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; //用数组下标遍历数组元素 for (int i = 0; i < 10; i++) { printf("%d ", arr[i]); } printf("\n"); //利用指针遍历数组元素 int* p = arr; for (int i = 0; i < 10; i++) { printf("%d ", *(p + i)); } }
因为数组名就是数组元素首地址,所以在这个程序里arr与p是完全相同的。
同时这里可以发现用数组下标遍历数组和利用指针遍历数组都可以遍历,所以arr[i]和*(p+i)是等价的
也就是有一个指针p,p[i] <=> *(p+i)
并且我们还可以推理:因为加法有交换律,所以*(p+i)和*(i+p)肯定是是等价的,通过*(i+p)又可以推出i[p]
所以p[i]是可以写成i[p]的,只是因为i[p]这种写法太过于逆天,所以不常见。
7.二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
这就是 二级指针 。
int main() { int a = 10; int* pa = &a; //一级指针 int** ppa = &pa; //二级指针 return 0; }
上面代码中的ppa就是二级指针
二级指针有两个*,怎么理解呢?
先看一级指针 ,int* pa = &a,一级指针中的那个*就表示pa是一个指针,剩下的那个int表示指针pa指向一个int类型变量
二级指针也同理
int** ppa = &pa,离ppa近的那个*表示ppa是一个指针变量,剩下的int*表示ppa这个指针指向int*类型
二级指针的解引用也和一级指针一样:**ppa
指针的内容还有很多
还有 字符指针 ~ 指针数组 ~ 数组指针 ~ 函数指针 ~~~~ 等等
~Coming soon~
🎸🎸🎸