数组

一、数组基础概念

(1)数组大小固定。
(2)存放类型相同的对象的容器。定义数组的时候必须指定数组的类型,不允许使用 auto 关键字由初始值的列表推断类型。
(3)数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就数说,维度必须是一个常量表达式。
(4)数组的元素应该为对象,因此 不存在引用的数组
(5)在使用数组下标的时候,通常将其定义为 size_t类型。size_t是一种机器相关的无符号类型,它被设计的足够大以便能表示内存中任意对象的大小。
(6)通过数组名字和数组中首元素的地址都能得到指向首元素的指针。

二、数组的声明

    数组的声明形如a[d],其中a是数组的名字,d是数组的维度。维度说明数组中的元素的个数,因此必须大于0。数组中元素的个数也属于数组类型的一部分,编译的时候维度应该也是已知的。也就是说,维度必须是一个常量表达式:
    定义数组的时候必须指定数组的类型,不允许使用auto关键字由初始化的列表推断类型。

1. 默认初始化

    和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。

	const int cNumb = 5;
	int nArrary[cNumb];
	string strArrary[cNumb];

在这里插入图片描述
在这里插入图片描述

2. 显式初始化数组

    可以对数组的元素进行列表初始化,此时可以忽略数组的维度。如果在声明时候没有指定数组的维度,编译器可以根据初始值的数量计算并推测出来;相反,如果指明了维度,那么初始值的总数量不应该超过指定的大小。如果维度比提供的初始值数量大,则用提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值。(如int中的0,string中的“”)

	constexpr int sz = 3;

	int ia1[sz] = { 0,1,2 };

	int a2[] = { 0,1,2 };

	int a3[5] = { 1,2,3 };		//等价于a3[5]={1,2,3,0,0}

	string a4[3] = { "hi","bye" };	//等价于a4[3]={"hi","byr",""}

	int a[2] = { 1,2,3 };		//错误,初始值过多

3. 字符数组的特殊性

    字符数组有一种额外的初始化形式,我们可以用字符串字面值对此类数组进行初始化。当使用这种方式初始化时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串其他字符一样被拷贝到字符数组中去。

	char a1[] = { 'C', '+', '+' };	//列表初始化,没有空字符

	char a2[] = { 'C', '+', '+', '\0' };

	char a2[] = "C++";	//自动添加表示字符串结束的空字符

	const char a4[6] = "Daniel";	//错误:没有空间可以存放空字符 

在这里插入图片描述

4. 复杂的数组声明

    数组能存放大多数类型的对象。例如,可以定义一个存放指针的数组。又因为数组本身就是对象,所以允许定义数组的指针及数组的引用。

  • int *ptrs[10]; //ptrs是含有10个整型指针的数组

默认情况下,类型修饰符从右向左依次绑定,所以ptrs首先是一个大小为10的数组,它的名字是ptrs,然后数组中存放的是指向int的指针。

  • int &refs[10] = /* ? */; //错误:不错在引用的数组,因为数组存放的是对象,而引用不是对象。

引用在定义的时候必须初始化,而什么叫定义,凡是有内存占用行为的就是定义,否则就是声明。

  • int (*Parray)[10] = &arr; //Parray指向一个含有10个整数的数组

其实这里很好理解,就是按照运算符优先级理解,首先()与[]优先级一样,所以按照结合,先执行()再[],()括号里表明Parray是一个指针,然后在执行右边的[],所以得知Parray是一个指向大小为10的数组的指针,然后左边的int表明数组类型为int。

  • int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组

这个和上面的一样。只不过arrRef是大小10,类型为int的数组的引用。因为这里是对数组的引用,所以右边arr理解为数组而不是首元素地址。

三、数组的一些特性

1. 不允许拷贝和赋值

    不能将数组的内容拷贝给其他数组作为初始值,也不能用数组为其他数组赋值。

	int a[] = { 0,1,2 };

	int a2[] = a;  //错误:不允许使用一个数组初始化另一个数组

	a2 = a;    //错误:不能把一个数组直接赋值给另一个数组

2. 数组名在什么时候退化为指针

    以下情况均不能将数组名s视为指针:

  • sizeof(s)
  • &s;
  • 用数组对数组的引用进行初始化时

四、多维数组

1. 定义

    严格来说,C++语言中没有多维数组,通常说的多维数组其实是数组的数组。

	int a[2][3];			   //大小为2的数组,每个元素是含有3个整数的数组

	int b[10][20][30] = {0};   //大小为10的数组,每个元素是含有20个数组的数组,然后这些数组是含有30个整数的数组

    对于二维数组来说,常把第一个维度称作行,第二个维度称作列。

