一、如何区分自定义类与标准库中的同名类
// string.h
std;
bit {
{
};
}
详细讲解了 C++ 中 string 类的底层模拟实现。内容包括如何避免命名冲突(使用命名空间)、构造函数与析构函数的内存管理、深拷贝机制、运算符重载(赋值、比较、流插入)、以及核心功能函数如 reserve、push_back、append、insert、erase、find 和 substr 的实现逻辑。重点涉及堆内存分配、字符串复制及边界检查,帮助理解标准库 string 的工作原理。

// string.h
std;
bit {
{
};
}
既然要模拟实现 string 底层,那就得先理解为什么我写的 string 和库 string 里面不会冲突。
当我们用双引号 "" 包围头文件时,编译器会首先在当前目录下查找这个头文件。这意味着它会优先找到我们自己定义的头文件(比如 string.h),而不是标准库中的头文件。因为标准库的头文件通常在其他系统目录中,所以不会发生冲突。
虽然使用双引号包含头文件可以让编译器优先使用我们自定义的头文件,但如果我们在头文件中定义了一个与标准库相同名称的类或函数,仍然可能导致混淆或冲突。
为了解决这个问题,我们可以使用命名空间。命名空间就像是一个独立的区域,把我们的代码和标准库的代码隔离开来。比如,我们创建一个 bit 命名空间,然后在这个命名空间里定义一个与标准库同名的类或函数,这样就可以避免冲突。
当我们在代码中使用这些定义时,需要明确指明是哪个命名空间下的。比如,使用 bit::string 来表示我们自定义的 string 类,而 std::string 则表示标准库中的 string 类。这样编译器就能清楚地区分它们。
string() :_str(new char[1]), _size(0), _capacity(0) {
_str[0] = '\0';
}
在 std::string 类,空字符串的初始化会创建一个包含单个字符 ('\0') 的字符,用于表示字符串的结束。
那么 _str(new char[1]) 就是在堆上 new 一个包含 1 个 char 元素的数组。然后给这唯一的元素赋值 '\0'(赋值运算都是在构造函数体内进行)。
string(const char* str) : _str(new char[strlen(str)+1]), _size(strlen(str)), _capacity(strlen(str)) {
strcpy(_str, str);
}
既然要把常量字符串复制过来,首先要知道它的长度 (strlen(str) 计算长度不包括 '\0',所以要 +1),然后让成员变量 _str 指向堆上新 new 的空间。空间有了接下来就可以把字符串复制过来了,而 C 语言中 strcpy 函数就是一个很好的选择。从上面代码可以看出来我们多次调用 strlen 函数来计算字符串长度,可不可以只使用一次,并且将有值构造和默认构造合二为一?
string(const char* str) : _size(strlen(str)) {
_str = new char[_size + 1];
_capacity = _size;
strcpy(_str, str);
}
合并之后它可以同时作为默认构造函数和常量字符串构造函数。我们首先在初始化列表中确定了字符串的长度并初始化了大小和容量,然后在构造函数体中分配适当大小的内存并将字符串复制到新内存中。
// 深拷贝
string(const string& s) {
_str = new char[s._capacity + 1];
_size = s._size;
_capacity = s._capacity;
strcpy(_str, s._str);
}
// 浅拷贝
string(const string& s) {
_str = s._str; // 直接复制指针
_size = s._size;
_capacity = s._capacity;
}
回顾一下深拷贝与浅拷贝基本知识内容就基本上懂为什么要写深拷贝而不是以往的浅拷贝。
深拷贝: 复制对象时,分配新的内存,并复制指针指向的内容。结果是两个对象各自拥有独立的内存,不会互相影响。
浅拷贝: 复制对象时,仅复制指针,而不复制指针指向的内容。结果是两个对象共享同一块内存,这可能导致一个对象修改数据,另一个对象的数据也被修改,甚至导致双重释放问题(两个对象在析构时都试图释放同一块内存)。
~string() {
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
讲完了如何初始化,那就该讲讲怎么遍历了。
遍历字符串有 operator[]、迭代器两种方式,那现在让我们模拟实现它们吧。
重载 operator[] 可以让我们像访问数组元素一样访问类的成员变量。
char& operator[](size_t pos) {
assert(pos < _size);
return _str[pos];
}
我们一般说迭代器类似指针但是却不是指针。
typedef char* iterator;
iterator begin() { return _str; }
iterator end() { return _str + _size; }
// 开空间
void reserve(size_t n) {
if(n > _capacity) {
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
// 尾部添加单个字符
void push_back(char ch) {
if(_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
// 尾部添加一个字符串
void append(const char* str) {
size_t len = strlen(str);
if(len + _size > _capacity) {
reserve(len + _size);
}
strncpy(_str + _size, str, len);
_size += len;
}
// 尾插单个字符版
string& operator+=(char ch) {
push_back(ch);
return *this;
}
// 尾插字符串版
string& operator+=(const char* str) {
append(str);
return *this;
}
// 尾插字符串测试代码
int main() {
using namespace bit;
string str;
str += "Hello, ";
str += "world!";
return 0;
}
当执行 str += "Hello, "; 时,编译器会识别出这是 operator+= 操作,并且调用我们为 String 类定义的 operator+= 函数。
在 operator+= 函数内部,this 是一个指向 str 对象的指针。所以在函数内部,this-> 和 str. 是等价的。
在 operator+= 函数中,调用了 append(str),其中 str 是传入的 C 字符串 "Hello, "。此时,this 仍然指向 str 对象。
operator+= 返回当前对象的引用 *this,即 str 的引用。
// 在指定位置插入字符
void insert(size_t pos, char ch) {
assert(pos <= _size);
if(_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size;
while(end >= pos) {
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
_size += 1;
}
// 在指定位置插入字符串
void insert(size_t pos, const char* str) {
assert(pos <= _size);
size_t len = strlen(str);
if(_size + len > _capacity) {
reserve((_size + len) * 2);
}
size_t end = _size;
while(end >= pos) {
_str[end + len] = _str[end];
--end;
}
strncpy(_str + pos, str, len);
_size += len;
}
// 从指定位置删除指定长度的字符
void erase(size_t pos, size_t len = static_cast<size_t>(-1)) {
assert(pos < _size);
if(len == static_cast<size_t>(-1) || len + pos > _size) {
_str[pos] = '\0';
_size = pos;
} else {
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
当 pos + len 超过了有效数据的总长度时,说明操作已经超出范围,这时候最简单的做法就是从 pos 开始删除后面的所有内容。反之,如果 len 没有超出范围,那么就不需要做任何删除操作。
len 参数,它默认会使用 npos 作为 len 的值。npos 通常表示一个无效位置或者未找到的值,通常等同于 -1(但在 size_t 类型中,-1 实际上是一个非常大的正整数)。len == npos 时,意味着你没有指定要删除或操作的长度。在这种情况下,函数通常会理解为你希望操作直到字符串的末尾。// 查找字符
size_t find(char ch, size_t pos = 0) const {
assert(pos < _size);
for (size_t i = pos; i < _size; i++) {
if (_str[i] == ch) return i;
}
return static_cast<size_t>(-1);
}
// 查找字符串
// const 在函数签名后面:表示这个成员函数不能修改所属对象的状态。
size_t find(const char* str, size_t pos = 0) const {
assert(pos < _size);
// 用于查找一个字符串在另一个字符串中的首次出现位置
// 找到返回它在 _str + pos 中首次出现的位置;如果未找到,m 将为 nullptr。
const char* m = strstr(_str + pos, str);
if(m) {
return m - _str; // 指针减去指针返回它们相差的个数
} else {
return static_cast<size_t>(-1);
}
}
// 在原字符串中获取子串
string substr(size_t pos = 0, size_t len = static_cast<size_t>(-1)) {
string Tmp;
assert(pos < _size);
if(pos + len > _size || len == 0) {
for(int i = pos; i < _size; i++) {
// 字符被添加到了 Tmp 对象所指向的字符串存储空间中,而这个存储空间的指针是 Tmp 对象的一部分。
Tmp += _str[i];
}
} else {
for(int i = pos; i < len + pos; i++) {
Tmp += _str[i];
}
}
return Tmp;
}
// 赋值重载
string& operator=(const string& s) {
if(this != &s) {
char* Tmp = new char[strlen(s._str) + 1];
strcpy(Tmp, s._str);
delete[] _str;
_str = Tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
// 几个比较函数
bool operator==(const string& s1, const string& s2) {
int m = strcmp(s1._str, s2._str);
return m == 0;
}
bool operator<(const string& s1, const string& s2) {
int x = strcmp(s1._str, s2._str);
return x < 0;
}
// 流插入与提取
ostream& operator<<(ostream& out, const string& s) {
for(auto e : s) {
out << e;
}
return out;
}

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online