【C++修炼】string类:string类的模拟实现

string类的模拟实现

该实现为简化版本,仅作了解string类之用,不求功能之全面与完善,只选择性实现一些关键接口或功能点,实现尽量靠近STL的设计规范。暂不考虑字符编码问题,故不实现为 basic_string,不将string类设计成模板。

共包含3个代码文件:头文件 string.h、源文件 string.cpp、功能测试文件 Test.cpp。

string类模拟实现的主要内容如下:

cpp 复制代码
// string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace fym
{
    class string
    {
        friend ostream& operator<<(ostream& out, const string& s);
        friend istream& operator>>(istream& in, string& s);
    public:  // 成员函数
        // 迭代器
        typedef char* iterator;
        typedef const char* const_iterator;
        iterator begin();
        iterator end();
        const_iterator begin() const;
        const_iterator end() const;

        // 默认成员函数:默认构造、拷贝构造、赋值重载、析构
        string(const char* str = "");
        string(const string& s);
        string& operator=(const string& s)
        ~string();
        
        // Capacity接口:字符存储与空间容量相关接口
        size_t size() const;
        size_t capacity() const;
        void clear();
        bool empty() const;
        void resize(size_t n, char c = '\0');    
        
        // Element access接口:数组元素访问相关接口
        char& operator[](size_t index);
        const char& operator[](size_t index) const;
        
        // Modifiers接口:字符串修改相关接口
        void swap(string& s);
        void push_back(char c);
        void append(const char* str);
        string& operator+=(char c);
        string& operator+=(const char* str);
        string& insert(size_t pos, char c);
        string& insert(size_t pos, const char* str);
        string& erase(size_t pos, size_t len = npos);
        
        // String operations接口:string对象操作接口
        const char* c_str() const;
        size_t find(char c, size_t pos = 0) const;
        size_t find(const char* s, size_t pos = 0) const;
        string substr(size_t pos = 0, size_t len = npos) const;
    private:  // 成员变量
        char* _str;
        size_t _size;
        size_t _capacity;
        static const size_t npos;  // 最大无符号整型
    };
    // 重载为全局函数的string类接口
    bool operator<(const string& s1, const string& s2);
    bool operator<=(const string& s1, const string& s2);
    bool operator>(const string& s1, const string& s2);
    bool operator>=(const string& s1, const string& s2);
    bool operator==(const string& s1, const string& s2);
    bool operator!=(const string& s1, const string& s2);
    ostream& operator<<(ostream& out, const string& s);
    istream& operator>>(istream& in, string& s);
}

1. string类的默认成员函数

默认构造、析构

string类的基础成员变量有3个:_str、_size 与 _capacity。

    1. string类对象存储字符串,其数据结构应为顺序表,即数组,且为动态数组,故需要一个 char* 类型指针 _str,指向动态申请的空间的地址,扩容时更新空间地址即可。
    1. _size 记录当前存储的有效字符个数,即字符串长度 (不包括'\0'),单位元素个数。
    1. _capacity 记录数组的最大容量,方便知道动态开辟的空间大小,单位元素个数。

构造初始化需求:

    1. string类有资源申请,所以必须显式实现默认构造,可选择无参构造 或全缺省构造 。这里选择实现全缺省构造。实现全缺省构造同时也是实现了带参构造。
    1. 默认构造就是无需传实参的构造。不传参的情况下,_str 的初始化,不可以是空指针 nullptr,应是空字符串,即字符数组存储的有效字符数为0。但空字符串也要跟一个 '\0',可以不存储有效字符,但一定要存储 '\0',故初始化 _str 至少要动态申请1 byte的空间存储 '\0'。
    1. '\0' 不算入容量 _capacity。_capacity 只记录数组最大可存储的有效字符数。但实际开空间时,为了存储 '\0',需要多开1 byte的空间,即每次扩容实际开空间 _capacity+1。

析构清理资源:

  • 资源的清理主要指释放 _str 中动态申请的字符数组的空间。

string.h代码示例 1.1

cpp 复制代码
// string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

// 命名空间隔离:避免与标准库实现的string类冲突
namespace fym
{
    class string
    {
    public:
        /* 
          短小频繁调用的函数,直接定义在类里面string.h文件中,默认是内联函数。一般成员函
          数声明与定义分离,类中声明函数,类外定义函数,函数定义可放到string.cpp,不同文
          件的同一命名空间fym可以合并,相当于定义在全局。
        */
        // 用函数体内赋值的方式进行初始化
        // 易错点:缺省参数是"",而不是"\0"
        string(const char* str = "")  // 默认构造:全缺省构造
        {
            /* 
              全缺省构造初始化思路:相当于既要实现带参构造,又要实现无参构造。若有传实参
              ,则按形参str初始化,计算其有效字符个数,再动态开辟相应容量的空间,然后拷
              贝str中的字符到开辟的空间中;若未传实参,str有缺省参数,默认值为空字符串
              "",也相当于传实参了,初始化逻辑与传实参一致。
            */
            _size = strlen(str);  // strlen()可计算有效字符个数并返回,不包括'\0'
            _capacity = _size;  // '\0'不计入_capacity
            _str = new char[_capacity + 1];  // 实际开空间_capacity+1
            // strcpy先拷贝再判断,先拷贝str各个字符,拷贝到'\0'则停止
            strcpy(_str, str);  
        }
        ~string()  // 析构
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }
    
        // 不建议用初始化列表的方式初始化     
        // string(const char* str = "")
        //     :_str(new char[strlen(str)+1])
        //     ,_size(strlen(str))
        //     ,_capacity(_size)
        // { 
        //     strcpy(_str, str);
        // }
    private:
        // 成员变量的声明
        char* _str;
        size_t _size;
        size_t _capacity;
    };
}
  • 关于缺省参数,string(const char* str = "\0") 是错误的,正确应该是 "" ,双引号里面什么都不写,空格也没有。"" 两个双引号即可表示此处是字符串,其后自动跟了一个 '\0',如果在双引号内写字符,不管是 '\0' 还是空格 ' ',都会被视作有效字符,字符数组的一个元素,其作用和 "a" 里面的字符 'a' 一样。因此,"\0" 其实相当于字符数组存储了两个 '\0'。
  • 当成员变量的初始化顺序必须为特定次序时,不建议用初始化列表初始化。初始化的顺序是由声明顺序决定,初始化列表中成员变量的书写次序并不是它们的初始化顺序,因此通常建议声明次序与初始化列表的次序保持一致。此处需要先计算形参 str 的字符串长度,才能据此开辟足够空间,并将其地址赋值给指针 _str。计算字符串长度需要调用 strlen() 函数,有性能消耗,如果先初始化 _str 调用 strlen(),字符串长度没保存下来,初始化 _size 还要调用一次 strlen(),这就增加了不必要的消耗。当然,也++不建议通过修改成员变量的声明顺序解决问题++,这样写代码健壮性低,错误不易观察,一旦被不熟悉代码的人不小心修改代码,徒增问题排查难度。

Test.cpp代码示例 1.2

cpp 复制代码
// Test.cpp
#include<iostream>
// 引入C++的std::string类库文件string,所属命名空间为std
#include<string>
using namespace std;
// 引入自己编写的fym::string类头文件fym::string.h,所属命名空间为fym
#include"string.h"

int main()
{
    // 用模拟实现的fym::string定义s1、s2对象
	fym::string s1;
	fym::string s2("A123");

    // 用标准库中的std::string定义s3、s4对象
	string s3;  
	string s4("B456");

	return 0;
}

运行调试,结果:

拷贝构造、赋值重载

string.h代码示例 1.3

cpp 复制代码
// string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

// 命名空间隔离:避免与标准库实现的string类冲突
namespace fym
{
    class string
    {
    public:
        // 默认成员函数:默认构造、拷贝构造、赋值重载、析构
        string(const char* str = "")  // 默认构造
        {
            _size = strlen(str);
            _capacity = _size;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }
        ~string()  // 析构
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }
        string(const string& s)  // 拷贝构造
        {
            // 初始化:开空间,拷贝现有对象s的字符串到新空间
            _str = new char[s._capacity + 1];
            strcpy(_str, s._str);
            _size = s._size;
            _capacity = s._capacity;
        }
        string& operator=(const string& s)  // 赋值重载
        {
            // 赋值重载类似拷贝构造,但不是初始化新对象,是赋值修改已有对象
            if (this != &s)  // 检查自己给自己赋值的情况
            {
                // 释放原空间
                delete[] _str;
                // 根据s开辟相同大小的空间,再拷贝字符串
                _str = new char[s._capacity + 1];
                strcpy(_str, s._str);
                _size = s._size;
                _capacity = s._capacity;
            }
            return *this;
        }
    private:
        char* _str;
        size_t _size;
        size_t _capacity;
    };
}