2. 初始化

	// 1. 
	int ia[3][4] = {	 //三个元素,每个元素都是大小为4的数组

		{0,1,2,3},		 // 第一行的初始值

		{4,5,6,7},	     // 第二行的初始值

		{8,9,10,11}		 // 第三行的初始值

	};

	// 2.
	int ia[3][4] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };

	// 3.
	int ia[3][4] = { { 0 },{ 4 },{ 8 } };	//其他未列出的元素执行默认值初始化,但是若省去花括号就不一样了:

	int ia[3][4] = { 0, 4, 8 };				//这里显示初始化第一行前3个元素,后面的元素默认初始化

五、数组和指针

  1. 很多用数组名字的地方,编译器会自动地将其替换为一个指向数组首元素的指针
  2. 指针加上一个整数得到的结果还是一个指针
#include <iostream>
using namespace std;

int main(){
    string nums[] = {"i","love","you","haha"};
    string *p = &nums[0];       //p指向nums的第一个元素
    string *p2 = nums;          //等价于string *p2 = &nums[0] ,此时p2指向nums[0] 
	++p2;						//此时p2指向nums[1]; 
   
	string *last = &nums[4];	//得到数组尾元素之后那个并不存在的元素的地址。尾后指针不可以执行解引用和递增操作
    return 0;
}

  • 当数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组。
  • 但是当使用deltype关键字的时候,便不会发生上述转换。
int a[] = {0,1,2,3,4};
auto a2(a);                 	// a2 是一个整型指针,指向a的第一个元素。等价于 auto a2(&a[0]),a2的类型是int *

decltype(a) a3 = {0,1,2,3,4};	// a3 是一个含有10个整数的数组
a3[4] = 33;

指针也是迭代器

    允许使用递增运算符将指向数组元素的指针向前移动到下一个位置上。

int a[] = {0,1,2,3,4};

int *p = a;       // p 指向arr的第一个元素
++p;                // p 指向arr[1]

int *p1 = a + 10; 		// 错误写法:a只有5个元素,p1的值未定义。但是编译器无法发现错误
int *p2 = a + 5;		// 指向a的尾后指针,但不能解引用!

    和迭代器一样,两个指针相减的结果是他们之间的距离。参与运算的两个指针必须指向同一个数组当中的元素。如果两个指针分别指向不相关的对象,则不能比较他们。
    两个指针相减的类型是ptrdiff_t的标准库类型。和size_t一样,ptrsiff_t是定义在cstddef头文件中机器相关的类型。因为差值可能为负,所以ptrdiff_t是一种带符号类型
    指针运算除了适用于指向数组的指针,还适用于空指针。后一种情况中,两个指针必须指向同一个对象后者该对象的下一个位置、如果p是空指针,允许给p加上或减去一个值为0的整型常量表达式。两个空指针也允许彼此相减,结果是0。

int a[] = {0,1,2,3,4};

auto n = end(a) - begin(a);     // n的值是4.也就是a中元素的数量

解引用与指针运算的交互:

    虽然标准库类型string和vector也能执行下标运算,但是数组与他们还是不同的。标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求。内置的下标运算符可以处理负值,但是结果地址必须指向原来的指针所指向的同一个数组中的元素(或是同一数组尾元素的下一位置)。

#include <iostream>
#include <iterator>
using namespace std;

int main()
{
	int a[] = {0,2,4,5,7,8};
	
	int s = *a;			//a[0]的值,即为0
	int s1 = *(a + 3);	//a[4]的值,即为5
	int s2 = *a + 3;	//a[0] + 3的值,即为3

	int *p = &a[2];		//p指向索引为2的元素
	int val = p[1];		//p[1]等价于 *(p+1),也就是a[3]表示的那个元素 
	int val1 = p[-2];   //p[-2]等价于 *(p-2),也就是a[0]表示的那个元素 

    return 0;
}

六、数组的遍历

  1. 可以用尾后指针进行数组的遍历:
int a[] = {0,1,2,3,4};

int *e = &a[5];    // e 指向arr尾元素的下一个位置的指针。尾后指针不指向具体的元素,不能解引用或递增操作

// 可以使用尾后指针进行遍历
for(int *b = a; b != e; ++b)
{
    cout << *b << endl;
}

  1. 使用标准库函数 begin 和 end 进行遍历
