【C语言学习】结构体

结构体(Struct)从本质上讲是一种自定义的数据类型,只不过这种数据类型比较复杂,是由 int、char、float 等基本类型组成的。你可以认为结构体是一种聚合类型

在实际开发中,我们可以将一组类型不同的、但是用来描述同一件事物的变量放到结构体中。例如,在校学生有姓名、年龄、身高、成绩等属性,学了结构体后,我们就不需要再定义多个变量了,将它们都放到结构体中即可。

C语言中的结构体的概念类似于面向对象语言中的的概念

结构体(struct)

C语言中的数组(Array),它是一组具有相同类型的数据的集合。但在实际的编程过程中,我们往往还需要一组类型不同的数据,例如对于学生信息登记表,姓名为字符串,学号为整数,年龄为整数,所在的学习小组为字符,成绩为小数,因为数据类型不同,显然不能用一个数组来存放。

在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不能少

与Java中声明类的语法不同,C语言中声明结构体变量时,需要额外加上struct关键字,这样才能被C语言编译器成功解析。

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;

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

无结构体名的结构体可以类比Java中的匿名类。

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

在这里插入图片描述

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

在这里插入图片描述

成员的获取和赋值

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

结构体变量名.成员名;

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

#include <stdio.h>
int main(){
    struct{
        char *name;  //姓名
        int num;  //学号
        int age;  //年龄
        char group;  //所在小组
        float score;  //成绩
    } stu1;
    //给结构体成员赋值
    stu1.name = "Pycro";
    stu1.num = 22;
    stu1.age = 22;
    stu1.group = 'E';
    stu1.score = 127.5;
    //读取结构体成员的值
    printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", stu1.name, stu1.num, stu1.age, stu1.group, stu1.score);
    return 0;
}
/*
Pycro的学号是22,年龄是21,在E组,今年的成绩是127.5!
*/

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

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

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

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

结构体数组

所谓结构体数组,是指数组中的每个元素都是一个结构体。在实际应用中,C语言结构体数组常被用来表示一个拥有相同数据结构的群体,比如一个班的学生、一个车间的职工等。

在C语言中,定义结构体数组和定义结构体变量的方式类似,请看下面的例子:

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

表示一个班级有5个学生。

结构体数组在定义的同时也可以初始化:

struct stu{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组 
    float score;  //成绩
}class[5] = {
    {"Li ping", 5, 18, 'C', 145.0},
    {"Zhang ping", 4, 19, 'A', 130.5},
    {"He fang", 1, 18, 'A', 148.5},
    {"Cheng ling", 2, 17, 'F', 139.0},
    {"Wang ming", 3, 17, 'B', 144.5}
};

当对数组中全部元素赋值时,也可不给出数组长度:

struct stu{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组 
    float score;  //成绩
}class[] = {
    {"Li ping", 5, 18, 'C', 145.0},
    {"Zhang ping", 4, 19, 'A', 130.5},
    {"He fang", 1, 18, 'A', 148.5},
    {"Cheng ling", 2, 17, 'F', 139.0},
    {"Wang ming", 3, 17, 'B', 144.5}
};

结构体数组的使用也很简单:

获取 Wang ming 的成绩:

class[4].score;

修改 Li ping 的学习小组:

class[0].group = 'B';

计算全班学生的总成绩、平均成绩和以及 140 分以下的人数:

#include <stdio.h>
struct{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
}class[] = {
    {"Li ping", 5, 18, 'C', 145.0},
    {"Zhang ping", 4, 19, 'A', 130.5},
    {"He fang", 1, 18, 'A', 148.5},
    {"Cheng ling", 2, 17, 'F', 139.0},
    {"Wang ming", 3, 17, 'B', 144.5}
};
int main(){
    int i, num_140 = 0;
    float sum = 0;
    for(i=0; i<5; i++){
        sum += class[i].score;
        if(class[i].score < 140) num_140++;
    }
    printf("sum=%.2f\naverage=%.2f\nnum_140=%d\n", sum, sum/5, num_140);
    return 0;
}
/*
sum=707.50
average=141.50
num_140=2
*/