2. string类的迭代器Iterators

迭代器:可用指针实现最简单的迭代器。

迭代器是C++封装特性的体现。提供了一种统一的方式,统一的接口,用类似的方式便利地访问不同数据结构,如顺序表、链表等,而无需关心底层实现和细节。

string.h、Test.cpp代码示例 2.1

cpp 复制代码
// string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace fym
{
    class string
    {
    public:
        // 迭代器
        // 反向迭代器暂不实现,仅实现正向迭代器及其const版本
        // 指针是最简单的迭代器,更复杂一点可以是类或类模板等
        typedef char* iterator;  // 普通对象用普通迭代器
        typedef const char* const_iterator;  // const对象用const迭代器
        iterator begin()  // 返回首元素的位置
        {
            return _str;
        }
        iterator end()  // 返回末尾元素的下一个位置
        {
            return _str + _size;
        }
        const_iterator begin() const
        {
            return _str;
        }
        const_iterator end() const
        {
            return _str + _size;
        }
        
        // 默认成员函数:默认构造、析构
        string(const char* str = "")  // 默认构造
        {
            _size = strlen(str);
            _capacity = _size;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }
        ~string()  // 析构
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }
    private:
        char* _str;
        size_t _size;
        size_t _capacity;
    };
}



// Test.cpp
#include<iostream>
#include<string>
using namespace std;
#include"string.h"

// 测试函数
void Test()
{
	fym::string s1("1234567");
	
	// iterator就是string类中char*类型的重命名,it底层就是指针,begin底层返回地址
	fym::string::iterator it = s1.begin();
	// 这里的it!=s1.end(),本质是指针运算,指针地址比较
	while (it != s1.end())
	{
		// it指针属于内置类型,无需*运算符重载
        // 用类或类模板实现的迭代器属于自定义类型就需要*运算符重载
		cout << *it << " ";
		// 本质是指针的解引用,字符元素的值+=2
		*it += 2;
		cout << *it << endl;
		// 指针的自增++运算,地址+1,即指向下一元素的地址
		++it;
	}

	// 范围for的底层就是迭代器,编译器在底层调用begin/end等
    // 但注意,迭代器相关函数begin/end等需严格按照标准库来命名,否则范围for无法识别
    // 例如begin函数不能写成Begin、bEgin函数或其他不同形式
	cout << endl;
	for (auto& e : s1)
	{
		cout << e << " ";
		e -= 2;
		cout << e << endl;
	}
}

int main()
{
	Test();

	return 0;
}

Test函数测试结果:

3. Capacity相关接口

成员函数size、capacity、clear、empty

  • size 方法:返回string类的成员变量 _size 的值,即字符串长度/大小,以字节byte为单位。
  • capacity 方法:返回string类的成员变量 _capacity 的值,即字符数组空间大小,字符最大容量,以字节byte为单位。
  • clear 方法:清空字符串,得到空字符串,字符串长度 _size 归零,容量 _capacity 不变。
  • empty 方法:判断字符串是否为空,若字符串长度 _size 为0,则返回 true;若不为0,则返回 false。

string.h、Test.cpp代码示例 3.1

cpp 复制代码
// string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace fym
{
    class string
    {
    public:
        // 默认成员函数:默认构造、析构
        string(const char* str = "")  // 默认构造
        {
            _size = strlen(str);
            _capacity = _size;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }
        ~string()  // 析构
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }

        // Capacity接口:字符存储与空间容量相关接口
        size_t size() const  // 获取字符串长度/大小
        {
            return _size;
        }
        size_t capacity() const  // 获取空间容量
        {
            return _capacity;
        }
        void clear()  // 清空字符串
        {
            // 空字符串也有一个元素,即'\0',无需更改其余元素,首元素为'\0'即空字符串
            _str[0] = '\0';
            // 字符串长度/大小,置为0即可
            _size = 0;
        }
        bool empty() const  // 字符串判空
        {
            return _size == 0;
        }
    private:
        char* _str;
        size_t _size;
        size_t _capacity;
    };
}



// Test.cpp
#include<iostream>
#include<string>
using namespace std;
#include"string.h"

void Test()
{
	fym::string s1("1234567");
	for (auto& e : s1)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << "size:" << s1.size() << endl;
	cout << "capacity:" << s1.capacity() << endl;
	if (s1.empty() == true) cout << "字符串为空!" << endl;
	else cout << "字符串不为空!" << endl;
	
	// s1对象字符串置为空
	s1.clear();
	for (auto& e : s1)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << "size:" << s1.size() << endl;
	cout << "capacity:" << s1.capacity() << endl;
	if (s1.empty() == true) cout << "字符串为空!" << endl;
	else cout << "字符串不为空!" << endl;
}

int main()
{
	Test();

	return 0;
}

Test函数测试运行结果:

成员函数reserve

reserve 方法:预留空间,发出改变空间容量 _capacity 的请求。如果请求的空间大小 n 大于当前容量 _capacity,则扩容。如果 n 小于当前容量 _capacity,标准库中的 reserve 是发出一个不具有约束力的缩容请求,但具体是否执行不确定,看编译器。一般情况下,默认 reserve 是只扩容不缩容的,此处模拟实现也是如此。

string.h、string.cpp、Test.cpp代码示例 3.2

cpp 复制代码
// string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace fym
{
    class string
    {
    public:
        // 迭代器
        typedef char* iterator;
        typedef const char* const_iterator;
        iterator begin()  // 返回首元素的位置
        {
            return _str;
        }
        iterator end()  // 返回末尾元素的下一个位置
        {
            return _str + _size;
        }
        const_iterator begin() const
        {
            return _str;
        }
        const_iterator end() const
        {
            return _str + _size;
        }

        // 默认成员函数:默认构造、析构
        string(const char* str = "")  // 默认构造
        {
            _size = strlen(str);
            _capacity = _size;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }
        ~string()  // 析构
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }

        // Capacity接口:字符存储与空间容量相关接口
        size_t size() const  // 获取字符串长度/大小
        {
            return _size;
        }
        size_t capacity() const  // 获取空间容量
        {
            return _capacity;
        }
        // 较复杂的成员函数声明与定义分离,类中声明,类外定义
        void reserve(size_t n);  // 函数声明
    private:
        char* _str;
        size_t _size;
        size_t _capacity;
    };
}



// string.cpp
#include"string.h"

// 不同文件的相同名称命名空间可以合并成同一命名空间
namespace fym
{
    // 类外定义成员函数,相当于在string.h文件的命名空间fym中进行定义
    void string::reserve(size_t n)  // 预开空间
    {
        // 多数情况下,默认只扩容不缩容
        if (n > _capacity)
        {
            // 异地扩容:存储字符串默认多开1个空间,n+1
            char* tmp = new char[n + 1];
            // 拷贝数据到新空间,释放旧空间,_str指向新空间,更新空间容量
            strcpy(tmp, _str);
            delete[] _str;
            _str = tmp;
            _capacity = n;
        }
    }
}



// Test.cpp
#include<iostream>
#include<string>
using namespace std;
#include"string.h"

void Test()
{
	fym::string s1;
	cout << "s1:";
	for (auto& e : s1)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << "size:" << s1.size() << endl;
	cout << "capacity:" << s1.capacity() << endl;

	// 预开空间,开9个字符元素的空间,'\0'不用考虑不计入_capacity,但会自动多开一个空间
	s1.reserve(9);
	cout << "s1:";
	for (auto& e : s1)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << "size:" << s1.size() << endl;
	cout << "capacity:" << s1.capacity() << endl;
}

int main()
{
	Test();

	return 0;
}

Test测试函数运行结果:

4. Element access相关接口

方括号\[\]运算符重载:让string类对象能模仿数组,通过下标访问或修改底层的字符数组中的元素。重载两个运算符重载,一个适用普通对象,可以访问加修改,返回普通引用;一个适用const对象,只可访问不可修改,返回const引用。

