1. 条款7:在创建对象时注意区分()和{}
1️⃣眼花缭乱:(以下都没有赋值!!!!)
int a(5);
int a = 5;
int a{5};
int a = {5} //编译器会将其等同于上者
而对于自定义对象,以上更加不是学术问题,而是实际不同:
A a; //默认构造
A b = a; //拷贝构造
b = a; //拷贝赋值
为了解决众多初始化语法带来的困惑,C++11引入了统一初始化,基础是大括号形式:
std::vector<int> L{1, 3, 5};
2️⃣大括号可以用来为非静态成员指定默认初始化值,也可使用=
,但不能使用()
:
class A
{
private:
int x{0};
int y = 0;
int z(0); //error!!!!
};
对于不可复制的对象,则是=
不行,其他可以。这些说明了为什么{}
是统一初始化。
3️⃣大括号初始化,禁止进行隐式窄化型别转化:(这种转化,意味着精度丢失)
double x,y,z;
int a(x + y + z);
int b{x + y + z};//error!!!!
它还对解析语法免疫:
Widget w2(); //可能会被解析成一个函数,而不是进行默认初始化。
Widget w2{}; //无问题
4️⃣{}
的问题除了auto
的推导问题,还有就是其太霸道:只要有任何可能,大括号初始化物就会与带有std::initialized_list
型别的形参相匹配,即使其他重载版本有着貌似更加匹配的形参表。具体讨论见书 P58。
2. 条款8:优先选用nullptr,而非0或NULL
1️⃣0会NULL
都不具备指针型别(0是int
,NULL一般是long
)。而nullptr可以隐式转换到所有的裸指针型别,且不具备整型型别。
3. 条款9:优先选用别名声明,而非typedef
using List_int = std::vector<int>;
1️⃣优越的压倒性理由在于:别名声明可以模板化。
template<typename T>
using AllocList = std::list<T, Mylloc<T>>;
而且这种模板化可以避免依赖型别,而带有依赖型别,则必须在前面加个typename
。
或者说,别名模板可以让人免写::type
后缀,并且在模板内,对于内嵌typedef
的引用经常要求加上typename
前缀。
2️⃣<type_trait>
的类型特征模板:
std::remove_const<T>::type //由const T生成T
std::remove_reference<T>::type //由T&,T&&生成T
std::add_lvalue_reference<T>::type //由T生成T&
4. 条款10:优先选定限定作用域的枚举型别,而非不限定的
1️⃣第一个理由在于,不限类型的枚举(C++98风格)会泄漏到其所在作用域:
//不限定
enum Color {red, blue};
auto red = 1; //error!!!
----------------------------------------
//限定作用域
enum class Color {red, blue};
auto red = Color::red; //right!!!!
2️⃣第二个压倒性理由是:限定枚举是强型别的(也称为枚举类
),不能进行隐式类型转换,而非限定的,可能在使用时,隐式转换成整数,然后进一步转换成浮点数类型。(如果实在想要进行,则进行强制类型转换)。
3️⃣都支持默认底层类型指定,限定作用域的枚举的底层类型是int
,而不限定则没有默认底层类型。如果想要修改,两者都可以通过如下方式:
enum Color : std::size_t;
4️⃣关于限定枚举的一个缺点以及解决方法,还有更多详细信息,见书 P72。
5. 条款11:优先选用删除函数,而非private
未定义函数
1️⃣如果定义了某个函数,却不希望用户去调用(常见的如:拷贝构造函数、拷贝赋值函数),旧有做法是,将其声明为private
,然后不去定义它。
2️⃣新方法是使用删除函数,在其末尾加一个= delete
。相对于private
只能定义成员函数,删除函数可以应用在所有函数上。
3️⃣还有一个好处是:阻止那些不应该进行的模板具现。举个例子:指针世界有两个个例,void*
和char*
,因为各自的原因,和其他指针的处理方法不一样,那么就需要考虑如下模板的具现问题:
template<typename T>
void processPtr(T* ptr);
...
template<>
void processPtr(void*) = delete;
template<>
void processPtr(char*) = delete;
所以,总结一句话:请始终使用= delete
。
6. 条款12:为意在改写的函数添加override
声明
科普向,之前还不知道啥是墨菲定律,现在知道了:只要一件事可能出错,那么它一定会出错。
引用饰词
1️⃣虚函数的改写的发生条件如下:
-
基类的函数必须是虚函数;基类和派生类的函数名字、形参、函数常量性必须相同。
-
函数返回值和异常规格必须兼容。
-
C++11又加了一条:函数引用饰词必须完全相同。这个又是干嘛的?他们是为了限制成员函数仅用于左值或右值:
class A{ public: void Func()&; void Func()&&; }; ... A a; A.Func(); //调用Func()&,左值版本 MakeA().Func(); //调用右值版本。
2️⃣正因为条件这么苛刻,所以很容易就导致没有重写,所以我们需要override
来显式表明。
7. 条款13:优先选用const_iterator
具体分析见书。
8. 条款14:只要函数不会抛出异常,就为其加上noexcept
声明
具体分析见书P 91。
1️⃣总结:
noexcept
声明是函数接口的组成部分,这意味着调用方可能会对它有依赖。- 相对于不带
noexcept
的函数,带有的函数有更多机会得到优化。 noexcept
性质对于移动操作、swap、内存释放函数和析构函数最有价值。- 大多数函数都是异常中立,不具备
noexcept
性质。
9. 条款15:只要有可能使用constexpr
,就使用它
1️⃣它很令人困惑。当它应用对象时,其实就是个加强版的const
,但应用于函数时,却有着相当不同的意义。表面上看,constexpr
表示的是这样的值:它不仅是const
,而且是编译时已知的。但应用在函数时却两者都不符合,这更是个不错的设计!
2️⃣对于对象,这是明确的:
constexpr auto a = 10;
int b[a]; //right!!!
3️⃣对于函数,我们可以总结:如果传入的参数都是constexpr
(编译时已知的),那么函数的返回值也会被认为是编译时已知的(constexpr
);否则和普通函数没有区别。
constexpr
int pow(int base,int exp) noexcept
{
...
}
...
constexpr auto num = 5;
std::array<int, pow(3, num)> results; //right!
4️⃣甚至可以创建编译时已知的对象(因为构造函数可以是constexpr
)。(具体见书)。规则和函数一样。
10. 条款16:保证const成员函数的线程安全性
1️⃣对于const
成员函数,理论上它不应该修改对象的数据,但是我们想想mutable
,它让const
对象的const
成员函数可以修改标记为mutable
的关键字。(当然这也是我们引入mutabel
的原因)。
2️⃣在多线程条件下,会产生数据竞争的问题。一个解决方法是加入互斥量mutex
:
class A{
void roots() const{
std::lock_guard<std::mutex> g(m);
...
}
...
mutable std::mutex m;
};
由于
mutex
是只移类别(move-only type
),所以加入它,类A会失去复制性,但依然有移动性。
3️⃣但使用互斥量有点杀鸡焉用牛刀的感觉,一个成本更低的方法是使用std::atomic
:
class A{
void roots()const{
...
callcount++;
...
}
...
mutable std::atomic<unsigned> callcount{0};
}
但这个对于多个线程访问量,表现很差。
4️⃣总结:使用std::atomic
型别的变量会比运用互斥量提供更好的性能,但前者仅适用对单个变量或内存区域的操作。
11. 条款17:理解特种成员函数的生成机制
1️⃣新的特种成员函数:移动赋值和移动构造函数(所以不用说那些老人
了吧)。这些函数一般是public
、inline
、且非虚的(除了当基类的析构函数是虚函数时,派生类自动生成虚析构函数)
对于那些不支持移动操作的
残余
,哪怕进行移动操作,实际上也是进行拷贝操作。
2️⃣两个拷贝操作是独立的,声明其中一个,不会影响编译器在我们需要时生成另外一个。但移动操作不是,例如:我们写了一个移动构造函数,那么编译器不会生成移动赋值函数。尤有进者,声明了拷贝操作,就不会生成移动操作。
我们自定义,则说明按成员复制不是我们想要的,我们可能在进行一些骚操作,而编译器咋知道,它可能会认为这个时候为我们生成移动操作,不是我们想要的结果(它可不会乘上一个矩阵,再移动或拷贝)。所以干脆不生成了。
这么一想,我们自定义移动,那么编译器也不会自动生成拷贝操作。
3️⃣大三律(具体见书)。总结来说,移动操作的生成条件如在:
- 该类未声明任何复制操作。
- 该类未声明任何移动操作。
- 该类未声明任何析构函数。
未来,这样的机制也会延生伸到拷贝操作。当然,如果我们需要,可以显示声明,在末尾加上=default
。
A(const A&) = default;
4️⃣成员函数模板在任何情况下都不会抑制特种成员函数的生成。