C++ STL标准库解析|简单实现STL容器string代码


我们在使用STL内置的string类型的时候:

#include <string>
int main () {
    string str1;
    string str2 = "aaa"; // String(const char*)
    string str3 = "bbb";
    string str4 = str2 + str3;
    string str5 = str2 + "ccc";
    string str6 = "ddd" + str2;
    
    cout << "str6: " << str6 << endl;
    if (str5 > str6) {
        cout << str5 << " > " << str6 << endl;
    } else {
        cout << str5 << " < " << str6 << endl;
    }
    int len = str6.length();
    for (int i = 0; i < len; ++i) {
        //str6.operator[](i)
        cout << str6[i] << " ";
    }
    cout << endl;

    // string -> char*
    char buf[1024] = { 0 };
    strcpy(buf, str6.c_str()); //c_str()返回一个const char*类型
    cout << "buf: " << buf << endl;
    
    return 0;
}

从上文代码可以看出,STL中的string类型包含了加法运算符重载、拷贝构造、重载赋值运算符,重载了<< ,重载了[],让string能够像数组一样完成调用。
现在我们一步步实现代码吧。

确定成员变量

class string {
public:
private:
	char *_pstr;
}

确定构造函数和析构函数

String(const char *p = nullptr) {
	if (p != nullptr) {
		_pstr = new char[strlen(p) + 1]
		strcpy(_pstr, p);
	} else {
		_pstr = new char[1];
		*_pstr = '\0';
	}
}
  1. 构造函数参数
    string str2 = "aaa";可以看出,我们的构造函数应该接受一个常量指针参数’p’,默认值为nullptr,意味着如果创建对象时没有提供参数,p 将默认为空指针。
  2. 检查参数是否为空
  3. 分配内存并复制字符串
  • strlen(p) + 1:计算字符串 p 的长度,并加上 1 以包含终止字符 \0。
  • new char[strlen(p) + 1]:为 _pstr 分配足够的内存来存储传入的字符串。
  • strcpy(_pstr, p):将传入的字符串 p 复制到 _pstr 所指向的内存区域。
  1. 处理空指针的情况
    如果 p 是空指针,则分配一个大小为 1 的字符数组,并将其初始化为空字符串(即仅包含终止字符 \0)。

关于析构函数就比较简单了。
直接delete掉指针指向的堆内存,然后指针置为空。

~String() {
	delete[] _pstr;
	_pstr = nullptr;
}

确定拷贝构造和复制构造

String(const String &str) {
	_pstr = new char[strlen(str._pstr) + 1];
	strcpy(_pstr, str._pstr);
}

拷贝构造很简单,直接开辟一个新内存,然后把源字符串的内容拷贝进去即可。

String& operator=(const String &str) {
	if (this == &str) return *this;
	
	delete[] _pstr;
	_pstr = new char[strlen(str._pstr) + 1];
	strcpy(_pstr, str._pstr);
	return *this;
}

关于赋值运算符,首先要看自赋值的情况,如果没有自赋值,释放当前对象的已有内存资源,以防止内存泄漏。

重载 > == <

bool operator>(const String &str) const {
	return strcmp(_pstr, str._pstr) > 0;
}

bool operator<(const String &str) const {
	return strcmp(_pstr, str._pstr) < 0;
}

bool operator==(const String &str) const {
	return strcmp(_pstr, str._pstr) == 0;
}

比较简单不解释

重载[]

重载下标运算符 operator[],以便访问字符串中的字符。这里定义了两个重载版本的 operator[],分别用于非常量和常量对象。这两个重载函数实现了不同的访问权限:一个允许修改字符串中的字符,另一个只允许读取字符。

char& operator[](int index) { return _pstr[index]; }
const char& operator[](int index) const { return _pstr[index]; }

实现length()和c_str()

length()和c_str()分别返回字符串的有效字符长度和C风格字符串,其实就是返回一个const char*类型

int length() const { return strlen(_pstr); }
const char* c_str() const { return _pstr;}

