数组与指针和引用一样是C++中的一种复合类型,数组一旦定义后大小确定不变,不能往数组中添加元素。由于数组大小固定,对于某些应用来说性能上优于vector。
1. 定义与初始化
数组声明形如a[d],d是维度,必须是大于0的常数或常量表达式(或者constexpr函数)。维度是数组对象的一部分,定义时必须指定。
数组如果不显示初始化,将使用默认初始化,数组可能含有未定义的值,因此定义数组的同时应该初始化。
1.1 显示初始化
使用列表初始化,如果列表中的元素个数比维度小,剩下的元素初始化成默认值。但是列表元素多余维度则会报错。
int a[] = {2, 3, 5}; //维度为3的数组
int a2[5] = {2, 5, 1};// 维度为5的数组,等价于 int a2[] = {2, 5, 1, 0, 0};
1.2 字符数组
字符数组一般应该在结尾处添加一个空字符'\0'表示字符串结束
char a1[] = {'c', '3', '2'}; //没有空字符
char a2[] = {'3', 'c', '+', '\0'}; //含有空字符
char a3[] = "3c+"; //自动添加空字符,维度为4,等价于a2
char a4[3] = "3c+"; //错误,维度应为4
1.3 数组不允许拷贝和赋值
1.4 包含指针的数组、指向数组的指针、指向数组的引用
指针本身是对象,因此数组元素可以为指针,但是引用不是对象,所以没有包含引用的数组。数组本身是对象因此可以定义指向数组的指针或者引用。
int *ptrs[10]; //含有10个int指针的数组
int &refs[10] = ...; //错误,引用不是对象,数组元素不能为引用
int (*Parray)[10] = &arr; //Parray是一个指针,指向包含10个int元素的数组
int (&Rarray)[10] = arr; //Rarray是一个引用,指向包含10个int元素的数组
一般情况下,修饰符从右往左解读。对于ptrs,最右是[10],因此ptrs首先是一个包含10个元素的数组,然后往左看是int *,可知ptrs中的元素类型是int *,所以是一个包含了10个int指针的数组。
但是Parray从右往左解读不合理,对于有括号的修饰符,我们遵循由内往外解读。首先看括号内的部分,*Parray表明Parray是一个指针,再由右至左看外部,最右是[10],所以Parray是一个指向包含10个元素数组的指针,往左是int,由此确定Parray是一个指针,指向包含10个int元素的数组。
同理看Rarray,首先是一个引用,指向包含10个int元素的数组。
试试解读下面的定义:
int *(&array)[10] = ptrs; //首先array是一个引用,指向包含10个int指针的数组
2. 指针和数组
2.1 数组当指针使用
C++中使用数组时,编译器一般会把数组编译成指针,此指针指向该数组的首元素。
int a[] = {2, 1, 3};
编译器用到数组的时候会自动将其替换成指向数组首元素的指针:
int *p = a; //实际等价于int *p = &a[0],p指向a的首元素
因此某些情况下,数组的操作其实是指针的操作。如数组作为auto变量的初始值时,推断得到的类型为指针而非数组:
int ia[] = {0, 2, 3};
auto ia2(ia); //ia2是一个int指针,指向ia的第一个元素
编译器实际执行的过程像下面这样:
auto ia2(&ia[0]); //ia2的类型为int*
但是使用decltype时确不会使用指针,decltype(ia)返回的类型是由3个int组成的数组:
decltype(ia) ia3 = {3, 2, 0};
ia3[2] = 5;
2.2 指针也是迭代器
vector和string的迭代器支持的运算,数组的指针全都支持。可以使用指针递增和尾后指针遍历数组:
int arr[] = {2, 3, 1, 5, 5, 6};
int *p = arr; //p指向arr的第一个元素
++p; //数组的指针支持递增操作,p指向arr[1]
--p; //也支持递减操作,p指向arr[0]
int *e = &arr[6]; //e指向arr尾元素的下一个位置,这是合法的
利用这个指向尾元素下一个位置的指针就可以像迭代器一样遍历数组了
for (int *b = arr; b != e; ++b) {
cout<<*b<<endl;
}
2.3 标准库函数begin和end
C++11引入了begin和end两个标准函数,用于获取数组的首元素指针和尾元素下一位置的指针(类似于容器中的begin和end函数)。注意,尾后指针没有指向任何一个合法对象,因此不能解引用和递增。
int a[] = {2, 3, 1, 5};
int *beg = begin(a); //beg指向a的首元素
int *last = end(a); //last指向a的尾元素的下一个位置
2.4 指针运算
指向数组元素的指针可以执行所有迭代器的运算,包括解引用、递增、比较、与整数相加、两个指针相减等。
指针加上或减去一个整数,结果还是指针,新指针指向隔原来元素整数个位置的元素
constexpr size_t sz = 5;
int arr[sz] = {1, 2, 5, 6, 7};
int *ip = arr; //指向arr[0]
int *ip2 = ip + 4; //指向arr[4],相当于arr[0 + 4]
int *ip3 = arr + sz; //指向尾后元素
int *ip4 = arr + 10; //错误,arr只有5个元素,此指针值未定义
两个指针相减得到他们间的距离:
auto n = end(arr) - begin(arr); //n为5,arr中元素个数
2.5 解引用和指针运算的交互
int ia[] = {0, 2, 2, 3, 1};
int last = *(ia + 4); //正确,ia + 4指向ia[4],然后解引用把ia[4]的值 1 赋给last
last = *(ia + 5); //错误,ia只有5个元素,ia + 5时间指向了ia的尾后元素,不能解引用
last = *ia + 5; //正确,ia指向ia[0],解引用值为0,0 + 5 = 5,值5赋给last
2.6 下标和指针
当数组使用下标运算符是,编译器会自动将其转换成指针运算
int ia[] = {1, 2, 3, 5, 6};
int i = ia[2]; //ia转换成指向数组首元素的指针,ia[2]得到(ia + 2)所指向的元素
int *p = ia; //p指向ia的首元素
i = *(p + 2); //等价于i= ia[2]
只要指针指向的是数组中的元素(没有越界,不是尾后指针),都可以执行下标运算:
int *p = &ia[2]; //p指向ia的第3个元素(索引为2)
int j = p[1]; //相当于int j = *(p + 1),也就是ia[3]
int k = p[-2]; //相当于int k = *(p - 2), 也就是ia[0]
标准库类型string和vector也能指向下标运算,但是他们的下标必须为无符号类型,这点与数组不同,内置的下标运算可以处理负值。
3. C风格字符串
C++程序中最好不要使用C风格字符串!
C风格字符串存放在字符数组中并以空字符介绍,也就是说字符数组最后一个元素为'\0'。
3.1 C标准库String函数
下面这些函数可用于操作C风格字符串,定义在cstring.h文件中,这个文件时C语言头文件string.h的C++版本。
strlen(p);//返回字符串长度
strcmp(p1, p2);//比较字符串
strcat(p1, p2);//p2附加到p1后面,返回p1
strcpy(p1, p2); //将p2拷贝给p1,返回p1
传入这些函数的字符串指针必须以'\0'结尾:
char ca[] = {'1', 'c', 'e'};
cout<<strlen(ca)<<endl; //错误,ca没有以'\0'结尾
3.2 比较字符串
C风格的字符串不能直接比较,如下
const char a[] = "navyhu";
const char b[] = "uuyhh";
if (a < b) { //实际比较的是分别指向a和b首元素的指针,由于他们指向不同对象,此操作将得到未定义的结果
xxxx;
}
应该使用strcmp进行C风格字符串比较:
if (strcmp(a, b) < 0) {
xxxx;
}
3.3 与旧代码的接口
string对象可以用C风格字符串初始化
char ca[] = "navyhus";
string str(ca); //使用C风格字符串初始化
但是字符指针不能直接指向string对象,string对象提供了返回C风格字符串的接口
string s("navyhu");
char *strca = s; //错误,不能直接指向string对象
const char *strca2 = s.c_str(); //正确,c_str返回一个C风格字符串,并且为const类型,确保不会通过该字符数组修改string对象
但是注意c_str返回的数组可能不会一直有效,当别的代码修改了string本身可能让之前的返回值失效。
使用数组初始化vector对象
获取到数组的首元素指针和尾后指针,就可以利用这两个指针初始化vector对象
int arr[] = {1, 2, 4, 5, 6};
vector<int> vec(begin(arr), end(arr));
//或者
vector<int> vec(arr, arr + 5);
//也可以使用数组中部分元素来初始化
vector<int> vec2(arr + 1, arr + 4); //使用index 1 - 3 这3个元素初始化,a[1], a[2], a[3]
4. 多维数组
C++中多维数组其实是数组的数组,记住这点对理解多维数组大有好处。
int a[3][4]; //表示一个含有3个元素的数组,每个元素是含有4个int的数组
int b[10][20][30]; //表示一个含有10个元素的数组,每个元素是包含20个元素的数组,此数组中的每个元素都是含有30个int型的数组
多维数组的解读也遵循由内向外(一般情况下从右向左)的原则,a首先是一个a[3]含有3个元素的数组,然后每个元素都是一个含有4个int型的数组。
4.1 初始化
用花括号初始化多维数组
int a[3][4] = {
{0, 2, 3, 4},
{2, 1, 3, 5},
{3, 4, 5, 6}
};
//其中嵌套的花括号并非必要,也可以去掉:
int a[3][4] = {0, 2, 3, 4, 2, 1, 3, 5, 3, 4, 5, 6}; //与上面的等价
//只显示的初始化每行首元素
int a[3][4] = { //其他未初始化的元素执行默认初始化,值为0
{0},
{2},
{3}
};
//如果把嵌套花括号去掉则默认初始化第一行, 其他元素默认初始化为0
int a[3][4] = {1, 2, 4, 5};
4.2 下标引用
int (&row)[4] = a[2]; //row是一个引用,指向含有4个int的数组,这里它绑定到多维数组a的第3个元素(含有4个int的数组)
4.3 使用范围for语句处理多维数组
size_t cnt = 0;
for (auto &row : a) { //获取外层数组的元素
for (auto &col : row) { //获取内层数组的元素
col = cnt; //给元素赋值
++cnt;
}
}
上例中我们用引用类型作为循环控制变量,除了需要改变数组元素的值外,还有一个更深的原因:避免编译器自动将数组转换成指针。
for (const auto &row : a) //引用类型,绑定到数组,编译器不会把它转换成指针
for (auto col : row)//row是绑定到数组的引用,不是指针,这是合法操作
cout << col <<endl;
但是如果外层循环不用引用,这个表达式就是错误的,编译无法通过
for (auto row : a) //row被自动转换成指向数组首元素的指针
for (auto col : row)//row是指针,不支持范围for,编译错误
cout << col <<endl;
4.4 指针和多维数组
使用多维数组的名字时,编译器同样会将它转换成指向首元素的指针,只不过这里的指针指向了一个数组。
int a[3][4];
int (*p)[4] = a; //p指向a的第一个含有4个int型的数组, 声明中*p两边的括号不可缺少,否则变成含有4个int*的数组
p = &a[2]; //p指向a的最后一个含有4个int型的数组
C++11新标准通过使用auto或decltype避免在数组前加上一个指针类型符:
</pre><pre name="code" class="cpp">for (auto p = a; p != a + 3;++p) { //p指向含有4个int型的数组
for (auto q = *p; q != *p + 4; ++q) //q指向一个int型元素
cout << *q << ' '; cout << endl;}
外层循环很好理解,p是一个指向包含4个int型的数组的指针,这样的话*p就是p指向的这个数组。据此来理解内层循环,auto q = *p;*p是一个数组(编译器会将*p转换成指向首元素的指针),因此q就是指向这个数组第一个元素的指针,后面的就好理解了。
也可以使用begin,end函数:
for (auto p = begin(a); p != end(a);++p) { //p指向含有4个int型的数组
for (auto q = begin(*p); q != end(*p); ++q) //q指向一个int型元素
cout << *q << ' ';
cout << endl;
}
4.5 类型别名简化多维数组指针
using int_array = int[4];
typedef int int_array[4]; //与上面的定义等价,typedef是老标准
for (int_array *p = a;p != a + 3;++p) {
for (int *q = *p; q != *p + 4; ++q)
cout << *q << ' ';
cout << endl;
}