iOS開發系列--C語言之儲存方式和作用域

KenshinCui發表於2014-07-18

概述

基本上每種語言都要討論這個話題,C語言也不例外,因為只有你完全瞭解每個變數或函式儲存方式、作用範圍和銷燬時間才可能正確的使用這門語言。今天將著重介紹C語言中變數作用範圍、儲存方式、生命週期、作用域和可訪問性。

  1. 變數作用範圍
  2. 儲存方式
  3. 可訪問性

變數作用範圍

在C語言中變數從作用範圍包括全域性變數和區域性變數。全域性變數在定義之後所有的函式中均可以使用,只要前面的程式碼修改了,那麼後面的程式碼中再使用就是修改後的值;區域性變數的作用範圍一般在一個函式內部(通常在一對大括號{}內),外面的程式無法訪問它,它卻可以訪問外面的變數。

//
//  main.c
//  ScopeAndLifeCycle
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

int a=1;
void changeValue(){
    a=2;
    printf("a=%d\n",a);
}
int main(int argc, const char * argv[]) {
    int b=1;
    changeValue(); //結果:a=2
    printf("a=%d,b=%d\n",a,b); //結果:a=2,b=1 ,因為changeValue修改了這個全域性變數
    return 0;
}

變數儲存方式

C語言的強大之處在於它能直接操作記憶體(指標),但是要完全熟悉它的操作方式我們必須要弄清它的儲存方式。儲存變數的位置分為:普通記憶體(靜態儲存區)、執行時堆疊(動態儲存區)、硬體暫存器(動態儲存區),當然這幾種儲存的效率是從低到高的。而根據儲存位置的不同在C語言中又可以將變數依次分為:靜態變數、自動變數、暫存器變數。

靜態變數

首先說一下儲存在普通記憶體中的靜態變數,全域性變數和使用static宣告的區域性變數都是靜態變數,在系統執行過程中只初始化一次(在下面的例子中雖然變數b是區域性變數,在外部無法訪問,但是他的生命週期一直延續到程式結束,而變數c則在第一次執行完就釋放,第二次執行時重新建立)。

//
//  2.1.c
//  ScopeAndLifeCycle
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

int a=1; //全域性變數儲存在靜態記憶體中,只初始化一次

void showMessage(){
    static int b=1; //靜態變數儲存在靜態記憶體中,第二次呼叫不會再進行初始化
    int c=1;
    ++b;
    a+=2;
    printf("a=%d,b=%d,c=%d\n",a,b,c);
}

int main(int argc, const char * argv[]) {
    showMessage(); //結果:a=3,b=2,c=1
    showMessage(); //結果:a=5,b=3,c=1
    return 0;
}

自動變數

被關鍵字auto修飾的區域性變數是自動變數,但是auto關鍵字可以省略,因此可以得出結論:所有沒有被static修飾的區域性變數都是自動變數。

//
//  1.3.c
//  ScopeAndLifeCycle
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>
#include <stdlib.h>

int main(int argc, const char * argv[]) {
    int a=1;
    auto int b=2;
    printf("a=%d,b=%d\n",a,b); //結果:a=1,b=2 ,a和b都是自動變數,auto可以省略
    
    //需要注意的是,上面的自動變數是儲存在棧中,其實還可以儲存到堆中
    char c[]="hello,world!";
    long len=strlen(c)*sizeof(char)+1;//之所以加1是因為字串後面預設有一個\0空操作符不計算在長度內
    char *p=NULL;//可以直接寫成:char *p;
    p=(char *)malloc(len);//分配指定的位元組存放c中字串,注意由於malloc預設返回“void *”需要轉化
    memset(p,0,len);//清空指向記憶體中的儲存內容,因為分配的記憶體是隨機的,如果不清空可能會因為垃圾資料產生不必要的麻煩
    strcpy(p,c);
    printf("p=%s\n",p);//結果:p=hello,world!
    free(p);//釋放分配的空間
    p=NULL;//注意讓p指向空,否則p將會是一個儲存一個無用地址的野指標
    
    
    return 0;
}

