C語言中一些亂七八糟的用法與細節(不斷更新)

用C語言比較多,這篇是平時攢下的。有些內容在工作後可能會很常見,但是不用容易忘,所以就寫篇博客吧。


一.printf的用法

%*可以用來跳過字符,可以用於未知縮進。像下面一樣.

for(i = 1; i < 10; i++)
{
 printf("%*c\r%*c\n",  9 - abs(i - 5), '*', abs(i - 5) + 1, '*');
}

%[]可以用來讀取指定的內容,%[^]可以用來忽略指定內容(正則表達式?)

%m可以不帶參數,輸出產生的錯誤信息


二.關於define的用法

#是將後面所跟隨內容字符串化,##則是將前後內容連接起來

還有linux下各種多層宏定義。。。宏定義可謂博大精深啊。。


三.關於setjmp和longjmp

目前在linux多線程的頭文件pthreadtypes.h中包含了此函數的頭文件。

這個主要作用是用來從異常恢復的.也可以做"軟重啓".

因爲setjmp把環境變量的設置和棧的值都保存在參數env數組中,當longjmp時,根據其值來恢復setjmp時的位置.

這個env通常是一個全局變量.畢竟這是要跨越函數的.


四.#include

其實這個東西可以寫在任何的地方,你可以試試將main裏面代碼寫在某個文件中,然後在main中包含這個文件。代碼照樣運行。


五.sizeof

當sizeof一個數組名的時候,如果數組和sizeof在同一個代碼塊中,那麼返回數組長,否則返回指針大小。

(當數組名傳遞給函數的時候傳遞的僅僅是個指針)


六.volatile

防止編譯器優化,讓每次讀取數據都從內存中讀取,而不是在其他位置。可以防止部分編譯器優化。在嵌入式中和多線程中有所應用。

最近學linux發現還可以用用來描述自己定義的鎖!從而避免了編譯器直接把鎖優化了。


七.main函數的奇妙用法.

其實main函數是可以遞歸的。

並且.C文件中就一句main;也是可以編譯過的..剛好連接器可以找到對象..


八.關於||和&&

其實||的優先級比&&低。


九,如何讓返回值有多重類型?

用union定義一個結構體,那麼就可以選擇返回多種類型了.


十.爲何通常c中的指針大小爲4個字節?

一種可能是CPU的地址線只有32根.另一種是由於最大程序限制在4G=2^32B來確定的.


十一.當函數返回double值的時候是80位的.

所以在返回值賦給一個double變量的時候會損失精度.

具體的可以看http://stackoverflow.com/questions/16888621/why-does-returning-a-floating-point-value-change-its-value


十二. __cdecl關鍵字

這個關鍵字是用來定義函數參數入棧順序從右向左的.並且我在VC上測試的時候發現是先計算再入棧.這個估計用的很少吧..

剛學AT&T的32位彙編,發現其實是用pushl來將參數逐一入棧,然後彈出傳給函數的.估計加了__cdecl的話就會使用pushl的方式來傳值吧.

貌似這樣就只能是從右到左傳參數給函數啊..嗯..還需學習..


十三.內存對齊

結構體和聯合體中,比如struct{int a;char b; double c};那麼這個結構體大小爲16(32位機),不是13.這一點我經常忘掉..但是發現這個錯誤的時候又很容易想起來..


十四.位段

這個是1位1位的來分配的..(原來還以爲是按字節來的..)

這個東西剛開始覺得似乎蠻有用.但是似乎用途只有在網絡協議需要這麼斤斤計較..

百度百科上說只能是unsigned和int類型指定,但是我在dev c++上寫c程序,也可以用char類型.

並且這個後面:加的值不能超過原來的類型的大小.

考慮到存儲器的對齊和效率應該應用不多.


十五.關於union的用法

其實這個可以用來顯示一些不能見的內容..比方說將一個指針和一個int定義在一起..如果指針是不能見的,那麼可以通過以int的形式來訪問..

union a{

   point A;

   int  b;

};

如果A不能直接訪問,那麼就可以用B來訪問。也算是編譯器的漏洞吧。

我記得C++好像有一種類型指針是這樣的,不允許見到其值.但是可以用int來知道其值.

原先刷題還遇到過一個問題.VC不能用long long類型的於是想用union{char a[8];int a}.這樣來變成long long的..結果printf貌似只能識別到32位長度的大小..

