C语言完整知识体系总结

C语言的知识体系总结

这里写目录标题

序言:C语言的概述历史、特点、标准)

1、嵌入式开发为什么选择C语言?(面试题!)

嵌入式开发中操作系统是核心,需要移植,并在上层和底层做开发,而操作系统的核心是内核,所有内核的开发都采用C语言,所以。。。(嵌入式开发 - 操作系统 - 内核)

2、为什么内核开发选择C语言?

a\ 能够直接访问硬件 ( C (硬件复杂操作) VS 汇编(效率 > C)(硬件初始化) )(对地址直接做操作,指针)
b\ 运行效率(运行时语言) 运行时语言 VS 解释性语言(C VS java \ C(面向结构) VS C++ (面向对象) )
c\ 移植性

3、C语言的缺点:

a)面向结构:

代码的复用性差、代码的维护性差、代码的扩展性差

b)面向对象的特点

代码复用性:指的是可以直接调用;
代码扩展性:增加新功能时,不修改原来的代码;
代码维护性:即可修改性,让软件能随着用户需求的变更而容易改变。

c)面向结构和面向对象的区别:

面向过程(面向结构)就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。 (按照步骤来实现)
面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。(按照功能来实现)

举例:

五子棋,面向过程的设计思路就是首先分析问题的步骤:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用分别的函数来实现,问题就解决了。而面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为 1、黑白双方,这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的i变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。
可以明显地看出,面向对象是以功能来划分问题,而不是步骤。同样是绘制棋局,这样的行为在面向过程的设计中分散在了总多步骤中,很可能出现不同的绘制版本,因为通常设计人员会考虑到实际情况进行各种各样的简化。而面向对象的设计中,绘图只可能在棋盘对象中出现,从而保证了绘图的统一。功能上的统一保证了面向对象设计的可扩展性。比如我要加入悔棋的功能,如果要改动面向过程的设计,那么从输入到判断到显示这一连串的步骤都要改动,甚至步骤之间的循序都要进行大规模调整。如果是面向对象的话,只用改动棋盘对象就行了,棋盘系统保存了黑白双方的棋谱,简单回溯就可以了,而显示和规则判断则不用顾及,同时整个对对象功能的调用顺序都没有变化,改动只是局部的。
再比如我要把这个五子棋游戏改为围棋游戏,如果你是面向过程设计,那么五子棋的规则就分布在了你的程序的每一个角落,要改动还不如重写。但是如果你当初就是面向对象的设计,那么你只用改动规则对象就可以了,五子棋和围棋的区别不就是规则吗?(当然棋盘大小好像也不一样,但是你会觉得这是一个难题吗?直接在棋盘对象中进行一番小改动就可以了。)而下棋的大致步骤从面向对象的角度来看没有任何变化。

d)面向对象方法的思想:

整个系统被分解成对象的集合,而不是功能的集合,每个对象有自己的职责,对象之间相互协作来完成用户交给的任务。
(1)同结构化相比,它不是层次结构,在结构化中,上一层依赖下一层,下一层又依赖下下一层,只要底层改变,上层也要跟着改变,它没有很好的隔离变化.而面向对象刚很好的封装了变化,外界只需使用对象的接口,而不用管这个接口是如何实现的
(2)一个重要区别是:这里的箭头是请求,而不是数据流.在结构化方法的DFD数据流程图中,箭头代表的是数据流,也就是说一个模块的输出变成另一个模块的输入,而这里,指的是对象A请求对象B完成某项任务,也就是A调用B的方法。
从结构化到面向对象的两个误区,也就是两个极端:
1.过大的类.一个类中包含了几十个函数,这种大杂烩严重违背面向对象的单一职责原则。也就是说,一个对象要干的事应该和它的名字一致,它要干哪些事,从它的名字就应该能看出来.
2.类中只有函数,没有属性.这是陷入了功能分解的误区,只是简单的把函数组装到一起.基本没有封装。
对象的一个重要特性是:对象是有状态的(也就是属性),对象的行为与它的所处的状态密切相关。

e)结构化程序具有以下几个特征:

自顶向下,逐步细化,模块化设计,结构化编码

4、C语言实现面向对象编程?

5、C语言的开发方向

操作系统(上层(库)、底层(BSP、 驱动)、实现(内核) )、硬件、中间件(sdk)

6、精通C语言、掌握C语言

需要:
代码的累积量
阅读书籍(整理笔记时)
项目经验

7、总结:

嵌入式开发为什么选择C语言?
在嵌入式开发中,操作系统是核心,需要移植操作系统,并在上层和底层做开发,而操作系统的核心是内核,所有的内核开发都采用C语言。内核开发之所以选择C语言,是因为C相比于汇编,他可以直接访问硬件,对地址直接操作;相比于Java和C++,他的运行效率也就是运行时语言要高;C语言具有可移植性。
由于C语言是面向结构的,这也就暴露出了他的缺点:代码的复用性差、代码的维护性差、代码的扩展性差。代码的复用性指的是可以直接调用;代码的扩展性指的是增加新功能时,不用修改原来的代码;代码的维护性值得是可修改性,让软件可以随着用户需求的变更而容易改变。相对于面向结构,面向对象的特点刚好与之相反。他俩的区别就在于:面向结构是按照步骤来实现,而面向对象是按照功能来实现。
所以,嵌入式开发要选择C语言。

一、C语言基础知识点

1、机器码(运行效率、访问硬件能力)

2、C语言之父:丹尼斯-里奇

3、C语言语法标准:K&RC、c89、c99、c11(微软)

4、C语言语法版本:GNUC+ASCI C (GUNC = ASCI C +扩展)

5、gcc:100%c89 + 部分c99

二、数据类型(基本数据类型、输入输出)

1、为什么要有数据类型:

计算机中内存也是有限的,为了提高内存使用效率,不浪费空间,自然是需要设计出数据类型来划定多大数据占多大内存空间,就有了数据类型。

2、数据类型分类:

基本数据类型(内置:编译器自带的类型) :
int 4个字节(指针 8个字节:int *…)
short 2个字节
long 8个字节
long long 8个字节
char 1个字节
float 4个字节
double 8个字节
(字节长度笔试题)
复合数据类型(多个内置类型的组成的新类型) :数组、struct, union、 enum;void类型: void * (万能指针) :多态

3、定义变量(注意事项 编码规范)

a)变量的可读性:形容词_名词

int sum_ result (错误示范 int a;int b; int c; int d)(int qiuhe)

b)循环变量:int i;int k;

c)变量的类型决定了什么?

(1)占用内存空间的大小(内存是计算机内的存储部件,代码和数据都存在其中。)
(2)数据的存储形式 (在Inter 的CPU上面,这个整型数他的存储是低位字节在前高位字节在后,相当于脚先进去;实型数的存储稍微复杂一点,我们通常说是定点数,但是在计算机中保存是按照浮点数来保存的)
(3)合法的取值范围
(4)可参与的运算种类

d) 内存的特点:

所有指令和数据都保存在内存里,速度快,可随访问,但掉电即失去(掉电就没了)。

e) 内存如何编址呢?

你声明一个变量,他可能就分配了一个内存空间,那么他得有个地址。
地址是按照字节编号的,内存中每个字节都有唯一的编号。(就像我们的宿舍不是按照床位编号的,是按房间编号的,没有编制到位而是字节,每一个字节有8位,每个字节都有一个唯一的编号,这个编号就是他的地址,这个地址是一个什么样的数?他是一个整数是无符号的)
地址是一个16进制无符号整数,字长与主机相同,32位计算机的内存地址编码是32位,从0x00000000,到0xFFFFFFFF。(为什么用16进制来表示,因为16进制是2进制压缩的一个版本,用16进制来表示2进制很简单,比较短;字长与主机相同,所以32位计算机呢,他的内存地址编码就是32位;16进制一个符号代表4位,总共8个符号,4x8=32)
按变量声明的类型分配内存空间,大小类似于博士生、硕士生、本科生分配寝室。(按变量声明的类型来分配内存大小,几个字节几个字节。)

