c++中四种显式类型转换操作符
static_cast,dynamic_cast,reinterpret_cast和const_cast的作用和区别-CSDN博客
| 转换操作符 | 用途 | 安全性 | 典型场景 |
|---|---|---|---|
static_cast |
编译时类型转换(相关类型之间) | ✅ 较安全 | 基本类型转换、继承类指针/引用向上/向下转换(无运行时检查) |
dynamic_cast |
运行时安全向下转型(多态类型) | ✅ 安全(带检查) | 基类指针 → 派生类指针(需虚函数) |
const_cast |
添加或移除 const / volatile |
⚠️ 危险(慎用) | 去掉 const(仅当对象本身非 const 时合法) |
reinterpret_cast |
低级位模式重解释 | ❌ 非常危险 | 指针 ↔ 整数、不同类型指针互转 |
static_cast vs dynamic_cast 对比
特性 static_castdynamic_cast是否需要虚函数 ❌ 不需要 ✅ 必须有(多态类型) 检查时机 编译时(无运行时检查) 运行时(RTTI 检查) 向下转型安全性 ❌ 不安全(程序员负责) ✅ 安全(自动验证) 失败行为 返回无效指针(未定义行为) 指针:返回 nullptr;引用:抛出std::bad_cast性能开销 无(和 C 风格转换一样快) 有(需查虚表、类型信息) 能否用于非多态类型 ✅ 可以 ❌ 编译错误
cpp#include <iostream> #include <cstring> // 用于 memset(可选) using namespace std; class Base { public: virtual ~Base() = default; // 关键:使类型成为多态 virtual void say() { std::cout << "Base\n"; } }; class Derived : public Base { public: void say() override { std::cout << "Derived\n"; } void special() { std::cout << "Special feature!\n"; } }; int main() { Base* b = new Derived(); // static_cast Derived* d1 = static_cast<Derived*>(b); d1->special(); // ✅ 正常工作(但靠程序员保证正确) d1->say(); // dynamic_cast Derived* d2 = dynamic_cast<Derived*>(b); if (d2) d2->special(); // ✅ 安全,且会检查 d2->say(); Base* b1 = new Base(); // 实际是 Base 对象! // static_cast ------ 危险! Derived* dd1 = static_cast<Derived*>(b1); dd1->special(); // ❌ 未定义行为(可能崩溃、数据错乱) // dynamic_cast ------ 安全! Derived* dd2 = dynamic_cast<Derived*>(b1); if (dd2) { //dd2 是否为 nullptr(空指针) dd2->special(); } else { std::cout << "Not a Derived object!\n"; // ✅ 正确处理 } }
c++的智能指针
| 特性 | unique_ptr |
shared_ptr |
weak_ptr |
|---|---|---|---|
| 所有权 | 独占 | 共享 | 无(仅观察) |
| 可复制 | ❌ | ✅ | ✅ |
| 可移动 | ✅ | ✅ | ✅ |
| 引用计数 | ❌ | ✅ | ❌(依赖 shared_ptr) |
| 性能开销 | 几乎无 | 有(原子操作) | 有(同 shared_ptr) |
| 解决循环引用 | 不适用 | ❌ | ✅ |
| 创建方式 | make_unique |
make_shared |
从 shared_ptr 构造 |
C++中的深拷贝和浅拷贝
explicit 禁止编译器进行隐式类型转换
explicit是 C++ 中一个极其重要且常用的关键字 ,用于禁止编译器进行隐式类型转换(implicit conversion),从而避免意外的、难以调试的错误。
场景 行为 explicit构造函数禁止从参数类型到类类型的隐式转换 explicit转换操作符禁止从类类型到目标类型的隐式转换 共同目标 提高类型安全性,防止意外转换
auto类型和decltype类型
auto:根据初始化表达式推导变量类型
auto会忽略顶层 const、引用、volatile,只保留底层类型。- 如果想保留引用或 const,需要显式加上:
const auto&。
cppint x = 10; const int cx = 20; int& rx = x; auto a = x; // a 是 int(值拷贝) auto b = cx; // b 是 int(顶层 const 被丢弃) auto c = rx; // c 是 int(引用被退化为值) // 想保留引用/const? const auto& d = cx; // d 是 const int& auto& e = x; // e 是 int&
decltype:根据表达式本身推导其精确类型
cppint x = 10; const int cx = 20; int& rx = x; decltype(x) a = x; // a 是 int decltype(cx) b = cx; // b 是 const int decltype(rx) c = x; // c 是 int& (因为 rx 声明为引用) decltype((x)) d = x; // d 是 int& (因为 (x) 是左值表达式) decltype(x + 1) e = 11; // e 是 int(表达式结果是右值)
constexpr 主要用法
constexpr变量
cppconstexpr int N = 100; // 编译期常量 constexpr double PI = 3.14159; // 必须用常量表达式初始化 int arr[N]; // ✅ 合法:N 是编译期常量
constexpr函数
cpp// C++14+ constexpr int factorial(int n) { if (n <= 1) return 1; int result = 1; for (int i = 2; i <= n; ++i) result *= i; return result; } // 使用 constexpr int f5 = factorial(5); // 编译期计算 → 120 int x = 6; int runtime = factorial(x); // 运行期计算
constexpr构造函数(用于自定义类型)
cppstruct Point { constexpr Point(int x, int y) : x(x), y(y) {} constexpr int distance_sq() const { return x * x + y * y; } int x, y; }; constexpr Point p(3, 4); constexpr int d = p.distance_sq(); // 编译期计算 → 25 Point arr[d]; // ✅ 合法!d 是编译期常量
什么是右值引用
C++ 构造函数 特殊函数
default 函数
在 C++11 及以后的标准中,
= default和= delete是两个非常重要的关键字,用于显式控制特殊成员函数的生成行为。
= default:显式要求编译器生成默认实现告诉编译器:"请为这个函数生成默认的实现",即使你已经定义了其他构造函数。
支持
= default的函数(特殊成员函数)
- 默认构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数(C++11)
- 移动赋值运算符(C++11)
⚠️ 注意:
= default只能用于特殊成员函数,不能用于普通成员函数
delete函数
= delete:显式禁止某个函数被使用作用:告诉编译器:"这个函数不允许被调用,即使语法上看起来合法"。
典型用途
- 禁止拷贝(如单例、资源管理类)
- 禁止某些参数类型的调用(防止隐式转换)
- 禁用不安全的操作
示例 1:禁止拷贝(常见于 RAII 类)
cppclass NonCopyable { public: NonCopyable() = default; // 显式删除拷贝构造和拷贝赋值 NonCopyable(const NonCopyable&) = delete; NonCopyable& operator=(const NonCopyable&) = delete; }; NonCopyable a; // NonCopyable b = a; // ❌ 编译错误! // a = b; // ❌ 编译错误!💡 这比将拷贝函数设为
private更清晰、更早报错(编译期 vs 链接期)。示例 2:禁止特定类型调用(防隐式转换)
cppclass Number { public: Number(int n) : value(n) {} // 允许 int,但禁止 double(防止意外转换) Number(double) = delete; private: int value; }; Number n1(42); // ✅ OK // Number n2(3.14); // ❌ 错误:调用了 deleted 函数示例 3:禁用动态分配(禁止 new)
cppclass StackOnly { public: void* operator new(size_t) = delete; void* operator new[](size_t) = delete; }; StackOnly s; // ✅ OK(栈上) // StackOnly* p = new StackOnly(); // ❌ 错误!
= delete可用于任何函数
= delete必须在第一次声明时就指定移动操作被 delete 后,可能退化到拷贝
- 如果移动被 delete,但拷贝存在,
std::move(obj)会调用拷贝(如果允许)。- 所以要禁用所有复制/移动,需同时 delete 拷贝和移动。
c++的多态
序号 名称 实际归属 说明 1 重载多态(Overloading Polymorphism) 编译时 函数/运算符重载 2 强制多态(Coercion Polymorphism) 编译时 隐式类型转换(如 int→double)3 参数多态(Parametric Polymorphism) 编译时 模板(泛型) 4 包含多态(Inclusion Polymorphism) 运行时 虚函数 + 继承(子类型多态)
- 编译时多态(静态多态)
- 在编译阶段就确定调用哪个函数。
- 实现方式:
- 函数重载(Function Overloading)
- 运算符重载(Operator Overloading)
- 模板(Templates) → 泛型编程的核心
📌 特点:无运行时开销,效率高。
- 运行时多态(动态多态)
- 在程序运行时根据对象实际类型决定调用哪个函数。
- 实现方式:
- 虚函数(virtual functions) + 继承
- 通过基类指针/引用调用派生类重写的函数
📌 特点:灵活,但有虚表(vtable)和间接调用的开销。
std::optional的使用
std::optional<T>封装了一个类型T的对象,这个对象可能被初始化(有值),也可能未被初始化(无值)。
- 函数可能无法返回有效结果
例如:查找操作可能失败。
cpp#include <iostream> #include <optional> #include <vector> #include <string> std::optional<int> findIndex(const std::vector<int>& vec, int target) { for (size_t i = 0; i < vec.size(); ++i) { if (vec[i] == target) { return static_cast<int>(i); // 有值 } } return std::nullopt; // 无值(类似 nullptr) } int main() { auto idx = findIndex({1, 3, 5, 7}, 5); if (idx.has_value()) { std::cout << "Found at index: " << *idx << "\n"; } else { std::cout << "Not found!\n"; } }
- 替代指针或引用表示"可为空"
避免裸指针带来的内存管理问题和歧义。
cppstd::optional<std::string> getConfigValue(const std::string& key); // 比返回 const char* 或 string* 更安全、更语义清晰
- 作为类成员表示"尚未设置"的状态
cppclass UserProfile { std::optional<std::string> nickname; // 用户可能还没设置昵称 public: void setNickname(const std::string& name) { nickname = name; } bool hasNickname() const { return nickname.has_value(); } std::string getNickname() const { return nickname.value_or("Anonymous"); } };常用接口
方法 说明 has_value()判断是否有值(等价于 bool(*this))operator*解引用获取值(前提是有值!) value()获取值,若无值则抛出 std::bad_optional_accessvalue_or(default)有值则返回值,否则返回默认值 reset()清除值(变为无值状态) emplace(args...)就地构造内部对象
cppstd::optional<int> x = 42; if (x) { std::cout << *x << "\n"; // 42 std::cout << x.value() << "\n"; // 42 } std::cout << x.value_or(0) << "\n"; // 42 x.reset(); std::cout << x.value_or(-1) << "\n"; // -1
std::optional与指针的区别
特性 T*std::optional<T>表示"无值" nullptrnullopt所有权 无(裸指针) 有(值语义,管理内部对象生命周期) 内存位置 堆/栈任意 内部存储(通常在栈上) 安全性 易悬空、误用 更安全(编译器帮助检查) ✅ 优先用
optional表示"可选值",用智能指针表示"可选所有权"
可变参数函数
在 C++ 中,可变参数函数(variadic function) 指的是可以接受任意数量、任意类型参数的函数。C++ 提供了两种主要方式实现:
1、C 风格可变参数(不推荐,仅用于兼容 C)
使用
<cstdarg>头文件中的宏:va_list,va_start,va_arg,va_end❌ 缺点:
- 无类型安全
- 不能处理引用、类对象(可能出错)
- 容易崩溃
示例(不推荐):
cpp#include <iostream> #include <cstdarg> // 计算 int 类型参数的和(必须提前知道参数个数!) int sum(int count, ...) { va_list args; va_start(args, count); // 从 count 后开始读取 int total = 0; for (int i = 0; i < count; ++i) { total += va_arg(args, int); // 假设都是 int } va_end(args); return total; } int main() { std::cout << sum(3, 10, 20, 30); // 输出: 60 }2、C++11 起:可变参数模板(Variadic Templates) ✅(推荐!)
这是类型安全、高效、现代 C++ 的标准做法。
typename... Args:模板参数包(表示 0 个或多个类型)Args... args:函数参数包(表示 0 个或多个参数)args...:包展开(pack expansion),把参数一个一个"拆开"- 必须有终止条件(通常是无参重载)
🔑 核心语法:
cpptemplate<typename... Args> void func(Args... args); // Args... 叫"参数包(parameter pack)"示例 1:打印任意数量、任意类型的参数
cpp#include <iostream> // 递归终止:无参数 void print() { std::cout << "\n"; } // 递归展开:取第一个参数,其余继续递归 template<typename T, typename... Args> void print(T first, Args... rest) { std::cout << first << " "; print(rest...); // 展开剩余参数 } int main() { print(1, 2.5, "hello", 'A'); // 输出: 1 2.5 hello A }示例 2:使用折叠表达式(C++17 起,更简洁)
cpp#include <iostream> template<typename... Args> void print(Args... args) { ((std::cout << args << " "), ...); // 折叠表达式 std::cout << "\n"; } int main() { print(1, 2.5, "world"); // 输出: 1 2.5 world }完美转发 + 构造对象(如
make_unique)
cpp#include <memory> #include <string> template<typename T, typename... Args> std::unique_ptr<T> my_make_unique(Args&&... args) { return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); } // 测试类 class Person { std::string name; int age; public: Person(const std::string& n, int a) : name(n), age(a) {} void show() { std::cout << name << ", " << age << "\n"; } }; int main() { auto p = my_make_unique<Person>("Alice", 30); p->show(); // 输出: Alice, 30 }
std::forward的作用? 完美转发(Perfect Forwarding)
它的核心作用是:
在不改变原始值类别(左值 / 右值)的前提下,将参数原样转发给另一个函数。
cpptemplate<typename T> void wrapper(T&& arg) { other_func(std::forward<T>(arg)); // 保持 arg 原来的"身份" }
- 如果调用
wrapper(x)(x是变量 → 左值 ),则other_func收到的是 左值引用- 如果调用
wrapper(42)(42是字面量 → 右值 ),则other_func收到的是 右值引用
为什么需要 std::forward?
问题:普通引用会"丢失"右值信息
cppvoid process(int& x) { std::cout << "左值\n"; } void process(int&& x) { std::cout << "右值\n"; } template<typename T> void bad_wrapper(T&& arg) { process(arg); // ❌ 总是调用左值版本! } int main() { int a = 10; bad_wrapper(a); // 输出: 左值 ✅ bad_wrapper(20); // 输出: 左值 ❌(期望是右值!) }💥 问题:
arg在函数体内是一个"命名变量" → 永远是左值!即使传入的是右值(如
20),arg本身也是左值,所以process(arg)总是匹配左值重载。解决方案:用
std::forward
cpptemplate<typename T> void good_wrapper(T&& arg) { process(std::forward<T>(arg)); // ✅ 根据 T 的类型决定转发为左值 or 右值 } int main() { int a = 10; good_wrapper(a); // 输出: 左值 good_wrapper(20); // 输出: 右值 ✅ }
std::forward<T>(arg)能还原arg最初的值类别!
什么是对象切片?
对象切片(Object Slicing) 是 C++ 中一个常见且危险的陷阱 ,发生在将派生类对象赋值给基类对象(非指针/引用)时,派生类的额外成员被"切掉",只保留基类部分。
- C++ 中,对象是值语义(value semantics)
- 当你把一个
Derived对象赋给Base类型的变量时,编译器只拷贝Base部分
cpp#include <iostream> class Base { public: int x = 1; virtual void say() { std::cout << "Base: " << x << "\n"; } }; class Derived : public Base { public: int y = 2; // 派生类特有成员 void say() override { std::cout << "Derived: " << x << ", " << y << "\n"; } }; int main() { Derived d; d.say(); // 输出: Derived: 1, 2 Base b = d; // ❌ 对象切片!只拷贝 Base 部分(x=1),y 被丢弃 b.say(); // 输出: Base: 1 (注意:不是 "Derived"!) }💥
b是一个纯粹的Base对象 ,没有虚表指向Derived,所以调用的是Base::say()!
对象切片发生的场景,如何避免对象切片?
场景 是否切片 说明 Base b = derived_obj;✅ 是 值拷贝,切片 void func(Base obj); func(derived);✅ 是 按值传参,切片 Base& b = derived;❌ 否 引用,多态有效 Base* b = &derived;❌ 否 指针,多态有效 std::vector<Base> v; v.push_back(derived);✅ 是 容器存储值,切片 如何避免对象切片?
方法 1:使用指针或引用
cppvoid process(const Base& obj) { // 用 const 引用 obj.say(); // 多态生效! } int main() { Derived d; process(d); // ✅ 输出 "Derived: 1, 2" }方法 2:禁止按值传递多态对象
- 将基类的拷贝构造函数设为
delete(C++11 起)
cppclass Base { public: Base() = default; Base(const Base&) = delete; // 禁止拷贝 Base& operator=(const Base&) = delete; // 禁止赋值 virtual ~Base() = default; virtual void say() { /*...*/ } };这样一旦写
Base b = derived;,编译直接报错!方法 3:容器中存储智能指针
cpp// 错误:会切片 std::vector<Base> shapes; // 正确:用指针保持多态 std::vector<std::unique_ptr<Base>> shapes; shapes.push_back(std::make_unique<Derived>());
对象切片 vs 多态
行为 对象切片 正确多态 存储方式 Base obj = derived;Base& ref = derived;或Base* ptr = &derived;调用虚函数 调用 Base版本调用实际对象的版本 成员数据 只有 Base成员完整对象(包括派生类成员) 对象切片只发生在"按值操作"多态对象时。
只要使用引用、指针或智能指针,就能安全享受 C++ 多态的好处!
static_assert的作用??
static_assert是 C++11 引入的一个编译期断言 机制,用于在编译阶段 检查某个常量表达式是否为true。如果条件不满足,编译将失败,并显示你提供的错误信息。它的核心作用是:在编译时捕获逻辑错误、类型约束违规或平台假设不成立等问题,而不是等到运行时才发现。
static_assert(常量表达式, "错误提示字符串");
- 常量表达式 :必须在编译期就能求值(如
sizeof(int) == 4、模板参数、constexpr变量等)。- 错误提示字符串:必须是字符串字面量(C++17 起可省略,但强烈建议保留)。
✅ 从 C++17 开始,允许只写一个参数:
cppstatic_assert(sizeof(int) >= 4); // 合法(C++17+)
- 验证类型属性(常用于模板)
确保模板参数满足某些要求:
cpptemplate<typename T> void process(T value) { static_assert(std::is_integral_v<T>, "T must be an integral type!"); // ... }如果用户调用
process(3.14),编译器会报错:
cpperror: static assertion failed: T must be an integral type!
- 检查平台或编译器假设
例如,确保指针大小符合预期:
cppstatic_assert(sizeof(void*) == 8, "This code requires 64-bit pointers!");如果在 32 位系统上编译,直接失败。
- 强制接口契约(设计约束)
比如,确保结构体没有填充(用于网络协议或硬件寄存器映射):
cppstruct Packet { uint32_t id; uint16_t size; }; static_assert(sizeof(Packet) == 6, "Packet must be exactly 6 bytes (no padding)!");
- 替代
#error(更灵活)比预处理器指令更强大,因为可以使用 C++ 类型系统和
constexpr:
cpp#if defined(_WIN32) static_assert(false, "Windows is not supported!"); // ❌ 错误!见下方说明 #endif⚠️ 注意:上面写法在 C++17 前可能有问题(因为
false是常量,即使代码路径不执行也会触发)。正确做法(依赖模板):
cpptemplate<typename T> void unsupported_platform() { static_assert(sizeof(T) == 0, "Platform not supported!"); }
static_assert是 C++ 中实现"编译期防御性编程"的利器。它把错误暴露在最早阶段(编译时),提升代码健壮性、可维护性和文档性。