第13章拷贝控制
当定义一个类的时候,我们显示的或隐式的指定在此类型的对象拷贝、移动、赋值和销毁时候做什么。一个类通过定义5种特殊成员函数来控制这些操作,包括
拷贝函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。拷贝和移动构造函数定义了当用同一类型的另一个对象初始化本对象的时候做什么.
拷贝和移动赋值运算符定义了将一个对象赋予同类的另一个对象做了什么
如果一个类没有定义拷贝成员,编译器会自动定义它为缺失操作。因此很多类会忽略这些拷贝操作
13.1拷贝赋值和销毁
13.1.1 拷贝构造函数
如果一个构造函数的第一个参数是自身类型的引用,且任何额外参数都有默认值,则这个函数是拷贝构造函数
class Foo{
public:
Foo();
Foo(const Foo&);
}
拷贝构造构造函数在一些情况下会被隐式使用,因此拷贝构造函数通常不应该是explict.
合成拷贝构造函数:
如果我们没有为一个类定义一个拷贝构造函数编译器会为我们定义一个。与合成默认构造函数不同,即使我们定义了一个其他构造函数,编译器也会为我们合成一个拷贝构造函数。
每个成员的类型决定了如何拷贝:对类类型的成员,会使用其拷贝构造函数来拷贝。虽然我们不能直接拷贝一个数组,但是合成拷贝构造函数会逐元素拷贝一个
数组类型成员。如果数组元素是类类型,则使用元素的拷贝构造函数来拷贝。
下面我们写一个例子来理解一下,上面说的都比较抽象:
class Sales_data {
public:
Sales_data(const Sales_data&);
private:
std::string bookNo;
int units_sold = 0;
double revenue = 0.0;
}
与Sales_data的合成的拷贝构造函数等价
Sales_data::Sales_data(const Sales_data &orig):bookNo(orig.bookNo),units_sold(orig.units_sold),revenue(orig.revenue)
{
}
到这里其实已经很明白了,当我们写一个拷贝构造函数其实等价于
Sales_data(const Sales_data&);
Sales_data::Sales_data(const Sales_data &orig):bookNo(orig.bookNo),units_sold(orig.units_sold),revenue(orig.revenue)
现在我们就应该理解拷贝初始化和直接初始化的差异了
//直接初始化
string dots(10,'')
string s(dots)
//拷贝初始化
string s2 = dots;
string null_book = "9999999999";
string nines = string(100,'9');
使用直接初始化的时候,我们要求编译器使用普通的函数初始化匹配。当我们使用拷贝初始化的时候,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,
如果需要的话还要进行类型转化。
拷贝初始化通常使用拷贝函数来完成。但是,如果我们将在13.6.2节,如果一个类有一个移动构造函数而非拷贝构造函数来完成。拷贝初始化依赖拷贝构造函
数或者移动构造函数就可以完成。
拷贝初始化不仅仅在我们使用定义变量的时候发生,在下面的情况下也会发生。
1.将一个对象作为实参传递给一个非引用类型的形参。
2.从一个返回类型为非引用类型的函数返回一个对象。
3.用花括号初始化一个数组中的元素或一个聚合类的成员。
上面的说法可能很抽象,所以我用一些例子来说明
1.=号赋值会触发拷贝构造函数
class OurTest
{
public:
OurTest()
{
}
OurTest(const OurTest& org)
{
a = org.a;
}
int a= 1;
};
int main()
{
OurTest test;
test.a = 100;
cout<<test.a<<endl;
OurTest test2 = test;
cout<<test2.a<<endl;
}
这个例子非常有意思,我们没有初始化通过OurTest(const OurTest& org),但是在OurTest test2 = test;的时候触发了,也就是在进行赋值等于的时
候会触发拷贝构造函数!!
2.我们在用一个例子细细体会书中的”将一个对象作为实参传递给一个非引用类型的形参”这句话是什么意思
class OurTest
{
public:
OurTest()
{
}
OurTest(const OurTest& org)
{
a = org.a;
printf("a111");
}
int a= 1;
};
class OurTest2
{
public:
void testCopy(OurTest test)
{
}
};
int main()
{
OurTest2 test2;
OurTest test;
test2.testCopy(test);
}
有意思的也发生了这会触发a111的输出结果,也就是当作为参数传入的时候也会触发拷贝构造函数,注意的点是如果我们参数是引用则不会触发
void testCopy(const OurTest& test)
void testCopy(OurTest& test)
均不会触发拷贝,这也说明了如果我们参数用引用传参速度会更快哦
3.c++的一些容器也会触发拷贝构造函数
class OurTest2
{
public:
void testCopy(const OurTest& test)
{
printf("%d\n",test.a);
printf("a111");
}
};
int main()
{
vector<OurTest> list;
OurTest a1;
list.push_back(a1);
}
我们发现也输出了a111
4.我们再看最后一种情况
class finalTest{
public:
int a;
OurTest b;
};
int main()
{
OurTest c;
finalTest data = {1,c};
}
参数和返回值
在函数调用过程当中,具有非引用类型的参数要进行拷贝初始化。类似的,当一个函数具有非引用的返回类型的时候,返回值会被用来初始化调用方法的结果。
拷贝构造函数被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。如果其参数类型不是引用类型,则调用永远不
会成功–为了调用拷贝构造函数,我们必须拷贝它的实参,但是为了拷贝实参,我们有需要调用拷贝构造函数,如此往复。
说道这里我们刚才已经做过实验作为参数,拷贝构造函数会在传参之时,如果参数不是引用就会触发,那么如果我们直接返回对象呢?
如果我们在函数内部声明变量并且直接返回,并不会触发拷贝构造函数
class OurTest2
{
public:
OurTest testCopy()
{
OurTest a;
a.a = 200;
return a;
}
};
int main()
{
OurTest2 c;
OurTest b = c.testCopy();
}
这样并不会发生什么,但是如果我们
class OurTest2
{
public:
OurTest testCopy(OurTest a)
{
a.a = 200;
return a;
}
};
int main()
{
OurTest2 c;
OurTest tetData;
OurTest b = c.testCopy(tetData);
}
这样会触发两次拷贝构造函数,第一次在传参的时候,第二次在返回的时候
编译器可以绕过拷贝构造函数
在拷贝的初始化过程中,编译器可以跳过拷贝构造函数,直接创建对象。
比如说
string null_bool("99999999")
这样的话可能被编译器忽略