编译时,-pthread and -lpthread的区别
gcc编译多线程代码时,参数不同导致结果的不同。
gcc编译多线程代码时,参数不同导致结果的不同。
C++中,引用为对象起了另外一个名字,引用类型refers to另外一种类型。引用和指针是不同的,可以汇编的角度来看引用。
传统的`typedef`机制允许对已存在的type提供synonym或者alias,被定义为新引入的alias的类型与被定义为原来类型的变量,完全一样,不会有一丁点行为上的差别,但是这种特性在某些场景下会有缺陷。
用倦Thindpad T420自带的键盘,也想尝试一下机械键盘的手感如何,就入手了一个机械键盘,KBC Poker 2。
入手Poker 2的原因不仅仅是Filco Minila Air和HHKB的价格有点高,还有一点吸引我的原因就是Poker 2支持全键位硬件可编程。
Poker 2已经是第二代了,相比起第一代,带来了许多的改进,网上的总结有这么几点:
- 保留了经典的US配列
- 增加了数控钢板,使得茶轴、青轴和红轴的手感大大增强,黑轴基本不变
- 内置7级亮度调节的DIY灯控支持,插上LED就会亮
- 全新加厚PBT键帽,带来目前最为细腻的PBT触感
- 增加了实用和进阶的附件六颗RGB加厚PBT套装和钢丝拔键器
- 换代的卫星轴进一步提升了手感,彻底让卫星轴翻身做主人
- 仍然保持一代的价格,加量完全不加价,499元
- USB任意6键无冲
- 自带延时编程的全键位硬件可编程
键盘从淘宝上购买,只是中通快递,等得我整个人都不好了。。。
除了键盘外,还随带附送的配件:分离式可拆卸的USB数据线,一个钢丝拔键器,一套RGB的PBT材质键帽。换下原来的键帽,把六个RGB键帽装上以后,就是这样了。
因为是60%尺寸的设计,省去了方向键和其他的一些功能键,改用Fn的组合键和Pn的编程键实现相应的功能,因此键盘更加玲珑小巧、方便携带。在文章的后面会给出自己修改键位的办法。
键帽由于是PBT材质的,而且还是加厚的键帽,手感很不错,空格键上的"Enjoy your feeling"正是说明了这一点。
但是细节处的做工就不是那么精细了,键帽边缘有少许的毛刺。但我不是强迫症,平时使用根本看不见,So it doesn't matter.
这次入手的Poker 2是茶轴的,网上茶轴的介绍如下:
全面兼顾:茶轴
茶轴的手感比较均衡,也可说是较为中庸,带有段落感,触发键程为2.0mm。另外,其压力克数比较小,只有60g,敲击显得非常轻松,能兼顾打字和游戏使用。
敲击茶轴的感觉如同小范围内的极速轻金属碰撞,很神秘的那么一下,结合了青轴和黑轴的特点,或者看成它是最没有特点的一种特殊轴,这也是手感最类似于传统键盘的机械键盘,压力在黑轴和青轴中间。
它的段落感,你按下一个键后段落感可以给你确认感,同时压力不太大,适合那种瞬间飚快捷键的快手。
由于没有使用过其他机械键盘,我只能和T420的键盘做对比。使用下来的感觉是,和T420的键盘相比,不需要多大的力量就可以触发,按下去的时候有段落感,触发段落感的力量也不大。用了Poker 2以后,感觉就是“回不去了。。。”。
USB数据线的接口,看着像micro USB的,但其实不是,有点像以前老式的好记星的接口。
背面有四个橡胶脚垫,不支持角度的调节,不过键盘正面的有弧度的设计,比较符合人体工程学,因此还是很舒适的。
这里就是DIP开关。最早见到这个DIP开关是在HHKB上,Poker 2的DIP开关与HHKB的还是有所区别的。记得当初关注HHKB并不是因为机械键盘的缘故,而是Caps和Control键可以互换,加上当时在Ubuntu下开发,所以一直眼馋HHKB,只是价格的原因,一直没入手。
Poker 2使用了小尺寸的设计,精简了多余的按键,加上分离式可拆卸的USB,大大提升了便携性。总体来说,做工中规中矩,PBT加厚键帽,内置钢板,但不足的是小细节的地方,比如键帽的边缘。
首先,用键盘编程在V键上,把Win键编程为:Pn+V。因为后面需要用开关把做Win改为左Fn,还有就是目前是在Windows下工作,Win键不可少,所以这里要提前设置Win的替代键位。
接下来,用键盘编程在L上,把Win+L键编程为:Pn+L;在,键上,把Win+Space编程为:Pn+,。其实这里不是必须的,我只是为了方便。
然后就是设置键盘后面的几个开关,背面的4个DIP开关的功能分别是:
将开关1和3都拨到ON的位置,效果:Caps变成了Fn,左Win变成了Caps。
到这里键位修改就完成了。
基类中类型相关的函数与派生类不做改变直接继承的函数是不同的,当希望派生类定义合适自身的版本,此时基类就将这些函数声明为虚函数。
基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作。
1 | class Quote { |
基类有两种成员函数,
派生类能访问公有成员和受保护的成员,但不能访问私有成员(继承了,但无权限访问)。
类派生列表中的访问说明是控制派生类从基类继承而来的成员是否对派生类的用户可见。如果一个派生类是公有的,则基类的公有成员也是派生类接口的组成部分。可以将公有派生类型的对象绑定到基类的引用或指针上。
1 | class BulkQuote : public Quote { |
派生类经常,但不总是,覆盖继承的虚函数。如果没有覆盖,则虚函数行为类似其他的普通成员,直接使用基类中的版本。
派生类必须对需要覆盖的重新声明,派生类可以在这样的函数前加上virtual
,但并非必须。C++11允许显式地在声明的最后用override
注明覆盖。
一个派生类对象中,继承自基类的部分和派生类的部分不一定是连续存储的。
由于派生类对象中含有与其基类对应的组成部分,因此能将派生类对象当成基类对象来使用,能将基类指针或引用绑定到派生类对象中基类的部分上。这叫做派生类到基类的类型转换。
由于一个基类对象可能是派生类对象的一部分,也可能不是,因此不存在从基类到派生类的自动类型转换,即使一个基类的指针或引用已经绑定在一个派生类对象上。
1 | Quote item; |
每个类控制它自己的成员初始化过程。派生类并不能直接初始化继承自基类的成员。初始化时,先初始化基类的部分,然后按照声明的顺序(与初始化列表顺序无关)依次初始化派生类的成员。
如果不明确指明初始化,派生类的基类部分会像数据成员一样执行默认初始化。
1 | BulkQuote::BulkQuote(const std::string& book, double p, std::size_t qty, |
每个类负责定义各自的接口,要想与类对象交互,必须使用该类的接口,即使这个对象是派生类的基类部分也是如此。因此应当调用基类的构造函数初始化从基类继承而来的成员。
派生类的作用域嵌套在基类的作用域之内。
如果基类定义了静态成员,则在整个继承体系中只存在该成员的唯一定义,每个静态成员只存在唯一的实例。
派生类的声明不能包含派生列表。
某个类要用作基类,必须已经定义,而非仅仅声明。因为派生类中,要使用继承自基类的成员,派生类必须知道他们是什么。
防止继承可以使用final
。
变量或表达式的静态类型:编译时总是已知的,变量声明时类型或表达式生成的类型。动态类型:变量或表达式表示的内存中对象的类型,运行时才可知。
基类指针或引用的静态类型可能与其动态类型不一致。
1 | double print_total(ostream &os, const Quote &item, size_t n) { |
上面item的静态类型是Quote&
,动态类型直到运行时调用函数时才会知道。如果传入的是BulkQuote,则动态类型是BulkQuote。
由于一个基类对象可能是派生类对象的一部分,也可能不是,因此不存在从基类到派生类的自动类型转换,即使一个基类的指针或引用已经绑定在一个派生类对象上。如果需要转换,
1 | BulkQuote bulk("awqef", 12, 23, 0.5); |
派生类类型到基类类型的转换是不存在的。但可以向基类的拷贝/移动操作传递一个派生类对象,实际运行构造/赋值的运算符将是基类中定义的,此时只能处理基类自己的成员,忽略派生类定义的成员,派生类的部分被slice down。
由于运行时才知道调用了哪个版本的虚函数,因此所有虚函数必须有定义。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那个。
动态绑定只有当通过指针或引用调用虚函数时才会发生,只有在这种情况下对象的静态类型才可能会与动态类型不同。
C++中的引用或指针的静态类型与动态类型不同,是C++支持多态性的根本所在。
当通过基类的指针或引用调用基类中的一个函数时,如果是虚函数,则运行时才会依据所绑定对象的真实类型(动态类型)来决定到底执行哪个版本。如果不是虚函数,则解析过程发生在编译时而非运行时。类似的,通过对象进行的函数调用(虚函数或非虚函数)也在编译时绑定。
一旦某个函数被声明为虚函数,则在所有派生类中都是虚函数。
一个派生类的函数如果覆盖类某个继承而来的虚函数,这它的形参类型必须与被它覆盖的基类函数完全一致。同时,返回值也必须相同。但有下述例外,
1 | class B { |
如果虚函数的返回类型是类本身的指针或引用时,返回值可以不同,但要求从D到B的类型转换是可访问的。
1 | class D : public Quote { |
为了避免出现上述的double net_price(double n) const
,可以使用override来说明派生类中的虚函数。
只有基类出现过的虚函数,且派生类中的函数声明与虚函数一致时,才能override,否则就会报错。 如果某个函数被指定为final,则之后的任何尝试覆盖该函数的操作都将引发错误。
final和override出现在形参列表(包括const和引用修饰符 )和位置返回类型**之后。
如果某次函数调用使用了默认实参,则该实参值有本次调用的静态类型(基类中定义默认实参)决定。
有时需要强制执行虚函数某个特定的版本,而不进行动态绑定。这时可以使用作用域运算符,
1 | double undiscounted = baseP->Quote::net_price(42); |
调用将在编译时完成解析。
通常只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数机制。 下面的调用中,派生类的虚函数调用了基类的版本,如果不回避虚函数机制,那么将会导致无限递归。
1 | double D::net_price(double n) const { |
一个纯虚函数无须定义,其中=0
只能出现在类内部的虚函数声明的语句处。但也可以为纯虚函数提供定义,不过函数体必须定义在类外部。
含有(或者未经覆盖直接继承)的纯虚函数的类似抽象基类。
派生类构造函数只能初始化它的直接基类。
1 | class BulkQuote1 : public DiscQuote { |
BulkQuote1包含三个子对象:空的BulkQuote1,DiscQuote和Quote。
派生类的成员或友元只能通过派生类对象来访问基类受保护成员(只能访问派生类对象中的基类部分的受保护成员),派生类对于一个普通的基类对象中的受保护成员没有任何访问特权。
派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没有什么影响,这是由基类中的访问说明符决定的。
派生访问说明符控制了派生类用户对于基类成员的访问权限。
1 | class B { |
继承自派生类(上面的PrivD)的新类,基类(B)成员的访问权限由派生类(PrivD)的访问说明符决定。即如果是private的,那么新类人不能访问基类(B)的成员。
友元不能传递和继承。
可以用using将类的直接或间接基类中任何可访问成员(非私有)标记出来。
1 | class B { |
每个类都会定义自己的作用域。 Outside the class scope, ordinary data and function members may be accessed only through an object, a reference, or a pointer using a member access operator (§ 4.6, p.150). We access type members from the class using the scope operator. In either case, the name that follows the operator must be a member of the associated class.
一旦遇到类名,定义的剩余部分就在类的作用域之内(参数列表和函数体)。返回类型中使用的名字都位于类的作用域之外。
类的定义分作,
这种两阶段处理的方式只适用于成员函数中使用的名字,声明中使用的名字(包括返回值或参数列表中使用的名字),都必须确保在使用前可见。
要注意的是,如果成员使用了外层作用域中的某个名字,且该名字代表一种类型,这内层作用域不能重定义这个名字。
1 | typedef double M; |
成员函数中使用的名字查找顺序,
派生类的作用域嵌套在基类的作用域之内,如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找改名字的定义。
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。
1 | class DiscQuote : public Quote { |
这与动态绑定不同。
如果派生类重用定义在其直接基类或间接基类中的名字,则定义在内层作用域(派生类)的名字将隐藏定义在外层作用域(基类)中的名字。
但可以使用作用域运算符覆盖原有的查找规则。一般来说,除了覆盖继承而来的虚函数外,派生类最好不要重用其他定义在基类中的名字。
查找顺序,
声明在内层作用域的函数并不会重载声明在外层作用域的函数,派生类中的函数也不会重载基类中的成员,而是隐藏基类的成员,即使它们的形参列表不一致。
假设形参列表不同,会发生隐藏,进而无法通过基类的引用或指针调用派生类的虚函数。
对应于之前提到的两个情况,
成员函数无论是否是虚函数都可被重载,派生类可以覆盖基类中重载函数的0个或多个实例。如果派生类希望所有的重载版本对于它来说都是可见的,那么派生类就必须覆盖所有的版本,或者一个也不覆盖。(如果只覆盖部分,此时会隐藏基类的重载函数)
简便的方法是using + 名字
(不需要形参列表)。需要保证基类函数的每个实例在派生类中都是可访问的。
1 | class B { |
如果一个类(基类或派生类)没有定义拷贝控制操作,则编译器会为它合成一个版本。
由于基类的引用或指针指向继承体系中的某个类型,有可能出现指针的静态类型与被删除对象的动态类型不符的情况。因此需要将析构函数定义为虚函数,以确保执行正确的析构函数版本。
如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为(只释放了基类部分的内存)。
基类的析构函数并不需要遵循三/五法则。虚析构函数(即使通过default的形式)将阻止合成移动操作。
合成拷贝控制成员的行为与其他合成的构造函数、赋值运算符或析构函数类似,它们对类本身的成员依次进行初始化、赋值或销毁。这些合成的成员还负责使用基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁。
无论是基类的合成版本还是自定义版本,都有上述的行为,唯一的要求是新颖的成员是可访问的,且不是一个被删除的函数。
1 | class B { |
由于虚析构函数的存在,编译器不会合成移动操作,因此所有左值和右值的情况(即b = b
,b = std::move(b)
,B b1(b)
和B b2(std::move(b))
)都将用拷贝操作处理。又因为拷贝构造函数是删除的,因此两个对拷贝构造函数的调用是错误的。
如果基类没有默认、拷贝或移动构造函数,则一般派生类也不会定义(可以,但必须考虑如何处理基类部分的成员)。
由于基类虚析构函数的存在,编译器不会合成移动操作。如果需要移动操作,则应该首先在基类中定义,与此同时必须显式地定义拷贝操作(否则会默认被定义为删除)。
派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。而析构函数只负责销毁派生类自己分配的资源,派生类对象的基类部分是自动销毁的。
定义派生类的拷贝或移动构造函数时,通常使用对应基类的构造函数处理对象的基类部分。
如果未处理使用基类的拷贝或移动构造函数,则基类部分被默认初始化。
在析构函数执行完成以后,对象的成员会被隐式销毁,对象的基类部分也是隐式销毁的(基类的析构函数被自动调用执行),派生类只负责销毁由派生类自己分配的资源。
对象的销毁顺序与创建顺序相反,派生类的析构函数先执行。
在构造或析构派生类对象的过程中,(从基类部分开始构造,派生类部分开始销毁)对象的类型就像是发生了改变一样。当前的构造函数或析构函数不能够调用未构造或已销毁的派生类版本的虚函数(可能会访问派生类部分的成员)。
如果构造函数或析构函数调用了某个虚函数,则应该执行与构造函数或析构函数所属类型相对应的虚函数版本。
1 | struct Erdos { |
上述代码中,并非是构造函数发生了继承。
这里的构造函数并非以常规的方式继承而来,且类不能继承默认、拷贝和移动构造函数。如果派生类含有自己的数据成员,则它们被默认初始化。
1 | class BulkQuote2 : public DiscQuote { |
上面的的using并不是令DiscQuote的构造函数在这里可见,而是令编译器生成一个与DiscQuote的构造函数对应的派生类构造函数,即
1 | BulkQuote2(const std::string& book, double price, std::size_t qty, |
注意: * 构造函数的using声明并不会改变该构造函数的访问级别 * 一个using声明不能指定explicit或constexpr,基类的是什么,继承后的也是什么 * 当一个基类构造函数含有默认实参,这些实参并不会被继承,而是派生类会获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。
1 | class C { |
由于上面的BulkQuote2只有继承的构造函数,因此编译器会合成一个默认构造函数,故BulkQuote2 b
正确。
1 | vector<shared_ptr<Quote>> vq; |
直接使用vector存储Quote对象是不行的,因为会使用Quote的拷贝构造函数,派生类对象的派生类部分会被截断。
派生类的智能指针可以转换为基类的智能指针。
重载的运算符是具有特殊名字的函数,除函数调用运算符外,其他重载运算符不能含有默认实参。
&&
,||
和,
,两个运算对象总是会被求值。&&
,||
,&
和,
,运算符。=
,[]
,()
和->
运算符**必须是成员。
复合赋值运算符一般来说应该是成员,但非必须。
改变对象状态或与给定类型密切相关的运算符,应该是成员。如:递增、解引用。
具有对称性的运算符可能转换任意一端的运算对象,通常应该是非成员。如:算术、相等性、关系和位运算符。如果想提供含有类对象的混合类型表达式,运算符必须是非成员函数。
1
2string s = "world";
string u = "hi" + s; // 如果是成员函数,则"hi".operator+(s),错误
对于友元要注意,在类内部虽然有友元声明,但这并非真正意义上的函数声明,因此在类外部还需要有函数声明。
ostream &operator<<(ostream &os, const Sales_data &item);
istream &operator>>(istream &is, Sales_data &item);
必须处理输入可能失败的情况
1
2
3
4
5
6
7
8
9
10istream& operator>>(istream &is, Sales_data &item) {
double price = 0.0;
is >> item.bookNo >> item.units_sold >> price;
if (is) {
item.revenue = price * item.units_sold;
} else {
item = Sales_data();
}
return is;
}
有时需要标识流的条件状态
通常把算术和关系运算符定义为非成员函数,使得左侧或右侧的运算对象可以转换
1
2string s = "world";
string u = "hi" + s; // 如果是成员函数,则"hi".operator+(s),错误
这些运算符一般不需要改变运算对象的状态,所以形参是常量引用
一般使用复合赋值来实现算术运算符
==
,也应该定义!=
关系运算符应该,
==
if the
class has both operators. In particular, if two objects are
!=
, then one object should be <
the
other.如果存在唯一一种逻辑可靠的<语义,才考虑定义<运算符。如果类同时还包含==,则当且仅当<的定义和==产生的结果一致时才定义<运算符。
由于改变了对象的状态,一般应为成员函数,且应该同时定义前置版本和后置版本。
1 | // 返回递增或递减后对象的引用 |
1 | string &StrBlobPtr::operator*() const { |
->
必须是类成员,*
非必需,但通常也是。函数调用运算符必须是成员函数。如果类定义了调用运算符,那么该类的对象叫做函数对象。调用函数对象实际上是在运行重载的调用运算符。
编写lambda后,编译器将生成一个未命名类的未命名对象。这个类中有一个重载的函数调用运算符,且默认情况下这个成员是const(lambda不能改变捕获的变量),除非lambda被声明为mutable。
引用捕获 由程序确保lambda执行时所引用的对象确实存在,生成的类中无须保存为数据成员。
值捕获 生成的类中需建立对象的数据成员,同时创建构造函数,用捕获的变量来初始化数据成员。
lambda产生的类中,不包含默认构造函数、赋值运算符和默认析构函数,默认拷贝和默认移动构造函数视捕获的数据成员类型而定。
标准库规定的函数对于指针同样适用。直接比较两个无关的指针将产生未定义的行为,但通过标准库定义的函数对象来比较是定义良好的。
关联容器使用less<key_type>
对元素排序,因此可以直接定义一个指针的set或map,而无须声明less
。
C++中的可调用对象多种有,它们的类型是不同的,
类型不同的可调用对象可能共享同一种调用形式。调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型。
定义funcion
类型时,需要指明调用形式。
1 | map<string, function<int(int, int)>> binops = {{"+", plus<int>()}, |
如果构造函数只接受一个实参,则它实际上定义了转换为此类型的隐式转换机制,这种构造函数叫做转换构造函数。
转换构造函数和类型转换运算符共同定义类类型转换,也叫用户定义的类型转换。
编译器一次只能执行一个用户定义的类型转换。
operator int() const;
从类类型转换为int1 | class S { |
S虽然没有定义-
,但隐式转换为int,可以执行内置的-
。
虽然编译器一次只能执行一个用户定义的类型转换,但隐式的用户定义类型转换可以至于一个标准(内置)类型转换之前或之后。
1 | // double->int->S |
explicit operator int() const;
1 | class S { |
但是当表达式作为条件时,编译器会将显式类型转换自动应用于它(仅仅是自动应用explicit operator bool() const
,以转换为bool)。
1 | class S { |
必须确保类类型和目标类型之间只存在唯一一种转换方式。无法通过使用强制类型转换来解决二义性问题,强制类型转换也会面临二义性问题。
多重转换路径可能由于,
两个类提供相同的类型转换
类定义了一组类型转换,它们的转换源(或者转换目标)类型本身可以通过其他类型转换联系在一起,由于所有算术类型转换的级别都一样,选择转换序列时会有多个转换序列,将会导致二义性。
1
2
3
4
5
6
7
8
9
10class S {
public:
S();
operator int() const;
operator double() const;
};
S s;
int a = s; // 精确匹配
float b = s; // 两个“可行函数”
如果已经定义了一个转换为算术类型的类型转换,不要再定义接受算术类型的重载运算符。在不定义以后,如果用户需要使用这样的运算符,则类型转换操作会转换此类型的对象,然后使用内置的运算符。
当调用重载的函数时,如果两个或多个类型转换都提供了可行的匹配,则这些类型转换一样好。
当调用重载的函数时,如果两个或多个类型用户定义的转换都提供了可行的匹配,则这些类型转换一样好。此时,不考虑任何可能出现的标准类型转换级别。
只有当重载函数能通过同一个类型转换函数得到匹配时(所有可行函数都请求同一个用户定义的类型转换),才考虑标准类型转换级别。
表达式中运算符的候选函数集包括成员函数和非成员函数。
如果对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符和内置运算符的二义性问题。
拷贝构造函数的第一个参数必须是一个引用类型,且几乎总是一个const引用。由于拷贝构造函数在多个情况下会被隐式使用,因此不能是explict的。
1 | public: |
如果没有定义拷贝构造函数,编译器会定义一个合成拷贝构造函数。不同于合成默认构造函数r,即使自己定义了其它的拷贝构造函数,编译器也会合成一个拷贝构造函数。
合成拷贝构造函数会将参数的每个非static成员逐个拷贝到正在创建的对象中。拷贝方式依据成员类型而定, * 对于类类型,会使用其拷贝构造函数; * 对于内置类型,会直接拷贝; * 如果成员有数组类型,合成拷贝构造函数会逐元素的拷贝。
合成的函数会被隐式地声明为内联的。
直接初始化会选择与参数最匹配的构造函数。拷贝初始化是件右侧运算对象拷贝到正在创建的对象中。
拷贝初始化通常使用拷贝构造函数来完成,以下情况会发生拷贝初始化,
=
定义变量;在进行拷贝初始化时,编译器可以跳过拷贝/移动构造函数,直接创建对象,但此时拷贝/移动构造函数必须存在且可访问。
如上文所说,拷贝构造函数会被隐式使用,下面是几个例子,
1 | class C { |
上述代码中,对vector使用列表初始化时,c会被copy两次。1. initializer_list的构造函数会copy一次,2. 从initializer_list到设计存储位置还会copy一次。
1 | class C { |
拷贝赋值运算符执行与析构函数和拷贝构造函数相同的工作。 如果没有定义拷贝赋值运算符,编译器会定义一个合成拷贝赋值运算符。
赋值运算符就是一个名为operator=的函数,其参数表示要收费的运算对象。定义为成员函数的运算符,其左侧运算对象就绑定到隐式的this参数。返回值通常为左侧运算对象的引用。
拷贝赋值运算符参数应为与所在类相同类型的参数。
合成拷贝赋值运算符会将右侧运算对象的每个非static成员赋予左侧运算对象的相应成员。类似拷贝构造函数逐个拷贝成员,这一工作是由成员类型的拷贝赋值运算符完成的。如果是数组类型的成员,则逐个赋值数组元素。
如果自己定义的拷贝赋值运算符或拷贝构造函数没有处理成员中的数组,逐个拷贝/赋值不会发生。
1 | class HasPtr { |
销毁对象的非static成员。没有返回值,不接受参数(因此不能够被重载)。对于一个给定类,只会有唯一一个析构函数。
销毁顺序按照初始化顺序的逆序进行,销毁内置类型成员不需要做什么,销毁类类型成员需要执行成员自己的析构函数,销毁内置指针类型的成员不会delete所指向的对象。
析构函数在以下情况进行调用,
合成析构函数函数体为空,当其函数体执行完以后,成员会被自动销毁。析构函数函数体并不直接销毁成员,成员是在析构函数函数体之后隐含的析构阶段中被销毁的。
类似=default
,使用=delete
可以定义删除的函数。=delete
必须出现在函数第一次声明的时候,且可以回任何函数指定=delete
。
如果一个类的析构函数或者一个成员的析构函数是=delete
,那么将无法定义该类型的变量或创建该类的临时对象,可以new但无法delete。
关于合成拷贝控制成员,当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。
行为像值的类有自己状态,副本和原对象是完全独立的。
由于赋值操作会销毁左侧运算对象的资源,在对如下类定义拷贝赋值运算符时,需要考虑将一个对象赋值给自身的情况。
1 | class HasPtr1 { |
如果拷贝赋值运算符是这样的,
1 | HasPtr1 &operator=(const HasPtr1 &rhs) { |
将一个对象赋值给自身时,解引用*rhs.ps
就是错误的,因为ps所指向的对象已经被delete了。因此需要用一个局部临时对象先保存右侧运算符对象的资源。
1 | HasPtr1 &operator=(const HasPtr1 &rhs) { |
行为像值的类共享状态。
1 | class HasPtr2 { |
对于以上类的拷贝构造函数,需要递增右侧运算对象的引用计数,递减左侧运算对象的引用计数。这里同样需要考虑同一个对象给自身赋值的情况,应该先递增右侧运算对象的引用计数,然后递减左侧的并检查,
1 | HasPtr2 &operator=(const HasPtr2 &rhs) { |
如果一个类没有定义自己的swap,需要的时候将调用标准库的swap。一般来说,一次swap需要一次copy和两次assign,但这并不是必须要的。如果一个类有动态分配的内存,可以交换指针,而不是既copy又assign。
1 | class HasPtr { |
要注意的是:
1 | void swap(Foo &lhs, Foo &rhs) { |
using std::swap;
并未隐藏HasPtr的swap。1 | HasPtr& HasPtr::operator=(HasPtr rhs) { |
这里的赋值运算符是用传值的方式。swap左侧运算对象和副本,然后销毁副本。
copy and swap天然就是异常安全的,因为可能抛出异常的情况就是传值时候的copy,如果此时抛出异常,左侧对象不会被修改。同时保证了自赋值的正确,因为是copy。
lvalue:有持久的状态,可以取地址。 rvalue:字面值常量或临时对象,不可以取地址。
必须绑定到右值,即要求转换的表达式、字面值常量或返回右值的表达式。但不能直接绑定到一个左值上。
1 | int i = 5; |
由于rvalue reference只能绑定到临时对象,因此这个对象,
这意味着使用rvalue reference可以自由地接管所引用对象的资源。
1 | int i = 5; |
进行右值引用后,得到的右值引用类型变量是左值。
1 | int i = 32; |
要将右值引用绑定到一个左值,应该显示地转换或move,
1 | int i = 5; |
使用move后,不能对移后源对象的值做任何假设,不能使用移后源对象的值。除了对rri赋值或销毁外,不能再使用它。
参数是一个右值引用,且任何额外的参数都必须有默认实参。完成移动后,必须保证销毁源对象是无害的。一旦完成移动,源对象必须不能再指向被移动的资源,资源所有权已归属新创建的对象。
1 | StrVec::StrVec(StrVec &&s) noexcept : elements(s.elements), first_free(s.first_free), cap(s.cap) { |
移动构造函数通常不分配任何新内存,因此通常不抛出异常。为避免标准库为了处理可能抛出异常而做的额外工作,一种通知标准库的方法是指明noexcept
。
1 | vector<StrVec> vs; |
基于上面两点,除非vector知道元素类型的移动构造函数不会抛出异常,否则在reallocate时,就必须使用拷贝构造函数(即前面所说的,为了处理可能抛出异常而做的额外工作)。如果希望在类似reallocate 的情况下使用移动而非拷贝,就必须显式的告诉标准库移动构造函数可以安全使用。
移动赋值运算符执行与析构函数和移动构造函数相同的工作。如果不抛出异常,则应该标记为noexcept
。
1 | StrVec &operator=(StrVec &&rhs) noexcept { |
这里必须check是否是同一对象,因为此右值可能是move调用返回的结果,再者不能在使用右侧运算对象的资源之前就释放左侧运算对象的资源。
从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会被销毁,因此需确保移后源对象必须可析构。
移动操作还应保证对象仍是有效的,即可以安全地为其赋予新值或可以安全地使用而不依赖其当前值。
移动后,源对象的值是没有保证的,不应依赖移后源对象中的数据。
1 | struct X { |
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动构造或移动赋值时,编译器才会合成移动构造函数和移动赋值运算符。
1 | struct Y { |
移动操作永远不会隐式定义为delete。但如果显式地要求编译器生成=default的移动操作,且编译器不能移动所有成员时,移动操作会被定义为delete。
定义了一个移动构造函数或移动赋值运算符的类必须也定义拷贝操作,否则这些成员默认定义为delete。
1 | class Foo { |
如果没有移动构造函数,就算试图调用move来移动,对象也会被拷贝。
这里与上面的hasY不同,hasY的移动构造函数是delete的(由于显式地要求生成,但编译器无法生成)。而这里的仅仅是未定义,函数匹配保证该类型的对象会被copy。
1 | class Hp { |
这里除了拷贝构造函数,还有移动构造函数和赋值运算符。对于之前未定义移动构造函数的情况下,调用赋值运算符,初始化形参时总是进行拷贝。
现在拷贝初始化依赖于实参的类型,
从而单一的赋值运算符,实现了两种功能。
上面的将拷贝赋值运算符和移动赋值运算符“合并”到一起的方式叫做copy and swap idiom。如果按照上面的方式实现了拷贝赋值运算符和移动赋值运算符,就不能再单独写两个拷贝赋值运算符和移动赋值运算符。
而两种实现拷贝赋值运算符和移动赋值运算符的效率是有区别的(C++ Primer 5th Chinese Edition, Exercise 15.53)。
1 | #define NDEBUG |
测试代码如下,这里最好不要把对象的创建放到循环中去,每次构造和销毁的开销会使得两种拷贝和移动的区别不太明显,
1 | HasPtr3 hp; |
结果如下(VMware 12, Archlinux x64, Intel i7-2620m),
hp = hp1; |
hp = std::move(hp2); |
mixed | |
---|---|---|---|
HasPtr3& operator=(const HasPtr3& rhs) |
4s | N/A | 4.8s |
HasPtr3& operator=(HasPtr3&& rhs) noexcept |
N/A | 0.7s | 4.8s |
HasPtr3& operator=(HasPtr3 rhs) |
6s | 2.5s | 8.8s |
可看出copy and swap idiom的效率是不如分开写好。
一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。对移动迭代器解引用生成的是一个右值。
1 | auto newe = uninitialized_copy(make_move_iterator(elements), make_move_iterator(cap), newb); |
通过调用make_move_iterator
可将一个普通迭代器转换为一个移动迭代器。上面的代码中,传递给uninitialized_copy
的是一个移动迭代器,解引用后得到的是右值,因此uninitialized_copy
将使用移动构造函数来构造元素。
标准库不保证哪些算法适用于移动迭代器。只有确认对象在传递给函数后不再访问,才能将移动迭代器传递给算法。
类似构造函数和赋值运算符,成员函数同样可以提供拷贝版本和移动版本。
1 | void push_back(const X&); // 拷贝 |
拷贝版本接受能够转换为类型X的任何对象。使用const X&
是因为拷贝操作不应该改变该对象。
而移动版本接受非const右值,对于非const右值是精确匹配。从源对象移动数据时,显然需要更改源对象,所以是X&&
。
对于赋值运算符,为了强制左侧运算对象是一个左值,可以类似const,在参数列表后使用引用限定符,引用限定符可以是&
或&&
,
1 | Foo &operator=(const Foo&) &; // 只能向可修改的左值赋值 |
const改变了this指针的类型,指明了this是指向常量的指针,这里类似,引用限定符说明了this可以指向一个左值还是右值。const和引用限定符只能用于非static成员函数。
当定义const成员函数是,可以根据有无const,定义两个重载版本。
1
2Foo sorted();
Foo sorted() const;
当定义有引用限定符的成员函数时,如果定义两个或两个以上具有相同名字和参数列表的成员函数,就必须对**所有重载函数*都加上引用限定符。
1
2
3Foo sorted(Comp*) &&;
Foo sorted(Comp*) const; // 错误
Foo sorted(Comp*) const &;
静态内存:存储局部static对象、类static数据成员和定义在函数之外的变量。 static对象:使用之前分配,程序结束时销毁。
栈内存:保存定义在函数内部的非static对象。 栈对象:仅在定义的程序块运行时才存在。
动态内存(free
store或heap):存储动态分配的对象,需要显示地销毁,分配和销毁由new
和delete
完成。
最安全的分配和使用动态内存的方法是调用make_shared
,返回指向在动态内存分配的对象的shared_ptr
。make_shared
类似emplace
,使用参数来构造指定类型的对象,如果没有参数,则进行值初始化。
当进行copy或assign时,每个shared_ptr
会记录有多少个其他shared_ptr
指向相同的对象。可看作shared_ptr
有reference
count,
1 | shared_ptr<string> p = make_shared<string>("hello"); // 1 |
shared_ptr
赋予一个新的值; 1 | shared_ptr<int> r = make_shared<int>(42); // 1 |
shared_ptr
被销毁;count的递减由shared_ptr
的析构函数完成,如果count变为0,shared_ptr
会释放所管理的对象。
在某个scope中,只要能够使用shared_ptr
,那么它的引用计数至少为1。
1 | p.get() // 返回内置的指针 |
使用new
和delete
会使得类对象的copy、assign和destroy不能依赖任何默认定义。
默认情况下,shared_ptr指向的是动态内存,因此被销毁时,默认调用delete。可以自定义释放操作,提供其他的deleter。deleter的参数必须为该shared_ptr的内置指针类型。
默认情况下,new的对象是默认初始化的。也可以使用值初始化的方式来初始化new的对象(圆括号+参数),还可以使用列表初始化,以及值初始化(空括号)。
可以使用auto从initializer来推断将要分配的对象类型,由于编译器需要从initializer来获得类型,因此圆括号中仅能有一个initializer,
1 | auto p1 = new auto(obj); |
和其他const对象相同,必须初始化。
1 | const string *pcs = new const string; // 调用默认构造函数 |
如果内存不足,new失败,就会抛出bad_alloc
,但可以告知不抛出。
1 | int *p1 = new int; |
传递给delete的必须是指针,且必须指向动态分配的内存,或是一个nullptr
。如果是动态分配的内存,或释放同一个指针多次,行为未定义。对const动态对象,销毁的方法也是一样的。
1 | // 使用clang++ 3.7编译 |
可以在delete后手动赋值为nullptr
。但也仅仅只解决了pd
的问题,多个指针指向同一个内存区域时,仍然有问题,pd1
仍然指向原内存区域,还是空悬指针。
可以用new返回的指针来初始化shared_ptr
。由于接受智能指针的构造函数是explicit的,因此必须使用直接初始化。
1 | shared_ptr<int> p(new int(1024)); |
shared_ptr
定义了get
函数,可以获得内置指针,指向shared_ptr
管理的对象。通过这种方式得到的指针不能被delete,必须保证代码不会delete的情况下,才能使用get。
1 | // 使用clang++ 3.7编译 |
上述代码在内部的scope中手动删除了p指向的内存,当这个scope结束时,sq被销毁,那部分内存还会被shared_ptr销毁一次。编译时不会报错,但运行时出现double free or corruption。 就算没有delete,内部的scope结束,那部分内存被销毁,这段代码结束时,又一次被销毁,同样也会有double free or corruption。
无论是函数正常结束或者发生异常,局部对象都会被销毁。智能指针被销毁时,如果引用计数为0,则释放内存。但new得到的内存不会被自动释放,如果有指向这块内存的指针,只有指针会被销毁。
unique_ptr拥有指向的对象。没有类似make_shared的函数,只能将其绑定到new返回的指针上。也是必须使用直接初始化。
除了将被销毁的unique_ptr外,不支持copy和assignment。
1 | u = nullptr // 释放u指向的对象,并置空 |
不同于shared_ptr,unique_ptr在重载deleter时,需要提供deleter的类型。重载unique_ptr的deleter,会影响到unique_ptr的类型和如何构造或reset该类型的对象。
1 | unique_ptr<objT, delT> p(new objT, fcn); |
1 | w = p // p可以是weak_ptr或shared_ptr |
分配动态数组的类必须定义自己的版本的操作来管理拷贝,复制以及销毁。
1 | int *pia = new int[32]; |
pia
中的元素是进行默认初始化的。但此时pia并不是一个数组类型的对象,只是一个数组元素类型的指针,因此不能够调用begin和end(它们使用数组的维度来得到首元素和尾后元素指针),也不能使用for。
new的数组和单个对象一样,默认情况下,new的数组是默认初始化的。可以对数组中的元素进行值初始化和列表初始化,也和单个对象一样。
1 | string *psa = new string[10](); |
如果new失败,类似bad_alloc,这里会抛出bad_array_new_length。
这样做是合法的,得到的是一个合法的非空指针,相当与数组的尾后指针,不能解引用。
1 | int *p = new int[0]; // 合法,但不可以解引用 |
1 | int *p = new int[10]; |
释放时,按逆序销毁。p还可以为nullptr。
标准库提供了一个管理new分配的数组的unique_ptr,但此unique_ptr不支持成员访问运算符。unique_ptr被销毁时,会自动使用delete []
。
1 | unique_ptr<T[]> u |
如果使用shared_ptr来管理,则必须提供自定义的删除器。如果没有提供,则shared_ptr会默认调用delete,行为未定义。
1 | shared_ptr<int> sp(new int[10], [](int *p) { delete [] p; }); |
1 | class C { |
new把内存分配和对象构造组合在了一起,可能造成外的开销;同时若类没有默认构造函数,则不能够分配动态数组。
allocator分离内存分配和对象构造,避免不必要的开销。所分配的内存是原始的,未构造的。
1 | allocator<C>::size_type n = 10; |
下列操作所需的内存是由allocate
分配的,而不是系统分配的,因此alloc_b
指向的内存必须足够大。
1 | uninitialized_copy(b, e, alloc_b); // 返回最后一个构造的元素之后的位置 |