C++ 第七章 指针、数组与引用 - 7.3 数组

7.3 数组

假设有类型T,T[size]的含义是“包含size个T类型元素的数组”。元素的索引值范围是0到size-1。例如:

float v[3];		//包含3个float的数组,分别是v[0]、v[1]、v[2]
char* a[32];	//包含32个char指针的数组,依次是a[0]...a[31]

可以使用下标运算符[]或指针(运算符*或运算符[],见7.4节)访问数组中的元素,例如:

void f()
{
    
    
	int aa[10];
	aa[6] = 9;			//为数组aa的第7个元素赋值
	int x = aa[99];		//未定义的行为
}

越界访问数组是一种未定义的行为,而且很有可能会引发严重的程序错误。在C++语言中,运行时边界检查既不常见、也无法保证。

数组中元素的数量(即数组的边界)必须是常量表达式(见10.4节)。如果你希望边界可变,最好使用vector(见4.4.1节和31.4节)。例如:

void f(int n)
{
    
    
	int v1[n];				//错误:数组的大小不是常量表达式
	vector<int> v2(n);		//OK:包含n个int元素的vector
}

多维数组表现为数组的数组(见7.4.2节)。

数组是C++表示内存中对象序列最基本的方式。如果你用到的只是内存中一个固定大小、固定元素类型的序列,那么数组完全可以满足你的要求。对于其他要求,数组就不一定可靠了。

C++允许静态地分配数组空间,也允许在栈上或者在自由存储上(见6.4.2节)分配数组空间。例如:

int a1[10];						//静态存储中的10个int

void f()
{
    
    
	int a2[20];					//栈上的20个int
	int* p = new int[40];		//自由存储上的40个int
	//...
}

C++的内置数组本质上是语言的一种底层功能,我们常常用数组来实现标准库vector和array等更高层级上的、行为定义更好的数据结构。数组不能执行赋值操作,一旦需要,数组名就会隐式地转换成指向数组首元素的指针(见7.4节)。特别是主要避免在接口中使用数组(比如作为函数的参数,见7.4.3节和12.2.2节),因为数组名隐式转换成指针是C代码和C风格的C++代码中很多错误的根源。如果是在自由存储上分配数组的,切记一定要在最后一次使用数组之后把对应的指针delete[]掉(见11.2.2节)。要让程序员时刻遵守这一要求并不容易,最简便且最可靠的办法是用资源句柄(比如string(见19.3节和36.3节)、vector(见13.6节和34.2节)和unique_ptr(见34.3.1节))控制自由存储上的数组的生命周期。如果你是静态地分配数组或者是在栈上分配数组,一定不要delete[]它。显然,因为C语言本身缺乏封装数组的能力,所以C程序员很难完全遵循上述建议;但是这些建议在C++程序中非常有用,不存在任何适用性的问题。

以0作为终止符的char数组是应用最广泛的一种数组。这是C语言存储字符串的基本方式,因此我们常把以0作为终止符的char数组称为C风格字符串(C-style string)。C++的字符串字面值常量沿用了这一传统(见7.3.2节),并且某些标准库函数(比如strcpy()和strcmp(),见43.4节)也是建立在这一用法之上的。通常情况下,char和const char指向以0结尾的字符序列。

7.3.1 数组的初始化器

我们能用值的列表初始化一个数组,例如:

int v1[] = {
    
    1, 2, 3, 4};
char v2[] = {
    
    ‘a’, ‘b’, ‘c’, 0};

如果声明数组的时候没有指定它的大小但是给出了初始化器列表,则编译器会根据列表包含的元素数量自动计算数组的大小。因此,v1和v2的类型分别是int[4]和char[4]。如果我们指定了数组的大小,但是提供的初始化器列表元素数量过多,则程序会发生错误。例如:

char v3[2] = {
    
    ‘a’, ‘b’, 0};		//错误:提供的初始化器过多
char v4[3] = {
    
    ‘a’, ‘b’, 0};		//OK

如果初始化器提供的元素数量不足,则系统自动把剩余的元素赋值为0。例如:

int v5[8] = {
    
    1, 2, 3, 4};

等价于

int v5[8] = {
    
    1, 2, 3, 4, 0, 0, 0, 0};

C++没有为数组提供内置的拷贝操作。不允许用一个数组初始化另一个数组(即使两个数组的类型完全一样也不行),因为数组不支持赋值操作:

