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;
- 数组无法做这两种运算
- p1 = (struct point){5, 10};//相当于p1.x = 5; p1.y = 10;
-
其它
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;
};
- 最后一个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
typedef struct {
int month;
int day;
int year;
} Date;
3.3.2 联合
他们会共用内存
#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的定义如下:
-
Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
-
Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。举一个例子,比如数字0x12 34 56 78在内存中的表示形式为:
3)下面是两个具体例子:
- 16bit宽的数0x1234在Little-endian模式(以及Big-endian模式)CPU内存中的存放方式(假设从地址0x4000开始存放)为:
- 32bit宽的数0x12345678在Little-endian模式以及Big-endian模式)CPU内存中的存放方式(假设从地址0x4000开始存放)为:
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处理器还可以由硬件来选择是大端模式还是小端模式。
参考资料