轉自:http://blog.csdn.net/gongyuan073/article/details/7856878
單片機C51學習筆記
一, C51內存結構深度剖析
二, reg51.頭文件剖析
三, 淺淡變量類型及其作用域
四, C51常用頭文件
五, 淺談中斷
六, C51編譯器的限制
七, 小淡C51指針
八, 預處理命令
一,C51內存結構深度剖析
在編寫應用程序時,定義一個變量,一個數組,或是說一個固定表格,到底存儲在什么地方;
當定義變量大小超過MCU的內存范圍時怎么辦;
如何控制變量定義不超過存儲范圍;
以及如何定義變量才能使得變量訪問速度最快,寫出的程序運行效率最高。以下將一一解答。
1 六類關鍵字(六類存儲類型)
data idata xdata pdata code bdata
code: code memory (程序存儲器也即只讀存儲器)用來保存常量或是程序。code memory 采用16位地址線編碼,可以是在片內,或是片外,大小被限制在64KB
作用:定義常量,如八段數碼表或是編程使用的常,在定義時加上code 或明確指明定義的常量保存到code memory(只讀)
使用方法:
char code table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90};
此關鍵字的使用方法等同於const
data data memory (數據存儲區)只能用於聲明變量,不能用來聲明函數,該區域位於片內,采用8位地址線編碼,具有最快的存儲速度,但是數量被限制在128byte或更少。
使用方法:
unsigned char data fast_variable=0;
idata idata memory(數據存儲區)只能用於聲明變量,不能用來聲明函數. 該區域位於片內,采用8位地址線編碼,內存大小被限制在256byte或更少。該區域的低地址區與data memory地址一致;高地址區域是52系列在51系列基礎上擴展的並與特殊功能寄存器具有相同地址編碼的區域。即:data memory是idata memory的一個子集。
xdata xdata memory 只能用於聲明變量,不能用來聲明函數,該區域位於MCU
外部,采用16位地址線進行編碼,存儲大小被限制在64KB以內。
使用方法:
unsigned char xdata count=0;
pdata pdata memory 只能用於聲明變量,不能用來聲明函數,該區域位於MCU外部,采用8位地址線進行編碼。存儲大小限制在256byte. 是xdata memory的低256byte。為其子集。
使用方法
unsigned char pdata count=0;
bdata bdata memory 只能用於聲明變量,不能用來聲明函數。該區域位於8051內部位數據地址。定義的量保存在內部位地址空間,可用位指令直接讀寫。
使用方法:
unsigned char bdata varab=0
注:有些資料講,定義字符型變量時,在缺省unsigned 時,字符型變量,默認為無符號,與標准C不同,但我在Keil uVision3中測試的時候發現並非如此。在缺省的情況下默認為有符號。或許在以前的編譯器是默認為無符號。所以看到有的資料上面這樣講的時候,要注意一下,不同的編譯器或許不同。所以我們在寫程序的時候,還是乖乖的把unsigned signed 加上,咱也別偷這個懶。
2函數的參數和局部變量的存儲模式
C51 編譯器允許采用三種存儲器模式:SMALL,COMPACT 和LARGE。一個函數的存儲器模式確定了函數的參數的局部變量在內存中的地址空間。處於SMALL模式下的函數參數和局部變量位於8051單片機內部RAM中,處於COMPACT和LARGE模式下的函數參數和局部變量則使用單片機外部RAM。在定義一個函數時可以明確指定該函數的存儲器模式。方法是在形參表列的后面加上一存儲模式。
示例如下:
#pragma large //此預編譯必須放在所有頭文前面
int func0(char x,y) small;
char func1(int x) large;
int func2(char x);
注:
上面例子在第一行用了一個預編譯命令#pragma 它的意思是告訴c51編譯器在對程序進行編譯時,按該預編譯命令后面給出的編譯控制指令LARGE進行編譯,即本例程序編譯時的默認存儲模式為LARGE.隨后定義了三個函數,第一個定義為SMALL存儲模式,第二個函數定義為LARGE第三個函數未指定,在用C51進行編譯時,只有最后一個函數按LARGE存儲器模式處理,其它則分別按它們各自指定的存儲器模式處理。
本例說明,C51編譯器允許采用所謂的存儲器混合模式,即允許在一個程序中將一些函數使用一種存儲模式,而其它一些則按另一種存儲器模式,采用存儲器混合模式編程,可以充分利用8051系列單片機中有限的存儲器空間,同時還可以加快程序的執行速度。
3絕對地址訪問 absacc.h(相當重要)
#define CBYTE ((unsigned char volatile code *) 0)
#define DBYTE ((unsigned char volatile data *) 0)
#define PBYTE ((unsigned char volatile pdata *) 0)
#define XBYTE ((unsigned char volatile xdata *) 0)
功能:CBYTE 尋址 CODE區
DBYTE 尋址 DATA區
PBYTE 尋址 XDATA(低256)區
XBYTE 尋址 XDATA區
例: 如下指令在對外部存儲器區域訪問地址0x1000
xvar=XBYTE[0x1000];
XBYTE[0x1000]=20;
#define CWORD ((unsigned int volatile code *) 0)
#define DWORD ((unsigned int volatile data *) 0)
#define PWORD ((unsigned int volatile pdata *) 0)
#define XWORD ((unsigned int volatile xdata *) 0)
功能:與前面的一個宏相似,只是它們指定的數據類型為unsigned int .。
通過靈活運用不同的數據類型,所有的8051地址空間都是可以進行訪問。
如
DWORD[0x0004]=0x12F8;
即內部數據存儲器中(0x08)=0x12; (0x09)=0xF8
注:用以上八個函數,可以完成對單片機內部任意ROM和RAM進行訪問,非常方便。還有一種方法,那就是用指鍾,后面會對C51的指針有詳細的介紹。
4寄存器變量(register)
為了提高程序的執行效率,C語言允許將一些頻率最高的那些變量,定義為能夠直接使用硬件寄存器的所謂的寄存器變量。定義一個變量時,在變量類型名前冠以“register” 即將該變量定義成為了寄存器變量。寄存器變量可以認為是一自動變量的一種。有效作用范圍也自動變量相同。由於計算機寄存器中寄存器是有限的。不能將所有變量都定義成為寄存器變量,通常在程序中定義寄存器變量時,只是給編譯器一個建議,該變量是否真正成為寄存器變量,要由編譯器根據實際情況來確定。另一方面,C51編譯器能夠識別程序中使用頻率最高的變量,在可能的情況下,即使程序中並未將該變量定義為寄存器變量,編譯器也會自動將其作為寄存器變量處理。被定義的變量是否真正能成為寄存器變量,最終是由編譯器決定的。
5內存訪問雜談
1指鍾
指鍾本身是一個變量,其中存放的內容是變量的地址,也即特定的數據。8051的地址是16位的,所以指針變量本身占用兩個存儲單元。指針的說明與變量的說明類似,僅在指針名前加上“*”即可。
如 int *int_point; 聲明一個整型指針
char *char_point; 聲明一個字符型指針
利用指針可以間接存取變量。實現這一點要用到兩個特殊運算符
& 取變量地址
* 取指針指向單元的數據
示例一:
int a,b;
int *int_point; //定義一個指向整型變量的指針
a=15;
int_point=&a; //int_point指向 a
*int_point=5; //給int_point指向的變量a 賦值5 等同於a=5;
示例二:
char i,table[6],*char_point;
char_point=table;
for(i=0;i<6;i++)
{
char_point=i;
char_point++;
}
注:
指針可以進行運算,它可以與整數進行加減運算(移動指針)。但要注意,移動指針后,其地址的增減量是隨指針類型而異的,如,浮點指針進行自增后,其內部將在原有的基礎上加4,而字符指針當進生自增的時候,其內容將加1。原因是浮點數,占4個內存單元,而字符占一個字節。
宏晶科技最新一代STC12C5A360S2系列,每一個單片機出廠時都有全球唯一身份證號碼(ID號),用戶可以在單片機上電后讀取內部RAM單元F1H~F7H的數值,來獲取此單片機的唯一身份證號碼。使用MOV @Ri 指令來讀取。下面介紹C51 獲取方法:
char id[7]={0};
char i;
char idata *point;
for(i=0;i<7;i++)
{
id[i]=*point;
point++;
}
(此處只是對指針做一個小的介紹,達到訪問內部任何空間的方式,后述有對指針使用的詳細介紹)
2對SFR,RAM ,ROM的直接存取
C51提供了一組可以直接對其操作的擴展函數
若源程序中,用#include包含頭文件,io51.h 后,就可以在擴展函數中使用特殊功能寄存器的地址名,以增強程序的可讀性:
注 此方法對SFR,RAM,ROM的直接存取不建議使用.因為,淡io51.h這個頭文件在KEIL中無法打開,可用指針,或是采用absacc.h頭文件,
3 PWM與PCA
STC12系列有兩路PWM/PCA
PWM:(Pulse Width Modulation)脈寬調制,是一種使用程序來控制波形占空比,周期,相位波形的技術。
PCA:(Programmable Counter Array)可編程計數陣列,它比通常的定時/計數器的定時能力強,需要CPU的干預少。其優勢一是軟件簡單,二是精度大有提高。
二, reg51.頭文件剖析
我們平時寫單片機應用程序的時候,所使用的頭文件大多都是用的的reg51.h或是用reg52.h。會寫C51的人都會用,但對其頭文件內部的定義有所了解的人確並不多。
下面對其內部做詳細解釋,方便讀者作進一步的了解,並能運用各類型號的單片機。因為增強型號的單片機的增強功能都是通過特殊功能寄存器控制。
打開 reg52.h 頭文件,會發現是由大量的 sfr ,sbit的聲明組成,甚至於還有sfr16.其實這樣的聲明都是與單片機內部功能寄存器(特殊功能寄存器)聯系起來的,下面對其做出詳細解釋
sfr: 聲明變量
SFR 聲明一個變量,它的聲明與其它的C變量聲明基本相同,唯一的區別,SFR在聲明的同時為其指定特殊功能寄存器作為存儲地址,而不同於C變量聲明的整型,字符型等等由編譯器自動分配存儲空間。
如reg52.h頭文件,第一條聲明就是sfr P0 = 0x80;
此處聲明一個變量P0,並指定其存儲地址為特殊功能寄存器0x80;,在加入reg52.h頭文件后。編寫應用程序時P0就可以直接使用而無需定義,對P0的操作就是,對內部特殊功能寄存器(0x80對應用MCU的P0口)的操作,可進行讀寫操作。
如果將第一條聲明改為sfr K0 = 0x80; 那么,如果要把單片機的P0口全部拉低,則不能寫P0=0x00;而應保存后再在應用程序中寫成K0=0x00;否則編譯器會提示“P0為未定義標識符”
使用方法:
sfr [variable] = [address] //為變量分配一個特殊功能寄存器。
1 等號右邊,只能是十進制,十六進制整型的數據常量,,不允許帶操作符的表達式
經典的8051內核支持的SFR地址從0x80H~0xFF 飛利浦80C51MX系列0x180H~0x1FF
2 SFR不能聲明於任何函數內部,包括main函數。只能聲明於函數外。
3 用SFR聲明一個變量后,不能用取地址運算符&獲取其地址, 編譯無法通過,編譯器會提示非法操作。
4 有一點須特別注意,51內核0x80~0xff,為特殊功能寄存器地址區間,但並不是所有的地址都有定義,如果說你所用的MCU芯片上對於某個地址沒有定義,那么用sfr在定義變量的時候,不要把變量的地址分配到未定義的特殊功能寄存器上,雖然編譯時能通過,用KEIL仿真時貌似是沒有問題,但下載到芯片里運行時,是會出問題的。比如說,向一個未定義的特殊功能寄存器執行讀操作,讀出來的就是一個未知的數。(讀者可自行測試,先把串口通信調通,然后做一個簡單的人機交互。讀出一個數后,再發給計算機,用串口調試助手或是串口監控查看。這用方法在仿真的時候很有用。)所以具體那些特殊功能寄存器能夠用,就要查看你使用的芯片手冊。
5 若遇到增強性的單片機,只要知道其擴展的特殊功能寄存器的地址,用SFR定
就可以很方便進行編程。
sbit: 聲明變量
sbit 同樣是聲明一個變量,和SFR 使用方法類似,但是SBIT是用來聲明一個位變量,因為,在51系列的應用中,非常有必要對SFR的單個位進行存取,而通過bit 數據類型,使其具備位尋址功能。
如,在reg52.h中有如下聲明
sfr IE = 0xA8;
sbit EA = IE^7;
sbit ET2 = IE^5; //8052 only
sbit ES = IE^4;
sbit ET1 = IE^3;
sbit EX1 = IE^2;
sbit ET0 = IE^1;
sbit EX0 = IE^0;
所以,對EA的操作即是對IE最高位的操作。
但如果想讓 SP DPL DPH PCON TMOC TL0 TL1 TH0 TH1 SBUF這些特殊功能寄存器具備位尋址,采用上述如IE類似的定義,是不行的,雖然修改后,在編譯的時候不會出現錯誤,但只要用到你定義的位變量名時就會出錯。原因是,只有特殊功能寄存器的地址是8的倍數(十六進制以0或8結尾)才能進行位尋址。
打開reg52.h頭文件可以看到,所有用sbit聲明了的特殊功能寄存器的地址均是以0或8結尾
如硬要達到上述要求,可用帶參的宏定義來完成。此處不做詳細說明(意義並不大)。
下面對sbit的使用做詳細介紹:
隨着8051的應用,非常有必要對特殊功能寄存器的單個bit位進行存取,C51編譯器通過sbit 數據類型,提供了對特殊功能寄存器的位操作。
以下是sbit的三種應用形式:
一, sbit name = sfr-name^bit-position;
sfr PSW =0xD0;
sfr IE =0xA8;
sbit OV= PSW^2;
sbit CY=PSW^7;
sbit EA= IE^7;
二, sbit name= sft-address^bit-position;
sbit OV =0xD0^2;
sbit CY =0xD0^7;
sbit EA =0xA8^7;
三, sbit name= sbit-address;
sbit OV =0xD2;
sbit CY =0xD7;
sbit EA =0xAF;
現對上述三種形式的聲明做必要的說明
第一種形式sbit name = sfr-name^bit-position;如sbit OV= PSW^2; 當中的這個特殊功能寄存器必須在此之前已經用sfr 定義,否則編譯會出錯。
bit-position范圍從0~7;
第二種形式 sbit name= sft-address^bit-position如sbit OV =0xD0^2; 與第一種形式不同之外在於,此處直接使用PSW的地址.第一種形式須先定義PSW
第三種形式. sbit name= sbit-address 如sbit OV =0xD2 是直接用的OV的地址
OV的地址計算方式,是OV所在的寄存器地址加上OV的bit-position
注意:
不是所有的SFR都可位尋址。只有特殊功能寄存器的地址是8的倍數(十六進制以0或8結尾)才能進行位尋址,並且sbit聲明的變量名,雖可以是任意取,但是最好不要以下划線開頭,因為以下划線開頭的都保留給了C51的頭文件做保留字。
sfr16: 聲明變量
許多8051的派生型單片機,用兩個連續地址的特殊功能寄存器,來存儲一個16bit的值。例如,8052就用了0xCC和0xCD來保存定時/計數寄存器2的高字節和低字節。編譯器提供sfr16這種數據類型,來保存兩個字節的數據。虛擬出一個16bit的寄存器。
如下:
sfr16 T2 = 0xCC
存儲方面為小端存儲方式,低字節在前,高字節在后。定義時,只寫低字節地址,如上,則定義T2為一個16位的特殊功能寄存器。 T2L= 0CCh, T2H= 0CDh
使用方法:
sfr [variable] = [low_address]
1 等號右邊,只寫兩個特殊功能寄存器的低地址,且只能是十進制,十六進制的整型數據常量,不允許帶操作符的表達式
2 SFR不能聲明於任何函數內部,包括main函數。只能聲明於函數外。
3 用SFR聲明一個變量后,不能用取地址運算符&獲取其地址, 編譯無法通過,編譯器會提示非法操作。
4 當你向一個sfr16寫入數據的時候,KEIL CX51 編譯器生成的代碼,是先寫高字節,后寫低字節,(可通過返匯編窗口查看)在有些情況下,這並非我們所想要的操作順序。使用時,須注意。
5 當你所要寫入sfr16的數據,當是高字節先寫還是低字節先寫非常重要的時候,就只能用sfr 這個關鍵字來定義,並且任意時刻只保存一個字節,這樣操作才能保證寫入正確。
三, 淺淡變量類型及其作用域
變量可分為 1.局部變量
(按變量的有效作用范圍划分)
2.全局變量
1.局部變量
是指函數內部(包括main函數)定義的變量,僅在定義它的那個函數范圍內有效,不同函數可使用相同的局部變量名,函數的形式參數也屬於局部變量,在一個函數的內部復合語句中也可以定義局部變量,該局部變量只在該復合語合中有效。
2.全局變量
是指函數外部定義的變量,以稱外部變量。可為多個函數共同使用,其有效作用范圍是從它定義開始到整個程序文件結束。如果全局變量,定義在一個程序文件的開始處,則在整個程序文件范圍都可以使用它,如果一個全局變量不是在程序文件的開始處定義,但又希望在它定義之前的函數中引用該變量,這時應在引用該變量的函數中用關鍵字extern將其聲明為“外部變量”。另個,如果在一個程序模塊文件中引用另一個程序模塊文件中定義的變量時,也必須用extern進行說明。
外部變量的說明與外部變量的定義是不同的,外部變量定義只能有一次,定義的位置在所有函數之外,而同一個程序文件中(不是指模塊文件)的外部變量聲明可以有多次,聲明的置在需要引用該變量的函數之內,外部變量的聲明的作用只是聲明該變量是一個已經在外部定義過了的變量而已。
如在同一個程序文件中,全局變量與局部變量同名,則在局部變量的有效作用范圍之內,全局變量不起作用,也就是說,局部變量的優先級比全局變量高。
在編寫C語言程序時,不是特別必要的地方一般不要使用全局變量,而應當盡可能的使用局部變量。因為局部變量只在使用它的時候,才為其分配內存單元,而全局變量在整個程序的執行過程中都要占用內存單元,且當全局變量使用過多時,會降低程序的可讀性。
變量的存儲種類
1自動變量(auto)
定義變量時,在變量類型名前加上 “auto” ,自動變量是C語言中使用最為廣泛的一類變量,在函數體內部或是復合語句內部定義的變量,如果省略了存儲種類說明,則該變量默認為自動變量。
例如:
{ 等價於 {
char x; auto char x;
int y; auto int y;
…… ……
} }
注:
自動變量的作用范圍在定義它的函數體或是復合語句內部,只有在定義它的函數內被調用,或是定義它的復合語句被執行時,編譯器才會為其分配內存空間,開始其生存期。當函數調用結束返回,或復合語句執行結束,自動變量所占用的內存空間就被釋放,變量的值當然也就不復存在,其生存期結束。當函數再次調用,或是復合語句被再次執行時,編譯器又會為其內部的自動變量重新分配內存空間。但不會保留上一次運行的值。而必須被重新分配。因此自動變量始終是相對於函數或復合語句的局部變量。
2 外部變量(extern)
用說明符“extern”定義的變量稱為外部變量。按缺省規則,凡是在所有函數之前,在函數外部定義的變量都是外部變量,定義時可以不寫extern說明符,但是一個函數體內說明一個已在該函數體外或別的程序模塊文件中定義過的外部變量時,剛必須要使用extern說明符。外部變量定義后,它就被分配了固定的內存空間。外部變量的生存期為程序的整個執行時間。 外部變量的存儲不會隨函數或復合語句執行完畢而釋放,因此外部變量屬於全局變量。
C語言允許將大型程序分解為若干個獨立的程序模塊文件,各個模塊可分別進行編譯,然后再將它們連接在一起,如果某個變量需要在所有程序模塊文件中使用,只要在一個程序模塊文件中將該變量定義成全局變量,而在其它程序模塊文件中用extern聲明該變量是已被定義過的外部變量就可以了。
函數是可以相互調用的,定義函數時,如果冠以關鍵字extern 即將其明確定義為一個外部函數。例如 extern int func2(char a,b) 。如果在定義函數時省略關鍵字extern,則隱含為外部函數。如果在調用一個在本程序模塊文件以外的其它模塊文件所定義的函數,則必須要用關鍵字extern說明被調用的函數是一個外部函數。對於具有外部函數相互調用的多模塊程序,可用C51編譯器分別對各個模塊文件進行編譯,最后再用L51連接定位器將它們連接成為一個完整的程序。
如下為一個多模塊程序
程序模塊1,文件名為file1.c
#include<stdio.h>
int x=5;
void main()
{
extern void fun1( );
extern viod fun2(int y);
fun1( );
fun1( );
fun1( );
printf( “\n%d %d\n”,x,fun2(x));
}
程序模塊2,文件名為file2.c
#include<stdio.h>
extern int x;
void fun1( )
{
static int a=5; //靜態變量只在第一次調用函數時賦值,退出函數時
//會保留上次的值,下次調用不再重新賦值。
int b=5;
printf(“%d %d %d |”,a,b,x);
a-=2;
b-=2
x-=2;
printf(“%d %d %d |”,a,b,x);
}
int fun2(int y)
{
return(35*x*y);
}
程序執行如果如下:
5 5 5 | 3 3 3
3 5 3 | 1 3 1
1 5 1 | -1 3 1
-1 35
注:
C語言不允許在一個函數內嵌套定義另一個函數。為了能夠訪問不同文件中各個函數的變量,除了可以采用參數傳遞的方法外,還可以采用外部變量的方法,上面的例子就說了這一點。不過,盡管使用外部變量在不同函數之間傳遞數據有時比使用函數參數傳遞更為方便,不過當外部變量過多時,會增加程序的調試排錯的困難。使得程序不便於維護。別外不通過參數傳遞直接在函數中改變全局變量的值,有時還會發生一些意想不到的副作用。因些最好還是使用函數參數來傳遞數據。
3寄存器變量(register)
為了提高程序的執行效率,C語言允許將一些頻率最高的那些變量,定義為能夠直接使用硬件寄存器的所謂的寄存器變量。定義一個變量時,在變量類型名前冠以“register” 即將該變量定義成為了寄存器變量。寄存器變量可以認為是一自動變量的一種。有效作用范圍也自動變量相同。由於計算機寄存器中寄存器是有限的。不能將所有變量都定義成為寄存器變量,通常在程序中定義寄存器變量時,只是給編譯器一個建議,該變量是否真正成為寄存器變量,要由編譯器根據實際情況來確定。另一方面,C51編譯器能夠識別程序中使用頻率最高的變量,在可能的情況下,即使程序中並未將該變量定義為寄存器變量,編譯器也會自動將其作為寄存器變量處理。被定義的變量是否真正能成為寄存器變量,最終是由編譯器決定的。
4靜態變量(static)
使用存儲種類說明符“static”定義的變量為靜態變量,在上面模塊2程序文件中使用了一個靜態變量:static int a =5 ;由於這個變量是在函數fun1( )內部定義,因此稱為內部靜態變量或局部靜態變量。局部靜態變量始終都是存在的,但只有在定義它的函數內部進行訪問,退出函數之后,變量的值仍然保持,但不能進行訪問。
還有一種全局靜態變量,它是在函數外部被定義的。作用范圍從它的定義點開始,一直到程序結束,當一個C語言程序由若干個模塊文件所組成時,全局靜態變量始終存在,但它只能在被定義的模塊文件中訪問,其數據值可為該模塊文件內的所有函數共享,退出該文件后,雖然變量的值仍然保持着,但不能被其它模塊文件訪問。在一個較大的程序中,這就方便了多人設計時,各自寫的程序模塊不會被別的模塊文件所引用。
全局靜態變量和單純的全局變量,在編譯時就已經為期分配了固定的內存空間,只是他們的作用范圍不同而已。
局部靜態變量是一種在兩次函數調用之間仍能保持其值的局部變量。
如下,局部變量的使用——計算度輸出1~5的階乘值。
#include<stdio.h>
int fac( int n)
{
static int f=1;
f=f*n;
return(f);
}
main( )
{
int i;
for(i=1;i<=5;i++)
printf(“%d!=%d\n”,i,fac(i));
}
程序執行結果
1!=1
2!=2
3!=6
4!=24
5!=120
注:
在這個程序中一共調用了5次計算階乘的函數fac(i),每次調用后輸出一個階乘值i!,同時保留了這個i!值,以便下次再乘(i+1).由此可見,如果要保留函數上一次調用結束時的值,或是在初始化之后變量只被引用而不改變其值,則這時使用局部靜態變量;較為方便,以免在每調用時都要重新進行賦值,但是,使用局部靜態變量需要占用較多的內存空間,而且降低了程序的可讀性,因此並不建議多用局部靜態變量。
靜態函數:
對於函數也可以定義成為具為靜態存儲種類的屬性,定義函數時在函數名前冠以關鍵字static即將其定義為一個靜態函數。例如static int func1(char x, y)函數是外部型的,使用靜態函數可以使該函數只局限於當前定義它的模塊文件中。其它模塊文件是不能調用它的。換名話說,就是在其它模塊文件中可以定義與靜態函數完全同名的另一個函數。不會因為程序中存在相同的函數名而發生函數調用時的混亂。 這一點對於進行模塊化程序設計是很有用的。
四, C51常用頭文件
在KEIL 中,對於單片機所使用的頭文件,除了reg51 reg52以外,還有一些從各芯片制商的官網下載與reg51,reg52功能類似的頭文件,需了解透外,還要對各類型單片機均可通用且相當有用的的頭文件,做相應的了解。因為,內部所包含的函數與宏定義,可以及大的方便我們編寫應用程序。
1字符函數 ctype.h
1 extern bit isalpha(char);
功能:檢查參數字符是否為英文字母,是則返回1
2 extern bit isalnum(char)
功能:檢查字符是否為英文字母或數字字符,是則返回1
3 extern bit iscntrl(char)
功能:檢查參數值是否在0x00~0x1f 之間或等於0x7f,是則返回1
4 extern bit isdigit(char)
功能: 檢查參數是否為數字字符,是則返回1
5 extern bit isgraph(char)
功能: 檢查參數值是否為可打印字符,是則返回1,可打印字符為0x21~0x7e
6 extern bit isprint(char)
功能:除了與isgraph相同之外,還接受空格符0x20
7 extern bit ispunct(char)
功能:不做介紹。
8 extern bit islower(char)
功能:檢查參數字符的值是否為小寫英文字母,是則返回1
9 extern bit isupper(char)
功能:檢查參數字符的值是否為大寫英文字母,是則返回1
10 extern bit isspace(char)
功能:檢查字符是否為下列之一,空格,制表符,回車,換行,垂直制表符和送紙。如果為真則返回1
11 extern bit isxdigit(char)
功能:檢查參數字符是否為16進制數字字符,是則返回1
12 extern char toint(char)
功能:將ASCII字符0~9 a~f(大小寫無關)轉換成對應的16進制數字,
返回值00H~0FH
13 extern char tolower(char)
功能:將大寫字符轉換成小寫形式,如字符變量不在A~Z之間,則不作轉換而直接返回該字符
14 extern char toupper(char)
功能:將小寫字符轉換成大寫形式,如字符變量不在a~z之間,則不作轉換而直接返回該字符
15 define toascii(c) ((c)&0x7f)
功能:該宏將任何整形數值縮小到有效的ASCII范圍之內,它將變量和0x7f相與從而去掉第7位以上的所有數位
16 #define tolower(c) (c-‘A’+’a’)
功能:該宏將字符與常數0x20 逐位相或
17 #define toupper(c) ((c)-‘a’+’A’)
功能:該宏將字符與常數0xdf 逐位相與
2數學函數 math.h
extern int abs (int val);
extern char cabs (char val);
extern long labs (long val);
extern float fabs (float val);
功能:返回絕對值。上面四個函數,除了形參和返回值不一樣之外,
其它功能完全相同。
extern float exp (float val);
extern float log (float val);
extern float log10 (float val);
功能: exp 返回eval
log 返回 val 的自然對數
log10 返回 以10為底,val的對數
extern float sqrt (float val);
功能: 返回val的正平方根
extern int rand();
extern void srand(int n);
功能: rand返回一個0到32767之間的偽隨機數,srand用來將隨機數發生器初始化成一個已知的(期望)值。
Keil uVision3中的math.h庫中,不包含此函數。
extern float sin (float val);
extern float cos (float val);
extern float tan (float val);
功能: 返回val的正弦,余弦,正切值。val為弧度 fabs(var) <=65535
extern float asin (float val);
extern float acos (float val);
extern float atan (float val);
extern float atan2 (float y, float x);
功能: asin 返回val的反正弦值。acos 返回val的反余弦值。
atan 返回val的反正切值。
asin atan acos的值域均為 -π/2~+π/2
atan2返回x/y,的反正切值,其值域為-π~+π
extern float sinh (float val);
extern float cosh (float val);
extern float tanh (float val);
功能:cosh返回var的雙曲余弦值,sinh返回var的雙曲正弦值,
tanh返回var的雙曲正切值。
extern float ceil (float val);
功能: 向上取整,返回一個大於val的最小整數。
extern float floor (float val);
功能: 向下取整,返回一個小於val的最大整數。
extern float pow (float x, float y);
功能: 計算計算xy的值。當(x=0,y<=0)或(x<0.y不是整數)時會發生錯誤。
extern void fpsave(struct FPBUF *p)
extern void fprestore(struct FPBUF *p)
功能:fpsave 保存浮點了程序的狀態,fprestore恢復浮點子程序的原始狀態,當中斷程序中需要執行浮點運算時,這兩個函數是很有用的。
注: Keil uVision3中的math.h庫中,不包含此函數。
3絕對地址訪問 absacc.h
#define CBYTE ((unsigned char volatile code *) 0)
#define DBYTE ((unsigned char volatile data *) 0)
#define PBYTE ((unsigned char volatile pdata *) 0)
#define XBYTE ((unsigned char volatile xdata *) 0)
功能:CBYTE 尋址 CODE區
DBYTE 尋址 DATA區
PBYTE 尋址 XDATA(低256)區
XBYTE 尋址 XDATA區
例: 如下指令在對外部存儲器區域訪問地址0x1000
xvar=XBYTE[0x1000];
XBYTE[0x1000]=20;
#define CWORD ((unsigned int volatile code *) 0)
#define DWORD ((unsigned int volatile data *) 0)
#define PWORD ((unsigned int volatile pdata *) 0)
#define XWORD ((unsigned int volatile xdata *) 0)
功能:與前面的一個宏相似,只是它們指定的數據類型為unsigned int .。
通過靈活運用不同的數據類型,所有的8051地址空間都是可以進行訪問。
如
DWORD[0x0004]=0x12F8;
即內部數據存儲器中(0x08)=0x12; (0x09)=0xF8
4 內部函數 intrins.h
extern unsigned char _cror_ (unsigned char var, unsigned char n);
extern unsigned int _iror_ (unsigned int var, unsigned char n);
extern unsigned long _lror_ (unsigned long var, unsigned char n);
功能:將變量var 循環右移 n 位。
上三個函數的區別在於,參數及返回值的類型不同
extern unsigned char _crol_ (unsigned char var, unsigned char n);
extern unsigned int _irol_ (unsigned int var, unsigned char n);
extern unsigned long _lrol_ (unsigned long var, unsigned char n);
功能:將變量var 循環左移 n 位。
上三個函數的區別在於,參數及返回值的類型不同
例如:
#include<intrins.h>
void main()
{
unsigned int y;
y=0x0ff0;
y=_irol_(y,4); //y=0xff00
y=_iror_(y,4); //y=0x0ff0
}
void _nop_(void);
功能:_nop_產生一個8051單片機的NOP指令,C51編譯器在程序調用_nop_ 函數的地方,直接產生一條NOP指令。
五,中斷淺談
| 0 |
外中斷0 |
| 1 |
定時器0 |
| 2 |
外中斷1 |
| 3 |
定時器1 |
| 4 |
串行口 |
定義中斷函數如下
void timer1() interrupt 3 using 1
{
……
……
}
注:在上述中,建議不要加using 選項。因為using是指定寄存器的組數。
C51中斷程序編寫要求:
1 中斷函數不能進行參數傳遞,否則,將導致編譯出錯
2 中斷中,不能包含任何參數聲明,否則,將導致編譯出錯。
3 中斷函數沒有返回值,如果企圖定義一個返回值將得到不正確的結果,因些
建議在定義中斷函數的時將其定義為void 類型,明確說明沒有返回值。
4 任何情況下都不能直接調用中斷函數,否則會主生編譯出錯。
5 如果中斷函數中用到了浮點運算,必須保存浮點寄存器的狀態。當沒有其它的程序執行浮點運算時(即只有中斷中用到浮點運算),可以不用保存。
6 如果中斷函數中調用了其它函數,則被調用的函數所使用的寄存器組必須與中斷函數相同,用戶必須保證按要求使用相同的寄存器組,否則會產生不正確的結果,這一點必須引起足夠的注意,如果定義中斷函數時沒有使用using選項,則由編譯器選擇一個寄存器組作絕對寄存器訪問。另外,不斷的產生不可預測,中斷函數對其它函數的調用可能形成遞規調用,需要時,可將被中斷調用的其它函數定義為再入函數。
函數的遞規調用與再入函數:
函數的遞規調用: 在調用一個函數的過程中雙直接或間接的調用該函數本身
再入函數: 一種可以在函數體內直接或間接調用其自身的一種函數。
C51編譯器采用一個擴展關鍵字reentrant 作為定義函數時的選項,需要將一個函數定義為再入函數時,只要在函數名后加上關鍵字reentrant即可。空不空格以及空幾格都無所謂。
再入函數小談:
再入函數可被遞歸調用,無論何時,包括中斷服務函數在內的任何函數都可調用再入函數。與非再入函數的參數傳遞和局部就是的存儲分配方法不同,C51編譯器為每個再入函數都生成一個模擬棧。模擬棧所在的存儲器空間根據再入函數的存儲模式的不同,可以分配到DATA,PDATA 或XDATA。
對再入函數有如下規定:
1. 再入函數不能傳送bit類型的參數。也不能定義一個局部位變量,再入函數不能包括位操作以及8051系列單片機的可位尋址區。
2. 與PL/ M51兼容的函數,不能具有reentrant屬性,也不能調用再入函數。
3. 編譯時,在存儲器模式的基礎上,為再入函數在內部或外部存儲中建立一個模擬堆棧區,稱為再入棧,再入函數的局部變量及參數被放在再入棧中,從而使得再入函數可以進行遞規調用,。再非再入函數的局部變量被放在再入棧之外的暫存區內,如果對非再入函數進行遞規調用,則上次調用時使用的局部變量數據將被覆蓋。
4. 在同一個程序中可以定義和使用不同存儲器模式的再入函數,任意模式的再入函數不能調用不同模式的再入函數,但可以任意調用非再入函數。
5. 在參數的傳遞上,實際參數,可以傳遞給間接調用的再入函數,無再入屬性的間接調用函數不能包含調用參數。但是可以使用定義的全局變量來進行參數傳遞。
六, C51編譯器的限制
1 名字最長為255個字符,但只有前32個字符有效,盡管C語言對大小寫敏感,但由於歷史原因,目標文件中的名字是否大小無關緊要。
2 C語言程序中的CASE語句變量的個數沒有限制,僅由可用內存大小和函數的最大長度限制
3 函數嵌套調用最大為10層
4 嵌套引用頭文件的最大數據為10層
5 預處理的條件編譯指令最大嵌套深度為20
6 功能塊({……})最大可嵌套15級
7 宏最多可嵌套8級
8 宏或函數名最多能傳遞32個參數
9 C語言語句或宏定義的一行中最多能寫510個字符,對於宏展開,其結果也不得超過510個字符。
七, 小淡C51指針
指針是C語言中的一個重要概念,使用也十分普遍,正確使用指針類型數據可以有效的表示復雜的數據結構,直接處理內存地址,而且可以更為有效的使用數組
在C語言中,為了能夠實現直接對內存單元的操作,引入了指針類型的數據,指針類型數據是專門用來確定其它數據類型的地址的,因此一個變量的地址就被稱為該變量的指針如: 一個整形變量i 存放在內存單元40H中,則該內存單元地址40H就是變量i 的指針。如果有一個變量專門用來存放另一個變量的地址,則稱之為“指針變量”
變量指針與指針變量
變量的指針: 是指某個變量的地址,而一個指針變量里面存放的是另一個變量在內存中的地址。擁有這個地址的變量則稱為該指針變量所指向的變量。 所以每個變量都有它自己的指針(地址),而每一個指針變量都是指向另一個變量的。C語言中用符號“*”來表示“指向”
如下
i=50;
*ip=50;
如果 指針ip這個指針變量指向i那么,兩個賦值表達或同義,第二個表達式可以解釋為“給指針變量ip所指向的變量賦值50”
1.指針變量的定義
指針變量的定義與一般變量的定義類似,其一般形式如下:
數據類型 [存儲器類型] * 標識符;
標識符, 是所定義的指針變量名
數據類型, 說明了該指針變量所指向的變量類型
存儲器類型,是可選的,它是C51編譯器的一種擴展,如果帶有此選項,指針被定義為基於存儲器的指針,無此選項時,被定義為一般指針,這兩種指針的區別在於它們的存儲字節不同,
一般指針: 占用三個字節,第一個字節存放該指針存儲器類型的編碼,第二和第三個字節分別存放該指針的高位和低位地址的偏移量
| 存儲器類型 |
IDATA |
XDATA |
PDATA |
DATA |
CODE |
| 編碼值 |
1 |
2 |
3 |
4 |
5 |
基於存儲器指針:則該指針長度可為一個字節,也可為兩字節
一個字節: (存儲器類型 idata data pdata)
兩個字節: (存儲器類型為code xdata)
注:在定義指針變量時最好指定其為基於存儲器的指針,這個生成的匯編代碼長精 練一些,而且也節省空間(讀者可自行到C51中寫一個程序,查看其反匯編程序)但在一些函數調用的參數中指針需要采用一般指針,為此C51編譯器允許這兩種指針相互轉換,轉換規則如下:
一般指針轉換成基於存儲器指針,采取截斷,基於存儲器類型指針轉換成一般指針采用擴展的
2.指針變量的引用
指針變量是含有一個數據對象地址的特殊變量,指針變量中只能存放地址
與指針變量有關的兩個運算符:
& 取地址運算符
* 間接訪問運算符
&a為取變量a的地址,*P為指針變量P所指向的變量。
如下:
int i , x, y;
int *pi,*px,*py;
pi=&i; //將變量i的地址賦給指針變量pi,也即pi指向i
px=&x;
py=&py;
*pi=0; //等價於i=0
*px+=6; //等價於 i+=6
(*py)++; //等價於 i++
注:指向同類數據的指針之間可以相互賦值。
如 pi=px;
3.指針變量作為函數的參數
函數的參數不僅可以是整型,字符型等數據,還可以是指針類型,指針變量作為函數的參數的作用是將一個變量的地址傳到另一個函數中去,地址傳遞是雙向的,即主調用函數不僅可以向被調用函數傳遞參數,而且還可以從被調用函數返回其結果
下面通過一個簡單的示例來進行說明。
#include<stdio.h>
swap(int *pi,int *pj)
{
int temp;
temp=*pi;
*pi=*pj; //把指針變量pj所指向的變量的值送給pi所指向的變量
*pj=temp;
}
main( )
{
int a,b;
int *pa, *pb;
a=9;
b=7;
pa=&a;
pb=&b;
if(a<b) swap(pa,pb);
printf(“\n max=%d,min=%d \n”,a,b);
}
上程序上定義了一個swap( )函數,兩個形參為指針變量,在調用函數時,所用的實參也是指針變量,在調用開始,實參變量將它的值傳遞給形參變量,采取的仍然是“值傳遞”方式,但這時傳遞的是指針的值(地址),傳遞后,形參pi的值為&a,pj的值為&b,即指針變量*pi 和*pa都指向了a, *pj和*pb指向了b。接着使*pj與*pi的值互換,從而達到了實現了a,和b值的互換。雖然函數返回時,pi pj被釋放而不存在,但main函數中a 與b的值已經交換。
4.數組的指針
在C語言中,指針與數組有着十分密切的關系,任何能夠用數組實現的運算都可以通過指針來完成,例如定義一個具有十個元素的整形數據可以寫成:
int a[10];
數組名a表示元素a[0]的地址,而*a 則表示a所代表地址中的內容,即a[0]
如果定義一個指向整形變量的指針pa並賦以數組a中的第一個元素a[0]的地址;
int *pa;
pa=&a[0]; //也可寫成pa=a;
則可通過指針pa來操作數組a了,即可用*pa代表a[0];*(pa+i)代表a[i],也可以上pa[0];pa[1];pa[2]……pa[9]的形式
5.字符數組的指針
用指針來描述一個字符數組是十分方便的,字符串是以字符數組的形式給出的,並且每個字符數組都是以轉義字符‘\0’作為字符串的結束標志。因此在判斷一個字符數組是否結束時,通常不采用計數的方法,而是以是否讀到轉義字符‘\0’來判別。利用這個特點,可以很方便的用指針處理字符數組。
示例如下:
#include<stdio.h>
main()
{
char *s1;
char xdata *s2;
char code str[]={“how are you?”};
s1=str;
s2=0x1000;
while((*s2=*s1)!=’\0’)
{
s2++;
s1++;
}
s1=str;
s2=0x1000;
printf(“%s \n,%s\n”,s1,s2);
}
注: 任何一個數組及其數組元素都可以用一個指針及其偏移值來表示,但要注意的是,指針是一個變量,因此像上例中的賦值 運算s1=str, s2=0x1000都是合法的。而數組名是一個常量,不能像變量那樣進行運算,即數組的地址是不能改變的。如上面程序中的語句
char code str[]={“how are you?”};
是將字符串“how are you?”置到數組str中作為初值,而語句
s1=str則是將數組str的首地址,即指向數組str的指針賦給指針變量s1,如果對數組進行如下的操作:
str=s1;
str++;
都是錯誤的。
6.指針的地址計算
指針的地址的計算包括以下幾個方面:
1 賦初值
指針變量的初值可以是NULL(零),也可以是變量,數組,結構及函數等的地址,例如
int a[10],b[10];
float fbuf[100];
char *cptr1=NULL;
char *cptr2=&ch;
int *iptrl=&a[5];
int *iptr2=&b;
float *flptr1=fbuf;
2 指針與整數的加減
指針可以與一個整數或整數表達式進行加減運算,從而獲得該指針當前所指位置前面或后面某個數據的地址。假設p為一個指針變量,n為一個整數,則p+n表示離開指針p當前位置的后面第n個數據的地址。
3 指針與指針相減
指針與指針相減的結果為一個整數值,但它並不是地址,而是表示兩個指針之間的距離或元素的個數,注意,這兩個指針必須是指向同一類型的數據。
4 指針與指針的比較
指向同一類型數據的兩個指針可以進行比較運算,從面獲得兩指針所指地址大小的關系,此外,在計算指針地址的同時,還可以進行間接取值運算,不過在這種情況下,間接取值的地址應該是地址計算后的結果,並且還必須注意運算符的優先級和結合規則。如下設p1是一個指針
a= *p1++;
*與++優先級相同,所以上述賦值操作過程是首先將指針p1 所指向的內容賦值給變量a, 然后p1再指向下一個數據,表明是地址增加而不是p1所指向的變量內容增加。
a=* - -p1;
與上例相同,此處是先p1減一,指向前面一個數據,然后再把p1此時所指向的內容賦給變得a
a=(*p2)++;
此處,由於使用了括號,使得結合次序發生了變化,因此首先是將p2所指的內容賦值給變量a,然后再把p2所指向的變量加1,表明是p1所指變量的大小加一,面不是p1指向下一個元素。
7.函數型指針
函數不是變量,但它在此內存中仍然需要占據一定的存儲空間,如果將函數的入口地址賦給一個指針,該指針就是函數型指針,由於函數型指針指向的是函數的入口地址,因此可用指向函數的指針代替函數來調用該函數。利用函數指針,可以將函數作為參數傳遞給另一個函數,此處還可以將函數型指針放在一個指針數組中,則該指針的數組中每一個元素都是指向某個函數的指針。
函數型指針定義形式:
數據類型 (*標識符)();
標識符為所定義的函數型指針變量名,數據類型說明了該指針指向函數的返回值類型。例如:
int ( *func1)( );
注:函數型指針變量是專門用來存放函數入口地址的,在程序中把哪個函數的地址賦給它,它就指向那個函數,在程序中可以對一個函數型指針多次賦值,該指針可以先后指向不同的函數。后面括號中不要加形參表。
函數型指針賦值形式
函數型指針變量名=函數名
注,賦值時不帶形參表,如有一個max(x,y), 可做執行下操作
func1=max;
引入了函數型指針后,對函數的調用就可以采用如下兩種方法。
1 z=max(x,y);
2 z=( *func1)(x,y);
注意 若采用函數型指針來調用函數,必須預先對該函數指針進行賦值,使之指向所需調用的函數。
函數型指針通常用來將一個函數的地址作為參數傳遞到另一個函數中去,這種方法對於要調用的函數不是某個固定函數的場合特別適用。
作如下示例讓讀者更加明白
#include<stdio.h>
int max( int x,int y)
{
if(x>y)
return(x);
else
return(y);
}
int min(int x,int y)
{
if(x<y)
return(x);
else
return(y);
}
int add(int x,int y)
{
return(x+y);
}
int process(int x,int y, int( * f)( ) )
{
int result=f(x,y );
printf(“%d\n”,result);
}
main()
{
int a,b;
printf(“Please input a and b:\n”);
scanf(“%d %d”,&a,&b);
printf(“max=”);
process(a,b,max);
printf(“min=”);
process(a,b,min);
printf(“sum=”);
process(a,b,add);
}
釋: 本例中三個函數max(),min(),add()
在第一次調用process( )函數時,除了將a b作為實參傳遞給了形參,還將函數名max作為實參將其入口地址傳遞給了process( )函數中的形參——指向函數的指針變量*f。process()中的函數調用語句,result=f(x,y)就相當於result=max(x,y),第二次調用時用了min作為實參,第三次用了add.從而實現每次調用process( )函數時完成了不同的功能。
8.返回指針型數據的函數
在函數的調用過程結束時,被調用的函數可以帶一個整形,字符等到類型的數據,也可以帶一個指針型數據。即地址。這種返回指針型數據的函數又稱為指針函數。
注意區別指針函數與函數指針。
指針函數定義如下:
數據類型 * 函數名(參數表);
其中數據類型說明了所定義的指針函數返回的指針所指向的數據類型。
int *x(a,b);
就定義了一個指針函數 *x 調用它以后可以得到一個指向整型數據的指針。注意,*x 兩則沒有括號。這與函數指針是完全不同的,並且定義函數指針時,后面的括號是不加形參表列的。也很容易混淆。下面分別定義一個指針函數和函數指針。
函數指針: int (*x)( );
指針函數: int *x(char a,b)
如下示例,指針函數的應用
#include<stdio.h>
main()
{
float T[3][4]=
{
{60.1,70.3,80.5,90.7},{30.0,40.1,50.2,60.3},{90.0,80.5,70.4,60.6}
};
float * search(float (*pointer)[4],int n);
float *p;
int i, m;
printf(“please enter the number of chanal:”);
scanf(“%d”,&m);
printf(“\n The temperature of chanal %d are: \n”,m);
p=search(T,m);
for(i=0;i<4;i++)
printf(“%5.1f”,*(p+i));
}
float *scarch(float (*pointer)[4],int n)
{
float *pt;
pt=*(pointer+n);
return(pt);
}
釋義:
上程序中,紅色標出來的那一行是定義了一個指針型函數,它的形參pointer是指向包含4個元素的一維數組的指針變量。於是pointer+i 就是指向二維數組T的第i行,而*(pointer+i)則指向第i行的第一個元素。pt是一個指針變量。調用search( )后,返回了一個指向第m行的首地址,
9.指針數組
由於指針本身也是一個變量,因此C語言允許定義指針數組,指針數組適合用來指向若干個字符串,使得字符串的處理更加方便。指針數組的定義方法與普通數組完全相同,一般格式如下:
數據類型 * 數組名[數組長度];
例如:
int *x[2];
char *sptr[5];
指針數據在使用之前往往需要先賦初值,方法與一般數組賦初值類似,。使用指針數組最典型的場合就是通過對字符數組賦初值而實現各維長度不一致的多維數組的定義
#include<stdio.h>
main( )
{
int i;
char code *season[4]=
{
“spring”,”summer”,”fall”,”winter”
};
for(i=0;i<4;i++)
printf(“\n%c--------%s”,*season[i],season[i]);
}
程序執行結果:
s--------spring
s--------summer
f--------fall
w--------winter
在這個例子中,在code區定義了指向char型數據的4個指針,其初值分別為“spring”,”summer”,”fall”和”winter,這樣可以使這四個數組保存在一段地址連續的地址空間里(此可以通過程序驗證),如果采用二維數組,那么會造成內存空間的浪費。因為二維數組的長度必須一致,且要等於最大的一列長度。
下面寫一個(經典)示例程序(不做解釋)
#include<stdio.h>
char code daytab[2][13]=
{
{0,31,28,31,30,31,30,31,31,30,31,30,31},
{0,31,29,31,30,31,30,31,31,30,31,30,31},
};
char *mname(int n)
{
char code *mn[]=
{
“llligal month”,”january”,”February”,
“March”,”April”,”May”,”June”,
“July”,”August”,”September”,
“October”,”Novenmber”,”December”
};
return((n<1||n>12)?mn[0];mn[n]);
}
monthday( int y,int yd)
{
int i,leap;
leap=y%4==0&&y%100!=0||y%400==0;
for(i=1;yd>daytab[leap][i];i++)
yd-=day[leap][i];
printf(“%s,%d\n”,mname(i),yd);
}
main( )
{
int year,yearday;
printf(“input year and yearday: \n”);
scanf(“%d,%d”,&year,&yearday);
monthday(year,yearday);
}
10.指針型指針
指針型指針所指向的是另一個指針變量的地址,故有時也稱為多級別指針。
定義指針型指針的一般形式:
數據類型 **標識符
標識符: 定義的指針型指針變量。
數據類型: 說明一個被指針型指針所指向的指針變量所指向的變量數據類型。
#include<stdio.h>
main( )
{
int x,*p,**q;
x=10;
p=&x;
q=&p;
printf(“ %d\n ”,x); //直接取值
printf(“ %d\n ”, *p); //單重間接取址
printf(“ %d\n ”, **q); //多重間接取址
}
三行均是打印出10
注:
一個指針型指針是一種間接取值的形式,而且這種間接取值的方式還可以進一步延伸,故可以將這種多重間接取值的形式看成一個打針鏈
使用指針型指針的例子
#include<stdio.h>
main( )
{
char i;
char **j;
char *season[4]=
{
“spring”,”summer”,”fall”,”winter”
} ;
for(i=0;i<4;++i)
{
j=season+i;
printf(“\n%c--------%s”,*season[i] , *j);
}
}
11.抽象型指針
ANSI新標准增加了一種 ”void *” 指針類型,這是一種抽象型指針,即可以定義一個指針變量,但不指定該指針是指向哪一種類型數據的,對於這種抽象型指針在給另一個變量賦值時,需要進行強制類型轉換,使之適合於被賦值的變量類型。例如:
char *p1;
void *p2;
p1=(char *)p2;
當然也可以用(void *)p)將p1的地址轉換成void *類型之后賦給p2;
P2=(void *)p1;
函數也可以定義為void *類型,例如:
void *fun(x,y)
表示函數fun返回的是一個地址,它指向“空類型”。
抽象型指針可以用來在每個存儲區內訪問任意絕對地址。或者用來產生絕對調用。
九, 預處理命令
C提供的預處理功能主要有以下3種:
1 宏定義。
2 文件包含
3 條件編譯
分別用宏定義命令,文件包含命令,條件編譯命令來實現,為了與一般的C語句相區別,這些命令以符號#開頭。
一 宏定義
1 不帶參的宏定義
用一個指定的標識符來代表一個字符串
# define 標識符 字符串
說明:
1 宏名一般大寫,區別於變量名
2 宏名的使用可以簡化編寫程序
3 宏定義不是C語句,不加分號,如加分號,則會連分號一起進行置換
4 宏定義命令在函數外
5 可用 # undef 來終止宏定義的作用域
6 可以用已有的宏名進行層層置換(小於8)
7 程序中用 “” 括起來的對象不做置換
8 宏定義只作字符替換,不分配內存
2 帶參數的宏定義
帶參的宏定義不是進行簡單的字符替換,還要進行參數替換
# define 宏名(參數表) 字符串
# define MIN(x,y) (((x)<(y)?(x);(y))
#define SQ(x) (x*x)
#define CUBE(x) (SQ(x)*x)
#define FIFTH(x) (CUBE(x)*SQ(x))
