首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C++初阶】3.C++ 模板与 string 类详解(含模拟实现及其代码)

【C++初阶】3.C++ 模板与 string 类详解(含模拟实现及其代码)

作者头像
用户11952558
发布2026-01-09 14:06:47
发布2026-01-09 14:06:47
1410
举报

模板

当我们写交换两个元素的函数时,通常会这样写:

代码语言:javascript
复制
void swap(int& x, int& y)
{
    int tmp = x;
    x = y;
    y = tmp;
}

但是,如果要交换 long long 类型、double 类型,甚至自定义类型,就需要写多个函数了。

为了解决这个问题,C++ 引入了模板这个概念。

1. 函数模板

代码语言:javascript
复制
template<class T>
void swap(T& a1, T& a2)
{
    T tmp = a1;
    a1 = a2;
    a2 = tmp;
}

注意点:

  1. class 可以改成 typename,现阶段两者没有区别
  2. 实质:编译器自动推断,生成对应函数(类比类的实例化,称为模板的实例化)
  3. 同一个 T 不能代表不同类型
代码语言:javascript
复制
int a = 1;
double b = 2.0;
swap(a, b); // 报错:"swap": 未找到匹配的重载函数
处理不同类型的参数
方法1:强制类型转换
代码语言:javascript
复制
template<class T>
T add(const T& a, const T& b)
{
    return a + b;
}

cout << add(a, (int)b) << endl;

这种方法有很多局限性。

方法2:显式实例化
代码语言:javascript
复制
add<int>(a, b); // 在前面加上类型
方法3:定义两个模板参数
代码语言:javascript
复制
template<class T1, class T2>
T1 add(const T1& a, const T2& b)
{
    return a + b;
}
模板参数作为返回值

有些时候,编译器无从推断 T 的类型:

代码语言:javascript
复制
template<class T>
T* func(int n)
{
    return new T[n];
}

这个时候就必须显式实例化:

代码语言:javascript
复制
int* a = func<int>(5);
模板与普通函数的优先级
代码语言:javascript
复制
template<class T1, class T2>
T1 add(const T1& a, const T2& b)
{
    return a + b;
}

int add(const int a, const int b)
{
    return a + b;
}

根据调试结果,优先调用对应的普通函数。

2. 类模板

为方便理解,先写一个简单的栈类:

代码语言:javascript
复制
template<class T>
class stack
{
public:
    stack(int n = 4)
        : capacity(n)
        , size(0)
        , _arr(new T[n])
    { }
    
    ~stack()
    {
        delete[] _arr;
        _arr = nullptr;
        capacity = 0;
        size = 0;
    }
    
    void push(const T& s)
    {
        if (capacity == size)
        {
            T* tmp = new T[2 * capacity];
            memcpy(tmp, _arr, sizeof(T) * size);
            delete[] _arr;
            capacity *= 2;
            _arr = tmp;
        }
        _arr[size++] = s;
    }

private:
    size_t capacity;
    size_t size;
    T* _arr;
};

注意: 类模板必须显式实例化

代码语言:javascript
复制
stack<int> st1;
stack<double> st2;

这样,就可以让栈中存储不同类型的元素了。

声明与定义分离
代码语言:javascript
复制
// 声明
template<class T>
class stack
{
public:
    // ...
    void push(const T& s);
    // ...
};

// 定义
template<class T>
void stack<T>::push(const T& s)
{
    if (capacity == size)
    {
        T* tmp = new T[2 * capacity];
        memcpy(tmp, _arr, sizeof(T) * size);
        delete[] _arr;
        capacity *= 2;
        _arr = tmp;
    }
    _arr[size++] = s;
}

甚至可以使用不同的模板参数名:

代码语言:javascript
复制
template<class X>
void stack<X>::push(const X& s)
{
    // ...
}

这也可以证明模板实质上是编译器自动生成特定元素类型的类。

有了模板,C++ 就可以写一份代码,无伤兼容各种类型,这就诞生了 STL:让顺序表、栈、队列等数据结构由编译器帮你生成。

