跳转到内容

第五章 函数

  1. 函数的定义格式(返回类型、函数名、形参表、函数体)
  2. 函数的声明与调用机制
  3. 参数传递:值传递机制
  4. 数组作为函数参数的特殊性(形参数组会调整为指针,传递的是首元素地址)
  5. 变量作用域:局部变量与全局变量
  6. 变量存储类别:auto、register、extern、static
  7. 重载函数的定义与匹配规则
  8. 函数模板的定义与使用
  9. 递归函数的设计(递归出口、递归调用)
  10. 默认参数与内联函数

学习目标(必考重点)

跳转到:学习目标(必考重点)

以下内容来自学习目标页面,是本章考核重点

  • 掌握函数的定义、声明、调用 [来源:第5章 p7,学习目标]
  • 理解内存层面工作机制 [来源:第5章 p7,学习目标]
  • 理解变量作用域 [来源:第5章 p7,学习目标]
  • 会读代码,给出执行结果 [来源:第5章 p7,学习目标]
  • 理解重载函数的使用 [来源:第5章 p7,学习目标]
  • 会递归调用函数 [来源:第5章 p7,学习目标]

  • 函数定义:完成某一特定功能的程序模块 [来源:第5章 p9]

  • 函数定义格式:

    返回类型 函数名(形式参数表) {
    变量定义部分
    语句部分
    }

    [来源:第5章 p10,函数定义格式]

  • return语句格式:

    return 表达式; // 或 return (表达式);
    return; // 不带返回值,只能用于void函数

    [来源:第5章 p10]

  • C/C++中不允许在一个函数内部定义另外一个函数(函数不能嵌套定义) [来源:第5章 p10,Notes]

  • 返回类型指出函数返回值的类型,返回值必须是返回类型(或能够自动类型转换成返回类型) [来源:第5章 p10]

  • 函数可以没有返回值,此时返回类型用 void [来源:第5章 p10]

  • return语句可以用在函数内任何需要返回的地方,遇到return语句表示函数执行结束 [来源:第5章 p10,Notes]

  • 无参数、无返回值:

    void printTriangle() {
    // 打印等腰三角形
    }

    [来源:第5章 p11]

  • 有参数、无返回值:

    void printTriangle(int numofline) {
    // 根据输入调整三角形尺寸
    }

    [来源:第5章 p11]

  • 有参数、有返回值(阶乘):

    int fact(int n) {
    if (n < 0) return 0; // 非法输入
    int s = 1;
    for (int i = 1; i <= n; ++i)
    s *= i;
    return s;
    }

    [来源:第5章 p12]

  • 返回表达式结果(判断闰年):

    bool IsLeapYear(int year) {
    return ((year%4==0)&&(year%100!=0)) || (year%400==0);
    }

    [来源:第5章 p13]

  • ⚠️ return 的功能是在不同程序模块之间传递信息,而 cout 的功能是向硬件输出设备传递信息 [来源:第5章 p12,Notes]

  • ⚠️ 阶乘计算注意运算溢出:13! 超出 int 范围 [来源:第5章 p12,Notes]

  • ⚠️ 即使函数有返回结果,func(); 可以独立存在,不用涉及赋值 [来源:第5章 p12,Notes]


  • 函数调用形式:函数名(实际参数表) [来源:第5章 p16]

  • 实际参数:带入本次函数调用的输入。通常要求个数与顺序匹配;类型需满足形参要求(可发生隐式类型转换/提升;若存在重载,由重载决议选择“最佳匹配”)。 [来源:第5章 p16]

  • 参数传递:将实际参数赋值给形式参数的过程 [来源:第5章 p16]

  • 值传递:实参的值被拷贝并赋值给形参变量,函数中对形参的修改不影响实参 [来源:第5章 p16]

  • 函数调用属于后缀表达式(与 []、.、->、后置++/--同级),优先级很高,可以作为表达式的一部分 [来源:第5章 p16]

  • 函数可以嵌套调用(使用过程中,不同函数间传递的是计算好的数值,而不是函数本身) [来源:第5章 p16,Notes]

