12 重忆C之 链表、共用体、枚举

链表

对于确定长度的同类型数据,之前学习了如何用数组存储。对于长度不确定的、经常要改变的数据,我们则会选择构造一个被称为 链表(linked list) 的结构。

链表

链表由一系列存储了数据的节点和他们之间的指向关系构成。从第一个节点(链表头)开始,每一个节点都会指向下一个节点,直到链表末端的一个节点为止。

C 语言通常会定义一个结构体,来对链表节点进行实现。在这结构体中,有一系列数据,同时还有一个指向这一结构体类型的指针。

在这种定义下,我们就可以通过将第二个节点的地址保存在第一个节点的指针变量中的方式实现节点之间的指向了。

相对于数组来说,链表有一定的优势和劣势。

  • 优势

存储的元素数不固定,数据结构所需的内存空间无需一开始就说明。
只要改变指针指向就可以动态地在两个节点之间插入数据。

  • 劣势

在内存中不连续,查询效率没有数组高,需要从链表头开始沿着指针方向依次访问每一个节点才能到达目标节点。

通过链表节点构造一个链表

在这里,我们没有太深入地对链表进行操作,但是可以想一下如果我们要删除一个节点、插入一个节点分别应该怎么办。

删除一个节点,我们需要从head开始,沿着->next逐一地到达目标节点。将目标节点的前一节点的->next改为目标节点当前的->next内保存的值。使用free将目标节点对应的内存空间释放。

插入一个节点则需要创建一个新的节点,将链表目标位置前的一个节点的->next修改为新的节点的地址,并将新的节点的->next插入目标位置前的一个节点的原->next取值。

值得注意的是,在这一课的代码中,我们在堆区内存上开了空间,但是没有释放。从内存安全的角度出发,我们应该还要在链表被使用完毕后释放所有开辟给链表节点的堆区上内存喔。

如果我们将链表的头尾相连,我们还可以得到一个环形的链表,被称为“循环链表”。

#include <stdio.h>
#include <stdlib.h>
typedef struct node {
    int number;
    struct node *next;
} Node;

链表的性质(判断):

image.png

问题:约瑟夫环

学习链表结构后,需要用链表解决一个稍有改动的“约瑟夫环(Josephus problem)”问题:
有N个同学围成一圈,顺序编号(分别为 1,2,3...n),从编号为 K的人开始报 1,他之后(顺初始数字增长方向计算序号)的人报 2,数到某个数字 M 的人出列。出列同学的下一人又从 1继续报,数到某个数 M 的人出列。重复直到所有人出列。

现需要根据同学人数 N和给出的 K 和 M 计算出同学的正确出列顺序。

  • 输入格式
    输入为一行,包括三个被空格分隔开的符合描述的正整数 N、K和 M
    (1≤K≤N≤1000,1≤M≤2000)。
  • 输出格式
    输出为一行,包含 N个整数,为依次顺序出列的学生编号,由空格分隔开。

样例输入1

9 1 1

样例输出1

1 2 3 4 5 6 7 8 9

样例输入2

8 5 2

样例输出2

6 8 2 4 7 3 1 5

思路:在删除循环链表节点的过程中,一般是先设置临时指针,然后对临时指针循环一圈,让 临时指针 在 当前的头指针 的前面,具体原理:链表修改的原理,临时指针的next越过下节点,指向下下节点。同时将删除的节点元素的内存释放出来。
遇到的坑主要有置空以后又引用,引起的段错误
代码如下:

#include <stdio.h>
#include <stdlib.h>
typedef struct node {
    int data;
    struct node *next;
} Node;                //构造链表结构体

Node *circle_create(int n);  //创建环的函数
void count_off(Node *head, int n, int k, int m);  //约瑟夫环出列函数

int main() {
    int n, k, m;
    scanf("%d%d%d", &n, &k, &m);
    Node *head = circle_create(n);
    count_off(head, n, k, m);
    return 0;
}

