- C语言关键字2
- 分支语句:if
- C语言中的 bool 类型
- 浮点数问题
- 浮点数总结:
- 类型转换的关系
- 指针变量与 "零值" 的比较
- 选择语句 switch-case的使用
- C程序的运行
- 循环语句的使用
- 对返回值的理解
- 函数调用的内存分配
- const关键字的认识
- 如何取地址
- 理解 常量指针 和 指针常量
- const 在函数中的使用
- 最易变的关键字:volatile
- const 和 volatile 并不冲突
- 最会帽子的关键字:extern
- 结构体关键字:struct
- 柔性数组
- 联合关键字 union
- 枚举关键字:enum
- 枚举与宏定义的区别
- 枚举与宏定义的区别
- 缝纫师关键字:typedef 关键字
- typedef使用的注意事项
- typedef 与 #include
分支语句:if,先执行 ( ) 中的表达式或者是函数,得到结果为真,则执行函数体,为假则不执行函数体。
在编码原则中,为了使代码执行清晰,会选择将执行概率更大的代码放在 if 判断中,而将执行概率小的放在 else 中,就是说,要将正常情况放在放在 if 后面进行处理,将异常的 / 意外的情况 放在 else中进行判断执行。
还有在使用 if-else if 结构时,最后应该以 else 结尾,最后的else 语句要 执行适当的动作,要包含合适的注释以说明为何没有执行动作,与switch 语句最后要求具有一个 default 子句是一致的。
C语言中的 bool 类型在 C99 标准之前,也就是C89 或者说是C90 中,认为是没有 bool 类型的,而在C99 中引入了 _Bool 类型,(就是 _Bool 类型,只是在新增的头文件 stdbool.h 中,使用宏定义写成了 bool ,为了保证C和C++ 的兼容性。
在使用微软的编辑器中(VS系列或VSCode)中,大写的 BOOL 类型和 TRUE / FALSE 都是可以支持的,但是这并不是C99 的标准,所以推荐些小写 : bool 和 true / false
对于bool型的使用,要注意以下几点:
#include#include int main() { int flag = 0; if (flag == 0) { //不推荐此写法 //一般用于 int 型的比较,容易造成代码可读性降低 printf("1n"); } if (flag==false) { //不推荐次写法 //false 仅在C99 及以上支持,代码可移植性不够 printf("2n"); } if (!flag) { //推荐,可以直观表示bool类型 printf("3n"); } return 0; }
总之,bool 类型直接作为 判定 条件,不需要把操作符和特定值进行比较
浮点数问题先来看一个简单的问题:
double x = 1.0;
double y = 0.1;
printf("%.50fn", x - 0.9);
printf("%.50fn", y);
if ((x - 0.9) == 0.1)
{
printf("I can hear you!!");
}
else
{
printf("Oh NO !!!");
}
从这个例子中,就要明白,在浮点数进行比较的时候,绝对不能使用 == 来进行比较!!! 浮点数本身就会有精度损失,所以会导致各种结果会有细微差别
所以,如果要比较浮点数的大小,就可通过 做差 与精度进行比较的方法来确定大小
就可以采用如下的精度比较的办法:
#include "test.h" #include#include #include #define EPS 0.00000001 int main() { double x = 1.0; double y = 0.1; printf("%.50fn", x - 0.9); printf("%.50fn", y); if(fabs((x-0.9)-y) 在浮点数的比较中,如果不使用自己宏定义的常量,也可以使用系统自定义的一个常量来作为比较对象
#include//需要包含的头文件 DBL_EPSILON //double 最下精度值 FLE_EPSILON //float 最小精度值 如下:
#include#include int main() { double x = 0.000000001; if (fabs(x) 如果足够小的话,就可以得到想要的结果了:
#include#include int main() { double x = 0.0000000000000001; if (fabs(x) 在进行比较时,尽量不要写等于号,因为即使 xxx_EPSILON 这个数很小, 也不能认为它 就是0,所以在使用它时,是不可以和 0 等价的
浮点数总结:类型转换的关系
- 浮点数存储 是有精度损失的
- 浮点数不能进行 == 的比较
- 使用 if ( fabs ( a - b ) < DEL_EPSILON ) { } 来进行浮点数比较
- 不需要使用 <= 或者 >= 等于号是没有必要的
强制类型转换:不改变内存中的数据(二进制数是不变的),只改变对应的类型
数据转换:改变内存中的数据,比如字符串 “12345” 转换为 int 型的 12345,是由6个字节转换为了四字节,内存中的二进制数发生了变化。
指针变量与 “零值” 的比较指针 就是地址
指针变量 就是一个变量,里面存放的内容是地址
对于空指针的判断,要注意规范
int *p = NULL; if(p == 0) if(p != 0 ) //不推荐 if ( p ) if( !p ) //不推荐 if( NULL == p ) if( NULL != p) //推荐写法选择语句 switch-case的使用case 作为条件判断的依据,break 则作为分支功能的存在
有以下注意点:
C程序的运行
- 每个case语句的结尾不要忘记了 break,否则将导致多个分支重叠(除非有意使多个分支重叠)
- 在所有case 语句之后,一定要加上 default 语句,表示所有case都不符合的情形
- 习惯上将 default 语句放在最后,即使将default 语句放在switch 语句的任何位置都可以,但是习惯如此,而且,在实际使用中,default语句最好是用于默认的其他情形,而不要是预想的最后一种情形。
- 匹配一个条件执行多个语句时,要注意在一个条件中是无法定义变量的,如果一定要定义新变量,则需要使用 { } 来进行包装,使之成为一个代码块。实际编程中,并不推荐这个写法,最好将代码块封装为函数
- 在case语句的匹配中,case后面的必须为真正的 int、char 等真正的常量,而不应该是被const 修饰的(虚假的)常量,这样的常量是无法通过编译的,但是使用 #define 定义的常量是可以通过编译的
- 一般在正常情况下需要按照一定的顺序将case 语句进行排列,遇到特殊的情形,也应该把最频繁执行的case 语句放在前面,执行次数较少的 case 语句放在后面
- 多个case 语句可以使用多条件匹配:
任何C程序,在默认编译好之后,在运行时,都会打开三个输入输出流: 可以理解为一切皆为文件
- stdin : 标准输入,FILE* stdin; --> 键盘
- stdout : 标准输出,FILE* stdout; --> 显示器
- stdrr : 错误输出,FILE* stderr; --> 显示器
所以可以将 键盘、显示器 称为 字符型设备
这样就需要正确理解 getchar 的使用,使用getcahr 进行接收字符时,先是接受各个字符,然后格式化为指定的类型,放入变量中,所以要注意该函数会默认读入输入的最后一个回车符,同样的,在进行输出时,显示在屏幕上的也全部都是字符,int 型也是以字符 格式化 并显示在屏幕上的,例子:
//在屏幕上输出的内容就是字符,并且每个都对应一个ASCII字符 int ret = printf("%dn", 1234); printf("%dn", ret); //四个字符和一个换行符,就是5了break终止本层循环
continue终止本次循环
循环语句的使用对返回值的理解
在多重循环中,如果有可能,应将最长的循环放在最内层,最短的循环放在最外层,可以减少CPU 跨切循环的次数(不强求)
使用for 循环控制变量的取值 采用 “半开半闭区间”写法(可以直观的表示出循环次数)
循环体要尽可能的简介,循环嵌套尽量不要超过三层
goto 语句尽量不要使用,虽然可以灵活跳转(在代码块内,无法跨函数,跨文件)但是如果不加限制,就会破坏结构化设计风格,甚至有可能会带来错误或隐患,会跳过变量的初始化、重要的计算等语句。
先看代码:
void test() { printf("hello world!"); return 1; } int main() { test(); return 0; }这样的代码是不会报错且可以正常运行的,运行的结果就是: hello world!虽说函数是void 类型,但是却强加了一个return 语句,因为在运行的时候,是直接运行了 test() 函数,并没有明确规定接受该函数的结果的类型,如果做了明确规定,比如 int a = test(); 那么就会直接报错了。
在C语言中也要注意,void是被解释为无大小的类型,但是他本身也是有大小,如果使用VS 来 输出 sizeof (void ) 会得到结果为 0 ,而如果使用gcc(Linux) 来输出 sizeof(void ) 则会得到结果为1,要切实注意。
C语言中函数可以不使用返回值,函数的默认的返回值为 int ,但是不推荐使用这种写法,要明确函数的返回值
void 修饰函数的返回值,可以作为占位符:让用户明确知道不需要返回值,也可以告知编译器,这个无法接受返回值
void 充当函数的形参列表,告知用户/编译器,该函数不需要参数
但是 void* 可以用来接受任意指针类型,这样就可以用来设计通用接口
为什么VS中和Linux中C语言有所不同?
函数调用的内存分配
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mxvZOjSM-1633229320859)(D:oneDrive - iswilliam博客学习C语言关键字12.png)]在 C语言中,在调用一个函数时,就是在内存的 栈区 开辟一段空间,这段空间用来存放该函数的所有内容,不会太大占用过多内存空间,也不会过小,导致无法成功放入函数,这样的可以正好放下一个函数的内存空间就可以成为 栈帧。
调用函数,形成栈帧, 函数返回,销毁栈帧(数据依然在内容中,只是指向该栈帧的指针移动了位置)
该栈帧的大小是由 程序在编译时,对各个变量类型的大小的预估得到的,所以大小是正好合适的。
这样也可以解释:为什么 临时变量是具有临时性的?
因为栈帧结构在函数调用完毕后,需要被释放
下面个例子可以更好的理解临时性:
#include#include int GetData() { int x = 0x11223344; printf("run get data!n"); return x; } int main() { int y = GetData(); printf("ret: %xn", y); return 0; } 只看结果的话,在函数调用时,成功的输出了函数中的临时变量的内容,而不是取到了 调用函数中的变量
通过反汇编的方式来观察地址跳转,可以发现: 函数的返回值,通过寄存器的方式返回给函数调用方,而且无论是否有变量接受函数返回值,函数都会将返回值放入寄存器中
const关键字的认识const 修饰的变量不可以被直接修改,但是可以通过修改指针来实现修改:
const int a = 10; //a 不可以被直接修改 int* p = (int*)&a; //转换为 指针类型 printf("before: %dn", a); *p = 20; //解引用 printf("after: %dn", a);所以使用const 进行修饰的意义是:
- 告知编译器进行 直接修改式 检查,防止被修改,可以防止可能出现的逻辑问题
- 告知其他程序员,此变量后续 不要进行修改,有"自描述"含义
真正的不可修改:
所以说使用const 修饰的变量并不真的是变成了常量,仅仅是作为不可轻易修改的变量:
如何取地址此段代码在 VS 编译器下无法正常运行,在 gcc (GNU 拓展) 下就可以正常运行了,也就是在Linux 平台可以正常编译运行,所以处于跨平台性,不要出现这种写法
在C语言中,任何变量在取地址时,都是从最低地址开始的
理解 常量指针 和 指针常量常量指针:
int a = 10; int b = 20; //常量指针 const int* p = &a; //p指向的变量不可以直接被修改,保存的地址可以被修改 //int const* p = &a; //表达含义与上相同,一般使用上面的定义方法,比较规范 / return 0; }联合体内所有变量的其实地址都是一样的(都是从低地址开始存放),每个联合体内的变量都可以认为是第一个元素
而且在数据存储时,采用的是小端存储,就是数据的低权值位是存储在联合体的低地址位 的。
还有一点,在判断联合体占用空间大小时要考虑内存对齐!
#includeunion un { int a; char b[5]; //在考虑内存对齐整除时,将这个数组看为char,为1,但是在计算大小时看做整体,为5 }; int main() { printf("un 的大小为:%dn", sizeof(union un)); return 0; } 联合体内存对齐:联合体所占的内存大小,需要可以整除联合体内任何一个变量的大小
枚举关键字:enum枚举关键字定义的均为常量,枚举变量可以直接作为常量使用,甚至可以将枚举常量看做是整数
如果对枚举常量进行赋值,则其后是连续赋值的,也可以实现阶段性赋值
#includeenum color { RED=1, YELLOW, BLACK=10, GREEN, BLUE=100 }; int main() { enum color c = RED; printf("%dn", RED); printf("%dn", YELLOW); printf("%dn", BLACK); printf("%dn", GREEN); printf("%dn", BLUE); return 0; } 枚举与宏定义的区别使用枚举可以很好的描述事物需要的特性(状态),尤其是含有多个,不适合使用 define
而且使用枚举可以很好的避免"魔鬼数字"问题,因为使用枚举可以很清晰的表名这个一个枚举变量,而不是像数字一样,如果没有注释说明,他人阅读代码很难明白这个数字的含义,可以增加 自描 属性
如果在项目中常量使用不多且相关性较低,就使用宏定义,否则就可以选择使用枚举
- #define 宏常量是在编译阶段进行简单替换,枚举常量则是在编译的时候确定其值
- 一般在调试器中,可以调试枚举常量,却不能调试宏常量
数如果对枚举常量进行赋值,则其后是连续赋值的,也可以实现阶段性赋值
#includeenum color { RED=1, YELLOW, BLACK=10, GREEN, BLUE=100 }; int main() { enum color c = RED; printf("%dn", RED); printf("%dn", YELLOW); printf("%dn", BLACK); printf("%dn", GREEN); printf("%dn", BLUE); return 0; } 枚举与宏定义的区别使用枚举可以很好的描述事物需要的特性(状态),尤其是含有多个,不适合使用 define
而且使用枚举可以很好的避免"魔鬼数字"问题,因为使用枚举可以很清晰的表名这个一个枚举变量,而不是像数字一样,如果没有注释说明,他人阅读代码很难明白这个数字的含义,可以增加 自描 属性
如果在项目中常量使用不多且相关性较低,就使用宏定义,否则就可以选择使用枚举
缝纫师关键字:typedef 关键字
- #define 宏常量是在编译阶段进行简单替换,枚举常量则是在编译的时候确定其值
- 一般在调试器中,可以调试枚举常量,却不能调试宏常量
- 枚举可以一次定义大量相关的常量,而#define宏 一次只能定义一个
typedef 的作用就是给变量做重命名
使用方法:
#includeusing namespace std; //将关键字名 重命名 typedef unsigned int u_int; //将结构体 进行重命名 typedef struct stu { char name[16]; int age; char sex; }stu_t; int main() { u_int x = 0; cout << "关键字重命名:" << x << "n"; stu_t s; s.age = 22; cout << "结构体重命名: " << s.age << "n"; return 0; } 在实际项目中,要切忌对 变量类型进行过度重命名,这样会大大降低 代码的可读性,这里推荐对结构体进行重命名,而不要对其他类型再进行重命名了
typedef使用的注意事项在 C/C++ 中要注意,在实际编码过程中,要注意编码规范
在这里,a 为指针(可以理解为靠 * 更近),b 为整形//甚至可以这样定义,但是不推荐这样使用 int *a = NULL, b=0; //a 为指针,b 为整形但是,如果使用 typedef 进行重命名之后,在使用这个语句进行定义后,就会发现 a,b 均为指针了
所以在理解typedef 时,不能简单的将其理解为给该类型取了个别名,而是可以认为 姓名字是 一个新的 类型,这个类型可以对 使用该类型进行定义的变量 进行统一定义
所以在实际开发中,如果要定义指针或是整形,最好单独的 分开 进行定义
typedef 与 #includetypedef 是类型重命名,但本质上并不属于宏 的替换,相当于是一个新的类型,所以在使用这个类型时,和使用 double/float 等没有本质区别,而 如果是 #define ,相当于全局替换,将所有的 该变量 都替换为 宏定义的变量
在代码中也可以看到,使用 #define 的效果和 使用 int* 效果是一模一样的
在使用 typedef 给变量起别名之后,这个变量就不再是之前的那个变量了,而可以看做是和原关键字效果一样的另一个关键字,并且能力受限
typedef int int32 int main() { //错误写法,会直接报错,在使用typedef进行取别名之后,就无法再继续满足之前该关键字所有的功能了 unsigned int32 a; return 0; }关于C语言关键字的总结就基本完成了,后面会继续更新其他内容的!(。・∀・)ノ*
感谢观赏,慢慢提高



