当前位置: 首页 > news >正文

聚焦string:C++ string 核心接口、编译器差异与自定义实现的深度剖析

[目录]

  • 1. 为什么要学string类?——解决C语言字符串的痛点
  • 2. C++11新特性:auto与范围for(string遍历必备)
  • 3. 标准库string类核心接口详解(附实战代码)
  • 4. 不同编译器下string的底层结构差异
  • 5. string类OJ实战:6道经典题解
  • 6. string类模拟实现:从浅拷贝到深拷贝(现代版+传统版)
  • 7. 自定义string类完整代码(头文件+源文件)
  • 8. 自定义string类测试用例与结果分析
  • 9. 常见易错点与避坑指南

1. 为什么要学string类?——解决C语言字符串的痛点

很多初学者会问:“C语言已经有字符串了,为什么还要学C++的string类?” 答案很简单——C语言的字符串太“难用”了,而string类把这些“难点”都封装成了“傻瓜操作”。

1.1 C语言字符串的3大痛点

痛点具体问题示例
内存需手动管理要自己用malloc/free分配释放空间,漏写free会内存泄漏,多写会double freechar* str = (char*)malloc(10);后,忘记free(str)导致内存泄漏
操作与数据分离字符串是char*,操作要靠strlen/strcpy等库函数,不符合面向对象思想复制字符串要写strcpy(dst, src),还要手动判断src长度,麻烦且容易错
越界风险高没有边界检查,strcpy时目标空间不足会越界访问,导致程序崩溃char dst[5]; strcpy(dst, "hello world");——直接越界写坏内存

1.2 学string类的2个实际需求

  • 面试/OJ刚需:LeetCode、牛客网等平台的字符串题目,90%以上用string类实现更简单;
  • 工作效率:实际开发中,用string类能少写80%的内存管理代码,减少bug率。

2. C++11新特性:auto与范围for(string遍历必备)

在学string类之前,必须先掌握C++11的两个“神器”——auto和范围for,这两个特性能让string的遍历和操作效率翻倍。

2.1 auto:让编译器帮你“写类型”

auto的核心作用是自动推导变量类型,不用手动写复杂的类型名(比如string::iterator)。

auto的5个核心规则(附代码解析)
#include<iostream>
#include<string>
#include<map>
using namespace std;int func1() { return 10; }// 错误:auto不能作为函数参数
// void func2(auto a) {} // 谨慎使用:auto可以作为返回值(需编译器支持C++14及以上)
auto func3() { return 3; }int main() {// 1. auto必须初始化(编译器要根据初始值推导类型)int a = 10;auto b = a;          // b推导为intauto c = 'a';        // c推导为charauto d = func1();    // d推导为int// auto e; // 错误:没有初始值,编译器无法推导类型// 2. 声明指针:auto和auto*无区别int x = 10;auto y = &x;         // y推导为int*auto* z = &x;        // z推导为int*(和y一样)// 3. 声明引用:必须加&auto& m = x;         // m是int&(引用x),修改m会改x// auto n = x;      // n是int(拷贝x),修改n不影响x// 4. 同一行声明多个变量:类型必须相同auto aa = 1, bb = 2; // 正确:都是int// auto cc = 3, dd = 4.0; // 错误:cc是int,dd是double,类型不同// 5. auto不能直接声明数组// auto array[] = {4,5,6}; // 错误:数组类型不能用auto推导// 实战场景:遍历map时,auto简化迭代器类型map<string, string> dict = {{"apple", "苹果"}, {"orange", "橙子"}};// 不用auto要写:map<string, string>::iterator it = dict.begin();auto it = dict.begin(); while (it != dict.end()) {cout << it->first << ":" << it->second << endl;++it;}return 0;
}
运行结果
apple:苹果
orange:橙子

2.2 范围for:遍历容器的“懒人语法”

范围for的核心是自动遍历容器/数组,不用手动写索引或迭代器,格式为:

for (迭代变量 : 容器/数组) {// 循环体
}
范围for实战(对比C++98遍历方式)
#include<iostream>
#include<string>
using namespace std;int main() {int array[] = {1,2,3,4,5};int n = sizeof(array)/sizeof(array[0]); // C++98要手动算长度// 1. C++98遍历数组(麻烦)for (int i = 0; i < n; ++i) {array[i] *= 2; // 数组元素翻倍}for (int i = 0; i < n; ++i) {cout << array[i] << " "; // 输出:2 4 6 8 10}cout << endl;// 2. C++11范围for遍历数组(简洁)for (auto& e : array) { // 加&:修改原数组;不加&:只读e *= 2; // 数组元素再翻倍}for (auto e : array) {cout << e << " "; // 输出:4 8 12 16 20}cout << endl;// 3. 范围for遍历string(最常用场景)string str("hello");for (auto ch : str) {cout << ch << " "; // 输出:h e l l o}return 0;
}
关键注意点
  • 遍历数组时,范围for会自动识别数组长度,不用手动计算;
  • 遍历string时,若要修改字符,迭代变量必须加&(引用),否则只是拷贝;
  • 范围for的底层是迭代器,只要容器支持begin()end(),就能用范围for(string、vector、map等都支持)。

3. 标准库string类核心接口详解(附实战代码)

标准库的string类有上百个接口,但实际开发中常用的只有20个左右。下面按“构造→容量→访问→修改”的顺序,讲解最核心的接口。

3.1 构造函数:创建string对象

构造函数功能代码示例
string()创建空字符串string s1;(s1是"")
string(const char* s)用C字符串构造string s2("hello");(s2是"hello")
string(size_t n, char c)创建含n个c的字符串string s3(5, 'a');(s3是"aaaaa")
string(const string& s)拷贝构造string s4(s2);(s4是"hello")
代码演示
#include<iostream>
#include<string>
using namespace std;int main() {string s1;          // 空字符串string s2("hello"); // 用C字符串构造string s3(5, 'a');  // 5个'a'string s4(s2);      // 拷贝s2cout << "s1: " << s1 << "(长度:" << s1.size() << ")" << endl;cout << "s2: " << s2 << "(长度:" << s2.size() << ")" << endl;cout << "s3: " << s3 << "(长度:" << s3.size() << ")" << endl;cout << "s4: " << s4 << "(长度:" << s4.size() << ")" << endl;return 0;
}
运行结果
s1: (长度:0)
s2: hello(长度:5)
s3: aaaaa(长度:5)
s4: hello(长度:5)

