在C++开发中,string作为STL核心容器之一,是处理字符串的"瑞士军刀"。它封装了字符数组的底层细节,提供了安全、高效的字符串操作接口,彻底解决了C语言字符数组的越界风险与内存管理痛点。本文将从基础认知、核心接口解析、实战测试到模拟实现,全方位拆解string,帮你真正吃透这个常用容器。
一、string 基础认知
1.1 本质与定位
string 定义于<string>头文件中,属于std命名空间,其本质是**basic_string<char>**的模板特化。它内部维护了三个核心指针(或类似结构):
-
指向字符串起始位置的指针(_Ptr)
-
指向字符串末尾('\0'前)的指针(_Myend)
-
指向内存缓冲区末尾的指针(_Alend)
这种设计实现了"容量(capacity)"与"大小(size)"的分离:size是当前字符串的实际长度,capacity是已分配内存可容纳的最大长度(不含'\0'),当size超过capacity时会触发自动扩容。
1.2 初始化方式
string提供了多种初始化方式,覆盖不同使用场景,实战中需根据需求选择:
cpp
#include <string>
#include <iostream>
using namespace std;
int main() {
string s1; // 1. 默认初始化:空字符串,size=0,capacity通常为15(VS下)或动态分配
string s2("hello world"); // 2. 字符串常量初始化
string s3(5, 'a'); // 3. 重复字符初始化:5个'a',结果为"aaaaa"
string s4(s2); // 4. 拷贝构造
string s5(s2, 6, 5); // 5. 子串初始化:从s2[6]开始取5个字符,结果为"world"
string s6(s2.begin(), s2.end()-5); // 6. 迭代器初始化:取s2前6个字符,结果为"hello "
string s7 = "direct init"; // 7. 赋值初始化(等价于string s7("direct init"))
cout << s5 << endl; // 输出:world
return 0;
}
二、核心接口详解与实战测试
string接口较多,本文按"基础查询、修改操作、字符串操作、迭代器"四大类讲述,每个接口均有测试代码与结果分析。
2.1 基础查询接口
用于获取字符串的大小、容量等核心信息,是内存管理与性能优化的基础。
| 接口 | 功能说明 | 测试代码 | 输出结果 |
|---|---|---|---|
| size() / length() | 返回字符串实际长度(不含'\0'),两者功能完全一致 | string s("test"); cout<<s.size()<<","<<s.length(); | 4,4 |
| capacity() | 返回当前内存缓冲区容量,即最多可存多少字符(不含'\0') | string s; for(int i=0;i<20;i++)s+= 'a'; cout<<s.capacity(); | 31(VS下扩容策略:15→31→63...) |
| empty() | 判断字符串是否为空,等价于size()==0,效率更高 | string s1; string s2("a"); cout<<s1.empty()<<","<<s2.empty(); | 1,0(1为true,0为false) |
| max_size() | 返回string可容纳的最大字符数(受系统内存限制) | string s; cout<<s.max_size(); | 4294967294(32位系统典型值) |
| reserve(n) | 预分配n个字符的容量,仅扩容不缩容,减少自动扩容开销 | string s; s.reserve(100); cout<<s.capacity(); | 100 |
| resize(n, c) | 将size调整为n,不足补c(默认'\0'),超出则截断 | string s("hello"); s.resize(8, 'x'); cout<<s; | helloxxx |
| shrink_to_fit() | 将capacity缩减至与size一致,释放多余内存(C++11) | string s(20, 'a'); s.resize(5); s.shrink_to_fit(); cout<<s.capacity(); | 5 |
2.2 元素访问接口
提供多种方式访问字符串中的单个字符,需注意越界问题。
cpp
void test_access() {
string s("hello world");
// 1. operator[]:无越界检查,越界行为未定义
cout << s[4] << endl; // 输出:o
// cout << s[20] << endl; // 危险:可能崩溃或乱码
// 2. at():有越界检查,越界抛out_of_range异常
try {
cout << s.at(10) << endl; // 输出:d
s.at(20);
} catch (out_of_range& e) {
cout << "异常:" << e.what() << endl; // 输出异常信息
}
// 3. 首尾字符访问
cout << s.front() << "," << s.back() << endl; // 输出:h,d
// 4. 转换为C风格字符串
const char* c_str = s.c_str();
cout << c_str << endl; // 输出:hello world
}
int main() {
test_access();
return 0;
}
注意:operator[]效率高于at(),但缺乏安全性;对用户输入等不确定场景,可以使用at()或先通过size()判断下标合法性。
2.3 修改操作接口
字符串的增删改操作是核心,熟练拼接、插入、删除等接口的使用场景。
2.3.1 拼接操作
cpp
void test_append() {
string s("hello");
// 1. operator+=:最简洁的拼接方式
s += " ";
s += 'w';
cout << s << endl; // 输出:hello w
// 2. append():支持多种拼接形式
s.append("orld"); // 拼接字符串
cout << s << endl; // 输出:hello world
string s2("!!!");
s.append(s2); // 拼接另一个string
cout << s << endl; // 输出:hello world!!!
s.append(3, '-'); // 拼接3个'-'
cout << s << endl; // 输出:hello world!!!---
}}
2.3.2 插入与删除
cpp
void test_insert_erase() {
string s("abcdef");
// 1. insert(pos, ...):在pos位置插入内容
s.insert(3, "123"); // 在索引3处插入"123"
cout << s << endl; // 输出:abc123def
s.insert(s.begin()+1, 'x'); // 迭代器位置插入字符
cout << s << endl; // 输出:axbc123def
// 2. erase(...):删除指定内容
s.erase(4, 3); // 从索引4开始删除3个字符(删除"123")
cout << s << endl; // 输出:axbcdef
s.erase(s.begin()+1); // 删除迭代器指向的字符
cout << s << endl; // 输出:abcdef
// 3. clear():清空字符串,size=0但capacity不变
s.clear();
cout << s.size() << "," << s.capacity() << endl; // 输出:0,15(VS下)
}}
2.3.3 替换操作
cpp
void test_replace() {
string s("I like C++");
// 1. 替换指定范围的字符
s.replace(7, 3, "Python"); // 从索引7开始,替换3个字符为"Python"
cout << s << endl; // 输出:I like Python
// 2. 用迭代器指定范围
s.replace(s.begin(), s.begin()+1, "We"); // 替换开头1个字符为"We"
cout << s << endl; // 输出:We like Python
// 3. 替换为重复字符
s.replace(3, 4, 5, 'x'); // 从索引3开始,替换4个字符为5个'x'
cout << s << endl; // 输出:We xxxxx Python
}}
2.4 字符串操作接口
包含子串提取、查找、比较等操作。
2.4.1 子串提取
cpp
void test_substr() {
string s("hello world");
// substr(pos, len):从pos开始取len个字符,len默认取到末尾
string sub1 = s.substr(6); // 从索引6开始取到末尾
string sub2 = s.substr(0, 5); // 从索引0开始取5个字符
cout << sub1 << "," << sub2 << endl; // 输出:world,hello
}
2.4.2 查找操作
string提供还提供了查找接口,返回匹配的起始索引,未找到返回**string::npos**(值为-1,无符号数表现为极大值)。
cpp
void test_find() {
string s("ababcabcdabcde");
string target("abc");
// 1. 从开头查找
size_t pos1 = s.find(target);
cout << pos1 << endl; // 输出:2(第一个"abc"的起始索引)
// 2. 从指定位置开始查找
size_t pos2 = s.find(target, 3);
cout << pos2 << endl; // 输出:5(从索引3开始的第一个"abc")
// 3. 从末尾查找(rfind)
size_t pos3 = s.rfind(target);
cout << pos3 << endl; // 输出:9(最后一个"abc"的起始索引)
// 4. 查找单个字符
size_t pos4 = s.find('d');
cout << pos4 << endl; // 输出:8
// 5. 检查是否找到
if (s.find("xyz") == string::npos) {
cout << "未找到目标字符串" << endl;
}
}
2.4.3 字符串比较
**operator==、!=、<、>**等已重载,内部按ASCII码逐字符比较;**compare()**接口返回整数,更灵活。
cpp
void test_compare() {
string s1("apple");
string s2("banana");
string s3("apple");
// 1. 运算符比较
cout << (s1 == s3) << endl; // 1(true)
cout << (s1 < s2) << endl; // 1('a'的ASCII小于'b')
// 2. compare()接口:返回0(相等)、正数(s1大)、负数(s1小)
int ret1 = s1.compare(s3);
int ret2 = s1.compare(s2);
int ret3 = s2.compare(s1);
cout << ret1 << "," << ret2 << "," << ret3 << endl; // 0,-1,1
// 3. 比较子串
int ret4 = s1.compare(1, 3, s3, 1, 3); // 比较s1[1-3]与s3[1-3]
cout << ret4 << endl; // 0(均为"ppl")
}}
三、string 模拟实现(核心功能)
理解string的底层实现,能更深刻地掌握其特性。下面模拟实现string的核心功能,包括构造、拷贝、赋值、析构及常用接口,采用**"深拷贝"**策略(可用C++常用移动语义优化,此处为基础实现)。
3.1 类结构设计
核心成员变量:_str(字符数组指针)、_size(实际长度)、_capacity(容量),并定义静态常量npos。
3.2 完整实现代码
cpp
#include <iostream>
#include <cstring>
#include <exception>
using namespace std;
class MyString {
public:
// 静态常量:表示未找到
static const size_t npos;
// 1. 构造函数
// 默认构造
MyString(const char* str = "") {
// 计算字符串长度
_size = strlen(str);
// 容量初始化:至少为1(存'\0'),此处简化为_size(实际可优化为15等固定值)
_capacity = _size == 0 ? 1 : _size;
// 分配内存(+1用于存'\0')
_str = new char[_capacity + 1];
// 拷贝字符串
strcpy(_str, str);
}
// 重复字符构造
MyString(size_t n, char c) {
_size = n;
_capacity = n;
_str = new char[_capacity + 1];
// 填充n个c
memset(_str, c, n);
// 末尾置'\0'
_str[_size] = '\0';
}
// 2. 拷贝构造(深拷贝)
MyString(const MyString& s) {
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
// 3. 赋值运算符重载(深拷贝)
MyString& operator=(const MyString& s) {
// 防止自赋值
if (this != &s) {
// 释放原有内存
delete[] _str;
// 拷贝新内容
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
return *this;
}
// 4. 析构函数
~MyString() {
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
// 5. 基础查询接口
size_t size() const { return _size; }
size_t capacity() const { return _capacity; }
bool empty() const { return _size == 0; }
// 6. 元素访问接口
char& operator[](size_t pos) {
// 简化版检查:实际可抛异常
if (pos >= _size) {
throw out_of_range("MyString::operator[]: pos out of range");
}
return _str[pos];
}
const char& operator[](size_t pos) const {
if (pos >= _size) {
throw out_of_range("MyString::operator[]: pos out of range");
}
return _str[pos];
}
char& front() { return _str[0]; }
const char& front() const { return _str[0]; }
char& back() { return _str[_size - 1]; }
const char& back() const { return _str[_size - 1]; }
// 7. 修改操作接口
// 扩容接口(核心辅助函数)
void reserve(size_t n) {
if (n > _capacity) {
char* newStr = new char[n + 1];
strcpy(newStr, _str);
delete[] _str;
_str = newStr;
_capacity = n;
}
}
void resize(size_t n, char c = '\0') {
if (n < _size) {
// 缩小:直接截断,置'\0'
_size = n;
_str[_size] = '\0';
} else if (n > _size) {
// 扩大:先扩容,再填充c
reserve(n);
for (size_t i = _size; i < n; i++) {
_str[i] = c;
}
_size = n;
_str[_size] = '\0';
}
}
MyString& operator+=(char c) {
if (_size == _capacity) {
// 扩容策略:翻倍(实际可优化为1.5倍)
reserve(_capacity == 0 ? 1 : _capacity * 2);
}
_str[_size++] = c;
_str[_size] = '\0';
return *this;
}
MyString& operator+=(const char* str) {
size_t len = strlen(str);
if (_size + len > _capacity) {
reserve(_size + len);
}
strcat(_str, str);
_size += len;
return *this;
}
MyString& append(const MyString& s) {
return *this += s._str;
}
// 插入字符(简化版:在pos位置插入c)
void insert(size_t pos, char c) {
if (pos > _size) {
throw out_of_range("MyString::insert: pos out of range");
}
if (_size == _capacity) {
reserve(_capacity * 2);
}
// 从后往前移动字符
for (size_t i = _size; i > pos; i--) {
_str[i] = _str[i - 1];
}
_str[pos] = c;
_size++;
_str[_size] = '\0';
}
// 删除字符(简化版:从pos开始删除n个字符)
void erase(size_t pos, size_t n = npos) {
if (pos > _size) {
throw out_of_range("MyString::erase: pos out of range");
}
// 计算实际要删除的长度
size_t len = n == npos ? _size - pos : n;
// 从pos+len位置往前覆盖
for (size_t i = pos; i < _size - len; i++) {
_str[i] = _str[i + len];
}
_size -= len;
_str[_size] = '\0';
}
// 8. 字符串操作接口
MyString substr(size_t pos, size_t n = npos) const {
if (pos > _size) {
throw out_of_range("MyString::substr: pos out of range");
}
size_t len = n == npos ? _size - pos : n;
MyString sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++) {
sub._str[i] = _str[pos + i];
}
sub._size = len;
sub._str[sub._size] = '\0';
return sub;
}
size_t find(const char* str, size_t pos = 0) const {
if (pos > _size) {
return npos;
}
// 调用C库函数strstr
const char* ret = strstr(_str + pos, str);
if (ret == nullptr) {
return npos;
}
return ret - _str;
}
// 重载<<运算符,便于输出
friend ostream& operator<<(ostream& os, const MyString& s) {
os << s._str;
return os;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
// 初始化静态常量
const size_t MyString::npos = -1;
// 测试模拟实现的MyString
void test_MyString() {
MyString s1("hello");
cout << "s1: " << s1 << ", size: " << s1.size() << ", capacity: " << s1.capacity() << endl;
MyString s2(5, 'a');
cout << "s2: " << s2 << endl;
s1 += ' ';
s1 += "world";
cout << "s1 after append: " << s1 << endl;
s1.insert(5, 'x');
cout << "s1 after insert: " << s1 << endl;
s1.erase(5, 1);
cout << "s1 after erase: " << s1 << endl;
MyString sub = s1.substr(6, 5);
cout << "substr: " << sub << endl;
size_t pos = s1.find("world");
cout << "find 'world' at: " << pos << endl;
}
int main() {
test_MyString();
return 0;
}
3.3 实现说明
-
内存管理:通过new[]分配内存,析构函数中delete[]释放,避免内存泄漏;扩容时采用"翻倍"策略,平衡性能与内存开销。
-
深拷贝:拷贝构造与赋值运算符重载均实现深拷贝,确保多个对象独立管理内存,避免浅拷贝导致的多次释放的问题。
-
接口设计:模仿STL string的接口命名与功能,降低使用成本,同时添加基础的越界检查。
四、总结
4.1 核心要点
-
string本质是字符数组的封装,核心是size与capacity的分离管理。
-
优先使用empty()判断空串,reserve()预扩容可提升性能,避免频繁自动扩容。
-
元素访问用at()更安全,查找操作需判断返回值是否为string::npos。
-
底层实现依赖动态内存分配,深拷贝是保证独立性的关键。
string是C++开发的基础工具,熟练掌握其接口与底层逻辑,能大幅提升字符串处理的效率与安全性。建议结合本文代码反复测试,在实际项目中灵活运用各类接口,逐步积累实战经验。
如果本文对你有帮助,欢迎点赞、收藏,如有疑问或补充,欢迎在评论区交流!