C进阶总结一 -- <<C语言深度解剖>>

打印 上一主题 下一主题

主题 906|帖子 906|积分 2718

C进阶总结 --



程序的本质:二进制文件

运行程序,即将程序中的数据加载到内存中运行
为什么要加载到内存? 1.冯诺依曼体系决定 2.快
变量

1.变量:内存上的某个位置开辟的空间

由于变量都是程序运行起来才开辟的
2.变量的初始化:

变量的空间被开辟后,就应当具有对应的数据,即必须要初始化.表示该变量与生俱来的属性就是该初始值
3.为什么要有变量

盘算机是为了解决人盘算能力不足的问题而诞生的.即,盘算机是为了盘算的.
而盘算,就需要数据
而要盘算,任何时刻,不是全部的数据都要立马被盘算,因此有的数据需要暂时被保存起来,等待后续处理. 以是需要变量
4.局部变量与全局变量


  • 局部变量:包含在代码块中的变量叫做局部变量.局部变量具有临时性.进入代码块,自动形成局部变量,退出代码块自动开释. 局部变量在栈区保存
  • 全局变量:在全部函数外定义的变量,叫做全局变量.全局变量具有全局性.全局变量在全局已初始化数据区保存.
  • 代码块:用花括号{}括起来的区域,就叫做代码块.
5.变量的大小由范例决定

6.任何一个变量,内存赋值都是从低地址开始往高地址

以是首地址和取地址永久都是低地址
1.1 关键字auto

默认情况下,编译器默认全部的局部变量都是auto的,auto一般只能用来修饰局部变量,不能修饰全局变量.ju'bu也叫自动变量.一般情况下都是省略auto关键字的.基本永不使用
1.2 关键字register

建议性关键字,建议编译器将该变量优化到寄存器上,详细情况由编译器决定
(不建议大量使用,由于寄存器数量有限)
什么样的变量可以采用register?


  • 局部的(全局会导致CPU寄存器被长时间占用)
  • 高频被读取的(提高访问效率)
  • 不会被写入的(写入就需要写回内存,后续还需要读取检测的话,register就没有意义了)
寄存器变量是不能被取地址的,由于不在内存中,就没有内存地址
register不会影响变量的生命周期,只有static会影响变量的生命周期
1.3.1 多文件(extern):