string.h、Test.cpp代码示例 4.1

cpp 复制代码
// string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace fym
{
    class string
    {
    public:
        // 默认成员函数:默认构造、析构
        string(const char* str = "")  // 默认构造
        {
            _size = strlen(str);
            _capacity = _size;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }
        ~string()  // 析构
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }

        // Element access接口:数组元素访问相关接口
        // 普通对象调用普通版本
        char& operator[](size_t index)  // []运算符重载
        {
            // assert断言,检查下标合法性,可强制中断程序报错
            assert(index < _size);
            // 返回index下标位置的元素
            return _str[index];
        }
        // const对象调用const版本
        const char& operator[](size_t index) const  // []运算符重载的const版本
        {
            assert(index < _size);
            return _str[index];
        }
    private:
        char* _str;
        size_t _size;
        size_t _capacity;
    };
}



// Test.cpp
#include<iostream>
#include<string>
using namespace std;
#include"string.h"

// 测试函数Test
void Test()
{
    fym::string s1("1234567");
	for (int i = 0; i < s1.size(); i++)
	{
		// 普通对象s1调用普通版本的方括号重载,返回普通引用,可访问可修改
		cout << s1[i] << " ";
		s1[i] += 2;
		cout << s1[i] << endl;
	}

	const fym::string s2("1234567");
	for (int i = 0; i < s2.size(); i++)
	{
		// const对象s2调用const版本的方括号重载,返回const引用,可访问不可修改
		cout << s2[i] << " ";
		// s2[i] += 2;
		cout << s2[i] << endl;
	}
}

int main()
{
	Test();

	return 0;
}

Test方法测试结果:

5. Modifiers相关接口

成员函数push_back、append、operator+=

  • push_back 方法:尾插一个字符,可能需要扩容,可用 reserve 扩容。
  • append 方法:尾插一个字符串,可能需要扩容,可用 reserve 扩容。
  • +=运算符重载:尾插一个字符串或一个字符,可复用 push_back 和 append。

string.h、string.cpp、Test.cpp代码示例 5.1

cpp 复制代码
// string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace fym
{
    class string
    {
    public:
        // 迭代器
        typedef char* iterator;
        typedef const char* const_iterator;
        iterator begin()  // 返回首元素的位置
        {
            return _str;
        }
        iterator end()  // 返回末尾元素的下一个位置
        {
            return _str + _size;
        }
        const_iterator begin() const
        {
            return _str;
        }
        const_iterator end() const
        {
            return _str + _size;
        }

        // 默认成员函数:默认构造、析构
        string(const char* str = "")  // 默认构造
        {
            _size = strlen(str);
            _capacity = _size; 
            _str = new char[_capacity + 1]; 
            strcpy(_str, str);
        }
        ~string()  // 析构
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }

        // Capacity接口:字符存储与空间容量相关接口
        size_t size() const  // 获取字符串长度/大小
        {
            return _size;
        }
        size_t capacity() const  // 获取空间容量
        {
            return _capacity;
        }
        void reserve(size_t n);  // 预开空间,成员函数声明

        // Modifiers接口:字符串修改相关接口
        void push_back(char c);  // 尾插字符,成员函数声明
        void append(const char* str);  // 尾插字符串,成员函数声明
        string& operator+=(char c);  // +=字符,成员函数声明
        string& operator+=(const char* str);  // +=字符串,成员函数声明
    private:
        char* _str;
        size_t _size;
        size_t _capacity;
    };
}



// string.cpp
#include"string.h"

namespace fym
{
    void string::reserve(size_t n)  // 预开空间
    {
        // 多数情况下,默认只扩容不缩容
        if (n > _capacity)
        {
            // 异地扩容:存储字符串默认多开1个空间,n+1
            char* tmp = new char[n + 1];
            // 拷贝数据到新空间,释放旧空间,_str指向新空间,更新空间容量
            strcpy(tmp, _str);
            delete[] _str;
            _str = tmp;
            _capacity = n;
        }
    }
    void string::push_back(char c)  // 尾插字符
    {
        // 判断扩容
        if (_size == _capacity)
        {
            // 检查空字符串的情况,_capacity为0开4个空间即可,否则直接二倍扩
            reserve(_capacity == 0 ? 4 : 2 * _capacity);
        }
        // 扩容完毕,插入字符,更新长度,字符串尾添加'\0'
        _str[_size] = c;
        _size++;
        _str[_size] = '\0';
    }
    void string::append(const char* str)  // 尾插字符串
    {
        // 判断扩容
        // 字符串str长度不确定,不能直接二倍扩
        size_t len = strlen(str);
        if (_size + len > _capacity)
        {
            // 判断:如果二倍扩,空间足够就二倍扩,空间不够就按需扩容,避免频繁二倍扩
            reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
        }
        // 插入字符串:拷贝字符串str到_str+_size及其后位置,即'\0'及其后位置
        strcpy(_str + _size, str);
        // strcpy连同str的'\0'一起拷贝到_str后,无需手动给'\0',直接更新_size即可
        _size += len;
    }
    string& string::operator+=(char c)  // string对象+=字符
    {
        // 复用push_back尾插字符的逻辑,并返回*this
        push_back(c);
        return *this;
    }
    string& string::operator+=(const char* str)  // string对象+=字符串
    {
        // 复用append尾插字符串的逻辑,并返回*this
        append(str);
        return *this;
    }
}



// Test.cpp
#include<iostream>
#include<string>
using namespace std;
#include"string.h"

void Test()
{
	fym::string s1;
	cout << "s1:";  // 空字符串
	for (auto& e : s1)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << "size:" << s1.size() << endl;  // 0
	cout << "capacity:" << s1.capacity() << endl;  // 0

	// push_back尾插字符
	s1.push_back('A');
	cout << "s1:";  // "A"
	for (auto& e : s1)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << "size:" << s1.size() << endl;  // 1
    // push_back第一次扩容,统一4字节,0->4
	cout << "capacity:" << s1.capacity() << endl;  

	// append尾插字符串
	s1.append("12345");
	cout << "s1:";  // "A12345"
	for (auto& e : s1)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << "size:" << s1.size() << endl;  // 6
    // append第二次扩容,字符串长度6,二倍扩,0->4->8
	cout << "capacity:" << s1.capacity() << endl;  

	// +=字符、+=字符串
	s1 += 'B';
	s1 += "9876543210";
	cout << "s1:";  // "A12345B9876543210"
	for (auto& e : s1)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << "size:" << s1.size() << endl;  // 17
    // +=字符串第三次扩容,字符串长度17,按需扩容,0->4->8->17
	cout << "capacity:" << s1.capacity() << endl;  
}

int main()
{
	Test();

	return 0;
}

Test测试函数运行结果:

成员函数insert、erase

insert 方法:

  • 在string对象字符串中,任意位置上插入字符或字符串,可头插,可尾插,也可在中间插入。

erase 方法:

  • 在string对象字符串中,任意删除 pos 位置开始的 len 个字符,不足 len 个字符删到字符串结束。
  • 缺省参数 len 的默认值是 npos,可近似看作无穷大,删除 npos 个字符即默认删除到字符串结尾。

npos 成员变量:

  • string类的另一成员变量 npos,代表无符号整型 size_t 能存储的最大整数,32位下最大值为 2^32-1,64位下最大值为 2^64-1。
  • 原型:static const size_t npos;,既是常量,又是静态成员变量。
  • 一、静态成员变量在类里面声明,但不走构造函数、初始化列表,只能在类外定义。二、.h头文件不能在全局定义函数或变量,头文件被引用会在其他文件展开,相当于这些文件都有一份函数/变量的全局定义。各文件链接时,全局上就会发生定义冲突、重定义。综合两点,npos 的定义最好放在源文件 string.cpp 中,只放声明到头文件 string.h 中,声明是可以有多个的。

static const 成员变量是一个具有常性的静态成员变量,如果是整型 ,语法则允许直接在类里面定义,无需在类外定义,则无需因为定义在全局而将声明与定义分离到两个文件 string.h 和 string.cpp。类中定义有类域保护,作用域不同,不会发生定义冲突。

如果不是整型(short、int、size_t、longlong等),bool、char等类型也可以,但浮点型(double、float等)不行,即使静态成员变量具有常性,仍需在类外定义。

该特点可实现一种写法:定义一个具有常性的整型静态成员变量,用于指定数组成员变量的大小。