3.2 容量操作:管理字符串的空间

容量相关接口是string的“性能关键”,用好reserve能避免频繁扩容,提升效率。

接口功能关键注意点
size()返回有效字符长度(不含’\0’)length()功能一样,推荐用size()(和其他容器统一)
capacity()返回总空间大小(不含’\0’)空间是预分配的,可能比size()
empty()判断是否为空返回bool,空则true,否则false
clear()清空有效字符只清字符,不释放空间(capacity()不变)
reserve(size_t n)预留n个字符的空间1. n比当前capacity()大才扩容;2. 不改变size()
resize(size_t n, char c)调整有效字符数为n1. n>当前size():用c填充;2. n<当前size():截断;3. 可能扩容
代码演示(含性能对比)
#include<iostream>
#include<string>
using namespace std;int main() {// 1. size()、capacity()、empty()string s("hello");cout << "s: " << s << endl;cout << "size(): " << s.size() << endl;       // 5cout << "capacity(): " << s.capacity() << endl; // 5(VS下可能是15,看编译器)cout << "empty(): " << (s.empty() ? "是" : "否") << endl; // 否// 2. clear():清空字符,保留空间s.clear();cout << "clear后:" << endl;cout << "size(): " << s.size() << endl;       // 0cout << "capacity(): " << s.capacity() << endl; // 5(空间还在)// 3. reserve():预留空间(提升性能)string s1, s2;// 不预留空间:每次扩容都会申请新空间、拷贝数据for (int i = 0; i < 10000; ++i) {s1 += 'a';}// 预留空间:只申请一次空间,效率更高s2.reserve(10000);for (int i = 0; i < 10000; ++i) {s2 += 'a';}cout << "s1 capacity: " << s1.capacity() << endl; // 可能是16383(自动扩容)cout << "s2 capacity: " << s2.capacity() << endl; // 10000(手动预留)// 4. resize():调整有效字符数string s3("hello");s3.resize(8, 'x'); // 扩容到8个字符,用'x'填充cout << "resize(8, 'x'): " << s3 << endl; // helloxxxs3.resize(3);      // 截断到3个字符cout << "resize(3): " << s3 << endl;      // helreturn 0;
}
关键性能结论
  • 频繁给string追加字符时,先调用reserve(n)预留足够空间,能避免多次扩容(每次扩容都会拷贝旧数据,耗时);
  • clear()不会释放空间,如果要释放空间,需要用“ swap技巧”:string().swap(s);(创建空string,交换后s的空间被释放)。

3.3 访问与遍历:获取字符串的字符

string的访问方式有3种:[]运算符、迭代器、范围for(推荐)。

接口功能代码示例
operator[](size_t pos)返回pos位置的字符s[2](获取第3个字符,从0开始)
begin()/end()获取迭代器(开始/结束)auto it = s.begin();(it指向第一个字符)
范围for遍历所有字符for (auto ch : s) { ... }
代码演示(3种遍历方式对比)
#include<iostream>
#include<string>
using namespace std;int main() {string s("hello world");// 1. 用[]遍历(最直观)cout << "用[]遍历:";for (size_t i = 0; i < s.size(); ++i) {cout << s[i] << " "; // h e l l o   w o r l d}cout << endl;// 2. 用迭代器遍历(最灵活,支持反向遍历)cout << "用迭代器遍历:";string::iterator it = s.begin();while (it != s.end()) {cout << *it << " "; // h e l l o   w o r l d++it;}cout << endl;// 反向遍历(rbegin()/rend())cout << "反向遍历:";string::reverse_iterator rit = s.rbegin();while (rit != s.rend()) {cout << *rit << " "; // d l r o w   o l l e h++rit;}cout << endl;// 3. 用范围for遍历(最简洁)cout << "用范围for遍历:";for (auto ch : s) {cout << ch << " "; // h e l l o   w o r l d}cout << endl;// 注意:const string只能用const迭代器或const []const string cs("const string");// cs[0] = 'C'; // 错误:const对象不能修改const char& c = cs[0]; // 正确:const引用,只读return 0;
}

3.4 修改操作:增删改查字符串

修改操作是string最常用的功能,重点掌握+=findsubstr

