1 函数重载
1.1 函数重载概念
函数重载(Function Overloading)是C++中一种允许在同一作用域内定义多个同名函数的机制,前提是这些函数的参数列表不同(参数个数不同或参数类型不同)。通过函数重载,可以让程序员为不同的参数类型编写专用的函数实现,提高代码的可读性和可维护性。
void print(int x) {
cout << "整数: " << x << endl;
}
void print(double x) {
cout << "浮点数: " << x << endl;
}
int main() {
print(10); // 调用print(int)
print(3.14159); // 调用print(double)
return 0;
}
1.2 支持函数重载的原理
编译器是如何支持函数重载的呢?原理如下:
- 编译器会根据函数的签名(参数个数、参数类型及参数顺序)为每个函数生成一个独一无二的符号名(Symbol Name)
- 在链接阶段,符号名不同的函数视为不同函数,从而实现了重载
- 当调用函数时,编译器会根据传入的实参类型和个数,选择最佳匹配的函数版本
例如下面这段代码:
void func(int) {}
void func(double) {}
void func(int, double) {}
编译器可能会为它们生成如下的符号名:
_func_int
_func_double
_func_int_double
值得注意的是:
- 函数返回值类型不属于函数签名的一部分,因此不影响函数重载
- 如果传入的实参类型都无法和任何一个函数版本精确匹配,编译器将尝试进行隐式类型转换,以寻找最佳匹配
- 如果存在多个函数版本都是最佳匹配(如同时有
func(int)
和func(double)
而实参为int
)则函数调用将是曲折的
1.3 使用时注意事项
- 仔细区分形参名称和形参类型,形参名称无关函数重载
- 引用形参会被编译器转换为对应的非引用类型,因此
func(int&)
和func(int)
被视为相同函数 - 返回值不同的两个函数也可以重载,但可读性较差,不推荐这样做
2 引用
2.1 引用概念
引用(Reference)为某个变量提供了一个别名,通过该别名与直接使用变量本身完全等价。引用允许在程序的不同上下文中使用同一个名称来表示同一个实体。可以认为,引用就是已经初始化了的常量指针。
int x = 10;
int& r = x; // r是x的别名
r = 20; // 等价于x = 20
cout << r << endl; // 输出20
2.2 引用的特性
- 引用必须在创建时就被初始化,初始化后就无法重新绑定到其他变量
- 引用本身并不占用额外的内存空间,它只是已有变量的一个别名
- 不存在无指向的引用,引用必须绑定到一个有效的现存对象
- 对引用的任何操作都直接作用在其所引用的对象上
int y;
int& r = y; // 合法,r是y的别名
int& s; // 非法,必须初始化
r = 123; // 等价于y = 123
2.3 常引用
常引用(Const Reference)是被const
修饰的引用,一旦初始化就无法通过它修改所引用对象的值。常引用常用作函数形参,防止意外修改了实参。
void foo(const int& x) {
x = 10; // 错误!不能修改常引用所引用的对象
}
int main() {
int y = 123;
foo(y); // y依然为123
return 0;
}
2.4 使用场景
引用最常见的用途是作为函数形参,用于避免不必要的数据复制,提高传递效率。
- 如果通过值传递方式,被调用函数将复制实参数据的副本,对于较大的对象开销较大
- 如果使用指针传递虽然高效但操作较为复杂
- 采用引用传递则可在不复制数据的情况下直接访问实参数据
void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int x = 1, y = 2;
swap(x, y); // x=2,y=1
return 0;
}
2.5 传值、传引用效率比较
通过值传递的开销包括创建临时对象以及在函数返回时销毁临时对象,如果对象较大则开销也较大。而引用传递仅是获取实参对象的地址,无需复制和创建临时对象,开销较小。因此对于较大的对象,采用引用传递将更加高效。
2.6 引用和指针的区别
引用和指针虽然在某些场景下可以互相替代,但它们是不同的概念,有以下区别:
- 引用必须在定义时就初始化,指针可以先定义后再赋值
- 引用只是为某个对象取了个别名,不能为空;指针是独立分配的存储空间,可以为空
- 引用只能绑定在一个对象上,不能重新绑定;指针可以在运行时指向不同对象
- 对引用进行操作就是作用在其引用的对象上;对指针进行操作只影响指针自身
- 引用相对于指针更加直观简洁,使用起来也更加安全
- 指针相对于引用更加灵活和通用,但也更容易出错(如空指针、内存泄漏等)
3 内联函数
3.1 内联函数概念
内联函数(Inline Function)使编译器在编译代码时,将函数体的完整代码插入到每一个调用该函数的点,这样就避免了函数调用的开销,从而提高了程序的运行效率。
inline int max(int a, int b) {
return a > b ? a : b;
}
int main() {
int x = max(3, 5); // max函数体被插入此处
return 0;
}
3.2 内联函数的特性
- 内联是一种编译期优化,发生在编译阶段而非运行时
- 内联函数在调用点展开,而宏在预处理阶段展开
- 内联函数必须定义在当前文件中,否则编译器无法对其内联
- 内联只是对编译器的一种建议,编译器有权力决定是否内联
- 内联可提高程序运行效率,但代码膨胀会影响程序的占用空间
- 内联函数不能是递归函数,因为展开后将导致无限循环
- 内联函数不能是虚函数,因为虚函数的地址需要在运行时确定
3.3 内联函数与宏的对比
宏是通过简单的文本替换实现的,在预处理阶段展开;而内联函数是由编译器在编译时展开,编译器会进行语法分析、类型检查等,因此内联函数比宏更加智能、安全,也更加符合语言本身的规范和约束。
宏可能会带来副作用,比如多次求值、类型错误等。内联函数则更加严谨,对于复杂表达式只计算一次。
// 宏
#define SQUARE(x) x * x
// 内联函数
inline int square(int x) {
return x * x;
}
int main() {
int a = 2, b = 3;
int x = SQUARE(a++); // 宏会先计算a++,导致a变为3
int y = square(b++); // 内联函数只计算一次b,b为3
return 0;
}
对于简单场景,两者的运行效率相当。但内联函数有利于编译器进行逃逸分析和其他优化,因此更适合复杂的调用场景。总的来说,推荐优先使用内联函数。
3.4 内联函数的优缺点
优点:
- 省去了函数调用的开销,提高了程序运行效率
- 将小函数的代码复制到调用点,可以让CPU缓存获得更高命中率
- 内联函数由编译器展开,可以实现有效的语法检查
缺点:
- 代码膨胀,可执行文件的体积增大
- 大量内联可能会增加编译时间
- 无法在调试时跟踪到内联函数的调用
3.5 内联说明符
C++使用inline
关键字对普通函数或成员函数进行内联说明。实际内联与否取决于编译器,因为过度内联也会导致可执行文件过大。
// 内联普通函数
inline int add(int x, int y) {
return x + y;
}
class Example {
public:
// 内联成员函数
inline int getValue() { return m_value; }
private:
int m_value;
};
建议对简单的getter/setter函数、只有一两行代码的小函数等进行内联,以提升性能。
3.6 内联与默认函数实参
如果内联函数带有默认参数值,那么无论调用时是否显式提供了实参值,编译器都必须为其生成非内联版本,因为链接器可能需要此版本的函数地址。
inline int foo(int a, int b = 1) { return a + b; }
int main() {
foo(1); // foo(int,int)会被内联
foo(2, 3); // foo(int,int)会被内联
// 但此处foo(int,int)也需要生成非内联版本的定义
// 否则链接器无法获取foo(int,int)的地址
}