C++的BIG THREE
拷贝构造函数、操作符=和析构函数称为C++的BIG THREE。原则上,如果需要定义其中一个,就必须定义全部三个。虽然,缺少任何一个,编译器都会帮你自动创建对应的函数,但是自动创建的函数可能达不到预期的效果。一般而言,如果类中的所有成员变量都是预定义类型(例如int、double等),那么编译器自动生成的拷贝构造函数和操作符=函数能很好的工作,但加入类中包含自定义类或指针成员变量,那么它们会表现失常。所以,通用性的结论是:凡是使用了操作符new的任何类(使用了new就必然会包含指针),最保险的做法就是手动定义自己的拷贝构造函数、重置操作符=、当然还有自定义析构函数释放占用的自由存储区域空间。
关于析构函数、重置赋值运算符和析构函数的内容结合一个自定义的字符串类StringVar来进行剖析。
#include <iostream>
using namespace std;
class StringVar
{
private:
char* value;
int maxLength;
public:
StringVar();
StringVar(int);
StringVar(const char[]);
int length() const;
void getLine(istream&);
friend ostream& operator<<(ostream&,const StringVar&);
};
注意,这是未完成版本的StringVar声明。
根据StringVar的声明,现在给予定义。
StringVar::StringVar():maxLength(100)
{
value = new char[maxLength+1];
value[0]='\0';
}
StringVar::StringVar(int size):maxLength(size)
{
value = new char[size+1];
value[0]='\0';
}
StringVar::StringVar(const char a[] ):maxLength(strlen(a))
{
value = new char[maxLength+1];
strcpy(value,a);
}
int StringVar::length() const
{
return strlen(value);
}
void StringVar::getLine(istream& ins)
{
ins.getline(value,maxLength+1);
}
//利用友元函数重载了<<运算符
//友元函数虽然定义在StringVar类中,但并不是StringVar的成员函数
ostream& operator<<(ostream& outs,const StringVar& sr)
{
outs<<sr.value;
return outs;
}
接下来是一段使用StringVar类的应用:
void conversation(int size);
int main(void)
{
conversation(30);
cout<<"end of applicaiotn."<<endl;
return 0;
}
void conversation(int size)
{
StringVar yourname(size),myname("tony");
yourname.getLine(cin);
cout<<"I am "<< myname <<endl; //友元函数重载了<<运算符
cout<<"Nice to meet you "<<yourname<<endl;
}
析构函数是类的成员函数,在类的对象离开作用域时被自动调用。例如:对象是某函数的局部变量,在函数调用结束时会销毁局部变量,而销毁的方式就是调用对象的析构函数。通过析构函数的调用,销毁对象中的动态变量,将其占用的内存还给自由存储。
上面的例子中,StringVar尚未定义析构函数。那么conversation执行结束要删除局部变量yourname和myname时,会调用编译器为StringVar自动生成的析构函数,但是自动生成的析构函数并不能正常的释放yourname和myname所占据的空间。如果conversation被重复执行,那么被占据的内存会越来越多,最终导致内存枯竭。
现在就为StringVar添加自己定义的析构函数,可以正常的释放value占据的自由存储空间。
#include <iostream>
#include <cstring>
using namespace std;
class StringVar
{
private:
char* value;
int maxLength;
public:
StringVar();
StringVar(int);
StringVar(const char[]);
~StringVar();//声明析构函数
int length() const;
void getLine(istream&);
friend ostream& operator<<(ostream&,const StringVar&);
};
StringVar::StringVar():maxLength(100)
{
value = new char[maxLength+1];
value[0]='\0';
}
StringVar::StringVar(int size):maxLength(size)
{
value = new char[size+1];
value[0]='\0';
}
StringVar::StringVar(const char a[] ):maxLength(strlen(a))
{
value = new char[maxLength+1];
strcpy(value,a);
}
//自定义析构函数,正确释放动态变量占用的空间
StringVar::~StringVar()
{
delete [] value;
}
int StringVar::length() const
{
return strlen(value);
}
void StringVar::getLine(istream& ins)
{
ins.getline(value,maxLength+1);
}
ostream& operator<<(ostream& outs,const StringVar& sr)
{
outs<<sr.value;
return outs;
}
~StringVar就是StringVar的析构函数。析构函数也是与类同名,只不过要在名称前面添加符号~。析构函数是无参的,不能指定返回值类型, 所以每个类只会有一个析构函数,不存在重载的析构函数。
接下来看一下拷贝函数的重要性。
在分析拷贝函数时,先看一段代码:
#include <iostream>
using namespace std;
typedef int* Intptr;
void myFunc(Intptr data);
int main()
{
Intptr i;
i = new int;
*i =100;
cout<<"i's value "<<*i<<endl;
myFunc(i);
cout<<"i's value "<<*i<<endl;
return 0;
}
void myFunc(Intptr data)
{
*data = 1000;
cout<<"inside myFunc data's value "<<*data<<endl;
}
凡是涉及指针就要明确两样东西:指针值和指针指向的值。
在执行myFunc函数之前,打印了一下变量(指针)i指向的值,随后,变量(指针)i作为实参以传值的形式传入函数myFunc。也就是将变量(指针)i的值复制给了data,换句话说,data(指针)与i指向了同一片内存区域。随即在函数中将data指向的值改变为了1000。随后,myFunc函数执行完毕,局部变量data被销毁。再次打印变量(指针)i指向的值时,这个值已然在函数中被修改为了1000,所以打印时输出的就是1000。
拷贝构造函数的名称与类同名,但要求一个参数,参数类型就是同类型,参数必须传引用,而且通常要附加const参数修饰符,使它成为常量参数。
void showContent(StringVar);
int main(void)
{
StringVar line("hello");
showContent(line);
cout<<line<<endl;
return 0;
}
void showContent(StringVar content)
{
cout<<"inside showContent function: "<<content<<endl;
}
上面的代码,首先创建了StringVar对象line,随后调用showContent函数,将line作为实参传入函数,此时要对line进行拷贝,此时调用的就是StringVar的拷贝构造函数。但是由于我们尚未定义这个函数,所以编译器会自动为StringVar生成一个拷贝构造函数,但是因为StringVar中含有指针内容,所以自动生成的拷贝构造函数并不能很好的完成拷贝构造函数应完成的功能。上面代码执行时,局部变量content中的value与实参line的value会指向同一个地址。当showContent函数执行完毕时,要调用StringVar的析构函数回收局部变量content中value所开辟的动态空间,随着析构函数的执行,content的value空间被回收,但同时也意味着line的value空间也被删除回收了。所以,当showContent函数执行完毕,再次利用cout输出line的内容时,是一片空白。
现在,我们手动定义StringVar的拷贝构造函数,拷贝构造函数的目的就是要确保通过参数创建对象时,要确保新建对象与参数对象是相互独立但内容完整的独立拷贝。
#include <iostream>
#include <cstring>
using namespace std;
class StringVar
{
private:
char* value;
int maxLength;
public:
StringVar();
StringVar(int);
StringVar(const char[]);
StringVar(const StringVar&);//声明拷贝构造函数
~StringVar();
int length() const;
void getLine(istream&);
friend ostream& operator<<(ostream&,const StringVar&);
};
StringVar::StringVar():maxLength(100)
{
value = new char[maxLength+1];
value[0]='\0';
}
StringVar::StringVar(int size):maxLength(size)
{
value = new char[size+1];
value[0]='\0';
}
StringVar::StringVar(const char a[] ):maxLength(strlen(a))
{
value = new char[maxLength+1];
strcpy(value,a);
}
//定义拷贝构造函数
//根据参数st的内容,开辟一段与st.value大小一样的新空间
//通过strcpy函数,在新空间中填充与st.value完全一样的内容
StringVar::StringVar(const StringVar& st):maxLength(st.length())
{
value = new char[maxLength+1];
strcpy(value,st.value);
}
StringVar::~StringVar()
{
delete [] value;
}
int StringVar::length() const
{
return strlen(value);
}
void StringVar::getLine(istream& ins)
{
ins.getline(value,maxLength+1);
}
ostream& operator<<(ostream& outs,const StringVar& sr)
{
outs<<sr.value;
return outs;
}
再次执行代码:
void showContent(StringVar);
int main(void)
{
StringVar line("hello");
showContent(line);
cout<<line<<endl;
return 0;
}
void showContent(StringVar content)
{
cout<<"inside showContent function: "<<content<<endl;
}
代码的执行结果是:
可以看到,showContent函数执行完毕,line的内容也能正常输出。这就说明这一次通过析构函数为参数开辟的是独立的空间,空间大小与内容与line完全一致。
拷贝构造函数一般会在三种情况下调用:
- 声明类的对象,并由同类型的另一个对象初始化。
- 函数返回类型的值。
- 函数的参数是类型参数。