来源:kingwei | 2006-4-28 | (有6297人读过)
这篇是寒假的时候写的,本来设想是要写C的很多方面,结果开学以后就再没动过。
对于派生类型的介绍,个人认为还是可以一看的,参考了一些书籍。
1 概述 1.1 发展历史 C语言是在70年代初问世的。一九七八年由美国电话电报公司(AT&T)贝尔实验室正式发表了C语言。同时由B.W.Kernighan和D.M.Ritchit合著了著名的“THE C PROGRAMMING LANGUAGE”一书。通常简称为《K&R》,也有人称之为《K&R》标准。但是,在《K&R》中并没有定义一个完整的标准C语言,后来由美国国家标准学会在此基础上制定了一个C 语言标准,于一九八三年发表。通常称之为ANSI C。 C语言是一种结构化语言。它层次清晰,便于按模块化方式组织程序,易于调试和维护。由于C语言实现了对硬件的编程操作,因此C语言集高级语言和低级语言的功能于一体,因此也有人称它为中级语言。此外,C语言还具有效率高,可移植性强等特点。因此广泛地移植到了各类计算机上,从而形成了多种版本的C语言。 目前最流行的C语言有以下几种: •Microsoft C 或称 MS C •Borland Turbo C 或称 Turbo C •AT&T C
1.2 程序结构举例 本文档中列举的代码均在Dev-C++中运行通过。 下面这个程序先重定向标准输入输出流,然后,从文件in.txt中读取数据,调用标准库函数qsort,升序排序后输出到文件out.txt。
#include <stdio.h> /* 包含头文件 */ #include <stdlib.h>
#define MAX_LENGTH 100 /* 宏定义 */
int length; /* 定义全局变量 */ int list[MAX_LENGTH];
int Comp(const void *p1, const void *p2) /* 定义函数体 */ { int e1 = *((int *)p1); /* 定义局部变量 */ int e2 = *((int *)p2); if (e1 > e2) /* 分支结构(if-else) */ return 1; else if (e1 < e2) return -1; else return 0; /* 向主调函数返回值 */ }
int main() /* 主函数 */ { int i; freopen("in.txt", "r", stdin); /* 顺序结构 */ freopen("out.txt", "w", stdout);
while (scanf("%d", &length) != EOF) /* 循环结构(while) */ { for (i=0; i<length; i++) /* 循环(for)控制输入 */ scanf("%d", &list[i]); qsort((void *)list, length, sizeof(int), Comp); for (i=0; i<length; i++) /* 循环(for)控制输出 */ printf("%d ", list[i]); printf("\n"); } return 0; /* 进程向系统返回值 */ }
1.3 编译、连接 任何一种计算机语言都要从某种人们容易理解的形式(源代码)转化成计算机可以执行的形式(机器指令)。对于C语言程序,首先需要对源代码作预处理,预处理器(preprocessor)对预编译指令进行处理,生成的代码通常存放在一个中间文件中。然后使用编译器(compiler),将中间文件编译成为目标文件。最后经过连接器(linker),将目标模块和库进行连接,并添加启动模块,才能最终成为系统可以加载和执行的程序。 开发环境 编译(compile) 连接(link) (工具) C源文件 --------------> 目标文件 ------------>最终文件
Windows *.c *.obj *.exe 可执行文件 (VC) *.lib 静态库文件 *.dll 动态库文件
Linux *.c *.o *.--- 可执行文件 (KDevelop) *.a 静态库文件 *.so 动态库文件
因此,将TC、VC等程序设计软件称为编译器是不恰当的,这些软件是集成了编辑、预处理、编译、连接、调试等工具的集成开发环境。以Dev-C++为例,它提供了一个窗口界面的编辑环境和一些配置选项,但内部编译器仍然是gcc/g++、内部调试工具是gdb.exe。
2 数据类型 C语言中,变量和函数有两个属性,数据类型和存储类别。
变量声明和定义的格式为: [存储类别] 数据类型 变量名; 如: extern unsigned char g_lumQuantTable[DCT_SIZE][DCT_SIZE];
函数声明的格式为: [存储类别] 数据类型 函数名(形参列表...); 如: extern void DCT_8x8(double dct[][DCT_SIZE], const double pic[][DCT_SIZE]);
函数定义的格式为: [存储类别] 数据类型 函数名(形参列表...) { ... } 如: static void DCT_AAN(double dct[], const double pic[]) { ... }
变量的数据类型有三大类:基本类型、派生类型和指针类型。 基本类型包括字节型(char)、整型(int)和浮点型(float/double)。 定义基本类型变量时,可以使用符号属性signed、unsigned(对于char、int),和长度属性short、long(对于int、double)对变量的取值区间和精度进行说明。
3 派生类型 包括数组、结构体(struct)、共用体(union)和枚举类型(enum)。其中前三种也合称为构造类型。 除数组类型以外,后三种类型在定义变量(实例)之前,均需要向编译器说明该类型的具体信息,struct和union需要说明组成成员,而enum则需要明确取值值域和枚举元素。
3.1 数组类型 是一种简单聚合构造方式,在变量名后加“[ ]”即可。数组中每个元素可以是任何基本类型、派生类型、指针类型,甚至也可以是数组类型,这时定义的也就是多维数组。 例如:unsigned char g_compQuantTable[2][DCT_SIZE*DCT_SIZE]; 在g_compQuantTable[DCT_SIZE*DCT_SIZE]的基础上,在数组名g_compQuantTable后再加“[2]”,就定义了一个二维数组,它由两个长度为DCT_SIZE*DCT_SIZE的unsigned char一维数组型元素组成。
3.2 结构体(struct) 结构是一种复杂聚合构造方式,通常用作:
(1) 将一般作为一个整体来使用的数据元素结合到一起,如: typedef struct { DWORD buffer; BYTE ptr; FILE *fp; }StrmType;
(2) 从函数中返回多个数据元素,如利用输出参数回传: int LoadBMPHeader(struct BMP_Header *image, const char *fileName) { ... } 或者返回结构体实例: TokenType Operate(TokenType a, TokenType theta, TokenType b) { TokenType r; ... return r; }
(3) 构造链式数据结构,如二叉树节点的定义: typedef struct HuffNode { BYTE val; struct HuffNode *left; struct HuffNode *right; }HuffNode, *HuffTree;
(4) 映射数据在硬件设备、网络链接和存储介质上的组织方式,如BMP文件头: typedef struct tagBITMAPFILEHEADER { WORD bfType; DWORD bfSize; WORD bfReserved1; WORD bfReserved2; DWORD bfOffBits; } BITMAPFILEHEADER 结构内字段的次序依赖于所处的架构和所用的编译器,结构内各元素的表达也依赖于架构和操作系统。即使是基本类型的数据,比如整型,字节的存储方式也会有所不同。因此,这种应用天生不可移植。
(5) 实现抽象数据类型
(6) 以面向对象的方式编程 将数据元素和函数指针聚合成结构,模拟类的字段和方法,创建与类相仿的实体。由于“对象”可以使用不同的“方法”(函数指针)来初始化,并通过相同的界面来使用(通过同一结构成员名调用),这种技术实现了面向对象语言中的虚方法(virtual method)和多态编程(polymorphic programming)。
3.3 共用体(union) 与聚合构造不同,共用体是一种联合构造类型,多个成员共享同一存储区域。通常用于:
(1) 有效地利用存储空间
(2) 实现多态(polymorphism),例如表达式分析中记号(token)的定义: typedef enum { OPTR,FUNC,OPND }TagType; typedef union { char optr; FuncType func; double opnd; }DataType; typedef struct { TagType tag; DataType data; }TokenType; 每个记号只能是操作符、函数或操作数,利用标记成员进行区分和识别。 同一对象(结构)用于代表不同的类型,这些不同类型的数据都存储在独立的共用体成员中,这种方法实现了面向对象语言中的多态数据(polymorphic data)。
(3) 使用不同的内部表达方式对数据进行访问,例如为了方便地对WORD的每位进行访问而定义如下共用体: typedef union { WORD val; struct { WORD bit0: 1; WORD bit1: 1; WORD bit2: 1; WORD bit3: 1; WORD bit4: 1; WORD bit5: 1; WORD bit6: 1; WORD bit7: 1; WORD bit8: 1; WORD bit9: 1; WORD bit10: 1; WORD bit11: 1; WORD bit12: 1; WORD bit13: 1; WORD bit14: 1; WORD bit15: 1; }bits; }WORD_REG_TYPE;
3.4 枚举类型(enum) 枚举类型不是一种构造类型, 因为它不能再分解为任何类型。因此,C语言教科书上将它归为基本数据类型。 从本质上来看,它可以算作是整型的一种变体。 这里暂且将它看作派生类型,是因为在定义枚举变量前,需要向编译器提供额外的信息,这点与结构体、共用体相似,而且类型声明的结构也极为相象。
4 指针类型 指针变量中存放的是内存地址,若该地址处存放的是变量,则称该指针为指向变量的指针;若该地址对应某函数的入口,则称该指针为指向函数的指针,简称函数指针。
4.1 指向变量的指针 在变量名之前添加“*”号,即可实现指针的定义。注意,这里说“在变量名之前”,而不是“在类型说明之后”,因为后一种做法容易引起误解。后一种做法看似是顺理成章的:类型说明之后加“*”号,构造了一种新的数据类型——指向该类型的指针类型。 但是,如果程序中出现这样的定义: ArcNode* p, q; 那么,q是指针吗?回想一下数组的定义,如果程序中出现: int zlen[DCT_SIZE*DCT_SIZE], cnt; 没有人会认为cnt也是数组。“*”和“[ ]”的作用对象都是紧邻它们的变量。那么为什么要将“*”紧靠在类型说明之后,故意造成误解?一个折中的办法是:在涉及指针的定义时,一行定义一个变量,于是上面的定义也就成了: ArcNode* p; ArcNode q;
还有两个容易混淆的概念,指针数组和指向数组的指针: int *ptr; ptr是指向整型的指针变量,在变量名后加“[ ]”,变为: int *(ptr[MAX_LENGTH]); 或 int *ptr[MAX_LENGTH]; 这时,ptr就成了长度为MAX_LENGTH指针数组,它的元素是指向整型的指针变量。 类似地: int val[MAX_LENGTH]; val是整型数组,在变量名前后加“*”,变为: int (*val)[MAX_LENGTH]; 由于默认运算符结合优先级的关系,必须加上括号,以强调结合次序。 这时,val成了指向整型数组的指针。 这与前面所述“变量名后加‘[ ]’,原变量类型简单聚合,成数组;变量名前加‘*’,成指针,指向原变量类型。”的规则相符。
4.2 函数指针 函数指针是将函数参数化的工具,许多C库函数使用函数指针作为参数,如bsearch、qsort: qsort((void *)list, length, sizeof(int), Comp); 函数名Comp标识了函数的入口地址,将它作为实际参数,传递给qsort的函数指针形式参数,该形参被定义为: int (*fcmp)(const void *, const void *) 对比一下Comp函数的定义: int Comp(const void *p1, const void *p2) { ... } qsort通过形参fcmp调用Comp函数对数组中的元素比较排序。
函数指针还可以参数化代码体内的控制。另外,利用函数指针数组,也可以创建基于表格驱动的应用程序。
4.3 指针的应用
(1) 构造链式数据结构,见结构体的应用(3)
(2) 引用动态分配的数据结构,如线性链表的构造: void CreateList_L(LinkList *L, int n) { LinkList p; int i; *L = (LinkList)malloc(sizeof(LNode)); (*L)->next = NULL; for(i=n; i>0; --i) { p = (LinkList)malloc(sizeof(LNode)); scanf("%d", &p->data); p->next = (*L)->next; (*L)->next = p; } }
(3) 实现引用调用(call by reference),见结构体的应用(2)
(4) 访问和迭代数据元素,如: Status GetElem_L(LinkList L, int i, int *e) { int j = 1; LinkList p = L->next; while(p!=NULL && j<i) { p = p->next; ++j; } if(p==NULL || j>i) return ERROR; *e = p->data; return OK; }
(5) 传递数组参数,如: void MakeHuffTable(DWORD code[], BYTE size[], const BYTE bit[], const BYTE val[]){ ... } code、size、bit和val虽然均以数组形式定义,用以接收数组实参,但其实是指针变量。
(6) 引用函数,例如二叉树遍历中,访问函数的调用: Status PreOrderTraverse(BiTree T, Status (* Visit)(char)) { if(T != NULL) { if(Visit(T->data)) if(PreOrderTraverse(T->lchild, Visit)) if(PreOrderTraverse(T->rchild, Visit)) return OK; return ERROR; } else return OK; }
(7) 作为其它值的别名
(8) 代表字符串,如: LPCTSTR lpszMenuName = "TestBMPMenu";
(9) 直接访问系统内存,以下是DOS环境中,访问显示存储器的代码片段: #define MK_FP(seg, ofs) ((void far *)(((unsigned long)(seg)<<16)|(unsigned)(ofs))) ... BYTE *p = MK_FP(0xa000, offset); ... if (T_bpp == 1) *p = g; else memcpy(p, &T_color[g], T_bpp); DOS存储空间的布局为: 段地址 长度 用途 0000H 640K 基本内存 A000H 128K 映射显示存储器 C000H 256K BIOS ROM区 宏MK_FP用于按给定的段地址(16位)和段内偏移(16位)构造远端指针(32位,段地址<<16 + 段内偏移)。而实际存储位置的物理地址值为20位(段地址<<4 + 段内偏移)。8086/8088只有20根地址线,因此,最大寻址空间为1M。 此时,远指针p指向的内存区域没有被定义为变量,它直接访问了系统内存。
5 函数的数据类型 函数的数据类型也就是函数返回值的数据类型,原则上可以是除了数组类型以外的任何类型。当然,函数也可以没有返回值,这时函数的数据类型为void。
|