f) 如何衡量这个内存的大小呢?

计算机存储数据的最基本单元,衡量物理存储器容量的最小单位----------位,也称为比特。那一个位有多大呢?要么是0,要么是1;那我们知道了为什么计算机只能识别0和1,因为它存在里边的就是0和1,他存的不是别的就是0和1,就是二进制。

g) 为什么计算机用二进制存储,不用十进制?

第一个原因:双稳态在电器元件中容易实现;(计算机的他的内部的元器件,像三极管、二极管啊他有通和断,就像我们的电灯他有开和闭一样,他两种稳定的状态这样的电气原件很容易实现,但是你想用十进制的话,你必须找到一种电器元件,它能够有10种稳定的状态,很难找。但是两种稳态的电器原件很容易找,二极管通和断等太多了,三极管也是。)
第二个原因:计算机进行二进制运算比进行十进制运算要简单的多。(二进制的话2X2=4就4条0+0,0+1,1+0,1+1就4种。但是10进制的话你得10x10=100种)

4、基本数据类型需掌握的知识点:

a)各种数据类型的字节长度(变量占用内存的大小)int num = 5;

注:计算机表示内存大小的单位: bit位、 字节、半字、字.双字、kB、MB、G. T
8bit位= 1个字节、
16bit = 2个字节=半字
32bit = 4个字节=字
1024kB = 1MB
如何测量数据类型和变量的内存的大小?
sizeof(运算符) sizeof(变量名) sizeof (数据类型) strlen统计字符串中字符的个数
假如我们给变量赋的值超出了他的表示范围,这个时候可能会导致系统的崩溃。发生数值溢出。

b)数值溢出的危害;

(1)编译器对它熟视无睹;(某些编译器并不给你提示,所以发生的时候你没有预防)
整形溢出导致死循环

c)解决数值溢出的对策;

(1)了解处理问题的规模,选择取值范围更大的变量类型。
比如明明我可以用单精度实型我用双精度实型?那这样是不是影响他的运算效率呢?确实,你使用更大范围的、精度更高的这样类型的话,他的运行效率会受到一点点影响。
(2)不要对变量所占的内存空间字节数想当然。
(3)用sizeof获得变量或者数据类型的长度。

d)保存地址;

操作系统中地址的长度是固定长度,是有操作系统位数决定的,64位系统是64位系统是8个字节,32位系统是4个字节

e)数组的长度

数组的长度 * 元素类型的长度

f)字符串长度:int strlen(char *src);不统计‘\0’

二进制:

​ 二进制逢二进一,所有的数组是0、1组成

十进制转二进制;

除二反序取余法:将十进制数每次除以2 取出余数 按照结果倒叙依次获取结果

二进制转十进制;

权值法:将二进制数各个位数从0位开始乘以2的N幂 将各个位数的结果相加

八进制:

​八进制逢八进一,所有的数组是0到7组成

十进制转八进制;

除八反序取余法:将十进制数每次除以8 取出余数 按照结果倒叙依次获取结果

十六进制:

十六进制逢十六进一,所有的数组是0到9和A到F组成 字母不区分大小写

十进制转十六进制;

除十六反序取余法:将十进制数每次除以16 取出余数 按照结果倒叙依次获取结果

笔试题:

在这里插入图片描述
输出结果: 8 100 11 11
正数:
原码=补码
负数:
补码=原码取反+ 1;
原码=补码取反+ 1;
在这里插入图片描述

g)各种数据类型的取值范围? (计算机是以补码形式保存数据, 为了解决+0, -0问题)

注:大数计算20030434043243204324 * 32432432432432432? ? ?
笔试题
1\计算机为什么提出补码存储? +0 -0
00000000=+0
10000000=-0
I
2、printf ("%d\n",~2) ;
0000 0010
1111 1101
000 0010
+1
000 0011 = -3
‘0’ a A 0
扩展(计算机为什么用补码储存):
a)为什么不以原码形式存储?
首先,原码是站在用户角度的,是原始的二进制!
求原码:
1.用户的数字分为正负数,需要有一位存储符号
2.最高位为符号位:0为正,1为负
3.左边是高位,右边是低位
由原码的计算方式可以发现源码存储会引发2个问题:
1.0有两个存储方式
我们以char型(占1字节,8位)为例(下同):
+0: 0000 0000
-0: 1000 0000
不难发现+0和-0的原码是不一样的,而在计算过程中,+0和-0是没有任何区别的。
2. 正数和负数相加,结果不正确(计算机只会加)
1 - 1 =1 + (-1)
1: 0000 0001
-1: 1000 0001
1000 0010 = -2
很显然1-1=0,而用原码进行计算得出的结果却相差甚远!
b)为什么不以反码形式存储?
既然原码不适合作为计算机的存储方式,人们在解决这个问题的过程中又提出了反码的概念
求反码:
求原码
符号为不变,其他位取反
注意:正数原码、反码一样!
接下来我们检验一下1 - 1:
1: 0000 0001

  • 1: 1111 1110
    1111 1111 ->1000 000(转换为原码,因为原码是站在用户角度的) = -0
    不难可能出反码已经解决的正负数相加结果不正确的问题
    然后我们再检验+0、-0:
    +0 0000 0000
    -0 1111 1111
    由此看出,反码并没有解决0有两种形式的问题
    反码存储会引发1个问题:
    0有两个存储方式
    c)补码存储
    在反码的基础上,人们有提出了补码的概念
    求补码:
    补码为其反码+1
    注意:正数的原码、反码、补码都一样!
    接下来我们对补码存储进行验证:
    +0: 0000 0000
    -0: 原码: 1000 0000
    反码: 1111 1111
    补码:1 0000 0000(char占1字节八位,最高位丢弃)= 0000 0000
    可以看出补码解决了+0 -0不一样的问题
    +1: 0000 0001
    -1: 1111 1111
    1 0000 0000(最高位丢弃)=0000 0000 = 0
    那么补码也解决了正负数相加结果不正确的问题!
    8、typedef 关键字:给数据类型重命名
    a\ 解决signed,unsigned带来的代码移植性问题
    b\ 提高了代码的可读性
    c\ 提高编码效率

5、变量和常量:

1、变量的三大特点

字节长度、生命周期&作用域、存储区域

2、作用域:可见范围

局部变量:在函数体里定义的变量–所在函数(出了函数不可见)
全局变量:在函数体外定义的变量-整个全局(看不见需要extern外部声明)

3、生命周期

所在内存空间的分配-释放的过程
局部变量:所在函数体执行时,分配空间,执行结束,释放空间
全局变量:所在程序执行时,分配空间,执行结束,释放空间

4、存储区域

局部变量;存储在栈空间
全局变量;存储在数据段
栈空间局部变量未初始化默认初始化
数据段全局变量未初始化默认初始化为0
gcc 4.0以上
在这里插入图片描述
定义:分布内存空间,只能定义一次。
声明:不分配内存空间,可声明多次。

5、堆和栈的使用原则

a)如果明确知道数据占用多少内存,数据量较小的用栈,较大的用堆
b)如果不知道数据量大小(可能需要占用较大的内存),最好用堆(这样保险)
c)如果需要用动态创建数组,则用堆

6、语言类型转换

int,char:相关类型
不安全的:可以将任何类型之间转换,有可能造成数据丢失
安全的:检查两个类型是否可以转换
隐式类型转换:char->short->int->long->float->double
强制类型转换:(变量名称) 变量名

类型转换缺点

隐式类型转换 可能会因为整形提升或者数据截断导致 精度的丢失,并且有时候会因为 忽略隐式类型转换导致错误发生。
显示类型转换 代码不够清晰,没有很好的将各种情况划分开,而是全部混在一起使用。

什么情况下发生隐式类型转换?

算术运算中,低类型转换为高类型
赋值表达式中,赋值符“=”右边的变量值转换为左边变量的类型
函数调用时,实参转换为形参的类型
函数返回时,函数返回值,ruturn表达式转换为返回值类型

隐式类型转换的规则

