51单片机的数据类型及指针使用学习笔记


前言

本笔记只为复习和总结C51单片机编程的数据类型,构造类型,指针应用等。如有错误,请指正。
作者:孙韶辉

Ⅰ:数据类型

数据是具有一定格式的数据或者数值,他是计算机操作的对象。不管使用何种语言,何种算法设计程序,都要对数据进行处理,最终在计算机中运行的只有数据流。
数据的格式通常称作为数据类型。按照数据类型对数据进行的排列、组合、架构则成为数据结构
C51的数据类型如下:

数据类型
基本类型
构造类型
指针类型
空类型
位型bit
字符型char
整型int
浮点型Float
双精度浮点型double
数组类型array
结构体类型struct
共用体union
枚举enum

一、基本类型

在这里插入图片描述
在单片机C语言中默认规则如下:short int即为int
long int即为long,前面若无unsigned 符号则一律认为是signed型。
除了上面C51数据类型外,还有针对8051系列单片机内的特殊功能寄存器而设置的Sfr和Sfr16类型的数据和位操作特殊寄存器中的特定位置而设置的Sbit类型的数据。常用数据类型如上图。
在51单片机中,除了题序计数器PC和1组通用寄在器组之处,其他所有的寄存器均称为特殊功能寄存器(SFR),它们外做在单片机内RAM区的商128B中,地址为801~OFFH,为了能访问这些特殊功能寄存器, Keil C51编译器扩充了关键字sfr和sfr16,在程序中要用到这些特殊功能寄存器时,必须要在程序的最前面对这些名称进行声明,声明的过程就是将这不存器在内存中的地址编号赋给这个名称,这些编译器就可以知道这些名称所对应的寄存器了。实际上这些寄存器的声明已经包含在51单片机的特殊功能寄存器声明头文件"regs51, h"中了,如果在程序中引用了这个头文件,就相当于将这个头文件中的全部内容放到引用头文件的位置处,就不用再对这个背存器声明了.
对特殊功能寄存器的声明方法如下:

  • sfr特殊功能寄存器名一地址常数,如sir P0=0x80.
  • sfr16特殊功能寄存器名一地址常数,如slr16 T2=0xCC;
  • 对特殊功能寄存器中的某一位声明方法如下:.
  • sbit位变设名一位地址,如sbit OV=0XD2.
  • sbit位变量名=特殊动能寄存器名^位位置,如sbit OV=PSW^2.
  • sbit位变量名=字节地址^位位置,如sbit OV=0xD0^2。
    其中:
    sfr—特殊功能寄存器的数据说明,声明一个8位的寄存器
    sfr16—16位特殊功能寄存器的数据声明
    sbit-----特殊功能位声明,也就是声明某一个特殊功能寄存器中的某一位
    例如:
    sfr P0=0X80;
    sbit OV=PSW^2;//表示寄存器PSW的倒数第三位

二、构造类型

1.数组类型

数组是同类型的一组变量,引用这些变量时可用同一个表示符,借助于下标来区分各个变量。数组中的每一个变量成为数组元素。数组有连续的存储区域组成,最低地址对应于数据的第一个元素,最高的地址对应于最后的一个元素。数组可以是一维数组,也可以是多维数组。

1.1 一维数组

表达式如下:

类型说明符 数组名 [常量];
例:

uint  LED_datae[8]={0X01,0X01,0X01,0X01,0X01,0X01,0X01,0X01};

1.2 二维数组

表达式如下:

类型说明 数组名 [行数][列数];

例:

unsigned char LED_DATA[3][4]=
{
{0X01,0X01,0X01,0X01},
{0X01,0X01,0X01,0X01},
{0X01,0X01,0X01,0X01}
};
1.3 字符数组

char a[ ]={‘1’,’-’,‘j’,‘d’};
char string[ ]={"holle,world "};
或者直接写成
char string[ ]="holle,world ";
要特别注意,字符串是以’\0’作为结束标志的,所以,当把一个字符串存入数组时,也把结束标志位’\0’存入了数组。因此,上面定义的字符数组string[12],最后一个元素不是‘D’,而是’\0’。

