文章程式碼顯示

2017年11月13日 星期一

《筆記》C語言 - 06:一維陣列、#define、函式與陣列、static 陣列

一維陣列

這章節會進入 C 語言中第一個門檻「陣列」,前面幾個章節都算是 C語言中很好理解的部分(區域變數、全域變數的部分有比較混亂一些,但沉下心慢慢理解會發現其實也不困難)

前文中當我們需要處理資料時都使用「變數」來存放資料。但一個變數只能代表一個資料,若需要處理數百項不同的資料時,便要使用數百個不同的變數名稱

這不但會增加變數,就連要如何命名這些變數也會造成困擾

透過「陣列」我們可以用一個陣列名稱但使用不同的索引指標(index),分別代表同性質但數值不同的變數。

Def : 陣列是一群具有相同名稱以及相同型別(type)的變數集合,在記憶體中擁有連續存放空間的集合,而陣列中每個元素都相當於一個變數

陣列本質上就跟國高中數學會學到的「矩陣」是一樣的東西(可想像為只有一行或一列的矩陣)

首先我們要定義一些名詞,假設有我們宣告一個名為 x 的一維陣列

int x[5] = { 1, 2, 3, 4, 5 };

意思是一個 "名為 x 的一維整數陣列" 。而這個陣列具有 5 個元素(elements) (表明在中括號裡)

後方用大括弧將陣列的元素成員以逗號分別填入。如此一來就完成了陣列的建立(宣告)

在計算機應用裡,我們要時時刻刻的記住起始數字是 0 ,也就是說凡是有數(ㄕㄨˇ)數(ㄕㄨˋ)的部分,都要從 0 開始算起

以我們剛剛宣告的一維陣列來說 「數字 1 代表第 0 個元素 」、「數字 2 代表第 1 個元素 」(恰好與我們的數值錯開)

註 : 在原文書中把數字 1 稱為第 1 個元素,這是比較正統的說法,但我認為這樣又要額外去記憶跟「位置編號」(索引 index ) 的區別,不如就把數字 1 稱為第 0 個元素比較省事。

接著我們學習一下要如何抓出陣列中的值來讓我們使用

#include "stdio.h"

int main(void) {

 int x[5] = { 1, 2, 3, 4, 5 };
 int x0, x1, x2, x3, x4;

 x0 = x[0];
 x1 = x[1];
 x2 = x[2];
 x3 = x[3];
 x4 = x[4];

 printf( "%d\n", x0 );
 printf( "%d\n", x1 );
 printf( "%d\n", x2 );
 printf( "%d\n", x3 );
 printf( "%d\n", x4 );

 return 0;
}



我們宣告了五個整數變數(x0~x4)來抓出陣列中的各個元素,在沒有需要宣告初始值的時候,運用第 6 行一次宣告多個變數是可被編譯器所接受的。

引用陣列的某個位置或元素我們必須指定陣列名稱,以及此元素在陣列中的位置編號(position number)

假設我們要用 x0 來儲存矩陣第 0 個元素,寫上矩陣名稱接著中括弧內填入第幾個位置編號即可

而位置編號正式名稱為索引(index),此名稱較常被我們使用。



#include "stdio.h"

int main(void) {

 int x[10];
 int i;

 printf("Print array x : \n");

 for ( i=0; i < 10; i++ ){
  x[i] = 0;
  printf( "%d\n", x[i] );
 }

 return 0;
}



我們在第 5 行的地方宣告一個「名為 x 的一維陣列」,且 "預留" 十個位置給它

所謂的預留指的是在記憶體內部先建立一個「預留」的區塊給這個一維陣列

注意宣告一個普通變量(變數)時,若沒有指定初始值則編譯器會自動給 0 ,但在陣列並不是如此

若在宣告陣列的時候我們沒有給予初始值,則陣列會被隨機填入不知名的數值,這並不是我們想要的。

我們在第 10 行的地方使用 for 迴圈來替陣列賦予初始值

#include "stdio.h"

int main(void) {

 int x[10] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
 int i;

 printf("Print array x : \n");

 for ( i=0; i < 10; i++ ){
     printf( "%d\n", x[i] );
 }

 return 0;
}