C++ string 类详解

简介

在认识 string 之前,先介绍两个有用的网站:

  1. C++ 官方文档
    • 优点:更新及时
    • 缺点:较杂乱
  2. cppreference.com(非官方文档)
  3. cplusplus.com - The C++ Resources Network(非官方文档)

在接下来的内容中,我们将使用非官方文档进行讲解。

1. 使用方法

注意: 这里的一些代码不需要彻底理解,我们在模拟实现部分会详细讲解。

一、构造函数

在 C++98 中,string 构造函数有 7 个,先讲前 6 个:

代码语言:javascript
复制
// 1. 默认构造
string s0;

// 2. 拷贝构造
string s3(s1);

// 3. 子串构造
string s4(s1, 6);        // 索引6到末尾
string s(s1, 6, 2);      // 索引6开始,接下去2个字符

// 4. C风格字符串构造
string s1("hello world");

// 5. C风格字符串部分构造
string s2("hello world", 5);  // 前5个字母

// 6. 填充构造
string s5(11, 'X');

由于很多string的接口都是一个套路:string(string s3(s1)),string的一部分(string s(s1, 6, 2)),char*(string s1("hello world")),char*的一部分(string s2("hello world", 5)),迭代器(下文会讲),因此接下去就只演示传string的方法,其它的同理

二、基本知识
1. npos

我们注意到在 substring 的构造中:

代码语言:javascript
复制
string (const string& str, size_t pos, size_t len = npos);

npos 是一个缺省参数,表示最后一个字符。查看 npos 的定义,可以看到它是一个静态成员,表面值是 -1,实际是极大值。

代码语言:javascript
复制
// npos 可以用 string::npos 指定调用
static const size_t npos = -1;

因此,我们不写,或写一个较大的值都是没问题的,都会截取至末尾。

2. [] 重载
代码语言:javascript
复制
string s1("hello world");
cout << s1[2] << endl;

上面的语句会像数组一样,可以访问并修改对应元素。原因就是 [] 被重载了。

大概逻辑:

代码语言:javascript
复制
char& operator[](size_t t)
{
    return _str[t];
}

我们返回引用值,就可以查询并且修改了。

3. 遍历方式
(1)下标 + [] 重载
代码语言:javascript
复制
for (int i = 0; i < s.size(); i++)
{
    cout << s[i] << " ";
}
(2)迭代器
代码语言:javascript
复制
string::iterator it = s.begin();
while (it != s.end())
{
    cout << *it << " ";
    it++;
}

我们可以用指针去理解:begin 指向开头,* 重载后有类似于解引用的功能,后置 ++ 重载有指向下一个元素的功能。这个方法在 C++ 标准容器中都可以用。

(3)范围 for(C++11)
代码语言:javascript
复制
for (char e : s)
{
    cout << e << " ";
}

在范围 for 中,e 自动递增,自动停止。但其本质还是迭代器,只是多了一步将字符赋给 e

迭代器接近于指针,我们可以给取到的字符加减:

代码语言:javascript
复制
string::iterator it = s.begin();
while (it != s.end())
{
    *it += 2;
    cout << *it << " ";
    it++;
}
cout << endl;

for (char e : s)
{
    e -= 2;
    cout << e << " ";
}

在这个代码结果中,我们发现成功加了 2,但没有成功减去,因为 e 是字符的拷贝。当我们加一个引用,就可以用范围 for 修改了:

代码语言:javascript
复制
for (char& e : s)
{
    e -= 2;
    cout << e << " ";
}

同时,数组也可以用范围 for 遍历:

代码语言:javascript
复制
int arr[] = {0, 3, 2, 4};
for (int e : arr)
{
    cout << e << " ";
}
4. 迭代器

刚才的 beginend 为正向迭代器,但我们要反向遍历数组时,由于迭代器为左闭右开区间,就需要先减,再打印:

代码语言:javascript
复制
string::iterator it = s.end();
while (it != s.begin())
{
    it--;
    cout << *it << " ";
}
cout << *s.begin() << endl;