1.4 指针数组(包含指针概念及一些分类和用法)

一个数组的名字表示该数组的首地址,所以用数组名 作为函数的参数时,被传递的就是数组的首地址,被调用的函数的形式参数必须定义为指针型变量。

1.4.1 指针的概念:

一个数据的指针就是他的地址,通过变量的地址能找到该变量在内存中的存储单元,从而能得到他的值。指针时一种特殊类型的变量。他对一般变量的三要素:名字,类型和值, 指针的命名与一般变量是相同的,他与一般变量的区别在于类型和值上
1).指针的值
指针存放的是某个变量在内存中的地址值。被定义过的变量都有一个内存地址,如果指针存放了某个变量的地址值,就称这个指针指向该变量。由此可见,指针本身具有一个内存地址。另外,他还存放了它所指向的变量的地址值。
2).指针的类型
指针的类型就是该指针所指向的变量类型。例如,一个指针只指向int型变量,该指针就是int型指针。
3).指针的定义格式
指针变量不同于整型或者字符型等其他类型的数据,使用前必须将其定义为“指针类型”,指针定义一般形式如下:

类型说明符 *指针名字
示例:
int i;//定义一个整型变量i
int *p;//定义一个整型指针,名字为P
p=&i ; //将I的地址存放与指针变量P中
&为取地址运算符
在定义指针时注意两点:

  • 指针前面的* 表示该变量为指针类型
  • 一个指针变量只能只想同一个类型的变量,如整型指针不能指向字符型变量
1.4.2 指针的初始化

初始化格式如下:
类型说明符 指针变量 =初始地址值
示例:
unsigned char i;
unsigned char *p;
p=&i ;

1.4.3 指针数组

指针可以指向某类变量,也可以指向数组。以指针变量为元素的数据成为指针数组。这些指针应具有相同的储存类型,并且指向的数据类型也必须相同。
格式如下:
类型说明符 *指针数组名 [元素个数];

1.4.4 指向数组的指针

一个变量有地址,一个数组元素也有地址,所以可以用一个指针指向一个数组元素。如果一个指针存放了某数组的第一个元素的地址,就说该指针指向的这一数据的指针。数组的指针即数组的起始地址。
示例:

unsigned char a[]={0,1,2,3,4,5};
unsigned char *p;
p=&a[0];//将数组A的首地址存放在指针变量P中

那么指针P 就是数组A的指针。
C语言规定:数组名代表数组的首地址,也就是第一个元素的地址。例如,下面两个是等价的:

p=&a[0];
p=a;

C语言规定:P指向数组a的首地址后,p+1就指向数组的第二个元素a[1],
p+2指向a[2];一次类推,p+i指向a[i];
引用数组元素可以用下标(如A[3]),但使用指针速度更快,且很用内存更小。这正是使用指针的优点和C语言的精华所在。
对于二维数组,C语言规定:如果指针P指向该二维数组的首地址(可以用a表示,也可以用&a[0][0]表示),那么P[i]+j指向的元素就是a[i][j]。这里的i和j分别表示二维数组的第i行和第j列。
程序实例:

//--------------------------------------------主函数:
OLED_ShowString(0,4,"v:18149072101   ");
//-------------------------------------------被调用函数
//显示一个字符号串
void OLED_ShowString(u8 x,u8 y,u8 *chr)//将一个字符串放入指针,那么char[0],就是第一个字符的指针地址
{
	unsigned char j=0;
	while (chr[j]!='\0')            //如果指针指向的变量位'\0',说明到达结束位,这句意思,指针变量如果不等于\0,说明是真,就进入下方循环
	{		OLED_ShowChar(x,y,chr[j]);   //跳入写单个字符函数
			x+=8;                        //x代表列数,因为是8x16字符,所以每次写完一个字符,加8个列数点阵
		if(x>120){x=0;y+=2;}            //一行总列数位128,所以X变量如果大于120,就=0,页数翻2页
			j++;                         //一个字符写完,指针加1,写入下个字符
	}
}

