《C++ Primer Plus》14. C++ 中的代码复用

包含对象成员的类

即将属性作为类的成员,是一种“has-a”关系

当初始化列表中包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序

私有继承

公共继承是“is-a”关系,而私有继承是一个实现“has-a”关系的途径

使用私有继承,基类的公共方法将成为派生类的私有方法,不会继承基类的接口

包含将对象作为命名的成员对象添加到类中,而私有继承将对象作为未命名的继承对象添加到类中,被继承或者被包含的对象叫做子对象(subobject)

使用多个基类的继承被叫做多重继承(multiple inheritance,MI),通常会有一些问题,尤其是公有继承,必须使用额外的语法规则来解决它们

相比于包含,私有继承中,派生类可以重新定义虚函数

初始化基类组件

假设先我们有一个类:

class Student : private std::string, private std::valarray<double> {
};

我们可以通过初始化列表语法来初始化基类组件:

Student(...) : std::string(...), std::valarray<double>(...);

访问基类的方法

通过作用域解析运算符,如:

double Student::average() const {
    if (std::valarray<double>::size() > 0) {
        return std::valarray<double>::sum() / std::valarray<double>::size();
    } else {
        return 0;
    }
}

访问基类对象

可以通过强制类型转换的方式来访问基类对象,如:

const std::string& Student::name() const {
    return (const std::string&) *this;
}

访问基类的友元函数

用类名显式限定函数名并不适用于友元函数,可以显式地转换为基类来调用正确的函数:

ostream& operator<<(std::ostream& os, const Student& stu) {
    os << "Score for " << (const std::string&) stu << ":\n";
    ...
}

stu 不会自动转换为 string 引用,因为在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针

但是在这个例子中,即使使用公有继承,也必须显式类型转换。如果不转换的话会递归调用,而且 MI 编译器不知道调用哪个基类的 operator<<() 方法

保护继承

各种继承方式:

特征公有继承保护继承私有继承
公有成员变成派生类的公有成员派生类的保护成员派生类的私有成员
保护成员变成派生类的保护成员派生类的保护成员派生类的私有成员
私有成员变成只能通过基类接口访问只能通过基类接口访问只能通过基类接口访问
是否能够隐式向上转换是(但只能在派生类中)

可以使用 using 重新定义访问权限,如:

class Student : private std::string, private std::valarray<double> {
    ...
public:
    using std::valarray<double>::min;
    using std::valarray<double>::max;
    
    using std::valarray<double>::operator[];
    ...
};

现在 minmax[] 变成了公有方法

多重继承

多重继承会出现两个问题:

  • 重名函数使得方法调用出现二义性
  • 基类对象重复

虚基类

用来解决基类对象重复的问题

假设我们现在有这样几个类:

class People {
public:
    People(...);
    ...
};

class Doctor : public People {
public:
    Doctor(...) : People(...);
    ...
};

class Teacher : public People {
public:
    Teacher(...) : People(...);
    ...
};

class TeacherDoctor : public Doctor, public Teacher {
public:
    TeacherDoctor(...) : Doctor(...), Teacher(...);
    ...
};

我们发现 TeacherDoctor 会包含两个 People

当我们希望得到 People 对象时,需要强制类型转换:

TeacherDoctor td;

People* p1 = (Doctor*) &td;
People* p2 = (Teacher*) &td;

这样将使得使用基类指针来引用不同的对象(多态性)复杂化

有时,我们并不需要包含多个 People(比如在这个例子中),我们使用虚基类来解决这个问题:

class People {
public:
    People(...);
    ...
};

class Doctor : public virtual People {
public:
    Doctor(...) : People(...);
    ...
};

class Teacher : public virtual People {
public:
    Teacher(...) : People(...);
    ...
};

class TeacherDoctor : public Doctor, public Teacher {
public:
    TeacherDoctor(...) : Doctor(...), Teacher(...), People(...);
    ...
};

这样,TeacherDoctor 就只包含一个 People

注意看构造函数,如果类有虚基类,除非只需要使用该虚基类的默认构造函数,否则必须显式调用该虚基类的某个构造函数

构造函数调用顺序:

  • 虚基类构造函数,按声明次序?
  • 非虚基类构造函数,按声明次序
  • 类成员构造函数,按声明次序
  • 派生类初始化列表
  • 派生类初始化函数体

可以混合使用虚基类和非虚基类,比如:

class B {};

class C : public virtual B {};
class D : public virtual B {};

class X : public B {};
class Y : public B {};

class M : public C, public D, public X, public Y {};

这样 M 包含三个 B 子对象

多个同名方法

在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域,二者作用域不同:基类在外层,派生类在内层。

  • 如果派生类声明了一个和基类成员同名的新成员,那么会覆盖基类成员
  • 如果派生类声明了一个和基类成员同名的新函数,那么会覆盖基类所有同名的重载函数

假设我们需要调用 derived.show() 方法

在单继承中,如果派生类中没有定义 show() 方法,那么使用最近祖先中的定义

但是在多重继承中,如果每个直接祖先都有一个 show() 方法,这会方法调用出现二义性

可以使用作用域解析运算符:

derived.Base1::show();

更好的方法是在派生类中重新定义 show

类模板

