第13章 拷贝控制
1、拷贝构造函数 P440
如果一个类的构造函数第一个参数是自身类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
拷贝初始化 和 直接初始化
如果初始化的时候使用 = 来初始化一个对象,右侧对象拷贝到左侧对象中(函数传递实参给非引用形参,列表初始化),那就是拷贝初始化。
使用最匹配的构造函数,那就是直接初始化。
2、拷贝初始化发生在:
传递实参对象给非引用形参。
返回对象作为非引用返回值。(如果以该对象继续初始化一个新对象,这个过程就不调用拷贝构造函数了)
用花括号列表初始化数组中的一个元素或一个聚合类的成员.
vector等对象用花括号初始化或使用push_back和insert等操作时.
class A{
public:
int v;
A(){}//默认构造函数
A(int v):v(v){}//构造函数1
A(const A &o){//拷贝构造函数
v = o.v+10;
}
};
A test(A a){
cout << a.v << endl;
return a;
}
int main()
{
A a(1);
A c = a;
cout << c.v << endl;//11
puts("**********************");
A b = test(a) ;//11
cout << b.v << endl;//21,不是31
puts("**********************");
cout << test(a).v << endl;//11 21
puts("**********************");
vector< A >v;
v.push_back(a);//继续添加会因分配空间而继续拷贝
cout << a[0].v << endl;
puts("**********************");
return 0;
}
3.拷贝构造函数的形参必须是引用类型
拷贝构造函数是用来被初始化非引用类型参数。假设构造参数的对象不再是引用了,现在将实参B传递给非引用形参A,那么A要调用自己的拷贝构造函数来构造B…依次循环下去
4.拷贝赋值运算符。
赋值的时候,不是初始化。
赋值运算符返回一个指向其左侧运算符对象的引用。
class A{
public:
A(){}
A(int v):v(v){}
A(const A &b){//拷贝构造函数
v = b.v;
}
A& operator = (const A &rhs){//拷贝赋值运算符
v = rhs.v + 10;
return *this;
}
int v;
};
int main()
{
A a(1);
A b;
b = a;//拷贝赋值运算符
A c = a;//拷贝构造函数
cout << b.v << endl;//11
cout << c.v << endl;//1
return 0;
}
5. 使用 = default
通过将拷贝控制成员(默认构造函数)定位为=default 来显示要求编译器生成合成的版本。
class A{
public:
A() = default;
A(A &a) = default;
~A() = default;
int *p;
int v;
};
6.阻止拷贝
可以通过在拷贝构造函数和拷贝赋值运算符后面加=delete来阻止拷贝,为什么要阻止拷贝?iostremm类阻止拷贝,避免多个对象写入或读取相同的IO缓存。
c++11之前,通过将类的拷贝函数和拷贝赋值运算符声明为private的且不定义该类对象的拷贝,就算是友元也不能拷贝该类型,因为未定义。
P450:
几种编译器会定义为删除的函数。
删除析构函数—> 合成的默认构造函数、拷贝构造函数被定义为删除的。
无法默认构造的const成员、引用—>默认构造函数删除。
本质上:当不能拷贝、赋值、或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。
7. 行为像值得类
1.定义一个拷贝构造函数,完成string的拷贝,而不是拷贝指针。
2.定义一个析构函数来释放string。
3.定义一个拷贝赋值运算符来释放当前的string,并从右侧运算对象拷贝string。
class A{
public:
A(string s = ""):p(new string (s) ),v(0) {}
A(A &a):p(new string(*(a.p)) ),v(a.v) {}
A& operator = (const A &rhs){
string s = *(rhs.p);//必须先保存,否则有可能两个对象是同一个
v = rhs.v;
delete p;//释放之前的
p = new string (s);//申请新空间
return *this;//返回自身引用
}
~A(){delete p;}
int v;
string *p;
};
int main()
{
A a,b;
*(a.p) = "111";
b = a;
cout << a.p << endl;
cout << b.p << endl;
cout << *(a.p) << endl;
cout << *(b.p) << endl;
return 0;
}
8.定义像指针的类(shared_ptr)
#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,n) for (int i=a;i<n;i++)
#define per(i,a,n) for (int i=n-1;i>=a;i--)
#define oo cout<<"!!!"<<endl;
typedef long long ll;
typedef unsigned long long ull;
#define ms(s) memset(s, 0, sizeof(s))
const int inf = 0x3f3f3f3f;
const int mod = 1e9+7;
//head
const int maxn = 1e6+11;
class A{
public:
A(string p = ""):sp( new string(p)),num(new int(1)){}
A(A &a){//拷贝构造函数
sp = a.sp;
num = a.num;
++(*num);
}
A& operator = (A &rhs){
++(*rhs.num);//必须先+
if(--(*num) == 0){
delete num;
delete sp;
}
sp = rhs.sp;
num = rhs.num;
}
~A (){
if(--(*num) == 0){
delete num;
delete sp;
}
}
string *sp;
int *num;
};
int main()
{
A a,b;
*(a.sp) = "dfg";
cout << *(a.num) << endl;//1
b = a;
cout << *(a.num) << endl;//2
cout << a.sp << "\t" << b.sp << endl;//一样
return 0;
}
9.给值行为的类定义自己的swap函数.P459
拷贝并交换技术
class A;
void swap(A &a1,A &a2);
class A{
friend void swap(A &a1, A &a2);
public:
A(string s = ""):p(new string(s)),v(0){}
A(A &a):p( new string (*(a.p) )) ,v(a.v){}
A &operator = (A rhs){//按值传递。
swap(*this,rhs);
return *this;
}//rhs指向本对象原来的内存,被销毁,从而delete了原来的指针。
~A(){delete p;}
int v;
string *p;
};
void swap(A &a1,A &a2){
using std::swap;
swap(a1.v,a2.v);
swap(a1.p,a2.p);
}
int main()
{
A a,b;
*(a.p) = "111";
b = a;
cout << a.p << endl;
cout << b.p << endl;
cout << *(a.p) << endl;
cout << *(b.p) << endl;
return 0;
}
10.左值引用 和 右值引用
左值持久、右值短暂
右值引用是即将被销毁的对象。右值引用不能绑定到一个变量上。
int i = 1;
const int &a1 = i*2;
int &a2 = i;
int &&a3 = i*2;
// int &a4 = i*2;
// int &&a5 = i;
//int && a6 = a3;
int &&a7 = std::move(i);
cout << a7 << "\t" <<i;
return 0;
11. 移动构造函数与移动赋值运算符。
移动构造函数的本质其实就是夺取一个对象的内存资源给被构造的对象,且原来对象不能继续控制本来的内存了。
class A{
public:
A(string s = ""):sp(new string (s)){}
A(A && rhs) noexcept{
sp = rhs.sp;
rhs.sp = nullptr;
}
A& operator = (A &&rhs)noexcept{
if(this != &rhs){
delete sp;
sp = rhs.sp;
rhs.sp = nullptr;
}
return *this;
}
~A(){delete sp;}
string *sp;
};
int main()
{
A a1("asd");
cout << a1.sp << endl;
A a2(std::move(a1));
cout << a1.sp << endl;//0
cout << a2.sp << endl;//
string s1("abc"),s2;
s2 = std::move(s1);
cout << s1 << endl;//""
cout << s2 << endl;//abc
return 0;
}
12.合成的移动操作
只有一个类没有定义任何拷贝控制成员(拷贝构造函数,拷贝赋值运算符,析构函数)时,且类内非static成员都是可以移动的,此时编译器才会给该类合成移动构造函数和移动赋值运算符。
class A{
public:
A():i(0),s(""){}
int i;
string s;//string定义了自己的移动操作。
void print(){
cout << i << "\t" << s << endl;
}
};
int main()
{
A a1;
a1.i = 11;
a1.s = "abc";
A a2 = std::move(a1);
a1.print();//11
a2.print();//11 abc
return 0;
}
13.当既有拷贝操作也有移动操作时,使用哪个?
一个原则:移动右值,拷贝左值
即当右边是一个右值时,就优先使用移动操作。(也可以拷贝)
当右边是一个左值时,只能使用拷贝操作。(除非std::move() )
class A{
public:
A(string s = ""):sp(new string(s)){}
A(A &rhs){
string s = (*(rhs.sp)) + "111";
sp = new string(s);
}
A& operator = (const A& rhs){
string s = ( *(rhs.sp)) + "111";
delete sp;
sp = new string(s);
return *this;
}
A(A&& rhs){
sp = rhs.sp;
sp = nullptr;
}
A& operator =(A &&rhs){
if(this != &rhs){
delete sp;
sp = rhs.sp;
rhs.sp = nullptr;
}
return *this;
}
~A(){delete sp;}
string *sp;
};
A get(string s){
A a(s);
return a;
}
int main()
{
A a1("aaa"),a2("bbb");
A a3 = a1;//调用拷贝构造函数
A a4 = get("bbb");//调用移动构造函数
cout << *(a3.sp) << "\t" << *(a4.sp) << endl;
return 0;
}
14.如果没有移动操作,只有拷贝操作,那么右值也被拷贝。
也就是必要的时候,右值也能被拷贝构造函数和拷贝赋值运算符拷贝。
但是拷贝构造函数和拷贝赋值运算符的参数必须是const的引用才可以,如果不是就会出错。
class A{
public:
A(string s = ""):sp(new string(s)){}
A(const A &rhs){
string s = (*(rhs.sp)) + "111";
sp = new string(s);
}
A& operator = (const A &rhs){
string s = (*(rhs.sp)) + "222";
delete sp;
sp = new string(s);
return *this;
}
~A(){delete sp;}
string *sp;
};
int main(){
A a1("aaa");
A a2 = std::move(a1);///aaa111
cout << *(a2.sp) <<endl;
A a3;
a3 = std::move(a2);///aaa111222
cout << *(a3.sp) <<endl;
}
15.赋值运算符实现拷贝赋值和移动赋值两种功能。
如果赋值运算符的形参是传值调用,那么用实参初始化形参就要调用拷贝构造函数或移动构造函数(看实参左值还是右值)。那么可以用下面的调用方式实现等价的拷贝赋值和移动赋值。 **注意:=操作符内的swap是自定义的swap,因为标准库的swap需要支持=操作符,但是=操作符我们还没定义。**class A{
public:
A (string s = ""):sp(new string(s)) {}
A(const A &rhs){
string s = (*(rhs.sp)) + "111";
sp = new string(s);
}
A(A &&rhs) noexcept {
sp = new string ((*rhs.sp)+"222");
rhs.sp = 0;
}
//必须定义swap,标准库的swap需要定义=操作符
void swap(A &a,A &b){
string *sp = a.sp;
a.sp = b.sp;
b.sp = sp;
}
// = 实现了拷贝赋值和移动赋值两种功能
//主要看实参是左值还是右值
A& operator = (A rhs){
swap(*this,rhs);
return *this;
}
~A(){delete sp;}
string *sp;
};
int main()
{
A a1("a");
A a2,a3;
a2 = a1;
a3 = std::move(a1);
cout <<*(a2.sp) <<endl;//a111
cout <<*(a3.sp) << endl;//a222
return 0;
}
16.右值和左值引用成员函数。
在类的成员函数后面加上& 或者 && 可以限定成员函数只能接受左值或右值参数。 可以避免对右值对象使用 = 赋值。class A{
public:
A(int v = 0):v(v){}
A operator + (A a2){
return A(v + a2.v);
}
A &operator = (const A &a)&
{
v = a.v;
return *this;
}
void print()&&
{
cout << v << endl;
}
int v;
};
int main()
{
A a1(1),a2(2),a3(3);
//a1 + a2 = a3;//调用=的参数必须为左值
//a3.print();//必须为右值
(a1+a2).print();
return 0;
}