第10章 枚举与结构类型

10.1 枚举

10.1.1 枚举

常量符号化

#include <stdio.h>

const int red = 0;
const int yellow = 1;
const int green = 2;

int main(int argc, char const *argv[]) {
    int color = -1;
    char *colorName = NULL;
    
    printf("输入你喜欢的颜色的代码:");
    scanf("%d", &color);
    switch(color) {
        case red: colorName = "red"; break;
        case yellow: colorName = "yellow"; break;
        case green: colorName = "green"; break;
        default: colorName = "unknown"; break;    
    }
    printf("你喜欢的颜色是%s\n", colorName);
  
    return 0;
}
  • 用符号而不是具体的数字来表示程序中的数字

枚举

#include <stdio.h>

enum COLOR {RED, YELLOW, GREEN};

int main(int argc, char const *argv[]) {
    int color = -1;
    char *colorName = NULL;
    
    printf("输入你喜欢的颜色的代码:");
    scanf("%d", &color);
    switch(color) {
        case RED: colorName = "red"; break;
        case YELLOW: colorName = "yellow"; break;
        case GREEN: colorName = "green"; break;
        default: colorName = "unknown"; break;    
    }
    printf("你喜欢的颜色是%s\n", colorName);
  
    return 0;
}
  • 用枚举而不是定义独立的const int变量
  • 更方便的形式去定义名字

枚举

  • 枚举是一种用户定义的数据类型,它用关键字enum以如下语法来声明:

    enum 枚举类型名字{名字0, ...., 名字n};
    
  • 枚举类型名字通常并不真的使用,要用的是在大括号里的名字,因为它们就是常量符号,它们的数据类型是int,值则依次从0到n

  • 例如

    enum colors{red, yellow, green};
    
    • 就创建了三个常量,red的值是0,yellow的值是1,而green是2
    • 当需要一些可以排列起来的常量值时,定义枚举的意义就是给出了这些常量值名字

代码

#include <stdio.h>
enum color {red, yellow, green};
void f(enum color c);

int main() {
    enum color t = red;//0
    scanf("%d", &t);
    f(t);
    
    return 0;
}

void f(enum color c) {
    printf("%d\n", c);
}
  • 枚举量可以作为值
  • 枚举类型可以跟上enum作为类型
  • 但是实际上是以整数来做内 部计算和外部输入输出的

套路:自动计数的枚举

#include <stdio.h>
enum COLOR {RED, YELLOW, GREEN, NumCOLORS};
int main(int argc, char const *argv[]) {
    int color = -1;
    char *ColorName[NumCOLORS] = {
        "red", "yellow", "green",
    };
    char *colorName = NULL;
    
    printf("输入你喜欢的颜色的代码:");
    scanf("%d", &color);
    if(color>=0&&color<NumCOLORS) {
        colorName = ColorNames[color];
    } else {
        colorName = "unknown";
    }
    printf("你喜欢的颜色是%s\n", colorName);
    
    return 0;
}

枚举量

  • 声明枚举量的时候可以指定值
    • enum COLOR {RED=1, YELLOW, GREEN = 5};
#include <stdio.h>
enum COLOR {RED=1, YELLOW, GREEN = 5, NumCOLORS};
int main() {
    printf("code for GREEN is %d\n", GREEN);
    return 0;
}

枚举只是int

  • 即使给枚举类型的变量赋不存在的整数值也没有任何warning,error

枚举

  • 虽然枚举类型可以当作类型使用,但是实际上很少用或者是不好用
  • 如果有意义上排比的名字,用枚举比const int方便(用于常量符号化)
  • 枚举比宏好,因为枚举有int类型

10.2 结构

10.2.1 结构类型

如果我们需要表达三个值一起,怎么办?比如年月日三个一起,或者是一个人的信息

struct people {
    char name[11];
    int gender;
    int height;
    int grade;
};
  • 需要表达由不同数据组成的数据,就得需要用到C语言的结构

声明结构类型

#include <stdio.h>

int main(int argc, char const *argv[]) {
    struct date {
        int month;
        int day;
        int year;
    };
    struct date today;
    today.month = 07;
    today.day = 31;
    today.year = 2014;
    printf("Today's date is %i-%i-%i. \n", today.year, today.month, today.day);
    return 0;
}