int v6[8] = v5;		//错误:不允许拷贝数组(不允许把int*赋给数组)
v6 = v5;			//错误:不存在数组的赋值操作

同样,不允许以传值方式传递数组(见7.4节)。

如果你想给一组对象赋值,可以使用vector(见4.4.1节,13.6节和34.2节)、array(见8.2.4节)或者valarry(见40.5节)。

我们可以用字符串字面值常量(见7.3.2节)初始化字符的数组。

7.3.2 字符串字面值常量

字符串字面值常量(string literal)是指双引号内的字符序列:

this is a string”

字符串字面值常量实际包含的字符数量比它表现出来的样子多一个。它以一个取值为0的空字符 ‘\0’结尾,例如:

sizeof(“Bohr”)==5

字符串字面值常量的类型是“若干个const字符组成的数组”,因此 “Bohr”的类型是const char[5]。

在C和旧式的C++代码中,允许把字符串字面值常量赋给一个非常量char*:

void f()
{
    
    
	char* p = “Plato”;		//错误,但是被C++11之前的代码接受
	p[4] = ‘e’;				//错误:试图为常量赋值
}

显然上面的赋值语句是不安全的。这种用法有出错的风险,因此如果某些老代码因为这个原因无法编译通过再正常不过了。令字符串字面值常量的内容保持不变是一种显而易见的选择,这样做有助于在具体实现环节对字符串字面值常量的存储和访问方式加以改进。

如果我们希望字符串能被修改,最好把字符放在一个非常量的数组中:

void f()
{
    
    
	char p[] = “Zeno”;			//p是含有5个字符的数组
	p[0] = ‘R’;					//OK
}

字符串字面值常量是静态分配的,因此函数返回字符串字面值常量是很安全的行为,不会有什么问题。例如:

const char* error_message(int i)
{
    
    
	//...
	return “range error”;
}

调用error_message()后,存放“rang e error”的内存区域不会消失。

两个完全一样的字符串字面值常量是在同一个数组中还是在两个不同的数组中依赖于实现(见6.1节),例如:

const char* p = “Heraclitus”;
const char* q = “Heraclitus”;

void g()
{
    
    
	if(p==q) cout<< “one!\n”;		//结果依赖于实现
		//...
}

当符号==作用于指针时,比较的是地址(指针本身的值)而非指针所指的值。

空字符串记作一对紧挨着的双引号 “”,其类型是const char[1]。空字符串中唯一的一个字符是结束 ‘\0’。

只要表示的是非图形化字符,反斜线(见6.2.3.2节)就能用在字符串中。这使得我们可以在字符串中表示双引号(“)和转义字符反斜线(\)。我们最常用的是换行符 ‘\n’,例如”

cout<< “beep at end of message\a\n”;

转义字符 ‘\a’是ASCII字符集中的BEL(警告,alert),它的作用是发出报警的声音。

在一个非原始字符串字面值常量中,不存在“真正的”换行:

this is not a string
but a syntax error”

我们可以用空格把一条长字符串分割开以使程序文本显得整洁美观,例如:

char alpha[] = “abcdefghijklmnopqrstuvwxyz”
“ABCDEFGHIJKLMNOPQRSTUVWXYZ”;

编译器会自动把相邻的字符串连接起来,因此用下面这条长字符串字面值常量初始化alpha,从效果上来说是等价的:

“abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ”;

允许在字符串中间存在空字符,但是大多数程序都会忽略空字符之后的内容。例如,标准库strcpy()和strlen()都把字符串 “Jens\000Munk”当成 “Jens”处理(见43.4节)。

7.3.2.1 原始字符串

要想在字符串字面值常量中表示反斜线(\)或者双引号(“),我们需要在这些符号的前面再加一个反斜线。这种做法合情合理,并且在大多数时候都有效。然而,如果我们要表达的字符串字面值常量中含有太多的反斜线和双引号,问题就变得比较复杂了。特别是在正则表达式中反斜线会频繁地出现,它既用来表示转义字符,又能表现某些字符类别(见37.1.1节)。我们无权更改或优化正则表达式的规则,毕竟包括C++在内的很多编程语言都在使用它,几乎已经形成传统了。因此,当你在使用标准regex库(第37章)书写正则表达式时,反斜线可以表示转义字符这一事实很可能会造成某些潜在的错误。请思考一个问题,如果我们想表示两个用反斜线隔开的单词,可能需要这么些:

