C++学习笔记(拷贝、赋值、销毁)

C++学习笔记(拷贝、赋值、销毁)


在对类进行定义时,除了对类对象可执行操作等定义

还会显示或隐式地指定在此类型的对象拷贝、移动、赋值和销毁的具体操作

这些操作具体通过拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数

拷贝:

拷贝构造函数:

C++中,如果我们没有设置自定义的拷贝构造函数,编译器会自动定义一个合成拷贝构造函数(即将对象中非static成员拷贝到正在创建的对象中)

#include<iostream>
#include<vector>

using namespace std;
class initclass
{
private:
	int pri;
public:
	int pub;
	int getpri()
	{
		return pri;
	}
	initclass()
	{
		pri =1;
		pub = 1;
	};
	//initclass(const initclass& fa)
	//{
	//	this->pri = fa.pri;
	//	this->pub =fa.pub+1;
	//};//与合成拷贝构造函数等价
	initclass(const initclass& fa)
	{
		this->pri = fa.pri;//当有对象成员没有拷贝时,未拷贝的成员就是未定义值
		this->pub =fa.pub*2;

	}//自定义功能拷贝构造函数


};

int main()
{
	initclass fa;
	initclass ch=fa;//拷贝初始化
	initclass ch1(ch);//直接初始化

	cout << fa.pub<<" "<<fa.getpri()<<endl;
	cout << ch.pub << " " << ch.getpri() << endl;
	cout << ch1.pub << " " << ch1.getpri() << endl;
	return 0;

}
pri未定义:
1 1
2 -858993460
4 -858993460
所有对象都有拷贝,但pub不是直接赋值,需要*2:
1 1
2 1
4 1
不定义拷贝构造(使用合成构造函数):
1 1
1 1
1 1

直接初始化与拷贝初始化:

直接初始化:直接调用与实参匹配的构造函数;

拷贝初始化:首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将临时对象复制到创建的对象中。

一般来说,初始化尽量使用直接初始化,拷贝初始化效率会低一些。

下面看看这两种初始化方法具体流程效果:

设置类a,采用两种初始化,查看构造和拷贝构造函数调用情况

	class a
{
	
public:
	a()
	{
		val = pp;
		++pp;
		cout << "调用构造函数1" << endl;
	};
	a(int x)
	{
		++pp;
		val = pp +10;
		cout << "调用构造函数2" << endl;
	}
	a(const a& fb)
	{
		val = pp;
		++pp;
		cout << " 调用拷贝构造函数" << endl;
	}
	int val;
	static int pp;
};
int a::pp = 0;




	a aa;//直接初始化
	bb = aa;//拷贝初始化
	a c(bb);//直接初始化
	
	cout << aa.val << " " << bb.val << " " << c.val << endl;
	a d = a(10);//拷贝初始化
	cout << d.pp << " " << d.val << endl;	

结果显示:

直接初始化:aa:直接调用构造函数1;但是cc:显示调用了拷贝构造函数;

所以直接初始化不是只能调用构造函数的,拷贝构造函数也符合其与实参匹配的构造函数条件

拷贝构造化:bb:只调用了拷贝构造函数

dd:只调用了拷贝构造函数。????不对呀,怎么只调用了构造函数呢,根据定义右侧对象不是要临时构造吗?一看,哦,大意了,编译器已经对这块进行了优化,可以直接跳过拷贝构造函数,直接使用构造函数来构造对象

1 1
2 1
4 1
调用构造函数1
 调用拷贝构造函数
 调用拷贝构造函数
 
///
0 1 2
调用构造函数2
4 14

那两者区别在哪里呢?

1.有的文献和博客里面会说:使用“=”初始化的,一定是拷贝初始化,这也不一定,如果存在“=”(拷贝赋值运算符重载),就不会调用这个拷贝构造函数

2.直接初始化可以调用拷贝构造函数、或者构造函数;

拷贝初始化一定要调用拷贝构造函数。

这里会有疑问,上例中dd不是因为编译器优化,不调用拷贝构造函数吗?

咳咳,这块我也是挺疑惑,看了几个牛人博客,自己也将拷贝构造函数放入private中,再次运行时,就出现以下错误。

修改:
private:
a(const a& fb)
	{
		val = pp;
		++pp;
		cout << " 调用拷贝构造函数" << endl;
	}
结果:
/
严重性	代码	说明	项目	文件	行	禁止显示状态
错误	C2248	“a::a”: 无法访问 private 成员(在“a”类中声明)	ProjectC++_Base	d:\code_study\projectc++_base\projectc++_base\c++拷贝控制.cpp	84	

原因结论

主要是因为复制构造函数是可以由编译默认合成的,而且是公有的(public),编译器就是根据这个特性来对代码进行优化的

