第7章 函数、模块和宏定义
7.1 预处理指令
预处理指令 都是#开头
包括
1. 文件包含
- #include<库函数> 大于小于号 表示预处理器要从标准库路径开始搜索这个头文件
- #include”自己的头文件” 从用户的工作路径开始搜索
2. 条件编译 #if #ifndef #undef #endif
- 使程序支持跨平台编译
- 防止头文件的重复包含
3. 宏替换 有两种类型
- object-like
- function-like
- #define NUM 10 在预处理阶段将源码中出现的NUM都换为10。
- 我们在写代码的时候,会使用NUM去代替10,因为在编译前的预处理阶段会把它换为10
#define PI 3.14159 //object-like 类型的宏替换
#define PERIMETER(r) (2*PI*(r)) //function-like 类型的宏替换 (需要加很多括号)
- 当#define语句很长的时候,你可以把它断成多行,不过除了最后一行外,其他每行的末尾都需要一个反斜杠‘\’,表示与上一行是接续的关系。
- 要尽量避免使用#define。
7.2 main函数
- 为什么需要main函数?
- 程序(小弟)是跑在操作系统(boss)里面的,
- 运行程序时,操作系统需要把电脑的部分控制权交给你的程序
- 运行完后,操作系统需要回收这部分控制权。
- 问题转化为,把控制权交给哪个函数呢?
- 为方便,C规定,那部分控制权交给程序中的main函数
- 程序(小弟)是跑在操作系统(boss)里面的,
- main函数的三种定义方式
int main();
int main(void);
int main(int argc, char* argv[]);//或者int main(int argc, char **argv);
- argc代表的是传入参数的个数
- argv为一个指针数组,其中每一个指针指向了传入参数的具体的值。
- argv[0]指向的是可执行程序的名字
7.3 命令行解析
Windows平台下的命令行解析
main(int argc, char* argv[]) {
int argi, int Nflag = 0;
char *xval = NULL;
for(argi = 1; argi < argc && argv[argi][0] == '-'; argi++){
char *p;
for(p = &argv[argi][1]; *p != '\0' ; p++ ){
switch(*p){
case 'N':
Nflag = 1;
printf("-N seen\n");
break;
case 'x':
xval = argv[++argi];
printf("-x seen (\"%s\")\n",xval);
break;
default:
fprintf(stderr,"unkonwn option -%c\n",*p);
}
}
}
}
7.4 static 和 const
const和指针变量配合使用的时候,易混淆
const char* p;
char* const p;
区分方法:
- 画一条垂直线穿过上述指针声明中的 星号(*)位置,
- const在线的左边 指针指向的数据为常量
- const在线的右边 指针本身为常量
static有两个含义:
- 静态
- 隐藏(其英文名并没有将此含义表达出来)
- static定义的函数和变量只在此模块(文件)中有效
定义一个允许被其它.c文件使用的全局变量
- 在任意一个.c文件中定义出这个全局变量(不能加static修饰符)
- 其它变量使用这个全局变量前,需要使用 extern 修饰符对这个变量进行声明。
7.5 编译和链接
- 编译器 只要确认每个标识符的类型,做到能认识它即可。
- 链接器 的功能是在内存中定位这个标识符,它需要唯一地找到它
能通过编译,但是不能运行,因为没有定义。
#include <stdio.h>
int foo(int i);
int main(){
foo(1);
return 0;
}
7.6 声明和定义
- 对声明的变量或函数编译器并不申请内存,只是保留一个引用
- 当 执行链接 的时候,把正确的内存地址连接到那个引用上。
对定义来说,编译器要做实际的事,无论是变量还是函数,定义时都要分配一段内存,以便把定义的变量和函数装入内存中去。
extern只用于声明变量,不用于声明函数。事实上也可以在函数前面加extern,但是与没有使用extern是等价的
- 不分配内存的声明,可以声明很多次,也可以放到头文件,也可以放到.c文件中
- 分配内存的声明,只能声明1次,一定要放到.c文件中
7.8 头文件的重复包含
- 头文件中只应该包含那些不申请内存的声明语句
- #ifndef #endif
- 宏定义
- struct 、union 、enum等类型的typedef类型定义
- 全局变量 及 不申请内存的声明
- 变量前面带extern
- 函数无具体定义
- 避免重复包含头文件的本意,并非是为了避免重复定义,而只是为了提高编译器的效率
7.9 多文件项目
在一个项目中。最好把所有的声明放到一个 统一的global.h文件中。
- 头文件范例
#ifndef GLOBAL_H
#define GLOBAL_H
#include "other.h" //其他头文件
typedef long WORD //类型定义
typedef struct student{
int num;
char name[20];
int age;
} STUD; //类型typedef,但是没有定义一个变量
int f(int i); //函数的声明
extern int foo;// 全局变量的声明
#endif
7.10 C和C++混编项目
extern “C”
C++编译文件时,为了支持函数重载,编译器执行一项name mangle 的过程。
- name mangle 编译的时候,会改变函数的名字,链接的时候会出错。
- 为了支持.c 和 .cpp混编,需要使用 extern “C”关键字
用于.c和.cpp文件的通用的 头文件格式
#ifdef __cplusplus
extern "C"{
#endif
sum(int,int);
#ifndef __cplusplus
}
#endif
遵守两个基本原则:
1. C语言的源文件 C++的.cpp文件没必要使用 extern “C” 关键字
2. C或C++混编的项目中,C++要用到C中定义的函数 sum,或者 C要用到C++中定义的 sum,都是需要上面描述的头文件对函数sum进行声明
- extern “C”只对函数有作用,对C++中的类没有任何作用
第8章 库函数
8.1 数学相关
- 初等函数,例如
- 绝对值abs和fabs
- 幂函数 exp和pow
- 对数函数 log 和 log10
- 开平方函数sqrt
- 上下界函数 ceil floor
- 三角函数
- 反三角函数
…
需要使用某个数学功能的时候,先去api找一下
注意:
- 不同的函数可能要求不同的头文件
- 使用数学函数时 大部分需要include math.h
- 特例:abs 函数 被包含在 stdlib.h 中
- 三角函数 和 反三角函数,都是以弧度为参数,而不是角度
- 求45°的sin值,可以写成 sin(45 / pi *180);
8.2 字符串相关
一些字符串判断函数,声明于 ctype.h中
- isalnum
- isalpha
- islower
- isupper
- isspace
处理字符串相关的函数主要包括 String manipulation ,String examination 。
- String manipulation 字符操作
- strcpy
- strcat
- …
- strcpy和strcat会改变传入的字符串的内容
字符串函数发生溢出
#include<string.h>
int main(){
char src[8] = {"world"};
char des[10] = {"hello"};
strcat(des,src);//溢出
}
String examination 字符检查
- strlen
- strcmp
…
- 为了解决字符串函数的溢出问题,C引入了strncpy 和 strncat 两个n族函数, n 意思为指定追加或者复制的个数。
- 标准字符串中并没有substr函数
- 可以自己简单用strncat(des, src+pos ,len);来实现
- strtok 函数用于将一个字符串根据分隔符分割
- 这个函数会把字符串中的每个delimiters都替换成 ‘\0’
8.3 字符和数字相互转换
中间是to:
- atof 转换为双精度浮点数
- atoi 转换为整数
- atol 转换为long类型数
printf的“大表弟”,sprintf() 目测s是string的意思或者是字符数组
- sprintf()把内容输出到第一个参数指定的字符串str中,其余功能都是一样的。
- Java中字符串可以直接加, 不需要这么麻烦
数字换成字符串
int main(){
float f = 1234.56789f;
int i = 123;
char str[20];
sprintf(str,"%d",i);
sprintf(str,"%10.3f",f);
printf("%s\n",str);
}
- sscanf,scanf的大表弟,从一个字符串中按格式提取出对应的子字符串
- 对于不确定输入的格式,先调用fgets()函数读入一行,并把读入的内容存入字符数组中,然后再利用sscanf函数。。。
char sentence[] = "sep. 12 1993";
char month[20];
int day,year;
sscanf(sentence,"%s %d %d",month,&day,&year);
printf("month-> %s\n",month);
printf("day-> %d\n",day);
printf("year-> %d\n",year);
8.4 时间函数
抓住 一个中心,两个基本点。
- 一个中心 指 time_t time(time_t* timer)函数
- 两个基本点 分别为
- 系统时间 time_t
- 通过typedef来定义的,实际上是 long
- 日历时间(或分解时间)struct tm。
两种利用time函数取得系统时间的方法,分别为
1. 利用函数参数,
time_t now;
time(&now);
2. 利用函数的返回值。
now = time(NULL)
- char *ctime( const time_t *time );
- 函数转换参数time为本地时间格式:
- size_t strftime( char *str, size_t maxsize, const char *fmt, struct tm *time );
- 函数按照参数fmt所设定格式将time类型的参数格式化为日期时间信息,然后存储在字符串str中(至多maxsize 个字符)。
#include <stdio.h>
#include <time.h>
int main(void) {
char a[100];
time_t now;
time(&now);//或者 now = time(NULL) ;
printf("%s",ctime(&now));
printf("%d\n",time(NULL) + 60*60*24*7);
strftime(a,100,"%m-%d-%Y",localtime(&now));
printf("%s",a);
}
8.5 随机数
随机数函数的实现
unsigned long int next = 1;
//伪随机数
int rand(void){
next = next * 1103515245 + 12345;
return (unsigned int)(next/65535) % 32768;
}
//通过给定的种子生成“真正的”随机数
void srand(unsigned int seed){
next = seed;
}
每次调用前先调用srand(time(NULL));将随机数种子赋值为当前time函数运行时候的系统时间。以便产生真正的随机数。
int main(){
srand(time(NULL));
int i;
for(int i = 0; i<5; i++){
printf("%d\t",rand());
}
}
模拟扑克洗牌。使用“交换”的技巧模拟一副扑克洗牌的过程。(牌即为长度为54的)
for(int i = 0; i < 54; i++){
//生成随机数
int c = rand()%54;
while(i+c > 54){
c = rand()%54;
}
//将生成的随机数作为位移,交换
int t = a[i];
a[i] = a[i+c];
a[i+c] = t;
}
生成 0到1之间的随机数
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(){
srand(time(NULL));
int i;
int rand_int;
float ran_float;
for(int i = 0; i<100; i++){
rand_int = rand();
ran_float = (float)rand_int/RAND_MAX;
printf("%f\n",ran_float);
}
}
8.6 系统相关函数
- exit函数用于终止你当前程序的运行,其优点如下
- 可以在程序中的任何一个地方调用以终止当前的程序
- 与return 不同,当exit 函数被调用时,它会执行一些额外的操作,刷新所有用于流的缓冲区,关闭打开的文件。
- 可以通过atexit函数来注册一些退出函数,当调用exit函数时,这些退出函数也会被执行
- system函数把传入的字符串参数传送给宿主操作系统,由数组操作系统的shell来执行
- signal 函数就是在程序收到指定信号的时候
- 信号这个称呼,基本上来源于UNIX系统。
- Dos系统中称之为中断
- Window系统上称之为消息。
- 信号这个称呼,基本上来源于UNIX系统。
第9章 数组与函数
数组的初始化与赋值
eg:
int ary[3];
//高效方法
memset(ary,0,sizeof(ary));
for(int i = 0; i< 3; i++){
printf("%d\n",ary[i]);
}
//复制的高效方法
int b[3];
memcpy(b,ary,sizeof(ary));
9.3 数组与函数
- 传递数组的时候,遵循的是引用传递
- 向一个函数传递一个数组,实质是把数组的首地址传入。
C语言的函数不支持用return返回一个数组
- -
9.4 数组越界
下标从零开始
遍历时使用半开半闭的区间访问数组
9.5 debug和release的区别
VS的每一个项目,都可以分别建立debug版本和release版本。release版本,通常不进行边界检查。
- debug版下,当定义一块数组的时候,它会在数组的后面加两个元素,一旦入侵了,这两个元素会引发一个异常(越界三个位以上也还是检查不出来的),无论是报警还是不报警,都发生在debug下。
9.6 二维数组的定义和初始化
- 每一个一维数组都是有数据类型的。
- 二维数组在内部只是被当做 一维数组型一维数组。二维数组中,每一个元素都是一个一维数组
- 实际操作一维数组,推荐使用a[m][n]这种方式,而不推荐a[m*N+n]这种方式,虽然两者对于编译器而言是完全等价的
- C语言的 二维数组必须指明列数。
- java的必须指明第一维的长度
- 二维数组在内存中也是线性连续保存的(物理结构),而不是保存为行列结构(逻辑结构)。
第10章 指针
10.1 指针的基本知识和定义
- 硬盘保存数据是基于物理的磁性,访问靠机械运动,所以速度比较慢
- 内存保存数据是基于电信号,速度比较快,但是所有数据不能长久保存,掉电即失。
内存的基本单位是字节(byte),每一个字节都有一个独一无二的地址(它的编号)。
- 内存地址都是16进制表示的。
- 32位机 地址用4个字节表示,最多支持2^32字节内存,也就是4G内存。
- 地址从0x00000000到0xFFFFFFFFF。
- 64位机 的地址占8个字节表示,最多支持2^64字节内存,也就是16G
10.2 指针类型和指针指向的类型
- 任何指针类型变量都有两个属性
- 本身保存的地址
- 指向变量的类型(所占的长度)
- 指针本身只存储地址,编译器根据指针指向的变量的类型从指针保存的地址向后寻址,指针指向的变量类型不同则寻址范围也不同。
10.3 指针定义中常见的错误
10.3.1野指针
避免方法,定义一个指针变量的时候带等号,暂时不确定就让它指向NULL。
10.4 void 和 NULL
10.4.1 void
- void 一般用在两种情况下。
- 函数没有 返回值 或 参数
- 如果不指定为void 有的编译器默认返回的int类型,造成潜在的移植错误。
- 声明一个void类型的指针,只保存一个地址,不包含指向变量的类型信息
- 指针内部都包含一个地址信息和一个类型(长度)信息。
- void指针, 类型(其实就是长度)信息丢失了,因此对void的指针进行算术运算和进行取值操作都是不允许的。
- 函数没有 返回值 或 参数
#include <stdio.h>
int main(){
int i = 10;
void *vp = NULL;
int *ip = &i;
int k;
vp = ip;
// vp++;//错误
// k = *vp;//错误
ip = (int*)vp;
ip++; //ok
k = *ip; // ok
}
10.4.2 NULL
NULL用来描述指针的值
10.5 指针和数组的关系初探
- a[i] 用在一个表达式中的时候,编译器自动地将其转换为指针偏移量*(a+i)的形式。a[i]这种书写方式,只是方便一些程序员。
- 不过叫建议使用 a[i]的方式,因为两种书写生成的机器码都是一样的,运行效率也一样,只是第一种需要编译时会用多一点点时间
- 函数声明func(int a[]),编译器自动将其转换为func(int *a)
- 有趣的事实:数组的变量和下标可以互换 a[i] 和 i[a] 都会提通过编译,而且运行结果一样
- 5[“abcdef”] 等价于 f
- 编译器并不是一看到数组声明 例如: int a[5];就把它转换为指针常量。
- sizeof或者取地址运算符&作用于数组变量a的时候,a并不转化为指针常量。这也是 sizeof(a)会返回整个数组的长度而不是一个指针的长度的原因。
10.6 指针和数组关系的再探
- a——array
- p——pointer
指针 | 数组 | |
---|---|---|
指针 | int **pp | int *pa[5] |
数组 | 数组指针 int (*ap)[5] | int aa[2][3] |
10.6.2 指针型指针和指针数组
使用指针型数组的一个优点在于对多个字符串排序,排序的过程并不需要移动真实的字符串,而只需要改变指针的指向
#include <stdio.h>
#include <string.h>
//字符串排序指针数组实现
int main(){
char *temp = NULL;
char *ptr[] = {"Pascal","Basic","Fortran","Java","Visual C"};
for(int i = 0; i< 5; i++){
for(int j = i+1; j < 5; j++){
if(strcmp(ptr[i],ptr[j])>0){
temp = ptr[i];
ptr[i] = ptr[j];
ptr[j] = temp;
}
}
}
for(int i = 0; i<5; i++){
printf("%s\n",ptr[i]);
}
}
换成二维数组的方式实现字符串排序,效率要明显低很多,因为涉及到利用strcpy函数来复制和移动整个字符串
#include <stdio.h>
#include <string.h>
//字符串排序二维数组实现
int main(){
char tmp[10];
char ptr[][10] = {"Pascal","Basic","Fortran","Java","Visual C"};
for(int i = 0; i< 5; i++){
for(int j = i+1; j<5; j++){
if(strcmp(ptr[i],ptr[j])>0){
strcpy(tmp,ptr[i]);
strcpy(ptr[i],ptr[j]);
strcpy(ptr[j],tmp);
}
}
}
for(int i = 0; i<5; i++){
printf("%s\n",ptr[i]);
}
}
10.6.3 数组指针和二维数组
如何定义一个指针数组?
- 由于[] 的优先级 比 * 的 要高 int p[3] 等价于 int (p[3])这是一个指针型数组;
- 故应该加括号 int (*p)[3];
有了 int (*p)[3] = ary[2][3]; 二维数组也可以用指针来访问了
- p即 &ary[0]
- 代表二维数组的首地址,第0行的地址
- 一维int数组型变量 int[] 的地址
- p+i即 &ary[i]
- 代表二维数组的第i行的地址
- 一维int数组型变量 int[] 的地址
- *(p+i) 即 ary[i]
- 代表地i行第0列的地址
- 一维数组型int[]变量,等价于 int类型的地址
- *(p+i)+j 即 &(ary[i][j])
- 代表第i行第j列的地址
- int类型的地址
- ((p+i)+j ) 即 ary[i][j]
- 代表第i行第j列的元素
- int类型的值
xx型的指针指向xx型的地址
10.7 C语言的内存映像
C语言的内存映像
*************************
* 命令行数据 *
* 环境变量数据 *
*************************
* 栈( stack ) *
* ***************************** *
* . *
* . *
* *
*--------------------------- --*
* 堆( heap ) * malloc分配的内存,使用完以后必须free
*************************
* 静态存储区 自动初始化为0 *
*************************
* 常量存储区 *
*************************
* 代码段 *
*************************
- 与变量有关的主要有三个内存区域
- 静态存储区 (编译时就已经分配好)
- 栈
- 堆
- 栈和堆是对向增长的
10.10 字符串
#include <stdio.h>
char *gp = "hello";//常量存储区
char ga[] = "hello";//静态存储区
char *foo(){
char *p = "hello";//常量存储区
char a[] = "hello";//栈
// p[0] = 'z';// 运行时错
// gp[0] = 'z';//运行时错
gp = a;
gp[0] = 'z';
return a;
}
int main(){
char *str = foo();
// str[0] = 'z'; //运行时错
ga[0] = 'z';
return 0;
}
初始化指针时创建的字符串常量与数组中的字符串的区别:
- 在ANSI C中,初始化指针时所创建的字符串常量被定义为只读。如果试图通过指针修改这个字符串的值,程序就会出现未定义的行为。在有些编译器中,字符串常量被存放在只允许读取的文本段中,以防止它被修改。
- 数组也可以用字符串常量进行初始化:
Char a[]=”abcdefg”; - 和指针相反,由字符串常量初始化的数组是可以修改的。静态存储区或者栈
10.12 函数指针
10.12.1 函数指针基本知识
函数指针最常见的一个用处·就是“回调函数”
1. 如何声明?
- int (*pf)(); 指向的就是一个返回值为int类型的函数。函数的参数,声明时并不重要,无须写出。
2. 如何初始化
- pf = &f; 通过取地址运算符来让函数指针指向一个特定的函数f
3. 如何使用?
- pf() 或者 (*pf)() 来调用 这个函数
10.13 复杂声明
10.13.1 复杂声明分析
10.13.2 返回函数指针的函数
10.13.3 复杂声明的定义
typedef的主要功能就是帮助定义一个新的数据类型,并给这个新的数据类型一个别名
- 分三步定义出 void (*ap[10]) (void (*)() );
void (*ap[10]) (void (*)() );
//利用typedef定义
typedef void (*pfv)();
typedef void (*pf_taking_pfv) (pfv);
pf_taking_pfv ap[10];
复用typedef定义的新类型:
void (*f()) ();
//利用typedef 定义
typedef void (*pfv)();
pfv f();
10.13.4 复杂声明用于强制类型转换
第11章 结构体
11.1 自定义数据类型
C语言是强类型语言,但是C中预定义的数据类型却远远不够。C允许用struct来定义自己的数据类型
实际上struct 就是 所有成员 都是public 的一个class
11.2 定义一个结构体变量的三种方法
- 先用 typedef 为 已存在的结构体名 typedef struct stu STU; 可以直接使用STU来声明。STU stu1, stu2;
- 利用结构体定义变量 struct stu stu3;
- 缺点:麻烦,每次定义的时候都要写上一个struct
- 定义结构体时,不定义类型,直接在结构体后 定义变量。
- 不足:如果别的地方需要在定义一个同类型的变量,将无法实现。
struct {
...
} stu1,stu2;
11.3 结构体中的“洞”
结构体的尺寸。
#include<stdio.h>
struct {
char c;
int i;
}s;
int main(){
s.c = 'a';
s.i = 0x0a0b0c0d;
printf("%d",sizeof(s));
}
- 上面结构体中的尺寸为8,而不是5(char(1) + int(4))
- 变量i 在内存上,它并不是紧挨着变量c,这种现象叫内存对齐。对齐以后的地址通常是2或者4 的倍数。
- 对齐的目的是:使处理器能够更快速地进行寻址,以便执行的额速度更快。这是一种空间换时间的策略。
计算方法ref
- 公式1:前面的地址必须是后面的地址正数倍,不是就补齐
- 公式2:整个Struct的地址必须是最大字节的整数倍
- 注意:
每个特定平台上的编译器都有自己的默认“对齐系数”。可以通过预编译命令#pragma pack(n)
栗子:
Struct E1 {
int a;
char b;
char c
} e1;
第一地址肯定存放a是4Byte地址,第二地址,b要1Byte的地址,来欢迎 公式一登场: 4 == 1*N (N等于正整数) 答”是”!地址现在为5Byte,下一个c要1Byte的地址同上,所以,就是6Byte。来欢迎公式二登场,在这个E1中最大的字节是4,而我们的地址字节是6,4的整数倍不是6,所以,要加2Byte(总地址),So,整个字节为8!
- 不支持用 “==” 来实现结构体变量的比较。这符合C的低层特性。因为结构体的中一般都有“空洞”,空洞中的数据是完全随机的。简单那地按位比较会由于那些随机数据而失败。
- 结构体中含有指针
- 一般认为指向内容相等,即是相等。而不需要地址相同。
struct {
char *str;
} s1,s2;
int main(){
s1.str == s2.str; //比较指针指向的地址
strcmp(s1.str,s2.str); //比较指针的指向的内容
s1.str = s2.str; // 两个指针指向同一个地址
s1.str = (char*)malloc(100,sizeof(char)) ;//分配空间,指向不同的地址
strcpy(s1.str,s2.str);
free(s1.str);//使用完 释放空间
}
11.5 结构体的读写
写一个结构体到流fp中
int fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
struct {
//...
} s
fwrite(&s,sizeof(s),1,fp);
- 优点:把整块的结构体的内存直接写到文件中,比起分别操作结构体中的每个成员,速度要快。
- 缺点:可能会造成移植性问题。不同处理器的对齐策略是不同的(结构体的“空洞”)。
11.6 函数和结构体
向函数传递一个结构体,遵循的是值传递。需要将传入的实参拷贝一次。基于效率的考虑,我们将函数的形参声明为结构体类型的指针。
11.7 枚举
实现考虑到某一变量 所有可能取的值,那么就可以尽量用含义清楚的单词来表示它的每一个值。按此种方式定义的类型,称为枚举类型(是一种基本数据类型)
- 自己不设置,则默认从0开始赋值
- 枚举符并不是字符串
枚举描述的一个星期的例子
#include <stdio.h>
enum day {MON=1,TUE,WEN,THUR,FRI,SAR,SUN};
int main() {
day d = MON;
// d = 1; //错误
d = (enum day)54;//霸王硬上弓
printf("%d\n",d);
// printf("%s\n",d); //错,枚举符并不是字符串
}
第12章 文件
12.1 文件基础知识
12.1.1 文件指针的解释
FILE 是C中定义的一个结构类型。
- 当使用fopen函数打开一个文件时,会返回一个FILE* (指针)变量。
- 每个FILE指针变量标识一个特定的磁盘文件
- 为什么要返回一个指针呢?
- 因为FILE结构体比较大,所以传回这个结构的首地址明显比复制整个结构体并传回的效率高。
12.1.2 路径名
- 斜杠’/’与地平线成锐角
- 反斜杠’\’与地平线成钝角。
- UNIX在辈分上 是Windows的舅老爷,所以UNIX优先使用了 斜杠
- Linux作为unix的”盗版”,自然也是用斜杠
后来的windows 想追求不同,只能用反斜杠了
编程的时候最好使用斜杠 分割路径名。
- C语言中 windows支持用斜杠分割路径名。(由于\是转义字符,用反斜杠得用两个\)
12.1.3 r+ 和 w+的区别
- “r” 代表读
- “w” 代表写
- “+” 代表读写
- “r+” 读写
- 文件不存在,“r+就失败”
- 文件存在,“r+” 不清空文件
- “w+” 读写
- 文件不存在,“w+” 新建一个
- 文件存在,“w+” 清空文件(覆写)
- 注意:输入模式的参数是 字符串“”而不能是”
12.2 同时读写
- 交替读写易出问题,因为读写其实共用一个缓冲区。每次读写都隐含着文件位置指针的改变。
- 交替读写要遵守的两个原则
- 如果中间没有fflush、fseek、fsetpos、或rewind,则后面的输出不能直接跟随输入
- 如果中间没有fseek、fsetpos、或rewind,或者一个输出操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。
#include <stdio.h>
#include <ctype.h>
int main(){
FILE *fp = fopen("file.txt","r+");
int c;
do{
c = fgetc(fp);
if(islower(c) != 0){
fseek(fp,-1L,SEEK_CUR);
fputc(toupper(c),fp);
fseek(fp,0L,SEEK_CUR);//必不可少,输入和输出之间,必须要有一个fseek来进行分割
}
} while(!feof(fp));
}
把一个文件中的小写字母变成大写字母。
- 读文件中的一个一个字符,如果读到小写字母,退一个位置,把它改成大写的,写到文件中(会把小写的给覆盖掉)。
12.3 断行标识符
- 为了准确定义一行文本,必须引入一个断行标志符或行尾标志符的概念。
- 历史上的电传打印机
- 回车 “return”,告诉打字机把打印头定位在左边界 0D
- 换行 “line feed”,告诉打字机把纸下移一行。 0A
文本文件的行尾标志符定义
- UNIX/Linux下: 用“换行” 表示
- DOS/Windows: 回车+换行,与传统一致。
- Mac : 回车
C内部统一使用’\n’ 来表示断行。
- C遇到’\n’时,C会根据不同的平台调用对应的函数,完成相应的转换,然后把正确的内容写到文件中。
- 在Windows上, ‘\n’被转换为 0x0D 0x0A (‘\n’也被叫做 回车换行符)
- Mac 0x0D
- UNIX 或 Linux 不做转换
- 不仅写出时做这种转换,在读入时,也做相反的转换
12.4 文本格式和二进制格式
在打开模式中增加字母“b”,就是以二进制模式打开文件。”wb”,”rb”
- 为何有两种模式?
- 不同系统有不同的断行符定义 造成的
- 两种模式都只在Windows或Mac 的操作系统上才有意义。
- 正常情况下,按字符读写,应该以 文本形式 打开文件
- 按数据块读写,应该以二进制的方式读写。
12.5 文件末尾和feof函数
12.5.1 feof的基本知识
feof到达文件结尾时返回真,否则返回假
- test.txt文件中只有一个a时
- 以下程序会 输出 97-1,
- -1从何而来?
#include <stdio.h>
//feof的用法思考
int main(){
int c;
FILE *fp = fopen("test.txt","r+");
while(!feof(fp)){
c = getc(fp);
printf("%d",c);
}
}
当我们从缓冲区读取一个文件内容的时候,缓冲区的末尾处会添加一个EOF标志。这个EOF标志只出现在缓冲区中,而不出现在硬盘上的文件中。
- 标志位是上一个操作设置
- 如果读操作,如fgets、fgetc、fscanf、fread 读取到了 EOF符的时候,会设置这个标志位。
- 位置指针指向了EOF,但是还没有读到的时候,返回假
//fgets函数的特点
#include <stdio.h>
//fgets函数的特点
int main(){
FILE *fp = fopen("test.txt","r+");
char str[100];
while(!feof(fp)){
fgets(str,100,fp);
printf("%s",str);
}
}
假设test.txt只包含abc和一个回车符,以上程序会输出两行abc。
- 原因fgets第二遍读取时,读取到了EOF并且没有其他字符被读入,所以str中的内容不变
12.5.1 正确使用feof函数
鉴于feof函数需要是上一个读操作读取到EOF才为真,应该给它取名为freadeof才对。
几乎所有的读函数,当它们读取到末尾 或 读取出错时,都会返回相同的值。
- 所以这个时候应该分别使用 feof 和 ferror函数
//判断读取到文件末尾
#include <stdio.h>
int main(){
int c;
FILE *fp = fopen("test.txt","r+");
while(c = fgetc(fp)){
if(feof(fp)){
break;
}
if(ferror(fp)){
//error handle
}
}else{
//do sth
}
}
第13章 Bug,错误和调试
13.1 Bug,错误(error),异常(exception)
13.1.1 三个概念的区别
第一代的计算机是由许多庞大且昂贵的真空管组成,并利用大量的电力来使真空管发光。可能正是由于计算机运行产生的光和热,通常吸引一些小虫子。如果引得一只小虫子 Bug 钻进了一支真空管内,将导致整个计算机无法工作。所以人们发现计算机不工作了,第一件事就是打开机子看看是否又飞进去了bug。后来,Bug这个名词就沿用下来,表示电脑系统或程序中隐藏的错误、缺陷或问题。 与Bug相对应,人们将发现Bug并加以纠正的过程叫做“Debug”,意即“捉虫子”或“杀虫子”
1947年9月9日,葛丽丝·霍普(Grace Hopper)发现了第一个电脑上的bug。当在Mark II计算机上工作时,整个团队都搞不清楚为什么电脑不能正常运作了。经过大家的深度挖掘,发现原来是一只飞蛾意外飞入了一台电脑内部而引起的故障(如图所示)。这个团队把错误解除了,并在日记本中记录下了这一事件。也因此,人们逐渐开始用“Bug”(原意为“虫子”)来称呼计算机中的隐错。现在在华盛顿的美国国家历史博物馆中还可以看到这个遗稿。
- bug是设计和实现上的固有缺陷
- 错误是运行时发生的用户错误,各种软硬件系统错误或突发情况
- 异常只是错误这个概念在**面向对象语言中的另外一种描述。
第14章 进阶之路
14.1 程序的效率
- 想要优化程序前,首先要问自己“我的程序是否是时间敏感的”?
- 算法就像是计算机科学中最坚硬的壳,你必须咬穿它才能吃到里面的果实。最好像作者一样,每天都去咬一咬。
- 二八定律,一个程序的80%的时间都是在运行20%的代码,优化的时候,应该优先优化那20%的代码。这20%的代码段称为Hot Spots。
- 如何找到那20%的代码?
- 使用工具。eg: VS的 性能分析工具Performance Profiter 、第三方的工具 dotTrace
- 如何找到那20%的代码?
- 一个好的编译器会做很多方面的优化,涵盖了很多内容,所以选择一个好的编译器并打开相应的优化开关进行编译,比自己手动优化更有效率。
14.2 C语言的使用原则
- 尽量使用“简单的C语言”,不要过分炫耀和使用一些不常用的特性和技巧。
- 最好能保留一些经典代码片段(snippet),学习snippet可以提高水平,开阔思路。 栗子
- 8.5节 模拟扑克洗牌的程序
- 4.7节 中的位操作宏
14.3 加深对C语言的理解
直到目前为止,我们只是在使用C语言,但是并不理解C语言,只知道how,而不知道 why。
- 有的时候,你如何理解C语言并不重要,编译器是如何理解C语言的才是终极答案。
下面的两个函数中
- f1 会打印出一个4(32位机),而不是数组a的长度(因为函数参数 数组变量a 会被编译器 转换为一个 指针变量,所以sizeof(a) 为指针变量的长度)
- f2 中可以对一个“数组变量”进行赋值 也是这个道理
void f1(char a[10]){
int i = sizeof(a);
printf("%d\n",i);
}
int f2(char str[]){
if(str[0] == '\0'){
str = "none";
}
printf("%c",str);
}
14.4 C、C++以及C#(java)
一般地,
- C#、java主要用于编写直接面向用户的各种(GUI)应用程序
- C++多用于开发各种后台使用的算法和逻辑库
- C则更底层,主要用于开发更核心的算法和靠近硬件的各种驱动程序库和控制程序。
14.5 我们现在在哪里
踏踏实实地一点一点学习,慢慢地融会贯通。
14.6 计算机领域的继续学习
作者的某一位学生(大学开发了各种语言的编译器)的观点。
- 编程语言、工具、所在的系统是什么都没关系,真正有用的算法和设计模式,建议学好的东西。
1. 计算机结构
- 计算机组成原理、体系结构
2. 数据结构
- 每种结构动手实现一下,形成自己的数据结构小类库
3. 操作系统的基本知识
- 不是window,也不是linux,而是 并发、调度、缓存机制、文件系统等算法型的东西
4. 算法 无穷无尽,首先把基础算法弄明白
- eg:动态规划、贪婪、分支限界 此类的经典经典算法