C++ string的常用函数以及简单模拟实现

在C语言,字符串就是一个以'\0'结尾的char类型的数组,管理字符串可以使用string库中提供的一系列函数。然而,这些函数与字符串是分开的,不方便操作,还容易越界访问。

在C++中,string是代表字符顺序的对象,标准的string类提供了类对象的支持,其接口与标准字符容器接口相似。

string类是basic_string类的一个实例化(char类型),并用char_traits 和allocator作为basic_string的默认参数。

一、string的常用函数

1.构造函数

cpp 复制代码
string();  //构造空字符串 (默认构造函数)

string (const string& str);  //拷贝构造函数

string (const char* s);  //用C-string来构造string类对象

string (size_t n, char c);  //构造包含n个c的字符串

size_t是无符号整型

cpp 复制代码
void test_string1()
{
	string s1;
	string s2("Hello world!");
	string s3(6, 'b');
	string s4(s2);
}

2.容量大小函数

cpp 复制代码
size_t size() const;  
size_t length() const;  //这两个函数作用相同,都是返回字符串的长度,不包含'\0'

size_t max_size() const;  //返回字符串能存放的最大长度

void reserve (size_t n = 0);  //申请将字符串的容量扩大n个字符

void resize (size_t n);
void resize (size_t n, char c);  //重新调整字符串的长度至n,c用于填充扩充的新空间

*注意 resize 和 reserve 的区别:

简单来说,reserve 只是调整容量(内存预留),不改变内容;resize 则直接修改字符串的长度及其内容。

  • reserve 一般只能增加或保持当前的容量(capacity),不能缩小它。
    • 但是实际reserve的容量实际可能会比要求的更多,这是因为编译器为了满足内存对齐,这个取决于编译器的底层实现
  • resize 用来改变字符串的长度,调整 string 的大小(size),如果新的大小比当前小,字符串将被截断;如果比当前大,字符串会扩展并用字符(默认通常是 '\0')填充。
  • 扩充,并以指定字符填充
  • 缩小,直接截断字符串
    • 但是注意,截断并不会在字符串截断后自动添加 `\0`,因为 stirng 自己管理长度,不需要依赖 `\0` 作为结尾标志。
cpp 复制代码
void test_string2()
{
	string str = "hello world";
	cout << str.size() << endl;
	cout << str.length() << endl;
	cout << str.max_size() << endl;
	cout << str.capacity() << endl;

	str.reserve(20);
	cout << str << endl;

	str.resize(15, 'x');
	cout << str << endl;

	cout << str << endl;
}

利用 reserve 可以提高插入数据的效率,在需要多次插入字符时能避免多次异地扩容带来的开销。

3.访问及遍历操作函数

cpp 复制代码
      char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;  //返回字符串在pos位置的引用

      iterator begin();
const_iterator begin() const;  //返回指向字符串起始位置的迭代器
      iterator end();
const_iterator end() const;  //返回指向字符串末尾位置的迭代器
cpp 复制代码
void test_string3()
{
	string s1("Hello world!");

	//用[]遍历s1
	for (size_t i = 0; i < s1.size(); i++)
	{
		cout << s1[i] << " ";
	}

	cout << endl;

	for (size_t i = 0; i < s1.size(); i++)
	{
		s1[i]++;
	}

	cout << s1 << endl;

    //用迭代器遍历s1
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << s1 << endl;
}

基于迭代器,还有一种迭代方式:范围for循环(range-based for loop)**

范围for循环底层是通过迭代器实现的。在C++中,当使用范围for循环时,编译器实际上会调用begin()和end()函数来获取迭代器,然后通过这些迭代器遍历集合中的元素。

cpp 复制代码
for (auto ch : s1)
{
	cout << ch << " ";
}
cout << endl;

(反汇编代码,可以看到调用了begin 和 end 来获取迭代器)

4.修改操作函数

cpp 复制代码
void push_back (char c);  //在字符串后增加一个字符c

//在字符串后拼接另一个字符串
string& append (const string& str);  //另一个字符串的拷贝
string& append (const char* s);   //C形式的字符串

//在字符串后拼接另一个字符串
string& operator+= (const string& str);  
string& operator+= (const char* s);

const char* c_str() const;  //返回C形式的字符串(数组容器)

size_t find (const string& str, size_t pos = 0) const;  //从字符串pos位置开始往后找字符串str,返回该字符在字符串中的位置

size_t find (char c, size_t pos = 0) const;  //从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置

string substr (size_t pos = 0, size_t len = npos) const;  //在str中从pos位置开始,截取n个字符,然后将其返回
//npos 意思是 "字符串的结尾"
cpp 复制代码
std::string str1 = "Hello";
str.append(" C++");
std::cout << str1 << std::endl;  // 输出: Hello C++

std::string str2 = "Hello";
str += " World";
std::cout << str2 << std::endl;  // 输出: Hello World

std::string str3 = "Hello";
str += " C++";
std::cout << str3 << std::endl;  // 输出: Hello C++

std::string str4 = "Hello";
const char* cstr = str4.c_str();
std::cout << cstr << std::endl;  // 输出: Hello

std::string str5 = "Hello, World!";
size_t pos = str5.find("World");
if (pos != std::string::npos) 
{
    std::cout << "Found at position: " << pos << std::endl;
    // 输出: Found at position: 7  
}

std::string str6 = "Hello, World!";
std::string sub = str6.substr(7, 5);
std::cout << sub << std::endl;  // 输出: World

分析 += 和 + 的函数重载的定义

string& operator+= (const string& str);

string operator+ (const string& lhs, const string& rhs);

