C++11:从C++98到C++11惯用法的变化

文章强调了在C++编程中使用auto关键字以避免类型错误和提高重构效率,选用nullptr以增强指针类型的安全性,以及通过using声明简化命名空间。同时提倡使用限定作用域的枚举类型以防止类型转换问题,并讨论了删除函数优于私有未定义函数的原因。此外,文章还提到了const_iterator的使用,noexcept声明在异常安全方面的意义,以及constexpr在编译期计算的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

优先使用auto,而非显式类型声明

  1. auto变量必须初始化,因此使用auto就可以避免未初始化变量带来的问题。并且基本上对会导致兼容性和效率问题的型别不匹配现象免疫,还可以简化重构流程。
  2. 使用auto声明变量在形式上更加简洁
  3. 使用auto可以避免使用未初始化变量的问题
  4. 使用auto可以避免写出错误的类型来
  5. 由于auto使用了类型推导,可以使用其直接保存只有编译期才知道的类型,如lambda类型
  6. 在C++14中,lambda表达式的形参可以使用auto代替
  7. 使用auto可以减少重构时的工作量
  8. 使用auto可以避免隐式类型转换,如size()的返回值问题,哈希表中key的常量性问题
  9. auto是一个可选项,如果经过判断后认为显式类别声明将会带来更加清晰的代码或者可维护性更高的代码,那么完全可以放弃使用auto
  10. 使用auto来接收lambda返回值相对于使用std::function接收lambda返回值的优势
    a. 使用auto在形式上更加简洁
    b. 使用auto会更加节省内存
    c. 使用auto会有更高的执行效率(可以内联,而间接的函数调用则不会内联)

当auto推导的类型不符合要求时,使用带显式类型的初始化物习惯用法

  1. 隐形的代理类型可能导致auto根据初始化表达式推导出错误的类型
  2. 带显式类型的初始化物习惯用法强制auto得到你想要的类型

多种多样的初始化形式

  1. 初始化物在小括号内int x(0)
    a.
  2. 初始化物在等号之后int x(0)
  3. 初始化物在大括号内int x{0}
    a. 应用场景广
    b. 禁止内建类型之间的隐式窄化类型转换
    c. 免疫C++令人苦恼的解析语法int x() 被解析为函数声明
    d. 缺陷在于伴随大括号初始化物、std::initializer_list以及构造函数重载决议之间的纠结关系
    e. 在构造函数被调用时,只要所有的构造函数中都没有带std::initializer_list的形参,那么小括号和大括号没有区别
    f. 在构造函数被调用时,如果有一个及以上构造函数具有std::initializer_list型别的形参,那么采用大括号初始化语法的调用语句会强烈优先选用带有std::initializer_list形参的重载版本
  4. 同时使用等号和大括号int x={0}
    a. 效果同3

优先选择nullptr,而非0或者NULL

  1. nullptr是一个真正的通用类型指针,而不是0或者void*(0)之类的东西
  2. nullptr不会产生重载决议问题
  3. 可以提高程序可读性
  4. 涉及到模板时,0或者NULL可能推导出错误的类型,而nullptr不会

优先选择using声明,而非typedef

  1. using声明要更好理解
  2. using声明对模板更加友好
  3. using声明可以让人避免写"::type"后缀,并且在模板内,对于内嵌typedef的引用经常要求加上typename前缀