如:static const size_t N = 10;
int buffN;

虽然语法允许这样直接定义,但不建议这样写,尽量遵循成员变量的语法定义:类里面仅声明成员变量,在初始化列表定义与初始化成员变量。这种写法当作特殊处理即可。

string.h、string.cpp、Test.cpp代码示例 5.2

cpp 复制代码
// string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace fym
{
    class string
    {
    public:
        // 默认成员函数:默认构造、析构
        string(const char* str = "")  // 默认构造
        {
            _size = strlen(str);
            _capacity = _size; 
            _str = new char[_capacity + 1]; 
            strcpy(_str, str);
        }
        ~string()  // 析构
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }

        // Capacity接口:字符存储与空间容量相关接口
        size_t size() const  // 获取字符串长度/大小
        {
            return _size;
        }
        size_t capacity() const  // 获取空间容量
        {
            return _capacity;
        }
        void reserve(size_t n);  // 预留空间

        // Element access接口:数组元素访问相关接口
        char& operator[](size_t index)  // []运算符重载
        {
            assert(index < _size);
            return _str[index];
        }

        // Modifiers接口:字符串修改相关接口
        string& insert(size_t pos, char c);  // 插入字符
        string& insert(size_t pos, const char* str);  // 插入字符串
        string& erase(size_t pos, size_t len = npos);  // 删除字符/字符串
    private:
        // 成员变量的声明
        char* _str;
        size_t _size;
        size_t _capacity;
        static const size_t npos;
        // 可直接在声明处定义npos,如static const size_t npos = 1;
        // 但建议只在类里面声明成员变量,npos在类外定义,放到string.cpp中
    };
}



// string.cpp
#include"string.h"

namespace fym
{
    // 将npos的声明与定义分离到两个文件,类外定义成员变量需要声明类域string::
    const size_t string::npos = -1;

    void string::reserve(size_t n)  // 预开空间
    {
        // 只扩容
        if (n > _capacity)
        {
            char* tmp = new char[n + 1];
            strcpy(tmp, _str);
            delete[] _str;
            _str = tmp;
            _capacity = n;
        }
    }
    string& string::insert(size_t pos, char c)  // 插入字符,只实现不带次数n
    {
        // insert任意插入字符,可头插也可尾插,有效范围pos>=0且pos<=_size
        assert(pos <= _size);
        // 判断是否需要扩容
        if (_size == _capacity)
        {
            reserve(_capacity == 0 ? 4 : 2 * _capacity);
        }
        // 中间插入:pos位置及其后位置的字符需要后移,空出1个字符的位置
        size_t end = _size + 1;  // end指向'\0'位置
        while (end > pos)  // 最后一次循环end=pos+1
        {
            // 直到把pos位置的字符也后移完为止,即end=pos+1,end-1=pos
            _str[end] = _str[end - 1];
            end--;
        }

        // 如下写法有bug,循环的结束不能是end=pos-1
        // size_t end = _size;
        // while (end >= pos)
        // {
        //     _str[end + 1] = _str[end];
        //     end--;
        // }
        /*
          需注意:end是无符号整型size_t,最小等于0,不能存负数。
          while循环需要end=pos-1才能结束,而insert是可以头插的,如果pos=0,则永远无
          法结束,pos=-1就相当于变成npos了。
          也不能将end改为int,因为形参pos是size_t类型。当操作符两端操作数类型不匹配时
          会进行强转,进行类型提升或类型截断,像比较或加减运算等,会将类型值范围小的提
          升成范围大的类型,32位下,int和size_t都是4个字节,但一个有符号,一个无符号,
          无符号可存储的值更大,因此这里int会被类型提升为size_t。除非修改形参pos的类型
          ,但牵一发动全身,而且pos是下标位置,本身就不可为负数,不建议改为int。
          或者下面这种方式,显式类型转换也是可以的:
            while (end >= (int)pos)
          也可以不用下标+循环,使用指针+循环或memmove,但需要指针_str与地址:
            memmove(_str+pos+1, _str+pos, _size-pos+1);
            或char* end=_str+_size;while (end >= _str+pos)
        */
        
        // 字符后移完毕,位置空出,则插入字符,更新长度_size,并返回
        _str[pos] = c;
        _size++;
        return *this;
    }
    string& string::insert(size_t pos, const char* str)  // 插入字符串
    {
        assert(pos <= _size);
        // 判断是否扩容
        size_t len = strlen(str);
        if (_size + len > _capacity)
        {
            // 判断是按需扩容还是二倍扩容
            reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
        }
        // 后移空出len个字符的位置
        // 注意循环结束条件,不能让end=-1,pos和len都可以为0
        size_t end = _size + len;
        while (end > pos + len - 1)
        {
            // 将pos位置及其后位置所有字符后移len个位置
            // 最后一次循环,end=pos+len,即_str[pos+len]=_str[pos]
            _str[end] = _str[end - len];
            end--;
        }
        // 逐个插入len个字符,更新长度,并返回
        for (int i = 0; i < len; i++)
        {
            _str[pos + i] = str[i];
        }
        _size += len;
        return *this;
    }
    // 注意对比erase的声明与定义的细节:缺省参数只在声明时给,定义时无需给,也不能给
    string& string::erase(size_t pos, size_t len)  // 删除字符/字符串
    {
        // 从任意的pos位置开始删除字符,有效范围:头删pos=0,尾删pos=_size-1
        assert(pos < _size);
        // 删除字符,无需操作字符,无需缩容,只需要移动'\0',同时更新字符串长度
        if (len >= _size - pos)  // 情况一:需要删除到字符串结束
        {
            // _size-pos是pos及其后所有字符个数,即可删除字符数,len>=,则全部删除
            // 移动'\0',更新长度
            _str[pos] = '\0';
            _size = pos;
        }
        else  // 情况二:删除到中间位置停下
        {
            /*
              删除[pos, pos+len-1]范围的len个字符,将pos+len-1后所有字符移到前面即
              可即使其后字符个数不足len个,但包括'\0',相当于'\0'后所有字符都删除了
            */
            for (int i = pos + len; i <= _size; i++)
            {
                _str[i - len] = _str[i];
            }
            // 删去len个字符,更新长度
            _size -= len;
        }
        return *this;
    }
}



// Test.cpp
#include<iostream>
#include<string>
using namespace std;
#include"string.h"

void Test()
{
	fym::string s("12345");
	// insert插入字符
	s.insert(0, 'A');  // "A12345"
	for (int i = 0; i < s.size(); i++)
	{
		cout << s[i] << " ";
	}
	cout << endl;
	s.insert(s.size(), 'A');  // "A12345A"
	for (int i = 0; i < s.size(); i++)
	{
		cout << s[i] << " ";
	}
	cout << endl;
	// insert插入字符串
	s.insert(0, "BB");  // "BBA12345A"
	for (int i = 0; i < s.size(); i++)
	{
		cout << s[i] << " ";
	}
	cout << endl;
	s.insert(s.size(), "CC");  // "BBA12345ACC"
	for (int i = 0; i < s.size(); i++)
	{
		cout << s[i] << " ";
	}
	cout << endl;
	// erase删除字符
	s.erase(0, 1);  // "BA12345ACC"
	for (int i = 0; i < s.size(); i++)
	{
		cout << s[i] << " ";
	}
	cout << endl;
    // 删除字符串
	s.erase(s.size() - 4, 3);  // "BA1234C"
	for (int i = 0; i < s.size(); i++)
	{
		cout << s[i] << " ";
	}
	cout << endl;
	s.erase(2, 100);  // len过大,则删到字符串结束,"BA"
	for (int i = 0; i < s.size(); i++)
	{
		cout << s[i] << " ";
	}
	cout << endl;
}

int main()
{
	Test();

	return 0;
}

运行结果:

成员函数swap

swap 方法:交换成员变量的值,即交换 _str、_size 与 _capacity。注意:swap 类似浅拷贝,不能申请资源,即动态开辟空间,它只是交换两个已经动态开辟的空间。

通过 swap 我们可以实现拷贝构造与赋值重载的另一种玩法。

