第07讲:函数初步

《计算机程序设计》

苏醒

计算机学院计算机研究所编译系统研究室

课前准备

  • 在WSL Ubuntu中建立一个专门的目录用于课上练习,如~/course
$ mkdir ~/course
  • 打开VSCode并连接到WSL,打开此目录并新建文件,如~/course/main.cpp
  • 编辑main.cpp,随堂编程练习的代码请直接在此文件中编辑
main.cpp
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
  // ============= begin =============

  // =============  end  =============
  return 0;
}

温故

考考你

使用IO操纵子,输出主战坦克信息表的表头与前两行数据

  • 三列的列宽分别为6、10、20,对齐方式分别为左、左、右
  • 浮点数使用科学记数法,输出小数点后两位数字,指数基底大写


|------|----------|--------------------|
|ID    |Name      |              Weight|
|------|----------|--------------------|
|1     |99A       |              5.50E1|
|2     |15        |              3.50E1|
打印主战坦克信息表
#include <iomanip>
#include <iostream>
using namespace std;

int main() {
  cout << setfill('-') << right << '|' << setw(7) << '|' << setw(11) << '|' << setw(21) << '|' << endl;
  cout << setfill(' ') << '|' << left << setw(6) << "ID" << '|' << left << setw(10) << "Name" << '|'
       << right << setw(20) << "Weight" << '|' << endl;
  cout << setfill('-') << right << '|' << setw(7) << '|' << setw(11) << '|' << setw(21) << '|' << endl;
  // ============= begin =============

  // =============  end  =============
  return 0;
}

学习目标

  • 学习内容
    • 函数定义
    • 函数调用
    • 变量的作用域与存储类别

学习目标

  • 能够定义自己的函数
  • 理解函数调用的过程,掌握传值传引用方式传递参数
  • 根据应用场景选择变量的作用域存储类别

函数定义

C++函数定义

  • 函数是模块化编程的基本单元,在给定输入的前提下,产生相应输出
函数定义示例
返回类型 函数名(形式参数列表)
  函数体
函数定义语法
double circleArea(double r) {
  const double PI = 3.1415926;
  if (r <= 0)
    return 0;
  return PI * r * r;
}
  • 函数名(circleArea):函数的标识符,用于调用函数
  • 返回值类型(double):说明函数返回值的类型,可以是void,表示无返回值
  • 形式参数列表(double r):说明函数接受的参数,包括参数类型与参数名
  • 函数体:函数定义的主体,形式为复合语句

返回类型与返回值

  • 返回类型声明了是函数返回给调用者的结果类型,是一个类型
    • 若函数无返回值,返回类型为void
  • 返回值函数实际传递给调用者的值,是一个表达式
    • 返回值须匹配返回类型
  • 使用return语句传递返回值
return语句
int add(int a, int b) {
  int c = a + b;
  return c;
}
  • 返回类型为voidreturn语句可省略
void返回类型
void printHello() {
  cout << "Hello, World!" << endl;
  return; // 可以省略
}

返回值与返回类型不匹配

考考你

若返回值与返回类型不匹配,会发生什么?

非void函数无返回值
int add(int a, int b) {
  double c = a + b;
  return;
}
g++ -DERR1 -c code/return-type-errors.cpp
code/return-type-errors.cpp: In function ‘int add(int, int)’:
code/return-type-errors.cpp:8:3: error: return-statement with no value, in function returning ‘int’ [-fpermissive]
    8 |   return;
      |   ^~~~~~
void函数有返回值
void printHello() {
  cout << "Hello, World!" << endl;
  return 0;
}
g++ -DERR2 -c code/return-type-errors.cpp
code/return-type-errors.cpp: In function ‘void printHello()’:
code/return-type-errors.cpp:17:10: error: return-statement with a value, in function returning ‘void’ [-fpermissive]
   17 |   return 0;
      |          ^

避坑

void函数无返回值或void函数有返回值,编译器会报错

返回值与返回类型不匹配

考考你

若返回值与返回类型不匹配,会发生什么?

