【C++】string类 从 入门使用 到 底层深浅拷贝模拟实现

发布时间:2026/8/3 4:58:33
【C++】string类 从 入门使用 到 底层深浅拷贝模拟实现 文章目录前言一、为什么需要 string 类1、C 语言字符串的问题2、C 的解决方案string 类二、前置知识auto 和范围 forC11 语法1、auto 关键字2、范围 for范围 for 循环三、string 类常用接口详解1、构造函数4 种最常用2、容量操作3、访问与遍历4、修改操作常用的5、非成员函数四、string 类的深浅拷贝模拟实现1、浅拷贝的坑2、深拷贝比较传统的写法3、深拷贝现代版写法总结前言在 C 语言中我们操作字符串靠的是 char* 指针加 str 系列的库函数容易踩坑越界访问、内存泄漏、忘记加\0……。到了 C有了string类字符串操作一下子变得简单又安全。不用自己管内存不用手动处理\0拼接、查找、替换都有现成的接口。本文主要讲string类的入门使用涉及常用接口、遍历方式、修改操作、深浅拷贝等知识点帮助理解和掌握string类。前置知识C 语言字符串、C 类与对象基础、深浅拷贝知识一、为什么需要 string 类1、C 语言字符串的问题C 语言中字符串是以\0结尾的字符数组标准库提供了strcpy、strlen、strcat等函数操作字符串但问题很多不符合面向对象思想字符串和操作函数是分离的不是一个整体底层空间需要用户自己管理strcat、strcpy都要自己保证空间够大容易出现越界容易出 bug忘记加\0、内存泄漏等等问题。2、C 的解决方案string 类C 用类封装了字符串把数据和操作绑在一起自动管理内存用起来简单高效。二、前置知识auto 和范围 forC11 语法在正式讲string之前先补两个 C11 的实用语法后面遍历字符串会大量用到。1、auto 关键字auto让编译器自动推导变量类型不用手动写长长的类型名。以下代码只是让大家直观感受auto作用不需要完全理解。#includeiostream#includemap#includestringusingnamespacestd;intmain(){inta10;autoba;// b自动推导为intautoca;// c自动推导为charmapstring,stringdict{{apple,苹果},{orange,橙子}};// 不用auto的写法类型名非常长mapstring,string::iterator it1dict.begin();// 用auto简洁多了autoit2dict.begin();return0;}注意事项auto声明的变量必须有初始值否则编译器推不出来同一行声明多个变量类型必须一致auto不能做函数参数不能直接声明数组。2、范围 for范围 for 循环C11 引入的更简洁的遍历方式。#includeiostream#includestringusingnamespacestd;intmain(){string shello;// 传统遍历for(inti0;is.size();i)couts[i] ;coutendl;// 范围for遍历C11for(autoch:s)coutch ;coutendl;// 要修改元素用引用for(autoch:s){ch1;// 每个字符1coutch ;}coutendl;return0;}范围 for 的底层就是迭代器所有支持迭代器的容器都能用。三、string 类常用接口详解string的接口非常多我只讲最常用的剩下的查文档就好。文档链接cplusplus1、构造函数4 种最常用构造函数功能string()构造空字符串string(const char* s)用 C 语言风格字符串构造string(size_t n, char c)用 n 个字符 c 构造string(const string s)拷贝构造#includeiostream#includestringusingnamespacestd;voidTestString(){string s1;// 空字符串strings2(hello);// 用C字符串构造strings3(5,a);// 5个a → aaaaastrings4(s2);// 拷贝构造s3couts1endl;couts2endl;couts3endl;couts4endl;}2、容量操作函数功能size()返回有效字符长度最常用length()返回有效字符长度和 size 效果一样历史遗留下来的capacity()返回底层空间总大小empty()判断是否为空串clear()清空有效字符不释放空间reserve(n)预留 n 个字符的空间避免频繁扩容resize(n, c)把有效字符个数改成 n多出的用字符 c 填充#includeiostream#includestringusingnamespacestd;intmain(){strings(hello);couts.size()endl;// 5couts.length()endl;// 5couts.capacity()endl;// 底层空间大小couts.empty()endl;// 0非空cout------------------------------endl;// clear清空有效字符不释放空间s.clear();couts.size()endl;// 0couts.capacity()endl;// 空间不变cout------------------------------endl;// reserve预留空间只改容量不改有效长度s.reserve(100);couts.capacity()endl;// 100reserve仅保证容量传入值couts.size()endl;// 0cout------------------------------endl;// resize改有效长度s.resize(10,x);// 变成10个xcoutsendl;// xxxxxxxxxxcouts.size()endl;// 10cout------------------------------endl;return0;}重点注意size()和length()完全一样引入size()是为了和其他容器接口统一一般用size()clear()只清空数据不释放空间capacity不变reserve()只预留空间不改变有效字符数reserve()参数小于当前容量时VS所有版本会忽略不缩容原因这是一个非约束性请求标准不强制要求缩容resize()改变有效字符数空间不够会扩容3、访问与遍历方式说明operator[]下标访问像数组一样用最常用迭代器 begin()、end()正向迭代器反向迭代器 rbegin()、rend()反向遍历范围 forC11 简洁写法#includeiostream#includestringusingnamespacestd;intmain(){strings(abcdef);// 1. 下标访问最常用for(size_t i0;is.size();i)couts[i] ;coutendl;// 2. 迭代器string::iterator its.begin();while(it!s.end()){cout*it ;it;}coutendl;// 3. 反向迭代器从后往前遍历string::reverse_iterator rits.rbegin();while(rit!s.rend()){cout*rit ;rit;}coutendl;// 4. 范围for最简洁for(autoch:s)coutch ;coutendl;return0;}4、修改操作常用的函数功能push_back( c )末尾追加单个 char 字符仅支持字符字符串需用 appendappend(s)末尾追加支持 C 字符串、string 对象、多个重复字符operator运算符重载可追加字符 / 字符串 /string 对象写法简单常用c_str()返回带 ‘\0’ 结尾的 const char * 指针对接 C 接口对象修改后指针失效find(c, pos)从 pos 位置往后找字符 / 字符串返回下标不传pos默认从开头 0 开始找找不到返回 nposrfind(c, pos)从 pos 位置往前找字符 / 字符串返回下标不传 pos 默认从字符串末尾开始找找不到返回 npossubstr(pos, n)从 pos 位置开始截取 n 个字符生成新字符串不传 n 则截取到末尾不修改原串nposfind/rfind/substr 共用的查找失败标记是一个极大的无符号数#includeiostream#includestringusingnamespacestd;intmain(){string s;s.push_back(h);// 尾插字符s.append(ello);// 尾插字符串s ;// 最常用字符字符串都能加sworld;coutsendl;// hello world// c_str返回C语言风格字符串用于和C语言接口兼容printf(%s\n,s.c_str());// find查找size_t poss.find(world);if(pos!string::npos)cout找到了下标posendl;// substr截取子串string subs.substr(0,5);// 从0开始截5个coutsubendl;// helloreturn0;}小技巧如果知道字符串大概多长先用reserve预留空间可以避免频繁扩容提升效率。5、非成员函数函数功能operator字符串拼接尽量少用传值返回有深拷贝开销operator输入operator输出getline()读取一整行包含空格operator、operator、operator等比较运算符按字典序比较大小getline 的重要性cin s遇到空格就停止想读取带空格的一行字符串必须用getlinestring line;getline(cin,line);// 读取一整行包含空格四、string 类的深浅拷贝模拟实现1、浅拷贝的坑如果只写构造和析构不写拷贝构造和赋值重载编译器自动生成的是浅拷贝#includestring.h#includeassert.hclassString{public:String(constchar*str){if(strnullptr){assert(false);return;}_strnewchar[strlen(str)1];// 1是方便存\0strcpy(_str,str);}~String(){delete[]_str;_strnullptr;}private:char*_strnullptr;};intmain(){Strings1(hello);Strings2(s1);// 默认拷贝构造浅拷贝return0;// 崩溃s1和s2指向同一块内存析构时重复释放}问题两个对象共享同一块堆内存析构时会重复释放程序就崩溃了。2、深拷贝比较传统的写法#includestring.h#includeassert.hclassString{public:// 构造String(constchar*str){if(strnullptr){assert(false);return;}_strnewchar[strlen(str)1];strcpy(_str,str);}// 拷贝构造深拷贝String(constStrings):_str(newchar[strlen(s._str)1]){strcpy(_str,s._str);}// 赋值重载深拷贝Stringoperator(constStrings){if(this!s){// 防止自赋值char*pStrnewchar[strlen(s._str)1];strcpy(pStr,s._str);delete[]_str;// 释放旧空间_strpStr;}return*this;}// 析构~String(){if(_str){delete[]_str;_strnullptr;}}private:char*_strnullptr;};intmain(){Strings1(hello);Strings2(s1);// 深拷贝String s3;s3s1;// 赋值重载return0;}3、深拷贝现代版写法利用拷贝构造和std::swap代码更简洁。#includeiostream#includeassert.hclassString{public:String(constchar*str){if(strnullptr){assert(false);return;}_strnewchar[strlen(str)1];strcpy(_str,str);}// 拷贝构造构造临时对象交换String(constStrings):_str(nullptr){StringstrTmp(s._str);// 构造一个临时对象std::swap(_str,strTmp._str);// 交换指针}// 赋值重载传值参数swapStringoperator(String s){// 传值自动调用拷贝构造std::swap(_str,s._str);// 交换s出作用域自动析构旧资源return*this;}~String(){if(_str){delete[]_str;_strnullptr;}}private:char*_strnullptr;};intmain(){Strings1(hello);Strings2(s1);// 深拷贝String s3;s3s1;// 赋值重载return0;}现代写法的好处编译器自动调用拷贝构造完成深拷贝不用手动开辟内存、复制字符通过std::swap仅交换底层指针代价极低原有旧数据交由临时形参接管临时变量出函数作用域自动析构释放不会内存泄漏兼容s s自赋值场景不需要额外加自赋值判断做防护代码更简洁。总结本文章主要是介绍了STL库中string类的用法和底层深浅拷贝有部分成员函数只是笼统解释一下需要实际运用中好好理解。