
1. 项目概述从一行代码看C面向对象编程的深度实践看到这个标题很多C老手会心一笑。这行std::cout “C语言中的面向对象编程实践与优化技巧” std::endl本身就是一个绝佳的隐喻。它用C标准库中最经典的流输出对象std::cout来“输出”关于C面向对象编程OOP本身的内容。这恰恰体现了C OOP的核心思想之一抽象与封装。标准库将底层复杂的I/O操作封装成一个简单易用的对象我们只需关心“输出什么”而无需关心数据是如何被格式化、缓冲最终送到控制台的。这个项目标题或者说这个“代码项目”旨在深入探讨如何像设计std::cout一样去设计和实现我们自己的C面向对象程序并在这个过程中融入那些能让代码跑得更快、更稳、更优雅的优化技巧。面向对象编程在C中绝非简单的“类与对象”概念堆砌。它是一套从设计思想封装、继承、多态、到内存模型对象布局、虚函数表、再到运行时行为动态绑定、RAII的完整体系。对于初学者它可能是入门门槛对于有经验的开发者它则是构建大型、可维护、高性能系统的基石。本文将从一个资深C开发者的视角拆解在真实项目中运用OOP时遇到的典型场景、必须掌握的实践模式以及那些教科书上不会细讲却能显著影响性能与稳定性的优化技巧。无论你是正在学习C OOP基础还是已经写过不少代码但总感觉“不够C味儿”或是正在为性能瓶颈头疼这篇文章都将提供可直接参考的实战思路和代码示例。2. 面向对象编程的核心设计原则与C实现在动手写类之前理清设计原则比记忆语法更重要。C的OOP能力强大且灵活但也因此容易写出低效或难以维护的代码。遵循一些核心原则能让你的面向对象设计事半功倍。2.1 封装不仅仅是private封装是OOP的基石其目的是隐藏对象的内部实现细节仅对外暴露必要的接口。在C中这通常通过访问说明符public、protected、private来实现。但高水平的封装远不止于此。1. 接口与实现分离这是封装的更高层次体现。头文件.h或.hpp应只包含类的公开接口声明而将所有实现细节放在源文件.cpp中。这能最小化编译依赖提升编译速度。例如如果一个类NetworkFetcher内部使用了某个第三方网络库这个库的头文件不应出现在NetworkFetcher.h中而应仅在NetworkFetcher.cpp里包含。// NetworkFetcher.h - 干净的接口 class NetworkFetcher { public: NetworkFetcher(const std::string url); ~NetworkFetcher(); std::string fetchData(); // 只声明不暴露实现细节 private: class Impl; // 前向声明一个实现类Pimpl惯用法 std::unique_ptrImpl pImpl; // 使用智能指针管理实现 };2. 使用Pimpl惯用法Pointer to Implementation如上例所示Pimpl是C中实现编译防火墙和完全封装的经典技巧。它将所有私有成员包括数据成员和函数移到一个独立的实现类中在主类中仅保留一个指向该实现类的指针。这样做的好处是二进制兼容性修改实现类的私有成员不会导致使用该类的客户端代码重新编译。降低耦合头文件变得非常简洁依赖减少。隐藏实现彻底。注意Pimpl会带来一次额外的指针间接访问和堆内存分配的开销。在性能极度敏感或对象生命周期极短的场景下需权衡使用。2.2 继承谨慎使用“是一个is-a”关系继承用于建立类之间的层次关系实现代码复用和多态。C支持公有、保护和私有继承最常用的是公有继承它表示“派生类对象是一个基类对象”。1. 遵循Liskov替换原则LSP这是继承设计的黄金法则。任何基类出现的地方都应该可以透明地替换成其派生类而程序的行为不变。违反LSP的继承设计是脆弱的。例如让Square类继承Rectangle类就是一个经典的反例因为修改正方形边长的方法会同时影响长和宽这与长方形的行为不一致。2. 区分接口继承和实现继承纯虚函数只继承接口必须在派生类中实现。用于定义抽象基类ABC。虚函数继承接口和默认实现。派生类可以选择覆盖override它。非虚函数继承接口和强制实现。派生类不应改变其行为。一个好的经验是如果基类中的某个函数在派生类中需要有不同行为它应该是虚函数或纯虚函数如果它的行为在所有派生类中都应该一致则应为非虚函数。2.3 多态动态绑定的力量与成本多态允许我们通过基类的指针或引用来操作派生类对象并在运行时调用正确的函数版本。这是通过虚函数表vtable实现的。1. 理解虚函数表开销每个包含虚函数的类或从包含虚函数的类派生都会有一个关联的虚函数表。每个对象会包含一个指向该表的指针vptr。这意味着内存开销每个对象增加一个指针大小通常4或8字节。性能开销虚函数调用比普通函数调用多一次间接寻址通过vptr找到vtable再找到函数地址。在紧密循环中调用大量虚函数可能成为瓶颈。2. 何时使用多态当你的代码需要处理一组具有共同接口但具体行为不同的对象时多态是利器。例如图形编辑器中的Shape基类和Circle、Rectangle等派生类。3. 替代方案如果性能是关键且类型集合在编译时已知可以考虑使用std::variant或访问者模式这属于“编译时多态”没有运行时开销。// 基于 std::variant 和 std::visit 的编译时多态示例 using Shape std::variantCircle, Rectangle; void draw(const Shape s) { std::visit([](auto shape) { shape.draw(); // 调用具体类型的 draw在编译时决定 }, s); }3. 关键优化技巧从对象构造到内存管理C的“零开销抽象”哲学意味着良好的OOP实践本身不应带来不必要的性能损失。以下技巧能帮助你在享受OOP好处的同时写出高效的代码。3.1 对象构造、拷贝与移动的优化对象的创建和复制是常见的性能热点。1. 返回值优化RVO和命名返回值优化NRVO这是编译器的一项优化可以避免在函数返回对象时发生不必要的拷贝或移动。现代编译器在大多数情况下都能很好地应用RVO/NRVO。// 良好的写法通常会被RVO优化 std::vectorint createVector() { std::vectorint vec {1, 2, 3, 4, 5}; return vec; // 编译器可能会直接在调用者的栈帧上构造vec避免拷贝 } auto myVec createVector(); // 可能没有拷贝发生实操心得为了最大化利用RVO应该直接返回局部对象而不是返回std::move(局部对象)。后者反而会阻止RVO强制使用移动语义。2. 移动语义C11及以上对于管理资源的类如动态数组、文件句柄实现移动构造函数和移动赋值运算符至关重要。它们通过“窃取”临时对象右值的资源来避免深拷贝。class Buffer { public: Buffer(size_t size) : data_(new int[size]), size_(size) {} // 移动构造函数 Buffer(Buffer other) noexcept : data_(other.data_), size_(other.size_) { other.data_ nullptr; // 将源对象置于有效但可析构状态 other.size_ 0; } // 移动赋值运算符 Buffer operator(Buffer other) noexcept { if (this ! other) { delete[] data_; // 释放已有资源 data_ other.data_; size_ other.size_; other.data_ nullptr; other.size_ 0; } return *this; } ~Buffer() { delete[] data_; } private: int* data_; size_t size_; };3. 使用初始化列表在构造函数中使用成员初始化列表来初始化成员变量而不是在构造函数体内赋值。这对于常量成员、引用成员以及没有默认构造函数的类类型成员是必须的同时对于其他类型也更高效避免了一次默认构造加一次赋值的开销。3.2 内存管理优化智能指针与对象池手动管理new/delete是错误和内存泄漏的主要来源。现代C提供了更好的工具。1. 优先使用智能指针std::unique_ptrT用于独占所有权的资源。轻量无额外开销。std::shared_ptrT用于共享所有权的资源。有引用计数的开销。std::weak_ptrT配合shared_ptr使用解决循环引用问题。2. 避免循环引用shared_ptr的循环引用会导致内存泄漏。使用weak_ptr来打破循环。class Node { public: std::vectorstd::shared_ptrNode children; std::weak_ptrNode parent; // 使用 weak_ptr 指向父节点避免循环引用 };3. 对于高频创建/销毁的小对象考虑对象池如果程序中需要频繁创建和销毁某个固定大小的类对象例如网络连接、游戏中的粒子每次new/delete或malloc/free的系统调用开销会很大。此时可以实现一个简单的对象池预先分配一大块内存并在其中复用对象。templatetypename T class SimpleObjectPool { public: T* acquire() { if (freeList_.empty()) { // 分配新块这里简化处理实际可能批量分配 auto* obj new T(); allocated_.push_back(obj); return obj; } else { auto* obj freeList_.back(); freeList_.pop_back(); return obj; } } void release(T* obj) { // 调用析构函数清理对象状态但不释放内存 obj-~T(); freeList_.push_back(obj); } ~SimpleObjectPool() { for (auto* ptr : allocated_) { delete ptr; } } private: std::vectorT* allocated_; std::vectorT* freeList_; };注意事项对象池的实现需要考虑线程安全、对象状态重置、内存碎片等问题。对于大多数应用标准库的分配器已经足够高效。只有在性能剖析Profiling明确指向内存分配是瓶颈时才考虑引入自定义对象池。3.3 虚函数与运行时多态的性能考量虚函数调用有开销但在设计良好的系统中这点开销通常是值得的。然而在极端性能敏感的代码路径中可以考虑以下优化1. 减少虚函数调用频率例如在循环内部如果可能将虚函数调用移到循环外部或者使用静态分派。2. 使用final关键字如果确定某个类不会被继承或者某个虚函数不会被进一步重写将其标记为final。这给了编译器更多的优化空间在某些情况下编译器可能能够去虚拟化devirtualize该调用将其转换为直接调用。class Widget final { // 这个类不能被继承 public: virtual void draw() const final; // 这个函数不能在派生类中重写 };3. 使用CRTP实现静态多态奇异递归模板模式Curiously Recurring Template Pattern可以在编译时实现多态行为完全消除虚函数开销。它适用于类型在编译时已知的场景。template typename Derived class Shape { public: void draw() const { // 将调用静态分派到派生类的实现 static_castconst Derived*(this)-drawImpl(); } }; class Circle : public ShapeCircle { public: void drawImpl() const { /* 绘制圆的实现 */ } }; class Square : public ShapeSquare { public: void drawImpl() const { /* 绘制正方形的实现 */ } }; templatetypename T void render(const ShapeT shape) { shape.draw(); // 编译时决议无虚函数开销 }4. 设计模式在C OOP中的高效实践设计模式是针对常见设计问题的经典解决方案。在C中应用它们时需要结合语言特性进行优化。4.1 单例模式Singleton的线程安全实现单例模式确保一个类只有一个实例。在C中需要特别注意线程安全和初始化顺序。1. C11之后的推荐实现Meyers‘ Singleton利用局部静态变量的线程安全初始化特性C11标准保证这是最简洁、高效的实现。class Singleton { public: static Singleton getInstance() { static Singleton instance; // C11保证此初始化是线程安全的 return instance; } // 删除拷贝构造和赋值操作 Singleton(const Singleton) delete; Singleton operator(const Singleton) delete; private: Singleton() default; // 私有构造函数 ~Singleton() default; };2. 需要传递参数的Singleton如果Singleton构造需要参数上述方法不适用。可以使用std::call_once配合std::once_flag。class ConfigManager { public: static ConfigManager getInstance(const std::string configPath) { std::call_once(initFlag, []() { instance_.reset(new ConfigManager(configPath)); }); return *instance_; } private: static std::unique_ptrConfigManager instance_; static std::once_flag initFlag; ConfigManager(const std::string path) { /* 加载配置 */ } }; // 在.cpp文件中定义静态成员 std::unique_ptrConfigManager ConfigManager::instance_; std::once_flag ConfigManager::initFlag;4.2 工厂模式与对象创建优化工厂模式将对象创建逻辑封装起来。结合C的移动语义和完美转发可以写出非常高效的工厂函数。class Widget { /* ... */ }; class Gadget { /* ... */ }; templatetypename T, typename... Args std::unique_ptrT makeWidget(Args... args) { // 使用完美转发将参数原封不动地传递给构造函数 return std::make_uniqueT(std::forwardArgs(args)...); } // 使用 auto w1 makeWidgetWidget(100, “foo”); // 构造 Widget(100, “foo”) auto w2 makeWidgetGadget(); // 构造 Gadget()std::make_unique和std::make_shared不仅是语法糖它们将内存分配和对象构造合并可能更高效并且是异常安全的。4.3 观察者模式与避免虚函数风暴观察者模式定义了一种一对多的依赖关系。传统的实现中每个观察者都需要实现一个虚函数如update。当主题状态变化通知所有观察者时会引发一系列虚函数调用。优化思路使用std::function和信号槽我们可以用std::function来存储可调用对象函数指针、lambda、绑定表达式等从而解除观察者必须继承自某个基类的限制也提供了更大的灵活性。这类似于Qt的信号槽机制的精简版。class Subject { public: using Callback std::functionvoid(const std::string); void attach(Callback cb) { observers_.push_back(std::move(cb)); } void notify(const std::string data) { for (const auto cb : observers_) { if (cb) cb(data); // 直接调用无虚函数开销 } } private: std::vectorCallback observers_; }; // 使用 Subject s; // 观察者1一个自由函数 void freeFunc(const std::string msg) { std::cout “Free: ” msg std::endl; } s.attach(freeFunc); // 观察者2一个lambda表达式 s.attach([](const std::string msg) { std::cout “Lambda: ” msg std::endl; }); // 观察者3一个类的成员函数 struct MyClass { void method(const std::string msg) { std::cout “Method: ” msg std::endl; } } obj; s.attach(std::bind(MyClass::method, obj, std::placeholders::_1)); s.notify(“Event happened!”);这种方法更灵活性能也可能更好取决于std::function的实现和调用方式但失去了观察者类型的统一性它们不再是同一继承体系。5. 现代C特性对OOP的增强与优化C11/14/17/20引入的新特性极大地改变了我们编写面向对象代码的方式。5.1 自动类型推导与auto简化代码提升可读性auto关键字让编译器在编译时推导变量类型。在OOP中它能简化冗长的类型声明特别是涉及复杂模板或嵌套命名空间时。// 旧风格 std::unique_ptrMyNamespace::SomeConcreteClass ptr(new MyNamespace::SomeConcreteClass()); std::mapstd::string, std::vectorint::iterator it myMap.begin(); // 使用 auto auto ptr std::make_uniqueMyNamespace::SomeConcreteClass(); auto it myMap.begin();注意事项虽然auto很方便但在一些情况下显式写出类型可以提高代码可读性尤其是当初始化表达式不能清晰表达意图时例如auto result process();。在阅读代码时清晰的类型有助于理解。5.2 基于范围的for循环Range-based for loop它提供了一种更简洁、更安全的方式来遍历容器其底层依赖于容器的begin()和end()成员函数或自由函数。std::vectorstd::shared_ptrWidget widgets; // 旧风格 for (std::vectorstd::shared_ptrWidget::iterator it widgets.begin(); it ! widgets.end(); it) { (*it)-draw(); } // 基于范围的for循环 for (const auto widget : widgets) { widget-draw(); }对于自定义的集合类如果需要支持基于范围的for循环需要提供begin()和end()成员函数。5.3 Lambda表达式轻量级的可调用对象Lambda本质上是匿名函数对象它极大地简化了需要传递短小函数逻辑的场景特别是在STL算法和异步编程中。std::vectorWidget* widgets; // 使用lambda查找第一个满足条件的Widget auto found std::find_if(widgets.begin(), widgets.end(), [threshold](const Widget* w) { // 捕获外部变量threshold return w-value() threshold; });Lambda的捕获列表[ ]需要特别注意[]以引用方式捕获所有外部变量。要小心悬垂引用。[]以值方式捕获所有外部变量C20起不推荐默认使用可能产生不必要的拷贝。[var]或[var]明确指定捕获方式。最佳实践尽量使用显式捕获只捕获真正需要的变量并优先考虑按值捕获简单类型按引用捕获大对象或需要修改的对象。5.4 右值引用与完美转发深入移动语义T并不总是代表右值引用。在模板推导的语境下它可能是一个“转发引用”或称万能引用。templatetypename T void wrapper(T arg) { // 这里T是转发引用 // 使用 std::forward 保持 arg 的值类别左值/右值 someFunction(std::forwardT(arg)); }std::forwardT(arg)被称为完美转发。当arg是一个左值时它被转发为左值当arg是一个右值时它被转发为右值。这使得我们能够编写泛型函数将参数原封不动地传递给其他函数这是实现高效工厂函数、容器emplace操作等的基础。6. 实战构建一个简单的、高性能的日志系统让我们综合运用上述OOP实践和优化技巧设计一个简单的日志系统。这个系统需要支持不同日志级别INFO, WARN, ERROR输出到不同目标控制台、文件并且要高效避免日志输出成为性能瓶颈。6.1 核心类设计我们将采用基于策略的设计将日志输出Sink和日志格式化Formatter作为可插拔的策略。// LogLevel.h #pragma once #include string enum class LogLevel { Debug, Info, Warn, Error }; // Formatter.h - 格式化策略接口 #pragma once #include string #include “LogLevel.h” class Formatter { public: virtual ~Formatter() default; virtual std::string format(LogLevel level, const std::string message, const std::string file, int line) 0; }; // SimpleFormatter.h - 一个简单的实现 #pragma once #include “Formatter.h” #include sstream #include iomanip #include chrono class SimpleFormatter : public Formatter { public: std::string format(LogLevel level, const std::string message, const std::string file, int line) override { auto now std::chrono::system_clock::now(); auto time std::chrono::system_clock::to_time_t(now); std::ostringstream oss; oss std::put_time(std::localtime(time), “%Y-%m-%d %H:%M:%S”) “ [” levelToString(level) “] ” file “:” line “ - ” message; return oss.str(); } private: static const char* levelToString(LogLevel lvl) { switch(lvl) { case LogLevel::Debug: return “DEBUG”; case LogLevel::Info: return “INFO”; case LogLevel::Warn: return “WARN”; case LogLevel::Error: return “ERROR”; default: return “UNKNOWN”; } } }; // Sink.h - 输出目标策略接口 #pragma once #include string class Sink { public: virtual ~Sink() default; virtual void write(const std::string formattedMessage) 0; }; // ConsoleSink.h #pragma once #include “Sink.h” #include iostream class ConsoleSink : public Sink { public: void write(const std::string formattedMessage) override { std::cout formattedMessage std::endl; } }; // FileSink.h #pragma once #include “Sink.h” #include fstream #include mutex class FileSink : public Sink { public: explicit FileSink(const std::string filename) : file_(filename, std::ios::app) {} void write(const std::string formattedMessage) override { std::lock_guardstd::mutex lock(mutex_); // 确保线程安全 if (file_.is_open()) { file_ formattedMessage std::endl; } } private: std::ofstream file_; std::mutex mutex_; }; // Logger.h - 核心日志器 #pragma once #include “LogLevel.h” #include “Formatter.h” #include “Sink.h” #include memory #include vector class Logger { public: Logger(LogLevel minLevel, std::unique_ptrFormatter formatter) : minLevel_(minLevel), formatter_(std::move(formatter)) {} void addSink(std::unique_ptrSink sink) { sinks_.push_back(std::move(sink)); } void log(LogLevel level, const std::string message, const std::string file, int line) { if (level minLevel_) return; // 过滤低于阈值的日志 auto formatted formatter_-format(level, message, file, line); for (auto sink : sinks_) { sink-write(formatted); } } private: LogLevel minLevel_; std::unique_ptrFormatter formatter_; std::vectorstd::unique_ptrSink sinks_; };6.2 性能优化异步日志与缓冲上面的实现是同步的每次日志调用都会立即进行格式化和I/O操作。在高并发场景下这可能会阻塞业务线程。一个常见的优化是引入异步日志。1. 异步日志架构业务线程将日志消息放入一个线程安全的队列如无锁队列或带锁的队列。一个独立的后台线程消费者从队列中取出消息进行格式化和写入。这样业务线程的日志调用开销就降低为一次内存写入操作。2. 实现一个简单的异步日志器我们可以使用std::queue配合std::mutex和std::condition_variable实现一个生产者-消费者模型。为了简化这里只展示核心思想。// AsyncLogger.h (简化版) #include queue #include thread #include mutex #include condition_variable #include atomic class AsyncLogger { public: AsyncLogger() : running_(true), worker_([this] { this-consume(); }) {} ~AsyncLogger() { running_ false; cv_.notify_all(); if (worker_.joinable()) worker_.join(); } void log(const std::string rawMsg) { { std::lock_guardstd::mutex lock(mutex_); queue_.push(rawMsg); } cv_.notify_one(); // 通知后台线程 } private: void consume() { while (running_ || !queue_.empty()) { std::unique_lockstd::mutex lock(mutex_); cv_.wait(lock, [this] { return !queue_.empty() || !running_; }); while (!queue_.empty()) { auto msg std::move(queue_.front()); queue_.pop(); lock.unlock(); // 释放锁进行实际的I/O操作 // 这里调用实际的Sink进行写入 std::cout “[ASYNC] ” msg std::endl; lock.lock(); } } } std::queuestd::string queue_; std::mutex mutex_; std::condition_variable cv_; std::atomicbool running_; std::thread worker_; };3. 缓冲优化即使使用异步频繁的队列操作也可能有开销。可以进一步优化让业务线程先积累一定数量的日志消息或达到一定时间间隔后再批量提交到队列减少锁的竞争和系统调用次数。6.3 使用宏简化日志调用为了自动获取文件名和行号我们通常使用宏来包装日志调用。// LogMacros.h #pragma once #include “Logger.h” // 假设有一个全局的Logger实例 globalLogger #define LOG_DEBUG(msg) globalLogger.log(LogLevel::Debug, (msg), __FILE__, __LINE__) #define LOG_INFO(msg) globalLogger.log(LogLevel::Info, (msg), __FILE__, __LINE__) #define LOG_WARN(msg) globalLogger.log(LogLevel::Warn, (msg), __FILE__, __LINE__) #define LOG_ERROR(msg) globalLogger.log(LogLevel::Error, (msg), __FILE__, __LINE__)这样用户代码中就可以简单地写LOG_INFO(“Server started on port 8080”);。踩坑记录在实现异步日志时要特别注意日志器的销毁顺序。必须确保后台消费线程在日志器析构前能够处理完队列中所有的剩余消息并安全退出否则可能导致程序退出时丢失最后的日志甚至引发未定义行为。上面的示例使用了std::atomic标志和join()来确保这一点但在更复杂的场景下可能需要更精细的生命周期管理。7. 常见问题排查与调试技巧在面向对象C开发中一些问题具有典型性。7.1 对象切片Object Slicing当派生类对象通过值传递给一个接受基类对象的函数时会发生对象切片。派生类特有的部分会被“切掉”只保留基类子对象。class Base { public: int x; }; class Derived : public Base { public: int y; }; void func(Base b) { /* 只能访问 b.x */ } Derived d; func(d); // 发生切片d.y 丢失解决方法总是通过指针或引用来传递多态对象。即使用Base或Base*作为参数类型。7.2 虚析构函数问题如果一个类打算被继承即作为基类并且通过基类指针来删除派生类对象那么基类的析构函数必须是虚函数。否则会导致派生类的析构函数不被调用可能发生资源泄漏。class Base { public: ~Base() { std::cout “Base dtor\n”; } // 非虚析构函数 }; class Derived : public Base { public: ~Derived() { std::cout “Derived dtor\n”; } }; Base* ptr new Derived(); delete ptr; // 只输出 “Base dtor”Derived的析构函数没被调用黄金法则如果一个类有虚函数它就应该有一个虚析构函数。7.3 循环引用与std::shared_ptr内存泄漏如前所述shared_ptr的循环引用会导致引用计数永远不为零从而内存泄漏。使用weak_ptr是标准解决方案。7.4 运行时类型识别RTTI与dynamic_cast的开销dynamic_cast和typeid操作符依赖于RTTI这可能会带来运行时开销并且某些嵌入式环境可能禁用了RTTI。如果设计良好通常可以通过虚函数来避免使用dynamic_cast。如果必须使用请确保它不在性能关键路径上。7.5 使用工具进行性能剖析与内存检查性能剖析Profiling使用像gprof、Valgrind的callgrind、perfLinux或 Visual Studio Profiler 等工具找到代码中的热点Hotspot。优化应该基于剖析数据而不是猜测。内存检查使用Valgrind的memcheck或 AddressSanitizerASan来检测内存泄漏、越界访问、使用未初始化内存等问题。在面向对象程序中资源泄漏尤其是由于异常安全未保证导致的泄漏是常见问题。静态分析使用编译器的警告如-Wall -Wextra -pedantic和静态分析工具如Clang-Tidy、Cppcheck来在编译期发现潜在问题如未使用的变量、可能的空指针解引用、违反Rule of Three/Five等。我个人在大型C项目中的体会是面向对象设计就像搭积木封装和接口定义是积木的接口继承和多态是连接件。一开始就把接口设计得清晰、稳定、最小化比后期修修补补要省力得多。而优化永远应该在清晰正确的代码基础上进行并且要有性能剖析数据作为依据避免过早优化和过度优化。最后善用现代C的工具智能指针、移动语义、lambda等和惯用法RAII、Pimpl等能让你的C OOP代码既安全又高效。