要开始使用稍微复杂一点的数据结构了,就是结构与联合,当然还会附带枚举。在开始之前,还要再回顾一下make的使用。
make的简单使用
make的使用其实就是按照依赖关系编译,依次将所需的文件,依赖关系,执行的指令放在一个文件中,通过make执行就可以了。
现在使用Clion IDE,无需自己管理make文件,但还是要掌握一下。
头文件存放函数声明,然后在需要使用函数(还有宏)的文件和提供这些函数的文件里都包含头文件就可以了。
Clion中,可以在创建一个C source file的同时指定创建对应的h文件,之后就可以看到头文件,而且自动生成了避免重复导入的内容。比如创建一个encrypt.c和encrypt.h:
#include "encrypt.h" void encrypt(char * message){ while(*message){ *message = *message ^ 31; message++; } }
#ifndef C_ENCRYPT_H #define C_ENCRYPT_H #endif //C_ENCRYPT_H void encrypt(char * message);
然后可以在主程序里调用,这其实是一个把字符数组替换成加密后的内容的函数:
#include <stdio.h> #include "chapter04/encrypt.h" int main(int argc, char *argv[]) { char msg[80]; while (fgets(msg, 80, stdin)) { encrypt(msg); printf("%s\n", msg); } }
使用make
其实很简单,在通常使用GCC的时候,是一次性从源代码编译完成。现在要加上一个步骤,也就是生成中间代码.o
文件,即gcc -c *.c。
然后再使用gcc - o *.0 launch编译成可执行文件。
编译的顺序是:
-
使用
.c
和.h
生成.o
文件 - 使用
.o
文件生成可执行文件
只要将生成的依赖顺序定义好在一个叫做makefile的文件中就可以了,clion可以自动生成makefile文件。
launch.o: launch.c launch.h thruster.h gcc -c launch.c thruster.o: thruster.h thruster.c gcc -c thruster.c launch: launch.o thruster.o gcc launch.o thruster.o -o launch
每一行的目标文件之后是所需要的文件,然后是执行的指令,注意第二行必须要以一个制表符tab缩进。
补充一下,结构作为一种类型,也是可以定义在头文件,然后由导入的文件进行使用的,和函数很类似。宏也是可以的,在之前使用stdio.h中的FILE宏的时候应该有所感觉了。
结构
上一次学结构的时候基本上是稀里糊涂,因为对于自定义类型还不是理解的很透彻。现在就OK多了,看起来也更加理解了。
结构就是将一部分数据打包在一起,也是很多复杂的数据结构的基础。
struct也是一种数据类型,定义的方式就是采用类型的关键字,外加需要给这种类型取一个具体名字,这个名字加上struct,构成完整的类型名称:
struct fish {
const char *name;
const char *species;
int teeth;
int age;
};
这里一定要理解,struct fish
一起构成新的类型名称,相当于一个类。而创建具体的struct结构的时候,要用这个完整的类型名加上具体类型名,就像new一个对象一样:
struct fish snappy = { "Snappy", "Piranha", 69, 4 };
蓝色的就是具体的一个结构的名称。
知道了这个就理解容易多了,其实就是取名有点绕而已。还可以用typedef一次性将struct fish
起一个别名,这样使用起来就更像类了:
typedef struct fish { const char *name; const char *species; int teeth; int age; } afish; int main(int argc, char *argv[]); { afish snappy = { "Snappy", "Piranha", 69, 4 }; printf("%s\n", snappy.name); printf("%s\n", snappy.species); printf("%d\n", snappy.teeth); printf("%d\n", snappy.age); }
访问其中的属性用.
,如果结构中再有结构,就连续用.
,确实很像对象。
如果拥有一个指向结构的指针s,而不是结构名,则可以用(*s).age
来访问属性,但还有一种简写更常用就是s->age
:
void grown(afish * fish){ fish->age++; } int main(int argc, char *argv[]){ afish snappy = { "Snappy", "Piranha", 69, 4 }; printf("%s\n", snappy.name); printf("%s\n", snappy.species); printf("%d\n", snappy.teeth); printf("%d\n", snappy.age); grown(&snappy); printf("%d\n",snappy.age); }
C语言的语法其实也真的很灵活。要注意的是,一般最好还是不要直接使用typedef匿名的结构,看上去会有些奇怪。
struct是很多数据结构的节点,一定要掌握使用方法。
联合
联合简单的说,就是同一个东西可以存多种属性,然后同时只能有一个存在,但大家共用一个内存,只是因为数据类型不同,取出时候的解释也不同。
C语言并没有限制存一个然后取另外一个数据类型。所以单独使用联合容易出问题。
联合通常是作为struct的一部分存在,而且不是仅仅依赖联合,通常另外设置一个枚举字段,在取值的时候通过检测枚举字段,来决定从联合中按照何种数据类型来取值。
比如我们有一个时间struct,其中有一个联合打算存放年和月两种类型的数据,年因为是可能是几万年,所以采用long类型,而月只有1-12,用二进制一个字节就放的下,可以采用char类型。
很显然,如果存的是年,而按照月取出来,可能就会有问题。所以另外加一个枚举字段,标示当前的这个数据类型是年还是月。各个数据类型定义如下:
enum datetype {YEAR, MONTH}; union date { char month; long year; }; struct time { union date adate; enum datetype enumdatetype; };
实际使用如下:
int main(int argc, char *argv[]){ struct time time1 = {12,MONTH}; struct time time2; time2.adate.year = 2019; //错误的用法,随心所欲取联合中的数据 //由于long比char长,time1显示正确 printf("time1的年份是:%ld\n", time1.adate.year); //由于存入long,按照char读取,显示了错误的结果为-29 printf("time2的月份是:%d\n", time2.adate.month); //正确的用法,先检测枚举类型,再根据结果取数据 if (time1.enumdatetype == MONTH) { printf("time1的月份是:%d\n", time1.adate.month); } else { printf("time1的年份是:%ld\n", time1.adate.year); } if (time2.enumdatetype == YEAR) { printf("time2的年份是:%ld\n", time2.adate.year); } else { printf("time2的月份是:%d\n", time2.adate.month); } }
位字段
有的时候可能存放的内容其实一个字节都不用,可能几位数就可以表示。比如我们有一个测验成绩对象,其中有实际成绩0-50分,成绩的档位A-E,以及是否及格三个字段。
通常情况下,可能我们会用一个布尔值存放是否及格,用char类型来存放实际分数和成绩的档位。然而实际上我们知道,是否及格只需要一个二进制位0或者1就可以存放。而成绩的档位A-E共有5档,可以用0-4来表示。用三位二进制就可以存的下。而成绩最高到50,用六位二进制就可以存放的下。结构中可以在变量的末尾指定二进制位数:
struct result { unsigned int passed:1; unsigned int score:6; unsigned int grade:3; };
使用位字段的时候要注意,所有的数据类型必须都是unsigned int类型,然后在末尾用冒号加上位数来指定具体的二进制位。
这样的结构相比原来一个布尔和两个char类型的数据,至少要节省一个字节的空间。来看看实际使用:
struct result { unsigned int passed:1; unsigned int score:6; unsigned int grade:3; }; void pri(struct result myresult){ printf("%d\n", myresult.passed); printf("%d\n", myresult.score); printf("%d\n", myresult.grade); printf("-----------------------------\n"); } int main(int argc, char *argv[]){ struct result result1 = {1,32,2}; struct result result2 = {3,70,7}; struct result result3 = {1,53,3}; pri(result1); pri(result2); pri(result3); }
这个的输出结果如下:
1 32 2 ----------------------------- 1 6 7 ----------------------------- 1 53 3 -----------------------------
result1的三个属性的值都在对应范围内,所以正常显示。
result2就有些问题了,首先是第一个布尔值,由于3的二进制是11,所以就存了最低位的1进去。然后是70,二进制是1000110,超过了6位二进制数的范围,所以取了后边六位000110,变成了十进制的6。最后一个属性是7,虽然超过了0-4的取值范围,然而依然是三位二进制,所以打印出来是7。
result3的成绩和result2的档位的问题类似,数据超出了实际范围,但位数相同,所以打印了出来。
其实这些编译器都有警告。C语言还是很灵活的,就算规定了位数,也要小心存入了错误的数值。
2019年上半年的最后一天要过去了。6月份完成入门之后休息了几天拿完了血污:夜之仪式的全成就,然后顺利开工C语言。到今天把结构联合枚举复习完,下半年开始又可以搞回链表和其他数据结构了,搞底层了。加油吧。