C++11基础语法知识总结(四)

本文详细介绍了C++11中的拷贝控制,包括拷贝构造函数、拷贝赋值运算符、析构函数、三/五法则,以及=delete和=defaukt的作用。此外,还探讨了深拷贝和浅拷贝的概念,以及对象移动、右值引用和移动构造函数的重要性,阐述了如何通过移动语义提高性能。
摘要由CSDN通过智能技术生成

类的拷贝控制

一、拷贝构造函数

拷贝构造函数必须满足:
第一个参数是自身类型的引用,且任何额外参数都有默认值。

1.1、合成拷贝构造函数

如果没有定义一个类拷贝构造函数,则编译器会合成一个拷贝构造函数。
一般情况下,合成拷贝构造函数会将其除static成员的其他所有参数逐个拷贝到正在创建的对象中。

1.2、拷贝初始化

string dots(10, '.');//直接初始化,调用构造函数
string s(dots);//直接初始化,调用拷贝构造函数
string s3 = "string";//拷贝初始化,char*隐式转化为string对象,再拷贝s3。
string s4 = string(100, '9');//拷贝初始化

a、直接初始化:从上面的例子中可以看出,直接显示调用类的构造函数,包括拷贝构造函数都属于直接初始化。
b、拷贝初始化:采用“=”赋值运算符,将右边的运算对象通过调用拷贝构造函数拷贝或移动构造函数移动到正在创建的左边对象中。拷贝初始化可能会涉及类类型的隐式转换。

用explicit关键字声明的构造函数不能用于存在隐式转换的拷贝初始化。

c、拷贝初始化不仅在用“=”是会发生,在其他情况下也会发生

1)将一个对象作为实参传递给一个非引用类型的形参
2)从返回类型为非引用类型的函数返回一个对象
3)用花括号列表初始化一个数组中的元素或一个聚合类的成员

d、为什么拷贝构造函数参数必须是引用类型:
在函数调用过程中,具有非引用类型的参数要被拷贝初始化;在一个函数返回时,具有非引用的返回类型时,返回值会被用来拷贝初始化调用方的结果。
因为如果参数不是引用类型,则它为了调用拷贝构造函数要拷贝它的形参,但为了拷贝形参,又需要调用拷贝构造函数,一直循环,导致失败。

e、编译器可以绕过拷贝构造函数
拷贝初始化:

string null_book = "string";//构造+拷贝

编译器优化:

string null_book("string");//构造

但即使编译器略过了拷贝构造函数,在这个程序点上,拷贝构造函数必须是存在且可访问的

二、拷贝赋值运算符

2.1、重载赋值运算符

如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this指针参数。对于一个二元运算符,其右侧运算对象作为显式参数传递。
赋值运算符通常应该返回一个指向其左侧运算对象的引用。

2.2、合成拷贝赋值运算符

一个类未定义自己的拷贝赋值运算符,编译器会生成一个合成拷贝赋值运算符。拷贝赋值运算符会将右侧运算对象的每个非jingt唉成员赋予左侧运算对象的相应成员。,然后返回一个指向左侧运算对象的引用。

class Screen
{
public:
	explicit Screen(const string& b):a(b){}
	string a;
};
	Screen sc1(s4);
	Screen sc2(s);
	sc2 = sc1;//调用合成拷贝赋值运算符

三、析构函数

1、析构函数没有返回值,也不接受参数。因此析构函数不能被重载,一个给定类只会有唯一一个析构函数。

2、析构函数所作的工作:首先执行函数体,然后销毁成员,其中成员按初始化顺序的逆序销毁。函数体通常会释放对象在生存期分配的所有资源(比如在堆上new的空间)。

3、析构函数销毁成员的做法完全依赖成员的类型。销毁类类型的成员需要执行成员的析构函数;销毁内置类型则任何事情都不做。

隐式销毁一个内置指针类型的成员不会delete它所指向的对象。

4、调用析构函数的时机:
1)变量在离开其作用域时被销毁
2)当一个对象被销毁时,其成员被销毁
3)容器被销毁时,其元素被销毁。
4)对于动态分配的对象,当指向它的指针应用delete运算符时被销毁。
5)对于临时对象,当创建它的完整表达式结束时被销毁。

5、合成析构函数
当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数,这个析构函数的函数体为空。所以当如果类中有指向堆中空间的内置指针类型,就不能用合成析构函数,需要显式地定义析构函数,且函数体中必须调用delete运算符。