所以還是老老實實去linux下編譯了...


十六.關於typedef與const聯合使用時一個值得注意的地方

typedef char* zifu;

const zifu a;

那麼a是個什麼樣的指針呢?是不能改變所指向地址的值還是不能改變自身?

這個問題我一開是也想錯了...想成用define的效果了...其實上面的定義等價於

char* const a;

也就是說不能改變指針自身,但是可以改變所指向的值..typedef與define相比會擴展一些內容.


十七.關於typedef的其他容易忘記的方法.

函數指針 typedef  int (*)(char ) a;

那麼 a c; c就是一個返回值爲int,參數爲char類型的函數指針.

避免弄成int (*)(*f)(int ,char)(char)這種一團麻亂的樣子..很容易就看錯的..

申請數組 typedef int int_shuzu[10];

那麼int_shuzu a; a就是一個數組指針了..這種很容易看不懂的..


十八。關於參數傳遞

void fun(a,b,c)

int a,b,c;

{}

你很可能沒見過這種傳值方式,不過它確實是可以編譯成功的。可以少寫幾個int~


十九.assert

C的異常處理宏。最近才發現有這個東西。這個比try--catch簡單多了。用於判斷某個條件是否滿足,不滿足的話跳轉到自己的函數上來顯示信息。用來調試程序很方便的。建議學習~


二十.關於如何把遞歸用函數指針來代替的方式.

這個方法很奇葩..感覺還是能有點用的. 

 
#include <stdio.h>
typedef int (*PFUN)(int);
PFUN ptr[2];
int end(int a){
	return 0;
}
int sum(int a){
	return a + ptr[!!a](a - 1);
}
int main(){
	int T, m;
	scanf("%d", &T);
	while(T--){
		ptr[0] = end;
		ptr[1] = sum;
		scanf("%d", &m);
		printf("%d\n", sum(m));
	}
	return 0;
}
        

這是通過函數指針加位運算來模仿遞歸...目標是求1+2+...+n..

兩次邏輯取反剛好把大於1的數轉爲1,原來爲零仍然爲零.

這個技巧就當作好玩吧~~


二十一:字符串常量

 sizeof( 'a' );你猜猜是幾?結果是4!!!!這是C裏面估計很少人注意到的差別..當然也沒什麼用就是了..(C++裏面是1,但是C++還有'aa'這種單引號裏面兩個字符的情況..結果是4)


二十二.強制轉換

這是我看一個opencv程序中發現的。你可能永遠都只將強制轉換用於賦值符號的右邊,但其實在左邊也是可以的.

就向這樣

((float*)(img->imageData + i* img->widthStep))[j]=mapp[i][j];


二十三。重定向調試

freopen("文件名","r",stdin);寫在所有從輸入流讀取信息的函數之前。

將標準輸入重定向爲文件中的內容。對於那些喜歡刷ACM的人來說,用了這個函數調試真是方便多了啊。

對於stdin這個是操作系統默認分給C程序的3個流指針之一,在linux下貌似文件描述符號0就是指stdin。當然還有1和2,分別是stdout,stderr。

三個指針定義在stdio.h中。



二十四。關於指針與int類型

其實int類型可以賦值給指針類型!!

以前一直記得是必須加強制轉換的,但是在一本書上看到居然可以直接這樣!!

只是給個warming!!這對於底層程序員是多麼方便的事情啊!!!

在dev c++和VC上測試通過。還可以換成short*,double*,long*~

  int i;
  char*p;
  i=1354;
  p = i;


二十五。關於宏函數與逗號表達式

最近看操作系統內核源碼發現的。。

其實逗號表達式和宏是一對好基友啊~不僅讓宏函數的功能更多,還可以讓宏函數帶上返回值!

並且和內聯函數一樣高效率!!

好比說交換a和b兩個值並且返回它們的和的宏函數,可以這樣寫:

#define  swap(a,b,c)   (c=a,a=b,b=c,a+b)

這樣只用保證abc三個變量都是同一類型即可,就不用寫那麼多函數了~~

是不是有點類似C++的模版啊~~~還提高了不少開發效率呢~~~

由於是宏函數,所以就無法用於下面的回調函數中了,不過宏函數本身就這麼方便和高效,這點缺點還是可以容忍的.


二十六.回調函數

