第十一周:结构类型
11.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 = "default";break; } printf("你喜欢的颜色是%d",colorName); return 0; }
枚举
- 用枚举而不是定义独立的const int变量
#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 = "default";break; } printf("你喜欢的颜色是%d",colorName); return 0; }
- 枚举是一种用户定义的数据类型,它用关键字enum 以如下语法来声明:
- enum 枚举类型名字{名字0,.....,名字n};
- 枚举类型名字可以省略
- 枚举类型名字通常并不真的使用,要用的是在大括号里的名字,因为它们就是常量符号,它们的类型是int,值则依次从0到n。如:
- enum colors{red,yellow,green};
- 这样就创建了3个常量,red的值是0,yellow的值是1,而green的值是2
- 当需要一些可以排列起来的常量值时,定义枚举的意义就是给了这些常量名字
#include<stdio.h> enum color{ red,yellow,green}; void f(enum color c); int main(void) { enum color t = red; 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,NumCOLOR}; int main(int argc, char const *argv[]) { int color = -1; char *ColorNames[NumCOLOR] = {"red","yellow","green",}; char *colorName = NULL; printf("请输入你喜欢的颜色的代码:"); scanf("%d",&color); if( color >= 0 && color < NumCOLORS ){ colorName = ColorNames[color]; }else{ colorName = "unknown"; } printf("你喜欢的颜色是%s",colorName); return 0; }
- 这样需要遍历所有的枚举量或者需要建立一个用枚举量做下标的数组的时候就很方便
- 上面的套路:在进行枚举的时候最后面在放上一个数(NumCOLORS),这样就能够表示NumCOLORS前面有几个数了(例如里面有3个数,索引值到0-2,在后面加上一个数,索引值刚好等于实际我们想要表达的数量)
枚举量
- 声明变量可以指定值
- enum COLOR{RED = 1,YELLOW,GREEN = 5};
#include<stdio.h> enum COLOR {RED = 1; YELLOW,GREEN=5,NumberCOLORS}; int main(int argc,char const *argv[]) { printf("code for GREEN is %d\n",GREEN); return 0; } //这样输出的话,会从1开始计数,YELLOW则变成1+1,然后到GREEN的5之前都会跳过了
枚举只是int
- 即使给枚举类型的变量赋不存在的整数值也没有任何warning或error
枚举
- 虽然枚举类型可以当作类型使用,但是实际上很(bu)少(hao)用
- 如果有意义上排比的名字,用枚举比const int方便
- 枚举比宏(macro)好,因为枚举有int类型,宏没有类型(宏我在后面会解释是啥)
11.2-1结构类型
声明结构类型
#include<stdio.h> int main(int argc,char const *argv[]) { struct date{ int month; int day; int year; };//声明在这里,最后要加上分号哦,这是在函数内部声明的,通常放在函数外面 struct date today;//在这里我们定义了一个变量是today,类型是struct date的 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 //这是第二种形式,没有名字(没有声明point) //只是定义了两个变量,因为作者并不打算接下来继续在其他地方去调用 -------------------------------------------------------- struct point{ int x; int y; }p1,p2; p1和p2都是point,里面有x和y的值t //这是第三种声明方式
结构的初始化
#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); //给的值会被填进去,没给的值跟数组一样默认为0 return 0; }
结构成员
- 结构和数组有点像
- 数组用[]运算符和下标访问其成员
- a[0] = 10;
- 结构用.运算符和其名字访问其成员
- today.day
- student.firstName
- p1.x
- p2.y
结构运算
- 要访问整个结构,直接用结构变量的名字
- 对于整个结构,可以做赋值、取地址,也可以传递给函数参数
- p1 = (struct point){5,10}; //相当于p1.x = 5;p1.y = 10;
- p1 = p2; //相当于p1.x = p2.x;p1.y = p2.y;
- 数组无法做这两种运算!但结构可以
结构指针
- 和数组不同,结构变量的名字并不是结构变量的地址,必须使用&运算符
- struct date *pDate = &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); printf("address of today is %p\n",pDate); return 0; }
11.2-2结构与函数
结构作为函数参数
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 (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; }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.\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}; 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; }
输入结构
- 没有直接的方式可以一次scanf——一个结构
- 如果我们打算写一个函数来读入结构
- 但是读入的结构如何送回来呢?
- C语言在函数调用时是传值的
- 在函数读入了p的数值之后,没有任何东西回到main,所以y还是{0,0}
- 解决方案
- 之前的方案,把一个结构传入了函数,然后在函数中操作,但是没有返回回去
- 问题在于传入函数的是外面那个结构的克隆体,而不是指针
- 传入结构和传入数组是不同的
- 在这个输入函数中,完全可以创建一个临时的结构变量,然后把这个结构返回给调用者
#include<stdio.h> struct point { int x; int y; }; void getStruct(struct point); void output(struct point); int main(int argc,char const *argv[]) { struct point y = {0,0}; getStruct(y); output(y); } 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); }
结构指针作为参数
- K&R说过(p.131)
指向结构的指针
用->表示指针所指的结构变量中的成员
struct date{ int month; int day; int year; }myday; struct date *p = &myday; (*p).month = 12; p->month = 12; //第九行跟第十行是一样的意思,第十行会更便捷 #include<stdio.h> 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)); print(getStruct(&y)); *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); }
11.2-3结构中的结构
结构数组
- struct date dates[100]; //这是在初始化数组
- struct date dates[] = {{4,5,2005},{2,4,2005}};
#include<stdio.h> struct time{ int hour; 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; } } } } struct dateAndTime{ struct date sdate; struct time stime; };
嵌套的结构
struct point{ int x; int y; }; struct rectangle{ struct point pt1; struct point pt2; }; 如果有变量 struct rectangle r; 就可以有: r.pt1.x、r.ptl.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 但是没有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[] = {{{1,2},{3.4}},{{5,6},{7,8}}};//2 rectangles for(i = 0;i < 2;i++)printRect(rects[i]); }
11.3-1类型定义
自定义数据类型(typedef)
- C语言提供了一个叫做typedef的功能来声明一个已有的数据类型的新名字
- 比如:typedef int Length;
- 使得Length成为int类型的别名
- 这样,Length这个名字就可以替代int出现在变量定义和参数声明的地方了:
- Length a,b,len;
- Length numbers[10];
Typedef
- 声明新的类型的名字
- 新的名字是某种类型的别名
- 改善了程序的可读性
typedef long int64_t; //重载已有的类型名字 新名字的含义更清晰 具有移植性 typedef struct ADate{ int month; int day; int year; }Date; //简化了复杂的名字 //在这里Date等价于struct ADate,Date代表了到达struct ADate之前的所有 int64_t i = 10000000000; Date d = {9,1,2005}; typedef int Length;//Length就等价于int类型 typedef *char[10]Strings; //String是10个字符串的数组的类型 typedef struct node{ int data; struct node*next; }aNode; 或 typedef struct node aNode;//这样用aNode就可以替代struct node
11.3-2联合
联合
- 存储
- 所有的成员共享一个空间
- 同一时间只有一个成员是有效的
- union的大小是其最大的成员
- 初始化
- 对第一个成员做初始化
#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; } //输出D20400, 1234的十六进制是0x04D2 //低位在前(相当于倒置),小端的方式
第十二周:程序结构
12.1-1全局变量:定义在函数之外的变量,全局的生存期和作用域
全局变量
- 定义在函数外面的变量是全局变量
- 全局变量具有全局的生存期和作用域
- 他们和任何函数无关
- 在任何函数内部都可以使用他们
#include<stdio.h> int f(void); int gAll = 12;//全局变量 int main(int argc,char const *argv[]) { printf("in %s gAll=%d\n",__func__,gAll); f(); printf("agn in %s gAll = %d\n",__func__,gAll); return 0; } //_func_是一个字符串,表达的是当前函数的名字,也就是main的名字 int f(void) { printf("in %s gAll=%d\n",__func__,gAll); gAll += 2; printf("agn in %s gAll=%d\n",__func__,gAll); return gAll; }
全局变量初始化
- 没有做初始化的全局变量会得到0值
- 指针会得到NULL值
- 只能用编译时刻已知的值来初始化全局变量
- 它们的初始化发生在main函数之前
被隐藏的全局变量
- 如果函数内部存在与全局变量同名的变量,则全局变量被隐藏(局部的优先度会更高噢,会覆盖掉外面的变量)
12.1-2静态本地变量:能在函数结束后继续保有原值的本地变量
静态本地变量
- 在本地变量定义时加上static修饰符就成为静态本地变量
- 当函数离开的时候,静态本地变量会继续存在并保持其值
- 静态本地变量的初始化只会在第一次进入这个函数时做,以后进入函数时会保持上次离开时的值
#include<stdio.h> int f(void); int gAll = 12;//全局变量 int main(int argc,char const *argv[]) { f(); f(); f(); return 0; } //_func_是一个字符串,表达的是当前函数的名字,也就是main的名字 int f(void) { static int all = 1; printf("in %s gAll=%d\n",_func_,gAll); gAll += 2; printf("agn in %s gAll=%d\n",_func_,gAll); return gAll; }
- 静态本地变量实际上是特殊的全局变量
- 它们(静态本地变量和全局变量)位于相同的内存区域
- 静态本地变量具有全局的生存期,函数内的局部作用域
- static 在这里的意思是局部作用域(本地可访问)
12.1-3后记:返回指针的函数,使用全局变量的贴士
*返回指针的函数
- 返回本地变量的地址是危险的
- 返回全局变量或静态本地变量的地址是安全的
- 返回在函数内malloc的内存是安全的,但是容易造成问题
- 最好的做法是返回传入的指针
tips(贴士)
- 不要使用全局变量来在函数间传递参数和结果
- 尽量避免使用全局变量
- 丰田汽车的案子(ks) 这里给出传送门(想知道的可以看看):https://www.sohu.com/a/133455549_464086
- *使用全局变量和静态本地变量的函数是线程不安全的
12.2-1宏定义
编译预处理指令
- #开头的是编译预处理指令
- 它们不是c语言的成分,但是C语言程序离不开它们
- #define用来定义一个宏 ,其实只是一个原始的文本替换
#define
- #define<名字><值>
- 注意没有结尾的分号,因为不是c的语句
- 名字必须是一个单词,值可以是各种东西
- 在C语言的编译器开始编译之前,编译预处理程序(cpp)会把程序中的名字换成值
- 完全的文本替换
- gcc——save-temps
宏
- 如果一个宏的值中有其他宏的名字,也是会被替换的
- 如果一个宏的值超过来一行,最后一行之前的行末需要加\
- 宏的值后面出现的注释不会被当作宏的值的一部分
没有值的宏
- #define_DEBUG
- 这里宏是用于条件编译的,后面有其他的编译预处理指令来检查这个宏是否已经被定义过了
预定义的宏
- LINE表达代码所在的行号
- FILE表达代码所在的文件名
- DATE编译时候的日期
- TIME编译时候的时间
- STDC
12.2-2带参数的宏
像函数的宏
- #define cube(x)((x)(x)(x))
- 宏可以带参数
#include <stdio.h> #define cube(x)((x)*(x)*(x)) int main(int argc,char const *argv[]) { printf("%d\n",cube(5)); //cube(5)会被替换成cube((5)*(5)*(5)); return 0; }
错误定义的宏
- #define RADTODEG(x)(x*57.29578)
- #define RADTODEG(x)(x)*57.29578
带参数的宏的原则
- 一切都要括号
- 整个值要括号
- 参数出现的每个地方都要括号
- #define RADTODEG(x)((x)*57.29578)
带参数的宏
- 可以带多个参数
- #define MIN(a,b)((a)>(b)?(b):(a))
- 也可以组合(嵌套)使用其他宏
- 定义宏的时候后面千万不要加分号
- 在大型程序的代码中使用非常普遍
- 可以非常复杂,如"产生"函数
- 在#和##这两个运算符的帮助下
- 存在中西方文化差异
- 部分宏会被inline函数替代
宏的缺陷
没有可以去检查宏有没有问题的机制
其他预编译
- 条件编译
- error
- ...
12.3-1多个源代码文件
多个.c文件
- main()里的代码太长了适合分成几个函数
- 一个源代码文件太长了适合分成几个文件
- 两个独立的源代码文件不能编译形成可执行的程序
项目
因为看的是翁恺老师2014年的版本(比后来的课程内容多不少,后来的课程缩水了),所以这部分已经有更好的就进行省略了
编译单元
- 一个.c文件是一个编译单元
- 编译器每次编译只处理一个编译单元
12.3-2头文件
- 把函数原型放到一个头文件(以.h结尾)中,在需要调用这个函数的源代码文件(c文件)中#include这个头文件,就能让编译器在编译的时候知道函数的原型
#include
- #include是一个编译预处理指令,和宏一样,在编译之前就处理了
- 它把那个文件的全部文本内容原封不动地插入到它所在的地方
- 所以也不是一定要在.c文件的最前面#include
“”还是<>
- #include有两种形式来指出要插入的文件
- “”要求编译器首先在当前目录(.c文件所在的目录)寻找这个文件,如果没有,到编译器指定的目录去找,例如“MAX.h”啥的自己设定的那些
- <>让编译器只在指定的目录去找,例如
- 编译器自己知道自己的标准库的头文件在哪里
- 环境变量和编译器命令行参数也可以指定寻找头文件的目录
#include的误区
- #include不是用来引入库的
- stdio.h里只有printf的原型,printf的代码在另外的地方,某个.lib(Windows)或.a(Unix)中
- 现在的C语言编译器默认会引入所有的标准库
- #include只是为了让编译器指定printf函数的原型,保证年调用时给出的参数值是正确的类型
头文件
- 在使用和定义这个函数的地方都应该%include这个头文件
- 一般的做法就是任何.c都有对应的同名的.h,把所有对外公开的函数的原型和全局变量的声明都放进去
- 全局变量是可以在多个.c之间共享的
不对外公开的函数
- 在函数前面加上static就使得它成为只能在所在的编译单元中被使用的函数
- 在全局变量前面加上static就使得它成为只能在所在的编译单元中被使用的全局变量了
12.3-3声明
变量的声明
- int i;是变量的定义
- extern int i;是变量的声明
声明和定义
- 声明是不产生代码的东西
- 函数原型
- 变量声明
- 结构声明
- 宏声明
- 枚举声明
- 类型声明
- inline函数
- 定义是产生代码的东西
头文件
- 只有声明可以被放在头文件中
- 是规则不是法律
- 否则会造成一个项目中多个编译单元里有重名的实体
- *某些编译器允许几个编译单元中存在同名的函数,或者用week修饰符来强调这种存在
重复声明
- 同一个编译单元里,同名的结构不能被重复声明
- 如果你的头文件里有结构的声明,很难这个头文件不会在一个编译单元里被#include多次所以需要"标准头文件"
标准头文件结构
- 运用条件编译和宏,保证这个头文件在一个编译单元中只会被#include一次
- #pragma once也能起到相同的作用,但是不是所有的编译器都支持
#ifndef __LIST_HEAD__ #define __LIST_HEAD__ #include"node.h" typedef struct _list{ Node*head; Node*tail; }List; #endif
第13周:文件
13.1-1格式化输入输出格式
%[flags][width][.prec][hIL]type
Flag |
含义 |
- |
左对齐 |
+ |
在前面放+或- |
(space) |
正数留空 |
0 |
0填充 |
#include<stdio.h> int main(int argc,char const *argv[]) { printf("%9d\n",123);//需要占9个字符的空间,123前还有6个空格 printf("%-9d\n",123);//一样需要占9个字符,123后面还有6个空格 printf("%+9d\n",123);//在123前面多上一个+,是可以0时跟左对齐同时进行的 printf("%09d\n",123);//空格地方变成0,000000123 return 0; }
width或prec |
含义 |
number |
最小字符数 |
* |
下一个参数是字符数 |
.number |
小数点后的位数 |
.* |
下一个参数是小数点后的位数 |
#include<stdio.h> int main(int argc,char const *argv[]) { printf("%9.2f\n",123.0);//占据9个字符且其中有两位小数 printf("%*d\n",6,123);//让格式更灵活,6这个数是会替换到*号上的,具体效果就是占据6个字符 return 0; }
类型修饰 |
含义 |
hh |
单个字节 |
h |
short |
l |
long |
ll |
long long |
L |
long double |
#include<stdio.h> int main(int argc,char const *argv[]) { printf("%hhd\n",12345);//只能输入单个字符,输出多个报错 printf("%hhd\n",(char)12345);//但可以强制类型转换,可以得到57的结果 printf("%9.2f\n",123.0); return 0; }
type |
用于 |
type |
用于 |
i或者d |
int |
g |
float |
u |
unsigned int |
G |
float |
o |
八进制 |
a或者A |
十六进制浮点 |
x |
十六进制 |
c |
char |
X |
字母大写的十六进制 |
s |
字符串 |
f或者F |
float,6 |
p |
指针 |
e或者E |
指数 |
n |
输入/写出的个数 |
#include<stdio.h> int main(int argc,char const *argv[]) { int num; printf("hhd%n\n",(char)12345,&num); printf(num);//以上的意思是,截至到%n为止前面有几位字符,会将多少位字符传递给指针&num的地址 //然后在下面num就会输出2,因为上面(char)12345的值输出是57,是2位数 return 0; }
scanf:%[flag]type
flag |
含义 |
flag |
含义 |
* |
跳过 |
l |
long,double |
数字 |
最大字符数 |
ll |
long long |
hh |
char |
L |
long double |
h |
short |
type |
用于 |
type |
用于 |
d |
int |
s |
字符串(单词) |
i |
整数,可能为十六进制或八进制可以将读入的八进制和十六进制格式的数值转化为十进制 |
[...] |
所允许的字符 |
u |
unsigned int |
p |
指针 |
o |
八进制 |
||
x |
十六进制 |
||
a,e,f,g |
float |
||
c |
char |
printf和scanf的返回值
- 读入的项目数
- 输出的字符数
- 在要求严格的程序中,应该判断每次调用scanf或printf的返回值,从而了解程序运行中是否存在问题
13.1-3文件输入输出
文件输入输出
- 用>和<做重定向
FILE
- FILE*fopen(const charrestrict path,const char*restrict mode);
- int fcolose(FILE*stream);
- fscanf(FILE,...)
- fprintf(FILE*,...)
- fprintf(FILE,...)
打开文件的标准代码
FILE*fp = fopen(“file(文件名)”,“r”); if(fp){ fscanf(fp,...); fcolose(fp); }else{ ... } ----------------------------------------------------------------- #include<stdio.h> int main(int argc,char const *argv[]) { FILE *fp = fopen("12.in","r"); if( fp ){ int num; fscanf(fp,"%d",&num); printf("%d\n",num); fclose(fp); }else{ printf("无法打开文件\n"); } return 0; }
fopen
r |
打开只读 |
r+ |
打开读写,从文件头开始 |
w |
打开只写。如果不存在则新建,如果存在则清空 |
w+ |
打开读写。如果不存在则新建,如果存在则清空 |
a |
打开追加。如果不存在则新建,如果存在则从文件尾开始 |
..x |
只新建,如果文件已存在则不能打开(防止对文件造成破坏) |
13.1-3二进制文件
- 其实所有的文件最终都是二进制的
- 文件无非是用最简单的方式可以读写的文件
- more、tail
- cat
- vi
- 而二进制文件是需要专门的程序来读写的文件
- 文本文件的输入输出是格式化,可能经过转码
文本文件 VS 二进制文件
- Unix喜欢用文本文件来做数据存储和程序配置
- 交互式终端的出现使得人们夏欢用文本和计算机“talk”
- Unix的shell提供了一些读写文本的小程序
- windows喜欢二进制文件
- DOS是草根文化,并不继承和熟悉Unix文化
- PC刚开始的时候能力有限,DOS的能力更有限,二进制进行输入输出更接近底层
优劣:
- 文本的优势是方便人类读写,而且跨平台
- 文本的缺点是程序输入输出要经过格式化,开销大
- 二进制的缺点是人类读写困难,而且不跨平台
- int的大小不一致,大小端的问题
- 二进制的优点是程序读写快
程序为什么要文件
- 配置
- Unix用文本,Windows用注册表
- 数据
- 稍微有点量的数据都放数据库了
- 媒体
- 这个只能是二进制的
- 现实是,程序通过第三方库来读写文件,很少直接读写二进制文件了
二进制读写(可跳过,底层)
- size_t fread(voidrestrict ptr,size_t size,size_t nitems,FILErestrict stream);
- size_t fwrite(const voidrestruct ptr,size_t size,size_t nitems,FILErestrict stream);
- 注意FILE指针是最后一个参数
- 返回的是成功读写的字节数
为什么nitems
- 因为二进制文件的读写一般都是通过对一个结构变量的操作来进行的
- 于是nitem就是用于说明这次读写几个结构变量!
在文件中定位
- long ftell(FILE*stream);
- int fseek(FILE*stream,long offset,int whence);
- SEEK_SET:从头开始
- SEEK_CUR:从当前位置开始
- SEEK_END:从尾开始(倒过来)
#include<stdio.h> #include"student.h" void read(FILE*fp,int index); int main(int grac,char const grav[]) { FILE*fp = fopen("student.data","r"); if( fp ) { fseek(fp,0L,SEEK_END); long size = ftell(fp); int number = size/sizeof(Student); int index = 0; printf("有%d个数据,你要看第几个:",number); scanf("%d",&index); read(fp,index-1); fcolose(fp); } return 0; } void read(FILE*fp,int index) { fseek(fp,index*sizeof(Student),SEEK_SET); Student stu; if( fread(&stu,sizeof(Student),1,fp) == 1){ printf("第%d个学生:",index+1); printf("\t姓名:%s\n",stu.name); printf("\t性别:"); switch ( stu.gender ){ case 0:printf("男\n");break; case 1:printf("女\n");break; case 1:printf("其他\n");break; } printf("\t年龄:%d\n",stu.age); } }
可移值性
- 这样的二进制文件不具有可移植性
- 在int为32位的机器上写成的数据文件无法直接在int为64的机器上正确读出
- 解决方法之一就是放弃使用int,而是使用typedef具有明确大小的类型
- 更好的方案是用文本
13.2*位运算
13.2-1按位运算
- C有这些按位运算的运算符:其实就是把整数的东西当做二进制来进行运算
·& |
按位的与 |
·| |
按位的或 |
·~ |
按位取反 |
·^ |
按位的异或 |
·<< |
左移 |
·>> |
右移 |
按位与&
- 其实就是两组二进制的数,对应的数字必须都为1,新形成的数对应的数才会是1,否则就是0
- F:1111 E:1110,FE的作用就是使得跟他对应形成新的数最低位为0
按位或|
- 也是两组二进制的数,对应的数字必须都为0,新形成的数对应的数才会是0,否则就是1。跟上面那个相反
按位取反~
#include<stdio.h> int main(int argc,char const *argv[]) { unsigned char c = 0xAA; printf("% c=%hhx\n",c);//aa printf("%~c=%hhx\n",(char)~c);//按位取反 55 printf("%-c=%hhx\n",(char)-c);//补码 56 }
逻辑运算vs按位运算
- 对于逻辑运算,它只看到两个值:0和1
- 可以认为逻辑运算相当于把所有非0值都变成1,然后做按位运算
- 5&4——>4而5&&4——>1&1——>1
- 5|4——>5而5||4——>1|1——>1
- ~4——>3而!4——>!1——>0
按位异或^
- 两组二进制,上下位数值对应相等为0,上下不相等为1
- 做两次相同的异或运算数值就翻回去了
移位运算
左移<<
- i << j
- i中所有的位向左移动j个位置,而右边填入0
- 所有小于int的类型,移位以int的方式来做,结果是int
- x <<= 1 等价于x*=2
- x <<= n 等价于x*=2的n次方
#include<stdio.h> int main(int argc,char const *argv[]) { unsigned char c = 0xA5; printf(" c=%d\n",c);//165 printf("c<<=%d\n",c<<2);//660 return 0; }
右移>>
- i >> j
- 所有小于int的类型,移位以int的方式来做,结果是int
- 对于unsigned的类型,左边填入0
- 对于signed的类型,左边填入原来的最高位(保持符号不变)
- x >>= 1 等价于x/=2
- x >>= n 等价于x/=2的n次方
#include<stdio.h> int main(int argc,char const *argv[]) { int a = 0x80000000; unsigned int b = 0x80000000; printf("a=%d\n",a);//-2147483648 printf("b=%u\n",b);//2147483648 printf("a>>1=%d\n",a>>1);//-1073741824 printf("b>>1=%u\n",b>>1);//1073741824 return 0; }
no zuo no die
- 移位的位数不要用负数,这是没有定义的行为
- x<<-2 //!!NO!!
13.2-3位运算例子
输出一个数的二进制
#include<stdio.h> int main(int argc,char const *argv[]) { int number; scanf("%d",&number); number = 0x55555555;//输出01010101...,其实就是16个01(32个值) unsigned mask = 1u<<31;//int被省略但是其实是有生效的 for(; mask ; mask >>=1 ){ printf("%d",number & mask?1:0); } printf("\n"); return 0; }
13.2-4位段
- 把一个int的若干位组合成一个结构
struct{ unsigned int leading : 3;//冒号后面的数字表示占几个比特 unsigned int FLAG1:1; unsigned int FLAG2:1; int trailing:11; };
- 可以直接用位段的成员名称来访问
- 比移位、与、或还方便
- 编译器会安排其中的位的排列,不具有可移植性
- 当所需的位超过一个int时会采用多个int
第十四周:*链表
14.1-1*可变数组
the Interface
- Array array_create(int init_size);创建数组
- void array_free(Array*a);回收数组
- int array_size(const Array*a);告诉我们数组里面现在有几个单元可以使用
- intarray_at(Arraya,int index);访问数组某个单元
- void array_inflate(Array*a,int more_size);让数组
#ifndef _ARRAY_H_ #define _ARRAY_H_ typedef struct { int *array; int size; }Array; Array array_create(int init_size); void array_free(Array *a); int array_size(const Array*a); 4. int*array_at(Array*a,int index); 5. void array_inflate(Array*a,int more_size); #endif #include "array.h" Array array_create(int init_size) { Array a;//首先是数组的创建,需要用到动态内存分配,结合所需要的size,来创建一个数组 a.size = init_size; a.array = (int*)malloc(sizeof(int)*a.size); return a; } void array_free(Array *a)//再然后就是内存的释放、得到数组的大小 { free(a->array); a->array = NULL; a->size = 0; } int array_size(const Array*a); int*array_at(Array*a,int index); void array_inflate(Array*a,int more_size); int main(int argc,char const *argv[]) { Array a = array_create(100); return 0; }
14.1-2可变数组的数据访问
//从上述第16行内容延续,这里需要多加两个标准头文件 #include<stdio.h> #include<stdlib.h> //3-6行的代码叫做封装,能够将里面的内容保护起来,这样别人就不知道你里面什么样子的了,保持神秘感哈哈 int array_size(const Array*a) { return a->size; } int*array_at(Array*a,int index)//在然后是进行该数组的访问和修改 //这里面需要注意的是array_at的返回需要是一个指针,如此便可以做到修改 { return &(a->array[index]); } int*array_get(const Array*a,int index); { return a->array[index]; } void array_set(Array *a,int index, int value) { a->array[index] = value; } void array_inflate(Array*a,int more_size); int main(int argc,char const *argv[]) { Array a = array_create(100); printf("%d\n",array_size(&a)); //printf("%d\n",a.size);十七行跟十六行一个意思 *array_at(&a,0) = 10;//将一个值写到数组里面 printf("%d\n",*array_at(&a,0)); array_free(&a); return 0; }
14.1-3可变数组的自动增长
//从上述24行延续 void array_inflate(Array*a,int more_size) { int*p = (int*)malloc(sizeof(int)(a->size + more_size)); int i; for( i = 0; i < a->size; i++ ) { p[i] = a->size[i]; }//可以换成标准库的函数mencpy,效率更高 free(a->array); a->array = p; a->size += more_size; }//核心代码 int main(int argc,char const *argv[]) { Array a = array_create(100); printf("%d\n",array_size(&a)); //printf("%d\n",a.size);十七行跟十六行一个意思 *array_at(&a,0) = 10;//将一个值写到数组里面 printf("%d\n",*array_at(&a,0)); int number; int cnt = 0; while(number != -1 ){ scanf("%d",&number); if( number != -1 ){ *array_at(&a,cnt++) = number; //scanf("%d",array_at(&a,cnt++)); }//无限的读入整数,让自己不断的自己增长 array_free(&a); return 0; } //第九行的内容改动 int*array_at(Array*a,int index)//在然后是进行该数组的访问和修改 //这里面需要注意的是array_at的返回需要是一个指针,如此便可以做到修改 { if( index >= a->size ){ array_inflate(a,(index/BLOCK_SIZE+1)-a->size+1);//这里有点乱,后续要来修正 } return &(a->array[index]); }
14.2-1可变数组的缺陷
- 申请使用内存,当我们需要更大的内存而之前的不需要了之后,之前的就会被废弃掉,在内存受限的情况下(比如单片机)就会导致内存明明还有,但是却已经申请不了更大的内存了(浪费内存空间)
- 效率极低
14.2-2链表
- 这是为百度进来补充的图,翁恺的课程是没有静态的图,用动态进行演示的
#include"node.h" #include<stdio.h> #include<stdlib.h> //typedef struct _node{ // int value; // struct _node *next; //}Node; int main(int argc,char const argv[]) { Node*head = NULL; int number; do{ scanf("%d",&number); if( number != -1 ) { //add to linked-list Node*p = (Node*)malloc(size(Node)); p->next = NULL; //find the last Node*last = head; if( last ){ while (last ->next){ last = last->next; } //attach last->next = p; }else{ head = p; } } }while( number != -1 ); return 0; }
14.2-3链表的函数
#include"node.h" #include<stdio.h> #include<stdlib.h> //typedef struct _node{ // int value; // struct _node *next; //}Node; void add(Node*head,int number); typedef struct _list{ Node*head; Node*tail; }List; int main(int argc,char const argv[]) { Node*head = NULL; List list; int number; list.head = list.tail = NULL; do{ scanf("%d",&number); if( number != -1 ){ add(&list,number); } }while( number != -1); return 0; } void add(List*pList,int number) { //add to linked-list Node*p = (Node*)malloc(size(Node)); p->value = number; p->next = NULL; //find the last Node*last = pList->head; if( last ){ while (last ->next){ last = last->next; } //attach last->next = p; }else{ head = p; } return head; }
14.2-4链表的搜索
//从上述16行延续 void print(List *pList)//函数原型置顶,另外说明我没有把这个置顶到其他代码块里面 void add(List*pList,int number) int main(int argc,char const argv[]) { Node*head = NULL; List list; int number; list.head = list.tail = NULL; do{ scanf("%d",&number); if( number != -1 ){ add(&list,number); } }while( number != -1); printf(&list); scanf("%d",&number); Node*p; int isFound = 0; for( p = list.head; p; p = p->next){ if( p->value == number ){ printf("找到了\n"); isFound = 1; break; } } if ( !isFound ){ printf("没找到\n"); } //Node*p; //for( p=list.head; p; p = p->next){ // printf("%d\t",p->value); //}//遍历,把链表每个节点的值打出来 //printf("\n"); 这些内容转移到下面了,并且有了一部分的改动 return 0; } void add(List*pList,int number) { //add to linked-list Node*p = (Node*)malloc(size(Node)); p->value = number; p->next = NULL; //find the last Node*last = pList->head; if( last ){ while (last ->next){ last = last->next; } //attach last->next = p; }else{ head = p; } } void print(List *pList){ Node*p; for( p=list->head; p; p = p->next){ printf("%d\t",p->value); }//遍历,把链表每个节点的值打出来 printf("\n"); }
14.2-5链表的删除
//从上述第5行开始 int main(int argc,char const argv[]) { Node*head = NULL; List list; int number; list.head = list.tail = NULL; do{ scanf("%d",&number); if( number != -1 ){ add(&list,number); } }while( number != -1); printf(&list); scanf("%d",&number); Node*p; int isFound = 0; for( p = NULL; p=list.head; q = p,p = p->next){ if( p->value == number ){ //需要考虑到边界效应,q没有进行限制需要进行限制 if(q){ q->next = p->next; } else { list.head = p->next; } //q->next = p->next; free(p); break; } } //Node*p; //for( p=list.head; p; p = p->next){ // printf("%d\t",p->value); //}//遍历,把链表每个节点的值打出来 //printf("\n"); 这些内容转移到下面了,并且有了一部分的改动 return 0; }
14.2-6链表的清除
如何整个链表都清除掉
//从上述19行开始 for( p = NULL; p=list.head; q = p,p = p->next){ if( p->value == number ){ //需要考虑到边界效应,q没有进行限制需要进行限制 if(q){ q->next = p->next; } else { list.head = p->next; } //q->next = p->next; free(p); break; } } for( p=head; p; p=q ){ q = p->next; free(p); }//链表这样就删除掉了 return 0; }