结构体指针

当一个指针变量指向结构体时,我们就称它为结构体指针。C语言结构体指针的定义形式一般为:

struct 结构体名 *变量名;

一个定义结构体指针的实例:

//结构体
struct stu{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 };
//结构体指针
struct stu *pstu = &stu1;

也可以在定义结构体的同时定义结构体指针:

struct stu{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 }, *pstu = &stu1;

注意,结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&,所以给 pstu 赋值只能写作:

struct stu *pstu = &stu1;

而不能写作:

struct stu *pstu = stu1;

使用结构体指针时,可以将结构体变量当成一个普通的数据类型对待。

还应该注意,结构体和结构体变量是两个不同的概念:结构体是一种数据类型,是一种创建变量的模板,编译器不会为它分配内存空间,就像 int、float、char 这些关键字本身不占用内存一样;结构体变量才包含实实在在的数据,才需要内存来存储。下面的写法是错误的,不可能去取一个结构体名的地址,也不能将它赋值给其他变量:

//struct stu *pstu = &stu;
//struct stu *pstu = stu;

获取结构体成员

通过结构体指针可以获取结构体成员,一般形式为:

(*pointer).memberName

或者:

pointer->memberName

第一种写法中,.的优先级高于*(*pointer)两边的括号不能少。如果去掉括号写作*pointer.memberName,那么就等效于*(pointer.memberName),这样意义就完全不对了,会直接报错。

第二种写法中,->是一个新的运算符,习惯称它为“箭头”,有了它,可以通过结构体指针直接取得结构体成员;这也是->在C语言中的唯一用途。

上面的两种写法是等效的,我们通常采用后面的写法,这样更加直观。

结构体指针的使用:

#include <stdio.h>
int main(){
    struct{
        char *name;  //姓名
        int num;  //学号
        int age;  //年龄
        char group;  //所在小组
        float score;  //成绩
    } stu1 = { "Tom", 12, 18, 'A', 136.5 }, *pstu = &stu1;
    //读取结构体成员的值
    printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", (*pstu).name, (*pstu).num, (*pstu).age, (*pstu).group, (*pstu).score);
    printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", pstu->name, pstu->num, pstu->age, pstu->group, pstu->score);
    return 0;
}
/*
Tom的学号是12,年龄是18,在A组,今年的成绩是136.5!
Tom的学号是12,年龄是18,在A组,今年的成绩是136.5!
*/

结构体数组指针的使用:

#include <stdio.h>
struct stu{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
}stus[] = {
    {"Zhou ping", 5, 18, 'C', 145.0},
    {"Zhang ping", 4, 19, 'A', 130.5},
    {"Liu fang", 1, 18, 'A', 148.5},
    {"Cheng ling", 2, 17, 'F', 139.0},
    {"Wang ming", 3, 17, 'B', 144.5}
}, *ps;
int main(){
    //求数组长度
    int len = sizeof(stus) / sizeof(struct stu);
    printf("Name\t\tNum\tAge\tGroup\tScore\t\n");
    for(ps=stus; ps<stus+len; ps++){
        printf("%s\t%d\t%d\t%c\t%.1f\n", ps->name, ps->num, ps->age, ps->group, ps->score);
    }
    return 0;
}
/*
Name            Num     Age     Group   Score
Zhou ping       5       18      C       145.0
Zhang ping      4       19      A       130.5
Liu fang        1       18      A       148.5
Cheng ling      2       17      F       139.0
Wang ming       3       17      B       144.5
*/

结构体指针作为函数参数

结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编译器转换成一个指针。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速。

计算全班学生的总成绩、平均成绩和以及 140 分以下的人数:

