文章目录

    • 一.结构体
      • 1. 定义
      • 2. 声明
    • 二.联合体
      • 1.定义
      • 2.赋值
      • 3.应用
    • 三.枚举
      • 1.枚举的由来
      • 2. 枚举定义
      • 3.枚举变量
      • 4.枚举实例测试
    • 四.宏
      • 1. 宏的理解
      • 2. 常量宏定义
      • 3. 常量宏定义注意点
      • 4. 含参数宏定义
      • 5. 含参数宏注意点
      • 6.宏参数的拼接
      • 7. 含参数宏定义与函数区别
    • 五.typedef类型重命名
      • 1.基本数据类型
      • 2.数组类型
      • 3. 结构体类型
      • 4.指针类型
      • 5. typedef 和 #define 的区别

一.结构体

在c语言中,数据类型分为基本类型(int、double、float、short、long、char)和构造类型(数组,结构体,共用体和枚举)。

我们在构建代码时,往往同种属性的变量并不属于同种基本数据类型,这样使得数组不能够满足我们当前的需求,所以结构体和共用体就应运而生了。今天我们主要讲结构体的应用。

结构体所占内存应该是大于等于成员所占内存总和。这涉及到字节对齐,这方面也是一个比较重要的点,我们在以后会详细介绍。并且各个成员在内存中的储存是连续的。

1. 定义

结构体的成员可以是基本的数据类型,也可以是构造类型,相对来说比较自由。

  1. 结构体定义的****一般形式如下所示:

    struct 结构体名 {类型名1 成员名1;类型名2 成员名2;...类型名n 成员名n;
    };
    

    举例:

    struct stu{char *name;  //姓名int num;  //学号int age;  //年龄char group;  //所在学习小组float score;  //成绩
    };
    

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

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

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

  2. 定义并声明结构体变量

    struct 结构体名 {类型名1 成员名1;类型名2 成员名2;...类型名n 成员名n;
    }变量1,变量2,...;
    

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

    举例:

    struct stu{char *name;  //姓名int num;  //学号int age;  //年龄char group;  //所在学习小组float score;  //成绩
    } stu1, stu2;
    

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

    struct{  //没有写 stuchar *name;  //姓名int num;  //学号int age;  //年龄char group;  //所在学习小组float score;  //成绩
    } stu1, stu2;
    

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

  3. 定义结构体并赋初值:

    struct 结构体名 {类型名1 成员名1;类型名2 成员名2;...类型名n 成员名n;
    }数组[2]={{a,b,...,n},{a1,b1,...,n1}};
    

    举例:

    struct student {char name[10];float score;int num;
    }stu[2]={{"tom",95.5,4},{"jhon",93,3}};
    
2. 声明

在声明的时候加上关键字“struct”。

  1. 声明的一般形式

    struct 结构体名 变量1,变量2;
    

    举例:
    struct student stu1,stu2;

    
    定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字`struct`不能少。stu 就像一个“模板”,定义出来的变量都具有相同的性质。也可以将结构体比作“图纸”,将结构体变量比作“零件”,根据同一张图纸生产出来的零件的特性都是一样的。
  2. 声明时赋初值**

    struct 结构体名 变量1 = {a,b,...,n},变量2;举例:
    struct student stu1 = {"tom",95.5,4},stu2;
    

成员的获取和赋值

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

结构体变量名.成员名;

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

#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 };

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

需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。

二.联合体

1.定义

我们知道结构体(Struct)是一种构造类型或复杂类型,它可以包含多个类型不同的成员。在C语言中,还有另外一种和结构体非常类似的语法,叫做共用体(Union),它的定义格式为:

union 共用体名{成员列表
};

共用体有时也被称为联合或者联合体,这也是 Union 这个单词的本意。

结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。

结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。

2.赋值
  1. 共用体也是一种自定义类型,可以通过它来创建变量,例如:

    union data{int n;char ch;double f;
    };
    union data a, b, c;
    
  2. 上面是先定义共用体,再创建变量,也可以在定义共用体的同时创建变量:

    union data{int n;char ch;double f;
    } a, b, c;
    
  3. 如果不再定义新的变量,也可以将共用体的名字省略:

    union{int n;char ch;double f;
    } a, b, c;
    

程序实例

#include <stdio.h>
union data{int n;char ch;short m;
};
int main(){union data a;printf("%d, %d\n", sizeof(a), sizeof(union data) );a.n = 0x40;printf("%X, %c, %hX\n", a.n, a.ch, a.m);a.ch = '9';printf("%X, %c, %hX\n", a.n, a.ch, a.m);a.m = 0x2059;printf("%X, %c, %hX\n", a.n, a.ch, a.m);a.n = 0x3E25AD54;printf("%X, %c, %hX\n", a.n, a.ch, a.m);return 0;
}

运行结果:

4, 4
40, @, 40
39, 9, 39
2059, Y, 2059
3E25AD54, T, AD54

这段代码不但验证了共用体的长度,还说明共用体成员之间会相互影响,修改一个成员的值会影响其他成员。

要想理解上面的输出结果,弄清成员之间究竟是如何相互影响的,就得了解各个成员在内存中的分布。以上面的 data 为例,各个成员在内存中的分布如下:

成员 n、ch、m 在内存中“对齐”到一头,对 ch 赋值修改的是前一个字节,对 m 赋值修改的是前两个字节,对 n 赋值修改的是全部字节。也就是说,ch、m 会影响到 n 的一部分数据,而 n 会影响到 ch、m 的全部数据。

3.应用

共用体在一般的编程中应用较少,在单片机中应用较多。对于 PC 机,经常使用到的一个实例是: 现有一张关于学生信息和教师信息的表格。学生信息包括姓名、编号、性别、职业、分数,教师的信息包括姓名、编号、性别、职业、教学科目。请看下面的表格:

Name Num Sex Profession Score / Course
HanXiaoXiao 501 f s 89.5
YanWeiMin 1011 m t math
LiuZhenTao 109 f t English
ZhaoFeiYan 982 m s 95.0

f 和 m 分别表示女性和男性,s 表示学生,t 表示教师。可以看出,学生和教师所包含的数据是不同的。现在要求把这些信息放在同一个表格中,并设计程序输入人员信息然后输出。

如果把每个人的信息都看作一个结构体变量的话,那么教师和学生的前 4 个成员变量是一样的,第 5 个成员变量可能是 score 或者 course。当第 4 个成员变量的值是 s 的时候,第 5 个成员变量就是 score;当第 4 个成员变量的值是 t 的时候,第 5 个成员变量就是 course。

经过上面的分析,我们可以设计一个包含共用体的结构体,请看下面的代码:

#include <stdio.h>
#include <stdlib.h>
#define TOTAL 4  //人员总数
struct{char name[20];int num;char sex;char profession;union{float score;char course[20];} sc;
} bodys[TOTAL];
int main(){int i;//输入人员信息for(i=0; i<TOTAL; i++){printf("Input info: ");scanf("%s %d %c %c", bodys[i].name, &(bodys[i].num), &(bodys[i].sex), &(bodys[i].profession));if(bodys[i].profession == 's'){  //如果是学生scanf("%f", &bodys[i].sc.score);}else{  //如果是老师scanf("%s", bodys[i].sc.course);}fflush(stdin);}//输出人员信息printf("\nName\t\tNum\tSex\tProfession\tScore / Course\n");for(i=0; i<TOTAL; i++){if(bodys[i].profession == 's'){  //如果是学生printf("%s\t%d\t%c\t%c\t\t%f\n", bodys[i].name, bodys[i].num, bodys[i].sex, bodys[i].profession, bodys[i].sc.score);}else{  //如果是老师printf("%s\t%d\t%c\t%c\t\t%s\n", bodys[i].name, bodys[i].num, bodys[i].sex, bodys[i].profession, bodys[i].sc.course);}}return 0;
}

运行结果:

Input info: HanXiaoXiao 501 f s 89.5↙
Input info: YanWeiMin 1011 m t math↙
Input info: LiuZhenTao 109 f t English↙
Input info: ZhaoFeiYan 982 m s 95.0↙Name            Num     Sex     Profession      Score / Course
HanXiaoXiao     501     f       s               89.500000
YanWeiMin       1011    m       t               math
LiuZhenTao      109     f       t               English
ZhaoFeiYan      982     m       s               95.000000

三.枚举

1.枚举的由来

在实际编程中,有些数据的取值往往是有限的,只能是非常少量的整数,并且最好为每个值都取一个名字,以方便在后续代码中使用,比如一个星期只有七天,一年只有十二个月,一个班每周有六门课程等。

以每周七天为例,我们可以使用#define命令来给每天指定一个名字:

#include <stdio.h>
#define Mon 1
#define Tues 2
#define Wed 3
#define Thurs 4
#define Fri 5
#define Sat 6
#define Sun 7
int main(){int day;scanf("%d", &day);switch(day){case Mon: puts("Monday"); break;case Tues: puts("Tuesday"); break;case Wed: puts("Wednesday"); break;case Thurs: puts("Thursday"); break;case Fri: puts("Friday"); break;case Sat: puts("Saturday"); break;case Sun: puts("Sunday"); break;default: puts("Error!");}return 0;
}

运行结果:
5↙
Friday

#define命令虽然能解决问题,但也带来了不小的副作用,导致宏名过多,代码松散,看起来总有点不舒服。C语言提供了一种枚举(Enum)类型,能够列出所有可能的取值,并给它们取一个名字。

2. 枚举定义

枚举类型的定义形式为:

enum typeName{ valueName1, valueName2, valueName3, ...... };

enum是一个新的关键字,专门用来定义枚举类型,这也是它在C语言中的唯一用途;typeName是枚举类型的名字;valueName1, valueName2, valueName3, ......是每个值对应的名字的列表。注意最后的;不能少。

例如,列出一个星期有几天:

enum week{ Mon, Tues, Wed, Thurs, Fri, Sat, Sun };

可以看到,我们仅仅给出了名字,却没有给出名字对应的值,这是因为枚举值默认从 0 开始,往后逐个加 1(递增);也就是说,week 中的 Mon、Tues … Sun 对应的值分别为 0、1 … 6。

我们也可以给每个名字都指定一个值:

enum week{ Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 };

更为简单的方法是只给第一个名字指定值:

enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };

这样枚举值就从 1 开始递增,跟上面的写法是等效的。

3.枚举变量

枚举是一种类型,通过它可以定义枚举变量:

enum week a, b, c;
有了枚举变量,就可以把列表中的值赋给它:enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
enum week a = Mon, b = Wed, c = Sat;或者:
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a = Mon, b = Wed, c = Sat;
4.枚举实例测试

程序实例:

#include <stdio.h>
int main(){enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day;scanf("%d", &day);switch(day){case Mon: puts("Monday"); break;case Tues: puts("Tuesday"); break;case Wed: puts("Wednesday"); break;case Thurs: puts("Thursday"); break;case Fri: puts("Friday"); break;case Sat: puts("Saturday"); break;case Sun: puts("Sunday"); break;default: puts("Error!");}return 0;
}
-------------------------------------------------------------------------------------------
运行结果:
4↙
Thursday

需要注意的两点是:
1) 枚举列表中的 Mon、Tues、Wed 这些标识符的作用范围是全局的(严格来说是 main() 函数内部),不能再定义与它们名字相同的变量。

2) Mon、Tues、Wed 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量。

枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。

对于上面的代码,在编译的某个时刻会变成类似下面的样子:

#include <stdio.h>
int main(){enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day;scanf("%d", &day);switch(day){case 1: puts("Monday"); break;case 2: puts("Tuesday"); break;case 3: puts("Wednesday"); break;case 4: puts("Thursday"); break;case 5: puts("Friday"); break;case 6: puts("Saturday"); break;case 7: puts("Sunday"); break;default: puts("Error!");}return 0;
}

Mon、Tues、Wed 这些名字都被替换成了对应的数字。这意味着,Mon、Tues、Wed 等都不是变量,它们不占用数据区(常量区、全局数据区、栈区和堆区)的内存,而是直接被编译到命令里面,放到代码区,所以不能用&取得它们的地址。这就是枚举的本质。

四.宏

1. 宏的理解

作用:enum给整数命名,typedef给类型命名,我这个宏可以给一切“重命名”(当然本质是替换)。

本质:单纯的替换

define 叫做宏定义命令,它也是C语言预处理命令的一种。所谓宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。

2. 常量宏定义

我们先通过一个例子来看一下 #define 的用法:

#include <stdio.h>
#define N 100
int main(){int sum = 20 + N;printf("%d\n", sum);return 0;
}
---------------------------------------------------------------------------------------------
运行结果:
120

注意第 6 行代码int sum = 20 + NN100代替了。

#define N 100就是宏定义,N为宏名,100是宏的内容(宏所表示的字符串)。在预处理阶段,对程序中所有出现的“宏名”,预处理器都会用宏定义中的字符串去代换,这称为“宏替换”或“宏展开”。

宏定义是由源程序中的宏定义命令#define完成的,宏替换是由预处理程序完成的。

宏定义的一般形式为:

#define  宏名  字符串

#表示这是一条预处理命令,所有的预处理命令都以 # 开头。宏名是标识符的一种,命名规则和变量相同。字符串可以是数字、表达式、if 语句、函数等。这里所说的字符串是一般意义上的字符序列,不要和C语言中的字符串等同,它不需要双引号。

程序中反复使用的表达式就可以使用宏定义,例如:

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

它的作用是指定标识符M来表示(y*y+3*y)这个表达式。在编写代码时,所有出现 (yy+3y) 的地方都可以用 M 来表示,而对源程序编译时,将先由预处理程序进行宏代替,即用 (yy+3y) 去替换所有的宏名 M,然后再进行编译。

程序实例:

#include <stdio.h>
#define M (n*n+3*n)
int main(){int sum, n;printf("Input a number: ");scanf("%d", &n);sum = 3*M+4*M+5*M;printf("sum=%d\n", sum);return 0;
}
----------------------------------------------------------------------------
运行结果:
Input a number: 10↙
sum=1560

程序开头首先定义了一个宏 M,它表示 (nn+3n) 这个表达式。在 9 行代码中使用了宏 M,预处理程序将它展开为下面的语句:

sum=3*(n*n+3*n)+4*(n*n+3*n)+5*(n*n+3*n);

需要注意的是,在宏定义中表达式(n*n+3*n)两边的括号不能少,否则在宏展开以后可能会产生歧义。下面是一个反面的例子:

#difine M n*n+3*n

在宏展开后将得到下述语句:

s=3*n*n+3*n+4*n*n+3*n+5*n*n+3*n;

这相当于:

3n2+3n+4n2+3n+5n2+3n

这显然是不正确的。所以进行宏定义时要注意,应该保证在宏替换之后不发生歧义。

3. 常量宏定义注意点
  1. 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单粗暴的替换。字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。

  2. 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。

  3. 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。例如:

    #define PI 3.14159
    int main(){// Codereturn 0;
    }
    #undef PI
    void func(){// Code
    }
    

    表示 PI 只在 main() 函数中有效,在 func() 中无效。

  4. 代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替,例如:

    #include <stdio.h>
    #define OK 100
    int main(){printf("OK\n");return 0;
    }
    ----------------------------------------------------------------------------------

    该例中定义宏名 OK 表示 100,但在 printf 语句中 OK 被引号括起来,因此不作宏替换,而作为字符串处理。

  5. 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。例如:

    #define PI 3.1415926
    #define S PI*y*y    /* PI是已定义的宏名*/
    

    对语句:

    printf("%f", S);
    

    在宏代换后变为:

    printf("%f", 3.1415926*y*y);
    
  6. 习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。

  7. 可用宏定义表示数据类型,使书写方便。例如

    #define UINT unsigned int
    

    在程序中可用 UINT 作变量说明:

    UINT a, b;
    

    应注意用宏定义表示数据类型和用 typedef 定义数据说明符的区别。宏定义只是简单的字符串替换,由预处理器来处理;而 typedef 是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型。

    实例:

    #define PIN1 int *
    typedef int *PIN2;  //也可以写作typedef int (*PIN2);
    

    从形式上看这两者相似, 但在实际使用中却不相同。

    下面用 PIN1,PIN2 说明变量时就可以看出它们的区别:

    PIN1 a, b;
    

    在宏代换后变成:

    int * a, b;
    

    表示 a 是指向整型的指针变量,而 b 是整型变量。然而:

    PIN2 a,b;
    

    表示 a、b 都是指向整型的指针变量。因为 PIN2 是一个新的、完整的数据类型。由这个例子可见,宏定义虽然也可表示数据类型, 但毕竟只是简单的字符串替换。在使用时要格外小心,以避出错。

4. 含参数宏定义

C语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”,这点和函数有些类似。对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。

带参宏定义的一般形式为:

#define 宏名(形参列表) 字符串

在字符串中可以含有各个形参。

带参宏调用的一般形式为:

宏名(实参列表);

实例:

#define M(y) y*y+3*y  //宏定义
// TODO:
k=M(5);  //宏调用

在宏展开时,用实参 5 去代替形参 y,经预处理程序展开后的语句为k=5*5+3*5

【示例】输出两个数中较大的数。

#include <stdio.h>
#define MAX(a,b) (a>b) ? a : b
int main(){int x , y, max;printf("input two numbers: ");scanf("%d %d", &x, &y);max = MAX(x, y);printf("max=%d\n", max);return 0;
}
-----------------------------------------------------------------------
运行结果:
input two numbers: 10 20
max=20

程序第 2 行定义了一个带参数的宏,用宏名MAX表示条件表达式(a>b) ? a : b,形参 a、b 均出现在条件表达式中。程序第 7 行max = MAX(x, y)为宏调用,实参 x、y 将用来代替形参 a、b。宏展开后该语句为:

max=(x>y) ? x : y;
5. 含参数宏注意点
  1. 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。例如把:

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

    写为:

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

    将被认为是无参宏定义,宏名 MAX 代表字符串(a,b) (a>b)?a:b。宏展开时,宏调用语句:

    max = MAX(x,y);
    

    将变为:

    max = (a,b)(a>b)?a:b(x,y);
    

    这显然是错误的。

  2. 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。

    这一点和函数是不同的:在函数中,形参和实参是两个不同的变量,都有自己的作用域,调用时要把实参的值传递给形参;而在带参数的宏中,只是符号的替换,不存在值传递的问题。

    【示例】输入 n,输出 (n+1)^2 的值。

    #include <stdio.h>
    #define SQ(y) (y)*(y)
    int main(){int a, sq;printf("input a number: ");scanf("%d", &a);sq = SQ(a+1);printf("sq=%d\n", sq);return 0;
    }
    -------------------------------------------------------------------------------------
    运行结果:
    input a number: 9
    sq=100
    

    第 2 行为宏定义,形参为 y。第 7 行宏调用中实参为 a+1,是一个表达式,在宏展开时,用 a+1 代换 y,再用 (y)*(y) 代换 SQ,得到如下语句:

    sq=(a+1)*(a+1);
    

    这与函数的调用是不同的,函数调用时要把实参表达式的值求出来再传递给形参,而宏展开中对实参表达式不作计算,直接按照原样替换。

  3. 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。例如上面的宏定义中 (y)*(y) 表达式的 y 都用括号括起来,因此结果是正确的。如果去掉括号,把程序改为以下形式:

    #include <stdio.h>
    #define SQ(y) y*y
    int main(){int a, sq;printf("input a number: ");scanf("%d", &a);sq = SQ(a+1);printf("sq=%d\n", sq);return 0;
    }
    ---------------------------------------------------------------------------------------
    运行结果为:
    input a number: 9
    sq=19
    

    同样输入 9,但结果却是不一样的。问题在哪里呢?这是由于宏展开只是简单的符号替换的过程,没有任何其它的处理。宏替换后将得到以下语句:

    sq=a+1*a+1;

    由于 a 为 9,故 sq 的值为 19。这显然与题意相违,因此参数两边的括号是不能少的。即使在参数两边加括号还是不够的,请看下面程序:

    #include <stdio.h>
    #define SQ(y) (y)*(y)
    int main(){int a,sq;printf("input a number: ");scanf("%d", &a);sq = 200 / SQ(a+1);printf("sq=%d\n", sq);return 0;
    }
    

    与前面的代码相比,只是把宏调用语句改为:

    sq = 200/SQ(a+1);

    运行程序后,如果仍然输入 9,那么我们希望的结果为 2。但实际情况并非如此:

    input a number: 9

    sq=200

    为什么会得这样的结果呢?分析宏调用语句,在宏展开之后变为:

    sq=200/(a+1)*(a+1);

    a 为 9 时,由于“/”和“”运算符优先级和结合性相同,所以先计算 200/(9+1),结果为 20,再计算 20(9+1),最后得到 200。

    为了得到正确答案,应该在宏定义中的整个字符串外加括号:

    #include <stdio.h>
    #define SQ(y) ((y)*(y))
    int main(){int a,sq;printf("input a number: ");scanf("%d", &a);sq = 200 / SQ(a+1);printf("sq=%d\n", sq);return 0;
    }
    

    由此可见,对于带参宏定义不仅要在参数两侧加括号,还应该在整个字符串外加括号。

6.宏参数的拼接
  1. \ 拼接

    #define THREE printf("asd"); \printf ("qwe");
    

    \后面不能有空格,最后一个语句不用加\

  2. #define ONE(x) #x
    

    把传进来的东西都解释成字符串,

    printf ("%s", ONE(qweq));
    

    相当于给x的参数加了双引号

  3. 字符串拼接

    #define ONE(x, y) #x###y
    printf ("%s\n", ONE(a, qwe));
    

    输出aqwe

7. 含参数宏定义与函数区别

带参数的宏和函数很相似,但有本质上的区别:宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。而函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码。

【示例①】用函数计算平方值。

#include <stdio.h>
int SQ(int y){return ((y)*(y));
}
int main(){int i=1;while(i<=5){printf("%d^2 = %d\n", (i-1), SQ(i++));}return 0;
}
----------------------------------------------------------------
运行结果:1^2 = 12^2 = 43^2 = 94^2 = 165^2 = 25

【示例②】用宏计算平方值。

#include <stdio.h>
#define SQ(y) ((y)*(y))
int main(){int i=1;while(i<=5){printf("%d^2 = %d\n", i, SQ(i++));}return 0;
}

在 Visual Studio 和 C-Free 下的运行结果(其它编译器的运行结果可能不同,这个++运算的顺序有关):
3^2 = 1
5^2 = 9
7^2 = 25

在示例①中,先把实参 i 传递给形参 y,然后再自增 1,这样每循环一次 i 的值增加 1,所以最终要循环 5 次。

在示例②中,宏调用只是简单的字符串替换,SQ(i++) 会被替换为 ((i++)*(i++)),这样每循环一次 i 的值增加 2,所以最终只循环 3 次。

由此可见,宏和函数只是在形式上相似,本质上是完全不同的。

带参数的宏也可以用来定义多个语句,在宏调用时,把这些语句又替换到源程序中,请看下面的例子:

#include <stdio.h>
#define SSSV(s1, s2, s3, v) s1 = length * width; s2 = length * height; s3 = width * height; v = width * length * height;
int main(){int length = 3, width = 4, height = 5, sa, sb, sc, vv;SSSV(sa, sb, sc, vv);printf("sa=%d, sb=%d, sc=%d, vv=%d\n", sa, sb, sc, vv);return 0;
}
-------------------------------------------------------------------------------
运行结果:sa=12, sb=15, sc=20, vv=60

五.typedef类型重命名

1.基本数据类型

C语言允许为一个数据类型起一个新的别名,就像给人起“绰号”一样。

起别名的目的不是为了提高程序运行效率,而是为了编码方便。例如有一个结构体的名字是 stu,要想定义一个结构体变量就得这样写:

struct stu stu1;

struct 看起来就是多余的,但不写又会报错。如果为 struct stu 起了一个别名 STU,书写起来就简单了:

STU stu1;

这种写法更加简练,意义也非常明确,不管是在标准头文件中还是以后的编程实践中,都会大量使用这种别名。

使用关键字 typedef 可以为类型起一个新的别名。typedef 的用法一般为:

typedef  oldName  newName;

oldName 是类型原来的名字,newName 是类型新的名字。例如:

typedef int INTEGER;
INTEGER a, b;
a = 1;
b = 2;

INTEGER a, b;等效于int a, b;

2.数组类型
typedef char ARRAY20[20];

表示 ARRAY20 是类型char [20]的别名。它是一个长度为 20 的数组类型。接着可以用 ARRAY20 定义数组:

ARRAY20 a1, a2, s1, s2;

它等价于:

char a1[20], a2[20], s1[20], s2[20];

注意,数组也是有类型的。例如char a1[20];定义了一个数组 a1,它的类型就是 char [20],

3. 结构体类型
typedef struct stu{char name[20];int age;char sex;
} STU;STU 是 struct stu 的别名,可以用 STU 定义结构体变量:
STU body1,body2;
它等价于:
struct stu body1, body2;
4.指针类型
typedef int (*PTR_TO_ARR)[4];
表示 PTR_TO_ARR 是类型int * [4]的别名,它是一个二维数组指针类型。接着可以使用 PTR_TO_ARR 定义二维数组指针:PTR_TO_ARR p1, p2;
按照类似的写法,还可以为函数指针类型定义别名:
typedef int (*PTR_TO_FUNC)(int, int);
PTR_TO_FUNC pfunc;

程序实例:

#include <stdio.h>
typedef char (*PTR_TO_ARR)[30];
typedef int (*PTR_TO_FUNC)(int, int);
int max(int a, int b){return a>b ? a : b;
}
char str[3][30] = {"http://c.biancheng.net","C语言中文网","C-Language"
};
int main(){PTR_TO_ARR parr = str;PTR_TO_FUNC pfunc = max;int i;printf("max: %d\n", (*pfunc)(10, 20));for(i=0; i<3; i++){printf("str[%d]: %s\n", i, *(parr+i));}return 0;
}
----------------------------------------------------------------------------------
运行结果:
max: 20
str[0]: http://c.biancheng.net
str[1]: C语言中文网
str[2]: C-Language

需要强调的是,typedef 是赋予现有类型一个新的名字,而不是创建新的类型。为了“见名知意”,请尽量使用含义明确的标识符,并且尽量大写。

5. typedef 和 #define 的区别

typedef 在表现上有时候类似于 #define,但它和宏替换之间存在一个关键性的区别。正确思考这个问题的方法就是把 typedef 看成一种彻底的“封装”类型,声明之后不能再往里面增加别的东西。

  1. 可以使用其他类型说明符对宏类型名进行扩展,但对 typedef 所定义的类型名却不能这样做。如下所示:
#define INTERGE int
unsigned INTERGE n;  //没问题typedef int INTERGE;
unsigned INTERGE n;  //错误,不能在 INTERGE 前面添加 unsigned
  1. 在连续定义几个变量的时候,typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证。例如:
#define PTR_INT int *
PTR_INT p1, p2;经过宏替换以后,第二行变为:
int *p1, p2;这使得 p1、p2 成为不同的类型:p1 是指向 int 类型的指针,p2 是 int 类型。
相反,在下面的代码中
typedef int * PTR_INT
PTR_INT p1, p2;p1、p2 类型相同,它们都是指向 int 类型的指针。

