拷贝构造函数的应用场景

//Test.h
class Test
{
public:
	Test() {
		cout << "Test()" << endl;
		m_x = 0;
		m_y = 0;
	}
	Test(int x, int y) {
		cout << "Test(int x, int y)" << endl;
		m_x = x;
		m_y = y;
	}
	Test(const Test& another) {
		cout << "Test(const Test& another)" << endl;
		m_x = another.m_x;
		m_y = another.m_y;
	}
	~Test() {
		cout << "~Test" << endl;
	}
	void printT() {
		cout << "m_x= " << m_x << " m_y= " << m_y <<endl;
	}
	void operator=(const Test& another) {
		cout << "operator=(const Test& another)" << endl;
		m_x = another.m_x;
		m_y = another.m_y;
	}
private:
	int m_x = 0;
	int m_y = 0;
};

Test1

//析构函数调用顺序和构造顺序相反,先构造的后析构,类似栈先进后出
void test1() {
	//调用有参和拷贝构造函数
	Test t1(10, 20);
	Test t2(t1); //Test t2 = t1;
}

  • 有参构造->拷贝构造->t2析构->t1析构
  • 注意析构函数调用顺序和构造顺序相反,先构造的后析构,类似栈先进后出。

alt

Test2

void test2() {
	//调用有参、无参和=重载操作符
	Test t1(10, 20);
	Test t2;
	t2 = t1;
}
  • 有参构造->非默认无参构造->重载=运算符->t2析构->t1析构

alt

Test3

void func(Test t) { //Test t = test3::t1;
	cout << "func begin" << endl;
	t.printT();
	cout << "func end" << endl;
}

void test3() {
	cout << "Test3 begin..." << endl;
	Test t1(10, 20);
	func(t1);
	cout << "Test3 end..." << endl;
}

  • 有参构造->func函数实参传递值给形参发生只拷贝调用拷贝构造->func形参t先析构->t1析构

alt

Test4

Test func2() {
	Test temp(10, 20);
	return temp; //匿名对象=temp 匿名对象.拷贝构造(temp)
}
void test4() {
	func2();//当一个函数返回一个匿名对象的时候,函数外部没有任何变量接受它,编译器会直接将这个匿名对象回收,而不是等待整个函数执行完毕再回收
	//此时匿名对象已经回收
	cout<< "test4 end";
}
  • temp有参构造->func2 return时匿名对象拷贝temp调用拷贝构造->temp析构->test4内返回的匿名对象没有被接收立刻析构->test结束
  • 当一个函数返回一个匿名对象的时候,函数外部没有任何变量接受它,编译器会直接将这个匿名对象回收,而不是等待整个函数执行完毕再回收

alt

Test5

Test func2() {
	Test temp(10, 20);
	return temp; //匿名对象=temp 匿名对象.拷贝构造(temp)
}
void test5() {
	Test t1 = func2();//会不会触发t1的拷贝构造? 不会,匿名对象直接转正,匿名改个名字
	cout << "test5 end" << endl;
}
  • 有参构造->func2 return时匿名对象拷贝temp调用拷贝构造->temp析构->test5结束时t1析构
  • 和test4的区别在于,test5返回的匿名对象被接收,不会调用析构释放匿名对象,匿名对象已经转正
  • return时返回的匿名对象如果有变量接受,则不会再次调用拷贝构造,匿名对象改名后直接转正

alt

Test6

Test func2() {
	Test temp(10, 20);
	return temp; //匿名对象=temp 匿名对象.拷贝构造(temp)
}
void test6() {
	Test t1;
	t1 = func2();//func2返回的匿名对象立刻被析构
}
  • t1无参构造->temp有参构造->func2返回匿名对象调用拷贝构造->temp析构->重载运算符拷贝->func2返回的匿名对象立刻被析构->t1析构
  • 注意t1 = func2()发生的是操作符重载的赋值,匿名对象没有被接收,立刻被析构

alt

PS:test4,5,6发生的返回对象时调用拷贝构造函数产生匿名对象应该避免,返回类对象应该用引用。

NRVO/RVO 返回值优化

结合拷贝构造函数的匿名对象转正展开。

alt


#include <iostream>
class MyObject {
public:
    MyObject() {
        std::cout << "Constructor" << std::endl;
    }
    MyObject(const MyObject& other) {
        std::cout << "Copy Constructor" << std::endl;
    }
    ~MyObject() {
        std::cout << "Destructor" << std::endl;
    }
};

MyObject CreateObject() {
    //MyObject obj;
    return MyObject(); //返回匿名对象
    //return obj; // 这里返回临时对象
}