优先选择限定作用域的枚举类型,而非不限定作用域的枚举类型

  1. 限定作用域的枚举类型可以降低名字空间污染的可能性
  2. 限定作用域的枚举类型时强类型的,不可以隐式转换到整形,可以强制类型转换到整形
  3. 一切枚举类型在C++中都会由编译器来选择一个整数类型作为其底层类型
  4. 枚举类型在C++98中会增加编译依赖性,原因无法进行前置声明,根本原因是编译器为了进行空间优化。
  5. 为什么C++11中的枚举类型可以进行前置声明,而C++98中不行呢?在C++11中,限定作用域的枚举类型和不限定作用域的枚举类型都支持底层类型指定,限定作用域的枚举类型的底层类型是已知的(默认为int),而对于不限范围的枚举类型,没有默认的底层类型,因此如果你想前置声明不限作用域的枚举类型,必须指定其底层类型
  6. 不限定作用域的枚举类型在需要与整形进行隐式类型转换的场景还是有一席之地的,使用限定作用域的枚举,虽然能获得以上优点,但是写法相对繁琐
  7. 限定作用域的枚举类型总是可以进行前置声明,而不限范围的枚举类型却只有在指定了默认底层类型的前提下才可以进行前置声明。

优先选择删除函数,而非私有未定义函数

  1. 删除函数无法通过任何方法调用,而私有未定义的方式在友元以及成员函数的调用情况下, 将会推迟到链接阶段才能报错。
  2. 声明删除函数时在public作用域下能够得到更加清晰地诊断信息
  3. 删除函数可以阻止不应该进行的模板具现
  4. 任何函数都能成为删除函数,但只有成员函数可以使用私有未定义的方式

为意在改写的函数添加override声明

  1. 成员函数的引用饰词使得对左值和右值对象的处理能够区分开来,即引用饰词也是函数重载的一个条件

函数重写条件

  1. 基类中的函数必须是虚函数
  2. 基类和派生类中的函数名一致(析构除外)
  3. 基类和派生类中函数的形参一致
  4. 基类和派生类中函数常量性一致
  5. 基类和派生类中的函数返回值和异常规格必须兼容
  6. 基类和派生类的函数引用饰词一致(C++11)

优先选择const_iterator,而非iterator

只要函数不会发射异常,就为其加上noexcept声明

  1. noexcept声明是函数接口的组成部分,这意味着调用方可能对它有依赖
  2. 相对于不带noexcept声明的函数,带有noexcept声明的函数有更多的机会得到优化
  3. 带有noexcept的函数有可能会将复制操作改为移动操作
  4. noexcept性质对于移动操作、swap、内存释放函数和析构函数最有价值
  5. 大多数函数都是异常中立的,不具备noexcept性质
  6. 一个函数是否抛出异常远比该函数抛出的是什么异常要重要的多,因此C++98的异常规格虽然依旧能够使用,但已经成为废弃特性了。在C++11中noexcept就是为不会发射异常的函数准备的
  7. 函数是否加上noexcept声明,试管接口设计。如果你明明知道一个函数不会发射异常却没有给他加上noexcept声明的话,这就是接口缺陷
  8. 在C++11中默认内存释放函数和所有析构函数都具备noexcept性质

只要有可能使用constexpr,就使用它

  1. constexpr对象都具备const属性,由编译期已知的值完成初始化,在编译器可以确定。被放置在只读内存中,可以被使用在C++中要求常量表达式的位置。
  2. const对象并未提供和constexpr一样的保证,因为const对象不一定经由编译期已知的值来初始化。
  3. 所有的constexpr对象都是const对象,但是并非所有的const对象都是constexpr对象。如果你想让编译器提供保证,让变量拥有一个值,用于要求编译期常量的语境,那么能达到这个目的的工具就是constexpr而非const
  4. constexpr函数不一定是const的,并且其值不一定在编译期已知。
  5. constexpr函数可以用在要求编译期常量的语境中,在这样的语境中,若你传给一个constexpr函数的实参是在编译期已知的,则结果也会在编译期间计算出来,如果任何一个实参在编译期未知,则你的代码将无法通过编译。
  6. 在调用constexpr函数时,若传入的实参有一个或多个在编译期未知,则他的运作方式和普通函数没有区别,即在运行期得到计算结果
  7. constexpr在C++11中,函数体不能多于一条可执行语句,在C++14中可以有多条
  8. constexpr函数仅限于传入和返回字面类型,即这样的类型能够持有编译期可以决议的值。在C++11中,所有内建类型除了void都符合这个条件,但用户自定义类型同样可能也是字面类型。在C++11中,constexpr函数都隐式地被声明为const的了,并且返回值不能是void,这两个限制在C++14中都被解除了
  9. 比起非constexpr对象或函数,constexpr对象和函数的使用场景更加广泛,并且可以提高运行效率
    10.constexpr作为对象和函数接口声明的一部分,必须保证其长期有效性

