一、什么是数组
数组与vector类似,可以储存固定大小、类型相同的顺序集合,但是在性能和灵活性的权衡上与vector不同。并且元素应为对象,所以不存在引用的数组,但是存在数组的引用。与vector不同的是,数组的大小确定不变,不能随意向数组增加元素。如果不清楚元素的确切个数,请使用vector。定义数组的时候必须指定数组的类型,不允许使用 auto 关键字由初始值的列表推断类型。
二、定义和初始化内置数组
数组的声明形如a[ b ],其中a是数组的名字,b是数组的维度。维度必须大于0,且维度是一个常量表达式,这也符合数组的大小确定不变的要求。
unsigned cnt = 42; //不是常量表达式
constexpr unsigned sz = 42; //常量表达式constexpr声明,能够让编译器判断是否为常量表达式,且得出表达式的结果
int arr[10]; //含有10个整数的数组
int *parr[sz]; //含有42个整数指针的数组
string bad[cnt]; //错误,cnt不是常量表达式
string strs[get_size()]; //当get_size()是constexpr时正确;否则错误
//默认初始化会让数组含有未定义的值
constexpr int a = 10;
int b[a];
for (int i = 0; i < a; i++)
cout << b[i] << endl;
运行结果:
(1)显式初始化数组元素
可以对数组的元素进行列表初始化,此时允许忽略数组的维度。如果声明时没有指明维度,编译器会根据初始值的数量计算并推测出来。如果指明了维度,那么初始值的总数量不应该超出指定的大小。如果维度比提供的初始值大,那么初始化初始值后,剩下没初始值的维度元素被初始化为默认值
const unsigned sz = 3;
int ia[sz] = {0, 1, 2}; //含有3个元素的数组,元素值分别是0,1,2
int a2[] = {0, 1, 2}; //维度是3的数组
int a3[5] = {0, 1, 2}; //等价于a3[] = {0, 1, 2, 0, 0}
string a4[3] = {"hi", "bye"}; //等价于a4[] = {"hi", "bye", " "}
inr a5[2] = {0, 1, 2}; //错误,初始值过多
(2)字符数组的特殊性
与介绍string一样,将char数组拷贝给string时,必须将' \0 '作为结尾。
在进行列表初始化时,必须以' \0 '结尾,或者直接用" "自动添加表示初始化
C标准库中的字符串处理程序,是只认'\0'的,只要没找到'\0',它就认为字符串没有结束,拼命地往后找,这个寻找的过程不理会可能已经超过书柜的格数了(计算机其实很蠢);同样,也可能你在一排书中的中间抽走一本,在那个位置上写上'\0',那么愚蠢的计算机也会认为书到这里为止,它不理会后面其实还有(这是某种截断字符串的技巧)。
char c1[] = { 'c','+','+' }; //列表初始化,没有空字符,会出现多的内容
char c2[] = { 'c','+','+','\0' }; //列表初始化成功
char c3[] = "c"; //自动添加' /0 '到尾部
const char c4[6] = "abcdef"; //错误,没有空余位置存放空字符
cout << c1 << endl;
cout << c2 << endl;
cout << c3 << endl;
将c4注释后的运行结果:
错误提示:
(3)不允许数组与数组之间的拷贝和赋值
不能讲数组的内容拷贝给其他数组作为初始值,也不能用数组为其他数组赋值
int a[] = {0, 1, 2}; //含有3个整数的数组
int a2[] = a; //错误,不允许用数组初始化另一个数组
a2 = a; //错误,不能把一个数组直接赋值给另一个数组
(4)理解复杂的数组声明
数组本身就是对象,所以允许定义数组的指针及数组的引用。
引用数组是不合法的,而且指针数组完全可以代替引用数组,编译器也不知道给引用的数组分配多少内存,所以这种做法是不存在的。
int arr[10];
int *ptrs[10]; //ptrs是含有10个整型指针的数组
int &refs[10] = /* ?*/; //错误,不存在引用的数组
int (*Parray)[10] = &arr; //Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
理解数组声明,应从内向外理解,比如int (*Parray)[10];,我们先看内,也就是括号内,是一个指针,然后看括号的右边,所以理解为是指向大小为10的数组的指针,再看括号左边,理解为数组中10个元素都为int类型。
接下来让我们理解一下,什么是引用的数组和数组的引用
//arrs首先向右结合,相当于(int&) arr[10],表示arr是一个数组,其中的元素是引用,称之为引用的数组
int &arr[10];
//arr首先和&结合,所以arr是引用,引用的对象是数组,称之为数组的引用
int (&arr)[10];
为什么引用的数组是不合法的呢?
引用必须被初始化,但是引用的本意是不含内存空间的,如果强行说占空间,也只是占的指针指向的对象的空间,只能说他不占新的空间。而且引用数组是直接拿另外一个数组初始化引用,但是我们知道数组不具备拷贝的功能,所以引用的数组不能初始化。引用的数组完全可以用指针数组实现,所以引用的数组完全没有出现的意义
char c1[] = "C++"; //自动添加' \0 ',所以这个字符数组维度为4
char(*a)[4] = &c1; //指针数组,指向c1
cout << *a << endl; //输出内容为:C++
3. 编译器并不知道应该给引用的数组分配多大的内存
数组的引用:
char c1[] = "C++";
char(&a)[4] = c1;
cout << a << endl; //输出:C++
引用的数组,数组的引用区别:
int &arr[] = arr1; //(int&) arr[] = arr1,arr[]是数组,相当于arr1拷贝给arr
int (&arr[]) = arr2; //&arr[] = arr2,相当于一种指向,相当于arr指向了arr2,成为别名
(5)访问数组元素
与vector和string一样,可以用for语句或下标运算符来访问。数组索引从0开始,包含10个元素的数组,他的索引从0到9。
例子:输入分数,输出分段计数,以10分为一个分段,0-9,10-19以此类推,输入非数字为结束符输出分段
unsigned scores[11] = {};
unsigned grade;
while (cin >> grade)
{
if (grade <= 100)
++scores[grade / 10];
}
for (auto i : scores)
cout << i << "";
cout << endl;
输出结果:
三、指针和函数
在C++中,使用数组时,编译器会把他转换成指针。使用取地址符来获取指向某个对象的指针。对数组使用取地址符,就能得到指向该元素的指针。
string nums[] = {"one", "two", "three"}; //数组的元素是string对象
string *p = &nums[0]; //p指向nums的第一个元素
当直接拿指向对象名是,编译器会默认将对象替换为一个指向数组首元素的指针。
string *p2 = nums; //等价于&nums[0]
因为数组在使用时会替换成指针,所以将数组auto给一个变量的初始值时,推断得到的类型是指针而非数组
int ia[] = {0,1,2,3,4,5,6,7,8,9}; //设置一个含有10个数的数组
auto ia2(ia); //ia2是一个整型指针,指向ia的第一个元素,等价于int *ia2=(&ia[0])
ia2 = 42; //错误,ia2是一个指针,不能指向字面值
如果想让编译器推断出数组,给变量赋予类型声明,则需要用到decltype(ia)函数,ia是想要编译器推断类型的对象。
int ia1[] = { 0,1,2,3,4,5,6,7,8,9 }; //ia1是含有10个元素的数组
decltype(ia1) ia2 = {1,2}; //让编译器推断ia1类型
for (auto i : ia2)
cout << i << " ";
cout << endl;
输出结果:很显然,decltype顺便把ai1的维度推断并赋予了ia2
(1)指针也是迭代器
vector和string的迭代器支持的运算,数组的指针全部支持。使用指针也可以遍历整个数组。直接指向数组对象名则是指向第一位类似begin()函数,如果指向尾元素后的一个不存在的元素,则与end()函数相似,但是这种方法容易出现错误。
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
inr *p = arr; //p指向arr的第一个元素
++p; //P指向arr的第二个元素
int *e = arr[10]; //arr含10个元素,下标从0-9,如果指向10,则是第11个元素,是不存在的
//我们可以使用数组这种特殊的性质,遍历输出整个数组
//注意一点:当我们使用这种方法遍历,我们不能对尾后指针进行解引用,因为尾后指针并不指向具体元素,解引用会发生错误
for(int *p = arr; p != e; p++)
cout<< *p <<endl;
(2)标准函数begin和end
与迭代器函数相似,但是数组始终不是类类型,所以不能用点操作符(.)使用函数,而是应该讲数组作为他们的参数。
注意:使用这种操作时,需要带上<iterator>头文件,当解引用和递增尾后元素的时候,编译器出错,与迭代器失效类似。
#include<iterator>
using std::begin;
using std::end;
int arr[] = {0, 1, 2, -1};
int *beg = begin(arr); //beg是指向首元素的指针
int *last = end(arr); //last是指向尾元素的下一个元素的指针(简称尾后元素)
//例子:遍历寻找函数中第一个负数
while(beg != last && *beg >= 0) //如果使用beg不为尾后元素的指针与上beg解引用得到的值大于等于0则继续遍历
++beg;
//如果beg已经是尾元素的下一个元素,则跳出循环
//如果beg解引用后的值为负,则跳出循环
(3)数组指针运算
指向数组元素的指针可以执行vector和string迭代器的所有迭代器运算符。包括解引用、递增、比较、与整数相加、两个指针相减等,用在指针和用在迭代器上意义完全一致。
当数组指针加或者减去一个整数时,指针的指向会向前或向后移动一个整数位置,得到的结果仍是一个指针。
constexpr size_t sz = 5; //使用size_t类型需要带上<cstddef>头文件,增强了可移植性,相当于unsigned int,其大小足以保证存储内存中对象的大小
int arr[sz] = {1, 2, 3, 4, 5};
int *p1 = arr; //p1指向arr[0]
int *p2 = p1 + 4; //相当于让p1移动4位,p2指向arr[4]
int *p = arr + sz; //相当于arr+5,让p指向arr[6],运行正常,但是值是内存中存放的未知数值,编译器不会发现错误
int *p2 = arr + 10; //超出范围,直接显示目标内存中存放的数值
//和迭代器一样,如果让两个指针相减,结果是他们之间的距离。参与运算的两个指针必须是指向同一个数组当中的元素
auto n = end(arr) - begin(arr); //n的值为5,是arr中的元素数量
//两指针相减,结果的类型是ptrdiff_t的标准库类型,和size_t一样,他也是定义在cstddef头文件的机器相关的类型,因为相减可能为负数,所以他是个signed类型
//注意:使用end参数时需要带上iterator头文件
由上面的代码可知,我们还可以使用另外一种方法指向尾后元素:arr + sz
int *a = arr, *e = arr + sz; //遍历arr所有元素,此例子没有意义,但是能说明另一种遍历方法
while(b<e) //前提是指向的内容都为同一数组内的元素才可以这样做
++b;
(4)解引用和指针运算的交互
int ia[] = {0, 1, 2, 3, 6};
int last = *(ia + 4); //正确:把last初始化为6,指针加上个整数表示向前移动4位,而对象名默认下标为0,所以是ia第4个下标的数值
//等价于ia[4]
int last = *ia + 4; //last初始化为4,由于优先级,先解引用ia后得到的0与4相加
运算符优先级表在《C++ Primer》第147页。
(5)下标和指针
多数情况下使用数组的名字其实用的是一个指向数组首元素的指针。
string和vector也可以使用下标,但是他们的下标必须是无符号类型。而数组允许处理负值这也是与string和vector的区别,但必须指向原来的指针所指的同一数组中的元素或尾后元素。
int ia[] = {0, 1, 2, 3, 4};
int i = ia[2]; //ia先转换为ia[0],再(ia[0] + 2)得到ia[2]
int *p = ia; //p指向ia的首元素
i = *(p + 2) //等价于i = ia[2];
int *p = &ia[2]; //p指向索引为2的元素
int j = p[1]; //p[1]等价于*(p + 1),也就是元素ia[3]
int k = p[-2]; //p[-2]等价于*(p - 2),相当于ia[0]
四、C风格字符串(char[])
C++支持C风格字符串,但是C风格字符串使用起来不方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。字符串面值的结构就是C++由C继承而来的C风格字符串。C风格字符串不是类型,而是约定俗成的表达和使用字符串的写法。按照此习惯必须在字符串中以空字符串' \0 '结束。
(1)C标准库string函数
下面列举了C语言标准库提供的一组函数,他们呗定义在cstring头文件中。
strlen(p) 返回p的长度,空字符不计算在内
strcmp(p1, p2) 比较p1和p2的是否相等。如果相等返回0,p1>p2返回一个正值,p1<p2返回一个负值
strcat(p1, p2) 将p2附加到p1之后,返回p1
strcpy(p1, p2) 将p2拷贝给p1,返回p1
上面所列举的函数,不负责验证其字符串参数。
传入此类函数的指针必须指向以空字符作为结束的数组:
char ca[] = {'C', '+', '+'}; //不以空字符结束
cout << strlen(ca) << endl; //输出长度会长于ca,输出15
strlen可能会沿着ca在内存中的位置不断向前寻找,直到遇到空字符才停下来。
(2)比较字符串
当使用string进行比较时,用的是普通的关系运算符和相等性运算符:
string s1 = "ONE";
string s2 = "two";
if(s1<s2) //true,s1<s2
当使用C风格字符串进行比较是,实际比较的是指针而非字符串本身,在数组的知识当中,我们知道了直接使用数组名,编译器则会将数组直接转换为指向第一个数组对象的指针。
const char ca1[] = "one";
const char ca2[] = "two";
if(ca1 < ca2) //未定义,试图比较两个无关的地址
//根据上面的知识我们知道,指针数组的元素比较,需要是指向同一个数组的元素才能进行比较
如果想要比较两个C风格字符串需要用strcmp函数,这时候就不是进行指针比较了,而是字符串与字符串本身的对比。
const char ca1[] = "one";
const char ca2[] = "two";
if(strcmp(ca1. ca2)<0) //与s1<s2含义相同,字符串本身的对比
//如果ca1 = ca2返回0,ca1 > ca2返回正值,ca1 < ca2返回负值
五、与旧代码的接口
如果我们新写成的代码,想要跟没有string与vector时代的代码相关联,为了衔接这一操作,C++提供了一些功能。比如旧程序的某处需要使用一个C风格字符串,但编译器无法直接用string对象来替换他,我们就可以使用c_str()函数返回一个C风格字符串。
(1)混用string对象和C风格字符串
为了让旧程序与string衔接:
string s("string");
char *sr = s; //错误,不能用string初始化char *
const char *str = s.cstr(); //正确,cstr将s转换成了const char*
//当我们改变了s值,上述的指针则会失效,这时候我们需要重新cstr赋值一遍
string s = s + "s";
const char *str = s.cstr();
(2)使用数组初始化vector对象
我们不可以拿一个数组为另一个内置类型(最原始的数组char [])的数组赋初值也不运行使用vector来初始化数组对象。但是允许数组初始化vector对象总之,数组的领地我们不能触犯,但是允许数组触犯其他类型的领地
vector可以拷贝数组,只要明确拷贝区域的首元素和尾后地址就可以了
int ia[] = {0, 1, 2, 3, 4, 5}
vector<int> ivec(begin(ia), end(ia)); //跟之前指针指向数组首、尾后地址一样,将begin和end用做参数即可
//如果想拷贝2-4下标范围内的元素给vector对象
vector<int> ivec(ia + 2, ia + 4); //数组对象指向下标0的位置,直接递增即可
六、多维数组
C++当中并没有多维数组,多维数组其实就是数组的数组。
当一个数组的元素仍是数组时,通常用两个维度来定义他:
一个维度表示数组本身大小另一个维度表示其元素大小
int ia[3][4]; //数组总体积为3个元素,每个元素都是4个整数的数组
//对于数组的理解都是由内向外的,从定义的名字开始,
//ia是含有3个元素的数组,而这3个元素的数组中,
//每个元素都含有4个元素的数组,
//由左边我们知道,
//这些元素都是int类型
int arr[10][20][30] = 0; //数组大小为10,10个元素大小都为20的数组,20个数组中每个数组都有30个整数元素
(1)多维数组的初始化
允许使用嵌套式的列表初始化方法,也可以不用嵌套,直接一个列表初始化。可以只初始化每一个二维数组当中的第一个元素,但是这种情况必须用嵌套。
int ia[3][4] = { //数组大小为3个元素,每个元素都是4个整数的数组
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; //与上面的嵌套初始化等价
int ia[3][4] = {{0}, {1}, {2}}; //只初始化每行的首元素,其他元素为0
int ia[3][4] = {0, 1, 2, 3}; //如果没有嵌套,则只初始化第一行的4个元素,其他元素为0
(2)多维数组的下标引用
可以使用下标运算符来访问多维数组的元素,此时数组的每个维度对应一个下标运算符。如果表达式中含有的下标运算符数量和数组的维度一样,那么表达式的结果是那个数组的原形。如果小于原始数组下标,则给的是索引出的一各内层数组。
int arr[10][20][30];
ia[3][4]; //三行四列
ia[2][3] = arr[0][0][0]; //下标从0开始,左值则为第三行的第四列元素
//利用arr的首元素给ia最后一行的最后一个元素赋值
int (&row)[4] = ia[1]; //先定义一个含有4个元素的数组的引用,将引用绑定到第二列四个元素上
用for语句处理多维数组:
constexpr size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt];
for (size_t i = 0; i != rowCnt; ++i) //每一行
{
for (size_t j = 0; j != colCnt; ++j) //每一列
{
ia[i][j] = i * colCnt + j;
}
}
for(int i = 0;i < rowCnt;i++)
{
for(int j = 0; j < colCnt;j++)
cout << ia[i][j] << " " <<endl;
}
//输出0-11,总共12个整数
(3)使用范围for语句处理多维数组
在c++11新标准中新增了范围for语句,上面的for语句可以简化为下面的形式:
size_t cnt = 0;
for(auto &row : ia) //外层数组每一个元素(每一行)
for(auto &col : row){ //内层数组每一个元素(每一列)
col = cnt;
++cnl;
}
每次迭代都将cnt的值赋给ia的当前元素,然后将cnt+1。
这里将row和col定义为引用的原因是,如果不采用引用,则每个元素都会直接指向ia数组的首元素,这与我们需要遍历整个元素的目的区别太大。所以必须要把遍历的元素全部变为数组的引用才可以进行此项操作。
(4)指针和多维数组
当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。
int ia[3][4] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
int (*p)[4] = ia; //让p指向含有4个整数的数组
for(auto i:*p)
cout << i << endl; //输出内容为0,1,2,3
p = &ia[2]; //让p指向四个尾元素
cout << i << endl; //输出8,9,10,11
int (*p)[4] = ia; //指向含有4个元素的数组,遍历输出0-3
int *p[4] = {*ia}; //整型指针的数组,遍历输出0-3
//上述内容主要看符号优先级,()优先级大于[]大于*
指针数组和数组指针
int *p[n] 指针数组(p+1指向下一个):首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身的大小决定,每一个元素都是一个指针,在32 位系统下任何类型的指针永远是占4 个字节。它是“储存指针的数组”的简称。
int (*p)[n] 数组指针(行指针,如果p+1,直接指向下一行):首先它是一个指针,它指向一个数组。在32 位系统下任何类型的指针永远是占4 个字节,至于它指向的数组占多少字节,不知道,具体要看数组大小。它是“指向数组的指针”的简称。
C++11新标准的提出,通过使用auto或者decltype就能尽可能避免使用指针数组和数组指针了。
for(auto p = ia;p != ia + 3;++p)
{
for(auto q = *p;q != *p + 4;++q)
cout << *q << '';
cout << endl;
}
运行结果:
我们使用begin和end也可以实现以上功能,而且要更加简洁。
for (auto p = begin(ia); p != end(ia); ++p)
{
for (auto q = begin(*p); q != end(*p); ++q)
cout << *q << " ";
cout << endl;
}
*p相当于ia[p][q],第一个for是从p[0]首元素开始,第二个for是从p[0][0]开始到p[0][3]结束一个循环,跳出再到第一个个for,p[1]开始,以此类推直到尾元素为止。
(5)类型别名简化多维数组的指针
这项操作能让我们更简便地去读写一个指向多维数组的指针。
using int_array = int[4]; //让int[4]的别名为int_array
typedef int int_array[4]; //与上面的作用相同
//循环输出ia
for(int_array *p = ia;p != ia + 3;++p)
{
for(int_array *q = ia;q != ia + 4;++q)
cout << *q << ' ';
cout << endl;
}
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。