C再学习 03 - 结构与联合

C再学习 03 - 结构与联合

要开始使用稍微复杂一点的数据结构了,就是结构与联合,当然还会附带枚举。在开始之前,还要再回顾一下make的使用。 make的简单使用 make的使用其实就是按照依赖关系编译,依次将所需的文件,依赖关系,执行的指令放在一个文件中,通过make执行就可以了。 现在使用Clion IDE,无需自己管理ma

要开始使用稍微复杂一点的数据结构了,就是结构与联合,当然还会附带枚举。在开始之前,还要再回顾一下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编译成可执行文件。 编译的顺序是:
  1. 使用.c.h生成.o文件
  2. 使用.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语言。到今天把结构联合枚举复习完,下半年开始又可以搞回链表和其他数据结构了,搞底层了。加油吧。
LICENSED UNDER CC BY-NC-SA 4.0
Comment