1.源码文件如何变成可执行文件(*)
需要以下4个步骤:
(1)预处理阶段:预处理器根据以#开头的指令,修改主要包括#include、#define和条件编译三个方面,修改源码内容,比如如果源码中有 #include<stdio.h> 则预处理器会读取文件 stdio.h 文件中的内容,并将其直接插入到原来的源码文件中,通常另存为以 .i 为扩展名的文件。(gcc -E hello.c -o hello.i)
(2)编译阶段:编译器读取 .i 文件中的内容,并将其翻译为以 .s 为扩展名的汇编语言文件。(gcc -S hello.c -o hello.s)
(3)汇编阶段:汇编器将 .s 文件翻译成机器码,并保存为 .o为扩展名的文件。
(gcc -c hello.s -o hello.o)
(4)链接阶段:链接器将不同的 .o 文件合并到一起,组成最终的可执行文件;比如我们的程序里调用了 printf 函数,作为一个C标准函数,printf 单独存在于一个 printf.o 的文件中,那么链接器将会找到这个 printf.o 文件,将其中的内容合并到我们自己的 .o 文件中,生成可以被加载到内存中执行的文件。
C语言主要分为几个版本:Old Style C、C89、C99和C11。其中,C89、C99和C11是标准语言规范,现在广泛使用的是C99。
2.面向过程和面向对象的区别(*)
(1)面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
(2)面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
比如:五子棋
面向过程:1.开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。
面向对象:1.黑白双方,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。
特点
- 面向过程(蛋炒饭)
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展 - 面向对象(盖浇饭)
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点:性能比面向过程低
3.基本数据类型
(1)数值类型
数值类型分为整型(整数)和浮点型(小数)。按照表示数字范围的从大到小。
<1>整型
整数分为五类:字符型(char)、短整型(short)、整型(int)、长整型(long)和长长整型(long long)
<2>浮点型
浮点型分三类:单精度型(float)、双精度型(double)和长双精度型(long double)
(2)获取类型的大小
关键字sizeof:查看变量或者类型大小。
(3)字节
sizeof获得数据的单位是Byte(字节)。Byte(字节)是计量存储容量的一种计量单位,一个字节是8位二进制,可容纳256个数字。一个ASCII字符就是一个字节。
用B表示Byte(字节),用b表示bit(比特/位).
(4)输入输出格式化
double的输入占位符必须是%lf,输出占位符可以是%f。
(5)整数类型
<1>表示范围
类型的表示范围与类型大小存在如下关系:
n表示的是bit位,比如:char是一个字节,8位,int是4个字节,32个二进制位,由于存在负数,所以数的大小会减半。
<2>无符号整型
在一些特殊情况下,数据只用0和整数,不存在负数,这时可以使用无符号整型unsigned。无符号整型只是在原来的整型前加上关键字unsigned。因为没有负数,所以数值的表示范围扩大了一倍。
总之,类型的表示范围与类型大小存在如下关系:
<3>整数类型的选择
- 大多数情况下使用int。
- 如果int范围不够,使用long long。
- 避免使用long。
- 谨慎使用unsigned。
(5)浮点类型
<1>浮点数的范围
示例 | 输出 | 含义 |
---|---|---|
1.0/0.0 | inf | 表示正无穷大 |
-1.0/0.0 | -inf | 表示负无穷大 |
0.0/0.0 | nan | 不存在 |
<2>浮点数的精度
注意:浮点类型没有无符号unsigned类型。
如何比较浮点数?使用最小误差。
float a = 10.2;
float b = 9;
float c = a - b;
if(fabs(c - 1.2) < 0.000001){
printf("%f == 1.2\n",c);//1.200000 == 1.2
}
在误差范围内认为相等。即绝对值差小于精度最小值。
float
浮点数误差通常为1-6(6位有效数字)。
double
浮点数误差通常为1-15(15位有效数字)。
<3>浮点类型选择
- 大多数情况下使用double。
- 尽量不要使用float。
- 过程运算可以使用long double。
既然浮点数这么不准确,为什么还需要?
浮点数通过损失精度,获取更大的表示范围。
(6)字符类型
字符类型是一个特殊类型,是整型的一种。使用单引号表示字符字面量,例如:字母'a'、数字'1'、空字符''、转义字符\n。
- 通常使用%c作为格式化占位符输入输出,有时也可以使用%d输出字符对应ASCII编码。(C语言中字符即数字。)
<1>ASCII编码
ASCII编码使用7 位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字0 到9、标点符号, 以及在美式英语中使用的特殊控制字符。
- ASCII表的特点:
1.字母在ASCII表中是顺序排列的。
2.大写字母和小写字母是分开排列的。
<2>运算
- 字符类型可以像整型一样参与运算。
1.一个字符加上一个数字得到ASCII表中对应新字符。
2.两个字符相减,得到这两个字符在表中的距离。
<3>转义字符/逃逸字符
在ASCII表中,除了常见的字符(如:大小写字母、数字等),还包含一些无法打印出来的控制字符或特殊字符。这些字符以反斜线\开头,后面接着一个字符,这种字符被称作转义字符/逃逸字符。
(7)布尔类型
在C99中新增bool类型表示逻辑值,它只有两种值(true和false)
。使用前需要加上头文件stdbool.h
。
(8)数据类型转换
<1>自动类型转换
当运算符左右两边操作数的类型不一致时,会自动转换成较大类型。
- 整型:
char
→short
→int
→long
→long long
- 浮点型:
int
→float
→double
→long double
<2>强制类型转换
当把一个较大的类型转换成较小的类型,需要强制转换。
强制转换语法:
(转换后的类型)值
-
浮点数转整数采用的是截断方式。
printf("%d\n",(int)3.14);//3
-
整型转浮点型也需要强制转换。
printf("%f\n",(double)2);//2.000000
4.运算符
(1)算术运算符
+ 、-、*、 /(取整)、 %(取余)
(2)关系运算符
==、!=、>、 <、 >=、 <=
(3)逻辑运算符
&& 、 || 、 !
闰年判断:year%4==0&&year%100!=0 || year%400==0(year能被4整除 and 不能被100整除 or year能被400整除 )
(4)复合赋值运算符
+=、-=、*=、/=、%=
(5)自增、自减运算符
自增运算符与自减运算符优先级高于算术运算符。
<1>前缀自增/自减
++a、--a
<2>后缀自增/自减
(6)运算的优先级顺序
- 自增/自减的优先级大于算数运算符的优先级;
- 自增/自减的优先级大于解引用
*
的优先级 - 中括号[ ]的优先级大于解引用
*
的优先级 - 结构体中用点
.
的优先级大于取地址&
的优先级 - 结构体中用点
.
的优先级大于接引用*
5.变量
初始化和赋值:
初始化时在生成变量时放入数值,赋值是在已经生成变量时放入数值。
6.控制语句
(1)条件判断
<1>if-else
代码块与if之间使用空格或者Tab缩进,不影响编译和执行,只是为了提高代码可读性。
if(condition1){
}else if(condition2){
}else{
}
<2>switch case
switch(表达式){
case 整型常量1:
/* 表达式等于整型常量1执行的代码 */
break; /* 可选的 */
case 整型常量2:
/* 表达式等于整型常量2执行的代码 */
break; /* 可选的 */
default : /* 可选的 */
/* 表达式不等于上面所有情况执行的代码 */
}
(2)循环
<1>while语句
while(条件){
/* 如果条件为真将重复执行的语句 */
}
<2>do-while
do {
/* 如果表达式为真将重复执行的语句 */
}while(条件);//注意while()后的分号;。
do-while循环是先循环后判断,循环体至少执行一次;while循环是先判断后循环,循环体可能一次也不执行。
<3>for
for (初始值;条件;递增或递减){
/* 如果条件为真将重复执行的语句 */
}
在while和for循环中,break是结束一个循环体;continue是结束单次循环。
(3)简化
<1>省略大括弧
如果if语句、while语句、for语句中只有一个执行语句,可以省略大括弧。
<2>三元运算符:?
如果if-else语句只有单个执行语句,可以使用三元运算符:?。
7.进制
(1)转换
<1>十进制转R进制
十进制转任何进制用短除法。从下往上来统计。
<2>R进制转十进制
加权和。
0x2A=2*16^1+A*16^0=32+10=42
代码:
2进制数1011转10进制:1
(1*2)+0)*2+1)*2+1)=11
int res=0;
res=res*2+每一位
(2)C语言中的进制
<1>进制常量表示
C语言不能直接表示二进制常量。八进制数字以0开头,十六进制数字以0x或0X开头。
<2>进制打印(输出)
进制的输出其实与字符输出是一样的,根据占位符的不同输出不同。
- 十进制:%d
- 八进制:%#o
- 十六进制:%#x
char a = 'a';
printf("%c\t%d\t%#o\t%#x\n",a,a,a);//a,97,0141,0x61
<3>进制输入
- 10进制:%d
- 8进制:%o
- 16进制:%x
scanf("%o",&n);//010
printf("%d\n",n);//8
scanf("%x",&n);//0xa
printf("%d\n",n);//10
<4>%i
%i 可以匹配八进制、十进制、十六进制表示的整数。
例如: 如果输入的数字有前缀 0(018),%i将会把它当作八进制数来处理,如果有前缀0x (0x54),它将以十六进制来处理。
scanf("%i",&n);//0xa
printf("%d\n",n);//10
8.文件操作
(1)文件输入输出
- 使用
printf()
和命令行重定向>
实现文件输出; - 使用
scanf()
和命令行重定向<
实现文件输入。
<1>实例
hello.c
char name[256];
scanf("%s",name);
printf("Hello %s\n",name);
-
编译
gcc hllo.c -o hello
-
执行
echo zhangsan > namefile //把张三重定向到namefile文件中
./hello < namefile > output //把输入定向到./hello 中,然后把执行结果定向到output中。
(2)文件的打开和关闭fopen和fclose
- 头文件
<stdio.h>
<1>打开文件fopen
FILE *fopen(const char *pathname, const char *mode);
- 参数
No. | 参数 | 作用 |
---|---|---|
1 | filename | 需要打开的文件 |
2 | mode | 文件打开方式 |
- 返回值
No. | 类型 | 说明 |
---|---|---|
1 | 成功 | 返回值是指向这个文件流的文件指针 |
2 | 失败 | NULL |
- 打开方式
No. | 打开方式 | 含义 |
---|---|---|
1 | r(read) | 读 |
2 | w(write) | 写,不存在创建,存在清空写入(w),追加(a) |
3 | a(append) | 追加 |
4 | +(plus) | 读或写,主要是配合r、w、a使用 |
5 | t(text) | 文本文件(默认) |
6 | b(binary) | 二进制文件 |
<2>关闭文件fclose
int flcose(FILE* stream);
- 参数
stream
文件指针。 - 返回值
如果成功释放,返回0
; 否则返回EOF(-1)
.
(3)文本读写fprintf()
和fscanf()
<1>函数原型
int printf(const char *format, ...);
int fprintf(FILE *stream, char *format, argument...);
int fscanf(FILE *stream, char *format, argument... );
fprintf()
/fscanf()
与printf()
/scanf()
使用非常相似,区别在于fprintf()/fscanf()
第一个参数stream
是文件描述符。
<2>用法示例
- 从文件中读出数据
int i ;
float f ;
char c;
char str[10];
fscanf(fp, "%d %f %c %s\n", &i, &f, &c, str); //字符串是不需要加&
- 将数据写入文件
int i = 10;
float f = 3.14;
char c = 'C';
char str[10] = "haha";
fprintf(fp, "%d %f %c %s\n", i, f, c, str);
如果不需要从文件里面写入字符串,那么就可以用逗号或者其他符号来分隔;如果文件里需要写入字符串,那么字符串与其他数据之间只能用空格和回车来分隔。
<3>实例
- 将多个学生信息写入文件并读出。
struct Student {
char name[32]; //姓名
int age; //年龄
float score; //成绩
};
int main() {
//打开文件
FILE* fp =fopen("./info.txt","r");
if(NULL==fp) {
perror("文件fp打开失败");
return 1;
}
//从文件中读取数据
int n;
fscanf(fp,"%d",&n);
struct Student student[n];
for(int i=0; i<n; ++i) {
fscanf(fp,"%s %d %f",student[i].name,&student[i].age,&student[i].score);
}
//将数据写入文件
FILE* fq=fopen("./res.txt","w");
if(NULL==fq) {
perror("fq打开失败");
return 1;
}
for(int i=0; i<n; ++i) {
fprintf(fq,"%s\t%d\t%f\n",student[i].name,student[i].age,student[i].score);
}
//关闭文件
fclose(fq);
fclose(fp);
}
(4)二进制读写:fread()和fwrite()
对于二进制文件的数据读取和写入是一对,只有以二进制的方式写入到文件才能读出来,不能从文本文件中读取数据。
<1>函数原型
size_t fread(void *ptr, size_t size, size_t count, FILE* stream);
size_t fwrite(void *ptr, size_t size, size_t count, FILE* stream);
- 参数
No. | 参数 | 作用 |
---|---|---|
1 | ptr | 一个指针,在fread()中是从文件里读入的数据存放的地址;在fwrite()中是写入到文件里的数据存放 的地址。 |
2 | size | 每次要读写的字节数 |
3 | count | 读写的次数 |
4 | stream | 文件指针 |
- 返回值
成功读取/写入的字节数。
<2>用法示例
- 从文件中读出字符串
char str[100];
fread(str, sizeof(str), 1, fp);//每次读取sizeof(str)个字节,读一次
或者:
fread(str,1,sizeof(str),fp);//每次读取1个字节,读取sizeof(str)次,和上面一样。
- 将字符串写入文件
char str[] = "Hello World";
fwrite(str, sizeof(str), 1, fp);
或者:
fwite(str,1,sizeof(str),fp);
<3>实例
- 往文件中写数据(结构体)
typedef struct {
char name[32]; //姓名
int age; //年龄
float score; //成绩
}Student;
int main(){
FILE* fp = fopen("./1.txt","wb");
if(NULL==fp){
perror("open failed");
return 1;
}
Student student[2];
strcpy(student[0].name,"张三");//结构体中的字符串赋值要使用strcpy
student[0].age=20;
student[0].score=92.8;
strcpy(student[1].name,"李四");
student[1].age=22;
student[1].score=76.2;
for(int i=0;i<2;++i){
fwrite(&student[i],sizeof(Student),1,fp);
}
fclose(fp);
}
- 从文件中读数据(结构体)
int main() {
//打开文件
FILE* fp =fopen("./1.txt","rb");
if(NULL==fp) {
perror("文件fp打开失败");
return 1;
}
Student student;//由于不知道读取的个数,所以使用while循环
while(fread(&student,sizeof(Student),1,fp)){//读到数据就显示,否则就退出
printf("%s\t%d\t%f\n",student.name,student.age,student.score);
}
fclose(fp);
}
(1)写操作fwrite()后必须关闭流fclose()。
(2)不关闭流的情况下,每次读或写数据后,文件指针都会指向下一个待写或者读数据位置的指针。
(3)sizeof和strlen的区别,否则就会读不到数据(在读数据时,使用strlen(buf))或者读到一半数据(写数据时,使用sizeof(msg)=8)。
(4)对于二进制文件的写入和读取是同时存在的,不能从普通文件中读取数据。
(5)其他类型数据的读取:https://www.cnblogs.com/xudong-bupt/p/3478297.html
<4>二级制文件和文本文件
比较 | 文本 | 二进制 |
---|---|---|
优势 | 便于人类读写,跨平台 | 文件较小,机器读写比较快 |
劣势 | 文件较大,机器读写比较慢 | 不便人类读写,不跨平台 |
配置 | Unix用文件 | Windows用注册表 |
- 说明:
(1)Unix喜欢用文本文件来做数据存储和程序配置。
(2)windows喜欢用二进制文件。
(3)数据量较多使用数据库
(4)多媒体使用二进制
(5)通常使用第三方库读写文件,很少直接读写二进制文件。
(5)文件定位:ftell()
和fseek()
<1>函数原型
// 获取位置
long ftell(FILE* stream);
// 设置位置
int fseek(FILE* stream,long offset,int whence);
- 参数
No. | 参数 | 含义 |
---|---|---|
1 | stream | 文件指针 |
2 | offset | 偏移量,基于起始点偏移了offset个字节 |
3 | whence | 起始点 |
No. | whence |
数值 | 含义 |
---|---|---|---|
1 | SEEK_SET | 0 | 从头开始 |
2 | SEEK_CUR | 1 | 从当前开始 |
3 | SEEK_END | 2 | 从结束开始 |
- 返回值
ftell()
返回文件指针当前位置,基于文件开头的偏移字节数。
<2>示例:
fseek(stream, 0, SEEK_END);
// 将文件指针指向文件结尾,并偏移了 0 个字节,也就是直接将文件指针指向文件结尾
fseek(stream, -10, SEEK_CUR);
// 将文件指针指向当前位置,并偏移了 -10 个字节,也就是将文件指针往前移动10个字节
应用
获取文件大小。
char path[1024];
scanf("%s",path);
FILE* fp = fopen(path,"r");
if(fp){
fseek(fp,0,SEEK_END);
long size = ftell(fp);
printf("%ldB\n",size);
}
(6)文件结尾判断feof()
<1>函数原型
int feof(FILE* stream);
- 返回值
一旦文件指针指向文件结尾,就返回一个真值;否则返回非真值。
(7)返回开头rewind()
<1>函数原型
void rewind(FILE* stream);
<2>举例
FILE *fp = fopen("./text.txt", "r+");
fseek(fp, 0, SEEK_END); // 将文件指针指向文件结尾
long len = ftell(fp); // 获取文件指针位置,得到文件的大小(Byte)
rewind(fp); // 将文件指针重新指向文件开头
(8) 清空数据流fflush()
<1>函数原型
void fflush(FILE* stream);
<2>使用
在使用多个输出函数连续进行多次输出时,有可能发现输出错误。因为下一个数据再上一个数据还没输出完毕,还在输出缓冲区中时,下一个printf就把另一个数据加入输出缓冲区,结果冲掉了原来的数据,出现输出错误。 在 prinf();后加上fflush(stdout); 强制马上输出,避免错误。
(9)文件重名名rename
int rename(const char *old_filename, const char *new_filename);
(10)文件删除remove
int remove(char * filename);
(11)putc
和getc
<1>函数原型
int getc ( FILE * stream );//从stream中获取一个字符
int putc ( int character, FILE * stream );//将字符写入到stream中
<2>实现cp命令
int main(int argc,char* argv[]){
if(3!=argc){
perror("argc error");
return 1;
}
FILE* srcfp=fopen(argv[1],"rb");
if(NULL==srcfp){
perror("srcfile error");
return 1;
}
FILE* dstfp=fopen(argv[2],"wb");
if(NULL==dstfp){
perror("dstfile error");
return 1;
}
while(!feof(srcfp)){//到达文件末尾返回true,否则返回flase
putc(getc(srcfp),dstfp);
}
fclose(srcfp);
fclose(dstfp);
}
9.宏定义
(1) 宏定义是什么?
宏是用来表示一段代码的标识符。
宏也是标识符,也要满足标识符的规则。但通常习惯使用大写字母和下划线命名。
(2)宏定义怎么用?
<1>宏定义通常有三种用法:
- 当作常量使用。
- 当作函数使用。
- 编译预处理。
<2>宏定义常量
- 预定义宏
ANSI C标准有定义好的宏定义,称为预定义宏。这些宏定义以双下划线__
开头结尾。
printf("%s:%d",__FILE__,__LINE__);//源文件名:当前语句所在的行号
printf("%s:%s",__DATE__,__TIME__);//月 日 年 时间
- 自定义宏
除了使用标准定义的宏,可以使用#define
指令用来定义一个宏。
(1)语法
#define 标识符 值
#define PI 3.1415926
(2)说明
-
注意没有结尾的分号,因为不是C的语句。
-
名字必须是一个单词,值可以是各种东西。
-
在C语言的编译器开始之前,编译预处理程序会把程序中的名字换成值,是完全的文本替换。
-
如果一个宏的值有其他宏的名字,也会被替换
#define PI_2 2*PI
-
如果一个宏的值超过一行,最后一行之前行末需要加
\
#define PI_2 2 \ * \ PI
-
宏的值后面出现的注释不会被当做宏的值的一部分。
#define PI_2 2*PI // 二倍的PI
<3>带参数的宏
宏可以带参数,使用上有些像函数。这种宏称为带参数的宏。
- 语法
#define 标识符(参数...) 代码
示例:
#define square(x) ((x)*(x))
#define cube(x) ((x)*(x)*(x))
- 错误示范
#define square(x) x*x
square(10);//100
square(10+1);//10+1*10+1=21
- 说明
上面因为缺少括号导致错误,称为宏定义边际效应,所以带参数的宏需要在以下两个位置加上括号:
(1)参数出现的每个地方都要加括号。
(2)整个值要加括号 - 参数的宏也可以有多个参数
#define MIN(a,b) ((a)<(b)?(a):(b))
swap函数:
#define SWAP(m,n) {\
int t=m;\
m=n;\
n=t;\
}
尽量避免使用宏定义。
<4>编译预处理
有时我们会使用没有值的宏,这种宏用于条件编译的,#ifdef #ifndef
用于检查宏是否被定义过。控制代码的编译。
#define TEST
#ifdef TEST
printf("Test\n");//如果前面定义了:#define TEST,则执行这个,否则执行后面的。
#else
printf("No Test\n");
#endif
(3)宏展开
宏的本质是指编译前(编译预处理阶段),用定义中的值或者代码完全替换宏的标识符。
只替换条件编译中的宏。使用下面指令来查看宏的展开。
gcc -E hello.c -o hello.i
(4)编译预处理指令
以#
开头的都是编译预处理指令。除了宏定义,还有文件包含#include
和条件编译指令#if、#ifdef #ifndef、#else、#elif、#endif
,一般写在.h
文件中。
- 文件包含
#include
,把文件内容包含到代码中。 - 条件编译指令,根据编译条件,选择编译或者编译某段代码。
- 格式
#ifndef __HELLO_H
#define __HELLO_H
函数声明(函数原型)
#endif //__HELLO_H
10.头文件
(1)经验
编写小的程序可以把代码写在一个文件中,当编写大程序中,需要把代码分在多个文件中。
- 多个源代码文件
(1)main()里面代码太长适当分成几个函数。
(2)一个源代码文件太长适当分成几个文件。
(3)两个独立的源代码文件不能编译成可执行文件。
(2)头文件概念
<1>#include
指令
把函数原型放到一个头文件.h
中,在需要调用这个函数的源代码文件.c
时,使用#include
指令包含这个头文件,使编译器在编译的时候知道函数的原型。
<2>头文件作用
头文件主要用于编译器的编译阶段,告诉编译器在代码中使用了这么一个函数。只是告诉编译器函数的原型,保证调用时参数类型和个数正确。
(3)头文件的使用
在使用和定义函数的地方都要#include
头文件,#include
指令不一定要放在.c
文件的最前面,但是通常习惯这样做。
#include
指令分类
#include
指令有两种形式:
(1)#include <>
:编译器到指定目录查找,主要用于查找标准库的头文件。
(2)#include ""
:编译器先到当前目录查找(.c文件所在目录),如果没有再到指定目录查找。
#include
指令是一个编译预处理指令,和宏一样,在编译之前就处理了。它会把指定的文件原封不动的插入到它所在的地方。
(4)头文件怎么写
头文件通常用来存放所有对外公开的函数的原型和全局变量的声明。
通常任何.c文件都有对应同名的.h文件
<1>声明
- 常见的声明
- 函数声明
- 变量声明
- 结构体声明
- 宏声明
- 枚举声明
- 类型声明
通常声明只能可以放在头文件中,否则,编译器连接会出现重名函数错误。
- 重复声明
在一个编译单元中,不允许重复声明,尤其是结构体声明。为了防止头文件不被多次#include导致重复声明。 -
定义与声明
声明是不产生代码的语句。定义是产生代码的语句。
int i;// 变量的定义,在.c文件中
extern int i; // 变量的声明,在.h文件中
<2>标准头文件结构
避免头文件多次包含,必须使用标准头文件结构。
#ifndef _文件名_H__
#define _文件名_H__
// 声明
#endif
使用条件编译和宏,保证头文件在一个编译单元中只会#include
一次。
#pragma once指令也起到相同作用,但是并不是所有编译器支持。
-
分析
在cord.cpp 中第一次遇到coordin.h的文件的时候,它的内容会被读取,找到#ifndef COORDIN_H_,并且会给COORDIN_H_设定一个值,之后再次看到coordin.h头文件时,COORDIN_H_就已经被定义了,coordin.h的内容就不会再次被读取了。
- 误区:
(1)#include
不是用来引入库的,是告诉编译器函数声明,
(2)头文件只有函数原型,函数实现在.a(Unix)或者.lib(Windows)中。
(3)现代的C语言编译器会默认引入所有的标准库。
11. 变量的作用域和生存周期
(1)作用域
<1>作用域是什么?
在什么范围内可以访问这个变量。
<2>作用域怎么用?
局部变量的作用域在变量定义的大括号以内。
(2)生存周期
<1>生存周期是什么?
变量什么时候出现到什么时候灭亡。对于局部变量,生存期与作用域一致。
<2>使用
不要返回局部变量的地址(注意是地址,不是值),返回局部变量的值是可以的。
- 认识
string& test_str(){
string str = "test";
return str;
}
int main(){
string& str_ref = test_str();
cout << str_ref << endl;
return 0;
}
A.编译警告
B.返回局部变量的引用,运行时出现未知错误
C.正常编译且运行
D.把代码里的&都去掉之后,程序可以正常运行
- 分析:
ABD。
(1)在C语言中,局部变量是分配在栈空间上的, 当函数调用结束后,由编译器释放.
(2)通过调用test_str得到了他的局部变量的内存地址, 然而在main函数中调用函数时,这个内存地址被”破坏”了,类似于野指针。在c语言中,一种典型的错误就是将一个指向局部变量的指针作为函数的返回值
(3) 如果返回指针(变量地址),应该返回堆区或者全局区的地址(全局变量或者静态变量)
(3)同名隐藏
在相同作用域中,同名变量会报错;在不同的作用域中,内部变量会隐藏外部变量,
int main() {
int n = 1;
{
printf("n = %d\n",n);//1
int n=10;
printf("n = %d\n",n);//10
n = 20;
}
printf("n = %d\n",n);//1
}
12.变量分类
(1)本地变量/局部变量
<1>概念
在大括号内定义的变量就是本地变量/局部变量。
<2>特点
- 1.本地变量是定义在代码块内的,可以定义在函数的块内,可以定义在语句的块内(for),可以定义在一个随意的大括弧里面。
- 2.程序进入块前,内部变量不存在,离开时失效。
- 3.块外定义的变量,块内仍然有效。(反过来不行)
函数的每次运行,都会产生一个独立的变量空间,在这个空间中的变量,是函数这次运行独有的。
1.定义在函数内部的变量就是本地变量;2.参数也是本地变量
<3>初始化
- 1.本地变量不会默认初始化
- 2.参数在进入函数时被初始化。
本地变量/局部变量的生存期和作用域都是在大括号内。
(2)全局变量
<1>定义
定义在函数外面的变量称为全局变量。
int n;//全局变量
int main(){
int m;//局部变量
}
<2>特点
全局变量有全局的生存周期和作用域。
- 1.不属于任何函数。
- 2.所有函数内部都可以使用。
<3>初始化
- 没有初始化的全局变量会自动初始化为0。
- 只能用编译时刻已知的值初始化全局变量。(只能用常量来初始化全局变量)
- 初始化发生在main()前。
<4>同名隐藏
如果函数内部存在与全局变量同名的变量,则全局变量被隐藏。
全局变量有全局的生存周期和作用域。
(3)局部静态变量
<1>定义
在本地变量定义时加上static变成静态本地变量。(只初始化1次)
<2>特点
当函数离开时,静态局部变量会继续存在并保存其值。
int inc(){
static int n = 1;
n = n + 1;
return n;
}
int main(){
printf("%d\n",inc());//2
printf("%d\n",inc());//3
printf("%d\n",inc());//4
}
(1)静态本地变量的初始化在第一次进入函数时执行,以后进入函数会保持离开的值。
(2)静态本地变量是特殊的全局变量,具有全局生存周期和局部作用域。
(4)全局静态变量
<1>定义
在全局变量前加上关键字static。
<2>全局变量与全局静态变量区别
- 1.若程序由一个源文件构成时,全局变量与全局静态变量没有区别。
- 2.若程序由多个源文件构成时:
非静态的全局变量的作用域是整个源程序,非静态的全局变量在各个源文件中都是有效的,而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。 - 3.非静态全局变量具有外部链接的静态,可以在所有源文件里调用,除了本文件,其他文件可以通过extern的方式引用。
extern int a,b;//在.h文件中全局变量的声明
(5)变量的作用域和生命期总结
- 作用域
变量或函数在运行时候的有效作用范围 。 -
生命期
变量或函数在运行时候的没被销毁回收的存活时间。
(6)static
关键字小结
static
在C语言里面既可以修饰变量,也可以修饰函数。
<1>static
变量
- 静态局部变量:在函数中定义的,生命周期是整个源程序,但是作用域和局部变量没区别。只能在定义这个变量的函数范围内使用,而且只在第一次进入这个函数时候被初始化,之后的初始化会跳过,并保留原来的值。退出这个函数后,尽管这个变量还在,但是已经不能使用了。
- 静态全局变量:全局变量本身就是静态存储的,但是静态全局变量和非静态全局变量又有区别:
1.全局变量:变量的作用域是整个源程序,其他源文件也可以使用,生命周期整个源程序。
2.静态全局变量:变量的作用域范围被限制在当前文件内,其他源文件不可使用,生命周期整个源程序。
静态变量的生命周期是整个源程序,而且只能被初始化一次,之后的初始化会被忽略。(如果不初始化,数值数据将被默认初始化为0, 字符型数据默认初始化为NULL)。
<2>static
函数(内部函数)
只能被当前文件内的其他函数调用,不能被其他文件内的函数调用,主要是区别非静态函数(外部函数)。
1.在函数前面加上
static
就使它成为只能所在编译文件中使用的函数。
2.在全局变量前加上static
使它成为只能所在编译文件中使用的全局变量
(7)实践经验
<1>不要返回本地变量的指针
- 返回本地变量的地址是危险的。
- 返回全局变量或静态本地变量的地址是安全的。
- 返回函数内的动态内存是安全的,但注意要记得释放。
- 最好的做法是返回传入的指针。(常用)
<2>慎用静态变量
- 不要使用全局变量在函数间传递参数和结果。
- 尽量避免使用全局变量。
- 使用全局变量和静态本地变量的函数是不可重入的,是线程不安全的。
13.内存
(1) 结构体字节对齐
在C语言里,结构体所占的内存是连续的,但是各个成员之间的地址不一定是连续的。所以就出现了"字节对齐"。
字节对齐默认原则
- 结构体变量的大小,一定是其最大的数据类型的大小的整数倍,如果某个数据类型大小不够,就填充字节。
- 结构体变量的地址,一定和其第一个成员的地址是相同的。
struct Box{
int height;
char a[10];
double width;
char type;
};
int main(void) {
struct Box box;
printf("box = %p\n", &box);
printf("box.height = %p\n", &box.height);
printf("box.a = %p\n", box.a);
printf("box.width = %p\n", &box.width);
printf("box.type = %p\n", &box.type);
printf("box = %ld\n", sizeof(box));
}
(2)内存四区
image.png<1>栈区(stack)
由编译器自动分配和释放,主要是存放函数参数的值,局部变量的值。
比如:int a; int *p; 这儿的a和p都存放在栈中
<2>堆区(heap)
由程序员自己申请分配和释放,需要malloc()、calloc()、realloc()
函数来申请,用free()函数来释放如果不释放,可能出现指针悬空/野指针。
函数不能返回指向栈区的指针,但是可以返回指向堆区的指针。
<3> 数据区(data)
** 数据区在程序结束后由操作系统释放**
- 存放常量。
包含字符串常量和其他常量。 char *p = "I love u"; 指针p指向的这块内存属于常量区。 - 存放全局变量和静态变量。
初始化的全局变量和静态变量在一块区域(data段);未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,称作BSS(Block Started by Symbol:以符号开始的块);
<4>代码区(code)
用于存放编译后的可执行代码,二进制码,机器码。
int a; //申请栈区内存
a = 4; //指向的代码,放在代码区。
(3)堆和栈的区别(重要)
No. | 比较方面 | 栈 | 堆 |
---|---|---|---|
1 | 管理方式 | 由系统自动管理,以执行函数为单位 | 由程序员手动控制 |
2 | 空间大小 | 空间大小编译时确定(参数+局部变量) | 具有全局性,总体无大小限制。 |
3 | 分配方式 | 函数执行,系统自动分配;函数结束,系统立即自动回收。 | 使用new/malloc()手动申请;使用delete/free()手动释放 |
4 | 优点 | 使用方便,不需要关心内存申请释放。 | 可以跨函数使用。(可以返回指向堆区的指针) |
5 | 缺点 | 只能在函数内部使用。 | 容易造成内存泄露。 |
(4)显示目标文件区段大小
- size命令:
dec与hex是前面三个区域的和,dec是十进制,hex是十六进制。
- 各区段的含义
No. | 区段 | 名称 | 含义 |
---|---|---|---|
1 | text | 代码段(code segment/text segment) | 存放程序执行代码的内存区域。该区域的大小在运行前已确定,且通常属于只读。可能包含一些只读的常数变量,例如字符串常量等。 |
2 | data | 数据段(data segment) | 存放程序中已初始化的全局变量的内存区域。数据段属于静态内存分配。 |
3 | bss | BSS段(bss segment) | 存放程序中未初始化的全局变量的内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。 |
- 没有显示的区段
No. | 区段 | 含义 |
---|---|---|
1 | 栈(stack) | 存放程序临时创建的局部变量,也就是函数括弧{}中定义的变量(不包括static声明的变量)。在函数被调用时,参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。 |
2 | 堆(heap) | 存放程序动态分配的内存段,大小并不固定,可动态扩张或缩减。当调用malloc()等函数分配内存时,堆被扩张;当调用free()等函数释放内存时,堆被缩减。 |
14.二进制
(1)位运算
位运算说穿了,就是直接对整数在内存中的二进制位进行操作.
<1>按位运算
No. | 操作符 | 功能 |
---|---|---|
1 | & | 按位与 |
2 | | | 按位或 |
3 | ~ | 按位取反 |
4 | ^ | 按位异或(相异为真) |
<2>运算规则
<3>按位与
1.让某一位或某些位为0(清零)
int n = 0xFFFF;
n = n & 0x0010;//0x10
2.取一个数中某些指定位:
比如a=23,我想取a的二进制的后面4位数,那么可以找一个后4位是1其余位是0的数b,即b=0x0f(十六进制,转换为二进制为00001111),a&b就得到了a的后四位。
a:00010111
b:00001111
a&b:00000111
3.保留指定位:
比如a=23(用8bit表示),我想保留其二进制的第4和第6位(最左边为第1位),其余位置0。那么可以找一个第4和第6位是1其余位是0的数b与a进行按位与运算.
a:00010111
b:00010100
a&b:00010100
4.应用:
- 判断某一位是否为1;
设置一个只有某一位是1的数,其余为是0,然后和判断的数进行位与。 - 判断一个数是否是偶数;
与1进行位与,如果其二进制的最末尾是0表示偶数,为1表示奇数。
结论:任何二进制位与0能实现置0;与1保持原值不变.
<4>按位或
1.让某一位或某些位为1,其余位不变。
int n = 0x0000;
n = n | 0x0010;
2.拼接两个二进制数。
int a = 0xab00;
int b = 0x0012;
int c = a|b;//0xab12
<5>按位取反
1,得到全部为1的数字~0
int n = ~0;// 等同于0xFFFF
2.使数字的部分清零x& ~7。
int n = 0xFFFF;
n = n & ~7;
<6>按位异或
1.两个相等数异或结果为0。
int n = 0x1234;
n = n^n;
2.对同一个变量两次异或相同值,变回原值。
int a = 0x1234;
int b = 0x1357;
a = a^b;//0x163
a = a^b;//0x1234
3.0和任何数字(或字符)异或都为任何数(或字符)
int n=21;
n = n^0;//21
4.应用:
- 把一个数据的某些位翻转,即1变为0,0变为1
如要把a的奇数位翻转,可以对a和b进行“按位异或”运算,其中b的奇数位置为1,偶数位置为0。 - 交换两个值,不用临时变量(***)
x ^= y;
y ^= x;
x ^= y;
- 加密解密
加密程序(a^b),解密程序是加密程序的逆过程,这里的加密和解密程序是完全相同的,原因是(a^b)^b=a。
5.例题
[136.只出现一次的数字]:
思路:每个数字全部异或,相同的会为0,直到最后一个数字。
[389.找不同]:
思路:字符也可以异或,相同字符异或为0.
逻辑运算与按位运算
1.逻辑运算结果只有0和1两种值,按位运算有多种值。
2.逻辑运算相当于把所有的非零值都变成1,再按位运算。
(2)移位运算
在移动过程中相当于操作二进制数
No. | 操作符 | 功能 |
---|---|---|
1 | << | 左移 |
2 | >> | 右移 |
<1>左移
i<<j
表示i
中所有位向左移动j
个位置,右边填入0
。
左移一位相当于乘以2,两位乘以4。
1<<1;//2
1<<2;//4
1<<3;//8
<2>右移
i>>j
表示i
中所有位向右移动j
个位置,对于unsigned
类型,左边填入0
;对于signed
类型,左边填入符号位。
右移一位相当于除以2,再取整。
14>>1;//7
14>>2;//3
14>>3;//1
14>>4;//0
1.应用:
-
循环移位的实现:
将一个无符号整数x的各位进行循环左移n位的运算,即把移出的高位填补在空出的低位处。b=(a<<n) | (a>>(16-n)) ;
-
求x的绝对值
y = x >> 31 ;//二进制最高位
return (x^y)-y ; //or: (x+y)^y
<3>位移运算与乘除运算
<4>综合
求一个无符号数的二进制中1的个数。
//就是判断最低位是否为1(最右边)
int numof1(int n){
int count=0;
while(n){
if(n & 1){
++count;
}
n = n >> 1;
}
return count;
}
(3)位域
<1>定义
位域是又称作位段,是把一个字节中的二进位划分为几个不同的区域。
<2>作用
节省空间,有些信息不需要占用一个完整的字节。
<3>使用
- 1.定义位域
定义位域与结构定义相仿。
struct 位域结构名{
类型 位域名:位域长度;
};
为了保证位域的可以移植性,成员类型通常为unsigned int
和int
,C99可以使用bool
。
示例:
struct Byte{
unsigned int b1:1;
unsigned int b2:1;
unsigned int b3:1;
unsigned int b4:1;
unsigned int b5:1;
unsigned int b6:1;
unsigned int b7:1;
unsigned int b8:1;
};
-
2.位域变量
定义和使用位域变量与结构体相同。每个域有一个域名,允许在程序中按域名进行操作。struct Byte a;
-
3.位域大小
整个结构体的总大小为最宽基本类型成员大小的整数倍。
网友评论