重载 operator+ 和 operator<<

为了实现字符串的拼接str1 + str2,我们必须重载operator+。并且选择将其作为全局函数并在类中声明为友元有一些优势,尤其是在涉及对称性和类型转换的情况下。

  1. 对称性主要指的是我们可以处理“其他类型+String”、“String+其他类型”和“String+String”,如果我们把operator+作为成员函数,那么第一个操作数必须是类的实例:String operator+(const String &str) const { ... }
  2. 类型转换主要指的也是上面这种情况,编译器可以处理 “其他类型 + String”和“String + 其他类型”的情况。(这是编译器自己进行处理的隐式类型转换)

具体实现如下:

String operator+(const String &lhs, const String &rhs) {
	char *ptmp = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1];
	strcpy(ptmp, lhs._pstr);
	strcat(ptmp, rhs._pstr);
	String tmp(ptmp);
	delete[] ptmp;
	return tmp;
	
}
  • 计算新字符串的长度:新字符串的长度是 lhs 和 rhs 两个字符串长度之和,再加上一个用于存储终止空字符 \0 的额外空间。
  • 分配内存:使用 new 分配足够的内存来存储新字符串。
  • 复制和连接字符串:使用 strcpy 将 lhs 的字符串复制到新分配的内存中。使用 strcat 将 rhs 的字符串连接到新字符串的末尾。
  • 创建临时 String 对象:使用连接后的字符串创建一个临时的 String 对象(这个临时对象在堆上)
  • 释放临时内存:删除用于临时存储连接字符串的内存。
  • 返回新 String 对象:返回包含连接字符串的临时 String 对象。

重载<<就相当简单了,就是基本套路

ostream& operator<<(ostream &out, const String &str) {
    out << str._pstr;
    return out;
}

随后我们记得声明友元:

private:
    char *_pstr;

    friend ostream& operator<<(ostream &out, const String &str);
    friend String operator+(const String &lhs, const String &rhs);

测试

这里再次使用代码进行测试:

int main () {
    String str1;
    String str2 = "aaa"; // String(const char*)
    String str3 = "bbb";
    String str4 = str2 + str3;
    String str5 = str2 + "ccc";
    String str6 = "ddd" + str2;
    
    cout << "str6: " << str6 << endl;
    if (str5 > str6) {
        cout << str5 << " > " << str6 << endl;
    } else {
        cout << str5 << " < " << str6 << endl;
    }
    int len = str6.length();
    for (int i = 0; i < len; ++i) {
        //str6.operator[](i)
        cout << str6[i] << " ";
    }
    cout << endl;

    // String -> char*
    char buf[1024] = { 0 };
    strcpy(buf, str6.c_str()); //c_str()返回一个const char*类型
    cout << "buf: " << buf << endl;
    
    return 0;
}

可以自行测试,肯定是和我们使用std::string输出是一样的。

结语

我们还需要重点关注operator+,其实该函数功能上确实是没有问题的,而且也没有内存泄漏的问题。

String operator+(const String &lhs, const String &rhs) {
	char *ptmp = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1];
	strcpy(ptmp, lhs._pstr);
	strcat(ptmp, rhs._pstr);
	String tmp(ptmp);
	delete[] ptmp;
	return tmp;
}

但是该函数的效率比较低,主要是因为

首先char *ptmp = new char[...],ptmp指向了我们new出来的一块大内存,然后我们分别进行字符串拷贝和字符串连接;

随后传入到tmp构造函数中String tmp(ptmp);。然后这个tmp又会根据我们外面传进来的指针再进行一次new,然后再次进行拷贝strcpy;

String(const char *p = nullptr) {
	if (p != nullptr) {
		_pstr = new char[strlen(p) + 1]
		strcpy(_pstr, p);
	} else {
		_pstr = new char[1];
		*_pstr = '\0';
	}
}

最后我们又delete掉ptmpdelete[] ptmp;

