auto :聲明自動變量 一般不使用
auto被解釋為一個自動存儲變量的關鍵字,也就是申明一塊臨時的變量內存。
auto int a=4;
表示a為一個自動存儲的臨時變量。
作用:C程序是面向過程的,在C代碼中會出現大量的函數模塊,每個函數都有其生命周期(也稱作用域),在函數生命周期中聲明的變量通常叫做局部變量,也叫自動變量。例如:
int fun(){
int a = 10; // auto int a = 10;
// do something
return 0;
}
整型變量a在fun函數內聲明,其作用域為fun函數內,出來fun函數,不能被引用,a變量為自動變量。也就是說編譯器會有int a = 10之前會加上auto的關鍵字。
auto的出現意味著,當前變量的作用域為當前函數或代碼段的局部變量,意味著當前變量會在內存棧上進行分配。
內存棧:
如果大家學過數據結構,應該知道,棧就是先進後出的數據結構。它類似於我們用箱子打包書本,第一本扔進去大英,第二本扔進行高數,第三本扔進行小說,那麼取書的時候,先取出來第一本是小說,第二是高數,第三本是大英。
棧的操作為入棧和出棧,入棧就是向箱子裡扔書,出棧就是從箱子裡取書。那麼這和我們的auto變量分配空間有什麼關系呢?
由於一個程序中可能會有大量的變量聲明,每個變量都會占有一定的內存空間,而內存空間對於計算機來說是寶貴的硬件資源,因此合理的利用內存是編譯器要做的一個主要任務。有的變量是一次性使用的,如局部變量。有的變量要伴隨著整個程序來使用的,如全局變量。為了節省內存空間,優化性能,編譯器通常會將一次性使用的變量分配在棧上。也就是說,代碼中聲明一個一次性變量,就在棧上進行入棧操作。當該變量使用完了(生命周期結束),進行出棧操作。這樣,在執行不同的函數的時候,就會在一個棧上進行出入棧操作,也就是說它們在頻繁的使用一個相同的內存空間,從而可以更高效的利用內存。
double :聲明雙精度變量或函數
C語言中,雙精度浮點(double)型,占8 個字節(64位)內存空間。其數值范圍為1.7E-308~1.7E+308,雙精度完全保證的有效數字是15位,16位只是部分數值有保證,而單精度保證7位有效數字,部分數值有8位有效數.
浮點型從狹義上說就是科學記數法
雙精度,即 double 。 double有二,兩個的意思。
C 標准要求 float 類型至少要能精確表示到小數點後6位,並且整數部分的表示范圍至少要達到 1.0-37 – 10+37 。float 一般是 32 位的。
C 標准規定double 類型的整數部分的最小表示范圍和 float 一樣,都是 1.0E-37 到 1.0E+37,但是它要求 double 類型的小數部分至少要能精確到小數點後 10 位。double 通常是 64 位的。
long double
C 還提供了 long double 類型,目的是提供一種比 double 更加精確的類型。
然而,C 標准僅僅規定 long double 至少要和 double 一樣精確。
double a=5.22;
int: 聲明整型變量或函數
C/C++編程語言中,int表示整型變量,是一種數據類型,用於定義一個整型變量,在不同編譯環境有不同的大小,不同編譯運行環境大小不同。在32/64位系統中都是32位,范圍為-2147483648~+2147483647,無符號情況下表示為0~4294967295。
int a;
struct:聲明結構體變量或函數
基本定義:結構體,通俗講就像是打包封裝,把一些有共同特征(比如同屬於某一類事物的屬性,往往是某種業務相關屬性的聚合)的變量封裝在內部,通過一定方法訪問修改內部變量。
“結構”是一種構造類型,它是由若干“成員”組成的。 每一個成員可以是一個基本數據類型或者又是一個構造類型。 結構即是一種“構造”而成的數據類型, 那麼在說明和使用之前必須先定義它,也就是構造它。如同在說明和調用函數之前要先定義一樣。
定義一個結構的一般形式為:
struct 結構名
{
//成員表列
};
成員表由若干個成員組成, 每個成員都是該結構的一個組成部分。對每個成員也必須作類型說明,其形式為:
類型說明符 成員名;
成員名的命名應符合標識符的書寫規定。
例如:
struct stu
{
int num;
char name[20];
char sex;
float score;
};
在這個結構定義中,結構名為stu,該結構由4個成員組成。 第一個成員為num,整型變量;第二個成員為name,字符型http://blog.csdn.net/a193314/article/details/數組;第三個成員為sex,字符型變量;第四個成員為score,浮點型變量。 應注意在括號後的分號是必不可少的。
值得一提的是,在C++中,struct的功能得到了強化,struct不僅可以添加成員變量,還可以添加成員函數,和class類似。所以完全可以利用結構提封裝出C++的一些特性,引入面向對象的C寫法。
對於結構體的sizeof計算需要理解內存對齊問題。
break:跳出當前循環
break語句通常用在循環語句和開關語句中。當break用於開關語句switch中時,可使程序跳出switch而執行switch以後的語句;如果沒有break語句,則會從滿足條件的地方(即與switch(表達式)括號中表達式匹配的case)開始執行,直到switch結構結束。
當break語句用於do-while、for、while循環語句中時,可使程序終止循環。而執行循環後面的語句,通常break語句總是與if語句聯在一起。即滿足條件時便跳出循環。
例:
int main(void)
{
int i=0;
char c;
while(1) /*設置循環*/
{
c='\0'; /*變量賦初值*/
while(c!=13&&c!=27) /*鍵盤接收字符直到按回車或Esc鍵*/
{
c=getch();
printf("%c\n",c);
}
if(c==27)
break; /*判斷若按Esc鍵則退出循環*/
i++;
printf("The No. is %d\n",i);
}
printf("The end");
return 0;
}
注意:
1. break語句對if-else的條件語句不起作用。
2. 在多層循環中,一個break語句只向外跳一層。
if else :條件分支語句
if語句是指C語言中用來判定所給定的條件是否滿足,根據判定的結果(真或假)決定執行給出的兩種操作之一。
c語言提供了三種形式的if語句:
1、if(表達式)語句。
if(x>y)
printf(“%d”,x);
if(表達式)語句1 else 語句2
例如:
if(x>y)
{
printf("%d",x);
}
else
{
printf("%d",y);
}
3、if(表達式1)
語句1
else if(表達式2)
語句2
else if(表達式3)
語句3
else if(表達式m)
語句m
else
語句 n
if(0==i)
{
//do something
}
else if(1==i)
{
//do something
}
else if(2==i)
{
//do something
}
else
{
//do something
}
在每個語句中,可以有多個語句,但需要加上大括號
例:
if(x>y)
{
printf("%d",x);
break;
}
多層嵌套:
if(x>100)
{
if(y>100)
{
//do somethings
}
}
多個條件:
if(x==y||x==z)
{
//do somethings
}
if(1==x&&1==y)
{
//do somethings
}
long :聲明長整型變量或函數
取值范圍:
1. unsigned long 0~4294967295
2. long 2147483648~2147483647
3. long long的最大值:9223372036854775807
4. long long的最小值:-9223372036854775808
5. unsigned long long的最大值:18446744073709551615
6. long 是C語言的一個關鍵字,代表一種數據類型,中文為長整型。
7. long是long int的簡寫,也就是說,在C語言中long int類型和long類型是相同的。
8. 每個long型占4個字節,在32位編譯系統下,long和int占的空間是相同的。這也導致了long型變量使用的越來越少了。
9. long型可以表示的整型數字范圍為-2,147,483,648 ~ 2,147,483,647, 即-2^32 ~ 2^32-1。在用在C的格式化輸入輸出時,long型的格式化字符為”%ld”。
10. long同其它整型類型一樣,可以同unsigned 聯合使用,形成unsigned long,即無符號長整型, 其格式化字符為”%lu”。
11. 在部分編譯器下,比如gcc, 兩個long合用,即long long類型,表示C語言目前最長的系統整型類型,每個long long類型占8字節,64位。其格式化字符為”%lld”。
switch case:開關語句
其一般形式為:
switch(表達式)
{
case 常量表達式1:
語句1;
case 常量表達式2:
語句2;
…
case 常量表達式n:
語句n;
default:
語句n+1;
}
其語義是:計算表達式的值。 並逐個與其後的常量表達式值相比較,當表達式的值與某個常量表達式的值相等時, 即執行其後的語句,然後不再進行判斷,繼續執行後面所有case後的語句。如表達式的值與所有case後的常量表達式均不相同時,則執行default後的語句。
#include
int main(void)
{
int a;
printf("input integer number: ");
scanf("%d",&a);
switch (a){
case 1:
printf("Monday\n");
break;
case 2:
printf("Tuesday\n");
break;
case 3:
printf("Wednesday\n");
break;
case 4:
printf("Thursday\n");
break;
case 5:
printf("Friday\n");
break;
case 6:
printf("Saturday\n");
break;
case 7:
printf("Sunday\n");
break;
default:
printf("error\n");
}
return 0;
}
根據不同的場合選擇是否需要break;case 語句用{}包含起來也有作用域的效果,也可多層嵌套。
enum :聲明枚舉類型
enum是計算機編程語言中的一種數據類型。枚舉類型:在實際問題中,有些變量的取值被限定在一個有限的范圍內。例如,一個星期內只有七天,一年只有十二個月,一個班每周有六門課程等等。如果把這些量說明為整型,字符型或其它類型顯然是不妥當的。為此,C語言提供了一種稱為“枚舉”的類型。在“枚舉”類型的定義中列舉出所有可能的取值,被說明為該“枚舉”類型的變量取值不能超過定義的范圍。應該說明的是,枚舉類型是一種基本數據類型,而不是一種構造類型,因為它不能再分解為任何基本類型。
定義說明:
1. 枚舉類型定義的一般形式為:
enum 枚舉名
{
枚舉值表
};
在枚舉值表中應羅列出所有可用值。這些值也稱為枚舉元素。
例如:
該枚舉名為weekday,枚舉值共有7個,即一周中的七天。凡被說明為weekday類型變量的取值只能是七天中的某一天。
2. 枚舉變量的說明
如同結構體(struct)和共用體(union)一樣,枚舉變量也可用不同的方式說明,即先定義後說明,同時定義說明或直接說明。
設有變量a,b,c被說明為上述的weekday,可采用下述任一種方式:
enum weekday
{
sun,
mon,
tue,
wed,
thu,
fri,
sat
};
enum weekday a,b,c;
//或者為:
enum weekday
{
sun,
mon,
tue,
wed,
thu,
fri,
sat
}a,b,c;
//或者為:
enum
{
sun, //start 默認為0
mon,//+1
tue,//+1
wed,//+1
thu,//+1
fri,//+1
sat//+1
}a,b,c;
內存空間:
enum是枚舉型 union是共用體,成員共用一個變量緩沖區。
賦值和使用:
枚舉類型在使用中有以下規定:
1. 枚舉值是常量,不是變量。不能在程序中用賦值語句再對它賦值。
2. 枚舉元素本身由系統定義了一個表示序號的數值,從0開始順序定0,1,2…。如在weekday中,sun值為0,mon值為1,sat值為6。
只能把枚舉值賦予枚舉變量,不能把元素的數值直接賦予枚舉變量。如一定要把數值賦予枚舉變量,則必須用強制類型轉換。
如:
a=(enum weekday)2;
其意義是將順序號為2的枚舉元素賦予枚舉變量a,相當於:
a=tue;
還應該說明的是枚舉元素不是字符常量也不是字符串常量,使用時不要加單、雙引號。
register:聲明積存器變量
這個關鍵字請求編譯器盡可能的將變量存在CPU內部寄存器中,而不是通過內存尋址訪問,以提高效率。注意是盡可能,不是絕對。你想想,一個CPU 的寄存器也就那麼幾個或幾十個,你要是定義了很多很多register 變量,它累死也可能不能全部把這些變量放入寄存器吧,輪也可能輪不到你。
一、寄存器
寄存器是個中轉站,並無別的功能。大部分情況應用於驅動編寫,對處理速度有較高要求的地方。
二、舉例
register修飾符暗示編譯程序相應的變量將被頻繁地使用,如果可能的話,應將其保存在CPU的寄存器中,以加快其存儲速度。例如下面的內存塊拷貝代碼,
memcpy (d, s, l)
{
register char *d;
register char *s;
register int i;
while (i--)
*d++ = *s++;
}
三、使用register 修飾符的注意點
但是使用register修飾符有幾點限制。
首先,register變量必須是能被CPU所接受的類型。這通常意味著register變量必須是一個單個的值,並且長度應該小於或者等於整型的長度。不過,有些機器的寄存器也能存放浮點數。
其次,因為register變量可能不存放在內存中,所以不能用“&”來獲取register變量的地址。
由於寄存器的數量有限,而且某些寄存器只能接受特定類型的數據(如指針和浮點數),因此真正起作用的register修飾符的數目和類型都依賴於運行程序的機器,而任何多余的register修飾符都將被編譯程序所忽略。
在某些情況下,把變量保存在寄存器中反而會降低程序的運行速度。因為被占用的寄存器不能再用於其它目的;或者變量被使用的次數不夠多,不足以裝入和存儲變量所帶來的額外開銷。
早期的C編譯程序不會把變量保存在寄存器中,除非你命令它這樣做,這時register修飾符是C語言的一種很有價值的補充。然而,隨著編譯程序設計技術的進步,在決定那些變量應該被存到寄存器中時,現在的C編譯環境能比程序員做出更好的決定。實際上,許多編譯程序都會忽略register修飾符,因為盡管它完全合法,但它僅僅是暗示而不是命令。
typedef:用以給數據類型取別名(當然還有其他作用)
typedef 聲明,簡稱 typedef,為現有類型創建一個新的名字。比如人們常常使用 typedef 來編寫更美觀和可讀的代碼。所謂美觀,意指 typedef 能隱藏笨拙的語法構造以及平台相關的數據類型,從而增強可移植性和以及未來的可維護性。
常見用法:
1.常規變量類型定義
例如:typedef unsigned char uchar
描述:uchar等價於unsigned char類型定義
uchar c聲明等於unsigned char c聲明
2.http://blog.csdn.net/a193314/article/details/數組類型定義
例如: typedef int array[2];
描述: array等價於 int [2]定義;
array a聲明等價於int a[2]聲明
擴展: typedef int array[M][N];
描述: array等價於 int [M][N]定義;
array a聲明等價於int a[M][N]聲明
3.指針類型定義
例如: typedef int *pointer;
描述: pointer等價於 int *定義;
pointer p聲明等價於int *a聲明
例如: typedef int *pointer[M];
描述: pointer等價於 int *[M]定義;
pointer p聲明等價於int *a[M]聲明明
4.函數地址說明
描述:C把函數名字當做函數的首地址來對待,我們可以使用最簡單的方法得到函數地址
例如: 函數:int func(void);
unsigned long funcAddr=(unsigned long)func;
funcAddr的值是func函數的首地址
5.函數聲明
例如: typedef int func(void);
func等價於 int (void)類型函數
描述1: func f聲明等價於 int f(void)聲明,用於文件的函數聲明
描述2: func *pf聲明等價於 int (*pf)(void)聲明,用於函數指針的生命,見下一條
6.函數指針
例如: typedef int (*func)(void)
描述: func等價於int (*)(void)類型
func pf等價於int (*pf)(void)聲明,pf是一個函數指針變量
7.識別typedef的方法:
a).第一步。使用已知的類型定義替代typdef後面的名稱,直到只剩下一個名字不識別為正確
如typedef u32 (*func)(u8);
從上面的定義中找到 typedef __u32 u32;typedef __u8 u8
繼續找到 typedef unsigned int __u32;typedef unsigned char __u8;
替代位置名稱 typedef unsigned int (*func)(void);
現在只有func屬於未知
b).第二步.未知名字為定義類型,類型為取出名稱和typedef的所有部分,如上為
func等價於unsigned unsigned int (*)(unsigned char);
c).第三部.定義一個變量時,變量類型等價於把變量替代未知名字的位置所得到的類型func f等價於unsigned unsigned int (*f)(unsigned char)
char :聲明字符型變量或函數
C語言基本類型:字符型(char)用法介紹
1.字符型(char)簡介
字符型(char)用於儲存字符(character),如英文字母或標點。嚴格來說,char 其實也是整數類型(integer type),因為 char 類型儲存的實際上是整數,而不是字符。計算機使用特定的整數編碼來表示特定的字符。美國普遍使用的編碼是 ASCII(American Standard Code for Information Interchange 美國信息交換標准編碼)。例如:ASCII 使用 65 來代表大寫字母 A,因此存儲字母 A 實際上存儲的是整數65。注意:許多IBM大型機使用另一種編碼——EBCDIC(Extended Binary-Coded Decimal Interchange Code 擴充的二進制編碼的十進制交換碼);不同國家的計算機使用的編碼可能完全不同。
ASCII 的范圍是 0 到 127,故而 7 位(bit)就足以表示全部 ASCII。char 一般占用 8 位內存單元,表示ASCII綽綽有余。許多系統都提供擴展ASCII(Extended ASCII),並且所需空間仍然在 8 位以內。注意,不同的系統提供的擴展 ASCII 的編碼方式可能有所不同!
許多字符集超出了 8 位所能表示的范圍(例如漢字字符集),使用這種字符集作為基本字符集的系統中,char 可能是 16 位的,甚至可能是 32 位的。總之,C 保證 char 占用空間的大小足以儲存系統所用的基本字符集的編碼。C 語言定義一個字節(byte)的位數為 char 的位數,所以一個字節可能是 16 位,也可能是 32 位,而不僅僅限於 8 位。
2. 聲明字符型變量字符型變量的聲明方式和其它類型變量的聲明方式一樣:
char good;
char better, best;
以上代碼聲明了三個字符型變量:good、better,和 best。
3. 字符常量與初始化
我們可以使用以下語句來初始化字符型變量:
char ch = 'A';
這個語句把 ch 的值初始化為 A 的編碼值。在這個語句中,’A’ 是字符常量。C 語言中,使用單引號把字符引起來就構成字符常量。我們來看另外一個例子:
char fail; /* 聲明一個字符型變量 */
fail = 'F'; /* 正確 */
fail = "F"; /* 錯!"F" 是字符串字面量 */
把字符用雙引號引起來構成字符串字面量,所以第三個語句是錯誤的。我們會在後續的教程中討論字符串,現在暫且把它放下。
因為字符實質上是以數字的形式存儲的,所以我們可以直接使用數字來初始化字符變量,或者給字符變量賦值:
在 ASCII 中,A 的編碼是 65,所以對於使用 ASCII 的系統來說,這個語句等同於 char ch = ‘A’;。使用非 ASCII 的系統中,65 代表的不一定是 A,而有可能是其它任何字符,所以使用數字來初始化字符變量,或者給字符變量賦值是一種不好的風格,因為移植性太差了!但是,使用字符常量(例如 ‘A’)來初始化字符變量,或者給字符變量賦值,字符變量得到的一定是我們所期待的字符的編碼值。例如:
char ch = 'A';
無論在使用任何編碼的系統中,ch 都能夠得到字符 A 所對應的編碼值。這是因為編譯器會自動把 ‘A’ 轉化成 A 所對應的編碼值。因此,我們應該使用字符常量來初始化字符變量,或者給字符變量賦值;而不要用數字。
有趣的是,C 使用 int 類型來處理字符常量,而不是 char 類型。例如,在使用32位 int 的ASCII 系統中,以下代碼
char ch = 'C';
的編碼值 67 被存儲於 32 位的內存單元中;不過 ch 仍然存儲於 8 位的內存單元中,只是它的值變成了 67。因此,我們可以定義形如 ‘good’ 的古怪字符常量。因為每個字符的編碼值占用 8 位的內存單元,所以這個常量剛好可以存儲於 32 位的內存單元。然而,用這種字符常量初始化字符變量,或者給字符變量賦值的話,導致的結果是,字符變量只能得到字符常量的最後 8 位。也就是說,以下代碼
char ch = 'good';
ch 得到的是 ‘d’ 的值。
4.不可打印字符(Nonprinting Characters)
有些 ASCII 字符是不可打印的。例如退格、另起一行、警報等。C 語言提供了兩種方法來表示這種不可打印字符。
第一種方法是使用 ASCII 編碼。例如,ASCII 編碼中,7 用於表示警報:
char beep = 7;
第二種方法是使用特殊符號序列,也就是所謂的轉義字符escape sequences)。參見下表:(
轉義字符 含義
\a 警報( Alert (ANSI C) )
\b 退格(Backspace)
\f 換頁(Form feed)
換行(Newline)
回車(Carriage return)
\t 水平制表符(Horizontal tab)
\v 垂直制表符(Vertical tab)
\ 反斜桿( Backslash () )
\’ 單引號( Single quote (‘) )
\” 雙引號( Double quote (“) )
\? 問號( Question mark (?) )
\0oo 八進制數( Octal value (o 代表一個八進制數字) )
\xhh 十六進制數( Hexadecimal value (h 代表一個十六進制數字) )
給變量賦值的時候,轉義字符必須使用單引號引住。例如:
char nl = ' ';
下面我們詳細學習每個轉移字符的含義。
\a(警報)是 ANSI C89 添加的,用於產生可聽或者可視的警報。\a 產生的效果取決於硬件。一般來說,輸出 \a 會產生鳴響。但是在某些系統,輸出 \a 不會產生任何效果,或者僅僅顯示一個特殊字符。標准明確指出,\a 不應該改變當前活躍位置(active position)。所謂活躍位置,是指顯示設備(顯示器、打字機、打印機等等)顯示下一個字符的位置。以顯示器為例,活躍位置就是指光標所處的位置,輸出 \a 不會導致光標移動位置。
\b、\f、 、 、\t,以及 \v 都是輸出設備控制符。退格符(\b)使當前行的活躍位置後退一個位置。換頁符(\f)使活躍位置跳到下一頁的開端。注:換頁符可用於控制打印機換頁,但不會導致 PC 機的顯示屏換頁。換行符( )使活躍位置跳到下一行的開端。回車符 ( ) 使活躍位置返回當前行的開端。水平制表符(\t)使活躍位置移動若干個位置(通常是8個)。垂直制表符(\v)使活躍位置換若干行。注:\v可用於控制打印機換若干行,但是不會導致PC機的顯示屏換行。 \、\’,以及 \” 使我們可以把 \,’ 和 ” 用作字符常量。如果要打印以下句子:”\ is called ‘backslash’.”
我們需要使用如下語句:
printf("\"\\ is called \'backslash\'.\"");
\0oo 和 \xhh 是ASCII碼的兩種特殊表示形式。如果想用八進制ASCII碼表示字符,可以在八進制數前面加上 \ ,然後用單引號引起來。例如:
beep = '\007'; /* \007 代表 \a */
打頭的那些0可以省略,也就是說,寫成 ‘\07’ 或者 ‘\7’ 都一樣。無論有沒有打頭的0 ,7 都會被當成八進制數處理。
從 C89 開始,C提供了用十六進制表示字符常量的方法:在反斜桿後面寫一個 x ,然後再寫 1 到 3 個十六進制數字。例如:
nl = '\xa'; /* \xa 代表 */
注意:使用ASCII碼時,要注意區分數字4的ASCII碼是52 ,’4’ 代表字符 4 ,而不是數字4。此外,盡管 ’ ’ 和 ‘\xa’ ,’\a’ 和 ‘\007’ 是等價的,但是我們應該盡可能使用 ’ ’ 和 ‘\a’ ,而不要用 ‘\xa’ 和 ‘\007’ 。這是因為前者易懂、便於記憶,而且移植性更高。而後者只對使用ASCII碼的系統有效。和數字字符。例如:字符
5. 字符輸出
printf 函數使用 %c 表示輸出字符。因為字符是以 1 字節整數的形式存取的,所以,如果使用 %d 的話,輸出的會是整數。例如:
/* 這個程序輸出字符以及字符的整數編碼 */
#include
int main(void)
{
char ch;
printf("Please enter a character. ");
scanf("%c", &ch); /* 由用戶輸入一個字符 */
printf("The code for %c is %d. ", ch, ch);
return 0;
}
請各位自行編譯執行此程序,查看其執行結果。輸入字符後記得要按回車鍵。
printf 函數輸出 ch 的值兩次,第一次以字符的形式輸出(因為格式限定符為 %c),第二次以十進制整數的形式輸出(因為格式限定符是 %d)。注意:格式限定符只是用於指定數據的輸出形式,而不是用來指定數據怎麼存儲。
6.字符類型的符號
某些編譯器中,char 默認是有符號的(signed)。對於這類型的編譯器來說,char 的表示范圍通常是 -128 到 127 。而另外一些編譯器中,char 默認是無符號的(unsigned)。對於這類型的編譯器來說,char 的表示范圍通常是 0 到 255 。一般來說,編譯器的使用說明會注明它默認把 char 當作有符號的還是無符號的。
從 C89 開始,我們可以使用關鍵字 signed 和 unsigned 來修飾 char 。這麼一來,無論編譯器默認 char 是有符號的也好,無符號的也罷,我們都可以用 signed char 表示有符號 char ,也可以用 unsigned char 表示無符號 char 。
在C中,默認的基礎數據類型均為signed,現在我們以char為例,說明(signed) char與unsigned char之間的區別。
首先在內存中,char與unsigned char沒有什麼不同,都是一個字節,唯一的區別是,char的最高位為符號位,因此char能表示-127~127,unsigned char沒有符號位,因此能表示0~255,這個好理解,8個bit,最多256種情況,因此無論如何都能表示256個數字。在實際使用過程種有什麼區別呢?主要是符號位,但是在普通的賦值,讀寫文件和網絡字節流都沒什麼區別,反正就是一個字節,不管最高位是什麼,最終的讀取結果都一樣,只是你怎麼理解最高位而已,在屏幕上面的顯示可能不一樣。二者的最大區別是:但是我們卻發現在表示byte時,都用unsigned char,這是為什麼呢?首先我們通常意義上理解,byte沒有什麼符號位之說,更重要的是如果將byte的值賦給int,long等數據類型時,系統會做一些額外的工作。如果是char,那麼系統認為最高位是符號位,而int可能是16或者32位,那麼會對最高位進行擴展(注意,賦給unsigned int也會擴展)而如果是unsigned char,那麼不會擴展。最高位若為0時,二者沒有區別,若為1時,則有區別了。同理可以推導到其它的類型,比如short, unsigned short,等等。
具體可以通過下面的小例子看看其區別
#include
void f(unsigned char v)
{
char c = v;
unsigned char uc = v;
unsigned int a = c, b = uc;
int i = c, j = uc;
printf("----------------\n");
printf("%%c: %c, %c\n", c, uc);
printf("%%X: %X, %X\n", c, uc);
printf("%%u: %u, %u\n", a, b);
printf("%%d: %d, %d\n", i, j);
}
int main(int argc, char *argv[])
{
f(0x80);
f(0x7F);
return 0;
}
結果輸出如下:
結果分析:
對於(signed)char來說,0x80用二進制表示為1000 0000,當它作為char賦值給unsigned int或 int 時,系統認為最高位是符號位,會對最高位進行擴展。而0x7F用二進制表示為0111 1111,最高位為0,不會擴展。對於unsigned char來說,不管最高位是0,還是1,都不會做擴展。
extern:聲明變量是在其他文件正聲明(也可以看做是引用變量)
extern可置於變量或者函數前,以表示變量或者函數的定義在別的文件中,提示編譯器遇到此變量或函數時,在其它模塊中尋找其定義。另外,extern也可用來進行鏈接指定。
在一個源文件裡定義了一個http://blog.csdn.net/a193314/article/details/數組:char a[6];
在另外一個文件裡用下列語句進行了聲明:extern char *a;
請問,這樣可以嗎?
答案與分析:
1)、不可以,程序運行時會告訴你非法訪問。原因在於,指向類型T的指針並不等價於類型T的http://blog.csdn.net/a193314/article/details/數組。extern char *a聲明的是一個指針變量而不是字符http://blog.csdn.net/a193314/article/details/數組,因此與實際的定義不同,從而造成運行時非法訪問。應該將聲明改為extern char a[ ]。
2)、例子分析如下,如果a[] = “abcd”,則外部變量a=0x12345678 (http://blog.csdn.net/a193314/article/details/數組的起始地址),而*a是重新定義了一個指針變量,a指向的地址可能是0x87654321,直接使用*a是錯誤的.
3)、這提示我們,在使用extern時候要嚴格對應聲明時的格式,在實際編程中,這樣的錯誤屢見不鮮。
4)、extern用在變量聲明中常常有這樣一個作用:你要在.c文件中引用另一個文件中的一個全局的變量,那就應該放在.h中用extern來聲明這個全局變量。
這個關鍵字真的比較可惡,在定義(函數)的時候,這個extern居然可以被省略,所以會讓你搞不清楚到底是聲明還是定義,下面分變量和函數兩類來說:
尤其是對於變量來說。
extern int a;//聲明一個全局變量a
int a; //定義一個全局變量a
extern int a =0 ;//定義一個全局變量a 並給初值。
//一旦給予賦值,一定是定義,定義才會分配存儲空間。
int a =0;//定義一個全局變量a,並給初值,
聲明之後你不能直接使用這個變量,需要定義之後才能使用。
第四個等於第三個,都是定義一個可以被外部使用的全局變量,並給初值。
糊塗了吧,他們看上去可真像。但是定義只能出現在一處。也就是說,不管是int a;還是int a=0;都只能出現一次,而那個extern int a可以出現很多次。
當你要引用一個全局變量的時候,你就要聲明extern int a;這時候extern不能省略,因為省略了,就變成int a;這是一個定義,不是聲明。
常見extern放在函數的前面成為函數聲明的一部分,那麼,C語言的關鍵字extern在函數的聲明中起什麼作用?
答案與分析:
如果函數的聲明中帶有關鍵字extern,僅僅是暗示這個函數可能在別的源文件裡定義,沒有其它作用。即下述兩個函數聲明沒有明顯的區別:
extern int f(); 和int f();
當然,這樣的用處還是有的,就是在程序中取代include “*.h”來聲明函數,在一些復雜的項目中,我比較習慣在所有的函數聲明前添加extern修飾。
extern函數2
當函數提供方單方面修改函數原型時,如果使用方不知情繼續沿用原來的extern申明,這樣編譯時編譯器不會報錯。但是在運行過程中,因為少了或者多了輸入參數,往往會造成系統錯誤,這種情況應該如何解決?
答案與分析:
目前業界針對這種情況的處理沒有一個很完美的方案,通常的做法是提供放在自己的xxx_pub.h中提供對外部接口的聲明,然後調用包涵該文件的頭文件,從而省去extern這一步。以避免這種錯誤。
寶劍有雙鋒,對extern的應用,不同的場合應該選擇不同的做法。
return :子程序返回語句(可以帶參數,也看不帶參數)
比方主函數
int main(void)
{
}
這裡就必須有一個return,只有void func()時可以不用返回值。
功能函數
int fun(void)
{
return 1;
}
這個時候fun函數的作用就是返回一個int類型的值,可以直接拿來用
比方
int a=fun();
這裡就相當於int a=1;另外一個作用return後面的語句不會執行,我們可以用它來結束程序比方找出三個數種最大的一個數
void main(void)
{
int a,b,c;
if(a>b)
{
if(b>c)
{
return printf("最大值為%d",a);
}
......
}
}
在這裡if(b>c)我們就可以直接得出a是最大了,就沒必要執行下面的語句了,return。這裡就起到了終止語句的作用了
int f(int a)
{
if(a<0)
return -1;
else if(a==0)
return 0;
else
return 1;
}
int b=f(c);
c的值不同函數返回,給b值也就不同,我認為返回值是函數與外界的接口之一,至於所謂的狀態應該是由人來規定的比如當返回值為0我們就知道f()的傳入值c是等於0
的至於是return值還是return表達式都是一個意思因為表達式最終的值也是由表達式計算的最終結果來存儲的返回值就是“函數值”學習C的時候天天都會遇到函數,而函數給一個自變量函數就會有一個函數值對吧.比如說正弦函數sin,sin(x),不同的x值會得到不同的正弦值y=sin(x)就是將函數值賦值給y,函數運算完畢y就有了一個值c語言函數意思一樣的
int f(int x)
{
return 2*x;
//函數返回值為x的2倍
}
int a=f(5);
那麼a是多少呢,就是2*5=10
return的作用是結束正在運行的函數,並返回函數值。return後面可以跟一個常量,變量,或是表達式。
函數的定義一般是這樣的,例如:
int a(int i)
/*第一個int是函數的返回值的類型,也就是return後面跟的值的型,a是函數的稱,括號裡的是傳遞給函數的參數,int是參數的類型,i是參數的名字*/
{
...//省略函數體內容
return b;//b必須與函數頭的返回值一致(此處為int型)
}
簡單函數舉例:
int addOne(int b)
{
return b+1;
}
該函數的作用是取得一個數,將這個數加上1,再將結果返回
調用時這樣:
int result=addOne(2);//此時result的值為3
函數括號裡的參數也可以為變量或能算出值的表達式
以上就是一個基本的函數,一般的函數都有返回值,也就是return後面跟的值,返回值可以為各種數據類型,如:int,float,double,char,a,*a(指針),結構或類(c++)但不是所有函數都有返回值,如果某個函數無返回值,那麼返回值的位置則為“void”關鍵字,此時函數體中無返回值,即無return的值。
但是函數中也可出現return,即一個空的return句子,其作用是使函數立即結束,如
void print()
//括號中為空表示無傳遞參數
{
printf("a");
printf("b");
return;//函數執行到此處結束
printf("c");
}//該函數只執行到return語句處,即屏幕上輸出的為"ab"
union:聲明聯合數據類型
union 關鍵字的用法與struct 的用法非常類似。
union 維護足夠的空間來置放多個數據成員中的“一種”,而不是為每一個數據成員配置空間,在union 中所有的數據成員共用一個空間,同一時間只能儲存其中一個數據成員,所有的數據成員具有相同的起始地址。例子如下:
union StateMachine
{
char character;
int number;
char *str;
double exp;
};
一個union 只配置一個足夠大的空間以來容納最大長度的數據成員,以上例而言,最大長度是double 型態,所以StateMachine 的空間大小就是double 數據類型的大小。
在C++裡,union 的成員默認屬性頁為public。union 主要用來壓縮空間。如果一些數據不可能在同一時間同時被用到,則可以使用union。
一、大小端模式對union 類型數據的影響
下面再看一個例子:
union
{
int i;
char a[2];
}*p, u;
p =&u;
p->a[0] = 0x39;
p->a[1] = 0x38;
p.i 的值應該為多少呢?
這裡需要考慮存儲模式:大端模式和小端模式。
大端模式(Big_endian):字數據的高字節存儲在低地址中,而字數據的低字節則存放在高地址中。
小端模式(Little_endian):字數據的高字節存儲在高地址中,而字數據的低字節則存放在低地址中。
union 型數據所占的空間等於其最大的成員所占的空間。對union 型的成員的存取都是相對於該聯合體基地址的偏移量為0 處開始,也就是聯合體的訪問不論對哪個變量的存取都是從union 的首地址位置開始。如此一解釋,上面的問題是否已經有了答案呢?
二、如何用程序確認當前系統的存儲模式?
上述問題似乎還比較簡單,那來個有技術含量的:請寫一個C 函數,若處理器是Big_endian 的,則返回0;若是Little_endian 的,則返回1。
先分析一下,按照上面關於大小端模式的定義,假設int 類型變量i 被初始化為1。
以大端模式存儲,其內存布局如下圖:
以小端模式存儲,其內存布局如下圖:
變量i 占4 個字節,但只有一個字節的值為1,另外三個字節的值都為0。如果取出低地址上的值為0,毫無疑問,這是大端模式;如果取出低地址上的值為1,毫無疑問,這是小端模式。既然如此,我們完全可以利用union 類型數據的特點:所有成員的起始地址一致。
到現在,應該知道怎麼寫了吧?參考答案如下:
int checkSystem( )
{
union check
{
int i;
char ch;
} c;
c.i = 1;
return (c.ch ==1);
}
現在你可以用這個函數來測試你當前系統的存儲模式了。當然你也可以不用函數而直接去查看內存來確定當前系統的存儲模式。如下圖:
圖中0x01 的值存在低地址上,說明當前系統為小端模式。
不過要說明的一點是,某些系統可能同時支持這兩種存儲模式,你可以用硬件跳線或在編譯器的選項中設置其存儲模式。<喎?http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4KCjxwPsH0uPbOyszio7rU2ng4NiDPtc2zz8KjrMrks/a1xNa1zqq24MnZo788L3A+CgoKCjxwcmUgY2xhc3M9"brush:java;">#include
const :聲明只讀變量
const在C語言中算是一個比較新的描述符,我們稱之為常量修飾符,意即其所修飾
的對象為常量(immutable)。
我們來分情況看語法上它該如何被使用。
1、函數體內修飾局部變量。
例:
void func()
{
const int a=0;
}
首先,我們先把const這個單詞忽略不看,那麼a是一個int類型的局部自動變量,
我們給它賦予初始值0。然後再看const.const作為一個類型限定詞,和int有相同的地位。
const int a;
int const a;
是等價的。於是此處我們一定要清晰的明白,const修飾的對象是誰,是a,和int沒有關系。const 要求他所修飾的對象為常量,不可被改變,不可被賦值,不可作為左值(l-value)。
這樣的寫法也是錯誤的。
const int a;
a=0;
這是一個很常見的使用方式:
const double pi=3.14;
在程序的後面如果企圖對pi再次賦值或者修改就會出錯。
然後看一個稍微復雜的例子。
const int* p;
還是先去掉const 修飾符號。
注意,下面兩個是等價的。
int* p;
int *p;
其實我們想要說的是,*p是int類型。那麼顯然,p就是指向int的指針。
同理
const int* p;
其實等價於
const int (*p);
int const (*p);
即,*p是常量。也就是說,p指向的數據是常量。
於是
p+=8; //合法
*p=3; //非法,p指向的數據是常量。
那麼如何聲明一個自身是常量指針呢?方法是讓const盡可能的靠近p;
int* const p;
const右面只有p,顯然,它修飾的是p,說明p不可被更改。然後把const去掉,可以
看出p是一個指向 int形式變量的指針。
於是
p+=8; //非法
*p=3; //合法
再看一個更復雜的例子,它是上面二者的綜合
const int* const p;
說明p自己是常量,且p指向的變量也是常量。
於是
p+=8; //非法
*p=3; //非法
const 還有一個作用就是用於修飾常量靜態字符串。
例如:
const char* name=David;
如果沒有const,我們可能會在後面有意無意的寫name[4]=’x’這樣的語句,這樣會
導致對只讀內存區域的賦值,然後程序會立刻異常終止。有了 const,這個錯誤就
能在程序被編譯的時候就立即檢查出來,這就是const的好處。讓邏輯錯誤在編譯
期被發現。
const 還可以用來修飾http://blog.csdn.net/a193314/article/details/數組
const char s[]=David;
與上面有類似的作用。
2、在函數聲明時修飾參數
來看實際中的一個例子。
void *memmove(void *dst, const void *src, size_t len);
這是標准庫中的一個函數,用於按字節方式復制字符串(內存)。
它的第一個參數,是將字符串復制到哪裡去(dest),是目的地,這段內存區域必須
是可寫。它的第二個參數,是要將什麼樣的字符串復制出去,我們對這段內存區域只做讀取,不寫。於是,我們站在這個函數自己的角度來看,src 這個指針,它所指向的內存內所存儲的數據在整個函數執行的過程中是不變。於是src所指向的內容是常量。於是就需要用const修飾。
例如,我們這裡這樣使用它。
const char* s=hello;
char buf[100];
memmove(buf,s,6); //這裡其實應該用strcpy或memcpy更好
如果我們反過來寫,
memmove(s,buf,6);
那麼編譯器一定會報錯。事實是我們經常會把各種函數的參數順序寫反。事實是編
譯器在此時幫了我們大忙。如果編譯器靜悄悄的不報錯,(在函數聲明處去掉
const即可),那麼這個程序在運行的時候一定會崩潰。
這裡還要說明的一點是在函數參數聲明中const一般用來聲明指針而不是變量本身。
例如,上面的size_t len,在函數實現的時候可以完全不用更改len的值,那麼是否
應該把len也聲明為常量呢?可以,可以這麼做。我們來分析這麼做有什麼優劣。
如果加了const,那麼對於這個函數的實現者,可以防止他在實現這個函數的時候修
改不需要修改的值(len),這樣很好。
但是對於這個函數的使用者,
1。這個修飾符號毫無意義,我們可以傳遞一個常量整數或者一個非常量整數過
去,反正對方獲得的只是我們傳遞的一個copy。
2。暴露了實現。我不需要知道你在實現這個函數的時候是否修改過len的值。
所以,const一般只用來修飾指針。
再看一個復雜的例子
int execv(const char *path, char *const argv[]);
著重看後面這個,argv.它代表什麼。
如果去掉const,我們可以看出
char * argv[];
argv是一個http://blog.csdn.net/a193314/article/details/數組,它的每個元素都是char *類型的指針。
如果加上const.那麼const修飾的是誰呢?他修飾的是一個http://blog.csdn.net/a193314/article/details/數組,argv[],意思就是
說這個http://blog.csdn.net/a193314/article/details/數組的元素是只讀的。那麼http://blog.csdn.net/a193314/article/details/數組的元素的是什麼類型呢?是char *類型的指
針.也就是說指針是常量,而它指向的數據不是。
於是
argv[1]=NULL; //非法
argv[0][0]='a'; //合法
3、全局變量。
我們的原則依然是,盡可能少的使用全局變量。我們的第二條規則 則是,盡可能多的使用const。如果一個全局變量只在本文件中使用,那麼用法和前面所說的函數局部變量沒有什麼區別。如果它要在多個文件間共享,那麼就牽扯到一個存儲類型的問題。有兩種方式。
1.使用extern
例如
/* file1.h */
extern const double pi;
/* file1.c */
const double pi=3.14;
然後其他需要使用pi這個變量的,包含file1.h
#include file1.h
或者,自己把那句聲明復制一遍就好。這樣做的結果是,整個程序鏈接完後,所有需要使用pi這個變量的共享一個存儲區域。
2.使用static,靜態外部存儲類
/* constant.h */
static const pi=3.14;
需要使用這個變量的*.c文件中,必須包含這個頭文件。
前面的static一定不能少。否則鏈接的時候會報告說該變量被多次定義。
這樣做的結果是,每個包含了constant.h的*.c文件,都有一份該變量自己的copy,
該變量實際上還是被定義了多次,占用了多個存儲空間,不過在加了static關鍵字
後,解決了文件間重定義的沖突。
壞處是浪費了存儲空間,導致鏈接完後的可執行文件變大。但是通常,這個,小小
幾字節的變化,不是問題。
好處是,你不用關心這個變量是在哪個文件中被初始化的。
最後,說說const的作用。
const 的好處,是引入了常量的概念,讓我們不要去修改不該修改的內存。直接的
作用就是讓更多的邏輯錯誤在編譯期被發現。所以我們要盡可能的多使用const。
但是很多人並不習慣使用它,更有甚者,是在整個程序 編寫/調試 完後才補
const。如果是給函數的聲明補const,尚好。如果是給 全局/局部變量補const,那
麼……那麼,為時已晚,無非是讓代碼看起來更漂亮了。
float:聲明浮點型變量或函數
浮點數使用 IEEE(電氣和電子工程師協會)格式。浮點類型的單精度值具有 4 個字節,包括一個符號位、一個 8 位 excess-127 二進制指數和一個 23 位尾數。尾數表示一個介於 1.0 和 2.0 之間的數。由於尾數的高順序位始終為 1,因此它不是以數字形式存儲的。此表示形式為 float 類型提供了一個大約在 3.4E–38 和 3.4E+38 之間的范圍。
您可根據應用程序的需求將變量聲明為 float 或 double。這兩種類型之間的主要差異在於它們可表示的基數、它們需要的存儲以及它們的范圍。下表顯示了基數與存儲需求之間的關系。
浮點類型
浮點變量由尾數(包含數字的值)和指數(包含數字的數量級)表示。
下表顯示了分配給每個浮點類型的尾數和指數的位數。任何 float 或 double 的最高有效位始終是符號位。如果符號位為 1,則將數字視為負數;否則,將數字視為正數。
指數和尾數的長度
由於指數是以無符號形式存儲的,因此指數的偏差為其可能值的一半。對於 float 類型,偏差為 127;對於 double 類型,偏差為 1023。您可以通過將指數值減去偏差值來計算實際指數值。
存儲為二進制分數的尾數大於或等於 1 且小於 2。對於 float 和 double 類型,最高有效位位置的尾數中有一個隱含的前導 1,這樣,尾數實際上分別為 24 和 53 位長,即使最高有效位從未存儲在內存中也是如此。
浮點包可以將二進制浮點數存儲為非標准化數,而不使用剛剛介紹的存儲方法。“非標准化數”是帶有保留指數值的非零浮點數,其中尾數的最高有效位為 0。通過使用非標准化格式,浮點數的范圍可以擴展,但會失去精度。您無法控制浮點數以標准化形式還是非標准化形式表示;浮點包決定了表示形式。浮點包從不使用非標准化形式,除非指數變為小於可以標准化形式表示的最小值。
下表顯示了可在每種浮點類型的變量中存儲的最小值和最大值。此表中所列的值僅適用於標准化浮點數;非標准化浮點數的最小值更小。請注意,在 80x87 寄存器中保留的數字始終以 80 位標准化形式表示;數字存儲在 32 位或 64 位浮點變量(float 類型和 long 類型的變量)中時只能以非標准化形式表示。
浮點類型的范圍
如果存儲比精度更重要,請考慮對浮點變量使用 float 類型。相反,如果精度是最重要的條件,則使用 double 類型。
浮點變量可以提升為更大基數的類型(從 float 類型到 double 類型)。當您對浮點變量執行算術時,通常會出現提升。此算術始終以與具有最高精度的變量一樣高的精度執行。例如,請考慮下列類型聲明:
float f_short;
double f_long;
long double f_longer;
f_short = f_short * f_long;
在前面的示例中,變量 f_short 提升到類型 double 並且與 f_long 相乘;然後,結果捨入到類型 float,然後賦給 f_short。
在以下示例中(使用前面示例中的聲明),將以浮點(32 位)精度對變量執行算術;結果隨後將提升到 double 類型:
f_longer = f_short * f_short;
short :聲明短整型變量或函數
C語言中,short是定義一種整型變量家族的一種。例如short i;表示定義一個短整型的變量i。
依據程序編譯器的不同short定義的字節數不同。
標准定義short短整型變量不得低於16位,即兩個字節。
unsigned, signed:聲明無符號類型變量或函數
所有的整型類型都有兩種變體:signed 和 unsigned。 有時候,要求整型變量能夠存儲負數,有時候則不要求。
沒有使用關鍵字unsigned生命的整型變量都被視為無符號的,這種變量可以為正,也可以為負;而unsigned整型變量只能為正
signed 和 unsigned 整型變量占用的內存空間大小相同,而signed整型變量的部分存儲空間被用於存儲指出該變量是為正還是為負的信息,
因此unsigned整型變量能存儲的最大值為signed整型變量能夠存儲的最大正數的兩倍
例如,如果short變量占用2字節,則unsigned short變量的取值范圍是0 - 65535,而signed short變量的取值范圍內一般為正數,即最大正數為32767,然後,signed short變量也能存儲負數,因此其取值范圍為-32768 - 32767
continue:結束當前循環,開始下一輪循環
continue語句的作用是跳過循環體中剩余的語句而強行執行下一次循環。continue語句只用在for、while、do-while等循環體中,常與if條件語句一起使用,用來加速循環。
對比一下break和continue。
while的用法:
while(表達式1){
……
if(表達式2) break;
……
}
continue的用法:
while(表達式1){
……
if(表達式2) continue;
……
}
#include
int main(void)
{
char c;
while(c!=13){ /*不是回車符則循環*/
c=getch();
if(c==0X1B)
continue; /*若按Esc鍵不輸出便進行下次循環*/
printf("%c\n", c);
}
return 0;
}
for:一種循環語句(可意會不可言傳)
int i;
int b=0;
for(i=0;i<10
void :聲明函數無返回值或無參數,聲明無類型指針(基本上就這三個作用)
goto:無條件跳轉語句
int main(void)
{
int err=-1;
err=func();
if(err<0)
goto error;
return 0;
error:
return -1;
}
sizeof:計算數據類型長度
int main(void)
{
printf("%d\n",sizeof(int));//注意sizeof 計算數據類型的大小
return 0;
}
volatile:說明變量在程序執行中可被隱含地改變
volatile提醒編譯器它後面所定義的變量隨時都有可能改變,因此編譯後的程序每次需要存儲或讀取這個變量的時候,都會直接從變量地址中讀取數據。如果沒有volatile關鍵字,則編譯器可能優化讀取和存儲,可能暫時使用寄存器中的值,如果這個變量由別的程序更新了的話,將出現不一致的現象。下面舉例說明。在DSP開發中,經常需要等待某個事件的觸發,所以經常會寫出這樣的程序:
short flag;
void test()
{
do1();
while(flag==0);
do2();
}
這段程序等待內存變量flag的值變為1(懷疑此處是0,有點疑問,)之後才運行do2()。變量flag的值由別的程序更改,這個程序可能是某個硬件中斷服務程序。例如:如果某個按鈕按下的話,就會對DSP產生中斷,在按鍵中斷程序中修改flag為1,這樣上面的程序就能夠得以繼續運行。但是,編譯器並不知道flag的值會被別的程序修改,因此在它進行優化的時候,可能會把flag的值先讀入某個寄存器,然後等待那個寄存器變為1。如果不幸進行了這樣的優化,那麼while循環就變成了死循環,因為寄存器的內容不可能被中斷服務程序修改。為了讓程序每次都讀取真正flag變量的值,就需要定義為如下形式:
volatile short flag;
需要注意的是,沒有volatile也可能能正常運行,但是可能修改了編譯器的優化級別之後就又不能正常運行了。因此經常會出現debug版本正常,但是release版本卻不能正常的問題。所以為了安全起見,只要是等待別的程序修改某個變量的話,就加上volatile關鍵字。
volatile的本意是“易變的”
由於訪問寄存器的速度要快過RAM,所以編譯器一般都會作減少存取外部RAM的優化。比如:
static int i=0;
int main(void)
{
...
while (1)
{
if (i)
do_something();
}
}
/* Interrupt service routine. */
void ISR_2(void)
{
i=1;
}
程序的本意是希望ISR_2中斷產生時,在main當中調用do_something函數,但是,由於編譯器判斷在main函數裡面沒有修改過i,因此可能只執行一次對從i到某寄存器的讀操作,然後每次if判斷都只使用這個寄存器裡面的“i副本”,導致do_something永遠也不會被調用。如果變量加上volatile修飾,則編譯器保證對此變量的讀寫操作都不會被優化(肯定執行)。此例中i也應該如此說明。
一般說來,volatile用在如下的幾個地方:
1、中斷服務程序中修改的供其它程序檢測的變量需要加volatile;
2、多任務環境下各任務間共享的標志應該加volatile;
3、存儲器映射的硬件寄存器通常也要加volatile說明,因為每次對它的讀寫都可能由不同意義;
另外,以上這幾種情況經常還要同時考慮數據的完整性(相互關聯的幾個標志讀了一半被打斷了重寫),在1中可以通過關中斷來實現,2中可以禁止任務調度,3中則只能依靠硬件的良好設計了。
二、volatile 的含義
volatile總是與優化有關,編譯器有一種技術叫做數據流分析,分析程序中的變量在哪裡賦值、在哪裡使用、在哪裡失效,分析結果可以用於常量合並,常量傳播等優化,進一步可以死代碼消除。但有時這些優化不是程序所需要的,這時可以用volatile關鍵字禁止做這些優化,volatile的字面含義是易變的,它有下面的作用:
1 不會在兩個操作之間把volatile變量緩存在寄存器中。在多任務、中斷、甚至setjmp環境下,變量可能被其他的程序改變,編譯器自己無法知道,volatile就是告訴編譯器這種情況。
2 不做常量合並、常量傳播等優化,所以像下面的代碼:
volatile int i = 1;
if (i > 0) …
if的條件不會當作無條件真。
3 對volatile變量的讀寫不會被優化掉。如果你對一個變量賦值但後面沒用到,編譯器常常可以省略那個賦值操作,然而對Memory Mapped IO的處理是不能這樣優化的。
前面有人說volatile可以保證對內存操作的原子性,這種說法不大准確,其一,x86需要LOCK前綴才能在SMP下保證原子性,其二,RISC根本不能對內存直接運算,要保證原子性得用別的方法,如atomic_inc。
對於jiffies,它已經聲明為volatile變量,我認為直接用jiffies++就可以了,沒必要用那種復雜的形式,因為那樣也不能保證原子性。
你可能不知道在Pentium及後續CPU中,下面兩組指令
inc jiffies
;;
mov jiffies, %eax
inc %eax
mov %eax, jiffies
作用相同,但一條指令反而不如三條指令快。
三、編譯器優化 → C關鍵字volatile → memory破壞描述符zz
“memory”比較特殊,可能是內嵌匯編中最難懂部分。為解釋清楚它,先介紹一下編譯器的優化知識,再看C關鍵字volatile。最後去看該描述符。
1、編譯器優化介紹
內存訪問速度遠不及CPU處理速度,為提高機器整體性能,在硬件上引入硬件高速緩存Cache,加速對內存的訪問。另外在現代CPU中指令的執行並不一定嚴格按照順序執行,沒有相關性的指令可以亂序執行,以充分利用CPU的指令流水線,提高執行速度。以上是硬件級別的優化。再看軟件一級的優化:一種是在編寫代碼時由程序員優化,另一種是由編譯器進行優化。編譯器優化常用的方法有:將內存變量緩存到寄存器;調整指令順序充分利用CPU指令流水線,常見的是重新排序讀寫指令。對常規內存進行優化的時候,這些優化是透明的,而且效率很好。由編譯器優化或者硬件重新排序引起的問題的解決辦法是在從硬件(或者其他處理器)的角度看必須以特定順序執行的操作之間設置內存屏障(memory barrier),linux 提供了一個宏解決編譯器的執行順序問題。
void Barrier(void)
這個函數通知編譯器插入一個內存屏障,但對硬件無效,編譯後的代碼會把當前CPU寄存器中的所有修改過的數值存入內存,需要這些數據的時候再重新從內存中讀出。
2、C語言關鍵字volatile
C語言關鍵字volatile(注意它是用來修飾變量而不是上面介紹的volatile)表明某個變量的值可能在外部被改變,因此對這些變量的存取不能緩存到寄存器,每次使用時需要重新存取。該關鍵字在多線程環境下經常使用,因為在編寫多線程的程序時,同一個變量可能被多個線程修改,而程序通過該變量同步各個線程,例如:
DWORD __stdcall threadFunc(LPVOID signal)
{
int* intSignal=reinterpret_cast(signal);
*intSignal=2;
while(*intSignal!=1)
sleep(1000);
return 0;
}
該線程啟動時將intSignal 置為2,然後循環等待直到intSignal 為1 時退出。顯然intSignal的值必須在外部被改變,否則該線程不會退出。但是實際運行的時候該線程卻不會退出,即使在外部將它的值改為1,看一下對應的偽匯編代碼就明白了:
mov ax,signal
label:
if(ax!=1)
goto label
對於C編譯器來說,它並不知道這個值會被其他線程修改。自然就把它cache在寄存器裡面。記住,C 編譯器是沒有線程概念的!這時候就需要用到volatile。volatile 的本意是指:這個值可能會在當前線程外部被改變。也就是說,我們要在threadFunc中的intSignal前面加上volatile關鍵字,這時候,編譯器知道該變量的值會在外部改變,因此每次訪問該變量時會重新讀取,所作的循環變為如下面偽碼所示:
label:
mov ax,signal
if(ax!=1)
goto label
3、Memory
有了上面的知識就不難理解Memory修改描述符了,Memory描述符告知GCC:
1)不要將該段內嵌匯編指令與前面的指令重新排序;也就是在執行內嵌匯編代碼之前,它前面的指令都執行完畢
2)不要將變量緩存到寄存器,因為這段代碼可能會用到內存變量,而這些內存變量會以不可預知的方式發生改變,因此GCC插入必要的代碼先將緩存到寄存器的變量值寫回內存,如果後面又訪問這些變量,需要重新訪問內存。
如果匯編指令修改了內存,但是GCC 本身卻察覺不到,因為在輸出部分沒有描述,此時就需要在修改描述部分增加“memory”,告訴GCC 內存已經被修改,GCC 得知這個信息後,就會在這段指令之前,插入必要的指令將前面因為優化Cache 到寄存器中的變量值先寫回內存,如果以後又要使用這些變量再重新讀取。
使用“volatile”也可以達到這個目的,但是我們在每個變量前增加該關鍵字,不如使用“memory”方便。
do while :循環語句的循環條件
do-while語句的一般形式為:
do
語句
while(表達式);
這個循環與while循環的不同在於:它先執行循環中的語句,然後再判斷表達式是否為真,如果為真則繼續循環;如果為假,則終止循環。因此,do-while循環至少要執行一次循環語句。其執行過程可用下圖表示。
//用do-while語句計算從1加到100的值
#include
int main(void)
{
int i,sum=0;
i=1;
do
{
sum=sum+i;
i++;
}
while(i<=100);
printf("%d\n",sum);
return 0;
}
//while和do-while循環比較。
//while循環:
#include
int main(void)
{
int sum=0,i;
scanf("%d",&i);
while(i<=10)
{
sum=sum+i;
i++;
}
printf("sum=%d",sum);
return 0;
}
//do-while循環
#include
int main(void)
{
int sum=0,i;
scanf("%d",&i);
do
{
sum=sum+i;
i++;
}
while(i<=10);
printf("sum=%d",sum);
return 0;
}
static :聲明靜態變量
在C語言中,static的字面意思很容易把我們導入歧途,其實它的作用有三條。
(1)先來介紹它的第一條也是最重要的一條:隱藏。
當我們同時編譯多個文件時,所有未加static前綴的全局變量和函數都具有全局可見性。為理解這句話,我舉例來說明。我們要同時編譯兩個源文件,一個是a.c,另一個是main.c。
下面是a.c的內容
char a = 'A'; // global variable
void msg()
{
printf("Hello\n");
}
下面是main.c的內容
int main(void)
{
extern char a; // extern variable must be declared before use
printf("%c ", a);
(void)msg();
return 0;
}
程序的運行結果是:
A Hello
你可能會問:為什麼在a.c中定義的全局變量a和函數msg能在main.c中使用?前面說過,所有未加static前綴的全局變量和函數都具有全局可見性,其它的源文件也能訪問。此例中,a是全局變量,msg是函數,並且都沒有加static前綴,因此對於另外的源文件main.c是可見的。
如果加了static,就會對其它源文件隱藏。例如在a和msg的定義前加上static,main.c就看不到它們了。利用這一特性可以在不同的文件中定義同名函數和同名變量,而不必擔心命名沖突。Static可以用作函數和變量的前綴,對於函數來講,static的作用僅限於隱藏,而對於變量,static還有下面兩個作用。
(2)static的第二個作用是保持變量內容的持久。存儲在靜態數據區的變量會在程序剛開始運行時就完成初始化,也是唯一的一次初始化。共有兩種變量存儲在靜態存儲區:全局變量和static變量,只不過和全局變量比起來,static可以控制變量的可見范圍,說到底static還是用來隱藏的。雖然這種用法不常見,但我還是舉一個例子。
#include
int fun(void){
static int count = 10; // 事實上此賦值語句從來沒有執行過
return count--;
}
int count = 1;
int main(void)
{
printf("global\t\tlocal static\n");
for(; count <= 10; ++count)
printf("%d\t\t%d\n", count, fun());
return 0;
}
程序的運行結果是:
global local static
1 10
2 9
3 8
4 7
5 6
6 5
7 4
8 3
9 2
10 1
(3)static的第三個作用是默認初始化為0。其實全局變量也具備這一屬性,因為全局變量也存儲在靜態數據區。在靜態數據區,內存中所有的字節默認值都是0x00,某些時候這一特點可以減少程序員的工作量。比如初始化一個稀疏矩陣,我們可以一個一個地把所有元素都置0,然後把不是0的幾個元素賦值。如果定義成靜態的,就省去了一開始置0的操作。再比如要把一個字符http://blog.csdn.net/a193314/article/details/數組當字符串來用,但又覺得每次在字符http://blog.csdn.net/a193314/article/details/數組末尾加’\0’太麻煩。如果把字符串定義成靜態的,就省去了這個麻煩,因為那裡本來就是’\0’。不妨做個小實驗驗證一下。
#include
int a;
int main(void)
{
int i;
static char str[10];
printf("integer: %d; string: (begin)%s(end)", a, str);
return 0;
}
程序的運行結果如下
integer: 0; string: (begin)(end)
最後對static的三條作用做一句話總結。首先static的最主要功能是隱藏,其次因為static變量存放在靜態存儲區,所以它具備持久性和默認值0。
下面是中興通訊2012校招筆試題的一道問答題:
1. static全局變量與普通的全局變量有什麼區別 ?
全局變量(外部變量)的說明之前再冠以static 就構成了靜態的全局變量。
全局變量本身就是靜態存儲方式, 靜態全局變量當然也是靜態存儲方式。 這兩者在存儲方式上並無不同。
這兩者的區別在於非靜態全局變量的作用域是整個源程序, 當一個源程序由多個源文件組成時,非靜態的全局變量在各個源文件中都是有效的。 而靜態全局變量則限制了其作用域, 即只在定義該變量的源文件內有效, 在同一源程序的其它源文件中不能使用它。由於靜態全局變量的作用域局限於一個源文件內,只能為該源文件內的函數公用,因此可以避免在其它源文件中引起錯誤。
static全局變量只初使化一次,防止在其他文件單元中被引用;
2. static局部變量和普通局部變量有什麼區別 ?
把局部變量改變為靜態變量後是改變了它的存儲方式即改變了它的生存期。把全局變量改變為靜態變量後是改變了它的作用域,限制了它的使用范圍。
static局部變量只被初始化一次,下一次依據上一次結果值;
3. static函數與普通函數有什麼區別?
static函數與普通函數作用域不同,僅在本文件。只在當前源文件中使用的函數應該說明為內部函數(static修飾的函數),內部函數應該在當前源文件中說明和定義。對於可在當前源文件以外使用的函數,應該在一個頭文件中說明,要使用這些函數的源文件要包含這個頭文件.
static函數在內存中只有一份,普通函數在每個被調用中維持一份拷貝