一、前言
数组是个很好的东西,其关键就在这两个字:数、组。
数,即数字,组,即组合,大致意思可能就是数字的组合,或者更好理解一点的是数字的集合。
其作用就是将多个数字组合在一起使用。
其最基本的作用就是可以让我们程序员少写点代码。
二、简单使用
使用前我们需要理解为什么有它是一个很有必要的步骤,因为只有这样你才知道自己以后应该在什么时候使用它。
比如编程中我们最常见的一个需求就是,需要存放很多数据,比如举个简单点的例子,需要用户输入3个数字:
#include<iostream>
int main() {
int a, b, c;
scanf_s("%d %d %d", &a, &b, &c); //vs中使用scanf的安全函数可以不用定义宏
}
在数量少的情况下,这是很正常的写法,但往往实际应用中并没有这么简单。
比如数学的矩阵运算,随便一个矩阵的数量就是数十上百,这种情况下你也不可能真的去定义数十上百的变量。
而这就是数组的意义:一次申请大量相同类型的内存空间。
注意这里的表述,类型是与内存相关的,如果你对此不甚了解,可以先去阅读一下本站的另一篇文章:数据类型。
它的使用方式和普通定义变量的方式基本是一样的:
int num[100];
只不过在后面添加了一个中括号[]
而已,这个中括号中,就可以填入你想要多大的存储int
类型大小的内存。
那么这个时候,如果我想要得到用户输入的100个数字,就可以这样写:
#include<iostream>
int main() {
int num[100];
for (int i = 0; i < 100; i++) {
scanf_s("%d", &num[i]); //取出第i个数字,并通过取址符号&得到存放这个数字的内存地址,然后再填入用户输入的数字
}
}
是不是相当的方便?如果不使用数组,你甚至可能真得自己定义100个变量,那可太痛苦了。
这就是数组的基本用法,唯一需要注意的点可能就是它是从数字0开始的。
所以我们这里定义了100个int大小的内存,但访问的时候,你只能写0-99
。
最基本的使用方式:
int a=num[0]; //访问索引0位置的数组数字
num[99]=100; //修改索引99位置的数组数字
虽然这里只用了int
类型,但其它类型都是一样的,比如short
、long long
、float
、double
等等。
甚至包括char
。
char类型的数组又常常被称为字符串:
char str[12]="hello world";
那你可能就会有疑问了,数组不是存放数字的吗?怎么这里存放的是字符串呐!
如果你对这个问题有疑问,请务必阅读这篇文章:数据类型。
它本质上就是数字,只不过是我们赋予它的解释方式不一样罢了,你应该时刻记住一件事:内存中只能存放数字。
三、进阶理解
上面的基本用法学会了,你就已经可以正常编程了。
但如果想要精通C/C++,这肯定还是不够的,你必须要理解数组的底层实现原理:连续的内存
比如下面的代码:
#include<iostream>
int main() {
int num[5];
for (int i = 0; i < 5; i++) {
printf("%p\n",&num[i]);
}
}
申请了5个int类型大小的内存,并分别打印了一下它们的地址,其在我的电脑上运行结果为:
000000DDE4BEF738
000000DDE4BEF73C
000000DDE4BEF740
000000DDE4BEF744
000000DDE4BEF748
这就是这5个存放数字的内存地址,只不过是用十六进制形式打印出来的。
前面都是一样的,你只需要看最后两位数字即可:
38 3C 40 44 48
发现规律没?相邻的两个变量内存地址差4
,还记得int
类型占多少个字节吗?不就是4字节嘛!
从这里你就可以看出来,数组申请的内存是连续的。
如果不信的话,可以再自己试试分别定义五个变量,然后打印它们的地址,看看还会不会挨在一起(即相邻两个变量的内存地址相差4)。
也正是因为它是连续的,所以你才能用下标的方式来索引到对应的数字:
int num[5];
num[3]=100; //因为内存连续,所以才能这么用
因为其本质上,就是用首地址加上偏移量来实现的,还原过来就是下面这样:
*(num+3)=100;
数组的名字,实际上存放的就是这块连续内存的地址而已,并且它是int
类型的,所以可以直接通过地址加3来到第四块int类型内存的位置。
然后再使用*
符号来操作这个内存位置上的数字。
如果不使用*
符号,肯定就直接报错了,因为此时没有任何含义,左边就是一个地址加数字,结果就是一个代表地址的数字(又被称为右值),是无法赋值的。
而这也正是它会从索引0开始计数的原因,因为数组名本身是这块内存的首地址,也同样是第一个变量的内存地址。
*(num+0);//等价于*num
这样就方便底层计算。如果从1开始,那么num[1]
还需要多一步转换到,才能得到第一个变量的地址:num+1
要转换为num+0
才是第一个变量的地址。
特别要注意的是指针直接相加的操作:
int nump[5];
*(num+2);//int占4字节,所以实际上是加了2*4字节的距离
*(int*)((char*)num+8); //强转为char,char占1字节,所以实际加了1*8字节
上面两种写法是等价的,特别是第二种,这是理解C/C++指针最好的方式:你可以任意定义解释指针的方式。
首先是(char*)num+8
,意思就是将num
转换为char类型的指针,然后加8个char的距离,即8字节。
然后将结果(也就是地址)有重新转换为int*
,然后再在最外层使用*
来取值,因为转换成为了int
类型的指针,所以此时*
取的是四个字节的长度。
如果不强制转换,那么为char*
,其取的就只有一个字节。
四、多维数组
多维数组也是我们经常遇见的,写法就是多个[]
:
int num[4][4];
int num3[4][4][4];
有几维,那就加几个[]
即可。
此时使用起来就需要两个索引了,同时两个索引也都是从0开始的:
num[0][0]=100; //(0,0)位置
int a=num[3][3]; //(3,3)位置
直白来看,这似乎就是个二维表格。
但如果你打印一下它每个元素的地址,你就会发现一件神奇的事情:
#include<iostream>
int main() {
int num[2][2];
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
printf("%p\n",&num[i][j]);
}
}
}
结果:
000000CF9057FB58
000000CF9057FB5C
000000CF9057FB60
000000CF9057FB64
发现没?这四个元素居然依然是挨着的!依旧相邻4个字节!
所以上面的二维数组,本质上其实就是一个一维数组而已:
int num[4];
只不过编译器帮我们换算了一下,很多时候,我们为了方便也会自己用一维数组来当做二维数组使用。
因为用new/malloc
函数动态分配内存时,并不能直接分配二维数组。
所以就是这样将一维数组当二维数组使用的:
#include<iostream>
int main() {
int *num=new int[4];
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
printf("%p\n",num+i*2+j);
}
}
delete[] num;
}
这两种方式完全等价。
稍微理解一下num+i*2+j
:
- num为数组的首地址,i作为第几行,j作为第几列
- 通过
i*2
计算得到当前到第几行了(每行2列,所以乘以2) - 最后加
j
就是第i行的第j列。
这样直接相加得到的就是这个位置的地址,如果想要取该位置的值,就:*(num+i*2+j)
包括更高维也是一样的。
也正因二维数组本质上就是一维数组,所以在函数传二维数组参数时,你可以省略第一个数字,但绝不能省略第二个数字:
void test(int num[][]) { //错误,第二个数字不能省略
}
void test(int num[][2]) { //正确,第一个数字可以省略
}
为什么?
因为第二个[]
中的数字代表一行有几列,编译器也只有知道一行有几列的情况下,才能算出第二行、乃至第n行的地址。
其原因依旧是二位数组本质是一维数组,你不知道第一行有多大,你怎么计算第二行呢?
而第一个[]
中的数字代表有几行,这个就无所谓了,C/C++是非常随便的,将所有内存管理权限都交给你了。
就算你只申请了4个内存大小,你也照样能访问之后100位置内存上的数字。
这就属于未定义行为,因为那个位置上的东西不是你申请的,你不知道它的含义,也不知道能不能访问或者修改。
如果乱来,可能程序就直接奔溃了,所以这就是程序员使用C/C++时需要注意的问题。
五、array与vector库
正如上面所说,即使数组只有四个元素,但你依旧去访问100索引为止的内存,是可以访问的,但这种行为并不推荐,因为我们不清楚100位置上内存存放的是什么值、如果随意修改甚至可能导致程序崩溃。
想要解决这个问题,可以使用官方封装好的array与vector这两个库。
前者属于静态数组,我们需要提前指定其大小,后者为动态数组,我们无需指定大小,直观数据存放即可。
首先是array的使用,由于其是静态大小数组,所以我们需要提前指定它的大小:
array<int,10> arr1;
同时由于它使用了模板,需要执行它存放的数据类型,所以这里填入的模板参数为int与10,分别代表存放int类型10个大小的数组空间,
然后重点就来了,由于使用[]
符号访问索引即使超出了数组本身的长度,依旧可以访问、但程序很可能会崩溃,并不推荐。
所以array这个结构便封装了at方法,其和[]
效果是一致的,参数便是索引。
唯一不同之处在于,如果使用at访问超出数组大小的索引,它就会抛出异常,而我们就可以使用try…catch结构捕获异常,从而在程序不崩溃的情况下查看问题所在。
至于vector和array相同,也提供了at函数、如果超出范围就会抛出异常:
vector底层使用的new/malloc函数动态分配内存,因此并不需要我们指定它的大小,我们可以近乎无限的往其中塞入数据。
C++标准库相关的内容可以参考文章:STL详解