C++读书笔记

超精简Effective Modern C++ 第三章 转向现代C++

条款7.在创建对象时区分()和{}

在大括号语法存在之前,不同的初始化方式有着不同的用处

在Class初始化中,无法使用小括号初始化:

//class中初始化
class Widget{

private:
	int x{0}; //可行
	int y = 0;//可行
	int z(0); //错误!
}

而在不可复制对象中:

std::atomic<int> ai1{0}; //可行
std::atomic<int> ai1 = 0;//不可行
std::atomic<int> ai1(0); //可行

大括号与小括号各自的优缺点

小括号还存在一个很蛋疼的解析语法歧义的问题

// 如果Widget存在该构造函数,则是构造函数,否则会被认为是一个函数声明
Widget w1();
//避免方式
Widget w1({});

虽然大括号可以避免上述问题,统一了初始化方式,并且避免了类型窄化,但是就像之前提到过的,与auto并不相合。

初始化列表构造函数的问题

初始化列表构造函数会覆盖所有可以进行转换的构造函数。

例如vector<int>的构造函数

vector<int> a(10, 5)
vector<int> a{10, 5}
//会有完全不同的结果

模板编程是大括号还是小括号

这是一个令人烦恼的问题,当然实际上也有方法做成弹性的设计,让用户选择用什么来初始化。

Intuitive interface – Part I

std::make_uniquestd::mk_shared使用了小括号初始化并且广而告之。

条款8.优先使用nullptr而不是0或者NULL

0 实际上是int型,而 nullptr 才是真正单独表示空指针的关键字。

如果使用重载的话会出现问题。

条款9.优先选用别名声明而不是typedef

比较一下两者:

typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS;
using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;

从功能上简单看是一样的。

但是using有以下优势:

  • 支持模板化
template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;

MyAllocList<Widget> lw;

//老式实现
template<typename T>
struct MyAllocList{
	typedef std::list<T, MyAlloc<T>> type;
}

MyAllocList<Widget>::type lw;
  • 函数指针更易读
typedef void (*FP)(int, const std::string&);
using FP = void (*)(int, const std::string&);

条款10.优先使用带作用域的enum而不是不限作用域的enum

enum Color {black, white, red};

auto white = false;//错误 white已经被定义

enum class Color {black, white, red};

auto white = false; //正确

不限作用域的enum:

  • 污染变量名
  • 可以和数字比较,这并不合理
  • 无法前置声明
  • 无法指定使用何种底层类型,如果途中发生变化,可能需要重新编译整个工程
  • 可以在tuple获取具体位置的时候作为标识符,但是可以写一个方法对限定enum进行转换也可以实现

条款12.为需要重写的函数加上override

避免重写错误导致的问题。

如果重载需要生效,一系列的满足需要满足:

  • 基类函数是虚函数
  • 函数名必须相同(除了析构函数)
  • 函数形参必须相同
  • 函数常量性必须相同
  • 函数返回值与异常规格必须相同
  • 函数的函数引用饰词必须相同

只要不满足以上条件,编译就无法通过,而不是产生新的虚函数。

什么是引用饰词

class Widget{

public:
	void doWork() &; //*this为左值,调用时才会使用
	void doWork() &&;//*this为右值,调用时才会使用
}

//使用实例
class Widget{

public:
	using DataType = std::vector<int>;

	DataType& data() &
	{return dataValues;}//当左值调用时使用复制
	
	DataType& data() &&
	{return std::move(dataValue);}//右值调用的时候采用移动语义

private:
	DataType dataValues;
}

auto vals1 = w.data();//左值,直接使用复制
auto vals2 = makeWidgets().data();//右值,使用移动语义避免复制

条款11.优先选用delete删除函数,而不是private

  • private只是对delete的一种模拟,优先使用delete。
  • delete不仅可以删除函数,也可以对模板和重载起效,禁用某些特化和重载。
bool isLucky(int number);

bool isLucky(char number) = delete;

bool isLucky(double number) = delete;

bool isLucky(bool) = delete;

if(isLuck('a'))//报错
//...

template<typename T>
void processNumber(T* ptr);

template<>
void processNumber<void*>(void*) = delete;//无法使用void*的特化

条款13.优先使用const_iterator而不是iterator

auto it = std::find(values.cbegin(),value.cend(), 1983);
value.insert(it, 1998);

在C++14中你可以用非成员函数版本

auto it = std::find(cbegin(values),cend(value), 1983);
value.insert(it, 1998);

条款14.只要函数不会发射异常就为其加上noexcept

int f(int x) throw(); //C++98写法
int f(int x) noexcept;//C++11写法

所有的内存释放函数与析构函数都自带noexcept。

带有noexcept的函数意味着更多潜在的编译优化。

条款15.只要有可能使用constexpr就使用它

  • 表达式可以在模板中使用了
  • 函数也以作为constexpr
constexpr int pow(int base, int exp) noexcept
{
	...
}

constexpr int numCond = 5;

std::Array<int, pow(3, numCond)> result;
  • 构造函数也可以是constexpr,这意味着可以在常量区初始化自定义类
class Point
{
	public:
	constexpr Point(double xVal = 0, double yVal = 0) noexcept : x(xVal), y(yVal){}
	
	constexpr double xValue() const noexcept { return x; }
	constexpr double yValue() const noexcept { return y; }
}

constexpr Point p1(1.0, 2.0);//可以在编译时运行

constexpr Point midPoint(const Point& p1, const Point& p2) noexcept {
	return { (p1.xValue() + p2.xValue()) / 2, (p1.yValue() + p2.yValue()) / 2};
}

constexpr Point mid = midPoint(p1, p2);//编译时运行

使用constexpr意味着更快的速度

条款16.保证const成员函数的线程安全性

除非确信不会使用在并发环境中,否则必须保证const成员函数的安全性

使用std::atomic的变量会比用互斥量有更好的性能,但是前者只适用于单个变量或内存区域

条款17.理解特种函数的生成机制

C++11添加了两个新的特种函数

  • 移动构造函数
  • 移动复制函数

大三律

当你声明了复制构造函数、复制赋值函数、析构函数中的任意一个,你就需要同时声明这三者。

这个产生于这样的思想:

  • 在复制操作中进行任何的资源管理,也极有可能在另一个复制操作中进行
  • 该类的析构函数也会参与到资源管理中

所以STL所有容器都遵循大三律。

C++11中有这样一个规定,只要用户声明了析构函数,就不会生成移动操作。

所以移动操作要生成就必须满足三个条件:

  • 未声明复制操作
  • 未声明移动操作
  • 未声明析构操作

如果发现无需额外实现,则可以使用 default 操作符。

如果用了析构操作,可以用 default 声明来重新恢复移动操作。

需要特别注意析构的影响,以前的移动操作会被改变为复制操作,带来可观的性能影响。

总结

  • 默认构造函数:与C++98相同。只有不含用户声明的构造函数才会生成。
  • 析构函数:与C++98基本相同。唯一区别是C++11默认为noexcept。仅基类中为虚函数时,析构函数才是虚函数
  • 复制赋值运算符:运行时行为与C++98相同。只有不包含用户的复制赋值运算符声明时才生成。如果该类声明了移动操作则会删除复制操作。如果存在复制构造函数或者析构函数的条件下仍然生成复制赋值运算符的规则已经废弃。
  • 移动构造和移动赋值运算符:按照成员进行非静态数据的移动。只有在用户未声明复制操作、移动操作、析构函数时才生成。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注