在函数内/外?

#include <stdio.h>

struct date {
        int month;
        int day;
        int year;
};

int main(int argc, char const *argv[]) {
    struct date today;
    today.month = 07;
    today.day = 31;
    today.year = 2014;
    printf("Today's date is %i-%i-%i. \n", today.year, today.month, today.day);
    return 0;
}
  • 和本地变量一样,在函数内部声明的结构类型只能在函数内部使用
  • 所以通常在函数外部声明结构类型,这样就可以被多个函数所使用了

声明结构的形式

struct point{
    int x;
    int y;
};
struct point p1, p2;//p1和p2都是point,里面有x和y的值

struct {
    int x;
    int y;
} p1, p2;//p1和p2都是一种无名结构,里面有x和y,但这个结构没有名字

//更常见的做法是下面这样的
struct point {
    int x;
    int y;
} p1, p2;

结构的初始化

#include <stdio.h>

struct date {
   int month;
   int day;
   int year;
};

int main(int argc, char const *argv[]) {
    struct date today = {07, 31, 2014};
    struct date thismonth = {.month = 7, .year = 2014};
    
    printf("Today's date is %i-%i-%i. \n", today.year, today.month, today.day);
    printf("This month is %i-%i-%i. \n", thismonth.year, thismonth.month, thismonth.day);
    return 0;
}
  • 没有给初始值的结构成员的值会是0

结构成员

  • 结构和数组有点像
    • 不同点:数组元素只能是同一类型,而结构成员可以是不同类型的
    • 数组用[]运算符和下标访问其成员
      • a[0] = 10;
    • 结构用.运算符和名字访问其成员
      • today.day
      • student.firstName
      • p1.x
      • pl.y
    • 结构类型没有意义,结构变量才是实体

结构运算

  • 要访问整个结构,直接用结构变量的名字

  • 对于整个结构,可以做赋值,取地址,也可以传递给函数参数

    • p1 = (struct point){5, 10};//相当于p1.x = 5; p1.y = 10;
      • 前面有个强制转换,要把这两个值转为point的变量,并赋给p1
    • p1 = p2; //相当于p1.x = p2.x; p1.y = p2.y;
    • 数组无法做这两种运算
  • 其它

    struct point p1 = {5, 10};
    struct point p1 = {.x = 5, .y = 10};
    
  • 测试代码

    #include <stdio.h>
    
    struct date {
       int month;
       int day;
       int year;
    };
    
    int main(int argc, char const *argv[]) {
        struct date today;
        today = (struct date){07, 31, 2014};
        struct date day;
        day = today;
        
        day.year = 2015;
        
        printf("Today's date is %i-%i-%i. \n", today.year, today.month, today.day);
        printf("The day's date is %i-%i-%i. \n", day.year, day.month, day.day);//2015-07-31
        return 0;
    }
    
    • %i是%d的老式写法
    • 而上面这段代码也说明了,year和month其它都是两个完全不同的变量,有各自自己的值。但就是day = today这条代码不一样

结构指针

  • 和数组不同,结构变量的名字并不是结构变量的地址,必须使用&运算符

  • 代码

    #include <stdio.h>
    
    struct date {
       int month;
       int day;
       int year;
    };
    
    int main(int argc, char const *argv[]) {
        struct date today;
        today = (struct date){07, 31, 2014};
        struct date day;
        
        struct date *pDate = &today;
            
        printf("Today's date is %i-%i-%i. \n", today.year, today.month, today.day);
        printf("The day's date is %i-%i-%i. \n", day.year, day.month, day.day);//2015-07-31
        printf("address of today is %p\n", pDate);
        return 0;
    }
    

结构作为函数参数

int numberOfDays(struct date d)
  • 整个结构可以作为参数的值传入函数
  • 这时候是在函数内新建一个结构变量,并复制调用者的结构的值
  • 也可以返回一个结构
  • 这与数组完全不同

给出明天的日子

#include <stdio.h>
#include <stdbool.h>

struct date {
    int month;
    int day;
    int year;
};

