当前课程知识点:基于Linux的C++ >  第三讲 函数 >  3.3 函数调用栈框架 >  LinuxCPP0303

返回《基于Linux的C++》慕课在线视频课程列表

LinuxCPP0303在线视频

LinuxCPP0303

下一节:LinuxCPP0304

返回《基于Linux的C++》慕课在线视频列表

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不用它吗

你限制不住

一旦你把它定义成了全局量

后面的所有函数都自动能够看到它

就能够用它 你没有办法限制它

就有可能给我们程序的正确性带来威胁

也就是说 使用过多的全局变量

是一个不好的设计方式

有的时候必须使用全局量

这没办法

为了整个编程的效率和方便性

但是如果你的全局量过多

那显然是一个不好的方案

后边会通过指针和引用两种方式

重新修改整数互换的例子

然后可以看到

在不使用全局量的情况下

它一样能够完成整数的互换

基于Linux的C++课程列表:

第一讲 C/C++基本语法元素

-1.1 提纲

--LinuxCPP0101

-1.2 程序设计的基本概念

--LinuxCPP0102

-1.3 简单C/C++程序介绍

--LinuxCPP0103

-1.4 程序设计的基本流程

--LinuxCPP0104

-1.5 基本语法元素

--LinuxCPP0105

-1.6 程序设计风格

--LinuxCPP0106

-1.7 编程实践

--LinuxCPP0107

-第一讲 C/C++基本语法元素--编程实践提交入口

第二讲 程序控制结构

-2.1 提纲

--LinuxCPP0201

-2.2 结构化程序设计基础

--LinuxCPP0202

-2.3 布尔数据

--LinuxCPP0203

-2.4 分支结构

--LinuxCPP0204

-2.5 break语句

--LinuxCPP0205

-2.6 循环结构

--LinuxCPP0206

-2.7 编程实践

--LinuxCPP0207

-第二讲 程序控制结构--编程实践提交入口

第三讲 函数

-3.1 提纲

--LinuxCPP0301

-3.2 函数声明、调用与定义

--LinuxCPP0302

-3.3 函数调用栈框架

--LinuxCPP0303

-3.4 编程实践

--LinuxCPP0304

-第三讲 函数--编程实践提交入口

第四讲 算法

-4.1 提纲

--LinuxCPP0401

-4.2 算法概念与特征

--LinuxCPP0402

-4.3 算法描述

--LinuxCPP0403

-4.4 算法设计与实现

--LinuxCPP0404

-4.5 递归算法(一)

--LinuxCPP0405

-4.6 递归算法(二)

--LinuxCPP0406

-4.7 容错与计算复杂度

--LinuxCPP0407

-4.8 编程实践

--LinuxCPP0408

-第四讲 算法--编程实践提交入口

第五讲 程序组织与开发方法

-5.1 提纲

--LinuxCPP0501

-5.2 库与接口

--LinuxCPP0502

-5.3 随机数库(一)

--LinuxCPP0503

-5.4 随机数库(二)

--LinuxCPP0504

-5.5 作用域与生存期

--LinuxCPP0505

-5.6 典型软件开发流程(一)

--LinuxCPP0506

-5.7 典型软件开发流程(二)

--LinuxCPP0507

-5.8 编程实践

--LinuxCPP0508

-第五讲 程序组织与开发方法--编程实践提交入口

第六讲 复合数据类型

-6.1 提纲

--LinuxCPP0601

-6.2 字符

--LinuxCPP0602

-6.3 数组(一)

--LinuxCPP0603

-6.4 数组(二)

--LinuxCPP0604

-6.5 结构体

--LinuxCPP0605

-6.6 编程实践

--LinuxCPP0606

-第六讲 复合数据类型--编程实践提交入口

第七讲 指针与引用

-7.1 提纲

--LinuxCPP0701

-7.2 指针基本概念

--LinuxCPP0702

-7.3 指针与函数

--LinuxCPP0703

