c语言从0开始_10 指针
指针
指针定义
指针是一种特殊的数据类型,使用指针可以定义指针变量,指针变量中存储的是整形数据,这种整数代表了内存的编号, 每个整数对应一个字节,使用指针变量可以访问对应的内存,具体访问的多个字节由指针的类型决定。
为什么要使用指针
1、函数之间是相互独立的,但是有时候需要共享变量
传参是单向值传递
全局变量命名容易冲突
使用数组还需要传递长度
虽然命名空间相互独立,但是地址空间是同一个,所以指针可以解决共享变量的问题
2、由于函数之间传参是值传递(内存拷贝),对于字节数较多的变量,值传递的效率较低,如果传递变量的地址只需要传递4|8个字节
3、堆内存无法取名字,它不能像data/bss/stack让变量名与内存之间建立联系,只能使用指针来记录堆内存的地址,以此来使用堆内存
什么情况使用指针
注意:从理论上讲指针可以访问任何位置的内存,但绝大部分的内存我们没有权限访问,因此非常容易产生段错误,因此建议只有合适的时候才使用指针。
1.函数之间共享局部变量:
由于全局变量浪费内存,还可能造成命名冲突,所有全局变量不适合大量共享。
而函数传递默认是值传递,无法共享,所以指针是函数之间共享局部变量的最好选择。
2.提高函数的传递效率:
而函数传递默认是值传递(内存拷贝),当变量的字节数较多时,传递变量的地址效率更高,只需要4/8字节。
但这样就带来了变量被修改的风险,可以使用const配合指针来保存变量。
3.配合堆内存使用:
堆内存无法取名(不能与标识符建立映射关系),因此必须与指针配合使用。
如何使用指针
定义指针变量
类型* 指针变量_p;
1、由于指针变量的用法与普通变量不同,为了避免与普通变量混淆,建议在取名时以p结尾加以区分
2、指针变量的类型表示了存储的是什么类型的变量的地址,它决定了通过这个指针变量可以访问的字节数
3、一个只能定义一个指针变量,即指针变量不能连续定义
int p1,p2,p3; // 只有p1是指针变量,p2 p3都是int类型变量
int *p1,*p2,*p3;// p1 p2 p3都是指针变量
4、指针变量与普通变量一样默认值是随机的(野指针),为了安全一定初始化,如果不知道赋值什么值,可以赋值为NULL(空指针)。
指针变量的解引用
*指针变量
通过指针变量中存储内存编号去访问内存,这叫做解引用。
该过程可能产生段错误,原因是指针变量中存储的是非法的内存编号
*p;
*p <==> num
注意:访问的字节数是由指针变量的类型决定的
使用指针需要注意的问题
空指针
指针变量的值为NULL的指针被称为空指针,这种指针不能解引用,否则会产生段错误。
大多数系统的NULL是0地址,可以把指针变量当逻辑值使用,但不建议这样。NULL也是一种错误标志,如果一个函数的返回值是指针类型时,当函数执行出错时可以返回NULL作为错误标志
为了避免空指针导致的段错误,当使用来历不明的指针时应判断是否是空指针。
if(NULL == p)
{
}
如何避免空指针带来的段错误:
对于来历不明的指针使用前,一定要进行判断
1、调用函数返回值为指针类型时,可能是空指针
2、当函数的参数是指针类型时,调用者传递过来的可能是空指针
注意:NULL在绝大多数系统中是0,极个别系统中是1,if(!p)
野指针
指针变量的值是随机的、不确定,这种指针称为野指针,使用野指针不一定立即出错,也不一定会出错,
与空指针相比它的危害更大,因为它是无法判断出来的,错误可能是隐藏型的,短时间内可能不会暴露。
对野指针解引用的后果:
1、段错误
2、脏数据
3、一切正常
所有的野指针都是人为制造的,无法判断一个指针变量是否是野指针,所以为了避免野指针导致的Bug,我们应该避免产生野指针。
1、定义指针变量时一定要初始化。 int* p = NULL;
2、指向变量所指向的内存被释放、销毁后,指针变量要及时赋值为NULL。
3、函数不返回局部变量的地址。
指针的运算
指针变量中存储的是整数,理论上整型数据能使用的运算符,指针变量都可以使用,但只有以下运算有意义:
指针 + n<=> 指针+(n字节数):指针+指针类型字节数n 前进了n个元素
指针 - n <=> 指针-(n字节数):指针-指针类型字节数n 后退了n个元素
指针 - 指针<=> (指针-指针)/字节数:(指针-指针)/指针类型字节数 计算出两个指针之间间隔了多少个元素,只有类型相同的指针才可能相减。
注意:指针相加没有意义,就像拿两个门牌号相加一样没有意义。问指针运算不能怎样就是不能相加。(其实可以,但因为无意义,就当做不能)
const与指针
当我们为了提高传参效率而使用指针时,传参的效率提高了,但是变量也有被修改的风险,这种情况可以使用const来进行保护
const int* p; 保护指针所指向的内存不被修改
int const* p; 同上
int* const p; 保护指针的指向不能修改
const int* const p; 保护指针指向的内存和指向都不能修改,既保存p也保存*p
int const* const p; 同上
数组指针与指针数组
指针数组
是由指针变量组成的数组,它的身份是数组,它的成员都是指针变量
类型* arr[长度]
数组指针
它的身体是指针,专门用来存储数组的地址,用于指向数组的指针
类型 (*变量名)[指向的数组的长度];
例: int (*arr) [5];
注意:数组指针是以数组字节宽度为移动单位。
指针与数组名的关系
数组名其实是一种特殊的指针,看作常指针,不能修改它的值,它与内存之间是映射关系,不能改变,而指针与内存之间是指向关系,可以改变
数组名是没有存储空间,而指针变量是有存储空间
指向数组的指针,可以当做数组名使用, 数组名可以使用解引用的语法访问成员,
指针也可以使用[]解引用,所以:
数组名也可以当做指针使用
数组名[i] == *(数组名+i)
*(指针名+i) == 指针名[i]
arr[n] <=> *(arr+n);
注意:当数组作为参数传递时变成了指针,所以长度丢失了
数组名与普通指针的区别
1、数组名是常量,普通指针是变量
2、普通指针变量有自己的存储空间,用来存储内存编号,它与目标内存是指向关系。
3、数组名没有自己的存储空间,它就代表数组空间的首地址,它与数组内存是映入关系。
4、对数组名取地址结果还是数组名的值。
int arr[5]
arr <=> &arr;
arr 类型 int*
&arr 类型 int (*)[5]
二级指针
一级指针里面存储普通变量的地址,二级指针里面存储的是一级指针变量的地址。
当跨函数共享指针变量时,需要使用二级指针。
定义:
类型** 变量名_pp;
赋值:
变量名_pp = &指针变量;
例 int num;
int* p = #
int** pp = &p;
解引用:
*变量名_pp <=> 指针
**变量名_pp <=> *指针
*pp <=> p;
**pp <=> *p;
函数指针
函数名就是地址(整数),它代表了函数在代码段中的位置
函数指针就是指向函数的指针,里面存储的是函数在代码段中的位置
134513520
函数会反翻译二进制指令存储到代码段,而函数名就这段指令的首地址。
可以定义特殊的指针变量存储函数的首地址,这样就可以把函数当前数据进行传递。
我们把存储函数地址的指针变量叫做函数指针。
如何定义函数指针:
1、复制函数的声明语句
2、给函数名加上小括号并在函数名前加一个*
3、给函数名重新取函数指针变量名
由于函数指针的类型比较长,一般会选择使用 typedef 进行类型重定义。
void func(int,int);
typedef void (*FP)(int,int);
回调模式
当把函数作为数据传递时,被调用的函数可以通过函数指针调用我们以参数形式提供的函数,这种模式叫回调模式,比如:qsort、bsearch。
void指针
1、以1字节为单位移动
2、不能解引用
3、可以与任何类型的指针自动类型转换(C语言)
4、一般用作函数的参数、返回值