本文搜集整理了一些之前博客中沒有提到的,C語言不常用的特性,算是對C系列的最后一次補充。
對C語言有興趣的朋友可以瀏覽一下,查漏補缺。
變長參數列表
<stdarg.h> 頭文件定義了一些宏,當函數參數未知時去獲取函數的參數
變量:typedef ?va_list
宏:
va_start()
va_arg()
va_end()
va_list類型通過stdarg宏定義來訪問一個函數的參數表,參數列表的末尾會用省略號省略?
(va_list用來保存va_start,va_end所需信息的一種類型。為了訪問變長參數列表中的參數,必須聲明va_list類型的一個對象?)
我們通過初始化(va_start)類型為va_list的參數表指針,并通過va_arg來獲取下一個參數。
【例子:】
求任意個整數的最大值
~~~
#include <stdio.h>
#include <stdarg.h>
int maxint(int n, ...) /* 參數數量由非變長參數n直接指定 */
{
va_list ap;
int i, arg, max;
va_start(ap, n); /* ap為參數指針,首先將其初始化為最后一個具名參數, 以便va_arg獲取下一個省略號內參數 */
for (i = 0; i < n; i++) {
arg = va_arg(ap, int); /* 類型固定為int, 按照給定類型返回下一個參數 */
if (i == 0)
max = arg;
else {
if (arg > max)
max = arg;
}
}
va_end(ap);
return max;
}
void main()
{
printf("max = %d\n", maxint(5, 2, 6, 8, 11, 7));
}
~~~
可變長數組
歷史上,C語言只支持在編譯時就能確定大小的數組。程序員需要變長數組時,不得不用malloc或calloc這樣的函數為這些數組分配存儲空間,且涉及到多維數組時,不得不顯示地編碼,用行優先索引將多維數組映射到一維的數組。
**ISOC99引入了一種能力,允許數組的維度是表達式,在數組被分配的時候才計算出來**。
~~~
#include <stdio.h>
int
main(void)
{
int n, i ;
scanf("%d", &n) ;
int array[n] ;
for (; i<n; i++)
{
array[i] = i ;
}
for (i=0; i<n; i++)
{
printf("%d,", array[i]) ;
}
return 0;
}
~~~
注意:
如果你需要有著變長大小的臨時存儲,并且其生命周期在變量內部時,可考慮VLA(Variable Length Array,變長數組)。**但這有個限制:每個函數的空間不能超過數百字節**。因為C99指出邊長數組能自動存儲,它們像其他自動變量一樣受限于同一作用域。即便標準未明確規定,VLA的實現都是把內存數據放到棧中。VLA的最大長度為SIZE_MAX字節。考慮到目標平臺的棧大小,我們必須更加謹慎小心,以保證程序不會面臨棧溢出、下個內存段的數據損壞的尷尬局面。
case支持范圍取值(gcc擴展特性) MinGW編譯通過
~~~
#include <stdio.h>
int main(void)
{
int i=0;
scanf("%d", &i) ;
switch(i)
{
case 1 ... 9: putchar("0123456789"[i]);
case 'A' ... 'Z': //do something
}
return 0;
}
~~~
非局部跳轉setjmp和longjmp
在C中,goto語句是不能跨越函數的,而執行這類跳轉功能的是setjmp和longjmp**宏**。這兩個**宏**對于處理發生在深層嵌套函數調用中的出錯情況是非常有用的。
此即為:非局部跳轉。非局部指的是,這不是由普通C語言goto語句在一個函數內實施的跳轉,而是在棧上跳過若干調用幀,返回到當前函數調用路徑的某個函數中。
#include <setjmp.h>
int? **setjmp**(jmp_buf env) ;? **/*設置調轉點*/**
void **longjmp**(jmp_bufenv,? int val) ;? **/*跳轉*/**
setjmp參數env的類型是一個特殊類型jmp_buf。這一數據類型是某種形式的數組,其中存放 在調用longjmp時能用來恢復棧狀態的所有信息。因為需在另一個函數中引用env變量,所以應該將env變量定義為全局變量。
longjmp參數val,它將成為從setjmp處返回的值。(很神奇吧。setjmp根據返回值可知道是哪個longjmp返回來的)
~~~
#include <stdio.h>
#include <setjmp.h>
static jmp_buf buf;
void second(void)
{
printf("second\n");
longjmp(buf,1);
// 跳回setjmp的調用處使得setjmp返回值為1
}
void first(void)
{
second();
printf("first\n");
// 不可能執行到此行
}
int main()
{
if (!setjmp(buf))
{
// 進入此行前,setjmp返回0
first();
}
else
{
// 當longjmp跳轉回,setjmp返回1,因此進入此行
printf("main\n");
}
return 0;
}
~~~
直接調用setjmp時,返回值為0,這一般用于初始化(設置跳轉點時)。以后再調用longjmp宏時用env變量進行跳轉。程序會自動跳轉到setjmp宏的返回語句處,此時setjmp的返回值為非0,由longjmp的第二個參數指定。
一般地,宏setjmp和longjmp是成對使用的,這樣程序流程可以從一個深層嵌套的函數中返回。
volatile屬性
如果你有一個自動變量,而又不想它被編譯器優化進寄存器,則可定義其為有volatile屬性。這樣,就明確地把這個值放在存儲器中,而不會被優化進寄存器。
setjmp會保存當前棧狀態信息,也會保存此時寄存器中的值。(longjmp會回滾寄存器中的值)
【如果要編寫一個使用非局部跳轉的可移植程序,則必須使用volatile屬性】
· IO緩沖問題
緩沖輸出和內存分配?
???當一個程序產生輸出時,能夠立即看到它有多重要?這取決于程序。??
???例如,終端上顯示輸出并要求人們坐在終端前面回答一個問題,人們能夠看到輸出以知道該輸入什么就顯得至關重要了。另一方面,如果輸出到一個文件中,并最終被發送到一個行式打印機,只有所有的輸出最終能夠到達那里是重要的。??
???立即安排輸出的顯示通常比將其暫時保存在一大塊一起輸出要昂貴得多。因此,C實現通常允許程序員控制產生多少輸出后在實際地寫出它們。??
???這個控制通常約定為一個稱為setbuf()的庫函數。如果buf是一個具有適當大小的字符數組,則??
setbuf(stdout,?buf);??
將告訴I/O庫寫入到stdout中的輸出要以buf作為一個輸出緩沖,并且等到buf滿了或程序員直接調用fflush()再實際寫出。緩沖區的合適的大小在中定義為BUFSIZ。??
???因此,下面的程序解釋了通過使用setbuf()來講標準輸入復制到標準輸出:??
~~~
#include <stdio.h>
int main()
{
int c;
char buf[BUFSIZ];
setbuf(stdout, buf);
while((c = getchar()) != EOF)
putchar(c);
return 0 ;
}
~~~
???不幸的是,這個程序是錯誤的,因為一個細微的原因。??
???要知道毛病出在哪,我們需要知道緩沖區最后一次刷新是在什么時候。答案:主程序完成之后,庫將控制交回到操作系統之前所執行的清理的一部分。在這一時刻,緩沖區已經被釋放了!??(即main函數棧清空之后)
???有兩種方法可以避免這一問題。??
???首先,使用靜態緩沖區,或者將其顯式地聲明為靜態:??
static?char?buf[BUFSIZ];??
或者將整個聲明移到主函數之外。??
???另一種可能的方法是動態地分配緩沖區并且從不釋放它:??
char?*malloc();?
setbuf(stdout,?malloc(BUFSIZ));??
注意在后一種情況中,不必檢查malloc()的返回值,因為如果它失敗了,會返回一個空指針。而**setbuf()可以接受一個空指針作為其第二個參數,這將使得stdout變成非緩沖的。這會運行得很慢,但它是可以運行的。**
預編譯和宏定義
C/C++中幾個罕見卻有用的預編譯和宏定義
**1:# error**
語法格式如下:
#error token-sequence
其主要的作用是**在編譯的時候輸出編譯錯誤信息**token-sequence,從方便程序員檢查程序中出現的錯誤。例如下面的程序
~~~
#include "stdio.h"
int main(int argc, char* argv[])
{
#define CONST_NAME1 "CONST_NAME1"
printf("%s\n",CONST_NAME1);
#undef CONST_NAME1
#ifndef CONST_NAME1
#error No defined Constant Symbol CONST_NAME1
#endif
{
#define CONST_NAME2 "CONST_NAME2"
printf("%s\n",CONST_NAME2);
}
printf("%s\n",CONST_NAME2);
return 0;
}
~~~
在編譯的時候輸出如編譯信息
fatal error C1189: #error : No definedConstant Symbol CONST_NAME1
**2:#pragma**
其語法格式如下:
# pragma token-sequence
此指令的作用是觸發所定義的動作。如果token-sequence存在,則觸發相應的動作,否則忽略。此指令一般為編譯系統所使用。例如在Visual C++.Net 中利用# pragma once 防止同一代碼被包含多次。
**3:#line**
此命令主要是為**強制編譯器按指定的行號,開始對源程序的代碼重新編號**,在調試的時候,可以按此規定輸出錯誤代碼的準確位置。
形式1
語法格式如下:
# line constant “filename”
其作用是使得其后的源代碼從指定的行號constant重新開始編號,并將當前文件的名命名為filename。例如下面的程序如下:
~~~
#include "stdio.h"
void Test();
#line 10 "Hello.c"
int main(int argc, char* argv[])
{
#define CONST_NAME1 "CONST_NAME1"
printf("%s\n",CONST_NAME1);
#undef CONST_NAME1
printf("%s\n",CONST_NAME1);
{
#define CONST_NAME2 "CONST_NAME2"
printf("%s\n",CONST_NAME2);
}
printf("%s\n",CONST_NAME2);
return 0;
}
~~~
~~~
void Test()
{
printf("%s\n",CONST_NAME2);
}
~~~
提示如下的編譯信息:
Hello.c(15) : error C2065: 'CONST_NAME1' :undeclared identifier
表示當前文件的名稱被認為是Hello.c, #line 10 "Hello.c"所在的行被認為是第10行,因此提示第15行出錯。
形式2
語法格式如下:
# line constant
其作用在于編譯的時候,準確輸出出錯代碼所在的位置(行號),而在源程序中并不出現行號,從而方便程序員準確定位。
**4:運算符#和##**
在ANSI C中為預編譯指令定義了兩個運算符——#和##。
**# 的作用**是實現文本替換(字符串化),例如
#define HI(x)printf("Hi,"#x"\n");
void main()
{
HI(John);
}
程序的運行結果
Hi,John
在預編譯處理的時候, #x的作用是將x替換為所代表的字符序列。(即把x宏變量字符串化)在本程序中x為John,所以構建新串“Hi,John”。
**##的作用**是串連接。
例如
#define CONNECT(x,y) x##y
void main()
{
? int a1,a2,a3;
? CONNECT(a,1)=0;
? CONNECT(a,2)=12;
? a3=4;
? printf("a1=%d\ta2=%d\ta3=%d",a1,a2,a3);
}
程序的運行結果為
a1=0 a2=12 a3=4
在編譯之前, CONNECT(a,1)被翻譯為a1, CONNECT(a,2)被翻譯為a2。
標準IO的妙用
~~~
//指定精確位數
#include <stdio.h>
int main(void)
{
int m ; //精確位數
double input ; //用戶輸入小數
puts("請輸入一個小數:") ;
scanf("%lf",&input) ;
puts("請輸入精確到小數點后位數") ;
scanf("%d" ,&m) ;
puts("結果為");
printf("%.*lf" ,m,input) ;
return 0 ;
}
~~~
**打印printf:**
每一個printf函數的調用都返回一個值——要么是輸出字符的個數,要么輸出一個負數表示發生輸出錯誤。
**帶域寬和精度的打印**:
printf函數允許你為欲打印的數據指定精度。對于不同類型的數據而言,精度的含義是不一樣的。
**精度與整型轉換說明符**一起使用時,表示要打印的數據的最少數字位數。如果將要打印的數據所包含的數字的位數小于指定的精度,同時精度值前面帶有一個0或者一個小數點,則加填充0.
**精度與浮點型轉換說明符**一起使用時,表示將要打印的最大有效數字位數。
**精度與字符串轉換說明符**s一起使用時,表示將要從一個字符串中打印出來的最大字符個數。(可用于控制打出的字符的個數)
表示精度的方法是:在百分號和轉換說明符之間,插入一個表示精度的整數,并在整數的前面加上一個小數點。
**域寬和精度**可以放在一起使用,方法是:在百分號和轉換說明符之間,先寫上域寬,然后加上一個小數點,后面再寫上精度。例如:
printf(“%9.3f”, 123.456789) ;
的輸出結果是123.456
還可以**用變量來控制域寬和精度(可用于關于精度的舍入)**
在格式控制字符串中表示域寬或精度的位置上寫上一個星號*,然后程序將會計算實參列表中相對應的整型實參值,并用其替換星號。
例如:
printf(“%*.*f”, 7, 2, 98.736) ; 將以7為域寬,2為精度,輸出右對齊的98.74
表示域寬的值可以是正數,也可以是負數(將導致輸出結果在域寬內左對齊)
**使用標記**
printf函數還提供了一些標記來增加它的輸出格式控制功能,在格式控制字符串中可以使用的標記有:
-(減號)?? 在域寬內左對齊顯示輸出結果
+(加號)? 在正數前面顯示一個加號,在負數前面顯示一個減號
空格???? 在不帶加號標記的正數前面打印一個空格
# ???????當使用的是八進制轉換說明符o時,在輸出數據前面加上前綴0
???????當使用的是十六進制轉換說明符x或X時,在輸出數據前面加上前綴0x或0X
0(零)???? 在打印的數據前面加上前導0
**逆向打印參數**(POSIX擴展語法)
printf("%4$d %3$d %2$d %1$d", 1, 2, 3, 9); ? ? ?//將會打印9 3 2 1
**格式化輸入scanf**
**掃描集(實用)**
一個字符序列可以用一個掃描集(Scanset)來輸入。掃描集是位于格式控制字符串中,以百分號開頭、用方括號[]括起來的一組字符。
尋找與掃描集中的字符相匹配的字符。一旦找到匹配的字符,那么這個字符將被存儲到掃描集對應的實參(即指向一個字符數組的指針)中。只有遇到掃描集中沒有包含的字符時,掃描集才會停止輸入字符。
如果輸入流中的第一個字符就不能與掃描集中包含的字符相匹配,那么只有空操作符被存儲到字符數組中。
(如果輸入的字符屬于方括號內字符串中某個字符,那么就提取該字符;如果一經發現不屬于就結束提取。該方法會自動加上一個'\0'到已經提取的字符后面。)
【例如】
char str[512] ;
printf(“Enter string:\n”) ;
scanf(“%[aeiou]”, str) ;
程序使用掃描集[aeiou]在輸入流中尋找元音字符,直到遇到非元音字符。
我們還可以用縮寫a-z表示abcd….xyz字母集。
scanf(“%[a-z]”, str) ;
同理,也可以用縮寫0-9? 縮寫A-Z。
想只取字母,那就可以寫成 %[A-Za-z]
對于字符串"abDEc123"如果想按照字母和數字讀到兩個字符串中就應該是?"%[a-zA-Z]%[0-9]",buf1,buf2?;
**逆向掃描集**
逆向掃描集還可以用來掃描那些沒有出現在掃描集中的字符。創建一個逆向掃描集的方法是,在方括號內掃描字符前面加一個“脫字符號”(^)。這個符號將使得那些沒有出現在掃描集中的字符被保存起來。只有遇到了逆向掃描集中包含的字符時,輸入才會停止。(即取其后字符們的補集作為掃描集)
scanf(“%[^aeiou]”, str) ;
即接受輸入流中的非元音字符。
用這種方法還可以解決scanf的輸入中不能有空格的問題。只要用
scanf("%[^\n]",str); 就可以了。很神奇吧。
【注意】
[]內的字符串可以是1或更多字符組成。空字符集(%[])是違反規定的,可導致不可預知的結果。%[^]也是違反規定的。
**指定域寬**
我們可以在scanf函數的轉換說明符中指定域寬來從輸入流中讀取特定數目的字符。
【例】
scanf(“%2d%d”, &x, &y) ;
程序從輸入流中讀取一系列連續的數字,然后,將其前兩位數字處理為一個兩位的整數,將剩余的數字處理成另外一個整數。
**賦值抑制字符**
即*。賦值抑制字符使得scanf函數從輸入流中讀取任意類型的數據,并將其丟棄,而不是將其賦值給一個變量。如果你想忽略掉某個輸入,使用在% 后使用* 。
%*[^=] 前面帶 * 號表示不保存變量。跳過符合條件的字符串。
char s[]="notepad=1.0.0.1001";
char szfilename [32] = "" ;
int i = **sscanf( s, "%*[^=]", szfilename )**;
// szfilename=NULL,因為沒保存
int i =**sscanf( s, "%*[^=]=%s", szfilename )**;
// szfilename=1.0.0.1001
所有對%s起作用的控制,都可以用于%[],比如"%*[^\n]%*c"就表示跳過一行,"%-20[^\n]"就表示讀取\n前20個字符。?
把掃描集、賦值抑制符和域寬等綜合使用,可實現簡單的正則表達式那樣的分析字符串的功能。
scanf的返回值是讀入數據的個數;
比如scanf("%d%d",&a,&b);讀入一個返回1,讀入2個返回2,讀入0個返回0;讀入錯誤返回EOF即-1
順便提一句,**你應該非常小心的使用scanf 因為它可能會是你的輸入緩沖溢出!通常你應該使用fgets 和sscanf 而不是僅僅使用scanf,使用fgets 來讀取一行**,然后用sscanf 來解析這一行,就像上面演示的一樣。
數據類型對應字節數
程序運行平臺
?????不同的平臺上對不同數據類型分配的字節數是不同的。
?????個人對平臺的理解是CPU+OS+Compiler,是因為:?
?????1、64位機器也可以裝32位系統(x64裝XP);?
?????2、32位機器上可以有16/32位的編譯器(XP上有tc是16位的,其他常見的是32位的);?
?????3、即使是32位的編譯器也可以弄出64位的integer來(int64)。?
?????以上這些是基于常見的wintel平臺,加上我們可能很少機會接觸的其它平臺(其它的CPU和OS),所以個人認為所謂平臺的概念是三者的組合。?
?????雖然三者的長度可以不一樣,但顯然相互配合(即長度相等,32位的CPU+32位的OS+32位的Compiler)發揮的能量最大。?
?????理論上來講 我覺得數據類型的字節數應該是由CPU決定的,但是實際上主要由編譯器決定(占多少位由編譯器在編譯期間說了算)。
常用數據類型對應字節數可用如sizeof(char),sizeof(char*)等得出
32位編譯器:
?????char :1個字節
?????char*(即指針變量): 4個字節(32位的尋址空間是2^32, 即32個bit,也就是4個字節。同理64位編譯器)
?????short int : 2個字節
?????int:??4個字節
?????unsigned int : 4個字節
?????float:??4個字節
?????double:???8個字節
?????long:???4個字節
?????long long:??8個字節
?????unsigned long:??4個字節
?64位編譯器:
?????char :1個字節
?????char*(即指針變量): 8個字節
?????short int : 2個字節
?????int:??4個字節
?????unsigned int : 4個字節
?????float:??4個字節
?????double:???8個字節
?????long:???8個字節
?????long long:??8個字節
?????unsigned long:??8個字節