返回值可转换为返回类型
int add(int a, int b) {
  double c = a + b;
  return c;
}
g++ -DERR3 -c code/return-type-errors.cpp
返回值不可转换为返回类型
char *add(int a, int b) {
  double c = a + b;
  return c;
}
g++ -DERR4 -c code/return-type-errors.cpp
code/return-type-errors.cpp: In function ‘char* add(int, int)’:
code/return-type-errors.cpp:35:10: error: cannot convert ‘double’ to ‘char*’ in return
   35 |   return c;
      |          ^

避坑

编译器会尝试将返回值转换为返回类型,若成功则返回转换后的值,否则报错

return语句缺失

考考你

void函数中,return语句缺失,会发生什么?

return语句缺失
int add(int a, int b) {
  int c = a + b;
  // return c;
}
g++ -DERR5 -c code/return-type-errors.cpp
code/return-type-errors.cpp: In function ‘int add(int, int)’:
code/return-type-errors.cpp:45:1: warning: no return statement in function returning non-void [-Wreturn-type]
   45 | }
      | ^
return语句在部分执行路径缺失
int min(int a, int b) {
  if (a > b)
    return b;
  // return a;
}
g++ -DERR6 -c code/return-type-errors.cpp
code/return-type-errors.cpp: In function ‘int min(int, int)’:
code/return-type-errors.cpp:55:1: warning: control reaches end of non-void function [-Wreturn-type]
   55 | }
      | ^

避坑

void函数中,return语句缺失或在部分执行路径缺失,编译器只会报告警告!但程序运行会有错误!

return语句使用指南

编程指南

  • 对于void函数,也写出return语句,以明确函数结束
  • 对于非void函数,确保所有执行路径都有return语句
  • 避免return表达式类型与返回类型不匹配引入的隐式类型转换
在void函数中使用return语句
void printHello() {
  cout << "Hello, World!" << endl;
  return; // 不要省略
}
避免隐式类型转换
double add(int a, int b) {
  double c = a + b; // 这里隐式转换
  return c;         // 无需再转换
}
所有执行路径都有return语句
int select(int score) {
  switch (score/10) {
  case 10:
  case 9:
    return 5;
  case 8:
  case 7:
    return 4;
  case 6:
  default:
    return 3;
  }
}

形式参数

  • 形式参数声明了调用者传递给函数的参数类型与参数名
  • 函数体内代码可以通过形式参数名访问传递给函数的实际参数值
形式参数示例
double circleArea(double r) {
  const double PI = 3.1415926;
  if (r <= 0)
    return 0;
  return PI * r * r;
}

重审main函数

  • 从语法上看,main函数与其他函数没有区别
  • 从执行上看,main是程序的入口(由操作系统shell调用),程序从这里开始执行
    • 若需要从命令行传递参数,可以通过argcargv两个参数接收
    • 返回值类型为int,表示程序退出状态,0表示正常退出,其他值表示异常退出
main函数示例
#include <iostream>
using namespace std;

int main(int argc, char *argv[]) {
  if (argc == 1) {
    cout << "No arguments provided." << endl;
    return 1;
  }
  for (int i = 0; i < argc; i++)
    cout << "Argument " << i << " is: " << argv[i] << endl;
  return 0;
}
g++ code/maindemo.cpp -o code/maindemo
./code/maindemo
echo $? # 查看上一个命令的返回值
No arguments provided.
1
./code/maindemo 1 2 3
echo $? # 查看上一个命令的返回值
Argument 0 is: ./code/maindemo
Argument 1 is: 1
Argument 2 is: 2
Argument 3 is: 3
0

函数声明

  • 函数声明是函数定义的前置声明,用于告诉编译器函数的存在
  • 函数声明包括函数名、形参列表、返回类型,也称函数原型函数签名函数头
circledemo.cpp
#include <iostream>
double circleArea(double r); // 函数声明
int main() {
  double r = 3.0;
  std::cout << "The area is: " << circleArea(r) << std::endl; // 函数调用
  return 0;
}
double circleArea(double r) { // 函数定义
  const double PI = 3.1415926;
  if (r <= 0) return 0;
  return PI * r * r;
}

Try it!

删掉第一行,编译时会发生什么?

重审编译、链接

  • 观察下面的两个文件,在编译、链接阶段会发生什么?
testcircle.cpp
#include <iostream>
double circleArea(double r);
int main() {
  double r = 3.0;
  std::cout << "The area is: "
            << circleArea(r) << std::endl;
  return 0;
}
circle.cpp
int circleArea(double r) {
  return 3 * (int)r * (int)r;
}
g++ code/testcircle.cpp code/circle.cpp -o code/testcircle
./code/testcircle
The area is: 3

