C51学习笔记

时间:2022-04-14 00:22:50

转自: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))