ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

13. C++代码重用

13. C++代码重用

C++代码重用

包含对象成员的类

组合(Composition):在类里面直接定义另一个类的对象作为成员变量。外部类只能通过成员的公有接口操作它(无法直接访问私有成员)。相比于继承属于低耦合。

构造顺序:先成员,后自己。析构顺序:先自己,后成员。

组合(成员对象):整体控制部件的生命周期(构造时创建,析构时销毁)。

聚合(指针/引用):整体使用部件,但不负责部件的创建和销毁(由外部管理)。

valarray 类

valarray 是由头文件 <valarray> 支持的模板类(template class),专门处理数值(或具有类似特性的类型),支持对数组所有元素求和、找出最大最小元素等操作。因为是模板类,声明对象时必须指定具体数据类型,如 valarray<int>、valarray<double>。其构造函数支持:长度为零的空数组、指定长度的空数组、所有元素初始化为指定值的数组、用常规数组初始化的数组

explicit 关键字:防止单参数构造函数的隐式转换,避免意外行为

explicit Student(const std::string & s) : name(s), scores() {}

成员初始化顺序:初始化列表含多个项目时,成员被初始化的顺序是它们在类定义中被声明的顺序,而不是它们在初始化列表中的书写顺序。

私有继承

私有继承中,基类的公有成员和保护成员都成为派生类的私有成员。派生类继承实现,但不继承接口

方式初始化语法示例
包含使用成员名: name(str), scores(pd, n)
私有继承使用类名: std::string(str), ArrayDb(pd, n)

“无论什么继承,基类子对象都是无名的。公有继承只是允许编译器自动把基类成员‘合并’到派生类的名字查找范围内。”

“私有继承切断了这种自动合并,迫使程序员必须手动使用 基类名:: 来访问那个原本就无名的基类子对象。”

但对于组合中的成员对象,天生是“有名”的(因为必须写变量名)。

继承中的访问控制

基类私有成员(private):无论何种继承方式(public、protected、private),派生类内部和外部永远无法直接访问。只能通过基类提供的公有(public)或保护(protected)成员函数间接操作。

私有继承(class Derived : private Base)的效果:把基类的 public 和 protected 成员,全部降级为派生类的 private 成员。即派生类内部还能用(因为降级后成了自己的私有成员),但派生类的子类(下一级)和外部对象都访问不到了。

私有继承中调用基类友元函数

友元函数不属于类,不能用类名显式限定函数名;但可以通过显式转换为基类来调用正确的基类版本。例如 operator<<(ostream&, const Student&) 内部写 os << (const string&)stu;,显式把 stu 转换为 string 引用,从而调用 operator<<(ostream&, const string&)。

引用不会自动转换。在私有继承中,不进行显式类型转换就不能把指向派生类的引用或指针赋给基类引用或指针。

规则(限制):即使使用公有继承也必须显式转换,原因有二:其一,不转换时 os << stu 会匹配友元函数自身原型,导致递归调用;其二,多重继承下编译器无法确定应转换到哪个基类(若两个基类都提供 operator<<)。

// 私有继承中调用基类友元:显式转换为基类 std::ostream& operator<<(std::ostream& os, const Student& stu) { os << (const std::string&)stu; // 显式转换后调用 string 的 operator<< return os; }
使用 using 重新定义访问权限

使用 using 声明(类似命名空间中的用法)指出派生类可以使用特定的基类成员,即使采用的是私有派生。例如在 Student 的公有部分写 using std::valarray<double>::min; 与 using std::valarray<double>::max;,使它们像 Student 的公有方法一样可用;写 using std::valarray<double>::operator[]; 可使 const 与非 const 两个版本都可用,从而删除手写的 operator[] 原型与定义。

私有/保护派生把基类公有方法降级后,外部无法调用;若需要把这些方法重新暴露为公有,除“包装成新方法”外,using 声明是更简洁的机制。

规则(限制):using 声明只使用成员名——不带圆括号、函数特征标和返回类型。 using 声明只适用于继承,不适用于包含。 老式做法(在派生类公有部分重新声明方法名,如 std::valarray<double>::operator[];)已被摒弃,编译器支持时应改用 using 声明。

// using 声明:把私有继承来的基类方法重新暴露为公有 class Student : private std::valarray<double> { public: using std::valarray<double>::min; // 使 min 成为公有方法 using std::valarray<double>::max; // 使 max 成为公有方法 using std::valarray<double>::operator[]; // 使 const/非 const 两版可用 };
对象切片(Object Slicing)