但是,还有反向迭代器

代码语言:javascript
复制
string::reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
    cout << *rit << " ";
    rit++;
}

这样,依旧用 ++ 就可以遍历。

此外,还有 const 版本的正向、反向迭代器,因此共 4 组:

代码语言:javascript
复制
// const 正向迭代器
string::const_iterator cit = s.cbegin();
while (cit != s.cend())
{
    cout << *cit << " ";
    cit++;
}

// const 反向迭代器
string::const_reverse_iterator crit = s.crbegin();
while (crit != s.crend())
{
    cout << *crit << " ";
    crit++;
}
5. auto(C++11)

在刚才迭代器名字要敲疯了?我们可以将那一长串单词改成 auto,交给编译器自动推断:

代码语言:javascript
复制
auto crit = s.crbegin();
while (crit != s.crend())
{
    cout << *crit << " ";
    crit++;
}

同样,范围 for 也可以用 auto

代码语言:javascript
复制
for (auto& e : s)
{
    cout << e << " ";
}

同时,auto 不能做参数,但能做返回值。但用 auto 的缺点是代码可读性会变差。

三、输入输出
流插入和流提取
代码语言:javascript
复制
string s;
cin >> s;
cout << s;

由于输入、输出都被重载过了,因此可以用流进行输入、输出。

四、容量操作
1. 大小
代码语言:javascript
复制
s.size();    // 推荐使用
s.length();  // 为了向前兼容而保留

为了向前兼容,因此 length 依旧保留,但是 STL 的容器都用 size(毕竟树的大小不能用 length 吧),因此推荐使用 size

2. 存储空间
(1)扩容规律

由于 string 会不断扩容,我们用 capacity 观察一下它的扩容规律(VS2022):

代码语言:javascript
复制
int main()
{
    string a;
    int cap = a.capacity();
    cout << cap << endl;
    
    for (int i = 1; i <= 500; i++)
    {
        a += 'c';
        if (a.capacity() != cap)
        {
            cap = a.capacity();
            cout << cap << endl;
        }
    }
    return 0;
}

由于这个容量是不算 \0 的,因此应该是:16 → 32 → 48 → 71... 开始扩 2 倍,之后扩 1.5 倍。

由于扩容的消耗比较大,因此在一些情况下,我们可以先估计需要多少容量,这就用到 reserve 了。

(2)reserve
代码语言:javascript
复制
a.reserve(500);

可以看到,加上这个语句,在加入 500 个字符时,扩容次数就减少了。

那会不会缩容?

代码语言:javascript
复制
a.reserve(500);
cout << a.capacity() << endl;
a += "aaa";
a.reserve(200);
cout << a.capacity() << endl;

可以看到,并没有缩容。

(3)清空
代码语言:javascript
复制
string a = "aaa";
cout << a.capacity() << endl;
a.clear();
cout << a.capacity() << endl;

可以看到,清除后容量依旧不变,因此只是清数据。

3. shrink_to_fit(了解)

缩容至 size

代码语言:javascript
复制
a.shrink_to_fit();
五、修改操作

这一块用的很少的接口可以要用到时查文档。

1. += 重载

为了方便使用,很多运算符都进行了重载:

代码语言:javascript
复制
string a = "aaa";
string b = "bbb";
b += a;

只需要简洁的运算符,就可实现 string 的追加。同时,和构造函数一样,+= 的对象可以是 stringchar* 等。

2. append

可以追加其它字符串,为 string 的冗余接口,可以用 += 的重载替代,没必要专门使用。

3. Insert

插入字符串,同样用的少,可以插入的对象多:

代码语言:javascript
复制
string a = "abcde";
string b = "fghij";
a.insert(3, b, 2, 4);  // 将b从2下标开始取4个字符插入到a的3下标后面
cout << a << endl;
4. erase

擦除,同样用的较少:

代码语言:javascript
复制
string a = "abcde";
a.erase(2, 2);  // 擦除a从2下标开始2个字母
5. Replace

