C++字符串

116 阅读14分钟

1. 概述

什么是字符串?一般来说,字符串本质上是一个接一个字符的一组字符,这些字符是指字母、数字、符号等,这类东西基本上就是文本,所以它对于我们来说非常常见。作为人类,当然想要在我们的电脑上以某种方式表示文本的形状或形式。所以我们有这样的问题,当我们编程时,我们想要能够表示文本或者一组文本,它可以是一个单一的字符,可以是一整个段落,也可以是一个单词,也可以是一堆单词,所有这些被称为字符串的东西都是一个文本字符串。所以我们需要一些方法能够在我们的程序中表现出来,这就是C++字符串。对于我们来说,这是一种能够表示和处理文本的方法。

要明白字符串是怎么工作的,这将帮助我们理解如何使用它,例如,如果 你只是告诉我遇到字符串的时候应该怎么做,很好,我知道字符串是什么。然而,在未来,当我遇到什么东西时,我不确定它是否能起作用,如果我知道底层技术是如何工作的,我可以做一个有根据的猜测,明确我做的事情,是否一开始就可行。

为了理解C++中字符串是如何工作的,我们需要理解字符是如何运作的,以及字符到底是什么?

字符就像字母符号,数字等以不同的形式呈现,这不是一个关于世界上所有不同类型的字符编码系统的文章,因为可能有太多数不清的东西,它们都很复杂,它们都取决于具体的说明是什么,那么我们该从哪里深入了解这个问题呢?也许是以后,这里我们要讨论的是字符是怎么工作的,现在我们可能已经注意到C++中,有一种数据类型叫做Char,是Character的缩写,到目前为止,我们已经用过了,这是一个字节的内存。它很有用,因为它能把指针转换成char型指针,所以我们可以用字节来做指针运算。它对于分配内存缓冲区也很有用,因为如果我们想分配1k的内存,我们可以分配1024个char。它对字符串和文本也很有用,因为C++对待字符的默认方式是通过Ascii字符进行文本编码,我们在C++中处理字符,是一个字符是一个字节,这就是AsciiAscii可以扩展为很多,比如UTF-8UTF-16UTF-32,我们有wide string(宽字符串),当然,字符是可以大于一个字节的,我们有二个字节的字符,三个字节的字符,四个字节的等等,例如中文、日文或其他语言,有一些不同的字符。我们需要能够使用这些,因为我们需要有足够多的字符。如果我们只有一个字节来表示一个字符,8个比特,这意味我们有2的8次方种可能的结果,也就是256种可能性。但是例如中文、日文或其他语言,远远超过了256个字符,如果我们把英文字母、数字、符号、中文、韩文等等所有这些。所以8个比特根本不够,所以比如有16个比特,也就是16位字符编码,这意味着我们有2的16次方种不同的可能性,也就是65536种字符可以表示。我们还有很多其他的编码。但是在C++中,只是基础语言,在不使用任何库,只是原始数据类型,char就是一个字节。

字符,我们提到的字符,就是char数据类型,而字符串实际上是字符数组,而数组又是一组元素的集合。所以,一组字符组成了字符串或文本。你可能已经注意到,我们经常将字符串称为cont char*,让我们来看看它是如何工作的。

2. 案例

1. 准备项目

创建一个简单的项目,项目中有一个main.cpp文件,文件内容如下

image.png

2. 开始案例

我们可以通过声明一个const char*加字符串名称来声明一个字符串,然后让它等于一个双引号下的某种文字,如const char* name = cherno;

image.png

这是C语言风格定义字符串的方式,因为我们C++有一个库,这使得字符串操作对我们来说要简单的多。但了解它是如何工作的仍然很重要,即便你知道了C++版本。其实我们不需要将name字符串声明为const

image.png

但我们通常加上const的原因是,我们不想去改变这些值。因为字符串是不可变的,意思是说,我们不能扩展字符串并使它变大,因为这是一个固定分配的内存块。如果我们想要一个更大的字符串,它需要执行一个全新的分配并删除旧的字符串。现在,这里的char*并不意味着它是在堆上分配的,我们不能通过delete[] name来删除。

image.png

我们还没有讲到new和delete,以及所有这类堆分配、栈分配等。在后续会讲到。经验法则告诉我们,如果我们不使用new关键字,就不要使用delete关键字。

