C++的string类

目录

一、导入

二、接口学习

1.默认成员函数

2.迭代器相关的函数iterator

3.与容量相关的函数Capacity系列

4.与成员权限相关的函数Element access:

5.修改器相关的函数Modifiers:

6.字符串操作接口函数String operations:​编辑

三、扩展


一、导入

学习过C++类的相关知识之后,我们便可以进行类的进阶学习。在C++的标准库中,有一些内置的类,string便是其中之一。它定义在头文件<string>中。这个类提供了丰富的接口来操作字符串,比如字符串的拼接、查找、替换、插入、删除等。

string也是一个模板,被typedef过。最开始是basic_string这样一个类。

值得一提的是,在学习string的过程中可能会有些许“逆风”。但是学习完这个类之后,再学习其他类,往后便是“大顺风”。

二、接口学习

string - C++ Reference (cplusplus.com),在CPP的官网中,提供了大量的关于string接口函数的介绍,本文旨在了解并熟悉使用string类的接口函数。

String class

Strings are objects that represent sequences of characters.    这是官网中对u有string类的介绍,说明了string类是一个管理字符数组的类。

在网站中对于string类的接口函数列举如下。

其中有默认成员函数Member functions,有迭代器相关的函数Iterators,有容量相关的函数Capacity,访问成员的接口函数:Element access,有负责修改的修改器函数Modifiers,还有String operations函数用来进行字符串操作,以及内部的一个成员常量npos和一些全局的函数Non-member function overloads。

可以看出,string的接口是非常繁多的,甚至有一些可以用冗杂形容。下面将以上述列举顺序为序,进行接口的一一列举学习。

1.默认成员函数

文献中列出了三个默认成员函数,constructor构造、destructor析构、operator=赋值重载。

constructor

这是构造函数内部的介绍,总共实现了7中函数重载,其中重要的有默认构造、拷贝构造……多种多样的函数,提供了大量不同的初始化方式。初始化可以传字符、字符串、对象……

(1) (4)是一个带参数、一个不带参数的类型。

这是关于函数的解释。(1)会被初始化为0。

演示:

#include <iostream>
#include <string>

using namespace std;

int main()
{
	string s1;
	string s2("hello world");
	
	cin >> s1;

	cout << s1 << endl;
	cout << s2 << endl;

	return 0;
}

在上述代码中,我们利用string类新建了两个对象,s1调用了默认构造中的缺省函数,s2调用了函数(4)。

由于string类中完成了 流提取和流插入 的重载,因此可以直接使用这两个操作符。当我们屏蔽s1的流提取操作之后,打印是这样的。

取消屏蔽之后,便可以完成打印输入的内容。

当然,内部也包括拷贝构造

int main()
{
	string s2("hello world");
	string s3(s2);


	cout << s3 << endl;
	cout << s2 << endl;

	return 0;
}

这段代码便是利用s2去拷贝构造s3。

我们也可以用字符串的一部分去传参。

对于hello world,假如我们只想传入world,便可以用这个函数。

int main()
{
	string s2("hello world");
	string s3(s2, 6, 5);
	cout << s3;

	return 0;
}

w的下标是6,字符串的长度是5。

对于这个长度5,我们也不需要手动计算。

    string s3(s2, 6, s2.size() - 6);
我们只需要让总的长度 - w的下标即可。size()接口计算的是大小(即长度length,不包含0)。

注意到,len参数给了一个缺省值,nops,这是定义在string中的一个成员。下面是string::npos的解释

给出的值是-1。但是由于是size_t类型,所以这是整数的最大值:42亿。但显然,这会造成越界。因此下面给出的解释中, means "until the end of the string".表示,如果字符串小于npos的长度,就到达字符串的末尾。

当然,我们给函数传入缺省参数时,必须是从右到左缺省!

还可以用n个字符去初始化。

还可以采用迭代器(下面介绍)去初始化。

传入迭代器的一段区间去初始化。当上述初始化时,由于迭代器支持++、--操作,我们去掉了首尾。

