C++ 复制控制 & 智能指针

本文深入探讨了C++中复制控制的概念,包括复制构造函数、赋值操作符和析构函数的作用与实现。重点阐述了在类包含指针成员时自定义复制控制的必要性,并详细介绍了三种管理指针成员的方法:常规指针行为、智能指针行为和独享指针行为。通过具体代码示例展示了如何实现每种方法,旨在帮助开发者理解和实现高效的复制控制机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文主要参考《C++ Primer》第13章

每种类型,无论是内置类型还是类类型,都对该类型对象的一组(可能为空)操作的含义进行了定义。比如,两个int值可以相加,vector对象可以进行size操作,等等。

每种类型还定义了创建该类型的对象时会发生什么——构造函数定义了该类类型对象的初始化。类型还能控制复制、赋值或撤销该类型的对象时会发生什么——类通过特殊的成员函数:复制构造函数、赋值操作符和析构函数来控制这些行为。

复制构造函数(copy constructor)是一种特殊构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用复制构造函数。当将该类型的对象传递给函数或从函数返回该类型的对象时,将隐式调用复制构造函数。

赋值操作符,重载operator=,赋值操作符返回对同一类类型的引用。

析构函数(destructor)是构造函数的互补:当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。不管类是否定义了自己的析构函数,编译器都自动执行类中非static数据成员的析构函数。

复制构造函数、赋值操作符和析构函数总称为复制控制(copy control)。编译器自动实现这些操作,但类也可以定义自己的版本。

通常,编译器合成的复制控制函数是非常精炼的,它们只做必需的工作。有一种特别常见的情况需要类定义自己的复制控制成员:类具有指针成员。


为什么具有指针成员的类需要自定义复制控制成员?原因是复制指针时只复制指针中的地址,而不会复制指针指向的对象。比如,一个具有指针成员的类,调用默认复制构造函数时,复制指针,两个指针指的是同一对象,可以使用任何一个指针改变所指对象,很可能一个指针删除了所指对象,另一个指针的用户还认为该对象仍然存在。

大多数C++类采用以下三种方法之一管理指针成员:

(1) 指针成员采用常规指针型行为。这样的类具有指针的所有缺陷,但是无需特殊的复制控制。

(2) 类可以实现所谓的“智能指针”行为。指针所指向的对象是共享的,但类能够防止悬垂指针。

(3) 类采取值型行为。指针所指向的对象是唯一的,由每个类对象独立管理。

第一种方法,一个带指针成员的简单类:

// class that has a pointer member that behaves like a plain pointer
class HasPtr {
public:
	// copy of the values we're given
	HasPtr(int *p, int i) : ptr(p), val(i) {}
	// const members to return the value of the indicated data member
	int *get_ptr() const { return ptr; }
	int get_int() const { return val; }
	// non const members to change the indicated data member
	void set_ptr(int *p) { ptr = p; }
	void set_int(int i) { val = i; }
	// return or change the value pointed to, so ok for const objects.
	int get_ptr_val() const { return *ptr; }
	void set_ptr_val(int val) const { *ptr = val; }
	
private:
	int *ptr;
	int val;
};

因为HasPtr类没有定义复制构造函数,所以复制一个HasPtr对象将复制两个成员变量:

int obj = 0;
HasPtr ptr1(&obj, 42);	// int* member pointers to obj, val is 42
HasPtr ptr2(ptr1);			// int* member pointers to obj, val is 42

指针共享同一对象,其中任意一个都可以改变共享对象的值:

ptr1.set_ptr_val(42);		// sets object to which both ptr1 and ptr2 point
ptr2.get_ptr_val();			// returns 42

可能出现悬浮指针:

int *ip = new int(42);	// dynamically allocated int initialized to 42
HasPtr ptr(ip, 10);			// HasPtr points to same object as ip does
delete ip;						// object pointed to by ip is freed
ptr.set_ptr_val(10);		// disaster: The object to which HasPtr points was freed!



第二种方法,定义智能指针(smart pointer)类:

定义智能指针的通用技术是采用一个使用计数(use count),也称为引用计数(reference count),智能指针类将一个计数器与类指向的对象相关联。引用计数跟踪该类有多少个对象共享同一指针。引用计数为0时,删除对象。