原來一直用qsort,但是不知道還有回調函數這一名字.

這就有點像C++中的泛型算法了.可以把判斷的部分通過函數封裝起來.

將判斷方法通過參數的方式傳遞給函數,然後實現泛型~~

void qsort(void *base,int nelem,int width,int (*fcmp)(const void *,const void *));

注意最後一個參數就是排序的條件判斷函數,也就是回調函數。

類似的還有多線程中指定的入口函數。


二十七.關於_exit和exit的差別

_exit終止調用進程,但不關閉文件,不清除輸出緩存,也不調用出口函數。
exit函數將終止調用進程。在退出程序之前,所有文件關閉,緩衝輸出內容.

這實在其他地方直接抄來的.在linux下如果fork子進程來做點事的話可能會用到.


二十八.關於int a[0];的用法。

原來學c的時候弄過這個,但只是當作實驗。。沒想到還真的有人會用這個東西。

主要用來當作內存地址的標籤用。可以通過編譯器來記住指針地址,但是不會消耗實際內存。

只有當分配空間之後才能使用。這是得多麼節約空間纔會這麼寫啊。。。。

注意,這貌似是C99標準才支持的一個用法,並且gcc編譯的時候需要帶上-pedantic選項。


二十九.關於unsigned整數做循環變量

unsigned 的循環變量,判斷條件 < -1相當於是小於最大的數= =。這一點千萬別弄錯了。

原來寫單片機程序就被坑過..編譯器雖然會警告,但是通常都被我忽略了...

記住,只要是for中的循環變量比較,要麼全是有符號的,要麼全是無符號的!!


三十.關於爲什麼要使用二進制讀文件和寫文件

這是學python的時候曉得的。。當你的文件保存爲二進制的話,就可以用你的方法來解析文件。

如果不用二進制,那麼就得看操作系統了。所以,對於保存獨有的數據,多用二進制保存吧。話說數據庫的數據文件都是這種方式讀寫文件的吧。

而且二進制也不容被人瞭解。。好多遊戲的文件都是明文的呢。。。


三十一.sizeof(表達式)的問題

sizeof的表達式並不計算!!!只得出是什麼類型的而已!!

int a=1;

printf("%d\n",sizeof(a=2));

printf("%d\n",a);

結果是4,1

測試環境dev c++


三十二.三目運算符與if/else哪個效率高?

個人在gcc下測試,發現彙編沒有差別。不知道其他平臺是否也是這樣。當然我測試的方式可能不對。下文還會再說三目運算符的。

在CSAPP中說gcc會優化三目運算符。所以估計在某些特定情況下效率比if高點。

三十三.一種簡單的宏交換

#define  swap(a,b)   (a ^= b^= a^= b)

多麼簡潔啊~雖然以前也寫過這種異或交換,但是從來想過還能用連等啊~~


三十四.long double

這是C99標準中的新類型.在<深入理解計算機系統>裏面說是擴展浮點數.有80個二進制位.

這80位剛好對應intel的CPU中的FPU.我的ATT彙編總結中講過這個寄存器,原來以爲只是浮點返回值和浮點運算中間過程用到.

現在看來還可以當作變量用.需要注意的是在gcc下一般是會按4字節對齊的,所以不是10個字節,而是12個字節.


三十五.關於<符號

這是看CSAPP上的.說<符號對於分支跳轉指令來說預測的準確率並不是特別的好.

所以剛好想到c++中循環的條件判斷用!=的習慣.雖然C++ primer上說是因爲迭代器可能在循環中改變的原因,但是從這個來看還是養成寫!=的習慣比較好.

可能得到的效率不一定提高特別多,但是對於高併發高吞吐量的系統來說,我猜這點細微的差別還是有變化的.

還是養成寫!=的習慣吧~


三十六.關於三目運算符

一般情況下沒什麼問題,但是在某些編譯器下,對於   xp?*xp:0這種xp是NULL指針的時候就不能用了,否則會造成內存錯誤!

這時因爲編譯器對三目運算符進行了優化。先算出後面兩個表達式的結果,然後再來判斷條件。

不過我用gcc測試了一下,發現gcc不是這樣的,所以在gcc下這麼用沒什麼問題。

以後見到這種情況別奇怪就是了~


三十七.關於宏函數中使用 #和##

之前一直不曉得這有什麼實際用處,但是在描述路徑的時候很有用.

