前言:
C++面向对象的编程过程中,凡是在类中运用到动态内存分配的时候总是会写一个显示的复制构造函数和赋值重载运算符,本文将结合C++ Primer Plus一书的内容分析下原因:
一、在C++编程中如果没有编写下列成员函数,系统会自动的提供:
(1)构造函数
(2)析构函数
(3)地址运算符
(4)赋值构造函数
(5)赋值运算符
其中(1)-(3)在编程中不会产生什么影响,但是(4)(5)会造成较大的影响
二、赋值构造函数
1、函数原型 Class_name(const Class_name &)
2、什么时候会用调用复制构造函数?
当同时满足以下两个条件的时候就会自动调用复制构造函数:
(1)新建一个对象;
(2)使用同类中现有对象初始化新对象。
除了直接看出来的一些表达式能满足以上两个条件,函数的按值传递(函数按值传递的是变量的副本)和函数返回对象的情况也同时满足了以上两个条件。而且有些情况编译器会生成临时变量,然后将临时变量在赋值给被传递的对象。
3、默认复制构造函数做了哪些事情?
默认赋值构造函数逐个复制非静态成员的值。注意是值,是一种浅复制。
浅复制会导致两个对象的指针指向同一个内存单元,这时如果某个对象已经析构执行delete,那么剩下的那个指针将会变成野指针,将造成灾难性的后果。特别当编译器会生成临时对象的情况,临时对象很快就执行析构函数了。。。
4、下面举个例子看看动态内存分配的情况不定义显示的赋值构造函数会出现什么问题
1 // 复制构造函数探索.cpp : 定义控制台应用程序的入口点。 2 // 3 4 #include "stdafx.h" 5 #include<iostream> 6 using namespace std; 7 class Str 8 { 9 public: 10 char * str; 11 int len; 12 static int num; 13 Str() 14 { 15 num++; 16 cout<<"新创建了对象"<<str<<"现在的对象个数一共是"<<num<<endl; 17 } 18 Str(const char *s) 19 { 20 len=strlen(s); 21 str=new char[len+1]; 22 strcpy(str,s); 23 num++; 24 cout<<"新创建了对象"<<str<<"现在的对象个数一共是"<<num<<endl; 25 } 26 ~Str() 27 { 28 num--; 29 cout<<"将析构了一个对象"<<str<<"析构后对象的个数是"<<num<<endl; 30 delete []str; 31 } 32 }; 33 34 //初始化静态变量 35 int Str::num=0; 36 void show1(Str & a) 37 { 38 cout<<a.str<<endl; 39 } 40 void show2(Str a) 41 { 42 cout<<a.str<<endl; 43 } 44 int _tmain(int argc, _TCHAR* argv[]) 45 { 46 Str s1("s1"); 47 show1(s1); 48 show2(s1); 49 return 0; 50 }
上述代码如果注释掉第48行,运行结果是这样的
如果注释掉第47行,而恢复第48行结果会变成这样
究其原因,因为void show1(Str & a)是按引用传递的,show1(s1);只需要将是s1的引用给a就可以了,并没有新建一个Str对象,所以不会调用默认的复制构造函数。
而void show2(Str a)是按值传递的,按值传递的过程是需要拷贝参数的副本到形参中的,这就需要新建一个Str对象,然后用已有的s1对象初始化,满足了调用复制构造函数的两个条件。而实际上过程比这还要麻烦一点,编译器会先生成一个临时对象,然后将s1拷贝给临时对象,再将临时对象拷贝给a,而由于临时对象析构的时候将str指向的内存释放掉了,而再执行析构s1(delete []str;)的时候,由于str指向的内容已被释放,所以cout<<"将析构了一个对象"<<str<<"析构后对象的个数是"<<num<<endl;会乱码,而试图delete已经delete过的指针将会造成程序异常终止。由于默认复制构造函数中没有num++,而不管用那个构造函数构造出的对象调用的都是同一个析构函数,而析构函数中含有num--,所以临时对象导致num多减了一次,所以最后一句话会出现,“析构后对象的个数是-1”这样的结果。
5、解决办法:
定义一个显示的复制构造函数
Str(const Str & s)
{
len=s.len;
str=new char[len+1];
strcpy(str,s.str);
num++;
cout<<"现在的对象个数一共是"<<num<<endl;
}
1 // 复制构造函数探索.cpp : 定义控制台应用程序的入口点。 2 // 3 4 #include "stdafx.h" 5 #include<iostream> 6 using namespace std; 7 class Str 8 { 9 public: 10 char * str; 11 int len; 12 static int num; 13 Str() 14 { 15 num++; 16 cout<<"新创建了对象"<<str<<"现在的对象个数一共是"<<num<<endl; 17 } 18 Str(const Str & s) 19 { 20 len=s.len; 21 str=new char[len+1]; 22 strcpy(str,s.str); 23 num++; 24 cout<<"新创建了对象"<<str<<"现在的对象个数一共是"<<num<<endl; 25 } 26 Str(const char *s) 27 { 28 len=strlen(s); 29 str=new char[len+1]; 30 strcpy(str,s); 31 num++; 32 cout<<"新创建了对象"<<str<<"现在的对象个数一共是"<<num<<endl; 33 } 34 ~Str() 35 { 36 num--; 37 cout<<"将析构了一个对象"<<str<<"析构后对象的个数是"<<num<<endl; 38 delete []str; 39 } 40 }; 41 42 //初始化静态变量 43 int Str::num=0; 44 void show1(Str & a) 45 { 46 cout<<a.str<<endl; 47 } 48 void show2(Str a) 49 { 50 cout<<a.str<<endl; 51 } 52 int _tmain(int argc, _TCHAR* argv[]) 53 { 54 Str s1("s1"); 55 //show1(s1); 56 show2(s1); 57 return 0; 58 }
三、赋值运算符
1、函数原型:Class_name & Class_name::operator=(const Class_name &)
2、什么时候调用默认的赋值运算符?
当将已有的对象赋给另一个对象时,将使用赋值运算符。
3、默认复制运算符做了什么事情?
其实它和默认的赋值构造函数差不多,都是进行浅复制。
4、还是浅复制造成的问题,下面举个例子
1 // 复制构造函数探索.cpp : 定义控制台应用程序的入口点。 2 // 3 4 #include "stdafx.h" 5 #include<iostream> 6 using namespace std; 7 class Str 8 { 9 public: 10 char * str; 11 int len; 12 static int num; 13 Str() 14 { 15 len=0; 16 str=new char[1]; 17 str[0]=‘\0‘; 18 num++; 19 //cout<<"现在的对象个数一共是"<<num<<endl; 20 } 21 Str(const char *s) 22 { 23 len=strlen(s); 24 str=new char[len+1]; 25 strcpy(str,s); 26 num++; 27 //cout<<"现在的对象个数一共是"<<num<<endl; 28 } 29 ~Str() 30 { 31 num--; 32 //cout<<"将析构了一个对象"<<str<<"析构后对象的个数是"<<num<<endl; 33 delete []str; 34 } 35 //显示的拷贝构造函数 36 Str(const Str & s) 37 { 38 len=s.len; 39 str=new char[len+1]; 40 strcpy(str,s.str); 41 num++; 42 //cout<<"现在的对象个数一共是"<<num<<endl; 43 } 44 45 }; 46 47 48 //初始化静态变量 49 int Str::num=0; 50 void show1(Str & a) 51 { 52 cout<<a.str<<endl; 53 } 54 void show2(Str a) 55 { 56 cout<<a.str<<endl; 57 } 58 int _tmain(int argc, _TCHAR* argv[]) 59 { 60 Str s1("hello"); 61 //show1(s1); 62 //show2(s1); 63 Str s2; 64 s2=s1; 65 cout<<s2.str<<endl; 66 return 0; 67 }
Str s2;s2=s1;这两句用到了赋值运算符,而浅复制导致s1和s2的指针指向了同一个位置,当s1被析构的时候s2指向的内存单元也被释放掉,所以再delete s2中的str的时候系统就崩溃啦。
4、解决办法:定义一个显示的赋值重载运算符
Str & Str::operator=(const Str & st)
{
if(this==&st)
{
return *this;
}
delete [] str;
len=st.len;
str=new char[len+1];
strcpy(str,st.str);
return *this;
}
注意:返回引用是为了链式表达式,检测不能自己给自己赋值,否则delete [] str;后往哪找值赋给自己?
1 // 复制构造函数探索.cpp : 定义控制台应用程序的入口点。 2 // 3 4 #include "stdafx.h" 5 #include<iostream> 6 using namespace std; 7 class Str 8 { 9 public: 10 char * str; 11 int len; 12 static int num; 13 Str() 14 { 15 len=0; 16 str=new char[1]; 17 str[0]=‘\0‘; 18 num++; 19 //cout<<"现在的对象个数一共是"<<num<<endl; 20 } 21 Str(const char *s) 22 { 23 len=strlen(s); 24 str=new char[len+1]; 25 strcpy(str,s); 26 num++; 27 //cout<<"现在的对象个数一共是"<<num<<endl; 28 } 29 ~Str() 30 { 31 num--; 32 //cout<<"将析构了一个对象"<<str<<"析构后对象的个数是"<<num<<endl; 33 delete []str; 34 } 35 //显示的拷贝构造函数 36 Str(const Str & s) 37 { 38 len=s.len; 39 str=new char[len+1]; 40 strcpy(str,s.str); 41 num++; 42 //cout<<"现在的对象个数一共是"<<num<<endl; 43 } 44 //显示的重载赋值运算符 45 Str & operator=(const Str & ); 46 }; 47 48 Str & Str::operator=(const Str & st) 49 { 50 if(this==&st) 51 { 52 return *this; 53 } 54 delete [] str; 55 len=st.len; 56 str=new char[len+1]; 57 strcpy(str,st.str); 58 return *this; 59 } 60 61 //初始化静态变量 62 int Str::num=0; 63 void show1(Str & a) 64 { 65 cout<<a.str<<endl; 66 } 67 void show2(Str a) 68 { 69 cout<<a.str<<endl; 70 } 71 int _tmain(int argc, _TCHAR* argv[]) 72 { 73 Str s1("hello"); 74 //show1(s1); 75 //show2(s1); 76 Str s2; 77 s2=s1; 78 cout<<s2.str<<endl; 79 return 0; 80 }
程序中除了注意上述两点外还要注意构造函数写的是否全面,一开始写重载运算符=的时候忽略了下面这个构造函数中的str和len,导致Str s2后一直报错,晕。。。
Str()
{
len=0;
str=new char[1];
str[0]=‘\0‘;
num++;
//cout<<"现在的对象个数一共是"<<num<<endl;
}