bool isLeap(struct date d);
int numberOfDays(struct date d);

int main(int argc, char const *argv[]) {
    struct date today, tomorrow;
    
    printf("Enter today's date (yyyy mm dd):");
    scanf("%i %i %i", &today.year, &today.month, &today.day);
    
    if(today.day != numberOfDays(today)) {
        tomorrow.day = today.day + 1;
        tomorrow.month = today.month;
        tomorrow.year = today.year;
    } else if(today.month == 12) {
        tomorrow.day = 1;
        tomorrow.month = 1;
        tomorrow.year = today.year + 1;
    } else {
        tomorrow.day = 1;
        tomorrow.month = today.month + 1;
        tomorrow.year = today.year;
    } 
    
    printf("Tomorrow's date is %i-%i-%i", tomorrow.year, tomorrow.month, tomorrow.day);
    
    return 0;
}

int numberOfDays(struct date d) {
    int result;
    const int numOfDays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    
    result = numOfDays[d.month - 1];
    if(d.month == 2 && isLeap(d)) result = 29;
    
    return result;
}

bool isLeap(struct date d) {
    bool result = false;
    if(d.year % 4 == 0 && d.year % 100 !=0 || d.year % 400 == 0) result = true;
    
    return result;
}

输入结构

  • 没有直接的方式可以一次scanf一个结构

  • 错误代码

    #include <stdio.h>
    
    struct point {
        int x;
        int y;
    };
    
    void getStruct
    

解决方案

  • 结构与数组不一样,因此没有办法直接修改

  • 而结构作为参数的值传入函数时,只不过就是克隆了另一个结构,而不是指针

  • 传入函数与传入数组是不同的

  • 在这个输入函数中,完全可以创建一个临时的结构变量,然后把这个结构返回给调用者

  • 正确代码

    #include <stdio.h>
    
    struct point {
        int x;
        int y;
    };
    
    struct point getStruct(void);
    void output(struct point p); 
    
    int main() {
        struct point y = {0, 0};
        y = getStruct();
        output(y);
        
        return 0;
    }
    
    struct point getStruct(void) {
        struct point p;
        scanf("%d %d", &p.x, &p.y);
        return p;
    }
    
    void output(struct point p) {
        printf("x=%d", p.x);
        printf("y=%d", p.y);
    }
    

结构指针作为参数

  • 经典C说过:传入一个较大的结构时,传入指针比返回结构更有效

指向结构的指针

struct date {
    int month;
    int day;
    int year; 
} myday;

struct date *p = &myday;

(*p).month = 12;
p->month = 12;
  • 用->表示指针所指的结构变量中的成员

结构指针参数

#include <stdio.h>

struct point* getStruct(struct point*);
void output(struct point);
void print(const struct point *p);

int main() {
    struct point y = {0, 0};
    getStruct(&y);
    output(y);
    output(*getStruct(&y));//现在这个指针指向的是函数的返回值
    print(getStruct(&y));
    getStruct(&y)->x = 0;
    *getStruct(&y) = (struct point){1,2};
}

struct point* getStruct(struct point *p) {
    scanf("%d", &(p->x));
    scanf("%d", &(p->y));
    printf("d, %d", p->x, p->y);
    return p;
}

void output(struct point p) {
    printf("%d, %d", p.x, p.y);
}

void print(const struct point *p) {
    printf("%d, %d", p->x, p->y);
}

10.2.2 结构与函数

结构数组

struct date dates[100];
struct date dates[] = {
    {4, 5, 2005}. {2, 4, 2005}};

代码案例

#include <stdio.h>
struct time {
    int hours;
    int minutes;
    int seconds;
};

struct time timeUpdate(struct time now);

int main(void) {
    struct time testTimes[5] = {
        {11, 59, 59}, {12, 0, 0}, {1, 29, 59}, {23, 59, 59}, {19, 12, 27}
    };
    
    int i;
    for(i=0;i<5;i++) {
        printf("Time is %.2i:%.2i:%.2i", testTimes[i].hour, testTimes[i].minutes, testTimes[i].seconds);
        testTimes[i] = timeUpdate(testTimes[i]);
        printf("...one second later it's %.2i:%.2i:%.2i\n", testTimes[i].hour, testTimes[i].minutes, testTimes[i].seconds)
    }
    return 0;
}

struct time timeUpdate(struct time now) {
    ++now.seconds;
    if(now.seconds == 60) {
        now.seconds = 0;
        ++now.minutes;
        
        if(now.minutes == 60) {
            now.minutes = 0;
            ++now.hour;
            
            if(now.hour == 24) {
                now.hour = 0;
            }
        }
    }
    
    return now;
}

结构中的结构

struct dateAndTime {
    struct date sdate;
    struct time stime;
}

嵌套的结构

struct point {
    int x;
    int y;
};

struct rectangle {
    struct point pt1;
    struct point pt2;
};
81
82
  • 最后一个rp->pt1->x,是因为pt1不是指针,无法指向,它就是一个结构

结构中的结构的数组

#include <stdio.h>

struct point {
    int x;
    int y;
};

struct rectangle {
    struct point p1;
    struct point p2;
};

void printRect(struct rectangle r) {
    printf("<%d, %d> to <%d, %d>\n", r.p1.x, r.p1.y, r.p2.x, r.p2.y);
}

int main(int argc, char const *argv[]) {
    int i;
    struct rectangle rects[2] = {
        {
            {1, 2}, {3, 4}
        },
        {
            {5, 6}, {7, 8}
        }
    };
    for(i=0;i<2;i++) printRect(rects[i]);
}

10.2.3 拓展:像类一样在结构体中封装函数

#include <stdio.h>
typedef struct point {
    int x, y;
    void (*setPoint)(struct point *s, int a, int b);
} Point;

void set(Point *s, int a, int b) {
    s->x = a;
    s->y = b;
}


int main() {
    Point p1;
    p1.setPoint = set;
    p1.setPoint(&p1, 6, 9);
    printf("%d, %d", p1.x, p1.y);
    return 0;
}

10.2.4 拓展:C++可以在结构体中定义函数

#include <bits/stdc++.h>
using namespace std;
//思路:先把毯子信息存在数组中,再得到要查看的点,遍历毯子信息,看哪个毯子盖住了这个点。 
typedef struct Carpet
{
	int xmin, xmax, ymin, ymax;
	Carpet()
	{
		set(0,0,0,0);
	}
	void set(int a, int b, int g, int k)
	{
		xmin = a;
		xmax = a + g;
		ymin = b;
		ymax = b + k;
	}
	bool contains(int x, int y)//地毯是否盖住(x,y)点
	{
		if(x >= xmin && x <= xmax && y >= ymin && y <= ymax)
			return true;
		else
			return false;
	}
}Carpet;

int main()
{
	int n, a, b, g, k, x, y, frontCptNum = -1;//frontCptNum:最上面毯子的编号 
	Carpet carp[10005];
	cin>>n;
	for(int i = 1; i <= n; ++i)
	{
		cin>>a>>b>>g>>k;
		carp[i].set(a, b, g, k);
	}
	cin>>x>>y;
	for(int i = 1; i <= n; ++i)
	{
		if(carp[i].contains(x, y))
			frontCptNum = i; 
	}	
	cout<<frontCptNum;
	return 0;
}

10.3 类型与联合

10.3.1 类型定义

自定义数据类型(typedef)

  • C语言提供了一个叫做typeof的功能来声明一个已有的数据类型的新名字

    比如:typedef int Length;

    使得Length成为int类型的别名

  • 这样,Length这个名字就可以代替int出现在变量定义和参数声明的地方了:

    Length a, b, len;
    Length numbers[10];
    

typedef

83
typedef struct {
    int month;
    int day;
    int year;
} Date;
84

3.3.2 联合

他们会共用内存

86 87
#include <stdio.h>

typedef union {
    int i;
    char ch[sizeof(int)];
} CHI;

int main(int argc, char const *argv[]) {
    CHI chi;
    int i;
    chi.i = 1234;
    for(i=0; i<sizeof(int); i++) {
        printf("%02hhX", chi.ch[i]);
    }
    printf("\n");
    return 0;
}

小端与大端

预备知识

  • 地址为什么会用十六进制?0000 0000 0062 FDDC,地址是这样的。
  • 一个十六进制数是用4个比特存储的,上面这个则用了64比特存储。而因为我们用的是64位的电脑,所以它的指针所占的字节是8字节(占多少与电脑的寻址能力有关)。64/8=8,你看,8字节,刚刚好

一、大端模式和小端模式的起源
关于大端小端名词的由来,有一个有趣的故事,来自于Jonathan Swift的《格利佛游记》:Lilliput和Blefuscu这两个强国在过去的36个月中一直在苦战。战争的原因:大家都知道,吃鸡蛋的时候,原始的方法是打破鸡蛋较大的一端,可以那时的皇帝的祖父由于小时侯吃鸡蛋,按这种方法把手指弄破了,因此他的父亲,就下令,命令所有的子民吃鸡蛋的时候,必须先打破鸡蛋较小的一端,违令者重罚。然后老百姓对此法令极为反感,期间发生了多次叛乱,其中一个皇帝因此送命,另一个丢了王位,产生叛乱的原因就是另一个国家Blefuscu的国王大臣煽动起来的,叛乱平息后,就逃到这个帝国避难。据估计,先后几次有11000余人情愿死也不肯去打破鸡蛋较小的端吃鸡蛋。这个其实讽刺当时英国和法国之间持续的冲突。Danny Cohen一位网络协议的开创者,第一次使用这两个术语指代内存中的字节顺序,后来就被大家广泛接受。

二、什么是大端和小端
Big-Endian和Little-Endian的定义如下:

  1. Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

  2. Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。举一个例子,比如数字0x12 34 56 78在内存中的表示形式为:

image-20210413195958069

3)下面是两个具体例子:

  • 16bit宽的数0x1234在Little-endian模式(以及Big-endian模式)CPU内存中的存放方式(假设从地址0x4000开始存放)为:
image-20210413200729657
  • 32bit宽的数0x12345678在Little-endian模式以及Big-endian模式)CPU内存中的存放方式(假设从地址0x4000开始存放)为:
image-20210413211447470

4)大端小端没有谁优谁劣,各自优势便是对方劣势:

  • 小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。
  • 大端模式 :符号位的判定固定为第一个字节,容易判断正负。

5)理解高位字节与低位字节

  • 0x12345678的二进制表示;0001 0010 0011 0100 0101 0110 0111 1000,它所说的高位字节是右边的,例如0001(4比特,而一个char我们是1字节的,也就是8比特,所以它会读取到0001 0010)

三、数组在大端小端情况下的存储:
  以unsigned int value = 0x12345678为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示value:
  Big-Endian: 低地址存放高位,如下:

高地址
---------------
    buf[3] (0x78) -- 低位
    buf[2] (0x56)
    buf[1] (0x34)
    buf[0] (0x12) -- 高位
---------------
低地址

Little-Endian: 低地址存放低位,如下:

高地址
---------------
    buf[3] (0x12) -- 高位
    buf[2] (0x34)
    buf[1] (0x56)
    buf[0] (0x78) -- 低位
低地址

代码验证

#include <stdio.h>

union {
    unsigned int i;
    unsigned char s[sizeof(int)]; //如果不加unsigned,会出现多个fff的情况,这是因为signed char会有数组元素是负数的情况
} test;

int main() {
    test.i = 0x12345678;
    //printf("%02x\n", test.i);
    int i;
    for(i = 0; i<sizeof(int); i++) printf("%d:0x%02X\n", i, test.s[i]);// 2是向右对齐,而0的意思是当有空的时候以0填充
    //printf("%010d", 1234);
    return 0;
}
#include <stdio.h>

union {
    unsigned int i;
    unsigned char s[sizeof(int)];
} test;

int IsBigEndian() {  
    int a = 0x1234;  
    char b =  *(char *)&a;  //通过将int强制类型转换成char单字节,通过判断起始存储位置。即等于 取b等于a的低地址部分  
    if( b == 0x12) return 1;  
    return 0;  
}

int main() {
    printf("%d", IsBigEndian());
    return 0;
}

四、为什么会有大小端模式之分呢?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

参考资料

赞赏