OOP
拷贝构造函数
A(const A & a);
- const:防止修改
- &:不仅为了节省空间,更为了防止递归!这里是值传递。
#include <iostream>
using namespace std;
class CExample
{
int m_nTest;
public:
CExample(int x):m_nTest(x)
{
cout << "constructor with argument/n";
}
CExample(const CExample & ex)
{
m_nTest = ex.m_nTest;
cout << "copy constructor/n";
}
CExample& operator = (const CExample &ex)
{
cout << "assignment operator/n";
m_nTest = ex.m_nTest;
return *this;
}
void myTestFunc(CExample ex)
{
}
};
int main()
{
CExample aaa(2);
CExample bbb(3);
bbb = aaa;
CExample ccc = aaa;
bbb.myTestFunc(aaa);
return 0;
}
假如拷贝构造函数参数不是引用类型的话, 则是值传递。由bbb.myTestFunc可知递归的后果。
使得 ccc.CExample(aaa)变成aaa传值给ccc.CExample(CExample ex),即CExample ex = aaa,因为 ex 没有被初始化, 所以 CExample ex = aaa 继续调用拷贝构造函数,接下来的是构造ex,也就是 ex.CExample(aaa),必然又会有aaa传给CExample(CExample ex), 即 CExample ex = aaa;那么又会触发拷贝构造函数,就会永远递归下去。
什么时候需要自定义拷贝构造函数?深拷贝。
浅拷贝与深拷贝
浅拷贝案例: 如果析构掉s1,abcd也被析构,s2变成悬挂指针(指向的地址不知道存了什么东西),很不好。
#include <iostream>
#include <cstring>
using namespace std;
class String{
private:
char *p;
public:
String(char *str){
p=new char[strlen(str)+1];
strcpy(p,str);
}
~String(){
delete[] p;
}
};
int main() {
String s1("abcd");
String s2 = s1;
}
深拷贝修改:
#include <iostream>
#include <cstring>
using namespace std;
class String{
private:
char *p;
public:
String(char *str){
p=new char[strlen(str)+1];
strcpy(p,str);
cout << "constructor" << endl;
}
String(const String & c){
p=new char[strlen(c.p)+1];
strcpy(p,c.p);
cout << "copy constructor" << endl;
}
~String(){
delete[] p;
}
};
int main() {
String s1("abcd");
String s2 = s1;
}
易错1:显式调用成员对象的默认构造函数
自定义拷贝构造函数会调用成员对象的默认构造函数!因为编译器以为你懂!
所以程序员需要显式调成员对象的拷贝构造函数!
#include <iostream>
using namespace std;
class A{
private:
int x, y;
public:
A(){
x=y=0;
}
void inc(){
x++;
y++;
}
void print(){
cout << " x= " << x << " y= " << y << endl;
}
};
class B{
int z;
A a;
public:
B(){
z=0;
}
B(const B &b): a(b.a){
z = b.z;
}
void inc(){
z++;
a.inc();
}
void print(){
a.print();
cout << "z= " << z << endl;
}
};
int main() {
B b1;
b1.print();
b1.inc();
b1.print();
B b2(b1);
b2.print();
}
易错2:NRVO(命名返回值优化)
而关于ppt上表示拷贝构造函数复杂性,用于引出移动构造函数的例子,经过代码测试并没有2次或3次构造对象,其实只有一次。原因是编译器优化了返回副本。这称为 NRVO(命名返回值优化)。下面两份代码可以证明。 NRVO参考链接
#include <iostream>
using namespace std;
class X {
public:
X() { cout << "Default Constructor" << endl; }
X(const X&) { cout << "Copy Constructor" << endl; }
};
X f(X x) { return x; }
X g() {
X y;
return y;
}
int main() {
cout << "First create an object" << endl;
X a;
cout << "Call f()" << endl;
f(a);
cout << "Call g()" << endl;
g();
}
这里主要证明了NRVO中函数或 catch 子句参数不会被优化。
in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value
#include <iostream>
#include <cstring>
using namespace std;
class String{
private:
char *p;
public:
String(char *str){
p=new char[strlen(str)+1];
strcpy(p,str);
cout << " constructor " << p << endl;
}
String(const String & c){
p=new char[strlen(c.p)+1];
strcpy(p,c.p);
cout << "copy constructor " << p << endl;
}
String& operator = (const String &c)
{
cout << "assignment operator" << endl;
p = c.p;
return *this;
}
~String(){
delete[] p;
}
};
String generate(){
return String("test");
}
String generate1(){
String s("test");
return s;
}
String generate2(String s){
return s;
}
int main() {
String tmp = generate();
String S = tmp;
String S2 = generate1();
String S3 = generate2(S2);
}
为什么tmp=generate()并不会多执行一次拷贝构造?因为编译器先把tmp的指针传进generate,然后在返回的时候先把结果store进tmp的指针指向的内容,然后再调用拷贝构造函数。即构造函数时的对象指针就是用的tmp,不是额外新开的内存。
移动构造函数
参考1 参考2
若没有自定义拷贝构造、拷贝赋值、析构函数,编译器会合成默认的移动构造函数和移动赋值函数。
- 拷贝构造有变,一般移动构造也会变
- 析构函数管理额外申请的资源,编译器不负责,所以没有默认的函数
触发时机: 如果临时对象即将消亡,并且它里面的资源是需要被再利用的,这个时候我们就可以触发移动构造。 如下面的代码只有S=S2后,S3=S时使用了移动构造函数,因为S是即将消亡的临时对象,且资源可以被利用。
#include <iostream>
#include <cstring>
using namespace std;
class String{
private:
char *p;
public:
String(char *str){
p=new char[strlen(str)+1];
strcpy(p,str);
cout << " constructor " << p << endl;
}
String(const String & c){
p=new char[strlen(c.p)+1];
strcpy(p,c.p);
cout << "copy constructor " << p << endl;
}
String(String &&s):p(s.p){
s.p = nullptr;
cout << "move constructor " << p << endl;
}
String& operator = (const String &c)
{
cout << "assignment operator" << endl;
p = c.p;
return *this;
}
~String(){
delete[] p;
}
};
String generate(){
return String("test");
}
String generate1(){
String s("test");
return s;
}
String generate2(String s){
return s;
}
int main() {
String tmp = generate();
String S = tmp;
String S2 = generate1();
String S3 = generate2(S2);
}
另外,vector分配内存时也需要注意标记noexcept:
为了避免这种潜在的问题,除非vector知道元素类型的移动构造函数不会抛出异常,否则在重新分配内存的过程中,它就必须使用拷贝构造函数而不是移动构造函数。如果希望在vector重新分配内存这类情况下对我们自定义类型的对象进行移动而不是拷贝,就必须显示的告诉标准库我们的移动构造函数可以安全使用。我们通过将移动构造函数(及移动赋值运算符)标记为noexcept来做到这一点。——《C++ primer》
下面是有noexcept的情况:
#include<iostream>
#include<vector>
#include<string>
using namespace std;
class Test
{
public:
Test(const string& s = "hello world") :str(new string(s)) { cout << "constructor" << endl; };
Test(const Test& t);
Test& operator=(const Test& t);
Test(Test&& t) noexcept;
Test& operator=(Test&& t) noexcept;
~Test();
public:
string * str;
};
Test::Test(const Test& t)
{
str = new string(*(t.str));
cout << "copy constructor" << endl;
}
Test& Test::operator=(const Test& t)
{
cout << "assignment" << endl;
return *this;
}
Test::Test(Test&& t)noexcept
{
str = t.str;
t.str = nullptr;
cout << "move constructor" << endl;
}
Test& Test::operator=(Test&& t)noexcept
{
cout << "move assignment" << endl;
return *this;
}
Test::~Test()
{
cout << "destructor" << endl;
}
int main()
{
vector<Test> vec(1);
Test t("what");
vec.push_back(std::move(t));
vec.push_back(t);
return 0;
}
当不写noexcept时:
- vec.push_back(t);//都是copy constructor
- vec.push_back(std::move(t));//第一个是move 第二个是copy 因为move显式将左值变右值 调移动构造函数
|