cpp 复制代码
// string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace fym
{
    class string
    {
    public:
        // 默认成员函数:默认构造、拷贝构造、赋值重载、析构
        string(const char* str = "")  // 默认构造
        {
            _size = strlen(str);
            _capacity = _size;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }
        ~string()  // 析构
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }
        string(const string& s)  // 拷贝构造
        {
            // 传统写法:直接逐个初始化*this对象的成员变量
            //_str = new char[s._capacity + 1];
            //strcpy(_str, s._str);
            //_size = s._size;
            //_capacity = s._capacity;

            /*
              现代写法:用s对象拷贝构造*this,可以先拷贝构造临时string对象,再交换给
              *this。复用默认构造的逻辑,构造一个临时string对象tmp,不止重新动态开辟
              了一块空间,且tmp与s的值完全一致,只需要把tmp的值包括动态开辟的空间全部
              交换给*this,就完成了拷贝构造。
            */
            string tmp(s._str);
            swap(tmp);
        }
        string& operator=(const string& s)  // 赋值重载
        {
            if (this != &s)  // 检查自己给自己赋值的情况
            {
                //// 传统写法
                //// 释放原空间
                //delete[] _str;
                //// 根据s开辟相同大小的空间,再拷贝字符串
                //_str = new char[s._capacity + 1];
                //strcpy(_str, s._str);
                //_size = s._size;
                //_capacity = s._capacity;

                /*
                  现代写法:在赋值重载中实现比在拷贝构造中实现还额外多一个好处,拷贝构
                  造的*this是未初始化的string对象,但赋值重载的*this对象存在资源,交
                  换后*this中的资源都到了tmp中,tmp出了函数作用域即销毁,自动调用析构
                  函数清理资源,省了传统写法中的delete的功夫。
                */
                string tmp(s._str);
                swap(tmp);
            }
            return *this;
        }
        // 或者string形参对象不用传引用传参,用传值传参
        // string& operator=(string tmp)
        // {
        //     // 形参tmp即上面的临时对象,直接交换给*this即可
        //     swap(tmp);
        //     return *this;
        // }

        // Modifiers接口:字符串修改相关接口
        void swap(string& s)
        {
            /*
              string类有一个std::string::swap成员函数,专为string类对象服务;还有一
              个算法库algorithm中实现的swap函数模板std::swap,支持各种类型。C++11之
              前用swap模板生成的函数是通过创建第三个临时对象来实现交换的,交换char*、
              size_t等内置类型时swap模板生成的函数效果还行,但遇到深拷贝有不少消耗。
              C++11后在标准库std全局专门为string实例化实现了一个swap函数:
                std::swap(string),原型void swap(string &x, string &y);
              string对象调用swap,有这个更匹配的实例化就不会用函数模板。其实,该实例
              化也相当于调用成员函数的swap,s1.swap(s2),只是出于安全等各方面考虑,
              借助在全局实现该实例化,阻止函数模板的特化。因此总共有3个swap,一个是函
              数模板,另两个是函数且都为string类服务,其中一个是string的成员函数,一
              个是函数模板关于string特别实现的实例化。
            */
            std::swap(_str, s._str);
            std::swap(_size, s._size);
            std::swap(_capacity, s._capacity);
        }
    private:
        char* _str;
        size_t _size;
        size_t _capacity;
        static const size_t npos;
    };
}

该现代写法更有封装的韵味,不关心string对象具体是如何初始化的,而是直接创建一个新的对象与其进行交换,从而达成初始化,不仅仅是值相同,资源也自然而然地申请了一份。

6. String operations相关接口

成员函数c_str、find、substr

c_str 方法:返回string类底层的字符串,一个指向字符数组空间的 char* 类型指针 _str。

find 方法:在string对象的字符串中,从 pos 位置开始查找匹配字符,或匹配子串,返回第一个匹配的字符元素的下标位置。

substr 方法:在string对象的字符串中,以 pos 位置开始的 len 个字符为子串,构造初始化一个string对象并返回。

string.h代码示例 6.1

cpp 复制代码
// string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace fym
{
    class string
    {
    public:
        // 默认成员函数:默认构造、拷贝构造、赋值重载、析构
        string(const char* str = "")  // 默认构造
        {
            _size = strlen(str);
            _capacity = _size;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }
        ~string()  // 析构
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }
        string(const string& s)  // 拷贝构造
        {
            // 初始化:开空间,拷贝现有对象s的字符串到新空间
            _str = new char[s._capacity + 1];
            strcpy(_str, s._str);
            _size = s._size;
            _capacity = s._capacity;
        }
        string& operator=(const string& s)  // 赋值重载
        {
            // 赋值重载类似拷贝构造,但不是初始化新对象,是赋值修改已有对象
            if (this != &s)  // 检查自己给自己赋值的情况
            {
                // 释放原空间
                delete[] _str;
                // 根据s开辟相同大小的空间,再拷贝字符串
                _str = new char[s._capacity + 1];
                strcpy(_str, s._str);
                _size = s._size;
                _capacity = s._capacity;
            }
            return *this;
        }

        // Capacity接口:字符存储与空间容量相关接口
        void reserve(size_t n);  // 预留空间

        // Modifiers接口:字符串修改相关接口
        void push_back(char c);  // 尾插字符
        string& operator+=(char c);  // +=字符

        // String operations接口:string对象操作接口
        const char* c_str() const  // 返回string对象字符串的指针
        {
            // 返回值类型为const char*,确保不可通过返回的指针修改字符串    
            return _str;
        }
        size_t find(char c, size_t pos = 0) const;  // 查找字符
        size_t find(const char* s, size_t pos = 0) const;  // 查找子串
        // 构造并返回子串的string对象
        string substr(size_t pos = 0, size_t len = npos) const;  
    private:
        char* _str;
        size_t _size;
        size_t _capacity;
        static const size_t npos;
    };
}


// string.cpp
#include"string.h"

namespace fym
{
    void string::reserve(size_t n)  // 预开空间
    {
        if (n > _capacity)
        {
            char* tmp = new char[n + 1];
            strcpy(tmp, _str);
            delete[] _str;
            _str = tmp;
            _capacity = n;
        }
    }
    void string::push_back(char c)  // 尾插字符
    {
        if (_size == _capacity)
        {
            reserve(_capacity == 0 ? 4 : 2 * _capacity);
        }
        _str[_size] = c;
        _size++;
        _str[_size] = '\0';
    }
    string& string::operator+=(char c)  // string对象+=字符
    {
        push_back(c);
        return *this;
    }
    size_t string::find(char c, size_t pos) const  // 查找字符
    {
        // 从pos位置开始查找字符
        for (int i = pos; i < _size; i++)
        {
            // 找到字符则返回其下标
            if (_str[i] == c) return i;
        }
        // 未找到则返回npos,表示无效下标
        return npos;
    }
    size_t string::find(const char* s, size_t pos) const  // 查找子串
    {
        assert(pos < _size);
        // strstr函数,在_str+pos指向的字符串中,匹配是否存在子串s,匹配上则返回其地址
        const char* ptr = strstr(_str + pos, s);
        // 未匹配上则ptr为空指针nullptr,返回npos
        if (ptr == nullptr) return npos;
        // 指针-指针,即地址-地址,ptr-_str为子串首元素的下标
        return ptr - _str;
    }
    // 构造并返回子串的string对象
    string string::substr(size_t pos, size_t len) const  
    {
        assert(pos < _size);
        // 选择pos位置开始的len个字符为子串,len可能过大,大于剩余字符长度则更新len
        if (len > _size - pos) len = _size - pos;
        // 定义string对象sub
        string sub;
        // 预开len个字符的空间
        sub.reserve(len);
        // 将pos位置开始的len个字符拷贝到新string对象sub中
        for (int i = 0; i < len; i++)
        {
            sub += _str[pos + i];  // +=字符,逐个字符尾插
        }
        // 传值返回,有临时对象的拷贝,自定义类型对象自动调用拷贝构造,用sub对象的值构造
        // 临时对象,对象销毁不影响值的传递。注意:此类情况必须显式实现拷贝构造,因为有
        // 资源申请。编译器默认生成的拷贝构造是浅拷贝,无法申请资源,必须自己实现深拷贝。
        return sub;
    }
}



// Test.cpp
#include<iostream>
#include<string>
using namespace std;
#include"string.h"

void Test()
{
	fym::string s("test.cpp.zip");
	size_t pos = s.find('.');
	fym::string suffix = s.substr(pos);
	cout << suffix.c_str() << endl;
}

int main()
{
	Test();

	return 0;
}

运行结果:

7. string类的非成员函数相关接口

