通过自己创造string来探究其内部构造
2015-09-19 19:08
357 查看
说string是编程时用的最多的库类一点也不为过 , 其实 , 我们也能通过编程来自己实现这个类 , 现在就让我们来一探string库内部的奥秘吧。
实现string可以用两种办法 , 因为string就是一个char的有序表 , 所以可以通过线性表和链表来实现 , 从直观上讲 , 线性表跟符合string的本质 , 不过我们选择用char指针动态分配char数组来实现 。在私有对象里 , 除了用来分配的char数组外 , 还要有一个int用来保存string长度 。对于构造函数 , 首先是一个无参数的默认构造函数 , 然后还要有一个接受C风格字符串的构造函数 , 还要在定义一个析构函数 。 所以先写出下列代码:
接下来我们考虑如何定义这两个函数:(已引入cstring头文件)
接下来我们得考虑几个问题 , 当我们用一个String构造另一个String时 (比如 String s1 = s2;), 如果没有定义拷贝构造函数 , 编译器会帮忙定义一个默认拷贝构造函数 ,然而这却是帮倒忙了 , 因为默认的拷贝构造函数是将函数中的成员变量全部复制 , 换句话说 , 就是对每一个变量执行=赋值操作 , 而对于指针复制之后 , 就变成两个str指向同一块地址 , 这样必然会导致一个问题 , 当我们其中一个对象被销毁后 , 就已经把那块内存delete了 , 那么另一个对象的str指针就指向一块没分配的内存
, 更糟的是 , 当另一个对象被销毁时 , 就要delete一块已经释放的内存 , 这样必然导致程序出错甚至崩溃 , 同样的 , 若我们没重载=运算符 , 也会发生上述问题 , 所以我们要在class中加入上述函数:
这里有几个问题要说明一下 , 首先是为什么参数都是引用常量 , 第一 , 引用比传值效率更高 , 第二 , 常量可以接纳常量和非常量 , 如果不是常量的话 ,是无法传常量进去的(其实就是非常量可以用常量引用 , 常量却不能用非常量引用) 。 为什么赋值函数返回的是自身的引用 , 其一也是从效率出发 , 其二是因为这是赋值函数的惯例 , 这样就能够进行连续赋值(比如 s1 = s2 = s3) , 最后 ,我们为什么应该对赋值函数定义一个接受C风格字符串的版本呢 , 因为之前我们定义了String(const
String &) 这个构造函数 , 当我们要把一个C风格字符串赋值给一个已经定义好的String(即已经执行过构造函数了)时 , 就在这里进行一次类型转换 , 把参数转化成String类型 , 在调用以String为参数的版本的赋值函数 。
接下来一个没有BUG的String类终于出来了 , 但是它还没什么功能 , 于是我们为他增加多一点功能 , 首先重载+运算符:
接下来还有一个问题 , 为什么这里返回的是一个局部的String , 原因是相加并不改变其中的任意一个String , 而且不能返回局部值得引用(那样的话离开函数作用域那个引用就无效了)所以我们只能返回一个新的String , 这也符合C++其他的类型的定义 。再者 , 为什么第二个函数在类内没有进行声明 , 反而跑到外面去声明 , 这是因为 + 号的左边(即+的调用者 ,const char* c) 并不是一个String , 而且该操作也不会访问String的私有成员 , 所以不需要声明友元函数。顺便说一句
, 关于参数为char*的版本是为了说明其内部的转换过程 , 即使不定义也会自动转换 , 但这其实是一种名为代理的模式 , 即使用其他同名重载函数来代替该函数工作。
既然有了+的定义 , 为什么没有+=的定义呢 , 这个作为练习留给读者完成(思考返回的是引用还是值)。
最后我们再来定义输入输出 , 说白了就是要重载<< 和>> , 对于输出<<非常简单 , 只需要将字符串输出即可 , 输入会稍微复杂一点 , 涉及重载>>和getline版本 :
与标准库一样 , 重载的版本返回了流本身 , 在这里提一下 , 255为输入的最大字符数 , 超过的话 , >>版本会将后面的字符留在流中(当然前提是没遇到空格或回车) ,
getline的话会将超出部分丢弃 。如果流被破坏 , 则原本的字符不会受到影响 。
还有两个比较重要的函数 , 一个就是返回字符串长度的size() , 另一个就是重载的[ ] 运算符:
我们已经做好了一个String的框架 , 还有很多的成员函数没有实现 , 迭代器也没有完成 , 这部分的任务留给读者自己完成 , 我想说的是 , 不必将函数内部实现的与原版一模一样 , 只需要知道它的实现思路即可 。
实现string可以用两种办法 , 因为string就是一个char的有序表 , 所以可以通过线性表和链表来实现 , 从直观上讲 , 线性表跟符合string的本质 , 不过我们选择用char指针动态分配char数组来实现 。在私有对象里 , 除了用来分配的char数组外 , 还要有一个int用来保存string长度 。对于构造函数 , 首先是一个无参数的默认构造函数 , 然后还要有一个接受C风格字符串的构造函数 , 还要在定义一个析构函数 。 所以先写出下列代码:
class String{ private: int length; char * str; public: String() = default; String(const char *); ~String(){ delete [] str} };
接下来我们考虑如何定义这两个函数:(已引入cstring头文件)
String::String(){ str = new char[1]; length = 0; str[0] = '\0'; } String::String(const char* c){ length = strlen(c); str = new char[length + 1]; strcpy(str , c); }
接下来我们得考虑几个问题 , 当我们用一个String构造另一个String时 (比如 String s1 = s2;), 如果没有定义拷贝构造函数 , 编译器会帮忙定义一个默认拷贝构造函数 ,然而这却是帮倒忙了 , 因为默认的拷贝构造函数是将函数中的成员变量全部复制 , 换句话说 , 就是对每一个变量执行=赋值操作 , 而对于指针复制之后 , 就变成两个str指向同一块地址 , 这样必然会导致一个问题 , 当我们其中一个对象被销毁后 , 就已经把那块内存delete了 , 那么另一个对象的str指针就指向一块没分配的内存
, 更糟的是 , 当另一个对象被销毁时 , 就要delete一块已经释放的内存 , 这样必然导致程序出错甚至崩溃 , 同样的 , 若我们没重载=运算符 , 也会发生上述问题 , 所以我们要在class中加入上述函数:
class String{ public: String(const String&); String& operator=(const String&); String& operator=(const char*); }; //在上述的class中增加的 String::String(const String& s){ length = s.length; str = new char[length + 1]; strcpy(str , s.str); } String& String::operator=(const String& s){ if(this == &s) return *this; delete [] str; length = s.length; str = new char[length + 1]; strcpy(str , s.str); return *this; } String& String::operator=(const char* c){ *this = String(c); return *this; }
这里有几个问题要说明一下 , 首先是为什么参数都是引用常量 , 第一 , 引用比传值效率更高 , 第二 , 常量可以接纳常量和非常量 , 如果不是常量的话 ,是无法传常量进去的(其实就是非常量可以用常量引用 , 常量却不能用非常量引用) 。 为什么赋值函数返回的是自身的引用 , 其一也是从效率出发 , 其二是因为这是赋值函数的惯例 , 这样就能够进行连续赋值(比如 s1 = s2 = s3) , 最后 ,我们为什么应该对赋值函数定义一个接受C风格字符串的版本呢 , 因为之前我们定义了String(const
String &) 这个构造函数 , 当我们要把一个C风格字符串赋值给一个已经定义好的String(即已经执行过构造函数了)时 , 就在这里进行一次类型转换 , 把参数转化成String类型 , 在调用以String为参数的版本的赋值函数 。
接下来一个没有BUG的String类终于出来了 , 但是它还没什么功能 , 于是我们为他增加多一点功能 , 首先重载+运算符:
class String{ public: String operator+(const String&); String operator+(const char*); }; String operator+(const char* c , const String& s); String String::operator+(const String& s){ char temp[length + s.length + 1]; strcpy(temp , str); strcpy(&temp[length] , s.str); return String(temp); } String String::operator+(const char* c){ return *this + String(c); } String operator+(const char* c , const String& s){ return String(c) + s; }
接下来还有一个问题 , 为什么这里返回的是一个局部的String , 原因是相加并不改变其中的任意一个String , 而且不能返回局部值得引用(那样的话离开函数作用域那个引用就无效了)所以我们只能返回一个新的String , 这也符合C++其他的类型的定义 。再者 , 为什么第二个函数在类内没有进行声明 , 反而跑到外面去声明 , 这是因为 + 号的左边(即+的调用者 ,const char* c) 并不是一个String , 而且该操作也不会访问String的私有成员 , 所以不需要声明友元函数。顺便说一句
, 关于参数为char*的版本是为了说明其内部的转换过程 , 即使不定义也会自动转换 , 但这其实是一种名为代理的模式 , 即使用其他同名重载函数来代替该函数工作。
既然有了+的定义 , 为什么没有+=的定义呢 , 这个作为练习留给读者完成(思考返回的是引用还是值)。
最后我们再来定义输入输出 , 说白了就是要重载<< 和>> , 对于输出<<非常简单 , 只需要将字符串输出即可 , 输入会稍微复杂一点 , 涉及重载>>和getline版本 :
class String{ public: friend ostream& operator<<(ostream& , const String&); friend istream& operator>>(istream& , String&); }; istream& getline(istream& , String&); ostream& operator<<(ostream& os , const String& s){ os << s.str; return os; } istream& operator>>(istream& is , String& s){ char c[255]; int i = 0; while(i < 254 && (c[i] = is.get()) != '\0' && c[i] != '\n'){ i++; } c[i] = '\0'; if(is) s = String(c); return is; } istream& getline(istream& is , String& s){ char c[255]; is.getline(c , 255); if(is){ s = String(c); } while(is && is.get() != '\n') continue; return is; }
与标准库一样 , 重载的版本返回了流本身 , 在这里提一下 , 255为输入的最大字符数 , 超过的话 , >>版本会将后面的字符留在流中(当然前提是没遇到空格或回车) ,
getline的话会将超出部分丢弃 。如果流被破坏 , 则原本的字符不会受到影响 。
还有两个比较重要的函数 , 一个就是返回字符串长度的size() , 另一个就是重载的[ ] 运算符:
class String{ public: int size(){ return length;} char& operator[](int index){ return str[index]; } const char& operator[](int index) const { return str[index]; } };这里提一下为什么要重载两个版本的[ ] 运算符 , 第一个版本是能够任意修改的 , 因为任意的数组和指针访问的结果都是引用 。 第二个版本是适用于不改变内容或常量String访问的。
我们已经做好了一个String的框架 , 还有很多的成员函数没有实现 , 迭代器也没有完成 , 这部分的任务留给读者自己完成 , 我想说的是 , 不必将函数内部实现的与原版一模一样 , 只需要知道它的实现思路即可 。
相关文章推荐
- win7反复自动关机
- Android动画操作
- 移动仲裁邮箱
- springMvc redis 配置开发案例
- 打造eBox生态圈
- HDU 5459 Jesus Is Here 2015沈阳区域赛网络赛1010题
- LeetCode -- Ugly Number
- [HDOJ5461]Largest Point
- Android多线程机制之Handler
- Juniper SRX防火墙系统会话链接的清除
- CoreTFManagerVC 强大的键盘躲避管理器,完全解耦版本!
- 保存图片到自定义相册文件夹中,实现处理相册被用户误删的情况
- 自信,有风范儿,人生处处有奇迹。
- 对似然函数的理解
- [HDOJ5455]Fang Fang
- 【转载】-java Socket用法详解
- 自定义View(三) switch开关按钮 ViewDragHelper的使用初级
- Android高级控件小练习
- HDU 5461 Largest Point(关键在于最值)——2015 ACM/ICPC Asia Regional Shenyang Online
- 使用ATL创建简单ActiveX控件(二) —— 添加方法/属性和枚举