这篇文章我们来讲字符串。字符串可以说是最重要的变量类型了,因为对字符串的读写极大地影响到我们的程序和用户之间的交互。甚至很多很庞大的程序就只是在处理字符串。
对于字符串,我们同时需要有关于数组和指针的关系,字符串的实现与数组是紧密相连的。字符串本质是group of characters,是一堆字符的合集。而一堆字符可以组成各种各样我们所需的文本,这些其实都是字符串,所以字符串是C++处理文本的方式。主要目的是对文本进行操作并将其展现出来给用户互动。
那么在了解字符串之前,我们首先需要了解字符character。我们常见的所有的字母,符号,都是字符。在C++当中,我们用数据类型char来表示字符。而C++默认的字符编码方式是ASCII,一个字符用一个字节来表示,那么就总共有 种不同的选择。但是我们知道,如果只有256种选择,那么当我们想表示中文或者日文的时候,这个选择数量是远远不够的。所以为了能够表示更多的语言,我们有很多其他的编码方式来进行处理,比如utf_16,允许我们用两个字节,16位来表示一个字符,那么就可以有 种不同的选择。这样可以表示的语言就更多了。当然在这里的话我们并不会深入编码的相关知识,只是给大家做个了解即可。
回到默认的char,因为char只有一个字节,所以可以用来按字节处理内存或者分配缓存等。在C++当中,我们通常会用单引号来表示一个字符。
文本和语言其实是非常非常复杂的东西,所以这里我们掌握基本的知识差不多就够用了。
那么有了字符之后,什么是字符串?字符串其实就是字符数组。我们可以举一个简单的例子。
cpp
#include<iostream>
int main() {
const char* name = "Cherno";
std::cout << name << std::endl;
std::cin.get();
}
这样我们就写下了一段有C风格的字符串定义代码。可以看到我们定义了一个const char类型的指针,指向了一个字符串。而在C++当中,字符串需要用双引号括起来。但是我们可以看到,这里我们是不需要写一个new的,也就是我们其实还是在栈上分配的内存。
接下来我们进入到内存里面去看看长什么样子:
可以在右侧看到我们数据的ASCII码,确实就是我们输入的Cherno。如果我们增加一句话,输出一下这个字符串的大小,看看是多少。
不过需要注意的是,如果我们使用上面的代码直接输出sizeof(name),那么它返回的其实是指针的大小,我们无法真的得到这个字符串的大小,所以我们这样写(VS2022下的结果):
cpp
#include<iostream>
int main() {
const char name[] = "Cherno";
std::cout << name << std::endl;
std::cout << sizeof(name) << std::endl;
std::cin.get();
}
这样我们可以看到,输出的长度是7而不是我们看到的6,这是因为我们在最后还有一位空字符,这个被称为空终止符,是编译器自动加上来的。这个是因为name本身是指针的情况下,编译器依然应该知道在哪里停止下来,所以我们才能够直接输出。
需要注意的一点是,如果我们定义好了一个字符串,就意味着我们没有办法再改变它的长度了,如果想要更长的字符串,我们只能删除掉重新写一个。当然如果添加了const关键字,那就什么都改变不了了。
但是如果我们做这样的定义:
cpp
char name[6] = { 'C', 'h', 'e', 'r', 'n', 'o' };
std::cout << name << std::endl;
那么我们就会得到下面这个经典的输出:
也就是所谓的"口中直喊烫烫烫",这个是因为什么,我们也可以进入内存里一探究竟。
因为未初始化的内存自动填充了cc,而0xcccc在GB2312当中刚好对应烫字,所以我们就会看到一堆烫烫烫了。这个成为stack guard,在debug模式下会出现的问题。
为了防止出现这么多烫烫烫,我们需要在最后面手动添加上空字符'\0'或者直接是0
cpp
char name[7] = { 'C', 'h', 'e', 'r', 'n', 'o', '\0'};
cpp
char name[7] = { 'C', 'h', 'e', 'r', 'n', 'o', 0};
这两种定义方式是等效的,都是可以正常使用的。
以上都是C风格的字符串,那么在C++当中,我们更多使用的是string,而string相对而言容易使用得多。string类是一个char以及一些用来操纵这个char的方法的集合。实际上string还有一个模板类叫做basic_string,而我们使用的string是对basic_string的template specialization。
cpp
#include<iostream>
#include<string>
int main() {
std::string name = "Cherno";
std::cout << name << std::endl;
std::cout << name.size() << std::endl;
std::cin.get();
}
这样我们就可以直接获得name的长度为6,而且如果我们把鼠标放到"Cherno"上面,会发现其真实的类型是const char[],这同样也是name的真实类型。这里的size则是C++风格的语句了,如果是C风格,还需要strlen()、strcpy()等函数。
如果我们有两个string类型的变量,我们可以直接对其进行相加操作,因为"+"在string类当中进行了重载,使得我们可以这样操作。
cpp
std::string name = "Cherno";
std::string language = "CPP";
std::string lesson = name + language;
但是需要注意的是,两个const char*是不能直接相加的,理由也很简单,两个指针类型怎么可能相加呢?但是因为"+"在string中被重载了,所以如果是一个string加一个const char*,这个是可以支持的。
cpp
std::string name = "Cherno";
std::string lesson = name + "CPP";
这样写是合法的。
那么如果就想直接把两个const char*相加,应该怎么办?答案是强制类型转换。
cpp
std::string lesson = (std::string)"Cherno" + "CPP";
这样就可以了。
string有很多方法,这里介绍另一个方法,叫做find,作用是寻找这个字符串内有没有对应的子串。但是因为string并没有contain方法来判断是否真的包含一个子串,所以需要我们自己写:
cpp
bool contains = lesson.find("no") != lesson.npos;
其中npos表示的是这个类型下最大的值,一般在如果find没有找到对应子串的时候返回。
最后讲一下有关于将string传递到函数中的问题。如果我们直接传递字符串,如下所示:
cpp
void PrintString(std::string string) {
std::cout << string << std::endl;
}
那么会涉及一次字符串的拷贝,这个是会非常浪费时间的做法,因为拷贝字符串是很慢的,所以会导致性能的降低。
在我们不改变字符串的内容的情况下,可以只传入引用:
cpp
void PrintString(const std::string& string) {
std::cout << string << std::endl;
}
添加const来表示我们也不会修改string的值。