2.8 signed、unsigned关键字
整型的存储
2.8.1 原码、反码、补码的补充
我们在将计算机中的数据拿出来,并将二进制形式转化为十进制形式有两种方法:
方法一:
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,在符号位不变,按位取反。
方法二:将原码到补码的过程在来一遍。
2.8.2 深入理解变量内容的存入和取出
在图中提到了空间是不关心内容的,那么这里的变量类型什么时候起效果呢?
我们在这里举个“栗子”:假如我口袋有一百元,那么我身上有多少钱?
答案可能是一百也可能不是,因为我们并不知道这里的100是哪种货币
so,100在这里是没有意义的;数字要带上类型才有意义
是在读取的时候,具有意义!!!
类型决定了如何解释空间内部保存的二进制序列
变量存的过程:字面数据必须先转成补码,在放入空间当中。所以,所谓符号位,完全看数据本身是否携带+-号。和变量是否有符号无关!
变量取的过程:取数据一定要先看变量本身类型,然后才决定要不要看最高符号位。如果不需要,直接二进制转成十进制。如果需要,则需要转成原码,然后才能识别。(当然,最高符号位在哪里,又要明确大小端)
-10:1111 1111 1111 1111 1111 1111 1111 0110
4294967286:1111 1111 1111 1111 1111 1111 1111 0110
虽然打印出来转化后的二进制序列数值是一样的,但是十进制数值却不同,这就是由于最高位是否为符号位造成的差异
2.8.3 二进制快速转化口诀
2.8.4 大小端
如何理解大小端:
大家应该都知道int 类型的变量大小是四个字节,而内存存储也是以字节为单位的,所以我们在存储数据时,数据也要按照字节为单位划分成若干块,数据按照字节为单位,也是有高权值和低权值之分的,所以我们存取的顺序是有区别的,但是,无论如何放,只要同等条件去取,都是可以的!
如何存取是由计算机、内存硬件厂商决定的,但是每个厂商都有不同的标准,由此产生了两种存储方案:
1.大端:按照字节为单位,低权值位数据存储在高地址处,就叫做大端
2.小端:按照字节为单位,低权值位数据存储在低地址处,就叫做小端
举个例子:
int a = 0x11223344
地址: 0xA0 0xA1 0xA2 0xA3
11 22 33 44 大端
44 33 22 11 小端
2.8.5 记忆口诀
低权值位 — 权值位比较小
低地址处 — 地址数字比较小
权值位比较小的放在地址数字比较小的地方,就叫做小端存储
所以我们可以用“小小小”来记忆小端存储,不然则为大端存储的
大小端是如何影响数据存储的
大小端存储方案,本质数据和空间按照字节为单位的一种映射关系
2.8.6 练习题
2.9 if、else组合
2.9.1 什么是语句
C语言中由一个分号;隔开的就是一条语句。
比如:
printf("hehe");
1+2;
2.9.2 什么是表达式
C语言中,用各种操作符把变量连起来,形成有意义的式子,就叫做表达式。
操作符:+,-,*,/,%,>,<,=,==...
2.9.3 基本语法
语法结构: //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; }
2.9.4 if和else匹配问题
else 匹配采取就近原则
它总是与离他最近的if进行匹配
在书写代码的过程中尽量使用锯齿型,如上面的嵌套代码,就不会有匹配问题
2.10 _BOOL类型
2.10.1 什么是布尔类型
//测试代码 #include <stdio.h> #include <stdbool.h> //布尔类型的头文件 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 //真
2.10.2 bool值和0比较
#include <stdio.h> #include <stdbool.h> 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; }
2.10.3 bool类型总结
C89,C90没有标准的bool类型,C99有正式的bool类型的
推荐写法:直接小写加true或false,在这里要记得C99标准的bool、true、false全部都要小写
如果大写的话就是微软的另一套布尔类型的标准了,Microsoft版本可移植性较差,尽量不用
和0比较的话,if直接判定布尔值结果,不用进行等于,等于0,或者等于false这样的写法,
就是bool类型,直接判定,不用操作符进行和特定值比较
2.10.4 浮点型和指针类型与零值的比较
相关分析在我的另一篇博客【C和0的不解之缘】里有详细的解析,请大家移步观看。
2.11 switch、case组合
2.11.1 基本语法
switch(整型变量/常量/整型表达式) { case var1: break; case var2: break; case var3: break; default: break; }
2.11.2 不要拿青龙偃月刀去削苹果
已经有if else为何还要switch case
switch语句也是一种分支语句,常常用于多分支的情况。这种多分支,一般指的是很多分支,而且判定条件主要以整型为主,如:
输入1,输出星期一
输入2,输出星期二
输入3,输出星期三
输入4,输出星期四
输入5,输出星期五
输入6,输出星期六
输入7,输出星期日
如果写成 if else 当然是可以的,不过比较麻烦
#include <stdio.h> 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; }
2.11.3 规则:
把 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;//不可,很可能违背了你的原意 … }
2.11.4 case在switch的作用是什么?
case本质是进行判断功能
2.11.5 break在switch中的作用是什么?
break本质其实是进行分支功能
2.11.6 没有break会有什么问题?
#include <stdio.h> 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 <stdio.h> 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修饰的只读变量
2.11.7 default语句相关问题
default可以出现在switch内的任何部分
尽管如此,我们依旧强烈推荐default应该放在case语句的最后
2.12 goto关键字
一般来说,编码的水平与 goto 语句使用的次数成反比。有的人主张慎用但不禁用 goto语句,但是在《C语言深度解剖》这本书中,作者却主张禁用
自从提倡结构化设计以来, goto 就成了有争议的语句。首先,由于 goto 语句可以灵活跳转,如果不加限制,它的确会破坏结构化设计风格;其次, goto 语句经常带来错误或隐患。它可能跳过了变量的初始化、重要的计算等语句。
struct student *p = NULL; … goto state; p = (struct student *)malloc(…); //被 goto 跳过,没有初始化 { ⋯ state: //使用 p 指向的内存里的值的代码 ⋯ }
如果编译器不能发觉此类错误,每用一次 goto 语句都可能留下隐患
2.13 void关键字
2.13.1 色即是空
void 有什么好讲的呢?如果你认为没有,那就没有;但如果你认为有,那就真的有。有点像“色即是空,空即是色”。
2.13.2 void a
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 *也可以无需强制类型转换地赋给其它类型的指针。因为“空类型”可
以包容“有类型”,而“有类型”则不能包容“空类型”。有些语句在编译时就可能会出错
2.13.3 void 修饰函数返回值和参数
如果函数没有返回值,那么应声明为 void 类型
在 C 语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。但是许多程序员却误以为其为 void 类型。为了避免混乱,我们在编写 C 程序时,对于任何函数都必须一个不漏地指定其类型。如果函数没有返回值,一定要声明为 void 类型。这既是程序良好可读性的需要。也是编程规范性的要求。另外,加上 void 类型声明后,也可以发挥代码的“自注释”作用。所
谓的代码的“自注释”即代码能自己注释自己
如果函数无参数,那么应声明其参数为 void
在 C 语言中,可以给无参数的函数传送任意类型的参数,若函数不接受任何参数,一定要指明参数为 void
2.13.4 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上面查查看
2.13.5 void不能代表一个真实的变量
void 不能代表一个真实的变量。
因为定义变量时必须分配内存空间,定义 void 类型变量,编译器到底分配多大的内存呢?
void简单不?现在你觉得它到底是色,还是空呢?
2.14 return 关键字
2.14.1 内存管理
首先在介绍这个关键字之前,我们先去认识一下“栈”的概念。
内存分成5个区,他们分别是堆,栈,自由存储区,全局/静态存续区,常量存续区。
(1)栈:内存由编译器在需要时自动分配和释放。通常用来存储局部变量和函数参数,函数调用后返回的地址。(为运行函数而分配的局部变量、函数参数、函数调用后返回地址等存放在栈区)。栈运算分配内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(2)堆:内存使用new进行分配,使用delete或delete[]释放。如果未能对内存进行正确的释放,会造成内存泄漏。但在程序结束时,会由操作系统自动回收。
(3)自由存储区:使用malloc进行分配,使用free进行回收。
(4)全局/静态存储区:全局变量和静态变量被分配到同一块内存中,C语言中区分初始化和未初始化的(全局变量、静态数据 存放在全局数据区)
(5)常量存储区:存储常量,不允许被修改。
更多的关于内存管理方面的知识大家可以去看看LoveMIss-Y这位前辈的文章
https://blog.csdn.net/qq_27825451/article/details/102572795
我在这里就直接说一个概念了:
调用函数,形成栈帧
函数返回,释放栈帧
知道了上面概念之后我们再看一下return