【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语言中定义的一个标识符常量,本质是 00 也是地址,但该地址位于内核之中,我们用户是无法使用的,读写该地址会报错。

#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语言的学习路上一起进步!