一、到底什么是指针
1.指针变量和普通变量的区别
(1)首先非常关键的是,指针的实质就是一个变量,它根普通变量没有任何本质区别。指针完整的名字应该叫指针变量,简称为指针。
#include<stdio.h>
void main(){
//a的实质其实就是一个编译器的符号,再编译器中a和一个内存空间联系起来
//这个内存空间就是a所代表那个变量
int a;//定义了一个int型变量,名字叫a
int* p;//定义了一个指针变量,名字叫p,p指向一个int型变量
a = 4;//可以操作
p = 4;//编译器不允许,因为指针变量虽然实质上也是普通变量,但是它的用途
//和普通变量不同。指针变量存储的应该是另外应该变量的地址
//而不是用来随意存一些int类型的数
p = (int*)4;//进行强制类型转换则可以进行操作
}
2.为什么需要指针
(1)指针的出现是为了实现间接访问。再汇编中都有间接访问,其实就是CPU的寻址方式中的间接寻址。
(2)间接访问(CPU的间接寻址)是CPU设计时决定的,这个决定了汇编语言必须能够实现间接寻址,又决定了汇编之上的c语言也必须实现间接寻址,所以必须使用指针变量。
(3)高级语言比如java,C#就没有指针,那么他们怎么实现间接访问?答案是语言本身帮我们封装好了。
3.定义指针的步骤
(1)当我们int *p定义应该指针变量p的时候,因为p是局部变量,所以也遵循c语言局部变量的一般规律(定义局部变量并且未初始化,则值是随机的),所以此时p变量中存储的是一个随机的数字。
(2)此时如果我们解引用p,则相当于我们访问了这个随机数字为地址的内存空间。这个空间到底能不能访问不知道(也许可以也许不可以),所以如果直接定义指针变量未绑定有效地址就去解引用几乎必死无疑
(3)定义一个指针变量,不经过绑定就去解引用,就好像拿一个上了膛了枪。
(4)指针绑定的意义在于:让指针指向一个可以访问,应该访问的地方(就好像拿着枪瞄准目标的过程一样),指针的解引用是为了间接访问目标变量(就好像开枪是为了打中目标一样)
1.定义指针变量
//演示指针的标志使用方式
//指针使用分3步:定义指针变量,给指针变量赋值(绑定指针),解引用
int a=23;
//第一步:定义指针变量
//此时p变量中实际上有值的
int* p;
printf("p=%p\n", p); //%p打印指针和%x打印指针,打印出来的结果是一样的
printf("p=0x%x\n", p);
2.关联指针变量
//第二步,绑定指针,其实就是给指针变量赋值,也就是让这个指针指向另外应该变量
//当我们没有绑定指针变量之前,这个指针不能被解引用
p = &a;//实现指针绑定,让p指向变量a
p = (int*)4;//实现指针绑定,让p指向内存地址为4的那个变量
3.解引用
//第三步:解引用
//如果没有绑定指针到某一个变量就去解引用,几乎一定会出错
*p = 555;//把555放入到p指向的变量中去
二、指针带来的一些符号理解
1.星号*
(1)C语言中*可以表示乘号,也可以表示指针符号。这两个用法是毫无关联,只是恰好用了同一个符号而已。
(2)星号再用于指针相关概念的时候有2种用法:
第一种是:*结合前面的类型用于表明要定义的指针的类型
第二种是:解引用,解引用时*p表示p指向的变量本身
//实际编译测试,p1到p4都没有警告,说明编译器认为是一样的
int *p;//*和int结合,表示p的类型是int
//p是指向int类型的指针
int* p1;
int * p2;
int*p3;
//把*和指针变量放在一起,而不是和int挨在一起,是为了一行定义多个变量时好理解
int* p5, * p6;//这样定义了2个int* 变量指针
int *p5, p6;//p5是int*指针,p6是int的普通变量
int* p5, p6;//p5是int*指针,p6是int的普通变量
//演示指针变量解引用
int a = 23;
int b = 0;
int* p; //*p就是我们说的星号的第一种用法
p = &a;
b =*p;//*p就是我们说的星号的第二种用法
printf("b=%d\n", b);
2.取地址符&
(1)取地址符使用时直接加在一个变量的前面,然后取地址符和变量加起来构成一个新的符号,这个符号表示这个变量的地址
int a;//&a就表示a的地址
int* p;
p = &a;//编译器一看到&a,就知道我们要把变量a的地址赋给指针变量p
//因为变量a的地址是编译器分配的,所以只有编译器才知道a的地址
//所以我们没办法直接把a的地址的数字赋值给p,只有用符号&a来替代
3.指针定义并初始化,与指针定义然后赋值的区别
(1)指针在定义时候可以初始化,指针的初始化其实就是给指针变量初值(根普通变量的初始化没有任何本质区别)
(2)指针变量定义同时初始化的格式是int a=32;int *p=&a;
(3)不初始化时指针变量先定义在赋值:int a=32; int *p ; p=&a;
*p=&a【错误的】
4.左值与右值
(1)放在赋值运算符左边的就叫左值,右边的就叫右值。所以赋值操作其实就是:左值=右值;
(2)当一个变量为左值时候,编译器认为这个变量符号的真实含义是这个变量所对应的那个内存空间;
当一个变量为右值时候,编译器认为这个变量符号的真实含义是这个变量的值,也就是这个变量所对应的内存空间中存储的那个数
int a = 3;
int b = 5;
a = b;//当a做左值时候,我们关心的是a所对应的内存空间,而不是起存储的3
b = a;//当a做右值时候,我们关心的是a所对应的存储值3,而不是内存空间
三、野指针
1.什么是野指针?
(1)野指针,就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)
(2)野指针很可能触发运行时段错误(Sgmentation fault)
(3)因为指针变量在定义时如果未初始化,值也是随机的。指针变量的值其实就是别的变量(指针所指向的那个变量)的地址,所以意味着这个指针指向了一个地址是不确定的变量,这时候取解引用就是去访问这个地址不确定的变量,所以结果是不可知的。
(4)野指针因为指向地址是不可以预知的,所以有3种情况
第一种:指向不可以访问(操作系统不允许访问的敏感地址,比如内核空间),结果是触发段错误。
第二种:指向一个可用的,而且没什么特别意义的空间(比如我们曾经使用过但是已经不用的栈空间或堆空间),这时候程序运行不会出错,也不会堆当前程序造成损害,这种情况下会掩盖你的程序错误,让你以为程序没有问题,其实是有问题的
第三种:指向一个可用的空间,而且这个空间其实在程序中正在被使用(比如是程序的一个变量x),那么野指针的解引用就会刚好修改这个变量x的值,导致这个变量莫名其妙的被改变,程序出现离奇的错误。一般最终都会导致程序奔溃,或者数据被损害,这种危害最大的。
#include<stdio.h>
void main(){
int* p;//局部变量,分配在栈上面,栈反复被使用,所以值是随机的
*p = 4;//运行时段错误
}
(5)指针变量如果是局部变量,则分配在栈上,本身遵从栈的规律(反复使用,使用完不擦除,所以是脏的,本次在栈上分配到的变量的默认值是上一次这个栈空间被使用时余留下来的值),就决定了栈的使用多少会影响整个默认值。因此野指针的值是有一定规律的不是完全随机,但是这个值的规律对我们没有意义。因为不管落在上面野指针3种情况中的哪一种,都不是我们想看到。
2.怎么避免野指针
(1)野指针的错误来源就是指针定义了没有初始化,也没有赋值(总之就是指针没有明确的指向一个可用的内存空间),然后去解引用。
(2)知道了野指针产生的原因,避免方法就出来了:在指针的解引用之前,一定确保指向一个绝对可用的空间
第一步:定义指针时,同时初始化NULL
第二步:在指针使用之前,将其赋值绑定给一个可用地址空间
第三步:在指针解引用之前,先去判断这个指针是不是NULL
第四步:指针使用完之后,将其赋值未NULL
//野指针的避免
int a;
int* p = NULL;
//中间省略无数代码
//p = (int*)4; //4这个地址不是确定可用访问,就不要用指针去解引用
p = &a;//正确的使用指针的方式,是解引用指针跟一个绝对可用的地址绑定
if (p != NULL) {
*p = 4;
}
(4)野指针的防止方案2绝对可行,但是略显麻烦。很多人懒得这么做,那实践中怎么处理?在中小型程序中,自己可用把握就不写。
3.NULL是什么
(1)在C语言中,int *p ;你可用写成p=(int *)0,但是不可以写p=0因为类型不同
(2)所以NULL的实质其实是0,第一层原因是0地址作为一个特殊地址(我们认为指针指向这里就就表示指针没有被初始化,就表示野指针)
第二层原因是这个地址0地址在一般的操作系统中都是不可以被访问的,然后c语言程序不按规矩(不检查是否等于NULL就去解引用)写代码直接去解引用就会触发段错误,这种已经是最好的结果。
(3)一般在判断指针是否未野指针,都写成
if(NULL!=p)
原因:如果NULL写在后面,当中间是==号的时候,有时候容易忘记写成了=,这时候其实程序已经错误,但是编译器不会报错。
四、const关键字和指针
1.const修饰指针的4种形式
(1)const关键字,在c语言中用来修饰变量,表示这个变量是常量
(2)const修饰指针有4种形式,区分:
第一种:const int *p p本身不是const的,而p指向的变量是const的
第二种:int const *p p本身不是const的,而p指向的变量是const的
第三种:int * const p p本身是const,p指向的变量不是const的
第四种:const int * const p p本身是const的,p指向的变量也是const的
记忆方法:当const紧挨着p就是指向p本身,如果隔着东西就是指向p所指向的地址
#include<stdio.h>
void main(){
int a;
//第一种:
const int* p1; //p本身不是const的,而p指向的变量是const的
*p1 = 3;//报错,因为p所指向的值是不能被修改
p1 = &a;//不报错
//第二种
int const* p2; //p本身不是const的,而p指向的变量是const的
*p2 = 3;//报错,因为p所指向的值是不能被修改
p2 = &a;//不报错
//第三种
int* const p3;//p本身是const,p指向的变量不是const的
*p3 = 3;//不会报错
p3 = &a;//报错,因为p是一个常量
//第四种
const int* const p4;//p本身是const的,p指向的变量也是const的
*p4 = 3;//报错,因为p所指向的值是不能被修改
p4 = &a;//报错,因为p是一个常量
}
(3)关于指针变量的理解,主要涉及到2个变量:
第一个是指针变量p本身
第二个是p指向的那个变量(*p)
一个关键字只能修饰一个变量。
2.const修饰的变量真的不能修改吗
(1)const修改的变量其实是可以修改的(前提是gcc环境下)
(2)在某些单片机环境下,const修饰的变量是不可以修改。const修饰的变量到底能不能修改,取决于具体的环境。c语言本身并没有完全严格一致的要求
(3)在gcc中,const是通过编译器在编译的时候指向检查来确保实现的(也就是说const类型的变量不能改是编译错误,不是运行错误)所以我们要想办法骗过编译器,就可以修改const定义的常量,而运行时不会报错
(4)更深入一层的原因,是因为gcc把const类型的常量也放在了data段,其实和普通的全局变量放在data段一样的实现。只通过编译器认定这个变量是const,运行时候并没有标记const标志,所以只要骗过编译器就可以修改。
int* p = NULL;
int const a = 9;
p =(int*) &a;
*p = 6;
printf("a=%d\n", a);//a=6,结果证明const类型的变量被改了
3.const究竟应该怎么使用?
(1)const是在编译器中实现的,编译时检查,并非不能骗过。所以在c语言中使用const,就好像 一种道德约束而且非法律约束,所以大家使用const时更多是传递一种信息,就是告诉编译器,也告诉读程序的人,这个变量是不应该也不比被修改的。
五、深入学习数组
1.从内存角度来理解数组
(1)从内存角度来说,数组变量就是一次分配多个变量,而且这多个变量在内存中的存储单元是依次相连的。
(2)我们分开定义多个变量(比如int a,b,c,d)和依次定义一个数组(int a[4])
这两种定义方法相同的是定义了4个int型变量,而且这4个变量都是独立的单个使用的;
不同的单独定义a,b,c,d在内存中的地址不一定相连,但是定义成数组后,数组中的4个元素地址肯定是依次相连的
(3)数组中多个变量虽然必须单独访问,但是因为他们的地址彼此相连,因此很适合用指针来操作,因为数组和指针是天生的。
#include<stdio.h>
void main(){
int a, b, c, d;//分开独立定义4个int型变量
int a[4];//一次定义一个数组,包含4个int型变量
a = { 1,2,3,4 };//错误,数组元素必须一个一个访问,不能一次性访问多个
}
2.从编译器骄傲都来理解数组
(1)从编译器角度来讲,数组变量也是变量,和普通变量和指针变量并没有本质不同,变量的本质就是一个地址,这个地址在编译器中决定具体数值,具体数值和变量名绑定,变量类型决定这个地址的延续长度
(2)搞清楚:变量,变量名,变量类型这三个概念的具体含义
int a;char a;
3.数组中几个关键符号(a a[0] &a &a[0])的理解
(1)a就是数组名
a做左值时表示整个数组的所有空间(10*4=40字节),又因为c语言规定数组操作时要独立单个操作,不能整个操作数组,所以a不能做左值
a做右值表示数组首元素(数组的第一个元素,也就是a[0])的首地址(首地址就是起始地址,就是4个字节中最开始第一个字节的地址)a做右值等同于&a[0]
(2)a[0]表示数组的首元素
也就是数组的第0个元素。
做左值时表示数组第0个元素对应的内存空间(连续4字节);
做右值时表示数组第0个元素的值(也就是数组第0个元素对应的内存空间中存储的那个数)
(3)&a表示数组名a取地址
字面意思来看就应该是数组的地址。
&a不能做左值(&a实质是一个常量,不能变量因为不能赋值)
&a做右值时表示整个数组的首地址
a和&a做右值时的区别:&a是整个数组的首地址,而a是数组首元素的首地址。这两个在字面上是相等的,但是意义不同。意义不同会导致他们在参与运算时有不同的表现
(4)&a[0]数组第0个元素的首地址
先搞清楚[]和&的优先级,[]的优先级高于&,所以a先和[]结合在取地址。
做左值时表示数组首元素对应的内存空间
做右值时表示数组首元素的值(也就是数组首元素对应的内存空间中存储的那个值)
做右值时&a[0]等同于a。
5.总结
(1)a和&a做右值时的区别:&a是整个数组的首地址,而a是数组首元素的首地址。这两个在字面上是相等的,但是意义不同。意义不同会导致他们在参与运算时有不同的表现
(2)a和&a[0]做左值时意义和数值完全相同,完全可以互相替代
(3)&a是常量,不能做左值
(4)a做左值代表整个数组所有空间,所以a不能做左值。
六、指针与数组
1.以指针方式来访问数组元素
(1)数组元素实用时不能整体访问,只能单个访问。访问方式有2种:数组形式和指针形式
(2)数组格式访问数组元素是:数组名【下标】;(注意下标从0开始)
(3)指针格式访问数组元素是:*(指针+偏移量)
如果指针是数组首元素地址(a或者&a[0]),那么偏移量就是下标;指针也可以不是首元素地址而是其他哪一个元素的地址,这个适合偏移量要考虑叠加了
(4)数组下标方式和指针方式均可访问数组元素,两者的实质其实是一样的,在编辑器内部都是用指针方式来访问数组元素的,数组下标方式只是编译器提供给编程者(语法糖)而已。所以用指针方式来访问数组才是本质的做法。
int a[5] = { 1,2,3,4,5 };
printf("a[3]=%d\n", a[3]);//4
//a:首元素的首地址
printf("a[3]=%d\n", *(a+3));//4
int* p;
p = a;//a做右值表示数组首元素首地址,等同于&a[0]
printf("a[3]=%d\n", *(p + 3));//4
printf("a[2]=%d\n", *(p - 2));//a[2]=-858993460--->此时溢出了
p = &a[2];
printf("a[4]=%d\n", *(p + 2));//5
printf("a[2]=%d\n", *(p - 2));//1
2.从内存角度理解指针访问数组的实质
(1)数组的特点:数组中各个元素的地址是依次相连的,而且数组还要一个很大的特点(其实就是数组中的一个限制)就是数组中各个元素的类型比较相同。类型相同就决定了每一个数组元素占几个字节是相同的。(比如int数组每一个元素都占4字节,没有例外)
(2)数组中的元素其实就是地址相连接,占地大小相同的一串内存空间。这两个特点就决定了只要知道数组中一个元素的地址,就很容易推算出其他元素的地址
3.指针和数组类型的匹配问题
(1)int *p;int a[5]; p=a;//;类型匹配
(2)int *p; int a[5]; p=&a; //类型不匹配
p是int *,&a是整个数组的指针,也就是一个数组指针类型,不是int指针类型,所以不匹配
(2)&a,a,&a[0]从数值上来看是完全相等的,但是意义来看不相同了。
从意义上来看,a和&a[0]是数组首元素首地址,而&a是整个数组的首地址;
从类型来看,a和&a[0]是元素的指针,也就是int *类型,而&a是数组指针,是int(*)[5]类型
4.总结:指针类型决定了指针如何参与运算
(1)指针参与运算时,因为指针变量本身存储的数值表示地址的,所以运算也是地址的运算。
(2)指针参与运算的特点,
指针变量+1,并不是真的加1,而是1*sizeof(指针类型)
如果是int*指针,则+1实际表示地址+4,
如果是char *指针,则+1表示地址+1
如果是double* 指针,则+1表示地址+8
(3)指针变量+1时实际不是加1而是加1*sizeof(指针类型),主要原因是希望指针+1后刚好指向下一个元素。
int a[4] = { 1,2,3,4 };
int* p;
p = a;//a=&a[0]实际上将数组的首元素的首地址交给p
printf("*(p+1)=%d\n", *(p+1));//2
printf("*(p+1)=%d\n", *((char*)p + 1));//0
printf("*(p+1)=%d\n", *(int*)((unsigned int)p + 1));//*(p+1)=33554432
char* p2;
p2 = (char*)p;
printf("*(p+1)=%d\n", *(p2 + 1));//0
七、强制类型转换
1.变量的数据类型的含义
(1)所有的类型的数据存储在内存中,都是按照二进制格式存储。所以内存中只知道有0和1,不知道int,double还是其他类型
(2)int,char,short等属于整形,他们的存储方式(数转换成二进制往内存中存放的方式)是相同的,只是内存格式大小不同(所以这几种整形就彼此叫二进制兼容格式),而float和double的存储方式彼此不同,和整形更不相同
(3)int a=5时,编译器给a分配4字节空间,并且将5按照int类型的存储方式转成二进制存到a所对应的内存中取(a做左值的);我们printf取打印a的时候,此时(a做右值),printf内部的vsprintf函数会按照格式化字符串(就是printf传参的第一个字符串参数中的%d之类的东西)所代表的类型去解析a所对应的内存空间,解析出来的值用来输出。也就是说,存进去时是按照这个变量本身的数据类型来存储的(比如本例中a为int所以按照int格式来存储);但是取出来时是按照printf中%d之类的格式化字符串的格式来取。虽然此时a代表的内存空间中的10101序列并没有变(内存并没有被修改)但是怎么理解不一定。
总结:C语言中的数据类型的本质,就是决定了这个数在内存中怎么存储的问题,也就是决定了这个数如何转换成二进制的问题。一定要记住一点是内存只存储1010序列,而不管这些1010怎么解析。所以要求我们平时数据类型不能吓唬乱搞。
按照int类型存却按照float类去取 一定会出错
按照int类型存却按照char类去取 可能会错
按照short类型存却按照int类去取 有可能出错
按照float类型存却按照double取 一定会出错
2.指针的数据类型的含义
(1)指针的本质:变量,指针就是指针变量
(2)一个指针涉及2个变量:一个是指针变量自己本身,一个是指针变量1指向的那个变量
(3)int * p;定义指针变量时,p是int*类型【指针变量本身】,*p是int类型【指针指向的那个变量】
(4)int * 类型:就是指针类型,只要是指针类型就都是占4字节,解析方式都是按照地址的方式来解析(意思是里面存的32个二进制加起来表示一个内存地址)的。结论就是:所有指针类型(不管是int* 还是char* 还是double*)的解析方式是相同的,都是地址。
(5)对于指针所指向的那个变量来说,指针的类型就很重要。指针所指向的那个变量的类型(int*),那么指针所指向的变量就是int类型的。
3.指针数据类型转换实例(int *-->char *)
(1)int和chcar类型都是整形,类型兼容的。所以互转的时候有时候错有时候对。
(2)int和char的不同在于char只有一个字节而且int有4个字节,所以int的范围比char大。在char所表示的范围内int和char是可以互转的不会出错。但是超过了char的范围后char转换成int不会错(向大方向转就不会错),而从int到char转就会出错。
//3.指针数据类型转换实例(int *-->char *)
int a = 3;
char* p = &a;
printf("*p=%c", *p);//超出范围时会出现奇怪的值
short* p2 = &a;
printf("*p2=%c", *p2);//超出范围时会出现奇怪的值
int a[3] = { 0x11223344,0x55667788,0 };
int* p1 = a;
printf("*p1=0x%x\n", *p1);//0x11223344
char* p2 = (char*)a;
printf("*p1=0x%x\n", *p2);//0x44
printf("*p1=0x%x\n", *(p2+1));//0x33
printf("*p1=0x%x\n", *(p2 + 2));//0x22
printf("*p1=0x%x\n", *(p2 + 3));//0x11
4.指针数据类型转换实例(int * -->float *)
int和float的解析方式是不兼容的,所以int* 转成float*再去访问绝对会出错
int a = 5;
int* p1 = &a;
float* p;
p = (float*)p1;//int*--->float*
printf("*p=%f", *p);