STL 之 string:auto 与范围 for、常用接口、模拟实现与深浅拷贝

1. 为什么 string 值得单独讲

C++ 标准库中的 std::string 是日常开发里使用频率最高的容器之一。它本质上是对字符序列的封装,但比 C 风格字符串多了自动内存管理、长度记录、丰富的成员函数以及和 STL 算法良好的配合能力。

学 STL 时习惯从 vector 开始,其实 string 更适合作为第一个"手写容器"的练手对象:它结构简单、接口直观,同时又完整覆盖了构造、拷贝、扩容、迭代器、运算符重载等核心知识点。把 string 吃透,再去看 vector 会轻松很多。

本文围绕四个关键词展开:auto 与范围 for 、常用接口 、模拟实现 、浅拷贝与深拷贝(含写时拷贝)。

2. auto 与范围 for:让遍历更简洁

2.1 auto 的基本用法

auto 让编译器在编译期根据初始化表达式推导变量类型,减少冗长的类型书写。对于 string 的迭代器尤其有用,因为迭代器类型名往往又长又难写。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

//auto + 范围for
void func1(auto e) //auto 不能作函数参数
{
	cout << "e"<< endl;
}

auto func2(int a) //auto 能作函数返回值,但是需要谨慎使用,比如想要知道返回的参数类型是啥,但是这个函数里面继续套娃其他函数,所以要谨慎使用
{
	cout << a << endl;
}

int main()
{
	int a = 0;
	auto b = a;

	double c = 1.0;
	auto d = c;

	cout << typeid(b).name() << endl;
	cout << typeid(d).name() << endl;

	auto a = 1, b = 2.0;//同一行的auto推导类型,参数必须是同一类型

	auto _a[] = { 1,2,3,4 };//auto 不能用来推导数组元素类型

	auto e;//必须进行初始化;
	return 0;
}

int main() {
    string s = "hello";

    // 传统写法:类型名冗长
    string::iterator it1 = s.begin();

    // auto 写法:类型由编译器推导
    auto it2 = s.begin();

    cout << *it1 << " " << *it2 << endl;  // h h
    return 0;
}

需要注意,auto 推导时会丢弃顶层 const 和引用 。如果希望拿到引用,必须显式写 auto& 或 const auto&。

2.2 范围 for 的语法与本质(自动赋值,自动迭代)

范围 for(range-based for)的语法形式是 for (声明 : 表达式),编译器会把它展开成基于 begin() 和 end() 的迭代器循环。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "hello";

    // 只读遍历:推荐 const auto&,避免拷贝
    for (const auto& ch : s) {
        cout << ch << ' ';
    }
    cout << endl;

    return 0;
}

这里有一个非常容易踩的坑:如果写成 for (auto ch : s),ch 是每个字符的拷贝,修改它不会影响原字符串。想修改元素,一定要写 auto&。

2.3 三种写法对比

写法 是否拷贝 能否修改原串 适用场景
auto ch 拷贝 否 字符很小,只读且不在意拷贝
auto& ch 不拷贝 是 需要修改字符
const auto& ch 不拷贝 否 只读遍历,最推荐

小结:只读用 const auto&,要改用 auto&,尽量别用 auto 值拷贝。

3. string 的常用接口

3.1 构造与赋值

cpp 复制代码
#include <string>
using namespace std;

int main() {
    string s1;                    // 空串
    string s2("hello");           // 由 C 字符串构造
    string s3(s2);                // 拷贝构造
    string s4(5, 'a');            // "aaaaa"
    string s5(s2, 1, 3);          // 从下标 1 起取 3 个字符:"ell"
    string s6 = s2 + " world";    // 拼接

    s1 = s2;                      // 赋值
    s1.assign("abc");             // 重新赋值
    return 0;
}

3.2 容量相关接口

