c++高频总结五
Cpp新特性知道哪些
C++ 自 C++11 开始引入了大量的新特性,每个标准版本都在语言和库的各方面进行了增强。以下是各个 C++ 标准(从 C++11 到 C++23)的重要新特性总结:
C++11(核心特性升级)
1. 语言特性
自动类型推导 (
auto
): 自动推导变量类型。1
auto x = 42; // x 推导为 int
右值引用和移动语义: 用于优化临时对象的资源管理,
std::move
可显式调用。1 2
std::string s = "hello"; std::string t = std::move(s); // s 的资源转移给 t
初始化列表: 方便地对容器或对象进行初始化。
1
std::vector<int> vec = {1, 2, 3};
Lambda 表达式: 提供匿名函数支持。
1
auto add = [](int a, int b) { return a + b; };
nullptr
: 统一的空指针类型,取代NULL
。1
int* ptr = nullptr;
强类型枚举 (
enum class
): 避免普通枚举类型的作用域污染。1
enum class Color { Red, Green, Blue };
关键字
constexpr
: 支持编译期计算的常量表达式。1
constexpr int square(int x) { return x * x; }
线程支持: 引入
std::thread
、std::mutex
等多线程支持。1 2
std::thread t([] { std::cout << "Hello, thread!"; }); t.join();
decltype
: 用于获取表达式的类型。1 2
int x = 0; decltype(x) y = 42; // y 的类型是 int
静态断言 (
static_assert
): 编译期检查条件。1
static_assert(sizeof(int) == 4, "int size must be 4 bytes");
2. 标准库特性
智能指针: 引入
std::shared_ptr
、std::unique_ptr
和std::weak_ptr
。1
std::shared_ptr<int> p = std::make_shared<int>(10);
容器改进:
std::array
(静态数组容器)。std::unordered_map
和std::unordered_set
(基于哈希表)。
正则表达式: 提供正则匹配支持。
1
std::regex re("\\w+");
C++14(增强和优化)
1. 语言特性
泛型 Lambda 表达式: Lambda 支持自动类型推导。
1
auto lambda = [](auto a, auto b) { return a + b; };
decltype(auto
): 根据返回值类型进行推导。1
decltype(auto) func() { int x = 42; return (x); }
二进制字面值: 提供二进制数支持。
1
int bin = 0b1010; // 等于 10
constexpr
的增强: 支持更复杂的逻辑。1 2 3
constexpr int factorial(int n) { return n <= 1 ? 1 : (n * factorial(n - 1)); }
C++17(简化与增强)
1. 语言特性
结构化绑定: 解构赋值。
1
auto [a, b] = std::pair<int, int>{1, 2};
if constexpr
: 编译期条件分支。1 2 3
if constexpr (sizeof(int) == 4) { std::cout << "int is 4 bytes"; }
内联变量: 允许在头文件中声明具有唯一定义的变量。
1
inline int global_value = 42;
折叠表达式: 用于参数包的累积操作。
1 2
template <typename... Args> auto sum(Args... args) { return (... + args); }
2. 标准库特性
std::optional
: 用于存储可能为空的值。1
std::optional<int> opt = 42;
std::variant
: 安全的联合类型。1
std::variant<int, std::string> var = "Hello";
std::any
: 用于存储任意类型的值。1
std::any val = 10;
并行算法: 标准算法支持并行执行。
1 2
std::vector<int> vec = {1, 2, 3}; std::sort(std::execution::par, vec.begin(), vec.end());
C++20(革命性升级)
1. 语言特性
模块(Modules): 替代传统头文件的模块化功能。
概念(Concepts): 用于约束模板参数。
1 2
template <typename T> concept Integral = std::is_integral_v<T>;
协程(Coroutines): 支持异步操作。
1
co_return 42;
范围库(Ranges): 提供强大的范围操作。
1 2
std::vector<int> v = {1, 2, 3}; auto res = v | std::views::filter([](int i) { return i % 2 == 0; });
constexpr
的进一步增强: 支持更多场景,例如动态分配。
C++23(现代化补充)
1. 语言特性
- 多维数组的支持: 引入
mdspan
,更方便处理多维数组。 - 静默终止的范围 (
std::ranges::to
): 提供更直观的范围转换。 - 新语法和优化:
std::print
提供更简便的格式化输出。constexpr
支持更强大的计算能力。
总结
版本 | 核心特性 |
---|---|
C++11 | Lambda 表达式、右值引用、智能指针、线程支持、auto 、constexpr 。 |
C++14 | 泛型 Lambda、decltype(auto) 、二进制字面值、增强的 constexpr 。 |
C++17 | 结构化绑定、if constexpr 、std::optional 、std::variant 、并行算法。 |
C++20 | 模块、概念、协程、范围库(Ranges)、增强的 constexpr 、std::format 。 |
C++23 | 多维数组支持、新范围特性、更简化的输出函数等。 |
每个版本的新增特性使 C++ 更加现代化、易用,同时提升了性能和开发效率!
类型转换
在 C++ 中,类型转换是将一种数据类型转换为另一种数据类型的操作。C++ 提供了多种类型转换方式,包括隐式类型转换、显式类型转换(也称为强制类型转换),以及 C++ 特有的四种类型转换运算符(static_cast
、dynamic_cast
、const_cast
和 reinterpret_cast
)。
以下是对类型转换的详细介绍:
1. 隐式类型转换(Implicit Type Conversion)
隐式类型转换是编译器自动执行的类型转换,无需程序员显式指定。也称为类型提升或类型兼容。
规则
算术类型的转换:
- 小范围类型会提升到大范围类型,例如
char
转换为int
。 - 浮点数和整数混合运算时,整数会转换为浮点数。
1 2 3
int a = 10; double b = 3.14; double c = a + b; // a 自动转换为 double
- 小范围类型会提升到大范围类型,例如
指针的转换:
- 非常量指针可以隐式转换为常量指针。
- 空指针常量(如
nullptr
)可以转换为任意指针类型。
1 2
int x = 42; const int* p = &x; // 非常量指针转为常量指针
2. 显式类型转换(Explicit Type Conversion)
显式类型转换是通过程序员明确指示编译器进行的类型转换,也称为强制类型转换。
C 风格的类型转换
1
(type) expression
这是 C 风格的强制类型转换,灵活但容易导致错误。
例子:
1
2
int a = 10;
double b = (double)a; // C 风格的类型转换
缺点:
- 不易读,难以定位问题。
- 不区分不同类型的转换意图,容易导致不安全的操作。
3. C++ 类型转换运算符
C++ 提供了四种类型转换运算符,分别针对不同的场景,提供了更明确和更安全的转换方式:
(1) static_cast
- 用于在类型兼容的情况下进行明确的类型转换。
- 通常用于基本类型之间的转换、类层次结构中的向上转换(从派生类到基类)等。
语法:
1
static_cast<type>(expression)
例子:
1
2
3
4
5
6
int a = 10;
double b = static_cast<double>(a); // 安全的类型转换
class Base {};
class Derived : public Base {};
Base* base = static_cast<Base*>(new Derived()); // 向上转换
特点:
- 编译时完成类型检查,安全性高。
- 不允许非相关类型之间的转换。
(2) dynamic_cast
- 用于多态类型(即存在虚函数的类)之间的类型转换。
- 通常用于向下转换(从基类到派生类),并在运行时进行类型检查。
语法:
1
dynamic_cast<type>(expression)
例子:
1
2
3
4
5
6
7
8
9
10
11
class Base {
virtual void func() {}
};
class Derived : public Base {};
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base); // 向下转换
if (derived) {
// 转换成功
}
特点:
- 运行时检查类型的合法性,不合法时返回
nullptr
(指针)或抛出异常(引用)。 - 必须用于包含虚函数的类。
(3) const_cast
- 用于移除或添加对象的
const
限定。 - 常用于需要修改常量对象时(但需谨慎使用)。
语法:
1
const_cast<type>(expression)
例子:
1
2
3
const int a = 10;
int* p = const_cast<int*>(&a); // 移除 const 限定
*p = 20; // 未定义行为(如果 a 原本是常量)
特点:
- 只能改变对象的
const
或volatile
限定符,不影响其他类型转换。 - 修改真正的常量对象会导致未定义行为。
(4) reinterpret_cast
- 用于将指针或引用类型转换为完全不相关的类型。
- 这是最低层次、最危险的类型转换。
语法:
1
reinterpret_cast<type>(expression)
例子:
1
2
3
int a = 10;
void* p = reinterpret_cast<void*>(&a); // 转为 void*
int* p2 = reinterpret_cast<int*>(p); // 再转换回 int*
特点:
- 没有类型检查,不保证结果的可用性。
- 主要用于操作底层数据结构,例如与硬件相关的编程。
4. 类型转换的优先级
类型转换按以下优先级进行:
- C++ 类型转换运算符(如
static_cast
)优先于 C 风格的强制类型转换。 - 隐式类型转换优先于显式类型转换。
5. 使用场景
转换类型 | 使用场景 |
---|---|
static_cast | 基本类型转换、类层次结构中安全的向上转换。 |
dynamic_cast | 类层次结构中带运行时检查的向下转换,通常用于多态场景。 |
const_cast | 移除或添加 const 限定,通常用于修正 API 的限制。 |
reinterpret_cast | 指针或引用的底层转换,通常用于低级编程或与 C 接口交互。 |
6. 注意事项
- 避免滥用类型转换:
- 使用类型转换可能隐藏代码中的设计缺陷。优先考虑重构代码,而非频繁使用类型转换。
- 谨慎使用
reinterpret_cast
和const_cast
:- 它们不进行类型检查,可能导致未定义行为。
- 优先使用 C++ 风格的类型转换:
- C 风格的类型转换可能不安全,也难以阅读。
dynamic_cast
需要虚函数:- 如果类中没有虚函数,
dynamic_cast
将无法使用。
- 如果类中没有虚函数,
总结
转换方式 | 类型检查 | 转换时间 | 用途 |
---|---|---|---|
隐式转换 | 自动 | 编译时 | 类型兼容时的自动转换。 |
C 风格转换 | 无 | 编译时 | 不推荐,容易引入隐患。 |
static_cast | 有 | 编译时 | 安全的显式转换,用于兼容类型之间的转换。 |
dynamic_cast | 有 | 运行时 | 多态类型之间的向下转换,带运行时检查。 |
const_cast | 有 | 编译时 | 添加或移除 const 或 volatile 。 |
reinterpret_cast | 无 | 编译时 | 最低级别的转换,用于底层操作。 |
选择合适的类型转换方式,可以提高代码的安全性和可维护性!
RAII基于什么实现的
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种 C++ 编程中广泛使用的资源管理惯用法。它的核心思想是将资源的生命周期绑定到对象的生命周期,通过对象的构造函数获取资源,在对象的析构函数中释放资源。
RAII 的实现基于以下两个核心特性:
1. 构造函数和析构函数
RAII 的核心依赖于 C++ 的构造函数和析构函数的机制:
- 构造函数:在对象创建时自动调用,用于初始化对象并获取所需资源(如内存、文件句柄、互斥锁等)。
- 析构函数:在对象销毁时自动调用,用于释放对象占用的资源。
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <fstream>
class FileRAII {
public:
FileRAII(const std::string& filename) {
file.open(filename);
if (!file.is_open()) {
throw std::runtime_error("Failed to open file");
}
}
~FileRAII() {
if (file.is_open()) {
file.close();
}
}
private:
std::ofstream file;
};
int main() {
try {
FileRAII fileManager("example.txt");
// 文件在此作用域内被管理
} // fileManager 的析构函数会在这里关闭文件
catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
return 0;
}
在上述例子中:
- 构造函数:在对象创建时打开文件。
- 析构函数:在对象销毁时自动关闭文件,即使发生异常也会自动释放资源。
2. 栈对象的生命周期管理
RAII 的资源管理依赖于 C++ 栈对象的作用域规则:
- 当栈对象超出其作用域时,编译器会自动调用该对象的析构函数。
- 这种自动销毁机制确保资源总能被正确释放,即使在异常情况下也是如此。
例子:
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <mutex>
std::mutex mtx;
void criticalSection() {
std::lock_guard<std::mutex> lock(mtx); // RAII 管理锁
// 代码在此区域内操作临界区
// 离开作用域时 lock_guard 会自动释放锁
std::cout << "In critical section" << std::endl;
}
在这个例子中,std::lock_guard
是一个 RAII 类型,它在构造时加锁,在析构时自动解锁。
RAII 的核心特性
- 自动化资源管理: RAII 通过构造函数和析构函数自动管理资源的获取和释放,减少手动管理资源的负担。
- 异常安全性: 即使在异常情况下,RAII 对象的析构函数仍会被调用,确保资源不会泄露。
- 作用域绑定: RAII 将资源的生命周期绑定到对象的作用域,从而实现资源的自动释放。
RAII 的实现依赖哪些语言特性
- 构造函数和析构函数:
- 构造函数用于获取资源。
- 析构函数用于释放资源。
- 栈对象的作用域规则: 栈对象超出作用域时,析构函数会被自动调用。
- 异常处理机制: RAII 能有效处理异常情况下的资源释放问题,因为析构函数始终会被调用。
- 封装: RAII 通常通过类封装资源及其管理逻辑。
RAII 的典型应用场景
智能指针:
std::unique_ptr
:独占管理动态内存。std::shared_ptr
:共享管理动态内存。std::weak_ptr
:避免循环引用。
1
std::unique_ptr<int> ptr(new int(10)); // 自动释放内存
文件管理: 自动打开和关闭文件。
锁管理: 使用
std::lock_guard
或std::unique_lock
管理多线程互斥锁。网络资源: 自动管理网络连接的打开和关闭。
数据库连接: 管理数据库连接的生命周期。
RAII 的优点
- 资源安全: 自动释放资源,减少内存泄漏和资源泄露。
- 代码简洁: 不需要显式调用资源释放函数,代码更直观。
- 异常安全: 即使发生异常,资源也能被安全释放。
- 与 STL 配合良好: RAII 机制与现代 C++ 标准库中的容器和算法高度兼容。
RAII 的局限性
- 不适合非作用域生命周期的资源: 如果资源的生命周期超出作用域范围,RAII 可能无法有效管理。
- 需要小心封装: 不恰当的封装可能导致析构函数误释放资源。
- 过于依赖对象销毁的时间点: 在某些情况下(如性能要求),依赖作用域可能导致资源释放的时机不够灵活。
RAII 与现代 C++ 的结合
现代 C++(尤其是 C++11 和之后的标准)为 RAII 提供了更强大的支持,如:
- 智能指针(
std::unique_ptr
,std::shared_ptr
)。 - 锁管理类(
std::lock_guard
,std::unique_lock
)。 - 容器类(
std::vector
,std::string
等)。
这些工具都充分利用了 RAII 的思想,将资源的管理变得更加简单和安全。
总结
RAII 的实现依赖于 构造函数和析构函数 的自动调用机制,以及 C++ 对象的作用域规则。它是一种极为重要的编程惯用法,确保资源在异常或正常情况下都能被正确管理,在现代 C++ 中得到了广泛应用和支持。
手撕:Unique_ptr,控制权转移(移动语义)
以下是一个手写实现 std::unique_ptr
的例子,包含核心功能(如控制权转移和移动语义),以帮助理解其工作原理。
Unique_ptr
的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <utility> // std::move
// 定义 Unique_ptr 模板类
template <typename T>
class Unique_ptr {
private:
T* ptr; // 原始指针,管理资源
public:
// 构造函数:接收一个指针
explicit Unique_ptr(T* p = nullptr) : ptr(p) {}
// 禁用拷贝构造函数
Unique_ptr(const Unique_ptr& other) = delete;
// 禁用拷贝赋值运算符
Unique_ptr& operator=(const Unique_ptr& other) = delete;
// 移动构造函数:转移所有权
Unique_ptr(Unique_ptr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr; // 清空原对象的指针,避免重复释放
}
// 移动赋值运算符:转移所有权
Unique_ptr& operator=(Unique_ptr&& other) noexcept {
if (this != &other) {
delete ptr; // 释放当前持有的资源
ptr = other.ptr; // 转移指针
other.ptr = nullptr; // 清空原对象的指针
}
return *this;
}
// 析构函数:释放资源
~Unique_ptr() {
delete ptr;
}
// 重载 `*` 运算符:解引用
T& operator*() const {
return *ptr;
}
// 重载 `->` 运算符:访问成员
T* operator->() const {
return ptr;
}
// 获取指针
T* get() const {
return ptr;
}
// 释放控制权并返回指针
T* release() {
T* temp = ptr;
ptr = nullptr; // 清空当前指针
return temp;
}
// 重置指针
void reset(T* p = nullptr) {
delete ptr; // 删除旧的资源
ptr = p; // 设置新的资源
}
};
功能实现说明
1. 控制权转移(移动语义)
- 移动构造函数:允许通过
std::move
将资源从一个Unique_ptr
转移到另一个对象。 - 移动赋值运算符:实现资源的转移,同时释放当前对象持有的资源。
- 关键点:
- 转移所有权后,原对象的指针必须置为
nullptr
,以防止重复释放资源。
- 转移所有权后,原对象的指针必须置为
2. 资源管理
- 构造函数:通过一个原始指针初始化
Unique_ptr
。 - 析构函数:在
Unique_ptr
对象销毁时自动释放资源。 reset
和release
:reset
用于替换管理的资源。release
用于放弃对资源的管理并返回原始指针。
测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int main() {
// 创建一个 Unique_ptr 管理整数
Unique_ptr<int> uptr1(new int(42));
std::cout << "Value: " << *uptr1 << std::endl;
// 转移控制权
Unique_ptr<int> uptr2 = std::move(uptr1);
if (!uptr1.get()) {
std::cout << "uptr1 is empty after move." << std::endl;
}
std::cout << "uptr2 value: " << *uptr2 << std::endl;
// 使用 release() 放弃资源管理
int* rawPtr = uptr2.release();
if (!uptr2.get()) {
std::cout << "uptr2 is empty after release." << std::endl;
}
std::cout << "Raw pointer value: " << *rawPtr << std::endl;
// 手动释放原始指针资源
delete rawPtr;
// 使用 reset() 管理新资源
uptr2.reset(new int(88));
std::cout << "uptr2 new value: " << *uptr2 << std::endl;
return 0;
}
测试结果
1
2
3
4
5
6
Value: 42
uptr1 is empty after move.
uptr2 value: 42
uptr2 is empty after release.
Raw pointer value: 42
uptr2 new value: 88
关键点总结
- 禁止拷贝:
Unique_ptr
不允许拷贝(删除拷贝构造和赋值运算符)。- 通过移动语义实现控制权的转移。
- 自动资源释放:
- 在
Unique_ptr
销毁时,析构函数自动释放资源,避免内存泄漏。
- 在
- 安全管理:
- 原指针在转移或释放后被置为
nullptr
,避免重复释放。
- 原指针在转移或释放后被置为
- 功能扩展:
- 支持
release()
和reset()
,以更灵活地管理资源。
- 支持
这就是一个简化版的 Unique_ptr
实现,展示了其核心功能及其与移动语义的结合!
手撕:类继承,堆栈上分别代码实现多态
以下是关于 类继承 和 多态 的代码实现,分别展示在 堆 和 栈 上如何实现多态。
1. 基本概念
- 继承:通过基类(父类)定义通用接口或功能,派生类(子类)可以扩展或重写这些功能。
- 多态:使用基类指针或引用调用子类的实现,行为取决于运行时的对象类型。
多态的实现依赖于以下两个关键点:
- 虚函数(
virtual
):在基类中定义虚函数,允许派生类重写。 - 运行时类型信息(RTTI):通过虚表(vtable)实现动态绑定,确保调用的函数与对象实际类型匹配。
2. 在堆上实现多态
在堆上创建对象,并通过基类指针操作,实现多态。
代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <memory> // 使用智能指针,防止内存泄漏
// 基类
class Shape {
public:
virtual void draw() const { // 虚函数
std::cout << "Drawing Shape" << std::endl;
}
virtual ~Shape() = default; // 虚析构函数,确保子类析构函数被正确调用
};
// 派生类:圆形
class Circle : public Shape {
public:
void draw() const override { // 重写虚函数
std::cout << "Drawing Circle" << std::endl;
}
};
// 派生类:矩形
class Rectangle : public Shape {
public:
void draw() const override { // 重写虚函数
std::cout << "Drawing Rectangle" << std::endl;
}
};
int main() {
// 在堆上分配对象
std::unique_ptr<Shape> shape1 = std::make_unique<Circle>();
std::unique_ptr<Shape> shape2 = std::make_unique<Rectangle>();
// 调用虚函数,基类指针调用子类的实现
shape1->draw(); // 输出:Drawing Circle
shape2->draw(); // 输出:Drawing Rectangle
return 0;
}
关键点
- 使用
virtual
定义虚函数,支持运行时动态绑定。 - 使用智能指针(如
std::unique_ptr
),自动管理堆内存,避免内存泄漏。 - 虚析构函数保证派生类的析构函数被正确调用。
3. 在栈上实现多态
在栈上创建对象,利用基类引用实现多态。
代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
// 基类
class Shape {
public:
virtual void draw() const { // 虚函数
std::cout << "Drawing Shape" << std::endl;
}
virtual ~Shape() = default; // 虚析构函数
};
// 派生类:圆形
class Circle : public Shape {
public:
void draw() const override { // 重写虚函数
std::cout << "Drawing Circle" << std::endl;
}
};
// 派生类:矩形
class Rectangle : public Shape {
public:
void draw() const override { // 重写虚函数
std::cout << "Drawing Rectangle" << std::endl;
}
};
int main() {
// 在栈上创建派生类对象
Circle circle;
Rectangle rectangle;
// 使用基类引用实现多态
Shape& shape1 = circle;
Shape& shape2 = rectangle;
// 调用虚函数,基类引用调用子类的实现
shape1.draw(); // 输出:Drawing Circle
shape2.draw(); // 输出:Drawing Rectangle
return 0;
}
关键点
- 使用基类引用(
Shape&
)操作派生类对象,实现多态。 - 栈上的对象在作用域结束时自动销毁,无需手动释放。
4. 区别总结
特性 | 堆上实现多态 | 栈上实现多态 |
---|---|---|
内存管理 | 动态分配,需要手动或智能指针管理资源。 | 自动分配,由作用域管理资源。 |
使用成本 | 内存分配和释放有一定开销。 | 无额外开销,自动管理更高效。 |
适用场景 | 对象生命周期复杂或大小动态确定时使用。 | 对象生命周期确定且大小固定时使用。 |
安全性 | 可能会导致内存泄漏(使用智能指针避免)。 | 无需担心内存泄漏。 |
灵活性 | 可以动态决定创建的对象类型。 | 类型必须在编译时确定。 |
5. 总结
- 堆上多态适合复杂场景,例如动态确定对象类型或对象需要在作用域之外存在。
- 栈上多态适合简单场景,例如对象生命周期短且确定。
两种方式都依赖 C++ 的 虚函数机制 和 动态绑定 实现多态。在实际开发中,根据具体需求选择合适的实现方式即可。