ARTICLE DETAIL

资讯详情

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

C++进阶特性解析:智能指针、模板与异常处理实战指南

C++进阶特性解析:智能指针、模板与异常处理实战指南 1. 从“裸奔”到“精装”为什么我们需要这些C进阶特性干了这么多年C我见过太多新手写的代码用一个词形容就是“裸奔”。内存自己new了不delete导致内存泄漏函数为了处理不同类型的数据复制粘贴出十几个几乎一样的版本程序一遇到文件打不开或者除零错误就直接崩溃用户体验极差。这些问题在小型练习项目里可能无所谓但一旦进入稍微有点规模的工程就是灾难的源头。今天要聊的这几个特性——智能指针、模板和异常处理——就是C这门语言从“能跑就行”的玩具进化到“稳定可靠”的工业级工具的关键一步。它们不是语法糖而是工程实践的必需品。智能指针帮你管理内存生命周期让你从手动内存管理的泥潭里解脱出来这是构建稳定程序的基石。模板让你写出通用、高效的代码避免重复劳动这是提升开发效率和代码质量的核心。异常处理则提供了一种结构化的错误处理机制让程序在遇到意外时能体面地处理而不是直接“暴毙”。很多人觉得这些是“高级特性”望而却步。其实不然它们更像是给房子做“精装修”。你当然可以用毛坯房基础语法写程序但住起来肯定不舒服还容易出各种问题内存泄漏、代码冗余、脆弱。掌握了这些你的代码才算是从“学生作业”升级到了“产品级代码”。接下来的内容我会结合我踩过的坑和实际项目中的用法把这些特性的核心讲透让你不仅能看懂更能用起来。2. 智能指针告别手动delete的“内存管家”手动管理内存是C程序员经典的“坑”。一个new必须对应一个delete但在复杂的函数调用、条件分支和异常抛出中保证这条铁律非常困难。智能指针的出现就是为了将资源管理的责任从程序员肩上转移到对象生命周期上利用RAIIResource Acquisition Is Initialization这一核心思想来确保资源安全。2.1std::unique_ptr独占所有权的“独行侠”std::unique_ptr如其名独占所指向对象的所有权。一个非空的unique_ptr永远独自拥有其指向的内存这种独占性通过禁止拷贝构造函数和拷贝赋值操作来保证移动语义是允许的。这使它成为替代裸指针最直接、最安全的选择。核心用法与原理#include memory #include iostream class MyClass { public: MyClass() { std::cout MyClass constructed\n; } ~MyClass() { std::cout MyClass destroyed\n; } void doSomething() { std::cout Doing something...\n; } }; void basicUsage() { // 1. 创建并管理一个动态分配的MyClass对象 std::unique_ptrMyClass ptr1(new MyClass()); // 更推荐使用make_unique (C14起) auto ptr2 std::make_uniqueMyClass(); ptr2-doSomething(); // 使用-操作符访问成员 // 2. 所有权转移ptr2的内容移动到ptr3ptr2变为nullptr std::unique_ptrMyClass ptr3 std::move(ptr2); if (!ptr2) { std::cout ptr2 is now empty after move.\n; } // ptr3 现在拥有对象并将在离开作用域时自动释放内存 // 3. 释放所有权并返回裸指针谨慎使用 MyClass* rawPtr ptr3.release(); // 此时ptr3为空rawPtr指向对象你必须手动管理rawPtr delete rawPtr; // 手动删除 }为什么用make_unique除了语法简洁更重要的是异常安全。考虑foo(std::unique_ptrT(new T), std::unique_ptrU(new U))编译器可能先执行两个new再构造两个unique_ptr。如果第二个new抛出异常第一个new出来的内存就泄漏了。而make_unique将分配和构造包装成一个原子操作避免了这个问题。自定义删除器默认情况下unique_ptr使用delete释放内存。但对于需要特殊清理的资源如文件句柄、malloc分配的内存可以指定自定义删除器。// 使用函数对象作为删除器 struct FileDeleter { void operator()(std::FILE* fp) const { if (fp) { std::fclose(fp); std::cout File closed.\n; } } }; std::unique_ptrstd::FILE, FileDeleter filePtr(std::fopen(data.txt, r)); // 使用Lambda表达式更简洁 auto del [](std::FILE* fp) { if(fp) std::fclose(fp); }; std::unique_ptrstd::FILE, decltype(del) filePtr2(std::fopen(data.txt, r), del);实操心得在项目中我几乎用std::unique_ptr全面替代了需要delete的裸指针。它轻量、零开销与裸指针大小相同并且所有权清晰。当你设计一个函数需要传递一个对象并且不希望被共享时使用unique_ptr作为参数类型是很好的选择它明确表达了“接收函数将接管所有权”的语义。2.2std::shared_ptr共享所有权的“团队协作者”当多个部分都需要访问同一个对象且无法确定谁最后使用它时std::shared_ptr就派上用场了。它通过引用计数来管理对象的生命周期。每复制一个shared_ptr计数加一每销毁一个或重置计数减一。当计数变为零时托管的对象被自动销毁。核心用法与陷阱#include memory #include iostream class Resource { public: Resource() { std::cout Resource acquired\n; } ~Resource() { std::cout Resource released\n; } }; void sharedUsage() { // 1. 创建shared_ptr引用计数为1 auto sp1 std::make_sharedResource(); std::cout sp1 use_count: sp1.use_count() \n; // 输出 1 { // 2. 拷贝构造引用计数1变为2 auto sp2 sp1; std::cout sp1 use_count after copy: sp1.use_count() \n; // 输出 2 // sp2 离开作用域析构引用计数-1变回1 } std::cout sp1 use_count after sp2 gone: sp1.use_count() \n; // 输出 1 // 3. 使用get()获取裸指针只读绝不delete Resource* rawPtr sp1.get(); // 4. 重置sp1放弃对当前对象的所有权引用计数-1。如果此时计数为0则释放对象。 sp1.reset(); std::cout sp1 is now (sp1 ? not null : null) \n; // 输出 null }循环引用问题这是shared_ptr最著名的陷阱。如果两个对象互相持有对方的shared_ptr它们的引用计数永远无法降到零导致内存泄漏。struct Node { std::shared_ptrNode next; std::shared_ptrNode prev; // 互相持有形成循环引用 ~Node() { std::cout Node destroyed\n; } }; void circularReference() { auto node1 std::make_sharedNode(); auto node2 std::make_sharedNode(); node1-next node2; // node2 引用计数: 2 (node2本身 node1-next) node2-prev node1; // node1 引用计数: 2 (node1本身 node2-prev) // 函数结束node1和node2局部变量销毁引用计数各减1但都还剩1。 // 对象无法被释放内存泄漏发生。 }解决方案std::weak_ptr。weak_ptr是对shared_ptr管理对象的一种“弱引用”。它不增加引用计数只用于观察资源是否还存在。需要通过lock()方法尝试获取一个临时的shared_ptr来访问对象。struct SafeNode { std::shared_ptrSafeNode next; std::weak_ptrSafeNode prev; // 使用weak_ptr打破循环 ~SafeNode() { std::cout SafeNode destroyed\n; } }; void safeReference() { auto node1 std::make_sharedSafeNode(); auto node2 std::make_sharedSafeNode(); node1-next node2; node2-prev node1; // 这里是weak_ptr赋值不增加node1的引用计数 // 访问weak_ptr指向的对象 if (auto sharedPrev node2-prev.lock()) { // lock()成功获得一个shared_ptr std::cout Previous node is still alive.\n; } else { std::cout Previous node has been destroyed.\n; } // 函数结束node1计数从1变0销毁。node2的prev变成空悬weak_ptr。 // 接着node2计数从1变0销毁。无内存泄漏。 }性能开销与使用建议shared_ptr的控制块存储引用计数等是动态分配的有额外的内存和原子操作开销。不要滥用。默认使用unique_ptr仅在确需共享所有权时才用shared_ptr。对于可能产生循环引用的场景提前设计使用weak_ptr。2.3std::weak_ptr与std::enable_shared_from_thisweak_ptr除了解决循环引用还常用于缓存、观察者模式等场景避免持有不必要的所有权而阻止对象释放。一个更进阶的用法是结合std::enable_shared_from_this。假设一个对象的成员函数需要返回指向自身的shared_ptr例如用于回调如果直接在函数里return std::shared_ptrT(this)会创建一个新的、独立的控制块导致同一内存被多个控制块管理最终被重复释放双重删除。class BadClass { public: std::shared_ptrBadClass getShared() { return std::shared_ptrBadClass(this); // 危险会创建新的控制块。 } }; void doubleDeleteDemo() { auto ptr1 std::make_sharedBadClass(); auto ptr2 ptr1-getShared(); // ptr2 拥有另一个控制块 // 退出时ptr1和ptr2各自销毁都会尝试delete同一个this指针 - 未定义行为通常是崩溃。 }正确的做法是让类继承自std::enable_shared_from_thisT并使用shared_from_this()成员函数。class GoodClass : public std::enable_shared_from_thisGoodClass { public: std::shared_ptrGoodClass getShared() { return shared_from_this(); // 安全返回与现有控制块关联的shared_ptr } }; void safeDemo() { auto ptr1 std::make_sharedGoodClass(); auto ptr2 ptr1-getShared(); // ptr2 与 ptr1 共享同一个控制块 std::cout ptr1.use_count() \n; // 输出 2 // 安全销毁 }注意shared_from_this()只能在对象已经被一个shared_ptr管理的情况下调用否则会抛出std::bad_weak_ptr异常。因此绝不能栈上创建GoodClass对象然后调用getShared()。3. 模板编写通用代码的“万能模具”如果你写过几个功能相似、只是数据类型不同的函数就会立刻理解模板的价值。模板是C泛型编程的基础它允许你编写与类型无关的代码编译器在编译期根据你使用的具体类型来生成对应的代码。3.1 函数模板一次编写多类型适用函数模板就像一个配方告诉编译器如何根据给定的类型T来生成一个具体的函数。// 经典的交换函数模板 template typename T // typename 也可以用 class 替换含义相同 void swap(T a, T b) { T temp a; a b; b temp; } // 使用 int x 1, y 2; swap(x, y); // 编译器实例化出 void swapint(int, int) std::string s1 hello, s2 world; swap(s1, s2); // 编译器实例化出 void swapstd::string(std::string, std::string)模板参数推导在上面的例子中我们并没有显式指定T是int还是std::string编译器根据调用时传入的实参类型自动推导出了模板参数。这是函数模板最方便的地方。多模板参数与非类型模板参数// 多类型参数 template typename T1, typename T2 auto add(const T1 a, const T2 b) - decltype(a b) { // 使用尾置返回类型 return a b; } // 使用add(1, 2.5) - T1int, T2double, 返回double // 非类型模板参数必须是编译期常量 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, 10 arr; // N在编译期确定为10特化与重载有时候对于某些特定的类型通用的模板实现可能效率低下甚至不正确这时可以使用特化。// 通用版本 template typename T bool isEqual(const T a, const T b) { return a b; } // 针对const char*的特化版本避免比较指针地址 template bool isEqualconst char*(const char* const a, const char* const b) { return std::strcmp(a, b) 0; } // 函数模板也可以重载通过参数数量或类型不同 template typename T void print(const T value) { std::cout value std::endl; } template typename T, typename U void print(const T value1, const U value2) { std::cout value1 , value2 std::endl; }实操心得写函数模板时要尽量让接口通用。使用const引用作为参数以避免不必要的拷贝考虑使用typename std::decayT::type或std::remove_reference_tT来获取“干净”的类型。对于复杂的返回类型C14的auto返回类型推导非常方便。记住模板是在编译期展开的过度复杂的模板元编程会导致编译时间急剧增加。3.2 类模板构建通用容器与工具类模板允许你定义一族类这些类除了所操作的数据类型不同结构完全一致。标准库中的vector,map,unique_ptr等都是类模板。基本定义template typename T class MyVector { public: explicit MyVector(std::size_t size) : size_(size), data_(new T[size]{}) {} ~MyVector() { delete[] data_; } 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 size_; } // 禁止拷贝简单示例 MyVector(const MyVector) delete; MyVector operator(const MyVector) delete; private: std::size_t size_; T* data_; }; // 使用 MyVectorint intVec(10); intVec[0] 42; MyVectorstd::string strVec(5);成员函数在类外定义当成员函数在类模板外部定义时也需要带上模板参数列表。template typename T class MyVector { public: void push_back(const T value); // ... }; template typename T // 每个成员函数定义前都需要这个模板声明 void MyVectorT::push_back(const T value) { // 实现扩容和添加的逻辑 // ... }默认模板参数与模板模板参数// 默认模板参数 template typename T, typename Allocator std::allocatorT class SimpleList { // 使用Allocator分配内存 }; SimpleListint list1; // 使用默认的std::allocatorint SimpleListint, MyCustomAllocatorint list2; // 使用自定义分配器 // 模板模板参数较高级用于传递模板本身 template template typename class Container, typename T void printContainer(const ContainerT c) { for (const auto elem : c) { std::cout elem ; } } // 可以接受 vectorint, listdouble 等可变参数模板C11引入的可变参数模板允许模板接受任意数量的模板参数这是实现tuple、function等高级设施的基础。// 递归终止函数 void print() { std::cout std::endl; } // 可变参数模板函数 template typename T, typename... Args void print(T first, Args... args) { std::cout first ; print(args...); // 递归展开参数包 } // 使用print(1, 2.5, hello, a); // 输出: 1 2.5 hello a类模板的特化与偏特化和函数模板一样类模板也可以特化。全特化是针对所有模板参数都指定具体类型偏特化是只针对部分参数指定。// 通用版本 template typename T struct IsPointer { static const bool value false; }; // 全特化版本针对任何类型的指针 template typename T struct IsPointerT* { static const bool value true; }; // 使用 std::cout IsPointerint::value; // false std::cout IsPointerint*::value; // true std::cout IsPointerconst char*::value; // true实操心得与陷阱类模板的代码通常全部放在头文件中因为编译器需要在看到使用它的地方实例化出具体的类。这可能导致编译依赖增加。使用类模板时要特别注意其成员函数的实例化时机。另外模板错误信息通常又长又晦涩学习使用static_assert和概念C20可以在编译期给出更清晰的错误提示。在大型项目中合理使用模板可以极大提升代码复用率但滥用或过度复杂的模板元编程会让代码难以理解和调试需要权衡。4. 异常处理让程序优雅地应对“意外”C语言中处理错误通常通过返回值如返回NULL、-1和全局变量如errno来实现。这种方式有几个问题错误信息必须通过额外渠道传递调用者可能忘记检查返回值错误处理代码和正常业务逻辑混杂在一起降低可读性。C的异常机制提供了一种将错误处理与正常流程分离的结构化方法。4.1 异常的基本机制throw,try,catch异常处理涉及三个关键字throw: 当检测到错误时抛出一个异常对象。执行流立即中断开始栈展开stack unwinding。try: 定义一个代码块在这个块中抛出的异常可以被后续的catch块捕获。catch: 捕获并处理特定类型的异常。可以有多条catch子句按顺序匹配。#include iostream #include stdexcept #include string double divide(int a, int b) { if (b 0) { // 抛出一个标准异常推荐使用标准库异常类型 throw std::runtime_error(Division by zero!); // 也可以抛出自定义类型甚至int、string等但最好用标准异常或自定义异常类 } if (a % b ! 0) { // 抛出一个不同的异常 throw std::domain_error(Result is not an integer.); } return static_castdouble(a) / b; } void process() { int x 10, y 0; try { // 可能抛出异常的代码放在try块中 double result divide(x, y); std::cout Result: result std::endl; // 如果divide抛出异常这行不会执行 std::cout Calculation successful. std::endl; } catch (const std::runtime_error e) { // 捕获 std::runtime_error 及其派生类的异常 std::cerr Runtime error caught: e.what() std::endl; // 进行错误恢复或清理 } catch (const std::domain_error e) { std::cerr Domain error caught: e.what() std::endl; } catch (...) { // 捕获所有其他类型的异常省略号语法 std::cerr An unknown exception was caught! std::endl; // 通常在这里做一些最基础的清理然后重新抛出或终止 throw; // 重新抛出当前异常 } // 如果没有异常被抛出或者异常被捕获并处理了没有重新抛出程序继续执行这里 std::cout Process function continues. std::endl; } int main() { try { process(); } catch (...) { std::cerr Exception escaped to main. Terminating. std::endl; return 1; } return 0; }栈展开Stack Unwinding当异常被抛出时当前函数停止执行并开始回溯调用栈依次退出析构栈上的局部对象直到找到一个匹配的catch块。这个过程中所有成功构造的局部对象的析构函数都会被调用这是RAII能够保证资源不泄漏的关键。如果直到main函数都没找到匹配的catch则调用std::terminate()终止程序。4.2 标准异常体系与自定义异常C标准库定义了一个异常类继承体系基类是std::exception定义在exception头文件中。它有一个虚成员函数what()返回一个描述错误的C风格字符串。常用标准异常std::logic_error: 程序逻辑错误理论上可以在编码阶段避免。如std::invalid_argument无效参数std::domain_error域错误std::length_error长度错误std::out_of_range越界。std::runtime_error: 运行时错误通常由外部因素引起难以在编码时预防。如std::overflow_error算术溢出std::underflow_error下溢std::range_error范围错误std::system_error系统相关错误。自定义异常类为了更好地表达特定领域的错误可以定义自己的异常类通常继承自std::exception或其子类。#include exception #include string class MyBusinessException : public std::runtime_error { public: explicit MyBusinessException(const std::string message, int errorCode) : std::runtime_error(message), errorCode_(errorCode) {} int getErrorCode() const { return errorCode_; } // 可以重写what()以提供更丰富的信息注意线程安全 const char* what() const noexcept override { // 简单示例实际中可能需要构造一个完整的字符串 static std::string fullMsg std::string(std::runtime_error::what()) [Code: std::to_string(errorCode_) ]; return fullMsg.c_str(); } private: int errorCode_; }; void riskyOperation(int param) { if (param 0) { throw MyBusinessException(Parameter cannot be negative, 1001); } // ... 正常操作 }异常安全保证函数提供的异常安全保证是其行为的重要部分通常分为几个级别不抛出保证Nothrow Guarantee函数承诺绝不抛出异常。如析构函数、swap函数通常应提供此保证。强异常安全保证Strong Exception Safety如果函数因异常退出程序状态保持不变如同函数从未调用。这通常通过“拷贝-交换”惯用法实现。基本异常安全保证Basic Exception Safety如果函数因异常退出无资源泄漏且对象处于有效状态但不一定和调用前相同。无异常安全保证No Exception Safety函数抛出异常可能导致资源泄漏或数据破坏。在编写库代码或关键组件时应至少提供基本异常安全保证并尽可能提供强保证。4.3 异常使用的注意事项与最佳实践按值抛出按引用捕获抛出异常对象时通常按值抛出throw MyException(...)。捕获时使用const引用catch (const MyException e)避免对象切片如果捕获基类和不必要的拷贝。不要抛出析构函数的异常如果析构函数在栈展开过程中因为异常退出程序会直接调用std::terminate()。确保析构函数能处理自身可能抛出的异常或者标记为noexcept。谨慎使用catch (...)它能捕获所有异常但你也失去了异常的类型信息。通常只在最高层如main函数或需要做最终清理的地方使用并且通常应该重新抛出throw;。异常 vs 错误码对于频繁发生的、可预期的错误如“文件未找到”使用错误码或std::optional可能更高效。异常更适合于罕见的、严重的、程序无法在本地处理的错误如“内存耗尽”、“网络连接中断”。避免在构造函数中抛出异常导致资源泄漏如果构造函数在初始化列表中或函数体内抛出异常已成功构造的成员子对象会被自动析构。但如果你在构造函数中手动获取了资源如new需要在异常抛出前手动释放或者更佳做法是使用智能指针等RAII对象来管理资源。noexcept说明符C11引入用于声明函数不会抛出异常。这有助于编译器优化并且如果noexcept函数抛出了异常程序会直接终止。移动构造函数和移动赋值运算符通常应标记为noexcept以确保标准库容器在重组时能使用高效的移动操作。实操心得在项目中统一异常使用规范非常重要。我们团队规定跨模块边界、或底层库向应用层报告不可恢复错误时使用异常。模块内部的可预期错误使用错误码或std::expectedC23。另外异常的处理成本性能开销在“不抛出”的路径上很小主要开销在抛出和捕获时。对于性能极度敏感的代码路径如内层循环要避免使用异常。5. 类型转换从C风格强制转换到更安全的C方式C语言中使用(type)expression进行强制类型转换这种方式强大但危险它几乎允许任何转换编译器很少警告错误往往在运行时才暴露。C引入了四种命名的强制类型转换运算符它们功能更明确在代码中像函数一样清晰可见便于搜索和维护并且编译器能提供更多的安全检查。5.1static_cast最常用的编译期类型转换static_cast用于在编译期已知的、有明确定义的类型转换。它是“静态”的不产生运行时开销。// 1. 基本数据类型转换有损或无损失 int i 42; double d static_castdouble(i); // int - double float f 3.14f; int j static_castint(f); // float - int (截断) // 2. void* 与其他指针类型的转换 void* pv i; int* pi static_castint*(pv); // 将void*转回原始类型 // 3. 类层次间的上行转换派生类指针/引用 - 基类指针/引用安全 class Base { /* ... */ }; class Derived : public Base { /* ... */ }; Derived derived; Base* pb static_castBase*(derived); // 上行转换安全 // 4. 类层次间的下行转换基类指针/引用 - 派生类指针/引用不安全 Base* pb2 new Derived; // Derived* pd static_castDerived*(pb2); // 编译通过但如果pb2实际指向的不是Derived行为未定义 // 应使用 dynamic_cast (见下文)关键点static_cast无法移除const或volatile限定符那是const_cast的活也无法在不同不相关的类指针之间转换那是reinterpret_cast的活。对于下行转换除非你百分百确定指针的实际类型否则不要用static_cast。5.2dynamic_cast用于多态类型的安全下行转换dynamic_cast专门用于含虚函数的类多态类型在继承层次间的指针或引用转换。它在运行时检查转换的安全性。class Base { public: virtual ~Base() {} }; // 必须有虚函数通常析构函数为虚 class Derived : public Base { public: void derivedFunc() {} }; Base* pb new Derived; // 1. 指针转换失败返回 nullptr Derived* pd dynamic_castDerived*(pb); if (pd) { // 转换成功 pd-derivedFunc(); } else { // 转换失败pb可能指向其他派生类或就是Base } // 2. 引用转换失败抛出 std::bad_cast 异常 try { Derived rd dynamic_castDerived(*pb); rd.derivedFunc(); } catch (const std::bad_cast e) { std::cerr Bad cast: e.what() std::endl; } Base* pb2 new Base; Derived* pd2 dynamic_castDerived*(pb2); // pd2 将为 nullptr // Derived rd2 dynamic_castDerived(*pb2); // 抛出 std::bad_cast运行时类型信息RTTIdynamic_cast需要RTTI支持。有些嵌入式或高性能场景会禁用RTTI编译器选项如-fno-rtti此时dynamic_cast无法使用。它的性能开销比static_cast大因为涉及运行时类型检查。使用场景当你有一个基类指针/引用但需要调用派生类特有的方法时使用dynamic_cast进行安全的向下转换。在设计模式如“访问者模式”中也可能用到。5.3const_cast添加或移除const/volatile限定符这是唯一可以操作const属性的转换。主要用途是调用历史遗留的、参数不是const但实际不会修改数据的C风格函数。void legacyPrint(char* str) { // 一个旧的、不修改str的函数但参数没加const printf(%s\n, str); } const char* message Hello, world!; // legacyPrint(message); // 错误不能将const char* 转换为 char* legacyPrint(const_castchar*(message)); // 移除const调用旧函数 // **危险示例修改常量数据导致未定义行为** const int ci 10; int* modifiable const_castint*(ci); *modifiable 20; // 未定义行为ci可能在只读内存段或者编译器已做优化。 std::cout ci std::endl; // 输出可能是10编译器优化后的结果也可能是20程序也可能崩溃。重要警告使用const_cast修改一个原本就是const的对象是未定义行为。只能用于修改“底层const不是对象本身属性”的情况例如一个函数返回了const T但你知道这个引用来自一个非const对象。5.4reinterpret_cast低级别的重新解释这是最强大也最危险的转换。它简单地将一块内存的比特位重新解释为另一种类型。它不进行任何运行期检查或转换操作。int i 0x12345678; // 将int的地址重新解释为char指针用于查看内存布局字节序 char* pc reinterpret_castchar*(i); for (int n 0; n sizeof(int); n) { std::cout std::hex (int)pc[n] ; } // 在不同类型的指针间转换不相关类型 struct S { int a; double b; }; S s; int* ip reinterpret_castint*(s); // 将S*转换为int*指向s.a // *ip 现在访问的是 s.a // 在指针和足够大的整数类型间转换如 void* 到 uintptr_t void* p malloc(100); uintptr_t addr reinterpret_castuintptr_t(p); void* p2 reinterpret_castvoid*(addr);使用场景极少reinterpret_cast通常用于底层编程如操作系统内核、设备驱动、序列化、或与C语言接口交互尽管很多C接口转换可以用static_cast通过void*中转。在应用程序级代码中除非你非常清楚自己在做什么并且没有其他安全的选择否则应避免使用。5.5 旧式强制转换与explicit构造函数C风格的(type)expr和函数风格的type(expr)在C中仍然有效。它们会尝试按以下顺序进行转换const_caststatic_cast可以包含向上/向下转换即使是不安全的static_cast后跟const_castreinterpret_castreinterpret_cast后跟const_cast由于其行为不明确且危险在现代C中应尽量避免使用旧式强制转换优先使用四种命名的强制转换。explicit构造函数它禁止编译器进行隐式类型转换要求必须显式调用构造函数。class MyString { public: explicit MyString(int size) { /* 分配size大小的空间 */ } MyString(const char* str) { /* 从C字符串构造 */ } }; void func(const MyString s) {} // func(10); // 错误不能隐式将int转换为MyString因为构造函数是explicit的 func(MyString(10)); // 正确显式转换 func(hello); // 正确允许隐式转换因为MyString(const char*)不是explicit的使用explicit可以避免很多意想不到的隐式转换提高代码安全性。类型转换总结记住这个简单的选择指南需要基本类型转换、void*转换、类层次上行转换 →static_cast需要多态类层次间的下行转换 →dynamic_cast安全有开销需要去掉或加上const/volatile→const_cast慎用别改真常量需要进行内存比特位的低级重新解释 →reinterpret_cast极少用非常危险其他情况尤其是旧代码中的C风格转换 →考虑重构使用上述安全的转换替代
返回列表