p(5) + p(4) + p(7) // 计算 5! + 4! + 7!
printstar(p(4)); // 嵌套调用

[来源:第5章 p16]

  • ⚠️ 函数调用中各实参的求值顺序不作保证(不同编译器/优化可能不同;C++17 起各实参彼此“有序但顺序未指定”)。因此应避免在实参中写依赖求值顺序的副作用表达式(例如在不同实参里同时读写同一变量)。 [来源:第5章 p16,Notes]

  • 函数声明格式:返回类型 函数名(形式参数说明); [来源:第5章 p17]

  • 形式参数说明只需指出参数个数和类型,可以不给名字 [来源:第5章 p17]

  • 方法一:定义在调用之前(不需要声明) [来源:第5章 p17]

  • 方法二:函数原型声明(定义在调用之后时使用) [来源:第5章 p17]

  • 函数声明与定义要保持一致:返回类型、函数名、参数个数、类型和顺序必须相同 [来源:第5章 p17]

  • 声明需要分号结尾,定义不用 [来源:第5章 p17,Notes]

// 声明
int max(int a, int b);
// 调用
cout << max(x + 5, y - 3);
// 定义
int max(int a, int b) {
if (a > b) return(a);
else return(b);
}

[来源:第5章 p18]

函数定义与声明的区别
跳转到:函数定义与声明的区别
比较项声明定义
形式类型、函数名、输入类型、函数名、输入、执行内容
功能告诉编译器去寻找该函数的定义告诉计算机该函数要执行什么

[来源:第5章 p19]


5.2.3 函数调用过程

跳转到:5.2.3 函数调用过程

函数调用的执行步骤:

  1. 在调用函数(caller)中计算被调用函数(callee)每个实际参数值
  2. 用实际参数值初始化形式参数
  3. 依次执行函数体的每个语句,直到遇见某个return语句或函数体结束
  4. 计算 return 表达式的值,并将其作为该次函数调用表达式的结果(很多情况下可发生拷贝消除/RVO/NRVO,不一定产生可观察的“临时对象”)
  5. 返回到调用点,用该返回结果继续完成外层表达式/赋值等后续计算

[来源:第5章 p20]

  • ⚠️ 自动存储期的局部变量在函数返回时结束生命周期;返回值由调用点接收(可能经过拷贝消除优化)。切勿返回局部变量的引用/指针。 [来源:第5章 p20,Notes]

  • C++在定义或声明函数时可以为一个或多个形参指定默认值 [来源:第5章 p22]
void print(int value, int base = 10);

[来源:第5章 p22]

  • 有声明,在声明中给出;没有声明,在定义中给出;不能重复定义 [来源:第5章 p22]

  • 缺省参数无论有几个,都必须放在参数列表的最后 [来源:第5章 p22]

  • 在函数调用时,若某个参数省略,则其后的参数皆应省略而取其缺省值 [来源:第5章 p22]

  • 同一函数在不同源文件(不同翻译单元)中的声明,默认参数应保持一致;否则会导致不同调用点使用不同默认值,接口行为不一致(C++20 起对此有更明确的约束)。 [来源:第5章 p23]

// file1.cpp - 默认基数是10
void print(int value=1, int base=10);
// file2.cpp - 默认基数是2
void print(int value=1, int base=2);

[来源:第5章 p23]


  • 内联函数的定义:在函数头部前加保留词 inline [来源:第5章 p24]
inline float cube(float s) {
return s*s*s;
}

[来源:第5章 p24]

  • inline 最初用于建议编译器进行“内联展开”,但是否展开由编译器决定;更重要的语义是允许该函数在多个翻译单元中出现相同定义(常用于头文件)。 [来源:第5章 p24]

  • 对于比较短小而又多次调用的函数,可以定义为内联函数 [来源:第5章 p24]

  • inline 函数的定义需在使用它的翻译单元可见(通常放在头文件);在同一源文件中不要求写在调用语句之前。 [来源:第5章 p24]

  • 定义一个函数为内联函数对于编译器只是一个建议(编译器可以选择不内联) [来源:第5章 p24]