同样较少用,演示 string 的替换,其它方式与构造函数类似:

代码语言:javascript
复制
string a = "abcde";
string b = "fghij";
a.replace(2, string::npos, b);  // 第二个数不能空着
// 从a的2个字符开始,数到末尾,替换成b

将空格替换为 %%:

代码语言:javascript
复制
string p = "ab cd e f ghi";
size_t pos = p.find(" ");
while (pos != string::npos)
{
    p.replace(pos, 1, "%%");
    pos = p.find(" ", pos);
}
cout << p << endl;

但是,由于是对 p 中间修改,需要后面字符挪动位置,因此时间复杂度较高。

我们新开数组,空间换时间:

代码语言:javascript
复制
string p = "ab cd e f ghi";
string tmp;
for (int i = 0; i < p.size(); i++)
{
    if (p[i] != ' ')
        tmp += p[i];
    else
        tmp += "%%";
}
cout << tmp << endl;

因此,虽然这些接口可以一句话解决修改问题,但是如果修改位置不在结尾,时间复杂度是较高的(可达 O(n²)),因此如果要调用较多次,推荐优先修改算法。

六、读取信息
1. c_str

返回 string 底层 char* 的指针,即为了和 C 兼容:

代码语言:javascript
复制
string b = "fghij";
printf("%p\n", b.c_str());
2. Copy
代码语言:javascript
复制
string a = "abcde";
char arr[20];
a.copy(arr, a.size());

缺点:要留足空间,不会自动扩容。

七、find 家族
1. find
代码语言:javascript
复制
string a = "abcde";
size_t p = a.find('c');        // 找第一个c字符的位置

string a = "abcde";
size_t p = a.find("cd");       // 找含有的"cd"字符串位置

string a = "abede";
size_t p = a.find('e', 3);     // 找3下标以后的e的位置

接下来 rfindfind_first_of 等用法类似。

实际应用示例
1. 获取文件后缀
代码语言:javascript
复制
string p = "E:\\博客\\C++\\模板\\blog.docx";  // Windows路径需要转义
size_t pos = p.rfind(".");
cout << p.substr(pos + 1) << endl;

只需要 rfind,从后面找 . 就可以了。

2. 获取 Windows、Linux 文件路径
代码语言:javascript
复制
void getpath(string p)
{
    size_t f = p.find_last_of("\\/");
    cout << "path:" << p.substr(0, f + 1) << endl;
}

string p1 = "E:\\博客\\C++\\模板\\blog.docx";
string p2 = "/home/user/documents/file.txt";
getpath(p1);
getpath(p2);
  • find_first_of:找到最前面在括号里的字符
  • find_last_of:找到最后面在括号里的字符
八、getline

首先,我们通过 cin 读取 "hello world":

代码语言:javascript
复制
string a;
cin >> a;  // 只能读到"hello"

由于 cin 遇到空格、换行就会停止,因此只能读到一个单词。

如果要读整行,就需要 getline

代码语言:javascript
复制
string a;
getline(cin, a);

同时,可以加第三个参数,读到哪个字符终止:

代码语言:javascript
复制
string a;
getline(cin, a, 'r');  // 读到r终止

C++ string 类模拟实现

模拟实现

元素
代码语言:javascript
复制
private:
    char* _str;
    size_t _capacity;
    size_t _size;
1. 返回地址
代码语言:javascript
复制
const char* c_str() const
{
    return _str;
}
2. 构造
初级
代码语言:javascript
复制
string()
    :_str(nullptr)
    ,_size(0)
    ,_capacity(0)
{ }

string(const char* str)
{
    _size = strlen(str);
    _capacity = _size;
    _str = new char[_capacity + 1];
    strcpy(_str, str);
}

注意点:

  1. 带参构造不能初始化列表,因为声明时char*在最上面,初始化时_size会随机值,无法构造
  2. _capacity是不包含\0的,因此要加1

但是加入c_str返回指针以后,以下会程序崩溃:

代码语言:javascript
复制
string s1;
string s2 = "abc";
std::cout << s1.c_str() << std::endl;
std::cout << s2.c_str() << std::endl;

原因: s1用默认构造,用nullptr,之后cout解引用,崩溃

进阶
代码语言:javascript
复制
string()
    :_str(new char[1]{'\0'})
    ,_size(0)
    ,_capacity(0)
{ }

string(const char* str)
{
    _size = strlen(str);
    _capacity = _size;
    _str = new char[_capacity + 1];
    strcpy(_str, str);
}

将默认构造改为直接开一个有\0的空间

最终

将两个函数合为一个默认构造:

代码语言:javascript
复制
string(const char* str = "")
{
    _size = strlen(str);
    _capacity = _size;
    _str = new char[_capacity + 1];
    strcpy(_str, str);
}

可以缺省值为""原因: strcpy会将\0一起拷贝(先拷贝再判断) 因此结尾自动有\0

3. 析构
代码语言:javascript
复制
~string()
{
    if (_str)
    {
        delete[] _str;
        _str = nullptr;
        _size = 0;
        _capacity = 0;
    }
}
4. 遍历
1. size,capacity
代码语言:javascript
复制
size_t size() const
{
    return _size;
}

size_t capacity() const
{
    return _capacity;
}

不可外界修改,直接const版本

2. []重载

有无const版本都要

代码语言:javascript
复制
char& operator[](size_t pos)
{
    assert(pos < _size);
    return _str[pos];
}

const char& operator[](size_t pos) const
{
    assert(pos < _size);
    return _str[pos];
}

但显然,由于还没有写迭代器,因此不支持范围for

由于之前迭代器类似于指针,因此我们先写最简单的指针改名的方法

代码语言:javascript
复制
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了

代码语言:javascript
复制
for (auto& e : s2)
{
    std::cout << e;
}

迭代器本质: 将细节底层屏蔽,统一typedef,提供统一访问方式,不用关心底层实现细节

5. 扩容,尾插(较大,声明,定义分离)
代码语言:javascript
复制
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 : 2 * _capacity);
    }
    _str[_size] = ch;
    ++_size;
    _str[_size] = '\0';  // 注意尾插后要在最后补\0
}
6. npos

在定义的private里加入这句话:

代码语言:javascript
复制
static const size_t npos = -1;

注意:

  1. 要用static,因为定义成静态,在我们实例化多次后,npos只用构造一次,节省时间
  2. 直接在声明时给值 C++中规定整型(int,size_t,long long)开成static const可以直接在声明是给值初始化(其它不行)
  3. 给-1 由于size_t为无符号整型,因此-1代表2^64-1,很大值
7. insert

两个版本,字符和字符串

代码语言:javascript
复制
void insert(size_t pos, char ch)
{
    assert(pos <= _size);
    if (_size == _capacity)
    {
        reserve(_capacity == 0 ? 4 : 2 * _capacity);
    }
    
    size_t end = _size + 1;
    while (end > pos)
    {
        _str[end] = _str[end - 1];
        end--;
    }
    _str[pos] = ch;
    ++_size;
}

void insert(size_t pos, const char* str)
{
    assert(pos <= _size);
    size_t len = strlen(str);
    if (_size + len > _capacity)
    {
        reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
    }
    
    int end = _size + len;
    while (end >= pos + len)
    {
        _str[end] = _str[end - len];
        end--;
    }
    
    for (int i = 0; i < len; i++)
    {
        _str[pos + i] = str[i];
    }
    _size += len;
}
8. Append
代码语言:javascript
复制
void append(const char* str)
{
    size_t len = strlen(str);
    if (_size + len > _capacity)
    {
        reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
    }
    strcpy(_str + _size, str);
    _size += len;
}
9. +=重载

可以直接用之前的函数

代码语言:javascript
复制
string& operator+=(char ch)
{
    push_back(ch);
    return *this;
}

string& operator+=(const char* str)
{
    append(str);
    return *this;
}
10. erase

注意,由于缺省值规定只用在声明中用一次,因此

声明:

