C++为我们提供了类作为自定义类型的实现,一个基本的类可以没有相应的功能操作函数,但是必须具备构造函数、复制构造函数、赋值操作符定义与析构函数。如果我们没有定义,那么编译器会使用以上函数的默认版本。复制构造函数、赋值操作符和析构函数总称为“复制控制”,也就是我们今天要探讨的主题。其中复制构造函数负责由一个已有的对象产生一个新的对象;析构函数负责回收或者释放对象资源;而赋值操作符则是【析构函数+复制构造函数】的结合版本。下面我们分别来探讨她们。
一、复制构造函数
C++编译器为我们提供的默认复制构造函数就像我们习以为常认为的一样:对于目标对象进行逐个成员的复制,对于类成员调用其提供的复制构造函数,对于内置类型和复合类型则直接进行复制。一般来说复制构造函数会发生在以下情况:
1. 对象初始化与对象复制
这里主要涉及两种对象的定义形式:
string str1 = "100000";
string str2("9999");
string str3(str2);
这里的区别想必诸位C++的大牛们早已耳熟能详了,关键在于第一种形式先构造一个string对象str1,然后再利用复制构造函数将"100000"复制到str1中;第二种则是调用构造函数在生成对象str2的同时即刻进行了初始化"9999"。效率上来说当然后者更高,也是我们提倡的初始化方式。第三种则显式调用了复制构造函数将str2复制给str3。
2. 作为函数的形参或者返回值
当函数的形参或者返回值为类类型时,由复制构造函数进行复制。如:
string make_plural(size_t, const string&, const string&); //注意这里的引用不会调用复制构造函数
3. 初始化顺序容器中的元素
4. 根据元素初始化式列表初始化数组元素
总而言之,复制构造函数发生在两个对象之间的信息传递上,只要二者不是同一的,内存中不是同一个对象,发生信息交换时就会触发复制构造函数。其实复制构造函数的用法并不复杂,关键是判断何时需要编写自己的复制构造函数。默认的复制构造函数执行成员对象的复制,但是却不能处理特殊的情况,比如想在复制发生的过程中设定更多的操作;或者包含指针成员。在后面的部分我们会分别说明这两种情况。下面先看一个简单的复制构造函数的例子体会体会:
-
#include <iostream>
-
#include <cstdlib>
-
-
using namespace std;
-
-
class A
-
{
-
public:
-
A():x(0),y(0){};
-
A(int a, int b):x(a),y(b){};
-
A(A& rA):x(rA.x), y(rA.y){};
-
void print()
-
{
-
cout << this<< "--->"<<x<<'\t'<<y<<endl;
-
}
-
private:
-
int x;
-
int y;
-
-
};
-
-
int main()
-
{
-
A obj_a1(100,200);
-
A obj_a2; //Cannot been written to A obj_a2();
-
obj_a1.print();
-
obj_a2.print();
-
-
A obj_b(obj_a1);
-
obj_b.print();
-
-
system("pause");
-
return 0;
-
-
}
这里是利用this的值来表征不同的对象的,可以看到成功实现了第一个对象到第三个对象的成员复制。除此以外还有两点需要特别说明:
<1>. class A obj-A()是错误的,C++编译器会将其视为一个函数而非定义一个类对象,若想调用默认构造函数只能使用class A obj-A的形式;
<2>. 如果想禁止复制构造函数的使用,就要定义自己的复制构造函数(屏蔽默认复制构造函数),让后将其放在private标号中
二、赋值操作符与析构函数
赋值操作符其实是根据自己的类特点对赋值运算进行重载,通过这种方法可以对"="所表示的操作重新定义。当定义赋值操作符时,只需要提供rhs操作数,lhs自动锁定为*this,如:A& operator=(const A&)
析构函数的一个用途是自动回收资源。一般来说撤销类对象时会自动调用析构函数来释放类的成员,但是对于在程序运行过程中动态分配的对象,则只有指向该对象的指针被删除时才可以撤销,由此容易导致内存泄漏问题。因此应将动态对象的释放写入到析构函数中去。
三、一个消息处理的示例:何时应当自定义复制构造函数(1)
前面已经说到,复制构造函数使用的最大难点不是自身,而是判断何时需要自定义复制构造函数来取代C++编译器的默认版本。一个原则是当你需要在复制构造一个新对象发生时比单纯的成员数据复制产生更多操作时,你就应当定义自己的复制构造函数以实现这些操作。下面是一个简单的消息处理的例子,类message负责存储消息以及一个所属目录的指针set,每个消息可以同时属于多个目录;每个目录中都需要维护一个自己所有的消息指针。为了定义一个类,我们最起码应当定义其所有的四种基本函数:
<1> 构造函数:创建新的message对象时,指定消息的内容而不需要指定所属的目录,调用save函数将message放入一个目录
<2> 复制构造函数:复制操作发生时,不仅要复制原先的消息内容和目录指针集合,还必须将目标消息的指针添加进源消息所有的目录对象的消息指针库中
<3> 赋值发生时,首先要在lhs消息所属目录的消息指针库中消除消息对象的指针,然后调用复制构造函数将rhs的消息复制到lhs消息,最后将rhs消息所属目录的消息指针库中添加新的消息指针
<4> 析构发生时,更新指向析构消息的目录;然后释放该对象的空间
经过以上简单的分析,我们已经开始着手构建我们的类message了:
-
class message
-
{
-
public:
-
message(const std::string &str = ""):contents(str){}; //构造函数,提供形参默认值
-
message(const message&); //复制构造函数
-
message& operator=(const message&); //赋值操作符
-
~message(); //析构函数
-
void save(dir&);
-
void remove(dir&);
-
private:
-
std::string contents;
-
std::set<dir*> dir;
-
void put_msg_in_dir(const std::set<dir*>);
-
void remove_msg_from_dir();
-
};
-
-
message::message(const message&m):contents(m.contents),dir(m.dir)
-
{
-
put_msg_in_dir(dir)
-
}
-
-
void message::put_msg_in_dir(const set<dir*> &rhs)
-
{
-
for(std::set<dir*>::iterator beg = rhs.begin(); beg != rhs.end(); beg++)
-
(*beg)->addmsg(this); //addmsg is class dir's function
-
}
-
-
message& message::operator=(const message &rhs)
-
{
-
if (&rhs != this) {
-
remove_msg_from_dir();
-
contents = rhs.contents;
-
dir = rhs.dir;
-
put_msg_in_dir(rhs.dir);
-
}
-
return *this;
-
}
-
-
void message::remove_msg_from_dir()
-
{
-
for(std::set<dir*>::iterator beg = dir.begin(); beg != dir.end(); beg++)
-
{
-
(*beg)->remsg(this); //remsg is class dir's function
-
}
-
}
-
-
message::~message()
-
{
-
remove_msg_from_dir();
-
}
一般来说赋值操作符要完成复制构造函数和析构函数也要完成的工作,建议将通用的工作放在private实现函数中。
四、管理指针成员:何时应当自定义复制构造函数(二)
除了上面所说的情况,默认的复制构造函数不能提供我们满意的操作意外,含有指针成员的类的复制构造函数也要特别小心。因为默认的复制构造函数是复制指针的值而非指针所指对象的数值,因此容易出现多个指针指向同一个对象的情况,由此容易出发悬垂指针的问题。对于含有指针成员的类的复制构造函数的处理一般有三种策略:
1. 继续采用默认构造函数
这样做当然是最简便的,但是由于没有使用任何的改进措施,因此具有指针问题本身所有的缺陷,当然也不能避免悬垂指针的出现。当然如果你能找到替代指针的方法,最好使用容器的迭代器来实现。
2. 采用智能指针
听着玄乎,说白了就是引入了一个计数器。像windows/linux系统对象一样,每个内核对象都有的自己的计数器,计数器为0时就释放该对象。这里将计数器和目标对象的指针封装成一个计数类,声明为自己的类为其友元之后就可以在自己的类中使用这个计数类了。其实就是自己类中的指针->计数类->目标对象。
-
class U_Ptr //定义计数类
-
{
-
friend class A;
-
int *ip;
-
size_t use;
-
U_Ptr(int *p):ip(p), user(1) {}
-
~U_Ptr() {delete ip;}
-
}
-
-
class A
-
{
-
public:
-
A(int *p, int i):ptr(new U_Ptr(p), val(1)) {} //成员ptr初始化为新分配对象U_Ptr(p),p为目标对象的指针
-
A(const A &obj):ptr(obj.ptr), val(obj.val)
-
A& operator=(const A&);
-
~A() {if (--ptr-use == 0) delete ptr;}
-
private:
-
U_Ptr *ptr;
-
int val;
-
}
3. 定义值类型
这种情况最为保险,类似于参数的值复制,这里直接为指针所指向的对象提供一份副本,前后两个指针指向独立的对象,当然不会相互影响了。
PS:这部分内容自己看的比较仓促,先了解了一个大概,后期编程中碰到时再回过头来好好仔细研究巩固一下
阅读(241) | 评论(0) | 转发(0) |