1. 程式人生 > >jyx1370979991的專欄

jyx1370979991的專欄

1、 Const:

(1)const修飾的是一個只讀變數

(2)節省空間,避免不必要的記憶體分配,提高效率

編譯器通常不為普通const只讀變數分配儲存空間,而是將它們儲存在符號表中,這使

得它成為一個編譯期間的值,沒有了儲存與讀記憶體的操作,使得它的效率也很高。

例如:

#define M 3 //巨集常量

const int N=5; //此時並未將N放入記憶體中

......

int i=N; //此時為N分配記憶體,以後不再分配!

int I=M; //預處理期間進行巨集替換,分配記憶體

int j=N; //沒有記憶體分配

int J=M; //再進行巨集替換,又一次分配記憶體!

const定義的只讀變數從彙編的角度來看,只是給出了對應的記憶體地址,而不是象#define

一樣給出的是立即數,所以,const定義的只讀變數在程式執行過程中只有一份拷貝(因為

它是全域性的只讀變數,存放在靜態區),而#define定義的巨集常量在記憶體中有若干個拷貝。

#define巨集是在預處理階段進行替換,而const修飾的只讀變數是在編譯的時候確定其值。

#define巨集沒有型別,而const修飾的只讀變數具有特定的型別。

(3)修飾一般變數,陣列,指標,函式引數,函式返回值

2、 Static

(1)    修飾全域性變數,稱為靜態全域性變數。由於全域性變數本身儲存在靜態區,因此本身就是靜態的,對全域性變數使用靜態是告訴編譯器這個變數只能在本檔案中被使用,不能被extern

(2)    修飾區域性變數,稱為靜態區域性變數。儲存在靜態區,即使函式下次呼叫也不改變其值。

(3)    修飾函式。表示這個函式的作用域僅限於本檔案。

3、 如果一個函式沒有顯式地宣告返回值,那返回值就是Int型的

在c語言中,如果一個函式沒有顯式地說明引數是void,那麼是可以使用引數的,如下所示:

#include<stdio.h>

void test(){

         printf("ok\n");

}

int main(){

         //test(3);

         return0;

}

在c++中不可以

4、 按照ANSI(AmericanNational Standards Institute)標準,不能對void指標進行演算法操作,

即下列操作都是不合法的:

void * pvoid;

pvoid++; //ANSI:錯誤

pvoid += 1; //ANSI:錯誤

ANSI標準之所以這樣認定,是因為它堅持:進行演算法操作的指標必須是確定知道其指

向資料型別大小的。也就是說必須知道記憶體目的地址的確切值。

例如:

int *pint;

pint++; //ANSI:正確