void OLED_ShowChar(u8 x,u8 y,u8 chr)
{      	
	unsigned char c=0,i=0;	
		c=chr-' ';               //获取字符偏移量,这是因为字库跟标准ASCII码表相差32,即一个空格,字符的地址-空格的地址,等于字符在数据表内位置
		if(x>Max_Column-1)
     {
		 x=0;y=y+2;
		 }
		if(SIZE ==16)                      //选择字体格式     
			{
			OLED_Set_Pos(x,y);	
			for(i=0;i<8;i++)                //在第一页写入前8个数据
			Write_IIC_Data(F8X16[c*16+i]);  //比如:chr='1',1的模在表1行,每行16个元素,1-空格0=1,1x16=16,那么'1'的模就从F8X16[16+i]开始写,写入8次数据
			 
					OLED_Set_Pos(x,y+1);            //开始写第二页
			for(i=0;i<8;i++)
			Write_IIC_Data(F8X16[c*16+i+8]);//在第二页写入后8个数据
			}
			else {	                           //否则另外字体
				OLED_Set_Pos(x,y+1);
			   for(i=0;i<6;i++)
				      {
					       Write_IIC_Data(F6x8[c][i]);
					    }
				
		    	}
}


上面比较难,下面是个简单的例子:

#include <reg52.h>
void delay()
{
unsigned int i,j;
for(i=1000;i>0;i--)
for(j=110;j>0;j--);

}
void main ()
{
unsigned char code Tab[]={0x7f,0xbf,0xdf,0xef,0xf7,0xfb,0xfd,0xfe};
unsigned char *p,m;
p=Tab;
for (;m<8;m++)
{
P2=*(p+m);//或者写成 P2=p[m];或者p2=*p[m];后面这个在使用时 必须先将指针数组元素都赋值(初始化)再使用。
delay();
}
while(1);
}

此处注意:经测试,P2=p+m;会出现编译警告,并且测试LED会出现无顺序显示。这里P里存入的时tab[0]的地址, 将LED=P+M,意思是将tab[0]地址加m送入P2口显示,显示的是地址。
“*”是指针运算符,前面加 星意思是取得各指针所指向元素的值
“&”是取地址运算符

1.4.5 指针作函数参数的应用

函数参数不仅可以是数据也可以是指针,他的作用是将一个变量的地址传送到另一个函数中。
首先定义一个指针指向存储流水控制码的数组的首地址,然后以这个指针作为实际参数传递给被调参数的形参,因为该形参也是一个指针,该指针也指向流水控制码的数组,所以只要用指针引用数组元素的方法就可以控制P2口八位流水灯点亮。

#include <reg52.h>
void delay()
{
unsigned int i,j;
for (i=1000;i>0;i--)
for(j=110;j>0;j--);
}
void xianshi(unsigned char *p)
{
unsigned char i;
while(1)//无限循环
{
i=0;
while( *(p+i)!='\0')//当数组中元素未取完时,接着取数送显示
{
P2 = *(p+i);//取数组中第1+1个元素送P口显示
delay();
i++;//修改循环计数变量1
}
}
}

void main()
{
unsigned char code tab[] = {0xfe,0xfd, 0xfb,0xf7, 0xef, 0xdf, 0xbf,0x7E};//定义显示码数组
unsigned char *pin;
pin = tab;
xianshi (pin);//指针指向数组首地址//调用显示子程序
}

此程序中首先定义了一个显示子程序,它的形参为无符号字符型指针。子程序的功能为:循环取数组中的元素,并送P2口显示。如果把指针p指向数组的首地址,则p+i指向的是数组中的第i+1个元素, * (p+i)则表示取数组中第i+1个元素的值。程序中"\0"表示数组元素的结束标志,数组元素在存储的时候,不仅要存储各个元素值,在最后还要存储结束标志。如果读数组元素时,读到结束标志就表示全部元素已经读完。所以指令行while(* (p+i)!=‘0’)的功能是:当读数组元素不是结束标志时,循环继续,即接着取数并送显示。在本例主程序中,首先定义了显示控制码数组,该数组元素按顺序取数的时候,可以实现一个流水灯向左移位循环点亮。主程序中又定义了和数组同类型的指针pin,并让该指针指向数组首地址,再调用刚定义的显示子程序时,就可以用指针pin作为它的实参。

