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