第三章 函数 (郑莉版C++程序语言设计学习笔记)

本文是郑莉版C++程序语言设计学习笔记的第三章,详细介绍了函数的定义、调用、内联函数、形参默认值、函数重载以及深入探讨了运行栈和函数调用执行的过程,强调了类型安全在函数声明中的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在面对面向过程的程序设计中,函数是模块划分的基本单位,是对处理问题的抽象
在面向对象的过程中,是对功能呢搞得抽象

3.1函数的定义和使用

主函数是程序执行的开始点。
调用其他函数的函数被称为主调函数。
被其他函数调用的函数被称为被调函数

3.1.1 函数的定义

语法形式
类型说明符 函数名 (含类型说明的形式参数表)
{
语句序列
}
形式参数

type1 name1,type2,name2.。。。,typen namen

main函数的形参是命令行参数,由操作系统进行初始化。

函数在没有调用的时候是静止的,此时的形参只是一个符号,它标志着在这个位置应该出现一个什么类型的数据。当函数被调用的时候,由主调函数实际参数(实参)赋予形参。

3.1.2 函数的调用

函数的调用形式
(略)

  1. 函数调用形式
  2. 嵌套调用
  3. 递归调用(汉诺塔问题)

3.2内联函数

函数调用时可以使用内联函数减少调用的开销。(把代码贴在被调用部分)
内联函数不是在调用时发生控制转移,二十在编译时将函数体嵌入在每一个调用处。节约了参数传递,控制转移等开销。
定义方式
inline 类型说明符 函数名(含类型说明的形参表)
{
语句语序
}
只需加上关键字 inline
inline关键字只是表示一个要求,编译器并不承诺将inline修饰的函数作为内联函数。
在现代编译器中,没有声明为内联函数的函数。通常,应该将简单函数定义为内联函数,结构简单,语句少。如果将复杂函数定义为内联函数,会造成代码膨胀,增大开销,这时编译器会自动将其转换为普通函数来处理。
处理策略有不同编译器不同决定。

3.3带有形参默认值的函数

函数在定义时可以预先声明带有默认的形参值。
调用时如果给出了实参,则使用实参初始换形参,否则,采用预先声明的默认形参值。
例如:

int add(int x = 5, int y =6){ //声明的形参默认值
		renturn x+y;
}

int main(){
add(10,20);//实参初始换形参
add(10);//形参采用实参10,y蚕蛹默认值6
add();//x,y都采用默认值,分别是5,6

}	

有默认值的形参必须在形参列表的最后,即有默认值的在形参表的右面。
int add(int x, int y, int z);//对
int add(int x = 1,int y =5,int z);//错
int add(int x =1,int y ,int z = 6);//错

在相同作用域中,不允许一个函数的多个声明中对同一个参数的默认值重复定义,即使前后定义值相同也不行。

3.4函数重载

两个以上函数,具有同样的函数名,但是形参的个数或者类型不同,编译器根据实参和形参的类型的最佳匹配,自动决定调用那个函数,这就是函数的重载。

如果函数值相同,形参个数和类型也相同,是语法错误。
习惯
不要将不同的功能的函数定义为重载函数。以免出现对调用结果的误解和混淆。

3.5C++系统函数

C++的系统库提供了几百个函数可供程序员使用。
程序员需要使用include指令嵌入头文件。

3.6深度探索

3.6.1 运行栈和函数调用的执行

变量定义写在函数以外的变量叫做全局变量,作用于全局。
写在函数内的叫做局部变量,作用于函数,区别是作用域。
问题:

  1. 局部变量生存周期小于程序的运行周期,如果为每个局部变量分配内存空间,空间利用率会下降。
  2. 发生递归调用时,存在当一个函数尚未返回,对它的另一个调用又发生的情况,对于多次调用,相同,名称的变量会有不同的值,这些值又必须同时保存在内存中, 而且不能互相影响,因此他们必须有不同的内存地址,但又不能分配唯一确定的地址。

所以,需要一种特殊的结构,就是栈。

定义(略)
一组嵌套的函数调用的特点是,越早开始调用,返回的越晚。其形参和局部变量生效的越早,失效的越晚,自然可以用栈结构来储存,这种栈叫做运行栈

  1. 运行栈实际上是一段区域的内存空间,与储存全局的区别只是,寻址方式的差别。
  2. 运行栈中的数据分为一个一个栈帧,每个栈帧对应一个函数调用,栈帧中包含这次形参值,一些控制信息和一些临时数据。
函数调用的执行过程

IA-32(i386)中,esp寄存器就是用来记录栈顶地址的,称为栈指针
但是,只有一个寄存器储存栈顶地址,还不够用,因为有些函数的栈帧大小是不确定的,这就会在函数返回前恢复栈指针时遇到麻烦,因此还需要另一个寄存器保存函数刚被调用时栈指针的位置。
IA-32中,ebp寄存器来完成这一任务,称为帧指针
形参和局部变量相对于帧指针的位置是确定的,函数的形参和局部变量的地址尝尝通过帧指针来计算。
举例:

int add(int a,int b){
int c = a+b;
return c;
}

int x =add(a,b);
主函数汇编代码如下:
8048459: movl $ 0x7,0x4(%esp) //write 7 to address (esp+4)
8048461: movl $ 0x5,(%esp) //write 5 to address (esp)
8048468: call  8048434  //call the function in 8048434
804846d:mov %eax,-0x8(%ebp) //write the value of eax to address(ebp-8)
地址位
被调函数汇编代码:
8048434: push %ebp		//将帧指针ebp的值压入运行栈
8048435: mov %esp,%ebp	//将栈指针esp的值赋给帧指针ebp
8048437: sub $0x4,%esp	//栈指针减去4
804843a: mov 0xc(%ebp),%eax //ebp+12地址内的整数载入eax寄存器
804843d: add 0x8(%ebp),%eax //ebp+8地址内的整数与eax寄存器内的原值相加
8048440:mov %eax,-0x4(%ebp) //eax寄存器内的值存入ebp-4地址内
8048443: mov -0x4(%ebp),%eax //将ebp-4地址内的整数装入eax寄存器中。
8048446: leave //恢复函数调用之初esp和ebp的值
8048447: ret //返回主调函数

3.6.2函数声明与类型安全

如果在调用一个函数前必须声明函数原型,就能够避免向一个函数传递数量不正确或类型不正确的的参数。因为在提供声明的情况下,执行函数调用,若传递的参数数量和类型不正确,编译器很容易检查出来。
进一步原理
不同类型的数据,在内存中都以二进制序列表示,在运行时并没有保存类型信息。有关类型的特性,全部蕴含在数据所执行的操作之中。所以,在使用变量前必须声明,这样才可以为某比那里那个所参与的每一个操作赋予完整的意义。函数在使用前必须声明,也是类似的原因。
一个函数的原型信息(参数个数,类型和返回值类型),并没有写在编译后的机器语言中,二十全部蕴含在了这个函数所执行的操作之中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值