另一個更好的方法,我們在陣列宣告的時候就將每個元素的值賦予上去

或是我們可以使用另一項技巧如下

#include "stdio.h"

int main(void) {

 int x[10] = { 0 };
 int i;

 printf("Print array x : \n");

 for ( i=0; i < 10; i++ ){
  printf( "%d\n", x[i] );
 }

 return 0;
}



前面有說過,當陣列在宣告的時候沒有給予任何東西時會造成不可預期的錯誤

但神奇的是,如果我們在一個預留 10 個位置的陣列上只填入第 0 個元素的值,由結果可以看出,所有元素都變成了 0 

這是因為替陣列進行宣告時,填入值的個數小於陣列預留位置,則後面沒有被賦值的就會被自動補 0 

簡單來說就是你不能連第 0 個元素的值都不給,但如果你給了第 0 個元素的值後,它就會把剩下沒有被賦值的元素自動補 0 。

注意是補 0 ,不是補上第 0 個元素的值,我們由下方的結果來理解

#include "stdio.h"

int main(void) {

 int x[10] = { 99 };
 int i;

 printf("Print array x : \n");

 for ( i=0; i < 10; i++ ){
  printf( "%d\n", x[i] );
 }

 return 0;
}



我們指定第 0 個元素的值為 99 ,後續由編譯器幫我們補上,而編譯器就只會幫你補 0 而已。

針對一維陣列而言,在宣告陣列時「元素個數」可以被省略,會由編譯器自動幫你填上

例如在第一個程式中我們可以寫成如下

#include "stdio.h"

int main(void) {

 int x[] = { 1, 2, 3, 4, 5 };
 int x0, x1, x2, x3, x4;

 x0 = x[0];
 x1 = x[1];
 x2 = x[2];
 x3 = x[3];
 x4 = x[4];

 printf( "%d\n", x0 );
 printf( "%d\n", x1 );
 printf( "%d\n", x2 );
 printf( "%d\n", x3 );
 printf( "%d\n", x4 );

 return 0;
}

省略了宣告時,中括弧內的元素個數。

#define

接下來我們嘗試使用 #define 來建立一個 "便於替換大小" 的陣列

#include "stdio.h"

#define SIZE 20

int main(void) {

 int x[SIZE];
 int i;

 printf("Print array x : \n");

 for ( i=0; i < SIZE; i++ ){
  x[i] = i;
  printf( "%d  ", x[i] );
 }

 return 0;
}


在程式碼中第 3 行我們使用了 #define 。我們稱這個為「定義一個名為 SIZE 的符號常數」

該行程式碼會由前置編譯器在編譯之前將它代換成文字,簡單來說它就是一個 word 的取代文字功能

當我們要進行編譯前,會將程式碼中的 SIZE 全部都替換成 20。因為它實質上是一個文字取代功能,所以並不會對記憶體空間造成任何負擔,這樣的寫法有助於我們快速的更改以及定義一些參數

在原本的程式碼中我們利用陣列宣告以及 for 迴圈的配合,列印顯示出 20 個數字,倘若我們現在突然想要換成列印 35 個數字,這時就只要更改為 #define SIZE 35 即可

:使用全大寫字母來命名符號常數,可使這些常數在程式中較為醒目,算是程式設計師的一種默契




#include "stdio.h"
#include "stdlib.h"
#include "time.h"

#define SIZE 35

int main(void) {

 int rand_array[SIZE];
 int i;
 srand( time(NULL) );

 for ( i=0; i < SIZE; i++ ){
  rand_array[i] = ( rand() % 100 ) +1;
  printf( "%d  ", rand_array[i] );
 }

 return 0;
}



上方的程式碼可以建立一個「具有 SIZE 個元素的隨機陣列」,且值限制於 1~100

函式與陣列

  若我們想在 "函式呼叫" 中,把「陣列」當成某個函式的引數,只需要指定陣列名稱即可,不必加上任何中括號

  但在這個函式呼叫 "函式標頭" 以及 "函式原型" 中我們必須指明要傳入的引數是一個陣列此時要加上中括號