虽然return tmp,由于tmp是这个函数的局部对象,所以tmp要析构,所以又把我们在String tmp(ptmp)中new的那块内存又delete掉了。

也就是说,里面总共两次new了内存,又两次delete(这里考虑了ROV),未免太麻烦了。在下一节,我们会讨论如何解决该问题

解决operator+效率低下的问题

我们采用如下解决方案:

String operator+(const String &lhs, const String &rhs) {
	//char *ptmp = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1];
	String tmp;
	tmp._pstr = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1]
	strcpy(tmp._pstr, lhs._pstr);
	strcat(tmp._pstr, rhs._pstr);
	
	//delete[] ptmp;
	return tmp;
}

直接定义一个tmp对象,我直接给它的底层进行一个内存开辟,然后在进行字符串拷贝和连接直接给到临时对象底层的内存中。这样我们就避免了一个new和delete

我们在第一次优化中,仍然返回的是一个tmp的值,这里面如果我想去返回引用那是不行的,因为我们不能返回一个局部对象的地址或者引用返回到函数外面去。

这里面我们一定要给我们的string提供右值引用的拷贝构造和赋值重载函数,这里一直还是我们要说的对象优化问题。以后有时间一定写出来…

扩展内容:实现string::iterator迭代器

迭代器是连接STL容器和STL算法的桥梁,所以无论如何,我们都应该实现它的容器迭代器。

请移步:【string字符串对象的迭代器实现string::iterator】

string总体代码(含迭代器)

class String {
public:
    String(const char *p = nullptr) {
       if (p != nullptr) {
            _pstr = new char[strlen(p) + 1];
            strcpy(_pstr, p);
        } else {
            _pstr = new char[1];
            *_pstr = '\0';
        }
    }
    ~String() { 
        delete[] _pstr; 
        _pstr = nullptr;
    }
    String(const String &str) {
        _pstr = new char[strlen(str._pstr) + 1];
        strcpy(_pstr, str._pstr);
    }
    String& operator=(const String &str) {
        if (this == &str)
            return *this;
        
        delete[] _pstr;
        _pstr = new char[strlen(str._pstr) + 1];
        strcpy(_pstr, str._pstr);
        return *this;
    }

    bool operator>(const String &str) const {
        return strcmp(_pstr, str._pstr) > 0;
    }

    bool operator==(const String &str) const {
        return strcmp(_pstr, str._pstr) == 0;
    }

    bool operator<(const String &str) const {
        return strcmp(_pstr, str._pstr) < 0;
    }

    int length() const { return strlen(_pstr); }
    // char ch = str6[6]; str6[6] = '7'
    char& operator[](int index) { return _pstr[index]; }
    // char ch = str6[6]; 不允许修改! str6[6] = '7'
    const char& operator[](int index) const { return _pstr[index]; }

    const char* c_str() const { return _pstr; }

    //给String字符串类型提供迭代器的实现
    class iterator {
    public:
        iterator(char *p = nullptr) : _p(p) { }
        bool operator!=(const iterator &it) {
            return _p != it._p;
        }
        void operator++() {
            ++_p;
        }
        char& operator*() { return *_p;}
    private:
        char *_p;
    };
    //begin返回的是容器底层首元素的迭代器表示
    iterator begin() { return iterator(_pstr); }
    //end返回的是容器底层末尾元素后继位置的迭代器表示
    iterator end() { return iterator(_pstr + length()); }
private:
    char *_pstr;

    friend ostream& operator<<(ostream &out, const String &str);
    friend String operator+(const String &lhs, const String &rhs);
};

String operator+(const String &lhs, const String &rhs) {
    //char *ptmp = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1];
    String tmp;
    tmp._pstr = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1];
    strcpy(tmp._pstr, lhs._pstr);
    strcat(tmp._pstr, rhs._pstr);
    //delete[] ptmp;
    return tmp;
}
ostream& operator<<(ostream &out, const String &str) {
    out << str._pstr;
    return out;
}
  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值