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