保证const成员函数的线程安全性

  1. 保证const成员函数的线程安全性,除非可以保证他们不会用在并发语境下
  2. 运用std::atomic变量往往比使用互斥量要有更好的性能,但是适用范围减小了,因此如果可以使用std::atomic替换到互斥锁,那么就应该立刻行动

理解特种成员函数生成时机

1 . 默认构造函数:与C++98的机制一样,只有当类中不包含用户声明的构造函数时才生成
2. 析构函数:与C++98基本一致,唯一的区别在于析构函数默认为noexcept,仅当基类的析构函数为虚函数时,派生类的析构函数才是虚函数
3. 赋值构造函数:运行期行为与C++98一致:按照成员进行非静态数据成员的赋值构造,仅当类中不包含用户声明的复制构造函数时才生成。如果该类声明了移动操作,则复制构造函数将被删除。在已经存在复制赋值运算符或者析构函数的条件下,仍然生成复制构造函数已经成为了被废弃的行为
4. 复制复制运算符:运行期行为与C++98一致,按照成员进行非静态数据成员的复制赋值。仅当类中不包含用户声明的复制赋值运算符时才成立。如果该类声明了移动操作,则复制赋值函数将被删除。在已经存在复制构造函数或析构函数的条件下,仍然生成复制赋值运算符已经成为被废弃行为
5. 移动构造函数和移动赋值函数:都按照成员进行非静态数据成员的移动操作,仅当类中不包含用户声明的复制操作、移动操作和析构函数时才能生成
6. 成员函数模板不会阻止编译器生成任何特种成员函数
7. 如果类的使用依赖编译器自动生成的函数,那么强烈建议将该函数手动声明出来,如果默认实现可以满足需求,可以使用=default来表明。

针对可复制的形参,在移动成本低并且一定会被复制的前提下,考虑按值传递

  • C++11中按值传递,如果实参是一个左值将会被复制构造,如果实参是一个右值,他将会被移动构造
  • C++11并没有从根本上颠覆 C++98 关于按值传递的智慧。一般地,按值传递仍会导致一些你本想避免的性能损失,按值传递还会导致切片问题。C++11 引入的新特性是左值和右值的区别对待。欲实现函数以利用可复制右值型别的移动语义,就需要重载或者使用万能引用两者之一,但这两者都有一定的缺点。对于可复制的移动成本低的型别,并且传人的函数总是对其实施复制这种特殊情况,在切片问题也无须担心前提下,按值传递可以提供一个易于实现的替代方法,它和按引用传递的效率相近,但是避免了它们的不足。

考虑置入(emplace_back)而非插入(push_back)

  • 原理上讲,emplace_back会比push_back快,但是实际上不一定

什么情况置入肯定比插入快

  • 欲添加的值是以构造而非赋值的方式加入容器的
  • 传递的实参类型和容器持有之物的类型不同
  • 容器不太可能由于出现重复情况而拒绝待添加的新值

置入函数注意点

  • 向持有资源管理对象的容器添加元素(如果像调用插入函数那样调用置入函数,倒是也行,只不过效率上没有差别)
  • 置入函数是直接初始化,可以使用explicit的构造函数,而插入函数是复制初始化,不可以使用explicit的构造函数
  • 在使用置入函数时要注意保证传递了正确的参数,因为即使是带有explicit的构造函数也会被编译期纳入考虑范围
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值