C++模板编程:从基础语法到实战应用,掌握泛型编程核心技能

C++模板编程:从基础语法到实战应用,掌握泛型编程核心技能
1. 项目概述为什么C模板是绕不开的硬核技能如果你写过一段时间的C尤其是当你开始接触标准库STL里的vector、map这些容器时心里多半会冒出几个问号为什么我声明一个vectorint就能存整数声明vectorstring就能存字符串而它们用的却是同一套vector的代码编译器是怎么知道该生成哪种类型代码的这背后就是C模板Template在发挥作用。很多人觉得模板是“高级特性”是库开发者才需要关心的东西但我的经验是一旦你开始尝试编写稍微通用一点的代码或者想优化重复的逻辑模板几乎是唯一优雅的解决方案。它能让你的代码在保持类型安全的同时获得接近宏的灵活性但又远比宏强大和安全。简单来说C模板是一种支持参数化多态的工具它允许你编写与类型无关的代码。你可以把它理解为一个“代码生成器”的蓝图。当你提供一个具体的类型比如int、string或一个自定义的Student类给这个蓝图时编译器就会根据这个蓝图为你使用的每一种具体类型“现场”生成一份特化Specialized的代码。这解决了C这类静态类型语言的一个核心痛点如何在不牺牲类型检查和运行时效率的前提下写出可复用的通用算法和数据结构。没有模板你可能需要为int写一个max函数为double再写一个几乎一样的max函数或者使用不安全的void*指针而模板让你只写一份代码就够了。这篇文章适合所有已经掌握了C基础如类、函数、指针并希望提升代码抽象和复用能力的开发者。无论你是想深入理解STL的工作原理还是打算自己设计一个通用的工具库或者仅仅是想在面试中不被“模板特化”、“偏特化”这些词难倒透彻理解函数模板和类模板都是必经之路。我会从最基础的语法开始拆解其背后的编译原理分享实际编码中的技巧和那些容易踩进去的坑目标是让你不仅能看懂模板代码更能自信地写出健壮、高效的模板代码。2. 函数模板从通用算法到编译期多态函数模板是模板中最直观、最常用的形式。它的目标很简单写一个函数定义让它能适用于多种数据类型而不是仅仅一种。2.1 基础语法与隐式实例化我们从一个经典的例子开始求两个值的最大值。没有模板时你可能需要重载多个函数int max(int a, int b) { return (a b) ? a : b; } double max(double a, double b) { return (a b) ? a : b; } // 如果需要string、自定义类型... 代码会急剧膨胀使用函数模板你可以这样写template typename T // 模板声明T是一个类型参数 T max(T a, T b) { return (a b) ? a : b; }这短短几行代码就是一个完整的函数模板。template typename T是模板引入的关键字它告诉编译器接下来的定义是一个模板T是一个“占位符”类型也叫模板类型参数。你可以用typename也可以用等价的class在函数模板中两者通常没区别即template class T。使用这个模板时你不需要显式指定T是什么int main() { int i1 5, i2 10; double d1 3.14, d2 2.71; std::string s1 hello, s2 world; std::cout max(i1, i2) std::endl; // T被推导为int std::cout max(d1, d2) std::endl; // T被推导为double std::cout max(s1, s2) std::endl; // T被推导为std::string }这个过程叫做模板实参推导。编译器看到max(i1, i2)时发现两个参数都是int于是推导出T int然后根据模板生成一个int max(int, int)的函数实体并调用它。这个由编译器根据模板和具体类型生成具体函数的过程称为实例化。上面这种通过函数调用让编译器自动推导类型进行的实例化就是隐式实例化。注意模板本身不是函数它只是一个蓝图。在编译阶段只有当代码中真正用到模板的某个具体版本如maxint时编译器才会实例化出对应的函数代码。如果整个程序都没有调用maxdouble那么double版本的函数就不会被生成。这被称为“惰性实例化”。2.2 模板参数推导的规则与陷阱实参推导是函数模板方便性的核心但它的规则需要仔细理解否则会写出编译通过但行为不符合预期的代码。规则一推导基于调用实参的类型。编译器只根据你调用函数时传入的实参类型来推导模板参数。它不会考虑函数的返回类型也不会考虑你可能“希望”它是什么类型。规则二推导时必须保证所有推导出T的地方类型一致。看这个例子template typename T T max(T a, T b) { ... } int a 5; double b 3.14; auto m max(a, b); // 错误编译失败这里第一个实参让T推导为int第二个让T推导为double冲突了。编译器无法确定T应该是int还是double。解决推导冲突的几种方法显式指定模板实参在函数名后使用尖括号指明类型。auto m maxdouble(a, b); // 告诉编译器请用double实例化模板 // 此时int类型的a会被隐式转换为double然后调用maxdouble使用多个模板类型参数让两个参数可以独立推导。template typename T1, typename T2 auto max(T1 a, T2 b) - decltype((a b) ? a : b) { // C11 返回类型后置 return (a b) ? a : b; } // C14 可以更简单 template typename T1, typename T2 auto max(T1 a, T2 b) { return (a b) ? a : b; }使用通用引用和std::common_type更高级的技巧这里先不展开。规则三当参数类型包含修饰符如引用、const时推导规则会变得复杂。这是模板推导中最容易出错的地方之一。template typename T void f(T param) {} int x 42; const int cx x; const int rx x; f(x); // T被推导为 int, param类型是 int f(cx); // T被推导为 int, param类型是 int (注意const被丢弃了) f(rx); // T被推导为 int, param类型是 int (引用和const都被丢弃了)这是因为在按值传参的模板中编译器会忽略实参的引用性和const性只关心其底层类型。如果你希望保留这些属性需要将参数声明为引用或万能引用template typename T void f(T param) {} // 传引用 f(x); // T - int, param - int f(cx); // T - const int, param - const int (const被保留) f(rx); // T - const int, param - const int实操心得在编写函数模板时我习惯先问自己这个函数需要对参数做修改吗如果不需要优先考虑按const T传递这既能接受各种类型包括临时对象又能避免不必要的拷贝。如果需要修改参数或支持移动语义再考虑T或T。对于简单的内置类型如int,double按值传递通常效率更高。2.3 非类型模板参数与模板特化模板参数不一定非得是类型。也可以是整型值、枚举、指针或引用指向具有静态存储期的对象这些被称为非类型模板参数。一个典型例子是编译期已知大小的数组template typename T, std::size_t N // N是非类型模板参数 class Array { T data[N]; // 数组大小在编译期就确定了 public: std::size_t size() const { return N; } T operator[](std::size_t i) { return data[i]; } }; Arrayint, 10 arr10; // 一个大小为10的int数组 Arraydouble, 100 arr100; // 一个大小为100的double数组这里的N必须是编译期常量。使用非类型参数可以实现编译期的计算和选择是模板元编程的基础。模板特化允许你为模板的特定参数提供定制化的实现。当通用模板的逻辑对某些特殊类型不适用或效率不高时特化就派上用场了。例如我们有一个比较对象是否相等的通用模板template typename T bool isEqual(T a, T b) { return a b; }但对于C风格字符串char*直接比较比较的是指针地址而不是字符串内容。这时我们可以为const char*提供一个特化版本// 通用模板 template typename T bool isEqual(T a, T b) { return a b; } // 全特化版本template 开头并指定了具体的类型 const char* template bool isEqualconst char*(const char* a, const char* b) { return std::strcmp(a, b) 0; } // 使用 std::cout isEqual(1, 1) std::endl; // 调用通用版本 std::cout isEqual(hello, hello) std::endl; // 调用const char*特化版本编译器在实例化模板时会优先选择最匹配的特化版本。全特化意味着为模板的所有参数都提供了具体类型。还有一种偏特化也叫部分特化主要用于类模板我们稍后讨论。3. 类模板构建通用数据结构的基石如果说函数模板让算法变得通用那么类模板就让数据结构的复用成为可能。STL中的vector、list、map无一不是类模板的杰作。3.1 类模板的定义与使用类模板的声明和定义通常都放在头文件.h或.hpp中。这是因为模板代码在编译期需要被“看到”才能实例化分离编译定义在.cpp文件对于模板来说非常棘手后面会讲解决方案。让我们实现一个简化的Box类模板它可以存放任何类型的单一物品// Box.h #ifndef BOX_H #define BOX_H template typename T // 类模板声明 class Box { private: T content; public: Box(const T item) : content(item) {} // 构造函数 T getContent() const { return content; } void setContent(const T item) { content item; } }; #endif使用这个类模板时你必须显式提供模板参数因为编译器无法像函数模板那样从构造函数调用中推导出类模板的类型在C17之前构造函数的模板参数推导不适用于类模板C17引入了类模板实参推导CTAD但显式指定仍是清晰的做法。#include Box.h #include string int main() { Boxint intBox(123); // 必须指定int Boxstd::string strBox(Hello Template); Boxdouble doubleBox(3.14159); std::cout intBox.getContent() std::endl; return 0; }当你写下Boxint时编译器就用int替换掉类模板定义中所有的T生成一个具体的Box_int类名称是修饰过的然后编译它。3.2 类模板中的成员函数定义在类模板内部定义的成员函数如上例中的getContent和setContent默认为内联函数。但如果你希望将成员函数的定义放在类的外部语法会稍有不同因为每个成员函数本身也是一个模板。template typename T class Box { private: T content; public: Box(const T item); T getContent() const; void setContent(const T item); }; // 构造函数在类外的定义 template typename T BoxT::Box(const T item) : content(item) {} // 成员函数在类外的定义 template typename T T BoxT::getContent() const { return content; } template typename T void BoxT::setContent(const T item) { content item; }注意定义格式每个成员函数前都需要重复template typename T并且函数名需要用BoxT::来限定表明这是BoxT类的成员而不是普通函数。3.3 默认模板参数与模板的嵌套类模板支持默认模板参数这为使用者提供了便利。template typename T int, int INIT_SIZE 10 // T默认为int大小默认为10 class Buffer { T data[INIT_SIZE]; // ... }; Buffer defaultBuffer; // 使用默认参数Bufferint, 10 Bufferdouble doubleBuffer; // Bufferdouble, 10 Bufferchar, 256 bigBuffer; // Bufferchar, 256函数模板从C11开始也支持默认模板参数。模板可以嵌套这意味着一个模板的内部可以包含另一个模板。这是构建复杂泛型组件的基础。例如一个Stack类模板其内部用于存储的容器也可以是一个模板参数template typename T, template typename class Container std::vector class Stack { private: ContainerT elems; // 使用传入的容器模板存储T类型元素 public: void push(const T elem) { elems.push_back(elem); } T pop() { if (elems.empty()) throw std::out_of_range(Stack::pop: empty stack); T elem elems.back(); elems.pop_back(); return elem; } }; // 使用默认的std::vector容器 Stackint intStack; // 使用std::deque作为底层容器 Stackdouble, std::deque doubleStack;这里的template typename class Container是一个模板模板参数。它允许Stack类接受一个容器模板如std::vector、std::deque、std::list作为参数极大地增强了灵活性。这也是STL中适配器如stack、queue的实现方式之一。4. 模板的编译与链接模型为什么定义要放在头文件这是C模板学习路上必然会撞上的“墙”。你可能会遇到这样的错误在main.cpp中使用了max函数模板在utils.cpp中实现了它编译utils.cpp和main.cpp都没问题但链接时却报错“未定义的引用”。4.1 “一次定义原则”与模板的实例化C的“一次定义原则”ODR要求每个函数、变量、类型等在程序中只能有一个定义。对于普通函数编译器在编译每个.cpp文件翻译单元时生成目标代码链接器再将它们合并。但模板不同模板的定义本身并不是一个完整的函数或类定义它只是一个蓝图。模板的实例化发生在编译阶段且在每个翻译单元内独立进行。当编译器在main.cpp中看到max(5, 10)时它需要max模板的完整定义蓝图来生成maxint的代码。如果max模板的定义在另一个.cpp文件utils.cpp里那么main.cpp在编译时就看不到这个蓝图它无法实例化只会假设这个函数在其他地方被实例化了于是在目标文件中留下一个未解决的符号引用。而utils.cpp虽然包含了模板定义但如果没有代码触发maxint的实例化比如在utils.cpp里调用max(5,10)那么utils.obj里也不会生成maxint的代码。链接时链接器在main.obj里找到了对maxint的调用但在所有.obj文件里都找不到maxint的实现于是报错“未定义”。4.2 解决方案将定义放在头文件中最直接、最常用的解决方案就是将模板的声明和定义都放在头文件.hpp中。这样任何包含了该头文件的.cpp文件在编译时都能看到完整的模板定义并能在需要时当场实例化出所需的特化版本。每个翻译单元都会生成自己的一份实例化代码但链接器很聪明它会选择其中一份丢弃重复的前提是这些实例化代码完全相同。这也是为什么你看到的STL头文件如vector里面充满了实现代码的原因——它们几乎全部是模板。4.3 显式实例化与分离编译的折中方案如果你确实希望将模板的实现细节隐藏到.cpp文件中或者模板实例化非常耗时希望集中管理可以使用显式实例化。步骤在头文件如utils.h中只放模板的声明。在实现文件如utils.cpp中放模板的定义并在文件末尾显式地告诉编译器“请为我实例化这几个特定类型的版本”。在使用这些特定类型的翻译单元中正常包含头文件并使用。// utils.h #ifndef UTILS_H #define UTILS_H template typename T T max(T a, T b); // 只有声明 #endif // utils.cpp #include utils.h template typename T T max(T a, T b) { // 定义 return (a b) ? a : b; } // 显式实例化告诉编译器请在这里生成int和double版本的max函数代码 template int maxint(int, int); template double maxdouble(double, double); // main.cpp #include utils.h int main() { max(5, 10); // 链接成功使用utils.cpp中显式实例化的版本 max(3.14, 2.71); // 链接成功 // max(“hello“, “world“); // 链接错误utils.cpp中没有显式实例化std::string版本 }这种方法的缺点是失去了模板的灵活性。你必须预先知道所有会用到的类型并为其进行显式实例化。对于用户自定义类型这几乎不可行。因此它通常用于已知只会使用少数几种基本类型如int,double,float的库中以减少编译依赖和编译时间。避坑技巧在大型项目中模板被广泛使用的头文件发生修改会导致包含它的所有源文件重新编译严重影响增量编译速度。一个常见的优化手段是使用“外部模板”C11。在某个公共头文件中使用extern template声明告诉编译器“请不要在这个翻译单元实例化这个模板我相信它在别处已经实例化了”。然后在某个专门的.cpp文件中进行一次显式实例化。这可以减少重复实例化的工作量但管理起来比较麻烦。5. 模板进阶技巧与元编程初探当你熟悉了基本语法后模板还能带你进入更奇妙的领域在编译期进行计算和类型操纵。5.1 类型萃取与SFINAE类型萃取是一系列模板技术的统称用于在编译期获取和判断类型的属性。标准库type_traits提供了大量工具如std::is_integralT、std::is_pointerT、std::remove_referenceT等。例如我们想写一个函数对于整数类型进行位运算对于其他类型则进行普通加法#include type_traits #include iostream // 通用版本非整数类型 template typename T typename std::enable_if!std::is_integralT::value, T::type process(T a, T b) { std::cout Using addition for non-integral types.\n; return a b; } // 整数类型特化版本 template typename T typename std::enable_ifstd::is_integralT::value, T::type process(T a, T b) { std::cout Using bitwise OR for integral types.\n; return a | b; } int main() { process(3, 5); // 输出Using bitwise OR... 返回 7 (3|5) process(3.14, 2.71); // 输出Using addition... 返回 5.85 }这里用到了std::enable_if和std::is_integral。std::enable_ifCondition, T::type是一个模板元函数如果Condition为true那么它的::type成员就是T如果为false则它没有::type成员。这触发了C模板中一个重要的规则SFINAESubstitution Failure Is Not An Error替换失败并非错误。SFINAE规则是指在模板重载解析过程中如果某个模板的实例化用实参替换模板参数导致了无效的代码比如访问了不存在的类型成员编译器不会立即报错而是简单地将其从候选函数集中剔除继续尝试其他重载。在上面的例子中当T是int时第一个process的std::enable_if条件为false导致该函数签名无效被SFINAE掉第二个版本条件为true签名有效被选中。反之亦然。5.2 可变参数模板C11引入了可变参数模板允许模板接受任意数量、任意类型的参数。这用于实现像printf、std::make_shared、std::tuple这样的功能。语法是使用省略号...template typename... Args // Args是一个模板参数包 void print(Args... args) { // args是一个函数参数包 // 无法直接遍历参数包需要借助递归或折叠表达式 }处理可变参数模板通常有两种方式1. 递归展开// 递归终止函数 void print() { std::cout std::endl; } // 递归处理函数 template typename T, typename... Args void print(T first, Args... rest) { std::cout first ; print(rest...); // 递归调用参数包被展开 } int main() { print(1, 2.5, hello, a); // 输出1 2.5 hello a }2. 折叠表达式C17更简洁高效。template typename... Args void print(Args... args) { (std::cout ... args) std::endl; // 二元左折叠 // 等价于 (((std::cout arg1) arg2) ...) argN) }可变参数模板是构建现代C库如元组、variant、any的基石它提供了前所未有的灵活性。5.3 编译期计算与模板元编程模板的强大之处在于很多计算可以在编译期完成从而做到“零开销抽象”。一个经典的例子是编译期计算阶乘template unsigned n struct Factorial { static const unsigned value n * Factorialn - 1::value; }; // 特化作为递归终止条件 template struct Factorial0 { static const unsigned value 1; }; int main() { std::cout Factorial5::value std::endl; // 输出 120 // 这行代码在编译期就已经计算出了120运行时直接使用这个常量。 }这看起来像递归函数但整个过程发生在编译期。Factorial5::value在编译时就被替换为常量120。这就是模板元编程的雏形——用模板语法在编译期执行计算。虽然这个例子很简单但模板元编程可以用于生成极其复杂的编译期逻辑如类型列表操作、编译期字符串处理等。现代CC11/14/17引入了constexpr关键字使得很多编译期计算可以用更直观的函数语法完成但模板元编程在类型操纵方面仍有不可替代的地位。6. 常见问题、陷阱与调试技巧即使理解了原理在实际使用模板时依然会遇到各种令人困惑的编译错误和运行时问题。6.1 编译错误信息冗长难懂这是模板最著名的“特性”。一个简单的类型不匹配错误可能产生几十行甚至上百行的错误信息其中充斥着大量的模板实例化路径和内部类型名称。应对策略从最后一行看起编译器错误信息通常像栈一样层层展开最后一行往往是最根本的原因。寻找第一个“error:”在长长的信息中定位第一个标为“error”的行它通常指出了问题的直接原因如“no matching function for call to...”。使用static_assert进行友好提示在模板代码中可以使用static_assert在编译期提供清晰的错误信息。template typename T void onlyForIntegrals(T value) { static_assert(std::is_integralT::value, This function only accepts integral types!); // ... 函数逻辑 } onlyForIntegrals(3.14); // 编译错误并显示清晰信息概念ConceptsC20这是解决此问题的终极武器。概念允许你对模板参数施加约束使错误信息更清晰。template std::integral T // 要求T必须是整数类型 void onlyForIntegrals(T value) { ... } onlyForIntegrals(3.14); // 错误信息会直接指出“double”不满足“std::integral”概念6.2 代码膨胀问题模板的每个不同实例都会生成一份独立的代码。如果你用vectorint、vectorlong、vectordouble、vectorMyClass那么最终的可执行文件中就会存在多份vector的代码如push_back,size, 迭代器等。这可能导致可执行文件体积增大即“代码膨胀”。缓解方法共用底层实现对于不依赖类型的操作如内存分配、指针操作可以将其抽取到非模板的基类或辅助函数中。使用类型擦除如std::function、std::any它们通过虚函数等机制将类型信息延迟到运行时处理牺牲一定性能换取灵活性。谨慎实例化避免在不必要的地方使用过多不同的模板参数类型。6.3 两阶段查找与依赖名称在模板中编译器对名称的查找分为两个阶段第一阶段模板定义时查找不依赖于模板参数的名称如全局变量、函数非依赖名称。如果找不到直接报错。第二阶段模板实例化时查找依赖于模板参数的名称依赖名称。此时才会去检查这些名称是否有效。这会导致一个常见问题template typename T void foo() { bar(); // 错误第一阶段查找找不到非依赖名称bar T t; t.baz(); // 正确。baz是依赖名称依赖于T第二阶段查找。 }对于依赖名称如果它是一个类型需要使用typename关键字来告知编译器template typename T void foo() { typename T::iterator it; // 告诉编译器T::iterator是一个类型名 // 因为T未知编译器不知道iterator是T内部的类型还是静态成员变量。 }6.4 调试模板代码调试模板化的代码尤其是深度嵌套的STL容器或自定义模板时调试器显示的类型名可能非常复杂如std::__1::vectorstd::__1::basic_stringchar, std::__1::char_traitschar, std::__1::allocatorchar , std::__1::allocatorstd::__1::basic_stringchar, std::__1::char_traitschar, std::__1::allocatorchar ::iterator。技巧使用typedef或using别名为复杂的模板实例化起一个简单的别名。using StringVec std::vectorstd::string; StringVec words; // 调试时words的类型显示为StringVec清晰多了。在IDE中利用“快速查看”或“表达式求值”现代IDE通常能很好地解析和简化这些类型名。打印类型信息可以使用typeid(T).name()但输出是编译器修饰过的名称如i代表int。可以使用__PRETTY_FUNCTION__GCC/Clang或__FUNCSIG__MSVC宏在编译时生成包含清晰类型名的字符串。7. 实战设计一个简单的智能指针模板让我们综合运用所学设计一个简化版的std::unique_ptr即独占所有权的智能指针。它将演示类模板、移动语义、模板特化用于处理数组等概念。7.1 基础版本管理单个对象template typename T class SimpleUniquePtr { private: T* ptr nullptr; public: // 构造函数接管原始指针 explicit SimpleUniquePtr(T* p nullptr) : ptr(p) {} // 禁止拷贝独占所有权 SimpleUniquePtr(const SimpleUniquePtr) delete; SimpleUniquePtr operator(const SimpleUniquePtr) delete; // 移动构造函数转移所有权 SimpleUniquePtr(SimpleUniquePtr other) noexcept : ptr(other.ptr) { other.ptr nullptr; } // 移动赋值运算符 SimpleUniquePtr operator(SimpleUniquePtr other) noexcept { if (this ! other) { delete ptr; // 释放当前资源 ptr other.ptr; other.ptr nullptr; } return *this; } // 析构函数释放资源 ~SimpleUniquePtr() { delete ptr; } // 解引用操作符 T operator*() const { return *ptr; } T* operator-() const { return ptr; } // 获取原始指针 T* get() const { return ptr; } // 释放所有权 T* release() { T* old ptr; ptr nullptr; return old; } // 重置资源 void reset(T* p nullptr) { delete ptr; ptr p; } // 布尔转换用于条件判断 explicit operator bool() const { return ptr ! nullptr; } };这个基础版本已经具备了unique_ptr的核心功能独占所有权、自动管理生命周期、支持移动语义。7.2 特化版本管理对象数组上面的版本使用delete ptr这适用于单个对象。但如果SimpleUniquePtr管理的是一个通过new[]分配的数组就需要使用delete[]来释放。我们可以为T[]提供一个偏特化版本。// 主模板管理单个对象 template typename T class SimpleUniquePtr { // ... 同上 ... }; // 偏特化版本用于管理 T[] 数组 template typename T class SimpleUniquePtrT[] { private: T* ptr nullptr; public: explicit SimpleUniquePtr(T* p nullptr) : ptr(p) {} SimpleUniquePtr(const SimpleUniquePtr) delete; SimpleUniquePtr operator(const SimpleUniquePtr) delete; SimpleUniquePtr(SimpleUniquePtr other) noexcept : ptr(other.ptr) { other.ptr nullptr; } SimpleUniquePtr operator(SimpleUniquePtr other) noexcept { if (this ! other) { delete[] ptr; // 注意使用 delete[] ptr other.ptr; other.ptr nullptr; } return *this; } ~SimpleUniquePtr() { delete[] ptr; // 注意使用 delete[] } // 为数组版本提供下标操作符 T operator[](std::size_t index) const { return ptr[index]; } T* get() const { return ptr; } T* release() { T* old ptr; ptr nullptr; return old; } void reset(T* p nullptr) { delete[] ptr; ptr p; } // delete[] explicit operator bool() const { return ptr ! nullptr; } // 注意不提供 operator* 和 operator-因为指向的是数组不是单个对象 };现在我们可以这样使用SimpleUniquePtrint singlePtr(new int(42)); // 使用主模板 SimpleUniquePtrint[] arrayPtr(new int[10]{1,2,3}); // 使用偏特化模板 arrayPtr[0] 100; // 可以使用下标操作符 // *arrayPtr; // 错误数组版本没有定义operator*通过这个实战例子你可以看到模板如何让我们用一套逻辑的蓝图生成适应不同需求单个对象 vs. 对象数组的、类型安全且高效的代码。这正是C模板强大威力的体现在编译期生成最优化的、定制的代码而运行时开销几乎为零。

最新新闻

日新闻

周新闻

月新闻