考考你

从这个例子中,你能看出“类型”在编译阶段的作用吗?链接器能否看到类型信息?

函数调用

函数调用过程

  • 函数仅在被调用时执行,执行完毕后返回调用点继续执行
  • 在调用函数时,需要传入实际参数
    • 实际参数是表达式,其值将在函数执行时绑定到形式参数
circledemo.cpp
#include <iostream>
double circleArea(double r); // 函数声明
int main() {
  double r = 3.0;
  std::cout << "The area is: " << circleArea(r) << std::endl; // 函数调用
  return 0;
}
double circleArea(double r) { // 函数定义
  const double PI = 3.1415926;
  if (r <= 0) return 0;
  return PI * r * r;
}

参数传递

考考你

观察下面的程序,输出是什么?

valuearg.cpp
#include <iostream>

double updateme(double x) {
  x = x + 1;
  return x;
}

int main() {
  double x = 0;
  std::cout << "The updated value is: " << updateme(x) << std::endl;
  std::cout << "After the function call, x is: " << x << std::endl;
  return 0;
}
g++ code/valuearg.cpp -o code/valuearg
./code/valuearg
The updated value is: 1
After the function call, x is: 0

传值参数

  • 默认情况下,C++以传值方式传递参数,即实参的值被复制到形参中
  • 传值传递时,形参的值改变不会影响实参的值
valuearg-inspect.cpp
#include <iostream>
double updateme(double x) {
  x = x + 1;
  std::cout << "Parameter x is at memory " << &x << std::endl; // &操作符用于获取变量的地址
  return x;
}
int main() {
  double x = 0;
  updateme(x);
  std::cout << "Argument x is at memory " << &x << std::endl; // &操作符用于获取变量的地址
  return 0;
}
g++ code/valuearg-inspect.cpp -o code/valuearg-inspect
./code/valuearg-inspect
Parameter x is at memory 0x7ffcc54bac08
Argument x is at memory 0x7ffcc54bac20

传引用参数

  • C++支持传引用方式传递参数,可使形参与实参共享内存地址
  • 传引用传递时,形参的值改变会影响实参的值,从而实现向调用者返回多个值
refarg.cpp
#include <iostream>
double updateme(double &x, double &y) { // &符号表示该参数以引用方式传递
  double z = x + y;
  std::cout << "Parameter x/y is at memory " << &x << '/' << &y << std::endl;
  return z;
}
int main() {
  double x = 0, y = 1;
  double z = updateme(x, y);
  std::cout << "Argument x/y is at memory " << &x << '/' << &y << std::endl;
  std::cout << "The updated value is: " << z << std::endl;
  return 0;
}
g++ code/refarg.cpp -o code/refarg
./code/refarg
Parameter x/y is at memory 0x7ffe4b08b470/0x7ffe4b08b478
Argument x/y is at memory 0x7ffe4b08b470/0x7ffe4b08b478
The updated value is: 1

参数传递方式对比

  • 传值
    • 简单、安全
    • 参数体积较大时拷贝开销大
    • 无法返回多个值
  • 传引用
    • 函数的输入/输出变得不透明
    • 传递参数时不复制,直接传递地址
    • 可以返回多个值

明察秋毫

  • 函数参数的传递方式体现在形式参数的类型声明上!
    • 实际上,intint&的区别不仅仅是一个符号,二者是两种不同的类型!

变量的作用域与存储类别

变量的作用域(复习)

  • 作用域:变量能被使用(可见)的代码区域
  • 复合语句中定义的变量只能在复合语句内部使用
    • 复合语句外定义的变量可以在复合语句内部使用
    • 嵌套复合语句中定义的变量会隐藏外部同名变量

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 28px
---

mindmap
Root(变量)
  变量名:标识符
  类型:数据类型
  值:存储的数据
  地址:内存中的位置
  作用域:变量的可见范围
  生存周期:变量的有效时间

变量的六个属性

