1、typedef的用法
typedef的作用相当于给变量类型起别名,举个例子,long long 太长,每次都写long long很烦,我们可以简化成这样
typedef long long ll;
ll b;
scanf("%lld", &b);
printf("%lld\n", b);
那么,对于结构体我们可以这样使用
typedef struct
{
int num;
char ch;
}
stu;
stu s;
scanf("%d", &s.num);
printf("%d\n", s.num);
这样子就可以直接用stu来创建结构体 而不需要用struct stu s;这么长一串了。
自己的例子:
typedef struct student
{
char name[20];
int age;
double grade;
}sss;
int main()
{
struct student xiaoming;
int i;
return 0;
}
C语言允许用户使用 typedef 关键字来定义自己习惯的数据类型名称,来替代系统默认的基本类型名称、数组类型名称、指针类型名称与用户自定义的结构型名称、共用型名称、枚举型名称等。一旦用户在程序中定义了自己的数据类型名称,就可以在该程序中用自己的数据类型名称来定义变量的类型、数组的类型、指针变量的类型与函数的类型等。
例如,C 语言在 C99 之前并未提供布尔类型,但我们可以使用 typedef 关键字来定义一个简单的布尔类型,如下面的代码所示:
- typedef int BOOL;
- #define TRUE 1
- #define FALSE 0
定义好之后,就可以像使用基本类型数据一样使用它了,如下面的代码所示:
- BOOL bflag=TRUE;
typedef的4种用法
在实际使用中,typedef 的应用主要有如下4种。
1) 为基本数据类型定义新的类型名
也就是说,系统默认的所有基本类型都可以利用 typedef 关键字来重新定义类型名,示例代码如下所示:
- typedef unsigned int COUNT;
而且,我们还可以使用这种方法来定义与平台无关的类型。比如,要定义一个叫 REAL 的浮点类型,在目标平台一上,让它表示最高精度的类型,即:
- typedef long double REAL;
在不支持 long double 的平台二上,改为:
- typedef double REAL;
甚至还可以在连 double 都不支持的平台三上,改为:
- typedef float REAL;
这样,当跨平台移植程序时,我们只需要修改一下 typedef 的定义即可,而不用对其他源代码做任何修改。其实,标准库中广泛地使用了这个技巧,比如 size_t 在 VC++2010 的 crtdefs.h 文件中的定义如下所示:
- #ifndef _SIZE_T_DEFINED
- #ifdef _WIN64
- typedef unsigned __int64 size_t;
- #else
- typedef _W64 unsigned int size_t;
- #endif
- #define _SIZE_T_DEFINED
- #endif
2) 为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称
以结构体为例,下面我们定义一个名为 Point 的结构体:
- struct Point
- {
- double x;
- double y;
- double z;
- };
在调用这个结构体时,我们必须像下面的代码这样来调用这个结构体:
- struct Point oPoint1={100,100,0};
- struct Point oPoint2;
在这里,结构体 struct Point 为新的数据类型,在定义变量的时候均要向上面的调用方法一样有保留字 struct,而不能像 int 和 double 那样直接使用 Point 来定义变量。现在,我们利用 typedef 定义这个结构体,如下面的代码所示:
- typedef struct tagPoint
- {
- double x;
- double y;
- double z;
- } Point;
在上面的代码中,实际上完成了两个操作:
1、定义了一个新的结构类型,代码如下所示:
- struct tagPoint
- {
- double x;
- double y;
- double z;
- } ;
其中,struct 关键字和 tagPoint 一起构成了这个结构类型,无论是否存在 typedef 关键字,这个结构都存在。
2、使用 typedef 为这个新的结构起了一个别名,叫 Point,即:
- typedef struct tagPoint Point
因此,现在你就可以像 int 和 double 那样直接使用 Point 定义变量,如下面的代码所示:
- Point oPoint1={100,100,0};
- Point oPoint2;
为了加深对 typedef 的理解,我们再来看一个结构体例子,如下面的代码所示:
- typedef struct tagNode
- {
- char *pItem;
- pNode pNext;
- } *pNode;
从表面上看,上面的示例代码与前面的定义方法相同,所以应该没有什么问题。但是编译器却报了一个错误,为什么呢?莫非 C 语言不允许在结构中包含指向它自己的指针?
其实问题并非在于 struct 定义的本身,大家应该都知道,C 语言是允许在结构中包含指向它自己的指针的,我们可以在建立链表等数据结构的实现上看到很多这类例子。那问题在哪里呢?其实,根本问题还是在于 typedef 的应用。
在上面的代码中,新结构建立的过程中遇到了 pNext 声明,其类型是 pNode。这里要特别注意的是,pNode 表示的是该结构体的新别名。于是问题出现了,在结构体类型本身还没有建立完成的时候,编译器根本就不认识 pNode,因为这个结构体类型的新别名还不存在,所以自然就会报错。因此,我们要做一些适当的调整,比如将结构体中的 pNext 声明修改成如下方式:
- typedef struct tagNode
- {
- char *pItem;
- struct tagNode *pNext;
- } *pNode;
或者将 struct 与 typedef 分开定义,如下面的代码所示:
- typedef struct tagNode *pNode;
- struct tagNode
- {
- char *pItem;
- pNode pNext;
- };
在上面的代码中,我们同样使用 typedef 给一个还未完全声明的类型 tagNode 起了一个新别名。不过,虽然 C 语言编译器完全支持这种做法,但不推荐这样做。建议还是使用如下规范定义方法:
- struct tagNode
- {
- char *pItem;
- struct tagNode *pNext;
- };
- typedef struct tagNode *pNode;
3) 为数组定义简洁的类型名称
它的定义方法很简单,与为基本数据类型定义新的别名方法一样,示例代码如下所示:
- typedef int INT_ARRAY_100[100];
- INT_ARRAY_100 arr;
4) 为指针定义简洁的名称
对于指针,我们同样可以使用下面的方式来定义一个新的别名:
- typedef char* PCHAR;
- PCHAR pa;
对于上面这种简单的变量声明,使用 typedef 来定义一个新的别名或许会感觉意义不大,但在比较复杂的变量声明中,typedef 的优势马上就体现出来了,如下面的示例代码所示:
- int *(*a[5])(int,char*);
对于上面变量的声明,如果我们使用 typdef 来给它定义一个别名,这会非常有意义,如下面的代码所示:
- // PFun是我们创建的一个类型别名
- typedef int *(*PFun)(int,char*);
- // 使用定义的新类型来声明对象,等价于int*(*a[5])(int,char*);
- PFun a[5];
小心使用 typedef 带来的陷阱
接下来看一个简单的 typedef 使用示例,如下面的代码所示:
- typedef char* PCHAR;
- int strcmp(const PCHAR,const PCHAR);
在上面的代码中,“const PCHAR” 是否相当于 “const char*” 呢?
答案是否定的,原因很简单,typedef 是用来定义一种类型的新别名的,它不同于宏,不是简单的字符串替换。因此,“const PCHAR”中的 const 给予了整个指针本身常量性,也就是形成了常量指针“char*const(一个指向char的常量指针)”。即它实际上相当于“char*const”,而不是“const char*(指向常量 char 的指针)”。当然,要想让 const PCHAR 相当于 const char* 也很容易,如下面的代码所示:
- typedef const char* PCHAR;
- int strcmp(PCHAR, PCHAR);
其实,无论什么时候,只要为指针声明 typedef,那么就应该在最终的 typedef 名称中加一个 const,以使得该指针本身是常量。
还需要特别注意的是,虽然 typedef 并不真正影响对象的存储特性,但在语法上它还是一个存储类的关键字,就像 auto、extern、static 和 register 等关键字一样。因此,像下面这种声明方式是不可行的:
- typedef static int INT_STATIC;
不可行的原因是不能声明多个存储类关键字,由于 typedef 已经占据了存储类关键字的位置,因此,在 typedef 声明中就不能够再使用 static 或任何其他存储类关键字了。当然,编译器也会报错,如在 VC++2010 中的报错信息为“无法指定多个存储类”。
2、结构体 Struct
1、结构体是一种工具,用这个工具可以定义自己 的数据类型。
2、与数组相比,结构体中各个元素的数据类型可 以不相同。
3、 在嵌入式系统开发中,结构体可以将多个变量 组合为一个有机的整体,例如将串口定义为一 个结构体。
4、这样在需要调整入口参数时,可以直接通过修 改结构体成员变量来完成,而不需要采用传统 的修改函数定义的方法。
1、结构体的申明与使用方法比较灵活,其一 般形式为:
struct 结构体名
{
类型名1 成员名1 ;
类型名2 成员名2 ;
……
类型名n 成员名n ;
} ;
2、当需要使用结构体类型的变量、指针变量 和数组时,有以下三种方法:
方法一:定义结构体类型时,同时定义该类型 的变量。例如:
struct Student
{
char name[10];
char sex;
int age;
float score;
} stu1, *ps, stu[5]; // 定义结构体类型 的普通变量、指针变量和数组
方法二:先定义结构体类型,再定义该类型的 变量。例如:
struct student
{
char name[10];
char sex;
int age;
float score;
};
struct student stu1, *ps, stu[5];
//定义结构体类型的普通变量、指针变量和数组
方法三:先用类型定义符typedef为结构体类 型命别名,再用别名定义变量。例如: typedef struct [student]
{
char name[10];
char sex;
int age;
float score;
}STU;
STU stu1, *ps, stu[5]; //用别名定 义结构体类型的普通变量、指针变量和数组
3、结构体变量赋初值的方法:
struct [student]
{
char name[10];
char sex;
int age;
float score;
}
stu[2]={{"Li", 'F', 22, 90.5}, {"Su", 'M', 20, 88.5}};
4、引用结构体变量中成员的方法:
结构体变量名. 成员名: stu1.name
结构体指针变量成员名: ps name
(*结构体指针变量). 成员名: (*ps).name
结构体变量数组名. 成员名: stu[0].name
在C语言中,可以使用结构体(Struct)来存放一组不同类型的数据。结构体的定义形式为:
struct 结构体名{
结构体所包含的变量或数组
};
结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员(Member)。请看下面的一个例子:
- struct stu{
- char *name; //姓名
- int num; //学号
- int age; //年龄
- char group; //所在学习小组
- float score; //成绩
- };
stu 为结构体名,它包含了 5 个成员,分别是 name、num、age、group、score。结构体成员的定义方式与变量和数组的定义方式相同,只是不能初始化。
注意大括号后面的分号
;
不能少,这是一条完整的语句。
结构体也是一种数据类型,它由程序员自己定义,可以包含多个其他类型的数据。
像 int、float、char 等是由C语言本身提供的数据类型,不能再进行分拆,我们称之为基本数据类型;而结构体可以包含多个基本类型的数据,也可以包含其他的结构体,我们将它称为复杂数据类型或构造数据类型。
结构体变量
既然结构体是一种数据类型,那么就可以用它来定义变量。例如:
struct stu stu1, stu2;
定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字struct
不能少。
stu 就像一个“模板”,定义出来的变量都具有相同的性质。也可以将结构体比作“图纸”,将结构体变量比作“零件”,根据同一张图纸生产出来的零件的特性都是一样的。
你也可以在定义结构体的同时定义结构体变量:
- struct stu{
- char *name; //姓名
- int num; //学号
- int age; //年龄
- char group; //所在学习小组
- float score; //成绩
- } stu1, stu2;
将变量放在结构体定义的最后即可。
如果只需要 stu1、stu2 两个变量,后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名,如下所示:
- struct{ //没有写 stu
- char *name; //姓名
- int num; //学号
- int age; //年龄
- char group; //所在学习小组
- float score; //成绩
- } stu1, stu2;
这样做书写简单,但是因为没有结构体名,后面就没法用该结构体定义新的变量。
理论上讲结构体的各个成员在内存中是连续存储的,和数组非常类似,例如上面的结构体变量 stu1、stu2 的内存分布如下图所示,共占用 4+4+4+1+4 = 17 个字节。
但是在编译器的具体实现中,各个成员之间可能会存在缝隙,对于 stu1、stu2,成员变量 group 和 score 之间就存在 3 个字节的空白填充(见下图)。这样算来,stu1、stu2 其实占用了 17 + 3 = 20 个字节。
关于成员变量之间存在“裂缝”的原因,我们将在《C语言内存精讲》专题中的《C语言内存对齐,提高寻址效率》一节中详细讲解。
成员的获取和赋值
结构体和数组类似,也是一组数据的集合,整体使用没有太大的意义。数组使用下标[ ]
获取单个元素,结构体使用点号.
获取单个成员。获取结构体成员的一般格式为:
结构体变量名.成员名;
通过这种方式可以获取成员的值,也可以给成员赋值:
纯文本复制
- #include <stdio.h>
- int main(){
- struct{
- char *name; //姓名
- int num; //学号
- int age; //年龄
- char group; //所在小组
- float score; //成绩
- } stu1;
- //给结构体成员赋值
- stu1.name = "Tom";
- stu1.num = 12;
- stu1.age = 18;
- stu1.group = 'A';
- stu1.score = 136.5;
- //读取结构体成员的值
- printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", stu1.name, stu1.num, stu1.age, stu1.group, stu1.score);
- return 0;
- }
运行结果:
Tom的学号是12,年龄是18,在A组,今年的成绩是136.5!
除了可以对成员进行逐一赋值,也可以在定义时整体赋值,例如:
- struct{
- char *name; //姓名
- int num; //学号
- int age; //年龄
- char group; //所在小组
- float score; //成绩
- } stu1, stu2 = { "Tom", 12, 18, 'A', 136.5 };
不过整体赋值仅限于定义结构体变量的时候,在使用过程中只能对成员逐一赋值,这和数组的赋值非常类似。
3、宏定义
预处理命令可以改变程序设计环境,提高编程效率,它们并不是 C 语言本身的组成部分,不能直接对 它们进行编译,必须在对程序进行编译之前,先对程序中这些特殊的命令进行“预处理” 。经过预处理后,程序就不再包括预处理命令了,最后再由编译程序对预处理之后的源程序进行编译处理,得到可供执行的 目标代码。C 语言提供的预处理功能有三种,分别为宏定义、文件包含和条件编译,下面将对它们进行简 单介绍。
宏定义 在 C 语言源程序中允许用一个标识符来表示一个字符串,称为“宏” ,被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。 宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的。在 C 语言中,宏分为 有参数和无参数两种。无参宏的宏名后不带参数,其定义的一般形式为:
#define 标识符 字符串;
其中“#”表示这是一条预处理命令(在 C 语言中凡是以“#”开头的均为预处理命令)“define”为宏定义命令,“标识符”为所定义的宏名, “字符串”可以是常数、表达式、格式串等。符号常量的定义就是一种无参宏定义。
此外,常常对程序中反复使用的表达式进行宏定义。例如:
1 |
|
它的作用是指定标识符 M 来代替表达式(y*y+3*y)。
在编写源程序时, 所有的(y*y+3*y)都可由 M 代替, 而对源程序进行编译时,将先由预处理程序进行宏代换,即用(y*y+3*y)表达式去置换所有的宏名 M,然后 再进行编译。
C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。对于带 参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。
带参宏定义的一般形式为:
#define 宏名(形参表) 字符串;
在字符串中含有各个形参。
带参宏调用的一般形式为:
宏名(实参表);
例如:
1 2 3 4 5 |
|
......
在上面的宏调用时,用实参 5 去代替形参 y,经预处理宏展开后的语句为:
k=5*5+3*5;
程序 2.26 给出了一个宏定义和调用的完整实例。
定义一个名为 MAX 的带参数的宏,可以通过它来选出参数 a、b 中的较大值:test26.c。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
程序运行结果如下(□表示空格,↙表示回车) :
1 2 |
|
可以看到,宏替换相当于实现了一个函数调用的功能,而事实上,与函数调用相比,宏调用更能提高
C 程序的执行效率。
4、条件编译
条件编译
预处理程序提供了条件编译的功能,可以按不同的条件去编译不同的程序部分,因而产生不同 的目标代码文件,这对于程序的移植和调试是很有用的。条件编译可分为三种形式。
第一种形式如下:
#ifdef 标识符
程序段 1
#else
程序段 2
#endif
它的功能是如果标识符已被 #define 命令定义过则对程序段 1 进行编译;否则对程序段 2 进行编译。
如果没有程序段 2(为空),本格式中的#else 可以没有,即可以写为:
#ifdef 标识符
程序段
#endif
第二种形式如下:
#ifndef 标识符
程序段 1 #else
程序段 2 #endif
与第一种形式的区别是将“ifdef”改为“ifndef”。它的功能是如果标识符未被#define 命令定义过则对程 序段 1 进行编译,否则对程序段 2 进行编译。这与第一种形式的功能正好相反。
第三种形式如下:
#if 常量表达式
程序段 1 #else
程序段 2 #endif
它的功能是如果常量表达式的值为真(非 0),则对程序段 1 进行编译,否则对程序段 2 进行编译。
因此可以使程序在不同的条件下完成不同的功能。
条件编译是指预处理器根据条件编译指令,有条件地选择源程序代码中的一部分代码作为输出,送给编译器进行编译。主要是为了有选择性地执行相应操作,防止宏替换内容(如文件等)的重复包含。常见的条件编译指令如表 1 所示。
条件编译指令 | 说 明 |
---|---|
#if | 如果条件为真,则执行相应操作 |
#elif | 如果前面条件为假,而该条件为真,则执行相应操作 |
#else | 如果前面条件均为假,则执行相应操作 |
#endif | 结束相应的条件编译指令 |
#ifdef | 如果该宏已定义,则执行相应操作 |
#ifndef | 如果该宏没有定义,则执行相应操作 |
#if-#else-#endif
其调用格式为:
#if 条件表达式
程序段1
#else
程序段2
#endif
功能为:如果#if后的条件表达式为真,则程序段 1 被选中,否则程序段 2 被选中。
注意,必须使用 #endif 结束该条件编译指令。
例如:
- #include<stdio.h>
- #define RESULT 0//定义 RESULT 为 0
- int main (void)
- {
- #if !RESULT //或者 0==RESULT
- printf("It's False!\n");
- #else
- printf("It's True!\n");
- #endif //标志结束#if
- return 0;
- }
上述程序中,首先定义了 RESULT 为 0,在 main 中使用 #if-#else-#endif 条件判断语句,如果 RESULT 为 0,则输出 It's False!,否则输出 It's True!。本例输出为:It's False!。
#ifndef-#define-#endif
其调用格式为:
#ifndef 标识符
#define 标识符 替换列表
//...
#endif
功能为:一般用于检测程序中是否已经定义了名字为某标识符的宏,如果没有定义该宏,则定义该宏,并选中从 #define 开始到 #endif 之间的程序段;如果已定义,则不再重复定义该符号,且相应程序段不被选中。
例如:
- #ifndef PI
- #define PI 3.1416
- #endif
上述程序段,用于判断是否已经定义了名为 PI 的宏,如果没有定义 PI,则执行如下宏定义。
- #define PI 3.1416
如果检测到已经定义了 PI,则不再重复执行上述宏定义。
该条件编译指令更重要的一个应用是防止头文件重复包含。
如果 f.c 源文件中包含 f1.h 和 f2.h 两个头文件,而 f1.h 头文件及 f2.h 头文件中均包含 x.h 头文件,则 f.c 源文件中重复包含 x.h 头文件。可采用条件编译指令,来避免头文件的重复包含问题。所有头文件中都按如下格式:
#ifndef _HEADNAME_H_
#define _HEADNAME_H_
//头文件内容
#endif
当该头文件第一次被包含时,由于没检测到该头文件名对应的符号(宏名)_HEADNAME_H_,则定义该头文件名对应的符号(宏),其值为该系统默认。并且,该条件编译指令选中 #endif 之前的头文件内容;如果该头文件再次被包含时,由于检测到已存在以该头文件名对应的符号(宏名),则忽略该条件编译指令之间的所有代码,从而避免了重复包含。
#if-#elif-#else-#endif
其调用格式为:
#if 条件表达式1
程序段 1
#elif 条件表达式2
程序段 2
#else
程序段3
#endif
功能为:先判断条件1的值,如果为真,则程序段 1 被选中编译;如果为假,而条件表达式 2 的值为真,则程序段 2 被选中编译;其他情况,程序段 3 被选中编译。
#ifdef-#endif
其调用格式为:
#ifdef 标识符
程序段
#endif
功能为:如果检测到已定义该标识符,则选择执行相应程序段被选中编译;否则,该程序段会被忽略。
例如:
- #ifdef N
- #undef N
- //程序段
- #endif
功能:如果检测到符号 N 已定义,则删除其定义,并选中相应的程序段。