但是大名鼎鼎的GNU(GNU's Not Unix的遞迴縮寫)則不這麼認定,它指定void *的演算法

操作與char *一致。因此下列語句在GNU編譯器中皆正確:

pvoid++; //GNU:正確

pvoid += 1; //GNU:正確

在實際的程式設計中,為符合ANSI標準,並提高程式的可移植性,我們可以這樣編寫

實現同樣功能的程式碼:

void * pvoid;

(char *)pvoid++; //ANSI:正確;GNU:正確

(char *)pvoid += 1; //ANSI:錯誤;GNU:正確

GNU和ANSI還有一些區別,總體而言,GNU較ANSI更“開放”,提供了對更多語法

的支援。但是我們在真實設計時,還是應該儘可能地符合ANSI標準。

【規則1-36】如果函式的引數可以是任意型別指標,那麼應宣告其引數為void *。

典型的如記憶體操作函式memcpy和memset的函式原型分別為:

void * memcpy(void *dest, const void *src,size_t len);

void * memset ( void * buffer, int c,size_t num );

這樣,任何型別的指標都可以傳入memcpy和memset中,這也真實地體現了記憶體操作

函式的意義,因為它操作的物件僅僅是一片記憶體,而不論這片記憶體是什麼型別。如果memcpy

和memset的引數型別不是void *,而是char *,那才叫真的奇怪了!這樣的memcpy和memset

明顯不是一個“純粹的,脫離低階趣味的”函式!

下面的程式碼執行正確:

例子:memset接受任意型別指標

int IntArray_a[100];

memset (IntArray_a, 0, 100*sizeof(int) );//將IntArray_a清0

例子:memcpy接受任意型別指標

int destIntArray_a[100],srcintarray_a[100];

//將srcintarray_a拷貝給destIntArray_a

memcpy (destIntArray_a, srcintarray_a,100*sizeof(int) );

有趣的是,memcpy和memset函式返回的也是void *型別,標準庫函式的編寫者都不是一

般人。

5、 void不能代表一個真實的變數。

因為定義變數時必須分配記憶體空間,定義void型別變數,編譯器到底分配多大的記憶體呢。

下面程式碼都企圖讓void代表一個真實的變數,因此都是錯誤的程式碼:

void a; //錯誤

function(void a); //錯誤

void體現了一種抽象

6、一個定義為volatile變數是說這變數可能會被意想不到地改變,這樣,編譯器就不會去假設這個變數的值了。精確地說就是,優化器在用到這個變數時必須每次都小心地重新讀取這個變數的值,而不是使用儲存在暫存器裡的備份。下面是volatile變數的幾個例子:

1. 並行裝置的硬體暫存器(如:狀態暫存器)

2. 一箇中斷服務子程式中會訪問到的非自動變數(Non-automaticvariables)

3. 多執行緒應用中被幾個任務共享的變數

這是區分C程式設計師和嵌入式系統程式設計師的最基本的問題:嵌入式系統程式設計師經常同硬體、中斷、RTOS等等打交道,所有這些都要求使用volatile變數。不懂得volatile內容將會帶來災難。

假設被面試者正確地回答了這是問題(嗯,懷疑是否會是這樣),我將稍微深究一下,看一下這傢伙是不是真正懂得volatile完全的重要性。

1. 一個引數既可以是const還可以是volatile嗎?解釋為什麼。

2. 一個指標可以是volatile 嗎?解釋為什麼。

3. 下面的函式被用來計算某個整數的平方,它能實現預期設計目標嗎?如果不能,試回答存在什麼問題:

1

2

3

4

intsquare(volatileint*ptr)

{

return*ptr**ptr;

}

下面是答案:

1. 是的。一個例子是隻讀的狀態暫存器。它是volatile因為它可能被意想不到地改變。它是const因為程式不應該試圖去修改它。

2. 是的。儘管這並不很常見。一個例子是當一箇中斷服務子程式修改一個指向一個buffer指標時。

3. 這段程式碼是個惡作劇。這段程式碼的目的是用來返指標*ptr指向值的平方,但是,由於*ptr指向一個volatile型引數,編譯器將產生類似下面的程式碼:

1

2

3

4

5

6

7

intsquare(volatileint*ptr)

{

inta,b;

a=*ptr;

b=*ptr;

returna*b;

}

由於*ptr的值可能在兩次取值語句之間發生改變,因此ab可能是不同的。結果,這段程式碼可能返回的不是你所期望的平方值!正確的程式碼如下:

1

2

3

4

5

6

longsquare(volatileint*ptr)

{

inta;

a=*ptr;

returna*a;

}

7、 大小端問題值得注意:跟處理器有關,可以使用程式判定。

8、 enum

  在編譯階段確定其值

9、 const修飾的只讀變數不能用來作為定義陣列的維數,

也不能放在case關鍵字後面。

1、 Strlen和sizeof的區別

Strlen是一個函式,sizeof是一個運算子。

1、 sizeof(...)是運算子,在標頭檔案中typedefunsigned int,其值在編譯時即計算好了,引數可以是陣列、指標、型別、物件、函式等。它的功能是:獲得保證能容納實現所建立的最大物件的位元組大小。由於在編譯時計算,因此sizeof不能用來返回動態分配的記憶體空間的大小。實際上,用sizeof來返回型別以及靜態分配的物件、結構或陣列所佔的空間,返回值跟物件、結構、陣列所儲存的內容沒有關係。具體而言,當引數分別如下時,sizeof返回的值表示的含義如下:陣列——編譯時分配的陣列空間大小;指標——儲存該指標所用的空間大小(儲存該指標的地址的長度,是長整型,應該為4);型別——該型別所佔的空間大小;物件——物件的實際佔用空間大小;函式——函式的返回型別所佔的空間大小。函式的返回型別不能是void

2、 strlen(...)是函式,要在執行時才能計算。引數必須是字元型指標(char*)。當陣列名作為引數傳入時,實際上陣列就退化成指標了。它的功能是:返回字串的長度。該字串可能是自己定義的,也可能是記憶體中隨機的,該函式實際完成的功能是從代表該字串的第一個地址開始遍歷,直到遇到結束符NULL。返回的長度大小不包括NULL

3、 實際例子:

char arr[10] = "What?";
             int len_one = strlen(arr);

             int len_two = sizeof(arr); 
             cout << len_one << " and " <<len_two << endl; 

返回值為5 and 10,因為strlen計算的是字串已經用掉的長度,因此應該為5,而sizeof返回的是獲得保證能容納實現所建立的最大物件的位元組大小,這就表示的是這個陣列的長度為10

char*t1[20];

   char (*t2)[20];

   printf("%d%d\n0",sizeof(t1),sizeof(t2));

返回值為80和4。*t1[20]是一個指標陣列,本質上是一個數組,他表示一個長為20的陣列,陣列的每一位是一個指標,因此sizeof(t1)相當於在求一個數組的長度,而這個陣列每一位所佔的空間是一個指標的大小為4,因此總大小為80;(*t2)[20]是一個數組指標,本質上是一個指標,每個指標下面有20個空間大小,因此sizeof(t2)相當於t2[0]是一個指標,因此所佔的大小為4。

2、 如何計算結構體的大小

首先需要明確,在c中,空結構體

structpoint{};

所佔的大小為0,在c++中所佔的大小為1。

structpoint{

   int num;

   char k;

   int c;

};

structpoint p;

   printf("%d\n",sizeof(p));

返回結果為12。這裡我們先要明確一點:如何計算結構體的大小。運算子sizeof可以計算出給定型別的大小,對於32位系統來說,

sizeof(char) = 1; sizeof(int) = 4

基本資料型別的大小很好計算,我們來看一下如何計算構造資料型別的大小。

    C語言中的構造資料型別有三種:陣列、結構體和共用體。

陣列是相同型別的元素的集合,只要會計算單個元素的大小,整個陣列所佔空間等於基礎元素大小乘上元素的個數。

結構體中的成員可以是不同的資料型別,成員按照定義時的順序依次儲存在連續的記憶體空間。和陣列不一樣的是,結構體的大小不是所有成員大小簡單的相加,需要考慮到系統在儲存結構體變數時的地址對齊問題。看下面這樣的一個結構體:

       structstu1

       {

 int    i;

       char c;

       int j;

       }

先介紹一個相關的概念——偏移量。偏移量指的是結構體變數中成員的地址和結構體變數地址的差。結構體大小等於最後一個成員的偏移量加上最後一個成員的大小。顯然,結構體變數中第一個成員的地址就是結構體變數的首地址。因此,第一個成員i的偏移量為0。第二個成員c的偏移量是第一個成員的偏移量加上第一個成員的大小(0+4,其值為4;第三個成員j的偏移量是第二個成員的偏移量加上第二個成員的大小(4+1,其值為5

實際上,由於儲存變數時地址對齊的要求,編譯器在編譯程式時會遵循兩條原則:

一、結構體變數中成員的偏移量必須是成員大小的整數倍(0被認為是任何數的整數倍)

二、結構體大小必須是所有成員大小的整數倍。

對照第一條,上面的例子中前兩個成員的偏移量都滿足要求,但第三個成員的偏移量為5,並不是自身(int)大小的整數倍。編譯器在處理時會在第二個成員後面補上3個空位元組,使得第三個成員的偏移量變成8

對照第二條,結構體大小等於最後一個成員的偏移量加上其大小,上面的例子中計算出來的大小為12,滿足要求。

再看一個滿足第一條,不滿足第二條的情況

struct stu2

       {

       int   k;

       short t;

}

成員k的偏移量為0;成員t的偏移量為4,都不需要調整。但計算出來的大小為6,顯然不是成員k大小的整數倍。因此,編譯器會在成員t後面補上2個位元組,使得結構體的大小變成8從而滿足第二個要求。

3、 算術運算子 > 關係運算符 > 賦值運算子

因此:

X > y+2 也意味著x>(y+2)

X=y>2  也意味著 x=(y>2)

4、 在c語言中所有的輸入實際上是一個輸入流,可以用getchar來接收。

5、 記憶體分配方式:

記憶體分配方式有三種:1從靜態儲存區域分配。記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式的整個執行期間都存在。例如全域性變數,static變數。2在棧上建立。在執行函式時,函式內區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限。(在函式中不要返回棧記憶體,但可以返回動態分配的記憶體)。3從堆上分配,亦稱動態記憶體分配。程式在執行的時候用malloc new 申請任意多少的記憶體,程式設計師自己負責在何時用free delete釋放記憶體。動態記憶體的生存期由我們決定,使用非常靈活,但問題也最多。

例子:1

voidGetMemory(char*p)
{
     p = (char *)malloc(100);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
}
請問執行Test 函式會有什麼樣的結果?答:程式崩潰(段錯誤)。因為GetMemory並不能傳遞動態記憶體,Test函式中的str 一直都是 NULLstrcpy(str,"helloworld");將使程式崩潰。

2
char *GetMemory(void)
{
    char p = "hello world";
    return p;
}
void Test(void)
{
    char *str = NULL;
    str = GetMemory();
    printf(str);
}
請問執行Test 函式會有什麼樣的結果?答:可能是亂碼。因為GetMemory返回的是指向棧記憶體的指標,該指標的地址不是 NULL,但其原現的內容已經被清除,新內容不可知。

3
void GetMemory2(char **p, int num)
{
    *p = (char *)malloc(num);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}
請問執行Test 函式會有什麼樣的結果?答:1)能夠輸出hello
2)記憶體洩漏。

4
void Test(void)
{
    char *str = (char *) malloc(100);
    strcpy(str, “hello”);
    free(str);//
沒有將str置為NULL
if(str != NULL)
{
    strcpy(str, “world”);
    printf(str);
}
}
請問執行Test 函式會有什麼樣的結果?答:篡改動態記憶體區的內容,後果難以預料,非常危險。因為free(str);之後,str 成為野指標,
if(str != NULL)
語句不起作用。

6、 陣列名實際上是該陣列首元素的地址,因此可以這麼定義:

Char *p,a[10];       p = a;

但是和陣列不同,一個結構的名字不是該結構的地址,因此必須使用&,例如:

Structguy *him, bar;       him = &bar;

7、 複雜的型別判斷中需要記住:

1.    表示一個數組的[ ]和表示一個函式的()具有同樣的優先順序,這個優先順序高於間接運算子 *的優先順序。

2.    [ ]()都是從左往右進行結合的。

所以,如下所示:

Int * risks[10]: 具有10個元素的陣列,每個元素是一個指向Int的指標。

Int (*risks)[10]: 一個指標,指向具有10個元素的陣列

Int *oof[3][4]: 一個3*4的陣列,每個元素是一個指向int的指標。

Int (*uuf)[3][4]: 一個指標,指向3*4int陣列

Int (*uuf[3])[4]: 一個具有3個元素的陣列,每個元素是一個指向具有4個元素的int陣列的指標

Typedef char(* frptc())[5]: frptc是一個函式,該函式返回一個指向含有5個元素的char陣列的指標

8、 函式指標:void (*pf)( );

指標函式:void *pf();

使用函式名的所有四種方法:

定義函式;宣告函式;呼叫函式;作為指標

9、 Register關鍵字

register:這個關鍵字請求編譯器儘可能的將變數存在CPU內部暫存器中,而不是通過記憶體定址訪問,以提高效率注意是儘可能,不是絕對。你想想,一個CPU 的暫存器也就那麼幾個或幾十個,你要是定義了很多很多register 變數,它累死也可能不能全部把這些變數放入暫存器吧,輪也可能輪不到你。

一、皇帝身邊的小太監----暫存器

不知道什麼是暫存器?那見過太監沒有?沒有?其實我也沒有。沒見過不要緊,見過就麻煩大了。^_^,大家都看過古裝戲,那些皇帝們要閱讀奏章的時候,大臣總是先將奏章交給皇帝旁邊的小太監,小太監呢再交給皇帝同志處理。這個小太監只是個中轉站,並無別的功能。好,那我們再聯想到我們的CPUCPU 不就是我們的皇帝同志麼?大臣就相當於我們的記憶體,資料從他這拿出來。那小太監就是我們的暫存器了(這裡先不考慮CPU的快取記憶體區)。資料從記憶體裡拿出來先放到暫存器,然後CPU 再從暫存器裡讀取資料來處理,處理完後同樣把資料通過暫存器存放到記憶體裡,CPU 不直接和記憶體打交道。這裡要說明的一點是:小太監是主動的從大臣手裡接過奏章,然後主動的交給皇帝同志,但暫存器沒這麼自覺,它從不主動幹什麼事。一個皇帝可能有好些小太監,那麼一個CPU也可以有很多暫存器,不同型號的CPU 擁有暫存器的數量不一樣。為啥要這麼麻煩啊?速度!就是因為速度。暫存器其實就是一塊一塊小的儲存空間,只不過其存取速度要比記憶體快得多。進水樓臺先得月嘛,它離CPU很近,CPU 一伸手就拿到資料了,比在那麼大的一塊記憶體裡去尋找某個地址上的資料是不是快多了?那有人問既然它速度那麼快,那我們的記憶體硬碟都改成暫存器得了唄。我要說的是:你真有錢!

二、舉例

register修飾符暗示編譯程式相應的變數將被頻繁地使用,如果可能的話,應將其儲存在CPU的暫存器中,以加快其儲存速度。例如下面的記憶體塊拷貝程式碼,

#ifdefNOSTRUCTASSIGN

memcpy(d, s, l)

{

register char *d;

register char *s;

register int i;

while (i--)

*d++ = *s++;

}

#endif

三、使用register 修飾符的注意點

    但是使用register修飾符有幾點限制。

  首先,register變數必須是能被CPU所接受的型別。這通常意味著register變數必須是一個單個的值,並且長度應該小於或者等於整型的長度不過,有些機器的暫存器也能存放浮點數。

  其次,因為register變數可能不存放在記憶體中,所以不能用“&”來獲取register變數的地址

  由於暫存器的數量有限,而且某些暫存器只能接受特定型別的資料(如指標和浮點數),因此真正起作用的register修飾符的數目和型別都依賴於執行程式的機器,而任何多餘的register修飾符都將被編譯程式所忽略。

  在某些情況下,把變數儲存在暫存器中反而會降低程式的執行速度。因為被佔用的暫存器不能再用於其它目的;或者變數被使用的次數不夠多,不足以裝入和儲存變數所帶來的額外開銷。

  早期的C編譯程式不會把變數儲存在暫存器中,除非你命令它這樣做,這時register修飾符是C語言的一種很有價值的補充。然而,隨著編譯程式設計技術的進步,在決定那些變數應該被存到暫存器中時,現在的C編譯環境能比程式設計師做出更好的決定。實際上,許多編譯程式都會忽略register修飾符,因為儘管它完全合法,但它僅僅是暗示而不是命令

10、          可變巨集和可變引數

可變引數函式實現原理:

首先在介紹可變引數表函式的設計之前,我們先來介紹一下最經典的可變引數表printf函式的實現原理。一、printf函式的實現原理C/C++中,對函式引數的掃描是從後向前的。C/C++的函式引數是通過壓入堆疊的方式來給函式傳引數的(堆疊是一種先進後出的資料結構),最先壓入的引數最後出來,在計算機的記憶體中,資料有2塊,一塊是堆,一塊是棧(函式引數及區域性變數在這裡),而棧是從記憶體的高地址向低地址生長的,控制生長的就是堆疊指標了,最先壓入的引數是在最上面,就是說在所有引數的最後面,最後壓入的引數在最下面,結構上看起來是第一個,所以最後壓入的引數總是能夠被函式找到,因為它就在堆疊指標的上方。printf的第一個被找到的引數就是那個字元指標,就是被雙引號括起來的那一部分,函式通過判斷字串裡控制引數的個數來判斷引數個數及資料型別,通過這些就可算出資料需要的堆疊指標的偏移量了,下面給出printf("%d,%d",a,b);(其中ab都是int型的)的彙編程式碼

1.  .section  

2.  .data  

3.  string out = "%d,%d"  

4.  push b  

5.  push a  

6.  push $out  

7.  call printf  

1.  .section  

2.  .data  

3.  string out = "%d,%d"  

4.  push b  

5.  push a  

6.  push $out  

7.  call printf  

你會看到,引數是最後的先壓入棧中,最先的後壓入棧中,引數控制的那個字串常量是最後被壓入的,所以這個常量總是能被找到的。二、可變引數表函式的設計標準庫提供的一些引數的數目可以有變化的函式。例如我們很熟悉的printf,它需要有一個格式串,還應根據需要為它提供任意多個其他引數。這種函式被稱作具有變長度引數表的函式,或簡稱為變引數函式。我們寫程式中有時也可能需要定義這種函式。要定義這類函式,就必須使用標準標頭檔案<stdarg.h>,使用該檔案提供的一套機制,並需要按照規定的定義方式工作。本節介紹這個標頭檔案提供的有關功能,它們的意義和使用,並用例子說明這類函式的定義方法。
      C
中變長實參頭檔案stdarg.h提供了一個數據型別va-list和三個巨集(va-startva-argva-end),用它們在被呼叫函式不知道引數個數和型別時對可變引數表進行測試,從而為訪問可變引數提供了方便且有效的方法。va-list是一個char型別的指標,當被呼叫函式使用一個可變引數時,它宣告一個型別為va-list的變數,該變數用來指向va-argva-end所需資訊的位置。下面給出va_listC中的原始碼:

1.  typedef char *  va_list;  

1.  typedef char *  va_list;  

 void va-start(va-list ap,lastfix)是一個巨集,它使va-list型別變數ap指向被傳遞給函式的可變引數表中的第一個引數,在第一次呼叫va-argva-end之前,必須首先呼叫該巨集。va-start的第二個引數lastfix是傳遞給被呼叫函式的最後一個固定引數的識別符號。va-start使ap只指向lastfix之外的可變引數表中的第一個引數,很明顯它先得到第一個引數記憶體地址,然後又加上這個引數的記憶體大小,就是下個引數的記憶體地址了。下面給出va_startC中的原始碼:

1.  #define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )   

2.  #define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )   //得到可變引數中第一個引數的首地址  

1.  #define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )  

2.  #define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )   //得到可變引數中第一個引數的首地址  

type va-arg(va-list ap,type)也是一個巨集,其使用有雙重目的,第一個是返回ap所指物件的值,第二個是修改引數指標ap使其增加以指向表中下一個引數。va-arg的第二個引數提供了修改引數指標所必需的資訊。在第一次使用va-arg時,它返回可變引數表中的第一個引數,後續的呼叫都返回表中的下一個引數,下面給出va_argC中的原始碼:

1.  #define va_arg(ap,type)    ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )    //將引數轉換成需要的型別,並使ap指向下一個引數  

1.  #define va_arg(ap,type)    ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )    //將引數轉換成需要的型別,並使ap指向下一個引數  

在使用va-arg時,要注意第二個引數所用型別名應與傳遞到堆疊的引數的位元組數對應,以保證能對不同型別的可變引數進行正確地定址,比如實參依次為char型、char * 型、int型和float型時,在va-arg中它們的型別則應分別為intchar *intdouble.
     void va-end(va-list ap)
也是一個巨集,該巨集用於被呼叫函式完成正常返回,功能就是把指標ap賦值為0,使它不指向記憶體的變數。下面給出va_endC中的原始碼:

1.  #define va_end(ap)      ( ap = (va_list)0 )  

1.  #define va_end(ap)      ( ap = (va_list)0 )  

 va-end必須在va-arg讀完所有引數後再呼叫,否則會產生意想不到的後果。特別地,當可變引數表函式在程式執行過程中不止一次被呼叫時,在函式體每次處理完可變引數表之後必須呼叫一次va-end,以保證正確地恢復棧。一個變引數函式至少需要有一個普通引數,其普通引數可以具有任何型別。在函式定義中,這種函式的最後一個普通引數除了一般的用途之外,還有其他特殊用途。下面從一個例子開始說明有關的問題。假設我們想定義一個函式sum,它可以用任意多個整數型別的表示式作為引數進行呼叫,希望sum能求出這些引數的和。這時我們應該將sum定義為一個只有一個普通引數,並具有變長度引數表的函式,這個函式的頭部應該是(函式原型與此類似):
int sum(int n, ...)
我們實際上要求在函式呼叫時,從第一個引數n得到被求和的表示式個數,從其餘引數得到被求和的表示式。在引數表最後連續寫三個圓點符號,說明這個函式具有可變數目的引數。凡引數表具有這種形式(最後寫三個圓點),就表示定義的是一個變引數函式。注意,這樣的三個圓點只能放在引數表最後,在所有普通引數之後。下面假設函式sum裡所用的va_list型別的變數的名字是vap。在能夠用vap訪問實際引數之前,必須首先用巨集a_start對這個變數進行初始化。巨集va_start的型別特徵可以大致描述為:va_start(va_list vap, 最後一個普通引數)在函式sum裡對vap初始化的語句應當寫為:

va_start(vap, n); 相當於  char*vap= (char *)&n + sizeof(int);此時vap正好指向n後面的可變引數表中的第一個引數。

在完成這個初始化之後,我們就可以通過另一個巨集va_arg訪問函式呼叫的各個實際引數了。巨集va_arg的型別特徵可以大致地描述為:型別 va_arg(va_list vap, 型別名)在呼叫巨集va_arg時必須提供有關實參的實際型別,這一型別也將成為這個巨集呼叫的返回值型別。對va_arg的呼叫不僅返回了一個實際引數的值(當前實際引數的值),同時還完成了某種更新操作,使對這個巨集va_arg的下次呼叫能得到下一個實際引數。對於我們的例子,其中對巨集va_arg的一次呼叫應當寫為:v = va_arg(vap, int);這裡假定v是一個有定義的int型別變數。在變引數函式的定義裡,函式退出之前必須做一次結束動作。這個動作通過對區域性的va_list變數呼叫巨集va_end完成。這個巨集的型別特徵大致是:
void va_end(va_list vap);
三、棧中引數分佈以及巨集使用後的指標變化說明如下:下面是函式sum的完整定義,從中可以看到各有關部分的寫法:

1.  #include<iostream>   

2.  using namespace std;  

3.  #include<stdarg.h>   

4.    

5.