简介

在 C++ 的标准模板库(STL)中,std::string 是最常用、最重要的类之一。
它封装了字符数组的复杂操作,为开发者提供了安全、便捷、功能强大的字符串处理能力。相比于传统的 C 风格字符串(即以 \0 结尾的 char* 数组),std::string 自动管理内存支持动态扩展提供丰富的成员函数,极大地提升了开发效率和代码安全性。
本文将从 std::string 的基本使用入手,逐步深入其底层实现原理,帮助读者会用、理解。

提示:由于std::string的底层实现并非简单地开辟空间,而是涉及内存池、SSO等优化技术,为了让有一定c++基础但是没有深入了解的读者也能看懂,本文的代码实现部分不涉及内存池等优化技术!

std::string的基础与使用

string的实现

std::string 是 C++ 标准库中定义的一个类模板实例化类型,其完整定义为:
typedef std::basic_string<char> string;
它位于头文件 <string> 中,是一个模板类 basic_stringchar 类型的特化。除了 string,标准库还提供了:

typedef std::basic_string<wchar_t> wstring;       // 宽字符字符串
typedef std::basic_string<char16_t> u16string;    // UTF-16
typedef std::basic_string<char32_t> u32string;    // UTF-32
  • 这里提及的宽字符、UTF等,对我们了解string没有太大帮助,不了解字符编码可以无视。如果没有兴趣可以不了解,感兴趣可以进行网络搜索或询问AI等
std::string 本质上是一个 动态数组容器,专门用于 存储操作字符序列。它具备以下核心特性:
动态大小:可根据需要自动增长或缩减。
自动内存管理:无需手动分配或释放内存。
丰富的接口:支持拼接、查找、替换、比较、子串等操作。
与 C 风格字符串兼容:可通过 c_str() 获取 const char*
迭代器支持:可与 STL 算法无缝结合。

基本使用示例

以下为简单使用示例 这些简单的部分就不过多赘述了

#include <iostream>
#include <string>
using namespace std;
int main() {
	// 构造
    string s1 = "Hello";
    string s2("World");

    // 拼接
    string s3 = s1 + ", " + s2 + "!";

    // 长度与访问
    cout << "Length: " << s3.size() << endl;
    cout << "First char: " << s3[0] << endl;

    // 查找与子串
    size_t pos = s3.find("World");
    if (pos != string::npos) {
        string sub = s3.substr(pos, 5);
        cout << "Found: " << sub << endl;
    }

    // 比较
    if (s1 < s2) {
        cout << s1 << " < " << s2 << endl;
    }

    return 0;
}

底层实现my::string(不涉及SSO/内存池优化)

代码整体(直接复制可用的代码)

string.h

头文件: string.h

#define _CRT_SECURE_NO_WARNINGS 1   //Visual Studio防报错用
#pragma once
#include<iostream>
#include<string.h>
#include<assert.h>
using namespace std;

namespace my
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;
		iterator begin();
		iterator end();
		const_iterator begin() const;
		const_iterator end() const;

		string(const char* str = "");
		~string();
		string(const string& s);
		string& operator=(string tmp);
		const char* c_str() const;

		void swap(string& s);
		size_t size() const;
		char& operator[](size_t i);
		const char& operator[](size_t i) const;

		void reserve(size_t n);

		void push_back(char ch);
		void append(const char* str);
		string& operator+=(char ch);
		string& operator+=(const char* str);

		string& insert(size_t pos, char ch);
		string& insert(size_t pos, const char* str);
		string& erase(size_t pos = 0, size_t len = npos);

		void pop_back();

		size_t find(char ch, size_t pos = 0) const;
		size_t find(const char* str, size_t pos = 0) const;

		string substr(size_t pos, size_t len = npos) const;
		void clear();

		bool operator<(const string& s) const;
		bool operator<=(const string& s) const;
		bool operator>(const string& s) const;
		bool operator>=(const string& s) const;
		bool operator==(const string& s) const;
		bool operator!=(const string& s) const;

	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	public:
		static const size_t npos;
	};

	ostream& operator<<(ostream& out, const string& s);
	istream& operator>>(istream& in, string& s);
	istream& getline(istream& in, string& s, char delim = '\n');
	void swap(string& x, string& y);
}

string.cpp

实现文件: string.cpp

#include "string.h"

namespace my
{
	const size_t string::npos = -1;

	string::string(const char* str)
		: _size(strlen(str))
	{
		_str = new char[_size + 1];
		_capacity = _size;
		strcpy(_str, str);
	}