1.4.6 函数型指针的应用

在c语言中,指针变量除了能指向数据对象外,也可以指向函数。一个函数在编译时,分配了一个入口地址,这个入口地址就称为函数的指针。可以用一个指针变量指向函数的入口地址,然后通过该指针变量调用此函数。
定义指向函数的指针变量的一般形式如下:

类型说明符 (*指针变量名)(形参列表)

函数的调用可以通过函数名调用,也可以通过函数指针来调用,要通过函数指针调用函数,只要把函数的名字赋给该指针就可以了。
例:

先定义流水等点亮函数,再定义函数型指针,然后将流水等点亮函数的名字(入口地址)赋给函数型指针,就可以通过该函数型指针调用流水灯点亮函数。注意函数型指针类型说明符必须和函数的类型说明符一致。

#include <reg52.h>
unsigned char code tab[]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f};
void delay()
{
unsigned int i, j;
for(i=1000;i>0;i--)
for(j=110;j>0;j--);
}
void xianshi()
{
unsigned char i;
for (i=0;i<8;i++)
	{
P2=tab[i];
delay();
	}
}

void main()
{ 
  void (*p)(void);//定义函数型指针P
  p=xianshi;
  while(1)
  (*p)();
}

因为该指针所指向的函数没有参数和返回值,所以该指针的参数和类型说明符都为空。
再用指令p=xianshi;把显示子程序名字赋给指针,使指针指向函数入口地址。P现在是函数入口的地址,*P指的才是函数本身的值。

2.结构体类型

2.1 结构体数据类型

举例DS1302的7个字节的时间放到一个缓冲区数组中,然后把数组中的值稍作转换显示到液晶上,这里就存在一个问题,DS1302时间寄存器的定义并不是常用的年月日时分秒的顺序,而是在中间加了个星期几的一个字节,而且每当要用这个时间的时候都要清楚的记得数组的第几个元素表示的是什么,这样以来,一是很容易出错,二是程序的可读性不强。当然可以把每一个元素都定一个明确的变量名字,这样就不容易出错,也易读,但结构体上却显得很零散了,于是就可以用结构体来讲这一数组彼此相关的数据做一个封装,他们即组成一个整体,易读也不易错,而且可以单独定义其中每一个成员的数据类型,比如年份用unsigned int类型,即4个十进制位表示显然比2位更符合日常习惯,而其他类型还是可以用2位来表示。

结构体本身不是一个基本的数据类型,而是构造的,它每个成员可以是一个基本的数据类型或者一个构造类型。结构体既然是一种构造而成的数据类型,那么在使用以前必须先定义它。

2.1.1 普通结构体

声明结构体变量的一般格式:

struct 结构体名
{
类型 1 变量名 1;
类型 2 变量名 2;

类型n 变量名n;
};
struct 结构体名 结构体变量名1,结构体变量名2,…,结构体变量名n;

例:

struct stime      //时间结构体定义
{
   unsigned int year;//年
   unsigned char mon;//月
   unsigned char day;//日
   unsigned char hour;//时
   unsigned char min;//分
   unsigned char sec;//秒
   unsigned char week;//星期
   };
   struct stime buftime;
   

struct 是结构体类型的关键字,stime是这个结构体的名字,buftime 就是定义了一个具体的结构体变量。如果要给结构体变量的成员赋值,写法就是

buftime.year=0x2013;
buftime.mon=0x10;

2.1.2 数组结构体

数组的元素也可以是结构体类型,因此可以构成结构体数组,结构体数组的每一个元素都是具有相同结构类型的结构体变量。例如前面构造的这个结构体类型,直接定义称struct stime buftime[3];就表示定义了一个结构体数组,这个数组的3个元素,每一个元素都是一个结构体变量。同样的道理,结构体数组中的元素成员如果需要赋值,就可以写成

buftime[0].year=0x2013;
buftime[0].mon=0x10;

2.1.3 指针结构体