#include <stdio.h>
struct stu{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
}stus[] = {
    {"Li ping", 5, 18, 'C', 145.0},
    {"Zhang ping", 4, 19, 'A', 130.5},
    {"He fang", 1, 18, 'A', 148.5},
    {"Cheng ling", 2, 17, 'F', 139.0},
    {"Wang ming", 3, 17, 'B', 144.5}
};
void average(struct stu *ps, int len);
int main(){
    int len = sizeof(stus) / sizeof(struct stu);
    average(stus, len);
    return 0;
}
void average(struct stu *ps, int len){
    int i, num_140 = 0;
    float average, sum = 0;
    for(i=0; i<len; i++){
        sum += (ps + i) -> score;
        if((ps + i)->score < 140) num_140++;
    }
    printf("sum=%.2f\naverage=%.2f\nnum_140=%d\n", sum, sum/5, num_140);
}
/*
sum=707.50
average=141.50
num_140=2
*/

枚举类型(enum)

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

以每周七天为例,我们可以使用#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)类型,能够列出所有可能的取值,并给它们取一个名字。

枚举类型的定义形式为:

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 开始递增,跟上面的写法是等效的。

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

enum week a, b, c;

也可以在定义枚举类型的同时定义变量:

enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } 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;

判断用户输入的是星期几:

#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 等都不是变量,它们不占用数据区(常量区、全局数据区、栈区和堆区)的内存,而是直接被编译到命令里面,放到代码区,所以不能用&取得它们的地址。这就是枚举的本质。

case 关键字后面必须是一个整数,或者是结果为整数的表达式,但不能包含任何变量,正是由于 Mon、Tues、Wed 这些名字最终会被替换成一个整数,所以它们才能放在 case 后面。

枚举类型变量需要存放的是一个整数,它的长度和 int 应该相同:

#include <stdio.h>
int main(){
    enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day = Mon;
    printf("%d, %d, %d, %d, %d\n", sizeof(enum week), sizeof(day), sizeof(Mon), sizeof(Wed), sizeof(int) );
    return 0;
}
//4, 4, 4, 4, 4

共用体(union)

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

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

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

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

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

共用体也是一种自定义类型,可以通过它来创建变量:

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

上面是先定义共用体,再创建变量,也可以在定义共用体的同时创建变量:

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

如果不再定义新的变量,也可以将共用体的名字省略:

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

共用体 data 中,成员 f 占用的内存最多,为 8 个字节,所以 data 类型的变量(也就是 a、b、c)也占用 8 个字节的内存:

#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 的全部数据。

共用体的应用

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

NameNumSexProfessionScore / Course
HanXiaoXiao501fs89.5
YanWeiMin1011mtmath
LiuZhenTao109ftEnglish
ZhaoFeiYan982ms95.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
*/

共用体可以看成是一个可以以不同数据类型作为展示的可变类型,当为其中一个成员变量赋值时,那个成员变量变得有意义,可以展示出来,而剩余的成员变量的值被修改,成为无意义的,通过为其中不同的类型的成员变量赋值再访问的方式,可以实现数据类型变化的变量。

位域(bit-field)

有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。例如开关只有通电和断电两种状态,用 0 和 1 表示足矣,也就是用一个二进位。正是基于这种考虑,C语言又提供了一种叫做位域的数据结构。

结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域。

struct bs{
    unsigned m;
    unsigned n: 4;
    unsigned char ch: 6;
};

:后面的数字用来限定成员变量占用的位数。成员 m 没有限制,根据数据类型即可推算出它占用 4 个字节(Byte)的内存。成员 n、ch 被:后面的数字限制,不能再根据数据类型计算长度,它们分别占用 4、6 位(Bit)的内存。

n、ch 的取值范围非常有限,数据稍微大些就会发生溢出:

#include <stdio.h>
int main(){
    struct bs{
        unsigned m;
        unsigned n: 4;
        unsigned char ch: 6;
    } a = { 0xad, 0xE, '$'};
    //第一次输出
    printf("%#x, %#x, %c\n", a.m, a.n, a.ch);
    //更改值后再次输出
    a.m = 0xb8901c;
    a.n = 0x2d;
    a.ch = 'z';
    printf("%#x, %#x, %c\n", a.m, a.n, a.ch);
    return 0;
}
/*
0xad, 0xe, $
0xb8901c, 0xd, :
*/

