[c++] 五种构造函数的相关行为

前言:

c++编译器为我们做了很多默认动作,这其中非常重要的一部分就是关于构造函数的。

 

默认构造函数:

默认构造函数是指 没有参数的构造函数,如果一个构造函数都没有定义,那么编译器会为我们创建默认构造函数,这个构造函数什么都不做。这个由编译器为我们创建的构造函数成为 合成的默认构造函数。

ps:所有由编译器为我们创建的构造函数都叫做 合成的 xxx 构造函数。

 

拷贝构造函数:

原型:A(const A& a)

  • 拷贝构造函数尽量不要被声明为explicit,因为很多时候拷贝构造都是隐式调用的,比如类作为函数参数进行传值调用;除非使用者知道自己在做什么!
  • 编译器始终会为类创建 合成的拷贝构造函数,不论当前是不是有其他构造函数,这点有别于默认构造函数;
  • 关闭拷贝构造函数的两个方法:1)使用 =delete;2)定义赋值构造/移动拷贝/移动赋值,这三者其中之一,而不定义拷贝构造;
  • 让拷贝构造函数为自定义行为 或者 为空白动作 的唯一方法就是显示声明和定义拷贝构造函数。

编译器生成的拷贝构造函数行为:大部分情况下,编译器生成的拷贝构造函数会将 非static成员变量挨个拷贝构造给新的对象。如果是基础类型,则直接赋值,如果是类类型,则调用类类型的拷贝构造。

注:如果成员变量中有类类型(不是指针),那么就要求这个成员变量的类类型具备拷贝构造函数,例子如下:

#include "pch.h"
#include <iostream>

class A {
public:
	A()=default;
	//A(const A& a) = delete;				//如果放开,则编译不通过
	A& operator=(const A& a) = delete;	    //禁用赋值构造不会影响 B b2(b1);这行,因为此行调用拷贝构造而不是赋值构造

};


class B {

public:
	int i;
	A a;
};

int main()
{
	B b1;
	b1.i = 10;

	B b2(b1);

    std::cout << "Hello World!\n"; 
}

 

赋值构造函数:

原型:A& operator=(const A& a)

所有特性类比拷贝构造函数的四点。

注:如果成员变量中有类类型(不是指针),那么就要求这个成员变量的类类型具备拷贝构造函数,注意,这里同样要求是具备拷贝构造,而不是要求具备赋值构造,可见编译器的默认动作都是拷贝,而不是赋值。例子如下:

#include "pch.h"
#include <iostream>

class A {
public:
	A()=default;
	//A(const A& a) = delete;				//如果放开,则编译不通过,赋值构造在进行类类型的成员变量拷贝赋值时使用的拷贝构造,而不是赋值构造
	A& operator=(const A& a) = delete;		//禁用赋值构造,不会影响B b2 = b1;这条语句,因为编译器在底层使用的是拷贝构造完成成员变量的拷贝赋值

};


class B {

public:
	int i;
	A a;
};

int main()
{
	B b1;
	b1.i = 10;

	B b2 = b1;

    std::cout << "Hello World!\n"; 
}

赋值构造函数的固定写法:

A& A::operator=(const A& a){
    ...			//成员变量挨个赋值 ,以及其他的一些想要附加的动作
    return *this;		//(!)把指向自己的指针this的值返回,即就是自己的引用
}

//注:在赋值运算符中,切记不可对static成员变量进行赋值,即上面的 ... 中不能对
//static成员做赋值,一是没意义,二是可能编译不通过,三是即便通过运行起来可能会
//有问题

 

移动拷贝构造函数:

原型:A(const A&& a)

 

移动赋值构造函数:

原型:A& operator=(const A&& a)

 

上述五个构造函数之间的关联:

