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。
- string类对象存储字符串,其数据结构应为顺序表,即数组,且为动态数组,故需要一个 char* 类型指针 _str,指向动态申请的空间的地址,扩容时更新空间地址即可。
- _size 记录当前存储的有效字符个数,即字符串长度 (不包括'\0'),单位元素个数。
- _capacity 记录数组的最大容量,方便知道动态开辟的空间大小,单位元素个数。
构造初始化需求:
- string类有资源申请,所以必须显式实现默认构造,可选择无参构造 或全缺省构造 。这里选择实现全缺省构造。实现全缺省构造同时也是实现了带参构造。
- 默认构造就是无需传实参的构造。不传参的情况下,_str 的初始化,不可以是空指针 nullptr,应是空字符串,即字符数组存储的有效字符数为0。但空字符串也要跟一个 '\0',可以不存储有效字符,但一定要存储 '\0',故初始化 _str 至少要动态申请1 byte的空间存储 '\0'。
- '\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;
}
}