C++学习笔记(八:函数与变量)
往篇内容:
C++学习笔记(一)
一、C++编译阶段※二、入门案例解析
三、命名空间详解
四、C++程序结构
C++学习笔记(二)
五、函数基础
六、标识符
七、数据类型
补充:二进制相关的概念
sizeof 运算符简介
补充:原码、反码和补码
C++学习笔记(三)
补充:ASCII码表
八、内存构成
补充:变量
九、指针基础
十、const关键字
十一、枚举类型
C++学习笔记(四)
十二、 类型转换
十三、define指令
十四、typedef关键字
十五、运算符
十六、 流程控制
C++学习笔记(六)
十七、数组
C++学习笔记(七)
十八、指针
目录
十九、函数进阶
1、内联函数
2、函数重载
3、缺省函数
4、递归调用
经典案例1:计算阶乘!
案例2:斐波那契数列(Fibonacci Sequence)
案例3:汉诺塔(Tower of Hanoi)
补充:尾递归
案例1:尾递归阶乘
案例2:尾递归斐波那契
二十、变量进阶
1、作用域
1.1 局部变量
1.2 全局变量
2、存储类型
🧩 2.1 auto
🧩 2.2 register
🧩 2.3 static
①局部静态变量
②全局静态变量
3、链接属性
4、内部函数
十九、函数进阶
1、内联函数
在 C++ 中,内联函数(inline function) 是一种优化机制,用来减少函数调用的开销。通常情况下,函数调用会有一些运行时开销:比如参数压栈、跳转到函数地址等。
为了提高效率,对于一些短小精悍、频繁调用的函数,我们可以将其声明为inline ,这样编译器就会尝试将该函数的代码直接插入到调用处,而不是进行常规的函数调用。
定义格式:
inline 返回类型 函数名(参数列表) {
// 函数体
}
作用与优点:
特性 | 描述 |
---|---|
⚡提升性能 | 避免了函数调用的开销(如栈操作、跳转等),适合频繁调用的小函数 |
🔍增强可读性 | 把常用逻辑封装成函数,又不会带来性能损失 |
📦代码复用 | 和普通函数一样,可以多次调用 |
与宏函数的区别(#define)
对比项 | 宏函数(#define ) | 内联函数(inline ) |
---|---|---|
类型检查 | 无 | 有 |
调试支持 | 无(预处理阶段替换) | 有(保留函数信息) |
运算符优先级问题 | 易出错 | 安全 |
编译器优化 | 不参与 | 参与 |
作用域 | 无 | 有(遵循 C++ 作用域规则) |
结论:在 C++ 中,应尽量避免使用宏函数,改用 inline 函数来替代!
注意事项:
- 声明和定义必须在一起,通常在头文件中定义并实现
- 内联函数的建议性的,不是强制性的,编译时函数嵌入
- 内联函数一般功能简单,频繁调用,代码量5行以内
- 如果函数太复杂(例如包含循环、递归、静态变量等),编译器可能不会将其内联
- 如果修改了内联函数体里面内容,则必须要重新编译链接才可以起作用
应用场景:
- 函数非常简单(如取绝对值、加减乘除)
- 函数被频繁调用(如类的 getter/setter 方法)
- 性能敏感区域(如游戏引擎、图形库)
2、函数重载
C++中函数重载(Function Overloading) 是指在同一作用域中可以有多个同名函数,但它们的参数列表不同(参数个数或类型不同)。编译器会根据调用时传递的参数来决定调用哪一个函数。
基本语法:
返回类型 函数名(参数列表1) { ... }返回类型 函数名(参数列表2) { ... }...
案例:
#include <iostream>
using namespace std;// 函数重载示例
int add(int a, int b) {return a + b;
}double add(double a, double b) {return a + b;
}int add(int a, int b, int c) {return a + b + c;
}int main() {cout << "add(3, 4) = " << add(3, 4) << endl; // 调用第一个add函数cout << "add(3.5, 4.2) = " << add(3.5, 4.2) << endl; // 调用第二个add函数cout << "add(1, 2, 3) = " << add(1, 2, 3) << endl; // 调用第三个add函数return 0;
}
编译器判断函数是否构成重载的因素
判断标准 | 是否影响重载 |
---|---|
函数名相同 | ✅ 必须相同 |
参数数量不同 | ✅ 可以重载 |
参数类型不同 | ✅ 可以重载 |
参数顺序不同 | ✅ 可以重载(如 void foo(int, double) vs void foo(double, int) ) |
返回值类型不同 | ❌ 不可重载 |
返回值类型不同就不可重载吗?
答:是的,仅返回值类型不同不能构成函数重载。函数重载的核心是参数列表必须不同(参数数量、类型或顺序不同),而返回值类型不参与重载的判断。
为什么?
因为 C++ 编译器根据调用时提供的参数来决定调用哪个重载函数,而不是根据返回值类型。例如:
add(3, 4); // 编译器如何知道该调用 int 还是 double 版本?
此时编译器无法通过返回值类型区分,因此会报错。
总结
情况 示例 是否重载? 参数列表相同,仅返回值不同 int add(int, int)
double add(int, int)
❌ 不构成重载(编译错误) 参数列表不同,返回值可相同或不同 int add(int, int)
double add(double, double)
✅ 构成重载
底层机制
函数重载是静态多态的一种形式,也称为编译时多态(后续会讨论)。它是由编译器在编译阶段完成的:
- 编译器根据函数名和参数列表生成不同的内部符号(name mangling)
- 在链接阶段,链接器会找到正确的函数地址
例如,两个重载函数:
int add(int a, int b);
double add(double a, double b);
可能被编译为类似
_add_int_int
_add_double_double
应用场景:
- 功能相似但参数类型或数量不同
- 提供更灵活的接口(如构造函数)
- 类型安全的替代宏函数
- 实现类的多种初始化方式
3、缺省函数
基本语法:
返回类型 函数名(参数类型 参数名 = 默认值, ...) {
// 函数体
}
案例:
#include<iostream>
using namespace std;void greet(string name="Guest")
{cout<<"Hello,"<<name<<"!"<<endl;
}int main()
{greet();//使用默认参数greet("Alice");//传递参数return 0;
}//输出:Hello,Guest Hello,Alice
- 缺省参数必须放在参数列表的最后面
- 也就是说:一旦某个参数有默认值,它后面的所有参数也必须有默认值
//正确写法
void func(int a, int b = 10, int c = 20);
//错误写法
void func(int a = 5, int b, int c); // 错误:a 之后的参数没有默认值
缺省参数作用域与可见性:
-
缺省参数通常在函数声明中给出(头文件中)
-
函数定义中不再重复指定缺省值
-
如果在定义中给缺省值,在声明中就不应该再给(否则会冲突)
注意:当函数即作为重载函数,又有默认参数时,注意不要产生二义性。
错误案例:
#include <iostream>
using namespace std;// 函数1:带缺省参数的版本
void display(int a, int b = 10) {cout << "display(" << a << ", " << b << ")" << endl;
}// 函数2:单参数版本(与函数1冲突)
void display(int a) {cout << "display(" << a << ")" << endl;
}int main() {display(5); // 编译错误:二义性调用return 0;
}
- 参数有合理默认值(如缓冲区大小、超时时间等)
- 提高接口可读性和易用性
- 避免过多函数重载
4、递归调用
- 递归函数:一个函数在其函数体内调用了自己
- 递归终止条件(Base Case):必须存在一个或多个不进行递归调用的条件,否则会导致无限递归和栈溢出
- 递归步骤(Recursive Step):将大问题分解为更小的子问题,并通过递归调用来解决这些子问题
基本结构:
返回类型 函数名(参数列表)
{
if (满足终止条件) {
return 某个值; // 终止递归
} else {
// 递归调用
函数名(缩小后的参数);
}
}
经典案例1:计算阶乘!
#include <iostream>
using namespace std;
int factorial(int n) {if (n == 0 || n == 1) { // 递归终止条件return 1;}return n * factorial(n - 1); // 递归调用
}
int main() {int num;cout << "请输入一个整数:";cin >> num;cout << num << "! = " << factorial(num) << endl;return 0;
}
案例2:斐波那契数列(Fibonacci Sequence)
#include <iostream>
using namespace std;int fibonacci(int n) {if (n == 0) return 0;if (n == 1) return 1;return fibonacci(n - 1) + fibonacci(n - 2);
}int main() {int n;cout << "请输入斐波那契数列的项数:";cin >> n;for (int i = 0; i < n; ++i)cout << fibonacci(i) << " ";cout << endl;return 0;
}
案例3:汉诺塔(Tower of Hanoi)
#include <iostream>
using namespace std;/*** 汉诺塔问题解决方案* 将n个盘子从source柱子,借助auxiliary柱子,移动到target柱子* @param n 盘子数量* @param source 源柱子* @param auxiliary 辅助柱子* @param target 目标柱子*/
void hanoi(int n, char source, char auxiliary, char target) {if (n == 1) {cout << "Move disk 1 from " << source << " to " << target << endl;return;}// 步骤1:将n-1个盘子从源柱子移动到辅助柱子hanoi(n - 1, source, target, auxiliary);// 步骤2:将最大的盘子从源柱子移动到目标柱子cout << "Move disk " << n << " from " << source << " to " << target << endl;// 步骤3:将n-1个盘子从辅助柱子移动到目标柱子hanoi(n - 1, auxiliary, source, target);
}int main() {int n;cout << "请输入汉诺塔的盘子数量:";cin >> n;hanoi(n, 'A', 'B', 'C');return 0;
}
递归优缺点
✅ 优点:
- 代码简洁清晰;
- 更符合某些问题的自然逻辑;
- 易于实现复杂的数据结构操作(如树、图等);
递归优缺点
✅ 优点:
- 代码简洁清晰;
- 更符合某些问题的自然逻辑;
- 易于实现复杂的数据结构操作(如树、图等);
补充:尾递归
如果一个函数的最后一步只是调用自身,而没有其他需要执行的操作(例如return f(x) 而不是 return x + f(x) ),则称为 尾递归(TailRecursion)。
尾递归优势
- 可以被编译器优化为循环,避免栈溢出
- 减少内存占用
- 提高性能
案例1:尾递归阶乘
int factorial(int n, int result = 1) {if (n == 0 || n == 1)return result;return factorial(n - 1, n * result); // 尾递归// f(5) == f(5,1) = f(4,5*1) = f(3,5*4)// = f(2,5*4*3) = f(1,5*4*3*2) == 5*4*3*2}
案例2:尾递归斐波那契
int fib(int n, int a = 0, int b = 1) {
if (n == 0) return a;
if (n == 1) return b;
return fib(n - 1, b, a + b); // 尾递归
}
2种递归对比:
特性 | 普通递归 | 尾递归 |
---|---|---|
是否有未完成的计算 | 是 | 否 |
是否可以优化 | 否 | 是 |
栈帧是否增长 | 是 | 否 |
实现难度 | 简单 | 稍微复杂 |
性能 | 差 | 好 |
注意:
- 并非所有递归都可以写成尾递归
- 不同编译器对尾递归优化的支持程度不同
- 使用 -O2 或 -O3 编译选项可以启用尾递归优化
二十、变量进阶
1、作用域
- 文件作用域
也称为全局作用域,在任何函数、类或命名空间之外声明的变量具有文件作用域
- 块作用域
变量声明在一对花括号 {} 内,其作用范围从定义点开始直到包含它的最内层的块结束 - 函数原型作用域
指的是函数原型中的参数名的作用域
void func(int param); // param 在这里是函数原型作用域
- 函数作用域
goto 标签是唯一具有函数作用域的实体,意味着标签名称在整个函数体内都是有效的,
void func() {
goto label;
label: //标签可以放置在函数内的任何位置,但必须在函数范围内使用
cout << "Jumped to label" << endl;
}
- 类作用域(Class Scope)
成员变量和函数属于其类的作用域,后续类和对象再讨论!- 命名空间作用域(Namespace Scope)
命名空间内的名字具有命名空间作用域,例如:namespace MySpace {int var = 10;}int main() {cout << MySpace::var << endl; // 使用命名空间限定符访问变量}
- 命名空间作用域(Namespace Scope)
1.1 局部变量
在一个函数内部定义的变量是内部变量,它只在本函数范围内有效。也就是说只有在本函数内才能使用它们,在此函数以外是不能使用这些变量的。
同样,在复合语句中定义的变量只在本复合语句范围内有效。
上面两种都称为局部变量 (local variable)。具体可见下图:
注意:函数声明和函数定义中的形式参数,相互独立!
1.2 全局变量
在函数内定义的变量是局部变量,而在函数之外定义的变量是外部变量,称为 全局变量 (global variable,也称全程变量)。全局变量的有效范围为从定义变量的位置开始到本源文件结束。
2、存储类型
在 C++ 中,存储类型(Storage Class)决定了变量的生命周期、可见性(作用域)以及是否可以被初始化等特性。C++ 中主要有4种存储类型: auto 、register 、 static 和 extern 。每种存储类型都有其特定的应用场景和特点。
⚪存储类型:存储变量值的内存类型
⚪决定了变量的创建时间、销毁时间、值的生命周期
⚪普通内存、运行时堆栈、硬件寄存器⚪存储类型:
静态变量:代码块之外或则代码块内部并用
static
修饰自动变量:代码块内部并且不使用
static
寄存器变量:使用
register
修饰的自动变量⚪生命周期并不等同于存储类型
⚪初始化
静态变量有默认初始化值
0
自动变量没有默认初始化
🧩 2.1 auto
- 默认存储类型:对于局部变量,默认即为 auto 类型。
- 作用域与生命周期:具有块作用域(Block Scope),从声明点开始直到包含它的最内层代码块结束;生命周期从进入作用域时开始,离开作用域时结束。
- 使用情况:自 C++11 起, auto 关键字更多地用于自动类型推导,而不是传统的存储类型指定。
void autoExample() {
auto x = 5; // 自动类型推导,x 是 int 类型
}
🧩 2.2 register
- 意图优化:建议编译器将变量存储在寄存器中以提高访问速度,但现代编译器通常会自动进行这种优化,因此很少显式使用 register
- 寄存器变量:为提高执行效率, C++允许将局部变量的值放在CPU中的寄存器中,需要用时直接从寄存器取出参加运算,不必再到内存中去存取。
- 限制:不能取地址(如 &var ),因为它们可能不在内存中
- 生命周期:与 auto 相同,具有块作用域
void registerExample() {
register int i;
// int *p = &i; // 错误,不能取地址
}
注意:C++17 已经弃用了 register 关键字作为存储类说明符
🧩 2.3 static
①局部静态变量
在函数内使用 static 修饰变量时,其存储类型会由栈空间修改为数据区(初始化或未初始化)。它只会在第一次执行到它的初始化语句时初始化一次,并且它的值会保留直到程序结束,即使函数多次被调用也不会重新初始化。
#include <iostream>
using namespace std;int f(int a) {auto b = 0;// 只初始化一次,存储在初始化数据区,函数帧销毁,c 不会销毁static int c = 3; b = b + 1; // 每次调用 b 重新从 0 加 1c = c + 1; // c 静态变量,值持续累加,依次为 4、5、6 return a + b + c;
}int main() {int a = 2;// 调用 3 次 f 函数for (int i = 0; i < 3; i++) {// 第一次:2 + 1 + 4 = 7// 第二次:2 + 1 + 5 = 8// 第三次:2 + 1 + 6 = 9 cout << f(a) << " "; }cout << endl;return 0;
}
静态局部变量在静态存储区内分配存储单元,在程序整个运行期间都不释放;
为静态局部变量赋初值是在编译时进行值的,即只赋初值一次,在程序运行时它已有初值。以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的值;
如果静态局部变量没有初始化,则编译时自动赋初值 0(对数值型变量 )或空字符 (对字符型变量);
注意:虽然static修饰局部变量会改变其存储类型,变量生命周期也发生改变(由所在代码块结束就销毁,变成进程结束才销毁),但其作用域并不会发生改变!
②全局静态变量
在程序设计中希望某些外部变量只限于被本文件引用,而不能被其他文件 引用。这时可以在定义外部变量时加一个 static声明。
总结:
3、链接属性
- 标识符的链接属性(linkage)用于决定如何处理在不同文件中出现的标识符,主要有
external
、internal
、none
三种 。
三种链接属性具体说明
-
None(无链接属性):带有这种链接属性的标识符,总是被当作单独的个体,各声明相互独立,不关联其他位置的同名标识符 。
-
Internal(内部链接属性):在同一个源文件内,所有对该标识符的声明都指向同一个实体;但在不同源文件中,同名标识符的声明会被视为不同实体,相互独立 。
-
External(外部链接属性):无论标识符被声明多少次、分布在几个源文件中,都表示同一个实体,可跨源文件关联使用 。
- 链接属性:声明一个变量或函数,告诉编译器该标识符是在其他地方定义的
- 用途:主要用于多文件编程中,允许在一个文件中引用另一个文件中定义的全局变量或函数
// file1.cpp
int globalVar = 10;// file2.cppextern int globalVar; // 声明 globalVar 存在于其他文件中
- 函数声明:通常省略 extern ,因为它是函数声明的默认存储类型
4、内部函数
如果一个函数只能被本文件中其他函数所调用,它称为内部函数,在定义函数时用 static 修饰。
格式: static 返回值类型 函数名(形参列表); 如: static int fun(int a, int b); 内部函数也称为静态(static)函数。
如果在不同的文件中有同名的内部函数,互不干扰。
static关键字总结
static 用于函数定义或则用于代码块之外的变量声明时,static 关键字用于修改标识符的链接属性,从 external 修改为 internal。修改标识符的存储类型和作用域。
static 用于代码块内部的变量声明时,static 用于修改变量的存储类型,从自动变量修改为静态变量。变量的链接属性和作用域不受影响。