string s = “\\w\\\\w”;	//这么多反斜线?祈祷自己千万别写错!

显然这种约定俗成的正则表达式太容易出错了,为了解决这个问题,C++提供了原始字符串字面值常量(raw string literal)。在原始字符串字面值常量中,反斜线就是反斜线,双引号就是双引号,上面的例子变成了:

string s = R“(\w\\w);	//嗯,没问题,肯定不会写错的!

原始字符串字面值常量使用R “(ccc)”的形式表示字符序列ccc,其中,开头的R用于把原始字符串字面值常量和普通的字符串字面值常量区别开来。一对括号的作用是允许我们使用非转义的双引号。例如:

R“(“quoted string”)//字符串的内容是“quoted string“

进一步,如果我们想在原始字符串字面值常量中加入字符序列 )”该怎么办呢?这个要求并不常见,不过即使真的遇到了也有解决的办法。因为 “(和)”并不是唯一的分隔符,在 “(…)”的框架中我们还可以在 ( 之前和 ) 之后加入其他分隔符。例如:

R “***(“quoted string containing the usual terminator()))***//“quoted string containing the usual terminator( “))”

规则要求:符号)后面的字符序列必须与符号(前面的序列完全一致。采用这种措施,我们就能处理几乎所有复杂的字符串模式了。

除非你正在处理正则表达式,否则原始的字符串字面值常量不会有太大的用处(顶多算是“茴香豆的茴的一种写法”)。但是正则表达式本身是非常非常有用的,读者不妨思考现实世界中的一个例子:

((?:[^\\\\’]|\\\\.)*|\”(?:[^\\\\\”]|\\\\.)*\”)|//这些反斜线的用法对吗?

面对这个例子,即使是有经验的程序员也很难做出判断,此时原始字符串字面值常量就派上用场了。

与普通的字符串字面值常量不同,在原始字符串字面值常量中允许出现换行(真正的换行,而非换行符)。例如:

string counts{
    
    R”(1
22
333)};

等价于

string x{
    
    1\n22\n333”};

7.3.2.2 大字符集

前缀是L的字符串(比如L”angst”)由宽字符(见6.2.3节)组成,它的类型是const wchat_t[]。类似地,前缀是LR的字符串(比如LR”(angst)”)也是由宽字符组成的(见7.3.2.1节),它的类型同样是const wchar_t[],它属于原始字符串字面值常量。这样的字符串以字符L’\0’结束。

有6种字符字面值常量支持Unicode(称为Unicode字面值常量,Unicode literal)。这听起来有点多,但是要知道Unicode本身有至少3种编码方式:UTF-8、UTF-16和UTF-32。对于这种编码方式,分别支持原始字符串以及“普通”字符串。因为每种UTF编码方式都支持全部Unicode字符,所以到底选用哪种编码方式主要看程序所要依赖的系统是什么要求。基本上,所有Internet应用(比如浏览器和电子邮件)都支持至少一种Unicode编码方式。

UTF-8是一种可变宽度的编码方式:常用字符占据1个字节,不常用的字符(根据使用的情况度量)占据2个字节,特别罕见的字符占据3或4个字节。众所周知,ASCII字符占1个字节,这些字符在UTF-8中的编码(对应的整数值)与在ASCII中完全一致。拉丁字母、希腊文、斯拉夫语、希伯来文、阿拉伯文以及其他字符占2个字节。

UTF-8字符串的结尾是’\0’,UTF-16是u’\0’,UTF-32是U’\0’。

显然,Unicode字符串的最终目的是处理Unicode字符,例如:

u8”The official vowels in Danish are: a, e, i, o, u, \u00E6, \u00F8,
\u00E5 and y.

输出该字符串所得的结果是:

The official vowels in Danish are: a, e, i, o, u, æ, ø, å and y.

\u之后的十六进制数是一个Unicode编码点(§ iso.2.14.3)[Unicode, 1996]。编码点独立于编码方式,事实上,在不同编码方式下编码点的表现形式会有所不同。例如,u’0430’(斯拉夫语的小写字母“a”)在UTF-8中是2字节的十六进制值D0B0,在UTF-16中是2字节的十六进制值0430,在UTF-32中是4字节的十六进制值00000430。这些十六进制值称为通用字符名字(universal character name)。

前缀u和R对于顺序和大小写敏感:RU和Ur都不是合法的字符串前缀。

猜你喜欢

转载自blog.csdn.net/qq_40660998/article/details/121810451