Week6 Inside Class
函数重载(overloaded functions)¶
函数重载是指在同一作用域内,多个函数共享相同的函数名,但是参数列表不相同(包括参数的数量、类型或顺序),C++编译器通过参数列表的差异来区分调用哪个函数。需要注意的是,返回值的类型并不作为重载的依据。
注意
析构函数不能重载,毕竟析构函数本来就没有参数列表
函数重载与自动类型转换¶
函数重载在调用的时候可能会涉及自动类型转换,也称为类型提升或类型强制转换,C++会根据参数的类型尝试找到最佳匹配的函数,如果没有完全匹配的函数,编译器可能进行类型转换以调用最接近的函数,如下面的例子:
void f(short i);
void f(double d);
f('a'); // 调用 f(short),因为字符 'a' 可以隐式转换为 short
f(2); // 调用 f(short),因为整数 2 可以隐式转换为 short
f(2L); // 调用 f(double),因为长整型 2L 更接近 double
f(3.2); // 调用 f(double),因为 3.2 是 double 类型
需要注意的是,自动类型转换有可能导致二义性(ambiguity
),比如下面的例子:
在上述情况下,编译器会报错,需要程序员显式指定类型
常量与非常量函数重载¶
同一个函数名可以有const
和非const
版本,用于区分是否允许修改对象状态,如
这样在类中,const
成员函数和非const
成员函数可以根据调用对象的常量性自动选择
注意
const和非const会影响函数重载
代理构造(Delegating constructor)[C++11标准引入,应该不考]¶
代理构造是指在一个构造函数的初始化列表中调用另一个构造函数(称为目标构造函数)来完成其初始化工作。目标构造函数先执行,然后再执行委托构造函数的其他初始化逻辑,能够有效地解决多个构造函数中存在重复初始化代码的问题。可以参考下面这个例子:
class class_c {
public:
int max;
int min;
int middle;
class_c(int my_max) {
max = my_max > 0 ? my_max : 10;
}
class_c(int my_max, int my_min) : class_c(my_max) {
min = my_min > 0 && my_min < max ? my_min : 1;
}
class_c(int my_max, int my_min, int my_middle) : class_c(my_max, my_min) {
middle = my_middle < max && my_middle > min ? my_middle : 5;
}
};
int main() {
class_c c1 { 1, 3, 2 }; // 调用 class_c(int, int, int)
}
class_c(int my_max)
初始化 max
,class_c(int my_max, int my_min)
委托给 class_c(int my_max)
初始化 max
,然后初始化 min
,class_c(int my_max, int my_min, int my_middle)
委托给 class_c(int my_max, int my_min)
初始化 max
和 min
,然后初始化 middle
。这种链式委托减少了代码重复,并保持了初始化逻辑的清晰性。
代理构造也有一些规则和限制:
- 委托构造函数的初始化列表中只能调用目标构造函数,不能同时初始化其他成员,例如:
解决方法是将额外的初始化放在构造函数体中,或者通过私有构造函数处理
-
目标构造函数的执行优先于委托构造函数的构造函数体
-
避免循环委托,委托关系是可以形成链的,比如\(A\to B\to C\),但是不能形成循环
-
如果有多个构造函数需要共享复杂的初始化逻辑,可以定义一个私有的目标构造函数,比如
class Info {
public:
Info() : Info(1) {} // 委托
Info(int i) : Info(i, 'a') {} // 委托
Info(char e) : Info(1, e) {} // 委托
private:
Info(int i, char e) : type(i), name(e) {} // 目标构造函数
int type;
char name;
};
私有构造函数Info(int,char)
负责初始化type
和name
,所有公共构造函数委托给它,避免了重复代码
默认参数(Default arguments)¶
默认参数是指在函数声明中为参数指定的默认值。当调用函数的时候,如果调用者没有提供该参数的值,编译器就会自动使用默认值。看下面的例子
Stash(int size, int initQuantity = 0);
Stash s1(100); // 等价于Stash(100,0)
Stash s2(100,50); // 显式地指定initQuantity=50
默认参数有以下几点规则:
- 必须从右到左指定默认值:如果某个参数有默认值,其右侧的所有参数都必须有默认值,例如
-
默认参数应当在函数原型中声明,通常在头文件中。函数定义中不允许重复指定默认值
-
调用函数的时候编译器会从左到右匹配参数,省略的参数会使用默认值(其实这点和默认值必须从右到左指定是匹配的)
默认构造函数(default constructor)¶
如果一个构造函数的所有参数都有默认值,则该构造函数可以作为默认构造函数,允许无参数构造对象,如
class Stash{
public:
Stash(int size = 10, int initQuantity = 0);
};
Stash s1; // 等价于Stash(10,0),用的是默认构造函数
Stash s2(100); // 等价于Stash(100,0)
Stash s3(100,50)
内联函数(Inline Functions)¶
内联函数是一种特殊的函数,编译器(在编译的时候将该函数的目标代码插入每个调用该函数的地方)直接在调用处展开其函数体(直接插入代码)而非调用(函数调用通常涉及参数压栈、返回地址保存、返回值处理等等步骤,这些步骤是会带来开销的),从而消除函数调用的开销,类似于C语言中的宏,但是具有类型检查和作用域等C++特性,下面举个例子说明:
int f(int i) {
return i * 2;
}
int main() {
int a = 4;
int b = f(a); // 正常函数调用
}
inline int f(int i) {
return i * 2;
}
int main() {
int a = 4;
int b = f(a); // 展开为 b = a * 2;
}
内联函数的声明与定义¶
内联函数在声明和定义的时候都需要用inline
关键字标记。
需要注意的是,内联函数的函数体必须放在头文件中,而非放到对应的.cpp
文件,并通过#include
将头文件引入到需要使用的地方,这是因为编译器需要函数体的完整定义来执行内联展开,如果头文件中只有函数签名而没有函数体,编译器就无法对内联函数进行展开。我们也不需要担心多重定义的问题,因为内联函数的定义被视为“声明”,不会导致连接错误
Tip
在类中直接定义(函数体直接在类内)的成员函数会自动成为内联函数,如果将成员函数的定义放在类外,想要将其变为内联函数则需要显式使用inline
关键字
内联函数的优缺点¶
- 优点:
- 性能提升:消除了函数调用的开销
- 类型安全:与C语言的宏相比,内联函数支持类型检查
- 缺点:
- 代码体积增加:内联函数在每个调用处都展开函数体,可能导致生成的二级制代码变大
- 适用性有限:并非所有函数都适合内联,有些时候编译器会忽略内联请求
正因如此,内联函数的使用场景也是比较明确的,小型函数(函数体小,此时调用开销占比较高,内联效果显著)、频繁调用的函数比较适合内联;而大型函数(代码体积过大,得不偿失)、递归函数(内联函数不支持递归,因为递归需要函数调用栈,而内联会展开函数体)等不适合内联