唯一的创新在于决定将引用计数放在哪里,这里我们定义一个单独的具体类用以封装引用计数和相关指针:

// private class for use by HasPtr only
class U_Ptr {
	friend class HasPtr;
	int *ip;
	size_t use;
	U_Ptr(int *p) : ip(p), use(1) {}
	~U_Ptr() { delete ip; }
};

这个类的所有成员均为private,我们不希望普通用户使用U_Ptr类,所以它没有任何public成员。将HasPtr类设置为友元,使其成员可以访问U_Ptr的成员。

引用计数类的使用:

class HasPtr {
public:
	// HasPtr owns the pointer; p must have been dynamically allocated
	HasPtr(int *p, int i) : ptr(new U_Ptr(p)), val(i) {}
	//copy members and increment the use count
	HasPtr(const HasPtr &orig) : ptr(orig.ptr), val(orig.val) { ++ptr->use; }
	HasPtr& operator=(const HasPtr &rhs)
	{
		++rhs.ptr->use;	// increment use count on rhs first
		if (--ptr->use == 0)
			delete ptr;			// if use count goes to 0 on this object, delete it
		ptr = rhs.ptr;			// copy the U_Ptr object
		val = rhs.val;			// copy the int member
		return *this;
	}
	// if use count goes to zero, delete the U_Ptr object
	~HasPtr() { if (--ptr->use == 0) delete ptr; }
	// accessors must change to fetch value from U_Ptr object
	int *get_ptr() const { return ptr->ip; }
	int get_int() const { return val; }
	// change the appropriate data member
	void set_ptr(int *p) { ptr->ip = p; }
	void set_int(int i) { val = i; }
	// return or change the value pointed to, so ok for const objects
	int get_ptr_val() const { return *ptr->ip; }	// *ptr->ip is equivalent to *(ptr->ip)
	void set_ptr_val(int i) { *ptr->ip = i; }

private:
	U_Ptr *ptr;	// points to use-counted U_Ptr class
	int val;
};


第三种方法,定义值型类。

处理指针成员的另一种完全不同的方法,是给指针成员提供值语义(value sematics)。复制值型对象时,会得到一个不同的副本,对副本所做的改变不会反映在原有对象上,反之亦然。string类是值型类的一个例子。

要使指针成员表现得像一个值,复制HasPtr对象时必须复制指针所指向的对象:

/*
*	Valuelike behavior even though HasPtr has a pointer member:
*	Each time we copy a HasPtr object, we make a new copy of the 
*	underlying int object to which ptr points.
*/
class HasPtr {
public:
	// no point to passing a pointer if we're going to copy it anyway
	// store pointer to a copy of the object we're given
	HasPtr(int &p, int i) : ptr(new int(p)), val(i) {}
	// copy members and increment the use count
	HasPtr(const HasPtr &orig) : ptr(new int(*orig.ptr)), val(orig.val) {}
	HasPtr& operator=(const HasPtr &rhs)
	{
		// Note: Every HasPtr is guaranteed to point at an actual int when it's constructed;
		// we konw that ptr cannot be a zero pointer
		*ptr = *rhs.ptr;	// copy the value pointed to
		val = rhs.val;		// copy the int
		return *this;
	}
	~HasPtr() { delete ptr; }
	// accessors must change to fetch value from Ptr object
	int get_ptr_value() const { return *ptr; }
	int get_int() const { return val; }
	// change the appropriate data member
	void set_ptr(int *p) { ptr = p; }
	void set_int(int i) { val = i; }
	// return or change the value pointed to, so ok for const objects
	int *get_ptr() const { return ptr; }
	void set_ptr_val(int p) const { *ptr = p; }

private:
	int *ptr;
	int val;
};



综上,复制构造函数、赋值操作符和析构函数,统称为复制控制,这三者密切相关,如果一个类需要析构函数,则它几乎也总是需要定义复制构造函数和赋值操作符。定义复制控制函数最为困难的部分通常在于认识到它们的必要性,分配内存或其他资源的类几乎总是需要定义复制控制成员来管理所分配的资源。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值