C++:渴望力量吗,少年?
文章目录
- 二、string类的介绍与使用
- 2. 使用
- (5)string类对象的修改操作
- 三、拷贝
- 1. 引入
- 2. 浅拷贝
- 3. 深拷贝
- 总结
二、string类的介绍与使用
2. 使用
(5)string类对象的修改操作
函数名称 | 功能说明 |
---|---|
push_back | 在字符串后尾插字符 |
append | 在字符串后追加一个字符串 |
operator+= (重点) | 在字符串后追加字符串str |
c_str(重点) | 返回C格式字符串 |
find + npos(重点) | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
注意:
a. 在string尾部追加字符时,s.push_back(c ) / s.append(1, c) / s += ‘c’ 三种的实现方式差不多,一般情况下string类的+=操作用得比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
b. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
void test_string6()
{
cout << "我是test_string6 :" << endl;
string s;
//s.reserve(100);//保留至少100个字节的空间,如果提前知道所需的空间,可以提前开好,因为扩容需要代价
//容器的容量不一定就是我们开的那个,可能大于这个数字
size_t old = s.capacity();//Return size of allocated storage(返回已分配的空间大小)
cout << "初始" << s.capacity() << endl;
for (size_t i = 0; i < 100; i++)//观察扩容的情况
{
s.push_back('x');
if (s.capacity() != old)
{
cout << "此时i = " << i << ", 扩容:" << s.capacity() << endl;
old = s.capacity();
}
}
s.reserve(10);//string可以自由地优化,并使字符串的容量大于 n。
cout << s.capacity() << endl;
}
void test_string7()//考虑现有的size和capacity的关系
{
cout << "我是test_string7 :" << endl;
string s1("hello world");
cout << s1 << endl;
cout << s1.size() << endl;//s.size() == s.length(),这两个都不包括斜杠零
cout << s1.capacity() << endl;
//s1.resize(13);//多出来的空间放置斜杠零
//s1.resize(13, 'x');//已有数据个数小于resize的个数,相当于插入新的数据
s1.resize(20, 'x');//如果resize开的空间大于最开始的capacity也就是15,就会自动扩容
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(5);//已有数据的空间大于resize的空间,相当于删除数据
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
string s2;
s2.resize(10, '#');//字符串没有数据/没有初始化,相当于直接填充数据
cout << s2 << endl;
cout << s2.size() << endl;
cout << s2.capacity() << endl;
s2[0]++;
s2.at(0)++;//Return value:The character at the specified position in the string.(返回特定位置的字符)
cout << s2 << endl;
}
void test_string8()
{
cout << "我是test_string8 :" << endl;
string ss("world");
string s;
s.push_back('#');
s.append("hello");//在对象s的结尾追加,和pushback、+=的效果差不多,一般使用+=多一些
s.append(ss);
cout << s << endl;
s += '#';
s += "hello";
s += ss;
cout << s << endl;
string ret1 = ss + '#';//尽量使用 +=,因为 + 的拷贝构造次数会更多,影响效率
string ret2 = ss + "hello";
cout << ret1 << endl;
cout << ret2 << endl;
}
void test_string9()
{
cout << "我是test_string9 :" << endl;
std::string str("xxxxxxx");
std::string base = "The quick brown fox jumps over a lazy dog.";
//str = base;//可以直接用 = 赋值
//cout << str << endl;
str.assign(base);//相当于base赋值给str,会覆盖原来的值
std::cout << str << '\n';
str.assign(base, 5, 10);//从base下标为5的位置开始将10个字符赋值给str
std::cout << str << '\n';
//str.assign(base.begin()+16,base.end()-12);//参数也可以是迭代器
}
void test_string10()
{
cout << "我是test_string10 :" << endl;
// insert/erase/repalce能不用就尽量不用,因为他们都涉及挪动数据,效率不高
// 接口设计复杂繁多,需要时查一下文档即可
std::string str("hello world");
str.insert(0, 1, 'x');//在下标为第一个参数的位置插入个数为第二个参数的第三个参数
str.insert(str.begin(), 'x');
cout << str << endl;
str.erase(5);//从下标为5的位置开始擦除内容,第二个参数是缺省参数,不写默认擦除第一个参数后面的所有内容
cout << str << endl;
std::string s1("hello world");
s1.replace(5, 3, "%%20");//将从指定位置开始的指定个数字符替换为指定字符(串)
//3:Number of characters to replace (if the string is shorter, as many characters as possible are replaced).
cout << s1 << endl;
// 要求将空格替换为20%(以空间换时间)(replace效率不够高)
std::string s2("The quick brown fox jumps over a lazy dog.");
string s3;
for (auto ch : s2)
{
if (ch != ' ')
{
s3 += ch;
}
else
{
s3 += "20%";
}
}
//s2 = s3;
//s2.assign(s3);
printf("s2:%p\n", s2.c_str());//c_str可以返回这个对象的指针
printf("s3:%p\n", s3.c_str());
//交换的目的是让s2的内容变成修改之后的内容
swap(s2, s3);//注意有swap模板库,也有全局函数swap,还有string自己提供的swap,如果用的话一般自定义类型不要用模板的swap,因为要拷贝构造的次数比较多
//不过一般直接调用swap交换字符串也不会调用模板库的,因为匹配的模板和匹配的已经实现的函数之间编译器会优先使用已经实现的函数
//s2.swap(s3);
printf("s2:%p\n", s2.c_str());
printf("s3:%p\n", s3.c_str());
cout << s2 << endl;
}
void test_string11()
{
cout << "我是test_string11 :" << endl;
string s1("test.cpp.tar.zip");
//返回找到的下标
//size_t i = s1.find('.');//正向查找第一个 .
size_t i = s1.rfind('.');//反向查找第一个 .
cout << i << endl;
string s2 = s1.substr(i, 3);//从下标为i的位置开始取子串,第二个参数没写就取剩下的全部
cout << s2 << endl;
//string s3("https://legacy.cplusplus.com/reference/string/string/rfind/");
string s3("ftp://www.baidu.com/?tn=65081411_1_oem_dg");
// 协议
// 域名
// 资源名
string sub1, sub2, sub3;
size_t i1 = s3.find(':');
if (i1 != string::npos)//因为找不到对应的字符是返回npos
sub1 = s3.substr(0, i1);//注意是左闭右开的区间,substr返回一个子串
else
cout << "没有找到i1" << endl;
size_t i2 = s3.find('/', i1 + 3);
if (i2 != string::npos)
sub2 = s3.substr(i1 + 3, i2 - (i1 + 3));
else
cout << "没有找到i2" << endl;
sub3 = s3.substr(i2 + 1);//取到末尾
cout << sub1 << endl;
cout << sub2 << endl;
cout << sub3 << endl;
}
void test_string12()
{
cout << "我是test_string12 :" << endl;
/*std::string str("Please, replace the vowels in this sentence by asterisks.");
std::size_t found = str.find_first_not_of("abc");//找到字符串第一个不是abc的字符并返回下标
while (found != std::string::npos)
{
str[found] = '*';
found = str.find_first_not_of("abcdefg", found + 1);//第二个参数:Position of the first character in the string to be considered in the search.
}
std::cout << str << '\n';*/
std::string str("Please, replace the vowels in this sentence by asterisks.");
std::size_t found = str.find_first_of("abcd");//找到第一个是abcd中其中一个字符的并返回下标
while (found != std::string::npos)
{
str[found] = '*';
found = str.find_first_of("abcd", found + 1);
}
std::cout << str << '\n';
}
int main()
{
test_string6();
test_string7();
test_string8();
test_string9();
test_string10();
test_string11();
test_string12();
return 0;
}
string类中还有一些其他的操作,这里不一一列举,大家在需要用到时查文档即可。
三、拷贝
1. 引入
上面已经对string类进行了简单的介绍,其他不太常用的函数就不再写了,只要能够保证上面的内容都能熟练运用即可。为什么模拟实现string类?模拟string类最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数,只有能够模拟出来,才是真正了解它的底层逻辑。
问题:以下string类的实现是否有错?
代码如下:
class String {
public:
String(const char* 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);
}
这段代码其实是有很大的问题的,这个关于拷贝的问题也是我们学习C++经常会遇到的问题,所以还是得理解为什么。
上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
2. 浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中存在管理的资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
3. 深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
上面的代码可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不和其他对象共享。
如图,显式地定义拷贝构造函数,在初始化列表重新为_pStr开辟一个新的空间,再把数据从就空间拷贝过去,这样就实现了深拷贝。
总结
前面这两小节主要还是为了下一节地模拟实现做准备,要尽可能做到理解并熟练运用。