当然,最多的:默认构造、拷贝构造、字符串构造

destructor析构函数

这是这是析构函数,用来完成资源的清理。由于析构函数自动调用,所以不需要过多处理。

operator=赋值重载

可以采用字符、字符串、对象传参,去完成赋值运算。其返回值是*this。

实例:

2.迭代器相关的函数iterator

引入

遍历字符串时,我们可以有两种方式,但是学习完迭代器之后,方式便多了一种。

方式一:下标法

由于string重载了[]操作符,我们可以直接用[]去完成资源的访问。

int main()
{
	string s2("hello world");
	for (size_t i = 0; i < s2.size(); i++)
	{
		cout << s2[i] << ' ';
	}
	cout << '\n';

	return 0;
}

方法二:迭代器法

string提供了begin 和 end 两个成员函数,函数简介中说明了,返回的是一个迭代器的beginning和end,用来遍历字符串。begin和end的行为和指针类型类似,但是只能说是用起来类似,却不是完全一致!

这是关于begin和end的函数介绍

值得一提的是,end指向字符串最后一个字符的迭代器,即返回的是/0的位置,而不是最后一个输入的有效字符。因为字符串除了存储我们需要的有效数据,还会增加一个/0。

他们的返回类型都是iterator和const修饰后的iterator两种。因此我们在使用的时候,应该用iterator类型来建立变量。由于迭代器是众多类都有的,因此我们应该声明,这是string的迭代器。

实例:

int main()
{
	string s2("hello world");
	
	string::iterator bg = s2.begin();
	string::iterator end = s2.end();

	while (bg != end)
	{
		cout << *bg << ' ';
		bg++;
	}

	cout << endl;
	return 0;
}

在上述遍历中,我们采用了解引用*和++操作。这两种操作都是指针在遍历时常常用到的操作,所以说迭代器的行为类似指针。

注意,我们写的是bg != end。这种写法主要为了和别的迭代器完成形式的统一。

其实string的迭代器可以写成bg < end;也可以。原因是string的字符串内部的存储空间是连续的。

但是到了vector这种类,内部的空间不连续时,我们只能使用!=,而不是<。

对于不连续的空间,指针减法、指针加法都是未定义的! 在后续的容器中,指针++是允许的,这是因为进行了操作符的重载。否则要进行cur = cur->next;的操作。

这就体现了C++的封装!只需要给你接口,让你用就好!

当然,上述的迭代器由于没有被const修饰,因此可以完成写的操作。

我们将字符串修改成了a。

当然也有被const修饰过的迭代器,来完成只读操作。

这时候,迭代器的返回类型就成了const_iterator类型。

int main()
{
    const string s2("hello world");
	
	string::const_iterator bg = s2.begin();
	string::const_iterator end = s2.end();

	while (bg != end)
		cout << *bg++;

	cout << endl;
	return 0;
}

这就是只读操作。迭代时,先用*bg,再bg++。当然,对于这种复杂的类型,我们可以直接用auto代替。

int main()
{
    const string s2("hello world");

    auto bg = s2.begin();
    auto end = s2.end();

    while (bg != end)
        cout << *bg++;

    cout << endl;
    return 0;
}

方式三:采用范围for

int main()
{
    
    string s2("hello world");

	for (auto& ch : s2)
	{
        ch = 'a';
		cout << ch;
	}
	cout << endl;

	return 0;
}

范围for只需要用 变量 :容器,就可以完成容器中内容的读取。范围for的特点是:自动迭代、自动结束。此处可以用&类型,也可以用auto ch : s2进行迭代,但是无法完成数据的修改!

当然,如果我们观察反汇编,就会观察到其实范围for的本质还是迭代器!

在begin和end的下面,还有两个

rbegin和rend表示的是reverse,表示的是逆置的迭代器。

int main()
{
	string s2("hello world");

	auto bg = s2.rbegin();
	auto end = s2.rend();

	while (bg != end)
		cout << *bg++;

	return 0;
}