當然儲存自動變數的棧和堆其實是兩個完全不同的空間(雖然都在執行時有效的空間內):棧一般是程式自動分配,其儲存結果類似於資料結構中的棧,先進後出,程式結束時由編譯器自動釋放;而堆則是開發人員手動編碼分配,如果不進行手動釋放就只有等到程式執行完作業系統回收,其儲存結構類似於連結串列。在上面的程式碼中p變數同樣是一個自動變數,同樣可以使用auto修飾,只是它所指向的內容放在堆上(p本身存放在棧上)。

這裡說明幾點:malloc分配的空間在邏輯上連續,物理上未必連續;p必須手動釋放,否則直到程式執行結束它佔用的記憶體將一直被佔用;釋放p的過程只是把p指向的空間釋放掉,p中存放的地址並未釋放,需要手動設定為NULL,否則這將是一個無用的野指標;

暫存器變數

預設情況下無論是自動變數還是靜態變數它們都在記憶體中,不同之處就是自動變數放在一塊執行時分配的特殊記憶體中。但是暫存器變數卻是在硬體暫存器中,從物理上來說它和記憶體處在兩個完全不同的硬體中。大家都是知道暫存器儲存空間很小,但是它的效率很高,那麼合理使用暫存器變數就相當重要了。什麼是暫存器變數呢?使用register修飾的int或char型別的非靜態區域性變數是暫存器變數。沒錯,需要三個條件支撐:register修飾、必須是int或char型別、必須是非靜態區域性變數。

除了儲存位置不同外,暫存器變數完全符合自動變數的條件,因此它的生命週期其實是和自動變數完全一樣的,當函式執行結束後它就會被自動釋放。由於暫存器空間珍貴,因此我們需要合理使用暫存器變數,只有訪問度很高的變數我們才考慮使用暫存器變數,如果過多的定義暫存器變數,當暫存器空間不夠用時會自動轉化為自動變數。

//
//  1.3.c
//  ScopeAndLifeCycle
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

int main(int argc, const char * argv[]) {
    register int a=1;
    printf("a=%d\n",a);
    return 0;
}

 

上面我們說到變數的儲存型別,其實在C語言中還有兩種儲存型別:常量儲存區和程式碼儲存區,分別用於儲存字串常量、使用const修飾的全域性變數以及二進位制函式程式碼。

可訪問性

在C語言中沒有其他高階語言public、private等修飾符,來限定變數和函式的有效範圍,但是卻有兩個類似的關鍵字能達到類似的效果:extern和static。

extern

extern作用於變數

我們知道在C語言中變數的定義順序是有嚴格要求的,要使用變數則必須在使用之前定義,extern用於宣告一個已經存在的變數,這樣一來即使在後面定義一個變數只要前面宣告瞭,也同樣可以使用。具體的細節通過下面的程式碼相信大家都可以看明白:

//
//  2.1.c
//  ScopeAndLifeCycle
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

//如果在main函式下面定義了一個變數a,如果在main上面不進行宣告是無法在main中使用a的;
//同樣如果只進行了extern宣告不進行定義一樣會報錯,因為extern並不負責定義變數a而僅僅是宣告一個已經定義過的變數;
//當然如果說在main上面定義int a;去掉main下面的定義同樣是可以的,相當於在上面定義,但如果兩個地方都定義a的話(main上面的extern去掉),則程式認為上面的定義是宣告,只是省略了extern關鍵字;

//第一種情況,在下面定義,不進行宣告,報錯
int main(int argc, const char * argv[]) {
    
    printf("a=%d\n",a);
    return 0;
}

int a;

//第二種情況,在上面定義,正確
int a;
int main(int argc, const char * argv[]) {
    
    printf("a=%d\n",a);
    return 0;
}


//第三種情況,在下面定義在上面宣告,正確
extern int a;
int main(int argc, const char * argv[]) {
    
    printf("a=%d\n",a);
    return 0;
}

int a;


//第四種情況,只在上面宣告(編譯時沒有問題,因為上面的宣告騙過了編譯器,但執行時報錯,因為extern只能宣告一個已經定義的變數),錯誤
extern int a;
int main(int argc, const char * argv[]) {
    
    printf("a=%d\n",a);
    return 0;
}


//第五種情況,上下同時定義(這種方式是正確的,因為上面的定義會被認為是省略了extern的宣告),正確
int a;
int main(int argc, const char * argv[]) {
    
    printf("a=%d\n",a);
    return 0;
}
int a;
//其實下面的情況也是不會出錯的
int a;
int a;
int main(int argc, const char * argv[]) {
    
    printf("a=%d\n",a);
    return 0;
}
int a;
int a;