四、三/五法则(类的拷贝操作定义数量)

1、需要定义析构函数的类也需要拷贝和赋值操作
举例:
类中含有new返回的内置指针类型,都需要显式定义析构函数对指针进行delete,这时如果类采用默认的拷贝构造函数和拷贝赋值运算符,在拷贝这个对象时只会拷贝指针变量,不会拷贝指针所指内容,从而造成多个指针指向同一块内存,这就可能造成指向同一块内存的指针delete多次,造成错误。
2、需要拷贝操作的类也需要赋值操作,反之亦然
举例:
一个类为每个对象分配一个唯一的序号。这是类的拷贝构造函数要为新创建的对象生成一个新的、独一无二的序号,其他成员都直接从给定对象中拷贝。这时如果采用默认拷贝赋值运算符,在赋值时就会导致新对象的序号和给定对象相同。

五、=default

将拷贝控制成员定义为=default来显式要求编译器生成合成的版本。

a、类的构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动拷贝函数、析构函数都会有默认版本。除拷贝构造函数以外,其他所有函数在显式重载后,编译器就不会合成相应默认版本了。而使用=default可以在定义其他类型后依然要求编译器合成相应的版本。
当在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联的,如果不希望是内联的,则应该在类外定义时使用=default。

b、=default有什么作用呢?
举例:
一个类A,派生出类B,类A中只有有参数的构造函数,在类B的构造函数中没有显式调用类A的有参构造函数,则当用户使用类B的构造函数时,它会去调用类A的默认构造函数,这就会报错。如果声明一个无参构造函数+=default可以让编译器仍然合成一个默认构造函数,保证调用成功。

c、以构造函数为例,=default和显式地写不加参数的构造函数有什么区别呢?
使用default指示的办法,可以生成比用户自定义的无参构造函数更优的代码,而且可以让使用者一眼就可以看出这是一个合成版本的构造函数。

六、阻止拷贝

6.1、使用=delete将函数定义为删除的函数

1、与=default不同,=delete必须出现在函数第一次声明时,因为编译器要一开始就知道这个函数是删除的,以便禁止使用它的操作。
与=default另一个不同是,可以对任意函数指定=delete。
2、析构函数不能是删除的成员
3、合成的拷贝控制成员可能是删除的(详见C++primer P450)
如果一个类有数据成员不能默认构造、拷贝、赋值或销毁,则对应的合成拷贝控制成员将被定义为删除的。

6.2、private拷贝控制

类通过将拷贝构造函数和拷贝赋值运算符声明为private的来阻止拷贝。但是友元和成员函数仍旧拷贝对象。为此,可以将拷贝控制成员声明为private的,但不定义他们。这样的话,试图拷贝该对象的代码会在编译阶段被标记为错误,成员函数或友元函数中的拷贝操作将会在链接时报错。

七、浅拷贝和深拷贝

类的拷贝行为看上去可以分成两种,行为像值(深拷贝),行为像指针(浅拷贝)。

7.1、深拷贝(行为像值的类)

为了实现像值的行为,对于类所管理的资源要采用深拷贝,即每个对象对资源都要有一份自己的拷贝,而不是共享一份。

1、类值拷贝构造函数
类中含有一个指针指向动态内存,在定义拷贝构造函数时必须重新申请动态内存,并将原对象所管理的动态内存中内容拷贝到新对象的动态内存中。

2、类值拷贝赋值运算符
再将=右侧的对象的成员拷贝到左侧对象中时,应该采用类似拷贝构造函数一样的深拷贝。同时,为了避免对象的自赋值(=两边的对象管理同一个动态内存)时出错,应该拷贝右侧对象动态内存到临时变量中,再释放左侧对象动态内存,然后再用临时变量进行赋值。

class HasPtr
{
public:
	//构造函数
	HasPtr(const string& s)
		:ps(new string(s)), i(0) {};
	//拷贝构造函数
	HasPtr(const HasPtr& p):
		ps(new string(*p.ps)),i(p.i){}**//深拷贝,拷贝指针所指内容**
	HasPtr& operator=(const HasPtr& rhs)
	{
		**//深拷贝,采用临时变量,防止自赋值导致错误**
		auto newp = new string(*rhs.ps);
		delete ps;
		ps = newp;
		i = rhs.i;
		return *this;
	}
private:
	string *ps;
	int i;
};

7.2、(行为像指针的类)浅拷贝