通过上述的使用,就实现了逆置的迭代。

因此迭代器有const修饰和非const修饰、正向、逆向四个版本。

其实每个容器都有自己的迭代器,但是这些迭代器的行为都是十分相似!

c修饰的这几个迭代器则是C++11才支持的语法。c表示const,但是我们基本不用,由于原来的begin本身就是有重载过const过的,我们只需要auto去推断类型就可以。

3.与容量相关的函数Capacity系列

Part 1

这里面已经有我们之前用过的size()函数。

我们为什么说string设计的十分冗杂,这里也能体现。

其实size()与length()接口都是计算长度的,只不过为了和其他容器统一(string出现的比较早,出现string时,STL还没有问世呢!),才出现了size()接口。

Part 2

下一个时max_size函数,这个函数用来返回string字符串的最大容量。Returns the maximum length the string can reach.但其实一般来说,我们很少去用这个函数,主要是对于最大容量的需求极少。

resize接口:

当我们建立好一个string对象之后,可以用这个函数去提前开辟一个空间,来防止多次扩容的影响。

int main()
{
	string s;
	s.resize(10);

	cout << s.size() << endl;
	return 0;

在上述代码中,我们将s的空间初始化为10。我们也可以借助这个函数去完成初始化。

int main()
{
	string s;
	s.resize(10, 'a');

	cout << s.size() << endl;
	cout << s << endl;
	return 0;
}

这段代码,就把s字符串的空间初始化为了10个‘a'。

capacity接口。

对于string的实现,我们姑且可以认为是由三部分组成:_str、_size、_capacity。其中的_capacity就是给_str开辟的容量。我们可以利用capacity接口去访问容量。

int main()
{
	string s;
	s.resize(10, 'a');
		
	cout << s.capacity() << endl;	//15
	cout << s.size() << endl;	//10
	return 0;
}

在上述的代码中,capacity被开辟到了15字节,size为10字节。

Part 3

reserve

reserve(保存、留存)不同于reverse(逆置)。reserve是用来申请容量的变化的。

int main()
{
	string s;
	s.resize(10, 'a');
		
	cout << s.capacity() << endl;	//15

	s.reserve(20);

	cout << s.capacity();  //31

	return 0;
}

可以看到,我们申请了20个字节,但是最终给我们了31个字节。这就跟reserve的特性有关。

当我们进行资源申请时,我们至少会得到n个字节的内存,甚至可能会多给我们一些内存。

clear接口

这个接口是用来清楚string的内容的。他会消除内容,但不会释放空间。

int main()
{
	string s;
	s.resize(10, 'a');
		
	cout << s.capacity() << endl;	//15

	s.clear();

	cout << s.size() << endl;
	cout << s.capacity();  //15

	return 0;
}

empty接口

就像是栈、堆等empy函数,这个函数也是用来判断有没有内容的。

shrink_to_fit接口

这个接口是用来减少容量的。它会将容量减少到适宜的空间大小。但一般我们不用用这个接口,毕竟减少容量再去扩容是由风险的

4.与成员权限相关的函数Element access:

1.[]的运算符重载

[]的重载支持在string中直接yong[]来访问内部内容。

2.at

at函数就是[]的另一种形式,也是用来访问内部元素的

int main()
{
	string s("hello world");
	
	cout << s.at(4) << endl;	//打印0
	
	return 0;
}

back函数:

返回最后一个字符的索引。

front函数:

返回第一个字符的索引。

5.修改器相关的函数Modifiers

Part 1:

1.operator+=

这是用来尾插的运算符重载。

int main()
{
	string s("hello world");
	
	s += "ace";

	cout << s;
	
	return 0;
}

完成了尾插ace。

2.append(附加)接口

append是用来追加字符串的。类似于+=

int main()
{
	string s("hello world");
	
	s.append(" hello");

	cout << s;
	
	return 0;
}

3.push_back接口

同理,也是完成+=的尾插操作的。

Part 2:

1.assign函数

Assigns(分派) a new value to the string, replacing its current contents。用一个新的值,去覆盖原来字符串的值。注意,此处的覆盖是完全覆盖,需要抹去之前的内容。

int main()
{
	string s("hello world");
	
	s.append(" hello");

	s.assign("ni hao");

	cout << s;
	
	return 0;
}

同理,我们可以用一部分值去替换。

int main()
{
	string s("hello world");
	
	string s2("nihao");

	s.assign(s2, 2, 2);

	cout << s;
	
	return 0;
}

用s2串的下标为2处,用长度为2的字符串去替换原来的字符串。

insert接口

类比栈和队列的insert,用来完成任意位置处数据的插入。

既可以在任意位置插入想要的内容,也可以在任意位置插入想要的某个字符串的子串(2)。

int main()
{
	string s("hello world");
	
	string s2("ni hao ");

	s.insert(0, s2);

	cout << s;
	
	return 0;
}

在上述代码中,我们将s2插入到s的头部。

当我们只想把hao插入到s中时,需要用(2)这个接口。

int main()
{
    string s("hello world");
    
    string s2("ni hao ");

    s.insert(0, s2, 3, 4);

    cout << s;
    
    return 0;
}

当然,也支持用迭代器去操作。

由于接口繁多,我们在使用的时候,查阅文档就好。同时,对于大部分修改内容、查阅内容的接口,都是支持迭代器的。

但是需要注意的是,insert能不用就不用,因为牵扯到数据的挪动,消耗极大。

replace接口

replace不同于assign,replace是“替换” ,而assign是“分配” 。

假如我们想让ni hao替换hello,只需要用(3)就可以

int main()
{
	string s("hello world");
	
	s.replace(0, 5, "ni hao ");

	cout << s;
	
	return 0;
}

当然,我们也可以直接传入对象来替换。

当然,replace与insert都是不建议经常使用的,否则内部会进行数据的挪动,消耗极大。

erase接口

如其他的erase,这个接口就是用来完成删除操作的。

1)传入位置与长度   2).传入一个迭代器,来删除一个字符   3).传入迭代器的一段区间去删除。

