目录
引言:
1. 关于变量
1.1 什么是变量
1.2 变量的定义与声明
1.3 为什么要定义变量
1.4 变量定义的本质
2. 关键字
2.1 最宽宏大量的关键字 - auto
2.1.1 局部变量
2.1.2 全局变量
2.1.3 概念补充
2.1.4 auto相关
2.3 最快的关键字—register
2.3.1 计算机的分级存储
2.3.2 寄存器
2.3.3 register 修饰变量
2.4 最名不符实的关键字 - static
2.4.1 认识多文件
2.4.2 static相关
2.5 基本数据类型—short、int、 long、char、float、double
2.6 变量命名
2.7 最冤枉的关键字----sizeof
2.8 signed、unsigned关键字
整型的存储
2.8.1 原码、反码、补码的补充
2.8.2 深入理解变量内容的存入和取出
2.8.3 二进制快速转化口诀
2.8.4 大小端
2.8.5 记忆口诀
2.8.6 练习题
2.9 if、else组合
2.9.1 什么是语句
2.9.2 什么是表达式
2.9.3 基本语法
2.9.4 if和else匹配问题
2.10 _BOOL类型
2.10.1 什么是布尔类型
2.10.2 bool值和0比较
2.10.3 bool类型总结
2.10.4 浮点型和指针类型与零值的比较
2.11 switch、case组合
2.11.1 基本语法
2.11.2 不要拿青龙偃月刀去削苹果
2.11.3 规则:
2.11.4 case在switch的作用是什么?
2.11.5 break在switch中的作用是什么?
2.11.6 没有break会有什么问题?
2.11.7 default语句相关问题
2.12 goto关键字
2.13 void关键字
2.13.2 void a
2.13.3 void 修饰函数返回值和参数
2.13.4 void指针
2.13.5 void不能代表一个真实的变量
2.14 return 关键字
2.14.1 内存管理
2.14.2 return相关
2.14.3 拓展:
2.15 const 关键字
2.15.1 const关键字也许应该被替换为readonly
2.15.2 const价值
2.15.3 const修饰数组
2.15.4 const修饰指针
2.15.6 const修饰函数参数
2.15.7 const修饰函数返回值
2.16 最易变的关键字----volatile
2.17 最会带帽子的关键字----extern
2.18 struct 关键字
2.18.1 概念:
2.18.2 空结构体大小
2.18.3 柔性数组
2.19 union 关键字
2.19.1 union相关规则
2.19.2 大小端模式对于union类型数据的影响
2.20 enum 关键字
2.20.1 枚举
2.20.2 枚举与#define 宏的区别
2.21 伟大的缝纫师----typedef 关键字
2.21.1 历史的误会----也许应该是 typerename
2.21.2 扩展:
3. 关键字分类
3.1 数据类型关键字(12个)
3.2 控制语句关键字(12个)
3.2.1 循环控制(5个)
3.2.2.条件语句(3个)
3.2.3. 开关语句 (3个)
3.2.4. 返回语句(1个)
3.3.存储类型关键字(5个)
3.4.其他关键字(3个)
结语:
C语言简洁、紧凑,使用方便、灵活。ANSI C标准C语言共有32个关键字,9种控制语句,程序书写形式自由,区分大小写。把高级语言的基本结构和语句与低级语言的实用性结合起来。 C 语言可以像汇编语言一样对位、字节和地址进行操作,而这三者是计算机最基本的工作单元。
简洁紧凑、灵活方便
ANSI C一共只有32个关键字,9种控制语句,程序书写形式自由,区分大小写。把高级语言的基本结构和语句与低级语言的实用性结合起来。 C 语言可以像汇编语言一样对位、字节和地址进行操作,而这三者是计算机最基本的工作单元。
C是结构式语言
结构式语言的显著特点是代码及数据的分隔化,即程序的各个部分除了必要的信息交流外彼此独立。这种结构化方式可使程序层次清晰,便于使用、维护以及调试。C语言是以函数形式提供给用户的,这些函数可方便的调用,并具有多种循环、条件语句控制程序流向,从而使程序完全结构化。
语法限制不太严格,程序设计自由度大:虽然C语言也是强类型语言,但它的语法比较灵活,允许程序编写者有较大的自由度。
允许直接访问内存地址,对硬件进行操作。由于C语言允许直接访问内存地址,可以直接对硬件进行操作,因此它既具有高级语言的功能,又具有低级语言的许多功能,能够像汇编语言一样对位、字节和地址进行操作,而这三者是计算机最基本的工作单元,可用来写系统软件。
生成目标代码质量高,程序执行效率高:一般只比经过高效优化的汇编程序生成的目标代码效率低10~20%。
适用范围大,可移植性好:C语言有一个突出的优点就是适合于多种操作系统,如DOS、UNIX、windows 98.windows NT;也适用于多种机型。C语言具有强大的绘图能力,可移植性好,并具备很强的数据处理能力,因此适于编写系统软件,三维,二维图形和动画,它也是数值计算的高级语言。
运算符丰富
C语言的运算符包含的范围很广泛,共有34种运算符。C语言把括号、赋值、强制类型转换等都作为运算符处理。从而使C语言的运算类型极其丰富,表达式类型多样化。灵活使用各种运算符可以实现在其它高级语言中难以实现的运算。
数据类型丰富
C语言的数据类型有:整型、实型、字符型、数组类型、指针类型、结构体类型、共用体类型等。能用来实现各种复杂的数据结构的运算。并引入了指针概念,使程序效率更高。另外C语言具有强大的图形功能,支持多种显示器和驱动器。且计算功能、逻辑判断功能强大。
同时对于不同的编译器也有各种强大的扩展功能。
另外C语言如此丰富数据类型及强大指针功能,其对硬件的管控能力极强,所以许多操作系统内核及MCU芯片程序开发都偏爱硬件。
在内存中开辟特定大小的空间,用来保存数据
所有的变量,本质都是要在内存中的某个位置开辟空间的!
类型 变量名 = 默认值 //定义类型格式
char c = 'c'; //初始化
c = 'd'; //赋值
赋值的意思就是这个变量c已经存在了,存在的本质就是这个变量对应的空间已经开辟出来了 ,换言之就是将'd'字符直接放到一个已经存在的空间中。
很不幸,在C语言中,初始化和赋值的差别不是很大,唯一可能会出现的差别是在结构体数组那块内容,但是对于一般的内置类型基本上是没什么差别的,但是在C++和Java里面还是有差别的,这里不再赘述。
计算机是为了进行计算的,而计算就需要数据,但是不是所有的数据都要立马被计算,有的数据是要先保存起来,等待后续处理
就像是吃饭,不是所有的饭菜都要立马被吃掉,要一口口吃,还没吃到的菜就需要暂时放到盘子里
盘子就如同变量,饭菜就相当于数据。
那么有人可能会说为什么要用“盘子”,直接去锅里不就行了吗?这当然没问题,但是效率太低了
定义就是用来开辟空间的 — 只能有一次
声明是用来告知的 — 声明可以多次
C语言一共多少个关键字呢?一般的书上,都是32个(包括本书),但是这个都是 C90(C89) 的标准。其实 C99 后又新增了5个关键字。不过,目前主流的编译器,对 C99 支持的并不好,我们后面默认情况,使用 C90 ,即认为32个
关键字 | 说明 |
---|---|
auto | 声明自动变量 |
short | 声明短整型变量或函数 |
int | 声明整型变量或函数 |
long | 声明长整型变量或函数 |
float | 声明浮点型变量或函数 |
double | 声明双精度变量或函数 |
char | 声明字符型变量或函数 |
struct | 声明结构体变量或函数 |
union | 声明共用数据类型 |
enum | 声明枚举类型 |
typedef | 用以给数据取别名 |
const | 声明只读变量 |
unsigned | 声明无符号类型变量或函数 |
signed | 声明有符号类型变量或函数 |
extern | 声明变量是在其它文件正声明 |
register | 声明寄存器变量 |
static | 声明静态变量 |
volatile | 说明变量在程序执行中可被隐含地改变 |
void | 声明函数无返回值或无参数,声明无类型指针 |
if | 条件语句 |
else | 条件语句否定分支(与if连用) |
switch | 用于开关语句 |
case | 开关语句分支 |
for | 一种循环语句 |
do | 循环语句的循环体 |
while | 循环语句的循环条件 |
goto | 无条件跳转语句 |
continue | 结束当前循环,开始下一轮循环 |
break | 跳出当前循环 |
default | 开关语句中的“其他”分支 |
sizeof | 计算数据类型长度 |
return | 子程序返回语句(可以带参数,也可不带参数)循环条件 |
包含在代码块中的变量叫做局部变量,局部变量具有临时性。
int x = 10;
if(x == 10)
{
int y = 20;
printf("code1: %d,%d",x,y);
}
printf("code2: %d,%d",x,y);
此时系统会报错:最后一行代码中的y变量没有定义
这是因为y变量是局部变量,只有在if代码块里面才可以使用,y的生命周期和作用域只在代码块内可以使用,出来之后y就被注销,所以code2中的y是没有定义的
在所有函数外定义的变量,叫做全局变量,全局变量具有全局性。
全局变量整个程序运行期间都有效,且在任何代码块中的都可以被访问,甚至是修改
下面程序输出的是局部,也就是局部和全部同名的时候,优先局部。
int g_val = 100;//全局变量
int main()
{
int g_val = 200;//局部变量
printf("%d",g_val);
}
//程序的打印结果为200
//局部优先
代码块:用{}括起来的区域,就叫做代码块
作用域:指的是可以被正常访问的代码区域
生命周期:指的是该变量从定义到被释放的时间范围,所谓的释放,指的是曾经开辟的空间“被释放”
局部变量:进入代码块,形成局部变量【开辟空间】,退出代码块,“释放”局部变量
全局变量:定义完成之后,程序运行的整个生命周期内,该变量一直有效
作用域本质上是这个变量从什么地方开始有效,到什么地方开始无效
生命周期本质上是从什么时候开始有效,到什么时候开始无效
作用域更多的是描述影响范围,生命周期更多的是描述生存时间长短的问题
如何使用:auto一般修饰代码块内部定义的变量,即局部变量,默认都是auto修饰的,不过一般省略,,但是默认的所有变量并不都是auto,只是一般用来修饰局部变量
局部变量,自动变量,临时变量,都是一回事。我们统称局部变量
auto已经很老,近几年新出的C标准都不使用auto来修饰局部变量了,可以直接省略
CPU主要是负责进行计算的硬件单元,但是为了方便运算,一般第一步需要先把数据从内存读取到CPU内,那么也就需要CPU具有一定的数据临时存储能力。注意:CPU并不是当前要计算了,才把特定数据读到CPU里面,那样太慢了。
所以现代CPU内,都集成了一组叫做寄存器的硬件,用来做临时数据的保存。
金字塔从上到下容量越来越大、速度越来越慢、价格越来越低
一般距离CPU越近,存储效率越高,单价成本越高且容量一般是比较小的。
其中,快的设备给慢的设备做缓存内存,显示器。
计算机的分级存储让我们用最小的成本,使用上最高效率的计算机。我们现在使用的计算机,就是考虑成本和效率的结果
我们可以不关系硬件细节,只要知道CPU内集成了一组存储硬件即可,这组硬件叫做寄存器
寄存器存在的本质:在硬件层面上,提高计算机的运算效率。因为不需要从内存里读取数据啦
我们知道了寄存器的作用之后再来看看寄存器变量—register吧
尽量将所修饰变量,放入CPU寄存区中,从而达到提高效率的目的
那么什么样的变量,可以采用register呢?
1. 局部的(全局会导致CPU寄存器被长时间占用)
2. 不会被写入的(写入就需要写回内存,后续还要读取检测的话,register的意义在哪呢?)
3. 高频被读取的(提高效率所在)
4. 如果要使用,请不要大量使用,因为寄存器数量有限
register修饰的变量,不能取地址(因为已经放在寄存区中了嘛,地址是内存相关的概念)
最后呢,这个关键字现在可以不用管,因为现在的编译器已经很智能了,能够进行比人更好的代码优化
在讲这个关键字之前我们先看一个补充的内容
.h:我们称之为头文件,一般包含函数声明,变量声明,宏定义,头文件等内容(header)
.c: 我们称之为源文件,一般包含函数实现,变量定义等 (.c: c语言)
//test.h
#pragma once //防止头文件被重复包含,现在只需要记住,后面用的多
#include
#include //包含了其他的windows头文件,调用函数时可以使用
//test.c
#include "test.h" //""里面包含头文件,目前只需要知道是自己写的头文件,就用""包含即可
//main.c
#include "test.h" //""里面包含头文件,目前只需要知道是自己写的头文件,就用""包含即可
int main()
{
printf("hello files!\n");
return 0;
}
简单看了上面的多文件调用之后,下面我们来看两个问题:
1. 全局变量可以跨文件吗?
可以
2. 函数可以跨文件访问吗?
可以
为什么要跨文件?有一定规模的项目,一定是多文件的,多个文件之间,后续一定要进行数据“交互”(#include "test.h" , main.c 里的函数访问 test.c 里的函数)跨文件其实就是使代码模块化,方便别人使用时可以直接看到,所以在写项目的时候,跨文件是刚需。
如果不能跨文件,“交互”成本会比较高
如果我的这个文件比较自私,不想把自己定义的变量和函数给别的文件用,那么有没有可能,我们不让全局变量或者函数跨文件访问,只在本文件内部被访问呢?
我们跨文件的时候总有些代码是不能够开源的,这时就出现了static
static修饰全局变量和函数时,该全局变量和函数只能在本文件内被使用,是不能够被其他文件直接访问的,就相当于将代码给封装起来了,只给一个接口,这样的话别人在使用我们的程序的时候,就不能直接通过该全局变量来直接修改程序。static是一套很好的项目维护机制,提供了安全保证。
static修饰全局变量,影响的是作用域的概念,函数类似。而生命周期是不变的
static修饰局部变量,变量的生命周期变成全局周期。(作用域不变)
static void fun()
{
static int i = 0;
i++;
printf("i = %d\n", i);
}
int main()
{
for(int i = 0; i < 10; i++)
{
fun();
}
return 0;
}
/*不加static时打印结果应全为1
加上static后打印结果应为:
i = 1
i = 2
……
i = 10
两次结果不一样,其实是因为fun函数每次被调用之后,都会把内存释放,这样一来下一次进入时就会从i = 0开始执行。而后面加上static之后,i 在fun运行过程中,并没有被释放!!!
所以得到结论:static修饰局部变量,更改局部变量的生命周期,作用域不变
static总结:
i 的初始化动作,永远只会初始化一次,第一次!
C语言常见内置类型:short、int、 long、char、float、double等
什么是内置类型:由C语言给我们定义好的就叫做C语言的内置类型
与C语言相对应的还有一批自定义类型:指针,结构体,联合体,枚举等
前面已经说过,定义变量的本质:在内存中开辟一块空间,用来保存数据。
而定义一个变量,是需要类型的,这个是基本语法决定的。
那么,类型决定了:变量开辟空间的大小。
我们为什么要在关键字这里学变量命名呢?
1.变量名应由“数字字母下划线”组成,且不能以数字开头
2.见名知义
命名应当直观且可以拼读,可望文知意,便于记忆和阅读
标识符最好采用英文单词或其组合,不允许使用拼音。程序中的英文单词一般不要太复
杂,用词应当准确。3.大小驼峰
当标识符由多个词组成时,每个词的第一个字母大写,其余全部小写。比如:
int CurrentVal;
这样的名字看起来比较清晰,远比一长串字符好得多。4.避免混淆
程序中不得出现仅靠大小写区分的相似的标识符。
例如: int x, X; 变量 x 与 X 容易混淆
void foo(int x); 函数 foo 与 FOO 容易混淆1(数字 1)和 l(小写字母 l)之间, 0(数字 0)和 o(小写字母 o)之间的区别
5.所有宏定义、枚举常数、只读变量全用大写字母命名,用下划线分割单词。
一般来说习惯上用 n,m,i,j,k 等表示 int 类型的变量; c, ch 等表示字符类型变量; a 等表示数组; p 等表示指针。当然这仅仅是一般习惯,除了 i,j,k 等可以用来表示循环变量外,别的字符变量名尽量不要使用
C语言关键字命名清楚以上这些就足够了
求特定一种类型,它对应开辟空间的大小的写法的过程中,下面哪些写法是正确的
int a = 10;
printf("%d\n",sizeof(a)); //1
printf("%d\n",sizeof(int)); //2
printf("%d\n",sizeof a); //3
printf("%d\n",sizeof int); //4
4. err: sizeof作为一个关键字不能直接去求另外一个关键字的大小
其余三个都是可以的,换言之sizeof求一种特定开辟空间的大小时,有三种方法帮我们去求
分别对应上面代码块里的1,2,3 种写法
- 一种是(变量名)
- 一种是根据类型
- 还有是直接加变量名
下面我们通过数组、指针、指针数组这三个概念来理解sizeof
对了,在一般的学校考试,二级考试都会经常性的让你求字符串的空间容量,这里大家要注意的就是sizeof求字符串所占空间容量时,会把字符串结束符‘\0’也当作一个字节算在内
我们在将计算机中的数据拿出来,并将二进制形式转化为十进制形式有两种方法:
方法一:
1111 1111 1111 1111 1111 1111 1110 1100 补码
1111 1111 1111 1111 1111 1111 1110 1011 反码(由补码减1得到)
1000 0000 0000 0000 0000 0000 0001 0100 原码 (符号位不变,其余为按位取反)
方法二:原反补码之间的转化是由计算机硬件完成的,原码<=>反码<=>补码
按照下面的方法转换可以使用一条硬件电路完成转化
1111 1111 1111 1111 1111 1111 1110 1100 补码
1000 0000 0000 0000 0000 0000 0001 0011 反码 (符号位不变,其他位直接按位取反)
1000 0000 0000 0000 0000 0000 0001 0100 原码
方法一:先-1,在符号位不变,按位取反。
方法二:将原码到补码的过程在来一遍。
在图中提到了空间是不关心内容的,那么这里的变量类型什么时候起效果呢?
我们在这里举个“栗子”:假如我口袋有一百元,那么我身上有多少钱?
答案可能是一百也可能不是,因为我们并不知道这里的100是哪种货币
so,100在这里是没有意义的;数字要带上类型才有意义
是在读取的时候,具有意义!!!
类型决定了如何解释空间内部保存的二进制序列
变量存的过程:字面数据必须先转成补码,在放入空间当中。所以,所谓符号位,完全看数据本身是否携带+-号。和变量是否有符号无关!
变量取的过程:取数据一定要先看变量本身类型,然后才决定要不要看最高符号位。如果不需要,直接二进制转成十进制。如果需要,则需要转成原码,然后才能识别。(当然,最高符号位在哪里,又要明确大小端)
-10:1111 1111 1111 1111 1111 1111 1111 0110
4294967286:1111 1111 1111 1111 1111 1111 1111 0110
虽然打印出来转化后的二进制序列数值是一样的,但是十进制数值却不同,这就是由于最高位是否为符号位造成的差异
如何理解大小端:
大家应该都知道int 类型的变量大小是四个字节,而内存存储也是以字节为单位的,所以我们在存储数据时,数据也要按照字节为单位划分成若干块,数据按照字节为单位,也是有高权值和低权值之分的,所以我们存取的顺序是有区别的,但是,无论如何放,只要同等条件去取,都是可以的!
如何存取是由计算机、内存硬件厂商决定的,但是每个厂商都有不同的标准,由此产生了两种存储方案:
1.大端:按照字节为单位,低权值位数据存储在高地址处,就叫做大端
2.小端:按照字节为单位,低权值位数据存储在低地址处,就叫做小端
举个例子:
int a = 0x11223344
地址: 0xA0 0xA1 0xA2 0xA3
11 22 33 44 大端
44 33 22 11 小端
低权值位 — 权值位比较小
低地址处 — 地址数字比较小
权值位比较小的放在地址数字比较小的地方,就叫做小端存储
所以我们可以用“小小小”来记忆小端存储,不然则为大端存储的
大小端是如何影响数据存储的
大小端存储方案,本质数据和空间按照字节为单位的一种映射关系
C语言中由一个分号;隔开的就是一条语句。
比如:
printf("hehe");
1+2;
C语言中,用各种操作符把变量连起来,形成有意义的式子,就叫做表达式。
操作符:+,-,*,/,%,>,<,=,==...
语法结构:
//1
if(表达式)
语句;
//2
if(表达式)
语句1;
else
语句2;
//3.多分支
if(表达式1)
语句1;
else if(表达式2)
语句2;
else
语句3;
//4.嵌套
if(表达式1){
语句1;
if(表示式x){
语句x;
}
else{
语句y;
}
}
else if(表达式2){
语句2;
}
else{
语句3;
}
else 匹配采取就近原则
它总是与离他最近的if进行匹配
在书写代码的过程中尽量使用锯齿型,如上面的嵌套代码,就不会有匹配问题
//测试代码
#include
#include //布尔类型的头文件
int main()
{
bool ret = false;
ret = true;
printf("%d\n", sizeof(ret)); //vs2013 和 Linux中都是1
system("pause");
return 0;
}
为了深刻的理解布尔类型,我们要去查看布尔类型的源码
在vs中,光标选中bool,双击,可以转到定义,就能看到BOOL是什么
bool就是用宏定义表示_Bool , 0就用false表示;1就用true表示所以bool就是表示真假的
#define bool _Bool //c99中是一个关键字哦,后续可以使用bool
#define false 0 //假
#define true 1 //真
#include
#include
int main()
{
int pass = 0; //0表示假,C90,我们习惯用int表示bool
//bool pass = false; //C99
if(pass == 0)
{ //理论上可行,但此时的pass是应该被当做bool看待的,==用来进行整数比较,不推荐
//TODO
}
if(pass == false)
{ //不推荐,尽管在C99中也可行
//TODO
}
if(pass)
{ //推荐
//TODO
}
//理论上可行,但此时的pass是应该被当做bool看待的,==用来进行整数比较,不推荐
//另外,非0为真,但是非0有多个,这里也不一定是完全正确的
if (pass != 1){
//TODO
}
if (pass != true){ //不推荐,尽管在C99中也可行
//TODO
}
if (!pass)
{ //推荐
//TODO
}
return 0;
}
C89,C90没有标准的bool类型,C99有正式的bool类型的
推荐写法:直接小写加true或false,在这里要记得C99标准的bool、true、false全部都要小写
如果大写的话就是微软的另一套布尔类型的标准了,Microsoft版本可移植性较差,尽量不用
和0比较的话,if直接判定布尔值结果,不用进行等于,等于0,或者等于false这样的写法,
就是bool类型,直接判定,不用操作符进行和特定值比较
相关分析在我的另一篇博客【C和0的不解之缘】里有详细的解析,请大家移步观看。
http://t.csdn.cn/4R2Uhhttp://t.csdn.cn/4R2Uh
switch(整型变量/常量/整型表达式)
{
case var1:
break;
case var2:
break;
case var3:
break;
default:
break;
}
已经有if else为何还要switch case
switch语句也是一种分支语句,常常用于多分支的情况。这种多分支,一般指的是很多分支,而且判定条件主要以整型为主,如:
输入1,输出星期一
输入2,输出星期二
输入3,输出星期三
输入4,输出星期四
输入5,输出星期五
输入6,输出星期六
输入7,输出星期日
如果写成 if else 当然是可以的,不过比较麻烦
#include
int main()
{
int day = 1;
switch (day){
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
break;
case 4:
printf("星期四\n");
break;
case 5:
printf("星期五\n");
break;
case 6:
printf("星期六\n");
break;
case 7:
printf("星期日\n");
break;
default:
printf("bug!\n");
break;
}
return 0;
}
把 default 子句只用于检查真正的默认情况。
有时候,你只剩下了最后一种情况需要处理,于是就决定把这种情况用 default 子句来处理。这样也许会让你偷懒少敲几个字符,但是这却很不明智。这样将失去 case 语句的标号所提供的自说明功能,而且也丧失了使用 default 子句处理错误情况的能力。所以,奉劝你不要偷懒,老老实实的把每一种情况都用 case 语句来完成,而把真正的默认情况的处理交给default 子句
在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数。
建议 for 语句的循环控制变量的取值采用“半开半闭区间”写法
半开半闭区间写法和闭区间写法虽然功能是相同,但相比之下,半开半闭区间写法写法更加
直观。循环次数明显,便于进行个数计算
半开半闭区间写法 | 闭区间写法 |
for(n = 0; n < 10; n++) { …… } |
for(n = 0; n <= 9; n++) { …… } |
不能在 for 循环体内修改循环变量,防止循环失控
for (n = 0; n < 10; n++)
{
…
n = 8;//不可,很可能违背了你的原意
…
}
case本质是进行判断功能
break本质其实是进行分支功能
#include
int main()
{
int day = 1;
switch (day){
case 1:
printf("星期一\n");
case 2:
printf("星期二\n");
case 3:
printf("星期三\n");
case 4:
printf("星期四\n");
break;
case 5:
printf("星期五\n");
break;
case 6:
printf("星期六\n");
break;
case 7:
printf("星期日\n");
break;
default:
printf("bug!\n");
break;
}
return 0;
}
//如果多个不同case匹配,想执行同一个语句,推荐做法:
#include
int main()
{
int day = 6;
switch (day){
case 1:
case 2:
case 3:
case 4:
case 5:
printf("周内\n");
break;
case 6:
case 7:
printf("周末\n");
break;
default:
printf("bug!\n");
break;
}
return 0;
}
结论:case之后,如果没有break,则会依次执行后续有效语句,直到碰到break
这里补充一个知识点:case后面不能是const修饰的只读变量
default可以出现在switch内的任何部分
尽管如此,我们依旧强烈推荐default应该放在case语句的最后
一般来说,编码的水平与 goto 语句使用的次数成反比。有的人主张慎用但不禁用 goto语句,但是在《C语言深度解剖》这本书中,作者却主张禁用
自从提倡结构化设计以来, goto 就成了有争议的语句。首先,由于 goto 语句可以灵活跳转,如果不加限制,它的确会破坏结构化设计风格;其次, goto 语句经常带来错误或隐患。它可能跳过了变量的初始化、重要的计算等语句。
struct student *p = NULL;
…
goto state;
p = (struct student *)malloc(…); //被 goto 跳过,没有初始化
{
⋯
state:
//使用 p 指向的内存里的值的代码
⋯
}
如果编译器不能发觉此类错误,每用一次 goto 语句都可能留下隐患
2.13.1 色即是空
void 有什么好讲的呢?如果你认为没有,那就没有;但如果你认为有,那就真的有。有点像“色即是空,空即是色”。
void 真正发挥的作用在于:
(1) 对函数返回的限定;
(2) 对函数参数的限定。
众所周知, 如果指针 p1 和 p2 的类型相同, 那么我们可以直接在 p1 和 p2 间互相赋值;如果 p1 和 p2 指向不同的数据类型,则必须使用强制类型转换运算符把赋值运算符右边的指针类型转换为左边指针的类型。
例如:float *p1; int *p2; p1 = p2;
其中 p1 = p2 语句会编译出错,提示“'=' : cannot convert from 'int *' to 'float *'”,必须改为:
p1 = (float *)p2;
而 void *则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换:void *p1; int *p2; p1 = p2;
但这并不意味着, void *也可以无需强制类型转换地赋给其它类型的指针。因为“空类型”可
以包容“有类型”,而“有类型”则不能包容“空类型”。有些语句在编译时就可能会出错
如果函数没有返回值,那么应声明为 void 类型
在 C 语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。但是许多程序员却误以为其为 void 类型。为了避免混乱,我们在编写 C 程序时,对于任何函数都必须一个不漏地指定其类型。如果函数没有返回值,一定要声明为 void 类型。这既是程序良好可读性的需要。也是编程规范性的要求。另外,加上 void 类型声明后,也可以发挥代码的“自注释”作用。所
谓的代码的“自注释”即代码能自己注释自己
如果函数无参数,那么应声明其参数为 void
在 C 语言中,可以给无参数的函数传送任意类型的参数,若函数不接受任何参数,一定要指明参数为 void
千万小心又小心使用 void 指针类型
按照 ANSI(American National Standards Institute)标准,不能对 void 指针进行算法操作,
ANSI 标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指向数据类型大小的。也就是说必须知道内存目的地址的确切值。那么我们该怎么做呢?
可以进行强制类型转化,把void*型指针类型装华为其他类型指针,然后再进行对应的操作
如果函数的参数可以是任意类型指针,那么应声明其参数为 void *
典型的如内存操作函数 memcpy 和 memset 的函数原型分别为:
void * memcpy(void *dest, const void *src, size_t len);
void * memset ( void * buffer, int c, size_t num );
这两个函数就用到了void*类型,关于函数的具体用法感兴趣的话可以自己去cplusplus上面查查看
void 不能代表一个真实的变量。
因为定义变量时必须分配内存空间,定义 void 类型变量,编译器到底分配多大的内存呢?
void简单不?现在你觉得它到底是色,还是空呢?
首先在介绍这个关键字之前,我们先去认识一下“栈”的概念。
内存分成5个区,他们分别是堆,栈,自由存储区,全局/静态存续区,常量存续区。
(1)栈:内存由编译器在需要时自动分配和释放。通常用来存储局部变量和函数参数,函数调用后返回的地址。(为运行函数而分配的局部变量、函数参数、函数调用后返回地址等存放在栈区)。栈运算分配内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(2)堆:内存使用new进行分配,使用delete或delete[]释放。如果未能对内存进行正确的释放,会造成内存泄漏。但在程序结束时,会由操作系统自动回收。
(3)自由存储区:使用malloc进行分配,使用free进行回收。
(4)全局/静态存储区:全局变量和静态变量被分配到同一块内存中,C语言中区分初始化和未初始化的(全局变量、静态数据 存放在全局数据区)
(5)常量存储区:存储常量,不允许被修改。更多的关于内存管理方面的知识大家可以去看看LoveMIss-Y这位前辈的文章
https://blog.csdn.net/qq_27825451/article/details/102572795
我在这里就直接说一个概念了:
调用函数,形成栈帧
函数返回,释放栈帧
知道了上面概念之后我们再看一下return
函数调用时先给main函数一次性在栈上开辟足够的空间,我们称之为“栈帧”。关于如何估算我们整个程序所需相应的空间大小,那么就要回到我们文章刚开始时关于变量定义那里了,实质上是根据我们定义的变量类型以及核算关键字来估计我们程序所需的空间大小。
所有的一般临时变量基本都是在一个栈帧结构中定义的,需要使用栈帧中的其他临时变量,是要以我们的栈帧为依托来开辟自己的栈帧空间。栈帧结构都被释放掉了,那么给该变量的或该栈帧内的变量开辟的空间也就不存在了,他也应该被释放掉了,这里所谓的释放就是允许被覆盖。
但是我们栈帧空间有限,所以我们要主动的去释放掉空间,return就是用来终止一个函数(释放空间)并返回其后面跟着的值
return (Val); //此括号可以省略。但一般不省略,尤其在返回一个表达式的值时。return 可以返
回些什么东西呢?看下面例子:
char * Func(void) { char str[30]; … return str; }
str 属于局部变量,位于栈内存中,在 Func 结束的时候被释放,所以返回 str 将导致错误。
return 语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁
函数的返回值,就是通过寄存器的方式返回给函数调用方!
//习惯格式
const int a = 10;
const修饰的变量不可直接被修改,正常情况是只读,可以通过指针的形式间接去修改。例如:
int *p = &a;
//之后再通过对指针p进行修改来改变a的值
那么就有人会有疑问,既然能够被改变,那么我们要const何用?难不成是看C语言太简单了,给我们增加点难度?请看下去。
所有的报错都是在我们编译代码时报错,而不是把程序编过来运行起来报错,所以我们就要想办法早点发现代码错误,以减少成本。const修饰的变量就是不想让别人或者自己忘了,对这个变量进行了修改,让编译器直接进行修改式检查。所以const不能称为真正的常量(见下面代码),程序只有在运行的时候才能知道它的大小。
#include
int main()
{
const int n = 100;
int arr[n]; //vs中是不通过的
//因为数组空间开辟时,他的元素个数是一定的,中括号里的必须是常量
//这就间接的说明了const修饰的不能算是真正的常量,我们叫它常变量
return 0;
}
而相对的,真正不可被修改的是字符串常量,大家下去可以自己动手定义一个字符型指针,将字符串赋给这个指针,让后再对指针进行解引用外加赋值,这时候编译器就会无情的发出告警!
定义或说明一个只读数组可采用如下格式:
int const a[5]={1, 2, 3, 4, 5};
const int a[5]={1, 2, 3, 4, 5};
const int *p; const int *const p; |
// p 可变, p 指向的对象不可变 //指针 p 和 p 指向的对象都不可变 |
2.15.5 记忆和理解的方法:
先忽略类型名(编译器解析的时候也是忽略类型名),我们看 const 离哪个近。 “近水楼台先得月”,离谁近就修饰谁。
const int *p; const int *const p; |
//const 修饰*p,p 是指针, *p 是指针指向的对象,不可变 //前一个 const 修饰*p,后一个 const 修饰 p,指针 p 和 p 指向的对象都不可变 |
const 修饰符也可以修饰函数的参数,当不希望这个参数值被函数体内意外改变时使用。例如:
void Fun(const int i);
告诉编译器 i 在函数体中的不能改变, 从而防止了使用者的一些无意的或错误的修改。
#include
//告诉编译器,告诉函数调用者,不要试图通过指针修改返回值指向的内容
const int* test()
{
static int g_var = 100;
return &g_var;
}
int main()
{
int *p = test(); //有告警
//const int *p = test(); //需要用const int*类型接受
*p = 200; //这样,【在语法/语义上】,限制了,不能直接修改函数的返回值
printf("%d\n", *p);
return 0;
}
//一般内置类型返回,加const无意义
这个关键字是最不为人知,但确实最考察一个程序员C语言学习深度一个关键字。
volatile 关键字和 const 一样是一种类型修饰符, 用它修饰的变量表示可以被某些编译器
未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编
译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
int i=10;
int j = i; //(1)语句
int k = i; //(2)语句
这时候编译器对代码进行优化,因为在(1)、(2)两条语句中, i 没有被用作左值。这时候
编译器认为 i 的值没有发生改变,所以在(1)语句时从内存中取出 i 的值赋给 j 之后,这个值并没有被丢掉,而是在(2)语句时继续用这个值给 k 赋值。编译器不会生成出汇编代码重新从内存里取 i 的值,这样提高了效率。但要注意: (1)、(2)语句之间 i 没有被用作左值才行。
在这里我解释一下左值和右值的问题:
int x; x = 100; //x的空间,变量的属性,左值 int y = x; //x的内容,数据的属性,右值 //同时也可以说明,任何一个变量名在不同的应用场景中代表不同的含义! int *p =&a; p = &b; //左值就是空间,我们将右值内容放到左值(空间)里去 q = p; //右值是内容:就是一个地址 //so,指针就是地址 //指针变量是一个变量,只不过里面保存的是地址数据
我们再看一个例子:
volatile int i=10;
int j = i; //(3)语句
int k = i; //(4)语句
volatile 关键字告诉编译器 i 是随时可能发生变化的,每次使用它的时候必须从内存中取出 i 的值,因而编译器生成的汇编代码会重新从 i 的地址处读取数据放在 k 中。
这样看来,如果 i 是一个寄存器变量或者表示一个端口数据或者是多个线程的共享数据,就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问
const要求你不要进行写入就可以。volatile意思是你读取的时候,每次都要从内存读。两者并不冲突。
虽然volatile就叫做易变关键字,但这里仅仅是描述它修饰的变量可能会变化,要编译器注意,并不是它要求对应变量必须变化!这点要特别注意。
我们在写代码的时候并不是只在一个文件里面写,只在一个文件里面写会提高我们维护成本,所以我们要尽量使用多文件,关于多文件,我们在static那里提到过,忘记的可以回头瞅两眼再回来。我们使用多文件时,要保证我的文件之间是可以交互的(可以互相传递信息),那么这个时候就需要extern来帮我们告诉编译器:这个变量,这个函数……来自其它文件。
extern 可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,下面的代码用到的这些变量或函数是外来的,不是本文件定义的,提示编译器遇到此变量和函数时在其他模块中寻找其定义。就好比在本文件中给这些外来的变量或函数带了顶帽子,告诉本文件中所有代码,这些家伙不是土著。下面是《C语言深度解剖》中的例子,大家可以看看
A.c 文件中定义: B.c 文件中用 extern 修饰:
int i = 10; extern int i; //写成 i = 10;行吗?
void fun(void) extern void fun(void); //两个 void 可否省略?
{
//code
}
C.h 文件中定义: D.c 文件中用 extern 修饰:
int j = 1; extern double j; //这样行吗?为什么?
int k = 2; j = 3.0; //这样行吗?为什么
struct是一个神奇的关键字,我叫它“打包员”,因为它将一些相关联的数据打包成一个整体,方便使用。它就是我们C语言后面说的结构体。
在网络协议、通信控制、嵌入式系统、驱动开发等地方,我们经常要传送的不是简单的字节流(char 型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。经验不足的开发人员往往将所有需要传送的内容依顺序保存在 char 型数组中,通过指针偏移的方法传送网络报文等信息。这样做编程复杂,易出错,而且一旦控制方式及通信协议有所变化,程序就要进行非常细致的修改,非常容易出错。这个时候只需要一个结构体就能搞定。平时我们要求函数的参数尽量不多于 4 个,如果函数的参数多于 4 个使用起来非常容易出错(包括每个参数的意义和顺序都容易弄错), 效率也会降低。这个时候,可以用结构体压缩参数个数。
结构体所占的内存大小是其成员所占内存之和,这里绝不是简单的各个结构体成员大小相加,而是涉及到了另外一个概念:结构体的内存对齐,现在主要讨论空结构体的问题,之后再专门对“内存对齐”进行讲解,这里不多赘述。
struct student
{
}stu;
这个空结构体的内存是多大呢,我们再不同的编译器下,编译结果是不同的:
(1)VS下,我们的代码是不能被编译通过的
(2)gcc它允许我们的代码被编过,编译结果为0
(3)VC++ 6.0下也让编过,编译结果为1byte
编译器认为任何一种数据类型都有其大小,用它来定义一个变量能够分配确定大小的空间。既然如此,编译器就理所当然的认为任何一个结构体都是有大小的,哪怕这个结构体为空。那万一结构体真的为空,它的大小为什么值比较合适呢?假设结构体内只有一个 char 型的数据成员,那其大小为 1byte(这里先不考虑内存对齐的情况) .也就是说非空结构体类型数据最少需要占一个字节的空间,而空结构体类型数据总不能比最小的非空结构体类型数据所占
的空间大吧。所以我们VC就选了1做空结构体大小。
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少一个其他成员。 柔性数组成员允许结构中包含一个大小可变的数组。sizeof 返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
typedef struct st_type
{
int i;
int a[0];
}type_a;
有些编译器会报错无法编译可以改成:
typedef struct st_type
{
int i;
int a[];
}type_a;
这样我们就可以定义一个可变长的结构体,用 sizeof(type_a)得到的只有 4,就是sizeof(i)=sizeof(int)。那个 0 个元素的数组没有占用空间,而后我们可以进行变长操作了。通
过如下表达式给结构体分配内存:
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
这样我们为结构体指针 p 分配了一块内存。用 p->item[n]就能简单地访问可变长元素,但是这时候我们再用 sizeof(*p)测试结构体的大小,发现仍然为 4。这是为什么?其实柔性数组就相当于模子,在定义这个结构体的时候,模子的大小就已经确定不包含柔性数组的内存大小。它与结构体没有什么关系,只是一个编外人员,不占结构体的编制。
上面既然用 malloc 函数分配了内存,肯定就需要用 free 函数来释放内存:free(p);
union 关键字的用法与 struct 的用法非常类似,但在本质上确实截然相反的。
union 维护足够的空间来置放多个数据成员中的“一种”,而不是为每一个数据成员配置空间,在 union 中所有的数据成员共用一个空间,同一时间只能储存其中一个数据成员,所有的数据成员具有相同的起始地址。
union StMa{
char character;
int number;
char *str;
double exp;
};
一个 union 只配置一个足够大的空间以来容纳最大长度的数据成员,以上例而言,最大
长度是 double 型态,所以 StMa 的空间大小就是 double 数据类型的大小。
请看下面的代码片段:
union
{
int i;
char a[2];
}*p, u;
p = &u;
p->a[0] = 0x39;
p->a[1] = 0x38;
//p.i 的值应该为多少呢?
大小端在前面signed、unsigned关键字那里已经讲过了,这里就不再多说了。
union 型数据所占的空间等于其最大的成员所占的空间。对 union 型的成员的存取都是相对于该联合体基地址的偏移量为 0 处开始, 也就是联合体的访问不论对哪个变量的存取都是从 union 的首地址位置开始。如此一解释,相信大家心里都已经有了答案了吧?大家可以把自己的答案打在评论区
enum就是C语言里面的枚举类型,枚举不是像很多人说的那样没什么用,相反,它是很重要的
//定义方式
enum enum_type_name
{
ENUM_CONST_1,
ENUM_CONST_2,
...
ENUM_CONST_n
} enum_variable_name;
enum_type_name 是自定义的一种数据数据类型名,而 enum_variable_name 为enum_type_name类型的一个变量, 也就是我们平时常说的枚举变量。 实际上enum_type_name
类型是对一个变量取值范围的限定,而花括号内是它的取值范围,即 enum_type_name 类型的变量 enum_variable_name 只能取值为花括号内的任何一个值,如果赋给该类型变量的值不在列表中,则会报错或者警告。enum存的都是常量,,所以枚举类型几乎等价于整型,简单理解enum的本质就是制作一组具有强相关性的常量。
枚举具有自描述性,不用做过多的解释
1), #define 宏常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值。
2),一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
3),枚举可以一次定义大量相关的常量,而#define 宏一次只能定义一个。
枚举显然是可以用宏常量来代替的,但是如果常量多且相关性强,最好使用枚举,而且如果我们需要大量的相关性强的常量,用宏定义就要定义很多次,这会让我们的精力白白浪费在定义上。还有就是枚举有语法检查
顾名思义,type 是数据类型的意思; def(ine)是定义的意思,合起来就是定义数据类型啦!可是这种理解是不正确的,我们或许应该叫它typerename,因为typedef的这种意思是给一个已经存在的数据类型(注意:是类型不是变量)取一个别名,而非定义一个新的数据类型。
在实际项目中,为了方便,可能很多数据类型(尤其是结构体之类的自定义数据类型),需要我们重新取一个适用实际情况的别名。这时候 typedef 就可以帮助我们。例如:
typedef struct student
{
//code
}Stu_st,*Stu_pst;
//涉及到的命名规则前面已经提过了
struct student stu1和 Stu_st stu1是相同的
struct student *stu2;和 Stu_pst stu2;和 Stu_st *stu2;没有区别(如果看不明白可以去看看结构体再过来)
用typedef定义一种新类型时,不能使用该新类型再拼上其他的关键字 / 不能再引入其他关键字来修饰类型或者变量,比如:
typedef int int32;
unsigned int 32 b;
//这种修饰方式很明显就是错的
//不符合语法规范
目前,已经把 C89(C90) 的所有关键字全部介绍完了。下面就要对关键字进行一下分类,方便大家理解。
char :声明字符型变量或函数
short :声明短整型变量或函数
int : 声明整型变量或函数
long :声明长整型变量或函数
signed :声明有符号类型变量或函数
unsigned :声明无符号类型变量或函数
float :声明浮点型变量或函数
double :声明双精度变量或函数
struct :声明结构体变量或函数
union :声明共用体(联合)数据类型
enum :声明枚举类型
void :声明函数无返回值或无参数,声明无类型指针
for :一种循环语句
do :循环语句的循环体
while :循环语句的循环条件
break :跳出当前循环
continue :结束当前循环,开始下一轮循环
if : 条件语句
else :条件语句否定分支
goto :无条件跳转语句
switch :用于开关语句
case :开关语句分支
default :开关语句中的“其他”分支
return :函数返回语句(可以带参数,也看不带参数)
auto :声明自动变量,一般不使用
extern :声明变量是在其他文件中声明
register :声明寄存器变量
static :声明静态变量
typedef :用以给数据类型取别名(但是该关键字被分到存储关键字分类中,虽然看起来没什么相关性)
存储关键字,不可以同时出现,也就是说,在一个变量定义的时候,只能有一个
const :声明只读变量
sizeof :计算数据类型长度
volatile :说明变量在程序执行中可被隐含地改变
好了,C语言关键字部分已经全部讲完了,如果其中有问题的地方请大家指出来;如果觉得有用的千万不要忘了点赞、关注、收藏哦!我们下期再见!