前言
C++ 的两个强大且常被考查的主题就是 模板(templates) 和 继承/多态(inheritance & polymorphism)。模板把"泛型"和"编译期多态"带入语言,允许你写出与类型无关但高效的代码;继承把"运行时多态"带入语言,方便用公共接口操纵不同实现。
本文按教学博客的风格来写:先给出模板函数的使用与进阶技巧 (含现代 C++ 的常见高级手段),再讲 C++ 的继承特性(含虚函数、对象切片、虚继承等),每部分配上示例、常见陷阱与最佳实践,最后做统一总结。示例尽量贴近工程实战,适合直接放在博客里。
主要内容简介
-
1、模板函数基础:函数模板、类型推导、非类型模板参数、模板重载/特化
-
2、模板进阶:完美转发、折叠表达式、变参模板、SFINAE/enable_if、std::void_t 检测、C++20 Concepts
-
3、继承基础:单继承、派生类、访问控制、构造/析构顺序、切片问题
-
4、运行时多态:虚函数、纯虚函数(抽象类)、虚析构函数、override/final、dynamic_cast
-
5、多重继承与虚继承:菱形问题、虚基类、二义性解决
-
6、模板与继承的交互:CRTP、用模板约束继承关系(is_base_of / concepts)
-
7、常见坑 & 最佳实践总结
一、C++ 模板函数(从基础到进阶)
1.1 函数模板 --- 基本用法
cpptemplate<typename T> T Add(T a, T b) { return a + b; } int x = Add<int>(1, 2); // 显示指定 auto y = Add(1, 2); // 编译器推导为 int
要点:
模板定义通常放在头文件(因为编译器需要在使用点见到定义进行实例化)。
支持显式模板参数 或由编译器类型推导。
函数模板可以与普通函数重载共存,重载解析遵循普通函数重载规则 + 模板优先级。
1.2 非类型模板参数 & 模板重载/特化
cpptemplate<typename T, int N> class Array{ private: T _a[N]; }; int main(){ Array<int, 1000> a; }
非类型模板参数(如 N)允许在编译期传入常量。
函数模板 可以做完整特化 (rare),但不能做部分特化。部分特化只适用于类模板。
可以作为非类型模板参数的类型
整数类型(int, char, bool, 枚举等)
指针或引用(必须是指向具有静态存储期的对象或函数的常量指针/引用)
std::nullptr_t
注意:
浮点型及自定义类型无法作为非类型模版参数
模版的特化(针对某些类型进行特殊化处理):
在函数模板显式特化时,函数签名必须和主模板一致,只有模板参数的具体化不同,否则不是有效的特化。
cpptemplate<typename T> bool IsEqual(const T& left , const T& right){ return left == right; } // 模版的特化(特殊化处理) template<> bool IsEqual<char*>(char* const& left , char* const& right){ return (strcmp(left, right) == 0); } int main(){ int a = 0, b = 1; cout << IsEqual(a,b)<<endl; const char* p1 = "Hello"; const char* p2 = "World"; cout<< IsEqual(p1,p2) << endl; }
输出描述:
0
0
特化的分类:1、全特化。2、偏特化
cpptemplate<class T1, class T2> class Data{ public: Data(){ cout<< "Data<T1,T2>"<<endl; } private: T1 _d1; T2 _d2; }; template<> class Data<int, char>{ public: Data(){ cout<< "全特化 Data<int,char>"<<endl; } private: ; }; template<class T2> class Data<int,T2>{ public: Data(){ cout<<"偏特化 Data<int,T2>"<<endl; } private: ; }; int main(){ Data<short, short> d1; Data<int, char> d2; Data<int, double> d3; }
输出描述:
Data<T1,T2>
全特化 Data<int,char>
偏特化 Data<int,T2>
1.3 变参模板(variadic templates)与折叠表达式(C++17)
cpp// 递归版本(老式) template<typename T> T Sum(T v){ return v; } template<typename T, typename... Ts> T Sum(T head, Ts... tail) { return head + Sum(tail...); } // C++17 折叠表达式,简洁高效 template<typename... Ts> auto Sum2(Ts... args) { return (args + ... + 0); // fold expression }
1.4 转发引用与完美转发(perfect forwarding)
用于实现通用工厂/封装函数,避免不必要拷贝:
cpptemplate<typename T, typename... Args> std::unique_ptr<T> MakeUnique(Args&&... args) { return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); }
注意:
T&& 在模板上下文中可能是 转发引用(又称万能引用)。
使用 std::forward 将参数按原始值类别(左值/右值)传递,保留移动语义。
二、C++ 模板的分离编译
🔹 1. 模板的基本编译机制
-
普通函数 / 类:编译器在编译 .cpp 文件时就能生成确定的符号。
-
模板函数 / 类:只有在**实例化(instantiation)**时(即编译器遇到具体类型实参)才会生成代码。
👉 因此模板代码必须在编译时可见 ,这就是为什么我们通常把模板的实现,函数,类的声明都写在 .h 文件里,而不是 .cpp。
🔹 2. 为什么模板不能直接"分离编译"
假设你写了两个文件:
cpp// MyTemplate.h template<typename T> T add(T a, T b);
cpp// MyTemplate.cpp #include "MyTemplate.h" template<typename T> T add(T a, T b) { return a + b; }
然后在 main.cpp 里:
cpp#include "MyTemplate.h" int main() { int x = add(1, 2); // 想用 int 版本 }
⚠️ 这会导致 链接错误 :因为 main.cpp 编译时看到 add 的声明,但 .o 文件里没生成 add<int> 的实例化。
因为mytemplate.cpp 中函数模版并不知道其对应类型。实例化是在main.cpp
编译器不会自动去 MyTemplate.cpp 里"搜模板定义"。
🔹 3. 解决方法
有三种常见方法:
✅ 方法一:全写在头文件里(最常见)
cpp// MyTemplate.h template<typename T> T add(T a, T b) { return a + b; }
这样所有用到 add 的翻译单元都能看到实现,编译器就能实例化。
✅ 方法二:显式实例化(explicit instantiation)
你可以在 .cpp 里告诉编译器:
"我需要这些具体类型的实例化,帮我生成一次即可"。
cpp// MyTemplate.h template<typename T> T add(T a, T b);
cpp// MyTemplate.cpp #include "MyTemplate.h" template<typename T> T add(T a, T b) { return a + b; } // 显式实例化 template int add<int>(int, int); template double add<double>(double, double);
这样 main.cpp 里用 add<int> 或 add<double> 时,链接器能找到在 MyTemplate.o 里已经生成的符号。
👉 适合库开发,不想把实现暴露在头文件时。
✅ 方法三:把实现放在 .tpp 或 .inl 文件中
约定俗成的做法:
-
.h 里只放声明
-
.tpp(或 .inl)里放实现
-
在 .h 文件末尾 #include "MyTemplate.tpp"
这样使用者只需要 #include "MyTemplate.h",实现还是"逻辑分离"的。
-
模板必须在实例化时可见,所以不能像普通函数那样分离编译。
-
常见解决办法:
-
全部写在 .h(最常见)。
-
在 .cpp 中显式实例化需要的类型。
-
.h + .tpp 组合,让逻辑结构更清晰。
-
三、总结
-
模板:提供"编译期泛型"与静态多态。掌握变参模板、完美转发、SFINAE(或 Concepts)、std::void_t 检测,是写出高质量模板库的关键。优先用 Concepts(C++20)以获得更可读的接口约束。