关系运算符重载

string.h、string.cpp、Test.cpp代码示例7.1

cpp 复制代码
// string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace fym
{
    class string
    {
        // 非成员函数使用私有成员需要设为友元函数
        friend ostream& operator<<(ostream& out, const string& s);
        friend istream& operator>>(istream& in, string& s);
    public:
        // 默认成员函数:默认构造、拷贝构造、析构
        string(const char* str = "")  // 默认构造
        {
            _size = strlen(str);
            _capacity = _size; 
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }
        ~string()  // 析构
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }
        string(const string& s)  // 拷贝构造
        {
            _str = new char[s._capacity + 1];
            strcpy(_str, s._str);
            _size = s._size;
            _capacity = s._capacity;
        }
    private:
        char* _str;
        size_t _size;
        size_t _capacity;
        static const size_t npos;
    };
    // 重载为全局函数的string类接口
    bool operator<(const string& s1, const string& s2);
    bool operator<=(const string& s1, const string& s2);
    bool operator>(const string& s1, const string& s2);
    bool operator>=(const string& s1, const string& s2);
    bool operator==(const string& s1, const string& s2);
    bool operator!=(const string& s1, const string& s2);
}



// string.cpp
#include"string.h"

namespace fym
{
    // 重载为全局函数的string类接口
    bool operator<(const string& s1, const string& s2)
    {
        // 按ASCLL码比较字符串,逐个字符比较,s1<s2则strcmp返回<0的值
        return strcmp(s1.c_str(), s2.c_str()) < 0;
    }
    bool operator<=(const string& s1, const string& s2)
    {
        return s1 < s2 || s1 == s2;
    }
    bool operator>(const string& s1, const string& s2)
    {
        return !(s1 <= s2);
    }
    bool operator>=(const string& s1, const string& s2)
    {
        return !(s1 < s2);
    }
    bool operator==(const string& s1, const string& s2)
    {
        // s1==s2则strcmp返回0
        return strcmp(s1.c_str(), s2.c_str()) == 0;
    }
    bool operator!=(const string& s1, const string& s2)
    {
        return !(s1 == s2);
    }
}



// Test.cpp
#include<iostream>
#include<string>
using namespace std;
#include"string.h"

void Test()
{
	fym::string s1("hello world");
	fym::string s2("hello world");

	cout << (s1 < s2) << endl;  // 不小于,否,0
	cout << (s1 <= s2) << endl;  // 等于,是,1
	cout << (s1 > s2) << endl;  // 不大于,否,0
	cout << (s1 >= s2) << endl;  // 等于,是,1
	cout << (s1 == s2) << endl;  // 等于,是,1
	cout << (s1 != s2) << endl;  // 等于,否,0

	// 字符串也可与string对象比较,通过构造函数支持字符串到string对象的隐式类型转换
	// "hello world"通过隐式类型转换,构造为string类的临时对象
	cout << ("hello world" == s2) << endl;  
	cout << (s1 == "hello world") << endl;
}

int main()
{
	Test();

	return 0;
}

运行结果:

流插入<<流提取>>

string.h、string.cpp、Test.cpp代码示例7.2

cpp 复制代码
// string.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace fym
{
    class string
    {
    public:
        // 迭代器
        typedef char* iterator;
        typedef const char* const_iterator;
        iterator begin()  // 返回首元素的位置
        {
            return _str;
        }
        iterator end()  // 返回末尾元素的下一个位置
        {
            return _str + _size;
        }
        const_iterator begin() const  // begin的const版本重载
        {
            return _str;
        }
        const_iterator end() const  // end的const版本重载
        {
            return _str + _size;
        }

        // 默认成员函数:默认构造、析构
        string(const char* str = "")  // 默认构造
        {
            _size = strlen(str);
            _capacity = _size;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }
        ~string()  // 析构
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }

        // Capacity接口:字符存储与空间容量相关接口
        void clear()  // 清空字符串
        {
            _str[0] = '\0';
            _size = 0;
        }
        void reserve(size_t n);  // 预留空间

        // Modifiers接口:字符串修改相关接口
        void push_back(char c);  // 尾插字符
        void append(const char* str);  // 尾插字符串
        string& operator+=(char c);  // +=字符
        string& operator+=(const char* str);  // +=字符串
    private:
        // 成员变量的声明
        char* _str;
        size_t _size;
        size_t _capacity;
        static const size_t npos;
    };
    // 重载为全局函数的string类接口
    ostream& operator<<(ostream& out, const string& s);
    istream& operator>>(istream& in, string& s);
}



// string.cpp
#include"string.h"

namespace fym
{
    void string::reserve(size_t n)  // 预开空间
    {
        if (n > _capacity)
        {
            char* tmp = new char[n + 1];
            strcpy(tmp, _str);
            delete[] _str;
            _str = tmp;
            _capacity = n;
        }
    }
    void string::push_back(char c)  // 尾插字符
    {
        if (_size == _capacity)
        {
            reserve(_capacity == 0 ? 4 : 2 * _capacity);
        }
        _str[_size] = c;
        _size++;
        _str[_size] = '\0';
    }
    void string::append(const char* str)  // 尾插字符串
    {
        size_t len = strlen(str);
        if (_size + len > _capacity)
        {
            reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
        }
        strcpy(_str + _size, str);
        _size += len;
    }
    string& string::operator+=(char c)  // string对象+=字符
    {
        push_back(c);
        return *this;
    }
    // 重载为全局函数的string类接口
    ostream& operator<<(ostream& out, const string& s)
    {
        // 不需要访问string类的私有成员,函数可以不设置为友元
        // 例如这里通过迭代器访问string对象s,没有直接访问成员变量
        for (auto ch : s)
        {
            out << ch;
        }
        return out;
    }
    istream& operator>>(istream& in, string& s)
    {
        // 输入前清空s对象的原字符串
        s.clear();

        /*
          大量输入的情况下,向s不断插入会频繁扩容,但用reserve一次性申请空间容易造成空
          间浪费。可利用缓冲数组buff[N],当数组存满了,或者输入提前结束,再将数组中所有
          元素统一+=到s中,相当于将字符串拆分为多个N,有效减少扩容次数,且相比reserve
          没有空间浪费,数组是函数内部的局部对象,函数结束则释放buff空间,对象中不存储。
        */
        const int N = 256;  // 可灵活调整N,输入量一定,N越大扩容次数越少
        char buff[N];
        int i = 0;

        char ch;
        // in >> ch;  // 流提取>>空格或换行是分隔符ch读不到,循环无法结束
        // get读取字符,空格或换行也能读
        ch = in.get();
        while (ch != ' ' && ch != '\n')
        {
            // 循环输入读取字符,先逐个存入缓冲数组buff
            // 需注意,buff存储的是字符串,因此最后一位元素后应主动存储一个'\0'
            buff[i++] = ch;
            if (i == N - 1)
            {
                // i是下标,当i=N-1,为数组最后一位,设为'\0'
                buff[i] = '\0';
                // 缓冲数组buff满了,+=字符串统一尾插到s对象中
                s += buff;
                i = 0;  // 重置i,重新向buff存字符
            }
            ch = in.get();  // 读取下一个字符
        }
        // 输入提前结束但缓冲数组可能还有未读取的字符,if判断是否有剩余
        if (i > 0)
        {
            // 有剩余,则向i位置存入一个'\0'表示字符串
            buff[i] = '\0';
            // +=字符串,buff尾插到s对象
            s += buff;
        }
        return in;
    }
}



// Test.cpp
#include<iostream>
#include<string>
using namespace std;
#include"string.h"

void Test()
{
	fym::string s1("hello world");
	// 自定义类型对象使用运算符,自动调用其运算符重载
	// 流插入<<,输出s1
	cout << s1 << endl;
	// 流提取>>,向s1输入
	cin >> s1;  // 输入"12345"
	cout << s1 << endl;
}

int main()
{
	Test();

	return 0;
}

运算结果:

完整代码

string.h

