目录
前言
本期介绍内容:
一、指针是什么?
二、指针和指针类型
指针类型的意义:
三、野指针
3.1什么是野指针?
3.2野指针的成因
1.指针未初始化
2.指针越界访问
3.指针指向的那块空间已经释放(还给操作系统了)
3.3如何规避野指针
四、指针运算
4.1指针加减整数
4.2指针-指针
4.3指针的关系运算
五、指针和数组
六、二级指针
6.1什么是二级指针?
七、指针数组
前言
我们经常听到很多大佬都说指针是C语言中的精髓、指针很重要,但对于很多初学者都觉得指针很难、不都不知道指针是什么?指针到底指向哪里??指针和地址是什么关系?本期小编将带您弄清楚指针的基本问题!
本期介绍内容:
1.指针是什么
2.指针和指针类型
3.野指针
4.指针运算
5.指针和数组
6.二级指针
7.指针数组
一、指针是什么?
1.指针是内存中一个最小的单元编号,也就是地址(唯一标识一个内存单元)。
2.我们一般所说的指针是指针变量,是用来存放内存地址的变量。
也就是说:指针就是地址,口语中的指针是指针变量
为了更好地管理内存,计算机把内存分为了若干的内存单元,每一个内存单元都有一个唯一的编号也就是地址。由于该地址(编号)指向该内存空间因此也形象的把地址(编号)称为指针!
OK,画个图理解一下:
指针变量:我们知道地址在内存中是以十六进制的形式存储,通过&操作符就可以取出变量的地址,取出来要一个变量接收与存储,这个变量就是指针变量!
举个栗子:
int main()
{
int a = 3;
int* pa = &a;
return 0;
}
int* pa 的*号说明pa是一个指针变量(口语中是指针),前面的int说明pa指向对象的类型是一个整型。后面的&a取出a的地址赋值给pa此时pa就指向a;
总结:指针变量是用来存放地址的变量(存放在指针变量的值都被当成地址来处理)
到这里我们就基本了解清楚了指针是什么以及指针和地址的关系,指针和指针变量的关系!
通上面的介绍我们知道指针唯一标识一块内存单元。但又有一些新的问题出现了:例如
一个内存单元的大小是多大呢?
还有地址是十六进制的数他又是如何进行编址的呢?
指针变量的大小是多少?
下面我们一起来分析一下:
通过了解计算机组成原理发现一个内存单元给一个字节较为合适(既不浪费也不会不够)。
解释:我们知道C语言中char类型的变量占一个字节,如果说每一个内存单元大于1个字节,char类型的变量开辟空间的时候就会出现浪费的情况,因此综合下来一个内存单元为一个字节刚好!
另外,它的编址是由地址线产生的高低电频来进行的!
对于32位平台(x86)机器,假设有32根地址线(物理电线),那么假设每根地址线在寻址的时候都会产生高电平(高电压)和低电频(低电压)就可以把电信号转换换为数字信号也就是0和1(这块是电路方面的数电和模电小编了解过一二,它里面还介绍了加法器,二极管等等有兴趣的可以看看);
那么32根地址线就会是下面这中情况:
这里就是2^32个地址。而我们上面知道每一个地址标识唯一的一个内存单元,我们就可计算一下2^32个地址可以给多大的空间进行编址(2^32Byte == 2^32/1024KB == 2^32/1024/1024MB == 2^32/1024/1024/1024GB == 4GB)也就是说2^32个地址可以为4GB的内存空间编址。64位平台的机器同理,感兴趣的可以算一算!
在32位平台的机器上,地址是32个0或1组成的二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小是4个字节!
在64位平台的机器上,地址是64个0或1组成的二进制序列,那地址就得用8个字节的空间来存储,所以一个指针变量的大小是8个字节!
举个栗子验证一下:
int main()
{
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(float*));
printf("%d\n", sizeof(double*));
printf("%d\n", sizeof(long*));
return 0;
}
32位平台(x86):
64位平台(x64):
这里还要注意一点sizeof返回值是size_t实际上是unsigned int
打印的时候要用%zd否则会报警告! ! !
总结
指针变量是用来存放地址的,地址是唯一标识一个内存单元的。
指针的大小在32位平台上是4个字节,在64位平台上是8个字节。
二、指针和指针类型
我们都知道,变量有不同的类型,有整型、浮点型,字符型等,那么指针变量有没有类型呢?
答案是:有的!
我们将一个变量的地址取出来(&)存到pa中,pa就是一个指针变量,给他的类型是什么呢?
答案是:指针指向的那个变量的类型加*即:type + *
举个栗子:
int main()
{
int a = 3;
int* pa = &a;
char b = 'a';
char* pb = &b;
float f = 1.2f;
float* pf = &f;
double d = 13.4;
double* pd = &d;
return 0;
}
这里int*类型的指针存放的是int类型变量的地址,char*的指针存放的是char类型的地址...上面我们知道指针类型要么都是4个字节要么8个字节,那为什么还要分类型呢?
指针类型的意义:
先说结论:
(1)指针类型可以决定指针解引用的时候的访问权限(可以访问多好个字节)
(2)指针类型可以决定指针+/-的步长
例如:int类型指针+1跳过4个字节,char类型的指针+1跳过1个字节,那+/-n 就是跳过n *sizeof(type)个字节!!!
解释(1):
int main()
{
int a = 0x11223344;
int* pa = &a;
*pa = 0;
return 0;
}
关于这段代码解释两点,首先a存的这个数(16进制)不会溢出,十六进制的1位相当于二进制的4位,int类型的a32个二进制位所以11223344刚好把a放满!其次,会发现他在内存中是倒着存的,这个是大小端字节序的问题,后面会专门有一期介绍!
这是执行到*pa = 0;发现变成了00 00 00 00
我们把int *改为char *再来看看:
int main()
{
int a = 0x11223344;
char* pa = &a;
*pa = 0;
return 0;
}
一开始还是和int*的一样!我们接着往下看:
我们发现只改掉了 一个字节的内容!这就完美的证明了上面的结论1!
解释(2):
int main()
{
int a = 3;
int* pa = &a;
char* pc =(char*) &a;
printf("%p\n", pa);
printf("%p\n", pc);
printf("%p\n", pa+1);
printf("%p\n", pc+1);
return 0;
}
我们分析,前两个打印的是一样的,主要看后两个:
前两个果然一样(都存的是第一个字节的地址),后面是因为一个是int类型一次访问4个字节,另一个是char类型一次访问一个字节 ,访问权限不一样步长也就不一样!
三、野指针
3.1什么是野指针?
野指针就是指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针。
3.2野指针的成因
1.指针未初始化
int main()
{
int* p;
*p = 10;
return 0;
}
此时,p是一个指针变量未初始化,也是局部变量(在函数内)由前面的函数栈帧的创建与销毁可知,局部变量未初始化是随机值(0hccccc),把随机的一块内存给改成10,这多少有点危险!
2.指针越界访问
int main()
{
int arr[10] = { 1 };
int* p = arr;
int i = 0;
for (i = 0; i <= 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
arr数组只有十个元素,下标0--9,你直接访问10将是一个随机值!
3.指针指向的那块空间已经释放(还给操作系统了)
int* test()
{
int a = 3;
return &a;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
这里a 是在test函数里面创建的,当return &a后test的函数栈帧就已经销毁了(还给操作系统了),当下面在去解引用的时候就指向随机位置了!
3.3如何规避野指针
(1)指针初始化
(2)小心指针越界
(3)指针指向的空间释放及时置为NULL(NULL空指针实际上就是0)
(4)避免返回局部变量的地址
(5)指针使用之前检查其有效性(if判断和断言)
举个栗子:
#include<assert.h>
int main()
{
int* a = NULL;//指针初始化
int p = 8;
int* pp = &p;
assert(pp);
*pp = 20;
printf("%d ", *pp);
int b = 12;
int* pb = &b;
if (pb != NULL)
{
*pb = 20;
}
printf("%d\n", *pb);
return 0;
}
四、指针运算
4.1指针加减整数
指针加减整数的结果我们知道还是一个指针,我们可以通过指针的加减的偏移量来访问指针指向的变量的值(例如数组)!
举个栗子:
int main()
{
int arr[10] = { 1 };
int* pp = &arr[0];
for (int i = 0; i < 10; i++)
{
*pp++ = 0;
}
int* p = arr;
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
这个栗子就很好的展现了指针加一个整数去通过其偏移量来访问元素的值!
补充:
这里我们可以看到在打印的时候是以*(p + i)打印的,我们在没有了解指针的时候是以arr[i]打印的,而p里面存的是arr数组的首元素的地址,也就是说*(arr + i) 和arr[i]是等价的!而由加法的交换律我们可以变形一下: *(arr + i) == *(i + arr)而*(arr + i) ==arr[i] 我们可以推断 *(i + arr) == i[arr]是否也成立呢?
验证一下:
答案是成立的!这里小编验证这个不是为了说以后建议大家写代码就这样写,这样写多少有点太装了!!!这里介绍这个主要是为了再次说明一下:下标引用操作符[ ] 它的两个参数一个是数组名一个是索引(下标),他和 + 一样,a + b == b + a 一个道理!
4.2指针-指针
不知您是否想过指针减指针(指向同块内存)的结果是什么?是指针还是一个数?下面小编带您一起来讨论一下:
先来看个栗子:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d", &arr[9] - &arr[0]);
return 0;
}
考虑一下答案是多少?
答案是9!为什么是9呢?下面我来画个图解释一下 :
我们通过对数组和内存的了解画出了上面的这个图!并得出结论:
指针减指针得到的绝对值(可能是低地址减高地址也有可能是高地址减低地址,所以取绝对值)是两指针之间的元素个数!并且注意的是:指针-指针的前提是两指针指向同一块内存!!否则无意义!
介绍到这里我就想到了一个与这个高度贴合的题(模拟实现strlen 指针-指针)当然模拟实现strlen 的方式有三种这里我将介绍这一种(后面会一一把其他两种介绍):
int MyStrlen(char* str)
{
char* p = str;
while (*str)
{
str++;
}
return str - p;
}
int main()
{
char str[] = "abcdef";
int len = MyStrlen(str);
printf("%d ", len);
return 0;
}
strlen 是求字符串长度(‘\0’之前)的函数!当然我们这仅仅是一个雏形,基本功能已经实现,已经能精准的测量出字符串的长度了,后面还有优化的点等到后面介绍了断言和const了再来优化!
看一下结果:
4.3指针的关系运算
我们知道地址有大小,而指针的关系运算就是比较指针的大小!
OK!举个栗子:
int main()
{
int arr[5] = { 1 };
int* p = NULL;
for (p = &arr[5]; p > &arr[0]; )
{
*--p = 0;
}
return 0;
}
这段代码不是您是否看的懂?我们一起来看一看:
此时聪明的你肯定能想到优化方案:
int main()
{
int arr[5] = { 1 };
int* p = NULL;
for (p = &arr[4]; p >= &arr[0]; p--)
{
*p = 0;
}
return 0;
}
OK,这个方案的确看起来好多了!但,这样写的代码对吗?
答案是:在有的编译器上是不一定对的会报越界的警告!
C语言标准规定:
允指向数组元素的指针与指向数组左后一个元素后面的那个内存中位置的指针比较,但不允许与指向数组第一个元素的那个内存位置的指针比较!
什么意思我们画个图理解一下:
虽然现在部分编译器对上面的第二段代码不会报错但有的编译器会,为了以后不会出现类似的问题还请大家遵循标准!!!
我们现在来看看他为什么不对:
我们发现当你初始化到&arr[0] 的空间时候还是成立的,那就先初始化再调整!p--就到数组前面的一块空间去了!
五、指针和数组
我们说:指针是指针,数组是数组,一般无关系!只有数组名和首元素地址有关!
OK,我们先来看个栗子:
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
他的结果是什么呢?
哦吼~他两的结果一样,所以可以得出结论:数组名一般情况下表示数组首元素的地址!两种情况除外(1.sizeof(数组名)计算的是整个数组的大小 2.&数组名取出的是整个数组的地址)。这块在数组和操作符都介绍过,这里不在多介绍了!
既然数组名是首元素的地址也就是指针,那么能不能用指针的偏移量来不访问数组元素呢?
答案是肯定的:
六、二级指针
我们知道指针变量里面存的是变量的地址,不知您有没有想过对指针变量取地址该存到哪里?
6.1什么是二级指针?
其实指指针变量也是变量,他也有地址(和普通变量一样)&后就得用指针变量存起来,而这个指针变量里面存的就是指针的地址(指针),也就是说一个指针变量里面存的是另一个一级指针变量的地址这样的指针变量就叫二级指针!
举个栗子:
int main()
{
int a = 3;
int* pa = &a;
int** ppa = &pa;
return 0;
}
ppa就是一个二级指针!! int** ppa后面的一颗*说明paa是一个指针变量!前面的int*则说明ppa指向的对象是(一级)指针类型!
理解到这里就可以以二级指针类比三级指针了:三级指针就是里面存的是二级指针变量地址的指针变量!
int main()
{
int a = 3;
int* pa = &a;
int** ppa = &pa;
int*** pppa = &ppa;
*(*(*pppa)) = 10;
printf("%d ", a);
return 0;
}
我我们这里pppa就是一个三级指针,*pppa指向ppa,*ppa指向pa,*pa指向a!然后赋值10;
七、指针数组
我们经常听说的两个东西:一个是指针数组一个是数组指针很多人都搞不清楚这个东西,其实数组指针本质是指针!而指针数组本质是数组!下面小编带您look一look一下指针数组,数组指针会在后面的指针进阶里面讲!本期仅仅在这里提一下,后面重点对比!!!
指针数组本质是一个数组!是存放指针的数组!
我们知道一个整型或字符行的数组,里面都存放的是相对应类型或比该类型小的元素,那指针数组里面存的应该就是地址!
举个栗子:
int main()
{
int arr1[] = { 1,2 };
int arr2[] = { 3,4 };
int arr3[] = { 5,6 };
int* arr[] = { arr1,arr2,arr3 };
return 0;
}
此时的arr就是一个指针数组!画个图理解一下:
看到这里就很应该很清楚了!不知您看到这个图的时候有没有一点感觉很熟悉? 这个是不是和二维数组很相似。arr管理的三个一维数组,我们前面说过二维数组是数组的的数组!是不是和这个很相似?那我们能不能访问呢?
int main()
{
int arr1[] = { 1,2 };
int arr2[] = { 3,4 };
int arr3[] = { 5,6 };
int* arr[] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 2; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
for (int j = 0; j < 2; j++)
{
printf("%d ", *(*(arr + i) + j));
}
printf("\n");
}
return 0;
}
看结果,他这两种打印结果一样:
OK,本期指针基础就分享到这里!好兄弟我们下期再见!