对于 n 和 ch,第一次输出的数据是完整的,第二次输出的数据是残缺的。

第一次输出时,n、ch 的值分别是 0xE、0x24(‘$’ 对应的 ASCII 码为 0x24),换算成二进制是 1110、10 0100,都没有超出限定的位数,能够正常输出。

第二次输出时,n、ch 的值变为 0x2d、0x7a(‘z’ 对应的 ASCII 码为 0x7a),换算成二进制分别是 10 1101、111 1010,都超出了限定的位数。超出部分被直接截去,剩下 1101、11 1010,换算成十六进制为 0xd、0x3a(0x3a 对应的字符是 :)。

C语言标准规定,位域的宽度不能超过它所依附的数据类型的长度。通俗地讲,成员变量都是有类型的,这个类型限制了成员变量的最大长度,:后面的数字不能超过这个长度。

例如上面的 bs,n 的类型是 unsigned int,长度为 4 个字节,共计 32 位,那么 n 后面的数字就不能超过 32;ch 的类型是 unsigned char,长度为 1 个字节,共计 8 位,那么 ch 后面的数字就不能超过 8。

我们可以这样认为,位域技术就是在成员变量所占用的内存中选出一部分位宽来存储数据。

C语言标准还规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int、signed int 和 unsigned int(int 默认就是 signed int);到了 C99,_Bool 也被支持了。

但编译器在具体实现时都进行了扩展,额外支持了 char、signed char、unsigned char 以及 enum 类型,所以上面的代码虽然不符合C语言标准,但它依然能够被编译器支持。

位域的存储

C语言标准并没有规定位域的具体存储方式,不同的编译器有不同的实现,但它们都尽量压缩存储空间。

位域的具体存储规则如下:

  1. 当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。

以下面的位域 bs 为例:

#include <stdio.h>
int main(){
    struct bs{
        unsigned m: 6;
        unsigned n: 12;
        unsigned p: 4;
    };
    printf("%d\n", sizeof(struct bs));
    return 0;
}
// 4

m、n、p 的类型都是 unsigned int,sizeof 的结果为 4 个字节(Byte),也即 32 个位(Bit)。m、n、p 的位宽之和为 6+12+4 = 22,小于 32,所以它们会挨着存储,中间没有缝隙。

如果将成员 m 的位宽改为 22,那么输出结果将会是 8,因为 22+12 = 34,大于 32,n 会从新的位置开始存储,相对 m 的偏移量是 sizeof(unsigned int),也即 4 个字节。

如果再将成员 p 的位宽也改为 22,那么输出结果将会是 12,三个成员都不会挨着存储。

  1. 当相邻成员的类型不同时,不同的编译器有不同的实现方案,GCC 会压缩存储,而 VC/VS 不会。

下面的位域 bs:

#include <stdio.h>
int main(){
    struct bs{
        unsigned m: 12;
        unsigned char ch: 4;
        unsigned p: 4;
    };
    printf("%d\n", sizeof(struct bs));
    return 0;
}

在 GCC 下的运行结果为 4,三个成员挨着存储;在 VC/VS 下的运行结果为 12,三个成员按照各自的类型存储(与不指定位宽时的存储方式相同)。

  1. 如果成员之间穿插着非位域成员,那么不会进行压缩,对于下面的 bs:
struct bs{
    unsigned m: 12;
    unsigned ch;
    unsigned p: 4;
};

在各个编译器下 sizeof 的结果都是 12。

通过上面的分析,我们发现位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用&获取位域成员的地址是没有意义的,C语言也禁止这样做。地址是字节(Byte)的编号,而不是位(Bit)的编号。

无名位域

位域成员可以没有名称,只给出数据类型和位宽:

struct bs{
    int m: 12;
    int  : 20;  //该位域成员不能使用
    int n: 4;
};

无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域不能使用。

上面的例子中,如果没有位宽为 20 的无名成员,m、n 将会挨着存储,sizeof(struct bs) 的结果为 4;有了这 20 位作为填充,m、n 将分开存储,sizeof(struct bs) 的结果为 8。

位运算

所谓位运算,就是对一个比特(Bit)位进行操作。比特(Bit)是一个电子元器件,8个比特构成一个字节(Byte),它已经是粒度最小的可操作单元了。

C语言提供了六种位运算符:

|运算符|&|||^|~|<<|>>|
|-|-|-|-|-|-|-|
|说明|按位与|按位或|按位异或|取反|左移|右移|

按位与运算(&)

一个比特(Bit)位只有 0 和 1 两个取值,只有参与&运算的两个位都为 1 时,结果才为 1,否则为 0。例如1&1为 1,0&0为 0,1&0也为 0,这和逻辑运算符&&非常类似。

C语言中不能直接使用二进制,&两边的操作数可以是十进制、八进制、十六进制,它们在内存中最终都是以二进制形式存储,&就是对这些内存中的二进制位进行运算。其他的位运算符也是相同的道理。

例如,9 & 5可以转换成如下的运算:

0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (9 在内存中的存储)

& 0000 0000 – 0000 0000 – 0000 0000 – 0000 0101 (5 在内存中的存储)


0000 0000 – 0000 0000 – 0000 0000 – 0000 0001 (1 在内存中的存储)

也就是说,按位与运算会对参与运算的两个数的所有二进制位进行&运算,9 & 5的结果为 1。

又如,-9 & 5可以转换成如下的运算:

1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)

& 0000 0000 – 0000 0000 – 0000 0000 – 0000 0101 (5 在内存中的存储)


0000 0000 – 0000 0000 – 0000 0000 – 0000 0101 (5 在内存中的存储)

-9 & 5的结果是 5。

**&是根据内存中的二进制位进行运算的,而不是数据的二进制形式;**其他位运算符也一样。以-9&5为例,-9 的在内存中的存储和 -9 的二进制形式截然不同:

1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)
-0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (-9 的二进制形式,前面多余的 0 可以抹掉)

按位与运算通常用来对某些位清 0,或者保留某些位。例如要把 n 的高 16 位清 0 ,保留低 16 位,可以进行n & 0XFFFF运算(0XFFFF 在内存中的存储形式为 0000 0000 – 0000 0000 – 1111 1111 – 1111 1111)。

#include <stdio.h>
int main(){
    int n = 0X8FA6002D;
    printf("%d, %d, %X\n", 9 & 5, -9 & 5, n & 0XFFFF);
    return 0;
}
//1, 5, 2D

按位或运算(|)

参与|运算的两个二进制位有一个为 1 时,结果就为 1,两个都为 0 时结果才为 0。例如1|1为1,0|0为0,1|0为1,这和逻辑运算中的||非常类似。

例如,9 | 5可以转换成如下的运算:

0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (9 在内存中的存储)

| 0000 0000 – 0000 0000 – 0000 0000 – 0000 0101 (5 在内存中的存储)


0000 0000 – 0000 0000 – 0000 0000 – 0000 1101 (13 在内存中的存储)

9 | 5的结果为 13。

又如,-9 | 5可以转换成如下的运算:

1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)

| 0000 0000 – 0000 0000 – 0000 0000 – 0000 0101 (5 在内存中的存储)


1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)

-9 | 5的结果是 -9。

按位或运算可以用来将某些位置 1,或者保留某些位。例如要把 n 的高 16 位置 1,保留低 16 位,可以进行n | 0XFFFF0000运算(0XFFFF0000 在内存中的存储形式为 1111 1111 – 1111 1111 – 0000 0000 – 0000 0000)。

