ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++类模板从入门到实践:语法、特化与工程应用

C++类模板从入门到实践:语法、特化与工程应用 写C代码年头长了几乎不可避免地会跟类模板打交道。你写一个通用的环形队列、一个线程池或者干脆就是个能存任意类型的容器适配器如果不想用void*加强制转换那套老把戏类模板就是绕不开的方案。我早期学的时候被templatetypename T这种写法搞得很晕总觉得像魔法后来自己动手写了几遍、看编译器报错看出经验了才算真正把类模板的基本语法吃透。这篇就从一个可编译、可运行的实例讲起把类模板声明、定义、实例化、特化这些基础语法一次说清楚再分享一些从实际项目中踩坑踩出来的经验。内容适合刚学完类和对象、想写通用代码的初学者也适合那些能读懂模板代码但自己动手写就卡壳的朋友。1. 类模板的定义与基础写法1.1 为什么需要类模板先想一个很简单的场景你要做一个存放整数的栈于是写了一个IntStack类里面有push(int)、pop()、top()这些方法。过两天需求改了要存放double你怎么办复制一份改成DoubleStack那再要存std::string呢再复制一份代码会变得极其冗余而且逻辑一模一样的地方一旦要修 bug得改好几处。类模板的存在就是为了解决这个问题——把类型也变成一种“参数”。定义类的时候不写死里面操作的具体类型而是留一个“类型占位符”T等到真正使用的时候再告诉编译器“这里我存的是int”“那里我存的是double”。编译器根据你给的实参帮你生成一份对应类型的代码。这个过程在术语里叫“模板实例化”生成的类叫“模板类”。这就像做月饼模具。你不需要为豆沙馅、莲蓉馅、五仁馅各做一套模具只需要一套模具换不同的馅料就能做出不同口味的月饼。类模板里的T就是那个“馅料槽位”。1.2 两步看懂类模板声明与定义类模板的语法结构拆开看其实就两块template typename T class Stack { public: void push(const T val); T pop(); bool empty() const; private: std::vectorT data_; };第一行template typename T是模板声明头它告诉编译器“接下来这个类是一个模板里面的T是一个类型参数”。typename表示T代表一种类型你也可以写成class T两者在这个语境下完全等价我习惯用typename因为语义更明确——它就是个类型名字的占位符。从class Stack { ... };到分号结束这一整块跟普通类的写法几乎一样唯一的区别就是内部凡是涉及到“具体类型”的地方全用T替换。这里用std::vectorT作为底层存储好处是栈的容量可以自动增长不用自己管理内存和析构省掉一大半麻烦。注意这里的push(const T val)参数用的是const T而不是T val。原因很朴素模板的T可能是int这样的内置类型也可能是std::string这种重拷贝的类型。如果传值每次压栈都拷贝一次用const T传引用无论T是什么类型都只传一个引用进去同时const保证不会意外修改原值。这是一个应该从一开始就养成的习惯。1.3 类的具体化在尖括号里填上真实类型定义完模板怎么用关键动作叫“实例化”语法就是在类名后面跟一对尖括号把真实类型填进去Stackint intStack; Stackdouble doubleStack; Stackstd::string stringStack; intStack.push(42); doubleStack.push(3.14); stringStack.push(hello);注意Stack本身不是一个完整的类型Stackint才是。没有尖括号的那一坨只是“模板”你没法直接声明它的对象。编译器看到Stackint才真正生成一份针对int的类定义。同一个模板生成了多少个不同的类取决于你用了多少种不同的类型实参。Stackint和Stackdouble是两个完全独立的类型它们之间没有任何继承或转换关系。Stackint类型的指针也不能指向Stackdouble对象这个跟普通类一样的类型隔离规则没什么区别。有些人会误以为模板能自动做类型转换实际不行——你传double给Stackint的push编译器会尝试把double转成int再压栈但Stackint不会因此变成Stackdouble。2. 成员函数在类外定义时别漏掉模板头2.1 不带模板头的定义是初学者最常见的编译错误类模板的成员函数有两种写法第一种是把函数体直接写在类定义内部叫内联定义第二种是在类外定义。很多初学者老忘记类外定义需要额外带一遍模板头。内联写法最省事前面的Stack就是内联定义函数体直接写在类里编译器会自动内联处理短函数。但项目里代码一多经常需要把接口和实现分开这时候就得在类外定义成员函数template typename T void StackT::push(const T val) { data_.push_back(val); } template typename T T StackT::pop() { if (empty()) { throw std::runtime_error(Stack underflow); } T top data_.back(); data_.pop_back(); return top; } template typename T bool StackT::empty() const { return data_.empty(); }关键点有两个函数名前面必须再次写template typename T。类名必须写成StackT带上模板参数列表。第一个template typename T是说“这个函数本身也是一个模板它的模板参数也是T”第二个StackT是说“这个函数属于类模板Stack的T特化版本”。两个少了哪个都会编译报错。少第一个编译器会说你认识的这个Stack类不存在少第二个编译器会找不到Stack这个类名。2.2 类外定义时模板参数名可以换但别这么干有一个听起来很奇怪的语法点类外定义成员函数时函数声明里的模板参数名字跟类定义里写的可以不一样。比如类里写的是template typename T类外定义时你可以写template typename U只要位置对得上编译器就认template typename U U StackU::pop() { // ... 这里用的 U 就是类定义里的 T }注意类名后面的尖括号里必须写U不能写T——因为编译器要按照你当前这行的模板参数作用域来解析。这种情况在真实代码里很少见反而容易让人头晕。我建议始终使用同一个参数名类里类外都叫T或都叫U保持一致性省得看代码时还要在脑子里做映射。2.3 模板类成员函数与普通类成员函数的一个隐藏差异普通类的成员函数如果你写了但没调用编译器也可能不报错。但模板类的成员函数有个特点只有当某个成员函数被实际使用时编译器才会为它生成代码。如果Stackint里你从头到尾只调用了push没调用pop那么Stackint::pop()不会被实例化即使它内部有错误有时也可能不报错。这个叫“延迟实例化”算是模板的一个特性。这个特性有好处也有坑。好的一面是你可以给模板写一些“只对特定类型有意义”的成员函数只要不用到就不会编译失败。坏的一面是你无法只靠编译通过来保证整个模板类在所有类型下都是对的。最好的办法是写完之后用几个有代表性的类型内置类型、类类型、指针类型各实例化一遍并把所有成员函数都调用一遍。3. 模板参数不止有类型还有非类型参数3.1 非类型参数的语法数组大小就是个典型场景很多人以为类模板的尖括号里只能放类型其实还能放非类型参数比如整数、枚举、指针和引用。最常见的用途就是让容器的大小成为模板参数的一部分例如编译期固定容量的数组template typename T, std::size_t N class FixedArray { public: T operator[](std::size_t index) { return data_[index]; } const T operator[](std::size_t index) const { return data_[index]; } std::size_t size() const { return N; } private: T data_[N]; }; // 使用 FixedArrayint, 16 arr; arr[0] 1;这里的std::size_t N就是非类型模板参数。N不是一个类型而是一个值并且在编译期就必须确定下来不能在运行期再传一个变量进去。N是16和N是32的两个FixedArray照样是两个不同的类型。用这种写法size()函数甚至不需要存储任何容量字段因为N在编译期就是已知常量sizeof(FixedArrayint, 16)也直接等于16 * sizeof(int)不会像std::vector那样在堆上分配内存。对性能敏感的嵌入式场景来说这个特性特别好用。3.2 非类型参数有哪些限制非类型模板参数的类型不是随意的合法类型包括整型int、char、bool、std::size_t等枚举类型指针类型比如int*、const char*引用类型比如intstd::nullptr_tC20 开始还可以是字面量类类型比如自定义的 struct 可以在模板参数里用要求比较严格这里先不展开浮点数在 C20 之前是不能直接作为非类型模板参数的比如template double V在旧标准里直接报错C20 开放了限制。字符串字面量也不行你得用指针或者包装成自定义类型。这些都是早期标准留下的硬约束写代码时一遇到才知道难受。3.3 自动推导非类型参数的语法糖C17 引入了一个偷懒写法非类型参数可以用auto来声明让编译器自己推断类型。比如template auto N struct ValueHolder { static constexpr auto value N; }; ValueHolder42 intHolder; ValueHolder3.14f floatHolder;N在这种写法里可以是int也可以是float实例化时由实参的类型自动决定。不过这种特性的实用场景有限我见过最多的是用来做编译期常量表或者元编程里的标记类型。初学者了解一下存在即可不必强求使用。4. 默认模板参数与模板参数包的实用技巧4.1 默认参数给T一个“保底类型”类模板的参数也支持默认值语法跟函数默认参数很像template typename T int class DefaultContainer { public: T data; }; // 这样写合法 DefaultContainer defaultIntContainer; DefaultContainerdouble doubleContainer;注意区别如果模板参数有默认值声明对象时尖括号不能省略不写尖括号的DefaultContainer dc;是错的必须写DefaultContainer dc;。那个空空的尖括号是在告诉编译器“全部用默认参数”。我给栈加上默认值时template typename T int class Stack { // ... };然后Stack s;就相当于Stackint s;。这个语法在实现某种“策略类”的时候很好用比如一个容器既能用std::vector做底层也能用std::deque做底层你就可以把底层类型也变成模板参数并给一个默认值。4.2 模板模板参数把模板作为参数传进去再进一步类模板的参数还可以是“另一个模板”。这种写法叫模板模板参数语法看起来容易劝退其实逻辑很直接。比如你想让Stack类能自由选择底层容器是std::vector还是std::dequetemplate typename T, template typename class Container std::vector class Stack { public: void push(const T val) { data_.push_back(val); } T pop() { T top data_.back(); data_.pop_back(); return top; } bool empty() const { return data_.empty(); } private: ContainerT data_; };这里第二行template typename class Container的意思是Container本身是一个类模板它接受一个类型参数实例化后得到一个具体的容器类。所以在类的内部ContainerT才是一个完整的类型可以用来声明成员。实际使用要注意一个坑std::vector有两个模板参数第二个是分配器有默认值而std::array有两个参数且第二个没有默认值。直接写template typename class Container std::vector在现代标准里一般能通过匹配但想传std::map这种多个参数的容器就会遇到“参数数量不匹配”的报错。解决办法是放宽模板模板参数的定义template typename T, template typename... class Container std::vector class Stack { ContainerT data_; };这里用了typename...来吸收任意数量的模板参数兼容性一下就强了很多。这种写法在 stl 容器适配器的源码里非常常见理解了这一层再去读库代码会顺很多。4.3 模板参数包不止一个参数类模板如果想支持可变数量的模板参数就用参数包。最典型的例子是std::tuple它可以接受任意多个类型参数template typename... Types class Tuple { public: Tuple(const Types... values) : values_(values...) { } private: std::tupleTypes... values_; }; Tupleint, double, std::string triple(1, 2.5, hello);typename... Types表示Types是一个“类型参数包”可以代表零个或多个类型。展开的时候Types...会把包里的所有类型依次展开。这个语法刚接触时很绕但它其实是现代 C 可变参数模板的地基。类模板里配合std::tuple或者递归继承做类型列表是元编程里的常规操作。这篇讲基础语法就不展开但建议在学会基本类模板之后专门找资料把参数包和std::tuple一起吃掉。5. 类模板的特化与偏特化5.1 类模板名称不能重复特化的前置知识先说一个基本概念在同一作用域内类模板的名字和普通类的名字不能重复两个类模板之间如果模板参数列表不同名字也可能引起冲突。比如你不能先声明一个普通类Box又声明一个template typename T class Box编译器不会允许这种重复定义。搜“类模板名称不能重复”这个问题的人多半就是踩了这个坑。这个“不能重复”的规则在特化场景下要特别留意。特化指的是在保留类模板整体框架的前提下针对某些特定类型单独写一份实现。特化的语法仍然是template class LoadContent...但它不是简单的新类而是某个已知模板的“具体版本”。5.2 全特化针对某个特定类型单独写一份实现假设你有一个通用的打印类模板对一个类型做了专门处理#include iostream #include string template typename T class Printer { public: void print(const T value) { std::cout generic: value std::endl; } }; // 全特化T int 时的专门版本 template class Printerint { public: void print(const int value) { std::cout int version, value value std::endl; } };全特化的写法是template 尖括号里什么都不留表示“没有模板参数已经完全具体化了”。后面紧跟的class Printerint明说了这是在为Printerint这个具体类型定制实现。实际开发里全特化常用于对bool做位压缩、对const char*做字符串专门处理、对某种特定类型跳过某些逻辑。比如一个SerializerT模板通用版本用二进制序列化但对std::string特化成带长度的字符串序列化就是典型用法。5.3 偏特化只锁定参数的一部分偏特化是类模板独有的特性函数模板没有C20 之前。它的意思是模板参数仍然存在但是已经被部分限制了。最常见的偏特化场景是指针类型template typename T class PointerWrapper { public: void describe() { std::cout generic type std::endl; } }; template typename T class PointerWrapperT* { public: void describe() { std::cout pointer type std::endl; } };用法PointerWrapperint obj1; // 走通用版本 PointerWrapperint* obj2; // 走指针偏特化版本 PointerWrapperstd::string* obj3; // 走指针偏特化版本这里template typename T class PointerWrapperT*的意思可以这样读当用户在尖括号里传入的是一个指针类型不管它指向什么就使用这一份实现。特化后的模板参数列表T*会与实例化的实参进行模式匹配匹配成功就用这份专门代码。偏特化还能按const、按引用、按std::vectorT这种容器类型做匹配。这是模板元编程里最强大的武器之一也是理解std::is_pointerT、std::remove_referenceT这些类型萃取工具实现原理的关键。比如std::remove_referenceT的偏特化版本就是为T和T各写了一份空壳类让type指向T本身。懂得了偏特化你再看这类工具源码会有一种“原来如此”的通透感。5.4 特化的一个易错点名称歧义有一个细节非常容易被忽视一旦你在某个命名空间里定义了类模板Foo就不能再定义另一个非模板的类Foo也不能再定义另一个不同参数数量的模板Foo。类模板名称在同一作用域内不允许重复这不是一个“风格建议”而是语言规则。跨作用域比如放在不同的命名空间则互不影响。还有一个相关易错点特化版本的成员函数可以不跟通用版本一致。类模板的全特化本质上是“一个全新的类”所以Printerint里可以只写print(const int)不写print(const T)这完全合法。但你在泛型代码里假设所有特化都有相同的接口时就会出问题。写通用库时务必保证所有特化版本的公共接口保持一致这是使用者的隐式契约。6. 类模板成员函数、友元函数和静态成员的处理细节6.1 友元函数模板类的友元声明有三种情况友元函数friend function在模板类里是个容易翻车的地方。最常见的情况是重载operator来打印类内容。假设你有这样一个类模板template typename T class Point { public: Point(T x, T y) : x_(x), y_(y) { } private: T x_; T y_; };你希望std::cout p能打印坐标于是写了普通的友元函数template typename T class Point { friend std::ostream operator(std::ostream os, const PointT p); // ... };这种写法声明了一个“为所有PointT都存在的友元函数模板”。实现时要额外小心template typename T std::ostream operator(std::ostream os, const PointT p) { os ( p.x_ , p.y_ ); return os; }注意因为Pointint里的友元声明会为int生成一个友元函数Pointdouble里会再生成一个double版本它们是不同的函数实例。如果你在类外定义函数模板时漏掉了template typename T就会出现链接错误——类里声明了一个函数但外面找不到定义。另一个选择是在类定义内部直接实现友元函数体这种叫“友元函数定义”本质上为每个特化都生成了一个非模板的普通友元函数template typename T class Point { friend std::ostream operator(std::ostream os, const PointT p) { os ( p.x_ , p.y_ ); return os; } };这种方式编译链接都更省心我实际项目里多数用这种。缺点是这个函数只能在参数推导时被找到如果你需要显式取它的函数指针会比较麻烦。无论如何友元的两种方式要分清类内定义的友元是“普通函数”类外声明的友元是“函数模板的实例”。6.2 静态成员每个特化都有一份独立的静态成员类模板的静态成员跟普通类的静态成员有本质区别。普通类的static成员只有一份所有对象共享。类模板的静态成员则是“每一个特化版本都有一份”。举例template typename T class Counter { public: static int count; }; template typename T int CounterT::count 0;Counterint::count、Counterdouble::count是两个完全不同的变量分别存储在不同地址互不影响。这个行为的一个实际用途是你想统计每个类型的对象创建数量不需要为每种类型手写一个计数器模板自动帮你生成了一份套独立计数。定义静态成员时同样要带template typename T和CounterT::前缀template typename T int CounterT::count 0;如果不写这行定义静态成员在链接时就会报“undefined reference”错误。这是类模板静态成员最典型的坑。6.3 类模板内的类型别名与嵌套类在类模板内部可以定义类型别名template typename T class Container { public: using value_type T; using reference T; using const_reference const T; // 嵌套类 class Iterator { public: Iterator(const_reference ref) : ref_(ref) { } private: const_reference ref_; }; };外部访问时Containerint::value_type x 10; Containerint::Iterator iter(x);这里值得注意的一个语法现象是在类模板外部、需要写“依赖类型”的地方不能光写ContainerT::value_type因为编译器在看到::后面接一个名字时如果不确定它是类型还是静态成员就会当作不是类型来解析。标准做法是在类型前面加typenametemplate typename T void PrintValueType() { typename ContainerT::value_type value T{}; }这个typename在这里不是多余的而是语法必需。缺少它编译器可能会报一个奇怪的错误说你用了一个非类型名。这个坑我在写模板工具类时不止一次踩到过。7. 类模板定义和声明的文件组织方式7.1 头文件里写完整定义不是“风格偏好”而是硬性要求普通类你可以在.h里放声明在.cpp里放成员函数实现然后链接器把所有.cpp编译出来的目标文件拼起来。类模板不行因为类模板不是一个具体的类编译器必须看到完整的模板定义才能在你写Stackint的时候实例化出真正的代码。如果你只在.h里写了类声明成员函数实现在.cpp里另一个.cpp文件里用了Stackint编译时处于“看到声明、看不到定义”的状态于是它自己尝试实例化Stackint::push时找不到函数体只能留下一个外部符号引用链接阶段就会报“undefined reference”。所以类模板的普遍做法是把模板的完整定义直接写在头文件里也就是“定义即声明”。这也是为什么 STL 头文件里全是大段大段的模板实现代码看起来不像普通库那样把接口和实现分开。7.2 如果非要分离显式实例化导出有一种情况可以做到类模板实现放在.cpp里就是你自己知道只会用到有限的几个类型然后提前把它们的实例化写出来。语法是// Stack.h template typename T class Stack { // ... }; // Stack.cpp #include Stack.h template typename T void StackT::push(const T val) { // ... } // 显式实例化明确告诉编译器生成 int 和 double 的版本 template class Stackint; template class Stackdouble;这样写之后Stackint和Stackdouble的实现确实编译进了Stack.cpp的目标文件使用方只需要头文件里能看到声明就能链接成功。缺点是需要手动维护“支持的类型清单”每新增一个类型就得回来加一行显式实例化。如果类型不确定还是老老实实把实现写在头文件里最稳。7.3 头文件里模板代码的编译开销问题模板定义全放头文件的一个副作用是编译时间会变长。每次#include那个头文件编译器都要重新解析、实例化一遍用到的模板。项目大了以后改动一个模板定义可能导致大量源文件重新编译。解决办法有前向声明模板、把大模板拆成小的基类叫“模板基础类模式”、或者用显式实例化配合减少实例化类型数量。这些属于工程优化范畴但理解了“模板必须在定义处实例化”之后你看到编译器报错里出现的模板展开过程就不会再一头雾水。8. 常见编译错误与排查思路8.1 错误一找不到匹配的类模板定义这是最常见的一类报错表现形式类似error: Stack is not a template error: use of class template Stack requires template arguments排查思路看有没有遗漏template typename T头。看类名后面的尖括号和模板参数列表是否配对。看是不是把Stack和StackT混用了比如在类外成员函数定义处写成void Stack::push()当然不行得写void StackT::push()。有时候是模板定义本身在一个namespace里但使用处却没有加namespace前缀。这种报错会特别迷惑因为编译器说“找不到”的名字实际上就在头文件里。养成用std::、项目命名空间全限定名的习惯这类问题很好避免。8.2 错误二类型与模板参数数量不匹配error: wrong number of template arguments (1, should be 2)这种往往是把需要两个参数的模板只给了一个参数或者反过来。比如std::map需要两个类型参数你在模板模板参数里只写了一个就会触发这个错误。检查模板定义里到底声明了几个参数有没有默认值匹配一下就能定位。8.3 错误三依赖类型前缺少 typenameerror: missing typename prior to dependent type name FooT::Bar这个报错信息基本是明说照着加typename就行。不过要理解它在讲什么FooT::Bar这个类型依赖模板参数T编译器无法在解析模板定义的“第一阶段”确定Bar到底是类型还是成员变量必须靠typename给它一个担保。8.4 错误四模板特化与主模板不匹配error: template-id Fooint for Fooint does not match any template declaration这个报错通常出现在特化声明写错模板参数数量或者特化的类名跟某个通用模板对不上。检查特化时的尖括号参数全特化是空的template 偏特化是template typename U然后用FooU*这样的模式匹配参数数量务必跟主模板一致。8.5 编译错误阅读心态模板报错是堆叠出来的模板编译错误的信息量很大一次会刷出几百行因为编译器在实例化时会把整个调用链都堆叠出来。我的经验是看第一个报错和最后一个报错中间的过程往往是被牵连的。先处理第一处真实的语法或类型错误重新编译报错通常就会雪崩式减少。不要试图在一个报错清单里把所有问题都解决完模板错误的“连锁反应”特别强越修到后面越乱。9. 类模板的工程实践心得这里分享几个从实际项目里总结出来的经验不一定写在教科书上但非常实用。9.1 用static_assert给模板加“约束条件”类模板的T理论上可以是任意类型但很多时候你的实现只对某些类型有意义。比如你的Stack底层用了std::vectorT那T必须是可拷贝的。如果用户传了一个不可拷贝的类型错误信息会藏在 STL 头文件深处极其难读。解决办法是用static_assert在入口处直接拦截#include type_traits template typename T class Stack { static_assert(std::is_copy_constructibleT::value, Stack requires copy-constructible types); // ... };这样用户一实例化Stackstd::unique_ptrint编译错误会直接指向你的static_assert那一行提示信息一目了然。C20 开始有concepts和requires子句能更优雅地做这件事但static_assert在今天仍是兼容性最好、最朴素的手段。9.2 让类模板支持const正确性模板类里成员函数的const修饰同样重要。比如StackT的top()函数如果你希望不修改栈内容时也能查看栈顶元素需要提供两个版本template typename T T top() { return data_.back(); } template typename T const T top() const { return data_.back(); }注意两个版本的返回类型不同一个是T一个是const T。对const Stackint对象调用top()会匹配第二个版本对非const对象匹配第一个版本。模板在这里跟普通类完全一样别因为模板语法复杂就忘了const正确性。还有一点返回内部引用意味着调用者可以修改栈内元素打破栈的不变量所以现实里要么返回const T要么干脆不提供top()只给pop()这是设计取舍。9.3 类模板与继承结合的两个方向类模板可以作为基类被继承这是实现通用代码复用的经典方式。比如你写一个BaseServiceT然后专门派生出UserService、OrderServicetemplate typename T class BaseService { public: void save(const T entity); T findById(int id); }; class UserService : public BaseServiceUser { // 业务逻辑 };这里BaseServiceUser已经是一个具体类型派生类不用再写模板参数直接继承即可。另一个方向是派生类也是模板比如template typename T class Derived : public BaseT这种写法在实现通用框架时很常见但要注意基类的模板参数如果是“依赖类型”有时候需要this-来访问基类成员否则编译器在模板的两阶段查找中可能找不到名字。这个问题遇到的频率很高单独提一下在类模板的派生类里访问基类模板的成员函数建议显式用this-member()能少踩很多编译器的坑。9.4 模板代码的性能与可读性平衡类模板在实例化后生成的代码是“定制的”编译器能做很多优化比如FixedArrayint, 16不涉及堆分配、不存储容量字段性能可能比std::vectorint好。但这也带来代码膨胀问题Stackint、Stackdouble、Stackstd::string会生成三份几乎一样的机器码。如果对象非常大这种膨胀可以接受如果模板定义本身就很庞大比如几千行实例化十几个类型二进制体积和编译时间都会显著上升。我的习惯是模板定义里只放真正与T类型相关的逻辑把与T无关的辅助逻辑抽到基类或独立函数里。这样既保持了模板的灵活性又避免重复生成无意义代码。这个专业技巧叫“模板瘦身”读大型模板库源码时经常能看到这种设计影子。最后再分享一个小习惯我写完类模板一定会特意用三种类型做编译测试——一个内置类型、一个复杂类类型比如std::string、一个指针类型。只有这三种都编译通过、运行正确我才敢说这个类模板是真的写对了。模板代码跟普通代码不一样编译过不代表永远对只有把“类型多样”这件事亲自验证过才算是真正掌握了一个类模板的语法和语义。
返回列表