C++ 数组、数组指针、指针数组、动态数组等详解



数组基础概念

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

//数组的初始化
#include <iostream>
using namespace std;

int main(){
	const unsigned sz = 3;	
	
	int a1[3] = {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] = {"i","and"};		//等价于a4[] = {"i","and",""} 
	
	//int a5[2] = {0,1,2};			//错误:初始值太多。维度是2,但是初始化的变量是3,编译报错
		
	return 0;
}



数组的列表初始化

对数组进行列表初始化时,允许忽略数组的维度。因为如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来。但是如果指明了维度,初始值的总数量不应该超出指定的大小,否则会编译报错


数组的赋值和拷贝?×

不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值
一些编译器支持数组的赋值,这就是所谓的编译器扩展。但一般来说,最好避免使用非标准特性,因为含有非标准特性的程序很可能在其他编译器上无法正常工作。

//不允许拷贝和赋值
#include <iostream>
using namespace std;

int main()
{
	int a[] = {0,1,2};
	//int a2[] = a;		//错误:不允许使用一个数组初始化另一个数组 
	//a2 =a;			//错误:不能把一个数组直接赋值给另一个数组
	return 0;
}



数组的维度

数组中元素的个数,必须大于0,编译的时候维度应该是已知的,也就是说,维度必须是一个常量表达式

//数组的维度
#include <iostream>
using namespace std;

int main()
{
	unsigned uint = 10;				//不是常量表达式
	constexpr unsigned cuint = 10;	//常量表达式 
	
	//string str[uint];				//编译报错:uint不是常量表达式
	string str[cuint];				//含有10个整数的数组 
	string *cstr[cunit];			//含有10个整型指针的数组
	
	return 0;
}



字符数组

字符数组有一种额外的初始化形式,可以通过字符串字面量对此类数组初始化 ,使用这种方式需要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其它字符一样被拷贝到字符数组中去。

//字符数组的初始化
#include <iostream>
using namespace std;

int main(){
	char a1[] = { 'C' , '+', '+'};              // 维度:3,采取的列表初始化的方式,没有空字符
	char a2[] = { 'C', '+' , '+' ,'\0'};        // 维度:4,采取的列表初始化的方式,含有显式的空字符
	char a3[] = "C++";                          // 维度:4,采取字面量初始化的方式,字符串字面值末尾还有一个空字符。即自动添加表示字符串结束的空字符。
	//const char a4[5] = "Hello";	//错误。"Hello"看起来只有5个字符,但是数组大小必须是6,其中5个位置存放字面值的内容,另外一个存放结尾处的空字符 
	
	return 0;
}



复杂数组的解读

复杂数组定义的解读方法:由名字开始由内向外解读

//复杂数组的声明
#include <iostream>
using namespace std;

int main()
{
	int *ptrs[10];			//含有10个整形指针的数组
	
	int arr[10];
	int (*parray)[10] = &arr;	//parray 指向 一个含有10个整数的数组 
	int (&arrRef)[10] = arr;	//arrRef 引用 一个含有10个整数的数组 
	int *(&array)[10] = ptrs;   //
	return 0;
}

(1)默认情况下,类型修饰符从右向左依次绑定,对于ptrs:首先我们定义的是一个大小为10的数组,它的名字是prts,然后知道数组中存放的是指向int的指针。
(2)但是对于parray来说,从左到右理解,即由内向外阅读就更合适。首先是圆括号括起来的部分,(*parray)意味着parray是一个指针,接下来观察右边(*parray)[10],可以知道parray是个指向大小为10的数组的指针,然后在观察左边,int (*parray)[10],知道数组的元素是int。这样就知道parray是一个指针,它指向一个int数组,数组中包含10个元素。
(3)int *(&array)[10] = ptrs; 首先知道array是个引用,然后观察右边知道引用的对象是一个大小为10的数组,然后观察左边,数组的元素是一个指向int的指针。这样,就得出了array就是一个含有10个int型指针的数组的引用。




数组和指针

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



数组的遍历

可以用尾后指针进行数组的遍历:

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

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

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




使用标准库函数 beginend 进行遍历:

#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;
}



多维数组

#include <iostream>
using namespace std;

int main(){
	int a[3][4];			//大小为3的数组。每个元素是含有4个整数的数组 
	int (*p)[4] = a;		//p指向含有4个整数的数组 
	
	for(auto &row:a){
		for(auto &col:row){
			cout << *col << " ";
		}
		cout << endl;
	}
	
	for (int (*p)[4] = begin(a);p != end(a);p++){
	    for (int *q = begin(*p);q != end(*p); q++){
	        cout << *q <<endl;
	    }
	}

    return 0;
}



动态数组

new和delete运算符都是一次分配/释放一个对象。
但是像vector和string都是在连续内存中保存它们的元素,因此,当容器需要重新分配内存时,必须一次性为很多元素分配内存。

通过new来分配一个对象数组
分配一个数组会得到一个元素类型的指针***。虽然我们称new T[]分配的内存为“动态数组”,当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针*。由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end。也不能用for语句来处理动态数组中的元素。

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

int main(){
	int *pa = new int[10];				//10个未初始化的int ,pa指向第一个int元素
	int *pa2 = new int[10]();			//10个值初始化为0的int
	int *pa3 = new int[10]{0,1,2,3,4,5,6,7,8,9};	//使用初始化器初始化 
	
	string *spa = new string[10];		//10个空string 
	string *spa2 = new string[10]();	//10个空string
	string *spa3 = new string[10]{"a","b","c",string(3,'x')};	//前四个使用初始化器初始化,后面的就默认进行值初始化 
	
	return 0;
}

释放动态数组
通过delete []a,销毁a指向的数组中的元素,并释放对应的内存。数组中的元素按照逆序进行销毁。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章