5.2.6 数组作为函数参数(重要)

跳转到:5.2.6 数组作为函数参数(重要)
int average(int array[5]) {
int i, sum = 0;
for (i = 0; i < 5; ++i)
sum += array[i];
return sum / 5;
}

[来源:第5章 p25]

  • 数组名在大多数表达式中会发生数组到指针的衰变,转换为指向首元素的指针 &a[0];但在 sizeof、取地址 &a、decltype(a) 等场景不会衰变。 [来源:第5章 p28]

  • 按照值传递:调用时 score 会衰变为指向首元素的指针并按值传递给形参 array(其类型实际被调整为 int*),因此二者指向同一段数组存储。 [来源:第5章 p28]

  • 在函数形参中写 T a[5]/T a[] 等价于写 T* a:数组形参会被调整为指针形参,因此维度不会成为形参类型的一部分。 [来源:第5章 p28]

  • 形参 array 指向实参数组的同一片内存:在函数内通过 array[i] 修改元素会影响实参数组。 [来源:第5章 p28]

  • 若形参写成 T*/T[],传递的只有首元素地址,不携带数组长度;如需长度可另传 num,或改用 T(&)[N]/std::array/std::span 等保留尺寸信息。 [来源:第5章 p28]

  • 需要额外用一个整型参数来提供数组规模 [来源:第5章 p28]

// 正确写法:额外传递数组大小
int average(int array[], int num) {
int i, sum = 0;
for (i = 0; i < num; ++i)
sum += array[i];
return sum / num;
}
// 调用
average(score, 10);

[来源:第5章 p30]

  • ⚠️ 数组参数传递的是地址,函数内对数组元素的修改会影响实际数组!
int average(int array[5]) {
// ...
array[3] = 90; // 这会修改原数组!
return sum / 5;
}
int main() {
int score[5] = {90,65,72,83,55};
average(score);
cout << score[3]; // 输出 90,不是原来的 83
}

[来源:第5章 p27]

  • ⚠️ strcpy() 等 C 字符串函数依赖结尾的 \0 判断停止,因此不需要把长度作为参数传入;但目标数组必须足够大,否则会发生缓冲区溢出。实务中更推荐使用 std::string 等更安全的方案。 [来源:第5章 p30,Notes]

5.3 变量作用域与存储类别

跳转到:5.3 变量作用域与存储类别

5.3.1 变量的作用域

跳转到:5.3.1 变量的作用域
  • 作用域:变量名能被引用的程序范围 [来源:第5章 p32]

  • 局部变量:函数内声明的变量以及函数的形式参数,作用域局部于函数内部;任何复合语句块内声明的变量,作用域局部于该语句块内部 [来源:第5章 p32]

  • 全局变量(外部变量):声明/定义在所有函数之外的变量;其名字的作用域从声明处开始到本翻译单元结束(其他源文件如需使用,通常通过 extern 声明)。 [来源:第5章 p32]

  • 当碰到一个变量名,优先用当前作用域的定义,没有再找外部作用域(如果存在的话),最后找全局定义 [来源:第5章 p32]

  • 不同作用域中可以存在同名的不同变量 [来源:第5章 p33]

编译器如何确定一个名字引用的是哪个变量:

  1. 编译器保存一个所有全局声明的标识符表
  2. 编译器为每个作用域都保存一个局部声明的标识符表
  3. 当碰到一个标识符时,先查当前作用域的标识符表,再查外部作用域表(如果存在的话),最后查全局标识符表

[来源:第5章 p33]

【执行结果分析】Q1(p34)

int p = 1, q = 5, r = 3; // 全局变量
void f1() { int p = 3, r = 2; q = p+q+r; } // 使用局部p,r,全局q
void f2() { p = p+q+r; } // 全部使用全局变量
void f3() { int q; r = 2*r; q = r+p; } // 局部q,全局r,p

执行结果:

  • f3: p,q,r = 1,7,6
  • after f3: p,q,r = 1,5,6(f3的q是局部的)
  • f1: p,q,r = 3,10,2
  • after f1: p,q,r = 1,10,6(f1的p,r是局部的,q是全局的)
  • f2: p,q,r = 17,10,6
  • after f2: p,q,r = 17,10,6