cpp 复制代码
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace fym
{
    class string
    {
    public:
        // 迭代器
        // 反向迭代器暂不实现,仅实现正向迭代器及其const版本
        typedef char* iterator;
        typedef const char* const_iterator;
        iterator begin()  // 返回首元素的位置
        {
            return _str;
        }
        iterator end()  // 返回末尾元素的下一个位置
        {
            return _str + _size;
        }
        const_iterator begin() const  // begin的const版本重载
        {
            return _str;
        }
        const_iterator end() const  // end的const版本重载
        {
            return _str + _size;
        }

        // 默认成员函数:默认构造、拷贝构造、赋值重载、析构
        string(const char* str = "")  // 默认构造
        {
            _size = strlen(str);
            _capacity = _size;  // '\0'不计入_capacity
            _str = new char[_capacity + 1];  // 实际开空间_capacity+1
            strcpy(_str, str);
        }
        ~string()  // 析构
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }
        string(const string& s)  // 拷贝构造
        {
            //// 传统写法
            //// 初始化:开空间,拷贝现有对象s的字符串到新空间
            //_str = new char[s._capacity + 1];
            //strcpy(_str, s._str);
            //_size = s._size;
            //_capacity = s._capacity;

            // 现代写法
            // 通过默认构造创建一个临时的tmp对象,tmp与s完全一致,值全部交换给*this
            string tmp(s._str);
            swap(tmp);
        }
        string& operator=(const string& s)  // 赋值重载
        {
            if (this != &s)  // 检查自己给自己赋值的情况
            {
                //// 传统写法
                //// 释放原空间
                //delete[] _str;
                //// 根据s开辟相同大小的空间,再拷贝字符串
                //_str = new char[s._capacity + 1];
                //strcpy(_str, s._str);
                //_size = s._size;
                //_capacity = s._capacity;

                // 现代写法
                // 在赋值重载中实现比在拷贝构造中实现还额外多一个好处
                // 拷贝构造的*this是未初始化的string对象,但赋值的*this对象可能存在资源
                // 交换后*this中的资源都到了tmp中,tmp出了函数作用域即销毁,自动调用析构函数清理资源,省了传统写法中delete的功夫
                string tmp(s._str);
                swap(tmp);
            }
            return *this;
        }
        // 或不用传引用传参,用传值传参
        // string& operator=(string tmp)
        // {
        //     // 形参tmp即上面的临时对象,直接交换给*this即可
        //     swap(tmp);
        //     return *this;
        // }

        // Capacity接口:字符存储与空间容量相关接口
        size_t size() const  // 获取字符串长度/大小
        {
            return _size;
        }
        size_t capacity() const  // 获取空间容量
        {
            return _capacity;
        }
        void clear()  // 清空字符串
        {
            // 空字符串也有一个元素,即'\0',无需更改其余元素,首元素为'\0'即空字符串
            _str[0] = '\0';
            // 字符串长度/大小,置为0即可
            _size = 0;
        }
        bool empty() const  // 字符串判空
        {
            return _size == 0;
        }
        void reserve(size_t n);  // 预留空间
        // void resize(size_t n, char c = '\0');

        // Element access接口:数组元素访问相关接口
        char& operator[](size_t index)  // []运算符重载
        {
            assert(index < _size);
            return _str[index];
        }
        const char& operator[](size_t index) const  // []运算符重载的const版本
        {
            assert(index < _size);
            return _str[index];
        }

        // Modifiers接口:字符串修改相关接口
        void push_back(char c);  // 尾插字符
        void append(const char* str);  // 尾插字符串
        string& operator+=(char c);  // +=字符
        string& operator+=(const char* str);  // +=字符串
        string& insert(size_t pos, char c);  // 插入字符
        string& insert(size_t pos, const char* str);  // 插入字符串
        string& erase(size_t pos, size_t len = npos);  // 删除字符/字符串
        void swap(string& s)
        {
            /*
              string类有一个std::string::swap成员函数,专为string类对象服务;还有一
              个算法库algorithm中实现的swap函数模板std::swap,支持各种类型。C++11之
              前用swap模板生成的函数是通过创建第三个临时对象来实现交换的,交换char*、
              size_t等内置类型时swap模板生成的函数效果还行,但遇到深拷贝有不少消耗。
              C++11后在标准库std全局专门为string实例化实现了一个swap函数:
                std::swap(string),原型void swap(string &x, string &y);
              因此总共有3个swap,一个是函数模板,另两个是函数且都为string类服务,其中
              一个是string的成员函数,一个是函数模板关于string特别实现的实例化。
            */
            std::swap(_str, s._str);
            std::swap(_size, s._size);
            std::swap(_capacity, s._capacity);
        }

        // String operations接口:string对象操作接口
        const char* c_str() const  // 返回string对象字符串的指针
        {
            return _str;
        }
        size_t find(char c, size_t pos = 0) const;  // 查找字符
        size_t find(const char* s, size_t pos = 0) const;  // 查找子串
        string substr(size_t pos = 0, size_t len = npos) const;  // 构造并返回子串的string对象
    private:
        // 成员变量的声明
        char* _str;
        size_t _size;
        size_t _capacity;
        static const size_t npos;
        // 具有常性的整型静态成员变量可在类里定义static const size_t npos = -1; 
    };
    // 重载为全局函数的string类接口
    bool operator<(const string& s1, const string& s2);
    bool operator<=(const string& s1, const string& s2);
    bool operator>(const string& s1, const string& s2);
    bool operator>=(const string& s1, const string& s2);
    bool operator==(const string& s1, const string& s2);
    bool operator!=(const string& s1, const string& s2);
    ostream& operator<<(ostream& out, const string& s);
    istream& operator>>(istream& in, string& s);
}

string.cpp

cpp 复制代码
#include"string.h"

namespace fym2
{
    const size_t string::npos = -1;

