C++中的string
类是标准库提供的用于处理字符串的类,它比C风格的字符数组(char[]
)更安全、更方便。
1. 基本使用
包含头文件
#include <string>
using namespace std; // 或者使用 std::string
声明和初始化
string s1; // 默认初始化,空字符串
string s2 = "hello"; // 拷贝初始化
string s3("world"); // 直接初始化
string s4(5, 'a'); // 包含5个'a'的字符串 "aaaaa"
string s5(s2); // 拷贝构造,s5是s2的副本
2. 常用操作
字符串连接
string s = "hello";
s += " world"; // s变为 "hello world"
string s2 = s + "!"; // s2为 "hello world!"
访问字符
string s = "hello";
char c1 = s[0]; // 'h' (不检查边界)
char c2 = s.at(1); // 'e' (会检查边界,越界抛出异常)
字符串比较
string s1 = "apple";
string s2 = "banana";
if (s1 < s2) { // 按字典序比较
cout << "apple comes before banana";
}
容量操作
string s = "hello";
cout << s.size(); // 输出5
cout << s.length(); // 输出5
string s;
s.reserve(100);//返回当前为字符串分配的总空间大小(以字符为单位)
//capacity >= size
//字符串自动增长时,capacity通常会以某种策略(如倍增)增加
cout << s.capacity(); // 输出至少100(可能更大)
string s1;
string s2 = "hello";
cout << s1.empty(); // 输出1(true)
cout << s2.empty(); // 输出0(false)
string s = "hello";
s.clear();//清空字符串内容,使其变为空字符串
cout << s.size(); // 输出0
cout << s.capacity(); // 可能仍为之前的大小
//注意:只清空内容,不释放内存(capacity通常不变)等效于erase(begin(), end());
reserve()详细
void reserve(size_t n = 0);
- 功能:预留存储空间,避免频繁重新分配
- 特点:
- 如果n大于当前capacity,则重新分配至少能容纳n个字符的空间
- 如果n小于当前capacity,实现可能忽略此请求(非强制缩小)
- 不影响字符串内容或size
- 使用场景:预先知道字符串大致大小时,可提高性能
- 示例:
string s;
s.reserve(1000); // 预先分配空间
for(int i = 0; i < 1000; ++i) {
s += 'a'; // 不会频繁扩容
}
resize()详细
void resize(size_t n);
void resize(size_t n, char c);
- 功能:改变字符串的size(有效字符数)
- 参数:
- n:新的size值
- c:填充字符(仅用于第二个重载)
- 行为:
- 如果n < size():截断到前n个字符
- 如果n > size():
- 单参数版本:用
'\0'
填充(不推荐) - 双参数版本:用字符c填充
- 单参数版本:用
- 可能影响capacity(如果需要更多空间)
- 示例:
string s = "hello";
s.resize(3); // s变为"hel"
s.resize(5, 'x'); // s变为"helxx"
s.resize(10); // s变为"helxx\0\0\0\0\0"(不推荐这样用)
综合比较表
函数 | 功能 | 是否改变内容 | 是否可能改变capacity |
---|---|---|---|
size()/length() | 获取长度 | 否 | 否 |
capacity() | 获取容量 | 否 | 否 |
empty() | 检查是否为空 | 否 | 否 |
clear() | 清空内容 | 是 | 通常否 |
reserve() | 预留空间 | 否 | 可能增加 |
resize() | 调整大小 | 是 | 可能增加 |
使用技巧
- 需要频繁追加内容时,使用
reserve()
预先分配空间 - 检查字符串是否为空时,优先使用
empty()
而非size() == 0
- 使用
resize()
时,建议指定填充字符,避免不可见字符 - 需要彻底释放内存时(C++11起):
string s;
s.resize(1000);
// ...使用后...
s.clear();
s.shrink_to_fit(); // 请求减少capacity以匹配size
常用成员函数
string s = "hello";
s.size(); // 或 s.length(),返回5
s.empty(); // 判断是否为空
s.clear(); // 清空字符串
s.substr(1, 3); // 从位置1开始,取3个字符,返回"ell"
s.find("ll"); // 返回2,
s.find("ll", 1, 3);// 只查找"ell",返回2
//首次出现的位置如果找到,返回子串/字符首次出现的位置(从0开始的下标)
//如果未找到,返回string::npos(一个特殊常量,通常是size_t的最大值)
s.replace(2, 2, "xx"); // 从位置2开始,替换2个字符为"xx",变为"hexxo"
s.insert(1, "yy"); // 在位置1插入"yy",变为"hyyello"
s.erase(1, 2); // 从位置1开始删除2个字符,变为"hlo"
3. 输入输出
输入
string s;
cin >> s; // 读取一个单词(遇到空格停止)
getline(cin, s); // 读取一行(包括空格)
输出
string s = "hello";
cout << s << endl;
4. 字符串与数值转换
C++11引入了方便的转换函数:
// 字符串转数值
int i = stoi("42"); // string to int
double d = stod("3.14"); // string to double
// 数值转字符串
string s1 = to_string(42); // "42"
string s2 = to_string(3.14); // "3.140000"
5. 字符串遍历
可以使用多种方式遍历字符串:
string s = "hello";
// 1. 下标访问
for (size_t i = 0; i < s.size(); ++i) {
cout << s[i];
}
// 2. 迭代器
for (auto it = s.begin(); it != s.end(); ++it) {
cout << *it;
}
// 3. 范围for循环 (C++11)
for (char c : s) {
cout << c;
}
6. 注意事项
string
是动态大小的,不需要预先指定长度string
会自动管理内存,不需要手动分配/释放string
以'\0'
结尾- 与C字符串互操作:
string s = "hello"; const char* cstr = s.c_str(); // 获取C风格字符串
7.vs和g++下string结构的说明
注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
- vs下string的结构
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间:
- 当字符串长度小于16时,使用内部固定的字符数组来存放
- 当字符串长度大于等于16时,从堆上开辟空间
union _Bxty
{ // storage for small buffer or pointer to larger one
value_type _Buf[_BUF_SIZE];
pointer _Ptr;
char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建
好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量
最后:还有一个指针做一些其他事情。
故总共占16+4+4+4=28个字节。
- g++下string的结构
G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个
指针,该指针将来指向一块堆空间,内部包含了如下字段: - 空间总大小
- 字符串有效长度
- 引用计数
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
- 指向堆空间的指针,用来存储字符串。
8.模拟实现
深浅拷贝问题
1. 默认拷贝构造函数的行为
当类中没有显式定义拷贝构造函数时,C++编译器会为我们生成一个默认的拷贝构造函数,它的行为是:
对类的每个成员进行逐成员拷贝:
- 对于基本类型(int, float, pointer等):直接复制值
- 对于类类型:调用该类的拷贝构造函数
对于我们的string
类:
class string {
char* _str; // 指针
size_t _size; // 基本类型
size_t _capacity; // 基本类型
};
默认拷贝构造函数相当于:
string(const string& other)
: _str(other._str), // 仅复制指针值
_size(other._size), // 复制值
_capacity(other._capacity) {} // 复制值
2. 浅拷贝的问题演示
假设有以下代码:
string s1("hello");
string s2 = s1; // 调用默认拷贝构造函数
内存布局如下:
s1:
_str -> "hello\0"
_size -> 5
_capacity-> 5
s2:
_str -> (与s1._str相同的内存地址)
_size -> 5
_capacity-> 5
3. 浅拷贝导致的问题
问题1:双重释放(Double Free)
当这两个对象析构时:
~string() { delete[] _str; }
会发生:
s2
先析构,释放了_str
指向的内存s1
再析构,尝试释放同一块内存- 程序崩溃(未定义行为)
问题2:修改相互影响
s1[0] = 'H'; // 修改s1
std::cout << s2[0]; // 输出'H',因为s2和s1共享同一内存
问题3:悬空指针(Dangling Pointer)
如果其中一个对象先被修改:
string s3("world");
s1 = s3; // s1现在指向"world"
// s2._str现在指向什么?
4. 图解浅拷贝与深拷贝
浅拷贝(错误方式)
s1: [_str:0x1000] -> "hello"
s2: [_str:0x1000]
两个对象共享同一内存
深拷贝(正确方式)
s1: [_str:0x1000] -> "hello"
s2: [_str:0x2000] -> "hello"
每个对象有自己的内存副本
5. 为什么其他类成员没问题?
_size
和_capacity
是基本类型,直接复制值是完全安全的- 只有指针成员
_str
需要特殊处理,因为我们需要复制指针指向的内容,而不是指针本身的值
6. 解决方案:实现深拷贝
正确的拷贝构造函数应该:
string(const string& other)
: _str(new char[other._capacity + 1]), // 分配新内存
_size(other._size),
_capacity(other._capacity) {
strcpy(_str, other._str); // 复制内容
}
这样每个string对象都拥有自己独立的字符数组存储。
传统写法和现代写法
- 传统写法
namespace sty {
class string {
public:
// 传统写法 - 默认构造函数
string(const char* str = "") {
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 传统写法 - 拷贝构造函数
string(const string& s) {
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
// 传统写法 - 赋值运算符
string& operator=(const string& s) {
if (this != &s) {
// 先删除原有数据
delete[] _str;
// 分配新空间并拷贝数据
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
return *this;
}
~string() {
delete[] _str;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
- 现代写法
传值调用调用时隐式调用拷贝构造函数创建临时对象,交换资源将临时对象的资源与当前对象交换,临时对象析构时自动清理旧资源,从而实现现代写法
namespace sty {
class string {
public:
// 现代写法 - 默认构造函数
string(const char* str = "")
: _size(strlen(str))
, _capacity(_size)
, _str(new char[_capacity + 1]) {
strcpy(_str, str);
}
// 现代写法 - 拷贝构造函数 (copy-and-swap)
string(const string& s)
: _str(nullptr)
, _size(0)
, _capacity(0) {
string temp(s._str);
swap(temp);
}
// 现代写法 - 赋值运算符 (copy-and-swap)
string& operator=(string s) { // 注意这里是传值,不是引用
swap(s);
return *this;
}
~string() {
delete[] _str;
}
void swap(string& other) noexcept {
std::swap(_str, other._str);
std::swap(_size, other._size);
std::swap(_capacity, other._capacity);
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
总代码
#include <iostream>
#include <cstring> // strlen, strcpy等
#include <algorithm> // std::swap
#include <stdexcept> // std::out_of_range
#include <cassert> // assert
namespace sty {
class string {
public:
// 类型定义
typedef char* iterator;
// 默认构造函数
string(const char* str = "") {
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝构造函数
string(const string& s) : _str(nullptr), _size(0), _capacity(0) {
string tmp(s._str);
this->swap(tmp);
}
// 赋值运算符
string& operator=(const string& s) {
if (this != &s) {
string temp(s);
this->swap(temp);
}
return *this;
}
// 析构函数
~string() {
if (_str) {
delete[] _str;
_str = nullptr;
}
}
/******************** 迭代器相关 ********************/
iterator begin() { return _str; }
iterator end() { return _str + _size; }
/******************** 修改操作 ********************/
// 在末尾添加字符
void push_back(char c) {
if (_size == _capacity) {
reserve(_capacity * 2);
}
_str[_size++] = c;
_str[_size] = '\0';
}
// += 字符重载
string& operator+=(char c) {
push_back(c);
return *this;
}
// 追加字符串
void append(const char* str) {
size_t len = strlen(str);
if (_size + len > _capacity) {
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
// += 字符串重载
string& operator+=(const char* str) {
append(str);
return *this;
}
// 清空字符串
void clear() {
_size = 0;
_str[_size] = '\0';
}
// 交换两个字符串
void swap(string& s) {
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 获取C风格字符串
const char* c_str() const {
return _str;
}
/******************** 容量相关 ********************/
// 获取字符串长度
size_t size() const {
return _size;
}
// 获取容量
size_t capacity() const {
return _capacity;
}
// 判断是否为空
bool empty() const {
return _size == 0;
}
// 调整字符串大小
void resize(size_t newSize, char c = '\0') {
if (newSize > _size) {
if (newSize > _capacity) {
reserve(newSize);
}
memset(_str + _size, c, newSize - _size);
}
_size = newSize;
_str[newSize] = '\0';
}
// 预留空间
void reserve(size_t newCapacity) {
if (newCapacity > _capacity) {
char* str = new char[newCapacity + 1];
strcpy(str, _str);
delete[] _str;
_str = str;
_capacity = newCapacity;
}
}
/******************** 访问元素 ********************/
// 下标运算符重载
char& operator[](size_t index) {
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index) const {
assert(index < _size);
return _str[index];
}
/******************** 关系运算符 ********************/
bool operator<(const string& s) const {
return strcmp(_str, s._str) < 0;
}
bool operator>(const string& s) const {
return strcmp(_str, s._str) > 0;
}
bool operator<=(const string& s) const {
return !(*this > s);
}
bool operator>=(const string& s) const {
return !(*this < s);
}
bool operator==(const string& s) const {
return strcmp(_str, s._str) == 0;
}
bool operator!=(const string& s) const {
return !(*this == s);
}
/******************** 查找操作 ********************/
// 查找字符
size_t find(char c, size_t pos = 0) const {
for (size_t i = pos; i < _size; ++i) {
if (_str[i] == c) {
return i;
}
}
return npos;
}
// 查找子串
size_t find(const char* s, size_t pos = 0) const {
assert(s);
assert(pos < _size);
const char* src = _str + pos;
while (*src) {
const char* match = s;
const char* cur = src;
while (*match && *match == *cur) {
++match;
++cur;
}
if (*match == '\0') {
return src - _str;
}
else {
++src;
}
}
return npos;
}
/******************** 插入操作 ********************/
// 插入字符
string& insert(size_t pos, char c) {
assert(pos <= _size);
if (_size == _capacity) {
reserve(_capacity * 2);
}
for (int i = _size; i >= (int)pos; --i) {
_str[i + 1] = _str[i];
}
_str[pos] = c;
_size++;
return *this;
}
// 插入字符串
string& insert(size_t pos, const char* str) {
size_t len = strlen(str);
if (_size + len > _capacity) {
reserve(_size + len);
}
for (int i = _size; i >= (int)pos; --i) {
_str[len + i] = _str[i];
}
while (*str != '\0') {
_str[pos++] = *str++;
}
_size += len;
return *this;
}
/******************** 删除操作 ********************/
// 删除字符
string& erase(size_t pos, size_t len) {
assert(pos < _size);
if (pos + len >= _size) {
_str[pos] = '\0';
_size = pos;
}
else {
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
private:
char* _str; // 字符串数据指针
size_t _capacity; // 容量(不包括'\0')
size_t _size; // 当前长度(不包括'\0')
// 静态成员
static const size_t npos = -1;
// 友元函数声明
friend std::ostream& operator<<(std::ostream& out, const string& s);
friend std::istream& operator>>(std::istream& in, string& s);
};
/******************** 流操作符重载 ********************/
// 输出流重载
std::ostream& operator<<(std::ostream& out, const string& s) {
for (size_t i = 0; i < s.size(); ++i) {
out << s[i];
}
return out;
}
// 输入流重载
std::istream& operator>>(std::istream& in, string& s) {
s.clear(); // 先清空原有内容
char ch;
while (in.get(ch) && !isspace(ch)) {
s.push_back(ch);
}
return in;
}
}