在这种情况下,我们做了这个cherno字符串,我们没有写上new char之类的,我们只是写了"cherno",所以不需要delete。

如果我们声明成const

image.png

当然,这意味着我们不能再改变它的内容,有关const的内容,后续的文章也会讲到。我们声明成const, 我们就不能像这样name[2] = 'a';来改变第三个字符

image.png

我们不能这么做,因为这会导致错误。所以如果我们知道我们不会修改字符串,就可以加上const,否则,我们要去掉const。

下一个问题,一个字符串在内存中是什么样的呢?它究竟是如何工作的?

我们可以加上一个断点

image.png

按下F5运行我们的程序。

image.png

在内存视图地址栏中输入name,因为name本身就是指针

image.png

回车

image.png

可以看到我们有一堆内存,可以看到有cherno这个词。内存视图的左边部分是ascii表示。这些都是单独的字节,这就是它的样子。如果我们把它转换成ascii,我们可以去一些网站,比如AsciiTable,上面有一个表格,上面是asccii码实际是什么

image.png

我们的内存视图上63 68 65 72 6e 6f有这些,这些是十六进制,以63为例,我们对照的上面的表,十六进制那一列,可以找到对应的char是字母c,这是cherno的第一个字母。

image.png

继续其他的,都可以被找到。可以看到cherno就是63 68 65 72 6e 6f这六个字符组成的。然后我们可以看到有一个被设为0的字节

image.png

这被称为空终止字符。这样我们就知道那是字符串结束的地方。

你会注意到,我们不知道cherno有多大,我们不能,它只是一个指针。那么我们怎么知道它的大小呢。空终止字符就是这样来的,字符串从指针的内存地址开始,然后继续下去,直到它碰到0。

当我们决定将其打印到控制台时,如下

image.png

我们按下F5运行我们的程序。

image.png

可以看到cherno被打印到了控制台,但它只是一个指针,那么它是如何知道终点的呢,它跑到0,然后意识到。这就是空终止字符,在字符串的结尾。

如果我们要自己声明这个,例如,我创建了另一个名为name2的字符串,我想完全手工操作,我要做一个char的数组,我们在大括号中初始化,C++中字符通过''单引号""双引号默认是char*,不是字符串,而是char* char指针,有六个字符char name2[6] = {'c', 'h', 'e', 'r', 'n', 'o'};

image.png

现在,char name2[6] = {'c', 'h', 'e', 'r', 'n', 'o'};这是一个数组,不是字符串,可以看到包含了六个字符的数组,这里没有空终止字符,如果我将name2打印到控制台,我们也可以检查它的内存情况

image.png

F5运行程序

image.png

打印到控制台,我们可以看到,我们得到了cherno,然后就是一堆随机的字符。

回到内存视图,我们在地址栏输入name2,回车

image.png

可以看到,我们有我们的cherno,然后一堆奇怪的字符,你可以看到,有一些设置为cc,这实际上是一个数组守卫,让我们知道内存是在我们分配的内存之外。每当我们在调试模式下分配数组,c标准库或者c++标准库,实际上会插入栈守卫之类的东西,这样我们就知道是不是在分配内存之外了。因为我们数组六个字符结束后没有0,std::cout就不知道打印到哪结束,这就是我们得到这个随机的东西。

然而,如果,我们扩展这个数组,最后一个元素写上0或者'\0''\0'实际上是ascii字符。

image.png

在ascii表中可以看到,那个null就是它的实际值。

image.png

或者写成

image.png

因为0也是它的实际值,可以看ascii表。

现在,我们按下F5来运行我们的程序

image.png

可以看到,现在它正确的打印cherno,这就是字符数组的工作原理。字符串就是这样工作的。这就是字符串,它是字符的集合。

我们应该如何在C++中使用字符串,在C++中的标准库中有一个名为String的类,实际上有一个类叫BaseString,它是一个模版类。std::string,基本上是baseString类的模版版本,模版参数是char。这叫模版特化(template specialization),就是将baseString模版类中的模版参数设为char,意思是char是每个字符背后的实际类型,所以我们真的应该用这个。

有一种叫做Wstring的东西,也就是宽字符串wide string,这里暂时先不讲它。

在C++中使用的字符串,应该用std::string,std::string是怎么工作的?