确定二元运算中的哪个操作数要转换为另一个操作数的类型时,其机制相当简单。其基本规则是,值域较小的操作数类型转换为另一个操作数类型,但在一些情况下,两个操作数都要转换类型。

为了准确地表述这些规则,需要比上述更复杂的描述,所以可以忽略一些细节,在以后需要时再考虑他们。

编译器按顺序采用如下规则,确定要使用的隐式类型转换:

如果一个操作数的类型是long double,把另一个操作数类型转换为long double类型。
否则,如果一个操作数的类型是double,就把另一个操作数类型转换为double类型。
否则,如果一个操作数的类型是float,就把另一个操作数类型转换为float类型。
否则,如果两个操作数的类型都是带符号的整数或无符号的整数,就把级别较低的操作数类型转换为另一个操作数的类型。无符号整数类型的级别从低到高为:
signed char, short, int, long, long long
每个无符号整数类型的级别都与对应的带符号整数类型相同,所以 unsigned int类型的级别与int类型相同。
否则,如果带符号整数类型的操作数级别低于无符号整数类型的级别,就把带符号整数类型的操作数转换为无符号整数类型。
否则,如果带符号整数类型的值域包含了无符号整数类型所表示的值,就把无符号整数类型转换为带符号整数类型。
否则,两个操作数都转换为带符号整数类型对应的无符号整数类型。

7、格式化输出:

a)printf函数的使用

基本使用语法:
a)格式:printf(“格式控制串”,输出表);
b)功能:按指定格式向显示器输出数据
c)反值:正常,返回输出字节数;出错,返回EOF(-1)

b)printf函数格式控制

类型:
在这里插入图片描述
在这里插入图片描述

标志:

在这里插入图片描述

示例:

在这里插入图片描述
输出结果:
在这里插入图片描述
在这里插入图片描述

示例:

在这里插入图片描述
输出结果:
在这里插入图片描述

转义字符:

在这里插入图片描述

c)printf函数缓冲区:

在 printf 的实现中,在调用 write 之前先写入 IO 缓冲区,这是一个用户空间的缓冲。系统调用是软中断,频繁调用,需要频繁陷入内核态,这样的效率不是很高,而 printf 实际是向用户空间的 IO 缓冲写,在满足条件的情况下才会调用 write 系统调用,减少 IO 次数,提高效率。
printf(…)在 glibc 中默认为行缓冲,遇到以下几种情况会刷新缓冲区,输出内容:
(1)缓冲区填满;
(2)写入的字符中有换行符\n或回车符\r;
(3)调用 fflush(…) 手动刷新缓冲区;
(4)调用 scanf(…) 从输入缓冲区中读取数据时,也会将输出缓冲区内的数据刷新。
可使用setbuf(stdout,NULL)关闭行缓冲,或者setbuf(stdout,uBuff)设置新的缓冲区,uBuff 为自己指定的缓冲区。也可以使用setvbuf(stdout,NULL,_IOFBF,0);来改变标准输出为全缓冲。全缓冲与行缓冲的区别在于遇到换行符不刷新缓冲区。
printf(…) 在 VC++ 中默认关闭缓冲区,输出时会及时输到屏幕如果显示开启缓冲区,只能设置全缓冲。因为微软闭源,所以无法研究 printf(…) 的实现源码。
Linux 和 Windows 下的缓冲区管理可见:C的全缓冲、行缓冲和无缓冲。

d)printf函数使用技巧:

printf 与 wprintf 不能同时使用
在输出宽字符串时,发现将 printf 和 wprintf 同时使用时,则后使用的函数没有输出。这里建议不要同时使用 printf 和 wprintf,以免发生错误。
这里是因为输出流在被创建时,不存在流定向,一旦使用了 printf(多字节字符串) 或 wprintf(宽字符串) 后,就 被设置为对应的流定向,且无法更改。可以使用如下函数获取当前输出流的流定向。
通过 fwide 可以设置当前流定向,前提是未有任何的 I/O 操作,也就是当前流尚未被设置任何流定向。顺带吐槽一下,不知为何标准库函数 fwide 实现的如此受限。具体操作如下:
设置标准输出流定向为多字节流定向
设置标准输出流定向为宽字符流定向
既然 GNUC 存在这个问题,那该如何解决呢?这里有两种办法:
(1)统一使用一种函数。
(2)使用 C 标准库函数 freopen(…) 清空流定向,可以让printf(…) 与 wprintf(…)同时使用。

e)其他输出函数的使用:

putchar,putc,puts

8、格式化输入:

a)scanf函数的使用 :

格式:scanf(“格式控制串”,地址表);
功能:按指定格式从键盘读入数据,存入地址表指定存储单元,并按回车结束
反值:正常;返回输入数据个数
地址表:变量的地址,常用取地址运算符&
输入数据时,遇到以下情况认为该数据结束:
空格、TAB、回车
宽度结束
非法输入

b)scanf函数注意要点:

在 scanf 的“输入参数”中,变量前面的取地址符&不要忘记。
scanf 中双引号内,除了“输入控制符”外什么都不要写。
“输出控制符”和“输出参数”无论在“顺序上”还是在“个数上”一定要一一对应。
“输入控制符”的类型和变量所定义的类型一定要一致。对于从键盘输入的数据的类型,数据是用户输入的,程序员是无法决定的,所以在写程序时要考虑容错处理,这个稍后再讲。
使用 scanf 之前先用 printf 提示输入。用格式串中

c)scanf留 下来的垃圾:

用getchar()清除
用格式串中空格或"%*c"来"吃掉"

d)其他输入函数:

getchar,getc,gets

三、基本语句(条件语句、循环语句、多路分支、goto语句)

1、条件语句:

a) if语句:

1)成立:非零
在这里插入图片描述
在这里插入图片描述2)if后的;!!!
在这里插入图片描述在这里插入图片描述
3)判断相等时,将常量写到左面
注意:“==”判断是否相等;“=”:赋值
4)规范:
if后面必须匹配else
在这里插入图片描述
5)if条件中的运算符优先级
6)判断条件的零值比较:
if(!num)VS if( 0 == num)
布尔变量:
if(flag) 表示flag为真
if(!flag)表示flag为假
整型:
if(value == 0)
if(value != 0)
浮点类型:
假设浮点类型的名字是x
if(x == 0.0) //隐含错误的比较
转化为:
if((x >= -EPSINON)&& (x <= EPSINON))
其中,EPSINON是允许误差(即精度)
指针:
if(p == NULL) //强调了p是指针变量
if(p != NULL)
若写成
if(p == 0) //误以为p是整型变量
if(p) //误以为p是布尔变量

2、循环语句:

嵌入式中的死循环:
for( ; ; )
while( 1 )
注意事项:
使用选择:
已知循环次数用for,未知循环次数用while
循环语句的条件不能多加分号,与if判断语句一样!!!
循环体里不要直接修改循环变量的值;(循环变量在循环体中没有发生变化)死循环
循环方式:
for循环:
在这里插入图片描述while循环
在这里插入图片描述
do…while循环
在这里插入图片描述

3、多路分支:

a)使用规则:

switch 语句中的 expression 是一个常量表达式,必须是一个整型或枚举类型。
在一个 switch 中可以有任意数量的 case 语句。每个 case 后跟一个要比较的值和一个冒号。
case 的 constant-expression 必须与 switch 中的变量具有相同的数据类型,且必须是一个常量或字面量。
当被测试的变量等于 case 中的常量时,case 后跟的语句将被执行,直到遇到 break 语句为止。
当遇到 break 语句时,switch 终止,控制流将跳转到 switch 语句后的下一行。
不是每一个 case 都需要包含 break。如果 case 语句不包含 break,控制流将会 继续 后续的 case,直到遇到 break 为止。 语句中的 expression 是一个常量表达式,必须是一个整型或枚举类型。
一个 switch 语句可以有一个可选的 default case,出现在 switch 的结尾。default case 可用于在上面所有 case 都不为真时执行一个任务。default case 中的 break 语句不是必需的。

b)流程图:

在这里插入图片描述

c)注意事项:

switch语句中表达式类型只能是整型或者字符型。
case里如果没有break,那么程序会一直向下执行。

d) switch VS if:

与if语句比,对于多条件判断时,switch的结构清晰,执行效率高,缺点是switch不可以判断区间。

e) break VS continue:

当它们用在循环语句的循环体时,break用于立即退出本层循环,而continue仅仅结束本次循环(本次循环体内不执continue语句后的其它语句,但下一次循环还会继续执行。
如果有多层循环时,break只会跳出本层循环,不会跳出其他层的循环。
break可用于switch语句,表示跳出整个switch语句块,而continue则不能单独的用于switch语句。但是continue可以用于循环内部的switch语句。
break和continue语句在循环内的switch语句中使用时,是有区别的。在这种情况下的break是指跳出switch语句块(switch语句块的代码仍然执行)。而这种情况下的continue是指结束本次循环(不在执行switch后面的代码),进行下一次循环。

四、数组(-维数组、二维数组、多维数组)

1、数据认知:

a)静态分配空间(int a [100]:400个字节) int a [?] --> 空间利用率差(1、不够用;2、浪费空间)
b)所占内存空间特点:连续的(物理连接)–> malloc分配空间是否物理连接?(malloc实验原理:链表连接所有空闲的空间,组成最终分配的空间)

2、如何使用数组:

a)定义数组:
数组该定义多大?char src[1024] --> 柔性数组!!!
b)注意事项:
可变长数组c99:可用变量来定义数组的长度(不能再使用过程中已修改变量的,来扩充数组的内存空间)
c99:定义数组是,必须确定的长度;(选择方式:通常用宏定义来表述数组的大小,提高代码移植性)
注意:
数组名的命名规则和变量名相同,遵循标识符命名规则。
在定义数组时,需要指定数组中元素的个数,方括号中的常量表达式用来表示元素的个数,即数组长度。例如,src[10],表示数组a[]有10个元素。注意,下标是从0开始的,这10个元素分别是src[0]…src[9]。一定要注意这里面不会有src[10].
定义a数组有10个元素,但是花括号内只给了4个初值,这表示只给前4个元素赋初值,其余元素系统自动给其赋初0。
c)数组名的作用:
一推数组名:指针常量(元素类型的指针),保存的数组首元素的地址
二维数组名:指针常量(一维数组指针),保存首个一维数组的地址

3、一维数组:

a:数组名,指针常量,保存数组首元素的地址
&a:对数组名取地址,等于数组的地址
*(&a)= a:对一维数组的地址取值等于数组首元素的地址;整型变量的得知用整形指针变量,字符变量的地址用字符指针变量,数组的地址用数组指针变量保存
数组指针变量:变量,保存的是地址,该地址是数组的地址
int(*pa)[3];pa + 1:

4、二维数组:

二维数组的定义:不能省略行,可以省略列
aa:指针常量,保存的是首个一维数组的地址
&aa:二维数组的地址
*(&aa)= aa:对二维数组的地址取值等于首个一维数组的地址
*aa:首个一维数组的收个元素的地址
(aa + i)+ j)
aa:二维数组中首个一维数组的地址
aa + i:二维数组中第i + 1个一维数组的地址
*(aa + i):二维数组中第i + 1个一维数组的首元素的地址
*(aa + i)+j:二维数组中第i + 1个一维数组的第j + 1个元素的地址
(aa + i)+ j):二维数组中第i + 1个一维数组的第j + 1个元素的地址的值

5、三维数组:

例:int aaa[2][2][2] = { {1, 2, 3, 4},{5, 6, 7, 8}}
第一个下标:第几个二维数组
第二个下标:第几个二维数组的第几行
第三个下标:第几个二维数组的几行第几列
三维数组的作用:首个二维数组的地址
aaa:首个二维数组的首个一维数组的地址
**aaa:首二维数组的首个一维数组的首元素的地址
**aaa:首个二维数组的首个一维数组的首元系的值
&aaa:三维数组的地址
(&aaa) = aaa;对三维数组的地址取值等于首个二维数组的地址
//
(
(*(aaa +i) +j)+k)aaa + i:三维数组中第i+1个二维数组的地址
*(aaa + i):三维数组中第i+1个二维数组的首个一维数组的地址
(aaa +i) +j:三维数组中第i+1个二维数组的第j+1个一维数组的地址
((aaa + i)+ j):三维数组中第i+1个二维数组的第j+1个一维数组的首元素的地址
((aaa + i) + j)+k:三维数组中第i+1个二维数组的第j+1个一维数组的第k+1个元素的地址
((
(aaa +i) + j)+ k):三维数组中第i+1个二维数组的第j+1个一维数组的第k+1个元素的值

五、指针(一 维指针、多维指针、数组指针、函数指针、函数指针组)

1、指针的作用:

a)谈谈你对指针的理解? (指针是什么? )

语法:
指针是一种数据类型,它可以定义变量,变量保存的值是地址,由于地址是固定长度,所以指针变量的长度是固定的;不同地址的步长不一样,需要不同指针类型的变量来保存
作用:
由于指针变量可以保存地址,所以可以直接操作地址,也就是可以直接操作硬件的寄存器地址,从而实现直接访问硬件
支持的运算符:
:间接运算符
&:取地址运算符(对应的内存空间,指向的内存空间)
(“&”和“
”都是右结合的。假设有变量 x = 10,则*&x 的含义是,先获取变量 x 的地址,再获取地址中的内容。因为“&”和“*”互为逆运算,所以 x = *&x。)
p++:p对应的内存空间
(*p)++:p指向的内存空间
多级指针:保存上一级指针变量的地址(远指针(巨指针),近指针)
注:什么时候使用多级指针?
应用场景:函数传参
指针之间的运算:
赋值:指针之间的赋值:相同类型指针之间的赋值
int *p;//整型指针变量:p是一个变量。保存的是地址,该地址是整型类型;
char *p2:;//字符指针变量:p2是一个变量,保存的是地址,该地址是字符类型的地址
关系运算

b)地址为什么分为不同类型? (不同类型的指针变 量保存不同步长的地址)

地址属性:步长(什么是步长:增加1操作所移动的字节)
地址 + 操作数 = 地址 eg:0x00000001 + 1 =

c)指针变量和指针的类型:

指针变量就是一个变量,它存储的内容是一个指针。(数据类型 *指针名)

在我们定义一个变量的时候,要确定它的类型。(int 变量的指针需要用 int 类型的指针存储,float 变量的指针需要用 float 类型的指针存储。就像你只能用酒店 A 的房卡存储酒店 A 中房间号的信息一样。)

d) 指针占用内存空间大小:

与指针指向的内容和内容的大小无关。

在不同的操作系统及编译环境中,指针类型占用的字节数是不同的:

编译生成16位的代码时,指针占2个字节

编译生成32位的代码时,指针占4个字节

编译生成64位的代码时,指针占8个字节

32位系统指针占4位,64位系统指针就是占8位了

对于某一个具体的环境,可以用下面的语句精确的知道指针类型占用的字节数:

printf("%d\n", sizeof(int *));

另外,int型占用4字节(无论32位系统还是64位系统)

e)变量的指针与指针变量:

变量的指针就是变量的存储地址
指针变量就是存储指针的变量数

2、野指针

a)什么是野指针?

定义未初始化指针
释放结束之后指针
越界访问的指针
注: char *p
野指针:指针变量里内存的地址对应空间无访问权限(指针变量所指向空间无访问权限)

b)野指针产生的问题?

内存泄漏—运行时错误—内存错误(段错误)

c)野指针的注意事项:

1、指针指向常量存储区对象
2、资源泄露
3、内存越界
4、返回值是指针
5、指针做形参

d)如何避免野指针?

养成良好的编码习惯:
1)定义指针变量时必须初始化:
当指针变量作为指向工具时,定义时初始化为NULL;
当向指针变量指向的空间赋值时,需要给动态申请空间
2)使用时:
检查内存空间是否分配成功
初始化内存空间
防止越界访问
3)使用结束时:
必须要释放空间
释放之后一定要将指针再次初始化为NULL
NULL:#define NULL (void *)0
NULL代表的是0地址(不能访问,不能储存数据段)
char *p = NULL;//EOF
注意:野指针不能杜绝,只能避免!!!

