二十二:
1:拷贝控制操作
拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符,析构函数。
这些,在类的数据成员都能默认构造,拷贝,复制,销毁时,编译器默认都会有合成的版本。
(1) 拷贝构造函数:
Foo(const Foo&);
第一个参数是自身类类型的引用,额外的参数都有默认值。
几种情况下会被隐式使用,所以,不能是explicit
默认拷贝构造函数,又叫合成拷贝构造函数,也会逐元素的拷贝数组的成员。
拷贝初始化是依靠拷贝构造函数和移动构造函数来完成的。
调用的情况:
0)初始化时,用=或者I()直接初始化时用:对象赋值/拷贝对象时,=非初始化会调用拷贝赋值运算符。
1)函数传递非引用参数的对象
2)函数返回非引用的对象
3)花括号列表初始化数组或者聚合类的成员时。
4)标准库容器初始化或调用insert/push成员时。而emplace是进行直接初始化。
其中第1)条解释了,拷贝构造函数的参数为什么是引用了,要拷贝实参,要调用拷贝构造函数,又要拷贝实参,无限循环。
拷贝初始化中,编译器可以跳过拷贝/移动构造函数,直接创建对象,即,允许:
stringstr = “123”; //拷贝构造函数
改写成:
stringstr(“123”);//略过了拷贝构造函数
即使略过了,拷贝/移动构造函数必须是存在且可访问的。
(2)拷贝复制运算符
名为operator=的函数。必须定义为成员函数。返回指向其左侧运算对象的引用。
(3)析构函数
首先指向函数体,然后销毁成员,成员按初始化顺序逆序销毁。(析构部分是隐式的),隐式销毁内置指针类型成员,并不会delete掉所指向的对象,而智能指针是类类型,所以只能指针在析构阶段会被自动销毁。
如果一个类需要析构函数,几乎可以肯定需要“拷贝构造函数”和“拷贝赋值运算符”。(这三个函数是联系很紧密的,涉及到资源的申请释放),拷贝赋值进行的操作相当于析构和拷贝构造的组合。
(4)
=default ,(1)类内是内联,类外不是内联(2)只能用于编译器可以合成的函数。
=delete (1)必须用于函数第一次声明时。(2)可以用于任何函数。
析构函数或者类成员的析构函数定义为删除的,那么不能定义该类的变量或临时对象,因为对象无法销毁。(虽然可以动态分配这种类型的对象,但是不能释放)
合成的拷贝控制成员可能是删除的:
虽然我们可以给引用赋值,但是改变的不是引用本身,而是他原来引用的对象。所以:对于有引用成员的类,合成拷贝赋值运算符被定义为删除的。
(5)move 定义在utility头文件中。使用时要直接调用std::move,防止类定义自己的move,即使我们在调用move前声明usingstd::move,但是实参的命名空间要优先于本作用域内的搜索。(可以将左值转化为右值,返回一个右值);
(6)移动,会大幅度提高性能。且io类和unique_ptr(包含不能被拷贝的资源:指针或IO缓冲)不能拷贝,但可以移动。
右值引用:必须绑定到右值的引用。性质:只能绑定到将要销毁的对象。
左值引用代表对象身份。右值引用代表对象的值。
int i = 42;
int &r1 = i*32;//错误
const int &r1 = i*32;//正确,可以将const引用绑定到右值上
int && r2 = i*32;//正确,右值引用
前置递增/递减 返回左值。
后置递增/递减 返回右值。
int ia = 10;
//ia++ = 5;//错误
++ia = 6;//正确
不能将右值引用绑定到右值引用类型的变量上。
int &&r1 = 42;
int &&r2 = r1;//错误
我们可以销毁一个移后原对象,也可以对他赋值,但是不能使用它,即希望他会有什么值。
(7)移动构造函数
第一个参数是右值引用,其他参数必须有默认实参。
不分配任何内存,但记得要把原对象做好处理:移后原对象可以被安全的销毁或者赋值。
如果,确认不会抛出异常,可以在参数列表的小括号后加上 noexcept(声明和定义中都要有)告诉编译器,否则编译器要做一些额外的工作。
必须定义拷贝控制成员的类,是有些类成员必须经过拷贝成员的操作。通常移动操作比拷贝要节省效率。
移动容器元素可以使用移动迭代器,解引用生成右值引用。
拷贝成员通常参数是const T&的,移动成员通常不是const,即T&&,逻辑上,一个不改变,一个改变。
(8)
有时右值的使用方式令人惊讶:
s1,s2为string:
s1+s2 = “wow”;//对右值赋值,这里是允许的。。
新标准库允许向右值赋值。我们可以阻止这种操作。即强制左侧运算对象(即,this指向的对象)是一个左值。
我们指出this左值/右值属性的方式与定义const成员函数相同。在参数列表后放置一个引用限定符。(声明与定义都要有),引用限定符必须跟在const之后
Foo &operator=(const Foo&)&;//只能向可修改的左值赋值。
Foo sorted() &&;//可用于可改变的右值
Foo sorted() const &;//可用于任何类型的Foo
定义const成员函数,可以一个有,一个没有。
但是对于引用限定的函数:如果函数名,参数都相同,必须“都加引用限定符或者都不加”
例子1:
#include
#include
#include
#include
#include
using namespace std;
class X
{
public:
X():data{1,4,3}
{
}
vector
Xsorted() const &
{
cout<<"const&"<
Xret(*this); //1/2
sort(ret.data.begin(),ret.data.end()); //1
returnret; //1
//returnret.sorted(); //2无限递归
//returnX(*this).sorted(); //3右值sorted
}
Xsorted() &&
{
cout<<"&&&&&&&&"<
sort(data.begin(),data.end());
return*this;
}
};
int main()
{
Xx;
Xx2 = x.sorted();
for(auto e : x2.data)
{
cout<< e << " ";
}
cout<< endl;
getchar();
return0;
}
例子2:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
usingnamespacestd;
classMyClass
{
public:
MyClass() :str("default")
{
cout <<"合成构造函数" <<endl;
}
MyClass(stringstr1) :str(str1)
{
cout <<"string参数构造函数" <<endl;
}
MyClass(constMyClass&you)
{
str =you.str;
cout <<"const拷贝构造" <<endl;
}
MyClass(MyClass &you)
{
str =you.str;
cout <<"非const拷贝构造"<<endl;
}
MyClass& operator=(MyClass &you)
{
cout <<"拷贝赋值"<<endl;
this->str = you.str;
return *this;
}
MyClass(MyClass&&you )
{
str =you.str;
cout <<"非const移动构造"<<endl;
}
MyClass(constMyClass &&you)
{
str =you.str;
cout <<"const移动构造" <<endl;
}
MyClass& operator=(MyClass&&you)
{
cout <<"移动赋值"<<endl;
str =you.str;
return *this;
}
voidstrOut()
{
cout <<str<<"-----------------------" <<endl<<endl;;
}
private:
stringstr;
};
intmain()
{
conststringss ="const str";
constMyClassm0;//默认构造
cout <<"const MyClass m0;" <<endl<<endl;
MyClassm1;//默认构造
cout <<"MyClass m1;" <<endl<<endl;
MyClassm2 ="m2";//string参数构造函数
cout <<"MyClass m2 =\"m2\";" <<endl <<endl;
MyClassm3("m3");//string参数构造
cout <<"MyClassm3(\"m3\");" <<endl <<endl;
MyClassm4(ss);//string参数构造
cout <<"MyClass m4(ss);" <<endl<<endl;
MyClassm5(std::move(ss));//string参数构造===
cout <<"MyClass m5(std::move(ss));=====" <<endl<<endl;
MyClassm6 =m2;//拷贝构造非const
cout <<"MyClass m6 = m2;" <<endl<<endl;
MyClassm6_1 =m0;//const拷贝构造
cout <<"MyClass m6_1 = m1;" <<endl<<endl;
MyClassm7(m2);//拷贝构造非const
cout <<"MyClass m7(m2);" <<endl<<endl;
MyClassm8(std::move(m3));//移动构造非const
cout <<"MyClassm8(std::move(m3));" <<endl <<endl;
m8 =std::move(m3);//移动赋值
cout <<"m8 = std::move(m3);" <<endl<<endl;
m1 =m2;//拷贝赋值
cout <<"m1 = m2;" <<endl<<endl;
m1 =std::move(ss);//string参数构造+移动赋值===========
cout <<"m1 =std::move(ss);========" <<endl <<endl;
getchar();
return 0;
}
二十三:重载与类型转换
(1)除了重载函数调用运算符operator()之外,其他重载运算符不能含有默认实参。
不能重载 :: .* . ?: (四个)
逻辑运算符,求值顺序规则和短路问题无法保留,所以,不建议重载。
逗号运算符,取地址运算符,已有内置的含义,不建议重载。
(2)赋值(=)/下标([])/调用() /成员访问箭头(->) 必须是成员函数。
算术/相等/关系可以换位置的,一般应是 友元函数。
(3)输入输出运算符必须是非成员函数。因为输入输出必须是i/o stream成员,我们无法给标准库类添加成员。所以只能当参数传递i/o stream。
istream&operator>>(istream &is,MyClass &it);
输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
(4) vector还定义了第三中赋值运算符,接受花括号的元素列表作为参数。
MyClass&operator=(initializer_list
(5)复合赋值 += *=等,为与内置类型保持一致,要返回左侧运算对象的引用。
(6)区分前置和后置运算符:(后置版本接受一个额外的(不被使用)int类型参数。
(7)箭头运算符,不能随便定义,获取成员这一事实永远不变。(编译器怎么限定的????我随便返回一个“你好”,也没报错。)
(8)如果类定义了函数调用运算符,则类的对象称作“函数对象”
lambda是函数对象。(不是对象调用,所以,我们只需对于sort等,传递对象就行,而不是对象后加括号)
当lambda通过引用捕获变量时,编译器可以直接使用该引用,而不必在lambda产生的类中将其存储为数据成员。
当lambda通过值捕获的变量时,lambda产生的类必须为每个值捕获的变量建立对应的数据成员。
因为需要带参数的构造函数,参数列表来初始化成员,所以lambda产生的类不包含默认构造函数/赋值运算符及默认析构函数,而默认拷贝/移动构造函数要视捕获的数据成员类型而定。(成员是否可以拷贝/移动)
(9)标准库函数对象。functional头文件
算术:plus
关系:equal_to
逻辑:logical_and
例:sort
(10)可调用对象:函数,函数指针,lambda表达式,bind创建的对象,重载了函数调用运算符的类
标准库function类型,function
function
对于同函数名,不同参数类型的,为解决function存储的二义性:1)函数指针 2)使用lambda表达式(函数体为 return add(a,b))
(11)类类型转换:转换构造函数,类型转换运算符
operatortype() const;
可以面向任何可以作为函数返回的类型。(不能是数组和函数,可以是指针和引用),没有返回类型,没有形参,必须是成员函数,一般是const
尽管编译器一次只能执行一个用户定义的类型转换,但是可以置于内置类型转换之前或之后。(连同内置转换,可以连续转)
类型转换时隐式的,所以没有参数,虽然没有返回值,但是都会返回一个对应类型的值。
(12)类型转换可能产生意想不到的后果。
int i = 42;
cin << i; //cin转换为bool,提升成int,然后左移。
C++11引入显示的类型转换,来防止。
explicit operator int() const { return val;}
MyClass si = 3;
si+3;//错误
static_cast
有个例外:如果表达式被用作条件,编译器将显式的类型转换自动应用于他。
while(cin>>value) //对结果转换,而不是一开始就把cin转换
对bool类型的转换一般用在条件部分,所以,operator bool一般定义成explicit
(13)避免二义性类型转换
1)
1:A有B--->A的转换构造函数
2:B有B--->A的类型转换函数
当需要B--->A时,会有歧义。当然显示的调用可以避免。
2)内置算术类型有自动转换,所以对于设计内置类型的转换,只定义一个就可。
3)提供了构造函数含一个算术类型参数(算术到类转换)
转换目标是算术类型的类型转换(类到算术转换)
重载了运算符(参数是类类型)
MyClassmy;
inti = my + 0;//有歧义。