:string(下)——OJ实战与手写模拟实现:浅拷贝、深拷贝、三个swap)
目录0.1概述序言一、OJ实战1.1仅仅反转字母力扣 9171.2找字符串中第一个只出现一次的字符力扣 3871.3字符串里面最后一个单词的长度牛客HJ11.4验证一个字符串是否是回文力扣1251.5字符串相加力扣4151.6字符串转换整数atoi牛客1.7如果对此类题还感兴趣的话此处有几道课后作业二、模拟实现string2.1一个经典的string类问题2.2浅拷贝两个孩子公用一个玩具2.3深拷贝传统版2.4深拷贝现代版写法三个swap2.5 写时拷贝了解三、最终总结四、该系列导航方便跳跃复习0.1概述序言这里是白杨上两篇我们把string的接口吃透了传送门C入门篇十string上——认识string构造与三大遍历一条龙讲透operator[]、迭代器、auto、范围for-CSDN博客C入门篇十一string中——容量与增删改查一篇吃透所有常用接口万字详解-CSDN博客会用只是第一步本篇进入硬核模式OJ实战六道经典字符串体含上篇的两道面试题检验你的string熟练度。模拟实现面试官最爱问的“手写string类”——“浅拷贝为什么会崩深拷贝怎么写现代swap到底妙在哪?探本溯源学有所得让我们开始吧。一、OJ实战1.1仅仅反转字母力扣 917917. 仅仅反转字母 - 力扣LeetCode题目给定一个字符串S反转其中的字母非字母保留在原来的位置如“a-bC-dEf-ghIj”→“j-Ih-gfE-dCba”。思路双指针begin/end从两端向中间走遇到非字母就跳过遇到字母就交换。class Solution { public: bool isLetter(char ch) { if (ch a ch z) return true; if (ch A ch Z) return true; return false; } string reverseOnlyLetters(string S) { if (S.empty()) return S; size_t begin 0, end S.size() - 1; while (begin end) { while (begin end !isLetter(S[begin])) begin; while (begin end !isLetter(S[end])) --end; swap(S[begin], S[end]); begin; --end; } return S; } };打印结果实测reverseOnlyLetters(a-bC-dEf-ghIj) j-Ih-gfE-dCba1.2找字符串中第一个只出现一次的字符力扣 387387. 字符串中的第一个唯一字符 - 力扣LeetCode题目给定一个字符串找到它的第一个不重复的字符返回其下标不存在则返回-1。如“loveleetcode”返回2第一个不重复的是‘v’。思路哈希计数。开一个256大小的数组统计每个字符出现次数在按字符次序从前往后找第一个次数为1的字符。class Solution { public: int firstUniqChar(string s) { int count[256] { 0 }; int size s.size(); for (int i 0; i size; i) count[s[i]] 1; for (int i 0; i size; i) if (1 count[s[i]]) return i; return -1; } };打印结果实测firstUniqChar(loveleetcode) 2假如面试追问为什么第二遍从前往后扫就能保证找到的是“第一个不重复字符”原因第二遍遍历的顺序就是字符串原顺序第一个命中“计数为1”的下标自然是全局第一个。哈希链表顺序是乱的所以不能直接遍历哈希表。1.3字符串里面最后一个单词的长度牛客HJ1字符串最后一个单词的长度_牛客题霸_牛客网题目输入一行字符串单词之间可能多个空格输出最后一个单词长度。如“hello world”→5。思路读一行带空格的字符必须用getlinecin遇空格停中篇7.3刚踩过该坑最后一个单词长度总长-最后一个空格下标-1.#include iostream #include string using namespace std; int main() { string line; while (getline(cin, line)) // 不要用 cinline遇空格就结束 { size_t pos line.rfind( ); // 从后往前找最后一个空格 cout line.size() - pos - 1 endl; } return 0; }打印结果实测输入hello world→输出5输入nowcoder →输出81.4验证一个字符串是否是回文力扣125125. 验证回文串 - 力扣LeetCode题目判断字符串在“只考虑字母和数字、忽略大小写”后是不是回文。如A man, a plan, a canal: Panama → true。思路先把小写字母同一转大写双指针从两端向中间走跳过非字母数字字符比较是否相等。小贴士同一成大写还是小写都行关键是统一——否则‘A和’a‘因该相等的却判falseclass Solution { public: bool isLetterOrNumber(char ch) { return (ch 0 ch 9) || (ch a ch z) || (ch A ch Z); } bool isPalindrome(string s) { for (auto ch : s) // 注意要 auto修改原字符串 { if (ch a ch z) ch - 32; // 小写转大写 } int begin 0, end s.size() - 1; while (begin end) { while (begin end !isLetterOrNumber(s[begin])) begin; while (begin end !isLetterOrNumber(s[end])) --end; if (s[begin] ! s[end]) return false; begin; --end; } return true; } };打印结果实测isPalindrome(A man, a plan, a canal: Panama) 11.5字符串相加力扣415415. 字符串相加 - 力扣LeetCode题目给定两个字符串形式的非负整数返回他们的和字符串形式。如“456”“789”“1245”。这就是上篇1.3预告的第二到面试题现在来解决它。思路从后往前逐位相加、处理进位。结果用尾插最后reverse回来头插insert一次挪一次On^2,别用。class Solution { public: string addStrings(string num1, string num2) { int end1 num1.size() - 1; int end2 num2.size() - 1; int value1 0, value2 0, next 0; // next 是进位 string addret; while (end1 0 || end2 0) { if (end1 0) value1 num1[end1--] - 0; else value1 0; if (end2 0) value2 num2[end2--] - 0; else value2 0; int valueret value1 value2 next; if (valueret 9) { next 1; valueret - 10; } else { next 0; } addret (valueret 0); // 尾插最后再反转 } if (next 1) addret 1; reverse(addret.begin(), addret.end()); return addret; } };打印结果实测“456”“789”1245“11”“123”1341.6字符串转换整数atoi牛客把字符串转换成整数_牛客题霸_牛客网还记得上篇 1.3 预告的面试题吗这道字符串转整形数字就是它——经典中的经典边读边转重点在边界条件前导空格、正负号、非数字字符、溢出。思路三步走——跳过前导空格→处理正负号→逐位转换。溢出检测用longlong中转超过int范围直接返回边界值。class Solution { public: int StrToInt(string s) { int i 0, n s.size(); // 1. 跳过前导空格 while (i n s[i] ) i; // 2. 处理正负号 int sign 1; if (i n (s[i] || s[i] -)) { if (s[i] -) sign -1; i; } // 3. 逐位转换同时检查溢出 long long result 0; while (i n s[i] 0 s[i] 9) { result result * 10 (s[i] - 0); if (result * sign INT_MAX) return INT_MAX; if (result * sign INT_MIN) return INT_MIN; i; } if(in) { return 0; } return (int)(result * sign); } };1.7如果对此类题还感兴趣的话此处有几道课后作业翻转字符串 II区间部分翻转LeetCode 541541. 反转字符串 II - 力扣LeetCode翻转字符串 III翻转字符串中的单词LeetCode 557557. 反转字符串中的单词 III - 力扣LeetCode字符串相乘LeetCode 4343. 字符串相乘 - 力扣LeetCode字符串中的单词数牛客找出字符串中第一个只出现一次的字符_牛客题霸_牛客网如果题目有问题欢迎到评论区留言我看到会一一为您解答问题。二、模拟实现string2.1一个经典的string类问题先看代码你觉得它有问题吗// 为了和标准库区分此处使用 String class String { public: String(const char* str ) { if (nullptr str) // 传了nullptr认为程序非法 { assert(false); return; } _str new char[strlen(str) 1]; strcpy(_str, str); } ~String() { if (_str) { delete[] _str; _str nullptr; } } private: char* _str; }; void TestString() { String s1(hello bit!!!); String s2(s1); // 调用编译器合成的默认拷贝构造——浅拷贝 }先说结论有问题而且是致命的。这个string类没有显示写拷贝构造和赋值重载编译器会生成默认的——默认实现是浅拷贝两个对象共用一块空间析构同时一块空间被释放两次程序崩溃。把TestString()放进 main 里跑一下就知道s2 先析构把堆空间释放了s1 再析构又释放一次同一块空间——重复释放程序直接崩溃。面试题什么样的类必须显示写拷贝构造和赋值重载只要类中涉及资源的管理申请了堆打开了文件等拷贝构造、赋值运算符重载、析构函数三个必须显示给出。人们给它取了一个名—— Rule of Three三大法则——要写就三个一起写。2.2浅拷贝两个孩子公用一个玩具先听个故事一家两个孩子父母只买了一份玩具。一起玩就万事大吉一旦不想分享你争我夺玩具就坏了。2.1 里的 s1、s2 就是这么共用玩具的——浅拷贝位拷贝编译器把对象里的值原样拷贝过去指针成员只拷贝了地址于是两个对象共享同一份资源。一个对象销毁把资源释放了另一个还蒙在鼓里继续用——访问违规崩溃。成员只有 int、double 这类普通值浅拷贝完全没问题一旦涉及资源管理new 出来的指针等共享资源就是定时炸弹。2.3深拷贝传统版深拷贝每个对象都拥有一份独立的资源不与其他对象共享。——父母给每个孩子各自买一份玩具各玩各的。传统版老老实实开空间→拷贝内容→释放旧空间。注意赋值重载里的自赋值检查和先new后deleteclass String { public: String(const char* str ) { if (nullptr str) { assert(false); return; } _str new char[strlen(str) 1]; strcpy(_str, str); } // 拷贝构造开新空间拷贝内容初始化列表一步到位 String(const String s) : _str(new char[strlen(s._str) 1]) { strcpy(_str, s._str); } // 赋值重载先开好新空间再释放旧空间注意自赋值检查 String operator(const String s) { if (this ! s) // 防止 s s不然先把自己的空间删了后面用啥 { char* pStr new char[strlen(s._str) 1]; strcpy(pStr, s._str); delete[] _str; _str pStr; } return *this; } ~String() { if (_str) { delete[] _str; _str nullptr; } } private: char* _str; };传统版的两个细节常被追问先new后delete如果先delete旧空间、new新空间又失败了对象就残废了自赋值检查this!sss时如果先delete自己空间后面的拷贝就是往野指针里写。2.4深拷贝现代版写法三个swap现代写法拷贝构造借助“零时工swap”赋值重载参数用传值传递让编译器自动调拷贝构造生成临时对象再swap把资源换过来。代码量少一半还不容易写错。class String { public: String(const char* str ) { if (nullptr str) { assert(false); return; } _str new char[strlen(str) 1]; strcpy(_str, str); } // 拷贝构造swap(1)——临时工干活资源换过来 String(const String s) : _str(nullptr) // 必须先置空否则swap后临时对象析构释放垃圾指针 { String strTmp(s._str); // 临时工new strcpy swap(_str, strTmp._str); // 资源换过来 } // strTmp 出作用域析构释放空指针安全 // 赋值重载swap(2)——值传递参数编译器自动拷贝构造 String operator(String s) // s 是实参的拷贝拷贝构造自动调用 { swap(_str, s._str); // 交换资源 return *this; } // s 出作用域析构顺带把旧资源释放了 ~String() { if (_str) { delete[] _str; _str nullptr; } } private: char* _str; };三个swap逐一拆解见上方代码注释swap(1)拷贝构造里的swap临时对象strTmp负责newstrcpy这就是临时工然后swap(_strstrTmp._str)把资源换到自己手上。strTmp出作用域自动析构——此时它的_str是空指针析构安全。注意_str要先初始化为nullptr否则swap之后临时对象析构时释放的时垃圾指针。swap(2) 赋值重载里的 swap参数是值传递编译器调用拷贝构造帮我们生成临时对象 s。swap 交换资源后函数结束时 s 自动析构——顺带把旧资源释放了。连自赋值检查都不需要s s 也安全传统版的两个坑全避开。swap(3) 全局 swap这里的 swap 是标准库的全局 std::swap交换两个指针的值代价极小。进阶玩法是自己再写一个成员 swap 函数 全局 swap 重载面试能聊这个就是加分项等进阶篇细讲。对比现代版 vs 传统版哪个好现代版更优雅简洁跟不容易出错而且面试写现代版逼格拉满。打印结果实测VS x64两种写法都验证过运行截图仅展示现代写法打印结果modern: hello bit!!! | hello bit!!! | hello bit!!!classic: hello bit | hello bit | hello bit2.5 写时拷贝了解写时拷贝是一种拖延症浅拷贝 引用计数。拷贝时不真拷贝只在某个对象真正修改数据时才拷贝。引用计数记录资源使用者的个数构造时计数置1每增加一个对象使用该资源计数1对象销毁时计数先-1减到0自己时最后一个使用者才释放资源好处是拷贝构造几乎零成本坏处是实现复杂、计数操作有线程安全隐患。旧版 g 的 string 就是写时拷贝新版本已经放弃了。了解即可三、最终总结OJ 六道题双指针反转字母、哈希计数找唯一字符、getlinerfind 求末单词、双指针验回文、进位模拟字符串相加、三步走 atoi类管理资源却不写拷贝构造/赋值重载 → 默认浅拷贝 → 共享资源 → 重复释放 → 崩溃rule of Three拷贝构造、赋值重载、析构要写三个一起写传统版开新空间 → 拷内容 → 释放旧空间赋值重载注意自赋值检查、先 new 后 delete现代版拷贝构造临时工 swap_str 必须先置空赋值重载值传递swap 完自动析构写时拷贝 浅拷贝 引用计数“要改的时候再拷贝”四、该系列导航方便跳跃复习C入门篇十string上——认识string构造与三大遍历一条龙讲透operator[]、迭代器、auto、范围for-CSDN博客C入门篇十一string中——容量与增删改查一篇吃透所有常用接口万字详解-CSDN博客本篇。好了string 三篇到此完结下一篇进入 STL 下一个重量级容器——vector预告动态数组、迭代器失效问题。如果对你有帮助不要忘记点赞三连一波哦我是白杨我们下期见。