c语言中的字符串

C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列
的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户
自己管理,稍不留神可能还会越界访问。

标准库中的string类

string类的简单理解

字符串是表示字符序列的对象。
标准字符串类为此类对象提供了类似标准字节容器接口的支持,但增加了专门设计用于处理单字节字符串的功能。
字符串类是basic_string类模板的实例化,使用char(即字节)作为字符类型,默认的char_traits配置类型(详见basic_string模板)。

简单来说就是串。在使用时必须包含#include头文件以及using namespace std;

auto和范围for

auto关键字

1、在早期 C/C++ auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量,后来这个
不重要了。 C++11 中,标准委员会变废为宝赋予了 auto 全新的含义即: auto 不再是一个存储类型
指示符,而是作为一个新的类型指示符来指示编译器, auto 声明的变量必须由编译器在编译时期
推导而得
2、用 auto 声明指针类型时,用 auto auto* 没有任何区别,但用 auto 声明引用类型时则必须加 &
3、当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际 只对第一个类型进行推导,然后用推导出来的类型定义其他变量
4、auto 不能作为函数的参数,可以做返回值,但是建议谨慎使用
// 不能做参数
void func2(auto a)
{
}
// 可以做返回值,但是建议谨慎使用
auto func3()
{
	return 3;
}

5、auto不能直接用来声明数组

下面来对上面进行一个总的演示

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;
	cout << typeid(c).name() << endl;
	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;
}

这里并不能显示出auto的用武之处,真正用得到的地方在于迭代器与map当中如下

#include<iostream>
#include <string>
#include <map>
using namespace std;
int main()
{
	std::map<std::string, std::string> dict = { { "apple", "苹果" },{ "orange",
	"橙子" }, {"pear","梨"} };
	// auto的用武之地
	//std::map<std::string, std::string>::iterator it = dict.begin();
	auto it = dict.begin();
	while (it != dict.end())
	{
		cout << it->first << ":" << it->second << endl;
		++it;
	}
	return 0;
}

范围for

1、对于一个 有范围的集合 而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11中引入了基于范围的 for 循环。 for 循环后的括号由冒号 分为两部分:第一部分是范围 内用于迭代的变量,第二部分则表示被迭代的范围 ,自动迭代,自动取数据,自动判断结束。
2、范围 for 可以作用到数组和容器对象上进行遍历
3、范围 for 的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
#include<iostream>
#include <string>
#include <map>
using namespace std;
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
// C++98的遍历
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
array[i] *= 2;
}
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
cout << array[i] << endl;
}
// C++11的遍历
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;
}

上述代码展示的是两种数组遍历的方式。第一种是下标+[ ]的方式,这种方式是之前常用的。下面的就是使用范围for的方式来对数组进行遍历。在auto后面加引用是的对e修改可以改变原数组数据。

string类的常用接口说明

在这里只讲解最常见的接口,并会一步步带你去认识以及如何使用一个你未曾学习或使用过的一个模块或函数。

首先先来到这个关于c++的网址,虽然不是官网但是这个网站很好使用,然后在上面的search栏输入你要了解的函数等。

点击go之后会进入下面的页面

其中在这里是对string类的解释

下面的那个类型简单看看就可以了。

然后来看下面的

这些是这里对string具体如何使用,如果你想要详细的了解一个点击他的名称

就会到达这个界面,其中绿色区域是该对construct中的使用方式下面的黑色字体是对上面的使用方式的具体解释。

如果还是没有彻底了解往下滑将会看到用一块紫色区域,该区域是对上方式的示例

在这里我来讲解一些常用的

1、string类对象的常见构造

下面我来对这些方式进行简单的实操一下

int main()
{
	string s1;
	string s2("hello JWN");
	string s3(s2);
	string s4(6, '$');//这里是对s4赋了5个字节的%字符
	string s5(6, 42);//如果字符位置是一个数字则为ASCII码值,需将其进行转化。
	cout << s1 << " " << s2 << " " << s3 << " " << s4 << endl;

	return 0;
}

2、string类对象的容量操作(capacity)

