【C语言】——函数栈帧
- 一、 c o n s tconstconst 修饰指针
- 1.1、constconst const 修饰变量
- 1.2、constconst const 修饰指针
- 二、野指针
- 2.1野指针的成因
- (1)指针未初始化
- (2)指针越界访问
- (3)指针指向的空间释放
- 2.2、如何规避野指针
- (1)指针初始化
- (2)小心指针越界
- (3)指针变量不再使用时,及时置 NULL,指针使用之前检查有效性
- (4)避免返回局部变量的地址
- 三、 a s s e r tassertassert 断言
- 四、传值调用与传址调用
- 4.1、传值调用
- 4.2、传址调用
- 五、二级指针
- 六、指针数组
- 6.1、指针数组的概念
- 6.2、指针数组模拟二维数组
一、 c o n s tconstconst 修饰指针
1.1、 c o n s tconstconst 修饰变量
当我们创建一个变量后,不想被修改,该怎么办呢?这时,我们就可以请出 constconst const 来修饰变量了
int main(){int m = 0;m = 20;//m可以被修改const int n = 0;n = 20;//n不能被修改return 0;}
运行结果:
如上图,若修改被 c o n s tconstconst 修饰的变量,则程序报错。
其实, nnn 本质上还是变量,只是被 c o n s tconstconst 修饰后,在语法上加了限制(此时的 nnn 称作常变量
),只要我们在代码中对 nnn 进行修改,就不符合语法规则,程序就会报错。
虽然通过正常手段无法修改 nnn 的值,但我们可以用间接手段:通过 nn n 的地址,来修改 nn n 。
#includeint main(){const int n = 0;printf("n = %d\n", n);int* p = &n;*p = 20;printf("n = %d\n", n);return 0;}
但我们用 c o n s tconstconst 修饰变量,本质上是不希望变量被修改的,你现在通过指针把我给改了,是不是有点不讲武德。就好像我不希望你进我家,我把家门给锁,你现在翻窗进来,是不是有点不合适?
所以我们应该让 ppp 拿到 nnn 的地址也修改不了 nnn ,那该怎么办呢?
我们可以用 constconst const 来修饰指针。
1.2、 c o n s tconstconst 修饰指针
constconst const 修饰指针变量的时候
- constconst const 如果放在 ∗*∗ 的左边,
修饰的是指针所指向的内容
,保证指针所指向的内容不能通过指针来修改,但是指针变量本身的内容可以改变。- constconst const 如果放在 ∗*∗ 的右边,
修饰的是指针变量本身
,保证了指针变量本身不能被修改,但是指针指向的内容可以通过指针来改变。
注:当然, c o n s tconstconst 也可以在 ∗*∗ 两边都放的,但一般很少这么做。(你两边都不给我改,是不是太过分了)
通过代码来感受一下:
(1)constconst const 在 ∗* ∗ 左边
#includeint main(){int n = 0;int m = 10;const int* p = &n;p = &m;printf("%d\n", *p);return 0;}
运行结果:
c o n s tconstconst 在 ∗∗∗ 左边 : 指针变量本身的内容可以改变
参考代码:
#includeint main(){int n = 0;int m = 10;const int* p = &n;*p = 20;printf("%d\n", *p);return 0;}
运行结果:
c o n s tconstconst 在 ∗*∗ 左边 : 指针所指向的内容不能通过指针来修改
(2)const 在 ∗* ∗ 右边
参考代码:
#includeint main(){int n = 0;int m = 10;int* const p = &n;*p = 20;printf("%d\n", *p);return 0;}
运行结果:
c o n s tconstconst 在 ∗*∗ 右边 : 指针指向的内容可以通过指针来改变
参考代码:
#includeint main(){int n = 0;int m = 10;int* const p = &n;p = &m;printf("%d\n", *p);return 0;}
运行结果:
c o n s tconstconst 在 ∗*∗ 右边 : 指针变量本身不能被修改
二、野指针
概念:
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
2.1野指针的成因
(1)指针未初始化
int main(){int* p;//局部变量指针未初始化,默认为随机值*p = 20;return 0;}
注:局部变量指针不初始化,默认为随机值
(2)指针越界访问
#includeint mian(){int arr[10] = { 0 };int* p = &arr[0];int i = 0;for (i = 0; i <= 11; i++){//当指针指向的范围超出数组arr的范围时,p就是野指针*p(p++) = i;}return 0;}
注:当指针指向超出 arrarr arr 数组的范围时,pp p 就是野指针
(3)指针指向的空间释放
#includeint* test(){int n = 100;return &n;}int mian(){int* p = test();printf("%d\n", *p);return 0;}
注:在【C语言】——详解函数一文中,曾提到函数中形参只是实参的一份拷贝,变量 nn n 在出函数范围时已释放,这时的指针变量就是野指针。
2.2、如何规避野指针
(1)指针初始化
- 明确知道指针指向哪里,就
初始化一个明确的地址
- 如果创建时,还不知道指针具体指向位置,则给指针赋值 NULLNULL NULL 。
注: N U L LNULLNULL 是C语言中定义的一个标识符常量,本质是 0
。0 也是地址,但该地址位于内核之中,我们用户是无法使用
的,读写该地址会报错。
#ifndef NULL#ifdef __cplusplus#define NULL 0#else#define NULL ((void *)0)#endif#endif
我们可以把野指针看成野狗,很危险。给野指针赋值 NULL 相当于把野狗拴在树上,这时,你只要不靠近就不会有危险,相对安全 。
(2)小心指针越界
创建变量时,向内存中申请了多少空间,通过指针也只能访问这些空间。超出这些空间访问,就是越界访问,指针也就成了野指针。上述例子中,数组的越界访问就是如此。
(3)指针变量不再使用时,及时置 NULL,指针使用之前检查有效性
当我们创建指针变量想访问某个区域,后期不再访问时,可以将指针变量置为 NULL。
使用指针变量前,我们先检查他的有效性
,即检查它是否为空指针,如果为空指针
,我们就不再访问
。
还是上面野狗的例子,虽然野狗被拴起来,但是靠近他还是很危险,要和他保持一定距离才算安全。
事实上,我们使用指针最合理的方式是:先判断,再使用。
(4)避免返回局部变量的地址
这一点,上述第三个例子中,不要返回函数中创建的局部变量的地址。
三、 a s s e r tassertassert 断言
C语言 > 头文件中,定义了宏 assertassert assert,那么 a s s e r tassertassert 的功能是什么呢” /> a s s e r tassertassert 用于在运行时程序符合规定条件,如不符合,则报错程序终止运行
,并给出报错信息提示
宏 a s s e r tassertassert 常常被称作断言
assert(p != NULL)
程序在运行到上述语句时,会先判断指针 ppp 是否为空指针,如果为空指针,则报错给出信息并终止程序运行。如果不是空指针,则程序正常运行。
a s s e r t ()assert()assert()宏接受一个表达式作为参数(该表达式不一定非要指针),该表达式为真 a s s e r t ()assert()assert()宏不会产生任何作用,程序正常运行,当表达式为假, a s s e r t ()assert()assert()宏就会报错,在标准错误流 stderrstderr stderr 中写入一条错误信息,表示没有通过的表达式,以及包含这个表达式的文件名和行号。
那么 a s s e r tassertassert 有什么好处呢?
- 他能
自动表示
文件和出问题的行号- 他有一种
无需更改代码
就开启和关闭的机制。如果想关闭 a s s e r tassertassert 断言,只需在 #include 前面,加上一个宏 NDEBUGNDEBUG NDEBUG
#define NDEBUG#include
而如果程序又出现问题,我们只需要将 NDEBUGNDEBUG NDEBUG 删除掉, a s s e r t ()assert()assert()就能重新启动了。
这时,可能还有小伙伴会问,我可以直接用 ifif if 语句来判断啊,为什么要用 a s s e r tassertassert 断言呢?
相比与if语句,assertassert assert 断言:
- 出现错误,直接报错,并指明文件哪一行
- i fifif 语句在不用时,要把他注释掉,因为让他放在那会占用内存空间
当然 a s s e r tassertassert 断言也有缺点:因为引用了额外的检查,增加了程序运行时间。
同时, a s s e r tassertassert 断言一般在 d e b u gdebugdebug 版本中使用,在 r e l e a s ereleaserelease 版本我们将它禁用就行,因为在 r e l e a s ereleaserelease 版本 a s s e r tassertassert 断言会直接被优化掉。
四、传值调用与传址调用
4.1、传值调用
在之前的学习中,我们知道,函数传参中,形参仅仅是实参的一份临时拷贝
,对形参的修改并不会改变实参的值(详情请看【C语言】——详解函数)。这种函数调用方式叫做传值调用。
但如果我们想封装一个函数,让他交换两个变量的值,该怎么实现呢?这时我们就需要传址调用来实现,让我们一起来看看。
4.2、传址调用
当我们函数传参时,将变量的地址传给函数,这种函数的调用方式叫做:传址调用。
我们来看下面代码:
#includevoid Swap(int* px, int* py){int tmp = 0;tmp = *px;*px = *py;*py = tmp;}int main(){int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交换前: a=%d b=%d\n", a, b);Swap(&a, &b);printf("交换后: a=%d b=%d\n", a, b);return 0;}
运行结果:
我们可以看到,在 m a i nmainmain 函数中将 aaa 和 bbb 的地址传给了 s w a pswapswap 函数, s w a pswapswap 函数通过地址,简介访问两个变量,实现了两个变量间的交换
结论:
- 当我们调用函数,
只需要主函数中变量的值时
,可以使用传值调用- 当我们需要
函数内部修改主函数中的值
,就需要传址调用
五、二级指针
在讲解二级指针之前,我们先来理清一级指针变量 ( p )(p)(p)的三种关系:
- pp p 中可以放 aa a 的地址
- pp p 可以通过解引用找到 aa a
- pp p 自己本身也有一个地址
如图:
那什么又是二级指针呢?
二级指针就是存放指针变量地址的变量
#includeint main(){int a = 10;int* pa = &a;int** ppa = &pa;return 0;}
图示:
在上述代码中, p p appappa 就是二级指针变量,变量类型 i n tintint**,怎么来理解呢?
- 前面 intint int* 表示他所
指向
的变量为指针变量,- 第二颗 ∗* ∗ 表示他
是
一个指针变量
对于二级指针的运算有:
- * p p appappa 通过对 p p appappa 中的地址进行解引用,这样找的是 p apapa, * p p appappa 其实访问的就是 p apapa
int b = 20;*ppa = &b;//等价于 pa = &b;
- ** p p appappa 先通过 * p p appappa 找到 p apapa,然后对 p apapa 进行解引用从操作: * p apapa,找到的是 aaa
**ppa = 30;//等价于 *pa = 30;//等价于 a = 30;
六、指针数组
6.1、指针数组的概念
首先,我先来问一个问题,指针数组是指针还是数组?
我们学习新知识的时候,可以用类似的旧知识来类比
:整形数组是整形变量还是数组?答案很明显嘛,就是数组,同理,指针数组也是数组
。
就像整形数组存放的元素类型是整形,指针数组存放的每个元素为指针
。
指针数组的每个元素是地址,又可以指向一块区域。
6.2、指针数组模拟二维数组
#includeint main(){int arr1[] = { 1,2,3,4,5 };int arr2[] = { 2,3,4,5,6 };int arr3[] = { 3,4,5,6,7 };int* parr[3] = { &arr1[0],&arr2[0],&arr3[0] };int i = 0;int j = 0;for (i = 0; i < 3; i++){for (j = 0; j < 5; j++){printf("%d ", parr[i][j]);}printf("\n");}}
运行结果:
p a r rparrparr [[[ iii ]]] 是访问 p a r rparrparr 数组的元素, p a r rparrparr [[[ iii ]]] 找到的数组元素指向了整型一维数组, p a r rparrparr [[[ iii ]]] [[[ jjj ]]] 就是整形一维数组中的元素。
上述代码模拟出二维数组的效果,实际上并非完全是二维数组,因为二维数组在内存中是连续存储
的,而上述代码每一行的存储可能差了十万八千里。
好啦,本期关于指针就介绍到这里啦,希望本期博客能对你有所帮助,同时,如果有错误的地方请多多指正,让我们在C语言的学习路上一起进步!