当前课程知识点:基于Linux的C++ > 第三讲 函数 > 3.3 函数调用栈框架 > LinuxCPP0303
接下来的主题就是函数调用规范
包括两个知识点
一个就是参数传递机制
一个是函数调用的栈框架
C++的参数传递机制有两种
一种称为值传递
一种称为引用传递
引用传递
我们会在讲指针和引用的时候才讨论
现在我们只讨论其中的一个传递机制
值传递机制
通过一个函数调用的例子
给大家演示值传递是如何工作的
看前面那个让用户输入两个整数求和
要求把这些代码尽可能抽取出来
写成函数
我们来观察函数在调用的时候
参数是如何进行传递的
首先分析main函数的主框架
要先从整体 然后再到局部
按照这个逻辑进行思考
俗称自顶向下 逐步求精
按照这样的一个方式去思考
程序逻辑是清晰的
如果一开始就特别关注细节
那么就会导致一个什么问题呢
叫一叶障目 不见森林
我们前面特别谈到过
应该是先从战略高度
然后再从战术角度来实现我们的代码
现在我们就讨论main函数应该怎么实现
对于我们这道题来讲
其实涉及到两个主要的工作
一个要拿到这样的两个数 二要求和
当然实际上还有另外两个工作
一要能够把求和之后的结果输出
更重要的是每一个程序其实都应该具有的
而大部分初学者非常容易忽略的
就是在程序启动的最初
应该给出整个程序功能性的简单说明
应该给出一个提示信息
让使用这个程序的程序员
或者那个用户知道这个程序的基本功能
所以我们要把整个程序的功能代码
都抽取成一个又一个的函数
这里面有一个Welcome函数
来输出程序的功能性的描述
有一个GetInteger函数 会得到一个整数
函数会被调用两次
第三个就是有一个Add函数来求和
第四个要把结果输出
我有意没有把这个函数抽取出来
同学们在课后可以自己尝试着
修改把最后一条输出
也抽取成单一的函数
Welcome的实现很简单
就是一条cout语句
输出提示信息
然后是GetInteger这个函数的实现
输出一个提示信息 输入这个数据
然后把这个结果返回
传过来那个参数索引
其实就是要输入的是第几个数
当这个程序真正的开始调用的时候
main函数将会调用GetInteger
得到一个整数
然后再调用一次GetInteger
得到第二个整数
然后调用Add 去完成我们的加法
在调用Add过程中
它会将原来得到的两个整数
作为参数传递进去
同学们一定要记住
它将按照一个特定的原则来进行参数传递
这个原则很复杂 包括好几条
我们一条一条地解释
同学们一定要记住
要有一个非常深刻的认识
第一个 形式函数
在函数调用的时候
才会分配存储空间
并接受实际参数的值
没有调用 形式参数是没有存储空间的
这是第一条
第二条 形式函数可以是复杂的表达式
不管它多复杂 没关系
编译器能够保证这样的表达式
不管它有多复杂
都会在这个函数调用前完成计算
第三个 形式参数和实际参数可以同名
也可以不同名 这个没关系
第四个 如果参数很多
实际参数逐一赋值
所以形式参数和实际参数
必须保证数目、类型、顺序一致
颠倒了 它就不能够吻合
赋值不兼容 这个编译器就会出错
第五个
值的复制过程是单向不可逆的过程
函数内部对形式参数的修改
不会影响实际参数的值
什么叫单向不可逆
就体现在这一点上
单向就是这个赋值的过程
一定是从实际参数向形式参数赋值
不可逆就是不能倒过来
你不可以把这个值
再从形式参数 赋值到实际参数里面去
这个赋值就是一个单向的动作
而且就是一个一次的单向动作
就在函数调用的那个时候
发生那么一次
这个事情一做完 形式参数和实际参数
这两者之间的关系就割裂了
在函数内部 对形式参数所做的任何改变
都不会影响实际参数的值
同学们一定要记住这一条
函数调用过程中 就像我刚才讲的
它一定要和外界进行信息交互
那么从外界接收的全部信息
构成了一个集合
我们称之为函数的输入集
函数向外界输出的信息构成的集合
我们就称它为输出集
对于函数来讲 它的形式参数
往往是它的输入集的一部分
它的返回值
就是它的输出集的一部分
现在按照目前对整个函数的理解来讲
这样的一个论断是正确的
后边其实也会看到
在讲指针和引用
作为函数参数的时候
它也可以带回来结果
也就是说
它也可以作为函数的输出集的一部分
通过函数调用栈框架
来跟踪函数调用的时候它的值传递的规则
这里面画了一个框图
有三个量a、b、sum
它们都是从属于main函数的
所以我们外面贴了一个标签
表示它从属于main函数的特征
这个就称它为函数的框架
这是在调用GetInteger之前
整个函数调用框架
a和b都还没有拿到数据呢
接下来会调用GetInteger这个函数
它当然是有两个量
一个是idx 一个是t
一个是它的形式参数
一个是内部定义的临时量
这是GetInteger这个函数的栈框架
事实上在进行函数调用的过程中
main函数会调用GetInteger这个函数
main函数是并没有结束的
也就是main函数那个栈框架
在计算机内部依然存在
但是又启动了GetInteger这个函数的栈框架
因为计算机执行程序的基本规则
导致了在整个计算机的内部
实际上是有两个函数栈框架的
那个时候
GetInteger其实是覆盖在main函数的上边的
能够看到的就只有GetInteger函数的栈框架
main被遮挡了 看到的就是GetInteger
假设t赋值了 最后变成10
其实就是用户输入了10
然后t就得到了这个10
这个函数一结束
这个栈框架就会跟着消失
它会把这个返回值传给a
10就会存到a的区域里面去
第二次调用GetInteger
要输入第二个数 我们输出20
t得到20
当GetInteger第二次调用
也结束了以后 b将得到20
这就是函数栈框架不断的变化
现在做加法了 是不是接下来
也要调用一遍Add这个函数
那Add这个函数的栈框架
是不是一样要覆盖main函数
Add这个函数的栈框架出现了之后
x得到了10 从哪来的
当然从main函数的a传给它的
y得到了20
当然是main函数b值传给它的
那么 x和y就是形式参数
main函数的a和b就是实际参数
a传给x
b传给y
这是一个单向的 不可逆的拷贝过程
传递完了以后
Add函数就得到了10和20
那就和main函数的a和b没关系了
在Add函数内部不管你怎么倒腾x和y
它都不会影响main函数的a和b的
一加法 t结果是30
返回给main函数
sum得到了这样的一个数据
这个就是栈框架的调整
我们现在演示另外一个例子
看看我们的栈框架会发生什么事情
现在重新的看我们的代码
main函数的实现
跟刚才那个函数的实现非常相似
首先是Welcome
然后是GetInteger、GetInteger得到两个数
然后是Swap交换 return
但实际上 为了跟踪整个程序的运行
在这里边封装了额外的几条cout语句
来输出验证性的数据
注意看三个函数原型
有一个Welcome 有一个Swap 有一个GetInteger
把那三个函数都写在main函数的前边
其实也是可以的
但实际上这种实现的策略是不好的
同学们你仔细想一想
我写这个程序让别人来看
别人看我的代码的时候
怎么样才能够尽快地理解
我这个程序的逻辑呢
当然是先从顶层开始看起
先理解了大纲 才能一章一章地去看
先有章 才会有节嘛
所以应该先把程序的主逻辑写在前边
这样的话他理解程序代码才方便
而把具体的函数内部的实现写在后边
但问题是
如果你真的把这三个函数写在后面的话
如果没有函数原型
写在main函数的前边
这个程序是没有办法编译通过的
这个主要是因为C++
编译器分析源代码编辑程序的时候
它不会倒过头来看第二遍
早期的编译器还会做两趟编译
有的甚至会做三趟编译
现在的编译器只做一遍
为了节省编译的时间
一个函数永远不能够调用
在它后边声明和定义的函数
因为找不到那个函数的入口地址
它就没有办法
把函数调用的那条语句
翻译成对应的指令
函数定义也好 函数原型也好
只要两者其中有一个
写在了函数调用的前边
这个编译就是OK的
如果两个都不在
或者没有函数原型 只有函数定义
而且很不幸地把函数定义
写在了函数调用的后边
那个函数调用是不OK的
它是找不到那个函数的入口地址的
回到Swap整数互换的这个例子
看看程序的代码
Welcome的实现 GetInteger的实现
和刚才没什么差别
Swap多的地方就是什么
又多了两条cout语句
在三步互换之前 输出x、y的值
然后在三步互换之后又打印了一遍x、y的值
所以为了和main里边的两条cout语句相区分
那么这里边就写了一个“In Swap”
(如此一来)就有四条cout语句会输出结果
你就能够根据这四条中间结果
跟踪我们的程序
有没有正确地运行下去
然后来跟踪互换栈框架
一开始只有main函数
a是10
b是20
GetInteger得到了这两个数
我们Swap
x得到了10
y得到了20
同学们特别要记住我刚才讲的
实际参数和形式参数的那个值传递
是单向不可逆的
它是一个值拷贝
x得到是10 没错 从a拿到的
y得到了20 没错 是从b拿过来的
但是 拿过来以后
x和y本身是和a、b没关系的
那么就意味着在Swap这个函数内部
对x和y 不管你怎么颠倒它们的数据
压根就不影响main函数里边的
a、b两个量的值
我们继续来看 三步互换一做完
x就得到了20
y就得到了10
t是临时量 那就是10
不管你怎么倒腾 它不影响a、b
整个Swap函数的栈框架
都已经把main函数的栈框架完全覆盖掉了
实际上这个函数的内部
压根就不知道a和b在什么地方
怎么能够修改它呢
除非你后边用指针和引用
那是我们后边讲到的
在目前技术条件下面
你压根就改变不了a、b的值的
所以你看Swap函数一做完
a该是10还是10
b该是20还是20
所以这是一个非常重要的地方
程序写出来了
看上去蛮正规的样子
但事实上根本就没有解决问题
我们就来第二版做一些修改
让这个程序能够得到正确的结果
解决方案 有一个很重要的知识点
就是a和b这样的两个量
如果定义在main函数里面
那么就是main函数所专有的
如果你把它定义在Swap那个函数里边
它就是Swap函数专有的
其他函数是看不见的
这样的量我们在下下一讲里边就会讨论
它其实称为局部变量
既然是局部变量
不可以被其他函数所共享
那我们就想一个招
就是让这两个量被所有的函数所共享
那么这样被共享的量叫什么呢
我们就称之为全局量
我们怎么定义这样的全局变量呢
把这样的量定义在所有的函数之外
就这么写int a, b;
写在#include、using namespace std的后边
所有的函数原型的前边
这就意味着a和b这样的全局量
将从它的定义处开始
到这个文件的结尾
中间这些代码中所有的函数
都能够使用这两个量a和b
将被后面所有的函数所共享
Welcome 可以使用这两个a、b
GetInteger可以使用
Swap可以使用 main可以使用
这些函数都可以使用这两个量 a和b
这叫全局量 被它们所共享
Swap就用这个a、b
main就用这个a、b
因为这个量是被这两个函数共享的
所以函数调用的时候我连参数都不用传
所以Swap函数形式参数列表空了
直接访问a、b就行了
在main函数实现的时候GetInteger两次调用
结果一个给a 一个给b
就直接访问两个全局量
调用Swap的时候
它就可以直接访问这个全局量a和b
把a给t 把b给a 把t再给b
这个典型的三步互换就把它做完了
因为全局量已经被main和Swap所共享了
在main函数里边
局部变量的定义都不需要了
这是最重要的一个地方
通过这种方式
我们保证了Swap对a和b两个量的修改
在main函数里边立即就能够看到
这个整数互换的第二版
解决了我们的问题吗 解决了
解决方案不太好
为啥不好呢 它有一个很重要的地方
把这样的两个量定义成了全局量
这就是我刚才讲的
后面定义的所有的函数都能够共享它
那这个GetInteger当然需要用它
要把值赋值给它
Swap要用它 main要用它
但是我们的Welcome需要用它吗 不需要
但是你能够限制Welcome不用它吗
你限制不住
一旦你把它定义成了全局量
后面的所有函数都自动能够看到它
就能够用它 你没有办法限制它
就有可能给我们程序的正确性带来威胁
也就是说 使用过多的全局变量
是一个不好的设计方式
有的时候必须使用全局量
这没办法
为了整个编程的效率和方便性
但是如果你的全局量过多
那显然是一个不好的方案
后边会通过指针和引用两种方式
重新修改整数互换的例子
然后可以看到
在不使用全局量的情况下
它一样能够完成整数的互换
-1.1 提纲
-1.2 程序设计的基本概念
-1.3 简单C/C++程序介绍
-1.4 程序设计的基本流程
-1.5 基本语法元素
-1.6 程序设计风格
-1.7 编程实践
-第一讲 C/C++基本语法元素--编程实践提交入口
-2.1 提纲
-2.2 结构化程序设计基础
-2.3 布尔数据
-2.4 分支结构
-2.5 break语句
-2.6 循环结构
-2.7 编程实践
-第二讲 程序控制结构--编程实践提交入口
-3.1 提纲
-3.2 函数声明、调用与定义
-3.3 函数调用栈框架
-3.4 编程实践
-第三讲 函数--编程实践提交入口
-4.1 提纲
-4.2 算法概念与特征
-4.3 算法描述
-4.4 算法设计与实现
-4.5 递归算法(一)
-4.6 递归算法(二)
-4.7 容错与计算复杂度
-4.8 编程实践
-第四讲 算法--编程实践提交入口
-5.1 提纲
-5.2 库与接口
-5.3 随机数库(一)
-5.4 随机数库(二)
-5.5 作用域与生存期
-5.6 典型软件开发流程(一)
-5.7 典型软件开发流程(二)
-5.8 编程实践
-第五讲 程序组织与开发方法--编程实践提交入口
-6.1 提纲
-6.2 字符
-6.3 数组(一)
-6.4 数组(二)
-6.5 结构体
-6.6 编程实践
-第六讲 复合数据类型--编程实践提交入口
-7.1 提纲
-7.2 指针基本概念
-7.3 指针与函数
-7.4 指针与复合数据类型(一)
-7.5 指针与复合数据类型(二)
-7.6 字符串
-7.7 动态存储管理(一)
-7.8 动态存储管理(二)
-7.9 引用
-7.10 编程实践
-第七讲 指针与引用--编程实践提交入口
-8.1 提纲
-8.2 数据抽象(一)
-8.3 数据抽象(二)
-8.4 链表(一)
-8.5 链表(二)
-8.6 链表(三)
-8.7 链表(四)
-8.8 函数指针(一)
-8.9 函数指针(二)
-8.10 抽象链表(一)
-8.11 抽象链表(二)
-8.12 编程实践
-第八讲 链表与程序抽象--编程实践提交入口
-9.1 提纲
-9.2 程序抽象与面向对象
-9.3 类类型
-9.4 对象(一)
-9.5 对象(二)
-9.6 类与对象的成员(一)
-9.7 类与对象的成员(二)
-9.8 类与对象的成员(三)
-9.9 继承(一)
-9.10 继承(二)
-9.11 继承(三)
-9.12 多态(一)
-9.13 多态(二)
-9.14 编程实践
-第九讲 类与对象--编程实践提交入口
-10.1 提纲
-10.2 四则运算符重载(一)
-10.3 四则运算符重载(二)
-10.4 关系与下标操作符重载
-10.5 赋值操作符重载(一)
-10.6 赋值操作符重载(二)
-10.7 赋值操作符重载(三)
-10.8 赋值操作符重载(四)
-10.9 赋值操作符重载(五)
-10.10 流操作符重载(一)
-10.11 流操作符重载(二)
-10.12 流操作符重载(三)
-10.13 操作符重载总结
-10.14 编程实践
-第十讲 操作符重载--编程实践提交入口
-11.1 提纲
-11.2 泛型编程概览
-11.3 异常处理机制(一)
-11.4 异常处理机制(二)
-11.5 运行期型式信息(一)
-11.6 运行期型式信息(二)
-11.7 模板与型式参数化
-11.8 题外话:术语翻译
-11.9 泛型编程实践(一)
-11.10 泛型编程实践(二)
-11.11 泛型编程实践(三)
-11.12 泛型编程实践(四)
-11.13 泛型编程实践(五)
-11.14 泛型编程实践(六)
-11.15 泛型编程实践(七)
-11.16 泛型编程实践(八)
-11.17 泛型编程实践(九)
-11.18 泛型编程实践(十)
-11.19 编程实践
-第十一讲 泛型编程--编程实践提交入口
-12.1 提纲
-12.2 程序执行环境(一)
-12.3 程序执行环境(二)
-12.4 程序执行环境(三)
-12.5 程序执行环境(四)
-12.6 输入输出(一)
-12.7 输入输出(二)
-12.8 文件系统
-12.9 设备
-12.10 库(一)
-12.11 库(二)
-12.12 makefile文件(一)
-12.13 makefile文件(二)
-12.14 makefile文件(三)
-12.15 编程实践
-第十二讲 Linux系统编程基础--编程实践提交入口
-13.01 提纲
-13.02 进程基本概念
-13.03 信号
-13.04 进程管理(一)
-13.05 进程管理(二)
-13.06 进程管理(三)
-13.07 进程间通信(一)
-13.08 进程间通信(二)
-13.09 进程间通信(三)
-13.10 进程间通信(四)
-13.11 进程池
-13.12 编程实践
-第十三讲 进程编程--编程实践提交入口
-14.1 提纲
-14.2 线程基本概念
-14.3 线程管理(一)
-14.4 线程管理(二)
-14.5 线程管理(三)
-14.6 线程管理(四)
-14.7 线程同步机制(一)
-14.8 线程同步机制(二)
-14.9 C++11线程库(一)
-14.10 C++11线程库(二)
-14.11 C++11线程库(三)
-14.12 C++11线程库(四)
-14.13 C++11线程库(五)
-14.14 编程实践
-第十四讲 线程编程--编程实践提交入口
-15.1 提纲
-15.2 Internet网络协议
-15.3 套接字(一)
-15.4 套接字(二)
-15.5 编程实践
-第十五讲 网络编程--编程实践提交入口