	string::~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = _capacity = 0;
	}

	void string::swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}

	string::string(const string& s)
	{
		string tmp(s._str);
		swap(tmp);
	}

	string& string::operator=(string tmp)
	{
		swap(tmp);
		return *this;
	}

	string::iterator string::begin()
	{
		return _str;
	}

	string::iterator string::end()
	{
		return _str + _size;
	}

	string::const_iterator string::begin() const
	{
		return _str;
	}

	string::const_iterator string::end() const
	{
		return _str + _size;
	}

	const char* string::c_str() const
	{
		return _str;
	}

	size_t string::size() const
	{
		return _size;
	}

	char& string::operator[](size_t i)
	{
		assert(i < _size);
		return _str[i];
	}

	const char& string::operator[](size_t i) const
	{
		assert(i < _size);
		return _str[i];
	}

	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* str = new char[n + 1];
			memcpy(str, _str, _size + 1);
			delete _str;
			_str = str;
			_capacity = n;
		}
	}

	void string::push_back(char ch)
	{
		if (_size >= _capacity)
		{
			size_t newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
			reserve(newCapacity);
		}
		_str[_size] = ch;
		++_size;
		_str[_size] = '\0';
	}

	void string::append(const char* str)
	{
		size_t len = strlen(str);
		if (_size + len > _capacity) {
			size_t newCapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
			reserve(newCapacity);
		}
		memcpy(_str + _size, str, len + 1);
		_size += len;
	}

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

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

	string& string::insert(size_t pos, char ch)
	{
		assert(pos <= _size);
		if (_size >= _capacity)
		{
			size_t newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
			reserve(newCapacity);
		}
		size_t end = _size + 1;
		while (end > pos)
		{
			_str[end] = _str[end - 1];
			--end;
		}
		_str[pos] = ch;
		++_size;
		return *this;
	}

	string& string::insert(size_t pos, const char* str)
	{
		assert(pos <= _size);
		size_t len = strlen(str);
		if (_size + len > _capacity) {
			size_t newCapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
			reserve(newCapacity);
		}
		int end = _size + len;
		while (end >= pos + len)
		{
			_str[end] = _str[end - len];
			--end;
		}
		memcpy(_str + pos, str, len);
		_size += len;
		return *this;
	}

	string& string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);
		if (len == npos || len >= (_size - pos))
		{
			_size = pos;
			_str[_size] = '\0';
		}
		else
		{
			size_t i = pos + len;
			memmove(_str + pos, _str + i, _size + 1 - i);
			_size -= len;
		}
		return *this;
	}

	void string::pop_back()
	{
		assert(_size > 0);
		--_size;
		_str[_size] = '\0';
	}

	size_t string::find(char ch, size_t pos) const
	{
		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == ch)
				return i;
		}
		return npos;
	}

	size_t string::find(const char* str, size_t pos) const
	{
		const char* p1 = strstr(_str + pos, str);
		if (p1 == nullptr)
			return npos;
		else
			return p1 - _str;
	}

	string string::substr(size_t pos, size_t len) const
	{
		if (len == npos || len >= _size - pos)
		{
			len = _size - pos;
		}
		string ret;
		ret.reserve(len);
		for (size_t i = 0; i < len; i++)
		{
			ret += _str[pos + i];
		}
		return ret;
	}

	void string::clear()
	{
		_size = 0;
		_str[0] = '\0';
	}

	bool string::operator<(const string& s) const
	{
		size_t i1 = 0, i2 = 0;
		while (i1 < _size && i2 < s._size)
		{
			if (_str[i1] < s[i2])
				return true;
			else if (_str[i1] > s[i2])
				return false;
			else {
				++i1; ++i2;
			}
		}
		return i2 < s._size;
	}
	bool string::operator<=(const string& s) const
	{
		return *this < s || *this == s;
	}
	bool string::operator>(const string& s) const
	{
		return !(*this <= s);
	}
	bool string::operator>=(const string& s) const
	{
		return !(*this < s);
	}
	bool string::operator==(const string& s) const
	{
		size_t i1 = 0, i2 = 0;
		while (i1 < _size && i2 < s._size)
		{
			if (_str[i1] != s[i2])
				return false;
			else {
				++i1; ++i2;
			}
		}
		return i1 == _size && i2 == s._size;
	}
	bool string::operator!=(const string& s) const
	{
		return !(*this == s);
	}

	ostream& operator<<(ostream& out, const string& s)
	{
		//out << s.c_str();	//无法访问'\0'后的数据
		for (size_t i = 0; i < s.size(); ++i)
		{
			out << s[i];
		}
		return out;
	}
	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char buff[128];
		int i = 0;
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			buff[i] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
			++i;
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}
	istream& getline(istream& in, string& s, char delim)
	{
		s.clear();
		char buff[128];
		int i = 0;
		char ch = in.get();
		while (ch != delim)
		{
			buff[i] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
			++i;
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}
	void swap(string& x, string& y)
	{
		x.swap(y);
	}
}

代码讲解

为什么不写默认构造函数string()?

如果我们手动写默认构造,那么会写成下面这样:

string::string()
	: _str(new char[1] {'\0'})
	, _size(0)
	, _capacity(0)
{
}

但是我们已经实现了带参构造函数,因此我们可以通过给带参构造函数缺省值的方式,实现两个构造函数的合并

	string::string(const char* str = "")
		: _size(strlen(str))
	{
		_str = new char[_size + 1];
		_capacity = _size;
		strcpy(_str, str);
	}

这样,如果是无参构造,就可以直接使用""来进行构造