3、内存空间分配:

1)静态分配:开销小,但是空间利用率高
2)动态分配:开销大,提高空间的利用率
动态分配(字节为单位):在堆空间分配
malloc:void *malloc(size_t size); size:sizeof(类型)*数量 char *p = (char *)malloc(sizeof(char) *100);
malloc VS calloc:callpc初始为0;
realloc:void *realloc(void *ptr,size_t size);
当ptr置为NULL时:相当于malloc;malloc(100) == realloc(NULL,100);
当ptr不为NULL时:
size大于原来分配的空间大小,数据不会丢失,但是存在越界访问的可能性;
size小于原来分配到空间大小,数据不会丢失,但是指针的指向会发生改变,因为realloc的机制是重新分配并不是追加,所以realloc开销较大(拷贝)
sizeof == 0相当于free();realloc(p,0)== free(p);
注意事项:检查是否分配成功;返回值类型转换(相同指针类型赋值);大小必须通过sizeof(类型)*数量(保证足够的空间)
动态分配内存为什么开销比较大:
多分配了内存用来保存使用内存信息

4、malloc、 free、 calloc、 reallc:

a) malloc:

void * malloc(size_t size);
这个函数可以再堆区上开辟内存连续可用的空间:
如果开辟成功,则返回一个指向开辟好空间的指针
如果开辟失败,则返回NULL指针,因此malloc的返回值一定要做检查
由于返回值的类型是void *,所以malloc函数并不知道开辟空间的类型
具体在使用的时候由使用者自己来决定
如果参数size为0,malloc的行为的标准是未定义的,取决于编译器

b) free:

void free(void * ptr);
如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的
如果参数ptr是NULL指针,则函数什么是都不用做

c) calloc:

void * callloc(size_t num,size_t size);
calloc与malloc的开辟方式唯一的不同点是在开辟内存的同时初始化

d) realloc:

void * realloc(void * ptr,size_t size);
realloc函数可以对动态开辟内存大小的调整
ptr是要调整的内存地址size调整之后新大小返回值为调整之后的内存其实位置
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间:
1)原有空间之后有足够大的空间
2)原有空间之后没有足够大的空间

六、内存管理(内存泄漏)

1、硬件(静态存储) VS内存(动态存储)区别?

hello.c hello(硬盘) ./hello(hello从硬盘加载内存中)
a)什么是静态存储方式和动态存储方式
静态存储方式:指在程序运行是,给变量分配固定的存储空间的方式
动态存储方式:指在程序运行时,根据需要给变量分配动态的存储空间的方式
b)存放的变量:
静态存储区:
全局变量、动态局部变量
动态存储区:
函数形式的参数、自动变量、函数调用时的现场保护和返回地址
a)对象:
静态对象是有名字的变量,可以直接对其进行操作
动态对象是没有名字的一段地址,需要指针间接地对它进行操作
b)分配与释放:
静态对象的分配与释放有编译器自动处理
动态对象的分配与释放必须由程序员显示地管理,他通过malloc()和free()两个函数来完成

2、为什么所有的编程都关注内存管理?

内存属于稀缺资源

3、编程过程中,内存管理主要做什么?

防止内存泄漏

4、内存错误的类别:

a)内存泄漏
b)错误分配(包括大量增加free()释放的内存和为初始化的引用)
c)悬空指针
d)数组边界违规

5、内存泄漏的原因:

a)内存分配未成功,却使用它
b)内存分配虽然成功,但尚未初始化就引用它
c)内存分配成功并且已经初始化,但操作越过了内存的边界
d)忘记释放内存,造成内存泄漏

6、如何防止内存泄漏?

用户自己管理:
缺点:对用户的要求比较高(良好的编码习惯,经验性)
优点:开销小,实时
系统管理:
缺点:开销大,实时性差,用户无法干预(GC:垃圾回收机制)
优点:能够有效的防止内存泄漏

7、C语言防止内存泄漏的方法:

a)养成良好的编码习惯
b)内存区域的划分(Linux虚拟地址空间)
c)动态分配内存的方式选择

8、如何检查内存泄漏?

内存分析诊断工具valgrind

七、函数(函数的声明、定义、调用、库函数的使用(字符串处理函数、时间函数、随机数函数) )

1、什么是函数?

函数是完成特定任务的独立程序代码单元,语法规则定义了函数的结构和使用方式

2、为什么要是用函数?

使用函数可以省去编写重复代码的苦差,函数可以让程序更加模块化,从而提高了程序代码的可读性和维护性

3、函数语法:

a) 函数三要素:

函数名:命名体现注释性、提高代码的可读性:动词——名词(4)
函数形参:传什么类型就要用什么类型的变量来接(例:a == 元素指针/aa == int(*a)[]/aa == int(*aa)[][])
函数返回值:return 0;(结束当前程序)/ exit(1)(结束整个程序)

b)函数的使用形式:

函数的声明(在调用函数前需声明函数)、函数的定义、函数声明
函数定义:函数名、函数的返回值、形参的类型及变量
函数调用:函数名、实参的变量名或者实参的地址
函数声明:函数名、函数返回值、形参的类型(变量名可以不提供):不分配内存空间
注意:
声明函数时,必须声明函数的类型,带返回值的函数类型应该与其返回值类型相同,而没有返回值的函数英声明为void类型

c)函数的调用过程:

1)通过函数名找到函数的入口地址:函数地址函数入口地址&函数名(函数指针)
2)给形参分配空间
3)传参:将实参的值传递给形参的空间储存
4)执行函数体里的语句
5)函数返回,并释放空间(函数的空间:局部变量)

d)传值(传实参变量名) VS传地址(传实参变量的地址)

1)当只是用不修改实参变量内存空间的值,在函数调用时,传实参变量名(只能使用不能修改)
2)当即使用也修改实参变量内存空间的值,在函数调用时,传实参变量的地址(既能使用也能修改)
传值:
是把实参的赋值给形参,对形参的更改不会影响实参的值,仅将对象的值传你提给目标对象,相当于copy,系统为目标对象重新开辟一个完全相同的内存栈空间,然后把对象的值赋值到栈中
传地址:
是传值的一种特殊方式,只是传递的是地址,不是普通的变量,那么传递地址以后,系统在栈中开辟空间存放地址,实参和形参都指向同一个对象
注意:
传指针时,不要误以为就是在传地址
判断:
修改指针变量对应空间值(传指针变量的地址),还是修改指针变量指向内存空间的值(传指针变量名)
传实参变量名:只能使用,不能修改

e)传出参数、传入参数(大多数函数)

传入参数:传递给函数只使用不能更改的实参
传出参数:传递给函数修改的实参(相当于函数的返回值)

f)函数如何返回多个值?

利用传出参数

g)函数返回值:

不能返回局部变量的地址

h)如何返回多个值?

结构体、传出参数

4、ANSI C函数:

a) ANSI C函数原型:

在ANSI C标准之前,声明函数的方案存在缺陷,因为值需要声明函数的类型,不用声明任何参数
下面是ANSI之前的函数声明:(告知编译器imin()返回int类型的值)
int imin();
然而,以上函数声明并未给出imin()函数的参数个数和类型,因此,如果调用imin()时,使用的参数个数不对或类型不匹配,编译器根本无法察觉。

b) ANSI的解决方案:

针对函数不匹配的问题,ANSI C标准要求在函数声明是还要声明变量的类型(即使用函数原型)来声明函数的返回类型、参数的数量和每个参数的类型。未标明imax()函数有两个int类型的参数可以使用下面两种函数原型来声明:
int imax(int,int);
int imax(int a,int b);

c)错误和警告的区别:

错误导致无法编译
警告仍然允许编译

5、函数原型

a)优点:

让编译器捕获在使用函数时可能出现的许多错误或疏漏(如果编译器没有发现这些问题,就很难察觉出来)

b)是否必须使用函数原型?

