從可變參數列表解析到cpu內存對齊問題

可變參數列表,簡單的理解一下就是該函數可以接受1個以上的任意多個參數(參數個數不確定)

剛開始我聽到這個概念就覺得好神奇啊,從來沒聽過這麼神奇的函數!

果然,打臉總是分分鐘就會來的。啥叫沒聽說過,我朋友告訴我,學習c語言我寫的第一個程序就用到了可變參數列表的概念

printf("hello world!");

沒錯,printf()函數就是一個典型的例子,讓我們深度剖析一下。。。


對printf的定義是這樣的,他的形參有一個這樣的東西不知道大家有沒有注意到:...(三個點)--> 這就表示該函數的參數列表是可變的,顧名思義就是傳的參數可以是任意多個。

那既然是任意爲什麼還要有這個參數:const char *format --> 上面已經提到過了“1個以上任意多個參數”,那你要不寫這個參數,我要說任意爲0呢,這不就沒參數了嘛。printf不就瘋掉了,這是讓我打印什麼。。。

好啦,正經一點。。。


我們平時使用printf打印時這樣寫的

int a = 10;
char b = 'A';
float c = 0.0;
printf("%d %c %f\n", a, b, c);

我寫過一篇關於棧幀的博客,我們知道,形參實例化是從右往左進行的,最接近被調用函數的參數(也就是地址最低的參數)其實是最左邊的參數 --> 那麼printf找到參數*format之後繼續尋找上面的參數,可參數的個數是任意的,它怎麼知道什麼時候就把參數找完了呢?找到參數之後他存放了多大,又應該怎麼取它的內容呢??

其實是這樣的,printf裏面有一些這樣的符號%d、%c、%f、%s、%u......這些內容就可以告訴你傳進了幾個參數,每個參數又存放在多大空間中

類似於上面的例子,*format指向“%d %c %f\n”--> 即後面還有三個參數,第一個是整型它放在format上面的四個字節的空間中、第二個是字符型它放在整型上面的一個字節的空間中、第三個是浮點型它放在了字符型上面的四個字節的空間中(我簡要畫一下臨時變量在棧中的分佈)


這樣就可以通過第一個參數確定其他所有參數的個數以及類型!!!

因此,可變參數能夠被使用,需要確定兩個信息:每個傳入參數的類型信息;一共傳入的參數個數

對上面棧的畫法以及形參實例化(所有和棧有關的),有不明白的,戳這裏:

https://blog.csdn.net/God_bless_TYY/article/details/80233081


接下來通過一個程序瞭解一下可變參數列表的概念

實現一個函數可以求任意個參數的平均值

#include <stdio.h>
#include <windows.h>

int average(int n, ...)
{
	va_list arg;
	int i = 0;
	int sum = 0;
	va_start(arg, n);
	for (i = 0; i < n; i++)
	{
		sum += va_arg(arg, int);
	}
	va_end(arg);
	return sum / n;
}

int main()
{
	int a = 1;
	int b = 2;
	int c = 3;
	int avg1 = average(2, a, c);
	int avg2 = average(3, a, b, c);
	printf("avg1 = %d\n", avg1);
	printf("avg2 = %d\n", avg2);
	system("pause");
	return 0;
}

以下是系統提供的宏定義:

1)va_list --> 用來聲明一個va_list類型的變量arg,用於訪問參數列表的不確定部分

typedef char * va_list;

2)va_start --> 對變量arg進行初始化。第一個參數是需要初始化的對象,第二個參數是省略號前最後一個有名字的參數。執行結束後可以使arg指向可變參數部分的第一個參數

#define va_start(ap, v) (ap = (va_list)&v + _INTSIZEOF(v))

含義是:先取出已知參數v的地址,再將該地址上移至第一個可變參數的位置

不知道大家有沒有注意到這個東西:_INTSIZEOF(v),在此可先簡單的理解爲sizeof(v)。至於具體的區別,我放在下面的擴充內容裏研究

3)va_arg --> 取出可變參數的值。第一個參數是變量arg,第二個參數是arg指向參數的類型。執行結束後可以取出該參數,並且將變量arg移至下一個可變參數的位置

#define va_arg(ap, t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZOF(t)))

含義是:先將ap移至需取出參數的下一個可變參數的位置,再取出需取出的參數

4)va_end --> 對變量arg置空

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

從上面的例子可以看出,對可變參數我們也是有限制的:

1)可變參數必須從頭到尾逐個訪問。如果你在訪問了幾個可變參數之後想半途終止,這是可以的。但是,你想一開始就訪問參數列表中間的參數,那是不可能的!

2)參數列表中至少有一個命名參數。如果連一個命名參數都沒有,就無法使用va_start。

3)這些宏是無法直接判斷實際存在參數的數量。

4)這些宏無法判斷每個參數的類型。

5)如果在va_arg中指定了錯誤的類型,那麼其後果是不可預測的。


擴充部分:_INTSIZEOF(n) --> 內存對齊

_INTSIZEOF(n)它的作用就是實現內存對齊問題,它也是系統提供的一個宏定義,是這麼寫的:

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

注意:cpu每次讀取內存是以整型的整數倍進行讀取的,也就是說一次讀取4字節(或者8字節、或者16字節、......)的內容。這就需要引入內存對齊的概念,當有些數據它所佔的空間不足4字節的整數倍時,那就把它放大爲4字節的整數倍(向上取整),這樣做的目的是爲了提高cpu讀取內存的效率。

換句話說,當我只有一個字節的數據時,cpu一次還是訪問四個字節的空間。因此我在上面畫的printf的棧圖,應該進行小小的修改


看!雖然臨時變量b是個字符型它的值只佔了一個字節,但仍然將它放在了四個字節的空間中,因爲cpu走一步就是四字節的整數倍


瞭解了內存對齊這個概念之後,我們再來看看這個宏是怎麼實現內存對齊的:

現在有x,y都是正整數,要把x向上取整爲y的整數倍,我現將x寫成ky+b的形式,其中0<=b<=y-1

這樣的話,當b爲0時,f(x) = ky,否則f(x) = (k+1)y

求整數倍,當b = 0時比較簡單,x/y就可以求得k。但b不爲0呢,如何通過x,y求得(k+1)?

直接用x/y得到的總是k,k+1是得不到的

那麼現在只能考慮增大分子了:(x+m) / y = (ky+b+m) / y,這個m可以使得當b=0時結果爲k,否則結果爲k+1

這個問題就等價於,m要小於y,並且要足夠大,大到能讓最小的餘數b=1加上它之後不再小於y

m<y && (m+1)>y --> m=y-1


現在回頭來看宏定義的式子,(sizeof(n) + sizeof(int) - 1)這個部分就實現了x+m

~(sizeof(int) - 1)其實就是對3按位取反:1111  1111  1111  1100

這兩個部分按位與的結果就實現了將sizeof(n)向上取整爲sizeof(int)的整數倍

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章