如果你自己定义这个复制构造函数,编译则不会自动生成,虽然编译不会自动生成,但是如果你自己定义的复制构造函数仍是公有的话,编译还是会为你做同样的优化。然而当它是私有成员时,编译器就会有很不同的举动,因为你明确地告诉了编译器,你明确地拒绝了对象之间的复制操作,所以它也就不会帮你做之前所做的优化(from http://t.csdn.cn/i0k8J)

所以说拷贝初始化,编译器还是会使用到拷贝构造函数,再这个上面再做优化,如果拷贝构造不可用,拷贝初始化就不可用了。

拷贝构造函数使用场景:

拷贝构造函数除了“=定义变量”,构造函数调用情况还有:

1.使用同一类型显式或隐式初始化一个对象;

2.对象作为实参传递给一个非引用对象

比如:使用函数FUN(a fa);
a aa;
FUN(aa);//此处会使用拷贝构造函数

3.标准库容器初始化,使用push、insert等操作时,也会调用拷贝构造函数

4.返回类型,返回一个非引用类对象;

5.花括号列表初始化一个数组元素或一个聚合类中的成员

vector<string> ss={"123454","234565"};

explict关键字:

作用:防止单参数构造函数的隐式自动转换;(不允许所修饰的构造方法,来隐式初始化对象)

class a
{
	
public:
	explicit a()
	{
		val = pp;
		++pp;
		cout << "调用构造函数1" << endl;
	};
	explicit a(int x)
	{
		++pp;
		val = pp +10;
		cout << "调用构造函数2" << endl;
	}	
	int val;
	static int pp;

	explicit a(const a& fb)
	{
		val = pp;
		++pp;
		cout << " 调用拷贝构造函数" << endl;
	}

};

	a aa;//不报错
	bb = aa;//报错
	a c(aa);//不报错
	a d = a(10);//报错

原因:aa:显示调用构造函数;bb隐式调用拷贝构造函数,编译报错

cc:显示调用拷贝构造函数;dd 隐式调用拷贝构造函数,编译报错

明显,explicit关键字修饰单参数构造函数,主要作用,禁止隐式转换、拷贝初始化(注意不是禁止拷贝构造函数!!,例子中c调用拷贝构造函数没有报错)

显式初始化:程序给定了初始化参数类型(比如例子中 c)

隐式初始化:没有给定初始化参数类型,编译器还需要隐式转换类型。

拷贝赋值运算符重载:

直接看代码

class qt
{
public:
	qt()
	{
		val1 = 5;
	}
	qt(qt&fq)
	{
		this->val1 = fq.val1;
		cout << "拷贝构造函数" << endl;
	}
	qt& operator=(const qt& fq)
	{
		this->val1 = fq.val1;
		cout << "拷贝构造运算符" << endl;
		return *this;
	}
	int val1;
};






//结果:拷贝构造函数
qt q1;
qt q2=q1;
//结果:拷贝构造运算符
qt q1;
qt q2;
q2 = q1;

结果分析:当使用“=”时,若左右值已经初始化了,将调用拷贝运算符,而不是拷贝构造函数

(这个好理解,都没有初始化,怎么去调用运算符重载函数)

析构函数:

析构函数时不能重载的,对于一个类来说是唯一的,主要作用:释放对象使用的资源。

编译器默认的析构函数是不能删除new运算符在堆中分配的对象或者对象成员,这需要程序员自定义析构函数来实现

比如:
class sa
{
public:
	sa()
	{
		n1 = new int(5);
		n2 = 5;
	}
	~sa() {
		delete n1;//必须手动去删除
	};
	static int b;
	int* n1;
	int n2;
};
int sa::b = 5;

当析构函数如果使用自定义的析构函数,或者说我们需要在析构时析构动态分配的对象时,如果使用默认的合成拷贝赋值函数与运算符,将会导致系统中断!

sa copysa(sa f)
{
	sa cf = f;
	return cf;
}

sa a1;
copysa(a1);

当运行该语句和函数时,会报错;

原因分析:使用copysa函数时,cf与f在函数作用域结束后会调用自己的析构函数;

而默认的拷贝函数是采用浅拷贝(不会给指针类型的数据分配新内存),这就导致 cf与f 的n1指针都指向了同一个对象,而在析构函数中,cf、f先后调用两次delete,这就会使得第二次的delete,已销毁的对象,导致报错。

措施:采用自定义的拷贝构造函数、运算符,对于指针类型数据进行深拷贝

	sa(sa&fa)
	{
		int *cpn1 = new int(*fa.n1);
		this->n1 = cpn1;
		this->n2 = fa.n2;
	}

加上自定义拷贝构造函数,这样就不会报错了,@-@;这也是自定义拷贝构造函数主要的使用原因。

阻止拷贝:

即使我们没有定义拷贝构造,编译器也会自动生成。其使用方法

CLASSA (const CLASSA&)=delete;//禁止拷贝
CLASSA &operator= (const CLASSA&)=delete;//禁止赋值
CLASSA()=default;//使用默认构造函数
~CLASSA()=default;//使用默认析构函数,注意析构函数不能删除

一般这个东西,平时使用情况很少的。可以了解的是,iostream类就使用了阻止拷贝,主要就是避免多个对象使用同一个IO缓冲

对象移动:

对于不能共享的资源、或者对象拷贝后很快就要销毁的情况,使用移动而非拷贝,可以提高程序的性能

左值引用与右值引用

C++为了支持移动操作,加入了一种新的引用类型:右值引用;

先看看左值与右值是啥:

特点总结:左值持久、右值短暂

左值:左值在内存一定有实体(有定义,有具体地址、名字)

右值:右值存储位置可以是寄存器也可以是在内存,但即将销毁,或者是临时值(不能取地址、没有具体名字)

左值引用: &

右值引用: &&

//左值引用
	int i = 4;
	int &j = i;
	const int &p = i * 5;
	const int &p1 = 5;
	
	//右值引用
	int &&na = i*5;
	int &&nb = 66;
	sa &nbb = sa();//报错,此处是右值,不能使用左值引用
	sa &&nc=sa();//正确,使用右值引用
	sa &&nd = nc;//报错,nc是左值,不能使用右值引用
	sa &&nd =move(nc);//move函数转换nc为右值;

标准库move函数:作用:显式将左值转换为一个右值;

nc.n1=5;//报错,在使用move后,n1就是右值,不能在进行修改,但可以使用
cout<<nc.n1;//不报错

移动构造函数与移动构造运算符

移动构造函数:与拷贝构造函数不同,其不分配任何内存,它直接接管给定右值对象的内存;将内存接管后,会将所给定的对象的指针置为nullptr,右值对象在销毁时不会释放所“窃取”的内存。

移动构造函数的第一个参数必须是右值引用,其余参数都必须有默认实参。

合成移动构造函数:只有在没有定义拷贝函数,所有成员都支持移动拷贝时,编译器才会自动生成移动拷贝函数。


class moveclass
{
public:
	moveclass()
	{
		 n1 =new int(1);
		 n2 = new int(2);
		 cout << "构造"<<endl;
	}
	moveclass(moveclass &&)noexcept; //引用参数必须是右值,noexcept 表示不抛出任何异常	
	moveclass(moveclass &fm)
	{
		int*cn1 = new int(*fm.n1);
		const int *cn2 = new int(*fm.n2);
		this->n1 = cn1;
		this->n2 = cn2;
		cout << "拷贝" << endl;
	}
	moveclass& operator= (moveclass &&fm) noexcept
	{
		if (this == &fm)
			return *this;
		n1 = fm.n1;
		n2 = fm.n2;
		fm.n1 = nullptr;
		fm.n2 = nullptr;
		cout << "  移动构造运算符" << endl;
		return *this;
	}
	~moveclass()
	{
		delete n1;
		delete n2;
		cout << "析构函数" << endl;
	}
	int* n1;
	const int* n2;
	static int n3;//静态变量不参与拷贝、构造
};
int moveclass::n3 = 3;
moveclass::moveclass(moveclass &&fm) noexcept :n1(fm.n1), n2(fm.n2)
{
	//delete fm.n1;
	//delete fm.n2;
	fm.n1 = nullptr;
	fm.n2 = nullptr;
	cout << "  移动构造函数" << endl;
}
moveclass getmc()
{
	moveclass a;
	return a;
}
moveclass getmc2()
{
return moveclass();
}



///



	cout << "移动拷贝例子"<<endl;
	moveclass ma;//直接初始化
	 ma=moveclass();
	
	 cout << "b" << endl;
	 moveclass mb= moveclass();//如果没有定义移动构造函数,会报错
	 cout << "c" << endl;
	 moveclass mc= getmc();
//moveclass mc= getmc2();
结果:
移动拷贝例子
构造
构造
  移动构造运算符
析构函数
b
构造
c
构造
  移动构造函数
析构函数
析构函数
析构函数
析构函数


///使用getmc2
c
构造
析构函数
析构函数
析构函数

结果分析:ma直接初始化,ma=右值,右值调用一次构造,=调用ma的移动构造运算符重载,右值作为临时值进行析构;

mb:可能是编译器优化,只调用一次构造,但是不能缺少移动构造函数定义;

mc:调用函数getmc中使用,构造一个临时 对象a,返回a时,调用移动构造函数对mc初始化,并析构返回值a;

调用getmc2时,值调用了一次构造,emmmm,又是编译器优化的结果

最后对ma、mb、mc析构

此外**:当一个类存在拷贝构造函数,未定义移动构造函数时,编译器不会合成移动构造函数,此时就会拷贝右值。**

三五法则:

三:定义一个类时,我们显式地或隐式地指定了此类型的对象在拷贝、赋值和销毁时做什么。一个类通过定义三种特殊的成员函数来控制这些操作:拷贝构造函数拷贝赋值运算符析构函数

五:在C++11标准中,为了支持移动语义,加入移动构造函数与移动赋值运算符,又被称为C++五法则,相比于三法则,移动在一些方面比拷贝优化;

主要特点:

析构函数不能删除;

需要析构函数的类也就需要拷贝、赋值操作

需要拷贝操作的类也需要赋值操作,反之亦然。

如果有一个类有删除的或不可访问的析构函数,其默认和拷贝构造函数就会被定义为删除的

将五个拷贝控制成员看成一个整体,一般来说只有一个类定义了其中任何一个的拷贝操作,就应该把所有的5个操作都定义。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值