[来源:第5章 p34,Notes]

【执行结果分析】Q2(p35)

int x = 1; // 全局
int f() {
int y = -2;
cout << "x in f is " << x << endl; // 输出全局x=1
if (x) {
int x; // if块内的局部x
for (x=0; x<3; x++)
cout << "Current x and y are " << x << " and " << y << endl;
}
cout << "x in f is " << x << endl; // 仍是全局x=1
return 0;
}
int main() {
int x = 12; // main的局部x
f();
cout << "x in main is " << x << endl; // main的x=12
}

输出:

x in f is 1
Current x and y are 0 and -2
Current x and y are 1 and -2
Current x and y are 2 and -2
x in f is 1
x in main is 12

[来源:第5章 p35,Notes]


5.3.2 变量的存储类别

跳转到:5.3.2 变量的存储类别
  • 完整的变量定义格式:存储类别 数据类型 变量名表; [来源:第5章 p36]

-(传统教材表述)常见的存储类别/说明符包括:

  • auto(自动变量;C++11 起不再作为存储类,改为类型推导关键字)
  • register(寄存器变量;C++17 起已移除,不建议使用)
  • extern(外部链接声明,用于跨源文件引用同一实体)
  • static(静态存储期;在命名空间作用域也可赋予内部链接)

-(补充)现代 C++ 还提供 thread_local(线程存储期)。

[来源:第5章 p36]

(1)自动变量 auto
跳转到:(1)自动变量 auto
  • 局部变量(函数内或块内定义的变量)默认是自动变量 [来源:第5章 p37]

  • 函数(语句块)被执行时,系统自动为自动变量分配空间,而函数返回(语句块执行结束)时,系统自动回收自动变量空间 [来源:第5章 p37]

int max(int x, int y) {
int rst; // 等价于 auto int rst;
rst = x>y?x:y;
return rst;
}

[来源:第5章 p37]

  • ⚠️ 在C++11标准中,auto已经不再作为存储类型标识符使用,现在是一个类型指示符,指导编译器推测变量的数据类型 [来源:第5章 p37,Notes]
(2)寄存器变量 register
跳转到:(2)寄存器变量 register
  • register 的本意是向编译器建议“频繁使用的局部变量可优先放入寄存器”;现代编译器通常会自行完成寄存器分配,该关键字往往无实际收益,且在 C++17 起已被移除(不建议使用)。 [来源:第5章 p38]

  • 只有局部变量可以声明为寄存器变量 [来源:第5章 p38]

  • register声明只是给编译器的一种建议,编译器可以不理会而当作一般的自动变量 [来源:第5章 p38]

register int x;

[来源:第5章 p38]

(3)外部变量 extern
跳转到:(3)外部变量 extern
  • extern 用于声明具有外部链接的变量/函数(通常表示“在别处定义”),常用于跨源文件共享同一个全局变量;extern 声明本身通常不分配存储(除非带初始化)。 [来源:第5章 p39]

  • 全局变量的定义只有一次,但可以在不同的源文件中用extern声明 [来源:第5章 p39]

  • 两种使用情况:

    1. 变量在其他文件中定义,需要在当前文件中使用
    2. 变量在同一文件中后面定义,需要在前面使用
file1.cpp
extern int x; // 外部变量声明
int main() {
f();
cout << "in main(): x= " << x << endl;
}
// file2.cpp
int x; // 全局变量的定义
void f() {
cout << "in f(): x= " << x << endl;
}

[来源:第5章 p39]

