通过自己创造string来探究其内部构造

    说string是编程时用的最多的库类一点也不为过 , 其实 , 我们也能通过编程来自己实现这个类 , 现在就让我们来一探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的框架 , 还有很多的成员函数没有实现 , 迭代器也没有完成 , 这部分的任务留给读者自己完成 , 我想说的是 , 不必将函数内部实现的与原版一模一样 , 只需要知道它的实现思路即可 。 






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值