都可以,也可以使用旧式的函数声明(即不用声明任何形参),但是这样做的弊大于利

c)为什么使用函数原型?

为了让编译器在第一次执行到该函数之前就知道如何使用它,因此把整个函数定义放在第一次调用该函数之前,也有同样的效果。因此,函数定义也相当于函数原型

5、字符串函数的使用:

a) strlen:

size_t strlen( const char* str)
功能:
计算字符串长度,不包含’\0’
返回值:
返回字符串的字符数
说明:

strlen() 函数计算的是字符串的实际长度,遇到第一个’\0’结束;
参数指向的字符串必须以 '\0’结束
函数返回值一定是size_t ,是无符号的
如果你只定义没有给它赋初值,这个结果是不定的,它会从首地址一直找下去,直到遇到’\0’停止
sizeof返回的是变量声明后所占的内存数,不是实际长度,此外sizeof不是函数,仅仅是一个操作符,strlen()是函数
在这里插入图片描述总结:
strlen:他是一个函数,用于计算字符串的实际长度,遇到第一个‘\0’结束,参数指向的字符创必须是已‘\0’结束,他的返回值一定是无符号的(size_t)。跟sizeof相比,sizeof返回的是变量声明后所占的内存数,不是实际的长度,sizeof计算‘\0’,而strlen不计算‘\0’,sizeof此外不是函数,仅仅只是一个操作符,strlen()是函数

b) strcpy:

char* strcpy(char* dest,char* src)
功能::
将参数src字符串拷贝至参数dest所指的地址
返回值::
返回参数dest的字符串起始地址
说明:

源字符串必须以’\0’结束
会将源字符串的’\0’拷贝到目标空间
目标空间必须可变
如果参数dest所指的内存空间不够大,可能会造成缓冲溢出的错误情况,在编写程序时需特别留意,或者用strncpy()来取代
在这里插入图片描述

c) strncpy:

char* strncpy(char* dest,const char* src,size_t num)
功能:
拷贝src字符串的前num个字符至dest
返回值:
dest字符串起始地址
说明:

如果src字符串长度小于num,则拷贝完字符串后,在目标后追加0,直到num个
strncpy不会向dest追加’\0’
src和dest所指的内存区域不能重叠,且dest必须有足够的空间放置n个字符在这里插入图片描述

d) strcat:

char* strcat(char* dest,const char* src)

功能:
字符串拼接
返回值:
返回dest字符串起始地址
说明:

源字符串必须’\0’结束
目标空间必须可修改
strcat() 会将参数src字符串复制到参数dest所指的字符串尾部
dest最后的结束字符’\0’会被覆盖掉,并在连接后的字符串的尾部再增加一个’\0’
dest与src所指的内存空间不能重叠,且dest要有足够的空间来容纳要复制的字符串
在这里插入图片描述

e) strncat:

char* strncat (char* dest,const char* src,size_t num)
功能:
将n个字符追加到字符串结尾
返回值:
返回dest字符串的起始地址
说明:

strncat将会从字符串src的开头拷贝n个字符到dest字符串尾部
dest要有足够的空间来容纳要拷贝的字符串
如果n大于字符串src的长度,那么仅将src全部追加到dest的尾部
strncat会将dest字符串最后的’\0’覆盖掉,字符追加完成后,再追加’\0’
在这里插入图片描述

h) strcmp:

int strcmp (const char* str1,const char* str2)

功能:
字符串比较
返回值:
若参数s1和s2字符串相同则返回0,s1若大于s2则返回大于0的值,s1若小于s2则返回小于0的值
说明:

判断两个字符串大小:
1)ASII码
2)长度
区分大小写比较的,如果希望不区分大小写进行字符串比较,可以使用stricmp函数在这里插入图片描述在这里插入图片描述

i) strncmp:

int strncmp(const char* str1,const char* str2,size_t num)

功能:
指定长度比较
返回值:
同strcmp
在这里插入图片描述

j) memset:

void* memset(void* str,int value,size_t num)

按字节设置,进行赋值
功能:
以str的起始位置开始的n个字节的内存区域用整数value进行填充
返回值:
目标str内存起始地址
说明:

memset用来对一段内存空间全部设置为某个字符,一般用在对定义的字符串进行初始化为’\0’或’\0’
如果用malloc分配的内存,一般只能使用memset来初始化
memset可以方便的清空一个结构类型的变量或数组,它可以一字节一字节地把整个数组设置为一个指定的值

八编码规范(华为编码规范)

低耦合高内聚! ! !

1、对所调用函数的错误返回码要仔细、全面地处理(malloc)
2、明确函数功能,精确(而不是近似)地实现函数设计(atoi)
3、任何函数都需要对入口参数检查
4、防止将函数的参数作为工作变量
5、规模尽量限制在200行以内
6、一个函数仅完成一件功能
7、为简单功能编写函数
8、不要设计多用途面面俱到的函数
9、尽量不要编写依赖于其他函数内部实现的函数(函数功能独立性)
10、避免设计多参数函数,不使用的参数从接口中去掉
11、非调度函数应减少或防止控制参数,尽量只使用数据参数
12、检查函数所有参数输入的有效性
13、检查函数所有参数输入的有效性
14、函数名应准确描述函数的功能
15、改进模块中函数的结构,降低函数间的耦合度,并提高函数的独立性以及代码可读性、效率和可维护性。优化函数结构时,要遵守以下原则:
(1)不能影响模块功能的实现。
(2)仔细考查模块或函数出错处理及模块的性能要求并进行完善。
(3)通过分解或合并函数来改进软件结构。
(4)考查函数的规模,过大的要进行分解。
(5)降低函数间接口的复杂度。
(6)不同层次的函数调用要有较合理的扇入、扇出。
(7)函数功能应可预测。
(8)提高函数内聚。(单一功能的函数内聚最高)
说明:对初步划分后的函数结构应进行改进、优化,使之更为合理。
16、避免使用BOOL参数

九、预处理(宏定义、宏函数、条件编译)

1、预处理主要任务:

a)头文件展开
b)宏替换
c)条件编译 gcc- E
宏定义:

2、头文件展开: #include<> VS #include"

#include<>:从根目录上查找头文件
#include’’:先从当前目录查找,如果没有再去根目录上查找头文件

3、宏替换:

a)宏语法:#define NAME VALE
b)替换逻辑:
傻瓜式替换 注意事项:使用()解决运算符优先级的问题
c)作用:
定义常量(杜绝幻数)提高代码可读性
定义函数(宏函数):(编译时间换内存)(用空间换运行时间:linine:修饰函数(不超过5行、不能有全局变量、静态变量)内嵌函数)
d)宏函数 VS 自定义函数
自定义函数:
开销大:给形参分配空间;
一系列操作:函数调用、函数韩慧、分配空间、释放空间(运行时间)
宏函数:
频繁调用且短小的函数,最好优化成宏函数(不安全:复杂函数)(为什么不安全:傻瓜式替换,不用占用空间,在预处理阶段处理,编译时间)
e)宏的使用注意:
1)#:将其后面的宏参数进行字符串化操作(Stringfication)
2)##:链接

4、内置宏介绍:

LINE,FUNC,DATE,TIME
注意:
自定义宏不要用__开头,容易与内置宏产生冲突

十、关键字(static、 extern、 register、 const、 typedef、 volatile、 inline)

1、register:

a)语法作用:

只能修饰局部变量,不能修饰函数

b)寄存器变量:

尽可能将变量保存到CPU的内部寄存器中,省去CPU从内存中获取数据的时间,从而提高程序运行效率

c) CPU:

运算器、控制器、存储器(寄存器)

d)使用场景:

频繁访问的变量用register修饰可以起到优化程序运行效率的结果

e)使用注意事项:

1)不能通过&来获取register修饰变量的地址(因为register变量可能不存放在内存中)
2)register修饰的变量类型一定是CPU所能处理的类型(这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。不过,有些机器的寄存器也能存放浮点数)
3)局部静态变量不能定义为寄存器变量。不能写成:register static int a, b, c;
(由于寄存器的数量有限(不同的cpu寄存器数目不一),不能定义任意多个寄存器变量,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。)