(4)静态变量 static
跳转到:(4)静态变量 static
  • static可以用于全局变量也可以用于局部变量 [来源:第5章 p40]

  • static用于全局变量:使该名字具有内部链接(internal linkage),只能在本翻译单元内被引用;其他源文件无法通过 extern 引用同一个名字来访问。 [来源:第5章 p40]

  • static用于局部变量:该局部变量将不会因为函数执行结束而消亡,下一次调用也不会自动分配新的空间,而一直使用同一空间;虽然局部静态变量在函数调用结束后仍然存在,但其他函数不能引用它 [来源:第5章 p40]

  • static变量在整个程序退出时才消亡 [来源:第5章 p40]

  • 函数也可以用 static 声明为具有内部链接的函数(传统称“静态函数”),只能在本翻译单元内被引用。 [来源:第5章 p40]

  • 【整理自 PPT】静态变量会被系统自动初始化,如果没有人为初始化,默认初始化值是0 [来源:第5章 p40,Notes]

【执行结果分析】Q1(p41)

int f(int a) {
int b = 0;
static int c = 3; // 静态局部变量,只初始化一次
b = b + 1;
c = c + 1;
return (a + b + c);
}
// C++中 main 必须返回 int,此示例不标准
void main() {
int a = 2, i;
for (i = 0; i < 3; ++i)
cout << f(a) << ' ';
}

输出:7 8 9

分析:

  • 第1次调用:b=1, c=4, return 2+1+4=7
  • 第2次调用:b=1, c=5, return 2+1+5=8
  • 第3次调用:b=1, c=6, return 2+1+6=9

[来源:第5章 p41,Notes]

【执行结果分析】Q2(p41)

int x = 2; // 全局
void a() { int x = 25; cout << "x in a is " << x << endl; }
void b() { static int x = 50; cout << "x in b is " << x++ << endl; }
void c() { x *= 10; cout << "x in c is " << x << endl; }
void d(int x) { cout << "x in d is " << ++x << endl; }
int main() {
int x = 6;
cout << "x in main is " << x << endl;
a(); b(); c(); d(x); x++; a(); b(); c(); d(x);
cout << "x in main is " << x << endl;
return 0;
}

输出:

x in main is 6
x in a is 25
x in b is 50
x in c is 20
x in d is 7
x in a is 25
x in b is 51
x in c is 200
x in d is 8
x in main is 7

分析:

  • a():使用局部x=25
  • b():静态局部x,保持累加
  • c():使用全局x
  • d():使用参数x的副本

[来源:第5章 p41,Notes]


  • 函数重载:可以定义同名但参数不同的多个函数 [来源:第5章 p43]
int f(int a1, int a2);
int f(double a1, double a2, double a3);
int f(int a1, int a2, double a3, double a4);

[来源:第5章 p43]

  • 在传统的C语言中,不允许定义多个同名函数 [来源:第5章 p43]

  • C++中在调用时,程序会根据调用时给出的实参表决定具体使用哪种函数定义 [来源:第5章 p43]

  • 编译器会给所有重名定义的函数各自取一个内部名称,加以区别 [来源:第5章 p43,Notes]

  • 调用时先确定可行函数:实参个数与形参匹配(必要时可用默认参数补齐),且每个实参都能通过隐式转换序列匹配到对应形参。 [来源:第5章 p43,Notes]

  • 在可行函数中比较隐式转换序列的优劣:通常 精确匹配 优于 提升(promotion),优于 标准转换(conversion);若无唯一最佳匹配则报二义性。默认参数主要影响“是否可行”,不直接决定“最优”。 [来源:第5章 p43,Notes]


  • 函数模板:实现类型的参数化(泛型化),即把函数中某些形参的类型定义成参数,称为模板参数 [来源:第5章 p45]

  • 在函数调用时,编译器根据实际参数的类型确定模板参数的值,生成不同的模板函数 [来源:第5章 p45]

  • 函数模板定义格式:

    template<模板参数类型表>
    返回类型 函数名(形式参数表) {
    // 函数体
    }

    [来源:第5章 p46]

  • 模板参数既可以是类型参数(用 class/typename 声明,二者等价),也可以是非类型参数(如 int N)或模板模板参数等。 [来源:第5章 p46]

template<class T1>
T1 max(T1 a, T1 b) {
return a > b ? a : b;
}

[来源:第5章 p46]

  • 通常每个模板参数都需要出现在函数形式参数表,否则调用时需要显式指定模板实参 [来源:第5章 p47]
max(3, 5); // T1 <- int
max('d', '4'); // T1 <- char

