而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。
下面看一个类对象拷贝的简单例子。 [c-sharp] view plaincopy 3 #include <iostream> 4 using namespace std; 5 6 class CExample { 7 private: 8 int a; 9 public: 10 //构造函数 11 CExample(int b) 12 { a = b;} 13 14 //一般函数 15 void Show () 16 { 17 cout<<a<<endl; 18 } 19 }; 20 21 int main() 22 { 23 CExample A(100); 24 CExample B = A; //注意这里的对象初始化要调用拷贝构造函数,而非赋值 25 B.Show (); 26 return 0; 27 }运行程序,屏幕输出100。从以上代码的运行结果可以看出,系统为对象 B 分配了内存并完成了与对象 A 的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。
下面举例说明拷贝构造函数的工作过程。 [c-sharp] view plaincopy 28 #include <iostream> 29 using namespace std; 30 31 class CExample { 32 private: 33 int a; 34 public: 35 //构造函数 36 CExample(int b) 37 { a = b;} 38 39 //拷贝构造函数 40 CExample(const CExample& C) 41 { 42 a = C.a; 43 } 44 45 //一般函数 46 void Show () 47 { 48 cout<<a<<endl; 49 } 50 }; 51 52 int main() 53 { 54 CExample A(100); 55 CExample B = A; // CExample B(A); 也是一样的 56 B.Show (); 57 return 0; 58 }CExample(const CExample& C) 就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量。
二. 拷贝构造函数的调用时机
在C++中,下面三种对象需要调用拷贝构造函数! 1. 对象以值传递的方式传入函数参数 59 class CExample 60 { 61 private: 62 int a; 63 64 public: 65 //构造函数 66 CExample(int b) 67 { 68 a = b; 69 cout<<"creat: "<<a<<endl; 70 } 71 72 //拷贝构造 73 CExample(const CExample& C) 74 { 75 a = C.a; 76 cout<<"copy"<<endl; 77 } 78 79 //析构函数 80 ~CExample() 81 { 82 cout<< "delete: "<<a<<endl; 83 } 84 85 void Show () 86 { 87 cout<<a<<endl; 88 } 89 }; 90 91 //全局函数,传入的是对象 92 void g_Fun(CExample C) 93 { 94 cout<<"test"<<endl; 95 } 96 97 int main() 98 { 99 CExample test(1); 100 //传入对象 101 g_Fun(test); 102 103 return 0; 104 }调用g_Fun()时,会产生以下几个重要步骤:
(1).test对象传入形参时,会先会产生一个临时变量,就叫 C 吧。 (2).然后调用拷贝构造函数把test的值给C。 整个这两个步骤有点像:CExample C(test);(3).等g_Fun()执行完后, 析构掉 C 对象。 2. 对象以值传递的方式从函数返回 105 class CExample 106 { 107 private: 108 int a; 109 110 public: 111 //构造函数 112 CExample(int b) 113 { 114 a = b; 115 } 116 117 //拷贝构造 118 CExample(const CExample& C) 119 { 120 a = C.a; 121 cout<<"copy"<<endl; 122 } 123 124 void Show () 125 { 126 cout<<a<<endl; 127 } 128 }; 129 130 //全局函数 131 CExample g_Fun() 132 { 133 CExample temp(0); 134 return temp; 135 } 136 137 int main() 138 { 139 g_Fun(); 140 return 0; 141 }当g_Fun()函数执行到return时,会产生以下几个重要步骤:
(1). 先会产生一个临时变量,就叫XXXX吧。 (2). 然后调用拷贝构造函数把temp的值给XXXX。整个这两个步骤有点像:CExample XXXX(temp);(3). 在函数执行到最后先析构temp局部变量。 (4). 等g_Fun()执行完后再析构掉XXXX对象。 3. 对象需要通过另外一个对象进行初始化; 142 CExample A(100); 143 CExample B = A; 144 // CExample B(A);后两句都会调用拷贝构造函数。
三. 浅拷贝和深拷贝
1. 默认拷贝构造函数 很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它一般具有以下形式: [c-sharp] view plaincopy 145 Rect::Rect(const Rect& r) 146 { 147 width = r.width; 148 height = r.height; 149 } 当然,以上代码不用我们编写,编译器会为我们自动生成。但是如果认为这样就可以解决对象的复制问题,那就错了,让我们来考虑以下一段代码:[c-sharp] view plaincopy 150 class Rect 151 { 152 public: 153 Rect() // 构造函数,计数器加1 154 { 155 count++; 156 } 157 ~Rect() // 析构函数,计数器减1 158 { 159 count--; 160 } 161 static int getCount() // 返回计数器的值 162 { 163 return count; 164 } 165 private: 166 int width; 167 int height; 168 static int count; // 一静态成员做为计数器 169 }; 170 171 int Rect::count = 0; // 初始化计数器 172 173 int main() 174 { 175 Rect rect1; 176 cout<<"The count of Rect: "<<Rect::getCount()<<endl; 177 178 Rect rect2(rect1); // 使用rect1复制rect2,此时应该有两个对象 179 cout<<"The count of Rect: "<<Rect::getCount()<<endl; 180 181 return 0; 182 }这段代码对前面的类,加入了一个静态成员,目的是进行计数。在主函数中,首先创建对象rect1,输出此时的对象个数,然后使用rect1复制出对象rect2,再输出此时的对象个数,按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。
说白了,就是拷贝构造函数没有处理静态数据成员。 出现这些问题最根本就在于在复制对象时,计数器没有递增,我们重新编写拷贝构造函数,如下: [c-sharp] view plaincopy 183 class Rect 184 { 185 public: 186 Rect() // 构造函数,计数器加1 187 { 188 count++; 189 } 190 Rect(const Rect& r) // 拷贝构造函数 191 { 192 width = r.width; 193 height = r.height; 194 count++; // 计数器加1 195 } 196 ~Rect() // 析构函数,计数器减1 197 { 198 count--; 199 } 200 static int getCount() // 返回计数器的值 201 { 202 return count; 203 } 204 private: 205 int width; 206 int height; 207 static int count; // 一静态成员做为计数器 208 };2. 浅拷贝
所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了,让我们考虑如下一段代码: [c-sharp] view plaincopy 209 class Rect 210 { 211 public: 212 Rect() // 构造函数,p指向堆中分配的一空间 213 { 214 p = new int(100); 215 } 216 ~Rect() // 析构函数,释放动态分配的空间 217 { 218 if(p != NULL) 219 { 220 delete p; 221 } 222 } 223 private: 224 int width; 225 int height; 226 int *p; // 一指针成员 227 }; 228 229 int main() 230 { 231 Rect rect1; 232 Rect rect2(rect1); // 复制对象 233 return 0; 234 }在这段代码运行结束之前,会出现一个运行错误。原因就在于在进行对象复制时,对于动态分配的内容没有进行正确的操作。我们来分析一下:
在运行定义rect1对象后,由于在构造函数中有一个动态分配的语句,因此执行后的内存情况大致如下: 在使用rect1复制rect2时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时 rect1.p = rect2.p,也即这两个指针指向了堆里的同一个空间,如下图所示: 当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。 3. 深拷贝 在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理: [c-sharp] view plaincopy235 class Rect
236 { 237 public: 238 Rect() // 构造函数,p指向堆中分配的一空间 239 { 240 p = new int(100); 241 } 242 Rect(const Rect& r) 243 { 244 width = r.width; 245 height = r.height; 246 p = new int; // 为新对象重新动态分配空间 247 *p = *(r.p); 248 } 249 ~Rect() // 析构函数,释放动态分配的空间 250 { 251 if(p != NULL) 252 { 253 delete p; 254 } 255 } 256 private: 257 int width; 258 int height; 259 int *p; // 一指针成员 260 };此时,在完成对象的复制后,内存的一个大致情况如下:
此时rect1的p和rect2的p各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”。3. 防止默认拷贝发生
通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。 [c-sharp] view plaincopy261 // 防止按值传递
262 class CExample 263 { 264 private: 265 int a; 266 267 public: 268 //构造函数 269 CExample(int b) 270 { 271 a = b; 272 cout<<"creat: "<<a<<endl; 273 } 274 275 private: 276 //拷贝构造,只是声明 277 CExample(const CExample& C); 278 279 public: 280 ~CExample() 281 { 282 cout<< "delete: "<<a<<endl; 283 } 284 285 void Show () 286 { 287 cout<<a<<endl; 288 } 289 }; 290 291 //全局函数 292 void g_Fun(CExample C) 293 { 294 cout<<"test"<<endl; 295 } 296 297 int main() 298 { 299 CExample test(1); 300 //g_Fun(test); 按值传递将出错 301 302 return 0; 303 } 四. 拷贝构造函数的几个细节 1. 拷贝构造函数里能调用private成员变量吗? 解答:这个问题是在网上见的,当时一下子有点晕。其时从名子我们就知道拷贝构造函数其时就是一个特殊的构造函数,操作的还是自己类的成员变量,所以不受private的限制。2. 以下函数哪个是拷贝构造函数,为什么?
[c-sharp] view plaincopy304 X::X(const X&);
305 X::X(X); 306 X::X(X&, int a=1); 307 X::X(X&, int a=1, int b=2); 解答:对于一个类X, 如果一个构造函数的第一个参数是下列之一: a) X& b) const X& c) volatile X& d) const volatile X& 且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数. [c-sharp] view plaincopy308 X::X(const X&); //是拷贝构造函数
309 X::X(X&, int=1); //是拷贝构造函数 310 X::X(X&, int a=1, int b=2); //当然也是拷贝构造函数 3. 一个类中可以存在多于一个的拷贝构造函数吗? 解答:类中可以存在超过一个拷贝构造函数。 [c-sharp] view plaincopy311 class X {
312 public: 313 X(const X&); // const 的拷贝构造 314 X(X&); // 非const的拷贝构造 315 };注意,如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.
[c-sharp] view plaincopy316 class X {
317 public: 318 X(); 319 X(X&); 320 }; 321 322 const X cx; 323 X x = cx; // error如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。
这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪一个。