尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C++泛型--模版--全解析-- 草履虫也能看懂!

C++泛型--模版--全解析-- 草履虫也能看懂! C 模板Template全解析C 模板是C泛型编程Generic Programming的核心机制其核心思想是将「类型 / 常量」作为 “参数” 传递给函数 / 类编写与具体类型无关的通用代码实现一次编写、适配多类型既避免重复代码又保证类型安全相比宏替换。模板分为两大核心类型函数模板和类模板衍生出特化、偏特化、可变参数等高级特性是 C 标准库STL的底层基础如vectorT、sort()均基于模板实现。一、核心概念为什么需要模板假设我们需要实现 “交换两个值” 的功能若没有模板需为每种类型写重复代码// 交换int void swapInt(int a, int b) { int tmp a; a b; b tmp; } // 交换double void swapDouble(double a, double b) { double tmp a; a b; b tmp; } // 交换string void swapString(string a, string b) { string tmp a; a b; b tmp; }模板可将 “类型” 抽象为参数用一份代码适配所有类型template typename T // T是类型参数泛型参数 void swap(T a, T b) { T tmp a; a b; b tmp; }二、函数模板Function Template1. 基本语法// 模板参数列表template 模板参数 // 模板参数typename/class 表示类型参数int/char等表示非类型参数 template typename T // typename 和 class 等价typename更推荐 返回值类型 函数名(参数列表) { // 通用逻辑T可作为类型使用 }2. 关键特性1模板实参推导编译器根据传入的参数类型自动推导模板参数T的具体类型无需手动指定#include iostream #include string using namespace std; template typename T T maxVal(T a, T b) { // 通用的求最大值函数 return a b ? a : b; } int main() { cout maxVal(3, 5) endl; // T推导为int输出5 cout maxVal(3.5, 2.8) endl; // T推导为double输出3.5 cout maxVal(apple, banana) endl; // T推导为const char*输出banana return 0; }2显式指定模板参数若推导失败如参数类型不一致可手动指定T的类型// 错误3是int3.5是double推导T冲突 // cout maxVal(3, 3.5) endl; // 正确显式指定T为double3会隐式转换为double cout maxValdouble(3, 3.5) endl; // 输出3.53函数模板重载模板函数可与普通函数重载优先级普通函数 模板函数匹配时优先选普通函数// 普通函数处理int int maxVal(int a, int b) { cout 普通函数; return a b ? a : b; } // 模板函数通用 template typename T T maxVal(T a, T b) { cout 模板函数; return a b ? a : b; } int main() { maxVal(3, 5); // 调用普通函数输出普通函数5 maxVal(3.5, 2.8); // 调用模板函数输出模板函数3.5 return 0; }三、类模板Class Template类模板用于定义 “通用的类”适用于容器如栈、链表、二叉树等需适配多类型的场景。1. 基本语法template typename T // 模板参数列表 class 类名 { public: // 成员变量/函数可使用T作为类型 T data; void set(T val) { data val; } T get() { return data; } };2. 核心示例通用二叉树节点#include iostream using namespace std; // 类模板通用二叉树节点支持int、string、double等任意类型 template typename T struct TreeNode { T val; // 泛型值 TreeNodeT* left; // 左子节点需指定模板参数 TreeNodeT* right; // 右子节点 // 构造函数 TreeNode(T x) : val(x), left(nullptr), right(nullptr) {} }; // 类模板的成员函数类外定义需重复模板参数列表 template typename T void preOrder(TreeNodeT* root) { if (!root) return; cout root-val ; preOrder(root-left); preOrder(root-right); } int main() { // 实例化int类型的二叉树节点 TreeNodeint* rootInt new TreeNodeint(1); rootInt-left new TreeNodeint(2); preOrder(rootInt); // 输出1 2 // 实例化string类型的二叉树节点 TreeNodestring* rootStr new TreeNodestring(A); rootStr-right new TreeNodestring(B); preOrder(rootStr); // 输出A B return 0; }3. 类模板的关键规则必须显式实例化类模板不能自动推导类型必须手动指定T如TreeNodeint、vectorstring成员函数的类外定义需在定义前重复template typename T且类名后需加T如TreeNodeT非类型模板参数可传入常量如整数、枚举作为模板参数常用于定长容器// N是非类型模板参数必须是常量表达式 template typename T, int N class Array { private: T arr[N]; // 定长数组 public: T operator[](int idx) { return arr[idx]; } }; int main() { Arrayint, 5 arr; // N5实例化一个长度为5的int数组 arr[0] 10; cout arr[0]; // 输出10 return 0; }模板推理过程:模板形式实参类型 (Expr)T 的推导结果形参最终类型核心逻辑T*int*intint*模式匹配T 是指向的内容。T*const int*const intconst int*保留指向内容的 const。Tconst intintint剥离引用和顶层 const发生拷贝。Tconst intconst intconst int保留const数组不退化。Tint (左值)intint特例左值推导为引用。Tint (右值)intint正常推导保留右值属性。#include iostream // 1. 对应表格【T*】的情况指针 templatetypename T void func_pointer(T* p) { // 这里的 T 会匹配指针指向的内容 std::cout func_pointer 被调用 std::endl; } // 2. 对应表格【T】的情况值传递 templatetypename T void func_value(T p) { // 这里的 T 会变成纯粹的值剥离引用和 const std::cout func_value 被调用 std::endl; } // 3. 对应表格【T】的情况左值引用 templatetypename T void func_lref(T p) { // 这里的 T 会保留 const但本身不带 std::cout func_lref 被调用 std::endl; } // 4. 对应表格【T】的情况万能引用 templatetypename T void func_universal(T p) { // 这里的 T 既能变引用也能变值 std::cout func_universal 被调用 std::endl; } int main() { // 准备一些基础变量 int x 10; // 普通 int int px x; // x变量的指针 const int cx 20; // 常量 int const int rx x; // 常量引用 std::cout 1. 测试 T* (指针) std::endl; // 实参是 int* func_pointer(px); // - T 被推导为 int // - 形参类型是 int* // 实参是 const int* func_pointer(cx); // - T 被推导为 const int (保留了内容的 const) // - 形参类型是 const int* std::cout \n 2. 测试 T (值传递) std::endl; // 实参是 const int (即变量 rx) func_value(rx); // - T 被推导为 int (const 和 统统被扔掉了) // - 发生拷贝函数里修改 p 不会影响外面的 rx func_value(x); // - T 被推导为 int* std::cout \n 3. 测试 T (左值引用) std::endl; // 实参是 const int (即变量 cx) func_lref(cx); // - T 被推导为 const int (保留了 const) // - 形参类型是 const int (T 加上模板里的 ) func_lref(px); //会直接报错, T无论等于什么都无法满足等式T int*; std::cout \n 4. 测试 T (万能引用) std::endl; // 实参是左值 (变量 x) func_universal(x); // - T 被推导为 int (特例T 变成了引用) // - 形参类型是 int // 实参是右值 (数字 10) func_universal(10); // - T 被推导为 int (正常推导) // - 形参类型是 int (右值引用) return 0; }四、模板特化Specialization模板特化是为特定类型定制模板的实现覆盖通用逻辑分为「全特化」和「偏特化」。1. 全特化Full Specialization针对模板参数的所有类型定制实现仅适用于特定类型。1函数模板全特化// 通用模板比较任意类型 template typename T bool isEqual(T a, T b) { return a b; } // 全特化针对char*字符串的定制实现比较字符串内容而非指针 template bool isEqualchar*(char* a, char* b) { return strcmp(a, b) 0; } int main() { char s1[] hello; char s2[] hello; cout isEqual(s1, s2); // 调用特化版本输出1true return 0; }2类模板全特化// 通用类模板 template typename T class MyContainer { public: void print() { cout 通用类型 typeid(T).name() endl; } }; // 全特化针对int类型的定制 template class MyContainerint { public: void print() { cout 特化类型int endl; } }; int main() { MyContainerdouble d; d.print(); // 输出通用类型double MyContainerint i; i.print(); // 输出特化类型int return 0; }2. 偏特化Partial Specialization类模板的偏特化Partial Specialization是C模板编程中的一种技术它允许我们为模板参数的一部分进行特化或者对模板参数施加一些条件进行特化。偏特化可以用于类模板但不能用于函数模板函数模板只能全特化但可以通过重载实现类似功能。偏特化的基本思想是我们有一个主模板然后为某些特定的模板参数组合提供另一个定义。这个定义是部分特化的因为它只针对一部分参数进行了特化。// 通用类模板双参数 template typename T, typename U class MyPair { public: void print() { cout 通用T typeid(T).name() , U typeid(U).name() endl; } }; // 偏特化1第二个参数固定为int template typename T class MyPairT, int { public: void print() { cout 偏特化T typeid(T).name() , Uint endl; } }; // 偏特化2第一个参数为指针类型 template typename T, typename U class MyPairT*, U* { public: void print() { cout 偏特化T* typeid(T).name() , U* typeid(U).name() endl; } }; /*class MyPairT*,U*{}其中T*和U*都表示某种指针类型且两者互不制约可以相同 T*,U*表示凡是符合类型的参数使用此偏特化模版实例化不再使用主模版实例化。*/ int main() { MyPairdouble, string p1; p1.print(); // 通用Tdouble, Ustring MyPairdouble, int p2; p2.print(); // 偏特化Tdouble, Uint MyPairint*, char* p3; p3.print(); // 偏特化T*int, U*char return 0; }五、可变参数模板C11C可变模板参数详细讲解https://blog.csdn.net/Howrun777/article/details/157581910?sharetypeblogdetailsharerId157581910sharereferPCsharesourceHowrun777spm1011.2480.3001.8118支持传入任意数量、任意类型的模板参数参数包是 STL 中tuple、printf封装的核心。1. 基本语法// Args是参数包可包含0~N个类型 template typename... Args void func(Args... args) { // 展开参数包需递归/逗号表达式等方式 }示例1C11递归式通用打印函数支持任意参数#include iostream using namespace std; // 递归终止函数参数包为空时调用 void print() { cout endl; } // 可变参数模板递归展开,所以需要单独指出第一个参数 template typename T, typename... Args void print(T first, Args... rest) { cout first ; // 打印第一个参数 print(rest...); // 递归打印剩余参数参数包展开 } int main() { print(1, 3.5, hello, A); // 输出1 3.5 hello A print(test); // 输出test print(); // 输出空行 return 0; }示例 2C17无递归打印任意参数最直观#include iostream #include string // 无递归折叠表达式直接展开参数包 templatetypename... Args void print_all(Args... args) { // 折叠表达式核心(操作, ...) 或 (... , 操作) // 这里是对每个参数执行 cout args 用逗号分隔 (std::cout ... args) | 打印完成\n; } int main() { // 测试任意参数 print_all(100, std::string(hello), 3.14, true); print_all(); // 0个参数也能处理折叠表达式空展开无输出 return 0; }六、模板的高级特性1. 模板别名C11用using简化复杂模板类型替代typedef的局限性// 简化vectorint 的别名 using IntVec vectorint; IntVec v {1,2,3}; // 模板别名适配任意类型 template typename T using MapStrKey mapstring, T; MapStrKeyint m1; // 等价于 mapstring, int MapStrKeystring m2; // 等价于 mapstring, string2. SFINAE替换失败并非错误编译期根据模板参数的特性筛选匹配的模板如判断类型是否有某个成员函数是 STL 类型萃取的核心示例简化版// 仅当T有size()成员函数时匹配 template typename T auto getSize(T t) - decltype(t.size(), int()) { return t.size(); } // 通用版本无size()时匹配 template typename T int getSize(T t) { return sizeof(t); } int main() { string s hello; cout getSize(s); // 调用第一个版本输出5 int a 10; cout getSize(a); // 调用第二个版本输出4int的大小 return 0; }3. 引用折叠与万能模板引用折叠的核心定义C 语法本身不允许直接定义 “引用的引用”比如int 、int 这类写法但在模板推导、auto类型推导、类型别名typedef/using等场景下会间接产生 “引用的引用”。此时编译器会按照 “引用折叠规则”将这种非法的复合引用类型折叠成一个合法的引用类型 —— 这就是引用折叠。引用折叠的核心规则只有 4 条引用折叠的规则可以总结为只要有一个是左值引用最终结果就是左值引用只有全部是右值引用结果才是右值引用。复合引用类型折叠后的最终类型核心逻辑左值引用 左值引用 (T )T含左值引用 → 左值引用左值引用 右值引用 (T )T含左值引用 → 左值引用右值引用 左值引用 (T )T含左值引用 → 左值引用右值引用 右值引用 (T )T全是右值引用 → 右值引用补充C 中有名字的右值引用是左值比如函数参数T paramparam本身是左值这也是引用折叠能发挥作用的重要前提。引用折叠的典型应用场景附代码示例引用折叠不是孤立的规则它是模板推导、完美转发的底层支撑以下是最常见的应用场景场景 1模板参数推导中的引用折叠模板参数T被称为 “万能引用”仅当T是模板参数时其最终类型由传入的实参类型决定核心就是引用折叠#include iostream #include type_traits // 用于类型判断 using namespace std; // 万能引用模板函数 template typename T void func(T param) { // 判断param的最终类型 if (is_lvalue_referencedecltype(param)::value) { cout param 最终是左值引用\n; } else if (is_rvalue_referencedecltype(param)::value) { cout param 最终是右值引用\n; } } int main() { int a 10; func(a); // 传入左值T推导为intT → int → 折叠为int左值引用 func(20); // 传入右值T推导为intT → int右值引用 func(std::move(a)); // 传入右值T推导为intT → int右值引用 return 0; }输出结果param 最终是左值引用 param 最终是右值引用 param 最终是右值引用4. 非类型模板参数:你可以把模板理解成 “做东西的模具”平时用的 “类型模板参数”比如templateclass T是选 “做东西的材料”—— 比如用这个模具做 “铁杯子”T 是 int还是 “塑料杯子”T 是 string。而非类型模板参数是选 “做东西的尺寸 / 具体数值”—— 比如用这个模具做 “5 厘米的杯子” 还是 “10 厘米的杯子”这个 “5”“10” 就是 “非类型模板参数”。非类型模板参数就是在写模板的时候提前把某个固定的数字或常量定死让编译器在 “把代码变成程序” 的阶段不是程序跑起来的时候就知道这个数不用等程序运行时再临时算。举个最直白的例子做个固定大小的 “盒子”比如你想做个装东西的盒子但每次要装的数量不一样// 这里的N就是“非类型模板参数”——提前定好盒子能装几个 templateint N class Box { int things[N]; // 这个盒子就固定能装N个int比如N5就装5个N10就装10个 }; // 用的时候 Box5 box5; // 做一个能装5个东西的盒子 Box10 box10; // 做一个能装10个东西的盒子它有啥用速度快这个 “能装几个” 的数字在程序运行前就定死了不用等程序跑起来再算省时间。不容易错比如你要装 5 个东西的盒子编译器直接给你弄好 “只能装 5 个” 的盒子不会出现 “装了 6 个导致溢出” 的问题比普通数组安全。省事儿比如 STL 里的array固定大小数组就是这么搞的 ——arrayint,5就是 “装 5 个 int 的数组”不用像普通数组那样担心写错大小。再通俗点类比就像你点奶茶“类型模板参数” 选奶茶的品类比如选 “奶茶” 还是 “果茶”“非类型模板参数” 选奶茶的杯型比如选 “中杯500ml” 还是 “大杯700ml”—— 这个 “500”“700” 就是提前定死的数值做的时候直接按这个量来不用等你喝的时候再调。定制化仿函数结合仿函数知识点用非类型模板参数定制仿函数的比较阈值避免运行时传参提升效率// 非类型模板参数Threshold比较的阈值编译期确定 templateint Threshold class CheckGreaterThan { public: bool operator()(int value) const { // Threshold是编译期常量无运行时开销 return value Threshold; } }; // 测试 int main() { // 实例化阈值为10的比较器 CheckGreaterThan10 check10; cout check10(15) endl; // 输出1true cout check10(8) endl; // 输出0false // 实例化阈值为20的比较器生成不同的模板实例 CheckGreaterThan20 check20; cout check20(15) endl; // 输出0 return 0; }5.仿函数:定义方式仿函数通过类 / 结构体定义核心是重载operator()运算符使其能像函数一样被调用。代码示例类 模板泛型templateclass T // 模板化支持任意类型 class Greater { public: // 重载()运算符实现“xy”的比较逻辑 bool operator()(const T x, const T y) { return x y; } };仿函数的调用方式实例化仿函数类后有两种调用形式// 实例化仿函数对象 Greaterint greaterFunc; // 方式1隐式调用operator()像普通函数一样用 cout greaterFunc(1, 2) endl; // 方式2显式调用operator()成员函数 cout greaterFunc.operator()(1, 2) endl;仿函数作为模板参数形参声明通过模板参数指定可设置默认仿函数格式示例templateclass Compare GreaterTGreaterT为默认比较器实参传入类实例化时直接传入仿函数类名即可仿函数在容器中的应用以 priority_queue 为例priority_queue优先队列的模板参数包含 “比较器”可通过自定义仿函数定制其排序逻辑。templateclass T, class Container vectorT, class Compare lessT class priority_queue { public: priority_queue(){} void push(const T x) { _con.push_back(x); // 底层容器插入元素 } // 省略empty()、top()、pop()等成员实现 private: Container _con; // 底层存储容器 };自定义仿函数结构体形式struct PDateCompare { // 比较两个指针指向的Date对象按*p1 *p2逻辑 bool operator()(const Date* p1, const Date* p2) { return *p1 *p2; } };priority_queue 的实例化与使用指定调用我们自己写的仿函数:// 实例化指定元素类型、底层容器、自定义仿函数 bit::priority_queueDate*, vectorDate*, PDateCompare pq; // 插入元素需提前定义Date类 pq.push(new Date(2025, 4, 11)); pq.push(new Date(2025, 4, 13)); pq.push(new Date(2025, 4, 12)); // 遍历输出按仿函数逻辑排序 while (!pq.empty()) { cout *pq.top() ; pq.pop(); } cout endl;string 类型示例// 自定义string比较的仿函数按字符串长度升序 struct StringCompare { bool operator()(const string s1, const string s2) const { return s1.size() s2.size(); // priority_queue是“大顶堆”这里用实现长度升序 } }; bit::priority_queuestring, vectorstring, StringCompare pqstr; pqstr.push(11111); pqstr.push(2222); pqstr.push(333); while (!pqstr.empty()) { cout pqstr.top() ; pqstr.pop(); } cout endl;泛型的编译实例化难点如果你看不懂也没关系会用就行看懂这个需要懂一定的编译原理{此处插入编译原理的链接}C 普通函数的编译 链接过程:预处理阶段编译器先把.cpp 文件里#include的头文件 “原样复制粘贴” 到.cpp 里生成一个完整的临时文件编译阶段编译器把这个临时文件翻译成机器码生成.objWindows/.oLinux目标文件过程中会检查变量 / 函数的声明和定义是否匹配比如头文件声明了int add(int);cpp 里定义的函数名 / 参数要一致同时会生成符号表记录当前文件里的函数 / 变量名以及它们对应的机器码地址链接阶段链接器把所有.cpp 生成的.obj文件合并成最终的可执行程序如果头文件里既声明又定义了普通函数比如int add(int a){return a1;}那么每个包含这个头文件的.cpp 都会生成一份add函数的机器码和符号表链接器一看有多个一模一样的add函数符号重复就会报 “重定义” 错误。最常用的 “头文件声明 源文件定义” 模式:第一步三个文件的原始内容// add.h 只有声明无函数体 int add(int); // func.cpp 有add的完整定义函数体 #include add.h int add(int a) { return a 1; // 函数体实际机器码的来源 } // main.cpp 只有声明来自头文件 调用无函数体 #include add.h int main(){ add(10); // 调用add但自己没定义 return 0; }第二步预处理阶段两个.cpp 文件独立处理预处理是每个.cpp 文件单独做的不会互相影响处理main.cpp把add.h里的int add(int);复制到main.cpp生成临时文件只有声明 main 函数无 add 函数体处理func.cpp把add.h里的int add(int);复制到func.cpp生成临时文件有声明 add 函数体。第三步编译阶段生成两个独立的.obj 文件重点看符号表编译器分别编译两个.cpp生成两个目标文件main.obj来自 main.cpp、func.obj来自 func.cpp核心差异在符号表1. 编译 func.cpp → 生成 func.obj编译器把 add 的函数体翻译成机器码比如 “接收 int 参数→加 1→返回” 对应的指令在func.obj的符号表里记录✅add(int)已定义符号有地址比如 0x0012FF00关联对应的机器码简单说func.obj 里 “有 add 的实际代码且标记了地址”。2. 编译 main.cpp → 生成 main.obj编译器检查add(10)的调用和声明int add(int);匹配语法没问题把 main 函数翻译成机器码但add(10)的调用处留一个 **“未解析的地址占位符”**在main.obj的符号表里记录❓add(int)未定义符号需要找外部的 add 代码地址暂时空着简单说main.obj 里 “知道要调用 add但不知道 add 的代码在哪先记下来”。第四步链接阶段核心链接器 “牵线搭桥”链接器的核心工作是「合并所有.obj 文件 解析未定义符号 填充地址」具体步骤链接器先收集所有目标文件main.objfunc.obj合并符号表把两个.obj 的符号表合在一起发现main.obj里有一个 “未定义的 add (int)”func.obj里有一个 “已定义的 add (int)”地址 0x0012FF00解析未定义符号链接器把main.obj里 “add (int)” 的占位符填充成 func.obj 里 add 的实际地址0x0012FF00清理 打包删除重复的声明、整理机器码顺序最终把main.obj和func.obj的机器码合并生成可执行程序比如 a.exe。关键细节程序运行时的调用过程当你运行 a.exe 时main 函数执行到add(10)CPU 会跳转到地址 0x0012FF00也就是 func.cpp 里 add 函数的机器码位置执行 add 的逻辑101执行完再跳回 main 函数继续运行。C 模板泛型函数的特殊编译 链接过程如果此函数是一个泛型函数,那么此函数必须在头文件声明并且定义,且不会有重定义报错,因为模板本身不是代码而是生成代码的蓝图。当编译器看到模板的定义时它并不会立即生成代码在编译阶段当编译器在某个编译单元中遇到模板的具体实例化如addint时它会在该编译单元中生成这个具体实例化的代码如果之前在同一编译单元中没有生成过,C标准允许模板在多个编译单元中实例化相同的代码并且要求链接器在链接时消除重复的模板实例化代码。也就是说链接器会确保每个模板实例在最终的可执行程序中只有一份定义。模板的注意事项编译模型模板是 “编译期实例化”需在头文件中定义模板声明 实现若分离到.cpp会导致链接错误编译器无法找到实例化代码非类型模板参数限制只能是常量表达式如整数、枚举、指针 / 引用需指向全局不能是变量typename 的使用在模板中访问嵌套类型时需用typename标识如typename T::iterator否则编译器会将其解析为变量模板不支持运行时多态模板实例化是编译期行为无法像虚函数一样运行时动态绑定代码膨胀不同类型的模板实例化会生成独立的代码如swapint、swapdouble可能增加可执行文件体积可通过特化、共用实现减少。模板的典型应用场景STL 容器vectorT、listT、mapK,V等均为类模板STL 算法sort()、find()、for_each()等均为函数模板自定义通用数据结构通用栈、链表、二叉树如你之前关注的二叉树节点模板泛型算法通用的排序、查找、遍历逻辑元编程编译期计算如编译期求阶乘、类型判断如判断是否为指针类型。模板是 C 中最强大的特性之一掌握模板能大幅提升代码复用性和扩展性也是理解 STL 底层、编写高效通用代码的关键。
返回列表