源代码(含注释与细节讲解)
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <algorithm>
#include <utility>
#include <assert.h>
using namespace std;
//using namespace mystring;
namespace mystring {
class string {
private:
char* _a;
size_t _size;
size_t _capacity;
//静态常量
public:
const static size_t npos;//静态常量类型npos
//一、构造、析构、赋值重载
public:
无参构造
//string()
//{
// _a = nullptr;
// _size = 0;
// _capacity = 0;
//}
指针拷贝构造
//string(const char* s)
// //:_a(s)//1.不能直接将常量字符串s赋值给_a,因为类型不匹配(_a得到的是常量字符串s的地址)
// //应当自己申请空间,再将常量字符串拷贝过去
// :_size(strlen(s))
//{
// _capacity = _size;//2.细节优化,_capacity不使用初始化列表,可以少调用一次strlen函数
// _a = new char[_size + 1];//3.开空间需要多开一个空间存放\0,但是_size和_capacity不需要
// strcpy(_a, s); //为什么要多开一个空间存放\0?
//} // 因为C++需要兼容C语言,使用c_str返回常量字符串时,如果末尾没有标识符\0,则打印该字符串后面就会出现乱码
//因为_size是看存了多少个有效字符,_capacity是看能存多少个有效字符
//即使_capacity为0,也要开一个空间,存放\0
//再优化:无参构造与指针拷贝构造合并
string(const char* s = "")//4.设置默认参数为"",常量字符串结尾默认有一个\0
:_size(strlen(s)) //不可以使用'\0',因为strlen()只能计算字符串
{ //也不建议使用"\0",因为常量字符串结尾默认有\0,相当于有两个\0,
_capacity = _size;
_a = new char[_size + 1];
strcpy(_a, s);
//_a[_size] = '\0';//5.不需要手动在数据末尾或者多开的位置存放\0,因为strcpy会拷贝空字符串中的\0到多开的位置
}
//6.还有一个无参构造与指针拷贝构造合并的原因是,string类中有一个函数是c_str
//该函数是返回C语言的字符串
//如果无参构造中_a的值设置为nullptr,那么c_str返回的就是空指针
//在打印输出时,不能打印空指针,所以报错!
//如果不想合并,无参构造函数应该如下这样
//无参构造
//string()
// :_a(new char[1])
// ,_size(0)
// ,_capacity(0)
//{
// _a[0] = '\0';
//}
//拷贝构造
//string(const string& str)
// :_a(new char[str._capacity + 1])
// , _size(str._size)
// , _capacity(str._capacity)
//{
// strcpy(_a, str._a);
// //_a[_size] = '\0';//7.不需要手动在数据末尾或者多开的位置存放\0,因为strcpy会拷贝空字符串中的\0到多开的位置
//}
//拷贝构造函数的现代写法
string(const string& str)
{
string tmp(str._a);
swap(tmp);
}
//子串构造
string(const string& str, size_t pos, size_t len = npos)
{
size_t length = str._size - pos;//从pos位置开始剩余的数据长度
if (len > length)
{
_a = new char[length + 1];
_size = length;
strcpy(_a, str._a + pos);//8.从pos位置拷贝数据,len长度大于数据剩余长度,将剩余数据全部拷贝过来,空间刚好够,最后多开的空间存放\0;
}
else
{
_a = new char[len + 1];
_size = len;
for (size_t i = pos, j = 0; i < pos + len; ++i, ++j) _a[j] = str._a[i];//9.从pos位置拷贝数据,不能用strcpy函数
_a[_size] = '\0';//10.数据末尾(即最后多开的空间)需要手动赋值为\0 //因为strcpy函数要求目标地址空间大小必须大于源地址空间大小
}
}
//指针限定拷贝构造函数
string(const char* s, size_t n)
:_a(new char[n + 1])
, _size(n)
, _capacity(n)
{
//strcpy(_a, s);//11.不能使用strcpy函数拷贝,因为strcpy函数要求目标地址空间大小必须大于源地址空间大小
for (size_t i = 0; i < n; ++i) _a[i] = s[i];
_a[_size] = '\0';//12.数据末尾(即最后多开的空间)需要手动赋值为\0
}
//填充构造函数
string(size_t n, char c)
:_a(new char[n + 1])
, _size(n)
, _capacity(n)
{
for (size_t i = 0; i < n; ++i) _a[i] = c;
_a[_size] = '\0';//13.数据末尾(即最后多开的空间)需要手动赋值为\0
}
//析构函数
~string()
{
delete[] _a;
_a = nullptr;
_size = 0;
_capacity = 0;
}
//赋值运算符重载operator=
//string& operator=(const string& str)
//{
// char* tmp = new char[str._capacity + 1];//14.赋值运算符重载不同于拷贝构造,原对象中存在数据,需要将原对象中的数据销毁,重新开辟新空间拷贝
// strcpy(tmp, str._a);
// delete[] _a;
// _a = tmp;
// _size = str._size;
// _capacity = str._capacity;
//}
//赋值运算符重载的现代写法
string& operator=(const string& str)
{
string tmp(str);
swap(tmp);
return *this;
}
//二、容量操作
public:
//求大小
size_t size() const//1.设置为常函数,普通对象和常对象都可以调用
{
return _size;
}
//求容量
size_t capacity() const
{
return _capacity;
}
//扩容
void reserve(size_t n = 0)
{
if (n > _capacity)//2.需要加上判断,新空间的大小比旧空间大才能扩容,防止变为缩容数据丢失
{
char* tmp = new char[n + 1];//3.开新空间,要多开一个空间存放\0,但\0不是有效数据
strcpy(tmp, _a);//拷贝数据
delete[] _a;//释放旧空间
_a = tmp;
_capacity = n;//修改容量
}
}
//改变容器
void resize(size_t n, char c = '\0')
{
if (n <= _size)//删除多余数据
{
_a[n] = '\0';
_size = n;
}
else
{
reserve(n);//4.不管需不需要扩容,都reserve一下,在reserve函数中会自行判断需不需要扩容
for (size_t i = _size; i < n; ++i)//5.此处需要画图理解
_a[i] = c;
_a[n] = '\0';
_size = n;
}
}
//清除数据
void clear()
{
_size = 0;
_a[_size] = '\0';
}
//三、元素访问
public:
//1.对于[]访问使用较多的函数,会被设置为内联函数,这样键能减少函数栈帧的建立
//2.为什么将常对象[]访问与普通对象[]访问重载为两个函数呢?
//直接将函数设置为常函数虽然可以被常对象调用,但是不能防止数据被修改
//所以需要重载为两个函数,其中常对象的[]访问函数的返回值要加const修饰,由于是返回引用,要防止被修改
//普通对象[]访问
char& operator[](size_t pos)
{
//assert(pos >= 0 && pos < _size);
//3.无需判断pos是否>0,因为pos是无符号类型size_t
assert(pos < _size);
return _a[pos];
}
//常对象[]访问
const char& operator[](size_t pos) const
{
//assert(pos >= 0 && pos < _size);
assert(pos < _size);
return _a[pos];
}
//四、迭代器
public:
typedef char* iterator;
typedef const char* const_iterator;
//begin
iterator begin()
{
return _a;
}
//常对象begin
const_iterator begin() const
{
return _a;
}
//end
iterator end()
{
return _a + _size;
}
//常对象end
const_iterator end() const
{
return _a + _size;
}
//反向迭代器
//反向迭代器涉及适配器模式,暂时略过
//五、修改操作
public:
//尾插
void push_back(char c)
{
//2倍扩容
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);//1.当容量为0时,2倍扩容后容量依然为0,所以要判断初始容量是否为0
}
_a[_size++] = c;//插入
_a[_size] = '\0';//2.数据末尾需要加上\0,因为原先数据末尾的\0会被新数据覆盖
}
//尾插字符串
string& append(const char* s)
{
//数据多少,扩容多少
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);//扩容
_capacity = _size + len;//修改容量
}
strcpy(_a + _size, s);//3.使用strcpy函数,将B位置的数据拷贝到A位置(从B位置开始搜索直到\0,包括\0也拷贝过去)
_size = _size + len;//修改大小
return *this;
}
//operator+=字符
string& operator+=(char c)
{
this->push_back(c);
return *this;
}
//operator+=字符串
string& operator+=(const char* s)
{
this->append(s);
return *this;
}
//insert单个字符
string& insert(size_t pos, char c)
{
if (_size == _capacity)
reserve(_capacity == 0 ? 4 : 2 * _capacity);//扩容
assert(pos <= _size);//4.检查插入位置的合法性
5.挪动数据
//int end = _size;//end指向\0位置,因为\0也要挪动
//while (end >= (int)pos)//6.为什么设置end的类型为int而不是size_t?
//{ //因为当pos为0时,--end永远不会小于0,因此程序进入死循环。
// _a[end + 1] = _a[end];//所以pos类型设置为int,但int类型的end与size_t类型的pos比较时,会发生算数转换,int会被转换为size_t,程序还会死循环
// --end; //因此还要将pos强制类型转换为int
//}
//_a[pos] = c;
//++_size;
//6.还有一种方法,也可以解决pos为0时的死循环问题,就是设置end初始为原数据最后位置的后一位
size_t end = _size + 1;
while (end > pos)
{
_a[end] = _a[end - 1];
--end;
}
_a[pos] = c;
++_size;
return *this;
}
//insert字符串
string& insert(size_t pos, const char* s)//7.insert字符串和insert字符原理相同,只是需要向后多移动几个数据
{
assert(pos <= _size);//检查插入位置合法性
size_t length = strlen(s);
if (_size + length > _capacity)//扩容
reserve(_size + length);
//挪动数据
size_t end = _size + length;
while (end > pos + length - 1)//8.此处的数据挪动要画图才能容易理解
{
_a[end] = _a[end - length];
--end;
}
strncpy(_a + pos, s, length);//9.拷贝可以使用strncpy函数,与strcpy函数的区别就是不会拷贝\0字符
//for (size_t i = 0; i < length; ++i)
// _a[i + pos] = s[i];
_size += length;
return *this;
}
//erase
string& erase(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);//检查删除位置合法性
//if (pos + len >= _size)//删除pos位置后所有数据
if (len >= _size - pos)//10.此处改为len >= _size - pos是为了防止当len为npos时或len接近npos时,pos + len会溢出
{
_a[pos] = '\0';
_size = pos;
}
else//删除pos位置后len个数据
{
//挪动数据
//for (size_t i = pos; i <= _size - len; ++i)
//{
// _a[i] = _a[i + len];
//}
//11.挪动数据可以直接利用strcpy函数,只要目标地址空间比源地址空间大即可使用strcpy函数
strcpy(_a + pos, _a + pos + len);
_size -= len;
}
return *this;
}
//swap
void swap(string& str)//12.常规操作是创建临时对象tmp,再交换两个对象,但是代价较高,需要调用一次拷贝构造+两次赋值运算符重载+一次析构函数
{ //(这也是算法库中的swap使用的方法,因此使用时尽量使用string类中swap函数,而不是算法库的函数)
//string tmp(*this);//可以使用算法库中的swap,直接交换两个对象的成员
//*this = str;
//str = tmp;
std::swap(_a, str._a);//13.需要使用命名空间std,因为编译器查找会根据就近原则
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
//14.关于swap函数,类内实现了swap函数,全局又实现了一个swap函数,算法库中刚还实现了一个模板swap,为什么要实现这么多swap函数呢?
//全局中的swap函数底层是调用类内的swap函数实现的,这是为了防止使用swap函数时编译器取调用算法库中的swap函数(算法库中的swap函数是模板函数效率低)
//类外实现的swap函数和算法库中的swap函数都是全局函数,但是算法库中的swap函数时模板函数,编译器会优先使用非模板函数,底层就睡调用类内swap函数
//效率更高
//六、字符串操作
public:
//c_str
const char* c_str() const
{
return _a;
}
//find查找某个字符
size_t find(char c, size_t pos = 0) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; ++i)
if (_a[i] == c) return i;
return npos;
}
//find查找某个字符串
size_t find(const char* s, size_t pos = 0) const
{
//字符串匹配问题,可以使用KMP算法,但是该算法不实用,效率并没有很高
//实际上可以直接使用暴力算法取匹配
//此处使用C语言库中的strstr函数
//strstr函数匹配成功了返回子串的初始位置指针,匹配失败返回空指针
assert(pos < _size);
char* p = strstr(_a + pos, s);
if (p != nullptr) return p - _a;
else return npos;
}
//substr取子串
string substr(size_t pos = 0, size_t len = npos) const
{
string s;
if (len == npos || len >= _size - pos)
{
for (size_t i = pos; i < _size; ++i)
s += _a[i];
}
else
{
for (size_t i = pos; i < pos + len; ++i)
s += _a[i];
}
return s;
}
};
//关于数据末尾放不放\0的问题?
//数据末尾当然要存放\0,这样在使用c_str返回常量字符串时才是一个正确的字符串
const size_t string::npos = -1;//静态成员变量,类内声明,类外定义
//1.类外swap(通过类内swap函数实现,防止调用算法库swap,提高效率)
void swap(string& x, string& y)
{
x.swap(y);
}
//关系运算符重载
//2.为什么关系运算符重载要重载为全局函数?
//以==运算符为例,如果写成成员函数则不支持"123"==str,但支持str=="123"
//因为调用成员函数的必须是对象,即使单参数的构造函数支持隐式类型转换,但是也不能先隐式类型转换转换为对象再调用函数
//==重载
bool operator==(const string& lhs, const string& rhs)
{
return strcmp(lhs.c_str(), rhs.c_str());//3.此处可以直接使用接口c_str,无需访问私有成员
}
//!=重载
bool operator!= (const string& lhs, const string& rhs)
{
return !(lhs == rhs);
}
//<重载
bool operator<(const string& lhs, const string& rhs)
{
int ret = strcmp(lhs.c_str(), rhs.c_str());
return ret < 0;
}
//<=重载
bool operator<=(const string& lhs, const string& rhs)
{
return (lhs < rhs) || (lhs == rhs);
}
//>重载
bool operator>(const string& lhs, const string& rhs)
{
int ret = strcmp(lhs.c_str(), rhs.c_str());
return ret > 0;
}
//>=重载
bool operator>=(const string& lhs, const string& rhs)
{
return (lhs > rhs) || (lhs == rhs);
}
//<<重载
ostream& operator<<(ostream& out, const string& str)
{
out << str.c_str();
return out;
}
//>>重载
//istream& operator>>(istream& in, string& str)
//{
// str.clear();//4.输入是覆盖操作,原先有字符的对象需要删除
// again:
// char ch;
// ch = in.get();//5.cin和scanf规定读不到空格和换行符(因为scanf和cin默认空格符是两个对象输入的分割,换行符是输入结束标志)
// if(ch != ' ' && ch != '\n')//所以改使用get,可以读取空格和换行
// { //get读取的字符是先存储到缓冲区,再一个一个去读取字符,即使在缓冲区输入如12345 12345
// str += ch; //get只会读取到空格之前的字符
// goto again; //剩下的字符仍保留在缓冲区,留给下一次字符输入时get读取
// }
// return in;
//}
//6.>>重载的改进
//在>>重载中,str+=ch,每一次+=如果空间不够需要扩容,频繁扩容效率很低
//可以使用一个buff字符数组临时存储需要+=的字符,最后一次性全部+=
istream& operator>>(istream& in, string& str)
{
str.clear();
char buff[128];
int i = 0;
again:
char ch = in.get();
if (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127)
{
buff[127] = '\0';
i = 0;
str += buff;
}
goto again;
}
if (i > 0)
{
buff[i] = '\0';
str += buff;
}
return in;
}
//getline
istream& getline(istream& in, string& str)
{
str.clear();
again:
char ch = in.get();
if (ch != '\n')
{
str += ch;
goto again;
}
return in;
}
void test()
{
string s1;
string s2("74829174589072390");
s1 = s2;
cout << s1 << endl;
//string s1;
getline(cin, s1);
//cin >> s1;
//cout << s1 << endl;
//string s1("123");
//string s2("456");
//cin >> s1 >> s2;
//cout << s1 << " " << s2 << endl;
//string s1("12345");
//cin >> s1;
//cout << s1.c_str() << endl;
//cout << s1 << endl;
//string s1("12345");
//string s2 = s1.substr(1, 6);
//size_t pos = s1.find("34", 0);
//s1[pos] = 'x';
//cout << s2.c_str() << endl;
//string s1("12345");
//s1.insert(3, "xxx");
//cout << s1.c_str() << endl;
//string s2("ccc");
//s1.swap(s2);
//cout << s1.c_str() << endl;
//cout << s2.c_str() << endl;
//string s1("123456789");
//s1.resize(5);
//cout << s1.c_str() << endl;
//s1.resize(20, 'c');
//cout << s1.c_str() << endl;
//s1.insert(3, 'x');
//cout << s1.c_str() << endl;
//string s2("12345");
//s2.insert(0, "xxxXXXXXXXXX");
//cout << s2.c_str() << endl;
//string str1("12345");
//str1 += '9';
//cout << str1.c_str() << endl;
//str1 += "xxxxxxxx";
//cout << str1.c_str() << endl;
//string str1;
//const char* s1 = str1.c_str();
//cout << s1 << endl;
//string str2("12345");
//const char* s2 = str2.c_str();
//cout << s2 << endl;
//string str3(str2);
//const char* s3 = str3.c_str();
//cout << s3 << endl;
//string str4(s3, 2, 2);
//const char* s4 = str4.c_str();
//cout << s4 << endl;
//char p[] = "123456789";
//string str5(p, 5);
//const char* s5 = str5.c_str();
//cout << s5 << endl;
//string str6(10, 'c');
//const char* s6 = str6.c_str();
//cout << s6 << endl;
//str6.push_back('c');
//const char* s7 = str6.c_str();
//cout << s7 << endl;
//str6.append("12345");
//const char* s8 = str6.c_str();
//cout << s8 << endl;
/*string str7;
str7.reserve(5);
const char* s7 = str7.c_str();
cout << s7 << endl;*/
/*string s1("12345");
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : s1)
cout << e << " ";*/
//const string s2("56789");
//string::const_iterator it = s2.begin();
//while (it != s2.end())
//{
// cout << *it << " ";
// ++it;
//}
//cout << endl;
//string s1("12345");
//s1.push_back('6');
//s1.append("123");
//for (auto e : s1)
//{
// cout << e << " ";
//}
}
}
int main()
{
mystring::test();
//未初始化的位置是乱码
//char* p = new char[5];
//for (int i = 0; i < 3; ++i) p[i] = 'a';
//cout << p << endl;
//printf("%s\n", p);
//string s1("12345");
//cin >> s1;
//cout << s1 << endl;
return 0;
}