在C语言,字符串就是一个以'\0'结尾的char类型的数组,管理字符串可以使用string库中提供的一系列函数。然而,这些函数与字符串是分开的,不方便操作,还容易越界访问。
在C++中,string是代表字符顺序的对象,标准的string类提供了类对象的支持,其接口与标准字符容器接口相似。
string类是basic_string类的一个实例化(char类型),并用char_traits 和allocator作为basic_string的默认参数。
一、string的常用函数
1.构造函数
string(); //构造空字符串 (默认构造函数)string (const string& str); //拷贝构造函数string (const char* s); //用C-string来构造string类对象string (size_t n, char c); //构造包含n个c的字符串
size_t是无符号整型
void test_string1()
{string s1;string s2("Hello world!");string s3(6, 'b');string s4(s2);
}
2.容量大小函数
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` 作为结尾标志。
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.访问及遍历操作函数
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; //返回指向字符串末尾位置的迭代器
void test_string3()
{string s1("Hello world!");//用[]遍历s1for (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;//用迭代器遍历s1string::iterator it = s1.begin();while (it != s1.end()){cout << *it << " ";++it;}cout << s1 << endl;
}
基于迭代器,还有一种迭代方式:范围for循环(range-based for loop)
范围for循环底层是通过迭代器实现的。在C++中,当使用范围for循环时,编译器实际上会调用begin()和end()函数来获取迭代器,然后通过这些迭代器遍历集合中的元素。
for (auto ch : s1)
{cout << ch << " ";
}
cout << endl;
(反汇编代码,可以看到调用了begin 和 end 来获取迭代器)
4.修改操作函数
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 意思是 "字符串的结尾"
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 Worldstd::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; // 输出: Hellostd::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域名等等......
// 提取文件的的后缀
string file1("string.cpp");
size_t pos = file.rfind('.');
string suffix(file.substr(pos, file.size()-pos));
cout << suffix << endl;
二、string的简单模拟实现
这里提供一种模拟实现的方式
#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;}
}
其中,拷贝构造函数还有一种写法:
//拷贝并交换
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 出作用域时,它会自动调用析构函数,释放原本在当前对象中的数据,避免了多余的内存拷贝和释放。