主要记录关于C++11特性的学习、思考、使用例子等,主要为了避免遗忘,以及随时回顾。
自动类型推导(auto)
关键字
auto作用
可以让编译器自动推导变量的类型,简化代码,减少重复的类型声明。
使用场景
- 当类型名称过长或过于复杂时。
- 在使用模板和迭代器时,避免编写复杂类型名称。
注意事项
- 必须在声明时初始化,编译器才能推导类型。
- 避免使用
auto导致代码的意图不明确。
- 不能用于推导数组类型。
示例代码
基于范围的 for 循环
作用
提供了一种更简单、更安全的方式来遍历容器,无需手动操作迭代器或关心容器的边界。
使用场景
适用于任何容器的遍历,如
std::vector, std::list, std::map 等。注意事项
- 如果需要修改容器中的元素,应使用引用方式遍历。
- 如果不需要修改元素,最好以
const引用遍历以提高效率。(避免复制)
示例代码
- 基础使用:
- 使用const:
Lambda表达式
作用
- 简洁的语法:Lambda 表达式提供了一种简洁的方式来定义和使用匿名函数,减少了编写额外函数声明的需要。
- 即时定义,即时使用:Lambda 允许在需要函数对象的地方直接定义和使用,增加了代码的局部性和可读性。
- 直接访问局部变量:Lambda 可以捕获其定义作用域中的变量,便于对这些变量进行操作。
- 支持闭包:通过捕获列表,Lambda 可以保持对外部变量的访问,是实现闭包(Closure)的强大工具。
使用场景
- STL算法:使用 Lambda 表达式作为自定义的比较函数或操作,如
std::sort、std::find_if等。
- 异步编程:结合
std::async和std::thread,使用 Lambda 表达式定义线程执行的任务。
- 事件处理:在 GUI 编程或其他需要事件处理的场景中,Lambda 表达式用于定义事件响应函数。
- 延迟计算:Lambda 表达式可以存储起来,按需调用,实现延迟计算。
注意事项
- 捕获列表:Lambda 的捕获列表定义了外部变量的捕获方式,可以是值捕获(
[=])、引用捕获([&])或混合方式。
- 变量生命周期:捕获的外部变量必须确保在 Lambda 被调用时仍然有效。
- 异常安全:在 Lambda 表达式中抛出异常时,需要确保合适的异常处理机制。
示例代码
智能指针
C++11 引入了三种智能指针
std::shared_ptr, std::unique_ptr, std::weak_ptr。作用
可以用以管理动态分配的内存,减少内存泄漏、野指针的风险。
种类
std::unique_ptr:独占对象所有权。
std::shared_ptr:共享对象所有权。
std::weak_ptr:解决std::shared_ptr之间的循环引用问题。
std::unique_ptr
简介:
std::unique_ptr是一种独占所有权的智能指针,意味着同一时间只有一个std::unique_ptr可以指向一个给定的对象。
- 当
std::unique_ptr被销毁时,它指向的对象也会被自动删除。
使用场景:
- 当对象的生命周期由单一所有者管理时。
- 用于实现 RAII(资源获取即初始化)封装。
注意事项:
- 不可以复制
std::unique_ptr,但可以移动其所有权。
- 使用
std::move()在函数返回或传递时转移所有权。
示例代码:
std::shared_ptr
简介:
std::shared_ptr可以共享所有权,即多个指针可以同时拥有一个对象的所有权
- 最后一个被销毁的
std::shared_ptr会释放所管理的对象。
使用场景:
- 用于复杂的对象关系,如图或树结构。
- 当一个对象需要被多个所有者共享时。
注意事项:
- 避免创建循环引用,否则会导致内存泄漏。
- 使用
std::make_shared<T>()来构造std::shared_ptr是首选,因为它可以更高效地分配内存。
代码示例:
std::weak_ptr
简介:
std::weak_ptr是一种非拥有(观察者)智能指针,它引用由某个std::shared_ptr所管理的对象,但不影响该对象的引用计数。
- 可以用来解决
std::shared_ptr相互引用时可能出现的循环引用问题。
使用场景:
- 用来观察
std::shared_ptr管理的对象,而不需要长期持有对象。
- 用于解决
std::shared_ptr间的循环引用问题。
注意事项:
- 在使用之前,需要将
std::weak_ptr提升为std::shared_ptr。
- 始终检查
std::weak_ptr是否为空,因为其引用的对象可能已经被销毁。
代码示例:
统一初始化列表{}
作用
- 防止窄化转换:统一初始化禁止隐式的窄化转换,确保了数值精度和类型安全。
- 提高代码一致性和可读性:使用同一种初始化语法可以使代码更加一致,易于阅读和维护。
- 简化复杂对象的初始化:特别是在初始化具有多个成员的类或结构体时,统一初始化可以清晰地展示如何初始化每个成员。
使用场景
- 初始化 POD 类型:如整数、浮点数等。
- 初始化数组和容器:如
std::vector、std::array等。
- 初始化类和结构体:特别是那些包含多个成员变量的。
- 在构造函数中初始化成员变量:使用初始化列表来初始化类的成员变量。
注意事项
- 与构造函数的区别:
- 使用大括号
{}进行统一初始化时,编译器会优先调用接受std::initializer_list参数的构造函数。 - 如果不存在这样的构造函数,编译器会尝试其他构造函数。
- 列表初始化和默认构造函数:
- 在某些情况下,使用空的大括号
{}初始化一个对象会调用默认构造函数,而不是生成一个空的初始化列表。
示例代码
基本类型初始化
数组和容器初始化
类和结构体初始化
初始化列表和构造函数选择
右值引用与移动语义
简介
- 右值引用:
- 是对临时对象的引用,允许标识一个对象的数据是可以被“移动”而非复制的。
- 通过
&&操作符标识。
- 移动语义:
- 允许资源(如动态内存、文件句柄、网络连接等)的所有权从一个对象转移到另一个对象,即只涉及指针的赋值,而非数据的复制。
- 通过移动构造函数和移动赋值操作符实现。
作用:
- 减少不必要的对象复制:避免在临时对象的创建和销毁过程中进行昂贵的深层复制。
- 提高性能:尤其是在涉及大型对象和容器的情况下,如大型向量、字符串、矩阵等。
- 资源管理更高效:使得资源管理(如内存分配)更加高效和直接。
使用场景
- 在实现自己的类或结构体时,特别是这些类中包含了指向大量数据的指针。
- 用于标准库中的容器,如
std::vector,它们在进行重新分配或调整大小时,可以利用移动语义优化性能。
- 从函数中返回大型对象时,利用移动语义避免复制。
注意事项
- 移动后的状态:
- 移动操作后,需要确保源对象处于有效但未定义的状态。
- 需要确保移动后的源对象在其生命周期内处于安全可用的状态,或直接销毁。
- 选择使用移动还是复制:
- 编译器在某些情况下可能会优化掉一些看似必要的复制操作,这称为返回值优化(RVO)。
- 因此,不是所有情况都必须使用移动语义。
RVO
RVO 是编译器的一种优化技术,可以消除函数返回对象时的临时对象,直接在调用位置构造返回值,从而避免了移动和复制构造函数的调用。
代码示例
nullptr关键字
作用
- 类型安全:
nullptr是std::nullptr_t类型的唯一值,可以避免与整数之间的隐式转换,提高代码的安全性。
- 清晰的语义:与
NULL(通常被定义为0或((void*)0))相比,nullptr明确表示空指针,使得代码的意图更加清晰。
使用场景
- 初始化指针:使用
nullptr初始化指针,明确表示指针为未指向任何对象。
- 函数重载解析:在涉及函数重载的场景中,
nullptr可以帮助编译器准确选择接受指针参数的版本。
- 与旧代码的兼容:
nullptr可以与旧式 C++ 代码或 C 代码互操作,替代之前使用的NULL或0。
注意事项
- 避免混用
NULL和nullptr:为了保持代码的一致性和清晰性,推荐在 C++11 及以后的代码中统一使用nullptr。
- 检查库函数或 API 文档:在使用第三方库或 API 时,确认其对
nullptr的支持情况,以避免兼容性问题。
代码示例
指针初始化和检查:
函数重载:
委托构造函数
允许一个构造函数在同一个类中调用另一个构造函数,从而避免初始化代码的重复。
作用
- 减少构造函数间的代码冗余,提高代码复用性。
- 可以集中处理共同的初始化逻辑,使得构造函数更加简洁明了。
使用场景
- 当类的多个构造函数有共同的初始化操作时,可以通过委托减少重复代码。
- 当需要在构造对象前进行某些检查或设置默认值时,委托构造函数可以作为主要的逻辑入口。
注意事项
- 委托构造函数必须在构造函数的初始化列表中直接调用目标构造函数。
- 委托链不能形成闭环,即一个构造函数不能直接或间接地委托给自己。
- 在委托构造函数中使用
this指针需要小心,因为对象的某些部分可能还没有初始化。
代码示例
强类型枚举
- 强类型枚举(枚举类或
enum class),是对传统枚举(enum)的一种增强,提供了更强的类型安全和作用域控制。
- 强类型枚举解决了传统枚举类型的一些常见问题,如枚举值污染命名空间、隐式转换到整数类型等。
作用
- 类型安全:强类型枚举不会隐式转换为其他类型,这可以避免许多由隐式类型转换导致的错误。
- 作用域控制:强类型枚举的成员不会自动导入到外层作用域中,必须使用枚举类型名访问。
- 自定义底层类型:可以显式指定强类型枚举的底层类型,控制其大小和性能。
使用场景
- 当需要定义一组固定的常量值时,使用强类型枚举比使用宏或传统枚举更合适。
- 在实现状态机、错误码、配置选项等需要明确区分的场景中,使用强类型枚举可以提供更好的类型安全。
注意事项
- 强类型枚举需要使用枚举类名加作用域解析运算符
::来访问成员。
- 强类型枚举不会自动转换为整数类型,如果需要进行转换,必须使用显式类型转换。
- 在定义强类型枚举时,可以选择底层类型来优化内存使用和性能。
代码示例
静态断言(static_assert)
允许在编译时进行条件检查,如果条件不满足,则产生编译错误。
作用与优势
- 编译时检查:静态断言的检查是在编译时进行的,如果断言失败,则编译不通过,这有助于尽早发现错误。
- 无运行时成本:静态断言不会增加程序的运行时开销,因为所有检查都在编译期完成。
- 提高代码安全性:通过确保编译时的条件满足,静态断言提高了代码的可靠性和安全性。
- 改善代码文档性:静态断言可作为代码自我文档的一部分,说明代码设计时的假设和要求。
使用场景
- 检查模板类型的属性:例如,确保模板类型支持某种操作,或者具有特定的特性。
- 验证常量表达式:检查表达式或常量是否满足特定条件。
- 强制编码规范:例如,强制要求某些类型的大小或对齐方式满足特定需求。
注意事项
- 静态断言的条件必须是可以在编译时求值的常量表达式。
- 静态断言的失败信息需要是一个编译时确定的字符串常量。
代码示例
thread_local
使用
thread_local,可以为每个线程创建变量的单独实例,每个线程都有自己的变量副本,这些变量在线程开始时创建,在线程结束时销毁。作用与优势
- 线程安全:每个线程都有自己的变量副本,从而避免了对共享数据的并发访问,减少了使用互斥量等同步机制的需要。
- 保持状态:可以用于在多个函数调用中保持线程的状态,无需显式地通过函数参数传递。
- 性能优化:减少了对互斥量的依赖,有助于提高多线程程序的性能。
使用场景
- 统计和计量:在多线程环境中,对函数调用次数、资源使用情况等进行独立统计。
- 线程特定数据:例如,存储随机数生成器的种子,使每个线程生成不同的随机数序列。
- 避免全局变量带来的问题:在设计需要全局访问,但又涉及到线程安全问题的场景时,
thread_local提供了一种解决方案。
注意事项
- 资源消耗:每个线程都有自己的副本,可能会增加程序的内存使用。
- 初始化和销毁:
thread_local变量的生命周期从线程开始到线程结束,需要注意构造和析构函数的调用可能带来的性能影响。
- 与静态和全局变量的区别:静态和全局变量为所有线程共享,而
thread_local变量为每个线程单独存储。
代码示例
异步编程
异步编程的特性定义,主要集中在
<future>, <thread>, 和 <mutex> 等头文件中。std::async
std::async函数可以用来启动一个异步任务,并返回一个std::future对象,该对象在未来某个时间点持有异步操作的结果。
- 使用
std::async可以简化线程的使用,无需直接管理线程的生命周期。
std::future
std::future提供了一种访问异步操作结果的机制,它可以在将来某个时刻获取异步操作的结果。
std::promise
std::promise对象可以与一个std::future对象相关联。
- 可以在另一个线程中通过关联的
std::future对象来获取结果。
std::thread
std::thread表示一个单独的线程,可以用来执行任何可调用的任务(函数、Lambda 表达式、绑定表达式等)。
std::thread提供了底层的线程操作,包括启动、同步(join/detach)、和线程的管理。
线程同步(互斥锁和条件变量)
std::mutex、std::recursive_mutex、std::lock_guard、std::unique_lock等用于同步线程访问共享数据。
std::condition_variable用来阻塞一个或多个线程,直到某个条件被满足。
使用场景
- 并行数据处理:使用异步操作处理数据集合,每个数据项或数据块由一个独立的线程处理。
- 后台任务执行:例如,后台进行文件的读取或写入,而不阻塞主线程。
- 事件驱动编程:异步处理外部事件(如网络请求或用户输入)。
注意事项
- 异常处理:在异步任务中抛出的异常会被存储在
std::future中,需要在调用future.get()时处理这些异常。
- 资源管理:确保异步任务中使用的资源在任务完成前不会被销毁。
- 死锁避免:使用多个互斥锁时要特别小心,避免死锁。
代码示例
使用std::async与std::future来执行异步任务并获取结果:
使用std::promise和std::future交换数据:
互斥锁和条件变量:
- 互斥锁(Mutex)用于保护共享数据,避免同时访问导致数据竞态。
- 条件变量用于线程间的同步,允许线程在某些条件未满足时阻塞等待。
元组类型(std::tuple)
允许将不同类型的值组合成单一对象。
作用与优势
- 组合多种数据类型:元组可以包含多种不同类型的数据项,这在传统的数组或结构体中不易实现。
- 函数多返回值:使用元组可以从函数返回多个值,而无需使用出参或封装成结构体/类。
- 泛型编程:元组支持泛型操作,可用于模板编程中处理不同类型的数据集合。
- 与标准库算法集成:元组可与标准库中的其他容器和算法无缝集成,如与
std::tie结合使用,可以非常方便地解包元组内容。
使用场景
- 多值返回:在需要从函数返回多个数据项时使用。
- 数据库查询:返回多列数据。
- 参数包:在模板元编程中用作接收和传递多个参数的容器。
注意事项
- 性能考虑:虽然元组非常灵活,但其创建和访问可能涉及类型推导和多次复制,需要注意性能影响。
- 代码可读性:元组使得代码更加紧凑,但也可能降低代码的直观性和可读性,特别是在元组用得过多或过于复杂时。
代码示例
创建和访问元组:
元组与函数多值返回:
正则表达式库
提供了对正则表达式的全面支持,定义在
<regex> 头文件中。作用与优势
- 模式匹配:允许定义复杂的模式,用于匹配字符串中的特定序列。
- 搜索和替换:提供功能强大的搜索和替换机制,用于处理文本数据。
- 数据验证:可以用于验证输入数据格式,如电子邮件地址、电话号码等。
- 分组捕获:支持捕获括号中的子表达式,方便数据提取和处理。
使用场景
- 数据解析:从复杂的文本输入中提取特定信息。
- 数据验证:检查输入数据是否符合预定义的格式。
- 复杂的字符串操作:进行复杂的查找、替换或数据抽取操作。
- 编程竞赛和算法:在需要快速解决复杂文本问题的环境中,正则表达式提供了一种快速的解决方案。
注意事项
- 性能:正则表达式的执行可能比简单的字符串操作慢,特别是对于复杂的模式。
- 复杂性:正则表达式语法本身可能很难理解和维护,尤其是对于复杂的模式。
- 调试困难:调试复杂的正则表达式可能比较困难,因为错误可能不容易被发现。
代码示例
基本的正则表达式匹配:
使用正则表达式进行数据验证:
类型特性与类型工具(type_traits)
- 通过标准库中的
<type_traits>头文件提供。
- 类型特性是模板元编程中的一种机制,允许在编译时查询和操作类型信息。
- 使得能够编写更通用、更健壮的模板代码。
作用与优势
- 类型检查:能够在编译时检查类型属性,如是否是整数类型、是否具有默认构造函数等。
- 条件编译:基于类型特性决定代码路径,例如选择不同的实现方案。
- 增强泛型编程:提供了更精细的控制,可以根据类型的具体特性调整模板行为。
- 无运行时开销:所有检查和操作都在编译时完成,不影响运行时性能。
使用场景
- 模板特化:基于类型属性实现特化,处理不同类型时具有不同行为。
- 编译时断言:验证类型属性,确保模板的正确使用。
- 优化模板实现:根据类型特性选择最优的算法或数据结构。
- 类型转换和管理:用于类型转换决策和安全的类型管理。
注意事项
- 复杂性增加:使用类型特性可能会使模板代码的理解和维护更加复杂。
- 编译时间:大量使用类型特性可能会增加编译时间。
代码示例
基本类型特性的使用:
使用类型特性进行条件编译
std::function
- 提供了一种通用、类型安全的方式来引用任何可调用的目标,如普通函数、Lambda 表达式、函数对象(functors)、以及其他函数指针。
- 这个功能定义在
<functional>头文件中。
作用与优势
- 类型安全的可调用对象封装:
std::function封装了任何具有特定签名的可调用实体。
- 灵活性和通用性:可以存储和调用任何返回类型和参数列表匹配的函数或可调用对象。
- 易用的接口:提供了统一的调用方式,简化了函数和其他可调用对象的管理和使用。
- 配合标准库使用:与
std::bind和std::thread等标准库功能配合得非常好,使得函数调度和异步操作更加方便。
使用场景
- 回调机制:在需要传递回调函数的编程场景中,
std::function允许动态地指定要调用的函数。
- 事件处理系统:用于事件驱动的编程,可以根据事件类型动态地绑定处理函数。
- 线程编程:可以用于定义线程执行的任务。
- 函数表:实现函数的查找表,如解释器中的指令集映射。
注意事项
- 性能开销:与直接调用函数相比,使用
std::function会有额外的开销,因为它需要管理底层的可调用对象的内存和类型安全的调用机制。
- 内存使用:
std::function可能会使用动态内存分配来存储可调用对象,特别是当封装的可调用对象比较大时。
代码示例
存储和调用不同类型的可调用对象:
使用std::function作为事件处理器:
std::bind
- 位于
<functional>头文件中。
- 提供了一种灵活的方式来创建新的可调用对象,通过绑定参数到现有的函数或函数对象(包括成员函数)。
作用与优势
- 参数预设:允许你提前指定一个或多个参数的值,返回一个新的可调用对象,这些参数在实际调用时自动使用。
- 参数重排序:可以改变参数的顺序,适应不同的调用需求。
- 成员函数适配:可以绑定成员函数,使其像普通函数一样使用,无需显式传递对象实例。
- 与
std::function兼容:std::bind生成的可调用对象可以存储在std::function中,便于管理和使用。
使用场景
- 事件处理和回调:预设回调函数的某些参数,使得接口更简洁。
- 线程函数调用:在创建线程时,可以绑定特定的参数到线程函数。
- 适配器模式:调整函数接口以适应不同的调用上下文。
- 延迟计算:创建可延后执行的函数绑定,等到需要时再调用。
注意事项
- 生命周期管理:当
std::bind绑定了对象的引用或指针时,需要确保绑定的对象在可调用对象执行期间仍然有效。
- 性能开销:
std::bind的使用可能引入额外的间接调用和对象创建开销。
- 复杂性和可读性:过度使用
std::bind可能导致代码难以理解和维护。
代码示例
基本的std::bind使用:
使用std::bind绑定成员函数:
参考\说明\推荐
文章主要内容由GPT帮忙整理,GPT确实是个好帮手!
- Author:CoderWdd
- URL:https://www.wuinsights.top//article/01HWKTZ8NZA8DK6THR673GGKFJ
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts