2021-02-03

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 关键字来定义一个简单的布尔类型,如下面的代码所示:

 

  1. typedef int BOOL;
  2. #define TRUE 1
  3. #define FALSE 0

定义好之后,就可以像使用基本类型数据一样使用它了,如下面的代码所示:

 
  1. BOOL bflag=TRUE;

typedef的4种用法

在实际使用中,typedef 的应用主要有如下4种。

1) 为基本数据类型定义新的类型名

也就是说,系统默认的所有基本类型都可以利用 typedef 关键字来重新定义类型名,示例代码如下所示:

 
  1. typedef unsigned int COUNT;

而且,我们还可以使用这种方法来定义与平台无关的类型。比如,要定义一个叫 REAL 的浮点类型,在目标平台一上,让它表示最高精度的类型,即:

 
  1. typedef long double REAL;

在不支持 long double 的平台二上,改为:

 
  1. typedef double REAL;

甚至还可以在连 double 都不支持的平台三上,改为:

 
  1. typedef float REAL;

这样,当跨平台移植程序时,我们只需要修改一下 typedef 的定义即可,而不用对其他源代码做任何修改。其实,标准库中广泛地使用了这个技巧,比如 size_t 在 VC++2010 的 crtdefs.h 文件中的定义如下所示:

 
  1. #ifndef _SIZE_T_DEFINED
  2. #ifdef _WIN64
  3. typedef unsigned __int64 size_t;
  4. #else
  5. typedef _W64 unsigned int size_t;
  6. #endif
  7. #define _SIZE_T_DEFINED
  8. #endif

2) 为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称

以结构体为例,下面我们定义一个名为 Point 的结构体:

 
  1. struct Point
  2. {
  3. double x;
  4. double y;
  5. double z;
  6. };

在调用这个结构体时,我们必须像下面的代码这样来调用这个结构体:

 
  1. struct Point oPoint1={100,100,0};
  2. struct Point oPoint2;

在这里,结构体 struct Point 为新的数据类型,在定义变量的时候均要向上面的调用方法一样有保留字 struct,而不能像 int 和 double 那样直接使用 Point 来定义变量。现在,我们利用 typedef 定义这个结构体,如下面的代码所示:

 
  1. typedef struct tagPoint
  2. {
  3. double x;
  4. double y;
  5. double z;
  6. } Point;

在上面的代码中,实际上完成了两个操作:
1、定义了一个新的结构类型,代码如下所示:

 
  1. struct tagPoint
  2. {
  3. double x;
  4. double y;
  5. double z;
  6. } ;

其中,struct 关键字和 tagPoint 一起构成了这个结构类型,无论是否存在 typedef 关键字,这个结构都存在。

2、使用 typedef 为这个新的结构起了一个别名,叫 Point,即:

 
  1. typedef struct tagPoint Point

因此,现在你就可以像 int 和 double 那样直接使用 Point 定义变量,如下面的代码所示:

 
  1. Point oPoint1={100,100,0};
  2. Point oPoint2;

为了加深对 typedef 的理解,我们再来看一个结构体例子,如下面的代码所示:

 
  1. typedef struct tagNode
  2. {
  3. char *pItem;
  4. pNode pNext;
  5. } *pNode;

从表面上看,上面的示例代码与前面的定义方法相同,所以应该没有什么问题。但是编译器却报了一个错误,为什么呢?莫非 C 语言不允许在结构中包含指向它自己的指针?

其实问题并非在于 struct 定义的本身,大家应该都知道,C 语言是允许在结构中包含指向它自己的指针的,我们可以在建立链表等数据结构的实现上看到很多这类例子。那问题在哪里呢?其实,根本问题还是在于 typedef 的应用。

在上面的代码中,新结构建立的过程中遇到了 pNext 声明,其类型是 pNode。这里要特别注意的是,pNode 表示的是该结构体的新别名。于是问题出现了,在结构体类型本身还没有建立完成的时候,编译器根本就不认识 pNode,因为这个结构体类型的新别名还不存在,所以自然就会报错。因此,我们要做一些适当的调整,比如将结构体中的 pNext 声明修改成如下方式:

 
  1. typedef struct tagNode
  2. {
  3. char *pItem;
  4. struct tagNode *pNext;
  5. } *pNode;

或者将 struct 与 typedef 分开定义,如下面的代码所示:

 
  1. typedef struct tagNode *pNode;
  2. struct tagNode
  3. {
  4. char *pItem;
  5. pNode pNext;
  6. };

在上面的代码中,我们同样使用 typedef 给一个还未完全声明的类型 tagNode 起了一个新别名。不过,虽然 C 语言编译器完全支持这种做法,但不推荐这样做。建议还是使用如下规范定义方法:

 
  1. struct tagNode
  2. {
  3. char *pItem;
  4. struct tagNode *pNext;
  5. };
  6. typedef struct tagNode *pNode;

3) 为数组定义简洁的类型名称

它的定义方法很简单,与为基本数据类型定义新的别名方法一样,示例代码如下所示:

 
  1. typedef int INT_ARRAY_100[100];
  2. INT_ARRAY_100 arr;

4) 为指针定义简洁的名称

对于指针,我们同样可以使用下面的方式来定义一个新的别名:

 
  1. typedef char* PCHAR;
  2. PCHAR pa;

对于上面这种简单的变量声明,使用 typedef 来定义一个新的别名或许会感觉意义不大,但在比较复杂的变量声明中,typedef 的优势马上就体现出来了,如下面的示例代码所示:

 
  1. int *(*a[5])(int,char*);

对于上面变量的声明,如果我们使用 typdef 来给它定义一个别名,这会非常有意义,如下面的代码所示:

 
  1. // PFun是我们创建的一个类型别名
  2. typedef int *(*PFun)(int,char*);
  3. // 使用定义的新类型来声明对象,等价于int*(*a[5])(int,char*);
  4. PFun a[5];

小心使用 typedef 带来的陷阱

接下来看一个简单的 typedef 使用示例,如下面的代码所示:

 
  1. typedef char* PCHAR;
  2. 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* 也很容易,如下面的代码所示:

 
  1. typedef const char* PCHAR;
  2. int strcmp(PCHAR, PCHAR);

其实,无论什么时候,只要为指针声明 typedef,那么就应该在最终的 typedef 名称中加一个 const,以使得该指针本身是常量。

还需要特别注意的是,虽然 typedef 并不真正影响对象的存储特性,但在语法上它还是一个存储类的关键字,就像 auto、extern、static 和 register 等关键字一样。因此,像下面这种声明方式是不可行的:

 
  1. 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)。请看下面的一个例子:

 
  1. struct stu{
  2. char *name; //姓名
  3. int num; //学号
  4. int age; //年龄
  5. char group; //所在学习小组
  6. float score; //成绩
  7. };

stu 为结构体名,它包含了 5 个成员,分别是 name、num、age、group、score。结构体成员的定义方式与变量和数组的定义方式相同,只是不能初始化。

注意大括号后面的分号 ;不能少,这是一条完整的语句。

结构体也是一种数据类型,它由程序员自己定义,可以包含多个其他类型的数据。

像 int、float、char 等是由C语言本身提供的数据类型,不能再进行分拆,我们称之为基本数据类型;而结构体可以包含多个基本类型的数据,也可以包含其他的结构体,我们将它称为复杂数据类型或构造数据类型。

结构体变量

既然结构体是一种数据类型,那么就可以用它来定义变量。例如:

struct stu stu1, stu2;

定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字struct不能少。

stu 就像一个“模板”,定义出来的变量都具有相同的性质。也可以将结构体比作“图纸”,将结构体变量比作“零件”,根据同一张图纸生产出来的零件的特性都是一样的。

你也可以在定义结构体的同时定义结构体变量:

 
  1. struct stu{
  2. char *name; //姓名
  3. int num; //学号
  4. int age; //年龄
  5. char group; //所在学习小组
  6. float score; //成绩
  7. } stu1, stu2;

将变量放在结构体定义的最后即可。

如果只需要 stu1、stu2 两个变量,后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名,如下所示:

 
  1. struct{ //没有写 stu
  2. char *name; //姓名
  3. int num; //学号
  4. int age; //年龄
  5. char group; //所在学习小组
  6. float score; //成绩
  7. } stu1, stu2;

这样做书写简单,但是因为没有结构体名,后面就没法用该结构体定义新的变量。

理论上讲结构体的各个成员在内存中是连续存储的,和数组非常类似,例如上面的结构体变量 stu1、stu2 的内存分布如下图所示,共占用 4+4+4+1+4 = 17 个字节。


但是在编译器的具体实现中,各个成员之间可能会存在缝隙,对于 stu1、stu2,成员变量 group 和 score 之间就存在 3 个字节的空白填充(见下图)。这样算来,stu1、stu2 其实占用了 17 + 3 = 20 个字节。

关于成员变量之间存在“裂缝”的原因,我们将在《C语言内存精讲》专题中的《C语言内存对齐,提高寻址效率》一节中详细讲解。

成员的获取和赋值

结构体和数组类似,也是一组数据的集合,整体使用没有太大的意义。数组使用下标[ ]获取单个元素,结构体使用点号.获取单个成员。获取结构体成员的一般格式为:

结构体变量名.成员名;

通过这种方式可以获取成员的值,也可以给成员赋值:

纯文本复制
 
  1. #include <stdio.h>
  2. int main(){
  3. struct{
  4. char *name; //姓名
  5. int num; //学号
  6. int age; //年龄
  7. char group; //所在小组
  8. float score; //成绩
  9. } stu1;
  10.  
  11. //给结构体成员赋值
  12. stu1.name = "Tom";
  13. stu1.num = 12;
  14. stu1.age = 18;
  15. stu1.group = 'A';
  16. stu1.score = 136.5;
  17.  
  18. //读取结构体成员的值
  19. printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", stu1.name, stu1.num, stu1.age, stu1.group, stu1.score);
  20.  
  21. return 0;
  22. }

运行结果:
Tom的学号是12,年龄是18,在A组,今年的成绩是136.5!

除了可以对成员进行逐一赋值,也可以在定义时整体赋值,例如:

 
  1. struct{
  2. char *name; //姓名
  3. int num; //学号
  4. int age; //年龄
  5. char group; //所在小组
  6. float score; //成绩
  7. } stu1, stu2 = { "Tom", 12, 18, 'A', 136.5 };

不过整体赋值仅限于定义结构体变量的时候,在使用过程中只能对成员逐一赋值,这和数组的赋值非常类似。

3、宏定义

预处理命令可以改变程序设计环境,提高编程效率,它们并不是 C 语言本身的组成部分,不能直接对 它们进行编译,必须在对程序进行编译之前,先对程序中这些特殊的命令进行“预处理” 。经过预处理后,程序就不再包括预处理命令了,最后再由编译程序对预处理之后的源程序进行编译处理,得到可供执行的 目标代码。C 语言提供的预处理功能有三种,分别为宏定义、文件包含和条件编译,下面将对它们进行简 单介绍。 
宏定义 在 C 语言源程序中允许用一个标识符来表示一个字符串,称为“宏” ,被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。 宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的。在 C 语言中,宏分为 有参数和无参数两种。无参宏的宏名后不带参数,其定义的一般形式为: 
#define 标识符 字符串; 
其中“#”表示这是一条预处理命令(在 C 语言中凡是以“#”开头的均为预处理命令)“define”为宏定义命令,“标识符”为所定义的宏名, “字符串”可以是常数、表达式、格式串等。符号常量的定义就是一种无参宏定义。 
此外,常常对程序中反复使用的表达式进行宏定义。例如: 

 

 

1

#define M (y*y+3*y);

    

它的作用是指定标识符 M 来代替表达式(y*y+3*y)。 
在编写源程序时, 所有的(y*y+3*y)都可由 M 代替, 而对源程序进行编译时,将先由预处理程序进行宏代换,即用(y*y+3*y)表达式去置换所有的宏名 M,然后 再进行编译。 
C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。对于带 参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。 
带参宏定义的一般形式为: 
#define 宏名(形参表) 字符串; 
在字符串中含有各个形参。 
带参宏调用的一般形式为: 
宏名(实参表); 
例如: 

   

1

2

3

4

5

#define M(y) y*y+3*y

/*宏定义*/

......

k=M(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

#include <stdio.h>

#define MAX(a,b) (a>b)?a:b

/*带参数的宏定义*/

main()

{

int x,y,max;

printf("input two numbers: ");

scanf("%d %d",&x,&y);

max=MAX(x,y);

printf("max=%d\n",max);

/*宏调用*/

}</stdio.h>

    

程序运行结果如下(□表示空格,↙表示回车) : 

   

1

2

input two numbers: 2009□2010↙

max=2010

    

可以看到,宏替换相当于实现了一个函数调用的功能,而事实上,与函数调用相比,宏调用更能提高 
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 所示。
 

表 1 常见的条件编译指令
条件编译指令说 明
#if如果条件为真,则执行相应操作
#elif如果前面条件为假,而该条件为真,则执行相应操作
#else如果前面条件均为假,则执行相应操作
#endif结束相应的条件编译指令
#ifdef如果该宏已定义,则执行相应操作
#ifndef如果该宏没有定义,则执行相应操作

#if-#else-#endif

其调用格式为:

#if 条件表达式
    程序段1
#else
    程序段2
#endif

功能为:如果#if后的条件表达式为真,则程序段 1 被选中,否则程序段 2 被选中。

注意,必须使用 #endif 结束该条件编译指令。

例如:

 
  1. #include<stdio.h>
  2. #define RESULT 0//定义 RESULT 为 0
  3. int main (void)
  4. {
  5. #if !RESULT //或者 0==RESULT
  6. printf("It's False!\n");
  7. #else
  8. printf("It's True!\n");
  9. #endif //标志结束#if
  10. return 0;
  11. }

上述程序中,首先定义了 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 之间的程序段;如果已定义,则不再重复定义该符号,且相应程序段不被选中。

例如:

 
  1. #ifndef PI
  2. #define PI 3.1416
  3. #endif

上述程序段,用于判断是否已经定义了名为 PI 的宏,如果没有定义 PI,则执行如下宏定义。

 
  1. #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

功能为:如果检测到已定义该标识符,则选择执行相应程序段被选中编译;否则,该程序段会被忽略。

例如:

 
  1. #ifdef N
  2. #undef N
  3. //程序段
  4. #endif

功能:如果检测到符号 N 已定义,则删除其定义,并选中相应的程序段。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值