[来源:第5章 p47]

  • 当模板参数未在形参表中使用时,需要显式指定类型:
    template <class T1, class T2, class T3>
    T3 calc(T1 x, T2 y) {
    return x + y;
    }
    calc<int, char, int>(5, 'a'); // return 102
    calc<int, char, char>(5, 'a'); // return 'f'
    [来源:第5章 p47]

  • 递归:用了自己。直接或间接地调用了自己的函数叫做递归函数 [来源:第5章 p49]

  • 如果原始问题可以分为多个子问题,每个子问题与原问题相比只是问题规模变小了,则可以用递归函数来解 [来源:第5章 p49]

  • 递归需要最终终止于某个递归出口 [来源:第5章 p49]

  • 递归出口没有设计好会导致:像死循环一样停不下来,更糟糕会内存溢出,系统死机 [来源:第5章 p49,Notes]

  • 递归是包含分支路径的,要确保分支之间彼此互斥,但又能完全包含问题所涉及的集合 [来源:第5章 p49,Notes]

阶乘的递归实现:

long fact(int n) {
if (n == 0) return 1; // 递归出口
else return n * fact(n-1); // 递归调用
}

[来源:第5章 p50]

斐波那契数的递归实现:

int fib(int n) {
if (n < 3) return 1; // 递归出口
return fib(n-1) + fib(n-2); // 递归调用
}

[来源:第5章 p53]

汉诺塔的递归实现:

void Hanoi(int n, char start, char finish, char temp) {
if (n == 1)
cout << start << "->" << finish << '\t';
else {
Hanoi(n-1, start, temp, finish);
cout << start << "->" << finish << '\t';
Hanoi(n-1, temp, finish, start);
}
}

[来源:第5章 p59]

  • ⚠️ 用递归调用求斐波纳契数产生大量重复计算,非常低效 [来源:第5章 p54]

  • ⚠️ 用循环实现的函数没有重复计算,效率很高:

    int loopfib(int n) {
    int curr = 1, prev = 1, tmp;
    for (int i = 0; i < n-2; i++) {
    tmp = curr;
    curr = curr + prev;
    prev = tmp;
    }
    return curr;
    }

    [来源:第5章 p55]

  • 【整理自 PPT】理论上来讲,所有递归算法都可以用循环实现,但一般情况下需要依赖一些特殊的数据结构(如堆栈)才能实现 [来源:第5章 p55,Notes]


  1. 函数可以将一段完成独立功能的程序封装起来。通过函数名就可执行这一段功能。使用函数可以将程序模块化 [来源:第5章 p60]

  2. C++的程序是由一组函数组成。每个程序必须有一个名为main的函数,它对应于一般的程序设计语言中主程序 [来源:第5章 p60]

  3. 函数也可以调用自己,这样的函数称为递归函数 [来源:第5章 p60]


编写一个函数模板,判断两个一维数组是否相同。模板参数是数组类型。

答案提示:typeid(a).name() 可以获取类型名称 [来源:第5章 p61,Notes]

编写一个函数 int count(),使得第一次调用时返回1,第二次调用时返回2,即返回当前调用次数。

答案提示:使用静态局部变量 [来源:第5章 p61,Notes]

int count() {
static int c = 0;
return ++c;
}

【整理自 PPT】(由静态局部变量特性推得)

设计一个基于分治法的函数,找出一组整型数中的最大值。

答案提示:递归,两两比较 [来源:第5章 p61,Notes]


考点要点
函数定义格式返回类型 + 函数名 + (形参表) + {函数体}
值传递形参是实参的副本,修改形参不影响实参
数组参数传递的是地址,修改会影响原数组
函数声明可省略形参名,只保留类型
默认参数必须放在参数列表最后,省略从右向左
作用域查找顺序当前作用域 → 外层作用域 → 全局
static局部变量不随函数结束消亡,保持上次值
static全局变量作用域限制在本文件
extern声明其他文件中定义的全局变量
函数重载同名不同参(个数/类型不同)
函数模板template<class T> 实现泛型
递归要点必须有递归出口,避免无限递归