数组是一种类似于标准库vector的数据结构,与vector相同的是,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。与vector不同的是,数组的大小确定不变,不能随意向数组中增加元素。因为数组的大小固定,因此对于某些特殊的应用程序来说程序的运行时性能较好,但是相应的也损失了一些灵活性。
1.定义和初始化内置数组
数组的声明形式如a[d],其中a是数组的名字,d是数组的维度。维度说明了数组的个数,因此必须大于0,必须是一个常量表达式。
unsigned int cnt = 42; //不是常量表达式
constexpr unsigned sz = 42; //是常量表达式
int arr[10]; //含有10个整数的数组
int *parr[sz]; //含有42个整数指针的数组
string bad[cnt]; //错误,cnt不是常量表达式
string strs[get_size()]; //当get_size()是constexpr时正确,否则错误
定义数组的时候必须指定数组的类型,不允许使用auto关键字由初始值列表推导类型。另外和vector一样,数组的元素应为对象,因此不存在引用的数组。
显示初始化数组
可以对数组的元素进行列表初始化,此时允许忽略数组的维度。如果在声明时没有指明维度,编译器会根据初始值的数量计算出来;相反,如果指明了维度,那么初始值的总数不应该超出指定的大小。如果维度比提供的初始值数量大,则用提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值:
const unsigned sz = 3;
int ia1[sz] = {0, 1, 2}; //含有3个元素的数组,元素值分别是0,1,2
int ia2[] = {0, 1, 2}; //维度是3的数组
int ia3[5] = {0, 1, 2}; //等价于a3[] = {0,1,2,0,0}
string sa4[3] = {"hi", "bye"}; //等价于sa4 = {"hi", "bye", ""}
int ia5[2] = {0, 1, 2}; //错误,初始值过多
字符数组的特殊性
字符数组有一种额外的初始化形式,我们可以用字符串字面值对此类数组初始化。当使用这在形式时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去:
char a1[] = {'C', '+', '+'}; //列表初始化,没有空字符
char a2[] = {'C', '+', '+', '\0'}; //列表初始化,含有显示空字符
char a3[] = "C++"; //自动添加表示字符串结束的空字符,数组大小是4
const char a4[6] = "Daniel"; //错误,没有空间可存放空字符
不允许拷贝和赋值
不能将数组的内容拷贝给其他数组作为初始值,也不能用数组为其他数组赋值:
int a[] = {0, 1, 2}; //含有3个整数的数组
int a2[] = a; //错误,不允许使用一个数组初始化另外一个数组
a2 = a; //错误,不允许把一个数组直接赋值给另外一个数组
一些编译器支持数组的赋值,这就是所谓的编译器扩展。但一般来说,最好避免使用非标准特性,因为含有非标准特性的程序可能在其他编译器上无法正常工作。
理解复杂的数组声明
和vector一样,数组允许定义数组的指针以及数组的引用:
int *ptrs[10]; //ptrs是含有是个整形指针的数组,是一个数组
int &refs[10] = ""; //错误,不存在引用的数组
int (*parray)[10] = &arr; //parray是一个指向10个整形数组的指针, 是一个指针
int (&arrRef)[10] = arr; //arrRef引用一个含有10个整形的数组
int *(&array)[10] = ptrs; //array是一个含有10个整形指针的数组的引用
要理解数组声明的含义,最好的办法就是从数组的名字开始由内往外的顺序阅读,比如array,首先array是一个引用,然后观察右边知道array引用的对象是一个大小为10的数组,最后观察左边知道,数组的元素类型是指向int的指针。这样,array就是一个含有10个int指针的数组的引用。
2.访问数组元素
与标准库vector一样,数组的元素也能使用范围for语句或者下标运算符来访问。使用下标运算符的时候,通常将其定义为size_t。size_t是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小,在cstddef文件中定义了size_t类型。
和vector一样,使用下标时应该检查下标的合理性,合理的下标大小应该是大于0而小于数组的大小。超出这个范围的下标会导致数组越界,使程序崩溃。
3.指针和数组
数组和指针有非常紧密的联系,使用数组的时候编译器一般会把它转换成指针。对数组的元素使用取地址符就能得到指向该元素的地址:
string nums[] = {"one", "two", "three"};
string *p = &nums[0]; //p指向nums中的第一个元素
string *p = nums; // 等价于string *p = &nums[0];
很多使用到数组名的地方,编译器会自动将其替换为一个指向数组首元素的指针。
使用数组作为一个auto的变量的初始值时,推导得到的是指针而非数组:
int ia = {0,1,2,3};
auto ia2(ia); //ia2是一个指针,指向ia的第一个元素
ia2 = 42; //错误,ia2是一个指针,不能用int给指针赋值
当使用decltype关键字时上述转换不会发生,decltype(ia)返回的类型是由4个整数构成的数组:
decltype(ia) ia3 = {0,1,2,3};
ia3 = p; //错误,不能用整型指针给数组赋值
ia3[2] = 1; //正确,把1赋值给ia3的第3个元素
指针也是迭代器
vector的迭代器支持的运算,数组的指针全部支持,包括指针递增递减运算,使用指针遍历数组元素:
int arr[] = {0,1,2,3,4,5,6,7,8,9};
int *p = arr; //p指向arr的第一个元素的指针
++p; //p指向arr[1]
int *e = &arr[10]; //e指向arr尾元素的下一位置的指针,不能对尾后指针指向解引用和递增操作
标准库begin和end
为了让指针的使用更简单,更安全,C++11新标准引入了两个名为begin和end的函数。这两个函数与容器中的两个同名成员功能类似,不过这两个函数不是成员函数:
int arr[] = {0,1,2,3,4,5,6,7,8,9};
int *beg = begin(arr); //指向arr的首元素的指针
int *last = end(arr); //指向arr尾元素的下一位置的指针
指针运算
给一个指针加上或减去某个整数值,结果仍然是指针,新指针指向的元素与原来的指针相比前进或后退了该整数值的为位置。
两个指针相减的结果是它们之间的距离。参与运算的两个指针必须指向同一个数组中的元素。相减的结果是一种名为ptrdiff_t 的标准类型,是一种带符号类型。
4.C风格字符串
C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。字符串放在字符数组中并以空字符结束。
C风格字符串函数:
函数 | 说明 |
---|---|
strlen§ | 返回p的长度,空字符不计算在内 |
strcmp(p1,p2) | 比较p1和p2的相等性,如果p1==p2则返回0,大于返回整数,小于返回负数 |
strcat(p1,p2) | 将p2附加到p1之后,并返回p1 |
strcpy(p1,p2) | 将p2拷贝到p1,并返回p1 |
传入此类的指针必须指向以空字符作为结束的数组:
char ca[] = {'C', '+', '+'};
cout << strlen(ca) << endl;
ca虽然是一个数组但它不是以空字符结束,因此上述程序将产生未定义的结果。strlen有可能沿着ca在内存中的位置不断向前寻找,直到遇到空字符才停止。
对大多数应用来说,使用标准库cstring要比使用C风格字符串更安全高效。
5.与旧代码的接口
很多C++程序在标准库出现之前就已经写成,它们肯定没有想到vector和string的类型。而且,有一些C++程序实际是与C语言或其他语言的接口程序,当然也无法使用C++标准库。因此,现代C++程序不得与那些充满数组或C风格字符串的代码衔接,为了使这一工作简单易行,C++专门提供来一组功能。
混用string对象和C风格字符串
任何出现字符串字面值的地方都可以用以空字符结束的字符数组替代:
允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。
在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是)。在string对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。
上述性质反过来就不成立了: 如果程序某处需要一个C风格字符串,无法直接使用string对象来替代它,string专门提供了一个名为c_str的成员函数:
char *str = s; //错误,不能用string对象初始化char *
const cahr *str = s.c_str(); //正确
我们无法保证c_str函数返回的数组一直有效,事实上,如果后续的操作改变了s的值就可能让之前返回的数组失去效用。如果执行完c_str函数后程序想一直都能使用其返回值,最好将该数组重新拷贝一份。
使用数组初始化vectr对象
允许使用数组来初始化vector对象,要想实现这一目的,只需要指明拷贝区的首元素地址和尾元素地址就可以了:
int arr[] = {0,1,2,3,4,5};
vector<int> ivec(begin(arr), end(arr)); // ivec中有6个元素,分别是arr数组中元素的副本
vector<int> ivec2(arr+1, arr+4); //拷贝3个元素arr[1],arr[2],arr[3]
尽量使用标准库而非数组
使用指针和数组很容易出错,一部分原因是概念上的问题:指针常用于底层操作,其他问题则源于语法错误,特别是指针声明时的错误。
现代C++程序应尽量使用vector和迭代器,避免使用内置数组和指针。应该使用string,避免使用C风格基于数组的字符串。