目录 Table of Contents
9 复习 string 类的实现过程
接口设计
我现在要写一个 string 类, 于是我们先把 class 的括号写出来
其实我们这些都是写在一个头文件里面的, 所以在这之前会有一个防卫式的声明
class String
{
};
设计一个 class 我得先去思考里面放什么数据. 字符串里面会有很多字符. 一种想法是我用一个数组, 数组里面都是字符, 但是这种想法不好, 因为数组你要先指明它有多大; 所以一般设计字符串这种东西, 都是在里面放一个指针, 将来要放这个字符串的内容, 就动态地去分配它的大小
class String
{
private:
char* m_data;
};
当然数据是放在 private 里面的
接下来我就去想我要准备哪一些函数给外界调用
首先我要准备一个构造函数, 要给外界创建字符串一定会调用构造函数, 我一定要放在 public 里面
然后它的参数应该怎样设计, 现在我要它接受一个字符串, 这个字符串是传统 C 的形式的字符串
传进来之后当然是不可能改变这个传进来的指针的, 我们只是把它拿来当初值而已, 所以前面加 const
并且我让它默认值为零
class String
{
public:
String(const char* cstr = 0); // 构造函数
private:
char* m_data;
};
接下来我想的是, 字符串这种类型里面有指针, 这是一种很经典的类型, class with pointer member, 所以我一定要去关注 3 个重要的特殊函数
第一个就是拷贝构造函数, 它是一个构造函数所以函数名和类名一样
它既然是拷贝构造函数, 它要拷贝, 所以它要一个蓝本, 这个蓝本就是它自己, 所以它的参数我们就可以写出来了
第一优先考虑的是传递引用, 然后考虑要不要加 const
这个东西一传进来, 我们只是拿它做蓝本, 并不会改变它的内容, 所以这里加 const
class String
{
public:
String(const char* cstr = 0);
String(const String& str); // 拷贝构造函数
private:
char* m_data;
};
刚刚是拷贝构造函数
下一个考虑的是拷贝赋值函数
赋值的动作是把来源端拷贝到目的端去, 来源端是和我们这个 class 是一样的, 所以把它本身当作参数
我们并不打算改它, 所以这里前面加上 const
它返回什么东西, 来源端拷贝到目的端, 返回的结果就是目的端这种东西, 也就是字符串
要不要 return by reference 呢 ? 怎么判断 ? 你这个函数执行的结果要放到什么地方去, 是不是放到 local object, 只要它不是 local object 就可以传递 reference.
这一个从来源端到目的端, 把它赋值到目的端, 这个目的端是本来就存在的, 并不需要在这个函数里面去创建, 所以这边我们可以加 reference
class String
{
public:
String(const char* cstr = 0);
String(const String& str);
String& operator=(const String& str); // 拷贝赋值函数
private:
char* m_data;
};
三大函数最后一个就是析构函数
class String
{
public:
String(const char* cstr = 0);
String(const String& str);
String& operator=(const String& str);
~String(); // 析构函数
private:
char* m_data;
};
除此之外我再想一想要不要再写一些其它的辅助函数
由于我要写让字符串能够丢到 cout, console out, 做法很简单, 我设计一个函数获取这个字符串
为什么起名为 get_c_str ? 因为指针指向的这一种字符串是 C 风格的字符串
然后这个函数并没有改动数据, 所以这里加上 const
class String
{
public:
String(const char* cstr = 0);
String(const String& str);
String& operator=(const String& str);
~String();
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
上面的三个函数其实都有更改这一个指针的, 所以它们不可以加 const
它们是一定要改变目的端的数据的, 不可能加 const
ctor 和 dtor (构造函数和析构函数)
接口的设计已经完成了, 我们现在看看构造函数和析构函数怎么写
构造函数
因为现在这种写法已经很复杂了, 所以我把构造函数写在类的外面了, 所以要写出全名
再来考虑它的参数, 这个刚才已经考虑过了
inline
String::String(const char* cstr = 0)
{
}
这个是构造函数, 我们要考虑要分配足够的空间来存放初值. 这里先写一个判断, 判断传进来的指针是否真的有东西, 如果这个指针不是 0, 我就要准备一个足够大的空间存放这个字符串
传进来的字符串最后面会有一个结束符号, 但是用这个 strlen 函数取得的长度并没有算进这个结束符号, 所以这里我们加 1 来存放这个结束符号
下一个动作就是把我传进来的这个东西拷贝到我新分配的空间里面去, 这样就完成创建了我的一个新的字符串
既然写到了这两个函数, 我们就要去查手册去看看我们需要 include 什么头文件, 你可以看前面 include 哪些东西, 我这里没有写
inline
String::String(const char* cstr = 0)
{
if(cstr){
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
}
如果它传进来的时候真的没有初值, 那我们也要准备一个空间放结束符号
inline
String::String(const char* cstr = 0)
{
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;
}
析构函数就是要把自己清干净, 如果你曾经分配过一块内存, 打开过一个文件, 等等, 你都要在这里把它释放掉
由于上面用的数 array new, 所以下面我们要用 array delete
copy ctor 拷贝构造函数
它是构造函数, 函数全名写好; 参数刚刚已经考虑过了; 然后它没有返回类型
拷贝构造就是来源端当作蓝本, 拷贝到目的端
目的端我是一个新东西, 我就 new
一个足够大的空间, 然后把指拷贝过去
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1]
strcpy(m_data, str.m_data);]
}
那么什么样的函数可以写 inline 什么样的函数不能呢 ? 你也不要有太大心理负担, 你就把它全部写成 inline 也没关系, 反正也不会带来副作用
copy assignment operator (拷贝赋值函数)
先写出来函数名称, 然后是参数, 再就是返回类型
你的东西是从来源端到目的端, 这个目的端是本来就已经存在的东西
所以目的端这边要先把自己清掉, 清掉之后重新分配一块足够的空间
再就是把来源端拷贝到目的端
按理来说, 你拷贝完了之后已经 OK 了, 没有人在乎最后要返回什么了, 所以很多人最后不 return 了, 然后函数类型设计为 void.
因为我们使用者从 C 以来, 就很习惯把东西连串地赋值, 这种情况下你的返回类型就不能是 void
当时讲复数的时候就说过, 传出去的时候不必考虑接收者是怎么接受的, 就是这里 return 的时候我不用去考虑你是不是 by reference 接收 是不是 by value 接收
我们还提到过一个概念, 就是拷贝赋值一定要注意是不是自我赋值. 前面有提到, 如果不做自我赋值检查的话, 不只是效率的问题, 还有正确不正确的问题
inline
String& String::operator=(const String& str)
{
if(this == &str)
return *this; // 检查是否为自我赋值
delete[] m_data; // 1 清掉自己
m_data = new char[ strlen(str.m_data) + 1]; // 2 分配足够大的空间
strcpy(m_data, str.m_data); // 3 来源端拷贝到目的端
return *this; // 来源端拷贝到目的端, 最后结果是在目的端这边, 也就是现在的这一个类, 我们把结果返回
}