好比說你之前文件在inc/現在改成在incc/下.那麼完全可以寫一個函數用來拼接

#define  f(PATH,FILE)   # x##y

這種形式.那麼更換文件目錄的時候就很方便了.


三十八.關於定義多個同名全局變量

在同一文件中是不能定義多個同名全局變量的,但是在多個文件中就可以。

CSAPP上寫的.gcc的連接器在linking的時候額會把全局變量分爲弱和強兩種狀態.

並且優先選擇強狀態,但可能類型會存在問題

好比在A文件如下定義了x和y

void other();
int x=112233;
int y=332211;

int main(){
     prntf("x=0x%x  y=0x%x \n",x,y);
     other();
     prntf("x=0x%x  y=0x%x \n",x,y);
}
在另一個文件中也定義了x,但類型是double

double x;

void other(){
   x=-0.0;
}
然後gcc 兩個文件。有一個警告。暫時先不管。

你運行了之後會發現,y的值居然變了!

也就是說,在other中,仍然認爲x是double類型的!

double由於是8字節而int是4字節,給x賦值會覆蓋原來的y!
表示如果other函數中換成純粹的0.那麼y就是0!


三十九.restrict關鍵字

這個常常忘記是什麼意思...是C99新加的標準.讓編譯器翻譯的時候,對其內存的操作只能通過其指針來完成.方便編譯器優化.

不過C++裏面好像還沒有這個關鍵字.


四十.const與define定義一個常量的區別

const指定的是一個變量,而define相當於直接替換.

const是可以帶指定類型的,define則是默認類型的.

同時const定義的變量還可以用取地址符&來得到其值.


四十一.關於GCC內聯彙編

用一般的asm volatile()形式的話,在默認情況下是沒什麼問題的。但是如果在編譯條件中加上了-std=c99的話則不行。

具體我也不清楚怎麼搞。。沒查到什麼有用的東西。自己試過在__asm__volatile_和__asm volatile這些形式都不行。

有高手會的話還請指教!


四十二.關於GNU C的__attribute__

這個是GNU C的一個特色。可以設置函數,變量,以及類型的屬性值。通常不用管這個東西,但是有一些軟件有要求會需要用到這個。

具體我就不展開了,百度可以搜出很多內容。這裏提一個__attribute__((aligned(n))).在結構體後面加上後可以按照n字節對齊。默認按最大變量字節對齊。


四十三.關於可變參數宏的實現(va_arg).

有一部分靠的是編譯器支持"..."這種參數形式,然後保證不等長參數都可以完全入棧。實際上,根據C標準的入棧規則,參數是依次從右往左入棧的。只要順着esp寄存器來找,那麼就可以依次讀出參數的其值。需要注意的是讀出多少個字節,不然棧就亂了。所以必須在讀出的時候加上參數類型。printf實現中也是先根據字符串中的類型再讀的。我個人實測後確實如此~

測試結果如下,Linux下gcc編譯。


如圖所示,顯然可以看到printf中實現的算法是先根據字符串對應的格式來從內存中讀取的。也就是說如果類型選錯了,內容顯示一定亂了。


四十四。關於返回結構體

最近看編譯器實現方面的東西,貌似C++那種傳遞性的使用(如cout<< xxx<<<xx;這種)應該是不難實現的。然後我就上測試了下,我發現GCC下函數返回結構體是可以直接在後面加上.成員來訪問成員的。。結果如下


四十五。關於**指針和(*)[]指針

我發現很多人還是不清楚**指針和(*)[]指針的差別。第一個我就叫它雙層指針,第二個叫行指針。有人喜歡把第一個叫二維指針吧,如果這麼叫很容易暈。

實際上前者是一個指向指針的指針,而後者是一個帶長度的指針。前者可以指向後者。後者與一般指針的差別就是你p++的時候是移動多個變量長度!其他指針只移動一個變量長度。這裏的長度說的是存儲變量地址的大小。前者指向一個指針變量的地址,而後者多用於指向一個二維數組的地址。所以傳遞二維數組參數的時候,用後者!


四十六。關於數組名是指針

這裏要注意一點。對數組名取地址,那麼得到的仍然是數組的地址值!而不是想當然的二級指針。也就是說加不加取地址符,其值都一樣。但兩者+1的值不同,前者是對指針增加元素長度,而&之後則成了行指針。