基本上,它是什么,它只是一个char数组,它是一个char数组和一些函数,用来操作他们(char数组)。

下面来看看,如何使用std::string

使用std::string的第一件事就是包含头文件#include <string>,在iostream里面其实已经有了string的定义了,但是,如果我们想把它打印到控制台,就必须加上#include <string>头文件。我们将char* name = "cherno";改成用std::string的方式

image.png

已经改好了,就是这么简单,string有一个构造函数,它接收char*const char*参数。将鼠标悬停在上面

image.png

可以看到它实际上是一个const char数组,不是char数组。这是我上面将const char*时提到过,为什么我们通常会把它赋值为const char*而不是char*,因为本质上,当我们定义字符串时,用双引号引起来的一个单词或多个单词,在C++中是const char数组,而不是char数组。但是通过char*的隐式转换,当我们需要完全操作字符串的时候,这没有问题。

我们再来打印name,去掉多余的代码。

image.png

按下F5运行

image.png

可以看到,结果是一样的。

现在,如果,我没有包含string的头文件#include <string>,只放了iostring的头文件

image.png

可以看到,我们得到了一个错误。输出流告诉我们,不能把字符串发送到cout流中。因为这个操作符的允许我们发送字符串到流的重载版本,是在string的头文件内部。这就是为什么,我们要包含string的头文件,尽管iostream对它有一个定义。

因为std::string是一个有很多功能的类,我们实际上所有的这些方法,如size(),name.size()可以找出它的尺寸。

image.png

如果我们是const char*char*,我们就需要用到c函数,比如strlen(),也就是字符串的长度。

image.png

所有的这c字符串操作功能,在string类中都可以找到。

另一件常见的事情是追加字符串,如下,我想在cherno后面加上hello

image.png

这里会出现错误。发生这种情况的原因是,我们实际上是想将两个const char的数组相加,这个双引号里面的东西是const char数组。它不是真正的字符串,不能把这两个指针相加,不能将这两个数组直接相加。如果我们想做这样的事情,一个简单的方法是把它分开成多行,然后我们要做的是name += "hello"

image.png

这样做是将一个指针,加到了name,name是一个字符串string,我们把它加到一个字符串上,+=这个操作符在string类中被重载了,所以可以这样写。

或者,我们将两个相加的字符数组的其中一个,显示调用一个string的构造函数

image.png

相当于创建了一个字符串,然后附加这个字符数组给它。

这样做,我们可能会得到更多的拷贝,但在大多数情况下,这没有问题。

如果我们想找字符串中的文本,可以使用find()函数,括号内放入指定的文本,如下

image.png

我想找"no",在cherno和hello中。我们可以写个不等式,不等于std::string::nposstd::string::npos这代表的是一个不存在的位置。用一个bool值来接收结果,name.find("no")返回的是no所在的首位置,在这个例子中,他会返回"no"的开始位置。

image.png

所以如果我们想说是否包含一些东西,就是用这个find。因为在string类中并没有contains函数。

下面提一下将string传递给其他函数。

如果我们写了一个PringString的函数,我想要传递一个字符串,我不会简单的写std::string string

image.png

我不会这样做的原因是,std::string string这个参数其实是一个副本,我们之前没有过多的讨论传值问题,但当我们这样把类对象传递给一个函数时,我们实际上是在复制这个类对象。所以,如果我要做像string += "h";这样的事情

image.png

它不会影响到传递的原始字符串,也就是name。

PrintString显然是一个只读函数,我们不修改任何东西,我们只是打印字符串,所以我们为什么要复制整个字符串呢,复制整个字符串意味着我们必须动态的在堆上分配一个全新的char数组来存储我们已经得到的完全相同的文本,这可不快。字符串复制实际上相当慢,在某些情况下,这是一个主要的短板,因为字符串操作是常见的。因此当我们传递一个这样的字符串,而且是只读的情况下,确保通过常量引用来传递它。我会在前面加上const和引用&

image.png

这告诉我们,这个参数是个引用,意味着他不会被复制,意味着我们承诺不会在这里修改它。这很有希望,因为从技术上讲,如果我们想的话,我们可以这么写

image.png

但是我们加上了const,承诺不会修改,所以这里会报错,我们不能修改它。

image.png

关于const的内容,后续的文章会继续研究。