c语言基础(七)---复杂数据类型相关推荐

  1. Go语言基础之基本数据类型

    直接上代码 package mainimport ("fmt""math" )func main() {// 整型fmt.Println("===整型 ...

  2. c语言无视数据类型字符串存储,C语言基础-第二课-数据类型与运算符(示例代码)...

    1   C语言中的数据类型 1.1   常量 常量就是在程序中不可变化的量,常量在定义的时候必须给一个初值. 1.1.1#define 定义一个宏常量 1.1.2const 定义一个const常量 1 ...

  3. c语言基本数据类型常量,C语言基础学习基本数据类型-变量和常量

    变量和常量 什么是变量和常量?有些数据在程序运行前就预先设定,并在运行过程中不发生变化,称之为常量:有些数据在程序运行中可能发生变化或被赋值,称之为变量. 使用变量前必须先声明一个变量.变量定义的语法 ...

  4. C语言怎么定义001为int,C语言基础-01-基本数据类型

    C语言的数据类型大致可以分为下图中的几类: 一.变量 跟其他语言一样,C语言中用变量来存储计算过程使用的值,任何变量都必须先定义类型再使用.为什么一定要先定义呢?因为变量的类型决定了变量占用的存储空间 ...

  5. JSD-2204-Java语言基础-八大基本数据类型-Day02

    1.变量:存数的 声明:---------在银行开个账户 int a; //声明一个整型的变量,名为a int b,c,d; //声明三个整型的变量,名为b,c,d //int a; //编译错误,变 ...

  6. go语言基础语法-容器数据类型-数组

    文章目录 前言 一. 数组 1. 数组的语法和声明 2 语法 1)语法: 2. 数组的初始化 a) 先声明再赋值 (先声明在初始化) b) 声明并初始化 c) 不限长赋值 d) 索引赋值 3. 访问和 ...

  7. C语言基础1(数据类型、常变量、运算符、基本语句、选择结构、循环结构、数组、字符串、函数、指针)

    数据类型及运算 数据类型 一.整形(int) 整形变量的定义.输入和输出 打印格式 含义 %d 输出一个有符号的10进制int类型 %o(字母o) 输出8进制的int类型 %x 输出16进制的int类 ...

  8. C语言基础知识【数据类型】

    C 数据类型 1.在 C 语言中,数据类型指的是用于声明不同类型的变量或函数的一个广泛的系统.变量的类型决定了变量存储占用的空间,以及如何解释存储的位模式. 2.C 中的类型可分为以下几种: 序号   ...

  9. c语言变量类型int,C语言基础学习基本数据类型-int类型与int变量

    C++学习笔记26:泛型编程概念 一.什么是泛型编程? 泛型就是通用的型式 编写不依赖数据对象型式的代码就是泛型编程 二.为什么需要泛型编程? 函数重载,相似类定义与型式兼容性 例如:设计函数,求两个 ...

  10. c语言中shift f12组合建,C++学习1-(C语言基础、VS快捷键)

    C语言基础复习 1.三码 正数: 3码合1 ,正数的反码/补码就是其本身 负数: 原码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值 原码:11010101 负数的反码是在其原码的基础上 ...

最新文章

  1. 通用社区登陆组件技术分享(开源)中篇:OAuth 登陆组件流程及组件集成方法...
  2. mac版lightroom cc_Photoshop问世30周年 Mac和iPad版获重要更新
  3. 微软向Linux社区开放60000多项专利:对开源微软是认真的
  4. ChEMBL数据库的官方python工具包
  5. python怎么定时弹窗_PyQt5弹框定时关闭(python)
  6. 单片机定时器之改良版:时间轮定时器
  7. 亏损208亿,滴滴橙心优选裁员关停!曾融资12亿,最高估值320亿
  8. ubuntu18.04新体验
  9. 819 c语言程序设计,大连海洋大学2021年考研819高级语言程序设计(C语言)考试大纲...
  10. ios python3.0编程软件_ios编程软件-7款学习Python编程的iPhone/iPad应用
  11. oracle清理磁盘空间
  12. python图片压缩算法_Python实现奇异值分解(SVD)压缩图片
  13. NUC10 i7 黑苹果Big Sur 11.4 + win10 双系统安装指南
  14. 华为防火墙简介及其工作原理
  15. mysql隔离级别 isolation_MySQL事务和隔离级别
  16. 国瀚实业|怎么才能做好互联网投资理财
  17. 深蓝卡通风人教版小学五年级英语课件PPT模板
  18. PC机插入麦克风后没有说话没有声音
  19. mysql mooc_爬取MOOC课程(1)
  20. 太阳直射点纬度计算公式_高中地理——每日讲1题(太阳高度角、太阳直射点、昼夜长短)...

热门文章

  1. computer management.lnk未指定的错误
  2. 微信论坛交流小程序系统毕业设计毕设(1)开发概要
  3. 使用调制解调器的基本 AT 命令
  4. Go json.Marshal禁用escapeHtml转义
  5. 孕妇的孕周和体重对胎儿游离DNA在母体cfDNA占比的影响
  6. 家园系统服务器,梦幻西游手游家园系统介绍 家园外观全面升级-游侠手游
  7. 死锁的四个必要条件及处理死锁
  8. opencv图片倾斜度检测(二)利用摄像头进行实时检测图片中物体并画出坐标轴和倾斜度
  9. Day 7 A - Age of Moyu HDU 6386 | 最短路 | SPFA | 链式前向星
  10. javaweb项目创建图片服务器