当然,由于数据的删除也是进行数据的挪动,因此也是消耗极大,尽量少使用。

int main()
{
	string s("hello world");
	
	s.erase(0, 3);
	cout << s << endl;
	
	return 0;
}

swap接口

这是用来交换字符串的内容的。

int main()
{
	string s("hello world");
	string s2("ni hao ma ");
	
	s.swap(s2);
	cout << s << endl;
	cout << s2 << endl;
	
	return 0;
}

当然,string还定义了一个全局的swap。

由于还存在模板swap,所以在string这个地方,就有三个swap可以使用。

但是由于存在全局的swap,并且参数匹配,string对象优先使用内部的swap,而不会使用全局swap。

这就省去了拷贝构造,大大提高了效率。

int main()
{
	string s("hello world");
	string s2("ni hao ma ");
	
	swap(s, s2);
	cout << s << endl;
	cout << s2 << endl;
	
	return 0;
}

pop_back接口

用来尾删的接口。

6.字符串操作接口函数String operations:

part 1:

c_str接口

我们都知道,C++是兼容C的语言,那我们在使用C的一些函数的时候,他只允许传入字符串,而不是传入自定义类型string对象,那该怎么办呢?这时候C++就提供了一个接口,c_str(),他能将string对象变成具有字符串一样行为的能力。

当我们使用strlen时,会报错,但是用这个接口之后,就不会报错。

同理,下一个接口

date接口

也是得到string字符串数据的,但是我们一般还是c_str()接口用的更多。

part 2:

get_allocator接口

get_allocator() 是 string 类的一个成员函数,它返回与 string 容器关联的分配器对象的副本。

分配器(Allocator)是C++标准库中的一个组件,它负责在容器(例如 vectorstringdequelist 等)中分配和释放内存。每个容器都有一个关联的分配器,用于管理容器的内存需求。

