跳转至

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),比如下面的例子:

void f(int i);
void f(float f);
f(5); // 二义性错误,5可以转换为int或float,编译器无法决定

在上述情况下,编译器会报错,需要程序员显式指定类型

常量与非常量函数重载

同一个函数名可以有const和非const版本,用于区分是否允许修改对象状态,如

void f() const;
void f();

这样在类中,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)初始化 maxclass_c(int my_max, int my_min) 委托给 class_c(int my_max) 初始化 max,然后初始化 minclass_c(int my_max, int my_min, int my_middle) 委托给 class_c(int my_max, int my_min) 初始化 maxmin,然后初始化 middle。这种链式委托减少了代码重复,并保持了初始化逻辑的清晰性。

代理构造也有一些规则和限制:

  • 委托构造函数的初始化列表中只能调用目标构造函数,不能同时初始化其他成员,例如:
class_c(int my_max,int my_min) : calss_c(my_max), min(my_min){

}

解决方法是将额外的初始化放在构造函数体中,或者通过私有构造函数处理

  • 目标构造函数的执行优先于委托构造函数的构造函数体

  • 避免循环委托,委托关系是可以形成链的,比如\(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)负责初始化typename,所有公共构造函数委托给它,避免了重复代码

默认参数(Default arguments)

默认参数是指在函数声明中为参数指定的默认值。当调用函数的时候,如果调用者没有提供该参数的值,编译器就会自动使用默认值。看下面的例子

Stash(int size, int initQuantity = 0);

Stash s1(100); // 等价于Stash(100,0)
Stash s2(100,50); // 显式地指定initQuantity=50

默认参数有以下几点规则:

  • 必须从右到左指定默认值:如果某个参数有默认值,其右侧的所有参数都必须有默认值,例如
void f(int a, int b = 2, int c); // 非法的,c应当有默认参数
void f(int a, int b = 2, int c = 1) // 合法
  • 默认参数应当在函数原型中声明,通常在头文件中。函数定义中不允许重复指定默认值

  • 调用函数的时候编译器会从左到右匹配参数,省略的参数会使用默认值(其实这点和默认值必须从右到左指定是匹配的)

默认构造函数(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语言的宏相比,内联函数支持类型检查
  • 缺点:
  • 代码体积增加:内联函数在每个调用处都展开函数体,可能导致生成的二级制代码变大
  • 适用性有限:并非所有函数都适合内联,有些时候编译器会忽略内联请求

正因如此,内联函数的使用场景也是比较明确的,小型函数(函数体小,此时调用开销占比较高,内联效果显著)、频繁调用的函数比较适合内联;而大型函数(代码体积过大,得不偿失)、递归函数(内联函数不支持递归,因为递归需要函数调用栈,而内联会展开函数体)等不适合内联

内联变量(C++17之后)

弱符号(Weak)


评论