Node *circle_create(int n) {
    Node *temp, *new_node, *head;
    int i;

    // 创建第一个链表节点并加数据
    temp = (Node *) malloc(sizeof(Node));  //临时申请空间
    head = temp;                                          //申请到的空间赋给头元素
    head->data = 1;

    // 创建第 2 到第 n 个链表节点并加数据
    for(i = 2; i <= n; i++) {              //”创指针“不断开辟新空间,利用temp指针进行跟随
        new_node = (Node *) malloc(sizeof(Node));   //”创指针“申请临时空间
        new_node->data = i;              
        temp->next = new_node;                      //临时节点节点指向”创指针“
        temp = new_node;                                //”创指针“赋给临时节点
    }

    // 最后一个节点指向头部构成循环链表
    temp->next = head;
    return head;
}

void count_off(Node *head, int n, int k, int m) {
Node *temp;
    temp = head;
    int i = 1;
    while(i<=n){
        temp = head;
        head = head->next;
        i++;         
    }                                            //置temp到head前一位
    i = 1;
    while(i<k){
        temp = head;
        head = head->next;
        i++;
    }                                        //head移到k号位置,temp移到k-1号位
    i = 1;
    while (1){
        while(i< m){
            temp = head;
            head = head->next;           //报m号,m号为head,temp则在head前一位
            i++;
        }
        i = 1;
        if (temp!= head){
            printf("%d ",head->data);                //输出m号,即head节点内容
            temp->next = head->next;              //这里把temp指向下下个节点
            free(head);                                      //输出完了释放head的内存
            head = temp->next;                       //现在head就得是刚才head的下一位了
        }else {
            printf("%d",head->data);
            free(head);
            break;                               //实际上还应该置野指针为NULL
        }
    }
    return;
}

共用体

它看起来和结构体很像的东西,称为 共用体(union)。
结构体特性解决了一系列不同类型的变量可以怎么被放在一起组织的问题;
而共用体,则使多种不会同时出现的变量共用同一块内存,十分方便。

共用体初始化:

定义共用体的关键字是union,一个共用体可以包括多个合法的类型描述成员,例如:

union register {
    struct {
        unsigned char byte1;
        unsigned char byte2;
        unsigned char byte3;
        unsigned char byte4;
    } bytes;
    unsigned int number;
};

这共用体所占用的内存空间是被公用的,可通过struct类型的bytesunsigned int类型的number两种不同的类型描述成员进行访问。

无论我们通过哪一种描述成员访问这一共用体,我们访问的都会是同一块内存空间。

image.png

如果用这个union register类型声明一个变量reg。我们将可以通过reg.bytes按字节访问或者通过reg.number整体访问两种不同的方式获得或修改同一片内存。

共用体在涉及到直接操作内存的嵌入式编程、需要极度节省空间的通信协议设计中都会有它的优势。

在之前的内容中,我们看到了一种通过共用体实现的可以整体修改,也可以按字节修改的类型。类似的,我们也可以定义一个既可以按位访问,也可以按字节访问的类型:

union {
    struct {
        unsigned char b1:1;
        unsigned char b2:1;
        unsigned char b3:1;
        unsigned char b4:1;
        unsigned char reserved:4;
    } bits;
    unsigned char byte;
}

这里有一个冒号是用来定义变量使用内存的“位长”的。这里:1:4表示冒号前的变量只需要占 1 个和 4 个二进制位,而不按照char类型默认的字节数占用内存。这样,用这个类型生成的变量就可以被我们就按字节或者按位进行访问和使用了(这个概念被称为 位域(bit field),在其它场景下也可以被使用)。

再举一个被设计出来专门储存IP地址的共用体结构。使用了它的变量,既可以存储 IPv4 的 IP 地址,也可以存储 IPv6 的 IP 地址,这些地址既可以作为一个整体被操作,也可以分几个部分分别操作。

union {
    // IPv4 地址
    union {
        struct {
            unsigned char _reserved[12];
            unsigned char _ip_bytes[4];
        } _raw;
        struct {
            unsigned char _reserved[12];
            unsigned char _o1;
            unsigned char _o2;
            unsigned char _o3;
            unsigned char _o4;    
        } _octet;    
    } _IPv4;

    // IPv6 地址
    union {
        struct {
            unsigned char _IpBytes[16];
        } _raw;
        struct {
            unsigned short _w1;
            unsigned short _w2;
            unsigned short _w3;
            unsigned short _w4;
            unsigned short _w5;
            unsigned short _w6;
            unsigned short _w7;
            unsigned short _w8;   
        } _word;
    } _IPv6;
} _IP;