get_allocator() 函数允许你在容器之外使用相同的分配策略来分配内存,这样可以保证程序中内存分配的一致性。这在某些情况下是有用的,比如当你需要在容器之外分配与容器内部数据结构兼容的内存时,或者当你需要测量或控制内存使用时。

使用 get_allocator() 可以确保分配的内存与 string 内部使用的内存来自相同的内存池,这可以减少内存碎片,提高内存使用效率。

由于返回类型及其复杂,我们可以用auto去自动推导。

#include <iostream>
#include <string>

int main() {
    std::string s("Hello, world!");
    std::allocator<char> alloc = s.get_allocator();

    // 使用与string相同的分配器来分配内存
    char* p = alloc.allocate(10);

    // ... 使用分配的内存 ...

    // 释放内存
    alloc.deallocate(p, 10);

    return 0;
}

这是STL的六大组件:

copy接口

在C++中,string 类提供了一个 copy 成员函数,用于将字符串中的字符复制到指定的字符数组中。这个函数的用法比较直接,下面是一个简单的例子来展示如何使用 string 类的 copy 函数:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";

    // 创建一个字符数组来接收复制的字符串
    char buffer[20];

    // 使用copy函数将字符串复制到buffer中
    // 注意:copy函数不会自动添加空字符('\0')到复制的字符串末尾
    str.copy(buffer, 5, 0); // 从字符串的第一个字符开始复制5个字符

    // 手动添加空字符以终止字符串
    buffer[5] = '\0';

    std::cout << "Copied string: " << buffer << std::endl;

    return 0;
}

在这个例子中,str.copy(buffer, 5, 0) 调用将 str 中的前5个字符复制到 buffer 中。copy 函数的前两个参数分别是目标数组和要复制的字符数量。第三个参数是可选的,表示从源字符串的哪个位置开始复制(偏移量)。如果省略第三个参数,默认从字符串的开头开始复制。

需要注意的是,copy 函数不会自动在复制的字符串末尾添加空字符(\0),因此在上面的例子中,我们需要手动添加空字符来确保 buffer 是一个以空字符结尾的字符串。如果不添加空字符,buffer 可能包含未初始化的数据,这可能导致未定义的行为,特别是当你尝试使用 std::cout 或其他字符串处理函数时。

可以看得出来,这其实是比较鸡肋的函数。

part 3 : find系列

在C++中,string 类提供了一个 find 成员函数,用于在字符串中查找子字符串或字符。find 函数返回找到子字符串或字符的第一个实例的位置,如果没有找到,则返回一个特殊的常量 std::string::npos

使用 find 时,你应该总是检查返回值是否为 std::string::npos,以确定是否找到了匹配的字符。

他的返回类型是size_t,返回的是数据的下标。

int main()
{
	string s1("a b c d e f g");

	cout << s1.find("d e f") << endl;


	return 0;
}

在上述代码中,打印了第一次出现d e f出现时的下标,打印为6。

find是正向查找,那么rfind就是反向查找

查找的是对应的内容最后一次出现时的位置。

find_first_of接口

find_first_of 函数的返回值是一个 size_t 类型的值,它表示在字符串中找到的匹配字符的第一个实例的位置。这个位置是从字符串的开头开始的索引,从0开始计数(但不一定返回0)。

同时也提供了查找的位置。


int main()
{
    string str("Please, replace the vowels in this sentence by asterisks.");
    size_t found = str.find_first_of("aeiou");
    while (found != std::string::npos)      //找到时
    {
        str[found] = '*';

        //从found + 1继续查找
        found = str.find_first_of("aeiou", found + 1);  //found是找到的索引,found + 1,表示往后查找
    }

    cout << str << '\n';

    return 0;
}

在上述代码中,我们不断查找位置,然后替换成*

find_last_of 是倒着查找。find_first_not_of 、find _last_not_of则是找到不是指定内容的对应内容。

下一部分

substr接口

substr接口的返回类型是string类型,因此可以用来取出一部分去初始化另一个string对象。