代码语言:javascript
复制
void erase(size_t pos, size_t len = npos);

定义:

代码语言:javascript
复制
void erase(size_t pos, size_t len)
{
    assert(pos < _size);
    if (len >= _size - pos)
    {
        _str[pos] = '\0';
        _size = pos;
    }
    else
    {
        for (int i = pos + len; i <= _size; i++)
        {
            _str[i - len] = _str[i];
        }
        _size -= len;
    }
}

但是,我们erase时出bug了:

代码语言:javascript
复制
a.erase(2);

我们调试,看到明明应该是 len >= _size - pos 的分支,却进入了第二个分支

原因: 由于 len + pos 会溢出,因此这个值大小是比 _size 小的

改后就没事了。

此外,如果 int -1size_t 1 比较,依旧会 -1 > 1 的神奇一幕,因为 C++ 兼容 C,会将范围小向范围大的提升,因此 int -1 提升为 2^64-1,出问题。

11. find
代码语言:javascript
复制
size_t find(char ch, size_t pos = 0)
{
    assert(pos < _size);
    for (size_t i = pos; i < _size; i++)
    {
        if (_str[i] == ch) return i;
    }
    return npos;
}

size_t find(const char* str, size_t pos = 0)
{
    assert(pos < _size);
    const char* c = strstr(_str + pos, str);
    if (c == nullptr)
    {
        return npos;
    }
    else
    {
        return (c - _str);
    }
}
12. substr
代码语言:javascript
复制
string substr(size_t pos, size_t len = npos)
{
    assert(pos < _size);
    string tmp;
    if (len > _size - pos)
    {
        len = _size - pos;
    }
    for (size_t i = 0; i < len; i++)
    {
        tmp += _str[i + pos];
    }
    return tmp;
}

但是,我们测试时程序崩溃了:

代码语言:javascript
复制
string a = "hello";
a = a.substr(2);

原因: 没有写拷贝构造,与赋值重载,默认浅拷贝,结束直接析构,无法打印

13. 拷贝构造,赋值重载
代码语言:javascript
复制
string(const string& s)
{
    _str = new char[s._capacity + 1];
    strcpy(_str, s._str);
    _size = s._size;
    _capacity = s._capacity;
}

string& operator=(const string& s)
{
    if (this != &s)
    {
        delete[] _str;
        _str = new char[s._capacity + 1];
        _capacity = s._capacity;
        _size = s._size;
        strcpy(_str, s._str);
    }
    return *this;
}
14. 比较大小

由于前一位不一定是string,我们重载成全局

代码语言:javascript
复制
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 || s1 == s2;
}

bool operator>(const string& s1, const string& s2)
{
    return !(s1 <= s2);
}

bool operator>=(const string& s1, const string& s2)
{
    return !(s1 < s2);
}

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);
}

同时 "sdf" < a 这样子也是可以的

原因: 单参数构造会隐式类型转换,因此 "sdf" 会直接转为 string 并比较

15. io流重载
代码语言:javascript
复制
std::ostream& operator<<(std::ostream& out, const string& s)
{
    for (auto& e : s)
    {
        std::cout << e;
    }
    return out;
}

std::istream& operator>>(std::istream& in, string& s)
{
    char ch;
    ch = in.get();
    while (ch != ' ' && ch != '\n')
    {
        s += ch;
        ch = in.get();
    }
    return in;
}

由于 cinscanf 会忽略空格和换行,因此用 get

那为什么可以不用友元函数?

回顾上一篇博客,我们的方法有:设置成公有(绝对不行),get函数架桥获取私有,友元

但是,由于我们重载了 [] 和设置了迭代器,因此这两个东西可以扮演get函数,以访问私有

但是,这样写刚开始程序会较为频繁的扩容,我们将扩容时打印出来

而扩容,多为异地扩,复杂度较高。对此,我们可以存一个临时字符组buff,存满了再放进去,减少扩容

