拓冰建站拓冰建站
首页 / 资讯中心 / 正文

C++进阶核心:从对象模型到并发编程的深度实践指南

1. 从“C提高知识”说起一名老码农的进阶心路看到“C提高知识”这个标题很多朋友可能会心一笑。这像极了我们当年在技术论坛里翻找的帖子标题朴实无华却又直指核心——我们已经度过了“Hello World”的新手期对语法有了基本了解能写一些小程序了但面对更复杂的项目、更刁钻的面试题或者想优化手头代码的性能时总感觉力不从心。那些藏在标准库深处的“神器”、那些让代码既高效又优雅的设计模式、那些内存深处稍不留神就引爆的“雷”才是我们真正需要攻克的堡垒。这篇文章我就以一个踩过无数坑的过来人身份和你聊聊C从“会用”到“用好”这道坎上必须啃下来的硬骨头。无论你是想夯实基础应对面试还是渴望在项目中写出更专业的代码这里的内容都会是你进阶路上的一块重要拼图。2. 核心能力跃迁理解C的“里世界”很多初学者对C的印象停留在“面向对象”、“难”这些标签上。但C的真正威力在于它提供了从硬件抽象到高级范式的完整控制力。提高的第一步就是转变视角不再仅仅把C当作一门描述“要做什么”的语言更要理解它“是如何做到的”。2.1 对象模型的深入透视你的类在内存中是何模样当你写下class MyClass { int a; char b; };并创建一个对象时编译器在背后做了什么这不仅仅是分配一块内存那么简单。理解对象模型是理解C一切高级特性的基石。首先要清楚成员变量在内存中的布局。在没有虚函数和继承的情况下成员变量通常按照声明顺序排列但编译器可能会为了内存对齐Alignment而插入填充字节Padding。比如在64位系统上一个int后跟一个charint占4字节char占1字节但为了满足8字节对齐编译器可能会在char后面填充3个字节。你可以用sizeof运算符和offsetof宏来探查。#include iostream #include cstddef // for offsetof struct MyStruct { int a; char b; }; int main() { std::cout Sizeof MyStruct: sizeof(MyStruct) std::endl; // 很可能输出8 std::cout Offset of a: offsetof(MyStruct, a) std::endl; // 0 std::cout Offset of b: offsetof(MyStruct, b) std::endl; // 4 return 0; }当引入继承和虚函数后情况变得复杂。单继承下派生类对象包含基类子对象和自身的成员。如果基类有虚函数则对象头部会有一个虚函数表指针vptr指向该类的虚函数表vtable。多重继承下一个派生类对象可能包含多个基类子对象也就可能有多个vptr。虚拟继承则引入了更复杂的机制来解决菱形继承问题通常会通过虚基类表指针来实现。实操心得在调试复杂内存问题或者进行底层数据序列化/反序列化时对象模型的知识至关重要。不要想当然地认为sizeof(派生类) sizeof(基类) sizeof(自身成员)。使用reinterpret_cast对类对象指针进行粗暴的内存操作是极度危险的根源就在于对对象模型的不了解。2.2 资源管理的艺术超越new/delete手动管理内存是C赋予的权力也是责任的来源。“提高”的标志之一就是学会如何优雅、安全地管理资源内存、文件句柄、网络连接等。RAIIResource Acquisition Is Initialization是C资源管理的核心哲学。其思想很简单在对象的构造函数中获取资源在析构函数中释放资源。这样只要对象生命周期结束无论是正常离开作用域还是因为异常栈展开资源都会被自动释放。标准库中的智能指针std::unique_ptr,std::shared_ptr,std::weak_ptr和容器std::vector,std::string都是RAII的典范。std::unique_ptr用于独占所有权不可拷贝只可移动。它是零开销抽象的最佳例子之一。#include memory #include iostream class Resource { public: Resource() { std::cout Resource acquired\n; } ~Resource() { std::cout Resource destroyed\n; } void use() { std::cout Using resource\n; } }; void process() { std::unique_ptrResource res(new Resource()); // C14后更推荐 std::make_uniqueResource() res-use(); // 离开作用域时res的析构函数会自动delete管理的指针 }std::shared_ptr通过引用计数实现共享所有权。但要小心循环引用这会导致内存泄漏此时需要引入std::weak_ptr来打破循环。注意事项优先使用std::make_unique和std::make_shared来创建智能指针而非直接使用new。原因有二一是异常安全如果new成功但构造智能指针时发生异常会导致内存泄漏而make_*系列函数是原子操作二是make_shared能将引用计数和控制块与对象本身分配在同一块内存中可能提升性能。移动语义是C11引入的革命性特性它使得资源所有权的转移变得高效且明确。通过右值引用T和移动构造函数/移动赋值运算符我们可以“偷”取临时对象或显式标记为可移动对象的资源避免不必要的深拷贝。class Buffer { size_t size_; int* data_; public: // 移动构造函数 Buffer(Buffer other) noexcept : size_(other.size_), data_(other.data_) { other.size_ 0; other.data_ nullptr; // 重要确保被移动的对象处于有效但可析构的状态 } // 移动赋值运算符 Buffer operator(Buffer other) noexcept { if (this ! other) { delete[] data_; // 释放已有资源 size_ other.size_; data_ other.data_; other.size_ 0; other.data_ nullptr; } return *this; } // ... 其他成员函数 };理解并正确实现移动语义能极大提升涉及大量资源转移如容器重排、返回局部对象的代码性能。3. 标准库的深度挖掘别重复造轮子C标准库STL是一个宝库但很多人只用了vector和string的皮毛。深入理解并熟练运用STL的各个组件是提高编码效率和代码质量的关键。3.1 容器与算法的精妙配合STL的核心思想是“数据与算法分离”通过迭代器作为粘合剂。除了常用的序列容器vector,deque,list和关联容器map,set更要理解它们的底层实现和适用场景。std::vector动态数组在尾部插入/删除效率高O(1)摊销在中间或头部插入/删除效率低O(n)。其元素在内存中连续存储因此缓存友好cache-friendly遍历速度极快。reserve()方法可以预分配内存避免多次扩容带来的性能开销。std::deque双端队列支持头尾高效插入/删除。它通常由一段段定长的连续空间组成因此不像vector那样所有元素严格连续但依然能提供不错的缓存性能。std::list/std::forward_list双向链表/单向链表。在任何位置插入删除都是O(1)但访问元素是O(n)且内存不连续缓存不友好。除非需要频繁在中间插入删除否则优先考虑vector。std::map/std::set基于红黑树实现元素自动排序。查找、插入、删除都是O(log n)。如果需要有序遍历它们是很好的选择。std::unordered_map/std::unordered_set基于哈希表实现元素无序。平均情况下的查找、插入、删除是O(1)但最坏情况可能退化到O(n)。性能高度依赖于哈希函数的质量和负载因子。算法是STL的另一半精华。algorithm头文件中提供了超过100个泛型算法。掌握它们能让你避免编写冗长且易错的循环。#include algorithm #include vector #include iostream int main() { std::vectorint vec {5, 2, 8, 1, 9, 3}; // 排序 std::sort(vec.begin(), vec.end()); // {1, 2, 3, 5, 8, 9} // 查找 auto it std::find(vec.begin(), vec.end(), 5); if (it ! vec.end()) { std::cout Found: *it std::endl; } // 累加 int sum std::accumulate(vec.begin(), vec.end(), 0); std::cout Sum: sum std::endl; // 条件计数 int count_gt_5 std::count_if(vec.begin(), vec.end(), [](int x){ return x 5; }); std::cout Count 5: count_gt_5 std::endl; // 变换 std::vectorint squared; squared.reserve(vec.size()); std::transform(vec.begin(), vec.end(), std::back_inserter(squared), [](int x){ return x * x; }); return 0; }常见问题std::remove算法并不真正删除容器元素它只是将不需要删除的元素移动到前面并返回一个指向新的“逻辑末尾”的迭代器。真正的删除需要配合容器的erase方法这就是著名的“erase-remove”惯用法。std::vectorint vec {1, 2, 3, 2, 5}; // 删除所有值为2的元素 vec.erase(std::remove(vec.begin(), vec.end(), 2), vec.end()); // 现在 vec {1, 3, 5}3.2 迭代器与仿函数的进阶用法迭代器不仅仅是指针的抽象。它们分为五类输入、输出、前向、双向、随机访问。不同容器提供不同类别的迭代器不同算法对迭代器类别有不同要求。例如std::sort要求随机访问迭代器所以std::list不能直接用std::sort但它有自己的sort成员函数。Lambda表达式是现代C中编写匿名仿函数的利器它让算法调用变得异常简洁。你需要掌握其捕获列表的用法[]不捕获任何外部变量。[]以值的方式捕获所有外部变量在C20中不推荐建议显式列出。[]以引用的方式捕获所有外部变量同样不推荐。[var]以值捕获var。[var]以引用捕获var。[this]捕获当前类的this指针。[, var]默认以值捕获但var以引用捕获。int base 10; std::vectorint nums {1, 2, 3}; std::for_each(nums.begin(), nums.end(), [base](int x) { x base; }); // nums 变为 {11, 12, 13}4. 模板元编程与编译期计算初探C的模板系统图灵完备这意味着你可以在编译期执行复杂的计算和类型操作。这听起来很玄乎但一些基础应用能显著提升代码的通用性和性能。4.1 类型萃取与SFINAE类型萃取Type Traits是编译期获取类型信息的技术。type_traits头文件提供了丰富的工具如std::is_integralT,std::is_pointerT,std::remove_referenceT等。#include type_traits #include iostream templatetypename T void printTypeInfo(const T value) { std::cout Is integral: std::is_integralT::value std::endl; std::cout Is pointer: std::is_pointerT::value std::endl; } int main() { int a 5; int* p a; printTypeInfo(a); // Is integral: 1, Is pointer: 0 printTypeInfo(p); // Is integral: 0, Is pointer: 1 return 0; }SFINAESubstitution Failure Is Not An Error是模板元编程的核心规则之一。当编译器在重载决议中尝试匹配模板时如果某个模板的实例化导致无效代码它不会报错而是简单地将其从候选集中移除。利用SFINAE我们可以根据类型特性选择不同的函数重载或模板特化。C11后常结合std::enable_if使用。#include type_traits #include iostream // 版本1针对整数类型 templatetypename T typename std::enable_ifstd::is_integralT::value, void::type process(T t) { std::cout Processing integral: t std::endl; } // 版本2针对浮点类型 templatetypename T typename std::enable_ifstd::is_floating_pointT::value, void::type process(T t) { std::cout Processing floating point: t std::endl; } int main() { process(42); // 调用版本1 process(3.14); // 调用版本2 // process(hello); // 编译错误没有匹配的版本 return 0; }C17引入了if constexpr使得很多需要SFINAE的场景可以用更直观的编译期分支来替代。templatetypename T void process(T t) { if constexpr (std::is_integral_vT) { std::cout Integral: t 1 std::endl; } else if constexpr (std::is_floating_point_vT) { std::cout Floating: t * 2.0 std::endl; } else { static_assert(std::is_integral_vT || std::is_floating_point_vT, T must be arithmetic); } }4.2 可变参数模板可变参数模板允许你编写接受任意数量、任意类型参数的函数或类模板。这是实现诸如std::make_shared,std::tuple等强大工具的基础。#include iostream // 递归终止函数 void print() { std::cout std::endl; } // 可变参数模板函数 templatetypename T, typename... Args void print(T first, Args... args) { std::cout first ; print(args...); // 递归调用展开参数包 } int main() { print(1, 2.5, hello, a); // 输出: 1 2.5 hello a return 0; }折叠表达式C17让可变参数模板的处理更加简洁templatetypename... Args auto sum(Args... args) { return (args ...); // 折叠表达式 } // sum(1, 2, 3, 4) 返回 105. 并发编程入门拥抱多核时代现代CPU都是多核的利用并发是提高程序性能的重要手段。C11在标准库中引入了线程支持使得编写跨平台并发程序成为可能。5.1 线程基础与数据竞争创建线程很简单使用std::thread。#include iostream #include thread void helloFunction() { std::cout Hello from thread! Thread ID: std::this_thread::get_id() std::endl; } int main() { std::thread t(helloFunction); std::cout Hello from main! Main Thread ID: std::this_thread::get_id() std::endl; t.join(); // 等待线程t结束 return 0; }并发编程最大的挑战之一是数据竞争当多个线程同时访问同一内存位置且至少有一个是写操作时行为未定义。解决数据竞争的核心工具是互斥量Mutex。#include thread #include mutex #include vector #include iostream std::mutex g_mutex; int shared_counter 0; void increment() { for (int i 0; i 100000; i) { std::lock_guardstd::mutex lock(g_mutex); // RAII方式加锁离开作用域自动解锁 shared_counter; } } int main() { std::vectorstd::thread threads; for (int i 0; i 10; i) { threads.emplace_back(increment); } for (auto t : threads) { t.join(); } std::cout Final counter value: shared_counter std::endl; // 应该是 1000000 return 0; }std::lock_guard和std::unique_lock是管理互斥量的RAII包装器能自动加锁解锁避免忘记解锁导致的死锁。std::unique_lock比std::lock_guard更灵活可以手动加解锁并支持条件变量。5.2 条件变量与原子操作条件变量std::condition_variable用于线程间的同步允许一个或多个线程等待某个条件成立。通常与互斥量和谓词判断条件的函数或Lambda一起使用。#include thread #include mutex #include condition_variable #include queue #include iostream std::mutex mtx; std::condition_variable cv; std::queueint data_queue; bool finished false; void producer() { for (int i 0; i 10; i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产耗时 { std::lock_guardstd::mutex lock(mtx); data_queue.push(i); std::cout Produced: i std::endl; } cv.notify_one(); // 通知一个等待的消费者 } { std::lock_guardstd::mutex lock(mtx); finished true; } cv.notify_all(); // 通知所有消费者结束 } void consumer(int id) { while (true) { std::unique_lockstd::mutex lock(mtx); // 等待条件队列不为空或生产结束 cv.wait(lock, []{ return !data_queue.empty() || finished; }); if (finished data_queue.empty()) { break; } int data data_queue.front(); data_queue.pop(); lock.unlock(); // 尽早释放锁 std::cout Consumer id consumed: data std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 模拟消费耗时 } } int main() { std::thread prod(producer); std::thread cons1(consumer, 1); std::thread cons2(consumer, 2); prod.join(); cons1.join(); cons2.join(); return 0; }对于简单的计数器或标志位使用互斥量可能开销过大。C提供了原子操作std::atomic它保证了对该变量的操作是原子的、不可分割的且通常能生成更高效的机器指令。#include atomic #include thread #include vector #include iostream std::atomicint atomic_counter{0}; void increment_atomic() { for (int i 0; i 100000; i) { atomic_counter.fetch_add(1, std::memory_order_relaxed); // 宽松内存序性能最高 } } int main() { std::vectorstd::thread threads; for (int i 0; i 10; i) { threads.emplace_back(increment_atomic); } for (auto t : threads) { t.join(); } std::cout Final atomic counter: atomic_counter std::endl; // 一定是 1000000 return 0; }重要提示原子操作的内存序std::memory_order是一个高级话题。memory_order_relaxed只保证原子性不保证同步顺序memory_order_acquire和memory_order_release用于建立线程间的同步关系memory_order_seq_cst顺序一致性是最严格的也是默认选项。除非你非常清楚自己在做什么否则在大多数情况下使用默认的memory_order_seq_cst是安全的选择。6. 性能调优与调试技巧写出能运行的代码只是第一步写出高效的代码才是进阶的追求。性能调优离不开对程序行为的深入洞察。6.1 剖析工具与热点分析不要靠猜来优化。使用性能剖析工具Profiler来定位真正的性能瓶颈热点。常见的工具有Linux/macOS:gprof,perf,Valgrind的callgrind工具。Windows: Visual Studio 自带的性能探查器。跨平台:Google PerfTools(gperftools)。这些工具能告诉你程序在哪个函数、哪行代码上花费了最多的CPU时间。优化应该集中在这些热点上遵循“二八定律”。6.2 缓存友好性与数据结构选择现代CPU的速度远快于内存。一次缓存未命中Cache Miss可能导致CPU空等数百个时钟周期。因此编写缓存友好的代码至关重要。局部性原理尽量让程序访问的数据在内存中连续。顺序访问数组比随机访问链表快得多因为CPU会预取连续的内存到缓存。数据结构选择在需要频繁遍历、随机访问的场景std::vector几乎总是比std::list快因为前者缓存友好。std::list的每个元素都可能在不同的内存页导致大量缓存未命中。对象大小尽量让频繁一起访问的数据成员在类中相邻声明以提高它们位于同一缓存行的概率。可以使用alignas关键字来控制对齐但需谨慎过度对齐会浪费内存。6.3 调试与问题排查复杂的C程序难免遇到崩溃、内存泄漏、死锁等问题。掌握调试器如GDB, LLDB, Visual Studio Debugger的基本用法是必备技能。核心转储分析程序崩溃后生成的核心转储文件core dump包含了崩溃瞬间的完整内存状态。用调试器加载它通过btbacktrace命令查看调用栈定位崩溃位置。内存检查工具ValgrindLinux/macOS和Dr. MemoryWindows是检测内存泄漏、非法内存访问、使用未初始化内存等问题的利器。在开发阶段定期使用它们能提前发现许多隐蔽的Bug。死锁检测死锁通常发生在多个互斥量锁定顺序不一致时。一些调试器或工具如helgrind能帮助检测潜在的死锁。预防死锁的最佳实践是总是以固定的全局顺序锁定多个互斥量或者使用std::lock或std::scoped_lockC17来一次性锁定多个互斥量避免死锁。// 可能引发死锁 std::thread t1([]{ std::lock_guardstd::mutex lock1(mutexA); std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::lock_guardstd::mutex lock2(mutexB); // t1等待mutexB }); std::thread t2([]{ std::lock_guardstd::mutex lock2(mutexB); std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::lock_guardstd::mutex lock1(mutexA); // t2等待mutexA }); // 使用 std::lock 避免死锁 std::thread t1([]{ std::scoped_lock lock(mutexA, mutexB); // 同时锁定内部使用死锁避免算法 // ... 操作共享资源 });7. 现代C实践C11/14/17/20核心特性拾遗除了前面提到的移动语义、智能指针、Lambda、原子操作等现代C还有许多提升开发效率和代码安全性的特性。7.1 自动类型推导与结构化绑定auto关键字让编译器自动推导变量类型使代码更简洁尤其是在迭代器和复杂类型声明时。std::vectorstd::mapstd::string, std::pairint, double complexVec; // 旧写法 for (std::vectorstd::mapstd::string, std::pairint, double::iterator it complexVec.begin(); it ! complexVec.end(); it) { ... } // 现代写法 for (auto it complexVec.begin(); it ! complexVec.end(); it) { ... } // 或者范围for循环 for (const auto innerMap : complexVec) { ... }decltype用于获取表达式的类型常用于模板元编程和尾置返回类型。templatetypename T, typename U auto add(T t, U u) - decltype(t u) { // 尾置返回类型推导规则更清晰 return t u; }结构化绑定C17允许你从元组、对或结构体中一次性解包多个变量。std::mapstd::string, int myMap {{Alice, 25}, {Bob, 30}}; for (const auto [name, age] : myMap) { // 结构化绑定 std::cout name is age years old.\n; } std::tupleint, double, std::string tup(1, 2.5, hello); auto [a, b, c] tup; // a1, b2.5, chello7.2 右值引用与完美转发这是实现高效泛型库如std::vector::push_back的关键。T在模板中是一个“万能引用”Universal Reference它可以根据实参是左值还是右值被推导为左值引用或右值引用。std::forward用于实现完美转发将参数以其原始的值类别左值或右值转发给另一个函数。这是编写泛型工厂函数或包装器的核心。templatetypename T, typename... Args std::unique_ptrT make_unique(Args... args) { // Args... 是万能引用包 return std::unique_ptrT(new T(std::forwardArgs(args)...)); // 完美转发所有参数 }7.3 constexpr 与编译期计算constexpr在C11中用于声明常量表达式在C14/17/20中能力被极大扩展现在可以用来修饰函数和变量指示其值或返回值可以在编译期计算。// C11 constexpr int factorial(int n) { return n 1 ? 1 : n * factorial(n - 1); } constexpr int fac10 factorial(10); // 编译期计算 // C14 以后constexpr函数内部可以更复杂 constexpr int fibonacci(int n) { if (n 1) return n; int a 0, b 1; for (int i 2; i n; i) { int next a b; a b; b next; } return b; } constexpr int fib10 fibonacci(10); // 编译期计算将尽可能多的计算移到编译期可以减少运行时开销。7.4 文件系统库C17filesystem库提供了跨平台的文件和目录操作接口终于告别了平台相关的API调用。#include filesystem namespace fs std::filesystem; void traverseDir(const fs::path dirPath) { if (!fs::exists(dirPath) || !fs::is_directory(dirPath)) { return; } for (const auto entry : fs::directory_iterator(dirPath)) { std::cout entry.path() std::endl; if (fs::is_directory(entry.status())) { traverseDir(entry.path()); // 递归遍历子目录 } } } int main() { fs::path currentPath fs::current_path(); std::cout Current path: currentPath std::endl; fs::path newDir currentPath / test_dir; // 使用 / 操作符拼接路径 if (!fs::exists(newDir)) { fs::create_directory(newDir); } traverseDir(currentPath); return 0; }8. 构建与工具链一个专业的C开发者不仅要会写代码还要会管理代码的构建、依赖和测试。8.1 构建系统CMake入门Makefile虽然经典但跨平台和项目管理复杂。CMake是目前事实上的标准跨平台构建系统生成器。一个最简单的CMakeLists.txt如下cmake_minimum_required(VERSION 3.10) project(MyAwesomeProject VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(my_app main.cpp utils.cpp) target_include_directories(my_app PRIVATE include) target_link_libraries(my_app PRIVATE some_library)CMake支持查找包、条件编译、安装规则、测试等复杂功能。学习CMake的现代用法Target-based commands能让你的项目结构更清晰更容易被其他项目集成。8.2 包管理vcpkg/conanC长期缺乏好用的包管理器但现在情况在改善。微软的vcpkg和Conan是两个流行的选择。vcpkg与Visual Studio和CMake集成良好。安装包后它会提供一个工具链文件-DCMAKE_TOOLCHAIN_FILE...CMake能自动找到安装的库。# 安装vcpkg git clone https://github.com/Microsoft/vcpkg.git ./vcpkg/bootstrap-vcpkg.sh # 安装库例如fmt ./vcpkg install fmt在CMake中指定工具链文件即可使用。Conan更灵活支持更多的构建系统和自定义配置。它通过生成conanfile.txt或conanfile.py来描述依赖然后生成对应的CMake文件或直接集成到你的构建系统中。使用包管理器能极大简化第三方库的获取、编译和链接过程。8.3 集成开发环境与调试选择一个顺手的IDE能事半功倍。Visual Studio (Windows)功能最全调试器强大对CMake的支持越来越好。Visual Studio Code (跨平台)轻量通过C/C扩展和CMake Tools扩展配合合适的编译器MSVC, GCC, Clang能获得接近IDE的体验。需要一定的配置。CLion (跨平台)JetBrains出品专为C/C设计对CMake支持极好代码分析、重构功能强大。无论用哪个都要熟练掌握其调试功能设置断点、单步执行、查看变量、监视表达式、调用栈分析等。9. 常见陷阱与最佳实践总结最后分享一些我多年踩坑换来的经验希望能帮你少走弯路。避免“未定义行为”Undefined Behavior, UB这是C中最危险的东西。访问越界数组、解引用空指针、有符号整数溢出、违反严格别名规则等都属于UB。UB意味着编译器可以做任何事情程序可能崩溃也可能产生诡异的结果甚至看起来“正常”运行。始终使用工具如编译器警告-Wall -Wextra -pedantic sanitizers-fsanitizeaddress,undefined来帮助检测UB。理解对象的生命周期栈对象在离开作用域时自动销毁。动态分配的对象new需要手动delete或者交给智能指针。警惕悬垂指针和引用指向已销毁对象的指针/引用。返回局部变量的引用或指针是经典错误。优先使用标准库除非有极特殊的性能需求或平台限制否则优先使用标准库的容器和算法。它们经过千锤百炼比你手写的更正确、更高效。为你的类定义正确的拷贝/移动语义遵循“三五法则”或“零法则”。如果你定义了析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个你可能需要定义全部三个或五个加上移动操作。更好的做法是使用智能指针管理资源让编译器生成默认的拷贝/移动操作零法则。使用nullptr而不是NULL或0nullptr有明确的指针类型能避免在重载函数中产生歧义。使用enum class代替传统的enumenum class是强类型的其枚举值不会隐式转换为整数也不会污染外层作用域。编写异常安全的代码利用RAII。在可能抛出异常的地方确保资源能被正确释放。避免在析构函数中抛出异常。性能优化三原则1) 不要过早优化2) 不要优化不重要的部分先剖析3) 在更高层次上优化算法和数据结构的选择往往比微观优化效果更显著。持续学习C标准大约每三年更新一次C11, 14, 17, 20, 23...。关注新特性但不必急于在生产环境中使用所有最新特性。理解其背后的思想在合适的时机引入能持续提升代码质量和开发效率。C的深度和广度决定了学习它是一个漫长的旅程。但每深入理解一个概念每掌握一项新特性你手中的工具就会更强大一分你解决问题的能力也会提升一个台阶。从理解对象模型和资源管理开始到熟练运用标准库和现代特性再到驾驭并发和元编程这条路没有捷径但每一步都算数。希望这篇长文能成为你“提高”路上的一个路标当你遇到困惑时不妨回头看看这些基础而核心的知识点或许会有新的启发。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门