int main() {
    MyObject result = CreateObject();
    return 0;
}

  • 如果被调用的函数返回值是一个函数内局部(临时)对象,不开启NRVO,则会发生一次拷贝构造函数,拷贝给函数调用点的匿名对象,此时直接转正(VS是这样的),开启了就只有一次构造,一次析构。

    alt

  • 如果被调用的函数返回值是匿名对象,此时RVO优化直接转正(VSDebug或者Release都默认开启,关不了),一次拷贝构造都不会进行。

    alt

  • 总结来说,无论是返回具名还是匿名对象,完全不优化会经过往函数外传拷贝一次,调用点传给等号左端拷贝一次,如果开启了RVO/NRVO,那一次拷贝构造都不执行,在VS里,Debug下回关闭NRVO,开启RVO,但默认NRVO也只靠背一次,调用单传给等号左端拿一次被自动优化了。

详解去看看,和深入探索C++对象书。

浅拷贝深拷贝

alt

如果类的成员含有指针,就需要显示提供一个拷贝构造函数完成深拷贝,避免指向堆空间已经被析构释放掉。

class Teacher {
public:
	Teacher(int id, const char* name) {
		this->id = id;
		int len = strlen(name);
		this->name = (char*)malloc(len + 1);//strlen不计算'\0',但它要占空间
		strcpy(this->name, name);
	}
	~Teacher() {
		cout << "~Teacher()"<< endl;
		//堆上开辟的空间要给他释放掉
		if (name != NULL) {
			free(this->name);
			this->name = NULL;
		}
	}
	//默认拷贝构造函数,是浅拷贝
	//Teacher(const Teacher& another) {
	//	this->name = another.name;
	//	this->id = another.id;
	//}

	//显示提供一个拷贝构造函数完成深拷贝
	Teacher(const Teacher& another) {
		int len = strlen(another.name);
		this->name = (char*)malloc(len + 1);
		this->id = another.id;
	}
	void printT() {
		cout << this->id << " " << this->name;
	}
private:
	int id;
	char* name;//对象有指针类型,那就应该显示提供一个拷贝构造函数完成深拷贝
};

void Test() {
	Teacher t1(2, "test1");
	t1.printT();
	Teacher t2(t1);//t2的默认拷贝函数
	//t2先析构,t1再析构,但此时t1->name指向的堆空间已经被释放掉,浅拷贝
}
void Test2() {
	Teacher t1(2, "test1");
	t1.printT();
	Teacher t2(t1);//深拷贝,t1和t2是两个对象

}

int main() {
	Test();
}

静态成员

class Box {
public:
	Box(int l, int w) {
		len = l;
		width = w;
	}

	int volume() {
		int v = hight * width * len;
		cout << hight << endl;
		return v;
	}
	static void ChangHeight(int h) {
		hight = h;
	}
private:
	int len;
	int width;
	static int hight;
};
int Box::hight = 100;//类外的静态成员变量要在全局定义,不能在Main里
int main() {
	Box a(5, 10);

	cout << a.volume();
	return 0;
}

new和Delete

#include<iostream>
using namespace std;

//C语言 malloc动态分配堆内存空间 malloc是个标准库函数 stdlib.h
void test1() {
	int* p = (int*)malloc(sizeof(int)); 
	*p = 10;
	if (p != nullptr) {
		free(p);
		p = nullptr;
	}
	//开辟数组
	int *array_p = (int*)malloc(sizeof(int) * 10);
	for (int i = 0; i < 10; i++) {
		array_p[i] = 1;
	}
	if (array_p != nullptr) {
		free(array_p);
		array_p = nullptr;
	}
		

}
//C++语言 new 动态分配堆内存空间 new是个关键字
int main() {
	int* p = new int;
	*p = 10;
	if (p != nullptr) {
		delete p;
		p = nullptr;
	}

	//开辟数组
	int* array_p = new int[10];//注意有[]
	if (array_p != nullptr) {
		delete []array_p; //注意加个[]表示是数组
		array_p = nullptr;
	}

	
}

命名空间

引入命名空间可能造成数重载同名,对于名字一样,参数列表还一样的,使用引用声明会报错冲突,而使用引用指示就不会,可以通过::指明调用的是哪一个函数。

#include<iostream>
using namespace std;
namespace test1 {
	void test(){
		cout << "test1";
	};
}

namespace test2 {
	using namespace test1;
	//using test1::test; 会发生冲突
	void test(){
		cout << "test2";
	}
	void doit() {
		test2::test(); //test2 test1::test那就是 test1
	}
}

int main() {
	test2::doit(); //输出test2
	return 0;
}