对于这种类,拷贝构造函数和拷贝赋值运算符只是拷贝指针成员本身而不是它所指向的资源。但是这个时候就可能出现问题。当两个对象同时指向一个资源,其中一个对象将资源释放了,那另一个类在使用时就可能会出错。因此就需要采用引用计数,只要有对象还在引用这个资源就不释放(类似智能指针)。、

如果将引用计数保存在每个对象中,那当一个该类的对象从另一个对象中拷贝时,该对象引用计数变化了,又该怎样让其他指向该资源的对象做出相应变化呢?

因此,要将引用计数器保存在一个动态内存中,该类的对象只是保存指向这个引用计数器对象的指针,这样在拷贝和赋值时,只需将这个计数器的指针复制到另一个对象中,同时将计数器中的引用计数进行相应的改变即可,和智能指针实现相同。

class HasPtr
{
public:
	//构造函数
	HasPtr(const string& s)
		:ps(new string(s)), i(0),use_count(new size_t(1)) {};
	//拷贝构造函数
	HasPtr(const HasPtr& p):
		ps(p.ps),i(p.i)//浅拷贝
	{
		*use_count++;//引用计数增加
	}
	HasPtr& operator=(const HasPtr& rhs)
	{
		//先执行引用计数自增,防止自赋值
		(*rhs.use_count)++;
		if (--*use_count==0)
		{
			delete ps;
			delete use_count;
		}
		ps = rhs.ps;
		i = rhs.i;
		use_count = rhs.use_count;
		return *this;
	}
	~HasPtr()
	{
		if (--*use_count==0)
		{
			delete ps;
			delete use_count;
		}
	}
private:
	string *ps;
	int i;
	size_t *use_count;
};

7.3、copy and swap技术

对于类似指针的类,应该定义新的swap()函数,从而在拷贝时只需浅拷贝,有优化效率。
在实际中,shared_ptr的拷贝赋值运算符就是使用了自定义的swap()函数,这种技术叫copy and swap技术。

shared_ptr& operator=(const shared_ptr& _Right) noexcept
	{	// assign shared ownership of resource owned by _Right
	shared_ptr(_Right).swap(*this);
	return (*this);
}

从源码中可以看出,这种copy and swap技术自动处理了自赋值,而且天然是异常安全的。

八、对象移动

在某些情况,对象拷贝后就立即被销毁了,这是,移动而非拷贝对象就会大幅度提升性能。I/O类和unique_ptr类可以移动但不可以拷贝。

8.1、右值引用

1、右值引用就是必须绑定到右值的引用

2、返回左值的表达式:返回左值引用的函数、赋值运算符、下标运算符、解引用运算符、前置递增/递减运算符

3、返回右值的表达式:返回非引用的函数、算术运算符、关系运算符、位运算符、后置递增/递减运算符

4、不能将一个左值引用绑定到一个右值上,但可以将一个const的左值引用或者右值引用绑定到一个右值上。

5、左值持久,右值短暂。使用右值引用的代码可以自由地接管所引用的对象的资源。

6、变量是左值,即使这个变量是右值引用类型,因此不能将右值引用绑定到一个变量上。

7、标准库move函数
使用move函数可以显式地将一个左值转换为对应类型的右值引用类型。

int&& r2 = std::move(r1);

使用move函数后,r1的值是不被保证的,因此不能使用,只能对r1这个变量赋值或销毁它。

8.2、移动构造函数和移动赋值运算符

8.2.1、移动构造函数

1、移动构造函数和拷贝构造函数一样,只能有一个无默认实参的形参,且这个形参必须是该类类型的右值引用。移动构造函数除了要完成资源的移动,还要保证移后源对象销毁不会损坏资源。

	HasPtr(HasPtr&& rhs)noexcept
		:ps(rhs.ps),i(rhs.i),use_count(rhs.use_count)
	{
		rhs.ps = nullptr;
		rhs.use_count = nullptr;
}

2、移动操作、标准库容器、异常
在上述例子中移动操作通常不会抛出异常,应该将此事通知标准库,否则标准库会认为它可能抛出异常,从而做一些额外的工作。
通过在函数参数列表后面加noexcept,可以告诉编译器一个函数不抛出异常。移动构造函数和移动赋值运算符通常不抛出异常,但抛出异常是允许的。而移动一个对象通常会改变它的值,如果在移动了部分元素抛出异常就会产生问题。
标准库为了避免这种问题,除非vector知道元素类型的移动构造函数不会抛出异常,否则在重新分配内存时,它就必须使用拷贝构造函数而不是移动构造函数。因此,为了使用移动构造函数就必须告诉编译器我们的移动构造函数不会抛出异常。