共用体其实也可以被视为一种特殊的结构体,但是一般的结构体中的成员会在内存中对齐排列(如果你对这块有兴趣可以在互联网上通过搜索多了解一些,我们在这里不做过多介绍),而共用体则都选择以同一位置作为起点,共用同一开始地址的内存。

  • 共用体类型的变量占用内存的大小将会和他所有成员中占用内存的最大的一致。
  • 类型别名在共用体类型上的使用方式与在其他类型上相同,没有区别。
  • 和其他类型一样,共用体类型也可以被用于结构体类型定义。
  • 和其他类型一样,共用体类型也可以被用于结构体类型定义。

验证共用体类型的变量取出的内存地址是不是完全一致

#include <stdio.h>
#include <stdlib.h>

typedef union type_x {
    char a;
    int b;
    float c;
} Type_x;

typedef struct type_y {
    char a;
    int b;
    float c;
} Type_y;

int main() {
    Type_x x;
    Type_y y;
    printf("%p %p %p\n", &(x.a),&(x.b),&(x.c));
    printf("%p %p %p\n", &(y.a),&(y.b),&(y.c));
    return 0;
}

可能的运行结果:

0x7ffd44294e40 0x7ffd44294e40 0x7ffd44294e40                                  
0x7ffd44294e50 0x7ffd44294e54 0x7ffd44294e58 

枚举

C 语言提供了一种数据类型叫 枚举(enumeration)。由一系列整数成员组成,表示这一数据类型的变量可以取的所有可能值;但这些值都不直接以字面量形式存在,每一个值都被单独给予了一个名字。例如:

enum week {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
};
这种方式定义了一个与周相关的枚举类型,其中每一个成员都会对应一个编号。

  • 当像上面例子这样没有对它们进行显性的编号时,系统默认编号会从 0开始。也就是如果直接使用SUNDAY,将和使用0一样,而使用MONDAY则会相当于使用了1,依此类推。

  • 也可以给枚举类型成员进行显性的编号。如果我们给SUNDAY编号为1

enum week {
    SUNDAY = 1,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
};

我们使用MONDAY则会相当于使用了2,每一个成员都比之前编号多1

  • 也可以给多个枚举类型成员进行显性的编号。
enum week {
    SUNDAY = 1,
    MONDAY,
    TUESDAY,
    WEDNESDAY = 1,
    THURSDAY,
    FRIDAY,
    SATURDAY
};

当将SUNDAYWEDNESDAY都编号为1的时候,使用MONDAY或者使用THURSDAY则都会相当于使用了2

不难发现, 其实可以对任何一个枚举成员进行显性的编号,那么:
没有显性编号的其他成员的编号将从它之前一个显性编号的成员开始计算,按顺序依次加一获得。

当一个枚举类型被定义以后,我们可以直接用这一类型声明变量。如:

enum week {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
};
enum week exam_date;

声明了一个enum week类型的变量exam_date,它只能取定义过的枚举类型中的成员名作为值,如exam_date = TUESDAY;

structunion以及其它类型一样,我们也可以给枚举类型通过typedef设置类型别名。

输出枚举类型举例:

#include <stdio.h>
typedef enum week {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
} Week;
int main() {
    Week meeting_date;
    meeting_date = FRIDAY;
    printf("%d\n", meeting_date);
    return 0;
}
  • 枚举类型中成员的编号只能是整数。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 第一章 Nginx简介 Nginx是什么 没有听过Nginx?那么一定听过它的“同行”Apache吧!Ngi...
    JokerW阅读 32,636评论 24 1,002
  • 注:这是第三遍读《C语言深度解剖》,想想好像自从大学开始就没读完过几本书,其中谭浩强的那本《C语言程序设计(第四版...
    HavenXie阅读 1,716评论 1 6
  • 谨记 什么是价值?或许没有多少人能够明白,其实价值并不是实际存在的,它应该是一种体现,比如为城市点缀最美好的一面而...
    长风留言阅读 2,434评论 0 15
  • 3.7女生节,3.8妇女节 小宝贝们,明天要不要过不过节, 就看今天咯~ 所以,今天的主题就是 春宵一刻值千金咯~...
    馒头纪阅读 373评论 0 0
  • 明日《大雪》 白全喜 大雪时分万事休, 回眸藐看弃年愁。 今晨望月洁依旧, 笑伴寒风绕指柔。 2016年12月6日
    白全喜阅读 152评论 0 0