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指向的數組中的元素,並釋放對應的內存。數組中的元素按照逆序進行銷燬。

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