在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 出作用域时,它会自动调用析构函数,释放原本在当前对象中的数据,避免了多余的内存拷贝和释放。