第13章 复制控制
当定义一个新类型的时候,需要显式或隐式地指定复制、赋值和撤销该类型的对象时会发生什么,这是通过定义特殊成员:复制构造函数、赋值操作符和析构函数来达到的,复制构造函数、赋值操作符和析构函数总称为 复制控制,编译器自动实现这些操作,但类也可以定义自己的版本
#include <iostream> using namespace std; class A{ public: A(){ cout << "默认构造" << endl; } A(const A&a){ cout << "复制构造" << endl; } ~A(){ cout <<"析构函数" << endl; } }; void func(A a){ }//析构 //a出了作用域 int main() { A a1; //默认构造 A a2(a1); //复制构造 func(a1); //复制构造 cout << "--------------" << endl; return 0; }
13.1 复制构造函数
复制构造函数(copy constructor)是一种特殊构造函数,具有单个形参,该形参用const修饰,是对该类类型的引用,当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用复制构造函数,当将该类型的对象传递给函数或从函数返回该类型的对象时,将隐式使用复制构造函数
A a1; //默认构造 A arr[10] = {a1}; //arr[0]复制构造,其他元素默认构造
对象定义的形式:C++支持两种初始化形式:直接初始化 和 复制初始化,直接初始化将初始化式放在圆括号中,复制初始化使用=符号,类类型对象 的直接初始化 直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数,复制初始化 首先使用 指定构造函数 创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象
ifstream file1("d:\1.txt" ); //类类型对象的直接初始化直接调用与实参匹配的构造函数 ifstream file2 = "d:\1.txt"; //error 复制构造是 private 的
如果没有为类类型数组提供元素初始化,则将用默认构造函数初始化每个元素,如果使用常规的花括号括住的数组初始化列表来提供显式元素初始化,则使用复制初始化来初始化每个元素,根据指定值创建适当类型的元素,然后用 复制构造函数 将该值 复制到相应元素
A arr[5] = { A(),A(200) };
13.1.1 合成的复制构造函数
合成复制构造函数的行为是,执行逐个成员初始化,将新对象初始化为原对象的副本,如果一个类具有数组成员,合成复制构造函数将复制数组
13.1.2 定义自己的复制构造函数
必须定义复制构造函数:一种情况是:有一个数据成员是指针或者有成员表示在构造函数中分配的其他资源,另一种情况是:在创建新对象时必须做一些特定工作
13.1.3 禁止复制
为了防止复制,类必须显式声明其复制构造函数为private,如果想要连友元和成员中的复制也禁止,就可以声明一个private复制构造函数,但不对其定义(声明而不定义成员函数是合法的,但是,使用未定义成员的任何尝试将导致链接失败)
不能复制的类对象 只能作为引用传递给函数 或 从函数返回,它们也不能用作容器的元素
#include <iostream> using namespace std; class A{ int x; /* A(const A&a); */ //只声明不定义,成员及友元中的复制也禁止 A(const A&a){ //私有的(private ) x = a.x; cout << "复制构造" << endl; }; public: A():x(100){ cout << "默认构造" << endl; } void func( ){ A b(*this); //成员仍可以进行复制 cout << b.x << endl; } A* func1(){ return new A( *this ); } ~A(){ cout <<"析构函数" << endl; } void print(){ cout << x << endl; } }; int main() { A a1; //默认构造 a1.func( ); //类成员函数func cout << "--------------" << endl; A* pA = a1.func1(); pA->print(); delete pA; cout << "--------------" << endl; return 0; }
如果定义了复制构造函数,也必须定义默认构造函数
13.2 赋值操作符
重载操作符 是一些函数,其名字为operator后跟着所定义的操作符的符号,因此,通过定义名为operator=的函数,可以对赋值进行定义,操作符函数的形参表必须具有与该操作符操作数数目相同的形参(如果操作符是一个成员,则包括隐式this形参),大多数操作符可以定义为成员函数或非成员函数,当操作符为成员函数时,它的第一个操作数隐式绑定到this指针,赋值必须是类的成员,右操作数一般作为const引用传递,返回对同一类类型的引用
如果类需要复制构造函数,它也会需要赋值操作符
13.3 析构函数
三个复制控制成员:复制构造函数、赋值操作符、析构函数,三法则(Rule of Three),如果需要析构函数,则需要所有三个复制控制成员
13.4 消息处理实例
对每个Message,保存一个指针集set,set中的指针指向该Message所在的Folder,每个Folder也保存着一些指针,指向它所在的Message
#include <vector> using std::vector; #include <string> using std::string; #include <iostream> using std::cout; using std::endl; #include "Folder.h" int main() { string s1("contents1"); string s2("contents2"); string s3("contents3"); string s4("contents4"); string s5("contents5"); string s6("contents6"); // all new messages, no copies yet Message m1(s1); Message m2(s2); Message m3(s3); Message m4(s4); Message m5(s5); Message m6(s6); Folder f1; Folder f2; m1.save(f1); m3.save(f1); m5.save(f1); //f1 m1.save(f2);m2.save(f2); m4.save(f2); m6.save(f2); //f2 m1.debug_print(); f2.debug_print(); // create some copies Message c1(m1); Message c2(m2), c4(m4), c6(m6); m1.debug_print(); f2.debug_print(); // now some assignments m2 = m3; m4 = m5; m6 = m3; m1 = m5; m1.debug_print(); f2.debug_print(); // finally, self-assignment m2 = m2; m1 = m1; m1.debug_print(); f2.debug_print(); vector<Message> vm; cout << "capacity: " << vm.capacity() << endl; vm.push_back(m1); cout << "capacity: " << vm.capacity() << endl; vm.push_back(m2); cout << "capacity: " << vm.capacity() << endl; vm.push_back(m3); cout << "capacity: " << vm.capacity() << endl; vm.push_back(m4); cout << "capacity: " << vm.capacity() << endl; vm.push_back(m5); cout << "capacity: " << vm.capacity() << endl; vm.push_back(m6); vector<Folder> vf; cout << "capacity: " << vf.capacity() << endl; vf.push_back(f1); cout << "capacity: " << vf.capacity() << endl; vf.push_back(f2); cout << "capacity: " << vf.capacity() << endl; vf.push_back(Folder(f1)); cout << "capacity: " << vf.capacity() << endl; vf.push_back(Folder(f2)); cout << "capacity: " << vf.capacity() << endl; vf.push_back(Folder()); Folder f3; f3.save(m6); cout << "capacity: " << vf.capacity() << endl; vf.push_back(f3); system("pause"); return 0; }
#ifndef FOLDER_H #define FOLDER_H #include <string> #include <set> class Folder; class Message { friend void swap(Message&, Message&); friend class Folder; public: // folders is implicitly initialized to the empty set explicit Message(const std::string &str = ""): contents(str) { } // copy control to manage pointers to this Message Message(const Message&); // copy constructor Message& operator=(const Message&); // copy assignment ~Message(); // destructor Message(Message&&); // move constructor Message& operator=(Message&&); // move assignment // add/remove this Message from the specified Folder's set of messages void save(Folder&); void remove(Folder&); void debug_print(); // print contents and it's list of Folders, // printing each Folder as well private: std::string contents; // actual message text std::set<Folder*> folders; // Folders that have this Message // utility functions used by copy constructor, assignment, and destructor // add this Message to the Folders that point to the parameter void add_to_Folders(const Message&); void move_Folders(Message*); // remove this Message from every Folder in folders void remove_from_Folders(); // used by Folder class to add self to this Message's set of Folder's void addFldr(Folder *f) { folders.insert(f); } void remFldr(Folder *f) { folders.erase(f); } }; // declaration for swap should be in the same header as Message itself void swap(Message&, Message&); class Folder { friend void swap(Message&, Message&); friend class Message; public: ~Folder(); // remove self from Messages in msgs Folder(const Folder&); // add new folder to each Message in msgs Folder& operator=(const Folder&); // delete Folder from lhs messages // add Folder to rhs messages Folder(Folder&&); // move Messages to this Folder Folder& operator=(Folder&&); // delete Folder from lhs messages // add Folder to rhs messages Folder() = default; // defaults ok void save(Message&); // add this message to folder void remove(Message&); // remove this message from this folder void debug_print(); // print contents and it's list of Folders, private: std::set<Message*> msgs; // messages in this folder void add_to_Messages(const Folder&);// add this Folder to each Message void remove_from_Msgs(); // remove this Folder from each Message void addMsg(Message *m) { msgs.insert(m); } void remMsg(Message *m) { msgs.erase(m); } void move_Messages(Folder*); // move Message pointers to point to this Folder }; #endif
#include <utility> // for move, we don't supply a using declaration for move #include <iostream> using std::cerr; using std::endl; #include <set> using std::set; #include <string> using std::string; #include "Folder.h" void swap(Message &lhs, Message &rhs) { using std::swap; // not strictly needed in this case, but good habit // remove pointers to each Message from their (original) respective Folders for (auto f: lhs.folders) f->remMsg(&lhs); for (auto f: rhs.folders) f->remMsg(&rhs); // swap the contents and Folder pointer sets swap(lhs.folders, rhs.folders); // uses swap(set&, set&) swap(lhs.contents, rhs.contents); // swap(string&, string&) // add pointers to each Message to their (new) respective Folders for (auto f: lhs.folders) f->addMsg(&lhs); for (auto f: rhs.folders) f->addMsg(&rhs); } Folder::Folder(Folder &&f) { move_Messages(&f); // make each Message point to this Folder } Folder& Folder::operator=(Folder &&f) { if (this != &f) { remove_from_Msgs(); // remove this Folder from the current msgs move_Messages(&f); // make each Message point to this Folder } return *this; } void Folder::move_Messages(Folder *f) { msgs = std::move(f->msgs); // move the set from f to this Folder f->msgs.clear(); // ensure that destroying f is harmless for (auto m : msgs) { // for each Message in this Folder m->remFldr(f); // remove the pointer to the old Folder m->addFldr(this); // insert pointer to this Folder } } Message::Message(Message &&m): contents(std::move(m.contents)) { move_Folders(&m); // moves folders and updates the Folder pointers } Message::Message(const Message &m): contents(m.contents), folders(m.folders) { add_to_Folders(m); // add this Message to the Folders that point to m } Message& Message::operator=(Message &&rhs) { if (this != &rhs) { // direct check for self-assignment remove_from_Folders(); contents = std::move(rhs.contents); // move assignment move_Folders(&rhs); // reset the Folders to point to this Message } return *this; } Message& Message::operator=(const Message &rhs) { // handle self-assignment by removing pointers before inserting them remove_from_Folders(); // update existing Folders contents = rhs.contents; // copy message contents from rhs folders = rhs.folders; // copy Folder pointers from rhs add_to_Folders(rhs); // add this Message to those Folders return *this; } Message::~Message() { remove_from_Folders(); } // move the Folder pointers from m to this Message void Message::move_Folders(Message *m) { folders = std::move(m->folders); // uses set move assignment for (auto f : folders) { // for each Folder f->remMsg(m); // remove the old Message from the Folder f->addMsg(this); // add this Message to that Folder } m->folders.clear(); // ensure that destroying m is harmless } // add this Message to Folders that point to m void Message::add_to_Folders(const Message &m) { for (auto f : m.folders) // for each Folder that holds m f->addMsg(this); // add a pointer to this Message to that Folder } // remove this Message from the corresponding Folders void Message::remove_from_Folders() { for (auto f : folders) // for each pointer in folders f->remMsg(this); // remove this Message from that Folder folders.clear(); // no Folder points to this Message } void Folder::add_to_Messages(const Folder &f) { for (auto msg : f.msgs) msg->addFldr(this); // add this Folder to each Message } Folder::Folder(const Folder &f) : msgs(f.msgs) { add_to_Messages(f); // add this Folder to each Message in f.msgs } Folder& Folder::operator=(const Folder &f) { remove_from_Msgs(); // remove this folder from each Message in msgs msgs = f.msgs; // copy the set of Messages from f add_to_Messages(f); // add this folder to each Message in msgs return *this; } Folder::~Folder() { remove_from_Msgs(); } void Folder::remove_from_Msgs() { while (!msgs.empty()) (*msgs.begin())->remove(*this); } void Message::save(Folder &f) { folders.insert(&f); // add the given Folder to our list of Folders f.addMsg(this); // add this Message to f's set of Messages } void Message::remove(Folder &f) { folders.erase(&f); // take the given Folder out of our list of Folders f.remMsg(this); // remove this Message to f's set of Messages } void Folder::save(Message &m) { // add m and add this folder to m's set of Folders msgs.insert(&m); m.addFldr(this); } void Folder::remove(Message &m) { // erase m from msgs and remove this folder from m msgs.erase(&m); m.remFldr(this); } void Folder::debug_print() { cerr << "Folder contains " << msgs.size() << " messages" << endl; int ctr = 1; for (auto m : msgs) { cerr << "Message " << ctr++ << ":\n\t" << m->contents << endl; } } void Message::debug_print() { cerr << "Message:\n\t" << contents << endl; cerr << "Appears in " << folders.size() << " Folders" << endl; }
13.5 管理指针成员
包含指针的类,需要特别注意复制控制,原因是 复制指针时,只复制指针中的地址,而不会复制指针指向的对象,指针成员默认具有与指针对象同样的行为,通过不同的复制控制策略,可以为指针成员实现不同的行为,C++管理指针成员的三种方法:指针成员采取常规指针型行为;类可以实现所谓的“智能指针”行为(指针所指向的对象是共享的,能够防止悬垂指针);类采取值型行为(指针指向的对象是唯一的,由每个类对象独立管理)
13.5.1 定义智能指针类
智能指针除了增加功能外,其行为像普通指针一样,智能指针负责删除共享对象,智能指针保证在 撤销 指向对象 的 最后一个对象 时 删除对象,“指向对象”、“删除对象”是智能指针类成员指针指向的对象,“最后一个对象”是智能指针类对象)
定义智能指针的通用技术 是采用一个使用计数(use count),使用计数有时也称为引用计数(reference count),实现使用计数有两种策略,其中一种 是:定义一个单独的具体类 ,封装使用计数和相关指针,另外一种在15.8.1
class U_Ptr{ friend class HasPtr; int* ip; //封装指针 size_t use; //封装使用计数 U_Ptr(int* p ):ip(p ), use(1) { } ~U_Ptr() { delete ip; } };
1.每次创建类的对象时,初始化指针并将使用计数置为1;2.当对象作为另一个对象的副本而创建时,复制构造函数复制指针并增加与之相应的使用计数的值;3.对一个对象赋值时,赋值操作符减少左操作数所指对象的使用计数的值(如果使用计数减至0,则删除对象),并增加右操作数所指对象的使用计数的值;4.调用析构函数时,析构函数减少使用计数的值,如果计数减至0,则删除对象
初始构造、复制构造、赋值操作符、析构函数
#include <iostream> using namespace std; class U_Ptr{ friend class HasPtr; int* ip; //封装指针 size_t use; //封装使用计数 U_Ptr(int* p ):ip(p ), use(1) { } ~U_Ptr() { delete ip; } }; class HasPtr{ U_Ptr *ptr; //指针成员 // 指向 new U_Ptr(p) public: HasPtr(int* p):ptr( new U_Ptr(p) ) { } //构造函数 HasPtr(const HasPtr& orig): ptr(orig.ptr ) { ++ptr->use; } //1.复制构造 HasPtr& operator = (const HasPtr& rhs ) //2.赋值运算符重载 { ++rhs.ptr->use; if(--ptr->use==0 ) //左操作数的use如果为0,表明其为指向的对象的最后一个对象,删除其指向对象 //结合上一行代码,同时可以防止自身赋值 delete ptr; *ptr = *rhs.ptr; //赋值后,指向新的对象 return *this; } ~HasPtr() {if(--ptr->use==0) delete ptr; } //3.析构 }; int main() {/* int* p1 = new int(42); { HasPtr hp1(p1 ); } //hp1没有复制、赋值//hp1指向对象,也是p1指向对象,被删除 */ /* //如何保证P1、p2指向空间有效? int* p1 = new int(42); int* p2 = new int(128); HasPtr hp2(p2 ); // hp2(new int(256)) !!! { HasPtr hp1(p1 ); //hp2 = hp1; //赋值后hp2的计数为0,释放p2指向空间 } cout << (*p1 ) << "," << (*p2) << endl; */ { HasPtr hp(new int(256) ); } return 0; }
13.5.2 定义值型类
给 指针成员 提供 值语义,具有值语义的类所定义的对象很像算术类型的对象,复制值型对象,会得到一个不同的新副本,对副本所做的改变不会反映在原有对象上
#include <iostream> using namespace std; //值语义,是给对象赋值,不是给指针赋值 class HasPtr{ int *ptr; //指针成员 //new int(p) public: HasPtr(const int& p):ptr( new int(p) ) { } //构造函数 // new int(p) //1. 值语义(*orig.ptr),拷贝构造 // 不再复制指针,复制指针指向的空间的值 *orig.ptr HasPtr(const HasPtr& orig): ptr(new int(*orig.ptr ) ) { } HasPtr& operator = (const HasPtr& rhs ) //2.赋值运算符重载 { *ptr = *rhs.ptr; //复制指针指向的空间的值 *rhs.ptr // 指针一定不为空 return *this; } ~HasPtr() { delete ptr; } //3.析构, 指针一定不为空 int get_ptr_val(){ //返回 ptr 指向的空间的值 return *ptr; } }; int main() { HasPtr p1(128 ); HasPtr p2(p1 ); HasPtr p3(256 ); cout << "p1:" << p1.get_ptr_val( ) << endl; cout << "p2:" << p2.get_ptr_val( ) << endl; cout << "p3:" << p3.get_ptr_val( ) << endl; p2 = p3; cout << "p2:" << p2.get_ptr_val( ) << endl; return 0; }