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