一个指针变量如过指向了一个结构体变量,就成为结构体指针变量。结构指针是指向的结构体变量的首地址,通过结构体指针也可以访问这个结构变量。

结构指针变量声明的一般形式如下:
struct stime *pbuftime;

这里要特别注意的是,使用结构体指针对结构体成员的访问和使用结构体变量名对结构体成员的访问,其表达式有所不同相比。结构体指针对结构体成员的访问表达式为

pbuftime ->year=2013;
或者是
(*puftime). year=2013;

  • 关于结构体需要了解的几个小知识:
  • 只有结构体变量才分配地址,而结构体的定义是不分配空间的
  • 结构体中个成员的定义和之前的变量定义一样,但在定义是也不分配空间。
  • 结构体变量 的声明需要在主函数之上或者主函数中声明,如果在主函数之下则会报错
  • C语言中的结构体不能直接进行强制转换,只有结构体指针才能进行强制转换。

3.共用体类型

共用体也成为联合体,共用体定义和结构体十分类似:

union{
数据类型 1 成员名1;
数据类型 2 成员名2;

数据类型 n 成员名n;
};

共用体表示的是几个变量共用一个内存位置 ,也就是成员1,成员2, ,成员n都用一个内存位置。共用体成员的访问方式和结构体是一样的,成员访问的方式是“共用体名.成,员名",使用指针来访问的方式是“共用体名一成员名”。共用体可以出现在结构体内,结构体也可以出现在共用体内,在编程的日常应用中,应用最多的是结构体出现在共用体内,例如:

union
{
unsigned int value;
struct
{
unsigned char first;//int高位8位
unsigned char second;//int低位8位
}half;//结构体变量名
}number;//共同体变量名

共用体和结构的主要区别:
1. 结构体和共用体都是由多个不同的数据类型成员组成,但在任何一个时刻,共用体只能存放一个被选中的成员,而结构体所有的成员都存在。
2. 对于共同体的不同成员的赋值,将会改变其他成员的值,而对于结构体不同成员的赋值是相互之间不影响的。

4. 枚举类型

在实际问题中,有些变量的取值被限定在一个有限的范围内。例如,一个星期内只有七天,一年只有十二个月,一个班每周有六门课程等等。如果把这些量说明为整型,字符型或其它类型显然是不妥当的。为此,C语言提供了一种称为“枚举”的类型。在“枚举”类型的定义中列举出所有可能的取值,被说明为该“枚举”类型的变量取值不能超过定义的范围。

1.枚举的定义

应该说明的是,枚举类型是一种基本数据类型,而不是一种构造类型,因为它不能再分解为任何基本类型。

enum 枚举名
{
标识符1[=整型常数],
标识符2[=整型常数],

标识符n[=整型常数],
};
enum 枚举名 枚举变量;

例如:该枚举名为weekday,枚举值共有7个,即一周中的七天。凡被说明为weekday类型变量的取值只能是七天中的某一天。

2. 枚举变量的说明

如同结构和联合一样,枚举变量也可用不同的方式说明,即先定义后说明,同时定义说明或直接说明。

设有变量a,b,c被说明为上述的weekday,可采用下述任一种方式:

enum weekday{ sun,mou,tue,wed,thu,fri,sat };

enum weekday a,b,c;

或者为: enum weekday{ sun,mou,tue,wed,thu,fri,sat }a,b,c;

或者为: enum { sun,mou,tue,wed,thu,fri,sat }a,b,c;

3.枚举类型变量的赋值和使用

枚举类型在使用中有以下规定:

  1. 枚举值是常量,不是变量。不能在程序中用赋值语句再对它赋值。 例如对枚举weekday的元素再作以下赋值: sun=5; mon=2; sun=mon; 都是错误的。

  2. 枚举元素本身由系统定义了一个表示序号的数值,从0开始顺序定义为0,1,2…。如在weekday中,sun值为0,mon值为1,…,sat值为6。

【例】

main()
{
enum weekday
{ 
sun,mon,tue,wed,thu,fri,sat 
} a,b,c;

a=sun;

b=mon;

c=tue;

printf("%d,%d,%d",a,b,c);

}

