一、入门
1.C的变量
1.1 字符串变量
在C语言中,没有“字符串”这个数据类型,而是用字符数组来存放字符串,并提供了丰富的库函数来操作字符串。
char name[21]; // 定义一个可以存放20字符的字符串。
注意几个细节:
1)如果要定义一个存放20个英文的字符串,数组的长度应该是20+1,因为字符串末位以空字符结束
2)中文的汉字和标点符号需要两个字符(两个char存放一个中文)宽度来存放(GBK编码)。
例如name[21]可以存放20个英文字符,或10个中文字符。
3)字符串不是C语言的基本数据类型,不能用“=”赋值,不能用“>”和“<”比较大小,不能用“+”拼接,不能用==和!=判断两个字符串是否相同,要用函数。
4)字符串的初始化不建议采用把第一个元素的值置为0的方式(strname[0]=0),这样会导致后面全都是垃圾值
//对字符串变量来说,初始化就是把内容清空,本质上也是赋0值。
char name[21]; // 定义一个可以存放20字符的字符串
memset(name,0,sizeof(name)); // 清空字符串name中的内容 !!不能用"=0"来初始化
strcpy(name,"西施");
// 对字符串变量赋值"西施" !!不能用"="
// 本质上strcpy的参数应该是两个char类型的指针,但是因为字符串本身就是数组,而数组名就是指针
//所以可以直接写字符串的变量名或直接写字符串就行
2.C的输入和输出
在C语言中,有三个函数可以从键盘获得用户输入。
getchar:输入单个字符,保存到字符变量中。gets:输入一行数据,保存到字符串变量中。scanf:格式化输入函数,一次可以输入多个数据,保存到多个变量中。
在C语言中,有三个函数可以把数据输出到屏幕。
putchar:输出单个字符。puts:输出字符串。printf:格式化输出函数,可输出常量、变量等。
2.1 printf输出
//printf函数是格式化输出函数, 用于向屏幕输出数据
printf(格式化字符串,参数列表);
printf("我心匪席,不可卷也,我心匪石,不可转也。\n");
//把输出的文字用双引号包含起来,文字中的\n表示换行,多个\n可以换多行。
printf("我年龄是%d岁。\n",18);
int age=18;
printf("我年龄是%d岁。\n",age);
//输出整数型常量或变量用%d表示,在参数中列出待输出的整数常量或变量。
printf("我姓别是:%c。\n",'x'); // 姓别:x-男;y-女
char xb='x';
printf("我姓别是:%c。\n",xb);
//输出字符型常量或变量用%c表示,在参数中列出待输出的字符常量或变量
printf("我体重是%lf公斤。\n",62.5);
double weight=62.5;
printf("我体重是%lf公斤。\n", weight);
//输出的浮点型常量或变量用%lf表示,在参数中列出待输出的浮点型常量或变量。
printf("我的姓名是%s。\n","西施");
char name[21];
memset(name,0,sizeof(name));
strcpy(name, "西施");
printf("我的姓名是%s。\n",name);
//输出字符串常量或变量用%s表示,在参数中列出待输出的字符串常量或变量
注意,printf函数第一个参数(格式化字符串)的格式与后面的参数列表(常量或变量的列表)要一一对应,一个萝卜一个坑的填进去,不能多,不能少,顺序也不能错,否则会产生意外的结果。
2.2 scanf输入
/*scanf函数是格式化输入函数,用于接受从键盘输入的数据,
用户输入数据完成后,按回车键(Enter)结束输入。*/
scanf(格式化字符串,参数列表);
//不要在scanf的格式化字符串的最后加\n
scanf("%d",&age); // 在变量名前要加符号&,先不要问原因,以后再介绍
//输入整数的格式用%d表示,在参数中列出整数型变量名,用于保存输入的数据
scanf("%c",&xb); // 在变量名前要加符号&
//输入字符的格式用%c表示,在参数中列出字符型变量名,用于保存输入的数据
scanf("%lf",&weight); // 在变量名前要加符号&。
//输入浮点数的格式用%lf表示,在参数中列出浮点型变量名,用于保存输入的数据
scanf("%s",name); // 注意了,字符串变量名前可以不加符号&,不要问原因,以后再介绍。
//输入字符串的格式用%s表示,在参数中列出字符串变量名,用于保存输入的数据
注意,scanf函数第一个参数(格式化字符串)的格式与后面的参数列表(变量的列表)要一一对应,一个萝卜一个坑的填进去,不能多,不能少,顺序也不能错,否则会产生意外的结果
3.C的数组
数据类型 数组名[数组长度]; // double money[20];
//数组(array)是一组数据类型相同的变量,可以存放一组数据
int no[10];
memset(no,0,sizeof(no));
//第一个参数是数组名,第二个参数填0,第三个参数是数组占用的内存总空间,用sizeof(变量名)获取
3.1 字符串
字符串就是一个以空字符’\0’结束的字符数组,是一个特别的字符数组,这是约定也是规则。(空字符’\0’也可以直接写成0。)
如果字符串不用0结束,会出现乱码,且每次执行程序的结果都随机不可知;如果字符串以0结束了,但是后面的内容并不是0,则后面的内容将被丢弃
4.C的函数
4.1 自定义函数
如果自定义函数只在调用者程序中使用,可以在调用者程序中声明和定义,声明一般为调用者程序的上部,定义一般在调用者程序的下部,这并不是C语言的规定,而是为了让程序更方便阅读,程序员约定的写法。
如果自定义函数是一个通用的功能模块,可以在公共的头文件中声明,在公共的程序文件中定义。如果某程序需要调用公共的函数,在调用者程序中用#include指令包含公共的头文件,编译的时候把调用者程序和公共的程序文件一起编译。#include <> 用于包含系统提供的头文件,编译的时候,gcc在系统的头文件目录中寻找头文件。#include "" 用于包含程序员自定义的头文件,编译的时候,gcc先在当前目录中寻找头文件,如果找不到,再到系统的头文件目录中寻找。
4.2 库函数
C语言标准库函数的声明的头文件存放在/usr/include目录中
5.变量的作用域
作用域是程序中定义的变量存在(或生效)的区域,超过该区域变量就不能被访问。C 语言中有四种地方可以定义变量。
1)在所有函数外部定义的是全局变量。
2)在头文件中定义的是全局变量。
3)在函数或语句块内部定义的是局部变量。
4)函数的参数是该函数的局部变量。
5.1 全局变量
全局变量是定义在函数外部,通常是在程序的顶部(其它地方也可以)。
全局变量在整个程序生命周期内都是有效的,在定义位置之后的任意函数中都能访问。
全局变量在主程序退出时由系统收回内存空间
5.2 局部变量
在某个函数或语句块的内部声明的变量称为局部变量,它们只能在该函数或语句块内部的语句使用。
局部变量在函数或语句块外部是不可用的。
局部变量在函数返回或语句块结束时由系统收回内存空间。
PS:局部变量和全局变量的名称可以相同,在某函数或语句块内部,如果局部变量名与全局变量名相同,就会屏蔽全局变量而使用局部变量。
6.C的指针
关于地址:
- 不管是整型、浮点型、字符型,还是其他的数据类型的内存变量,它的地址都是一个十六进制数,可以理解为内存单元的编号
- C语言采用运算符&来获取变量的地址
- 在printf函数中,输出内存地址的格式控制符是%p,地址采用十六进制的数字显示
printf("变量ii的地址是:%p\n",&ii);
关于指针:
- 指针是一种特别变量,全称是指针变量,专用于存放其它变量在内存中的地址编号
- 把指针指向具体的内存变量的地址,就是对指针赋值
- 调用scanf函数的时候,需要在变量前面加符号&,其实就是把变量的地址传给scanf函数,scanf函数根据传进去的地址直接操作内存,改变内存中的值
- 指针也是一种内存变量,是内存变量就要占用内存空间,在C语言中,任何类型的指针占用8字节的内存
数组&指针:
- 在C语言中,数组占用的内存空间是连续的,数组名是数组元素的首地址,也是数组的地址
- 数组名、对数组取地址和数组元素的首地址是同一回事(都表示数组的地址,因此可以认为数组名就是一个指针)
- 地址可以用加(+)和减(-)来运算,加1表示下一个存储单元的地址(不是数学意义上的加1),减1表示上一个存储单元的地址,一般情况下,地址的运算适用于数组,对单个变量的地址运算没有意义
- 在C语言中,数组名是数组元素的首地址,字符串是字符数组,所以在获取字符串的地址的时候,不需要用&取地址
7.C的数据类型
7.1 数据类型的别名
C语言许程序员使用 typedef 关键字来给数据类型定义一个别名,别名一般有两个特点:1)名称更短;2)更符合程序员的习惯。
typedef unsigned int size_t;
size_t ii; //等同于 unsigned int ii;
7.2 整数
C中的数字(整数只是数字的一部分而已)默认就是十进制的,表示一个十进制数字不需要任何特殊的格式。但是,表示一个二进制、八进制或者十六进制数字就不一样了,为了和十进制数字区分开来,必须采用某种特殊的写法,具体来说,就是在数字前面加上特定的字符,也就是加前缀
- 二进制由 0 和 1 两个数字组成,书写时必须以0b或0B(不区分大小写)开头(并不是所有的编译器都支持二进制数字,只有一部分编译器支持,并且跟编译器的版本有关系)
int c = 0B100001;
- 八进制由 0~7 八个数字组成,书写时必须以0开头(注意是数字 0,不是字母 o)(在C语言中,不要在十进制数前加0,会被计算机误认为是八进制数)
int c = 0177777;
- 十六进制由数字 0~9、字母 A~F 或 a~f(不区分大小写)组成,书写时必须以0x或0X(不区分大小写)开头
int c = 0xffff;
7.3 字符
字符就是整数,字符和整数没有本质的区别。可以给 char 变量一个字符,也可以给它一个整数;反过来,可以给 int 变量一个整数,也可以给它一个字符
char 变量在内存中存储的是字符对应的 ASCII 码值。如果以 %c 输出,会根据 ASCII 码表转换成对应的字符,如果以 %d 输出,那么还是整数。int 变量在内存中存储的是整数本身,如果以 %c 输出时,也会根据 ASCII 码表转换成对应的字符。
char类型占内存一个字节,signed char取值范围是-128-127,unsigned char取值范围是0-255。描述再准确一些,在char的取值范围内(0-255),字符和整数没有本质区别。
字符肯定是整数,0-255范围内的整数是字符,大于255的整数不是字符。
7.4 字符串
7.4.0 字符串初始化
memset(strname,0,sizeof(strname)); // 把全部的元素置为0
//不要使用strname[0]=0;初始化字符串
//不够规范,并且存有隐患,在实际开发中,一般采用memset的函数初始化字符串
7.4.1 字符串长度
size_t strlen( const char* str);
/*
功能:计算字符串的有效长度,不包含0。
返回值:返回字符串的字符数 。
1.strlen 函数计算的是字符串的实际长度,遇到第一个0结束。
函数返回值一定是size_t类型,是无符号的整数,即typedef unsigned int size_t。
如果您只定义字符串没有初始化,求它的长度是没意义的,它会从首地址一直找下去,遇到0停止
2.sizeof返回的是变量所占的内存数,不是实际内容的长度
*/
7.4.2 字符串赋值
char *strcpy(char* dest, const char* src);
/*
功 能: 将参数src字符串拷贝至参数dest所指的地址。
返回值: 返回参数dest的字符串起始地址。
1.复制完字符串后,在dest后追加0。
2.如果参数dest所指的内存空间不够大,可能会造成缓冲溢出的错误情况。
*/
char * strncpy(char* dest,const char* src, const size_t n);
/*
功能:把src前n字符的内容复制到dest中
返回值:dest字符串起始地址。
1.如果src字符串长度小于n,则拷贝完字符串后,在dest后追加0,直到n个。
2.如果src的长度大于等于n,就截取src的前n个字符,不会在dest后追加。(注意并不是不追加结尾空字符)
3.dest必须有足够的空间放置n个字符,否则可能会造成缓冲溢出的错误情况。
*/
7.4.3 字符串拼接
char *strcat(char* dest,const char* src);
/*
功能:将src字符串拼接到dest所指的字符串尾部。
返回值:返回dest字符串起始地址。
1.dest最后原有的结尾字符0会被覆盖掉,并在连接后的字符串的尾部再增加一个0。
2.dest要有足够的空间来容纳要拼接的字符串,否则可能会造成缓冲溢出的错误情况。
*/
char *strncat (char* dest,const char* src, const size_t n);
/*
功能:将src字符串的前n个字符拼接到dest所指的字符串尾部。
返回值:返回dest字符串的起始地址。
1.如果n大于等于字符串src的长度,那么将src全部追加到dest的尾部,
如果n小于字符串src的长度,只追加src的前n个字符。
2.strncat会将dest字符串最后的0覆盖掉,字符追加完成后,再追加0。
dest要有足够的空间来容纳要拼接的字符串,否则可能会造成缓冲溢出的错误情况。
*/
7.4.4 字符串比较
int strcmp(const char *str1, const char *str2 );
/*
功能:比较str1和str2的大小。
返回值:相等返回0,str1大于str2返回1,str1小于str2返回-1;
*/
int strncmp(const char *str1,const char *str2 ,const size_t n);
/*
功能:比较str1和str2前n个字符的大小(判断前n个字符是否相等)。
返回值:相等返回0,str1大于str2返回1,str1小于str2返回-1;
1.两个字符串比较的方法是比较字符的ASCII码的大小,从两个字符串的第一个字符开始,如果分不出大小,
就比较第二个字符,如果全部的字符都分不出大小,就返回0,表示两个字符串相等。
2.在实际开发中,程序员一般只关心字符串是否相等,不关心哪个字符串更大或更小。
*/
7.4.5 字符查找
char *strchr(const char *s,const int c);
/*
返回一个指向在字符串s中第一个出现c的位置,如果找不到,返回0。
*/
char *strrchr(const char *s,const int c);
/*
返回一个指向在字符串s中最后一个出现c的位置,如果找不到,返回0。
*/
char *strstr(const char* str,const char* substr);
/*
功能:检索子串在字符串中首次出现的位置。
返回值:返回字符串str中第一次出现子串substr的地址;如果没有检索到子串,则返回0。
*/
8.C的结构体
8.1 结构体初始化
memset(&queen,0,sizeof(struct st_girl));
/*
功能:将queen所指向的内存块中的每个字节的内容都设置为第二个参数指定的ASCLL值,块的大小由第三个参数指定
1.采用memset函数初始化结构体,全部成员变量的值清零。
2.注意结构体变量名并不能与指针等价,只能使用&取址
*/
8.2 结构体赋值
在C语言中,结构体的成员如果是基本数据类型(int、char、double)可以用=号赋值,如果是字符串,字符串不是基本数据类型,可以用strcpy函数赋值,如果要把结构体变量的值赋给另一个结构体变量,有两种方法:1)一种是把结构体变量成员的值逐个赋值给另一个结构体变量的成员,这种方法太笨,没人使用;
2)另一种方法是内存拷贝,C语言提供了memcpy(memory copy的简写)实现内存拷贝功能
void *memcpy(void *dest, const void *src, size_t n);
/*
@src: 源内存变量的起始地址。
@dest: 目的内存变量的起始地址。
@n: 需要复制内容的字节数。
函数返回指向dest的地址,函数的返回值意义不大,程序员一般不关心这个返回值。
*/
8.2.1 strcpy&memcpy
这两个函数从功能和实现原理上完本不同,甚至不应该放在一起比较
1)复制的内容不同,strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2)用途不同,通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy。
3)复制的方法不同,strcpy不需要指定长度,它遇到被复制字符的串结尾符0才结束,memcpy则是根据其第3个参数决定复制的长度。
9.C的动态内存管理
在C语言中,编写程序的时候不能确定内存的大小,希望程序在运行的过程中根据数据量的大小动态的分配内存。动态内存管理,就是指在程序运行过程中动态的申请和释放内存空间。
C语言允许程序动态管理内存,需要时随时开辟,不需要时随时释放。内存的动态管理是通过调用库函数来实现的,主要有malloc和free 函数。
void *malloc(unsigned int size);
/*
1.malloc的作用是向系统申请一块大小为size的连续内存空间,如果申请失败,函数返回0,
如果申请成功,返回成功分配内存块的起始地址。
2.malloc的返回值的地址的基类型为 void,即不指向任何类型的数据,只提供一个地址,
程序中需要定义一个指针来指向动态分配的内存地址
*/
void free(void *p);
//free的作用是释放指针p指向的动态内存空间,p是调用malloc函数时返回的地址,free函数无返回值
注意:
调用free函数把指针所指的内存给释放掉,但指针不一定会赋值 0(也与编译器有关),如果对释放后的指针进行操作,相当于非法操作内存。释放内存后应立即将指针置为0
free(pi);
pi=0;//指针置为0
二、规范
1.C语言代码的多行书写
在我们之前学习的过程中,编写的程序的功能很简单,一句代码很短,但是在实际开发中,函数参数往往很长很多,一句代码可能会很长,需要用多行才能书写。
如果我们在一行代码的行尾放置一个反斜杠,c语言编译器会忽略行尾的换行符,而把下一行的内容也算作是本行的内容。这里反斜杠起到了续行的作用
strcpy(str,"aaaaaaaaaa\
bbbbbbbbb");
2.main函数的参数
main函数有三个参数,argc、argv和envp,它的标准写法如下:
int main(int argc,char *argv[],char *envp[])
/*
int argc,存放了命令行参数的个数。
char *argv[],是个字符串的数组,每个元素都是一个字符指针,指向一个字符串,即命令行中的每一个参数。
char *envp[],也是一个字符串的数组,这个数组的每一个元素是指向一个环境变量的字符指针(envp数组存放了当前程序运行环境的参数)。
*/
注意事项:
1)argc的值是参数个数加1,因为程序名称是程序的第一个参数,即argv[0],在上面的示例中,argv[0]是./book101。
2)main函数的参数,不管是书写的整数还是浮点数,全部被认为是字符串。
3)参数的命名argc和argv是程序员的约定,您也可以用argd或args,但是不建议这么做。
三、进阶
1.文件操作
对计算机来说,一切皆数据。数据的存放方式有很多种,如内存、文件、数据库等,文件是极其重要的一种
根据文件中数据组织形式的不同,可以把文件分为文本文件和二进制文件,C语言源代码是文本文件,编译后的可执行程序是二进制文件
1.1 文本数据和二进制数据
文本数据由字符串组成,存放了每个字符的 ASCII 码值(假如采用的是ASCLL编码的话),每个字符占一个字节,每个字节存放一个字符;
| 字符 | ‘1’ | ‘2’ | ‘3’ |
|---|---|---|---|
| ASCLL值(十进制) | 49 | 50 | 51 |
二进制数据是字节序列,数字123的二进制表示是01111011;
例如数字 123,如果用文本格式存放,数据内容是’1’、’2’、’3’ 三个字符,占三个字节;如果用二进制格式形式存储,字符、短整型、短整型、长整型都可以存储123
01111011;//字符型一个字节
00000000 01111011;//短整型2个字节
00000000 00000000 00000000 01111011;//整型4个字节
00000000 00000000 00000000 00000000 00000000 00000000 00000000 01111011;//长整型8个字节
1.2 文本文件和二进制文件
按文本格式存放数据的文件称为文本文件或ASCII文件,文件可以用vi和记事本打开,看到的都是ASCII字符。
按二进制格式存放数据的文件称为二进制文件,如果用vi打开二进制文件,看到的是乱码,没有意义。
两者的区别:
- 二进制文件中存储的数据是二进制数据即01串,文本文件中存储的数据是字符串
- 文本文件只能存储char型字符变量。二进制文件可以存储char/int/short/long/float/……各种变量值
- 文本文件每条数据通常是固定长度的。以ASCII为例,每条数据(每个字符)都是1个字节。进制文件每条数据不固定。如short占两个字节,int占四个字节,float占8个字节
- 文本文件编辑器(记事本等文本编辑器内部自带解码工具)就可以读写。比如记事本、NotePad++、Vim等。二进制文件需要特别的解码器。比如bmp文件需要图像查看器,rmvb需要播放器
- 二进制文件是把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放,也就是说存放的是数据的原形式,因此二进制文件的读写速度非常快,但是可读性差;文本文件是把数据的终端形式的二进制数据输出到磁盘上存放,也就是说存放的是数据的终端形式,文本文件会选择一种编码方式(ASLL或者Unicode),在读写时先将数据按照选择的编码方式转为对应的编码,再将这个编码写进文件中,故读写速度较慢但可读性好
1.3 文件的打开和关闭
C 语言对文件进行操作之前必须先“打开”文件,操作(读和写)完成后,再“关闭”文件。
1.3.1 文件指针
操作文件的时候,C语言为文件分配一个信息区,该信息区包含文件描述信息、缓冲区位置、缓冲区大小、文件读写到的位置等基本信息,这些信息用一个结构体来存放(struct _IO_FILE),这个结构体有一个别名FILE(typedef struct _IO_FILE FILE),FILE结构体和对文件操作的库函数是在 stdio.h 头文件中声明的。
FILE结构体指针习惯称为文件指针(结构体属于自定义数据类型,因此有结构体变量)
打开文件的时候,fopen函数中会动态分配一个FILE结构体大小的内存空间,并把FILE结构体内存的地址作为函数的返回值,程序用FILE结构体指针存放这个地址。关闭文件的时候,fclose函数除了关闭文件,还会释放FILE结构体占用的内存空间。
1.3.2 打开文件
使用 C语言提供的库函数fopen来创建一个新的文件或者打开一个已存的文件,调用fopen函数成功后,返回一个文件指针( FILE *)
在Linux平台下,打开文本文件和二进制文件的方式没有区别
FILE *fopen( const char * filename, const char * mode );
/*
@filename 是字符串,表示需要打开的文件名,可以包含目录名,如果不包含路径就表示程序运行的当前目录。
实际开发中,采用文件的全路径。
@mode也是字符串,表示打开文件的方式(模式),打开方式可以是下列值中的一个
*/
| 方式 | 含 义 | 说 明 |
|---|---|---|
| r | 只读 | 文件必须存在,否则打开失败。 |
| w | 只写 | 如果文件存在,则清除原文件内容;如果文件不存在,则新建文件。 |
| a | 追加只写 | 如果文件存在,则打开文件,如果文件不存在,则新建文件。 |
| r+ | 读写 | 文件必须存在。在只读 r 的基础上加 ‘+’ 表示增加可写的功能。 |
| w+ | 读写 | 在只写w的方式上增加可读的功能。 |
| a+ | 读写 | 在追加只写a的方式上增加可读的功能。 |
1.3.3 关闭文件
int fclose(FILE *fp);
/*
@fp为fopen函数返回的文件指针
*/
注意:
1)调用fopen打开文件的时候,一定要判断返回值,如果文件不存在、或没有权限、或磁盘空间满了,都有可能造成打开文件失败。
2)文件指针是调用fopen的时候,系统动态分配了内存空间,函数返回或程序退出之前,必须用fclose关闭文件指针,释放内存,否则后果严重。
3)如果文件指针是空指针或野指针,用fclose关闭它相当于操作空指针或野指针,后果严重。
1.4 文本文件的读写
在实际开发中,文本文件以行的形式存放字符串,如C程序的源代码,一段文字等,所以一般是按行写入和读取数据
1.4.1 向文件中写入数据
int fprintf(FILE *fp, 格式化字符串,参数列表);//函数声明
/*
fprintf函数的用法与printf相同,只是多了第一个参数文件指针,表示把数据输出到文件。
程序员不必关心fprintf函数的返回值
*/
for (ii=0;ii<3;ii++) // 往文件中写入3行
{
fprintf(fp,"这是第%d个出场的超女。\n",ii+1);
}
1.4.2 从文件中读取数据
char *fgets(char *buf, int size, FILE *fp);
/*
fgets的功能是从文件中读取一行。
@buf是一个字符串(缓冲区),用于保存从文件中读到的数据。
@size是打算读取内容的长度。
@fp是待读取文件的文件指针。
1.如果文件中将要读取的这一行的内容的长度小于size,fgets函数就读取一行,
如果这一行的内容大于等于size,fgets函数就读取size-1字节的内容
(在读取到 size-1 个字符之前如果出现了换行,或者读到了文件末尾,则此次读取结束)
2.调用fgets函数如果成功的读取到内容,函数返回buf,如果读取错误或文件已结束,返回空,即0。
如果fgets返回空,可以认为是文件结束而不是发生了错误,因为发生错误的情况极少出现。
3.不管 size 的值多大,fgets函只读取一行数据,不能跨行。
在实际开发中,可以将 size 的值设置地足够大,确保每次都能读取到一行完整的数据。
*/
// 逐行读取文件的内容,输出到屏幕
while (1)
{
memset(strbuf,0,sizeof(strbuf));
if (fgets(strbuf,301,fp)==0) break;
printf("%s",strbuf);
}
1.5 二进制文件的读写
二进制文件没有行的概念,没有字符串的概念。
我们把内存中的数据结构直接写入二进制文件,读取的时候,也是从文件中读取数据结构的大小一块数据,直接保存到数据结构中。注意,这里所说的数据结构不只是结构体,是任意数据类型(当然我们最常用的还是结构体)。
1.5.1 向文件中写入数据
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
/*
功能:fwrite函数用来向文件中写入数据块
@ptr:为内存区块的指针,存放了要写入的数据的地址,它可以是数组、变量、结构体等。
@size:固定填1。
@nmemb:表示打算写入数据的字节数。
@fp:表示文件指针。
函数的返回值是本次成功写入数据的字节数,一般情况下,程序员不必关心fwrite函数的返回值。
*/
struct st_girl
{
char name[50]; // 姓名
int age; // 年龄
int height; // 身高,单位:厘米cm
char sc[30]; // 身材,火辣;普通;飞机场。
char yz[30]; // 颜值,漂亮;一般;歪瓜裂枣。
};//数据结构声明
struct st_girl stgirl; // 定义超女数据结构变量
FILE *fp=0; // 定义文件指针变量fp
//向数据结构变量中填入信息
strcpy(stgirl.name,"西施");
stgirl.age=18;
stgirl.height=170;
strcpy(stgirl.sc,"火辣");
strcpy(stgirl.yz,"漂亮");
//向文件中写入数据结构变量
fwrite(&stgirl,1,sizeof(stgirl),fp);
1.5.2 从文件中读取数据
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp);
/*
@ptr:用于存放从文件中读取数据的变量地址,它可以是数组、变量、结构体等。
@size:固定填1。
@nmemb:表示打算读取的数据的字节数。
@fp:表示文件指针。
如果成功的读取到内容,函数返回读取到的内容的字节数,如果读取错误或文件已结束,返回空,即0。
如果fread返回空,可以认为是文件结束而不是发生了错误,因为发生错误的情况极少出现
*/
while (1)
{
// 从文件中读取数据,存入超女数据结构变量中
if (fread(&stgirl,1,sizeof(struct st_girl),fp)==0) break;
// 显示超女数据结构变量的值
printf("name=%s,age=%d,height=%d,sc=%s,yz=%s\n",\
stgirl.name,stgirl.age,stgirl.height,stgirl.sc,stgirl.yz);
}
注意:
- fwrite和fread函数也可以写入和读取文本文件,但是没有换行的概念,不管是换行符或其它的特殊字符,无区别对待
- 一般来说,二进制文件有约定的数据格式,程序必须按约定的格式写入/读取数据,book115.c写入的是超女结构体,book117.c就要用超女结构体来存放读取到的数据(首先就需要定义相同的超女结构体)。这道理就像图片查看软件无法打开音频文件,音频播放软件也无法打开图片文件,因为音频文件和图片文件的格式不同
1.6 文件定位
在文件内部有一个位置指针(注意不是文件指针),用来指向文件当前读写的位置。在文件打开时,如果打开方式是r和w,位置指针指向文件的第一个字节,如果打开方式是a,位置指针指向文件的尾部。每当从文件里读取n个字节或文件里写入n个字节后,位置指针也会向后移动n个字节。
文件位置指针与C语言中的指针不是一回事。位置指针仅仅是一个标志,表示文件读写到的位置,不是变量的地址。文件每读写一次,位置指针就会移动一次,它不需要您在程序中定义和赋值,而是由系统自动设置,对程序员来说是隐藏的。
在实际开发中,偶尔需要移动位置指针,实现对指定位置数据的读写。我们把移动位置指针称为文件定位。
C语言提供了ftell、rewind和fseek三个库函数来实现文件定位功能。
long ftell(FILE *fp);
//ftell函数用来返回当前文件 位置指针 的值,这个值是当前位置相对于文件开始位置的字节数
void rewind ( FILE *fp );
//rewind函数用来将位置指针移动到文件开头
int fseek ( FILE *fp, long offset, int origin );
//fseek() 用来将位置指针移动到任意位置
/*
@fp 为文件指针,也就是被移动的文件。
@offset 为偏移量,也就是要移动的字节数。
之所以为 long 类型,是希望移动的范围更大,能处理的文件更大。
offset 为正时,向后移动;offset 为负时,向前移动。
@origin 为起始位置,也就是从何处开始计算偏移量。
C语言规定的起始位置有三种,分别为:0-文件开头;1-当前位置;2-文件末尾。
*/
2.编译预处理
C语言由源代码生成可执行程序的过程如下:
C源程序->编译预处理->编译->优化程序->汇编程序->链接程序->可执行文件
其中编译预处理阶段,读取C源程序,对其中的预处理指令(以#开头的指令如#include)和特殊符号进行处理。或者说是扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。
编译预处理(简称预处理)过程先于编译器对源代码进行处理(即编译),读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行转换。预处理过程还会删除程序中的注释和多余的空白字符
2.1 预处理指令
在C语言的程序中包括各种以符号#开头的编译指令,这些指令称为预处理命令。预处理命令属于C语言编译器,而不是C语言的组成部分,通过预处理命令可扩展C语言程序设计的环境。
预处理指令是以#号开头的代码行,#号必须是该行除了任何空白字符外的第一个字符。
#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。
预处理指令主要有以下三种:
1)包含文件:将源文件中以#include格式包含的文件复制到编译的源文件中,可以是头文件,也可以是其它的程序文件。
2)宏定义指令:#define指令定义一个宏,#undef指令删除一个宏定义。
3)条件编译:根据#ifdef和#ifndef后面的条件决定需要编译的代码。
2.1.1 包含文件
当一个C语言程序由多个文件模块组成时,主模块中一般包含main函数和一些当前程序专用的函数。程序从main函数开始执行,在执行过程中,可调用当前文件中的函数,也可调用其他文件模块中的函数。
如果在本模块中要调用其他文件模块中的函数,首先必须在主模块中声明该函数原型。一般都是采用文件包含的方法,包含其他文件模块的头文件(头文件中定义了函数原型)。
文件包含中指定的文件名既可以用引号括起来,也可以用尖括号括起来
#include<>
#include""
如果使用尖括号<>括起文件名,则编译程序将到C语言开发环境中设置好的 include文件中去找指定的文件(/usr/include)。因为C语言的标准头文件都存放在/usr/include文件夹中,所以一般对标准头文件采用尖括号;对程序员自己编写的文件,则使用双引号。
如果自己编写的文件不是存放在当前工作文件夹,可以在#include命令后面加在文件路径。
#include命令的作用是把指定的文件模块内容插入到#include所在的位置(并不是一定插入到文件首部),当程序编译时,系统会把所有#include指定的文件一起编译生成可执行代码。
#include包含文件,可以是 “.h”,表示C语言程序的头文件,也可以是“.c”,表示包含普通C语言源程序。
2.1.2 宏定义指令
使用#define命令并不是真正的定义符号常量,而是定义一个可以替换的宏。被定义为宏的标识符称为“宏名”。在编译预处理过程时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏替换”或“宏展开”。
在C语言中,宏分为有参数和无参数两种
#define 宏名 字符串
#define PI 3.141592//实际使用
/*
# 表示这是一条预处理命令(凡是以“#”开始的均为预处理命令)。
define 关键字“define”为宏定义命令。
宏名 是一个标识符,必须符合C语言标识符的规定,一般以大写字母标识宏名。
字符串 可以是常数,表达式,格式串等。在前面使用的符号常量的定义就是一个无参数宏定义。
*/
无参数的宏注意:
- 预处理命令语句后面一般不会添加分号,如果在#define最后有分号,在宏替换时分号也将替换到源代码中去。在宏名和字符串之间可以有任意个空格。
- 宏定义是宏名来表示一个字符串,在宏展开时又以该字符串取代宏名。这只是一种简单的代换,字符串中可以含任何字符,可以是常数,也可以是表达式,编译预处理时不会对它进行语法检查,如有错误,只能在编译已被宏展开后的源程序时发现。
- 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名。在宏展开时由预处理程序层层替换。建议不要这么做,会把程序复杂化
- 习惯上宏名用大写字母表示,以方便与变量区别。但也可以用小写字母
```c
define 宏名(形参表) 字符串
define MAX(x,y) ((x)>(y)?(x):(y))//在执行预编译指令后得到的文件中可看到原文件中的MAX(34,59)变成了((34)>(59)?(34):(59))
/*
define命令定义宏时,还可以为宏设置参数。与函数中的参数类似,在宏定义中的参数为形式参数,
在宏调用中的参数称为实际参数。 对带参数的宏,在调用中,不仅要宏展开,还要用实参去代换形参 */
带参数的宏注意:
- 在定义带参数的宏时,宏名和形参表之间不能有空格出现,否则,就将宏定义成为无参数形式,而导致程序出错。
<a name="zwnmG"></a>
#### 2.1.3 条件编译
```c
#ifdef 标识符
程序段 1
#else
程序段 2
#endif
/*
1.其意义是,如果#ifdef后面的标识符已被定义过,则对“程序段1”进行编译;
如果没有定义标识符,则编译“程序段2”。
一般不使用#else及后面的“程序2”
*/
#define LINUX
int main()
{
#ifdef LINUX
printf("这是Linux操作系统。\n");
#else
printf("未知的操作系统。\n");
#endif
}
//执行预编译指令后得到的新文件中内容如下
int main()
{
printf("这是Linux操作系统。\n");
}
#ifndef 标识符
程序段 1
#else
程序段 2
#endif
/*
#ifndef的意义与#ifdef相反
1.如果未定义标识符,则编译“程序段1”;否则编译“程序段2”
2.在实际开发中,程序员用#ifndef来防止头文件被重复包含
*/
3.gdb调试
程序员写在编写程序的时候不可能是一帆风顺的,gcc编译器可以发现程序代码的语法错误,但不能发现程序的业务逻辑错误,调试程序是软件开发的内容之一。调试程序的方法有很多种,例如可以用printf语句跟踪程序的运行步骤和显示变量的值(这是最简单效率最低的方式,适用于初学者),本章节介绍一个功能强大的调试工具gdb。(Linux系统需要shell使用命令安装,这里不赘述)
4.makefile
在软件的工程中的源文件是很多的,其按照类型、功能、模块分别放在若干个目录和文件中,哪些文件需要编译,那些文件需要后编译,那些文件需要重新编译,甚至进行更复杂的功能操作,这就有了我们的系统编译的工具(当然在集成IDE里面可以直接鼠标勾选)。
在linux和unix中,有一个强大的实用程序,叫make,可以用它来管理多模块程序(也就是我们常说的项目文件)的编译和链接,直至生成可执行文件。make程序需要一个编译规则说明文件,称为makefile,makefile文件中描述了整个软件工程的编译规则和各个文件之间的依赖关系。
makefile就像是一个shell脚本一样,其中可以执行操作系统的命令,它带来的好处就是我们能够实现“自动化编译”,一旦写好,只要一个make命令,整个软件功能就完全自动编译,提高了软件开发的效率。
make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说大多数编译器都有这个命令,使用make可以使得重新编译的次数达到最小化。(注意make工具并不需要向gdb调试一样额外安装,编译器一般自带)
5.静态库与动态库
我们通常把公用的自定义函数和类从主程序中分离出来,函数和类的声明在头文件(public.h)中,定义在程序文件中(public.c),主程序中要包含头文件,编译时和程序文件一起编译(因为头文件和程序文件中都#include了头文件,在预处理阶段头文件作为接口将两个程序文件连接到一起,编译阶段就会一起编译——其实这样理解不太准确,严谨的说在编译阶段头文件只是起到了声明的作用,两个源文件仍然是分别编译的,在链接阶段通过之前头文件的声明两个已经编译好的程序会链接在一起进而执行)
然而公用函数库的程序文件public.c程序文件是源代码,对任何程序员是可见的,没有安全性可言,在实际开发中,出于技术保密或其它方面考虑,开发者并不希望提供公用函数库的源代码
C/C++提供了一个可以保证代码安全性的方法——把公共的程序文件编译成库文件(因为文件经过编译后其源代码形式就已经看不出来了),库文件是一种可执行代码的二进制形式,可以与其它的源程序一起编译(其实准确来说应该是在链接阶段通过头文件与其他编译好的程序链接到一起),也可以被操作系统载入内存执行
库文件分为静态库和动态库,一定要注意,库函数的使用方式和包含文件不一样!!!在Linux的shell中需要我们输入特定的命令(在Windows的IDE中可以配置自动链接库或者使用头文件声明静态库)
详细使用方式:http://www.freecplus.net/05944de09a3942a89a571d523712b548.html
5.1 静态库
静态库在编译的时候,主程序文件与静态库一起编译,把主程序与主程序中用到的库函数一起整合进了目标文件(.exe文件)。这样做优点是在编译后的可执行程序可以独立运行,因为所使用的函数都已经被编译进去了。缺点是,如果所使用的静态库发生更新改变,我们的程序必须重新编译
静态库文件名的命名方式是“libxxx.a”,库名前加”lib”,后缀用”.a”,“xxx”为静态库名
5.2 动态库
动态库在链接时并不会被链接到目标代码中,而是在程序运行时才被载入,因此在程序运行时还需要指定动态库的目录。
动态库的命名方式与静态库类似,前缀相同,为“lib”,后缀变为“.so” “xxx”为动态库名
注意:
- 如果在动态库文件和静态库文件同时存在,优先使用动态库编译
- 动态库在编译的时候只做语法检查,并没有被编译进目标代码,当程序执行到动态库中的函数时才调用该函数库里的代码。动态函数库并没有整合进程序,所以程序的运行环境必须提供动态库路径。优点是,如果所使用的动态库发生更新改变,程序不需要重新编译,所以动态库升级比较方便
5.3 总结
dll,与lib是Windows下描述封装代码库的格式,.a,.so是linux下描述封装代码库的格式。dll与.so是动态库,lib与.a是静态库。
区别:
- 静态库(lib,.a)是以静态的方式(隐式方式)将lib文件加载到可执行文件中。通常需要在使用时包含其头文件和在链接设置中设置加载此静态库。
- 动态库(dll,.so)可以用(1)静态方式(隐式方式)或者(2)动态链接的方式加载到可执行文件中。以静态方式加载相同,需要在链接设置中配置dll的lib或者.so的.a文件。如果是以动态加载方式则需要dlopen等接口去加载dll或者.so。两种方式的区别是动态加载是在程序执行阶段根据程序实际情况进行加载dll。静态加载在链接阶段便已经将接口地址等信息加载到可执行文件中。
- 静态库将代码,符号等信息都编译到了lib文件中。所以如果程序使用,就必须要链接lib库。同时,多个程序之间不能分享lib库。动态库可以在多个程序见使用一份dll,减少资源浪费。
常见问题:
Q:一般在制作动态库时也会有lib文件或者是.a文件的产生,这些与静态库的文件有什么不同?
A:动态库的lib文件大小要比静态库的小很多,说明信息没有那么多,动态库的lib叫导出库。里面只包含了接口地址一些基本信息,没有实现等信息。而因为dll可以通过动态加载,其实在dll中也有函数地址。
Q:其他特殊文件
A:map文件记录了程序中的数据,变量,函数的地址,数据存放区域。如果程序发生奔溃,则可以通过奔溃的地址在map文件中判断崩溃的程序位置。需要在链接中开启生成map文件的设置。
pdb文件是bebug模式下的调试信息。有次文件才能调试程序。记录了变量位置,函数1地址等
