一个类中只要带有指针类型的成员,就必须自己写出big three(构造函数、拷贝构造函数,拷贝赋值函数),如果没有指针类型的成员,大部分情况下可以用默认的。
字符串类是一个经典的例子:
#ifndef __MYSTRING__
#define __MYSTRING__
#include <cstring>
#include <iostream>
using namespace std;
class String
{
public:
String(const char* cstr=0); //构造函数,加const表示不修改*cstr的值
String(const String& str); //拷贝构造函数,加const表示不修改str指向的值,用&只占用4个字节的内存
String& operator=(const String& str); //拷贝赋值函数,加const表示不修改str指向的值,用&只占用4个字节的内存
~String();
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
inline
String::String(const char* cstr)
{
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = '\0';
}
}
inline
String::~String()
{
delete[] m_data; //由于构造函数是new了一个数组,所以析构函数也需要使用delete []
}
//返回值为String&类型,&表示要赋值的目的端已经存在,即s1 = s2; s1已存在
//并且有时候会连续赋值,s1 = s2 = s3; 所以需要返回值为String&类型
inline
String& String::operator=(const String& str)
{
if (this == &str) //检测自我赋值,这一步必须有,本行的&str和形参的&str意义不一样。如果s1 = s2;则形参&str == s2,本行&str == &s2
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
//输出字符串操作符重载,不能是一个成员函数,否则使用的时候方向要相反,即需要这样用 s1 << cout
ostream& operator<<(ostream& os, const String& str)
{
os << str.get_c_str();
return os;
}
#endif
1.构造函数、析构函数和拷贝构造函数
inline
String::String(const char* cstr) //构造函数
{
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = '\0';
}
}
inline
String::~String() /析构函数
{
delete[] m_data; //由于构造函数是new了一个数组,所以析构函数也需要使用delete []
}
inline
String::String(const String& str) //拷贝构造函数
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
根据代码规范,不超过10行的函数可以定义为内联函数,所以在构造和析构函数前都加了inline,上面构造函数超过了10行,但是内联不内联是由编译器决定的,所以加上inline也没啥毛病。
构造函数中,如果传入的参数有效的话则new一个strlen(cstr)+1大小的数组给成员变量,并将参数拷贝到成员变量中;
如果传入的参数无效,则将’\0’传给成员变量,当然,也需要new一个字节的空间来保存。
构造函数的使用:
String s1(); //参数无效
String s2("world"); //参数有效
对于析构函数,由于构造函数是new了一个数组,所以析构函数也需要使用delete []。
拷贝构造函数的参数类型就是类的类型,初始化步骤如代码所示。
拷贝构造函数的使用:
String s2("world");
String s3(s2);
2.拷贝赋值函数
//返回值为String&类型,&表示要赋值的目的端已经存在,即s1 = s2; s1已存在
//并且有时候会连续赋值,s1 = s2 = s3; 所以需要返回值为String&类型
inline
String& String::operator=(const String& str)
{
if (this == &str) //检测自我赋值,这一步必须有,本行的&str和形参的&str意义不一样。如果s1 = s2;则形参&str == s2,本行&str == &s2
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
①拷贝赋值函数的返回值类型应是by reference(传引用),因为拷贝赋值函数是将一个对象赋值给另一个对象,即要复制的目的端已经存在,所以可以用引用;还有一个原因是调用拷贝赋值函数是有可能是连续赋值的,如s1 = s2 = s3; ,这也需要返回值类型为引用才可以。
②拷贝赋值函数必须要有检测自我赋值这一步,检测自我赋值一个好处是当是自我赋值时后面的几步不需要再执行,从而节省了时间。另一个必要原因就是如果没有检测自我赋值这一步,并且遇到自我赋值时,代码逻辑会出错。
构造函数在赋值前会delete自己的成员变量,如果是自我赋值的话,会清空当前对象的值,后面赋值时对象已经被delete了,所以对象中保存的内容已经不能确定是什么了。
关于返回值类型必须使用值传递的函数的问题
如下面的程序:
class complex
{
public:
complex (double r = 0, double i = 0): re (r), im (i) { }
double real () const { return re; } //加const表示不会改变数据内容,尽量加上const
double imag () const { return im; }
private:
double re, im;
};
inline double
imag (const complex& x)
{
return x.imag ();
}
inline double
real (const complex& x)
{
return x.real ();
}
inline complex
operator + (const complex& x, const complex& y) //返回值不能用by reference,因为如果是c1 + c2;这一句执行完后c1 + c2的值在函数结束后就被释放了,
//如果用by reference,会返回错误的东西
{
return complex (real (x) + real (y), imag (x) + imag (y));
}
上面的“+”操作符重载函数的返回值就必须使用值传递,因为当函数返回后,函数中的变量就会随着函数返回,其生命周期也就随之结束,如果返回类型使用引用的话,那么引用指向的内存也是不存在的,所以这种函数的返回类型不能使用引用传递。
注:上面的“+”操作符重载函数应该有三个,此处只写了一个。