#include <stdio.h>
int main(){
    int n = 0X2D;
    printf("%d, %d, %X\n", 9 | 5, -9 | 5, n | 0XFFFF0000);
    return 0;
}
//13, -9, FFFF002D

按位异或运算(^)

参与^运算两个二进制位不同时,结果为 1,相同时结果为 0。例如0^1为1,0^0为0,1^1为0。

例如,9 ^ 5可以转换成如下的运算:

0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (9 在内存中的存储)

^ 0000 0000 – 0000 0000 – 0000 0000 – 0000 0101 (5 在内存中的存储)


0000 0000 – 0000 0000 – 0000 0000 – 0000 1100 (12 在内存中的存储)

9 ^ 5的结果为 12。

又如,-9 ^ 5可以转换成如下的运算:

1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)

^ 0000 0000 – 0000 0000 – 0000 0000 – 0000 0101 (5 在内存中的存储)


1111 1111 – 1111 1111 – 1111 1111 – 1111 0010 (-14 在内存中的存储)

-9 ^ 5的结果是 -14。

按位异或运算可以用来将某些二进制位反转。例如要把 n 的高 16 位反转,保留低 16 位,可以进行n ^ 0XFFFF0000运算(0XFFFF0000 在内存中的存储形式为 1111 1111 – 1111 1111 – 0000 0000 – 0000 0000)。

#include <stdio.h>
int main(){
    unsigned n = 0X0A07002D;
    printf("%d, %d, %X\n", 9 ^ 5, -9 ^ 5, n ^ 0XFFFF0000);
    return 0;
}
//12, -14, F5F8002D

取反运算(~)

取反运算符~为单目运算符,右结合性,作用是对参与运算的二进制位取反。例如~1为0,~0为1,这和逻辑运算中的!非常类似。。

~9可以转换为如下的运算:
0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (9 在内存中的存储)

1111 1111 – 1111 1111 – 1111 1111 – 1111 0110 (-10 在内存中的存储)

所以~9的结果为 -10。

~-9可以转换为如下的运算:
1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)

0000 0000 – 0000 0000 – 0000 0000 – 0000 1000 (8 在内存中的存储)

所以~-9的结果为 8。

#include <stdio.h>
int main(){
    printf("%d, %d\n", ~9, ~-9 );
    return 0;
}
//-10, 8

左移运算(<<)

左移运算符<<用来把操作数的各个二进制位全部左移若干位,高位丢弃,低位补0。

例如,9<<3可以转换为如下的运算:

<< 0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (9 在内存中的存储)


0000 0000 – 0000 0000 – 0000 0000 – 0100 1000 (72 在内存中的存储)

所以9<<3的结果为 72。

又如,(-9)<<3可以转换为如下的运算:

<< 1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)


1111 1111 – 1111 1111 – 1111 1111 – 1011 1000 (-72 在内存中的存储)

所以(-9)<<3的结果为 -72。

如果数据较小,被丢弃的高位不包含 1,那么左移 n 位相当于乘以 2 的 n 次方。

#include <stdio.h>
int main(){
    printf("%d, %d\n", 9<<3, (-9)<<3 );
    return 0;
}
//72,-72

右移运算(>>)

右移运算符>>用来把操作数的各个二进制位全部右移若干位,低位丢弃,高位补 0 或 1。如果数据的最高位是 0,那么就补 0;如果最高位是 1,那么就补 1。

9>>3可以转换为如下的运算:

>>0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (9 在内存中的存储)


0000 0000 – 0000 0000 – 0000 0000 – 0000 0001 (1 在内存中的存储)

所以9>>3的结果为 1。

(-9)>>3可以转换为如下的运算:

>>1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)


1111 1111 – 1111 1111 – 1111 1111 – 1111 1110 (-2 在内存中的存储)

所以(-9)>>3的结果为 -2

如果被丢弃的低位不包含 1,那么右移 n 位相当于除以 2 的 n 次方(但被移除的位中经常会包含 1)。