带参构造string(const char* str)为什么不用初始化列表?

可能有人会疑惑,为什么这里不直接写为全初始化列表:

string::string(const char* str)
	:_str(new char[strlen(str) + 1])
	,_size(strlen(str))
	,_capacity(strlen(str))
{
	strcpy(_str, str);
}

我们不难发现,如果使用这种方法,会调用3次strlen函数,但是strlen函数的时间复杂度是n,这意味着每次调用都要遍历一次字符串str,这会造成极大的时间浪费。
但是我们可以发现,strlen的值保存在了_size之中,因此我们可以将_size作为参数进行传递。
这时,可能就会有同学这样想了:调换初始化列表的顺序不就好了吗?

string::string(const char* str)
	:_size(strlen(str))
	,_str(new char[_size + 1])
	,_capacity(_size)
{
	strcpy(_str, str);
}

但是很疑惑,如果试着运行一下,会发现代码是错误的!
因为对于初始化列表而言,初始化顺序与声明顺序相同,我们的声明顺序是

private:
	char* _str = nullptr;
	size_t _size = 0;
	size_t _capacity = 0;

因此初始化顺序依次为_str_size_capacity
(如果不太理解可以看看这里~)
CSDN文章:C++初始化列表是什么?有什么“隐藏坑”?初始化列表的顺序问题
当然,改变声明顺序也是解决办法之一,即将_size的声明放到最前面,变成

private:
	size_t _size = 0;
	char* _str = nullptr;
	size_t _capacity = 0;

但是这样会大大增加代码的耦合性,这是编程之大忌!
因此,这个时候就只能放弃全部使用初始化列表,只将_size放在初始化列表之中,其余部分手动实现

string::string(const char* str)
	: _size(strlen(str))
{
	_str = new char[_size + 1];
	_capacity = _size;
	strcpy(_str, str);
}

(可能有同学会疑惑,初始化列表、缺省值等,谁的优先级更高呢?可以看看我写的另一篇文章哦~)
CSDN文章:C++类中成员变量走初始化列表的逻辑

没看懂拷贝构造函数和赋值运算符重载?

文章里提供的拷贝构造函数和赋值运算符重载是经过优化后的版本,虽然不会提高运行效率,但是代码简洁性会大大提高!
具体如何优化的在下面这里,里面涉及的例子就是本文的my::string
CSDN文章:C++拷贝构造函数与operator=()的现代简洁写法,如何“优雅”地拷贝构造与赋值

string(const char* str)为什么使用strcpy而不使用my::string::swap?

在上一个问题中,我们已经写了my::string::swap函数来实现对一个新对象的赋值(点击上一个问题的蓝色链接即可看到),那么在这里为什么不调用swap来进行更"优雅"的操作呢?

原因有两个

1.类型不同:
_str的类型是char*,而str的类型是const char*,二者类型不同

2.存储位置不同
即使二者类型相同,二者的存储空间位置也不同
str指向的内存空间是常量区,值是不能修改
_str指向的内存空间是,值是可以修改

assert()是什么?

assert是c/c++都有的断言报错,可以判断代码运行到此处时是否符合括号内的条件语句,是debug时的好帮手!
不清楚的话可以看看这里~
CSDN文章:assert断言(如何让程序在不满足条件时报错)

reserve()中为什么使用memcpy而不是strcpy?

在正常的使用情况下,使用strcpy()其实并没有什么问题,因为一个字符串正常插入数据,结尾以\0结束,我们的代码逻辑可以很好地处理这种情况。
但是,为了代码的健壮性/鲁棒性,我们不得不考虑一种情况:
用户会不会插入\0这个字符?
例如:

void test()
{
	my::string s("hello");
	s.push_back('\0');
	s += "world";
	cout << s <<endl;
}

调用这个函数后,如果使用strcpy,我们会发现,打印结果为:

hello

但是在模板库std::string,会发现能正常打印出结果:

helloworld

原因分析
对于strcpy函数而言,看到\0就意味着字符串的结束,并不会考虑这个\0是否属于字符串内容的一部分,因此我们不应当使用strcpy,而是使用memcpy来手动设置拷贝_size + 1个字符,这样才能确保用户插入的\0以及后面的内容不会在拷贝中丢失!

补充
append()方法中,使用strcpymemcpy均可

流运算符重载

可能部分同学会疑惑:流运算符为何会被重载到全局?
我写了两篇关于这个问题的文章,第一篇比较全面但是较长,想要快速理解可以直接看第二篇~
C++流运算符重载为何不能作为类的成员函数?
C++流运算符重载为何不能作为类的成员函数?快速理解!

总结:为什么 std::string 如此重要?

特性 说明
安全性 避免缓冲区溢出,自动管理内存
易用性 支持 +=、 == 等运算符重载
功能丰富 提供 findreplacesubstr 等 50+ 成员函数
性能优化 SSO、移动语义、预留容量 (reserve) 等机制
标准兼容 与 STL 容器、算法、迭代器无缝集成

(文章如有错漏,欢迎在评论区/私信指出!)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