8.2.2、移动赋值运算符
HasPtr& operator=(HasPtr&& rhs) noexcept
{
if (this != &rhs)
{
	delete ps;
	delete use_count;
}
ps = rhs.ps;
i = rhs.i;
use_count = rhs.use_count;
rhs.ps = nullptr;
rhs.use_count = nullptr;
return *this;
}

这里参数即使是右值引用,依然要检查自赋值。因为传入的右值可能是move函数调用返回的结果。不检查的话仍然会释放掉资源。

8.2.3、合成的移动操作

1)合成的移动操作定义的时机
只有当一个类没有定义任何自定义版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值,编译器才会为它合成移动构造函数和移动赋值运算符。

2)什么时候合成移动构造函数会被定义为删除的?
a、如果显式要求编译器生成=default的合成移动构造函数,而编译器又不能移动所有的成员,则这个合成的移动构造函数会被定义为删除的。
b、如果一个类定义了自己的拷贝构造函数而未定义移动构造函数,则移动构造函数定义为删除的。

3)如果一个类定义了自己的移动构造函数或移动赋值运算符,则该类的合成拷贝构造函数或合成拷贝赋值运算符会被定义为删除的。

8.2.4、右值传参

在使用构造函数构造类的对象时传入一个右值,如果类定义了移动构造函数和拷贝构造函数,会精确匹配到移动构造函数。但如果只定义了拷贝构造函数,则会调用拷贝构造函数,将右值引用转换为一个const左值引用。

8.2.5、移动迭代器

通过调用标准库的make_move_iterator函数,可以将一个普通迭代器转换为移动迭代器,这可以更好地支持在vector扩容时的元素从旧内存向新内存的移动。

8.3、右值引用与成员函数

8.3.1、右值引用与函数重载

一个成员函数也可以同时定义拷贝和移动版本,这两者是会触发重载的。以vector的push_back函数为例:

void push_back(const _Ty& _Val)
	{
	emplace_back(_Val);
	}

void push_back(_Ty&& _Val)
	{	
	emplace_back(_STD move(_Val));
	}

这两个函数唯一的不同就是一个版本接受一个const T&,一个版本接受T&&。当我们调用push_back函数,传入参数为一个右值时,这和右值形参版本的push_back函数精确匹配。

8.3.2、右值和左值引用成员函数

新标准中,为了阻止类重载的赋值运算符=出现向右值赋值的情况,可以类似const关键字,在重载赋值运算符的参数列表后面加一个引用限定符来强制左侧运算对象是一个左值。

class Foo
{
	//只有类的左值对象可以调用
	Foo &operator=(const Foo &rhs)&
	{
		i = rhs.i;
		return *this;
	}
	int i;
};

这个引用限定符类似const,也是改变this指针的属性,将this指针所指对象确定为左值
当同时加const和引用限定符时,顺序如下:

Foo somemem() const &{}//const必须在&之前
8.3.3、重载和引用函数

类似const,右值引用函数和左值引用函数是可以触发重载的。

class Foo
{
public:
	//用于右值对象调用
	Foo sorted() && 
	{
		cout << "hello";
		return *this;
	};
	//可同时用于左值和右值对象调用
	Foo sorted() const & 
	{
		cout << "hello";
		return Foo();
	};
};

//版本1

void Func(Foo &&fom)
{
	(std::move(fom)).sorted();
}

//版本2

void Func(Foo &fom)
{
	fom.sorted();
}
Foo fom;
	Func(fom);//调用版本2
	Func(std::move(fom));//调用版本1

!!!需要注意的点:
a、const &版本返回的类型必须是非引用类型,因为这个版本是左值对象调用,这就意味这不能改变源对象的状态,因此必须生成一个副本,在副本上进行操作,然后返回副本的值。
b、同样由于这个版本是左值对象调用,不能源对象的值,const关键字也是必须的。这样才能保证this指针所指的源对象是const的。
c、&&版本也要返回非引用类型,但可以直接在源对象上进行操作,可以改变源对象的状态。在返回时,由于源对象的状态无法保证(本身是右值,调用后会销毁,如果是左值加move函数则源对象内容已改变),所以也要返回改变后对象的值,而不能是引用。
d、如果一个成员函数有引用限定符,则具有相同参数列表的其他版本都必须有引用限定符。这和const是不同的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值