#include <stdio.h>
int main(){
    printf("%d, %d\n", 9>>3, (-9)>>3 );
    return 0;
}
//1, -2

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/5117.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[技术经理]02 什么是技术经理?

目录01什么是技术经理02总结01什么是技术经理 什么是技术经理&#xff1f; 我用一句话概括为&#xff1a;专业技术团队的管理者。 技术经理&#xff0c;是一种管理职位&#xff0c;通常是在软件开发、互联网等科技公司或技术团队中担任。 技术经理的职责&#xff0c;**是管理…

Docker入门

文章目录Docker为什么出现Docker能干嘛学习途径Docker安装Docker的基本组成环境说明安装步骤阿里云镜像加速底层原理Docker为什么出现 一款产品从开发到上线&#xff0c;从操作系统&#xff0c;到运行环境&#xff0c;再到应用配置。作为开发运维之间的协作我们需要 关心很多东…

文献阅读(247)AIpa

题目&#xff1a;Alpa: Automating Inter- and Intra-Operator Parallelism for Distributed Deep Learning时间&#xff1a;2022会议&#xff1a;OSDI研究机构&#xff1a;UCB 传统的DNN并行策略&#xff1a; 现有的分布式训练系统要么需要用户手动创建并行化计划&#xff0c…

测试笔记:接口测试

目录1.接口&#xff08;1&#xff09;接口概念&#xff08;2&#xff09;接口类型2、接口风格&#xff08;1&#xff09;传统风格&#xff08;2&#xff09;RESTful风格接口3、接口测试&#xff08;1&#xff09;接口测试是什么&#xff08;2&#xff09;接口测试原理&#xff…

Node.js学习笔记——fs模块

fs全称为file system&#xff0c;称之为文件系统&#xff0c;是Node.js中的内置模块&#xff0c;可以对计算机中的磁盘进行操作。 本章节会介绍如下操作&#xff1a; 文件写入文件读取文件移动与重命名文件删除文件夹操作查看资源状态 一、文件写入 文件写入就是将数据保存…

利用nginx实现动静分离的负载均衡集群实战

前言 大家好&#xff0c;我是沐风晓月&#xff0c;今天我们利用nginx来作为负载&#xff0c;实现两台apache服务器的动静分离集群实战&#xff1b; 本文收录于沐风晓月的专栏《linux基本功-系统服务实战》&#xff0c;更多内容可以关注我的博客&#xff1a; https://blog.csd…

Visual Studio 2015 + cmake编译QT5程序

概述 由于QT的集成开发环境QTCreate&#xff0c;在代码调试功能上远不及Visual Studio方便&#xff0c;因此&#xff0c;在Windows平台&#xff0c;可以使用Visual Studio来开发调试QT程序&#xff0c;本文章就主要介绍下&#xff0c;如何使用CMAKE编译QT5程序&#xff0c;并使…

【JAVA真的没出路了吗?】

2023年了&#xff0c;转行IT学习Java是不是已经听过看过很多次了。随之而来的类似学Java没出路、Java不行了、对Java感到绝望等等一系列的制造焦虑的话题也在网上层出不穷&#xff0c;席卷了一大片的对行业不了解的吃瓜群众或是正在学习中的人。如果是行外人真的会被这种言论轻…

【教程】使用ChatGPT制作基于Tkinter的桌面时钟

目录 描述 代码 效果 说明 下载 描述 给ChatGPT的描述内容&#xff1a; python在桌面上显示动态的文字&#xff0c;不要显示窗口边框。窗口背景和标签背景都是透明的&#xff0c;但标签内的文字是有颜色。使用tkinter库实现&#xff0c;并以class的形式书写&#xff0c;方…

GPS时间序列分析---剔除跳跃点,拟合时间序列

通常利用GPS时间序列进行数据分析时&#xff0c;会遇到大地震的发生&#xff0c;这个时候会导致GPS的观测结果出现很大的跳跃值&#xff0c;这对后续的数据处理和分析带来了困难。这里分享一个最近了解的&#xff0c;可以用于处理这一问题的工具包---TSAnalyzer。下面主要介绍该…

