C++:String
2026/7/12 20:53:12 网站建设 项目流程

为什么学习string类?

C语言中的字符串

C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列 的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问

标准库中的string类

string类(了解)

https://cplusplus.com/reference/string/string/?kw=string

在使用string类时,必须包含#include头文件以及using namespace std;

string类的常用接口说明

string类对象的常见构造

函数名称功能说明
string()(重要)构造空的string类对象,即空字符串
string(const char* s)(重要)用c-string来构造string类对象
string(size_t n, char c)string类对象中包含n个字符c
string(const string&s)(重要)拷贝构造函数

范例:

void String() { string str; //构造空的对象 string str2("Hello World"); //用c格式的字符串构造string类对象 string str3(str2); //拷贝构造 string str4(str2, 1, 6); cout << str << endl; cout << str2 << endl; cout << str3 << endl; cout << str4 << endl; } int main() { String(); return 0; }

string类对象的容量操作

函数名称功能说明
size(重要)返回字符串有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
empty(重要)检测字符串释放为空串,是返回true,否则返回false
clear(重要)清空有效字符
reverve(重要)为字符串预留空间**
resize(重要)将有效字符的个数该成n个,多出的空间用字符c填充

size()和length()的底层逻辑几乎一样,引入size()的原因是要和其他的接口保持一致,正常情况下都是用size()

reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,resverse不会改变容量大小

string类对象的访问及遍历操作

函数名称功能说明
operator[](重要)返回pos位置的字符,const string类对象调用
begin+endbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
rbegin+rendbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
范围forC++11支持更简洁的范围for的新遍历方式

补充:

iterator和const_iterator

这俩都是迭代器,一个可以修改指向内容的数据一个无法对指向的内容进行修改,通常搭配begin和end使用

代码示例:

void String() { string s1("123456"); const string s2("Hello World"); string::iterator str = s1.begin(); while (str != s1.end()) { (*str)--; ++str; } cout << s1 << endl; string::const_iterator str2 = s2.begin(); while (str2 != s2.end()) { //(*str2)--; //不能修改 ++str2; } cout << s2 << endl; }

如果对const_iterator修改则会报错

运行结果:

除了上述两个外还有reverse_iterator和const_reverse_iterator,这俩和iterator和const_iterator相似,不过这俩是倒过来读取的

代码范例:

void String() { string s1("123456"); const string s2("Hello World"); string::reverse_iterator str3 = s1.rbegin(); while (str3 != s1.rend()) { cout << *str3 << " "; ++str3; } cout << endl; string::const_reverse_iterator str4 = s2.rbegin(); while (str4 != s2.rend()) { cout << *str4 << " "; ++str4; } cout << endl; }

运行结果:

string类对象的修改操作

函数名称功能说明
push_back在字符串后尾插字符c
appned在字符串后追加一个字符串
operator+=(重要)在字符串后追加字符串str
c_str(重要)返回C格式字符串
find+npos从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
rfind从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
substr在str中从pos位置开始,截取n个字符,然后将其返回

注意:

在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串

对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好

auto和范围for

auto关键字

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得

在C++中可以使用auto来进行类型推导

void String2() { auto x = 20; auto z = 23.4; cout << x << endl; cout << z << endl; } int main() { String2(); return 0; }

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加

当auto要声明引用类型时要加上引用操作符(“&”)

void String2() { int& y = x; auto& i = y; i++; }

我们来进行调试:

此时x为20,我们接着往下走

y也变成了20,继续往下

i也变成了20,接着往下走

再往下走后就变成了21

那么如果auto后不加引用会发生什么事?

此时的i是y的拷贝,并非引用

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际 只对第一个类型进行推导,然后用推导出来的类型定义其他变量

auto不能作为函数的参数,可以做返回值,但是建议谨慎使用

auto不能直接用来声明数组

范围for

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围 内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束

for (auto ch : s1) { cout << ch << " "; } cout << endl;

范围for可以作用到数组和容器对象上进行遍历

int array[] = { 1,2,3,4,5 }; for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++) { cout << array[i] << endl; } for (auto a : array) { cout << a << " "; } cout << endl;

范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到

string模拟实现

初始化

这里我们写上const的和非const版本

namespace MyString { String::String() :_str(new char[1]{'\0'}) ,_size(0) ,_capacity(0) {} String::String(const char* str) :_str(new char[strlen(str)+1]) ,_size(strlen(str)) ,_capacity(strlen(str)) { memcpy(_str, str); } }

取得字符串

namespace MyString { const char* String::c_str() const { return _str; } }

我们来运行看看

运行结果:

取得字符串中的长度

namespace MyString { size_t String:: size() { return _size; } }

这里我们要将字符串中的每个字符都往后+1

运行结果:

下标操作符重载

这里也写上const和非const的版本,这里需要使用迭代器

namespace MyString { char& String::operator[](size_t i) { assert(i < _size); return _str[i]; } const char& String::operator[](size_t i)const { assert(i < _size); return _str[i]; } }

这里补充一些东西,这里遍历时无法使用迭代器,因为自定义类型并没有调用库中的东西

所以这我们begin和end要自己实现

这要先将char*进行重命名

typedef char* iterator;

我们知道begin是要找到字符串中的第一个字符,end则是找到最后一个字符

namespace MyString { String::iterator String::begin() { return _str; } String::iterator String::end() { return _str + _size; } }

运行结果:

尾插

在写尾插之前我们先模拟实现一个函数“reserve”,“resever”是用于分配内存空间的一个函数,它可以避免内存多次进行分配,有效提升元素插入时的运行效率

现在开始写代码:

void String::reserve(size_t n) { if (n > _capacity) { char* str = new char[n + 1]; memcpy(str, _str, _size + 1); delete[] _str; _str = str; _capacity = n; } }

接下来继续写尾插代码

void String::push_back(char str) { if (_size >= _capacity) { size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity; reserve(newcapacity); } _str[_size] = str; ++str; _str[_size] = '\0'; }

这里写的和之前的不同,这里多加了一个‘\0’

运行结果:

追加字符串

append是用于追加字符串的函数,可以追加任何形式的字符而不要进行重新分配,接下来进行模拟实现

void String::append(const char* str) { size_t len = strlen(str); if (_size + len > _capacity) { size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len; reserve(newcapacity); } //strcpy(_str + _size, str); memcpy(_str, str, len + 1); //这里可以使用strcpy,但是使用memcpy会更好 _size += len; }

运行结果:

重载函数”+=“:

这里写const和非const版本

String& String::operator+=(char ch) { push_back(ch); return *this; } String& String::operator+=(const char* str) { append(str); return *this; }

这里和尾插和追加字符串相似,当我们想要追加字符和字符串可以使用这个符号来代替

运行结果:

流输出符号重载

这个需要在类外面进行实现,需要使用友元才能使用私有成员,不过这并不需要访问类的私有成员

ostream& operator<<(ostream& out, const string& s) { //out << s.c_str(); //如果使用这个会有”坑“ for (size_t i = 0; i < s.size();i++) { out << s[i]; } return out; }

我们来进行调试:

首先走到断点处

往下走,此时s1的值变为“hello world”

我们看到终端画面

这样就是输出成功,调试结束

指定位置插入字符

string& string::insert(size_t pos, char ch) { assert(pos <= _size); if (_size >= _capacity) { size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity; reserve(newcapacity); } /*int end = _size; while(end>= (int) pos) //size_t是无符号变量,end会从int变成unsigned int。这称为整型提升,因此这里要强转 { _str[end + 1] = _str[end]; --end; }*/ size_t end = _size + 1; while (end > pos) { _str[end] = _str[end - 1]; --end; } _str[pos] = ch; ++_size; return *this; }

指定位置插入字符串版本

string& string::insert(size_t pos, const char* str) { assert(pos <= _size); size_t len = strlen(str); if (_size >= _capacity) { size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity; reserve(newcapacity); } /*int end = _size; while(end >= (int)pos) { _str[end + len] = _str[end]; --end; }*/ size_t end = _size + len; while (end > pos + len - 1) { _str[end] = _str[end - len]; --end; } for (size_t i = 0; i < len; i++) { _str[pos + i] = str[i]; } _size += len; return *this; }

运行结果:

删除指定字符

在写这个代码前需要写npos的定义

const size_t string::npos = -1;//定义

继续写erase的代码

string& string::erase(size_t pos, size_t len) { assert(pos < _size); //要删除的数据,要大于pos后面的字符个数 if (len == npos || len >= (_size - pos)) { _size = pos; _str[_size] = '\0'; } else { size_t i = pos + len; memmove(_str + pos, _str + i, _size + 1 - i); _size -= len; } return *this; }

运行结果:

寻找指定字符

size_t string::find(char ch, size_t pos)const //寻找指定字符 { for (size_t i = pos; i < _size; i++) { if (_str[i] == ch) { return i; } } return npos; } size_t string::find(const char* str, size_t pos)const //寻找指定字符串和下一个字符的起始位置 { const char* p1 = strstr(_str + pos, str); if (p1 == nullptr) { return npos; } else { return p1 - _str; } }

这里还需要写上substr

substr是用于提取字符串中提取子字符串的函数

代码:

string string::substr(size_t pos, size_t len)const { if (len == npos || len >= _size - pos) { len = _size - pos; } string ret; ret.reserve(len); for (size_t i = 0; i < len; i++) { ret += _str[pos + i]; } return ret; }

这里find和substr要搭配使用

代码:

void spliturl(const string& url) { size_t i1 = url.find(':'); if (i1 != string::npos) { cout << url.substr(0, i1) << endl; } size_t i2 = i1 + 3; size_t i3 = url.find('/', i2); if (i3 != string::npos) { cout << url.substr(i2, i3 - i2) << endl; cout << url.substr(i3 + 1) << endl; } cout << endl; }

运行结果:

大于、大于等于、小于、小于等于、等于和不等于符号重载

bool string::operator<(const string& s)const //大于 { size_t s1 = 0, s2 = 0; while (s1 < _size && s2 < s._size) { if (_str[s1] < s[s2]) { return true; } else if(_str[s1] > s[s2]) { return false; } else { s1++; s2++; } } return s2 < s._size; } bool string::operator<=(const string& s)const//大于等于 { return *this < s || *this == s; } bool string::operator>(const string& s)const//小于 { return !(*this <= s); } bool string::operator>=(const string& s)const//小于等于 { return !(*this < s); } bool string::operator==(const string& s)const//等于 { size_t s1 = 0, s2 = 0; while (s1 < _size && s2 < s._size) { if (_str[s1] != s[s2]) { return false; } else { s1++; s2++; } } return s1 == _size && s2 ==s._size; } bool string::operator!=(const string& s)const//不等于 { return !(*this == s); }

这里我们以大于来做测试,这里如果是大于返回0否则返回1,最后的输出结果为:0、0、1

运行结果:

流输入符号重载

在写这个之前我们需要写一个clear的模拟实现,如果没写会出现bug

void string::clear() { _str[0] = '\0'; _size = 0; }

接下来继续写写上流输入符号重载的代码

istream& operator>>(istream& in, string& s) { s.clear(); char buff[128]; int i = 0; char ch = in.get(); while (ch != ' ' && ch != '\n') { buff[i++] = ch; if (i == 127) { buff[i] = '\0'; s += i; i = 0; } s += ch; ch = in.get(); } if (i < 0) { buff[i] = '\0'; s += buff; } return in; }

我们来进行调试:

先走到断点处

我们往下走并输入“Hello”和“world”

之后看到监视窗口,此时s1和s2分别为“hello”和“world”

调试结束

从流中提取字符串

istream& getline(istream& is, string& str, char delim) { str.clear(); char ch = is.get(); while (ch != delim) { str += ch; ch = is.get(); } return is; }

运行结果:

拷贝构造函数

拷贝构造函数分为传统写法和现代写法,这里先写上传统写法

传统写法:

string::string(const string& s) { _str = new char[s._capacity + 1]; memcpy(_str,s._str, s._size + 1); _size = s._size; _capacity = s._capacity; }

现代写法:

在写现代写法前要先写swap代码

void string::swap(string& s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); }

接下来写上现代写法的拷贝构造函数

string::string(const string& s) { string tmp(s._str); swap(tmp); }

赋值符号重载

赋值符号重载也分为两种写法,一个是传统写法一个是现代写法,先来写传统写法

传统写法:

string& string::operator=(const string& s)//赋值操作符重载(传统) { if (this != &s) { char* tmp = new char[s._capacity + 1]; memcpy(tmp, s._str, _size + 1); delete[] _str; _str = tmp; _size = s._size; _capacity = s._capacity; } return *this; }

现代写法:

string& string::operator=(const string& s)//赋值操作符重载(现代) { if (this != &s) { string tmp(s); swap(tmp); } return *this; }

现代写法还有更简洁的写法

string& string::operator=(string& tmp) //赋值操作符重载(现代) { swap(tmp); return *this; }

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询