可以发现,+= 是传引用返回 + 是传参返回,所以,能用 += 就用 +=, + 的代价大(产生临时对象,两次拷贝构造)

substr的应用场景常是提取文件后缀,url域名等等......

cpp 复制代码
// 提取文件的的后缀
string file1("string.cpp");
size_t pos = file.rfind('.');
string suffix(file.substr(pos, file.size()-pos));
cout << suffix << endl;

二、string的简单模拟实现

这里提供一种模拟实现的方式

cpp 复制代码
#pragma once
#include<assert.h>

namespace buider
{
	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(strlen(str))
			,_capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

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

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

		size_t capacity() const
		{
			return _capacity;
		}

		size_t size() const
		{
			return _size;
		}

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

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

		string& operator=(const string& str)
		{
			if (this != &str)
			{
				char* temp = new char[str._capacity + 1];
				strcpy(temp, str._str);
				delete[] _str;
				 
				_str = temp;
				_size = str._size;
				_capacity = str._capacity;
			}

			return *this;
		}

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

		void push_back(char ch)
		{
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* temp = new char[n+1];
				strcpy(temp, _str);
				delete[] _str;
				_str = temp;
				_capacity = n;
			}
		}

		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_capacity <= _size + len)
			{
				reserve(_size + len);
			}

			strcpy(_str + _size, str);
			_size += len;
		}

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

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

		void insert(size_t pos, const char ch)
		{
			assert(pos <= _size);
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			size_t end = _size + 1;
			while (end >= pos)
			{
				_str[end] = _str[end - 1];
				--end;
			}
			_str[pos] = ch;
			_size++;
		}

		//另一种实现:
		/*void insert(size_t pos, const char ch)
		{
			assert(pos <= _size);
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			size_t end = _size;
			while (end >= (int)pos)
			{
				_str[end + 1] = _str[end];
				--end;
			}
			_str[pos] = ch;
			_size++;
		}*/

		void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			size_t len = strlen(str);
			if (_capacity <= _size + len)
			{
				reserve(_size + len);
			}

			for (size_t i = _size; i >= pos; i--)
			{
				_str[i + len] = _str[i];
			}
			for (size_t i = 0; i < len; i++)
			{
				_str[pos + i] = str[i];
			}

			_size += len;
			//_str[_size] = '\0';
		}

		void erase(size_t pos, size_t len = npos)
		{
			assert(pos <= _size);
			for (size_t i = 0; i < (_size - len - pos); i++)
			{
				_str[pos + i] = _str[pos + i + len];
			}

			_size = _size - len;
			_str[_size] = '\0';
		}

		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 || *this == s;
		}

		bool operator>=(const string& s) const
		{
			return !(*this < s);
		}

		bool operator!=(const string& s) const
		{
			return !(*this == s);
		}

		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;

		const static size_t npos;
	};

	//类外初始化
	const size_t string::npos = -1;

	ostream& operator<<(ostream& out, const string& s) //类外重载,左操作符为ostream&对象
	{
		for (auto ch : s) //auto ch 自动推导出 ch 的类型。在这里,ch 会被推导为 char 类型,因为 s 是 string 类型,而 string 类的迭代器遍历的是字符。
			out << ch; //不是正在重载的<<, 而是标准库中已经定义的输出运算符 std::ostream& operator<<(std::ostream& out, char c);

		return out;
	}

	istream& operator>>(istream& in, string& s)
	{
		s.clear();//确保在执行输入操作时,s能够从空白状态开始存储输入数据

		char buff[129];
		size_t i = 0;

		char ch;
		ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 128)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}

			ch = in.get();
		}

		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	} 
}

其中,拷贝构造函数还有一种写法:

cpp 复制代码
//拷贝并交换
string(const string& s)
{
	string tmp(s._str);  // 这里调用的是 string(const char*)
	swap(_str, tmp._str);
	swap(_size, tmp._size);
	swap(_capacity, tmp._capacity);
}

拷贝-交换 是一种常用的 C\++ 编程技巧,这个方法通过交换资源来确保代码的简洁性,避免了内存泄漏和自赋值的问题。

在这里,tmp 是 s._str 的副本,接下来通过三个 swap 实现了 tmp 的赋值,这意味着 tmp 会拥有与 s 相同的字符串数据。当 tmp 出作用域时,它会自动调用析构函数,释放原本在当前对象中的数据,避免了多余的内存拷贝和释放。

相关推荐
长潇若雪11 分钟前
结构体(C 语言)
c语言·开发语言·经验分享·1024程序员节
feilieren14 分钟前
leetcode - 684. 冗余连接
java·开发语言·算法
Peter44722 分钟前
-bash: ./my_rename.sh: /bin/bash^M: bad interpreter: No such file or directory
开发语言·bash
The Future is mine24 分钟前
Java根据word模板导出数据
java·开发语言
ChinaDragonDreamer25 分钟前
HarmonyOS:@Watch装饰器:状态变量更改通知
开发语言·harmonyos·鸿蒙
随便取个六字33 分钟前
C++学习:类和对象(二)
c++·学习
一颗甜苞谷37 分钟前
开源一款前后端分离的企业级网站内容管理系统,支持站群管理、多平台静态化,多语言、全文检索的源码
java·开发语言·开源
星夜孤帆38 分钟前
Java面试题集锦
java·开发语言
论迹1 小时前
【Java】-- 接口
java·开发语言
DARLING Zero two♡1 小时前
关于我、重生到500年前凭借C语言改变世界科技vlog.12——深入理解指针(2)
c语言·开发语言·科技·1024程序员节