接口功能代码示例
push_back(char c)尾插一个字符s.push_back('!');
operator+=尾追加字符/字符串s += '!';s += "world";
find(char c, pos)从pos开始找c,返回位置(没找到返回nposs.find('o', 0);(找第一个’o’)
rfind(char c, pos)从pos开始反向找cs.rfind('o');(找最后一个’o’)
substr(pos, len)从pos开始截取len个字符,返回新strings.substr(2, 3);(从第3个字符截3个)
erase(pos, len)从pos开始删len个字符s.erase(2, 3);
代码演示(实战场景)
#include<iostream>
#include<string>
using namespace std;int main() {string s("hello");// 1. 尾插:push_back vs +=(推荐+=)s.push_back(' ');  // 尾插空格:"hello "s += "world!";     // 尾追加字符串:"hello world!"cout << "尾插后:" << s << endl; // hello world!// 2. 查找:find(找子串位置)size_t pos = s.find('o'); // 找第一个'o'if (pos != string::npos) { // 注意:判断是否找到(npos是-1,代表没找到)cout << "第一个'o'的位置:" << pos << endl; // 4}pos = s.rfind('o'); // 找最后一个'o'if (pos != string::npos) {cout << "最后一个'o'的位置:" << pos << endl; // 7}// 3. 截取子串:substr(实战:提取文件后缀)string file("test.cpp.zip");pos = file.rfind('.'); // 找最后一个'.'(区分多后缀)string suffix = file.substr(pos); // 从'.'开始截取到末尾cout << "文件后缀:" << suffix << endl; // .zip// 4. 删除:erase(删除指定部分)string s2("hello world!");s2.erase(5, 1); // 从位置5删1个字符(删除空格)cout << "删除后:" << s2 << endl; // helloworld!// 5. 清空:clear(也可以用s = "")s2.clear();cout << "清空后size:" << s2.size() << endl; // 0return 0;
}
关键注意点
  • npos是string的静态常量,值为-1,用来表示“未找到”或“到末尾”;
  • +=是最灵活的尾追加方式,支持字符、C字符串、string对象;
  • substrlen参数可选,默认截取到末尾(如substr(5)就是从5截到最后)。

4. 不同编译器下string的底层结构差异

很多初学者会疑惑:“为什么同样的string代码,在VS和Linux下运行结果不一样?” 因为不同编译器的string底层实现不同。

4.1 VS(Windows)下的string结构(32位)

VS的string采用“小字符串优化(SSO)”,结构占28字节,分为两种情况:

  • 当字符串长度≤15时:用内部固定数组存储(不需要堆空间,效率高);
  • 当字符串长度≥16时:从堆上申请空间存储。
结构拆解(32位)
部分大小功能
联合体(_Bx)16字节存储字符串:短字符串用数组,长字符串用指针
_Size4字节有效字符长度
_Capacity4字节总空间大小(不含’\0’)
其他(指针)4字节用于内存管理(如调试信息)
总计28字节-

4.2 GCC(Linux)下的string结构(32位)

GCC的string采用“写时拷贝(Copy-On-Write)”,结构占4字节,只有一个指针:

  • 指针指向堆上的一块空间,这块空间存储3个信息:引用计数、有效长度、总容量、字符串数据;
  • 拷贝时只复制指针(浅拷贝),直到修改时才真正拷贝数据(深拷贝)。
结构拆解(32位)
部分大小功能
指针(_M_p)4字节指向堆空间
堆空间内容-1. 引用计数(_M_refcount);2. 长度(_M_length);3. 容量(_M_capacity);4. 字符串数据

4.3 两种实现的对比

特性VS(SSO)GCC(COW)
内存占用28字节(固定)4字节(只存指针)
短字符串效率高(不用堆空间)低(需要堆空间)
拷贝效率低(每次都深拷贝)高(写时才深拷贝)
线程安全较安全(无共享)不安全(引用计数可能竞争)
代码验证(查看地址)
#include<iostream>
#include<string>
using namespace std;int main() {string s1("hello");string s2(s1); // 拷贝构造// 输出c_str()的地址(判断是否共享空间)cout << "s1的地址:" << (void*)s1.c_str() << endl;cout << "s2的地址:" << (void*)s2.c_str() << endl;// 修改s2(触发写时拷贝)s2 += '!';cout << "修改s2后:" << endl;cout << "s1的地址:" << (void*)s1.c_str() << endl;cout << "s2的地址:" << (void*)s2.c_str() << endl;return 0;
}
运行结果对比
  • VS下:修改前s1和s2地址不同(每次拷贝都深拷贝);
  • GCC下:修改前s1和s2地址相同(浅拷贝),修改后地址不同(深拷贝)。

5. string类OJ实战:6道经典题解

学完接口后,必须通过OJ题巩固。下面6道题是面试高频题,覆盖string的核心用法。

5.1 题目1:仅仅反转字母(LeetCode 917)

题目描述:给定一个字符串,反转其中的字母,非字母字符保持位置不变。
示例:输入 "ab-cd" → 输出 "dc-ba"

题解代码
#include<iostream>
#include<string>
using namespace std;class Solution {
public:// 判断是否是字母bool isLetter(char ch) {return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');}string reverseOnlyLetters(string S) {if (S.empty()) return S; // 空字符串直接返回size_t left = 0;         // 左指针(从左往右找字母)size_t right = S.size() - 1; // 右指针(从右往左找字母)while (left < right) {// 左指针找字母(跳过非字母)while (left < right && !isLetter(S[left])) {left++;}// 右指针找字母(跳过非字母)while (left < right && !isLetter(S[right])) {right--;}// 交换两个字母swap(S[left], S[right]);left++;right--;}return S;}
};int main() {Solution sol;string s = "ab-cd";cout << sol.reverseOnlyLetters(s) << endl; // 输出 dc-bareturn 0;
}
核心思路
  • 用双指针法:左指针从左找字母,右指针从右找字母,找到后交换;
  • 非字母字符直接跳过,不影响位置。

5.2 题目2:找第一个只出现一次的字符(LeetCode 387)

题目描述:给定一个字符串,找到第一个只出现一次的字符,返回其索引;若没有,返回-1。
示例:输入 "leetcode" → 输出 0('l’是第一个只出现一次的字符)。

题解代码
#include<iostream>
#include<string>
using namespace std;class Solution {
public:int firstUniqChar(string s) {// 用数组统计字符出现次数(ASCII码共256个)int count[256] = {0}; // 初始化为0int n = s.size();// 第一步:遍历字符串,统计每个字符的出现次数for (int i = 0; i < n; ++i) {count[s[i]]++; // s[i]是字符,自动转ASCII码作为索引}// 第二步:再次遍历字符串,找第一个出现次数为1的字符for (int i = 0; i < n; ++i) {if (count[s[i]] == 1) {return i; // 返回索引}}return -1; // 没有只出现一次的字符}
};int main() {Solution sol;string s = "leetcode";cout << sol.firstUniqChar(s) << endl; // 输出 0return 0;
}
核心思路
  • 用数组统计次数:字符的ASCII码作为数组索引,效率比哈希表高;
  • 两次遍历:第一次统计,第二次找结果,保证“第一个”的顺序。

5.3 题目3:最后一个单词的长度(LeetCode 58)

题目描述:给定一个字符串,返回最后一个单词的长度(单词由非空格字符组成)。
示例:输入 "Hello World" → 输出 5("World"的长度)。

题解代码
#include<iostream>
#include<string>
using namespace std;int main() {string s;// 注意:不能用cin >> s(cin遇到空格就停止,会漏掉后面的单词)getline(cin, s); // 读取整行字符串,包括空格size_t pos = s.rfind(' '); // 找最后一个空格的位置// 情况1:没有空格(整个字符串是一个单词)if (pos == string::npos) {cout << s.size() << endl;}// 情况2:有空格(最后一个单词的长度 = 总长度 - 最后一个空格位置 - 1)else {cout << s.size() - pos - 1 << endl;}return 0;
}
关键注意点
  • getline(cin, s)读取整行,避免cin跳过空格的问题;
  • rfind(' ')找最后一个空格,简化计算。

5.4 题目4:验证回文串(LeetCode 125)

题目描述:给定一个字符串,验证它是否是回文串(只考虑字母和数字,不区分大小写)。
示例:输入 "A man, a plan, a canal: Panama" → 输出 true

题解代码
#include<iostream>
#include<string>
using namespace std;class Solution {
public:// 判断是否是字母或数字bool isLetterOrNum(char ch) {return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');}bool isPalindrome(string s) {// 第一步:将所有小写字母转大写(或反之,统一大小写)for (auto& ch : s) {if (ch >= 'a' && ch <= 'z') {ch -= 32; // 小写转大写('a'-'A'=32)}}// 第二步:双指针法验证回文int left = 0;int right = s.size() - 1;while (left < right) {// 左指针找字母/数字(跳过其他字符)while (left < right && !isLetterOrNum(s[left])) {left++;}// 右指针找字母/数字(跳过其他字符)while (left < right && !isLetterOrNum(s[right])) {right--;}// 比较两个字符是否相等if (s[left] != s[right]) {return false;}left++;right--;}return true;}
};int main() {Solution sol;string s = "A man, a plan, a canal: Panama";cout << (sol.isPalindrome(s) ? "true" : "false") << endl; // truereturn 0;
}

5.5 题目5:字符串相加(LeetCode 415)

题目描述:给定两个非负整数(字符串形式),返回它们的和(字符串形式)。
示例:输入 "123" + "456" → 输出 "579"

题解代码
#include<iostream>
#include<string>
#include<algorithm> // 用reverse函数
using namespace std;class Solution {
public:string addStrings(string num1, string num2) {int i = num1.size() - 1; // num1的尾指针(从右往左加)int j = num2.size() - 1; // num2的尾指针int carry = 0;           // 进位(0或1)string res;              // 结果字符串// 循环条件:两个字符串没遍历完,或有进位while (i >= 0 || j >= 0 || carry > 0) {// 取当前位的数字(没数字了就取0)int digit1 = (i >= 0) ? (num1[i] - '0') : 0;int digit2 = (j >= 0) ? (num2[j] - '0') : 0;// 计算当前位的和 + 进位int sum = digit1 + digit2 + carry;carry = sum / 10; // 更新进位(sum>=10则为1,否则为0)int current = sum % 10; // 当前位的结果// 尾插当前位(最后要反转)res += (current + '0');// 移动指针i--;j--;}// 反转结果(因为尾插的顺序是从低位到高位)reverse(res.begin(), res.end());return res;}
};int main() {Solution sol;string num1 = "123", num2 = "456";cout << sol.addStrings(num1, num2) << endl; // 579return 0;
}
核心思路
  • 模拟手工加法:从右往左加,处理进位;
  • 尾插结果后反转:避免头插(头插效率低,尾插+反转效率高)。

5.6 题目6:字符串相乘(LeetCode 43)

题目描述:给定两个非负整数(字符串形式),返回它们的积(字符串形式)。
示例:输入 "123" * "456" → 输出 "56088"

题解代码
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;class Solution {
public:string multiply(string num1, string num2) {// 特殊情况:有一个数是0,结果就是0if (num1 == "0" || num2 == "0") {return "0";}int m = num1.size();int n = num2.size();// 结果的最大长度是m+n(如999*999=998001,3+3=6位)vector<int> res(m + n, 0);// 从右往左遍历两个数,计算每一位的乘积for (int i = m - 1; i >= 0; --i) {for (int j = n - 1; j >= 0; --j) {// 当前位的乘积(num1[i]和num2[j]都是字符,转数字)int product = (num1[i] - '0') * (num2[j] - '0');// 乘积在res中的位置:i+j+1是当前位,i+j是进位位int sum = product + res[i + j + 1];res[i + j + 1] = sum % 10; // 当前位的结果res[i + j] += sum / 10;    // 进位(累加到前一位)}}// 把res数组转成字符串(跳过前面的0)string result;int start = 0;// 找到第一个非0的位置(跳过前缀0)while (start < res.size() && res[start] == 0) {start++;}// 把剩余的数字转成字符for (int i = start; i < res.size(); ++i) {result += (res[i] + '0');}return result;}
};int main() {Solution sol;string num1 = "123", num2 = "456";cout << sol.multiply(num1, num2) << endl; // 56088return 0;
}
核心思路
  • 用数组存储结果:避免字符串频繁修改,效率更高;
  • 乘积的位置规律:num1[i] * num2[j]的结果存放在res[i+j+1](当前位)和res[i+j](进位位)。

6. string类模拟实现:从浅拷贝到深拷贝(现代版+传统版)

面试中经常会让你“手写string类的构造、拷贝构造、赋值运算符重载、析构函数”,这部分是重点,必须掌握。

6.1 浅拷贝的问题:同一块空间被释放两次

如果不自己写拷贝构造和赋值运算符重载,编译器会生成默认的“浅拷贝”版本——只拷贝指针,不拷贝数据。

浅拷贝的代码(错误示例)
#include<iostream>
#include<string>
#include<cassert>
using namespace std;class String {
public:// 构造函数String(const char* str = "") {if (str == nullptr) {assert(false); // 防止传nullptrreturn;}_str = new char[strlen(str) + 1]; // 申请空间(含'\0')strcpy(_str, str);}// 析构函数(释放空间)~String() {if (_str) {delete[] _str; // 释放堆空间_str = nullptr;}}private:char* _str; // 指向堆空间的指针
};int main() {String s1("hello");String s2(s1); // 浅拷贝:s1._str和s2._str指向同一块空间// 析构时:先析构s2,释放空间;再析构s1,释放已经释放的空间→崩溃!return 0;
}
问题分析
  • 浅拷贝导致s1s2_str指向同一块堆空间;
  • 程序结束时,析构函数会被调用两次,同一块空间被释放两次,导致“double free”错误,程序崩溃。

6.2 深拷贝:每个对象拥有独立的空间

深拷贝的核心是——拷贝时不仅拷贝指针,还要拷贝指针指向的数据,让每个对象拥有独立的堆空间。

6.2.1 传统版深拷贝(手动申请空间+拷贝数据)
#include<iostream>
#include<string>
#include<cassert>
using namespace std;class String {
public:// 1. 构造函数String(const char* str = "") {if (str == nullptr) {assert(false);return;}// 申请空间(长度+1,存'\0')_str = new char[strlen(str) + 1];strcpy(_str, str);}// 2. 拷贝构造函数(深拷贝)String(const String& s) {// 为当前对象申请新空间_str = new char[strlen(s._str) + 1];// 拷贝数据strcpy(_str, s._str);}// 3. 赋值运算符重载(深拷贝)String& operator=(const String& s) {// 防止自赋值(s = s)if (this != &s) {// 第一步:释放当前对象的旧空间delete[] _str;// 第二步:申请新空间,拷贝数据_str = new char[strlen(s._str) + 1];strcpy(_str, s._str);}return *this; // 支持链式赋值(s1 = s2 = s3)}// 4. 析构函数~String() {if (_str) {delete[] _str;_str = nullptr;}}// 辅助函数:获取字符串(用于测试)const char* c_str() const {return _str;}private:char* _str;
};// 测试
int main() {String s1("hello");String s2(s1); // 深拷贝:s2有独立空间String s3;s3 = s1;       // 深拷贝:s3有独立空间cout << "s1: " << s1.c_str() << endl; // hellocout << "s2: " << s2.c_str() << endl; // hellocout << "s3: " << s3.c_str() << endl; // hello// 修改s2,不影响s1String s4("world");s2 = s4;cout << "修改后s1: " << s1.c_str() << endl; // hello(不变)cout << "修改后s2: " << s2.c_str() << endl; // worldreturn 0;
}
6.2.2 现代版深拷贝(利用构造函数+swap)

传统版的赋值运算符重载需要“释放旧空间→申请新空间→拷贝数据”,步骤多且容易出错。现代版利用“拷贝构造+swap”简化代码,更优雅。

#include<iostream>
#include<string>
#include<cassert>
using namespace std;class String {
public:// 1. 构造函数String(const char* str = "") {if (str == nullptr) {assert(false);return;}_str = new char[strlen(str) + 1];strcpy(_str, str);}// 2. 拷贝构造函数(现代版)String(const String& s): _str(nullptr) { // 先初始化_str为nullptr,避免swap后tmp析构出错String tmp(s._str); // 用s._str构造tmp(调用构造函数,申请新空间)swap(_str, tmp._str); // 交换当前对象和tmp的_str}// 3. 赋值运算符重载(现代版1:参数传值,自动拷贝)String& operator=(String s) { // s是传值,会调用拷贝构造,生成临时对象swap(_str, s._str); // 交换当前对象和s的_strreturn *this;}/* // 现代版2:参数传引用(防止自赋值)String& operator=(const String& s) {if (this != &s) {String tmp(s); // 拷贝构造tmpswap(_str, tmp._str);}return *this;}*/// 4. 析构函数~String() {if (_str) {delete[] _str;_str = nullptr;}}// 辅助函数const char* c_str() const {return _str;}private:char* _str;
};// 测试
int main() {String s1("hello");String s2(s1);String s3 = s1;cout << "s1: " << s1.c_str() << endl; // hellocout << "s2: " << s2.c_str() << endl; // hellocout << "s3: " << s3.c_str() << endl; // helloreturn 0;
}
现代版的核心思路
  • 利用拷贝构造函数生成临时对象tmptmp有独立空间);
  • 通过swap交换当前对象和tmp_str,让当前对象拥有tmp的空间;
  • 临时对象tmp析构时,会释放当前对象原来的旧空间,避免内存泄漏。

7. 自定义string类完整代码(头文件+源文件)

下面是完整的自定义string类代码,包含所有核心接口,可直接编译使用。

7.1 头文件(string.h)

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<cstring>
#include<cassert>
using namespace std;namespace syj { // 自定义命名空间,避免和标准库冲突class string {public:// 迭代器(支持范围for)typedef char* iterator;typedef const char* const_iterator;iterator begin() { return _str; }iterator end() { return _str + _size; }const_iterator begin() const { return _str; }const_iterator end() const { return _str + _size; }// 1. 构造函数string(const char* str = "") {// 计算字符串长度(str为空时strlen返回0)_size = strlen(str);// 容量 = 长度(不含'\0')_capacity = _size;// 申请空间(容量+1,存'\0')_str = new char[_capacity + 1];// 拷贝数据strcpy(_str, str);}// 2. 拷贝构造函数(现代版)string(const string& s): _str(nullptr), _size(0), _capacity(0) {string tmp(s._str); // 构造临时对象swap(tmp); // 交换当前对象和tmp的成员}// 3. 赋值运算符重载(现代版)string& operator=(string tmp) {swap(tmp);return *this;}// 4. 析构函数~string() {if (_str) {delete[] _str;_str = nullptr;_size = 0;_capacity = 0;}}// 辅助函数:交换两个string对象void swap(string& tmp) {std::swap(_str, tmp._str);std::swap(_size, tmp._size);std::swap(_capacity, tmp._capacity);}// 5. 容量相关接口size_t size() const { return _size; }size_t capacity() const { return _capacity; }bool empty() const { return _size == 0; }// 清空有效字符(不释放空间)void clear() {_str[0] = '\0';_size = 0;}// 预留空间(只扩容,不缩容)void reserve(size_t n) {if (n > _capacity) {char* tmp = new char[n + 1]; // 申请新空间strcpy(tmp, _str); // 拷贝旧数据delete[] _str; // 释放旧空间_str = tmp; // 指向新空间_capacity = n; // 更新容量}}// 调整有效字符数void resize(size_t n, char c = '\0') {if (n > _size) {// 扩容(如果需要)if (n > _capacity) {reserve(n);}// 填充字符cmemset(_str + _size, c, n - _size);}// 更新size,添加'\0'_size = n;_str[_size] = '\0';}// 6. 访问接口char& operator[](size_t pos) {assert(pos < _size); // 越界检查return _str[pos];}const char& operator[](size_t pos) const {assert(pos < _size);return _str[pos];}const char* c_str() const {return _str;}// 7. 修改接口// 尾插一个字符void push_back(char c) {// 扩容(满了就扩2倍,空则扩4)if (_size == _capacity) {reserve(_capacity == 0 ? 4 : _capacity * 2);}// 尾插字符,更新size,加'\0'_str[_size] = c;_size++;_str[_size] = '\0';}// 尾追加字符串void append(const char* str) {size_t len = strlen(str);if (_size + len > _capacity) {// 扩容到足够的大小reserve(_size + len);}// 拷贝字符串strcpy(_str + _size, str);_size += len;}// 重载+=(字符)string& operator+=(char c) {push_back(c);return *this;}// 重载+=(字符串)string& operator+=(const char* str) {append(str);return *this;}// 重载+=(string对象)string& operator+=(const string& s) {append(s._str);return *this;}// 插入字符(pos位置)void insert(size_t pos, char c) {assert(pos <= _size); // pos可以等于size(尾插)// 扩容if (_size == _capacity) {reserve(_capacity == 0 ? 4 : _capacity * 2);}// 挪动数据(从后往前挪)size_t end = _size + 1;while (end > pos) {_str[end] = _str[end - 1];end--;}// 插入字符,更新size_str[pos] = c;_size++;}// 插入字符串(pos位置)void insert(size_t pos, const char* str) {assert(pos <= _size);size_t len = strlen(str);if (len == 0) {return; // 插入空字符串,直接返回}// 扩容if (_size + len > _capacity) {reserve(_size + len);}// 挪动数据size_t end = _size + len;while (end > pos + len - 1) {_str[end] = _str[end - len];end--;}// 插入字符串strncpy(_str + pos, str, len);_size += len;}// 删除字符(从pos开始删len个,默认删到末尾)void erase(size_t pos, size_t len = npos) {assert(pos < _size);// 计算实际要删除的长度(如果超过剩余字符,删到末尾)size_t actual_len = len;if (len == npos || pos + len > _size) {actual_len = _size - pos;}// 挪动数据覆盖要删除的部分size_t start = pos + actual_len;while (start <= _size) {_str[start - actual_len] = _str[start];start++;}// 更新size_size -= actual_len;}// 查找字符(从pos开始,默认从0开始)size_t find(char c, size_t pos = 0) const {assert(pos < _size);for (size_t i = pos; i < _size; i++) {if (_str[i] == c) {return i;}}return npos; // 没找到返回npos}// 查找字符串(从pos开始)size_t find(const char* str, size_t pos = 0) const {assert(pos < _size);// 用库函数strstr查找子串const char* ptr = strstr(_str + pos, str);if (ptr == nullptr) {return npos;}// 返回相对于_str的位置return ptr - _str;}// 截取子串(从pos开始,取len个,默认取到末尾)string substr(size_t pos = 0, size_t len = npos) const {assert(pos < _size);// 计算实际截取长度size_t actual_len = len;if (len == npos || pos + len > _size) {actual_len = _size - pos;}// 构造结果字符串string sub;sub.reserve(actual_len);for (size_t i = 0; i < actual_len; i++) {sub += _str[pos + i];}return sub;}private:char* _str = nullptr;   // 指向字符串的指针size_t _size = 0;       // 有效字符长度(不含'\0')size_t _capacity = 0;   // 总容量(不含'\0')static const size_t npos; // 静态常量,代表“未找到”或“到末尾”};// 初始化静态常量npos(值为-1,size_t是无符号类型,-1就是最大值)const size_t string::npos = -1;// 8. 非成员函数:比较运算符重载bool operator<(const string& s1, const string& s2) {return strcmp(s1.c_str(), s2.c_str()) < 0;}bool operator==(const string& s1, const string& s2) {return strcmp(s1.c_str(), s2.c_str()) == 0;}bool operator!=(const string& s1, const string& s2) {return !(s1 == s2);}bool operator<=(const string& s1, const string& s2) {return s1 < s2 || s1 == s2;}bool operator>(const string& s1, const string& s2) {return !(s1 <= s2);}bool operator>=(const string& s1, const string& s2) {return !(s1 < s2);}// 9. 非成员函数:输入输出运算符重载ostream& operator<<(ostream& out, const string& s) {// 遍历输出每个字符(避免'\0'提前结束)for (auto ch : s) {out << ch;}return out;}istream& operator>>(istream& in, string& s) {s.clear(); // 先清空schar buff[256] = {0}; // 缓冲区,避免频繁扩容int i = 0;char ch;// 读取字符,跳过空格和换行(cin默认跳过空白符,但这里手动处理更灵活)ch = in.get();while (ch != ' ' && ch != '\n' && ch != EOF) {buff[i++] = ch;// 缓冲区满了,追加到s中if (i == 255) {buff[255] = '\0';s += buff;i = 0;}ch = in.get();}// 追加剩余的字符if (i > 0) {buff[i] = '\0';s += buff;}return in;}// 10. 非成员函数:swap(调用成员swap)void swap(string& s1, string& s2) {s1.swap(s2);}
}

7.2 源文件(string.cpp)

#include"string.h"// 这里主要实现头文件中声明的成员函数(如果头文件中没有内联定义)
// 由于头文件中已经内联了短小的函数,这里只需要实现较长的函数(如reserve、insert等)
// 注意:如果头文件中已经实现了函数,这里不需要重复实现namespace syj {// 实现reserve(如果头文件中没有内联)void string::reserve(size_t n) {if (n > _capacity) {char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}// 实现insert(字符版本)void string::insert(size_t pos, char c) {assert(pos <= _size);if (_size == _capacity) {reserve(_capacity == 0 ? 4 : _capacity * 2);}size_t end = _size + 1;while (end > pos) {_str[end] = _str[end - 1];end--;}_str[pos] = c;_size++;}// 实现insert(字符串版本)void string::insert(size_t pos, const char* str) {assert(pos <= _size);size_t len = strlen(str);if (len == 0) {return;}if (_size + len > _capacity) {reserve(_size + len);}size_t end = _size + len;while (end > pos + len - 1) {_str[end] = _str[end - len];end--;}strncpy(_str + pos, str, len);_size += len;}// 实现erasevoid string::erase(size_t pos, size_t len) {assert(pos < _size);size_t actual_len = len;if (len == npos || pos + len > _size) {actual_len = _size - pos;}size_t start = pos + actual_len;while (start <= _size) {_str[start - actual_len] = _str[start];start++;}_size -= actual_len;}// 实现find(字符版本)size_t string::find(char c, size_t pos) const {assert(pos < _size);for (size_t i = pos; i < _size; i++) {if (_str[i] == c) {return i;}}return npos;}// 实现find(字符串版本)size_t string::find(const char* str, size_t pos) const {assert(pos < _size);const char* ptr = strstr(_str + pos, str);if (ptr == nullptr) {return npos;}return ptr - _str;}// 实现substrstring string::substr(size_t pos, size_t len) const {assert(pos < _size);size_t actual_len = len;if (len == npos || pos + len > _size) {actual_len = _size - pos;}string sub;sub.reserve(actual_len);for (size_t i = 0; i < actual_len; i++) {sub += _str[pos + i];}return sub;}// 实现输入运算符重载istream& operator>>(istream& in, string& s) {s.clear();char buff[256] = {0};int i = 0;char ch;ch = in.get();while (ch != ' ' && ch != '\n' && ch != EOF) {buff[i++] = ch;if (i == 255) {buff[255] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0) {buff[i] = '\0';s += buff;}return in;}
}

8. 自定义string类测试用例与结果分析

下面是针对自定义string类的测试代码,覆盖构造、容量、访问、修改等所有核心功能。

8.1 测试代码(TestString.cpp)

#include"string.h"
using namespace syj;// 测试1:构造函数和基本访问
void test_string1() {cout << "=== 测试1:构造函数和基本访问 ===" << endl;string s1;                  // 空字符串string s2("hello world");   // C字符串构造string s3(5, 'a');          // n个字符构造string s4(s2);              // 拷贝构造string s5 = s3;             // 拷贝构造(赋值形式)cout << "s1: " << s1 << "(size: " << s1.size() << ", capacity: " << s1.capacity() << ")" << endl;cout << "s2: " << s2 << "(size: " << s2.size() << ", capacity: " << s2.capacity() << ")" << endl;cout << "s3: " << s3 << "(size: " << s3.size() << ", capacity: " << s3.capacity() << ")" << endl;cout << "s4: " << s4 << "(size: " << s4.size() << ", capacity: " << s4.capacity() << ")" << endl;cout << "s5: " << s5 << "(size: " << s5.size() << ", capacity: " << s5.capacity() << ")" << endl;// 测试[]访问for (size_t i = 0; i < s2.size(); i++) {s2[i] += 1; // 每个字符加1('h'→'i','e'→'f'等)}cout << "s2修改后: " << s2 << endl;// 测试范围forcout << "s2范围for遍历: ";for (auto ch : s2) {cout << ch << " ";}cout << endl << endl;
}// 测试2:容量操作(reserve、resize、clear)
void test_string2() {cout << "=== 测试2:容量操作 ===" << endl;string s("hello");cout << "初始s: " << s << "(size: " << s.size() << ", capacity: " << s.capacity() << ")" << endl;// 测试reserves.reserve(10);cout << "reserve(10)后: " << s << "(size: " << s.size() << ", capacity: " << s.capacity() << ")" << endl;// 测试resize(扩容并填充)s.resize(8, 'x');cout << "resize(8, 'x')后: " << s << "(size: " << s.size() << ", capacity: " << s.capacity() << ")" << endl;// 测试resize(缩容)s.resize(3);cout << "resize(3)后: " << s << "(size: " << s.size() << ", capacity: " << s.capacity() << ")" << endl;// 测试clears.clear();cout << "clear后: " << s << "(size: " << s.size() << ", capacity: " << s.capacity() << ")" << endl;// 测试reserve(缩容,不生效)s.reserve(2);cout << "reserve(2)后: " << s << "(size: " << s.size() << ", capacity: " << s.capacity() << ")" << endl << endl;
}// 测试3:修改操作(+=、insert、erase)
void test_string3() {cout << "=== 测试3:修改操作 ===" << endl;string s("hello");// 测试+=s += ' ';s += "world!";cout << "s += ' ' + 'world!': " << s << endl;// 测试insert(插入字符)s.insert(5, '$');cout << "insert(5, '$'): " << s << endl;// 测试insert(插入字符串)s.insert(0, "start: ");cout << "insert(0, 'start: '): " << s << endl;// 测试erase(删除部分字符)s.erase(6, 6); // 从位置6删6个字符(删除"hello$")cout << "erase(6, 6): " << s << endl;// 测试erase(删除到末尾)s.erase(6); // 从位置6删到末尾cout << "erase(6): " << s << endl << endl;
}// 测试4:查找和截取(find、substr)
void test_string4() {cout << "=== 测试4:查找和截取 ===" << endl;string s("test.cpp.zip");// 测试find(找字符)size_t pos1 = s.find('.');if (pos1 != string::npos) {cout << "第一个'.'的位置: " << pos1 << endl;}// 测试rfind(反向找字符)size_t pos2 = s.rfind('.');if (pos2 != string::npos) {cout << "最后一个'.'的位置: " << pos2 << endl;}// 测试substr(截取后缀)string suffix = s.substr(pos2);cout << "文件后缀: " << suffix << endl;// 测试substr(截取指定长度)string prefix = s.substr(0, 4);cout << "文件前缀: " << prefix << endl << endl;
}// 测试5:赋值运算符和swap
void test_string5() {cout << "=== 测试5:赋值运算符和swap ===" << endl;string s1("hello");string s2("world");cout << "赋值前:s1=" << s1 << ", s2=" << s2 << endl;s1 = s2; // 赋值运算符重载cout << "s1 = s2后:s1=" << s1 << ", s2=" << s2 << endl;string s3("aaa");string s4("bbb");cout << "swap前:s3=" << s3 << ", s4=" << s4 << endl;swap(s3, s4); // 非成员swap函数cout << "swap后:s3=" << s3 << ", s4=" << s4 << endl << endl;
}// 测试6:输入输出
void test_string6() {cout << "=== 测试6:输入输出 ===" << endl;string s;cout << "请输入一个字符串: ";cin >> s; // 测试输入运算符cout << "你输入的字符串是: " << s << endl;// 测试getline(读取整行)cin.ignore(); // 忽略前面的换行符string s_line;cout << "请输入一行字符串(含空格): ";getline(cin, s_line); // 注意:需要包含<string>头文件cout << "你输入的一行是: " << s_line << endl << endl;
}int main() {test_string1();test_string2();test_string3();test_string4();test_string5();test_string6();return 0;
}

8.2 运行结果

=== 测试1:构造函数和基本访问 ===
s1: (size: 0, capacity: 0)
s2: hello world(size: 11, capacity: 11)
s3: aaaaa(size: 5, capacity: 5)
s4: hello world(size: 11, capacity: 11)
s5: aaaaa(size: 5, capacity: 5)
s2修改后: ifmmp!xpsme
s2范围for遍历: i f m m p ! x p s m e === 测试2:容量操作 ===
初始s: hello(size: 5, capacity: 5)
reserve(10)后: hello(size: 5, capacity: 10)
resize(8, 'x')后: helloxxx(size: 8, capacity: 10)
resize(3)后: hel(size: 3, capacity: 10)
clear后: (size: 0, capacity: 10)
reserve(2)后: (size: 0, capacity: 10)=== 测试3:修改操作 ===
s += ' ' + 'world!': hello world!
insert(5, '$'): hello$world!
insert(0, 'start: '): start: hello$world!
erase(6, 6): start: world!
erase(6): start: === 测试4:查找和截取 ===
第一个'.'的位置: 4
最后一个'.'的位置: 7
文件后缀: .zip
文件前缀: test=== 测试5:赋值运算符和swap ===
赋值前:s1=hello, s2=world
s1 = s2后:s1=world, s2=world
swap前:s3=aaa, s4=bbb
swap后:s3=bbb, s4=aaa=== 测试6:输入输出 ===
请输入一个字符串: hello
你输入的字符串是: hello
请输入一行字符串(含空格): hello world
你输入的一行是: hello world

8.3 结果分析

  • 所有测试用例均通过,自定义string类的功能正常;
  • 容量操作中,reserve只扩容不缩容,符合标准库行为;
  • 深拷贝生效:拷贝构造和赋值后,修改一个对象
http://www.dtcms.com/a/478140.html

相关文章:

  • 【Java集合体系】全面解析:架构、原理与实战选型
  • 999免费的网站北京网站设计方案
  • 复制和粘贴快捷键ctrl加什么?【图文详解】电脑复制粘贴快捷键?剪贴板历史记录?电脑快捷键大全?快捷键操作?
  • 手机网站样式专门做婚庆的网站
  • 知识付费产品:如何与用户建立长期价值共生关系?
  • 操作【GM3568JHF】FPGA+ARM异构开发板 使用指南:音频接口
  • Redis -持久化
  • [css]基础知识和常见应用
  • 电子商务网站的建设费用案例涿州网站建设
  • 企业网站推广哪家公司好惠州网站建设方案外包
  • 容器管理不再受限!PortainerCE+cpolar打造云端数字指挥中心
  • 无人机抗电磁干扰机理与抗干扰技术研究综述
  • Spring Batch 容错机制分析
  • 【C++ Primer】第三章:字符串、向量与数组
  • Allegro X Advanced Designer 23.1 设计约束
  • 【Leetcode hot 100】4.寻找两个正序数组的中位数
  • HTB 赛季9靶场 - Signed
  • 任务栏透明度调节工具
  • 网站企业电子商务网站建设教学计划
  • 淘宝一番赏抽赏小程序:开启趣味抽赏新体验
  • 上海专业网站建站扬州北京网站建设
  • HTTP初识(二)
  • 【10 分钟!M4 Mac mini 离线部署「私有 ChatGPT」完整实录】
  • 怎么给网站做动图网络营销中的四种方法
  • API测试 | 3步走,通过协作实现API的高质量交付
  • 消息鉴别码的种类
  • C++设计模式之行为型模式:策略模式(Strategy)
  • 接口安全测试实战:从数据库错误泄露看如何构建安全防线
  • h5游戏网站建设做网站被网警找
  • 微美全息(NASDAQ:WIMI)融合区块链+AI+IoT 三大技术,解锁物联网入侵检测新范式