十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

C++类与模板进阶:封装继承多态与泛型编程实战

C++类与模板进阶:封装继承多态与泛型编程实战 搞C的绕不开两个词一个是类一个是模板。从你第一次写出class Student到后来在项目里看到std::vectorstd::string这种嵌套模板这两个概念几乎贯穿了整个C开发周期。很多人学面向对象的时候会觉得“类就是把变量和函数放一起”学模板的时候又觉得“模板就是写一个能适配多种类型的函数”这种理解没错但只停留在“会用”层面。真正能把类和模板组合起来解决工程问题、能在编译报错时一眼定位到问题根因这才是C进阶的分水岭。这篇文章我会把类、继承、多态、模板特化这些核心机制拆开讲配合工程里最常遇到的场景从语法到底层原理、从代码到排查思路给你一条相对完整的实践路径。不管你是刚入门的初学者还是已经写了几年C想回头补系统知识的开发者这套思路都适用。1. 从C语言结构体到面向对象类到底解决了什么问题1.1 只有数据的结构体为什么不够用C语言里我们用struct组织数据比如学生信息struct Student { char name[64]; int age; float score; };但这类结构体有个天然的痛点数据和操作数据的函数是分离的。你维护一份学生成绩表所有处理Student的函数都得暴露在全局命名空间里比如printf_student(Student*)、update_score(Student*, float)。一旦项目变大函数和结构体之间完全是依靠开发者的自觉来保持一致没有强制捆绑机制。今天改动了Student的内部字段明天所有操作函数全要跟着排查一遍漏掉一个就在运行期出诡异问题。类在语法层面把数据和对数据的操作绑在了一起比如class Student { private: std::string name_; int age_; float score_; public: Student(std::string name, int age, float score) : name_(std::move(name)), age_(age), score_(score) {} void updateScore(float score) { if (score 0 score 100) { score_ score; } } void print() const { std::cout name_ , age_ 岁, score_ 分\n; } };虽然从汇编层面看类和结构体生成的机器代码差别并不大但源码的组织方式完全不同——它从语言层面强制你“以数据为中心”来组织代码。这就是面向对象的第一块基石封装。1.2 模板让编译器第一次替你写代码再说说模板。假设你要写一个求两个数最大值的函数没有模板的时候你在C语言里写宏#define MAX(a, b) ((a) (b) ? (a) : (b))宏有各种坑比如参数被求值两次、类型不安全。用重载倒是安全了但你要为每个类型写一遍max_int、max_double、max_string……代码几乎一模一样只是在类型上不一样。模板解决的问题就是把你从这种机械重复中解放出来template typename T T Max(T a, T b) { return a b ? a : b; }你可以对int、double、std::string甚至自定义的类调用Max前提是这个类型重载了operator。模板的本质不是“运行时多态”而是编译期的代码生成——编译器根据你传进去的类型帮你生成一份对应版本的函数代码。这也是模板会和“宏”被放在一起讨论的原因它们都是“写一份生成多份”的思路但模板远比宏安全、可控。所以类解决的是“数据和操作如何组织”模板解决的是“一套逻辑如何复用到多种类型”。一个管架构一个管复用两者配合才构成了现代C泛型编程的地基。2. 类的核心机制与工程用法2.1 类的基本构成成员变量、成员函数与访问控制class相比C的struct最直观的差异是访问控制。public、protected、private三个关键字不是摆设——它们决定了外部代码能在什么范围内触碰类的内部状态。工程上我建议你从一开始就养成好习惯默认把成员变量设为private通过公有接口对外暴露操作。有个很经典的例子是余额类class Account { private: double balance_{0.0}; public: void deposit(double amount) { if (amount 0) balance_ amount; } bool withdraw(double amount) { if (amount 0 amount balance_) { balance_ - amount; return true; } return false; } double getBalance() const { return balance_; } };如果把balance_直接设成public外部想扣多少钱就扣多少负余额也是合法的走函数接口业务规则就集中在一个地方管理。这就是private的真正价值——不是防黑客而是防止类的使用者绕过规则。struct和class的唯一语法区别是默认访问权限struct默认publicclass默认private。这个差别很小但能影响代码意图的表达。我个人的习惯是纯数据聚合用struct有行为、有内部状态的用class。另外两个加在成员函数上的修饰符需要专门说。const成员函数比如上面的getBalance() const表示这个函数不会修改对象状态这是在表达“只读”语义让编译器帮你检查。static成员函数则属于类本身调用时不需要对象实例适合放工厂函数或全局工具逻辑。2.2 构造函数、析构函数与拷贝控制的工程细节构造函数和析构函数是类实例的生命周期管理点。构造函数里完成资源的初始化析构函数里完成资源的释放。两者配对就是C里最核心的RAII思想——资源获取即初始化对象销毁即释放。一个典型的文件操作类class FileGuard { public: explicit FileGuard(const std::string path) { file_ std::fopen(path.c_str(), r); if (!file_) { throw std::runtime_error(打开文件失败); } } ~FileGuard() { if (file_) std::fclose(file_); } // 禁止拷贝避免同一文件指针被释放两次 FileGuard(const FileGuard) delete; FileGuard operator(const FileGuard) delete; private: std::FILE* file_; };这里有个关键点如果类里管理了裸资源指针、文件句柄、套接字必须同步考虑拷贝控制。默认的拷贝构造函数是浅拷贝两个对象会指向同一份资源析构时就会double-free。最简单的做法就像上面这样把拷贝直接禁掉。如果你确实需要拷贝语义就得实现深拷贝或者用shared_ptr这样的智能指针接管所有权。现代C里还有移动构造和移动赋值这对优化临时对象的拷贝开销很有用。一个类如果定义了析构函数编译器就不会默认生成移动操作这是一条容易踩的规则实际开发中要注意。2.3 抽象类和普通类的区别到底在哪这是面试高频题也是很多人一开始容易混淆的地方。普通类可以直接实例化生成对象抽象类包含纯虚函数不能实例化只能作为基类被继承。纯虚函数的语法是在函数声明后加 0class Shape { public: virtual double area() const 0; virtual ~Shape() default; };area()没有实现但它定义了一个“约定”任何继承Shape的类都必须提供自己的area()实现。这就是抽象类存在的意义——它定义接口契约把具体的计算逻辑延迟到派生类。普通类则更“具体”它可以有自己的完整实现可以直接拿来创建对象。区别用一张表看得很清楚对比项普通类抽象类能否实例化能不能是否含纯虚函数不含至少含一个主要用途直接描述实体定义接口/公共基类派生类必须实现什么不一定必须实现所有纯虚函数工程里我建议把抽象类当成一个纯接口来设计。一个抽象类里的纯虚函数越多它对派生类的约束就越明确但不要为了抽象而抽象——只有存在多个子类共享同一套行为、但具体实现各有差异时抽象类才有价值。3. 继承、多态与虚函数的实战细节3.1 继承的正确打开方式继承的核心目的是复用和扩展。“复用”就是说基类已经实现好的能力派生类可以直接用“扩展”是说派生类可以在基类基础上增加新功能或者改写基类的行为。最简单的继承class Animal { public: void eat() { std::cout 吃东西\n; } }; class Dog : public Animal { public: void bark() { std::cout 汪汪\n; } };Dog对象可以直接调用eat()不需要重新实现。这里有个细节继承方式有public、protected、private三种。绝大多数开发场景只用public继承因为它表达的是“是一个”关系——Dog是一种Animal。private继承在C里更多地被当成“组合”的一种变体来用新手阶段建议先不深究。构造和析构的顺序也容易踩坑构造时先调用基类构造函数再构造派生类成员析构时正好相反先析构派生类再析构基类。这个顺序是固定的写代码时要保证基类有合适的构造函数可用。另外继承并不总是最佳方案。如果你的本意只是“复用某个类的功能”而不是“它是某个类的子类型”组合往往更合适。比如Car需要引擎的启动能力让Car继承Engine就很怪正确的做法是Car里持有Engine对象。继承滥用会让类层次越来越深改一处影响一大片。3.2 虚函数、虚表与运行时多态虚函数是实现运行时多态的关键。把基类的函数声明为virtual派生类里重写它然后通过基类指针或引用调用时实际会执行派生类的版本。class Animal { public: virtual void sound() { std::cout 动物叫声\n; } virtual ~Animal() default; }; class Cat : public Animal { public: void sound() override { std::cout 喵喵\n; } }; class Dog : public Animal { public: void sound() override { std::cout 汪汪\n; } };调用时void makeSound(Animal a) { a.sound(); } int main() { Cat c; Dog d; makeSound(c); // 喵喵 makeSound(d); // 汪汪 }makeSound接收的是Animal它根本不知道传入的是Cat还是Dog但调用sound()时却能正确执行对应版本。这就是运行时多态——运行时根据对象的实际类型来决定调用哪个函数。底层机制是虚函数表vtable。每个含有虚函数的类在编译期会生成一张虚函数表里面存放指向各虚函数实现的函数指针。每个对象内部有一个隐藏的虚表指针vptr指向所属类的虚函数表。调用虚函数时先通过vptr找到虚表再从虚表中取出对应函数指针来调用。这就是为什么虚函数调用比普通函数调用多了一次间接跳转有一定性能开销。关于热词里提到的“msvc类内虚函数重载的虚表布局”MSVC的布局通常是对象起始处放vptr然后按声明顺序排成员变量如果有多重继承会有多个vptr每个基类子对象各有一个。这种细节在排查复杂继承结构的崩溃问题时特别有用新手可以先建立一个大致概念不用深挖每一行汇编。3.3 虚析构函数与纯虚析构函数的坑有一点必须反复强调基类析构函数必须是虚的。上面的例子我写了virtual ~Animal() default;这不是多余的。如果你通过基类指针delete一个派生类对象而基类析构函数不是虚的会调用基类的析构函数派生类的析构逻辑就被跳过了资源就泄漏了。Animal* p new Dog(); delete p; // 如果~Animal()不是virtualDog的析构不会执行这个坑在日常开发中非常常见。判断规则很简单只要一个类要被继承就把析构函数声明为virtual。还有一个冷门知识纯虚析构函数也是可以定义的。一个类可以有纯虚析构函数但仍然必须为它提供实现因为所有派生类析构时都会调用基类析构函数。如果一个抽象类的析构函数被声明为 0但没有实现链接时就会报错。工程上基类接口建议把所有析构函数都写成virtual需要多态删除时就加上virtual ~ClassName() default不需要多态删除时你也可以加损失的性能微乎其微换来的是安全。4. 模板从函数模板到类模板4.1 函数模板让算法适配任意类型函数模板不是普通的函数它是“产生函数的蓝图”。写的时候用template typename T声明类型参数调用时编译器根据实参推导出具体的T。回到开头的Max函数template typename T T Max(T a, T b) { return a b ? a : b; }你用Max(3, 5)编译时编译器推导出T int并生成一份int版本你用Max(3.14, 2.71)又生成一份double版本。这就是“代码生成”。但有个常见问题如果你写Max(3, 3.14)编译器会报错因为它推导出的T不一致一个是int一个是double。解决办法是显式指定模板参数Maxdouble(3, 3.14)让int隐式转换成double。这个细节经常会出现在入门练习题里。另一个容易被忽略的点函数模板的定义必须写在头文件里而不是源文件里。因为模板是在编译期实例化的编译器在调用点必须能看到完整定义才能生成代码。如果你把声明放.h、定义放.cpp链接时就会报“未定义引用”。这是初学者最常见的一类错误。4.2 类模板容器与通用数据结构类模板是函数模板的自然延伸它让类型本身也成为参数。写一个简单的Stacktemplate typename T class Stack { private: std::vectorT data_; public: void push(const T value) { data_.push_back(value); } void pop() { data_.pop_back(); } T top() { return data_.back(); } bool empty() const { return data_.empty(); } size_t size() const { return data_.size(); } };使用Stackint intStack; intStack.push(42); Stackstd::string strStack; strStack.push(hello);一个类模板就可以服务于任意元素类型。标准库里的std::vectorT、std::mapK, V、std::unique_ptrT全都是类模板的典型应用。成员函数如果定义在类外要重复写template声明template typename T void StackT::push(const T value) { data_.push_back(value); }每次写都要带着template typename T这是语法上的一个麻烦但也让你明确意识到“这不是一个普通类它属于一类类型”。C17以后支持了类模板参数推导CTAD你写std::vector v {1, 2, 3};编译器会自动推导出T int不需要写std::vectorint。这个特性让代码简洁了不少但也不是万能的嵌套模板、默认实参等场景推导失败时还是要老老实实写全模板参数。4.3 模板特化与偏特化处理特殊情况的利器模板覆盖的是“通用类型”的通用逻辑但有时候某个特殊类型需要完全不同的实现。比如你写了一个打印任意类型的函数模板但对于const char*你想要直接输出字符串内容而不是地址就可以用特化template typename T void print(const T value) { std::cout value std::endl; } template void printconst char*(const char* const value) { std::cout 字符串: value std::endl; }这叫全特化模板参数被完全指定。偏特化则是只指定一部分参数最常见的是指针类型的偏特化template typename T void print(const T* value) { std::cout 指针地址: value std::endl; }当print的参数是指针时会优先选择这个偏特化版本。热词里提到的“模板匹配”指的正是编译器在重载决议和特化选择中的匹配规则。简单来说全特化优先于偏特化偏特化优先于主模板。但要理解模板匹配的完整规则并不轻松涉及SFINAE、部分排序等机制。日常开发里你只需要记住只要有更具体的”匹配版本编译器就不会用通用版本。模板特化的典型场景字符串类型需要特殊处理某一特定数值类型需要更高精度的算法针对自定义类型的性能优化5. 类与模板的进阶组合5.1 非类型模板参数把数值也变成模板参数模板参数不一定是类型还可以是整数、枚举、指针等编译期常量。比如设计一个固定容量的数组类template typename T, size_t N class FixedArray { private: T data_[N]; public: size_t size() const { return N; } T operator[](size_t index) { return data_[index]; } const T operator[](size_t index) const { return data_[index]; } };使用时FixedArrayint, 10 arr;N是在编译期确定的所以data_可以直接用栈数组不需要动态分配内存。标准库里的std::arrayT, N就是这个思路的实现。非类型模板参数的优点是可以把尺寸、阈值等编译期常量写进类型系统编译器能据此做更多优化。缺点是类型会带上数值信息FixedArrayint, 10和FixedArrayint, 20是两种不同的类型不兼容。5.2 模板与继承的组合CRTP的奇技淫巧模板参数和继承组合有一个被广泛使用的模式叫CRTPCuriously Recurring Template Pattern奇异递归模板模式template typename Derived class Base { public: void interface() { static_castDerived*(this)-implementation(); } }; class DerivedImpl : public BaseDerivedImpl { public: void implementation() { std::cout 派生类实现\n; } };模板参数Derived正好是派生类自己。这种模式能实现编译期的“静态多态”没有虚函数也就没有虚表的性能开销。C标准库里很多迭代器、智能指针的实现都用到类似技巧。CRTP适合在需要通用框架逻辑、但不想付出运行时多态成本的时候使用。但它读起来非常绕新手很容易绕晕。我的建议是第一次接触先能看懂不要急着在项目里大量使用。CRTP一旦写不好模板报错信息能把人逼疯。5.3 模板匹配与编译期推导的判断规则模板推导的细节决定了模板能不能正常工作。理解下面几个规则能避开很多坑第一模板参数推导不自动做类型转换。Max(3, 3.14)会推导失败而不是把int转成double。如果你需要转换显式指定模板参数。第二const T的参数传const对象和普通对象都可以推导出T但T本身不带const。比如template typename T void f(const T t)传int时T推导为int参数类型是const int。第三数组和函数名在推导时会退化为指针除非参数类型是引用。比如template typename T void f(T value); template typename T void g(T value); int arr[5]; f(arr); // T 推导为 int* g(arr); // T 推导为 int()[5]这个区别在写按引用接收数组的模板时需要特别注意。热词里有“多维数组 c 指针”这里正好对应——如果你写的是T数组的维度信息会保留写成T就退化成指针了维度信息丢失。6. 从例题到实战类和模板落地练习6.1 用类和模板写一个简易容器类理论看再多不写代码等于白看。来做一个综合练习编写一个支持任意类型的简单栈。#include iostream #include stdexcept template typename T, size_t Capacity class TinyStack { public: void push(const T value) { if (size_ Capacity) { throw std::overflow_error(栈满了); } data_[size_] value; } T pop() { if (empty()) { throw std::underflow_error(栈空了); } return data_[--size_]; } bool empty() const { return size_ 0; } size_t size() const { return size_; } private: T data_[Capacity]; size_t size_{0}; };这个类包含了前面说的关键元素模板参数类型T和容量Capacity、构造函数初始化通过成员初始化器、异常处理、const成员函数。使用方式int main() { TinyStackint, 3 stack; stack.push(1); stack.push(2); stack.push(3); // stack.push(4); // 会抛异常 while (!stack.empty()) { std::cout stack.pop() ; } return 0; }这里要注意的是T data_[Capacity]这个写法要求T是默认可构造的。如果你传入一个没有默认构造函数的类型编译就会报错。解决思路是改用std::arrayT, Capacity或者直接使用std::vectorT。这个例子正好说明了模板代码“编译期对类型有潜在要求”这一特性。6.2 面向对象小游戏里的类设计热词里有“c小游戏”刚好可以拿这个场景讲类的设计。假设你要做一个文字冒险游戏游戏里有不同角色每个角色能攻击但攻击方式不同。基于抽象的基类设计class Character { protected: std::string name_; int health_; public: Character(std::string name, int health) : name_(std::move(name)), health_(health) {} virtual void attack(Character target) 0; void takeDamage(int damage) { health_ - damage; } bool alive() const { return health_ 0; } virtual ~Character() default; }; class Warrior : public Character { public: Warrior(std::string name) : Character(std::move(name), 150) {} void attack(Character target) override { std::cout name_ 用斧头砍!\n; target.takeDamage(25); } }; class Mage : public Character { public: Mage(std::string name) : Character(std::move(name), 90) {} void attack(Character target) override { std::cout name_ 释放火球!\n; target.takeDamage(40); } };游戏主循环里你可以用基类指针数组或容器管理所有角色std::vectorstd::unique_ptrCharacter characters; characters.push_back(std::make_uniqueWarrior(亚瑟)); characters.push_back(std::make_uniqueMage(梅林)); for (auto c : characters) { std::cout c-name_ 血量: c-health_ \n; }这里attack是纯虚函数每个派生类都有不同实现游戏循环里调用attack时展现的是多态行为。这个设计虽然简单它的好处在于以后加一个新角色类别只需要继承Character并实现attack不需要修改游戏循环的代码。这就是抽象类在真实项目中的典型用法。但name_和health_这两个成员是protected这意味着派生类可以直接访问。要不要设计成private加protected接口取决于你对封装程度的把控——我记得很多项目规范里是建议成员变量一律private用protected访问函数来暴露读取能力。6.3 排序算法模板化从冒泡排序开始热词里有“冒泡排序算法c”这里把冒泡排序写成模板版本比较器也变成参数template typename T, typename Comparator void bubbleSort(T arr[], size_t size, Comparator comp) { for (size_t i 0; i size - 1; i) { bool swapped false; for (size_t j 0; j size - 1 - i; j) { if (comp(arr[j 1], arr[j])) { std::swap(arr[j], arr[j 1]); swapped true; } } if (!swapped) break; } }比较器可以是函数指针也可以是函数对象甚至是lambdaint intArr[] {4, 2, 9, 1, 5}; bubbleSort(intArr, 5, [](int a, int b) { return a b; }); std::string strArr[] {banana, apple, cherry}; bubbleSort(strArr, 3, [](const std::string a, const std::string b) { return a.length() b.length(); });这个例子展示了模板的通用性一套排序逻辑既能排int也能排std::string既支持升序也支持按字符串长度排序。标准库的std::sort就是设计思路的极致版本底层用了更复杂的混合排序算法。热词里还出现了“c字符串数组初始化”、“c字符串转数组”。这两个话题正好可以串一下。数组初始化要注意// 正确 std::string arr1[] {a, b, c}; // 不要写成下面这样这是空数组编译报错 // std::string arr2[];字符串转数组通常指把std::string转成char数组std::string s hello; std::vectorchar chars(s.begin(), s.end()); chars.push_back(\0); // 保持C风格字符串的终止符这些语法细节在写模板代码时尤其容易遇到因为模板通常要求你传入的是随机访问容器或者数组类型处理不对就会编译失败。7. 环境配置与常见编译错误排查7.1 VSCode配置C/C开发环境写了这么多代码先得有顺手的环境。VSCode现在是很多人写C的首选配置其实不复杂但有不少人卡在第一步装完插件后不知道编译器在哪。具体步骤大概是这样的第一步安装编译器。Windows上推荐用MinGW-w64或者Visual Studio Build ToolsLinux/macOS直接用系统自带的g或clang即可。安装完成后命令行输入g --version能正常输出版本号说明编译器已经就绪。第二步安装VSCode扩展。打开扩展面板搜索“C/C”安装微软官方的C/C扩展。这个扩展提供IntelliSense、调试、代码补全能力。第三步配置编译任务。在项目根目录建.vscode/tasks.json内容大致是{ version: 2.0.0, tasks: [ { label: C 编译, command: g, args: [-g, ${file}, -o, ${fileDirname}/${fileBasenameNoExtension}.exe], group: { kind: build, isDefault: true } } ] }${file}表示当前打开的文件-g生成调试信息。如果你要编译多文件项目这里就要把所有.cpp文件都列出来或者换成./*.cpp通配符。第四步配置调试器。.vscode/launch.json里配置program字段指向刚才生成的可执行文件路径miDebuggerPath指向gdb或lldb。之后按F5就能单步调试。个人经验是第一次配置最好找一个简单的单源文件项目跑通确认编译和调试链路没问题再导入复杂项目。经常看到有人一上来就配置大型CMake项目配半天没反应其实问题往往出在最简单的路径上。7.2 常见报错速查与排查思路C的编译报错信息向来以复杂著称尤其是模板报错经常刷出几屏幕的“模板实例化链”。但大多数报错有规律可循我整理了几类最常见的错误信息常见原因排查方向expression must have class type用点号访问指针成员比如ptr.func()应为ptr-func()检查对象和指针是否搞混undefined reference to ...声明了函数/成员但没定义或模板声明和定义分离链接时检查符号模板定义放头文件no matching function for call to ...模板参数推导失败或传递了不可转换的类型看看是否缺显式模板参数、参数类型是否匹配error: use of deleted function调用了被 delete的函数比如拷贝构造函数被禁检查拷贝、赋值操作链接时多重定义模板或内联函数在多个源文件里重复定义考虑inline或统一放到头文件热词里“表达式必须包含类类型”的中文报错对应的就是上面第一行。实际场景里最常见的是声明了指针比如Student* p new Student()然后错写成p.name而不是p-name。编译器不傻知道你写错了但它只能在语法层面报“表达式必须包含类类型”因为你把指针当成对象用了。还有两个容易热词误解的“eclipse找不到或无法加载主类org.apache.catalina.startup.bootstrap”——这是Java Web项目里Tomcat配置问题和C的类没关系。“Windows仍在设置此设备的类配置代码56”——这是Windows设备管理器里的硬件驱动问题也不是C类的概念。搜问题的时候看到这些词要能区分出属于哪个圈子。7.3 类设计中的调试技巧调试类相关的代码关键要能看清“对象创建和销毁的时机”。推荐几个实用技巧第一在构造函数、析构函数里打断点观察对象生命周期的顺序。你可能会发现一个临时对象在表达式结束就析构了这就是“生命周期”在起作用。第二打印拷贝构造和移动构造的调用。在函数传参、返回值里拷贝构造可能在你看不见的地方被调用。用日志输出或者断点观察能帮助你理解为什么一个简单的传参会带来性能损耗。第三使用GDB的ptype命令查看对象的类型和成员布局ptype obj打印类型信息print obj查看成员值。调试多态对象时可以利用set print object on来让GDB打印出对象的动态类型。第四如果需要可视化类的关系可以用StarUML快速画类图。把类的属性和方法整理成图再设计继承关系这比直接在代码里反复改要快很多。类图画好后再动手写代码出问题的概率会低不少。类的设计不是一次就能定稿的。我实际开发里经常是先写一个能工作的版本然后通过单元测试、代码评审去发现哪些地方过度封装、哪些地方暴露太多细节慢慢地迭代出合适的边界。类和模板的用法最终还是要落到“解决问题”上。对于模板的错误信息我也分享一个经验不要从第一行开始读要往下翻到最后找真正报错的那一行文件位置。模板错误信息的大段背景资料都是编译器在描述“它尝试过哪些实例化路径”大部分时候真正问题都在最后几句“note: candidate template ignored”或者“error: static assertion failed”里面。记住这一点能省下很多抓狂的时间。最后再分享一个小技巧如果你发现自己总在写重复的类结构代码比如多个类都有同样的拷贝控制逻辑、同样的序列化方法不妨停下来想想是不是可以用一个基类模板把公共逻辑抽出来再用模板参数驱动差异部分。这不是让你滥用模板而是让类和模板真正成为你工具箱里的常备工具。学C最大的乐趣就是当你把这两个东西组合起来时代码能同时拥有优雅的架构和接近底层的性能这几乎是其他语言很难同时做到的事。
返回列表