突破编程_C++_基础教程(数组)

1 数组的基本用法

数组是一种存储固定大小同类型元素的数据结构。数组的定义可以通过指定元素类型、数组大小以及数组名称来完成。数组的每一项称为一个元素,每个元素的读写通过数组名加偏移来实现。

1.1 一维数组

一维数组是包含一组有序的同类型元素的线性结构。每个元素可以通过索引进行访问,索引从0开始计数。
如下是其定义的语法结构:

数据类型 数组名[整型常量];

例如,定义一个整型数组 vals ,包含6个元素:

int vals[6];

1.1.1 一维数组的初始化

一维数组可以在定义数组的同时指定初始值。例如:

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.1.2 一维数组的遍历

(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);
}

1.1.3 重点注意

数组的越界访问是无法被捕获异常的,会造成程序的崩溃。 C++ 编译器不限制下标越界,带来的好处是代码运行速度更快,效率更高,坏处就是需要开发人员对数组下标访问的控制要仔细,一定要保持在范围之内。
如下为一个越界访问的例子:

int vals[6]{};
printf("%d ", val[6]);	//数组下标的起始值为 0 ,所以这里属于越界访问,造成程序的崩溃。

1.2 多维数组

多维数组是一种包含多个维度的数组,其元素可以通过多个索引进行访问。多维数组可以看作是数组的数组,即在高维度中,每个元素本身也是一个数组。
如下是其定义的语法结构:

数据类型 数组名[整型常量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]

1.2.1 多维数组的初始化

与一维数组一样,多维数组也可以在定义数组的同时指定初始值,以二维数组为例:

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.1.2 多维数组的遍历

(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);
	}
}

1.3 数组与函数

在 C++ 的程序开发中,经常会把数组作为参数传递给函数,也会有函数将数组作为返回值的情况。数组变量与基本类型变量(如 int、float 等)对比,其占据的空间往往都比较大,所以即使是值传递的方式,数组在函数中也不会再创造一个副本,因此不管是作为参数传递还是作为返回值,实际操作的都是数组第一个元素的地址,也就是数组名。

1.3.1 数组作为参数

构建一个入参为整型数组的函数,其功能是打印数组的全部元素。
(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 。
从这个结果可以知道数组传参还需要把长度传进去的原因,在函数中已经无法计算出入参数组的长度了。

1.3.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

2 在堆上创建数组

在上面章节 数组作为返回值 中已经提及了在堆上创建数组的概念。由于数组一般占据空间较大,所以如果都创建在栈中,则很有可能会超过栈的最大容量(栈是向低地址扩展的数据结构,是一块连续的内存区域。栈顶的地址和栈的最大容量是系统预先规定好的,在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;
}

2.2 在堆上创建一维数组

(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[] 操作符来释放这块内存时,便能够从该内存中读取数组长度,从而完整的释放整个数组占用的内存空间,并将该内存空间标记为可用。

2.3 在堆上创建多维数组

以二维数组为例:

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 一定要加上中括号 []
}

3 array 容器

注:该部分内容涉及到 C++11 新特性。
array 容器是C++11新引入的类型(需要引入头文件 #include ),与上面章节介绍的内置数组相比,array 容器的大小也是固定的,提供了更安全(比如使用 at 方法来访问元素可以捕获异常)、更方便(提供多种数组处理函数)的方式来处理数组。

3.1 array 容器的定义与初始化

还是以 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

3.2 array 容器的常用方法

如下为样例代码:

#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 容器在很多方面都像一个数组,但它并不是一个真正的数组。它仍然是一个类,拥有自己的成员函数和数据成员。

3.3 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 。

4 数组常见应用

数组占据的内存是一块连续地址,所以非常方便对于整块数据的存储与处理,这种特性适用于多种应用场景,比如:大量数据的数据处理和计算,动态规划(动态规划问题中,状态转移方程通常会用到数组来存储中间结果),对象的序列化与反序列化,网络编程,图形和图像处理(使用数组来存储像素值或颜色信息)等。

4.1 对象的序列化与反序列化

在持久化开发(比如把程序运行过程中的一些数据对象存储到磁盘中)与网络开发(比如把 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;
}

4.2 大数据处理

一块连续的内存空间特别适合大数据的处理、拷贝等操作,比如针对 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;
}

你可能感兴趣的:(突破编程_C++_基础教程,c++)