接口 作用
size() / length() 返回字符个数,二者等价
capacity() 返回当前已分配空间能容纳的字符数,注意大小不包含'\0'
empty() 判断是否为空
clear() 清空内容,不释放容量
reserve(n) 预分配至少 n 的空间,避免频繁扩容(n<size,不会改变现有内容,不会缩小;n>capacity,扩容;size<n<capacity,行为不确定)
resize(n, ch) 把有效字符数改为 n,多出的位置用 ch 填充
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "hello";
    cout << s.size() << " " << s.capacity() << endl;

    s.reserve(100);               // 提前扩容,避免多次 reallocate
    cout << s.capacity() << endl; // 至少 100

    s.resize(8, 'x');             // "helloxxx"
    cout << s << endl;

    s.clear();                    // 内容清空,容量保留
    cout << s.size() << " " << s.capacity() << endl;
    return 0;
}

size 与 capacity 的区别 :size 是"已经用了多少",capacity 是"最多能装多少而不重新分配"。reserve 只改 capacity,resize 改 size。

3.3 访问与修改

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "hello";

    cout << s[0] << endl;        // 不检查越界
    cout << s.at(1) << endl;     // 越界抛 out_of_range
    cout << s.front() << endl;   // 首字符
    cout << s.back() << endl;    // 尾字符

    s[0] = 'H';                   // 修改
    s.push_back('!');             // 尾插字符
    s.append(" ok");              // 尾插字符串
    s += " fine";                 // 运算符重载
    s.insert(0, ">> ");          // 指定位置插入
    s.erase(0, 3);                // 删除 [0,3) 区间
    cout << s << endl;
    return 0;
}

3.4 查找与子串

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "hello world, hello cpp";

    size_t pos = s.find("hello");        // 首次出现位置
    cout << pos << endl;                 // 0

    pos = s.find("hello", 1);            // 从下标 1 开始找
    cout << pos << endl;                 // 13

    pos = s.rfind("hello");              // 最后一次出现
    cout << pos << endl;                 // 13

    cout << s.find("C") << endl;      // string::npos

    string sub = s.substr(6, 5);         // 从 6 开始取 5 个
    cout << sub << endl;                 // world
    return 0;
}

注意 find 找不到时返回 string::npos,它是一个很大的无符号数,判断时不要写成 if (pos > 0),而应写 if (pos != string::npos)。

3.5 迭代器接口

4. string 的模拟实现

4.1 类的基本框架

模拟实现的核心是三个成员:字符指针 _str、有效长度 _size、容量 _capacity。为了和标准库区分,这里命名为 String。

这里 _capacity 表示"能存的有效字符数",实际申请空间是 _capacity + 1,多出的 1 个字节留给 '\0'。

4.2 扩容与尾插

扩容策略采用"二倍扩容",这是均摊 O(1) 的关键。注意扩容后必须手动补 '\0',否则 c_str() 会越界读取。

4.3 运算符重载

提供 const 和非 const 两个版本的 operator[],是为了让 const 对象也能下标访问,同时保证 const 对象不能被修改。

4.4 迭代器

string 的迭代器本质上就是原生指针,这也是为什么范围 for 能直接作用于它。

复制代码
String.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
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(const char* str = "") //注意·中间不能有空格
	{
		_size = _capacity = strlen(str);
		_str = new char[_capacity + 1];
		strcpy(_str, str);
	}
	void swap(String& s2)
	{
		std::swap(_str, s2._str);
		std::swap(_size, s2._size);
		std::swap(_capacity, s2._capacity);
	}

	String& operator=(String s) //这里要是使用const String s,就涉及权限放大问题
	{
		swap(s);
		return *this;
	}

	String(const String& s)
	{
		_str = new char[s._capacity + 1];
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;
	}

	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}
	}


	void reserve(size_t n);
	void insert(size_t pos, char ch);
	void insert(size_t pos, const char* str);
	void push_back(char ch);
	void append(const char* str);
	String& operator+=(char ch);
	String& operator+=(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, size_t len);
	bool operator<(const String& s2);
	bool operator>(const String& s2);
	bool operator<=(const String& s2);
	bool operator>=(const String& s2);
	bool operator!=(const String& s2);
	bool operator==(const String& s2);

private:
	char* _str;
	size_t _size;
	size_t _capacity;
	static size_t npos;
};
size_t String::npos = -1;

