前言:本文只是限于说明空指针与void指针的基本性质和用法,关于更深层次的用法,则不介绍,因为本人自己还没有搞懂!!!
1:空指针
1.1空指针的基本定义
定义:在C语言中,如果一个指针不指向任何数据,我们就称之为空指针,用NULL表示,NULL在头文件中定义了。
空指针的定义
#include<stdio.h>
int *p1 = NULL;
char *P2 = NULL;
float *p3= NULL;
long *p4 = NULL;
注意:头文件是必不可少的,stdio.h中包含了NULL的定义。空指针定义时,是可以声明为不同的类型的(如整形,字符型 等。。。。)。虽然说在一个源文件中,同时定义多个不同类型空指针没有实际的作用。
#define NULL ((void *)0)
NULL(注意:全是大写) :为宏名;
((void*)0) 是宏参数,先看(void *)先强制定义一个void 类型的指针,0是常量,即把0变换为一个void类型的指针。看起来是不是很神奇
在不同的系统中,NULL并非总是和0等同,NULL仅仅代表空值,也就是指向一个不被使用的地址,在大多数系统中,都将0作为不被使用的地址,所以就有了这样的定义。
(void *)0表示把数值 0 强制转换为void *类型(从这里看出,任何常量都能被强制转换为指针变量),最外层的( )把宏定义的内容括起来,我们自己进行宏定义时也应该这么做。
但是,C规范中并没有将NULL与空指针强制一对一绑定。
结论:普通指针也能够被赋值为NULL
int *p1=NULL;
printf("打印p1中的地址%p\n",p1);
2 void指针
2.1 void指针的基本概念
void指针又称为空指针和泛型指针,他可以指向任意类型的数据
#include<stdio.h>
int main()
{
int Var1=10;
char Var2='a';
float Var3=3.1415927;
double Var4 = 1000.0001;
/*声明一个泛型指针Void,确认是否所有的数据都能被正确的写入地址*/
void *p;
p=&Var1;
printf("1:打印Var1的地址%p\n",&Var1);
printf("1:打印指针p的值%p\n",p);
p=&Var2;
printf("2:打印Var2的地址%p\n",&Var2);
printf("2:打印指针p的值%p\n",p);
p=&Var3;
printf("3:打印Var3的地址%p\n",&Var3);
printf("3:打印指针p的值%p\n",p);
p=&Var4;
printf("4:打印Var4的地址%p\n",&Var4);
printf("4:打印指针p的值%p\n",p);
}
看输出结果:
1:打印Var1的地址000000000061FE04
1:打印指针p的值000000000061FE04
2:打印Var2的地址000000000061FE03
2:打印指针p的值000000000061FE03
3:打印Var3的地址000000000061FDFC
3:打印指针p的值000000000061FDFC
4:打印Var4的地址000000000061FDF0
4:打印指针p的值000000000061FDF0
先定义的变量地址值越大。
延伸知识点,堆栈的基本概念和应用拓展:
堆栈的本质是一段内存。
先来了解栈
概念:栈是一种特殊的线性表,其只允许在固定的一端插入和删除操作。进行数据插入和删除操作的一端为栈顶,另一端为栈底(栈低是固定的,不允许变化的)。栈中的数据元素遵守后进先出(Last In First Out)和后进先出的原则。
1、内存没有方向的概念,只有高低的区别。
2、如果压栈的顺序是从低地址往高低地址依次压栈,则是向上生长。
高地址往低地址依次压栈,则是向下生长。
这里其实有个潜台词,要先确定栈底位置,比如linux,一块栈内存,栈底位于低地址,所以压栈是从低地址往上增长。windows,栈底则位于高地址。
3、关于函数参数,从右往左压栈没有问题,比如我们例子中的c–>b–>a从栈低依次压栈。
4、关于局部变量,其实也有规律,linux平台是先定义,后入栈。Windows,则是先定义,先入栈
可以看到void类型指针,可以指向任意数据,同样根据上面代码分析还能得出下面结论,就是各种不同类型的指针,也是可以直接赋值给void类型指针的。不需要进行类型转换,但是注意反过来却是不行的,即空指针不能直接赋值给其他类型指针。
/*以下是非法操作*/
int *p1;
void *p2=&Var1;
p1=p2; //错误不能进行赋值
p1=(int*)p2 //正确,可以进行赋值
补充说明: 从上面代码输出的结果,看栈结构的存储机制
2.2 另外一种类型的void指针
另外一种重要的空指针类型,(准确说是空指针,最常见应用)malloc()也就是在堆上分配指定内存。大家还记得学习malloc时的方法了吧!
教材中说,malloc在堆上开辟了一段指定Byte的内存,如果开辟成功则返回一个类型为void类型,地址为开辟内存首地址的指针变量。如果内存开辟不成功则返回一个空指针。此时我们学习的知识形成完美的闭环。
void *p3=malloc(5);
//代码解释:先在堆上开辟了5个Byte的内存,然后将首字节的地址作为voi类型的指针返回给我们定义的void类型指针
int *p4=(int*)malloc(sizeof(int))
//这是我们以前学习malloc使用的格式,前面需要加上一个强制类型转换
/*接下来,是对内存分配是否成功的判断,储备知识,分配不成功则返回NULL*/
if(p3==NULL)
printf("p3的内存方配不成功")
else
printf("p3的内存方配成功,且p3所存储的地址是:%p",p3);
2.3void类型指针不能直接操作指向的数据
根据上面的学习,我们知道了,无论哪种类型的指针变量,其位数都是固定的。且上面的代码中,我们将各种不同类型的数据,赋值给了void类型的数据,且都打印出了正确的地址。有同学会问,那为什么不直接废除指针类型的定义,所有指针都使用void修饰。
int c[2]={100,101};
void *p3=c;
p3+=1;
printf("打印p3中的值%d\n",*p3);
return 0;
以上代码,我们是要执行打印数组的第二个值 101,。先编译,我们发现编译阶段直接报错
且代码14行,其实就出现问题,void类型指针,+1,编译器是无法确定地址是增长几位的。修改上述代码
int c[2]={100,101};
void *p3=c;
printf("1:打印p3中的地址%p\n",p3);
p3+=1;
printf("2:打印p3中的地址%p\n",p3);
运行结果:
可以看到地址只是默认增加了一位。
总结:
我们对该地址中到底是个什么类型的对象并不了解,因此不能直接操作 void* 指针所指的对象,也就无法确定能在这个对象上做哪些操作。
概括来说,以 void* 的视角来看内存空间也就是仅仅是内存空间,没办法访问内存空间中所存的对象,因此只有对其进行恰当的类型转换之后才可以对其进行相应的访问。
也就是说一个 void 指针必须要经过强制类型转换以后才有意义。