正如本文开篇所属,编译器为我们做了很多隐藏的动作,如果我们搞不明白这些动作的运行机制,那么在开发的时候往往就是,丈二和尚摸不着头脑,遇到问题也是一脸懵逼。下面就来梳理一下这些默认动作的相互关联:

  • 如果用户没有定义任何构造函数,那么编译器为我们生成默认构造函数,如果自行定义了随便什么样的构造函数,那么编译器就不会为我们生成默认构造函数。假如我们也没有自行定义默认构造函数,那么所有使用此类默认构造函数的地方都将编译不通过。
  • 拷贝构造,赋值构造,移动拷贝构造,移动赋值构造,这四个构造函数编译器会为我们自动生成,除非发生如下情况:
  1. 一旦自行定义了上述四个构造函数中的任意一个,则其他三个必须也被定义,否则编译器会将其他未定义的置为 =delete;
  2. 编译器创建默认的 “移动拷贝构造” 和 “移动赋值构造” 要满足如下条件:所有非 static 都是可移动的;

 

三/五法则:

三:拷贝构造,赋值构造,析构函数,如果其中一个定义了,那么其他两个也要定义

五:拷贝构造,赋值构造,析构函数,移动拷贝构造,移动赋值构造,如果其中一个定义了,那么其他四个也要定义

是对 三 的 扩展

 

案例:

案例一:

#include "stdafx.h"
#include <vector>
#include <algorithm>

using namespace std;

class A{
public:
	A()=default;
	~A()=default;
	A(const A&& tmp){
		this->vec = tmp.vec;
	}

public:
	vector<int> *vec;

public:
	void setvec(){
		vec = new vector<int>(10, 100);
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	A a1;
	a1.setvec();

	printf("a1 -> %p\n", a1.vec);

	A a2(move(a1));				//这里用std::move来把类实例转换成右值引用

	printf("a1 -> %p\n", a1.vec);
	printf("a2 -> %p\n", a2.vec);

	getchar();
	
	return 0;
}


	输出: a1 -> 00495CA8
	       a1 -> 00495CA8		//只要未进行手动释放,那么原来的指针还是原来的位置
	       a2 -> 00495CA8 		//并没有分配新内存,而是直接指过去

案例二:

#include "stdafx.h"
#include <vector>
#include <algorithm>

using namespace std;

class A{
public:
	A()=default;
	~A()=default;
	A(A&& tmp){					//为了能释放源对象资源,这里不使用const
		this->vec = tmp.vec;
		tmp.vec = nullptr;			//手动释放
	}

public:
	vector<int> *vec;

public:
	void setvec(){
		vec = new vector<int>(10, 100);
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	A a1;
	a1.setvec();

	printf("a1 -> %p\n", a1.vec);

	A a2(move(a1));				//这里用std::move来把类实例转换成右值引用

	printf("a1 -> %p\n", a1.vec);
	printf("a2 -> %p\n", a2.vec);

	getchar();
	
	return 0;
}


        输出: a1 -> 00495CA8
               a1 -> 00000000		//原对象的资源指针已经被释放
               a2 -> 00495CA8           //并没有分配新内存,而是直接指过去

 

 

其他:

    拷贝行为 和 移动行为

如果一个类没有定义移动行为(移动拷贝构造和移动赋值构造),但是定义了拷贝行为(拷贝构造和赋值构造),那么试图通过move调用移动行为时不会成功,取而代之,会只用拷贝行为作为替代。

A a1,a2;

a1 = move(a2);        //如果没有移动赋值,那么这句话会被编译器翻译为  a1 = a2;

A a3(move(a1));        //如果没有移动拷贝,那么这句话会被编译器翻译为  a3(a1);

    左值和右值

    左值:可以被赋值的值,指内存中一块区域的代名词,即变量名
    右值:不可以被赋值的值,是一个运算结果,不对应内存中的任何一块区域,是一个表达式

    如果我们现在已经有一个左值,那么怎么把左值的实际值作为常量赋给右值引用呢???

    int i = 100;
    int &lr_i = i;
    int &&rr_i = i;            //错误
    int &&rr_i = std::move(i);    //正确
    int &&rr_i = std::move(lr_i);    //正确

    我们可以使用move函数(c++11)来实现一个动作 “把左值里的值提取来,同时把左值对应的内存销毁, 然后

    把左值的值作为右值赋值给右值引用“ ,注意这里的 “提取出来”,我们可以理解 move 动作是  “把指定内存

    单元中的值提出来作为常量” ,这样就方便理解为什么是右值了。
    
    move的伴随动作时销毁左值对应的变量。即一旦调用move,下文将无法再访问变量(实际上可以,为什么???)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章