声明,所有的朋友,如果要转我的帖子,务必注明"作者:黑啤来源:CSDN博客"和 具体的网络地址http://blog.csdn.net/nx500/archive/2007/10/24/1842509.aspx,并且我的所有 博客内容,禁止任何形式的商业用途,请大家尊重我的劳动.谢谢!
目 录
十三.复制控制.
001 当定义一个新类型时,需要显式或隐式地指定复制/赋值和撤销该类型的对象时会发生什么,这是通过定义特殊成员:复制构造函数/赋值操作符和析构函数来达到的.
如果没有显式的定义,编译器通常会为们隐式的定义.这三个成员总称为复制控制.有一种特别常见的情况需要类显式的定义自己的复制控制成员:类具有指针成员.
002 复制构造函数.只有单个形参,而且该形参是对本类类型对象的引用(通常为const),这样的构造函数称为复制构造函数.
它可以:
1.根据另一个同类型对象隐式或者显式的初始化一个对象.
2.复制一个对象,将它作为实参传给一个函数.
3.在函数返回时复制一个对象.
4.初始化顺序容器的元素.
5.根据元素初始化列表初始化数组元素.
回想:c++支持两种初始化形式:直接初始化和复制初始化,复制初始化使用=符号,直接初始化放在括号中.
对于类类型对象,直接初始化直接调用与实参类型匹配的构造函数,复制初始化总是调用复制构造函数.
复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建对象中.
string null_book = "9-999-99999-9"; // 复制初始化.
string dots(10,'.'); // 直接初始化.
string empty_copy = string(); // 复制初始化.
string empty_direct; // 直接初始化.
支持初始化的复制形式主要是为了与C的用法兼容.
通常直接初始化和复制初始化仅在低级别优化上存在用法诧异,但是对于不支持复制的类型,或使用非explicit构造函数的时候,它们有本质区别.
ifstream file1("filename"); // ok.
ifstream file2 = "filename"; // error. 复制构造函数是私有类型,不能显式调用
// 下面,只有Sales_item(const string&)构造函数不是explicit类型,且复制构造函数是公有类型才可以使用.
Sales_item item = string("9-999-9999-9");
003 已知,当形参为非引用类型时,将复制实参的值,当以非引用类型作返回值时,将返回return语句中的值的副本.
当形参或返回值为类类型时,由复制构造函数进行复制.
004 使用复制构造函数初始化顺序容器中的元素.
vector<string> svec(5); // 使用了默认构造函数和复制构造函数.
005 如果没有为类类型数组提供元素初始化式,则将用默认构造函数初始化每个元素.
如果使用常规的花括号括住的数组初始化列表来提供显式元素初始化式,则使用复制初始化来初始化每个元素.
Sales_item primer_eds[] = { string("0-201-16487-6"),
string("0-201-73460-2"),
string("0-201-26252-8"),
string("0-201-12345-1"),
Sales_item()};
006 默认的复制构造函数,采用逐个成员初始化,将对象初始化为原对象的副本.
编译器将现有对象的每个非static成员,依次复制到正创建的对象.
直接复制内置类型成员的值,类类型成员使用该类的复制构造函数进行复制,数组成员将复制数组中的每个元素.
007 定义自己的复制构造函数,接受类类型引用形参的构造函数.
class Foo {
public:
Foo();
Foo(const Foo&); // 复制构造函数,通常是const类型形参,而且该函数通常不为explicit
// ..
}
有些类必须对复制对象时发生的事情加以控制.这样的类经常有一个数据成员是指针,或者有成员表示在构造函数中分配的其他资源.
而另一些类在创建新对象时必须做一些特定工作.这两种情况下,都必须定义复制构造函数.
008 相对的,有些类就必须禁止复制,比如iostream类,此时,必须将复制构造函数显式的声明为private,这样将不允许用户代码复制该类类型的对象,编译器将拒绝任何进行复制的尝试,但是类的友元和成员仍然可以进行复制,如果这个也要禁止,就可以声明一个private复制构造函数,但不对其定义.声明而不定义是合法的,但使用未定义就会出现链接错误而失败.
009 需要复制构造函数的类一般也需要重载赋值操作符,操作符重载看下一节笔记.
010 析构函数.作为类构造函数的补充,在对象生命周期结束的时候释放构造函数申请的资源.当然,析构函数也不仅限制释放资源.
当对象的引用或指针超出作用域时,不会运行析构函数.只有删除指向动态分配对象的指针或实际对象超出作用域时(而不是引用超出作用域),才会运行析构函数.
动态分配的对象只有在指向该对象的指针被删除时才撤销.如果没有删除指向动态对象的指针,则永远不会运行该对象的析构函数,对象就一直存在,从而导致内存泄露.
如果类需要析构函数,则它也需要复制构造函数和赋值操作符,这个规则称为三法则.
同构造函数一样,如果没有显式的析构函数,编译器会隐式的生成一个析构函数,按照对象创建时的逆序撤销每个非static成员.
析构函数是个成员函数,它的名字是在类名字前加上一个~,它没有返回值,没有形参,当然也就不能重载.
011 一个消息处理示例
//
13011_message.h
#ifndef 13011_MESSAGE_H
#define 13011_MESSAGE_H
#include < string >
#include < set >
#include < iostream >
#include ' 13011_folder '
class Message {
public:
// 这个构造函数只有一个形参,并且提供默认参数,所以可以作为默认构造函数
// 该构造函数显式的初始化了contents,隐式的初始化了set<Folder*>
Message(const std::string &str = ""):contents(str){
}
// 复制构造函数
Message(const Message&);
// 重载了赋值操作符
Message& operator=(const message&);
~Message();
// 将Msg添加/删除 指定的Folder中
void save(Folder&);
void remove(Folder&);
private:
std::string contents; // 存放Msg
std::set<Folder*> folders; // 保存Msg所存放的Folders
void put_Msg_in_Folers(const std::set<Folder*>&);
void remove_Msg_from_Folders();
} ;
#endif
// 13011_message.cpp
#include ' 13011_message.h '
// 编写自己的复制构造函数时,必须显式复制需要复制的任意成员
Message::Message( const Message & m):constents(m.constents),folders(m.folders) {
put_Msg_in_Folders(folders);
}
// 通过迭代器,将msg加入到每个Folder中去
void Message::put_Msg_in_Folders( const set < Folders *> & rhs) {
for(std::set<Folder*>::const_iterator beg = rhs.begin(); beg != rhs.end(); ++beg){
(*beg)->addMsg(this);
}
}
// 赋值操作符通常要作复制构造函数和析构函数也要完成的工作,这种情况下的通用工作应放在private属性函数中
Message & Message:: operator = ( const Message & rhs) {
// 要保证对象赋值给自己的工作也能正确进行
if(&rhs != this){
// 在赋值之前,必须先从指向该Message的每个Folder中删除它
remove_Msg_from_Folders();
contents = rhs.contents;
folders = rhs.foldrs;
// 必须将该Message加到指向rhs的每个Folder中
put_Msg_in_Folders(rhs.folders);
}
return *this;
}
void Message::remove_Msg_from_Folders() {
for(std::set<Folders*>::const_iterator beg = folders.begin(); beg != folders.end(); ++beg){
(*beg)->remMsg(this);
}
}
Message:: ~ Message() {
remove_Msg_from_Folders();
}
#ifndef 13011_MESSAGE_H
#define 13011_MESSAGE_H
#include < string >
#include < set >
#include < iostream >
#include ' 13011_folder '
class Message {
public:
// 这个构造函数只有一个形参,并且提供默认参数,所以可以作为默认构造函数
// 该构造函数显式的初始化了contents,隐式的初始化了set<Folder*>
Message(const std::string &str = ""):contents(str){
}
// 复制构造函数
Message(const Message&);
// 重载了赋值操作符
Message& operator=(const message&);
~Message();
// 将Msg添加/删除 指定的Folder中
void save(Folder&);
void remove(Folder&);
private:
std::string contents; // 存放Msg
std::set<Folder*> folders; // 保存Msg所存放的Folders
void put_Msg_in_Folers(const std::set<Folder*>&);
void remove_Msg_from_Folders();
} ;
#endif
// 13011_message.cpp
#include ' 13011_message.h '
// 编写自己的复制构造函数时,必须显式复制需要复制的任意成员
Message::Message( const Message & m):constents(m.constents),folders(m.folders) {
put_Msg_in_Folders(folders);
}
// 通过迭代器,将msg加入到每个Folder中去
void Message::put_Msg_in_Folders( const set < Folders *> & rhs) {
for(std::set<Folder*>::const_iterator beg = rhs.begin(); beg != rhs.end(); ++beg){
(*beg)->addMsg(this);
}
}
// 赋值操作符通常要作复制构造函数和析构函数也要完成的工作,这种情况下的通用工作应放在private属性函数中
Message & Message:: operator = ( const Message & rhs) {
// 要保证对象赋值给自己的工作也能正确进行
if(&rhs != this){
// 在赋值之前,必须先从指向该Message的每个Folder中删除它
remove_Msg_from_Folders();
contents = rhs.contents;
folders = rhs.foldrs;
// 必须将该Message加到指向rhs的每个Folder中
put_Msg_in_Folders(rhs.folders);
}
return *this;
}
void Message::remove_Msg_from_Folders() {
for(std::set<Folders*>::const_iterator beg = folders.begin(); beg != folders.end(); ++beg){
(*beg)->remMsg(this);
}
}
Message:: ~ Message() {
remove_Msg_from_Folders();
}
012 管理指针成员.大多数C++类采用以下三种方法之一管理指针成员.
1.指针成员采取常规指针型行为.这样的类具有指针的所有缺陷但无需特殊的复制控制.
2.类可以采取所谓的"智能指针"行为.指针所指向的对象是共享的,但类能够防止悬垂指针(野指针).
3.类采取值行为.指针所指的对象是唯一的,由每个类对象独立管理.
013 常规指针行为,每个参数都定义了set和get行为,对于指针还要定义set_val和get_val行为.
class HasPtr{
public:
HasPtr(int *p, int i):ptr(p), val(i){
}
int *get_ptr() const {
return ptr;
}
int get_int() const {
return val;
}
void set_ptr(int *p){
ptr = p;
}
void set_int(int i){
val = i;
}
int get_ptr_val() const {
return *ptr;
}
void set_ptr_val(int val) const {
*ptr = val;
}
private:
int *ptr;
int val;
};
默认复制/赋值与指针行为.
int obj = 0;
HasPtr ptr1(&obj, 42);
HasPtr ptr2(ptr1);
通过默认的复制行为,ptr2和ptr1的val是独立的,但是ptr2和ptr1的指针指向了相同的对象,两个指针纠缠在了一起.
通过ptr1会修改ptr2中指针指向的值,而且还可能造成悬垂指针.
ptr1.set_ptr_val(9);
ptr1.set_int(20);
int *ip = new int(30);
HasPtr ptr(ip, 10);
delete ip;
删除ip之后,ptr中指针也不再指向有效的对象,然后,没有办法得知对象已经不存在了.
014 定义智能指针类型.智能指针除了增加功能外,其行为像普通指针一样.
智能指针负责删除共享对象,用户将动态分配一个对象,并将该对象的地址传给新的HasPtr类.用户仍然可以通过普通指针访问对象,但绝不能删除指针.
定义智能指针的通用技术是采用一个引用计数.职能指针将一个计数器类与类指向的对象相关联.使用计数跟踪该类有多少个对象共享同一个指针.
计数为0时,删除对象.
HasPtr类将保证在撤销指向对象的最后一个HasPtr对象时删除对象.
HasPtr在其他方面的行为与普通指针一样,复制对象时,副本和原对象将指向同一个基础对象,如果通过一个副本改变基础对象,则通过另一个对象访问的值也会改变.
计数器不能直接放在HasPtr对象中,举例说明原因.
int obj;
HasPtr p1(&obj, 42);
HasPtr p2(p1);
HasPtr p3(p1);
如果使用计数保存在HasPtr对象中,创建p3时如何更新计数?可以将p1中将计数增加并复制到p3,可此时没办法更新p2了.
使用计数的策略1(策略2在十四节说明).
// 类成员全是private,因为我们不希望用户使用U_Ptr类,将HasPtr声明为友元,使其成员可以访问U_Ptr的成员.
class U_Ptr{
friend class HasPtr;
int *ip; // U_Ptr保存指针和计数器,每个HasPtr都指向一个U_Ptr对象.
size_t use;
U_Ptr(int *p):ip(p), use(1){}
~U_Ptr(){
delete ip;
};
使用计数器类.
class HasPtr{
public:
// 构造函数创建一个U_Ptr对象,因为HasPtr是U_Ptr的友元,所以可以访问它的private属性的构造函数.
HasPtr(int *p, int i):ptr(new U_Ptr(p)), val(i){
}
HasPtr(const HasPtr &orig):ptr(orig.ptr), val(orig.val){
++ptr->use; // 指针指向同一个对象,并将对象的计数加1.
}
HasPtr& operator=(const HasPtr&);
~HasPtr(){
if( 0 == -- ptr->use ){
delete ptr;
}
}
private:
U_Ptr *ptr;
int val;
};
赋值操作符重载.
HasPtr& HasPtr::operator=(const HasPtr &rhs){
++rhs.ptr->use; // 因为左操作数也将指向右操作数指向的计数对象,所以右操作数ptr加一.
if(0 == --ptr->use){ // 判断左操作数指向的计数对象是否还有其他HasPtr对象使用,没有使用则删除计数对象.
delete ptr;
}
ptr = rhs.ptr; // 右操作数对象赋值到左对象当中.
val = rhs.val;
return *this;
}
智能指针的其他成员,与常规指针类基本一致,只是此时需要通过U_Ptr对象的解引用.
class HasPtr{
public:
int *get_ptr() const {
return ptr->ip;
}
int get_int() const{
return val;
}
void set_ptr(int *p){
ptr->ip = p;
}
void set_int(int i){
val = i;
}
int get_ptr_val() const {
return *ptr->ip;
}
void set_ptr_val(int) {
*ptr->ip = i;
}
private:
U_Ptr *ptr;
int val;
};
为了管理具有指针成员的类,必须定义三个复制控制成员:复制构造函数/赋值操作符/和析构函数.这些成员可以定义指针成员的指针行为或值行为.
015 值型类将指针成员所指基础值的副本给每个对象.
复制构造函数分配新元素,并从被复制对象处复制值,赋值操作符撤销所保存的原对象并从右操作数复制值,析构函数撤销对象.