//第六種情況,將全域性變數宣告為區域性變數,但是它的實質還是全域性變數,正確
int a;
int main(int argc, const char * argv[]) {
    extern int a;
    printf("a=%d\n",a);
    return 0;
}
int a;

//第七種情況,在函式內部重新定義一個變數a,雖然不會報錯,但是兩個a不是同一個
int a;
int main(int argc, const char * argv[]) {
    int a;
    printf("a=%d\n",a);//注意這裡輸出的a其實是內部定義的a,和函式外定義的a沒有關係
    return 0;
}
int a;

如果兩個檔案同時定義一個全域性變數,那實質上他們指的是同一個變數。從下面的例子可以看出,在main.c中修改了變數a之後message.c中的變數a值也修改了。extern

需要注意,在上面的程式碼中無論在message.h中將a定義前加上extern,還是在main.h中的a定以前加上extern結果都是一樣的,extern同樣適用。和在單檔案中一樣,不能兩個定義都新增extern,否則就沒有定義了。如果把message.c中a的定義(或宣告)去掉呢,那麼它能否訪問main.c中的全域性變數a呢,答案是否定的(這和在一個檔案中定義了一個函式在另一個檔案不宣告就直接用是類似的)。

extern作用於函式

extern作用於函式就不再是簡單的宣告函式了,而是將這個函式作為外部函式(當然還有內部函式,下面會說到),在其他檔案中也可以訪問。但是大家應該已經注意到,在上面的程式碼中message.c中的showMessage前面並沒有新增extern關鍵字,在main.c中不是照樣訪問嗎?那是因為這個關鍵字是可以省略的,預設情況下所有的函式都是外部函式。

externAndFunction

和作用於變數不同,上面main.c和message.c中的extern都可以省略,在這裡extern的作用就是定義或宣告一個外部函式。從上面可以看到在不同的檔案中可以定義同一個變數,它們被視為同一個變數,但是需要指出的是外部函式在一個程式中是不能重名的,否則會報錯。

static

static作用於變數

其實在前面的例子中我們已經看到static關鍵字在變數中的使用了,在例子中使用static定了一個區域性變數,而且我們強調static區域性變數在函式中只被初始化一次。那麼如果static作用於全域性變數是什麼效果呢?如果static作用於全域性變數它的作用就是定義一個只能在當前檔案訪問的全域性變數,相等於私有全域性變數

staticAndVarible

從上面的輸出結果可以看出message.c中的變數a和main.c中的變數a並不是同一個變數,事實上message.c中的變數a只能在message.c中使用,雖然main.c中的變數a是全域性變數但是就近原則,message.c會使用自己內部的變數a。當然,上面例子中main.c中的變數a定義成靜態全域性變數結果也是一樣的,只是這樣如果還有其他原始檔就不能使用a變數了。但是main.c中的a不能宣告成extern,因為main.c不能訪問message.c中的變數a,這樣在main.c中就沒變數a的定義了。

static作用於函式

static作用於函式和作用於變數其實是類似的,如果static作用於函式則這個函式就是內部函式,其他檔案中的程式碼不可以訪問。下面的程式碼在執行時會報錯,因為mesage.c中的showMessage()函式是私有的,在main.c中儘管進行了宣告,可以在編譯階段通過,但是在連結階段會報錯。

saticAndFunction

總結

最後做一下簡單總結一下:

  1. extern作用於變數時用於宣告一個已經定義的變數,但是並不能定義變數;使用extern你可以在其他檔案中使用全域性變數(當然此時extern可以省略);
  2. extern作用於函式時與它作用於全域性變數有點類似,宣告這個函式是外部函式,其他檔案可以訪問,但不同的是當它作用於函式時不僅可以宣告函式還可以定義函式(用在函式定義前面),不管是定義還是宣告都可以省略,C語言預設認為函式定義或宣告都是外部函式;
  3. static作用於變數時,該變數只會定義一次,以後在使用時不會重新定義,當static作用於全域性變數時說明該變數只能在當前檔案可以訪問,其他檔案中不能訪問;
  4. static作用於函式時與作用於全域性變數類似,表示宣告或定義該函式是內部函式(又叫靜態函式),在該函式所在檔案外的其他檔案中無法訪問此函式;

相關文章