本系列文章为浙江大学翁恺C语言程序设计学习笔记,前面的系列文章链接如下:
C语言程序设计学习笔记:P1-程序设计与C语言
C语言程序设计学习笔记:P2-计算
C语言程序设计学习笔记:P3-判断
C语言程序设计学习笔记:P4-循环
C语言程序设计学习笔记:P5-循环控制
C语言程序设计学习笔记:P6-数据类型
C语言程序设计学习笔记:P7-函数
C语言程序设计学习笔记:P8-数组
C语言程序设计学习笔记:P9-指针
C语言程序设计学习笔记:P10-字符串
之前我们讲过,如果程序中经常出现一些数字,应该用一些符号来表示它们,而不是让他们直接出现在程序中。这样做最大的好处就是可读性较好,别人看你的程序看到的是字母,很容易理解这些字母背后的意义。
举个例子,现在我输入喜欢颜色的代码,输出对应颜色的英文单词。如果是按照我们平时的做法,代码如下:(注意:Visual Studio里面编译不能通过,在case语句中会提示表达式必须含有常量值)
#define _CRT_SECURE_NO_WARNINGS
#include
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;
}
现在我们有更方便的方式:用枚举而不是定义独立的const int变量
#define _CRT_SECURE_NO_WARNINGS
#include
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;
}
枚举的定义
• 枚举是一种用户定义的数据类型,它用关键字 enum 以如下语法来声明:
• enum 枚举类型名字 {名字0, …, 名字n} ;
• 枚举类型名字通常并不真的使用,要用的是在大括号里的名字,因为它们就是就是常量符号。
• 它们的类型是int,值则依次从0到n。如:
• enum colors { red, yellow, green } ;
• 就创建了三个常量,red的值是0,yellow是1,而green是2。
• 当需要一些可以排列起来的常量值时,定义枚举的意义就是给了这些常量值名字。
枚举的用法
• enum color {red, yellow, green}
我们在声明一种新的数据类型,这种新的数据类型叫做color
• 可以当作像int、char这种类型使用,使用时必须在前面带上enum
如在当作函数参数时:void f(enum color c)
如定义一个变量:enum color t = red;
• 实际上是以整数来做内部计算和外部输入输出的
自动计数的枚举
我们一般在定义枚举量时都会在后面加上一个Num…,代表这个枚举量中有多少个值。然后我们可以用这个东西去定义数组、判断输入的数值是否在有效范围内、做遍历。代码如下所示:
#define _CRT_SECURE_NO_WARNINGS
#include
enum COLOR {RED, YELLOW, GREEN, NumCOLORS};
int main(int argc, char const *argv[])
{
int color = -1;
char *ColorNames[NumCOLORS] = {
"red", "yellow", "green"
};
char *colorName = NULL;
printf("请输入你喜欢的颜色代码:");
scanf("%d", &color);
if (color >= 0 && color < NumCOLORS) {
colorName = ColorNames[color];
}
else {
colorName = "unknow";
}
printf("你喜欢的颜色是%s\n", colorName);
return 0;
}
声明枚举量的时候可以指定值,比如我指定RED为1,GREEN为5,然后打印出它们的值看看。
#define _CRT_SECURE_NO_WARNINGS
#include
enum COLOR {RED=1, YELLOW, GREEN=5, NumCOLORS};
int main(int argc, char const *argv[])
{
printf("code for GREEN is %d\n", GREEN);
return 0;
}
运行可以发现结果正确。
枚举只是int,即使给枚举类型的变量赋不存在的整数值也没有任何warning或error。比如我这里给一个枚举变量赋一个不存在的值,看看会报错不。
#define _CRT_SECURE_NO_WARNINGS
#include
enum COLOR {RED=1, YELLOW, GREEN=5, NumCOLORS};
int main(int argc, char const *argv[])
{
enum COLOR color = 0;
printf("code for GREEN is %d\n", GREEN);
printf("and color is %d\n", color);
return 0;
}
最后,我们来讨论枚举的使用程度。
• 虽然枚举类型可以当作类型使用,但是实际上很(bu)少(hao)用
• 如果有意义上排比的名字,用枚举比const int方便
• 枚举比宏(macro)好,因为枚举有int类型
如果现在我们想表达的数据比较复杂,它不是一个值,比如日期,包含年月日。比如时间,包含时分秒。比如学生信息,包含学号姓名年龄。但是我们又想用一个整体去表达这些数据,这时候我们就需要使用结构。
一个结构就是一种复合的类型,它里面可以有各种类型的成员,然后我们用一个变量来表达那些数据。我们来看一下结构长什么样子。
#define _CRT_SECURE_NO_WARNINGS
#include
int main(int argc, char const *argv[])
{
struct date {
int month;
int day;
int year;
}; //别忘了这个分号
struct date today;
today.month = 01;
today.day = 31;
today.year = 2021;
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;
p1和p2都是point,里面有x和y的值
结构的初始化:
初始化结构有两种方式,如下面代码所示:
#define _CRT_SECURE_NO_WARNINGS
#include
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;
}
• 结构和数组有点像
• 数组用[]运算符和下标访问其成员
a[0] = 10;
• 结构用 . 运算符和名字访问其成员
today.day
student.firstName
p1.x
p1.y
结构运算:
• 要访问整个结构,直接用结构变量的名字
• 对于整个结构,可以做赋值、取地址,也可以传递给函数参数
• p1 = (struct point){5, 10}; //相当于p1.x = 5;p1.y = 10;
• p1 = p2; //相当于p1.x = p2.x; p1.y = p2.y;
我们测试一下结构的赋值运算:
#define _CRT_SECURE_NO_WARNINGS
#include
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;
printf("Today's date is %i-%i-%i.\n",
today.year,today.month,today.day);
printf("This month is %i-%i-%i.\n",
day.year, day.month, day.day);
return 0;
}
结构指针
• 和数组不同,结构变量的名字并不是结构变量的地址,必须使用&运算符
• struct date *pDate = &today;
我们写代码来测试一下:
#define _CRT_SECURE_NO_WARNINGS
#include
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 *pData = &today;
printf("address of today is %p\n", pData);
return 0;
}
当你声明了一种结构,你就拥有了一种自己的数据类型。这种结构类型也可以作为函数参数进行传递。
int numOfDays(struct date d)
• 整个结构可以作为参数的值传入函数
• 这时候是在函数内新建一个结构变量,并复制调用者的结构的值
• 也可以返回一个结构
• 这与数组完全不同
我们写一段代码来看看结构作为函数参数传递的具体细节。这个案例中,我们输入一个时间,包含年月日,然后输出那个时间第二天的时间。具体的代码和注释如下所示:
#define _CRT_SECURE_NO_WARNINGS
#include
#include
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 (mm dd yyyy):");
scanf("%i %i %i", &today.month, &today.day, &today.year);
//如果不是这个月的最后一天
if (today.day != numberOfDays(today)) {
tomorrow.day = today.day + 1;
tomorrow.month = today.month;
tomorrow.year = today.year;
}
//如果是12月的最后一天
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-%u-%i.\n",
tomorrow.year, tomorrow.month, tomorrow.day);
return 0;
}
int numberOfDays(struct date d)
{
int days;
//每个月的天数
const int daysPerMonth[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
//如果是2月且是闰年
if(d.month == 2 && isLeap(d))
days == 29;
else
days = daysPerMonth[d.month-1];
return days;
}
bool isLeap(struct date d)
{
bool leap = false;
//判断闰年的规则
if ((d.year % 4 == 0 && d.year % 100 != 0) || d.year % 400 == 0) {
leap = true;
}
return leap;
}
输入结构
1、我们常用的数据类型如int、double这些直接可以使用scanf输入。但是没有直接的方式可以一次scanf一个结构。
2、我们试着来写一个函数来读入结构
#define _CRT_SECURE_NO_WARNINGS
#include
struct point {
int x;
int y;
};
void getStruct(struct point);
void output(struct point);
int main() {
struct point y = {0, 0};
getStruct(y);
output(y);
return 0;
}
void getStruct(struct point p)
{
scanf("%d", &p.x);
scanf("%d", &p.y);
printf("%d,%d", p.x, p.y);
}
void output(struct point p)
{
printf("%d,%d", p.x, p.y);
}
然而C语言在函数调用时是传值的,所以函数中的p与main中的y是不同的。在函数读入了p的数值之后,没有任何东西回到main,所以y还是{0, 0}。
3、解决方案
• 之前的方案,把一个结构传入了函数,然后在函数中操作,但是没有返回去
• 问题在于传入函数的是外面那个结构的克隆体,而不是指针
• 传入结构和传入数组是不同的
• 在这个输入函数中,完全可以创建一个临时的结构变量,然后把这个结构返回给调用者
我们根据上面的思路写出代码:
#define _CRT_SECURE_NO_WARNINGS
#include
struct point {
int x;
int y;
};
struct point getStruct();
void output(struct point);
int main(int argc, char const *argv[]) {
struct point y = {0, 0};
y = getStruct();
output(y);
return 0;
}
struct point getStruct()
{
struct point p;
scanf("%d", &p.x);
scanf("%d", &p.y);
printf("%d,%d\n", p.x, p.y);
return p;
}
void output(struct point p)
{
printf("%d,%d", p.x, p.y);
}
• K & R 说过 (p.131)
• “If a large structure is to be passed to a function, it is generally more efficient to
pass a pointer than to copy the whole structure”
当你有一个较大的结构传进函数时,通常传递结构的指针比直接拷贝整个结构效率更高
指向结构的指针
用->表示指针所指的结构变量中的成员
struct date {
int month;
int day;
int year;
} myday;
struct date *p = &myday;
(*p).month = 12;
p->month = 12;
结构指针参数
有了上面指向结构的指针,我们就可以将输入结构的代码修改成下面这样。代码中我们有两个打印结构的函数,他们传递的参数分别是结构和结构指针。
#define _CRT_SECURE_NO_WARNINGS
#include
struct point {
int x;
int y;
};
struct point* getStruct(struct point*);
void output(struct point); //传结构的输出
void print(const struct point *p); //传结构指针的输出
int main(int argc, char const *argv[]) {
struct point y = { 0, 0 };
getStruct(&y);
output(y);
output(*getStruct(&y)); //*getStruct(&y)返回的就是y
print(getStruct(&y)); //getStruct(&y)返回的就是&y
*getStruct(&y) = (struct point){1,2}; //测试代码,给结构赋值
return 0;
}
//返回的就是这个p
struct point* getStruct(struct point *p)
{
scanf("%d", &p->x);
scanf("%d", &p->y);
printf("%d,%d\n", p->x, p->y);
return p;
}
void output(struct point p)
{
printf("%d,%d\n", p.x, p.y);
}
void print(const struct point *p)
{
printf("%d,%d\n", p->x, p->y);
}
和int、double等C语言固有的变量一样,一旦我们声明了一个结构类型后,我们可以做出这种结构类型的变量,也可以做出这种结构类型的数组,即结构数组。
struct date dates[100];
struct date dates[] = {
{4,5,2005},{2,4,2005}};
我们写段代码来详细理解下结构数组。这段代码实现的功能是让结构体数组中各个结构体中时间的下一秒时间。
#define _CRT_SECURE_NO_WARNINGS
#include
struct time {
int hour;
int minutes;
int seconds;
};
struct time timeUpdates(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] = timeUpdates(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 timeUpdates(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 == 25) {
now.hour = 0;
}
}
}
return now;
}
结构的成员也可以有结构。比如刚才我们定义了date和time两个结构
,现在我就可以定义一个dateAndTime结构,里面包含date和time
struct dateAndTime {
struct date sdate;
struct time stime;
};
举个例子,现在我有一个结构体point表示一个点的坐标。然后
我定义了一个结构体retangle表示矩形,它由两个point组成。
struct point {
int x;
int y;
};
struct rectangle {
struct point pt1;
struct point pt2;
};
如果有变量
struct rectangle r;
就可以有:
r.pt1.x、r.pt1.y,
r.pt2.x 和 r.pt2.y
如果有变量定义:
struct rectangle r, *rp;
rp = &r;
那么下面的四种形式是等价的:
r.pt1.x
rp->pt1.x
(r.pt1).x
(rp->pt1).x
结构中的结构的数组
结构、数组这些东西可以相互嵌套,比如我这里有个结构体数组rects,里面包含两个矩形结构体rectangle。而每个矩形结构体又包含两个表示点的结构体point。
#include
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[ ] = {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}; // 2 rectangles
for(i=0;i<2;i++) printRect(rects[i]);
}
1、有下列代码段,则输出结果是:
struct {
int x,y;
} s[2] = {
{1,3},
{2,7},
};
printf("%d\n", s[0].y/s[1].x);
A. 0
B. 1
C. 2
D. 3
答案:B
2、有如下变量定义,则对data中的a的正确引用是:
struct sk {
int a;
float b;
} data, *p=&data;
A. (*p).data.a
B. (*p).a
C. p->data.a
D. p.data.a
答案:B
3、以下两行代码能否出现在一起?
struct { int x; int y; } x;
struct { int x; int y; } y;
答案:可以
我们定义一个结构体后,以后要使用它都要带上struct。比如我定义一个结构体
struct point{
int x;
int y;
};
我要使用时,必须带上struct,比如struct point p。那么如何可以不带这个struct呢?在使用时直接将point当作类似于int的类型使用。这个时候我们需要使用自定义数据类型:typedef。
• C语言提供了一个叫做 typedef 的功能来声明一个已有的数据类型的新名字。比如:
• typedef int Length;
• 使得 Length 成为 int 类型的别名。
• 这样, Length 这个名字就可以代替int出现在变量定义和参数声明的地方了:
Length a, b, len ;
Length numbers[10] ;
我们来看看typef和结构搭配如何使用。
typedef long int64_t; //重载已有的类型名字,新名字的含义更清晰具有可移植性
typedef struct ADate {
int month;
int day;
int year;
} Date; //简化了复杂的名字
int64_t i = 100000000000;
Date d = {9, 1, 2005};
我们再来看看这种情况。定义的结构体没有名字,它有一个变量叫做Date。至于这个结构叫什么名字我们已经不关心了,我们有了更好的方式去表达它。
typedef struct {
int month;
int day;
int year;
} Date;
我们对typedef做一个小总结:
typedef int Length; //Length就等价于int类型
typedef *char[10] Strings; // Strings 是10个字符串的数组的类型
typedef struct node {
int data;
struct node *next;
} aNode;
或
typedef struct node aNode;
这样用aNode 就可以代替struct node
联合(union)和结构(struct)非常相似,我们来写一个联合看看。
union AnElt {
int i;
char c;
} elt1, elt2;
elt1.i = 4;
elt2.c = ’a’;
elt2.i = 0xDEADBEEF;
但是结构和联合有不一样的地方:如果AnElt是个结构,那么它有两个成员,随时可以使用。但是如果AnElt是个联合,它的两个成员占据了相同的内存空间,所以叫做联合(大家联合起来使用一份内存空间)。
对于union有以下性质:
• 存储
• 所有的成员共享一个空间
• 同一时间只有一个成员是有效的
• union的大小是其最大的成员,sizeof(union …) = sizeof(每个成员)的最大值
• 初始化
• 对第一个成员做初始化
那么,union用在什么场合呢?我们先写一段代码看看。
#include
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;
}
在这段代码中,细节如下图所示:
用union的场合:
通过这种方式得到一个整数内部的各个字节。同样的,我也可以使用这种方法得到double、float内部的各个字节。当我们做文件操作时,比如将一个整数以二进制形式写到文件中去,这就是我们可以用来做读写的一个中间媒介。