具有一定规模的项目是需要文件与文件之间进行交互的.如果不能直接跨文件调用,则项目在这方面一定需要很大成本解决.因此C默认是支持跨文件的

  • extern

    • 功能:声明,引入别的源文件的变量或函数
    • extern与头文件的渊源:
      最开始时,没有头文件,源文件之间的互相引用是通过extern进行的.当项目复杂后,引用需要写的声明越来越多,每个源文件引入别的源文件的变量或函数时都需要声明一次,维护变得麻烦.为了解决这种情况,头文件就出来了,只需声明一次就可以到处使用.
      现在基本上多文件项目都是头文件放声明,源文件放定义
    • 怎么声明变量和函数?

      • 变量:
        定义:int a = 10;
        声明:extern int a; (不能给声明赋值,由于声明不能开辟空间)
        (声明变量时必须带extern,由于不带会区分不了是声明还是定义)
      • 函数:
        定义:void print(){ //... }
        声明:extern void printf(); (不能带上函数体)
        (声明时建议带上extern,不带也行,由于声明不带函数体,且以分号结尾,有显着区分度)


  • 头文件


  • 头文件一般包含:

    • C头文件
    • 全部的变量的声明
    • 全部函数的声明
    • define, 范例typedef, struct

  • 头文件中函数的声明可以不带上extern, 但是变量的声明必须带上extern, 由于头文件最终是要展开到源文件中去的,源文件内可以定义和声明,那头文件也可以,如果头文件中变量声明不带extern,则无法区分该变量是声明还是定义(二义性).
1.03.2 static

static给项目维护给提供了安全保证,像封装一样,隐藏实现
功能:

  • static修饰全局变量,该变量只在本文件内被访问, 不能被外部其他文件直接访问
  • static修饰函数,该函数只能在本文件内被访问,不能被外部其他文件直接访问
  • static局部变量酿成静态变量,放在静态区,改变了局部变量的生命周期,使其生命周期变长。

    • static修饰全局变量和函数,改变的是它们的作用域,生命周期不变.static修饰局部变量,改变的是局部变量的生命周期,作用域不变
    • static修饰的局部变量会放在历程地址空间的 已初始化数据区,在历程的整个生命周期内都是有效的.


历程地址空间


  • 代码区
    程序执行代码存放在代码区,其值不能修改(若修改则会出现错误)。
    字符串常量和define定义的常量也有可能存放在代码区。
  • 常量区
    字符串、数字等常量存放在常量区。
    const修饰的全局变量存放在常量区。
    程序运行期间,常量区的内容不可以被修改。
  • 全局区(静态区)
    全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
  • 堆区(heap)
    堆区由程序员分配内存和开释。
    堆区按内存地址由低地址到高地址增长,用malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上。
  • 栈区(stack)
    存放内容
    临时创建的局部变量和const定义的局部变量存放在栈区。
    函数调用和返回时,其入口参数和返回值存放在栈区。


  • 为什么局部变量具有临时性? 由于局部变量是存在栈中的.栈具有后进先出(压栈)的特性,除了作用域后需要将该范围的全部变量弹出.
1.4范例


  • C语言为何有范例? 让我们能够对内存进行公道化划分,按需索取,存在范例的目标就是让我们能公道使用内存空间
  • 范例为什么有这么多种? 实际应用场景许多种,应用场景不同,解决对应的应用场景的盘算方式不同,需要空间的大小也是不同的.多种范例目标是让我们能以最下成本解决多样化的场景问题.
例如: 登记成绩,成绩只要0-100分,那使用一1个字节int8_t/char就足够. 如果带浮点,则需要浮点型.
1.5 关键字sizeof

sizeof是函数还是关键字?

  • 证明1:
  1. int a = 10;
  2. printf("%d\n",sizeof(a));    //正确用法
  3. printf("%d\n",sizeof(int));  //正确用法
  4. printf("%d\n",sizeof a );    //正确用法,证明sizeof不是函数
  5. printf("%d\n",sizeof int );  //不存在
复制代码

  • 证明2:函数调用栈帧中sizeof不会压栈
1.6关键字unsigned和signed

数据在盘算机中的存储


  • 任何数据在盘算机中都必须被转化成二进制,由于盘算机只认识二进制.而盘算机还要区分数据是正数还是负数,则二进制又分为符号位 + 数据位.
  • 盘算机内存储的整型必须是补码
  • 无符号数和正数的原反补码相等,直接存入盘算机中.负数需要将原码转化成补码再存储
  • 范例决定了怎样表明空间内部保存的二进制序列
  • 浮点数默认是double范例,如果想要float需要在数后加上f,如float f = 1.1f;
原码 与 补码的转化与硬件关系
  1. 例: int b = -20; //20 = 16+4 = 2^4^ (10000)~2~+ 2^2^(100)~2~  
  2. //有符号数且负数 原码转成补码:
  3. 1000 0000 0000 0000 0000 0000 0001 0100  原码
  4. 1111 1111 1111 1111 1111 1111 1111 1011  反码 = 原码取反
  5. 1111 1111 1111 1111 1111 1111 1111 1100  补码 = 反码+1
  6. //补码转原码
  7. 方法一: 原理
  8. 1111 1111 1111 1111 1111 1111 1111 1100  补码
  9. 1111 1111 1111 1111 1111 1111 1111 1011  反码 = 补码-1
  10. 1000 0000 0000 0000 0000 0000 0001 0100  原码 = 反码取反
  11. 方法二: 计算机硬件使用的方式, 可以使用一条硬件电路,完成原码补码互转
  12. 1111 1111 1111 1111 1111 1111 1111 1100  补码
  13. 1000 0000 0000 0000 0000 0000 0000 0011  补码取反
  14. 1000 0000 0000 0000 0000 0000 0000 0100  +1
复制代码
原,反,补的原理:

原反补的概念从时钟引入, 8点+2 = 10点. 而8点-10也等于10点.即2是-10以12为模的补码.
-10要转化成2 ,可以用模-10来得到,但硬件中位数是固定的,模数为1000...,最高位会溢出舍弃.即全0.无法做差.
引入反码转换盘算:即2 == 模-10 == 模-1+1-10 == 1111... -10 +1 == 反码+1; 这个111...-10就是反码,即反码+1==补码的由来
在二进制中,全1减任何数都是直接去掉对应的1.以是反码就是原码符号位不变,其余位全部取反
整型存储的本质

定义unsigned int b = -10; 可否正确运行? 答案是可以的.
定义的过程是开辟空间,而空间只能存储二进制,并不关心数据的内容
数据要存储到空间里,必须先转成二进制补码.而在写入空间时,数据已经转化成补码
变量存取的过程


  • 存: 字面数据必须先转成补码,再放入空间中.符号位只看数据本身是否携带+-号,和变量是否有符号无关.
  • 取: 取数据一定要先看变量本身范例,然后才决定要不要看最高符号位.如果不需要,则直接将二进制转成十进制.如果需要,则需要转成原码,然后才能辨认(还需要考虑最高符号位在哪里,考虑大小端)
范例目前的作用


  • 存数据前决定开辟多大的空间
  • 读数据时怎样表明二进制数据
特定数据范例能表示多少个数据,取决于本身全部比特位分列组合的个数
十进制与二进制快速转换
  1. (前置知识:需要熟记2^0到2^10的十进制结果)
  2.     1 -> 2^0
  3.    10 -> 2^1
  4.   100 -> 2^2
  5. 1000 -> 2^3  //1后面跟3个比特位就是2^3
  6. 规律: 1后n个0就是2^n,即n等于几1后面就跟几个0 --- 十进制转二进制
  7.         反过来就是1后面跟几个0,就是2的几次方 --- 二进制转十进制
  8. 因此:2^9 -> 10 0000 0000 // n
  9. 例: 67 = 64+2+1 -> 2^6+2^1+2^0 -> 1000000 + 10 + 1
  10.        = 0000 0000 .... 0100 0011
  11. 同理,二进制转十进制逆过程即可
复制代码
大小端字节序

征象: vs的内存窗口中,地址从上到下依次增大,从左到右也依次增大

  • 大端:按照字节为单位,低权值位数据存储在高地址处,就叫做大端
  • 小段:按照字节为单位,低权值位数据存储在低地址处,就叫做小端(小小小)
(基本上以小端为主,大端比力少(网络))
大小端存储方案,本质是数据和空间按照字节为单位的一种映射关系
(考虑大小端问题是1字节以上的范例.short,int,double...)
判断当前呆板的字节序


  • 方法1: 对int a = 1 取首地址,然后(char*)&a,得到的值是1则为小端,否则为大端
  • 方法2: 打开内存窗口查看地址与数据的字节序
"负零"(-128)的明白
  1. (负零的概念并不存在,只是碰巧相像)
  2. -128实际存入到计算机中是以 1 1000 0000 表示的(计组运算器).但空间只有8位,发生截断,因此得到1000 0000.
  3. 而[1111 1111,1000 0001]~[0000 0000,0111 1111]  
  4. 即[-127,-1]~[0,127] 自然数都已经被使用 .  
  5. 计算机不能浪费每一个空间(最小的成本尽可能解决大量的计算),自然1000 0000也需要有相应的意义. 因此赋予数值为-128.
  6. 因为截断后也不可能恢复,所以这是一种半计算半规定的做法.
复制代码

截断

截断是空间不足以存放数据时,将高位截断.
截断的是高位还是低位? 由于赋值永久都是从低地址赋起(从低到高依次赋值),因此空间不足时高位直接丢弃.
1 0000 0001 0100
1 1111 1110 1100
0 0000 0000 1010
1 1111 1111 0110
1 0000 0000 1010
建议在无符号范例的数值后带上u,

默认的数值是有符号的,在数值后加u更加严格,unsigned int a = 10u;
1.7 if-else组合


  • 表达式: 变量与操纵符的组合称为表达式
  • 语句: 以分号结尾的表达式称为语句
  • if(0){ //... }表明法,在看到if(0)时,有可能这是一个表明,不推荐这种做法,但是需要认识.
if的执行顺序


  • 盘算功能:先执行完毕if括号()中的表达式or某种函数,得到表达式的真假结果
  • 判定功能:根据表达式结果进行条件判定
  • 分支功能:根据判定结果进行分支
    (if有判定和分支两个功能,而switch只有判定而没有分支功能,因此必须使用break)

操纵符的执行顺序测试方法

printf("1   ") && printf("2   ");
printf("1   ") || printf("2   ");
C语言的布尔范例


  • C89/C90没有bool范例
  • C99 引入了关键字为_Bool的范例,在新增的头文件stdbool.h中.为了保证C/C++的兼容性,被重新用宏写成了bool.
  • 微软对C语言bool范例也有一套标准,BOOL,FALSE,TRUE. 不推荐使用微软这套标准,不具备可移植性
浮点数与"零值"比力


  • 精度损失:浮点值与实际值不等,可能偏大可能偏小,都属于精度损失

    • 验证浮点数是否存在精度损失

    • 验证浮点数的差值是否存在精度损失

    • 浮点数直接比力验证

      结论: 浮点数在进行比力时,绝对不能使用双等号==来进行比力.  浮点数本身有精度损失,进而导致结果可能有细微的差别.

  • 怎样进行浮点数比力
  1. 1. x - y == 0的条件是 |x - y| < 精度.
  2. 即 x - y > -精度 && x - y < 精度
  3. 2.还可以使用fabs函数,C90,<math.h>, double fabs(double x); 返回x的绝对值.
  4. 即 fabs(x-y) < 精度
复制代码
  1. //--------------------------------------------------------------
  2. //方法1,自定义精度
  3. #include<stdio.h>
  4. #include<math.h>
  5. #define EPSILON 0.0000000000000001 //自定义精度
  6. int main()
  7. {
  8.     double x = 1.0;
  9.     double y = 0.1;
  10.     //验证x - 0.9 是否等于 0.1
  11.     if(fabs((x-0.9)- y) < EPSILON ) printf("aaaa\n");
  12.     else printf("bbbb\n");
  13.     puts("hello world!");
  14.     return 0;
  15. }
复制代码
  1. //方法2:使用C语言提供的精度
  2. #include<stdio.h>
  3. #include<math.h>
  4. #include<float.h>
  5. int main()
  6. {
  7.     double x = 1.0;
  8.     double y = 0.1;
  9.     //验证x - 0.9 是否等于 0.1
  10.     //<float.h> 内置最小精度值 DBL_EPSILON 和 FLT_EPSILON ,1.0+DBL_EPSILON != 1.0 ,EPSILON是改变1.0的最小的值,数学概念,略
  11.     if(fabs((x-0.9)- y) < DBL_EPSILON ) printf("aaaa\n");
  12.     else printf("bbbb\n");
  13.    
  14.     return 0;
  15. }
复制代码


  • 浮点数与"零值"比力,只需要判定它是否小于EPSILON即可
  1. int main()
  2. {
  3.     double x = 0.0;
  4.     // double x  = 0.00000000000000000000000000001; //很小也可以认为等于0
  5.     if(fabs(x) < DBL_EPSILON ) printf("等于0\n");
  6.     else printf("不等于0\n");
  7.    
  8.     return 0;
  9. }
复制代码
补充:怎样明白逼迫范例转化

逼迫范例转化:不改变数据本身,只改变数据的范例

  • "123456" -> int:123456
  1. 字符串"123456"如何转化成整型值123456,能强转吗? 答案是不能,只能通过算法进行转化
  2. 因为"123456"的空间至少占了7个,而整型int只占4个字节.
复制代码

  • 不同范例的0
  1. printf("%d\n",0);
  2. printf("%d\n",'\0');
  3. printf("%d\n",NULL); //(void*)0
复制代码

1.8switch case组合


  • 基本语法结构
  1. //switch只能对整数进行判定
  2. switch(整型变量/常量/整型表达式){
  3.     case var1:
  4.         break;
  5.     case var2:
  6.         break;
  7.     case var3:
  8.         break;
  9.     default:
  10.         break;
  11. }
  12. 推荐使用switch的场景:只能用于整数判定且分支很多的情况下
复制代码

  • switch case 的功能
    switch本身没有判断和分支能力,switch是拿着结果去找case进行匹配,
    case具有判定能力,但没有分支能力,case是通过break完成分支功能
    break具有分支功能,相当于if的分支能力.
    default相当else,处理非常情况
(补充) 屏蔽警告的方法
  1. error C4996: 'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
  2. 方法1:
  3. #pragma warning(disable:4996)
  4. 方法2:
  5. #define _CRT_SECURE_NO_WARNINGS //该宏定义必须写在文件的首行(头文件的前面)才有效
  6. (如果宏没有宏值,则只能用在#ifdef等条件编译语句中,即只用于标识)
复制代码

  • 在case中执行多条语句,建议case后都带上花括号.

    在case中定义变量,直接写会警告,需要带上花括号,但不建议在case中定义变量,如果非要这么做,可以封装成函数来替代.而且

  • 多个case执行同样语句
  1. int main()
  2. {
  3.     int n = 0 ;
  4.     scanf("%d",&n);
  5.     switch (n)
  6.     {
  7.         case 1: case 2: case 3: case 4: case 5:
  8.             puts("周内");
  9.             break;
  10.         case 6:
  11.             puts("周六");
  12.             break;
  13.         case 7:
  14.             puts("周日");
  15.             break;
  16.         default:
  17.             break;
  18.     }
  19.     return 0;
  20. }
复制代码

  • default可以在switch中的任意位置,一般习惯放在最后的case后
  • switch中尽量不要单独出现return.一般习惯用break,突然return容易搞混
  • switch中不要使用bool值,不好维护
  • case的值必须是数字常量,不能是const int a = 1;这种
  • 按执行频率分列case语句,频率越高越靠前,能淘汰匹配次数
1.9 do、while、for

循环的基本结构


  • 一般的循环都必须要有3种功能:

    • 循环条件初始化
    • 循环条件判定
    • 循环条件更新

(死循环除外)
  1. int main()
  2. {
  3.     int count = 10; //1.循环条件初始化
  4.     while (count > 10) //2.循环条件判定
  5.     {
  6.         printf("%d\n", count); //3.业务逻辑
  7.         count--; //4.循环条件更新
  8.     }
  9.     return 0;
  10. }
复制代码

  • for循环
[code]使用样例:for(int i = 0; i

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

玛卡巴卡的卡巴卡玛

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表