C語言中變參函式傳參探究

lularible發表於2021-08-11

背景引入

近期在看一本書,叫做《嵌入式C語言自我修養》,寫的內容對我幫助很大,是一本好書。在第6章,GNU C編譯器擴充套件語法精講一節,這本書給出了一些變參函式的例子:

//1.變參函式初體驗
#include<stdio.h>
void print_num(int count,...)
{
	int *args;
	args = &count + 1;
	for(int i = 0;i < count;i++)
	{
		printf("*args:%d\n",*args);
		args++;
	}
}

int main(void)
{
	print_num(5,1,2,3,4,5);
	return 0;
}

上面的程式碼很好理解:定義一個變參函式print_num,在函式內部先取得第一個引數的地址賦值給一指標,然後將指標後移,取得後面的引數並列印出來。在main函式中,傳給print_num 6個引數,按這個邏輯,應該是列印出:

*args:1
*args:2
*args:3
*args:4
*args:5

但是結果卻出人意料:

在這裡插入圖片描述

列印出的值和傳進去的值完全不相等,甚至毫無規律可言。

問題分析

上述程式碼中,是通過取首個引數的地址,並往後移動這個指標來獲得後面引數的,那麼問題很可能出在兩個地方:

  1. 指標移動的方式不正確
  2. 引數的地址排布可能不是連續的

我們一個一個來看,先暫且假定這些引數地址是連續的,且相隔一樣的距離。那麼我們就可以聚焦於指標的移動方式了。指標移動是“args++”這一行語句來控制的。筆者修改了一下書上的程式碼:

#include<stdio.h>
void print_num(int count,...)
{
	int *args;
	args = &count;
	for(int i = 0;i <= count;i++)
	{
		printf("addr:%p\n",args);
		printf("*args:%d\n",*args);
		args++;
	}
}

int main(void)
{
	print_num(5,1,2,3,4,5);
	return 0;
}

主要增加了對於每個引數的地址的列印,執行結果如下:

在這裡插入圖片描述

筆者發現這個"args++"每次往後移動4個位元組,這是因為對於"int"型指標的移動操作,是以4(sizeof(int))為基本單位的。同理,對於"char"型指標的移動操作,以1(sizeof(char))為單位。

指標大小

一個"int"型指標大小如果等於4,那麼上述對於指標移動操作就沒問題。可是"int"型指標大小真的等於4嗎?

筆者用程式碼來測試下:

#include<stdio.h>

int main()
{
	char*	charPoint;
	int*	intPoint;
	double*	doublePoint;

	struct st{
		int first;
	};

	struct st *structPoint;

	printf("sizeof(char*):%ld\n",sizeof(charPoint));
	printf("sizeof(int*):%ld\n",sizeof(intPoint));
	printf("sizeof(double*):%ld\n",sizeof(doublePoint);
	printf("sizeof(struct*):%ld\n",sizeof(structPoint));
	return 0;
}

執行結果:

在這裡插入圖片描述

可以看到,不僅"int"型指標是8位元組大小,"char"、"double"和結構體指標也都是8位元組大小。這是因為筆者電腦安裝的是64位系統。所以書上程式碼的"int"型指標自增操作不適用於筆者,筆者將其改為“args += 2”,在dev c++這個IDE中可以得到正確的結果,但在ubuntu gcc下還是不對。

引數位置排布

解決了第一個指標移動步長問題,還是得不到正確答案。筆者懷疑引數地址很可能不連續。如何看函式的引數地址資訊?方法有很多,筆者就選一種比較快捷的方式——看彙編程式碼。

在ubuntu的終端框輸入

gcc -S [原始檔]

就能得到一個帶".s"字尾的彙編程式碼檔案。

我們對比著看main函式與print_num函式中關於引數傳遞的部分:

在這裡插入圖片描述

在這裡插入圖片描述

在main函式中,各個引數被放入不同的暫存器,在print_num函式中,又從暫存器中將引數取出來放入print_num的函式堆疊中。仔細看各個引數最終被放入的堆疊位置,發現第一個引數地址和第二個引數地址差了28個位元組,而後面的引數地址之間都是差8個位元組。這也就解釋了為何之前的程式碼結果不對了。

解決問題

所以只要在第一個引數地址的基礎上加上偏移量28即可("char*"型)。

在這裡插入圖片描述

執行結果符合預期:
在這裡插入圖片描述

但是為什麼第一個引數和第二個引數間隔28位元組,筆者暫時還不清楚,盲猜需要去看gcc中編譯器的相關知識。

額外的測試

以往對於固定引數個數的普通函式的傳參,是這樣處理的:前幾個引數放入暫存器,若個數超出,則壓入函式堆疊。筆者有點好奇變參函式是否也如此,就給這個print_num傳了18個引數:

在這裡插入圖片描述

彙編程式碼如下:

在這裡插入圖片描述

這說明了變參函式的傳參規則和普通函式並無兩樣。

總結

在看書的時候,我喜歡邊看邊敲程式碼,這一次照著書上敲的程式碼執行結果不對,就有了上面的一些探究過程。如果我沒有動手實踐,以後碰到類似問題時很可能會蒙圈。所以動手實踐很有必要。

另外,書上的東西並不一定全對,並且它的正確性需要有特定的前提做保證。比如,要是我使用的是32位系統,且編譯器在處理變參函式時將引數連續壓棧,那麼書上的程式碼就是完全正確的。我們無需害怕這些坑,我們需要做的就是去找到這些前提條件,去找到問題的本質點,最後解決問題。

參考資料

《嵌入式C語言自我修養——從晶片、編譯器到作業系統》

歡迎大家轉載本人的部落格(需註明出處),本人另外還有一個個人部落格網站:lularible的個人部落格,歡迎前去瀏覽。

相關文章