2、volatile:

a)语法作用:

防止优化:
告诉编译器,该该变量可以随时改变

b)使用场景:

修饰全局变量

c)原理:

volatile变量修饰的共享变量进行写操作时会在汇编代码前加上lock前缀,lock前缀的指令在多核处理器下会引发两件事情:

将当前处理器缓存行的数据写回到系统内存
该写回内存的操作会使在其他CPU里缓存了该内存地址的额数据无效

d) volatile变量自身具有下列特性:

可见性:
对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
原子性:
对任意单个volatile变量的 读/写 具有原子性,但类似于volatile++这种复合操作不具有原子性。

e)当且仅当满足以下所有条件时,才应该使用volatile变量:

对变量的写入操作不依赖变量的当前值,或者能确保只有单个线程更新变量的值
该变量不会与其他状态变量一起纳入不变性条件之中
在访问变量时不需要加锁

3、static:

a)语法作用:

修饰局部变量(静态数据区):延长局部变量的生命周期直至整个程序之后再释放
修饰全局变量:限定作用域只能在本文件访问,不能再其他文件访问
修饰函数:限定作用域只能在本文件访问,不能再其他文件访问

b)使用场景:

局部变量场景:替代全局变量(全局变量危险)
全局变量和函数场景:多人协作开发时,防止命名冲突(全局变量名,函数名)

c)注意事项:

局部变量场景:替代全局变量(全局变量危险)
全局变量和函数场景:多人协作开发时,防止命名冲突(全局变量名,函数名)

d)静态变量和非静态变量的区别:

静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。
而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

static成员变量的初始化顺序按照定义的顺序进行初始化。

4、extern:

在当前文件中访问其他文件定义的全局变量或者函数:extern 外部声明

5、const:

a)语法使用:

修饰变量(局部变量,全局变量):只读变量
const距离谁近,谁就不能改变!!!

b)什么是只读变量:

对应的内存空间是可变的,但是不能通过变量名来修改

c)使用场景: const:

修饰函数的形参,提供安全访问接口函数,防止函数实现过程中修改实参变量的值

6、inline:

inline和#define的区别:
linine:
由编译器一起处理,直接将之后的函数体插入调用的地方
#define:
由预处理器处理,进行简单地文本替换,没有任何编译过程
面试题
typedef VS #define
(1)typedef 仅限于为数据类型重命名
#define 不仅可以为类型定义别名,也能 为数值定义别名,比如可以定义 1 为 ONE
(2)typedef 是由编译器执行解释的
#define 语句是由预编译器进行处理的

十一、位操作(位运算)

1、位操作基础:

在这里插入图片描述注意事项:
a)在这6种操作符,只有~取反是单目操作符,其它5种都是双目操作符。
b)位操作只能用于整形数据,对float和double类型进行位操作会被编译器报错。
c)对于移位操作,在微软的VC6.0和VS2008编译器都是采取算术称位即算术移位操作,算术移位是相对于逻辑移位,它们在左移操作中都一样,低位补0即可,但在右移中逻辑移位的高位补0而算术移位的高位是补符号位。如下面代码会输出-4和3。
int a = -15, b = 15; printf("%d %d\n", a >> 2, b >> 2);
因为15=0000 1111(二进制),右移二位,最高位由符号位填充将得到0000 0011即3。-15 = 1111 0001(二进制),右移二位,最高位由符号位填充将得到1111 1100即-4。
d)位操作符的运算优先级比较低,因为尽量使用括号来确保运算顺序,否则很可能会得到莫明其妙的结果。比如要得到像1,3,5,9这些2^i+1的数字。写成int a = 1 << i + 1;是不对的,程序会先执行i + 1,再执行左移操作。应该写成int a = (1 << i) + 1;
f)另外位操作还有一些复合操作符,如&=、|=、 ^=、<<=、>>=。

2、位操作运算常用小技巧

a)、判断奇偶:

只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if (a & 1 == 0)代替if (a % 2 == 0)来判断a是不是偶数。
下面程序将输出0到100之间的所有奇数。
for (i = 0; i < 100; ++i) if (i & 1) printf("%d ", i); putchar(’\n’);

b)、交换两数:

一般的写法是:
void Swap(int &a, int &b) { if (a != b) { int c = a; a = b; b = c; } }
可以用位操作来实现交换两数而不用第三方变量:
可以这样理解:
第一步 a^=b 即a=(a^b);
第二步 b^=a 即b=b(ab),由于运算满足交换律,b(ab)=bb^a。由于一个数和自己异或的结果为0并且任何数与0异或都会不变的,所以此时b被赋上了a的值。
第三步 a^=b 就是a=ab,由于前面二步可知a=(ab),b=a,所以a=ab即a=(ab)^a。故a会被赋上b的值。

3.变换符号:

变换符号就是正数变成负数,负数变成正数。
如对于-11和11,可以通过下面的变换方法将-11变成11
1111 0101(二进制) –取反-> 0000 1010(二进制) –加1-> 0000 1011(二进制)
同样可以这样的将11变成-11
0000 1011(二进制) –取反-> 0000 1010(二进制) –加1-> 1111 0101(二进制)

4.求绝对值:

位操作也可以用来求绝对值,对于负数可以通过对其取反后加1来得到正数。对-6可以这样来得到6。
1111 1010(二进制) –取反->0000 0101(二进制) -加1-> 0000 0110(二进制)
因此先移位来取符号位,int i = a >> 31;要注意如果a为正数,i等于0,为负数,i等于-1。然后对i进行判断——如果i等于0,直接返回。否之,返回~a+1。
现在再分析下。对于任何数,与0异或都会保持不变,与-1即0xFFFFFFFF异或就相当于取反。因此,a与i异或后再减i(因为i为0或-1,所以减i即是要么加0要么加1)也可以得到绝对值。

3、位操作用法:

假设当前状态为x.
x<<1+1 在最后补1,也可以写成x<<1|1
x|1 将最后一位变成1
(x|1)-1 将最后一位变成0,也可以写成x&0xFFFFFFFE
x^1 将最后一位取反
x|(1<<(k-1)) 将右数第k位变成1
x&(~(1<<(k-1))) 将右数第k位变成0
(x&(1<<(k-1)))==0 判断右数第k位是否为0
x^(1<<(k-1)) 将右数第k位取反
x&((1<<k)-1) 取末k位
(x>>(k-1))&1 取右数第k位
x|((1<<k)-1) 把末k位变为1
x^((1<<k)-1) 末k位取反
x&(x+1) 将末尾连续的1变为0
x|(x+1) 将右数第一个0变为1
x|(x-1) 将末尾连续的0变为1
(x^(x+1))>>1 取末尾连续的1
x&(-x) 去掉右起第一个1的左边

十二、复合数据类型(struct. union、 enum)

1、struc结构体:

a)什么是结构体:

是由一系列具有相同类型或不同类型的数据构成的数据集合,也叫结构

b)结构体的作用:

结构体在函数中的作用不是简便,其最主要的任务就是封装,封装的好处就是可以再次利用

c) 定义结构体类型:

第一个字母大写
结构体名一定要体现结构的作用(自注释性)
在这里插入图片描述注意花括号后的分号!!!

d)结构体内能否保存函数?

不可以保存函数,但是可以保存函数指针
在这里插入图片描述

e)结构体类型定义变量:

在这里插入图片描述

f)初始化(野指针的问题,直接给数组名赋值(指针常量))

在这里插入图片描述两者相比,后者可读性更高

g)如何访问结构体变量的成员?

’.’ '->'
在这里插入图片描述结构体之间的赋值

2、union共用体:

a)使用:

定义类型、定义变量和指针、初始化

b)作用:

多种类型,多个对象,多个事物只取其一时(简称n选1)

c)注意:

同一个内存段可以用来存放几种不同类型的成员,但在每一个时刻,只能存在其中一种,而不能同时存放集中,即每一瞬间只有一个成员起作用,其他的成员不起作用,不能同时都存在和起作用
共用体变量中起作用的成员是最后一个存放的成员,并存入一个新的成员后,原有的成员就会失去作用,即所有的数据成员具有相同的起始地址

d) struct VS union大小

union大小:
保存的成员中最大字节擦航的的成员所决定(与struct保持字节对齐)
存储方式:
所有成员共用同一段内存空间(产生数据覆盖)在这里插入图片描述在这里插入图片描述

e) CPU属性:

大端字节序(高字节存放在低地址)
小端字节序(低字节存放在低地址)
如图所示,其中黑色为大端,红色为小端:在这里插入图片描述

d)笔试题:

面试题:利用共用体判断CPU大小端字节序
在这里插入图片描述在这里插入图片描述在这里插入图片描述
运算过程:
1)输入p.num:

在这里插入图片描述2)输入p.ch[0] = 0:在这里插入图片描述3)输入p.ch[1] = 1:在这里插入图片描述

3、enum枚举:

a)作用:定义一系列的整数宏

在这里插入图片描述

b)如何定义枚举变量:

枚举变量等同于整型变量
在这里插入图片描述

c)枚举能不能做形参?

枚举可以做形参(注:枚举不可以打印,只能通过整型来换取它的值!!!)在这里插入图片描述

enum VS #define

enum优点:
定义一系列整数宏更加便捷;在编译阶段
#define:
预处理,不安全,做傻瓜式替换
a)枚举常量是实体中的一种,而宏定义不是实体
b)枚举常量属于常量,但宏定义不是常量
c)枚举具有类型,但宏没有类型,枚举变量具有与普通变量相同的性质(如作用域,值等),但是宏没有
d)枚举是在编译阶段确定其值,而#define宏常量是在预编译阶段进行简单地替换
e)一般在编译器里,可以调试枚举常量,但不能调试宏常量
f)枚举可以一次定义大量相关的常量,而#define一次只能定义一个
相较于#define,enum的好处:
a)增加了程序的维护性
b)增加了程序的可读性

十三、文件操作(打开、关闭、读写、属性设置)

1、FILE指针:

结构体:(编号)
struct file{int fd}(C库) ; typedef struct file *FILE

2、什么是数据流?

就C程序而言,从程序移进,移出字节,这种字节流就叫做流。程序与数据的交互是以流的形式进行的。进行C语言文件的读写时,都会先进行“打开文件”操作,这个操作就是在打开数据流,而“关闭文件”操作就是关闭数据流。

3、什么是缓冲区?

在程序执行时,所提供的额外内存,可用来暂时存放准备执行的数据。它的设置是为了提高存取效率,因为内存的存取速度比磁盘驱动器快得多。
当使用标准I/O函数(包含在头文件stdio.h中)时,系统会自动设置缓冲区,并通过数据流来读写文件。当进行文件读取时,是先打开数据流,将磁盘上的文件信息拷贝到缓冲区内,然后程序再从缓冲区中读取所需数据。事实上,当写入文件时,并不会马上写入磁盘中,而是先写入缓冲区,只有在缓冲区已满或“关闭文件”时,才会将数据写入磁盘。

4、什么是文件

文件通常是在磁盘或固态硬盘上的一段已命名的存储区。
C把文件看作是一系列连续的字节,每个字节都能被单独读取。

5、文件类型

文本文件和二进制文件:
文本文件是以字符编码的方式进行保存的。
二进制文件将内存中的数据原封不动的进行保存,适用于非字符为主的数据。其实,所有的数据都可以算是二进制文件。二进制文件的优点在于存取速度快,占用空间小。

6、文件存取方式

顺序存取方式和随机存取方式:
顺序存取就是从上往下,一笔一笔读取文件的内容。写入数据时,将数据附加在文件的末尾。这种存取方式常用于文本文件。
随机存取方式多半以二进制文件为主。它会以一个完整的单位来进行数据的读取和写入,通常以结构为单位。

7、fopen () 函数

a)作用:

fopen函数用于打开文件

b)格式:

其调用格式为:

FILE *fopen(char *filename, *type);
第一个参数是待打开文件的名称(包含该文件名的字符串地址)
第二个参数是一个字符串(指定待打开文件的模式)

c)模式字符串:

“r”:只能从文件中读数据,该文件必须先存在,否则打开失败
“w”:只能向文件写数据,若指定的文件不存在则创建它,如果存在则先删除它再重建一个新文件
“a”:向文件增加新数据(不删除原有数据),若文件不存在则打开失败,打开时位置指针移到文件末尾
“r+”:可读/写数据,该文件必须先存在,否则打开失败
“w+”:可读/写数据,用该模式打开新建一个文件,先向该文件写数据,然后可读取该文件中的数据
“a+”:可读/写数据,原来的文件不被删去,位置指针移到文件末尾

8、fprintf ()、fputs () 和fputc () 函数

a)作用:

函数fprintf()、fputs()和fputc()均为文件的顺序写操作函数

b)格式:

其调用格式为:
int fprintf(FILE *stream, char *format, );
int fputs(char *string, FILE *steam);
int fputc(int ch, FILE *steam);

c)返回值:

返回值均为整型量。

d)用法:

fprintf() 函数的返回值为实际写入文件中的字罕个数(字节数)。如果写错误,则返回一个负数
fputs()函数返回0时表明将string指针所指的字符串写入文件中的操作成功, 返回非0时,表明写操作失败。
fputc()函数返回一个向文件所写字符的值, 此时写操作成功,否则返回EOF(文件结束结束其值为-1, 在stdio.h中定义)表示写操作错误。
fprintf( ) 函数中格式化的规定与printf( ) 函数相同,所不同的只是 fprintf()函数是向文件中写入。而printf()是向屏幕输出。

9、fclose () 函数

a)作用:

fclose(fp)函数用来关闭fp指定的文件,必要时刷新缓冲区

b)格式:

其调用格式为:

  				int fclose(FILE *stream);

该函数返回一个整型数。当文件关闭成功时, 返回0, 否则返回一个非零值。可以根据函数的返回值判断文件是否关闭成功。

10、 fscanf ()、fgets ()和fgetc ()函数

a)作用:

fscanf()、fgets()和fgetc()函数均为文件的顺序读操作函数

b)格式:

其调用格式为:
int fscanf(FILE *stream, char *format, );
char fgets(char *string, int n, FILE *steam);
int fgetc(FILE *steam);

c)用法:

fscanf()函数的用法与scanf()函数相似,只是它是从文件中读到信息。
fscanf()函数的返回值为EOF(即-1),表明读错误,否则读数据成功。
fgets()函数从文件中读取至多n-1个字符(n用来指定字符数),并把它们放入string指向的字符串中,在读入之后自动向字符串未尾加一个空字符,读成功返回string指针,失败返回一个空指针。
fgetc()函数返回文件当前位置的一个字符,读错误时返回EOF。

11、fwrite ()和frear () 函数

a)用法:

fread()函数是从文件中读count个字段,每个字段长度为size个字节,并把它们存放到buf指针所指的缓冲器中。
fwrite()函数是把buf指针所指的缓冲器中,长度为size个字节的count个字段写到stream指向的文件中去。

12、 size_ tfwrite ()和size_ tfread () 函数

a)用法:
fwrite()函数把二进制数据写入文件
fread()函数接受的参数和fwrite()函数相同
b)格式:
其调用格式为:
size_t fwrite(const void *restrict ptr, size_t size, size_t nmemb, FILE *restrict fp);
size_t:sizeof运算符返回的类型
ptr:待写入数据块的地址
size:待写入数据块的大小(一字节为单位)
nmemb:待写入数据块的数量
fp:指定待写入的文件
size_t read(const void *restrict ptr, size_t size, size_t nmemb, FILE *restrict fp);
size_t:sizeof运算符返回的类型
ptr:待读取文件数据在内存中的地址
size:待读取数据块的大小(一字节为单位)
nmemb:待读取数据块的数量
fp:指定读取的文件

猜你喜欢

转载自blog.csdn.net/qq_46186025/article/details/122845158