-7.4 指针与复合数据类型(一)

--LinuxCPP0704

-7.5 指针与复合数据类型(二)

--LinuxCPP0705

-7.6 字符串

--LinuxCPP0706

-7.7 动态存储管理(一)

--LinuxCPP0707

-7.8 动态存储管理(二)

--LinuxCPP0708

-7.9 引用

--LinuxCPP0709

-7.10 编程实践

--LinuxCPP0710

-第七讲 指针与引用--编程实践提交入口

第八讲 链表与程序抽象

-8.1 提纲

--LinuxCPP0801

-8.2 数据抽象(一)

--LinuxCPP0802

-8.3 数据抽象(二)

--LinuxCPP0803

-8.4 链表(一)

--LinuxCPP0804

-8.5 链表(二)

--LinuxCPP0805

-8.6 链表(三)

--LinuxCPP0806

-8.7 链表(四)

--LinuxCPP0807

-8.8 函数指针(一)

--LinuxCPP0808

-8.9 函数指针(二)

--LinuxCPP0809

-8.10 抽象链表(一)

--LinuxCPP0810

-8.11 抽象链表(二)

--LinuxCPP0811

-8.12 编程实践

--LinuxCPP0812

-第八讲 链表与程序抽象--编程实践提交入口

第九讲 类与对象

-9.1 提纲

--LinuxCPP0901

-9.2 程序抽象与面向对象

--LinuxCPP0902

-9.3 类类型

--LinuxCPP0903

-9.4 对象(一)

--LinuxCPP0904

-9.5 对象(二)

--LinuxCPP0905

-9.6 类与对象的成员(一)

--LinuxCPP0906

-9.7 类与对象的成员(二)

--LinuxCPP0907

-9.8 类与对象的成员(三)

--LinuxCPP0908

-9.9 继承(一)

--LinuxCPP0909

-9.10 继承(二)

--LinuxCPP0910

-9.11 继承(三)

--LinuxCPP0911

-9.12 多态(一)

--LinuxCPP0912

-9.13 多态(二)

--LinuxCPP0913

-9.14 编程实践

--LinuxCPP0914

-第九讲 类与对象--编程实践提交入口

第十讲 操作符重载

-10.1 提纲

--LinuxCPP1001

-10.2 四则运算符重载(一)

--LinuxCPP1002

-10.3 四则运算符重载(二)

--LinuxCPP1003

-10.4 关系与下标操作符重载

--LinuxCPP1004

-10.5 赋值操作符重载(一)

--LinuxCPP1005

-10.6 赋值操作符重载(二)

--LinuxCPP1006

-10.7 赋值操作符重载(三)

--LinuxCPP1007

-10.8 赋值操作符重载(四)

--LinuxCPP1008

-10.9 赋值操作符重载(五)

--LinuxCPP1009

-10.10 流操作符重载(一)

--LinuxCPP1010

-10.11 流操作符重载(二)

--LinuxCPP1011

-10.12 流操作符重载(三)

--LinuxCPP1012

-10.13 操作符重载总结

--LinuxCPP1013

-10.14 编程实践

--LinuxCPP1014

-第十讲 操作符重载--编程实践提交入口

第十一讲 泛型编程

-11.1 提纲

--LinuxCPP1101

-11.2 泛型编程概览

--LinuxCPP1102

-11.3 异常处理机制(一)

--LinuxCPP1103

-11.4 异常处理机制(二)

--LinuxCPP1104

-11.5 运行期型式信息(一)

--LinuxCPP1105

-11.6 运行期型式信息(二)

--LinuxCPP1106

-11.7 模板与型式参数化

--LinuxCPP1107

-11.8 题外话:术语翻译

--LinuxCPP1108

-11.9 泛型编程实践(一)

--LinuxCPP1109

-11.10 泛型编程实践(二)

--LinuxCPP1110

-11.11 泛型编程实践(三)

--LinuxCPP1111

-11.12 泛型编程实践(四)