什么是切片:当把派生类对象赋值/拷贝/按值传递/按值返回给基类类型时,编译器只拷贝基类的那部分成员,派生类新增的成员变量被直接“切掉”丢弃。

原对象会不会被截断。 切片发生时,原对象(derived)完好无损。被截断的是新生成的副本(比如赋值的目标变量 Base a,或者函数形参)。

指针(Base*)和引用(Base&)。它们只是“指向原对象,不会产生新对象,所以永远不会有数据丢失。

什么时候发生切片?:

赋值:Base b = derived;

函数值传递:void func(Base b); func(derived);

函数值返回:Base func() { Derived d; return d; }

显式强转成值类型:Base a = static_cast<Base>(derived);(这会创建一个截断的临时对象)。

函数与虚表(VTable)

非虚函数(普通函数):

  • 函数代码在代码段,不占对象内存。

  • 切片后,新对象调用的非虚函数由编译时类型决定(即 Base 版本)。重写函数和重定义函数都会被恢复到基类版本

  • 这不算“截断”,只是作用域隐藏(因为编译器只看指针/引用的类型)。

虚函数(多态行为):

  • 对象内存里虽没有函数代码,但有一个虚表指针(vptr)。

  • 切片发生时,新生成的 Base 对象,其 vptr 会被重置为指向 Base 类的虚函数表。

  • 因此,派生类的虚函数行为被截断了(即使数据没丢,但多态性丢失了)。

传递方式 (形参类型)实参传入方式调用的虚函数(func1)调用的普通函数(func2)
基类指针void test(Base* p)test(&derived)派生类版本(动态绑定)基类版本(静态绑定)
基类引用void test(Base& p)test(derived)派生类版本(动态绑定)基类版本(静态绑定)
基类值传递void test(Base p)test(derived)基类版本(切片重置 vptr)基类版本(类型降级)
赋值运算符Base b; b = derived;将派生类赋给已有的基类对象基类版本b本身就是基类,vptr 指向基类表)基类版本(类型降级)
基类指针 → 强转回派生类指针(向下转型)Derived* d = static_cast<Derived*>(b);(必须确保b实际指向派生类)派生类版本(原对象的 vptr 未变)派生类版本(静态绑定,看强转后的类型)
基类引用 → 强转回派生类引用(向下转型)Derived& d = static_cast<Derived&>(b);派生类版本(原对象的 vptr 未变)派生类版本(静态绑定,看强转后的类型)
基类对象->强转赋值给派生类对象未定义未定义

dynamic_cast :运行时去查虚表(vtable),比较类名字符串,有轻微性能开销。向下转型时,使用dynamic_cast 才安全。

基类中必须至少有一个虚函数(通常是虚析构函数),dynamic_cast 才能工作。

多重继承(MI)

多重继承(Multiple Inheritance,MI)描述的是有多个直接基类的类。

语法
class SingingWaiter : public Waiter, public Singer { // ... };

此时Waiter和 Singer都是公有继承。

class SingingWaiter : public Waiter, Singer { // Singer 是私有继承 // ... };

Waiter是公有继承, Singer是私有继承。默认是私有继承。

  1. 如果 Waiter 和 Singer 都继承自 Worker,则 SingingWaiter 包含两个 Worker 子对象

    class Worker { ... }; class Waiter : public Worker { ... }; class Singer : public Worker { ... }; class SingingWaiter : public Waiter, public Singer { ... };

    此时使用Worker对象会产生二义性。

    SingingWaiter ed; Worker * pw = &ed; // ❌ 二义性!哪个 Worker 子对象?

    此时通过显示类型转换可以解决

    Worker * pw1 = (Waiter *) &ed; // Waiter 中的 Worker Worker * pw2 = (Singer *) &ed; // Singer 中的 Worker