int   a[50];
<pre name="code" class="cpp">printf("%d\n",a);
printf("%d\n",a+1);printf("%d\n",&a+1);


我在gcc下測試下a的地址後三位爲656,a+1爲660,&a+1則是856.

四十七。關於%模運算

其實是可以模負數的!但和數學定義不同,對於負數的模仍然帶有符號。

四十七。關於作用域(do_div)

這個函數是我在調試我的內核的時候碰見的。發現很神奇。

#define do_div(n,base) ({ \
int __res; \
__asm__("divl %4":"=a" (n),"=d" (__res):"0" (n),"1" (0),"r" (base)); \
__res; })

意思是用n除以base。然後結果存入n,餘數存入__res並返回!這個可以當函數用。在gcc下可以編譯過。而且很神奇的是,外面有對()括號!替換到相應位置後就相當於是匿名函數了!還有哪些高級語言有的機制C語言也有呢,真是越學C越覺得博大精深啊!

再次測試,在VC6.0下無法通過。。


四十八。關於函數指針取地址

很多人不明白函數名爲什麼是個指針。。其實取了地址也是函數名的值,沒有任何變化。所以別再在函數名前面加&了,看着很彆扭。。


四十九。關於do while 0

在Linux內核的宏中很常見。用法感覺也蠻多的,百度也搜得到。我簡單羅列一下我覺得比較靠譜的說法。

1.打包指令。相當於把其中的指令包起來,就可以在用的時候當作一條指令來用來了。

2.對於空的宏來說,可以避免編譯器給出warming。

3.實際上可以當作函數了。畢竟可以在{}中定義變量。現在絕大部分都用inline吧。感覺也只能在老代碼裏面看見了。


五十:關於C語言中的typeof

注意是typeof.不是sizeof.主要用於強制轉化.下面就用到了.


五十一. 關於0地址的使用方法

估計很多人就只認爲NULL地址只能賦值給空指針吧.但其實還有另外的用途.在Linux內核中,數據結構中的指針是封裝的.

我用的<Linux內核設計與實現上的例子>

struct list_head{
	struct list_head *next;
	struct list_head *pre;
};

struct fox{
	unsigned long	tail_length;
	unsigned long	weight;
	bool			is_fantastic;
	struct list_head list;
};

#define constainer_of(ptr, type, member) ({	\
	const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
	(type *)( (char *)__mptr - offsetof(type, member) );})


也就是說,不是單獨給數據結構分配個指針,而是封裝好指針,塞進數據結構.

並且可以通過0地址+類型判斷來得到其相對偏移.這樣就可以通過指針地址+便宜來得到指針中的新地址了.

當然這樣很麻煩了...注意link的位置很重要!


五十二.關於多個if和switch效率比較

switch會根據代碼生成一張表,然後根據輸入直接跳轉到相應的代碼區域..而if..else則挨個的判斷..(取自CSAPP)


五十三.關於動態庫函數重名

爲什麼動態庫可以有重名函數呢?這是一位網友問我的問題,開始覺得不太可能,因爲動態庫函數應該有符號表來唯一確定每個函數的入口。

http://bbs.csdn.net/topics/350156218 這個是我搜出來的答案。gcc下,默認是把動態庫的符號設置爲全局的。加載一個動態庫中的函數後,如果後面的動態庫有重名的那麼就會捨棄後面一個重名的函數。到這裏我突然明白namespace 的意義了。gcc下用__attribute__ ((visibility("hidden")))來隱藏其標識,默認不是static 的全部導出到動態庫。


五十三.__stdcall, __cdecl, __pascal, __fastcall,__thiscall的差別

__cdecl這個是C的標準調用標識。從右向左入棧,清棧操作由調用者來執行。並不限定參數個數與類型的完全匹配(好像實際用到的時候還是要匹配的。。)。

__stdcall這個是C++的標準調用標識。從右向左入棧,函數棧由被調用者來清除。嚴格匹配參數的調用。返回指令用的retnX,X表示棧指針移動長度。

__pascal這個簡單來說就是和_cdecl的入棧和清棧方式相反。

__fastcall這個是用兩個寄存器來保存參數。返回方式和_stdcall相同。(這個估計很)

__thiscall則是用在類成員中,可以要求把this指針放在指定的寄存器中傳遞。

C只能用cdecl。C++就可以用下面幾個了。


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