96SEO 2026-02-20 08:24 13
介绍二、标准库中的string类1、string类介绍2、string类使用3.1

三、模拟实现string类四、windows下的VS中的string类和Linxu下的g中的string类。
libaray-标准模板库)是C标准库的重要组成部分不仅是一个可复用的组件库而且
在惠普实验室完成的原始版本本着开源精神他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码无需付费。
唯一的条件就是也需要向原始版本一样做开源使用。
本。
被GCC(Linux)采用可移植性好可公开、修改甚至贩卖从命名风格和编程
当我们查看c文档中关于string类的定义时可以发现string类是依靠basic_string类模板显示实例化出来的一个类。
我们可以查看basic_string这个类模板是这样定义的。
并且根据basic_string这个类模板生成了4个模板实例。
string类只是其中一个。
这是因为在计算机刚发明时因为计算机只能存储1010的二进制位数据而美国人使用的字母和符号是无法直接存储到计算机内的。
所以美国人就想到了一套ASCII码方式。
在计算机中所有的数据在存储和运算时都要使用二进制数表示因为计算机用高电平和低电平分别表示1和0例如像a、b、c、d这样的52个字母包括大写以及0、1等数字还有一些常用的符号例如*、#、等在计算机中存储时也要使用二进制数来表示而具体用哪些二进制数字表示哪个符号当然每个人都可以约定自己的一套这就叫编码而大家如果要想互相通信而不造成混乱那么大家就必须使用相同的编码规则于是美国有关的标准化组织就出台了ASCII编码统一规定了上述常用符号用哪些二进制数来表示。
这样使用0-127就将全部字符都表示出来了即还不到一个字节所以ASCII码中每个字符占一个字节。
但是随着计算机在全世界范围被使用计算机中只显示这127个欧美字符是不行的还需要显示其它国家的语言。
那么像我们中国使用的是象形文字ASCII编码那一套对我们就不适用了。
所以又出现了一种unicode码的编码方式。
统一码Unicode也叫万国码、单一码由统一码联盟开发是计算机科学领域里的一项业界标准包括字符集、编码方案等。
统一码是为了解决传统的字符编码方案的局限而产生的它为每种语言中的每个字符设定了统一并且唯一的二进制编码以满足跨语言、跨平台进行文本转换、处理的要求。
如果把各种文字编码形容为各地的方言那么统一码就是世界各国合作开发的一种语言。
在这种语言环境下不会再有语言的编码冲突在同屏下可以显示任何语言的内容这就是统一码的最大好处。
就是将世界上所有的文字用2个字节统一进行编码。
那样像这样统一编码2个字节就已经足够容纳世界上所有语言的大部分文字了。
在统一码中汉字“字”对应的数字是23383。
在统一码中我们有很多方式将数字23383表示成程序中的数据包括UTF-8、UTF-16、UTF-32。
UTF是“UCS
Format”的缩写可以翻译成统一码字符集转换格式即怎样将统一码定义的数字转换成程序数据。
所以在c中为了应对不同的编码格式才根据basic_string模板实例化了4中类。
在string类中一个字符占1个字节在wstring类中一个字符占2个字节在u16string类中一个字符占2个字节在u32string类中一个字符占4个字节。
string类有对应的构造函数和析构函数还有赋值运算符的重载函数。
{//会调用string类的默认构造函数string()string
s)构造函数因为参数不为string对象的引用而是字符串地址。
string
string字符串隐式转换为临时string对象然后调用构造函数将该临时对象初始化//然后再调用拷贝构造将临时对象拷贝给s3对象。
但是编译器会直接优化为调用构造函数。
//
npos)这个构造函数因为第一个参数为string对象的引用//意思为从s3字符的下标为6的位置开始拷贝拷贝的长度为6。
string
endl;//当拷贝的长度大于s3的长度时此时会只拷贝到s3末尾就停止了。
//该构造函数的第三个参数缺省值npos为-1而-1的补码按无符号整数解析就是int表示的最大的无符号整数//所以如果第三个参数不给的话就是默认将s3字符串拷贝到结尾再停止。
string
n)这个构造函数因为第一个参数为字符串地址。
//该函数表示的意思是将hello
endl;//当第二个参数n大于字符串长度时会拷贝到\0就停止了string
c)这个构造函数。
//会将s9中初始化为10个*字符string
size()与length()方法底层实现原理完全相同引入size()的原因是为了与其他容器的接口保持一致一般情况下基本都是用size()。
clear()只是将string中有效字符清空不改变底层空间大小。
endl;//缩容将s1对象的capacity缩到和size一样大//但是缩容的开销很大一般不会使用s1.shrink_to_fit();cout
当使用string存储字符串时如果字符串长度太大时string会自动进行扩容。
如果字符串过大但是string自动扩容每次扩的都很少时此时想要直接申请够存储字符串的空间就可以使用reserve和resize。
reserve和resize这两个成员函数可以预先开辟指定大小的空间。
当需要知道开辟多少空间时可以使用reserve提前将空间开好减少扩容提高效率。
因为要考虑对齐等因素开的空间会比实际要求的大但是绝对不会比实际要求的少。
c)都是将字符串中有效字符个数改变到n个不同的是当字符个数增多时resize(n)用0来填充多出的元素空间resize(size_t
c)用字符c来填充多出的元素空间。
注意resize在改变元素个数时如果是将元素个数增多可能会改变底层容量的大小如果是将元素个数减少底层空间总大小不变。
res_arg0)为string预留空间不改变有效元素个数当reserve的参数小于string的底层空间总大小时reserver不会改变容量大小。
即reserve只会改capacity不会改size。
而resize会将capacity和size都改变。
endl;//扩容初始化//如果resize不给第二个参数则会初始化为\0string
可以看到使用resize会将预先开辟出来的空间初始化。
而reserve只是将空间开辟出来并不会进行初始化。
所以reserve只会改capacity不会改size。
而resize会将capacity和size都改变。
如果不给第二个参数会初始化为’\0’如果给第二个参数则会将后面的值初始化为给定的字符。
resize还可以删除数据当传入的第一个参数比size小时就会删除数据比capacity大时就会扩容。
{//扩容初始化//当resize第一个参数比capacity大时就会扩容string
endl;//当resize第一个参数比size小时就会删除数据。
string
当我们想要遍历string类中存的字符串的每一个字符时可以使用三种方法来遍历。
其实使用范围for范围for底层还是使用迭代器范围for只是对迭代器又封装了一层。
可以先将迭代器·想象为指针begin就是返回字符串的起始地址而end就是返回结束位置的下一个字符的指针。
}迭代器除了有正向迭代器外还有被const修饰的正向迭代器还有反向迭代器和被const修饰的反向迭代器。
当想要遍历被const修饰的对象时此时就需要使用const_iterator迭代器。
并且const_iterator迭代器只能访问对象内容并不能改变对象的内容。
其中begin()和end都有两种一种是不被const修饰的一种是被const修饰的。
当创建的为iterator迭代器时会自动去调用不被const修饰的begin()当创建的为const_iterator迭代器时会自动去调用被const修饰的begin()。
正向迭代器只能遍历和读数据不能写数据。
string::const_iterator
反向迭代器只能遍历和读数据不能写数据。
string::const_reverse_iterator
world);//正向迭代器可以遍历数据也可以读写数据string::iterator
endl;//创建一个反向迭代器将从后向前遍历s1中的字符串。
可以遍历数据也可以读写数据//string::reverse_iterator
s1.rbegin();//此时可以使用auto来自动推断变量的类型。
//auto是根据右边的返回值来推左边的类型的虽然可以简化代码但是代码的可读性差了auto
使用operator[]和a都可以访问指定位置的元素只不过当使用[]越界访问时会直接中止程序而使用at越界访问时会抛异常。
endl;}//返回字符串的第一个字符也可以修改这个字符cout
endl;//返回字符串的最后一个字符就是\0前面的字符也可以修改这个字符cout
s1(hello);//push_back()为尾插一个字符。
s1.push_back(
endl;//append是尾插字符串s1.append(world);cout
endl;//可以尾插一个字符也可以尾插字符串//的底层还是调用的push_bakc和append只是多了一层封装使用起来更加简单明了s1
}当一直向string类中插入字符时就要涉及到扩容的问题了。
可以看到在VS下每次扩容为原来的1.5倍。
这是因为在string中当字符串长度小于16时其实并没有动态申请空间存放字符串而是将字符串存放到string类的_buf数组内当字符串长度大于16后才开始进行扩容然后动态申请空间将字符串存到动态申请的空间中。
这样做的目的是防止了每有一个小的字符串就要动态申请空间然后申请的空间很小就会产生很多碎片空间而浪费空间。
当我们使用sizeof查看s对象大小时可以看到大小为28这是因为除了两个int型变量和一个指针变量外还有_Buf数组的16个字节的大小。
string扩容在不同的编译器下是不同的比如windows下VS1.5倍扩容linux下g2倍扩容。
并且g中没有buf数组。
这是因为VS用的PJ版STLg中用了SGI版本STL。
str)函数在s1对象的pos位置插入s2对象的全部字符串s1.insert(0,
sublen);函数//在s1对象的pos位置插入s2对象的从subpos位置开始长度为sublen的字符串。
//如果sublen的长度超过s2的长度则会将s2的字符串全部插入到s1中然后就停止。
s1.insert(0,
n)函数//在s1对象的pos位置插入s字符串的前n个字符如果n大于字符串s的长度则会将字符串s全部插入s1对象的字符串中然后就停止。
s1.insert(0,
c)函数//在s1对象的pos位置插入n个c字符s1.insert(0,
c)函数//在s1对象的字符串的begin()向后移5个位置处插入n个字符c。
s1.insert(s1.begin()
c)函数//在s1对象的字符串的begin()向后移5个位置处插入字符c。
s1.insert(s1.begin()
insert和erase不推荐经常使用因为它们在字符串中间插入或删除数据都会挪动数据效率比较低开销还很大
npos)函数//将s1对象的字符串从pos位置开始向后删除len个字符s1.erase(5,
npos)函数//因为没有给第二个参数而npos-1,即代表无符号最大的数所以会将s1pos位置之后的字符全部删除s1.erase(5);cout
p)函数//将s1.begin()向后移5个位置然后将后面的一个字符删除s1.erase(s1.begin()
last)函数//将s1.begin()向后移5个位置然后到s1.end()之间的字符都删除s1.erase(s1.begin()
str)函数//将s1对象的字符串从pos位置开始的len长度的字符替换为s2对象中的字符串//如果len大于s1对象的字符串的长度则会将s1的全部字符串替换为s2的字符串。
//s1.replace(0,
str)函数//将s1.begin()向后移5个位置然后到s1.end()的位置之间的字符串替换为s2对象的字符串//s1.replace(s1.begin()
sublen)函数//将s1对象的字符串从pos位置开始的len长度的字符替换为s2对象中的字符串从subpos位置开始的sublen长度的字符//s1.replace(0,
s)函数//将s1对象的字符串从pos位置开始的len长度的字符替换为s字符串//如果len大于s1对象的字符串的长度则会将s1的全部字符串替换为s字符串。
//s1.replace(0,
s)函数//将s1.begin()向后移5个位置然后到s1.end()的位置之间的字符串替换为s字符串//s1.replace(s1.begin()
n)函数//将s1对象的字符串从pos位置开始的len长度的字符替换为s字符串的前n个字符//s1.replace(0,
n)函数//将s1.begin()向后移5个位置然后到s1.end()的位置之间的字符串替换为s字符串的前n个字符//s1.replace(s1.begin()
c)函数//将s1对象的字符串从pos位置向后len长度的字符替换为n个c字符//s1.replace(0,
c)函数//将s1.begin()向后移5个位置然后到s1.end()的位置之间的字符串替换为n个c字符s1.replace(s1.begin()
str)函数//将s1对象的字符串替换为s2对象的字符串s1.assign(s2);cout
sublen)函数//将s1对象的字符串替换为s2对象从subpos位置开始向后sublen长度的字符串s1.assign(s2,
s)函数//将s1对象的字符串替换为字符串ss1.assign(hello
n)函数//将s1对象的字符串替换为字符串s的前n个字符s1.assign(hello
c)函数//将s1对象的字符串替换为n个c字符s1.assign(10,
我们知道在std库中有一个swap的函数模板该模板实例的swap为浅拷贝即会将string类的内容都互换而string类里面的swap函数只是将两个string类类型对象的指向字符串的指针互换一下即完成了内容的互换所以string类里面的swap函数效率更高。
endl;//将s1和s2的字符串内容互换s1.swap(s2);cout
noexcept;函数//从s1对象的字符串的起始位置开始找s2对象的字符串并返回该字符串在s1对象中第一次出现的位置。
//如果找不到就返回npos.size_t
const;函数//从s1对象的字符串的起始位置开始找s字符串并返回该字符串在s1对象中第一次出现的位置。
//如果找不到就返回npos.size_t
const;函数//从s1对象的字符串的起始位置开始找s字符串的前n个字符组成的字符串并返回该字符串在s1对象中第一次出现的位置。
size_t
noexcept;函数//从s1对象的字符串的起始位置开始找字符c并返回该字符在s1对象中第一次出现的位置。
size_t
下面的代码可以完成替换但是使用replace将1个字符替换为3个字符时可能s1对象的容量不够会需要扩容此时会增加开销。
dog.);//练习将s1字符串中的空格都换为%20size_t
}所以可以先提前遍历s1对象的字符串中有多少空格然后提前将s1的空间开好避免replace时频繁扩容增加开销。
){num;}}//提前开空间避免replace时频繁开空间s1.reserve(s1.size()
num);//练习将s1字符串中的空格都换为%20size_t
}虽然我们提前将s1的空间开好可以提高一些效率但是replace方法替换都会移动数据移动数据的开销更大。
所以我们可以重新创建一个string类类型对象用追加的形式将转换好的字符都尾插到新对象中。
然后将新对象赋值给s1。
){num;}}//提前将newStr的空间开好newStr.reserve(s1.size()
c_str是返回一个以’\0’符结尾的字符串。
直接打印s1遇到\0不会停止因为符号的重载函数是按s1.size打印的size有多大就打印多少个字符。
但是c_str返回的是一个字符串的地址即一个const
char类型的指针所以遇到\0会停止。
即c_str返回的是c语言中的字符串。
c_str返回c语言中的字符串就是为了兼容c语言的一些接口因为有一些c语言的函数需要传入以’\0\结尾的字符串即const
char类型的指针。
而不是传入c中的string类类型的对象。
xxxxxxxx;//直接打印s1对象遇到\0不会停止打印cout
endl;//而s1.c_str()返回的是一个c语言中的字符串所以遇到\0就会结束cout
world);//substr函数为截取字符串即从pos位置截取len长度的字符串返回。
//如果第二个参数没有则默认从pos位置将字符串全部截取然后返回。
string
noexcept;函数//从s1对象的字符串的pos位置开始向前查找对象s2的字符串找到了就返回字符串在s1中的位置//如果不传第二个参数就默认从s1字符串的末尾开始查找。
size_t
file(string.cpp.tar.zip);//从后向前找字符.size_t
url(https://legacy.cplusplus.com/reference/string/string/rfind/);cout
start);//打印://到第一个/之间的字符串string
dog.);//在s1对象的字符串中查找abcd字符串中的字符第一个出现的位置并且将该位置返回size_t
find_first_of为从前向后查找而find_last_of就是从后向前查找。
dog.);//在s1对象的字符串中查找abcd字符串中的字符最后一个出现的位置并且将该位置返回size_t
find_first_of为查找s1中含有字符串中字符的位置返回而find_first_not_of是查找s1中不是这样字符串中字符的位置返回。
dog.);//在s1对象的字符串中查找不是abcd字符串中的字符的第一个出现的位置并且将该位置返回size_t
s1.find_first_not_of(abcd);while
*;//继续向后找s1对象的字符串中不是abcd的字符found
find_first_not_of是从前向后找而find_last_not_of为从后向前找。
当我们知道了string类的大致操作后我们也可以模拟实现一下string类。
当我们写到有参构造函数时会发现形参str在初始化列表赋值给_str时发生了错误。
这是因为str为const
char类型后虽然上面的情况不报错了但是又遇到了新的问题。
当我们对[]操作符进行重载时会看到发生了错误因为函数返回的是一个char类型的引用而_str[pos]是一个const
char类型以后就不可以修改_str字符串的值了这是肯定不可以的所以我们不能将_str设为const
char类型。
那么我们的有参构造函数就不能像上面那样在初始化列表中将成员变量全部初始化了我们需要像下面那样将_str在构造函数内初始化。
先使用new申请空间然后使用strcpy函数进行拷贝这样就完成了_str的初始化。
然后我们测试时发现报出了异常这是因为当创建对象使用默认的无参构造函数时此时_str为nullptr空指针然后s1.c_str()返回的是一个nullptr当使用coutnullptr时因为cout有自动识别类型的特性而s1.c_str()返回的是一个const
char类型的指针但是cout不会直接将这个指针打印出来它会打印出来这个指针指向的字符串即将这个指针解引用找到指针指向的字符串进行打印当遇到’\0’就停止。
而此时s1.c_str()返回的是nullptr所以cout就会对nullptr进行解引用然后就会造成对空指针解引用。
就会出现异常了。
所以在无参构造函数中不能将_str初始化为nullptr。
我们可以在无参构造函数中将_str赋值为’\0’这样当打印时遇到’\0’就停止打印而第一个就是’\0’就会打印空字符串了。
然后我们可以将有参构造函数和无参构造函数和为一个全缺省构造函数。
此时需要注意的时缺省参数给的缺省值不能给str的值为nullptr空指针而要使str指向’\0’字符即str的值为’\0’字符所在的空间而不是str的值为0。
然后我们再来看拷贝构造函数。
我们知道编译器默认生成的拷贝构造函数为浅拷贝即如果使用编译器自带的默认拷贝构造函数那么会出现两个string类类型对象指向同一片空间的情况例如下面的图片中的情况。
并且在s2对象销毁时调用析构函数delete[]
_str后在s3对象销毁时也会调用析构函数再次delete[]
_str而此时的_str已经被s2对象的析构函数给delete[]过了所以对s2的_str进行了两次delete[]故出现了错误。
并且使用浅拷贝当改变s2对象中_str的值时s3对象中_str的值也会改变。
所以我们需要自己重写深拷贝的拷贝构造函数。
拷贝构造函数也有初始化列表所以我们在初始化列表中将_size和_capacity直接初始化为要拷贝对象的_size和_capacity。
然后在拷贝构造函数内重新申请_capacity1大小的空间给新对象并且使用strcpy将s对象的_str内容拷贝给新对象的_str。
此时可以看到s2对象的_str指向的空间和s3对象的_str指向的空间不是一片空间。
但是s2对象和s3对象的内容都相同并且修改s2对象的内容时不会影响s3对象的内容。
接下来我们实现赋值运算符的重载函数。
该函数就需要分如下三种情况。
这样我们实现赋值运算符的重载函数时还需要进行判断。
但是我们也可以不进行判断直接将s1对象的_capacity设置为和s2的_capacity一样然后将s1对象_str的空间释放重新为s1的_str开辟一个大小为_capacity的新空间然后将s2的_str的内容拷贝给s1的_str。
下面的代码可以实现s1s2即将s2赋值给s1但是当将自己赋值给自己时就会打印乱码。
这是因为在赋值运算符重载函数中刚开始就将s1对象的_str的空间释放了然后这个空间的值就为随机值了然后strcpy(_str,s._str)将随机值拷贝过来给s1的_str所以s1的_str就为随机值了。
所以我们要先判断一下如果是s1s1的情况时就不需要做任何操作。
并且我们提前释放s1对象的_str的空间也有安全隐患。
例如如果我们申请空间不成功时此时s1对象的_str的数据就造成丢失了。
所以我们应该先申请空间然后再释放s1的_str的空间。
当我们实现了size()函数后我们实现一个Print函数用来遍历对象的字符串。
然后我们会发现在测试时可以调用size()函数和使用[]操作符的重载函数但是在Print函数中使用size()函数和[]操作符的重载函数发生了错误。
这是因为在Print中使用size()函数和[]操作符的重载函数发生了权限放大。
因为在Print函数中s被const修饰所以当调用成员函数size()和[]操作符的重载函数时传入的this指针为const
string类型而成员函数size()和[]操作符的重载函数的this为string*类型所以发生了权限放大。
此时就应该将size()函数和[]操作符的重载函数使用const修饰这样Print函数中就可以调用这两个函数了。
此时虽然Print函数可以调用这两个函数了但是[]操作符的重载函数被const修饰了那么就不能使用[]操作符来改变string类类型对象中_str的值了例如s[1]就不可以了这肯定是不合理的。
所以此时我们就需要写两个[]操作符的重载函数一个[]操作符的重载函数被const修饰这个函数给被const修饰的对象调用。
另一个[]操作符的重载函数不使用const修饰这个函数给那些需要改变对象的值的情况调用。
因为编译器会去调用最匹配的[]操作符的重载函数所以这样的设计就合理了。
然后我们再使用指针来实现string类的迭代器。
使用迭代器可以读数据也可以修改数据。
我们在前面说过范围for在底层就是使用迭代器来完成的其实范围for就类似于宏的替换当遇到范围for的语法就在底层替换为迭代器。
所以支持了迭代器就支持了范围for。
范围for会在底层傻瓜式的调用迭代器的begin、end等方法。
所以只要实现了begin、end方法就可以使用范围for了。
但是如果我们将begin改为Begin()此时再使用范围for就会出错也为范围for只能傻瓜式的调用begin和end等方法如果没有名字为这些的函数就会使用不了范围for。
当我们在测试时调用begin和end函数可以调用但是在Print函数中调用begin和end时就会发生错误这是因为此时又发生了权限放大在Print函数中对象s被const修饰而begin()和end()的隐藏形参为string
this没有被const修饰所以调用begin()和end()时发生了权限放大。
所以此时我们就需要再写一个被const修饰的begin和end方法并且这两个方法返回的类型都为const
类型的指针因为当创建const_iterator的迭代器就说明不希望通过这个迭代器改变对象的值所以返回一个const
char类型的指针就不能通过解引用这个指针来改变指针指向的值。
但是这个指针的值是可以改变的因为还需要通过这个指针然后访问后面的元素。
例如const
此时Print函数中被const修饰的对象s可以调用被const修饰的begin和end函数并且此时只能通过迭代器来读取对象s的数据了并不能通过迭代器来改变对象s的值了。
因为被const修饰的begin()函数返回的是const
简单的实现了迭代器后我们再来实现一下string类类型对象的比较即实现比较符的重载函数。
上面的这些比较运算符的重载函数当里面的this和const修饰的s对象位置交换一下时我们发现就会出错这是因为又发生了权限放大。
s对象被const修饰当s对象调用比较运算符的重载函数时这个函数的隐藏参数为string
此时我们就需要将上面这些比较运算符的重载函数都使用const修饰。
这也使我们明白了那些string类的成员函数中内部不修改成员变量的值的成员函数都建议加上const修饰这样才能减少代码出错的概率。
像c_str()这样的函数使用const修饰后那些被const修饰的对象也可以调用这些函数了。
而那些有时候需要修改成员变量数据的成员函数有时候又需要被const修饰的对象调用的函数。
就需要写两个版本一个不被const修饰的版本用于修改成员变量数据时调用。
一个被const修饰的版本用于给被const修饰的对象调用。
然后我们再来实现尾插字符和尾插字符串的函数。
但是当尾插字符和字符串时可能会遇到容量不够的情况此时就需要进行扩容。
所以我们要先实现reserve扩容函数然后再实现尾插字符和尾插字符串的函数。
当我们像下面这样实现了reserve函数后我们测试时会发现这个reserve函数会进行缩容即当我们传入的值n比s1对象的字符串长度小时此时也会开辟一个n1大小的新空间然后将原来的数据拷贝过去此时因为使用的strcpy拷贝即strcpy拷贝是遇到’\0’才停止所以tmp中的数据还是原来的_str但是因为n小于_str字符串的长度开辟的空间为n1大小根本不够放原来的_str字符串的所以就会出现错了。
所以我们就需要在扩容时先判断一下如果n_capacity时就不进行缩容操作了只有当n_capacity时才进行增容操作。
然后我们再实现push_back函数此时我们测试时会发现给s1的字符串尾插一个字符后打印的字符串出现了乱码。
这是因为在push_back()中我们将_str原来的’\0’字符变为了ch字符所以插入ch字符后_str字符串就没有了’\0’字符串结束符。
所以我们需要在尾插字符ch后重新将_str的最后一个字符后设置一个’\0’字符串结束符。
此时当我们测试时又会发现一个新的问题即当我们给一个_capacity为0的对象调用push_back()函数进行尾插时因为我们的扩容写的为_capacity2所以当_capacity为0时_capacity2还是0即我们没有给对象扩容成功。
此时我们有两种方法可以解决这个问题第一个办法是在构造函数中不让_capacity的起始为0即在构造函数初始化时就开辟少量的空间然后使_capacity不为0。
第二个办法就是在push_back函数调用reserve()函数进行扩容时先判断_capacity的值如果_capacity为0就不给reserve函数传参为_capacity*2而传入一个具体的值。
然后我们再实现append()函数。
因为append函数是尾插一个字符串所以开辟的空间为_sizelen大小。
然后将字符串插入到原字符串的后面即_str_size的位置。
因为_str为字符串首字符的位置而_str_size为字符串的最后一个字符的后面的位置即为’\0’的位置。
而尾插str就是将str插入到这个位置。
并且因为strcpy函数中会在最后将目标字符串的后面加上’\0’字符所以我们不需要关系乱码问题。
将strcpy换成strcat也可以实现但是使用strcat还需要自己去找\0的位置然后在\0的位置后追加str字符串但是本来就知道\0的位置为
实现了尾插字符和尾插字符串字符相当于上面这两个函数的结合所以接下来我们就实现运算符的重载函数。
因为运算符的重载函数可能需要处理单个字符或一个字符串所以我们对进行了重载当一个字符时在底层调用push_back()函数当一个字符串时在底层调用append()函数。
我们知道c中的string类中reserve函数为扩容函数并且不会对扩容的空间进行初始化。
而resize函数会对扩容的空间进行初始化如果调用resize函数时只传入一个参数则会将扩容的空间初始化为’\0’如果传入了第二个参数则会将扩容的空间初始化为第二个参数。
所以我们可以尝试来实现一下resize函数。
c中是对resize函数进行了重载写了两个版本一个是一个参数的版本还有一个是两个参数的版本。
而我们可以尝试使用半缺省参数的方法将这两个函数和为一个函数。
可以看到在测试代码中对象s1的最后面加上了’\0’字符并且s1和s2的_size和_capacity都被改变了。
而且官方实现的resize中当n小于_str字符串的长度时会将字符串直接删除为长度为n并且也不会实现缩容因为缩容需要很大的代价缩容也是开辟新空间再将值拷贝到新的空间中也会有很大的开销。
所以我们自己在实现时也不能缩容需要像实现reserve一样进行判断。
然后我们接着实现在某位置插入字符或字符串还有在某位置删除字符串。
我们知道在c官方的string库中有一个size_t的变量npos这个变量的值为-1但是该变量为无符号整型即其实npos表示int型整数表示的最大值。
我们自己实现string类时可以将npos定义为静态成员变量因为有很多成员函数都使用npos来作为缺省值。
当我们定义静态成员变量npos时发现不可以给静态成员变量缺省值。
这是因为普通成员变量给缺省值是在初始化列表中对成员变量进行初始化而静态成员变量并没有初始化列表进行初始化所以不可以给静态成员变量缺省值。
但是c语法中允许被const修饰的静态成员变量在定义时进行初始化并且这种语法只支持整型。
double类型的静态成员变量使用const修饰时就不能在在定义时被初始化了。
但是我们自己在写代码时还是使用正常的写法将静态成员变量在类中定义在全局域中进行初始化。
然后我们来实现在某位置插入n个字符。
我们想到的是当endpos时就将end位置的字符向后移n位这样移到最后就会在pos位置空出n个位置用来插入n个字符但是当pos为0时因为end为size_t类型为无符号整型所以当end-1时会按最大的int型整数来算end值所以此时end还是大于pos的就还会执行while后面的语句这样就会出现越界访问了所以会报异常。
此时我们可以将end_size1然后当pos为0时就不会再出现越界的现象了。
当实现了某位置插入字符后我们再写insert的重载函数来实现某位置插入字符串。
其基本思路和上面插入n个字符类似只不过在拷贝数据时需要使用strncpy函数因为strcpy函数遇到’\0’就停止拷贝如果str“abc\0defg”则使用strcpy只会拷贝abc就会少拷贝字符。
当insert函数实现了其实前面写的push_back函数和append函数就可以复用insert函数来实现了。
下面我们再来实现erase函数在c库中的erase函数在删除字符串时也不会进行缩容所以我们实现erase时也不需要进行缩容处理。
接下来我们实现string库里面的swap函数我们知道在std库中有一个swap函数的模板而这个模板的调换两个对象是先创建一个临时对象然后再调用对象的赋值运算符等进行交换这个开销是很大的。
而string类中的swap函数只是将三个成员变量交换一下即可所有string类中的swap函数效率更高。
然后我们再实现find函数进行字符或字符串的查找。
下面实现的是查找字符如果找到了就返回该字符第一次出现的下标如果没有出现该字符就将npos返回。
然后我们再实现查找子串在内部使用strstr函数来查找子串strstr返回的是子串的地址而find函数返回的是子串第一次出现的下标所以我们可以使用子串的地址减_str的头地址就得到了子串的下标。
然后我们再来实现流插入符的重载函数和流提取符的重载函数。
我们在实现日期类的流插入符重载函数和流提取符重载函数时使用了友元函数的概念那么我们实现这两个函数必须要实现成友元函数吗当然不需要因为对于对象的流插入我们不需要访问对象的_str成员变量我们可以使用迭代器或者范围for来遍历对象的字符串。
然后我们使用同样的思路来实现流提取符重载函数。
但是下面的代码在测试时我们发现当输入为空格或换行符时并没有结束输入还是一直让我们输入。
这是因为c或c中规定多个字符之间的间隔是空格或者换行所以使用inch时会将空格或者换行当作字符之间的间隔然后不会提取到缓冲区里面去这样就会一直没有结束符而一直循环输入。
此时我们可以使用get()函数来提取数据get()函数会将输入的所有字符都放入缓冲区中。
此时就可以正常输入了。
但是我们发现当测试时又出现了乱码这是因为在函数中为sch当结束时没有’\0’字符串结束符了。
并且库里面的流提取符重载函数会清除对象中原来字符串的内容。
所以我们实现的流提取符重载函数也要先清除原来的字符串内容我们需要写一个clear成员函数在每次使用流提取符重载函数时先将对象的字符串清空。
这样每次输入时都会将之前的字符串先清空。
我们使用来给对象s每次追加一个字符当我们输入的字符串比较长时就会频繁的进行扩容所以我们可以进行一下优化库里面是使用了一个buff数组每当buff数组满了之后就进行一次扩容。
这样可以减少扩容的频率。
到此我们模拟实现string类就基本完成了库里面还有很多方法我们只模拟实现了string类比较常见的一些方法。
四、windows下的VS中的string类和Linxu下的g中的string类。
在windows下的VS编译器中string类中有一个大小为16的Buf数组但是只能存15个有效字符因为最后一个空间要留个’\0’当string类中的字符串大小小于15时字符串的内容就存到_Buf这个数组中当string类中的字符串大小大于15时就会存在动态申请的堆区中了。
使用_Buf数据可以使string类的字符串很短时就不需要动态申请内存了这样也减少了内存碎片的产生。
而每个string类大小为28个字节因为_Buf占16个字节size_t类型的size和capacity共占8字节还有一个指针指向动态申请的空间也占4个字节。
在Linux下的g编译器中string类类型对象的大小在32位电脑下为4字节在64位电脑下为8字节即只有一个指针的大小。
在g中string是通过写时拷贝实现的string对象内部只包含了一个指针该指针将来指向一块堆空间内部包含了如下字段
空间总大小字符串有效长度引用计数指向堆空间的指针用来存储字符串。
在Linux下的g中创建一个string类类型的对象是这样实现的。
当通过拷贝构造创建s2时g中这样处理。
即先将s2中的_ptr指针也指向s1的空间s1和s2先同时访问这一片空间并且此时引用计数_refcount为2引用计数是为了防止重复析构的因为如果s1和s2都指向一个空间的话那么会调用两次析构函数来释放这片空间但是有了_refcount的概念后只有当_refcount1时才调用析构函数而如果_refcount不为1时如果有一个指向这片空间的对象销毁了_refcount就
如果向s2对象中插入数据或删除数据时即对这片空间进行写操作时此时会新开辟一片空间然后将s1指向的空间的内容都拷贝到新空间去这样s2修改的就是新空间的内容了就不会影响s1指向的空间里的内容。
而如果只对s2对象进行读操作时就不需要进行上面的拷贝操作这样就减少了一次深拷贝。
这就是写时拷贝的大致思想。
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback