数组是一种存储固定大小同类型元素的数据结构。数组的定义可以通过指定元素类型、数组大小以及数组名称来完成。数组的每一项称为一个元素,每个元素的读写通过数组名加偏移来实现。
一维数组是包含一组有序的同类型元素的线性结构。每个元素可以通过索引进行访问,索引从0开始计数。
如下是其定义的语法结构:
数据类型 数组名[整型常量];
例如,定义一个整型数组 vals ,包含6个元素:
int vals[6];
一维数组可以在定义数组的同时指定初始值。例如:
int vals1[] = { 1,2,3,4,5,6 };
int vals2[7] = { 1,2,3,4,5,6 };
int vals3[6] = { 0 };
int vals4[6] = {}; //C++11 新标准,之前的 C++ 标准不能编译这种格式,大括号内最少要有一个值
int vals5[6]{}; //C++11 新标准,数组初始化时可省略等号
该代码执行后,vals1 的长度为 6 。vals2 的长度为 6 ,其所包含的元素是 1,2,3,4,5,6,0 。vals3 的长度为 6 ,其所包含的元素全部是 0 。vals4 的长度为 6 ,其所包含的元素全部是 0 。vals5 的长度为 6 ,其所包含的元素全部是 0 。
注意点1: 数组初始化时不能缩窄转换。例如 double 转 int ,例如:
int vals[6]{ 1,2,3,4,5,6.2 };
此时编译器会报错误或告警信息:conversion from ‘double’ to ‘int’ requires a narrowing conversion
注意点2: 未初始化的局部数组变量会自动填充随机值(未初始化的全局数组变量默认初始化为 0 ),例如:
#include
using namespace std;
int main()
{
int vals[6];
for (const auto& val : vals)
{
printf("%d ", val);
}
printf("\n");
return 0;
}
上面代码输出:
-858993460 -858993460 -858993460 -858993460 -858993460 -858993460
(1)for 循环遍历
int vals[6]{};
for (size_t i = 0; i < sizeof(vals) / sizeof(int); i++)
{
printf("%d ", vals[i]);
}
注意计算一维数组长度的方法:sizeof(vals) / sizeof(int)
,也可以使用 sizeof(vals) / sizeof(vals[0])
(2)基于范围的 for 循环遍历
基于范围的 for 循环是在 C++11 标准中推出的新的语法,相比于传统的 for 循环,该方法无需关注数组的起始与长度,更适用于开发。
int vals[6]{};
for (const auto& val : vals)
{
printf("%d ", val);
}
数组的越界访问是无法被捕获异常的,会造成程序的崩溃。 C++ 编译器不限制下标越界,带来的好处是代码运行速度更快,效率更高,坏处就是需要开发人员对数组下标访问的控制要仔细,一定要保持在范围之内。
如下为一个越界访问的例子:
int vals[6]{};
printf("%d ", val[6]); //数组下标的起始值为 0 ,所以这里属于越界访问,造成程序的崩溃。
多维数组是一种包含多个维度的数组,其元素可以通过多个索引进行访问。多维数组可以看作是数组的数组,即在高维度中,每个元素本身也是一个数组。
如下是其定义的语法结构:
数据类型 数组名[整型常量1][整型常量2]...[整型常量3];
例如,定义一个二维整型数组 valLists :
int valLists[3][4];
上面的二维整型数组 valLists 可以被视作一个 3 行 4 列的二维表格:
第 1 列 | 第 2 列 | 第 3 列 | 第 4 列 | |
---|---|---|---|---|
第 1 行 | valLists[0][0] | valLists[0][1] | valLists[0][2] | valLists[0][3] |
第 2 行 | valLists[1][0] | valLists[1][1] | valLists[1][2] | valLists[1][3] |
第 3 行 | valLists[2][0] | valLists[2][1] | valLists[2][2] | valLists[2][3] |
第 4 行 | valLists[3][0] | valLists[3][1] | valLists[3][2] | valLists[3][3] |
与一维数组一样,多维数组也可以在定义数组的同时指定初始值,以二维数组为例:
int valLists1[][] = { 1,2,3,4,5,6 }; //错误:多维数组的定义必须要指定每一维的长度
int valLists2[2][] = { 1,2,3,4,5,6 }; //错误:多维数组的定义必须要指定每一维的长度,缺一不可
int valLists3[2][2] = { 1,2,3,4,5,6 }; //错误:初始值的个数超过了多维数组的最大容量(2*2=4)
int valLists4[2][3] = { 1,2,3,4,5,6 }; //OK
int valLists5[2][3] = { 1,2,3,4,5 }; //OK
int valLists6[2][3] = { {1,2,3},{4,5,6} }; //OK
int valLists7[2][3] = { {1,2,3},{4,5} }; //OK
int valLists8[2][3] = { 0 }; //OK
int valLists9[2][3] = {}; //OK C++11 新标准,之前的 C++ 标准不能编译这种格式,大括号内最少要有一个值
int valLists10[2][3]{}; //OK C++11 新标准,数组初始化时可省略等号
上面代码执行后对应的多维数组值如下:
valLists4 = 0x0000007d7476f9c8 {0x0000007d7476f9c8 {1, 2, 3}, 0x0000007d7476f9d4 {4, 5, 6}}
valLists5 = 0x0000007d7476f9f8 {0x0000007d7476f9f8 {1, 2, 3}, 0x0000007d7476fa04 {4, 5, 0}}
valLists6 = 0x0000007d7476fa28 {0x0000007d7476fa28 {1, 2, 3}, 0x0000007d7476fa34 {4, 5, 6}}
valLists7 = 0x0000007d7476fa58 {0x0000007d7476fa58 {1, 2, 3}, 0x0000007d7476fa64 {4, 5, 0}}
valLists8 = 0x0000007d7476fa88 {0x0000007d7476fa88 {0, 0, 0}, 0x0000007d7476fa94 {0, 0, 0}}
valLists9 = 0x0000007d7476fab8 {0x0000007d7476fab8 {0, 0, 0}, 0x0000007d7476fac4 {0, 0, 0}}
valLists10 = 0x0000007d7476fae8 {0x0000007d7476fae8 {0, 0, 0}, 0x0000007d7476faf4 {0, 0, 0}}
注意点: 未初始化的局部多维数组变量会自动填充随机值(未初始化的全局多维数组变量默认初始化为 0 ),例如:
int valLists1[2][3];
上面代码执行后对应的多维数组值如下:
valLists1 = 0x0000003d1270f878 {0x0000003d1270f878 {-858993460, -858993460, -858993460}, 0x0000003d1270f884 {-858993460, -858993460, -858993460}}
(1)for 循环遍历
方式1:使用数组各维度长度作为循环变量的范围(推荐)
int valLists[2][3] = { 1,2,3,4,5,6 };
for (size_t i = 0; i < 2; i++)
{
for (size_t j = 0; j < 3; j++)
{
printf("%d ", valLists[i][j]);
}
}
方式2:使用 sizeof 运算符计算,但是需要注意计算方法(不推荐,计算较为复杂)
int valLists[2][3] = { 1,2,3,4,5,6 };
for (size_t i = 0; i < sizeof(valLists)/ sizeof(valLists[0]); i++)
{
for (size_t j = 0; j < sizeof(valLists[0]) / sizeof(int); j++)
{
printf("%d ", valLists[i][j]);
}
}
(2)基于范围的 for 循环遍历
基于范围的 for 循环是用于多维数组更为方便,不需要考虑每个维度的长度。
int valLists[2][3] = { 1,2,3,4,5,6 };
for (const auto& vals : valLists)
{
for (const auto& val : vals)
{
printf("%d ", val);
}
}
在 C++ 的程序开发中,经常会把数组作为参数传递给函数,也会有函数将数组作为返回值的情况。数组变量与基本类型变量(如 int、float 等)对比,其占据的空间往往都比较大,所以即使是值传递的方式,数组在函数中也不会再创造一个副本,因此不管是作为参数传递还是作为返回值,实际操作的都是数组第一个元素的地址,也就是数组名。
构建一个入参为整型数组的函数,其功能是打印数组的全部元素。
(1)使用数组名作为入参
#include
void printVals(const int vals[], int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", vals[i]);
}
}
int main()
{
int vals[6] = { 1,2,3,4,5,6 };
printVals(vals, 6);
printf("\n");
return 0;
}
上面代码输出:
1 2 3 4 5 6
(2)使用指针作为入参
#include
void printVals(const int* vals, int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", vals[i]);
}
}
int main()
{
int vals[6] = { 1,2,3,4,5,6 };
printVals(vals, 6);
printf("\n");
return 0;
}
上面代码输出:
1 2 3 4 5 6
除了上述两种模式,还可以使用标准数组声明作为参数,如:void printVals(const int vals[6], int len)
,这里面 vals
后面中括号内的 6 ,实际上没有什么作用,调用者传入的数组长度可以不为 6 。
注意点:数组入参后,自动退化为数组第一个元素的地址,所以无法计算数组长度。样例代码如下(编译器选择 x64 ):
#include
using namespace std;
void printVals(const int vals[], int len)
{
size_t valsLen = sizeof(vals) / sizeof(int);
printf("after input parameter: %llu\n", valsLen);
}
int main()
{
int vals[6] = { 1,2,3,4,5,6 };
size_t valsLen = sizeof(vals) / sizeof(int);
printf("before input parameter: %llu\n", valsLen);
printVals(vals, 6);
printf("\n");
return 0;
}
上面代码输出:
before input parameter: 6
after input parameter: 2
代码输出结果中的 before input parameter: 6
比较容易理解,整个数组占据内存为 24 个字节,每个整形元素占据内存为 4 个字节,两者相除刚好得到数组的长度 6 。下一个输出 after input parameter: 2
是由于数组入参后,传入的实际是第一个元素的地址, x64 编译时每个地址占用内为 8 个字节( 64 位),每个整形元素占据内存为 4 个字节,两者相除结果为 2 。
从这个结果可以知道数组传参还需要把长度传进去的原因,在函数中已经无法计算出入参数组的长度了。
数组作为返回值时,实际返回的都是数组第一个元素的地址,也就是数组名。所以如果用如下方式返回数组,则在函数作用域结束后,数组占的内存也不复存在,会导致程序崩溃:
#include
int* getVals()
{
int vals[6] = { 1,2,3,4,5,6 };
return vals;
}
int main()
{
int* vals = getVals();
printf("%d ", vals[0]); //该语句会导致程序崩溃
return 0;
}
正确的使用方式应该在堆上创建数组,然后返回数组指针,如下为样例代码:
#include
int* getVals()
{
int* vals = new int[6]{ 1, 2, 3, 4, 5, 6 };
return vals;
}
int main()
{
int* vals = getVals();
printf("%d ", vals[0]);
return 0;
}
上面代码输出:
1
在上面章节 数组作为返回值
中已经提及了在堆上创建数组的概念。由于数组一般占据空间较大,所以如果都创建在栈中,则很有可能会超过栈的最大容量(栈是向低地址扩展的数据结构,是一块连续的内存区域。栈顶的地址和栈的最大容量是系统预先规定好的,在Window系统中,栈空间大小由编译器设置决定,默认为 1 MB ;Linux 下,默认栈空间大小为 8 MB ,可通过 ulimit -s 来设置)。所以在堆上创建数组是很常见的操作。如下为样例代码:
#include
int main()
{
int vals[6] = { 1,2,3,4,5,6 }; //创建在栈上
int* vals2 = new int[6]{ 1, 2, 3, 4, 5, 6 }; //创建在堆上
return 0;
}
(1)创建语法
在堆上创建一维数组需要使用动态内存分配函数,C++ 使用 new 运算符( C 语言用 molloc 标准库函数),如下为创建的语法结构:
数据类型* 指针名 = new 数据类型[整型常量];
样例代码:
int* vals = new int[6];
(2)初始化
int* vals1 = new int[6]; //未初始化
int* vals2 = new int[6](); //全部初始化为 0
int* vals3 = new int[6]{ 1,2,3,4,5,6 }; //C++11 新标准
int* vals4 = new int[6]{ 1,2 }; //C++11 新标准
该代码执行后,vals1 所有元素都是随机值 。vals1 所有元素都是 0 ,vals3 的元素值分别是 1,2,3,4,5,6 ,vals3 的元素值分别是 1,2,0,0,0,0 。
(2)释放内存
需要重点关注
在堆上申请的内存需要手动释放,如下为样例代码:
int* vals = new int[6];
delete vals; //错误:这样只会释放 vals 数组的第一个元素所占据的空间,从而造成内存泄漏
delete[] vals; //OK
释放在堆上的数组, delete 一定要加上中括号 [] 。
delete[] 的原理:在使用 new[] 操作符来动态分配数组时,内存管理器会为数组分配一块连续的内存空间,这个内存空间大小并不等于对象大小*数组长度,而是多了 8 个字节( x64 编译),这 8 个字节就是数组长度。在使用 delete[] 操作符来释放这块内存时,便能够从该内存中读取数组长度,从而完整的释放整个数组占用的内存空间,并将该内存空间标记为可用。
以二维数组为例:
int** vals = new int*[2];
for (size_t i = 0; i < 2; i++)
{
vals[i] = new int[3]();
}
释放二维数组也需要使用循环:
int** vals = new int*[2];
for (size_t i = 0; i < 2; i++)
{
vals[i] = new int[3]();
}
for (size_t i = 0; i < 2; i++)
{
delete[] vals[i]; //注意 delete 一定要加上中括号 []
}
注:该部分内容涉及到 C++11 新特性。
array 容器是C++11新引入的类型(需要引入头文件 #include ),与上面章节介绍的内置数组相比,array 容器的大小也是固定的,提供了更安全(比如使用 at 方法来访问元素可以捕获异常)、更方便(提供多种数组处理函数)的方式来处理数组。
还是以 int 类型为例:
#include
#include
using namespace std;
int main()
{
array<int, 6> vals = { 1,2,3,4,5,6 };
for (const auto& val : vals)
{
printf("%d ", val);
}
return 0;
}
上面代码输出:
1 2 3 4 5 6
如下为样例代码:
#include
#include
using namespace std;
int main()
{
array<int, 6> vals = { 1,2,3,4,5,6 };
vals[0] = 0; //使用下标访问
printf("%d\n", vals.at(0)); //使用 at 访问,注意该方法可以被捕获异常
//使用迭代器
for (auto it = vals.begin(); it != vals.end(); it++)
{
printf("%d\n", *it);
}
return 0;
}
array 容器在很多方面都像一个数组,但它并不是一个真正的数组。它仍然是一个类,拥有自己的成员函数和数据成员。
如下为样例代码:
#include
#include
using namespace std;
int main()
{
array<int, 6> vals1 = { 1,2,3,4,5,6 };
array<int, 6> vals2 = { };
memcpy(vals2.data(), vals1.data(),6*sizeof(int));
int vals3[6] = { 1,2,3,4,5,6 };
array<int, 6> vals4 = { };
memcpy(vals4.data(), vals3, 6 * sizeof(int));
array<int, 6> vals5 = { 1,2,3,4,5,6 };
int vals6[6] = { };
memcpy(vals6, vals5.data(), 6 * sizeof(int));
return 0;
}
上面代码执行后,变量 vals2 、 vals4 、 vals6 的元素值都被拷贝进入 1,2,3,4,5,6 。
数组占据的内存是一块连续地址,所以非常方便对于整块数据的存储与处理,这种特性适用于多种应用场景,比如:大量数据的数据处理和计算,动态规划(动态规划问题中,状态转移方程通常会用到数组来存储中间结果),对象的序列化与反序列化,网络编程,图形和图像处理(使用数组来存储像素值或颜色信息)等。
在持久化开发(比如把程序运行过程中的一些数据对象存储到磁盘中)与网络开发(比如把 A 电脑程序中的一个数据对象通过 SOCKET 传送给 B 电脑)中,对象的序列化与反序列化是一个非常重要的操作。如下为样例代码:
#include
#include
using namespace std;
class Student
{
public:
Student() {};
Student(string name, int age, double mathScore)
:m_name(name),
m_age(age),
m_mathScore(mathScore)
{};
~Student() {};
public:
char* serialize()
{
size_t len = 0;
size_t nameLen = m_name.length();
len += nameLen + 1; //字符串最后一位为 \0(使用 string 成员函数 length() 计算的长度不包含 \0)
len += sizeof(int);
len += sizeof(double);
char* data = new char[len] {}; //创建序列化数组
size_t offset = 0;
memcpy(data, m_name.c_str(), nameLen); //写入名称
offset += nameLen + 1;
memcpy(&data[offset], (char*)&m_age, sizeof(int)); //写入年龄
offset += sizeof(int);
memcpy(&data[offset], (char*)&m_mathScore, sizeof(double)); //写入数学成绩
return data;
}
void deserialize(char* data)
{
size_t offset = 0;
m_name = string(data); //读出名称
offset += m_name.length() + 1;
m_age = *(int *)(&data[offset]); //读出年龄
offset += sizeof(int);
m_mathScore = *(double *)(&data[offset]); //读出数学成绩
}
public:
string getName()
{
return m_name;
}
int getAge()
{
return m_age;
}
double getMathScore()
{
return m_mathScore;
}
private:
string m_name; //名称
int m_age; //年龄
double m_mathScore; //数学成绩
};
int main()
{
Student st1{ "zhangsan",15,92.2 };
char* data = st1.serialize();
Student st2;
st2.deserialize(data); //对象 st2 便获得了 st1 的全部属性值
delete[] data;
return 0;
}
一块连续的内存空间特别适合大数据的处理、拷贝等操作,比如针对 100 万整型数据的处理,从理论上来说,其所占据的空间应该等于 1000000*sizeof(int),结果为 3 MB。如果使用数组保存,则实际就是占据 3 MB空间,但是如果使用一些容器类型,比如 vector、list 等,则所占据的空间要远超于此(容器本身有一些成员变量需要占用空间)。另外,对于如此规模的数据拷贝,数组只需要一个 memcpy()
的操作即可,而容器类型则需要做耗时的循环操作,如下为样例代码:
#include
#include
using namespace std;
int main()
{
size_t len = 1000000;
char* data1 = new char[len * sizeof(int)];
//初始化
size_t offset = 0;
for (size_t i = 0; i < len; i++)
{
memcpy(&data1[offset], (char*)&i, sizeof(size_t));
offset += sizeof(int);
}
//拷贝
char* data2 = new char[len * sizeof(int)];
memcpy(data2, data1, len * sizeof(int));
return 0;
}