跳轉到內容

C++ 程式設計/運算子/陣列

來自華夏公益教科書,開放的書籍,開放的世界

一個陣列儲存一組大小恆定的連續塊,每個塊包含一個所選型別的數值,這些數值都儲存在同一個名稱下。陣列通常可以有效率且直觀地組織資料集合。

最簡單的理解方式就是將一個陣列看作是一個簡單的列表,每個數值都作為列表中的一個專案。透過陣列中的位置來訪問單個元素,稱為索引,也稱為下標。陣列中的每個專案都有一個從 0 到(陣列大小)-1 的索引,表示它在陣列中的位置。

陣列的優點包括:

  • 隨機訪問時間複雜度為 O(1) (大 O 符號)
  • 易於使用/移植:整合到大多數現代語言中

缺點包括:

  • 大小恆定
  • 資料型別恆定
  • 需要一個大的連續空閒塊來容納大型陣列
  • 當用作非靜態資料成員時,元素型別必須允許預設構造
  • 陣列不支援複製賦值(你不能寫 arraya = arrayb
  • 陣列不能用作標準容器的數值型別
  • 使用語法不同於標準容器
  • 陣列和繼承不能混合(派生類的陣列不是基類的陣列,但很容易被誤認為是基類的陣列)

注意
如果複雜度允許,你應該考慮使用容器(如 C++ 標準庫中的容器)。你可以使用例如std::vector它在大多數情況下與陣列一樣快,可以動態調整大小,支援迭代器,並且可以讓你像陣列一樣處理向量儲存。

在 C++11 中,std::array提供了一個固定大小的陣列,它保證與舊式陣列一樣高效,但具有一些優勢,例如可以查詢其大小,使用與其他容器相同的迭代器,並且擁有複製賦值運算子。

(現代 C 允許使用 VLA,即變長陣列,但這些在 C++ 中沒有使用,因為 C++ 已經提供了可調整大小的陣列功能,即std::vector.)

指標運算子,正如你將看到,它與陣列運算子類似。


例如,以下是一個名為 List 的整數陣列,它有 5 個元素,編號為 0 到 4。陣列的每個元素都是一個整數。與其他整數變數一樣,陣列的元素在初始化之前是未初始化的。這意味著在我們將任何內容賦值給它之前,它充滿了未知的值。(記住 C 中的基本型別不會初始化為 0。)

索引 資料
00 未指定
01 未指定
02 未指定
03 未指定
04 未指定

由於一個陣列儲存的是數值,因此在宣告陣列時必須定義儲存的數值型別和數量,以便可以分配所需的記憶體。陣列的大小必須是const大於零的整型表示式。這意味著你不能使用使用者輸入來宣告一個陣列。你需要分配記憶體(使用operator new[]),因此陣列的大小必須在編譯時已知。連續儲存方法的另一個缺點是必須存在一個足夠大的連續空閒塊來容納陣列。如果你有一個 500,000,000 個塊的陣列,每個塊長 1 位元組,那麼你需要大約 500 兆位元組的連續空間可用;這有時需要對記憶體進行碎片整理,這需要很長時間。

要宣告一個數組,你可以這樣做

int numbers[30]; // creates an array of 30 integers

或者

char letters[4]; // create an array of 4 characters

等等...

在宣告陣列時進行初始化,你可以使用

int vector[6]={0,0,1,0,0,0};

這不僅會建立一個包含 6 個 int 元素的陣列,還會將它們初始化為給定的值。

如果你在初始化陣列時提供的元素數量少於陣列的總元素數量,則剩餘的元素將被設定為預設值 - 對於數字來說是零。

int vector[6]={0,0,1}; // this is the same as the example above

如果你在宣告陣列時完全初始化了它,則可以允許編譯器計算陣列的大小

int vector[]={0,0,1,0,0,0};  // the compiler can see that there are 6 elements
賦值和訪問資料
[編輯 | 編輯原始碼]

你可以使用陣列的名稱後跟索引來給陣列賦值。

例如,要將數字 200 賦值給陣列中索引為 2 的元素

 
List[2] = 200;

將得到

索引 資料
00 未指定
01 未指定
02 200
03 未指定
04 未指定

你可以用相同的方式訪問陣列中元素的資料。

std::cout << List[2] << std::endl;

這將列印 200。

基本上,運算元組中的單個元素與操作普通變數沒有什麼不同。

正如你所看到的,訪問儲存在陣列中的值非常容易。再看另一個例子

int x;
x = vector[2];

上面的宣告將賦值x變數vector中索引為 2 的儲存的值,即 1。

陣列的索引從 0 開始,而不是從 1 開始。上面陣列的第一個元素是vector[0]. 陣列中最後一個值的索引是陣列的大小減 1。在上面的例子中,下標從 0 到 5。C++ 不會對陣列訪問進行邊界檢查。編譯器不會抱怨以下程式碼

char y;
int z = 9;
char vector[6] = { 1, 2, 3, 4, 5, 6 };
  
// examples of accessing outside the array. A compile error is not raised
y = vector[15];
y = vector[-4];
y = vector[z];

在程式執行期間,陣列訪問越界並不總是會導致執行時錯誤。你的程式可能在從vector[-1]中檢索值後仍然愉快地繼續執行。為了緩解索引問題,sizeof 表示式通常在編寫處理陣列的迴圈時使用。

int ix;
short anArray[]= { 3, 6, 9, 12, 15 };
  
for (ix=0; ix< (sizeof(anArray)/sizeof(short)); ++ix) {
  DoSomethingWith( anArray[ix] );
}

請注意,在上面的示例中,陣列的大小沒有明確指定。編譯器知道它的大小是 5,因為初始化列表中有五個值。在列表中新增一個額外的值會導致它的大小變為 6,並且由於sizeof 表示式位於for 迴圈中,程式碼會自動適應這種變化。

多維陣列
[編輯 | 編輯原始碼]

你也可以使用多維陣列。最簡單的型別是二維陣列。這將建立一個矩形陣列 - 每一行都有相同數量的列。要獲取一個包含 3 行 5 列的 char 陣列,我們可以寫...

char two_d[3][5];

要訪問/修改此陣列中的值,我們需要兩個下標

char ch;
ch = two_d[2][4];

或者

two_d[0][0] = 'x';

還有一些奇怪的表示法

int a[100];
int i = 0;
if (a[i]==i[a])
  printf("Hello World!\n");

a[i]i[a]指向同一個位置。在瞭解指標之後,你會更好地理解這一點。

要獲取一個陣列大小不同,你必須使用realloc, malloc, memcpy等顯式地處理記憶體。

為什麼從 0 開始?
[編輯 | 編輯原始碼]

大多數程式語言從 0 開始對陣列進行編號。這在陣列與指向陣列第一個元素的指標可以互換使用的語言中很有用。在 C++ 中,陣列中元素的地址可以從 (第一個元素的地址) + i 計算得出,其中 i 是從 0 開始的索引 (a[1] == *(a + 1))。請注意,這裡的 “(第一個元素的地址) + i” 不是對數字的字面加法。不同型別的資料具有不同的大小,編譯器會正確地考慮這一點。因此,如果索引從 0 開始,指標運算會更簡單。

為什麼陣列索引沒有邊界檢查?
[編輯 | 編輯原始碼]

C++ 允許但不要求邊界檢查實現,在實踐中很少或根本沒有進行檢查。它會影響儲存需求(需要“胖指標”)並影響執行時效能。但是,std::vector模板類,我們之前提到了它,我們將在後面詳細介紹(一個模板類容器,它代表一個數組,它提供了at()方法),它確實執行邊界檢查。同樣,在許多實現中,標準容器在除錯模式下包含了非常完整的邊界檢查。它們可能不支援這些檢查,因為任何容器類相對於內建陣列的效能下降都可能阻止程式設計師從陣列遷移到更現代、更安全的容器類。

注意
一些編譯器或外部工具可以幫助檢測語言規範之外的問題,即使是自動化的方式。有關更多資訊,請參見有關除錯的部分。

華夏公益教科書