Adobe:当创意工作遇上生成式AI

放眼全球IT行业&#xff0c;当前最炙手可热的领域是什么&#xff1f;答案显然只有一个&#xff1a;因为ChatGPT而火爆全球的生成式AI&#xff08;Artificial Intelligence Generated Content&#xff0c;简称AIGC&#xff09;&#xff0c;又称人工智能生成内容。那么当创意设计…

再学一下Feign的原理

简介 Feign是Spring Cloud Netflix组件中的一个轻量级Restful的HTTP服务客户端&#xff0c;它简化了服务间调用的方式。 Feign是一个声明式的web service客户端.它的出现使开发web service客户端变得更简单.使用Feign只需要创建一个接口加上对应的注解, 比如FeignClient注解。…

Go分布式爬虫学习笔记(十四)

文章目录14_context为什么需要Context?级联退出Context 的使用方法。context.ValuecontextContext 最佳实践Context 底层原理14_context Never start a goroutine without knowing how it will stop。 如果你不知道协程如何退出&#xff0c;就不要使用它。 为什么需要Context?…

家政服务系统APP小程序需具备哪些功能?

由于工作忙碌或者是懒人经济作祟&#xff0c;现代人对于家政服务的需求直线上升。而且互联网技术深入生活的方方面面&#xff0c;让上门家政服务系统开发成为很多线下家政公司转型互联网的方式&#xff0c;那么开发一款家政服务系统APP小程序需具备哪些功能呢&#xff1f; …

逻辑回归 算法推导与基于Python的实现详解

文章目录1 逻辑回归概述2 逻辑回归公式推导与求解2.1 公式推导2.2公式求解3 基于Python的实现3.1可接收参数3.2 完整代码示例1 逻辑回归概述 逻辑回归&#xff08;Logistic Regression&#xff09;是一种用于分类问题的统计学习方法。它基于线性回归的原理&#xff0c;通过将线…

【软考——系统架构师】架构、系分、软设的区别和联系

&#x1f50e;这里是【软考——系统架构师】&#xff0c;关注我考试轻松过线 &#x1f44d;如果对你有帮助&#xff0c;给博主一个免费的点赞以示鼓励 欢迎各位&#x1f50e;点赞&#x1f44d;评论收藏⭐️ 文章目录&#x1f440;三科相同点&#x1f440;三科不同点--上午题&am…

CISAW-CISDR灾难备份与恢复专业级认证

证书样板&#xff1a; 认证机构 中国网络安全审查技术与认证中心&#xff08;英文缩写为&#xff1a;CCRC,原为中国信息安全认证中心&#xff09;于 2006 年由中央机构编制委员会办公室批准成立&#xff0c;为国家市场监督管理总局直属事业单位。依据《网络安全法》 《网络安…

Java每日一练(20230401)

目录 1. 合并K个升序链表 &#x1f31f;&#x1f31f;&#x1f31f; 2. 最长有效括号 &#x1f31f;&#x1f31f;&#x1f31f; 3. 分割回文串 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 …

Linux系统一键安装最新内核并开启 BBR 脚本

本脚本适用环境 系统支持&#xff1a;CentOS 6&#xff0c;Debian 8&#xff0c;Ubuntu 16 虚拟技术&#xff1a;OpenVZ 以外的&#xff0c;比如 KVM、Xen、VMware 内存要求&#xff1a;≥128M 更新日期&#xff1a;2022 年 5 月 11 日 关于本脚本 1、本脚本已在 蓝易云 上…

后端Springboot框架搭建APi接口开发(第二章)

上一章我讲述了如何使用Mybatis操作数据库。这一章我讲述如何利用Sptring框架搭建API接口 第一节&#xff1a;封装SqlSessionFactory工具类 在API操作数据库大量调用SqlSessionFactory&#xff0c;因此应将SqlSessionFactory封装成工具类供方法随时调用 在文件结构中的util文…