说明: 只能把枚举值赋予枚举变量,不能把元素的数值直接赋予枚举变量。如:

a=sun;

b=mon;

是正确的。而:

a=0;

b=1;

是错误的。如一定要把数值赋予枚举变量,则必须用强制类型转换。 如:

a=(enum weekday)2;

其意义是将顺序号为2的枚举元素赋予枚举变量a,相当于:

a=tue; 还应该说明的是枚举元素不是字符常量也不是字符串常量,使用时不要加单、双引号。

【例】

main()
{
enum body
{
a,b,c,d
 } month[31],j;int i;

j=a;
for(i=1;i<=30;i++)
{
month[i]=j;

j++;if(j>d) j=a;

}
for(i=1;i<=30;i++){switch(month[i])

{casea:printf(" %2d %c\t",i,'a');break;caseb:printf(" %2d %c\t",i,'b');break;casec:printf(" %2d %c\t",i,'c');break;cased:printf(" %2d %c\t",i,'d');break;default:break;

}

}

printf("\n");

}

(1) 枚举型是一个集合,集合中的元素(枚举成员)是一些命名的整型常量,元素之间用逗号,隔开。

(2) DAY是一个标识符,可以看成这个集合的名字,是一个可选项,即是可有可无的项。

(3) 第一个枚举成员的默认值为整型的0,后续枚举成员的值在前一个成员上加1。

(4) 可以人为设定枚举成员的值,从而自定义某个范围内的整数。

(5) 枚举型是预处理指令#define的替代。

(6) 类型定义以分号;结束。

Ⅱ:C语言编译预处理

在C语言中,通过一些预处理命令可以在很大程度上为C语言本身提供许多功能和符号等方面的扩充,增强C语言的灵活性和方便性。
预处理命可以在编写程序时加在需要的地方,但它只在程序编译时才起作用,并且通常是按行进行处理的,因此又被成为编译控制行。

一.宏定义

C语言允许用一个标识符来表示一个字符串,成为宏。被定义为宏的标识符为宏名,在编译预处理是,程序中的所有宏名都用宏定义中的字符串代替,这个过程成为宏代换。
宏定义分为带参数和不带参数。

1. 不带参数的宏定义

#define uchar unsigned char

对于不带参数的宏定义说明如下:

  • 宏定义不是C语句,不能在行末加分号,如果加了会连分号一起替代。
  • 宏名的有效范围为定义命令之后到本源文件结束。
  • 可以用#undef 命令终止宏定义的作用域
2.带参数的宏定义

示例:

#define PI 3.1415926
#define S(r)  PI*(r)*(r)
main()
{
float a,area;
a=5.6;
area=S(a);
}

注意:
宏名和宏参数之间不能由空格,否则将空格以后的字符都作为替代字符串的一部分。
宏定义写成#define s() PIrr可能会引发歧意,所以R 加括号

二.文件包含

#include <reg52.h>

三.条件编译

一般情况下,对C语言程序进行编译时,所有的程序都参加编译,但有时希望对其中一部分内容只在满足一定条件下时才编译,这就是所谓的条件编译。
条件编译可以选择不同的编译范围,从而产生不同的代码。C51有三种形式:

1.形式1

#ifdef 标识符
程序段1
#else
程序段2
endif

如果指定的标识符已被定义,则程序段1参加编译,并产生代码,而忽略掉程序段2.否则,程序段2参加编译,忽略1.

2.形式2

#if 常量表达式

程序段1
#else 程序段2
#endif

如果常量表达式为真,那么编译该后语句的程序段1否则编译2。

3.形式3

#ifndef 标识符
程序段1
#else
程序段2
#endif

和第一条形式相反
实例:

#include <reg52.h>
#define F(a,b)  (a)*(b)/((a)+(b))
void main()
{
int i,j,k;
i=34;
j=45;
k=30;
P2=f(i+j,k);
while(1);
}

#inclued <reg52.h>
#define  max  100
void main()
{
#if  max>80;
P2=0xf0;
#else
P2=0x0f;
#endif
}
  • 5
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值