块作用域
{
  int a = 3, b = 2;     // a, b的作用域:2-12行
  {
    int c = 0, d = 0;   // c, d的作用域:4-8行
    int a = 0;          // a的作用域:5-8行,内层的a隐藏外层的a,
    a += b;             // 外层定义的变量可在内层使用
    cout << a << '\n';  // 2
  }
  cout << a << '\n';    // 3
  cout << c << '\n';    // 编译错误,已超出变量c的作用域
  return 0;
}

块作用域

  • 在复合语句内定义的变量/常量只能在块内使用,这种作用域称为块作用域
  • 函数体是一个复合语句,函数体内定义的变量/常量的作用域也是块作用域
    • 形式参数的作用域为整个函数体
块作用域
void circleinfo() {
  double pi = 3.14159;            // 局部变量,作用域5-12行
  double circumference = 0;       // 局部变量,作用域6-12行
  for (int r = 1; r <= 10; r++) { // 局部变量,作用域7-11行
    double area = pi * r * r;     // 局部变量,作用域8-11行
    std::cout << "The area of a circle with radius " << r << " is " << area
              << std::endl;
  }
}
  • 具有块作用域的变量称为局部变量

文件作用域

  • 文件作用域:变量在整个文件中可见
    • 在函数外部定义的变量/常量具有文件作用域
文件作用域
double pi = 3.14159; // 全局变量,作用域16行至文件尾部
double circleArea(double r) {
  if (r <= 0) return 0;
  return pi * r * r;
}
  • 具有文件作用域的变量/常量称为全局变量/常量

考考你

如果我希望一个变量能被另外一个文件访问,应该怎么做?我们都使用过这样的变量,你知道是哪一个吗?

生存周期

  • 变量的生存周期是指变量在内存中的有效时间,由变量的存储类别决定
  • 存储类别:变量的存储位置
    • 自动存储:局部变量
    • 静态存储:全局变量/静态局部变量
    • 外部存储:外部变量

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 28px
---

mindmap
Root(变量)
  变量名:标识符
  类型:数据类型
  值:存储的数据
  地址:内存中的位置
  作用域:变量的可见范围
  生存周期:变量的有效时间

变量的六个属性

存储类别
extern double E; // 全局变量声明:外部存储
int a = 3;       // 全局变量定义:静态存储
double circleArea(double r) {
  static double pi = 3.1415926; // 静态局部变量定义:静态存储
  double area = pi * r * r;     // 局部变量定义:自动存储
  return area;
}
double distance(double t) {
  extern double lightSpeed; // 局部变量声明:外部存储
  return lightSpeed * t;
}

自动存储

  • 具有块作用域的局部变量具有自动存储类别
    • 生存周期开始于所在语句块执行开始,结束于语句块执行结束
自动存储
{ // a的生存周期从这里开始
  int a = 3;
  cout << a << '\n';
} // a的生存周期到这里结束

明察秋毫

“自动存储”的含义可以简单理解(并不准确):当变量所在的语句块开始执行时分配内存,结束时释放内存

静态存储

  • 静态存储类别的变量在程序执行期间一直存在,即生存周期从同程序执行周期
    • 全局变量静态局部变量具有静态存储类别
    • 静态存储变量默认初始化为0
静态存储
#include <iostream>
void hello() {
  static int count; // 默认初始化为0
  std::cout << "Hello, world! " << count++ << std::endl;
}
int main() {
  for (int i = 0; i < 3; i++)
    hello();
  return 0;
}
$ g++ -o code/staticstorage code/staticstorage.cpp
$ ./code/staticstorage
Hello, world! 0
Hello, world! 1
Hello, world! 2

静态存储

考考你

全局变量和静态局部变量都具有静态存储类别,它们的区别在哪里?

静态局部变量与全局变量
#include <iostream>
bool enableCounter;
int counter() {
  static int count = 0;
  if (enableCounter) return 0;
  return ++count;
}
int main() {
  enableCounter = true;
  std::cout << "Counter: " << count << std::endl;
  return 0;
}
$ g++ -o code/staticlocal-global code/staticlocal-global.cpp
code/staticlocal-global.cpp: In function ‘int main()’:
code/staticlocal-global.cpp:10:31: error: ‘count’ was not declared in this scope; did you mean ‘counter’?
   10 |   std::cout << "Counter: " << count << std::endl;
      |                               ^~~~~
      |                               counter

外部存储

  • 外部存储类别用于声明外部变量,使用extern关键字实现
    • 外部变量具有外部存储类别
