自定义字符串String类,分析构造函数,拷贝构造函数,运算符重载以及析构函数等类的基本实现。
构造函数
构造函数分无参构造函数和带参构造函数。
系统会自动生成缺省无参构造函数。当显式实现带参构造函数时,必须同时显式实现无参构造函数,否则无参构造函数会被覆盖。
显式实现构造函数有两种表达:
Ⅰ. String() = default;
String(const char *m_data);
//default表示保留隐式定义成员函数,delete表示删除默认成员函数
Ⅱ. String(const char *m_pData = nullptr);
其中,第二种实现需在函数中对无参(即nullptr)情况进行判断处理。
拷贝构造函数
拷贝构造函数也是构造函数,因此并无函数返回值。缺省拷贝构造函数是浅拷贝的实现。
深拷贝与浅拷贝
深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。
浅拷贝: 只是对基本数据类型进行复制。当类含有指针类型的数据成员时,浅拷贝复制的也仅仅是指针,即指向内容的内存地址,结果就是两个不同的指针指向同一个内存空间;
深拷贝:在堆内存重新申请一块内存空间,对指针指向的内容也进行拷贝。
因此,浅拷贝存在两种隐患:
Ⅰ.若存在指针类型的数据成员,则对其中一个对象进行修改,由于指向同一块内存空间,另一个对象同时被修改。
Ⅱ.当对象生命周期结束调用析构函数时,会对同一块内存空间析构两次,造成内存泄漏。
赋值运算符重载
首先,赋值运算符重载函数声明如下:
String &operator =(const String &str);
为什么返回值和对象形参要用引用传值的方式?
I. 为了避免对象的多次拷贝与析构,提高效率。若采用值传递的方式,在执行return语句的时候,系统会产生一个临时变量,然后将该变量返回存储于外部变量中。函数结束时该临时变量会执行析构函数,降低代码执行效率,参数也是同理。
II. 返回值采用引用形式,还可以实现如(a = b) = c
的连续表达。若返回临时变量,则编译报错。因为c++规定临时变量都是const类型的变量,因此只能有一次初始化的过程,不能作为左值进行赋值。
III. 一般常见返回*this,即当前函数指针。不能返回函数内局部对象的引用,因为临时对象在函数结束时析构,此时引用指向未知的空间,会造成内存泄漏。(注:部分情况下可以正常使用,是因为临时对象存在于栈中还没有被覆盖。)
基本数据类型、数组类型和指针类型作形参为什么不用引用传值?
I. 基本数据类型和指针类型作为形参时,值传递方式或者引用传递方式的效率几乎相同。因为基本数据类型的复制不需要拷贝和析构的过程,因此没有必要用引用传值的方式。
II. 数组类型作为形参时,会“退化”成指针类型,无法获取数组的长度。程序在编译时也只检查参数类型,并不检查数组的长度。
为了防止数组在作为形参时“退化”成指针,可定义“数组的引用”方式 (不作形参时该定义是不被允许的):
I. String &operator = (const char (&arr)[10]);
//(&arr)括号不可省略,并且需精确指定数组长度(10)。
II. String &str = "abc"; //编译报错
赋值运算符重载实现:
String& String::operator = (const String& str) {
if(this == &str)
return *this;
delete []m_pData;
m_pData = nullptr;
m_pData = new char[strlen(str.m_pData) + 1];
strcpy(m_pData, str.m_pData);
return *this;
}
其中,对this == &str
的判断尤为重要。
I. 为了避免自我赋值,提高效率;
II. 若对象中含有指针型数据成员,则在赋值时需要先释放(delete)指向的内存空间,重新申请内存空间。如果是对象本身的自赋值,delete之后会成为野指针,无法进行赋值。
在上述的函数中,在分配内存之前先delete释放了实例m_pData的内存。如果此时内存不足导致new char抛出异常,则m_pData同样成为野指针。
考虑到函数的异常安全性,我们可以先new分配内存,然后在delete释放已有内容。如果分配内存时抛出异常,还没有修改原来的实例,保证了异常安全性。
先创建一个实例,再交换临时实例和原来的实例:
String &String::operator = (const String &str) {
if(this != &str) {
String sTemp(str);
char *mTemp = sTemp.m_pData;
sTemp.m_pData = m_pData;
m_pData = mTemp;
}
return *this;
}
利用创建临时实例sTemp,在构造函数里先new分配内存,之后交换sTemp与实例自身的m_pData,sTemp作用域结束调用析构函数时,也就把实例之前m_pData的内存释放掉了。
另外,在对赋值函数调用时需要注意以下三种形式的区别:
I. String str1 = str2; //初始化,不是赋值,调用拷贝构造函数
II. String str1; //初始化,调用无参构造函数
str = str2; //调用赋值函数
III. String str1 = "abc"; //先调用带参构造函数,再调用拷贝构造函数
IV.String str1;
str1 = "abc"; //String &operator =(const char *str);
如第四种方式注释的所示,重载赋值运算符函数也可以用除对象类型以外的其他类型作参数。此时,第四种方式会直接调用该赋值函数。
注意:只有赋值的时候会调用赋值函数,初始化(第三种)调用的是构造函数。
移动拷贝构造函数
默认拷贝构造函数是浅拷贝,可能造成指针悬挂,所以要重写拷贝构造函数实现深拷贝。
但是有些情况如,作为函数返回值或者函数传入形参时,是并不需要深拷贝的,大量的深拷贝会影响程序性能。因此,有了移动拷贝构造函数。
String::String(String &&str):m_pData(str.m_pData) {
str.m_pData = nullptr;
}
移动赋值运算符函数
实现原理与移动构造函数相同。
String &String::operator = (String &&str) {
if(this != &str) {
m_pData = str.m_pData;
str.m_pData = nullptr;
}
return *this;
}
其中,移动构造函数和移动赋值函数所用到的rvalue reference(右值引用),具体原理和应用参照c++11 特性 之 右值引用.
代码实现
实现基本成员函数,包括构造函数,拷贝构造函数,移动构造函数,赋值函数,移动赋值函数,运算符下标重载,输入输出运算符重载,等号运算符重载,加法运算符重载。
完整代码如下:
String.h
(注意:头文件不要包含using namespace std
等using编译命令,或者using std::cout
等using命令)
#include<iostream>
class String {
public:
/*String() = default;
String(const char *pData);*/ //等价于下面定义的构造函数
String(const char *pData = nullptr);
String(const String &str);
String(String &&str);
String &operator = (const String &str);
String &operator = (String &&str);
friend std::istream &operator >> (std::istream &in, String &str);
friend std::ostream &operator << (std::ostream &out, const String &str);
friend bool operator == (const String &str1, const String &str2);
char &operator [] (const unsigned int index) const;
friend String operator + (const String &str1, const String &str2);
virtual ~String();
private:
char *m_pData;
};
String.cpp
#include<iostream>
#include "String.h"
#include<cstring>
using namespace std;
//构造函数
String::String(const char *pData) { //不用再写nullptr
if(pData == nullptr) {
m_pData = new char('\0');
} else {
m_pData = new char[strlen(pData) + 1];
strcpy(m_pData, pData);
}
}
//拷贝构造函数
String::String(const String &str) {
m_pData = new char[strlen(str.m_pData) + 1];
strcpy(m_pData, str.m_pData);
}
//移动构造函数
String::String(String &&str):m_pData(str.m_pData) {
str.m_pData = nullptr;
}
//赋值函数
String &String::operator =(const String &str) {
if(this != &str) {
String sTemp(str);
char *mTemp = sTemp.m_pData;
sTemp.m_pData = m_pData;
m_pData = mTemp;
}
return *this;
}
//移动赋值函数
String &String::operator = (String &&str) {
if(this != &str) {
m_pData = str.m_pData;
str.m_pData = nullptr;
}
return *this;
}
//输入运算符重载
istream &operator >> (istream &in, String &str) {
char *pData = new char[100];
in>>pData;
str.m_pData = new char[strlen(pData) + 1];
strcpy(str.m_pData, pData);
delete []pData;
pData = nullptr;
return in;
}
//输出运算符重载
ostream &operator << (ostream &out, const String &str) {
out<<str.m_pData;
return out;
}
//等号运算符重载
bool operator == (const String &str1, const String &str2) {
if(strcmp(str1.m_pData, str2.m_pData) == 0)
return true;
else
return false;
}
//下标运算符重载
char &String::operator[](const unsigned int index) const {
int len = strlen(m_pData);
if(index < len)
return m_pData[index];
else
return m_pData[len];
}
//加法运算符重载
String operator + (const String &str1, const String &str2) {
int len = strlen(str1.m_pData) + strlen(str2.m_pData);
char *m_pData = new char[len + 1];
strcpy(m_pData, str1.m_pData);
strcat(m_pData, str2.m_pData);
return String(m_pData);
}
//析构函数
String::~String() {
delete []m_pData;
m_pData = nullptr;
}
main.cpp
#include<iostream>
#include"String.h"
using namespace std;
int main() {
return 0;
}