💬 欢迎讨论:在阅读过程中有任何疑问,欢迎在评论区留言,我们一起交流学习! 👍 点赞、收藏与分享:如果你觉得这篇文章对你有帮助,记得点赞、收藏,并分享给更多对C++感兴趣的朋友
本文将通过一个自定义的字符串类实现(zhh::string
zhh
是一个我自定义的作用域),深入探讨string
类的核心设计思路与实现细节,以及为什么在拷贝构造和赋值运算符重载的实现需要用深拷贝。该代码模拟了标准库std::string
的一些核心功能,包括动态内存管理、迭代器、常用操作符重载等。同时也借此对深浅拷贝进行实际上的应用。
源码在文章末尾
浅拷贝:也称位拷贝,仅仅这是将值拷贝过来。
我们平时C语言中使用的赋值,以及函数传值,都是浅拷贝。
如果在对象中使用,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问错误。
浅拷贝 仅复制指针的值(地址),导致多个对象共享同一块内存:
class String {
public:
char* data;
// 假设有浅拷贝构造函数
String(const String& other) : data(other.data) {}
};
String s1("Hello");
String s2 = s1; // 浅拷贝:s2.data 和 s1.data 指向同一内存
问题:
s1
会影响 s2
。s1
和 s2
会尝试释放同一块内存,导致崩溃。那么,如何使得每个对象都有一份独立的资源,不要和其他对象共享?
深拷贝 是一种内存管理技术,其核心是完整复制对象及其动态分配的资源,生成一个与原对象完全独立的新对象。在C++中,当类包含指针成员并指向堆内存时,必须通过深拷贝来避免以下问题:
深拷贝通过分配新内存并复制内容,确保对象间的独立性。
在实现string类中的拷贝构造函数以及赋值运算符重载都需要用到深拷贝:
string(const string& s) {
_str = new char[s._capacity + 1]; // 分配新内存
memcpy(_str, s._str, s._size + 1); // 复制内容(包含'\0')
_size = s._size;
_capacity = s._capacity;
}
传统写法:
string& operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
memcpy(tmp, s._str, s._size+1);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
现代写法:
//string类的swap接口:将每个成员变量交换即可
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
swap(tmp);
}
return *this;
}
s
构造tmp
——代替new
开辟空间与memcpy
拷贝步骤*this
与tmp
交换数据,利用局部对象tmp
出函数作用域调用析构释放原*this
的数据——代替delete
释放原空间数据步骤省略了大量代码,妙不可言
当今写法:
string& operator=(string tmp) {
swap(tmp); // 交换资源,tmp 析构时释放原内存
return *this;
}
点睛之笔:利用函数传值自动调用拷贝构造的原理——替代了手动调用拷贝构造
真是进了米奇妙妙屋了,妙的不能再妙了😏
tmp
的深拷贝,通过 swap
安全交换资源,天然避免自赋值问题。int
, double
)。通过正确实现深拷贝,可以避免内存泄漏、悬空指针和不可预测的行为,从而编写出健壮的C++程序。
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
class string {
private:
char* _str; // 字符串内容
size_t _capacity; // 总容量(不包含'\0')
size_t _size; // 当前长度
const static size_t npos; // 特殊值,表示无效位置
};
_str
:动态分配的字符数组,存储字符串内容(以'\0'
结尾)。_capacity
:当前分配的内存容量(不包含'\0'
的额外空间)。_size
:字符串实际长度(包含'\0'
的额外空间)。npos
:静态常量,表示无效位置(定义为-1
的无符号最大值)。string(const char* str = "") {
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
memcpy(_str, str, _size);
_str[_size] = '\0';
}
memcpy
高效拷贝数据,但需确保输入字符串以'\0'
结尾。前文在讲解深拷贝时已详细介绍了,这里不再赘述了。
string(const string& s) {
_str = new char[s._capacity + 1]; // 需分配足够空间(原实现有误)
memcpy(_str, s._str, s._size + 1); // 包含'\0'
_size = s._size;
_capacity = s._capacity;
}
string& operator=(string tmp) {
swap(tmp); // 通过交换资源实现赋值
return *this;
}
~string() {
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
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;
}
for
循环。测试代码:
void TestString1()
{
zhh::string s1("hello world");
zhh::string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
{
for (auto ch : s1)
cout << ch << "6";
}
cout << endl;
}
测试结果:
reserve
扩容void reserve(size_t n) {
if (n > _capacity) {
char* tmp = new char[n + 1];
memcpy(tmp, _str, _size + 1); // 包含'\0'
delete[] _str;
_str = tmp;
_capacity = n;
}
}
memcpy
提升性能,但需确保拷贝长度包含'\0'
。resize
调整大小void resize(size_t n, char c = '\0') {
if (n > _size) {
reserve(n);
for (size_t i = _size; i < n; ++i)
_str[i] = c;
}
_str[n] = '\0'; // 确保终止符
_size = n;
}
n > _size
,填充字符c
;否则截断字符串。size
获取大小size_t size()const
{
return _size;
}
capacity
获取容量 size_t capacity()const
{
return _capacity;
}
empty
判空bool empty()const
{
return _size == 0 ? true : false;
}
push_back
尾插字符尾插字符很简单,在字符串尾部赋值即可,需要考虑的问题有:
\0
结束标志?解决问题:
_capacity
是否大于_size +1
reserve
扩容。扩多大取决于自己,我这里是扩两倍_size
的值,最后在_size
的位置赋上\0
代码:
void push_back(char c)
{
//检查容量,不足就扩
if (_capacity < _size + 1)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = c;
++_size;
_str[_size] = '\0';
}
append
在字符串结尾拼接字符串思路与尾插字符差不多:检查容量->不足就扩->拼接->改变size 需要注意的是:
assert
断言防止空指针传入memcpy
从字符串尾部开始拷贝\0
,因为memcpy
时可以顺带完成代码:
void append(const char* str)
{
assert(str);
size_t len = strlen(str);
reserve(_size + len);
memcpy(_str + _size, str, len);
_size += len;
}
operator+=
这里复用push_back和append即可:
string& operator+=(char c)
{
push_back(c);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
💡:我们平时需要尾插字符或字符串时一般都用这个接口,因为他完全可以代替push_back和append。但我们需要知道他的底层其实是由push_back和append实现的。
clear
清空有效字符在_str[0]
的位置赋值\0
即可
void clear()
{
_str[0] = '\0';
_size = 0;
}
c_str
获取C属性的字符串(字符串指针)返回_str
成员变量即可
const char* c_str()const
{
return _str;
}
这个其实我们前面已经用过了,是不是有种自来熟的感觉😂 返回字符串对应位置的字符即可
char& operator[](size_t index)
{
assert(index < _size && index >= 0);
return _str[index];
}
const char& operator[](size_t index)const
{
assert(index < _size && index >= 0);
return _str[index];
}
find
查找字符或子串strstr
库函数实现高效查找。代码:
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const
{
assert(pos >= 0);
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const
{
char* ptr = strstr(_str + pos, s);
if (ptr)
{
return ptr - _str;
}
else
{
return npos;
}
}
insert
插入大体与push_back
类似,但多了一个步骤——挪动数据
从尾部开始,将字符赋值到后面的一个位置,到pos位置结束(pos位置也要挪动)。
代码(有bug):
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& insert(size_t pos, char c)
{
assert(pos < _size && pos >= 0);
//检查容量,不足就扩
if (_capacity < _size + 1)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size;
while (pos <= end)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = c;
++_size;
return *this;
}
聪明的你可以看出问题出在哪吗? 可以试试用pos = 0运行一下代码,会发现超时了,那应该就是循环的问题,我们锁定while的位置 走读代码,当end = 0,进入循环,–end得end = -1,end的类型是size_t,而-1是最大的无符号整数,所以我们循环进入了死循环。
处理方法:添加循环条件end != npos
正确代码:
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& insert(size_t pos, char c)
{
assert(pos < _size && pos >= 0);
//检查容量,不足就扩
if (_capacity < _size + 1)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size;
//当pos = 0 ,end会变为-1,成为最大无符号整数进入死循环
//因此这里需补充条件end != npos
while (pos <= end && end != npos)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = c;
++_size;
return *this;
}
插入字符串的挪动方式差不多:
方式是相同的,但挪动距离为len
(插入的字符串长度)
代码:
string& insert(size_t pos, const char* str)
{
assert(pos < _size && pos >= 0);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
//挪动数据
size_t end = _size;
//当pos = 0 ,end会变为-1,成为最大无符号整数进入死循环
//因此这里需补充条件end != npos
while (end >= pos && end != npos)
{
_str[end + len] = _str[end];
--end;
}
for (size_t i = pos; i < pos + len; ++i)
{
_str[i] = *str;
++str;
}
_size += len;
return *this;
}
erase
删除_size - pos
,直接在pos
位置赋值\0
即可\0
后面即可// 删除pos位置上的元素,并返回该元素的下一个位置
string& erase(size_t pos, size_t len)
{
if (len >= _size - pos)
{
_str[pos] = '\0';
}
else
{
size_t end = pos + len;
for (size_t i = end; i <= _size; ++i)
{
_str[end - len] = _str[end];
end++;
}
}
return *this;
}
substr
截取字符串核心思想:创建一个临时对象,将需的字符依次尾插到这个对象,最后返回这个对象的值即可 注意:这里只能传值返回
string substr(size_t pos = 0, size_t len = npos)
{
string tmp;
size_t n = len;
if (n > _size - pos || n == npos)
{
n = _size - pos;
}
tmp.reserve(n);
for (size_t i = pos; i < pos + n; ++i)
{
tmp += _str[i];
}
//返回值传值,调用拷贝构造
return tmp;//返回值不可用引用,否则出作用域调用析构,非法访问
}
一共有6个关系运算符:<
>
==
<=
>=
!=
其实我们只需要实现两个即可(<
或>
和==
)
核心问题:如何比较两个字符串大小?
在编程中,两个字符串的大小比较通常是基于字典序进行的,类似于字典中单词的排列顺序。具体规则如下:
比较规则
"apple"
和 "apricot"
:
第3个字符 'p'
(ASCII 112) < 'r'
(ASCII 114),所以 "apple"
< "apricot"
。"hello"
和 "hell"
:前4字符相同,但前者更长,所以 "hello"
> "hell"
。实现思路:
bool operator<(const string& s)
{
int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
return ret == 0 ? _size < s._size : ret < 0;
}
bool operator==(const string& s)
{
return _size == s._size && memcmp(_str, s._str, _size) == 0;
}
bool operator<=(const string& s)
{
return *this < s || *this == s;
}
bool operator>(const string& s)
{
return !(*this <= s);
}
bool operator>=(const string& s)
{
return !(*this < s);
}
bool operator!=(const string& s)
{
return !(*this == s);
}
我们上一期知道:这两个运算符重载不能作为成员函数(为了使得对象参数作为第二个参数),需要在类外实现,必要的话可以将它们声明为类的友元。
流插入比较简单,遍历流插入字符即可:
ostream& operator<<(ostream& out, const zhh::string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
而流提取就比较麻烦了。
clear
)。get()
可以解决这个问题buff[128]
,我们将输入的数据存入数组,
情况1:输入结束且数组未满,直接将数组中的字符尾插到目标字符串。
情况2:输入未结束且数组已满,将数组中的字符尾插到目标字符串,继续从数组的起始位置重新存输入的数据,循环往复。
注意:每次要尾插数据前,要在数组的有效数据后赋值\0
,否则会造成数据错乱。 istream& operator>>(istream& in, zhh::string& s)
{
//清理原数据
s.clear();
char ch = '\0';
//清理缓存区的空格与换行
while (ch == ' ' || ch == '\n')
{
ch = in.get();
}
char buff[128];
int i = 0;
while (ch != ' ' && ch != '\n')
{
ch = in.get();
buff[i++] = ch;
if (i == 127)//留一个位置放\0
{
buff[i] = '\0';
s += buff;
i = 0;
}
}
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
}
源码自取:模拟实现string类
万字文章,制作不易,留给赞再走吧~
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有