C++之string类的实现代码及其详解_赋值

C++ 标准库中的std::string是日常开发中最常用的类之一,它封装了字符串的存储与操作,提供了安全、便捷的字符串处理能力。深入理解string类的实现原理,不仅能帮助我们更好地使用标准库,还能掌握 C++ 类设计中的核心技术(如资源管理、拷贝控制等)。本文将从零实现一个简化版的String类,并详细讲解其关键功能的实现逻辑。

一、String 类设计核心要点

一个基础的String类需要包含以下核心功能:

  • 资源管理:动态内存分配与释放(存储字符串数据)
  • 构造函数:默认构造、带参构造、拷贝构造
  • 析构函数:释放动态分配的内存
  • 拷贝控制:拷贝赋值运算符(处理自我赋值问题)
  • 基本操作:获取长度、获取 C 风格字符串、字符串拼接等
  • 运算符重载:=、+、+=、[]、<<等常用运算符

实现过程中需重点关注内存安全(避免内存泄漏、野指针)和异常安全(确保操作过程中发生异常时资源状态一致)。

二、String 类完整实现代码

#include <iostream>#include <cstring>#include <algorithm>  // 用于std::swapclass String {public:    // 1. 构造函数    // 默认构造函数    String() : data_(new char[1]), size_(0) {        data_[0] = '\0';  // 空字符串以'\0'结尾    }    // 带参构造函数(从C风格字符串构造)    String(const char* str) {        if (str == nullptr) {  // 处理空指针输入            size_ = 0;            data_ = new char[1];            data_[0] = '\0';        } else {            size_ = std::strlen(str);            data_ = new char[size_ + 1];  // +1 用于存储'\0'            std::strcpy(data_, str);      // 复制字符串内容        }    }    // 拷贝构造函数(深拷贝)    String(const String& other) : size_(other.size_) {        data_ = new char[size_ + 1];        std::strcpy(data_, other.data_);  // 复制数据    }    // 移动构造函数(C++11,提升性能)    String(String&& other) noexcept : data_(other.data_), size_(other.size_) {        // 接管源对象资源后,将其指针置空避免二次释放        other.data_ = nullptr;        other.size_ = 0;    }    // 2. 析构函数    ~String() {        delete[] data_;  // 释放动态分配的字符数组        data_ = nullptr; // 避免野指针        size_ = 0;    }    // 3. 赋值运算符重载    // 拷贝赋值运算符(深拷贝)    String& operator=(const String& other) {        if (this != &other) {  // 避免自我赋值            // 先释放当前资源            delete[] data_;            // 分配新内存并复制数据            size_ = other.size_;            data_ = new char[size_ + 1];            std::strcpy(data_, other.data_);        }        return *this;    }    // 移动赋值运算符(C++11)    String& operator=(String&& other) noexcept {        if (this != &other) {  // 避免自我赋值            delete[] data_;    // 释放当前资源            // 接管源对象资源            data_ = other.data_;            size_ = other.size_;            // 源对象置空            other.data_ = nullptr;            other.size_ = 0;        }        return *this;    }    // 4. 字符串操作函数    // 获取字符串长度    size_t size() const {        return size_;    }    // 判断字符串是否为空    bool empty() const {        return size_ == 0;    }    // 获取C风格字符串(用于兼容C库函数)    const char* c_str() const {        return data_;    }    // 5. 运算符重载    // 下标访问运算符(非const版本,可修改)    char& operator[](size_t index) {        // 简单越界检查(实际生产环境可抛异常)        if (index >= size_) {            throw std::out_of_range("String index out of range");        }        return data_[index];    }    // 下标访问运算符(const版本,只读)    const char& operator[](size_t index) const {        if (index >= size_) {            throw std::out_of_range("String index out of range");        }        return data_[index];    }    // 字符串拼接运算符    String operator+(const String& other) const {        String result;        result.size_ = size_ + other.size_;        // 分配足够存储两个字符串的内存        delete[] result.data_;  // 释放默认构造的空字符串内存        result.data_ = new char[result.size_ + 1];        // 复制当前字符串        std::strcpy(result.data_, data_);        // 拼接另一个字符串        std::strcat(result.data_, other.data_);        return result;    }    // 字符串拼接赋值运算符    String& operator+=(const String& other) {        *this = *this + other;  // 复用operator+的逻辑        return *this;    }    // 6. 友元函数(用于输出字符串)    friend std::ostream& operator<<(std::ostream& os, const String& str) {        os << str.data_;  // 输出字符串内容        return os;    }    // 交换两个字符串的资源(用于实现拷贝并交换 idiom)    void swap(String& other) noexcept {        std::swap(data_, other.data_);        std::swap(size_, other.size_);    }private:    char* data_;    // 存储字符串数据(以'\0'结尾)    size_t size_;   // 字符串长度(不包含'\0')};// 全局swap函数(用于ADL查找)void swap(String& a, String& b) noexcept {    a.swap(b);}

三、关键功能详解

1. 数据成员设计

String类包含两个核心成员:

  • char* data_:指向动态分配的字符数组,存储字符串内容(以'\0'结尾,兼容 C 风格字符串)
  • size_t size_:记录字符串长度(不包含结尾的'\0'),避免每次调用size()时都计算长度

这种设计平衡了存储效率和访问效率,符合std::string的实现思路(尽管标准库可能包含更多优化,如 SSO 短字符串优化)。

2. 构造函数实现

(1)默认构造函数

默认构造函数初始化一个空字符串:

String() : data_(new char[1]), size_(0) {    data_[0] = '\0';}
  • 分配大小为 1 的字符数组(仅存储'\0')
  • 确保任何String对象都持有有效指针,避免后续操作出错

(2)带参构造函数

从 C 风格字符串(const char*)构造:

String(const char* str) {    if (str == nullptr) {  // 处理空指针输入        size_ = 0;        data_ = new char[1];        data_[0] = '\0';    } else {        size_ = std::strlen(str);        data_ = new char[size_ + 1];  // +1 留作'\0'位置        std::strcpy(data_, str);    }}
  • 先判断输入是否为空指针(容错处理)
  • 使用strlen计算字符串长度,strcpy复制内容
  • 必须分配size_ + 1的内存以存储结尾的'\0'

(3)拷贝构造函数

实现深拷贝(避免浅拷贝导致的 double free 问题):

String(const String& other) : size_(other.size_) {    data_ = new char[size_ + 1];    std::strcpy(data_, other.data_);}
  • 为新对象分配独立内存
  • 复制源对象的所有数据(而非仅复制指针)
  • 确保两个对象修改各自数据时互不影响

(4)移动构造函数(C++11)

用于处理右值引用,避免不必要的拷贝:

String(String&& other) noexcept : data_(other.data_), size_(other.size_) {    other.data_ = nullptr;    other.size_ = 0;}
  • 直接接管源对象(右值)的资源,而非复制
  • 将源对象的指针置空,避免析构时二次释放
  • noexcept声明确保移动操作不会抛出异常,提升容器使用时的性能

3. 析构函数

析构函数负责释放动态分配的内存:

~String() {    delete[] data_;  // 使用delete[]释放数组    data_ = nullptr; // 置空指针避免野指针    size_ = 0;}
  • 必须使用delete[]而非delete,因为data_指向数组
  • 置空指针是防御性编程,避免后续误用已释放的指针

4. 赋值运算符重载

(1)拷贝赋值运算符

处理对象间的赋值操作,需注意自我赋值问题:

String& operator=(const String& other) {    if (this != &other) {  // 检查自我赋值        delete[] data_;    // 释放当前资源        size_ = other.size_;        data_ = new char[size_ + 1];        std::strcpy(data_, other.data_);    }    return *this;}
  • 自我赋值检查:当this == &other时直接返回,避免释放自身资源后拷贝
  • 先释放当前资源,再分配新内存并复制数据
  • 返回*this允许链式赋值(如a = b = c)

(2)移动赋值运算符

高效处理右值对象的赋值:

String& operator=(String&& other) noexcept {    if (this != &other) {        delete[] data_;        data_ = other.data_;        size_ = other.size_;        other.data_ = nullptr;        other.size_ = 0;    }    return *this;}
  • 与移动构造函数类似,通过接管资源提升性能
  • 特别适合临时对象的赋值场景(如函数返回值)

5. 运算符重载

(1)下标访问运算符

提供类似数组的访问方式:

char& operator[](size_t index) {    if (index >= size_) {        throw std::out_of_range("String index out of range");    }    return data_[index];}const char& operator[](size_t index) const {    // 实现同上}
  • 提供非 const 和 const 两个版本,分别用于可修改和只读场景
  • 包含越界检查(实际标准库的operator[]通常不检查,而at()函数才检查)

(2)字符串拼接运算符

实现字符串的拼接功能:

String operator+(const String& other) const {    String result;    result.size_ = size_ + other.size_;    delete[] result.data_;  // 释放默认构造的内存    result.data_ = new char[result.size_ + 1];    std::strcpy(result.data_, data_);    std::strcat(result.data_, other.data_);    return result;}
  • 创建新对象存储拼接结果
  • 先复制当前字符串,再拼接另一个字符串
  • operator+=通过复用operator+实现,简化代码

6. 友元函数:输出运算符

允许使用cout << string直接输出字符串:

friend std::ostream& operator<<(std::ostream& os, const String& str) {    os << str.data_;    return os;}
  • 作为友元函数可直接访问私有成员data_
  • 输出data_(以'\0'结尾)符合 C++ 流输出的要求

四、使用示例

int main() {    // 构造函数测试    String s1;                  // 默认构造    String s2("hello");         // 从C字符串构造    String s3 = s2;             // 拷贝构造    String s4 = std::move(s3);  // 移动构造(s3变为空)    // 赋值运算符测试    String s5;    s5 = s2;                    // 拷贝赋值    String s6;    s6 = String("world");       // 移动赋值    // 字符串操作测试    std::cout << "s2: " << s2 << ", size: " << s2.size() << std::endl;    std::cout << "s2[1]: " << s2[1] << std::endl;  // 输出 'e'    // 拼接测试    String s7 = s2 + s6;    std::cout << "s2 + s6: " << s7 << std::endl;  // 输出 "helloworld"    s2 += s6;    std::cout << "s2 += s6: " << s2 << std::endl;  // 输出 "helloworld"    return 0;}

五、进阶优化方向

实际std::string的实现更为复杂,包含更多优化:

  1. 短字符串优化(SSO):对于短字符串,直接存储在栈上的缓冲区,避免动态内存分配
  2. 引用计数:部分实现使用写时复制(Copy-On-Write)策略,减少不必要的拷贝
  3. 预留容量:提供reserve()方法预分配内存,减少频繁扩容的开销
  4. 迭代器支持:实现随机访问迭代器,兼容 STL 算法
  5. 更多字符串操作:如find()、substr()、replace()等实用函数