一、标准库中的string类
1.string类
- string是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string;
- 不能操作多字节或者变长字符的序列。
- 在使用string类时,必须包含#include头文件以及using namespace std;
2.auto和范围for
- auto关键字:自动推断类型,作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期
推导而得。
- 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
- 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
- auto不能作为函数的参数,可以做返回值,但是谨慎使用。
- auto不能直接用来声明数组。
#include <iostream>
using namespace std;
int func1()
{
return 10;
}
// 不能做参数
// void func2(auto a)
//{
//} // 可以做返回值,但是建议谨慎使用
auto func3()
{
return 3;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = func1();
// 编译报错:rror C3531: “e”: 类型包含“auto”的符号必须具有初始值设定项
// auto e;
cout << typeid(b).name() << endl; // typeid(variable).name()返回的是类型名称的一个编码
cout << typeid(c).name() << endl; // int-i char-c int*-Pi
cout << typeid(d).name() << endl;
int x = 10;
auto y = &x;
auto *z = &x;
auto &m = x;
cout << typeid(x).name() << endl;
cout << typeid(y).name() << endl;
cout << typeid(z).name() << endl;
auto aa = 1, bb = 2;
// 编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型
// auto cc = 3, dd = 4.0;
// 编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型
// auto array[] = {4, 5, 6};
return 0;
}
#include <iostream>
#include <string>
#include <map> // 导入映射容器类 std::map 用于存储键值对
using namespace std;
int main()
{
// 表示键和值都是std::string类型的映射(即字典)。
std::map<std::string, std::string> dict = {{"apple", "苹果"}, {"orange", "橙子"}, {"pear", "梨"}};
// auto的用武之地
//::iterator 是 std::map 内部定义的一个类型,用来表示一个指向 std::map 容器元素的迭代器。
// 迭代器可以看作是一个指针,它用于遍历容器中的元素。
// std::map<std::string, std::string>::iterator it = dict.begin();
// it = dict.begin(); it 是声明的迭代器变量。
// dict.begin() 是一个方法,它返回一个迭代器,指向 dict 容器中的第一个元素。即访问 dict 中的第一个键值对。
auto it = dict.begin();
while (it != dict.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
return 0;
}
- 范围for
- 对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。
- for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
- 范围for可以作用到数组和容器对象上进行遍历
- 范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
#include <iostream>
#include <string>
#include <map>
using namespace std;
int main()
{
int array[] = {1, 2, 3, 4, 5};
// C++11的遍历
// 这相当于告诉编译器 请遍历 array 中的每一个元素,并在每次迭代时将当前元素的引用赋给 e。
for (auto &e : array) //: 表示遍历的范围 左边是循环变量 右边是要遍历的容器
e *= 2;
for (auto e : array)
cout << e << " " << endl;
string str("hello world");
for (auto ch : str)
{
cout << ch << " ";
}
cout << endl;
return 0;
}
3.string类的常用接口说明
- string类对象的常见构造
- string() 构造空的string类对象,即空字符串
- string(const char* s) 用C-string来构造string类对象
- string(size_t n, char c) string类对象中包含n个字符c
- string(const string&s) 拷贝构造函数
void Teststring()
{
string s1; // 构造空的string类对象s1
string s2("hello bit"); // 用C格式字符串构造string类对象s2
string s3(s2); // 拷贝构造s3
}
- string类对象的容量操作
- size 返回字符串有效字符长度
// string::size
#include <iostream>
#include <string>
int main ()
{
std::string str ("Test string");
std::cout << "The size of str is " << str.size() << " bytes.\n";
return 0;
}
- length 返回字符串有效字符长度
- capacity 返回空间总大小
- empty 检测字符串释放为空串,是返回true,否则返回false
- clear 清空有效字符
- reserve 为字符串预留空间
// string::reserve
#include <iostream>
#include <fstream>
#include <string>
int main ()
{
std::string str;
std::ifstream file ("test.txt",std::ios::in|std::ios::ate);
if (file) {
std::ifstream::streampos filesize = file.tellg();
str.reserve(filesize);
file.seekg(0);
while (!file.eof())
{
str += file.get();
}
std::cout << str;
}
return 0;
}
- resize 将有效字符的个数改成n个,多出的空间用字符c填充
// resizing string
#include <iostream>
#include <string>
int main ()
{
std::string str ("I like to code in C");
std::cout << str << '\n';
unsigned sz = str.size();
str.resize (sz+2,'+');
std::cout << str << '\n';
str.resize (14);
std::cout << str << '\n';
return 0;
}
output
I like to code in C
I like to code in C++
I like to code
- size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况
下基本都是用size()。- clear()只是将string中有效字符清空,不改变底层空间大小。
- resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
- reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserve不会改变容量大小。
- string类对象的访问及遍历操作
- operator[] 返回pos位置的字符,const string类对象调用
// string::operator[]
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str("Test string");
for (int i = 0; i < str.length(); ++i)
{
cout << str[i];
}
return 0;
}
- begin+ end begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
// string::begin/end
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str("Test string");
for (string::iterator it = str.begin(); it != str.end(); ++it)
cout << *it;
cout << '\n';
return 0;
}
- rbegin + rend rbegin一个指向容器中最后一个元素的反向迭代器 + rend返回一个指向字符串开头之前位置的反向迭代器。
// string::rbegin/rend
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str("now step live...");
// reverse_iterator C++ 标准库中提供的一种反向迭代器
for (string::reverse_iterator rit = str.rbegin(); rit != str.rend(); ++rit)
cout << *rit;
return 0;
}
- string类对象的修改操作
- push_back 在字符串后尾插字符c
// string::push_back
#include <iostream>
#include <fstream>
#include <string>
// 这段代码从 test.txt 文件中读取所有字符并将其存储在 str 字符串中,然后将 str 输出到控制台
int main()
{
std::string str;
// std::ios::in 表示以只读方式打开文件
std::ifstream file("test.txt", std::ios::in);
if (file)
{
while (!file.eof()) // file.get() 从文件中读取一个字符
str.push_back(file.get()); // str.push_back() 将读取到的字符添加到字符串 str 的末尾
}
std::cout << str << '\n'; // 将读取到的完整字符串 str 输出到控制台。
return 0;
}
- append 在字符串后追加一个字符串
// appending to string
#include <iostream>
#include <string>
int main()
{
std::string str;
std::string str2 = "Writing ";
std::string str3 = "print 10 and then 5 more";
// used in the same order as described above:
str.append(str2); // "Writing "
// 将字符串 str3 中从第 6 个字符开始的 3 个字符(即 "10 ")添加到 str 末尾,结果为 "Writing 10 "。
str.append(str3, 6, 3); // "10 "
// str.append("dots are cool", 5);:将字符串 "dots are cool" 的前 5 个字符(即 "dots ")添加到 str 末尾
str.append("dots are cool", 5); // "dots "
str.append("here: "); // "here: "
// 在 str 末尾添加 10 个 . 字符
str.append(10u, '.'); // ".........."
// 将字符串 str3 从第 8 个字符到末尾的所有字符(即 " and then 5 more")添加到 str 末尾,
str.append(str3.begin() + 8, str3.end()); // " and then 5 more"
// str.append(5, 0x2E);:在 str 末尾再添加 5 个 . 字符
str.append(5, 0x2E); // "....."
std::cout << str << '\n';
return 0;
}
- operator+= 在字符串后追加字符串str
// string::operator+=
#include <iostream>
#include <string>
int main()
{
std::string name("John");
std::string family("Smith");
name += " K. "; // c-string
name += family; // string
name += '\n'; // character
std::cout << name;
return 0;
}
- c_str 返回C格式字符串
// strings and c-strings
#include <iostream>
#include <cstring>
#include <string>
int main()
{
std::string str("Please split this sentence into tokens");
char *cstr = new char[str.length() + 1];
std::strcpy(cstr, str.c_str());
char *p = std::strtok(cstr, " ");
while (p != 0)
{
std::cout << p << '\n';
p = std::strtok(NULL, " ");
}
delete[] cstr;
return 0;
} // cstr now contains a c-string copy of str
// strtok函数用于分割字符串,它通过修改原字符串来实现分割操作,
// 即在找到分隔符后,会将其替换为'\0'(空字符),使得返回的子字符串是独立的。
// strtok函数的第一个参数在第一次调用时是要分割的字符串,
// 在后续调用时应传递NULL,以继续分割同一个字符串。
// char *p = std::strtok(cstr, " ");
- find + npos 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
// string::find
#include <iostream> // std::cout
#include <string> // std::string
int main()
{
std::string str("There are two needles in this haystack with needles.");
std::string str2("needle");
// different member versions of find in the same order as above:
// 使用find函数在字符串str中查找str2(即"needle")首次出现的位置。
// find返回的是子字符串的起始位置,如果未找到子字符串,则返回std::string::npos。
std::size_t found = str.find(str2);
// 如果find函数找到了str2,则found不等于std::string::npos,进入条件分支。
if (found != std::string::npos)
std::cout << "first 'needle' found at: " << found << '\n';
// find函数这次使用的是三个参数的版本
// needles are small":要查找的字符串。
// found + 1:从上次找到的位置的下一个字符开始查找,即在字符串str中查找第二个"needle"的出现位置
// 只使用前6个字符(即"needle")进行匹配查找。
found = str.find("needles are small", found + 1, 6);
if (found != std::string::npos)
std::cout << "second 'needle' found at: " << found << '\n';
// 查找字符串"haystack"的位置,并输出。
found = str.find("haystack");
if (found != std::string::npos)
std::cout << "'haystack' also found at: " << found << '\n';
// 查找第一个句号"."的位置,并输出。
found = str.find('.');
if (found != std::string::npos)
std::cout << "Period found at: " << found << '\n';
// let's replace the first needle:
// 使用replace函数将字符串str中第一个出现的str2("needle")替换为"preposition"。
// 1.str.find(str2):找到str2的起始位置。
// 2.str2.length():获取str2的长度。
// 3.preposition":用这个字符串替换找到的"needle"。
str.replace(str.find(str2), str2.length(), "preposition");
std::cout << str << '\n';
return 0;
}
- rfind 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
// string::rfind
#include <iostream>
#include <string>
#include <cstddef>
int main()
{
std::string str("The sixth sick sheik's sixth sheep's sick.");
std::string key("sixth");
// rfind函数用于在字符串str中从右到左查找key(即"sixth")最后一次出现的位置。
// rfind返回的是子字符串的起始位置索引。如果未找到,返回std::string::npos
std::size_t found = str.rfind(key);
if (found != std::string::npos)
// 第一个参数found是要替换的起始位置。
// 第二个参数key.length()是要替换的长度(即"sixth"的长度)。
str.replace(found, key.length(), "seventh");
std::cout << str << '\n';
return 0;
}
- substr 在str中从pos位置开始,截取n个字符,然后将其返回
// string::substr
#include <iostream>
#include <string>
int main()
{
std::string str = "We think in generalities, but we live in details.";
// (quoting Alfred N. Whitehead)
// 第一个参数3表示子字符串的起始位置(从0开始计数,所以位置3对应的是"think"中的't')。
// 第二个参数5表示子字符串的长度,也就是从位置3开始,提取5个字符。
std::string str2 = str.substr(3, 5); // "think"
std::size_t pos = str.find("live"); // position of "live" in str
// 再次使用substr函数,从位置pos开始提取子字符串,直到字符串str的末尾。
std::string str3 = str.substr(pos); // get from "live" to the end
std::cout << str2 << ' ' << str3 << '\n';
return 0;
}
- 在string尾部追加字符时,s.push_back© / s.append(1, c) / s += 'c’三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
- 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
- string类非成员函数
- operator+ 尽量少用,因为传值返回,导致深拷贝效率低
- operator>> 输入运算符重载
// extract to string
#include <iostream>
#include <string>
main()
{
std::string name;
std::cout << "Please, enter your name: ";
std::cin >> name;
std::cout << "Hello, " << name << "!\n";
return 0;
}
- operator<< 输出运算符重载
// inserting strings into output streams
#include <iostream>
#include <string>
main()
{
std::string str = "Hello world!";
std::cout << str << '\n';
return 0;
}
- getline 获取一行字符串
// extract to string
#include <iostream>
#include <string>
int main()
{
std::string name;
std::cout << "Please, enter your full name: ";
std::getline(std::cin, name); 从标准输入流(键盘输入)中读取一整行字符并存储到 name 变量中
std::cout << "Hello, " << name << "!\n";
return 0;
}
- relational operators 大小比较
// string comparisons
#include <iostream>
#include <vector>
int main()
{
std::string foo = "alpha";
std::string bar = "beta";
if (foo == bar)
std::cout << "foo and bar are equal\n";
if (foo != bar)
std::cout << "foo and bar are not equal\n";
if (foo < bar)
std::cout << "foo is less than bar\n";
if (foo > bar)
std::cout << "foo is greater than bar\n";
if (foo <= bar)
std::cout << "foo is less than or equal to bar\n";
if (foo >= bar)
std::cout << "foo is greater than or equal to bar\n";
return 0;
}
- vs和g++下string结构的说明
vs下string的结构:
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间:
当字符串长度小于16时,使用内部固定的字符数组来存放;当字符串长度大于等于16时,从堆上开辟空间
还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量
最后还有一个指针做一些其他事情
故总共占16+4+4+4=28个字节
g++下string的结构
G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块>堆空间,内部包含了如下字段:
- 空间总大小
- 字符串有效长度
- 引用计数
- 指向堆空间的指针,用来存储字符串。
二、string类的模拟实现
1. 经典的string类问题
#include <assert.h>
#include <string.h>
using namespace std;
// 为了和标准库区分,此处使用String
class String
{
public:
/*String()
:_str(new char[1])
{*_str = '\0';}
*/
// String(const char* str = "\0") 错误示范
// String(const char* str = nullptr) 错误示范
String(const char *str = "")
{
// 这个构造函数的参数 str 被赋予了一个默认值 "",
// 这是一个指向空字符串的指针(即一个只包含终止符 \0 的字符数组)。
// 如果在创建 String 对象时没有传递参数,那么 str 将自动被赋值为 "",即空字符串。
if (nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char *_str;
};
// 测试
void TestString()
{
String s1("hello bit!!!");
String s2(s1);
}
上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
2.浅拷贝
也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。
3.深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
- 对比下面的赋值运算符
String &operator=(String s)
{
swap(_str, s._str);
return *this;
}
传值方式的使用:
在赋值运算符中,使用传值意味着函数参数String s在调用时会创建一个副本。也就是说,当执行String a = b;时,会调用operator=(String s),而参数s是对象b的一个副本。即当operator=被调用时,编译器会生成一个新的String对象(s),它是被赋值对象的副本。
swap(_str, s._str);将当前对象的内部指针_str与副本对象s的内部指针进行交换。由于s是传值过来的副本对象,交换之后,当前对象_str指向了副本对象的内存,而副本对象s则指向了当前对象原来所指向的内存。
交换操作完成后,副本对象s会在函数结束时自动销毁,其析构函数会释放它持有的内存,即之前当前对象持有的内存。因此,这种方式下的赋值运算符不会导致内存泄漏。为更优写法。
String& operator=(const String& s)
{
if(this != &s)
{
String strTmp(s);
swap(_str, strTmp._str);
}
return *this;
}
这个赋值运算符首先检查是否是自我赋值,然后为字符串分配新内存并将字符串内容复制过来,最后释放旧内存。这样做的优点是直接了当,但缺点是容易引入内存泄漏的风险,特别是在内存分配成功但复制内容失败时。
- 对比拷贝构造函数
String(const String &s)
: _str(nullptr)
{
String strTmp(s._str);
swap(_str, strTmp._str);
}
由于在拷贝构造函数中,首先创建了一个临时对象 strTmp,即使在创建临时对象时发生异常,当前对象的状态也不会被改变。只有当 strTmp 构造成功后,才会进行交换操作。因此,当前对象总是保持在一种安全状态。为更优写法。
String(const String &s)
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
这个拷贝构造函数使用了直接分配新内存的方式。它根据传入对象String中的字符串长度,分配相同长度的新内存,然后将字符串内容复制到新内存中。