代码语言:javascript
复制
std::istream& operator>>(std::istream& in, string& s)
{
    s.clear();
    char ch;
    ch = in.get();
    const int N = 256;
    char buff[N];
    int i = 0;
    
    while (ch != ' ' && ch != '\n')
    {
        buff[i++] = ch;
        if (i + 1 == N)
        {
            buff[i] = '\0';
            s += buff;
            i = 0;
        }
        ch = in.get();
    }
    
    if (i > 0)
    {
        buff[i] = '\0';
        s += buff;
    }
    
    return in;
}

这样扩容次数就少了

16. clear
代码语言:javascript
复制
void clear()
{
    _size = 0;
    _str[0] = '\0';
}

这样,一个较为基础的string就实现好了

现代C++

现代C++,就是C++11及以后的写法,开销较少,这一准则的典范就是交换,让别人帮我做事

我们先将拷贝构造换成普通构造

代码语言:javascript
复制
string(const string& s)
{
    string tmp(s._str);  // 普通构造
    std::swap(_str, tmp._str);
    std::swap(_size, tmp._size);
    std::swap(_capacity, tmp._capacity);
}

同样,可以重载一个swap函数交换类

代码语言:javascript
复制
void swap(string& s)
{
    std::swap(_str, s._str);
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
}

string(const string& s)
{
    string tmp(s._str);  // 普通构造
    swap(tmp);
}

赋值重载也可以这么写

代码语言:javascript
复制
string& operator=(string tmp)
{
    swap(tmp);
    return *this;
}

那这么写什么时候能优化呢?

我们看一下库中的swap函数:

不得了,如果不做任何优化,交换string需要进行3次拷贝

但是,用交换以后,就不需要拷贝,交换指针指向即可

并且,编译器也知道这么做很浪费,因此,为string单独重载了一个swap函数,当交换string时直接调用交换指针指向的写法

代码汇总

string.h
代码语言:javascript
复制
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<assert.h>
#include<string.h>

namespace bit
{
	class string
	{
	public:
		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;
		}
		/*string()
			:_str(new char[1]{'\0'})
			,_size(0)
			,_capacity(0)
		{ }
		string(const char* str)
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}*/
		string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
		string(const string& s)
		{
			string tmp(s._str);//普通构造
			swap(tmp);

			/*_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;*/
		}
		void clear()
		{
			_size = 0;
			_str[0] = '\0';
		}
		//string& operator=(const string& s)
		//{
		//	if (this != &s)
		//	{
		//		delete[] _str;
		//		_str = new char[_capacity + 1];
		//		_capacity = s._capacity;
		//		_size = s._size;
		//		strcpy(_str, s._str);
		//	}
		//	return *this;
		//}
		string& operator=(string tmp)
		{
			swap(tmp);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}
		size_t size() const
		{
			return _size;
		}

		size_t capacity() const
		{
			return _capacity;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		const char& operator[](size_t pos) const
		{
			assert(pos < _size);
			return _str[pos];
		}
		void reserve(size_t n);
		void push_back(char ch);
		void append(const char* str);
		string& operator+=(char ch);
		string& operator+=(const char* str);

		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);
		void erase(size_t pos, size_t len = npos);
		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);
		string substr(size_t pos = 0, size_t len = npos);
		~string()
		{
			if (_str)
			{
				delete[] _str;
				_str = nullptr;
				_size = 0;
				_capacity = 0;
			}
		}

	private:
		char* _str=nullptr;
		size_t _capacity=0;
		size_t _size=0;
		static const size_t npos = -1;
	};
	bool operator<(const string& s1, const string& s2);
	bool operator<=(const string& s1, const string& s2);
	bool operator>(const string& s1, const string& s2);
	bool operator>=(const string& s1, const string& s2);
	bool operator==(const string& s1, const string& s2);
	bool operator!=(const string& s1, const string& s2);

	std::ostream& operator<<(std::ostream& out, const string& s);
	std::istream& operator>>(std::istream& in, string& s);
}
string.cpp
代码语言:javascript
复制
#define _CRT_SECURE_NO_WARNINGS
#include"string.h"