ostream& operator<<(ostream& out, const String& s);
cpp 复制代码
String.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"String.h"
using namespace std;
void String::reserve(size_t n)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;
		_capacity = n;
	}
}
void String::insert(size_t pos, char ch)
{
	assert(pos < _size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}
	size_t end = _size;
	while (end > pos)
	{
		_str[end] = _str[end-1];
		--end;
	}
	_str[pos] = ch;
	_str[++_size] = '\0';  //!!!
}
void String::insert(size_t pos, const char* str)
{
	assert(pos < _size);
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	}
	size_t end = _size + len;
	while (end > pos + len - 1)
	{
		_str[end] = _str[end - len];
		--end;
	}
	for (size_t i = 0;i < len;i++)
	{
		_str[pos+i] = *(str + i);
	}
	_size += len;
	_str[_size] = '\0';
}
void String::push_back(char ch)
{
	if (_size == _capacity)
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	_str[_size++] = ch;
    _str[_size] = '\0';
}
void String::append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	for (size_t i = 0;i < len;i++)
	{
		push_back(*(str + i));
	}
}

String& String::operator+=(char ch)
{
	push_back(ch);
	return *this;
}

String& String::operator+=(const char* str)
{
	append(str);
	return *this;
}


void String::erase(size_t pos, size_t len)// 默认参数在定义时,不需要重复给,否则会出现参数重定义
{
	assert(pos < _size);
	if (len >= _size - pos)
	{
		pos = _size;
		_str[_size] = '\0';
	}
	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 (size_t i = pos;i < _size;i++)
	{
		if (_str[pos] == ch)
			return i;
	}
	return npos;
}

size_t String::find(const char* str, size_t pos)
{
	assert(pos < _size);
	char* ptr = strstr(_str+pos, str);
	if (ptr == nullptr)
		return npos;
	else
		return ptr - _str;
}

String String::substr(size_t pos, size_t len)
{
	assert(pos < _size);
	if (len > _size - pos)
		len = _size - pos;
	String sub;
	sub.reserve(len);
	strcpy(sub._str, _str + pos);
	sub._size = len;
	return sub;
}

bool String::operator<(const String& s2)
{
	return strcmp(_str, s2._str) < 0;
}

bool String::operator>(const String& s2)
{
	return !(*this <= s2);
}
bool String::operator<=(const String& s2)
{
	return (*this < s2) || (*this == s2);
}
bool String::operator>=(const String& s2)
{
	return !(*this < s2);
}
bool String::operator!=(const String& s2)
{
	return !(*this == s2);
}
bool String::operator==(const String& s2)
{
	return strcmp(_str, s2._str) == 0;
}

ostream& operator<<(ostream& out,const String& s)
{
	for (auto ch : s)
	{
		out << ch;
	}
	return out;
}

int main()
{
	String s;
	String s1("hello");
	String s2;
	s2 = s1;
	s2.insert(2, 'x');//hexllo
	cout << s2 << endl;
	s2.append("aaa");//hexlloaaa
	cout << s2 << endl;
	s2.erase(3, 1);//hexloaaa
	cout << s2 << endl;
	String s3 = s2.substr(2, 8);
	if (s3 == s2)
		cout << "s3 == s2" << endl;
	else
		cout << "s3 != s2" << endl;// s3 != s2
	cout << s3 << endl;//xlloaaa
	return 0;
}

5. 浅拷贝与深拷贝

5.1 默认拷贝构造的问题

如果类里含有指针成员,而你没有自己写拷贝构造函数,编译器生成的默认版本会做逐字节的值拷贝 ,也就是把指针地址原样复制过去。这就是浅拷贝。

cpp 复制代码
MyString s1("hello");
MyString s2(s1);   // 浅拷贝:s1._str 和 s2._str 指向同一块内存

浅拷贝会带来两个致命问题:

  • 重复释放 :两个对象析构时都会 delete[] 同一块内存,程序崩溃。
  • 相互影响:修改 s2 的内容会连带改变 s1,违背值语义。

5.2 深拷贝的实现

深拷贝的思路是:为新对象单独申请一块内存,再把原内容复制过去,两个对象各自持有独立空间。

cpp 复制代码
// 传统写法
MyString(const MyString& s)
    : _size(s._size), _capacity(s._capacity) {
    _str = new char[_capacity + 1];
    strcpy(_str, s._str);
}