模板的具体实现被称为实例化或者具体化

可以有表达式参数,如:

template <typename T, int MAX>
class Array { ... };

表达式参数可以是整数、枚举、引用或者指针(不能是浮点)

模板的多功能性

可以将用于常规类的技术用于模板类,它可以做基类、组件类,也可也作为其他模板的类型参数

Array<Arrat<int, 10>, 10> array;

模板可以有默认类型:

template <typename T1, typename T2 = int>
class C { ... };

虽然可以为类模板类型参数提供默认值。但是对于函数模板来说,不能为模板类型参数提供默认值,可以为非类型参数提供默认值

模板的实例化

类模板与函数模板很相似,因为可以有隐式实例化、显式实例化

模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明

隐式实例化:声明对象,编译器会自动生成具体类定义

注意,编译器仅在需要对象时才进行隐式实例化,如:

Array<double, 30>* pt;
pt = new Array<double, 30>;

编译器会在第二句进行实例化,第一句不会

当使用 template 并指出所需类型来声明类时,编译器将生成类声明的显式实例化,声明必须位于模板定义所在的名称空间中,如:

template class Array<double, 30>;

这句话会显示生成类声明

模板的全特化与偏特化

通过全特化一个模板,可以对一个特定参数集合自定义当前模板,类模板和函数模板都可以全特化

全特化的模板参数列表应当是空的,并且应当给出"模板实参"列表:

template <>
class Array<double, 30>{ ... };

这样当 Array 的类型参数是 double30 时,编译器会生成我们新写的这个全特化类声明

函数模板的特化可以进行类型推导

全特化会立刻生成类的定义

偏特化类似于全特化,偏特化也是为了给自定义一个参数集合的模板,但偏特化后的模板需要进一步的实例化才能形成确定的签名:

template <typename T>
class Array<T, 30>{ ... };

函数模板不允许偏特化

成员模板

模板可用于结构体、类或模板类的成员:

template <typename T>
class C {
private:
    template <typename V>
    class D { ... };
    
public:
    template <typename U>
    U func(U u, T t);
};

template <typename T>
template <typename U>
U C<T>::func(U u, T t) { ... }

在外面定义时需要嵌套 template

将模板用作参数

模板可以包含类型参数和非类型参数,它还可以包含本身就是模板的参数,如:

template <template <typename T> class Thing>
class Crab

其中,template <typename T> class 是类型,Thing 是参数名

假设有下面的声明:

Crab<King> legs;

那么我们需要声明 King 为:

template <typename T>
class King { ... };

这样才能让它匹配

模板类与友元

模板类声明也可以有友元,模板的友元分为三类:

  • 非模板友元
  • 约束(bound)模板友元,即友元的类型取决于类被实例化时的类型
  • 非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元

非模板友元函数

https://blog.csdn.net/liujun3512159/article/details/123039906

该模板类的友元函数只是一个普通函数,并且该函数是非模板函数或该函数不视为模板函数

第一种是非模板函数,如:

template <typename T>
class HasFriend {
public:
    friend void counts();
}

上面的代码中,counts 函数成为模板所有实例化的友元(例如 HasFriend<int>HasFriend<std::string> 等)

counts 函数不是对象调用,也没有对象参数。但是它可以通过访问全局对象、创建自己的 HasFriend 对象等方式拿到 HasFriend 对象

第二种是不视为模板函数,如:

template <typename T>
class HasFriend {
public:
    friend void report(const HasFriend<T> &);
}

上面的代码中,友元函数带有参数,并且参数中含有模板类定义的类型变量

由于我们不将该函数视为模板函数,因此对模板类的每个实例化版本都需要提供该函数的一个重载版本,如:

void report(const HasFriend<int> &) { ... }
void report(const HasFriend<double> &) { ... }

这样,我们就有了两个友元函数

需要注意,这种用法编译器将会给出警告

约束模板友元函数

如果使用前面第二种方法来定义友元函数,那么局限性非常大:每当增加一个模板类的具体实例,就要相对应提供友元函数的一个重载版本

最好的办法就是应该将友元函数使用模板来实现,即约束模板友元函数,表示模板友元函数实例化取决于模板类被实例化时的类型

我们需要先声明友元函数,然后在类声明中再次声明:

template <typename T> void counts();
template <typename T> void report(T &);

template <typename TT>
class HasFriendT {
public:
    friend void counts<TT>();
    friend void report<>(HasFriendT<TT> &); // 这里自动推断出函数模板的类型参数
};

为啥叫约束呢?

举个例子,HasFriendT<int> 的友元只有:

  • void counts<int>();
  • void report<HasFriendT<int>>(HasFriendT<int> &);

其实与非模板友元函数差不多,只不过不需要手动重载

非约束模板友元函数

指的是友元函数的所有实例化版本都是模板类的每一个实例化版本的友元,如:

template <typename T>
class ManyFriend {
public:
    template <typename C, typename D>
    friend void show(C&, D&);
};

ManyFriend<int> 的友元是:

template <typename C, typename D>
void show(C&, D&);

不管 CD 是什么,都是它的友元,是 1 对无穷的关系

所以叫它非约束

模板别名(C++ 11)

template <typename T>
using arrtype = std::array<T, 12>;

arrtype<int> arri;