namespace bit
{
	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			//std::cout << "reserve:" << n << std::endl;
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}
	void string::push_back(char ch)
	{
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : 2 * _capacity);
		}
		_str[_size] = ch;
		++_size;
		_str[_size] = '\0';
	}
	string& string::operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}
	void string::append(const char* str)
	{
		size_t len = strlen(str);
		if (_size == _capacity)
		{
			reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
		}
		strcpy(_str + _size, str);
		_size += len;
	}
	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}

	void string::insert(size_t pos, char ch)
	{
		assert(pos < _size);
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : 2 * _capacity);
		}
		size_t end = _size + 1;
		while (end > pos)
		{
			_str[end] = _str[end - 1];
			end--;
		}
		_str[end] = ch;
		++_size;
	}
	void string::insert(size_t pos, const char* str)
	{
		assert(pos < _size);
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
		}
		int end = _size + len;
		while (end >= pos + len)
		{
			_str[end] = _str[end - len];
			end--;
		}
		for (int i = 0; i < len; i++)
		{
			_str[pos + i] = str[i];
		}
		_size += len;
	}
	void string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);
		if (len >= _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			for (int i = pos + len; i <= _size; i++)
			{
				_str[i - len] = _str[i];
			}
			_size -= len;

		}
	}
	size_t string::find(char ch, size_t pos)
	{
		assert(pos < _size);
		for (int i = pos; i < _size; i++)
		{
			if (_str[i] == ch)return i;
		}
		return npos;
	}
	size_t string::find(const char* str, size_t pos)
	{
		assert(pos < _size);
		const char* c = strstr(_str + pos, str);
		if (c == nullptr)
		{
			return npos;
		}
		else
		{
			return (c - _str);
		}
	}
	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);
		string tmp;
		if (len > _size - pos)
		{
			len = _size - pos;
		}
		for (int i = 0; i < len; i++)
		{
			tmp += _str[i + pos];
		}
		return tmp;
	}

	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 || s1 == s2;
	}
	bool operator>(const string& s1, const string& s2)
	{
		return !(s1 <= s2);
	}
	bool operator>=(const string& s1, const string& s2)
	{
		return !(s1 < s2);
	}
	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);
	}
	std::ostream& operator<<(std::ostream& out, const string& s)
	{
		for (auto& e : s)
		{
			std::cout << e;
		}
		return out;
	}
	std::istream& operator>>(std::istream& in, string& s)
	{
		s.clear();
		char ch;
		ch = in.get();
		const int N = 256;
		char buff[N];
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i + 1 == N)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}
		return in;
	}
}

写时拷贝(了解)

由于深拷贝是较浪费的,浅拷贝又会有多次析构的问题,能不能将两者结合呢?

因此,我们可以加一个数字,记录了拷贝了多少次,只有要用到了再进行深拷贝,其它时候都是挂着个名头,析构时也会根据数字决定是否析构

但这个方式已经几乎被更好的方式替代,我们未来的博客会讲到

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 模板
  • 1. 函数模板
    • 处理不同类型的参数
    • 模板参数作为返回值
    • 模板与普通函数的优先级
  • 2. 类模板
    • 声明与定义分离
  • C++ string 类详解
    • 简介
    • 1. 使用方法
      • 一、构造函数
      • 二、基本知识
      • 三、输入输出
      • 四、容量操作
      • 五、修改操作
      • 六、读取信息
      • 七、find 家族
      • 八、getline
  • C++ string 类模拟实现
    • 模拟实现
      • 元素
      • 1. 返回地址
      • 2. 构造
      • 3. 析构
      • 4. 遍历
      • 5. 扩容,尾插(较大,声明,定义分离)
      • 6. npos
      • 7. insert
      • 8. Append
      • 9. +=重载
      • 10. erase
      • 11. find
      • 12. substr
      • 13. 拷贝构造,赋值重载
      • 14. 比较大小
      • 15. io流重载
      • 16. clear
    • 现代C++
    • 代码汇总
      • string.h
      • string.cpp
    • 写时拷贝(了解)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档