    void string::reserve(size_t n)  // 预开空间
    {
        // 多数情况下,默认只扩容不缩容
        if (n > _capacity)
        {
            // 异地扩容:存储字符串默认多开1个空间,n+1
            char* tmp = new char[n + 1];
            // 拷贝数据到新空间,释放旧空间,_str指向新空间,更新空间容量
            strcpy(tmp, _str);
            delete[] _str;
            _str = tmp;
            _capacity = n;
        }
    }
    void string::push_back(char c)  // 尾插字符
    {
        // 判断扩容
        if (_size == _capacity)
        {
            // 检查空字符串的情况,_capacity为0开4个空间即可,否则直接二倍扩
            reserve(_capacity == 0 ? 4 : 2 * _capacity);
        }
        // 扩容完毕,插入字符,更新长度,字符串尾添加'\0'
        _str[_size] = c;
        _size++;
        _str[_size] = '\0';
    }
    void string::append(const char* str)  // 尾插字符串
    {
        // 判断扩容
        size_t len = strlen(str);
        if (_size + len > _capacity)
        {
            // 字符串str长度不确定,不能直接二倍扩,如果二倍扩空间足够就二倍扩,二倍扩空间还不够就按需扩容
            reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
        }
        // 插入字符串:将字符串str拷贝到'\0'及其后位置上,拷贝后str的'\0'成为插入后的字符串的'\0'
        strcpy(_str + _size, str);
        _size += len;
    }
    string& string::operator+=(char c)  // string对象+=字符
    {
        // 复用push_back尾插字符的逻辑,并返回*this
        push_back(c);
        return *this;
    }
    string& string::operator+=(const char* str)  // string对象+=字符串
    {
        // 复用append尾插字符串的逻辑,并返回*this
        append(str);
        return *this;
    }
    string& string::insert(size_t pos, char c)  // 插入字符,只实现不带次数n
    {
        // insert任意插入字符,可头插也可尾插,有效范围pos>=0且pos<=_size
        assert(pos <= _size);
        if (_size == _capacity)
        {
            reserve(_capacity == 0 ? 4 : 2 * _capacity);
        }
        // 中间插入:pos位置及其后位置的字符需要后移,空出1个字符的位置
        size_t end = _size + 1;  // end指向'\0'位置
        while (end > pos)  // 最后一次循环end=pos+1
        {
            // 直到把pos位置的字符也后移完为止,即end=pos+1,end-1=pos
            _str[end] = _str[end - 1];
            end--;
        }

        // 如下写法有bug,循环的结束条件不能是end=pos-1
        // size_t end = _size;
        // while (end >= pos)
        // {
        //     _str[end + 1] = _str[end];
        //     end--;
        // }
        /*
          需注意:end是无符号整型size_t,最小等于0,不能存负数。
          while循环需要end=pos-1才能结束,而insert是可以头插的,如果pos=0,则永远无
          法结束,pos=-1就相当于变成npos了。
          也不能将end改为int,因为形参pos是size_t类型。当操作符两端操作数类型不匹配时
          会进行强转,进行类型提升或类型截断,像比较或加减运算等,会将类型值范围小的提
          升成范围大的类型,32位下,int和size_t都是4个字节,但一个有符号,一个无符号,
          无符号可存储的值更大,因此这里int会被类型提升为size_t。除非修改形参pos的类型
          ,但牵一发动全身,而且pos是下标位置,本身就不可为负数,不建议改为int。
          或者下面这种方式,显式类型转换也是可以的:
            while (end >= (int)pos)
          也可以不用下标+循环,使用指针+循环或memmove,但需要指针_str与地址:
            memmove(_str+pos+1, _str+pos, _size-pos+1);
            或char* end=_str+_size;while (end >= _str+pos)
        */

        _str[pos] = c;
        _size++;
        return *this;
    }
    string& string::insert(size_t pos, const char* str)  // 插入字符串
    {
        assert(pos <= _size);
        size_t len = strlen(str);
        if (_size + len > _capacity)
        {
            // 判断是按需扩容还是二倍扩容
            reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
        }
        // 后移空出len个字符的位置
        // 注意循环结束条件,不能让end=-1,pos和len都可以为0
        size_t end = _size + len;
        while (end > pos + len - 1)
        {
            // 将pos位置及其后位置所有字符后移len个位置
            // 最后一次循环,end=pos+len,即_str[pos+len]=_str[pos]
            _str[end] = _str[end - len];
            end--;
        }

        for (int i = 0; i < len; i++)
        {
            _str[pos + i] = str[i];
        }
        _size += len;
        return *this;
    }
    // 注意对比erase的声明与定义的细节:缺省参数只在声明时给,定义时无需给,也不能给
    string& string::erase(size_t pos, size_t len)  // 删除字符/字符串
    {
        // 从任意的pos位置开始删除字符,有效范围:头删pos=0,尾删pos=_size-1
        assert(pos < _size);
        if (len >= _size - pos)  // 情况一:需要删除到字符串结束,无剩余字符
        {
            // _size-pos是pos及其后所有字符个数,即可删除字符数
            _str[pos] = '\0';
            _size = pos;
        }
        else  // 情况二:删除到中间位置停下,其后有剩余字符
        {
            /*
              删除[pos, pos+len-1]范围的len个字符,将pos+len-1后所有字符移到前面即
              可即使其后字符个数不足len个,但包括'\0',相当于'\0'后所有字符都删除了
            */
            for (int i = pos + len; i <= _size; i++)
            {
                _str[i - len] = _str[i];
            }
            _size -= len;
        }
        return *this;
    }
    size_t string::find(char c, size_t pos) const  // 查找字符
    {
        // 从pos位置开始查找字符
        for (int i = pos; i < _size; i++)
        {
            // 找到字符则返回其下标
            if (_str[i] == c) return i;
        }
        // 未找到则返回npos,表示无效下标
        return npos;
    }
    size_t string::find(const char* s, size_t pos) const  // 查找子串
    {
        assert(pos < _size);
        // strstr函数,在_str+pos指向的字符串中,匹配是否存在子串s,匹配上则返回其地址
        const char* ptr = strstr(_str + pos, s);
        // 未匹配上则ptr为空指针nullptr,返回npos
        if (ptr == nullptr) return npos;
        // 指针-指针,即地址-地址,ptr-_str为子串首元素的下标
        return ptr - _str;
    }
    string string::substr(size_t pos, size_t len) const  // 构造并返回子串的string对象
    {
        assert(pos < _size);
        // 选择pos位置开始的len个字符为子串,len可能过大,大于剩余字符长度则更新len
        if (len > _size - pos) len = _size - pos;
        // 定义string对象sub
        string sub;
        // 预开len个字符的空间
        sub.reserve(len);
        // 将pos位置开始的len个字符拷贝到新string对象sub中
        for (int i = 0; i < len; i++)
        {
            sub += _str[pos + i];  // +=字符,逐个字符尾插
        }
        // 传值返回,调用拷贝构造,用sub对象的值构造临时对象,对象销毁不影响
        // 注意:此类情况必须显式实现拷贝构造,因为有资源申请。
        // 编译器默认生成的拷贝构造是浅拷贝,需要自己实现深拷贝
        return sub;
    }
    // 重载为全局函数的string类接口
    bool operator<(const string& s1, const string& s2)
    {
        // 按ASCLL码比较字符串,逐个字符比较,s1<s2则strcmp返回<0的值
        return strcmp(s1.c_str(), s2.c_str()) < 0;
    }
    bool operator<=(const string& s1, const string& s2)
    {
        return s1 < s2 || s1 == s2;
    }
    bool operator>(const string& s1, const string& s2)
    {
        return !(s1 <= s2);
    }
    bool operator>=(const string& s1, const string& s2)
    {
        return !(s1 < s2);
    }
    bool operator==(const string& s1, const string& s2)
    {
        // s1==s2则strcmp返回0
        return strcmp(s1.c_str(), s2.c_str()) == 0;
    }
    bool operator!=(const string& s1, const string& s2)
    {
        return !(s1 == s2);
    }
    ostream& operator<<(ostream& out, const string& s)
    {
        // 不需要访问string类的私有成员,函数可以不设置为友元
        // 例如这里通过迭代器访问string对象s,没有直接访问成员变量
        for (auto ch : s)
        {
            out << ch;
        }
        return out;
    }
    istream& operator>>(istream& in, string& s)
    {
        // 输入前清空s对象的原字符串
        s.clear();

        // 大量输入的情况下,向s不断插入字符会频繁扩容,但用reserve一次性申请空间则造成空间浪费
        // 可利用缓冲数组buff[N],当数组输入满N个字符,或者输入提前结束,则将之前输入的所有字符
        // 一次性+=到s中,能有效减少扩容次数,而且相比reserve没有空间浪费,数组是函数内部的局部
        // 对象,函数结束则释放buff空间,对象中不存储。
        const int N = 256;  // 可灵活调整N,输入量一定,N越大扩容次数越少
        char buff[N];
        int i = 0;

        char ch;
        // in >> ch;
        // get读取字符,空格或换行也能读,若用流提取>>空格或换行是分隔符ch读不到,循环无法结束
        ch = in.get();
        while (ch != ' ' && ch != '\n')
        {
            // 循环输入读取字符,先逐个存入缓冲数组buff
            // 需注意,buff存储的是字符串,因此最后一位元素后应主动存储一个'\0'
            buff[i++] = ch;
            if (i == N - 1)
            {
                // i是下标,当i=N-1,为数组最后一位,设为'\0'
                buff[i] = '\0';
                // 缓冲数组buff满了,+=字符串统一尾插到s对象中
                s += buff;
                i = 0;  // 重置i,重新向buff存字符
            }
            ch = in.get();  // 读取下一个字符
        }
        // 输入提前结束但缓冲数组可能还有未读取的字符,if判断是否有剩余
        if (i > 0)
        {
            // 有剩余,则向i位置存入一个'\0'表示字符串
            buff[i] = '\0';
            // +=字符串,buff尾插到s对象
            s += buff;
        }
        return in;
    }
}
相关推荐
垆边人似月.1 小时前
移动长度 k 子串使字符串变成回文
数据结构·算法
geovindu1 小时前
rust: Iterator Pattern
开发语言·后端·设计模式·rust·行为模式·迭代器模
数据库小学妹1 小时前
MySQL内存占用过高怎么排查?分层定位与OOM根因
数据库·经验分享·mysql·内存·oom
Lxx_Zw1 小时前
先把轮子转稳,才不会烧心:编码器速度环 PID 怎么接回 PWM
c语言·stm32·单片机·嵌入式硬件
SDWAN_Cheap1 小时前
深度解析什么是CDN加速:从原理到实践的技术拆解
开发语言·cdn加速
鬼手点金1 小时前
模型即 Agent:当模型本身成为产品
开发语言·前端·人工智能·python·机器学习
lisw051 小时前
计算生物学学习笔记:NHGRI基因组数据科学分析、可视化和信息学实验室空间(AnVIL)
笔记·计算生物学
头像很酷1 小时前
嵌入式虚拟化-xvisor-物理内存大管家
c语言·hypervisor·xvisor·嵌入式虚拟化
染指11102 小时前
147.Agent-LangGraph归并函数
开发语言·人工智能·python·langchain·agents