MyString& operator=(const MyString& s) {
    if (this != &s) {                 // 防止自赋值
        char* tmp = new char[s._capacity + 1];
        strcpy(tmp, s._str);
        delete[] _str;                // 释放旧空间
        _str = tmp;
        _size = s._size;
        _capacity = s._capacity;
    }
    return *this;
}

赋值运算符里先申请新空间、再释放旧空间,是为了防止 new 抛异常时对象处于半销毁状态。

5.3 现代写法:拷贝并交换

cpp 复制代码
void swap(MyString& s) {
    std::swap(_str, s._str);
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
}

MyString(const MyString& s)
    : _str(nullptr), _size(0), _capacity(0) {
    MyString tmp(s._str);   // 复用构造函数
    swap(tmp);
}

MyString& operator=(MyString s) {   // 传值,天然得到一份拷贝
    swap(s);
    return *this;
}

这种写法把"拷贝"和"释放旧资源"交给临时对象完成,代码更短,异常安全性也更好,是 C++ 中推荐的惯用法。

5.4 浅拷贝与深拷贝对比

对比项 浅拷贝 深拷贝
内存关系 多个对象共享同一块堆内存 每个对象独立申请内存
析构风险 重复释放,程序崩溃 各自释放,安全
修改影响 互相影响 互不影响
实现成本 编译器默认生成 需手写拷贝构造与赋值

6. 写时拷贝(Copy-On-Write)

6.1 基本思想

深拷贝每次拷贝都要复制整块内存,如果拷贝后只读不写,这份开销就浪费了。写时拷贝 的思路是:拷贝时只复制指针,并维护一个引用计数;只有当某个对象要修改内容时,才真正为它复制一份独立内存。

6.2 优缺点分析

  • 优点:只读场景下拷贝代价极低,节省内存。
  • 缺点 :引用计数需要额外空间;多线程下计数需要原子操作,否则有竞态;operator[] 等接口需要区分读写,实现复杂度上升。

补充:C++11 之后,标准库实现普遍放弃了 string 的写时拷贝(早期 libstdc++ 曾使用),转而采用 SSO(短字符串优化)加移动语义。原因是 COW 在多线程下容易出问题,且与迭代器失效规则冲突。写时拷贝如今更多作为理解"引用计数 + 延迟复制"思想的教学案例。

7. 常见坑与最佳实践

  • 范围 for 修改元素必须用 auto&,只读推荐 const auto&。
  • find 的返回值要和 string::npos 比较,不要和 0 比较。
  • 已知大致长度时先 reserve,避免多次扩容。
  • c_str() 返回的指针不要跨修改保存。
  • 含指针成员的类,务必遵守"三法则":需要自定义析构函数时,通常也需要自定义拷贝构造和拷贝赋值。
  • 优先使用拷贝并交换实现赋值,代码简洁且异常安全。

8. 总结

本文从 auto 与范围 for 的遍历技巧出发,梳理了 string 在构造、容量、访问、查找、迭代器等方面的常用接口,并手写了一个简化版 MyString。在此基础上重点讨论了浅拷贝导致的重复释放问题、深拷贝的两种实现方式,以及写时拷贝的引用计数思想。

相关推荐
Nerd(BUG版)18 分钟前
KMP学习day5--Kotlin Flow异步流基础
开发语言·学习·kotlin
J.YA36 分钟前
String字符串[面试题]
java·开发语言
汉克老师11 小时前
CSP-J 2026 第二轮入门组真题解析(第一题 、旅游)
c++·csp-j·信息学竞赛·小学生学c++
xiaoye-duck11 小时前
《C++ 项目:手写大模型接入 SDK》Gemini接口设计(上):初始化、全量响应实现和测试
开发语言·网络·c++·语言模型
nice_lcj52014 小时前
Java Lambda 表达式入门:怎么写、怎么用、常用方法有哪些
java·开发语言·后端·面试
码农小河6614 小时前
基于SSM+JSP+MySQL的在线医疗服务系统的设计与实现
java·开发语言·mysql
老师我太想进步了202615 小时前
【数据结构-4】算法美学之顺序表
数据结构·算法
风影同志7832415 小时前
编码器是激活函数吗?—— 一个看起来很像、其实不是的类比
算法
UIU11415 小时前
P1481 魔族密码
c++·算法·动态规划