注意:
1. size() length() 方法底层实现原理完全相同,引入 size() 的原因是为了与其他容器的接
口保持一致,一般情况下基本都是用 size()
2. clear() 只是将 string 中有效字符清空,不改变底层空间大小。
3. resize(size_t n) resize(size_t n, char c) 都是将字符串中有效字符个数改变到 n 个,不
同的是当字符个数增多时: resize(n) 0 来填充多出的元素空间, resize(size_t n, char
c) 用字符 c 来填充多出的元素空间。注意: resize 在改变元素个数时,如果是将元素个数
增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
4. reserve(size_t res_arg=0) :为 string 预留空间,不改变有效元素个数,当 reserve 的参
数小于 string 的底层空间总大小时, reserver 不会改变容量大小。
string s("hello world");
cout << s.size() << endl;
cout << s.length() << endl;
cout << s.capacity() << endl;
cout << s.length() << endl;
char c;
string str;
cout << "Please type some lines of text. Enter a dot (.) to finish:\n";
do {
	c = cin.get();
	str += c;
	if (c == '\n')
	{
		cout << str;
		str.clear();
	}
} while (c != '.');

这里仅仅是对一些简单的举例

3、string类对象的访问及遍历操作

string s("hello world");
s[0] = 'm';//[]的重载就是为了让string类可以像数组一样同过下标对数组中的内容进行修改

string::iterator it = s.begin();//这里使用了迭代器
while (it != s.end())
{
	cout << *it <<" ";
	it++;
}
cout << endl;

string::reverse_iterator rit = s.rbegin();//这里是反向迭代器
while (rit != s.rend())
{
	cout << *rit << " ";
	rit++;
}
cout << endl;

上面的使用迭代器就是对数组便利的第三种方式。

下面这张图片是对上面迭代器遍历数组的图像解释

4、string类对象的修改操作

在这里主要讲解append、push_back、insert、erase、replace

这里我们依旧先点击我们要查找的操作名称,然后看绿色区域的方式,以及示例等,然后自己进行模拟。

append


int main()
{
	string str;
	string str2 = "Writing ";
	string str3 = "print 10 and then 5 more";

	// used in the same order as described above:
	str.append(str2);                       // "Writing "
	str.append(str3, 6, 3);                   // "10 "
	str.append("dots are cool", 5);          // "dots "
	str.append("here: ");                   // "here: "
	str.append(10u, '.');                    // ".........."
	str.append(str3.begin() + 8, str3.end());  // " and then 5 more"
	str.append<int>(5, 0x2E);                // "....."

	cout << str << '\n';
	return 0;
}

push_back


int main()
{
	string str;
	ifstream file("test.txt", std::ios::in);
	if (file) {
		while (!file.eof()) str.push_back(file.get());
	}
	cout << str << '\n';
	return 0;
}

insert

int main()
{
	string str = "to be question";
	string str2 = "the ";
	string str3 = "or not to be";
	string::iterator it;

	// used in the same order as described above:
	str.insert(6, str2);                 // to be (the )question
	str.insert(6, str3, 3, 4);             // to be (not )the question
	str.insert(10, "that is cool", 8);    // to be not (that is )the question
	str.insert(10, "to be ");            // to be not (to be )that is the question
	str.insert(15, 1, ':');               // to be not to be(:) that is the question
	it = str.insert(str.begin() + 5, ','); // to be(,) not to be: that is the question
	str.insert(str.end(), 3, '.');       // to be, not to be: that is the question(...)
	str.insert(it + 2, str3.begin(), str3.begin() + 3); // (or )

	cout << str << '\n';
	return 0;
}

erase

int main()
{
	string str("This is an example sentence.");
	cout << str << '\n';
	// "This is an example sentence."
	str.erase(10, 8);                        //            ^^^^^^^^
	std::cout << str << '\n';
	// "This is an sentence."
	str.erase(str.begin() + 9);               //           ^
	cout << str << '\n';
	// "This is a sentence."
	str.erase(str.begin() + 5, str.end() - 9);  //       ^^^^^
	cout << str << '\n';
	// "This sentence."
	return 0;
}

replace