    两个 Worker 子对象导致多态性复杂化,以及数据冗余(姓名和ID各存两份)。

虚基类(Virtual Base Class)

Worker声明为WaiterSinger虚基类

class Waiter : virtual public Worker { ... }; class Singer : public virtual Worker { ... }; class SingingWaiter : public Waiter, public Singer { ... };

除非调用虚基类默认构造函数,否则要显式调用虚基类的构造函数。

// ✅ 正确的构造函数 SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other) : Worker(wk), Waiter(wk, p), Singer(wk, v) { }
  1. 如果 Waiter 和 Singer 都有 Show() 方法,调用 SingingWaiter::Show() 会产生二义性

解决方法1:作用域解析

newhire.Singer::Show(); // 明确使用 Singer 版本

解决方法2:在派生类中重新定义

void SingingWaiter::Show() { Singer::Show(); // 只显示 Singer 部分 }
混合虚/非虚继承的内存模型

虚途径共享一份,非虚途径各管各的。

class B { /* ... */ }; ​ // C 和 D 通过 虚继承 派生自 B class C : virtual public B { /* ... */ }; class D : virtual public B { /* ... */ }; ​ // X 和 Y 通过 非虚(普通)继承 派生自 B class X : public B { /* ... */ }; class Y : public B { /* ... */ }; ​ // M 同时继承了这四条路径 class M : public C, public D, public X, public Y { /* ... */ };

当创建 M 对象时,内存中包含的 B 子对象个数为:

  • 来自虚途径(C 和 D):C++ 保证虚基类在整个继承树中只有一份。因为 C 和 D 都是虚继承自 B,所以它们共享同一个 B 子对象。 → 1 份

  • 来自非虚途径(X 和 Y):非虚继承各自独立。X 包含一个独立的 B,Y 包含另一个独立的 B。 → 2 份

  • 总数:1 (虚共享) + 2 (非虚独立) = 3 个 B 子对象。

虚继承下的支配规则(Dominance Rule)

在虚继承中,如果派生类(较近的类)中有同名的成员,它会“遮蔽”基类中的同名成员,即使派生类的这个成员是私有的,它依然优先于基类的公有成员。

但非派生类的兄弟类之间的同名会冲突(二义性)。

class C { public: int omg(); }; class E { public: int omg(); }; // E 和 C 平级 class F : public C, public E { /*...*/ };

F中调用omg()报错(二义性)。

class B { public: int val; }; // B 的 val 是公有 class C : virtual public B { private: int val; }; // C 的 val 是私有 class F : public C { /*...*/ };
  1. 编译器先查支配规则:C::val派生自B,离F更近 →C::val支配B::val

  2. 编译器锁定C::val作为目标。

  3. 编译器检查访问权限:发现C::valprivate

  4. 最终结果:编译报错,但报错是C::val是私有的,无法访问”,而不是“存在二义性”!

类模板

类模板是编译器用于生成类的编译期指令。它本身不产生符号表条目,也不产生机器码。

模板类示例
// 模板定义(必须放头文件) template <typename T> class Stack { T items[10]; public: Stack(); bool push(const T&); }; ​ template <typename T> Stack<T>::Stack() { /* 实现 */ } ​ // 使用(必须显式指定类型) Stack<int> intStack; Stack<std::string> strStack;
语法规则(类型参数)

声明:template <typename T> 或 template <class T>。

T 是占位符,代表任意类型。

在类定义内部,T 可以直接用于声明成员变量、成员函数参数和返回值。

注:

  1. 模板声明:使用template <class Type> 或 template <typename Type>

  2. 类限定符使用 Stack<Type>:: 而非 Stack::

  3. 使用场景模板定义位置是否可行
    单文件(所有代码在同一个.cpp任意位置(调用前可见即可)✅ 可行
    多文件(声明与定义分离)定义在.cpp,声明在.h链接报错
    多文件共享(标准做法)完整定义放在.h✅ 推荐
    多文件 + 显式实例化定义在.cpp,并在末尾显式实例化✅ 可行(但不灵活)
  4. 在类声明中定义的方法可省略模板前缀

模板定义的位置(编译期可见性)

核心原因:C++ 编译器采用独立编译模型。

编译 .cpp 时,编译器只处理当前翻译单元。它必须在当前文件中看到完整的模板定义(而不仅仅是声明),才能针对具体类型(如 int)生成机器码。

如果把定义放在 .cpp 中,编译主文件时只看到声明,链接器会因找不到 Stack<int>::push 的实现而报错(未定义引用)。

实例化

隐式实例化:当代码中定义对象(如 Stack<int> st;)时发生。

显式实例化:在 .cpp 中写 template class Stack<int>;,强制在当前位置生成代码(用于减少编译时间或处理库封装,但不推荐常规使用)。

类模板与函数模板的区别

函数模板:编译器可以根据实参类型自动推导模板参数(如 max(1, 2) 推导为 int)。

类模板:不能自动推导类型。必须在对象声明时用尖括号明确指定(如Stack<std::string> st;),除非使用 C++17 的类模板参数推导(CTAD)。

模板实例化

类模板被实例化时,编译器只是将模板参数替换为具体类型,然后编译生成的类。如果生成类型不支持代码中的操作,则编译失败;如果支持但语义不符合预期(如指针只复制地址,不复制对象),则运行时逻辑错误。

模板对赋值操作的要求

类模板中常出现 item = items[--top] 这样的语句。当 Type 是数组类型(如 char[40])时,赋值操作不合法(数组不能作为左值被赋值),编译失败。

非类型模板参数与数组模板

模板的主要动机之一是为容器类(container class)提供可复用代码:类型参数(type parameter)非常适合把相同的存储方案复用到不同类型上。除了类型参数,模板还能携带“非类型参数”,用于在编译期把数组大小等常量并入类型本身。

非类型(表达式)模板参数

模板参数除了类型参数外,还可以是非类型参数(non-type parameter),也称表达式参数(expression parameter)。它指定的不是泛型名,而是一个具体的类型,例如 template <class T, int n> 中,T 是类型参数,n 是类型为 int 的非类型参数。实例化(instantiation)时,编译器用具体值替换 n,从而在类内部直接声明定长数组。声明 ArrayTP<double, 12> eggweights; 会让编译器定义出名为 ArrayTP<double, 12> 的具体类,并以 double 替换 T、以 12 替换 n,随后创建该类型的 eggweights 对象。C++11 标准库的 array 容器正是采用这种方式在编译期指定数组大小。

// 数组模板:类型参数 T + 非类型参数 n(数组大小) template <class T, int n> // T 为类型参数,n 为 int 型非类型参数 class ArrayTP { private: T ar[n]; // 用非类型参数 n 声明编译期定长的内置数组 public: explicit ArrayTP(const T& v); // 构造函数:以 v 作为每个元素的初值 virtual T& operator[](int i); // 非 const 版本:返回引用以便修改元素 virtual T operator[](int i) const; // const 版本:只读访问 };

把大小直接并入类型,使数组的存储布局在编译期完全确定,既不需要运行时内存管理,也让“不同大小即不同类型”成为语言层面的保证。

非类型参数的类型只能是整型、枚举、引用或指针。因此 double m 不合法,而 double* rm(double 型引用)和 double* pm(double 型指针)合法。 模板代码不能修改非类型参数的值,也不能取它的地址,所以 n++、&n 这类表达式非法。 实例化时,用作非类型实参的值必须是常量表达式(constant expression),即编译期可求值。

栈内存方式与堆内存方式的差异

改变数组大小有两种实现方式。非类型参数法直接以内置数组作为类成员,内存来自为自动变量(automatic variable)维护的栈;构造函数参数法(如 Stack 模板)则通过 new/delete 管理堆内存,大小作为构造参数传入。

原因:栈内存由编译器自动分配与回收,不存在堆分配的内存管理开销,因此执行速度更快,尤其在需要大量小型数组时差异明显。

规则(限制):栈内存在作用域结束时自动销毁,生命周期由作用域决定;堆内存的分配与释放需显式管理,多出 new/delete 的开销和出错风险

每种数组大小生成一个独立模板类

非类型实参属于类型的一部分,因此下面的声明生成两个独立的类:ArrayTP<double, 12> eggweights; 与 ArrayTP<double, 13> donuts; 对应 ArrayTP<double, 12> 和 ArrayTP<double, 13> 两个互不相干的类型。而构造函数方式只生成一个 Stack<int> 类,数组大小以参数形式传给构造函数,Stack<int> eggg(12); 与 Stack<int> dunkers(13); 只是同一类型的不同对象。

原因:编译器按模板实参组合逐一实例化,非类型实参一旦不同,生成的类就不同。

规则(限制):使用很多不同尺寸会生成大量类,造成代码体积膨胀;使用构造函数方式则没有这一问题。

构造函数方式更具通用性

构造函数方式把数组大小作为类成员存储,而不是硬编码进类型定义中。

原因:大小一旦成为普通成员,就不再约束对象之间的赋值关系——可以把一种尺寸的数组对象赋给另一种尺寸的数组对象,也能构造出数组大小在运行时可变的类。这是硬编码大小的非类型参数法无法做到的。

规则(限制):ArrayTP<double, 12> 与 ArrayTP<double, 13> 属于不同类,两者之间不能互相赋值;构造函数方式则需要额外的内存管理代码来维护动态数组。

模板的多功能性

用于常规类的技术同样适用于模板类。模板类可以用作基类(base class)、组件类(component class),也可以用作其他模板的类型参数。例如:一个模板可以继承另一个模板(GrowArray 继承 Array),一个模板可以持有另一个模板的实例作为成员(Stack 内含 Array<TP>),还可以写出“栈的数组”这种嵌套结构。

原因:模板之间可以相互组合、层层复用,把可复用代码的粒度从“单个类”提升到“一类结构”,这是容器类可组合性的基础。

规则(限制):在 C++98 中,嵌套模板的相邻右尖括号之间必须至少有一个空格,写成 Array< Stack<int> >,以免与右移运算符 >> 混淆;C++11 起不再有此要求,可直接写 Array<Stack<int>>。

template <typename T> class Array { // 通用数组模板 private: T entry; // 单个元素 public: T& operator[](int i); // 元素访问声明 }; ​ template <typename Type> class GrowArray : public Array<Type> { // 继承:模板用作基类 // 扩展成员 }; ​ template <typename TP> class Stack { private: Array<TP> ar; // 组件:模板实例用作成员 }; ​ Array< Stack<int> > asi; // 嵌套:类型实参是另一个模板
模板的递归使用(构造多维数组)

模板可以递归使用——把一个模板的实例作为另一个模板实例的类型实参,从而构造多维结构。ArrayTP< ArrayTP<int, 5>, 10 > twodee; 表示一个含 10 个元素、每个元素都是含 5 个 int 的数组,与常规声明 int twodee10; 等价。

原因(为什么):解决了“用模板表达多维数组”的问题,使任意维度都可声明为编译期定长的模板类型。

规则(限制):模板语法中维度的顺序与等价的常规多维数组相反——外层实参(10)对应第二维,内层实参(5)对应第一维,即ArrayTP<ArrayTP<int,5>,10>对应int[10][5]而非int[5][10]

// 递归使用模板:外层实参是内层模板的实例 ArrayTP< ArrayTP<int, 5>, 10 > twodee; // 10 个元素,每个是含 5 个 int 的数组 // 等价于:int twodee[10][5];(维度顺序相反) twodee[i][j] = value; // 与二维数组相同的下标访问方式
多个类型参数

模板可以包含多个类型参数,让一个类同时保存两种不同类型的值。典型例子是 Pair<T1, T2> 模板(标准模板库提供了类似的 pair)。

原因:解决“一个类需要同时存储两种不同类型”的场景,例如把“名称”与“数值”成对保存。

规则(限制):实例化后的类名是完整实参组合 Pair<T1, T2>,而不是 Pair;Pair<string, int> 与 Pair<char*, double> 是彼此完全不同的类。返回数据成员引用的 first()、second() 既能读取也能通过赋值修改值。

// 多类型参数模板:Pair 保存两个不同类型的值 template <class T1, class T2> class Pair { private: T1 a; // 第一个值 T2 b; // 第二个值 public: Pair(const T1& aval, const T2& bval) : a(aval), b(bval) {} T1& first() { return a; } // 返回引用:可读取也可修改 T2& second() { return b; } T1 first() const { return a; } // const 版本:只读 T2 second() const { return b; } };
默认模板参数

可以为类模板的类型参数提供默认值,例如 template <class T1, class T2 = int> class Topo {...};。实例化时若省略该实参,编译器使用默认类型:Topo<double> m2; 中 T1 为 double、T2 取默认值 int。

原因:减少实例化时反复书写常用类型的重复劳动;标准模板库常把默认类型设为某个类,简化用户代码。

规则(限制):默认值按“省略则取默认”生效,Topo<double, double> m1; 显式给出两个类型时默认值不起作用。非类型参数也可以提供默认值,这对类模板和函数模板都适用;函数模板的类型参数默认值在 C++98 中不允许,C++11 起才允许提供。

// 默认类型模板参数:省略 T2 时使用 int template <class T1, class T2 = int> class Topo { private: T1 first; // 第一个成员 T2 second; // 第二个成员,默认 int public: void set(const T1& f, const T2& s); // 成员函数声明 }; ​ Topo<double, double> m1; // 显式给出两个类型 Topo<double> m2; // 省略 T2,使用默认值 int
模板的具体化(specialization)概述

模板以泛型(generic)方式描述类,具体化则是用具体类型生成类声明的过程。类模板与函数模板一样,存在隐式实例化(implicit instantiation)、显式实例化(explicit instantiation)和显式具体化(explicit specialization),它们统称为具体化。

原因:同一份泛型代码需要按不同实参生成不同版本,具体化机制决定了“何时生成”以及“生成哪个版本”。

规则(限制):四种形式的选择遵循优先级规则——泛型模板是兜底,具体化程度越高越优先

隐式实例化

声明一个或多个对象并指明所需类型,编译器按通用模板提供的“处方”生成具体的类定义。例如 ArrayTP<int, 100> stuff; 即触发隐式实例化。

原因(为什么):编译器按“用到了才生成”的原则工作,避免生成无用代码。

规则(限制):编译器在需要对象之前不会生成隐式实例化。仅声明指针不会触发类定义生成——ArrayTP<double, 30>* pt; 只是声明指针;pt = new ArrayTP<double, 30>; 因需要对象,此刻才生成类定义并创建对象。

ArrayTP<int, 100> stuff; // 隐式实例化:生成类定义并创建对象 ArrayTP<double, 30>* pt; // 仅声明指针,暂不生成类定义 pt = new ArrayTP<double, 30>; // 需要对象,此刻才生成类定义
显式实例化

使用关键字 template 并指明所需类型来声明类,编译器直接生成类声明(包括方法定义),即使没有创建或提及任何类对象。

原因:主动强制生成指定版本,可用于控制类定义在何处、何时生成,便于跨编译单元管理代码生成。

规则(限制):该声明必须位于模板定义所在的命名空间中;与隐式实例化一样,显式实例化仍依据通用模板生成具体化。

// 显式具体化:为 const char* 提供专用定义 template <> class SortedArray<const char*> { // 实现中使用 strcmp() 而非 > 比较字符串 }; ​ SortedArray<int> scores; // 使用通用模板 SortedArray<const char*> dates; // 使用显式具体化版本
部分具体化

部分具体化(partial specialization)是部分限制模板的通用性——例如给多个类型参数之一指定具体类型:template <class T1> class Pair<T1, int> { ... };。template 后尖括号内声明的是未被具体化的参数;若所有类型参数都被指定,尖括号内为空,即退化为显式具体化。也可为指针实参提供专用版本:template <class T> class Feeb<T*> { ... };。

原因:针对“某一类特殊情形”(某参数为 int、或实参是指针等)提供不同实现,同时其余参数仍保持泛型,兼顾定制与复用。

规则(限制):有多个模板可供选择时,编译器使用具体化程度最高的版本:通用模板 < 部分具体化 < 显式具体化。 指针部分具体化只在实参是指针时命中:Feeb<char> 用通用版,Feeb<char> 用指针专用版;若未提供指针具体化,后者也会按通用模板处理。 可以对多个参数组合施加限制,如 Trio<T1, T2, T2>(第三个参数固定为第二个的类型)、Trio<T1, T1, T1*>(后两个参数固定为第一个的指针)。

// 部分具体化:T2 固定为 int,T1 保持泛型 template <class T1> class Pair<T1, int> { // 为第二参数为 int 的情形定制实现 // ... }; ​ // 指针部分具体化:实参是指针时命中 template <class T> class Feeb<T*> { // 指针专用实现 // ... }; ​ Pair<double, double> p1; // 使用通用模板 Pair<double, int> p2; // 使用部分具体化版本 Feeb<char> fb1; // 使用通用版本 Feeb<char*> fb2; // 使用指针部分具体化版本
成员模板

模板可以用作结构、类或模板类的成员,这样的模板成员称为成员模板(member template)。一个模板类可以把另一个嵌套模板类和模板成员函数都作为自己的成员。完全实现 STL 的设计必须使用这项特性。例如外层模板 beta 在私有部分声明了嵌套模板类 hold,并用它声明两个数据成员 hold<T> q; 和 hold<int> n;——前者基于外层模板参数 T,后者基于固定类型 int;同时声明了模板成员函数 blab。

原因:成员模板让“外层类的类型参数”与“内部方法/成员的类型参数”相互独立:T 由对象实例化时的实参决定,而成员模板方法 blab 的 U 由该方法被调用时的实参决定。beta<double> guy(3.5, 3); 使 T 为 double、q 为 hold<double>;guy.blab(10, 2.3); 则因第一个实参 10 把 U 确定为 int。

规则(限制):嵌套模板类声明在私有部分时,只能在宿主类内部访问。 返回值类型是 U 而非计算结果的类型:guy.blab(10, 2.3) 中混合类型使计算以 double 进行,但返回类型 U 为 int,结果被截断为 28;改用 guy.blab(10.0, 2.3) 后 U 为 double,输出 28.2608。 第二个参数的类型由对象声明时的 T 决定,不由函数调用决定:guy.blab(10, 3); 仍实例化为 blab(int, double),并按常规函数原型规则把 3 转换为 double。

// 成员模板:外层模板 beta 包含嵌套模板类 hold 与模板方法 blab template <typename T> class beta { private: template <typename V> // 嵌套成员模板类 class hold { V val; public: hold(V v) : val(v) {} V value() const { return val; } }; hold<T> q; // 基于外层参数 T 的成员 hold<int> n; // 基于固定类型 int 的成员 public: template <typename U> // 成员模板方法声明 U blab(U u, T t); // 返回类型由调用实参决定 };
模板用作模板参数(template template parameter)

模板参数本身可以是一个模板,称为模板模板参数(template template parameter)。语法为 template <template <typename T> class Thing> class Crab,其中 template <typename T> class 是参数的类型,Thing 是参数名。实例化 Crab<Stack> nebula; 时,实参 Stack 必须是一个模板类,且其声明与 Thing 的声明匹配(如 template <typename T> class Stack)。Crab 内部的 Thing<int> s1;、Thing<double> s2; 会被分别实例化为 Stack<int> 与 Stack<double>。

模板新增的这项特性用于实现 STL——让一个模板类能接收另一个模板类作为参数,再基于该模板类实例化自己的成员。

规则(限制):宿主模板会对参数模板的接口做假设。例如 Crab 要求 Thing 表示的类具备 push()、pop() 方法且接口匹配;任何声明匹配且具备这些成员的模板类都可用作实参。若不满足假设,编译将失败。

// 模板模板参数:Thing 本身是模板类参数 template <template <typename T> class Thing> class Crab { private: Thing<int> s1; // 实参替换 Thing 后实例化 Thing<double> s2; public: bool push(int a, double x) { // 假设 Thing 提供 push 方法 return s1.push(a) && s2.push(x); } bool pop(int& a, double& x) { // 假设 Thing 提供 pop 方法 return s1.pop(a) && s2.pop(x); } };
模板参数与常规类型参数混合

模板模板参数可以与普通类型参数混合使用。template <template <typename T> class Thing, typename U, typename V> class Crab 中,Thing 是模板参数,U、V 是普通类型参数;成员声明为 Thing<U> s1;、Thing<V> s2;,其存储的数据类型由 U、V 决定,而不是硬编码。

把成员的存储类型也参数化,使模板模板参数与类型参数各司其职——前者决定容器结构,后者决定元素类型。

规则(限制):实例化时必须同时给出模板实参与类型实参,如 Crab<Stack, int, double> nebula; 对应 Thing=Stack、U=int、V=double。

// 模板模板参数与类型参数混合使用 template <template <typename T> class Thing, typename U, typename V> class Crab { private: Thing<U> s1; // 成员类型由类型参数 U 决定 Thing<V> s2; // 成员类型由类型参数 V 决定 public: bool push(U a, V x) { // 接口类型对应 U、V return s1.push(a) && s2.push(x); } };
模板类友元的三种类型

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

非模板友元(non-template friend):一个普通函数是所有模板实例化的友元;

约束模板友元(bound template friend):友元的具体化类型取决于类被实例化时的类型,类的每个具体化只获得与之匹配的函数具体化;

非约束模板友元(unbound template friend):友元模板的所有具体化都是类的每一个具体化的友元。 原因(为什么):三类友元对应三种访问控制粒度——不加区分地授予全部实例、只授予同型实例、或跨类型自由授予,满足不同的“谁能访问私有成员”需求。

规则(限制):三类友元的声明语法、实例化时机和匹配方式各不相同,详见后续知识点。

  1. 非模板友元函数

    在模板类中把一个常规函数声明为友元,如 friend void counts();,该声明使 counts() 成为模板所有实例化的友元——它既是 HasFriend<int> 的友元,也是 HasFriend<string> 的友元。该函数不是成员函数,也没有对象参数,访问对象的方式包括:访问全局对象、用全局指针访问非全局对象、创建自己的对象、访问独立于对象的模板类静态数据成员(每个具体化拥有各自独立的静态成员)。

    某些操作(如统计所有实例的个数)与具体对象无关,只需访问静态成员或全局状态,用一个共享的非模板友元即可完成。

    规则(限制):不能用模板类名本身作参数:friend void report(HasFriend&); 不合法,因为不存在 HasFriend 这样的类型,只有具体化;提供模板类参数必须指明具体化,写为 friend void report(HasFriend<T>&);。 report 本身不是模板函数,只是使用模板作参数,因此必须为要用到的每个具体化单独定义重载版本(显式具体化)。

    // 非模板友元:一个函数是所有实例化的友元 template <typename T> class HasFriend { private: T item; // 数据成员 static int ct; // 静态成员:每个具体化独立一份 public: HasFriend(const T& i) : item(i) { ct++; } ~HasFriend() { ct--; } friend void counts(); // 非模板友元:所有实例化的友元 friend void report(HasFriend<T>&); // 带模板具体化参数的友元声明 }; ​ template <typename T> int HasFriend<T>::ct = 0; // 静态成员定义
  2. 约束模板友元函数

    让友元函数本身成为模板,并使类的每一个具体化都获得与它匹配的函数具体化——HasFriendT<int> 获得 int 版本的函数友元,依此类推。需要三个步骤:

    1. 在类定义之前声明模板函数原型(如 template <typename T> void counts(); 与 template <typename T> void report(T&););

    2. 在类内部按类模板参数再次声明模板为友元,指明具体化;

    3. 为友元提供模板定义。

    当友元函数需要访问具体化实例的私有成员,且不同具体化的处理方式可能不同(如统计每个具体化各自的静态计数)时,让函数模板按类的类型参数生成专属版本。

    规则(限制):声明中的 <> 表示这是模板具体化:friend void counts<TT>(); 必须用显式模板参数语法 <TT> 指明具体化,因为 counts 没有可推导具体化的函数参数。 friend void report<>(HasFriendT<TT>&); 中 <> 可以为空,因为编译器可从函数参数 HasFriendT<TT> 推断出模板类型参数;也可显式写为 report<HasFriendT<TT>>(HasFriendT<TT>&)。 调用无参数的具体化必须显式给出模板实参(如 counts<int>()),有参数的则可从实参推导(如 report(hfi1))。 每种 T 类型都有自己独立的友元函数具体化(表现为各自报告不同的 sizeof)。

    // 步骤1:类定义前声明模板函数原型 template <typename T> void counts(); template <typename T> void report(T&); ​ // 步骤2:在类内按类型参数声明具体化为友元 template <typename TT> class HasFriendT { private: TT item; // 数据成员 static int ct; public: HasFriendT(const TT& i) : item(i) { ct++; } ~HasFriendT() { ct--; } friend void counts<TT>(); // 无参数:必须用 <TT> 指明具体化 friend void report<>(HasFriendT<TT>&); // 可从参数推导具体化 }; ​ // 步骤3:为友元提供模板定义 template <typename T> void report(T& hf) { // 可访问 hf 的私有成员 item // 使用 hf.item }
  1. 非约束模板友元函数

    在类内部声明模板,可创建非约束友元函数——每个函数具体化都是每个类具体化的友元。非约束友元的模板类型参数与模板类的类型参数相互独立,例如 ManyFriend<T> 中声明 template <typename C, typename D> friend void show2(C&, D&);,show2 的 C、D 与类的 T 无关。

    当某个函数需要同时访问两个不同具体化的私有成员时(如打印一个 ManyFriend<int> 和一个 ManyFriend<double> 的元素),约束友元无法胜任——它只能匹配同型对象;非约束友元因对所有具体化开放而可以跨类型访问。

    规则(限制):show2(hfi1, hfi2) 匹配具体化 show2<ManyFriend<int>&, ManyFriend<int>&>,作为所有 ManyFriend 具体化的友元,它有权访问所有具体化的 item 成员,但实际只访问所传对象。 show2(hfdb, hfi2) 匹配 show2<ManyFriend<double>&, ManyFriend<int>&>,可同时访问 ManyFriend<double> 与 ManyFriend<int> 的 item 成员。 非约束友元放宽了访问范围(跨类型),代价是任何具体化组合都能被授权访问,需权衡封装性。

    // 非约束模板友元:函数类型参数与类类型参数相互独立 template <typename T> class ManyFriend { private: T item; // 数据成员 public: ManyFriend(const T& i) : item(i) {} // 模板声明在类内部:每个函数具体化是每个类具体化的友元 template <typename C, typename D> friend void show2(C& c, D& d); // C、D 与类的 T 无关 }; ​ template <typename C, typename D> void show2(C& c, D& d) { // 可访问任意 ManyFriend 具体化的 item // 使用 c.item 与 d.item }
    总结
    知识点描述
    模板友元三类非模板友元(全实例共享)、约束模板友元(同型匹配)、非约束模板友元(全部具体化互相开放)。
    非模板友元常规函数成为所有实例化的友元;参数须用具体化类型(HasFriend<T>&),不能写HasFriend&;须为每个用到的具体化提供重载定义。
    约束模板友元三步:先声明模板原型、类内用<TT>指明具体化声明友元、再提供模板定义;无参函数须显式指定具体化,有参函数可推导。
    非约束模板友元在类内部声明模板友元,函数类型参数与类参数独立;每个函数具体化是每个类具体化的友元,可跨类型访问私有成员。
返回列表