int main()
{
    string str("Please, replace the vowels in this sentence by asterisks.");
    string s2(str.substr(0, 6));

    cout << s2 << endl;

    return 0;
}

compare接口

Compares the value of the string object (or a substring) to the sequence of characters specified by its arguments.它可以比较两个字符串或者两个字串,相同返回0,<0 和 >0是字符串的大小比较。

实例:

int main()
{
    string str("Please, replace the vowels in this sentence by asterisks.");
    string s2(str.substr(0, 6));

    cout << s2.compare(str) << endl;

    return 0;
}

7.全局的函数

除了我们知道的swap、<<   、>>  。还有加法,用来完成string类对象的相加,但是加法也得少用,由于是传值返回,消耗比较大。

关系操作符的重载

内部主要是一些常用的关系操作符,有 ==    >=    <=    <   >   != 。

输入控制:getline

这也是一个非成员的函数重载

我们都知道,在进行多次输入的时候,通常会把空格或者换行符作为两个string对象的间隔,此时当我们想要规定使用换行作为间隔时,可以用这个接口。

他的返回类型是i流,也就是说支持连续的输入。参数中的str是需要存储的字符串。

当我们想让s1存储 i love you  ,s2 存储 about you ?时,就不能使用简单的cin >> s1 >>s2;因为>>会把空格当成分隔符,这时候就需要getline。

int main()
{
    string s1, s2;

    getline(cin, s1);
    getline(cin, s2);

    //cin >> s1 >> s2;

    cout << s1 << endl;
    cout << s2 << endl;

    return 0;
}

三、扩展

关于utf-8和gbk的介绍:

UTF-8 和 GBK 是两种不同的字符编码方案,它们用于将字符转换为计算机可以理解的字节序列。
 

UTF-8
UTF-8(Unicode Transformation Format - 8-bit)是一种可变长度的编码方案,可以表示 Unicode 标准中的任何字符。UTF-8 使用 1 到 4 个字节来表示一个字符,具有以下特点:
- 对于 ASCII 字符(代码点在 0-127 之间),UTF-8 编码与 ASCII 编码相同,使用一个字节。
- 它向前兼容 ASCII,这意味着任何仅包含 ASCII 字符的文本文件也是有效的 UTF-8 编码。
- 它为每个字符提供了明确的字节顺序,因此不需要字节序标记(BOM)。
- 它是一种自同步编码,即使在传输过程中丢失或篡改了某些字节,也容易恢复同步。
- 它在全球范围内被广泛使用,特别是在互联网标准和开放源代码社区中。

GBK
GBK(汉字内码扩展规范)是一种双字节的编码方案,最初是为了在简体中文版的 Windows 操作系统中支持更多的汉字而设计的。GBK 编码具有以下特点:
- 它使用两个字节来表示一个汉字,兼容 GB2312 编码。
- GBK 编码包含了 GB2312 的所有字符,并且增加了更多的汉字和符号,总共可以表示超过 70000 个字符。
- GBK 在中国大陆地区被广泛使用,尤其是在一些旧的软件和文档中。
- GBK 不兼容 ASCII 编码,这意味着纯 ASCII 文本在 GBK 编码下可能会被错误地解释。

 区别
- 编码方式:UTF-8 是可变长度的,而 GBK 是固定长度的。
- 字符集:UTF-8 可以表示 Unicode 标准中的所有字符,而 GBK 主要用于中文字符和一些符号。
- 兼容性:UTF-8 向前兼容 ASCII,而 GBK 不兼容 ASCII。
- 使用范围:UTF-8 在全球范围内使用,特别是互联网和开放源代码项目。GBK 主要在中国大陆地区使用。
在选择编码方案时,如果需要支持多种语言和全球化的应用,通常会选择 UTF-8。如果应用主要面向中文用户,且需要兼容旧的系统和软件,可能会考虑使用 GBK。随着 Unicode 和 UTF-8 的普及,GBK 的使用正在逐渐减少。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值