--LinuxCPP1112

-11.13 泛型编程实践(五)

--LinuxCPP1113

-11.14 泛型编程实践(六)

--LinuxCPP1114

-11.15 泛型编程实践(七)

--LinuxCPP1115

-11.16 泛型编程实践(八)

--LinuxCPP1116

-11.17 泛型编程实践(九)

--LinuxCPP1117

-11.18 泛型编程实践(十)

--LinuxCPP1118

-11.19 编程实践

--LinuxCPP1119

-第十一讲 泛型编程--编程实践提交入口

第十二讲 Linux系统编程基础

-12.1 提纲

--LinuxCPP1201

-12.2 程序执行环境(一)

--LinuxCPP1202

-12.3 程序执行环境(二)

--LinuxCPP1203

-12.4 程序执行环境(三)

--LinuxCPP1204

-12.5 程序执行环境(四)

--LinuxCPP1205

-12.6 输入输出(一)

--LinuxCPP1206

-12.7 输入输出(二)

--LinuxCPP1207

-12.8 文件系统

--LinuxCPP1208

-12.9 设备

--LinuxCPP1209

-12.10 库(一)

--LinuxCPP1210

-12.11 库(二)

--LinuxCPP1211

-12.12 makefile文件(一)

--LinuxCPP1212

-12.13 makefile文件(二)

--LinuxCPP1213

-12.14 makefile文件(三)

--LinuxCPP1214

-12.15 编程实践

--LinuxCPP1215

-第十二讲 Linux系统编程基础--编程实践提交入口

第十三讲 进程编程

-13.01 提纲

--LinuxCPP1301

-13.02 进程基本概念

--LinuxCPP1302

-13.03 信号

--LinuxCPP1303

-13.04 进程管理(一)

--LinuxCPP1304

-13.05 进程管理(二)

--LinuxCPP1305

-13.06 进程管理(三)

--LinuxCPP1306

-13.07 进程间通信(一)

--LinuxCPP1307

-13.08 进程间通信(二)

--LinuxCPP1308

-13.09 进程间通信(三)

--LinuxCPP1309

-13.10 进程间通信(四)

--LinuxCPP1310

-13.11 进程池

--LinuxCPP1311

-13.12 编程实践

--LinuxCPP1312

-第十三讲 进程编程--编程实践提交入口

第十四讲 线程编程

-14.1 提纲

--LinuxCPP1401

-14.2 线程基本概念

--LinuxCPP1402

-14.3 线程管理(一)

--LinuxCPP1403

-14.4 线程管理(二)

--LinuxCPP1404

-14.5 线程管理(三)

--LinuxCPP1405

-14.6 线程管理(四)

--LinuxCPP1406

-14.7 线程同步机制(一)

--LinuxCPP1407

-14.8 线程同步机制(二)

--LinuxCPP1408

-14.9 C++11线程库(一)

--LinuxCPP1409

-14.10 C++11线程库(二)

--LinuxCPP1410

-14.11 C++11线程库(三)

--LinuxCPP1411

-14.12 C++11线程库(四)

--LinuxCPP1412

-14.13 C++11线程库(五)

--LinuxCPP1413

-14.14 编程实践

--LinuxCPP1414

-第十四讲 线程编程--编程实践提交入口

第十五讲 网络编程

-15.1 提纲

--LinuxCPP1501

-15.2 Internet网络协议

--LinuxCPP1502

-15.3 套接字(一)

--LinuxCPP1503

-15.4 套接字(二)

--LinuxCPP1504

-15.5 编程实践

--LinuxCPP1505

-第十五讲 网络编程--编程实践提交入口

课程文档

-课程PDF文件

LinuxCPP0303笔记与讨论

也许你还感兴趣的课程:

© 柠檬大学-慕课导航 课程版权归原始院校所有,
本网站仅通过互联网进行慕课课程索引,不提供在线课程学习和视频,请同学们点击报名到课程提供网站进行学习。