#include <iostream>
#include <iterator>
using namespace std;
int main(){
    string nums[] = {"i","love","you"};
   
   	//通过标准库函数begin和end遍历
	int *begin = begin(nums);		//指向nums首元素的指针 
	int *end = end(nums);			//指向nums尾元素的下一个位置的指针
	while(begin != end){
		cout << *begin << endl;
		++begin;
	} 
	
    return 0;
}

七、动态数组

    动态数组不需要在编译时就确定大小,它的大小在程序运行过程中确定,所以可以根据程序需要而灵活的分配数组的大小,相比静态数组,它更“灵活”、“自由”。但是动态数组需要进行显式的内存释放。

1. 动态数组内存分配

    动态数组进行内存分配的格式为new T[size],size可以不是常量表达式;如下面的例子所示。

int size = 10;                       //此处的size不是常量表达式。
 
int* Dynamic_Arr2 = new int[size];      //未初始化

可以看出,虽然我们分配了一个动态数组,其实返回的是一个T类型的指针,指针指向的是数组的第一个元素,从这个角度来说,动态数组更像是指针。也是由于这种性质,导致了动态数组中size可以取0,即返回一个空指针,即分配一个空动态数组是合法的。同样的道理,返回的是一个指针,那么就不能使用begin或者end迭代器进行遍历。

2. 动态数组初始化

上面的例子中,Dynamic_Arr2并未进行初始化,若想进行默认初始化,需要在数组后面加上一个小括号。

int* Dynamic_Arr3 = new int[size]();     //默认的初始化;

此时数组中的十个值都为0;
也可以利用花括弧进行显式的初始化,例子如下;

string* Dynamic_Arr4 = new string[size]{"aa", "bb","cc", "dd", string(2, 'e') };      //显式的初始化

3. 动态数组释放

    释放动态数组时,使用delete[ ] arr_name;即在数组名前加上一个中括弧;例如

delete [ ] Dynamic_Arr4;
  • 释放一个动态数组时,或者说是指向数组的指针时,空括号是必须的。它告诉编译器,指针指向一个数组的第一个元素。
  • delete释放数组是逆序进行的,最后一个元素被最先释放,第一个元素最后一个被释放。

    使用动态数组时,一定要记得显式的释放内存,否则很容易出错,比如在一个大的工程中,某一个for循环中或者某个函数中申请了内存却没释放,当函数不断地被调用,这些申请过的内存会一直堆积,直到最后退出程序。这很可能造成非常大的麻烦。

比如下面两个函数;

void not_delete_fun()
{
	int *arr_test = new int[10];
 
	cout << arr_test << endl;
 
	//delete[] arr_test;
}
void delete_fun()
{
	int *arr_test = new int[10];
 
	cout << arr_test << endl;
 
	delete[] arr_test;
}

分别调用和释放两个函数三次,看看结果;
在这里插入图片描述
    可以看出,释放过内存后,每次数组申请的内存是一致的,未释放内存的函数,每次申请数组的内存是不一样的,说明这些数组申请的内存并不会自动释放,而是会随着程序运行一致累积。

4. 多维动态数组的内存申请

//-----------------多维数组内存申请-------------------//
	int MAX_NUM = 10;
	int COL_NUM = 5, ROW_NUM = 3;
	double ***Arr3D = new double **[MAX_NUM];
 
	for (int i = 0; i < MAX_NUM; i++)
	{
		Arr3D[i] = new double *[ROW_NUM];
 
		for (int j = 0; j < ROW_NUM; j++)
		{
			Arr3D[i][j] = new double[COL_NUM];
		}
	}

    从上面可以看出,多维数组的申请与多维vector的定义类似,是一层一层的申请内存的,返回的是指向指针数组的指针。

    先依次声明维度的大小,然后从最低维度开始申请内存。先申请ROW_NUM个大小为COL_NUM的数组,将指向这些数组的首地址的指针传递到大小为ROW_NUM的数组中,构成一个新的数组,以此类推,完成内存申请。

    上面的例子中得到的是一个10层,3行,5列的数组。

5. 多维动态数组内存释放

    多维动态数组的释放是从最低维度开始的。先释放掉最低维度的一维数组,然后依次释放内存,直到释放掉最高维度。就跟一层一层的拆房子一样。

for (int i = 0; i < MAX_NUM; i++)
	{
		for (int j = 0; j < ROW_NUM; j++)
		{
			delete[] Arr3D[i][j];
		}
		delete[] Arr3D[i];
	}
	delete[] Arr3D;

6. allocator类进行动态数组的内存申请

参照:https://blog.csdn.net/qq_41453285/article/details/95665652

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值