#include "stdio.h"

#define SIZE 10

float array_average(int array[], int size);

int main(void) {

 int my_array[SIZE] = { 1,2,3,4,5,6,7,8,9,10 };
 float average;

 average = array_average(my_array, SIZE);
 printf( "Array average : %.2f\n", average );

 return 0;
}


float array_average(int array[], int size){

 int i;
 int total = 0;
 float average;

 for( i = 0; i < size; i++){
  total = total + array[i];
 }

 average = (float)total / size;

 return average;
}


第 9 行我們宣告了一個具有 10 個元素的整數陣列,並在第 12 行呼叫函式 array_average 。該函式具有兩個輸入引數,在第 5 的函式原型標明了第一個引數要是一個矩陣,第二個引數則必須為一個整數值

在 array_average 裡我們再次宣告了 average ,此為區域變數

在前一章我們有提到過的觀念在這就派上用場了,就算我們在 main 裡面同樣宣告了 average 這個變數,但是對於 array_average 函式內部而言它是看不見的,所以我們重複宣告了相同名稱(為了讓程式設計師明白這是一個存放 "平均值" 的變量)

若不這樣做則會出現 「'average' was not declared in this scope」的錯誤

array_average 函式會用 for 迴圈一一抓取陣列中的每一個元素出來,並且將它加總後存放在 total 裡面,最後再用 array_average 裡面的區域變數 average 來存放平均值,並且回傳

程式中的第 12 行, main 裡面的區域變數 average 將會接收來自 array_average 函式的回傳值,並且最後列印顯示出來

static 陣列

#include "stdio.h"

void useLocal(void);

int main(void) {

 int x[5];/* local array*/
 printf( "x[0] in main() is %d\n", x[0] );

 useLocal();

 printf( "Now, x[0] in main() is %d\n", x[0] );

 useLocal();

 printf( "Again, x[0] in main() is %d\n", x[0] );

 return 0;
}

void useLocal(void){
 static int x[30]; /* initialized only first time useLocal is called */
 printf( "x[30] is ");
 for ( int i=0; i < 30; i++){
  printf("%d ",x[i]);
 }

 printf("\nlocal x[0] in useLocal is %d after entering useLocal\n", x[0] );
 x[0] = x[0] +1 ;
 printf("local x[0] in useLocal is %d before exiting useLocal\n", x[0] );
}



在前面我們討論過將一個區域變數宣告為 static ,則該變數在程式執行期間只會初始化一次且當區塊結束時仍舊會一直存在同樣的,我們可以將 static 用一個區域陣列的定義上

將 static 用在區域陣列的定義有一個很大的好處,假設這個陣列是一個很大的陣列(例如具有 30 個元素)

若我們定義陣列為 static 型態,這樣就不會在每次函式呼叫的時候都要重新讓電腦花時間去產生這個龐大的陣列以及指定其初始值,陣列內的元素也不會在每次離開這個函式時被清除,且宣告成 static 的陣列會自動在編譯時期就進行初始化

我們在前面有提到一般整數陣列一定要進行初始值宣告,哪怕只有第 0 個元素被賦值而已。但宣告成 static 的陣列若沒有給予初始值則編譯器會自動將陣列元素的初始值設定為零

上列的程式碼中我故意在第 7 行的地方定義了一個具有 5 個元素的整數型態陣列,但沒有給初始値。由結果可以看出陣列內的元素是一個不明意義的値。

接著我在 useLocal 函數內定義一個 static 整數型態陣列,同樣也沒有給初始値。由結果可以得知編譯器的確會幫我自動將陣列內的所有元素都賦予 0

順道一提上一章的觀念,在 uselocal 函數裡面是看不見第 7 行所定義的 x 陣列,因為不滿足區域變數的作用域條件

最後,由結果可以看出 static 陣列在離開 useLocal 函數後又再次進入 useLocal 函數,其 static 陣列的元素保留了上次的値

↓↓↓ 連結到部落格方針與索引 ↓↓↓

Blog 使用方針與索引