int main()
{
	string base = "this is a test string.";
	string str2 = "n example";
	string str3 = "sample phrase";
	string str4 = "useful.";

	// replace signatures used in the same order as described above:

	// Using positions:                 0123456789*123456789*12345
	string str = base;           // "this is a test string."
	str.replace(9, 5, str2);          // "this is an example string." (1)
	str.replace(19, 6, str3, 7, 6);     // "this is an example phrase." (2)
	str.replace(8, 10, "just a");     // "this is just a phrase."     (3)
	str.replace(8, 6, "a shorty", 7);  // "this is a short phrase."    (4)
	str.replace(22, 1, 3, '!');        // "this is a short phrase!!!"  (5)

	// Using iterators:                                               0123456789*123456789*
	str.replace(str.begin(), str.end() - 3, str3);                    // "sample phrase!!!"      (1)
	str.replace(str.begin(), str.begin() + 6, "replace");             // "replace phrase!!!"     (3)
	str.replace(str.begin() + 8, str.begin() + 14, "is coolness", 7);    // "replace is cool!!!"    (4)
	str.replace(str.begin() + 12, str.end() - 4, 4, 'o');                // "replace is cooool!!!"  (5)
	str.replace(str.begin() + 11, str.end(), str4.begin(), str4.end());// "replace is useful."    (6)
	cout << str << '\n';
	return 0;
}

5、string类的操作

在这里主要来看c_str、find、rfind

c_str(返回c格式字符串)

int main()
{
	string file;
	cin >> file;
	FILE* fout = fopen(file.c_str(), "r");
	char ch = fgetc(fout);
	while (ch != EOF)
	{
		cout << ch;
		ch = fgetc(fout);
	}
	fclose(fout);
	return 0;
}

find(正着找字符串c,并返回位置)

int main()
{
	string s("hello world");
	size_t pos = s.find('w');
	//string suffix = s.substr(pos, s.npos);
	string suffix = s.substr(pos);
	cout << suffix << endl;

	return 0;
}

在这串代码中也顺便使用了substr和npos,这里也来简单的介绍一下substr是返回子串,从pos位置开始,到npos结束,npos指到字符串最后一个字符。如果那个位置啥也不写,也是从pos到结束。

rfind

其与find相反倒着寻找,并返回位置。依次类推下面的关于find的类型

6、string类的非成员函数

这里主要看operate+、getline

string s1("hello");
string s2 = s1 + "world";
string s3 = "world" + s1;

getline(cin, s1, '*');//后面*号位置若没有则默认换行结束
上面的几个接口大家了解一下,下面的 OJ 题目中会有一些体现他们的使用。 string 类中还有
一些其他的操作,这里不一一列举,大家在需要用到时不明白了查文档即可。

vs和g++下string结构的说明(了解)

注意:下述结构是在 32 位平台下进行验证, 32 位平台下指针占 4 个字节。

vsstring的结构

string 总共占 28 个字节 ,内部结构稍微复杂一点,先是 有一个联合体,联合体用来定义
string 中字符串的存储空间
当字符串长度小于 16 时,使用内部固定的字符数组来存放
当字符串长度大于等于 16 时,从堆上开辟空间
union _Bxty
{ // storage for small buffer or pointer to larger one
	value_type _Buf[_BUF_SIZE];
	pointer _Ptr;
	char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
这种设计也是有一定道理的,大多数情况下字符串的长度都小于 16 ,那 string 对象创建
好之后,内部已经有了 16 个字符数组的固定空间,不需要通过堆创建,效率高。
其次:还有 一个 size_t 字段保存字符串长度,一个 size_t 字段保存从堆上开辟空间总的
容量
最后:还 有一个指针 做一些其他事情。
故总共占 16+4+4+4=28 个字节。

g++string的结构

G++ 下, string 是通过写时拷贝实现的, string 对象总共占 4 个字节,内部只包含了一个
指针,该指针将来指向一块堆空间,内部包含了如下字段:
空间总大小
字符串有效长度
引用计数
struct _Rep_base
{
	size_type _M_length;
	size_type _M_capacity;
	_Atomic_word _M_refcount;
};
指向堆空间的指针,用来存储字符串

这次的介绍就结束了,下面一篇会讲到string类的底层实现。

感谢支持!!!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