外部存储
extern double PI; // 声明外部变量PI
double area(double r) {
  return PI * r * r;
}

明察秋毫

  • 外部存储的核心要义是声明但不定义,即只声明此变量存在于他处,而无需在此处分配内存
  • 外部变量必须定义在某处,否则会引起链接错误
  • 外部变量的生存周期由其在他处的定义决定

引用其他文件中的变量/常量

externdemo.cpp
#include <iostream>
using namespace std;
extern double PI; // 声明变量
int main(int argc, char *argv[]) {
  cout << "The value of PI is: " << PI << endl;
  return 0;
}
mathconstants.cpp
double PI = 3.14159265358979323846;
double E = 2.71828182845904523536;
double lightSpeed = 299792458.0;
double gravity = 9.80665;
double planck = 6.62607015e-34;
double boltzmann = 1.380649e-23;
$ g++ -c -o code/externdemo code/externdemo.cpp code/mathconstants.cpp
$ ./code/externdemo
The value of PI is: 3.14159

Try it!

  • 在externdemo.cpp第4行,如果不使用extern关键字,会发生什么?
  • 在mathconstants.cpp中,如果给变量使用extern关键字,会发生什么?
  • 在mathconstants.cpp中,如果给变量使用static关键字,会发生什么?

静态全局变量

  • static关键字用于局部变量时,会使变量具有静态存储类别
  • static关键字用于全局变量时,会使全局变量无法被其他文件访问
    • 无论是否使用static关键字,全局变量都具有静态存储类别
externdemo.cpp
#include <iostream>
using namespace std;
extern double PI; // 声明变量
int main(int argc, char *argv[]) {
  cout << "The value of PI is: " << PI << endl;
  return 0;
}
staticglobal.cpp
// 静态全局变量,仅能在本文件中访问,其他文件无法通过extern进行访问
static double PI = 3.14159265358979323846;
static double E = 2.71828182845904523536;
static double lightSpeed = 299792458.0;
static double gravity = 9.80665;
static double planck = 6.62607015e-34;
static double boltzmann = 1.380649e-23;
$ g++ -c -o code/externdemo1 code/externdemo.cpp code/staticglobal.cpp
/usr/bin/ld: /tmp/cc1cFyy2.o: warning: relocation against `PI' in read-only section `.text'
/usr/bin/ld: /tmp/cc1cFyy2.o: in function `main':
externdemo.cpp:(.text+0x32): undefined reference to `PI'
/usr/bin/ld: warning: creating DT_TEXTREL in a PIE
collect2: error: ld returned 1 exit status

作用域与存储类别小结

作用域与存储类别示例
extern int lightSpeed;          // 外部变量,文件作用域
static double PI = 3.1415926;   // 静态全局变量,文件作用域(不可被其他文件访问)
bool enableCounter = true;      // 全局变量,文件作用域(可被其他文件访问)
double circleArea(double r) {
  extern double E;              // 外部变量,块作用域
  static int counter = 0;       // 静态局部变量,块作用域
  double area = PI * r * r;     // 自动局部变量,块作用域
  return area;
}
作用域与存储类别
分类 存储类别 作用域 生存周期
自动局部变量 自动存储 块作用域 代码块执行开始至执行结束
静态局部变量 静态存储 块作用域 程序开始至程序结束
全局变量 静态存储 文件作用域,可被其他文件访问 程序开始至程序结束
外部变量 外部存储 由声明位置决定 由定义位置决定
静态全局变量 静态存储 文件作用域,不可被其他文件访问 程序开始至程序结束

总结

本节内容

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 20px
---
mindmap
  函数初步
    函数定义
      返回类型与返回值
      形式参数
      函数声明
    函数调用
      函数调用过程
      传值参数
      传引用参数
    作用域
      块作用域
      文件作用域
    存储类别
      自动存储
      静态存储
      外部存储
      static/extern关键字

学习目标

  • 能够定义自己的函数
  • 理解函数调用的过程,掌握传值传引用方式传递参数
  • 根据应用场景选择变量的作用域存储类别

课后作业

  • 实训(截止时间2025.03.25
    • 综合练习—C&C++函数(第5关不需要使用递归)
  • 预习
    • 教材6.1:一维数组

计算机程序设计