精华内容
下载资源
问答
  • @[C语言](用VS CodeC语言无法使用__attribute__(packed)) 用VS CodeC语言无法使用__attribute__(packed) 今天,Windows上写C语言程序,结果编写的结构体总是无法使用__attribute__((packed)),导致读取的二...

    今天,在Windows上写C语言程序,结果编写的结构体总是无法使用__attribute__((packed)),导致读取的二进制文件字节对不上。
    检查CMakeLists.txt和代码均没有发现明显错误。
    ···
    typedef struct
    {
    data_packet_header header;
    uint16_t* data; // 数据
    } attribute((packed)) data_packet_s;
    ···
    想到的第一个解决方法。在代码头加上

    extern{
    	C
    }
    

    发现没有用。
    搜了半天,得到的解决方法主要是在结构体前加上pragma pack(1)

    #pragma pack(1)
    typedef struct
    {
        data_packet_header header;
        uint16_t* data; // 数据
    } __attribute__((packed)) data_packet_s;
    

    然后竟然就好了。

    查询网络文章#pragma pack(1) 的意义是什么
    解释为 #pragma pack(1)让编译器将结构体数据强制连续排列

    展开全文
  • 单片机C语言指针意义浅析—Keil-C51

    千次阅读 2019-05-15 16:28:28
    单片机C语言指针意义浅析—Keil-C51 2017年06月24日 16:33:00lucky_草木皆兵阅读数:482 通常认为,C语言之所以强大,以及其自由性,很大部分体现其灵活的指针运用上,甚至认为指针是C语言的灵魂。这里说通常,...

    单片机C语言指针意义浅析—Keil-C51

    2017年06月24日 16:33:00 lucky_草木皆兵 阅读数:482

    通常认为,C语言之所以强大,以及其自由性,很大部分体现在其灵活的指针运用上,甚至认为指针是C语言的灵魂。这里说通常,是广义上的,因为随着编程语言的发展,指针也饱受争议,并不是所有人都承认指针的“强大”和“优点”。在单片机领域,指针同样有着应用,本章节针对Keil C-51环境下的指针意义做简要分析。



    1     指针与变量
    指针是一个变量,它与其他变量一样,都是RAM中的一个区域,且都可以被赋值,如程序①所示。
    #include "REG52.H"        
    unsigned int j;
    unsigned char *p;
    void main()
    {
             while(1)
             {
                       j=0xabcd;
                       p=0xaa;
             }
    }
    在Debug Session模式下,将鼠标指针移到到变量“j”“p”位置,可以显示变量的物理地址,如图1-1、1-2所示。 






    图中箭头所指处即为变量在RAM中的“首地址”,为什么是“首地址”呢?变量根据类型可分为8位(单字节)、16位(双字节),程序中变量“j”是无符号整型,所占物理空间应为2字节,而在8位单片机中,RAM的一个存储单元大小是8位,即1字节,因此需2个存储单元才满足变量“j”长度。所以实际上变量“j”的物理地址为“08H”“09H”。同理,“p(D:0x0A)”即变量“p”的首地址为“0AH”。
    下面通过单步执行程序来观察RAM内的数据变化,打开两个Memory Windows窗口,在Keil软件下方显示为Memory1和Memory2,在两个窗口中,分别做如图2-1、2-2所示的设置。






    两个Address填写的内容分别是:D:0x08、D:0x0A,即变量“j”和变量“p”的首地址,输入后回车,便可监视RAM中该地址下的数据。设置好后,准备调试。

    在Debug Session模式中,箭头所指处即为即将执行的语句,单击“Step”功能按钮(或按F11键),让程序运行,如图3所示。

    第一次单击“Step”按钮后,Memory1窗口内数据如图4所示。

    由调试结果可知,08H数据由00H变为ABH,09H数据由00H变为CDH,出现这种变化是因为执行了语句j=0xabcd;08H为变量“j”高八位,存储“AB”,09H为变量“j”低八位,存储“CD”。
    第二次单击“Step”按钮,执行语句:p=0xaa;此时需观察Memory2窗口内数据,如图5所示。

    由调试结果可知,0CH处值由00变为“AAH”,程序相吻合。这里需要注意,在Keil C-51编译环境下,指针变量,不管长度是单字节或是双字节,指针变量所占字节数为3字节。故此处“AAH”不是存储在0AH而存储在0CH(0A+2)地址中。
    综上所述,指针实际上是变量,都是映射到RAM中的一段存储空间,区别是,指针占用3字节,而其他变量可根据需要设定其所占RAM是1字节(char)、2字节(int)、4字节(long)。

    2       指针作用
    指针的作用是什么呢?先来看下面的程序:
    程序②
    #include "REG52.H"         
    unsigned chartab1[8]={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08};
    unsigned char codetab2[8]={0x10,0x20,0x30,0x40,0x50,0x60,0x70,0x80}; 
    unsigned char N1,N2;
    void main()
    {
                N1=tab1[0];
                N2=tab2[0];
    }
    显然,程序执行的结果是N1=0x01,N2=0x10。这里都是讲数组内的数据赋值给变量,但存在区别,tab1数组使用的是单片机RAM空间,而tab2数组使用的是单片机程序存储区(ROM)空间。尽管使用C语言为变量赋值时语句相同,但编译结果并不相同,此程序编译后的结果如图6所示。
     

    由编译结果可知,N1=tab1[0]语句实际上是直接寻址,而N2=tab2[0]是寄存器变址寻址。不管是何种寻址方式,都是将一个物理地址内的数据取出来使用:tab1数组中,tab[0]对应的RAM地址是0x0A,tab[1]对应的RAM地址是0x0B……以此类推;tab2数组中,tab[0]对应的ROM地址是0x00A5,tab[1]对应的ROM地址是0x00A6……以此类推。不管这些数组或变量所在的RAM或ROM地址如何,用户最终需要的是数组或变量的数据,而指针,就是通过变量或数组的物理地址访问数据,也就是说,通过指针,同样可以访问数组或变量数据。现将程序②做出调整,得到程序③如下:
    #include "REG52.H"         
    unsigned chartab1[8]={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08};
    unsigned char code tab2[8]={0x10,0x20,0x30,0x40,0x50,0x60,0x70,0x80};
    unsigned char N1,N2;
    unsigned char  *p;
    void main()
    {        
             unsignedchar i;
             p=tab1;
             for(i=0;i<8;i++,p++)
             N1=*p;
             
             p=tab2;
             for(i=0;i<8;i++,p++) 
             N2=*p;
    }
    程序执行结果:tab1数组内的8个数值依次被赋值给N1;tab2数组内的8个数值依次被赋值给N2;
    程序③执行Debug Session功能后,打Watch Windows窗口,在Watch1窗口下添加需要监视的变量,此处为“p”和“N1”,如图7所示。

    Value为当前变量数值,程序为运行前,p值为0x00,单击Step按键功能后,执行p=tab1;p值变为0x0A,如图8所示。

    0x0A是什么值呢?将鼠标移至tab1数组位置,可显示出数组所在的物理地址,0x0A就是数组tab1的首地址,如图9所示。

    p=tab1就是将tab1数组的首地址赋值给变量p,执行p++即地址值加1;*p则是此物理地址内的具体数据,因此for循环中,N1=*p是依次将tab1数组中的数据赋值给变量N1。由此可见,指针是作为一个变量,指向某一个地址。
    那么指针到底是如何将某个地址内的数据“拿”出来的?下面通过N1=*p语句做演示说明,N1=*p编译后的汇编代码如图10所示。

    C:0x00A0至C:0x00A9的汇编代码即是C程序中的N1=*p。程序先将变量p的值赋值给R3、R2、R1三个通用寄存器,程序为:
    MOV   R3,p(0x12)
    MOV   R2,0x13
    MOV   R1,0x14
    然后调用了一个子函数:LCALL  C?CLDPTR(C:00E4),而C程序中,未定义或使用任何子函数,那么这个子函数是哪里来的?作用是什么?根据标号C:00E4可找到该子函数,程序代码如下:

    C:0x00E4   BB0106   CJNE     R3,#0x01,C:00ED
    C:0x00E7   8982     MOV      DPL(0x82),R1
    C:0x00E9   8A83     MOV      DPH(0x83),R2
    C:0x00EB   E0       MOVX     A,@DPTR
    C:0x00EC   22       RET      
    C:0x00ED   5002     JNC      C:00F1
    C:0x00EF   E7       MOV      A,@R1
    C:0x00F0   22       RET      
    C:0x00F1   BBFE02   CJNE     R3,#0xFE,C:00F6
    C:0x00F4   E3       MOVX     A,@R1
    C:0x00F5   22       RET      
    C:0x00F6    8982    MOV      DPL(0x82),R1
    C:0x00F8   8A83     MOV      DPH(0x83),R2
    C:0x00FA   E4       CLR      A
    C:0x00FB   93       MOVC     A,@A+DPTR
    C:0x00FC   22       RET      

    此程序功能是:先用R3寄存器的值与0x01比较,当R3的值大于0x01时,再和0xFE做比较,比较的结果有如下情况:
    (1)R3的值等于0x01时,执行如下程序:
    C:0x00E7   8982     MOV      DPL(0x82),R1
    C:0x00E9   8A83     MOV      DPH(0x83),R2
    C:0x00EB   E0       MOVX     A,@DPTR
    C:0x00EC   22       RET      
    程序功能:读取扩展RAM内的数据并赋值给A,寻址范围0~65535。当数组用xdata定义时,会跳转到此处。
    (2)R3的值小于0x01即等于0x00时,执行如下程序:
    C:0x00EF   E7       MOV      A,@R1
    C:0x00F0   22       RET  
    程序功能:读取单片机内部256字节RAM内的数据并赋值给A,寻址范围0~255。当数组用data或idata定义时,会跳转到此处。如执行N1=*p语句时,即跳转到自处,读取内部RAM地址内的数据。    
    (3)R3的值不等于0x00或0x01时,通过JNC指令跳转到C:0x00F1处,开始与0xFE做比较。R3的值等于0xFE时,执行如下程序:
    C:0x00F4   E3       MOVX     A,@R1
    C:0x00F5   22       RET  
    程序功能:读取单片机片外RAM内的数据并赋值给A,寻址范围0~255。当数组用pdata定义时,会跳转到此处。通常8051单片机不使用pdata定义变量或数组。
    (4)R3的值不等于0xFE时,即R3的值等于0xFF时,跳转到C:0x00F6处执行如下程序:
    C:0x00F6   8982     MOV      DPL(0x82),R1
    C:0x00F8   8A83     MOV      DPH(0x83),R2
    C:0x00FA   E4       CLR      A
    C:0x00FB   93       MOVC     A,@A+DPTR
    C:0x00FC   22       RET
    程序功能:读取单片机内部ROM内的数据并赋值给A,寻址范围0~65535。当数组用code定义时,如程序③中,tab2数组用code定义,执行p=tab2后,R3的值被赋值为0xFF,再执行N2=*p语句时,即跳转到自处,读取内部ROM地址内的数据。  
    由此可见,子函数“C?CLDPTR”的作用是,根据数据所在存储空间,用不同的寻址方式读取某地址下的数据。R3用于确定寻址方式,R3的值与对应的寻址方式对应关系为:
    1、R3值等于0x00时,片内RAM间接寻址;此时数据用dataidata定义。
    2、R3值等于0x01时,片外RAM(扩展RAM)间接寻址;此时数据用xdata定义。
    3、R3值等于0xFE时,片外RAM(扩展RAM)低246字节间接寻址;此时数据用pdata定义
    4、R3值等于0xFF时,从存储存储器(ROM)进行变址寻址;此时数据用code定义。

    3、指针结构
    R3、R2、R1的值是RAM中0x12、0x13、0x14地址内的值,即变量p映射的RAM地址。而而8位单片机中,不管是何种寻址方式,最大寻址范围是2字节长度(0~65535),为什么指针*p却占用了3字节RAM空间呢?下面通过程序④说明。
    程序④:
    #include "REG52.H"         
    unsigned char tab1[8];
    unsigned char idata tab2[8];
    unsigned char xdata tab3[8];   
    unsigned char pdata tab4[8];
    unsigned char codetab5[8]={0x10,0x20,0x30,0x40,0x50,0x60,0x70,0x80}; 
    unsigned char  *p;
    void main()
    {        
             p=tab1;
             p=tab2;
             p=tab3;
             p=tab4;
             p=tab5;
    }
    在Debug Session模式下可知,程序中数组与变量所映射的物理地址为及物理存储区分别为:
    tab1 :        0x08~0x0F                        单片机内部RAM
    tab2:     0x03~0x1A                       单片机内部RAM(idata)
    tab3:     0x08~0x0F                        单片机扩展RAM(xdata)
    tab4:     0x00~0x08                        单片机扩展RAM低256字节(pdata)
    tab5:     0x0003D~0x0044            单片机程序存储区(code)
    p:            0x10~0x12                        单片机内部RAM
    注:扩展RAM可以在物理上可以分为片内或片外,如STC15系列增强型单片机的扩展RAM与单片机是封装在一起的,即片内扩展RAM;传统8051单片机没有片内扩展RAM,需连接外部RAM芯片,此为片外扩展RAM。
    在Memory Windows窗口下,监视变量p映射的RAM地址:0x10~0x12的数值变化,如图11所示。

    通过“Step”功能按钮执行住函数中的5调语句,可观察到0x10~0x12寄存器的数据变化:

    执行p=tab1后,0x10、0x11、0x12:0x00、0x00、0x08
    执行p=tab2后,0x10、0x11、0x12:0x00、0x00、0x13
    执行p=tab3后,0x10、0x11、0x12:0x01、0x00、0x08
    执行p=tab4后,0x10、0x11、0x12:0xFE、0x00、0x00
    执行p=tab5后,0x10、0x11、0x12:0xFF、0x00、0x3D
    由此可知,0x10的赋值取决于p指向的物理存储区,0x11、0x12的值是数据存储区的地址。指针所映射的首地址,会根据指向的物理存储区被编译器赋不同的值:0x00,0x01,0xFE,0xFF。这与程序③得到的结论一致,程序③中,寄存器R3、R2、R1对应值实际上就是指针所映射的3字寄存器数值。
    结合程序③编译分析,当需要引用某物理地址内数据时,会调用“C?CLDPTR”函数,函数功能就是根据这些赋值确定使用何种寻址方式引用数据。而这一过程包括“C?CLDPTR”函数都是编译器自动完成的。
    在汇编语言中,R1寄存器可以用于间接寻址,如:MOV  A,@R1。不能写为MOV A,@12H。因此在程序③中,将变量p对应的3字节数据赋值给R3、R2、R1。
    综上所述,Keil C-51编译环境下,指针是一个占3字节的特殊变量,编译器编译程序时,自动生成判断寻址方式的子函数,并根据根据目标数据所在的物理存储区不同,为指针首字节赋值,根据赋值的不同,进行不同方式的寻址;指针的后2字节,用于存放引用的地址。

    调试训练:
    下面的程序编译器会怎样编译?与程序③有何不同?请根据程序③和程序④的分析方式分析程序⑤的执行结果。
    程序⑤
    #include "REG52.H"         
    unsigned char tab1[8];
    unsigned char codetab2[8]={0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff}; 
    unsigned char  *p;
    void main()
    {        
             unsignedchar i;
             p=tab1;
             for(i=0;i<8;i++,p++)
             *p=i;
             
             p=tab2;
             for(i=0;i<8;i++,p++) 
             *p=i;
    }

    思考:下列语句中:

    p=tab2;

    for(i=0;i<8;i++,p++)

    *p=i;

    执行完for循环后,tab2数组内的值会改变吗?为什么?

    4、指针意义

    在汇编编程中,由于单片机数据存放的物理存储区不同,导致有不同的寻址方式,用户进行必须根据这一规律设计程序。而在C语言中,不管目标数据所在的物理存储区如何,指针都可指向该地址,并自动编译寻址方式。

    但指针并不是万能的,如程序⑤中:

    p=tab2;

    for(i=0;i<8;i++,p++)

    *p=i;

    这些语句编译时并不会报错,但却不能实现功能,因为tab2数组是定义在程序存储器(ROM)的常量数组,ROM内的数据更改是不能通过这种方式实现的。因此,当用户不明确单片机的物理存储区特性时,使用指针会容易出错。先将程序⑤中的主函数语句做如下修改,得到程序⑥:

    #include"REG52.H"         

    unsignedchar tab1[8];

    unsignedchar code tab2[8]={0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff};

    unsignedchar  *p;

    voidmain()

    {        

             unsigned char i;

             for(i=0;i<8;i++,p++)

             tab1[ i]=i;

             

             for(i=0;i<8;i++,p++)

             tab2[ i]=i;

    }

    单独看第一个for循环,可实现与程序⑤一样的效果,即tab1数组内被赋值为:0,1,2,3,4,5,6,7。

    第二个for循环从语句上可认为是与程序⑤功能相同,但实际上,不管是程序⑤还是程序⑥,都不能实现对tab2数组的赋值。但在程序⑥中,编译器会提示错误,如图12所示。

    因此,指针的使用不当,不仅会带来程序运行结果的不正确,同时也难以发现这些错误。

    对比程序⑤和程序⑥中的两段程序:

    p=tab1;                                                      for(i=0;i<8;i++,p++)                        

    for(i=0;i<8;i++,p++)                                   tab1[ i]=i;

    *p=i;


    它们执行的结果是一样的,那么哪种更好呢?对于初学者来说,显然是后者,因为后者更易于理解程序含义,而前者必须要理解指针在此处的作用;那么对于有经验的程序员呢?也是后者,因为程序执行效率上,后者也要大于前者,因为程序⑤在编译过程中,编译器始终会生成一个子函数用于确定寻址方式,再赋值;程序⑥则是直接确定了寻址方式执并行进行赋值。尽管执行效率的降低在接受范围内,但对于一个简单、明了的功能来说,用简单的方式实现要比复杂方式合理。
    设计者在程序中使用指针的目的往往是让程序具有可移植性,但8051单片机的功能是有限的,它实现的功能相对固化,如时间显示、数据采集等等,这些功能确定后,几乎不会做出更改,基于此特点,8051单片机的代码代码量都不长。因此即便是不同构架的单片机程序互相移植,代码的修改并不复杂,移植过程中,也几乎都是针对不同构架单片机的I/O工作方式不同、指令周期不同做常规修改;或是关键字的修改。因此合理的设计单片机程序,尽可能的提高程序的效率、稳定性、可阅读性才是程序设计的核心主旨。指针在8051单片机中固然可以使用,但并不能说明指针的使用就一定是高效、准确、易于他人理解。

    展开全文
  • 华为C语言编程规范(精华总结)

    万次阅读 多人点赞 2020-03-24 09:48:55
    10、只能通过包含头文件的方式使用其他 .c 提供的接口,禁止.c 中通过 extern 的方式使用外部函数接口、变量 若a.c使用了b.c定义的foo()函数,则应当b.h中声明extern int foo(int input);并a.c中通过#...

    目录

    1、代码总体原则

    2、头文件

    2、函数

    3、标识符命名与定义

    4、变量

    5、宏、常量

    6、表达式

    7、注释

    8、排版与格式

    9、代码编辑编译 


    “编写程序应该以人为本,计算机第二。”                                                               

                                                                                                                                           ——Steve McConnell

    “无缘进华为,但可以用华为的标准要求自己。”                                                               

                                                                                                                                           ——不脱发的程序猿


    1、代码总体原则

    1、清晰第一 

    清晰性是易于维护、易于重构的程序必需具备的特征。代码首先是给人读的,好的代码应当可以像文章一样发声朗诵出来。

    目前软件维护期成本占整个生命周期成本的40%~90%。根据业界经验,维护期变更代码的成本,小型系统是开发期的5倍,大型系统(100万行代码以上)可以达到100倍。业界的调查指出,开发组平均大约一半的人力用于弥补过去的错误,而不是添加新的功能来帮助公司提高竞争力。

    一般情况下,代码的可阅读性高于性能,只有确定性能是瓶颈时,才应该主动优化。

    2、简洁为美

    简洁就是易于理解并且易于实现。代码越长越难以看懂,也就越容易在修改时引入错误。写的代码越多,意味着出错的地方越多,也就意味着代码的可靠性越低。因此,我们提倡大家通过编写简洁明了的代码来提升代码可靠性。

    废弃的代码(没有被调用的函数和全局变量)要及时清除,重复代码应该尽可能提炼成函数。

    3、选择合适的风格,与代码原有风格保持一致

    产品所有人共同分享同一种风格所带来的好处,远远超出为了统一而付出的代价。在公司已有编码规范的指导下,审慎地编排代码以使代码尽可能清晰,是一项非常重要的技能。 如果重构/ / 修改其他风格的代码时,比较明智的做法是根据 现有 代码 的 现有风格继续编写代码,或者使用格式转换工具进行转换成公司内部风格。


    2、头文件

    对于C语言来说,头文件的设计体现了大部分的系统设计。 不合理的头文件布局是编译时间过长的根因,不合理的头文件实际上反映了不合理的设计。

    1、头文件中适合放置接口的声明,不适合放置实现

    头文件是模块(Module)或单元(Unit)的对外接口。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。

    要求:

    • 内部使用的函数(相当于类的私有方法)声明不应放在头文件中。
    • 内部使用的宏、枚举、结构定义不应放入头文件中。
    • 变量定义不应放在头文件中,应放在.c文件中。
    • 变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。 即使必须使用全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的。

    2、头文件应当职责单一,切忌依赖复杂

    头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。很多现有代码中头文件过大,职责过多,再加上循环依赖的问题,可能导致为了在.c中使用一个宏,而包含十几个头文件。

    错误示例:某平台定义WORD类型的头文件:

    #include <VXWORKS.H>
    #include <KERNELLIB.H>
    #include <SEMLIB.H>
    #include <INTLIB.H>
    #include <TASKLIB.H>
    #include <MSGQLIB.H>
    #include <STDARG.H>
    #include <FIOLIB.H>
    #include <STDIO.H>
    #include <STDLIB.H>
    #include <CTYPE.H>
    #include <STRING.H>
    #include <ERRNOLIB.H>
    #include <TIMERS.H>
    #include <MEMLIB.H>
    #include <TIME.H>
    #include <WDLIB.H>
    #include <SYSLIB.H>
    #include <TASKHOOKLIB.H>
    #include <REBOOTLIB.H>
    …
    typedef unsigned short WORD;
    …

    这个头文件不但定义了基本数据类型WORD,还包含了stdio.h syslib.h等等不常用的头文件。如果工程中有10000个源文件,而其中100个源文件使用了stdio.h的printf,由于上述头文件的职责过于庞大,而WORD又是每一个文件必须包含的,从而导致stdio.h/syslib.h等可能被不必要的展开了9900次,大大增加了工程的编译时间。

    3、头文件应向稳定的方向包含 

    头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。

    就我们的产品来说,依赖的方向应该是: 产品依赖于平台,平台依赖于标准库。某产品线平台的代码中已经包含了产品的头文件,导致平台无法单独编译、发布和测试,是一个非常糟糕的反例。除了不稳定的模块依赖于稳定的模块外,更好的方式是两个模块共同依赖于接口,这样任何一个模块的内部实现更改都不需要重新编译另外一个模块。在这里,我们假设接口本身是最稳定的。

    4、每一个 .c 文件应有一个同名 .h 文件,用于声明需要对外公开的接口

    如果一个.c文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如main函数所在的文件。

    现有某些产品中,习惯一个.c文件对应两个头文件,一个用于存放对外公开的接口,一个用于存放内部需要用到的定义、声明等,以控制.c文件的代码行数。编者不提倡这种风格。这种风格的根源在于源文件过大,应首先考虑拆分.c文件,使之不至于太大。另外,一旦把私有定义、声明放到独立的头文件中,就无法从技术上避免别人include之,难以保证这些定义最后真的只是私有的。

    5、禁止头文件循环依赖

    头文件循环依赖,指a.h包含b.h,b.h包含c.h,c.h包含a.h之类导致任何一个头文件修改,都导致所有包含了a.h/b.h/c.h的代码全部重新编译一遍。而如果是单向依赖,如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h/c.h的源代码重新编译。

    6、  .c/.h文件禁止包含用不到的头文件

    很多系统中头文件包含关系复杂,开发人员为了省事起见,可能不会去一一钻研,直接包含一切想到的头文件,甚至有些产品干脆发布了一个god.h,其中包含了所有头文件,然后发布给各个项目组使用,这种只图一时省事的做法,导致整个系统的编译时间进一步恶化,并对后来人的维护造成了巨大的麻烦。

    7、  头文件应当自包含 

    简单的说,自包含就是任意一个头文件均可独立编译。如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,就会增加交流障碍,给这个头文件的用户增添不必要的负担。

    示例:如果a.h不是自包含的,需要包含b.h才能编译,会带来的危害:每个使用a.h头文件的.c文件,为了让引入的a.h的内容编译通过,都要包含额外的头文件b.h。额外的头文件b.h必须在a.h之前进行包含,这在包含顺序上产生了依赖。

    注意:该规则需要与“.c/.h文件禁止包含用不到的头文件”规则一起使用,不能为了让a.h自包含,而在a.h中包含不必要的头文件。a.h要刚刚可以自包含,不能在a.h中多包含任何满足自包含之外的其他头文件。

    8、总是编写内部 #include 保护符( #define  保护)

    多次包含一个头文件可以通过认真的设计来避免。如果不能做到这一点,就需要采取阻止头文件内容被包含多于一次的机制。通常的手段是为每个文件配置一个宏,当头文件第一次被包含时就定义这个宏,并在头文件被再次包含时使用它以排除文件内容。所有头文件都应当使用#define 防止头文件被多重包含,命名格式为FILENAME_H,为了保证唯一性,更好的命名是PROJECTNAME_PATH_FILENAME_H。

    注:没有在宏最前面加上单下划线"_",是因为一般以单下划线"_"和双下划线"__"开头的标识符为ANSIC等使用,在有些静态检查工具中,若全局可见的标识符以"_"开头会给出告警。

    定义包含保护符时,应该遵守如下规则:

    • 保护符使用唯一名称;

    • 不要在受保护部分的前后放置代码或者注释。

    正确示例:假定VOS工程的timer模块的timer.h,其目录为VOS/include/timer/timer.h,应按如下方式保护:

    #ifndef VOS_INCLUDE_TIMER_TIMER_H
    #define VOS_INCLUDE_TIMER_TIMER_H
    ...
    #endif
    
    也可以使用如下简单方式保护:
    
    #ifndef TIMER_H
    #define TIMER_H
    ...
    #endif

    例外情况:头文件的版权声明部分以及头文件的整体注释部分(如阐述此头文件的开发背景、使用注意事项等)可以放在保护符(#ifndef XX_H)前面。

    9、禁止在头文件中定义变量

    在头文件中定义变量,将会由于头文件被其他.c文件包含而导致变量重复定义。

    10、只能通过包含头文件的方式使用其他 .c 提供的接口,禁止在.c 中通过 extern 的方式使用外部函数接口、变量

    若a.c使用了b.c定义的foo()函数,则应当在b.h中声明extern int foo(int input);并在a.c中通过#include <b.h>来使用foo。禁止通过在a.c中直接写extern int foo(int input);来使用foo,后面这种写法容易在foo改变时可能导致声明和定义不一致。

    11、禁止在 extern "C" 中包含头文件

    在extern "C"中包含头文件,会导致extern "C"嵌套,Visual Studio对extern "C"嵌套层次有限制,嵌套层次太多会编译错误。
    在extern "C"中包含头文件,可能会导致被包含头文件的原有意图遭到破坏。

    错误示例:

    extern “C”
    {
    #include “xxx.h”
    ...
    }
    

    正确示例:

    #include “xxx.h”
    extern “C”
    {
    ...
    } 

    12、一个模块通常包含多个 .c 文件,建议放在同一个目录下,目录名即为模块名。为方便外部使用者,建议每一个模块提供一个 .h ,文件名为目录名

    需要注意的是,这个.h并不是简单的包含所有内部的.h,它是为了模块使用者的方便,对外整体提供的模块接口。以Google test(简称GTest)为例,GTest作为一个整体对外提供C++单元测试框架,其1.5版本的gtest工程下有6个源文件和12个头文件。但是它对外只提供一个gtest.h,只要包含gtest.h即可使用GTest提供的所有对外提供的功能,使用者不必关系GTest内部各个文件的关系,即使以后GTest的内部实现改变了,比如把一个源文件c拆成两个源文件,使用者也不必关心,甚至如果对外功能不变,连重新编译都不需要。对于有些模块,其内部功能相对松散,可能并不一定需要提供这个.h,而是直接提供各个子模块或者.c的头文件。

    比如产品普遍使用的VOS,作为一个大模块,其内部有很多子模块,他们之间的关系相对比较松散,就不适合提供一个vos.h。而VOS的子模块,如Memory(仅作举例说明,与实际情况可能有所出入),其内部实现高度内聚,虽然其内部实现可能有多个.c和.h,但是对外只需要提供一个Memory.h声明接口。

    13、如果一个模块包含多个子模块,则建议每一个子模块提供一个对外的 .h,文件名为子模块名

    降低接口使用者的编写难度

    14、头文件不要使用非习惯用法的扩展名,如 .inc 

    目前很多产品中使用了.inc作为头文件扩展名,这不符合c语言的习惯用法。在使用.inc作为头文件扩展名的产品,习惯上用于标识此头文件为私有头文件。但是从产品的实际代码来看,这一条并没有被遵守,一个.inc文件被多个.c包含比比皆是。

    除此之外,使用.inc还导致source insight、Visual stduio等IDE工具无法识别其为头文件,导致很多功能不可用,如“跳转到变量定义处”。虽然可以通过配置,强迫IDE识别.inc为头文件,但是有些软件无法配置,如Visual Assist只能识别.h而无法通过配置识别.inc。

    15、同一产品统一包含头文件排列方式

    常见的包含头文件排列方式:功能块排序、文件名升序、稳定度排序。

    正确示例1:以升序方式排列头文件可以避免头文件被重复包含:

    #include <a.h>
    #include <b.h>
    #include <c/d.h>
    #include <c/e.h>
    #include <f.h>

    正确示例2:以稳定度排序,建议将不稳定的头文件放在前面,如把产品的头文件放在平台的头文件前面:

    #include <product.h>
    #include <platform.h>

    相对来说,product.h修改的较为频繁,如果有错误,不必编译platform.h就可以发现product.h的错误,可以部分减少编译时间。


    2、函数

    函数设计的精髓:编写整洁函数,同时把代码有效组织起来。

    整洁函数要求:代码简单直接、不隐藏设计者的意图、用干净利落的抽象和直截了当的控制语句将函数有机组织起来。

    代码的有效组织包括:逻辑层组织和物理层组织两个方面。逻辑层,主要是把不同功能的函数通过某种联系组织起来,主要关注模块间的接口,也就是模块的架构。物理层,无论使用什么样的目录或者名字空间等,需要把函数用一种标准的方法组织起来。例如:设计良好的目录结构、函数名字、文件组织等,这样可以方便查找。

    1、一个函数仅完成一件功能

    一个函数实现多个功能给开发、使用、维护都带来很大的困难。

    将没有关联或者关联很弱的语句放到同一函数中,会导致函数职责不明确,难以理解,难以测试和改动。

    2、重复代码应该尽可能提炼成函数

    重复代码提炼成函数可以带来维护成本的降低。

    重复代码是我司不良代码最典型的特征之一。在“代码能用就不改”的指导原则之下,大量的烟囱式设计及其实现充斥着各产品代码之中。新需求增加带来的代码拷贝和修改,随着时间的迁移,产品中堆砌着许多类似或者重复的代码。

    项目组应当使用代码重复度检查工具,在持续集成环境中持续检查代码重复度指标变化趋势,并对新增重复代码及时重构。当一段代码重复两次时,即应考虑消除重复,当代码重复超过三次时,应当立刻着手消除重复。

    3、避免函数过长,新增函数不超过 50 行 (非空非注释行) 

    过长的函数往往意味着函数功能不单一,过于复杂。

    函数的有效代码行数,即NBNC(非空非注释行)应当在[1,50]区间。

    例外:某些实现算法的函数,由于算法的聚合性与功能的全面性,可能会超过50行。

    延伸阅读材料: 业界普遍认为一个函数的代码行不要超过一个屏幕,避免来回翻页影响阅读;一般的代码度量工具建议都对此进行检查,例如Logiscope的函数度量:"Number of Statement" (函数中的可执行语句数)建议不超过20行,QA C建议一个函数中的所有行数(包括注释和空白行)不超过50行。

    4、避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过4层

    函数的代码块嵌套深度指的是函数中的代码控制块(例如:if、for、while、switch等)之间互相包含的深度。每级嵌套都会增加阅读代码时的脑力消耗,因为需要在脑子里维护一个“栈”(比如,进入条件语句、进入循环„„)。应该做进一步的功能分解,从而避免使代码的阅读者一次记住太多的上下文。优秀代码参考值:[1, 4]。

    错误示例:代码嵌套深度为5层:

    void serial (void)
    {
        if (!Received)
        {
            TmoCount = 0;
             switch (Buff)
            {
                case AISGFLG:
                    if ((TiBuff.Count > 3)&& ((TiBuff.Buff[0] == 0xff) || (TiBuf.Buff[0] == CurPa.ADDR)))
                    {
                        Flg7E = false;
                        Received = true;
                    }
                    else
                    {
                        TiBuff.Count = 0;
                        Flg7D = false;
                        Flg7E = true;
                    }
                    break;
                default:
                    break;
            }
        }
    }

    5、 可重入函数应避免使用共享变量;若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护

    可重入函数是指可能被多个任务并发调用的函数。在多任务操作系统中,函数具有可重入性是多个任务可以共用此函数的必要条件。共享变量指的全局变量和static变量。编写C语言的可重入函数时,不应使用static局部变量,否则必须经过特殊处理,才能使函数具有可重入性。

    示例:函数square_exam返回g_exam平方值。那么如下函数不具有可重入性。

    int g_exam;
    unsigned int example( int para )
    {
        unsigned int temp;
        g_exam = para; // (**)
        temp = square_exam ( );
        return temp;
    }

    此函数若被多个线程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另外一个使用本函数的线程可能正好被激活,那么当新激活的线程执行到此函数时,将使g_exam赋于另一个不同的para值,所以当控制重新回到“temp =square_exam ( )”后,计算出的temp很可能不是预想中的结果。此函数应如下改进。 

    int g_exam;
    unsigned int example( int para )
    {
        unsigned int temp;
        [申请信号量操作] // 若申请不到“信号量”,说明另外的进程正处于
        g_exam = para; //给g_exam赋值并计算其平方过程中(即正在使用此
        temp = square_exam( ); // 信号),本进程必须等待其释放信号后,才可继
        [释放信号量操作] // 续执行。其它线程必须等待本线程释放信号量后
        // 才能再使用本信号。
        return temp;
    }

    6、对参数的合法性检查,由调用者负责还是由接口函数负责,应在项目组/模块内应统一规定。缺省由调用者负责。

    对于模块间接口函数的参数的合法性检查这一问题,往往有两个极端现象,即:要么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处理过程,造成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种情况虽不会造成问题,但产生了冗余代码,降低了效率。

    7、对函数的错误返回码要全面处理

    一个函数(标准库中的函数/第三方库函数/用户定义的函数)能够提供一些指示错误发生的方法。这可以通过使用错误标记、特殊的返回数据或者其他手段,不管什么时候函数提供了这样的机制,调用程序应该在函数返回时立刻检查错误指示。

    8、设计高扇入,合理扇出(小于7)的函数

    扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。

    扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,例如:总是1,表明函数的调用层次可能过多,这样不利于程序阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力。通常函数比较合理的扇出(调度函数除外)通常是3~5。 

    扇出太大,一般是由于缺乏中间层次,可适当增加中间层次的函数。扇出太小,可把下级函数进一步分解多个函数,或合并到上级函数中。当然分解或合并函数时,不能改变要实现的功能,也不能违背函数间的独立性。扇入越大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。

    较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。

    9、废弃代码(没有被调用的函数和变量) ) 要及时清除

    程序中的废弃代码不仅占用额外的空间,而且还常常影响程序的功能与性能,很可能给程序的测试、维护等造成不必要的麻烦。

    10、函数不变参数使用const 

    不变的值更易于理解/跟踪和分析,把const作为默认选项,在编译时会对其进行检查,使代码更牢固/更安全。

    正确示例:C99标准 7.21.4.4 中strncmp 的例子,不变参数声明为const。

    int strncmp(const char *s1, const char *s2, register size_t n)
    {
        register unsigned char u1, u2;
        while (n-- > 0)
        {
            u1 = (unsigned char) *s1++;
            u2 = (unsigned char) *s2++;
            if (u1 != u2)
            {
                return u1 - u2;
            }
            if (u1 == '\0')
            {
                return 0;
            }
        }
        return 0;
    }

    11、函数应避免使用全局变量、静态局部变量和 I/O 操作,不可避免的地方应集中使用

    带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在C语言中,函数的static局部变量是函数的内部存储器,有可能使函数的功能不可预测。

    错误示例:如下函数,其返回值(即功能)是不可预测的。

    unsigned int integer_sum( unsigned int base )
    {
        unsigned int index;
        static unsigned int sum = 0;// 注意,是static类型的。
        // 若改为auto类型,则函数即变为可预测。
        for (index = 1; index <= base; index++)
        {
            sum += index;
        }
        return sum;
    }

    12、检查函数所有非参数输入的有效性,如数据文件、公共变量等

    函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入参数之前,应进行有效性检查。

    13、 函数的参数个数不超过5个

    函数的参数过多,会使得该函数易于受外部(其他部分的代码)变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量。

    函数的参数个数不要超过5个,如果超过了建议拆分为不同函数。

    14、除打印类函数外,不要使用可变长参函数。

    可变长参函数的处理过程比较复杂容易引入错误,而且性能也比较低,使用过多的可变长参函数将导致函数的维护难度大大增加。

    15、在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加static关键字

    如果一个函数只是在同一文件中的其他地方调用,那么就用static声明。使用static确保只是在声明它的文件中是可见的,并且避免了和其他文件或库中的相同标识符发生混淆的可能性。

    正确示例:建议定义一个STATIC宏,在调试阶段,将STATIC定义为static,版本发布时,改为空,以便于后续的打热补丁等操作。

    #ifdef _DEBUG
    #define STATIC static
    #else
    #define STATIC
    #endif

    3、标识符命名与定义

    标识符的命名规则历来是一个敏感话题,典型的命名风格如unix风格、windows风格等,从来无法达成共识。实际上,各种风格都有其优势也有其劣势,而且往往和个人的审美观有关。我们对标识符定义主要是为了让团队的代码看起来尽可能统一,有利于代码的后续阅读和修改,产品可以根据自己的实际需要指定命名风格,规范中不再做统一的规定。

    1、标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解

    尽可能给出描述性名称,不要节约空间,让别人很快理解你的代码更重要。

    正确示例:

    int error_number;
    int number_of_completed_connection;

    错误示例:

    int n;
    int nerr;
    int n_comp_conns;

    2、除了常见的通用缩写以外,不使用单词缩写,不得使用汉语拼音

    较短的单词可通过去掉“元音”形成缩写,较长的单词可取单词的头几个字母形成缩写,一些单词有大家公认的缩写,常用单词的缩写必须统一。协议中的单词的缩写与协议保持一致。对于某个系统使用的专用缩写应该在注视或者某处做统一说明。

    正确示例:一些常见可以缩写的例子:

    argument 可缩写为 arg
    buffer 可缩写为 buff
    clock 可缩写为 clk
    command 可缩写为 cmd
    compare 可缩写为 cmp
    configuration 可缩写为 cfg
    device 可缩写为 dev
    error 可缩写为 err
    hexadecimal 可缩写为 hex
    increment 可缩写为 inc
    initialize 可缩写为 init
    maximum 可缩写为 max
    message 可缩写为 msg
    minimum 可缩写为 min
    parameter 可缩写为 para
    previous 可缩写为 prev
    register 可缩写为 reg
    semaphore 可缩写为 sem
    statistic 可缩写为 stat
    synchronize 可缩写为 sync
    temp 可缩写为 tmp

    3、产品/项目组内部应保持统一的命名风格

    Unix like和windows like风格均有其拥趸,产品应根据自己的部署平台,选择其中一种,并在产品内部保持一致。

    4、用正确的反义词组命名具有互斥意义的变量或相反动作的函数等

    正确示例:

    add/remove begin/end create/destroy
    insert/delete first/last get/release
    increment/decrement put/get add/delete
    lock/unlock open/close min/max
    old/new start/stop  next/previous
    source/target show/hide  send/receive
    source/destination copy/paste up/down

    5、尽量避免名字中出现数字编号,除非逻辑上的确需要编号

    错误示例:如下命名,使人产生疑惑。

    #define EXAMPLE_0_TEST_
    #define EXAMPLE_1_TEST_

    正确示例:应改为有意义的单词命名。

    #define EXAMPLE_UNIT_TEST_
    #define EXAMPLE_ASSERT_TEST_

    6、标识符前不应添加模块、项目、产品、部门的名称作为前缀

    很多已有代码中已经习惯在文件名中增加模块名,这种写法类似匈牙利命名法,导致文件名不可读,并且带来带来如下问题:

    • 第一眼看到的是模块名,而不是真正的文件功能,阻碍阅读;
    • 文件名太长;
    • 文件名和模块绑定,不利于维护和移植。若foo.c进行重构后,从a模块挪到b模块,若foo.c
    • 中有模块名,则需要将文件名从a_module_foo.c改为b_module_foo.c。

    7、平台/ / 驱动等适配代码的标识符命名风格保持和平台

    涉及到外购芯片以及配套的驱动,这部分的代码变动(包括为产品做适配的新增代码),应该保持原有的风格。

    8、重构/修改部分代码时,应保持和原有代码的命名风格一致

    根据源代码现有的风格继续编写代码,有利于保持总体一致。

    9、文件命名统一采用小写字符

    因为不同系统对文件名大小写处理会不同(如MS的DOS、Windows系统不区分大小写,但是Linux系统则区分),所以代码文件命名建议统一采用全小写字母命名。

    10、全局变量应增加“g_” 前缀,静态变量应增加“s_”

    首先,全局变量十分危险,通过前缀使得全局变量更加醒目,促使开发人员对这些变量的使用更加小心。

    其次,从根本上说,应当尽量不使用全局变量,增加g_和s_前缀,会使得全局变量的名字显得很丑陋,从而促使开发人员尽量少使用全局变量。

    11、禁止使用单字节命名变量,但 允许 定义i 、j、k作为局部循环变量

    12、 不建议使用匈牙利命名法

    匈牙利命名法是一种编程时的命名规范。基本原则是:变量名=属性+类型+对象描述。匈牙利命名法源于微软,然而却被很多人以讹传讹的使用。而现在即使是微软也不再推荐使用匈牙利命名法。历来对匈牙利命名法的一大诟病,就是导致了变量名难以阅读,这和本规范的指导思想也有冲突,所以本规范特意强调,变量命名不应采用匈牙利命名法,而应该想法使变量名为一个有意义的词或词组,方便代码的阅读。

    变量命名需要说明的是变量的含义,而不是变量的类型。在变量命名前增加类型说明,反而降低了变量的可读性;更麻烦的问题是,如果修改了变量的类型定义,那么所有使用该变量的地方都需要修改。

    13、使用名词或者形容词+名词方式命名变量 

    14、函数命名应以函数要执行的动作命名,一般采用动词或者动词+名词的结构

    正确示例:找到当前进程的当前目录:

    DWORD GetCurrentDirectory( DWORD BufferLength, LPTSTR Buffer );

    15、函数指针除了前缀,其他按照函数的命名规则命名

    16、对于数值或者字符串等等常量的定义,建议采用全大写字母,单词之间加下划线“_”的方式命名(枚举同样建议使用此方式定义)

    正确示例:

    #define PI_ROUNDED 3.14

    17、除了头文件或编译开关等特殊标识定义,宏定义不能使用下划线“_”开头和结尾

    一般来说,‟_‟开头、结尾的宏都是一些内部的定义,ISO/IEC 9899(俗称C99)中有如下的描述(6.10.8 Predefined macro names):

    None of these macro names (这里上面是一些内部定义的宏的描述),nor the identifier defined,shall be the subject of a #define or a #undef preprocessing directive.Any other predefined macro names shall begin with a leading underscore fol lowedby an uppercase letter ora second underscore.


    4、变量

    1、一个变量只有一个功能,不能把一个变量用作多种用途

    一个变量只用来表示一个特定功能,不能把一个变量作多种用途,即同一变量取值不同时,其代表的意义也不同。

    错误示例:具有两种功能的反例

    WORD DelRelTimeQue( void )
    {
        WORD Locate;
        Locate = 3; 
        Locate = DeleteFromQue(Locate); /* Locate具有两种功能:位置和函数DeleteFromQue的返回值 */
        return Locate;
    }

    正确做法:使用两个变量

    WORD DelRelTimeQue( void )
    {
        WORD Ret;
        WORD Locate;
        Locate = 3;
        Ret  = DeleteFromQue(Locate);
        return Ret;
    }

    2、结构功能单一,不要设计面面俱到的数据结构 

    相关的一组信息才是构成一个结构体的基础,结构的定义应该可以明确的描述一个对象,而不是一组相关性不强的数据的集合。设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。

    错误示例:如下结构不太清晰、合理。

    typedef struct STUDENT_STRU
    {
        unsigned char name[32]; /* student's name */
        unsigned char age; /* student's age */
        unsigned char sex; /* student's sex, as follows */
        /* 0 - FEMALE; 1 - MALE */
        unsigned char teacher_name[32]; /* the student teacher's name */
        unsigned char teacher_sex; /* his teacher sex */
    } STUDENT;

    正确示例:若改为如下,会更合理些。

    typedef struct TEACHER_STRU
    {
        unsigned char name[32]; /* teacher name */
        unsigned char sex; /* teacher sex, as follows */
        /* 0 - FEMALE; 1 - MALE */
        unsigned int teacher_ind; /* teacher index */
    } TEACHER;
    
    typedef struct STUDENT_STRU
    {
        unsigned char name[32]; /* student's name */
        unsigned char age; /* student's age */
        unsigned char sex; /* student's sex, as follows */
        /* 0 - FEMALE; 1 - MALE */
        unsigned int teacher_ind; /* his teacher index */
    } STUDENT; 

    3、不用或者少用全局变量

    单个文件内部可以使用static的全局变量,可以将其理解为类的私有成员变量。

    全局变量应该是模块的私有数据,不能作用对外的接口使用,使用static类型定义,可以有效防止外部文件的非正常访问,建议定义一个STATIC宏,在调试阶段,将STATIC定义为static,版本发布时,改为空,以便于后续的打补丁等操作。

    4、防止局部变量与全局变量同名

    尽管局部变量和全局变量的作用域不同而不会发生语法错误,但容易使人误解。

    5、通讯过程中使用的结构,必须注意字节序

    通讯报文中,字节序是一个重要的问题,我司设备使用的CPU类型复杂多样,大小端、32位/64位的处理器也都有,如果结构会在报文交互过程中使用,必须考虑字节序问题。由于位域在不同字节序下,表现看起来差别更大,所以更需要注意对于这种跨平台的交互,数据成员发送前,都应该进行主机序到网络序的转换;接收时,也必须进行网络序到主机序的转换。

    6、严禁使用未经初始化的变量作为右值 

    在首次使用前初始化变量,初始化的地方离使用的地方越近越好。

    7、构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一全局变量的现象

    降低全局变量耦合度。

    8、使用面向接口编程思想,通过 API 访问数据:如果本模块的数据需要对外部模块开放 ,应提供接口函数来设置、获取,同时注意全局数据的访问互斥

    避免直接暴露内部数据给外部模型使用,是防止模块间耦合最简单有效的方法。定义的接口应该有比较明确的意义,比如一个风扇管理功能模块,有自动和手动工作模式,那么设置、查询工作模块就可以定义接口为SetFanWorkMode,GetFanWorkMode;查询转速就可以定义为GetFanSpeed;风扇支持节能功能开关,可以定义EnabletFanSavePower等。

    9、明确全局变量的初始化顺序,避免跨模块的初始化依赖 

    系统启动阶段,使用全局变量前,要考虑到该全局变量在什么时候初始化,使用全局变量和初始化全局变量,两者之间的时序关系,谁先谁后,一定要分析清楚,不然后果往往是低级而又灾难性的。

    10、尽量减少没有必要的数据类型默认转换与强制转换

    当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐患。

    错误示例:如下赋值,多数编译器不产生告警,但值的含义还是稍有变化。

    char ch;
    unsigned short int exam;
    ch = -1;
    exam = ch; // 编译器不产生告警,此时exam为0xFFFF。

    5、宏、常量

    1、用宏定义表达式时,要使用完备的括号 

    因为宏只是简单的代码替换,不会像函数一样先将参数计算后,再传递。

    错误示例:如下定义的宏都存在一定的风险

    #define RECTANGLE_AREA(a, b) a * b
    #define RECTANGLE_AREA(a, b) (a * b)
    #define RECTANGLE_AREA(a, b) (a) * (b)

    正确示例:

    #define RECTANGLE_AREA(a, b) ((a) * (b))

    这是因为:如果定义 #define RECTANGLE_AREA(a, b) a * b  或 #define RECTANGLE_AREA(a, b) (a * b)则 c/RECTANGLE_AREA(a, b)  将扩展成 c/a * b , c  与 b 本应该是除法运算,结果变成了乘法运算,造成错误。

    如果定义 #define RECTANGLE_AREA(a, b) (a * b)则 RECTANGLE_AREA(c + d, e + f) 将扩展成: (c + d * e + f), d 与 e 先运算,造成错误。 

    2、将宏所定义的多条表达式放在大括号中

    3、使用宏时,不允许参数发生变化

    错误示例:

    #define SQUARE(a) ((a) * (a))
    int a = 5;
    int b;
    b = SQUARE(a++); // 结果:a = 7,即执行了两次增。

    正确示例:

    b = SQUARE(a);
    a++; // 结果:a = 6,即只执行了一次增。

    同时也建议即使函数调用,也不要在参数中做变量变化操作,因为可能引用的接口函数,在某个版本升级后,变成了一个兼容老版本所做的一个宏,结果可能不可预知。

    4、不允许直接使用魔鬼数字 

    使用魔鬼数字的弊端:代码难以理解;如果一个有含义的数字多处使用,一旦需要修改这个数值,代价惨重。

    使用明确的物理状态或物理意义的名称能增加信息,并能提供单一的维护点。

    解决途径:对于局部使用的唯一含义的魔鬼数字,可以在代码周围增加说明注释,也可以定义局部const变量,变量命名自注释。对于广泛使用的数字,必须定义const全局变量/宏;同样变量/宏命名应是自注释的。0作为一个特殊的数字,作为一般默认值使用没有歧义时,不用特别定义。

    5、除非必要,应尽可能使用函数代替宏

    宏对比函数,有一些明显的缺点:

    • 宏缺乏类型检查,不如函数调用检查严格;
    • 宏展开可能会产生意想不到的副作用,如#define SQUARE(a) (a) * (a)这样的定义,如果是SQUARE(i++),就会导致i被加两次;如果是函数调用double square(double a) {return a * a;}则不会有此副作用;
    • 以宏形式写的代码难以调试难以打断点,不利于定位问题;
    • 宏如果调用的很多,会造成代码空间的浪费,不如函数空间效率高。

    错误示例:下面的代码无法得到想要的结果:

    #define MAX_MACRO(a, b) ((a) > (b) ? (a) : (b))
    
    int MAX_FUNC(int a, int b) {
        return ((a) > (b) ? (a) : (b));
    }
    
    int testFunc()
    {
        unsigned int a = 1;
        int b = -1;
        printf("MACRO: max of a and b is: %d\n", MAX_MACRO(++a, b));
        printf("FUNC : max of a and b is: %d\n", MAX_FUNC(a, b));
        return 0;
    }

    上面宏代码调用中,由于宏缺乏类型检查,a和b的比较变成无符号数的比较,结果是a < b,所以a只加了一次,所以最终的输出结果是:

    MACRO: max of a and b is: -1
    FUNC : max of a and b is: 2

    6、常量建议使用 const 定义代替宏

    “尽量用编译器而不用预处理”,因为#define经常被认为好象不是语言本身的一部分。看下面的语句:

    #define ASPECT_RATIO 1.653

    编译器会永远也看不到ASPECT_RATIO这个符号名,因为在源码进入编译器之前,它会被预处理程序去掉,于是ASPECT_RATIO不会加入到符号列表中。如果涉及到这个常量的代码在编译时报错,就会很令人费解,因为报错信息指的是1.653,而不是ASPECT_RATIO。如果ASPECT_RATIO不是在你自己写的头文件中定义的,你就会奇怪1.653是从哪里来的,甚至会花时间跟踪下去。这个问题也会出现在符号调试器中,因为同样地,你所写的符号名不会出现在符号列表中。
    解决这个问题的方案很简单:不用预处理宏,定义一个常量:

    const double ASPECT_RATIO = 1.653;

    这种方法很有效,但有两个特殊情况要注意。首先,定义指针常量时会有点不同。因为常量定义一般是放在头文件中(许多源文件会包含它),除了指针所指的类型要定义成const外,重要的是指针也经常要定义成const。例如,要在头文件中定义一个基于char*的字符串常量,你要写两次const:

    const char * const authorName = "Scott Meyers";

    延伸阅读材料:关于const和指针的使用,这里摘录两段ISO/IEC 9899(俗称C99)的描述: 

    7、宏定义中尽量不使用 return 、 goto 、 continue 、 break等改变程序流程的语句

    如果在宏定义中使用这些改变流程的语句,很容易引起资源泄漏问题,使用者很难自己察觉。

    错误示例:在某头文件中定义宏CHECK_AND_RETURN:

    #define CHECK_AND_RETURN(cond, ret) {if (cond == NULL_PTR) {return ret;}}
    //然后在某函数中使用(只说明问题,代码并不完整):
    pMem1 = VOS_MemAlloc(...);
    CHECK_AND_RETURN(pMem1 , ERR_CODE_XXX)
    pMem2 = VOS_MemAlloc(...);
    CHECK_AND_RETURN(pMem2 , ERR_CODE_XXX) /*此时如果pMem2==NULL_PTR,则pMem1未释放函数就返回了,造成内存泄漏。*/

    所以说,类似于CHECK_AND_RETURN这些宏,虽然能使代码简洁,但是隐患很大,使用须谨慎。 


    6、表达式

    1、表达式的值在标准所允许的任何运算次序下都应该是相同的

    2、函数调用不要作为另一个函数的参数使用,否则对于代码的调试、阅读都不利

    错误示例:如下代码不合理,仅用于说明当函数作为参数时,由于参数压栈次数不是代码可以控制的,可能造成未知的输出:

    int g_var;
    
    int fun1()
    {
        g_var += 10;
        return g_var;
    }
    
    int fun2()
    {
        g_var += 100;
        return g_var;
    }
    
    int main(int argc, char *argv[], char *envp[])
    {
        g_var = 1;
        printf("func1: %d, func2: %d\n", fun1(), fun2());
        g_var = 1;
        printf("func2: %d, func1: %d\n", fun2(), fun1());
    }

    上面的代码,使用断点调试起来也比较麻烦,阅读起来也不舒服,所以不要为了节约代码行,而写这种代码。

    3、赋值语句不要写在 if 等语句中,或者作为函数的参数使用

    因为if语句中,会根据条件依次判断,如果前一个条件已经可以判定整个条件,则后续条件语句不会再运行,所以可能导致期望的部分赋值没有得到运行。

    错误示例:

    int main(int argc, char *argv[], char *envp[])
    {
        int a = 0;
        int b;
        if ((a == 0) || ((b = fun1()) > 10))
        {
            printf("a: %d\n", a);
        }
        printf("b: %d\n", b);
    }

    作用函数参数来使用,参数的压栈顺序不同可能导致结果未知。

    4、用括号明确表达式的操作顺序,避免过分依赖默认优先级

    使用括号强调所使用的操作符,防止因默认的优先级与设计思想不符而导致程序出错;同时使得代码更为清晰可读,然而过多的括号会分散代码使其降低了可读性。

    5、赋值操作符不能使用在产生布尔值的表达式上 

    示例:

    x = y;
    if (x != 0)
    {
        foo ();
    }

    不能写成:

    if (( x = y ) != 0)
    {
        foo ();
    }

    或者更坏的:

    if (x = y)
    {
        foo ();
    }

    7、注释

     1、优秀的代码可 以自我解释,不通过注释即可轻易读懂

    优秀的代码不写注释也可轻易读懂,注释无法把糟糕的代码变好,需要很多注释来解释的代码往往存在坏味道,需要重构。

    错误示例:注释不能消除代码的坏味道:

    /* 判断m是否为素数*/
    /* 返回值:: 是素数,: 不是素数*/
    int p(int m)
    {
        int k = sqrt(m);
        for (int i = 2; i <= k; i++)
            if (m % i == 0)
                break; /* 发现整除,表示m不为素数,结束遍历*/
        /* 遍历中没有发现整除的情况,返回*/
        if (i > k)
            return 1;
        /* 遍历中没有发现整除的情况,返回*/
        else
            return 0;
    }

    重构代码后,不需要注释:

    int IsPrimeNumber(int num)
    {
        int sqrt_of_num = sqrt (num);
        for (int i = 2; i <= sqrt_of_num; i++)
        {
            if (num % i == 0)
            {
                return FALSE;
            }
        }
        return TRUE;
    }

    2、注释的内容要清楚、明了,含义准确,防止注释二义性

    有歧义的注释反而会导致维护者更难看懂代码,正如带两块表反而不知道准确时间。

    3、在代码的功能、意图层次上进行注释,即注释解释 代码难以直接表达的意图 , 而不是重复描述代码

    注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释。注释不是为了名词解释(what),而是说明用途(why)。

    4、修改代码时,维护代码周边的所有注释,以保证注释与代码的一致性,不再有用的注释要删除

    不要将无用的代码留在注释中,随时可以从源代码配置库中找回代码;即使只是想暂时排除代码,也要留个标注,不然可能会忘记处理它。

    5、文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者姓名、工号、内容、功能说明、与其它文件的关系、修改日志等,头文件的注释中还应有函数功能简要说明

    正确示例:下面这段头文件的头注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。

    6、函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、 设计约束等

    重要的、复杂的函数,提供外部使用的接口函数应编写详细的注释。

    7、全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明

    正确示例:

    /* The ErrorCode when SCCP translate */
    /* Global Title failure, as follows */ /* 变量作用、含义*/
    /* 0 -SUCCESS 1 -GT Table error */
    /* 2 -GT error Others -no use */ /* 变量取值范围*/
    /* only function SCCPTranslate() in */
    /* this modual can modify it, and other */
    /* module can visit it through call */
    /* the function GetGTTransErrorCode() */ /* 使用方法*/
    BYTE g_GTTranErrorCode;

    8、注释应放在其代码上方相邻位置或右方,不可放在下面,如放于上方则需与其上面的代码用空行隔开,且与下方代码缩进相同

    正确示例:

    /* active statistic task number */
    #define MAX_ACT_TASK_NUMBER 1000
    #define MAX_ACT_TASK_NUMBER 1000 /* active statistic task number */
    可按如下形式说明枚举/数据/联合结构。
    /* sccp interface with sccp user primitive message name */
    enum SCCP_USER_PRIMITIVE
    {
        N_UNITDATA_IND, /* sccp notify sccp user unit data come */
        N_NOTICE_IND, /* sccp notify user the No.7 network can not transmission this message */
        N_UNITDATA_REQ, /* sccp user's unit data transmission request*/
    };

    9、对于 switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释

    这样比较清楚程序编写者的意图,有效防止无故遗漏break语句。

    case CMD_FWD:
        ProcessFwd();
        /* now jump into c ase CMD_A */
    case CMD_A:
        ProcessA();
        break;
    //对于中间无处理的连续case,已能较清晰说明意图,不强制注释。
    switch (cmd_flag)
        {
            case CMD_A:
            case CMD_B:
        {
            ProcessCMD();
            break;
        }
        ……
    }

    10、避免在注释中使用缩写,除非是业界通用或子系统内标准化的缩写

    11、同一产品或项目组统一注释风格

    12、避免在一行代码或表达式的中间插入注释

    除非必要,不应在代码或表达中间插入注释,否则容易使代码可理解性变差

    13、注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达,对于有外籍员工的,由产品确定注释语言

    注释语言不统一,影响程序易读性和外观排版,出于对维护人员的考虑,建议使用中文。

    14、文件头、函数头、全局常量变量、类型定义的注释格式采用工具可识别的格式

    采用工具可识别的注释格式,例如doxygen格式,方便工具导出注释形成帮助文档。以doxygen格式为例,文件头,函数和全部变量的注释的示例如下:


    8、排版与格式

    1、程序块采用缩进风格编写, 每级缩进为4个空格

    2、相对独立的程序块之间、变量说明之后必须加空行 

    错误示例:如下例子不符合规范。

    if (!valid_ni(ni))
    {
        // program code
        ...
    }
    repssn_ind = ssn_data[index].repssn_index;
    repssn_ni = ssn_data[index].ni;

    正确示例:

    if (!valid_ni(ni))
    {
        // program code
        ...
    }
    
    repssn_ind = ssn_data[index].repssn_index;
    repssn_ni = ssn_data[index].ni;

    3、一条语句不能过长,如不能拆分需要分行写。一行到底多少字符换行比较合适,产品可以自行确定

    对于目前大多数的PC来说,132比较合适(80/132是VTY常见的行宽值);对于新PC宽屏显示器较多的产品来说,可以设置更大的值。换行时有如下建议:

    • 换行时要增加一级缩进,使代码可读性更好;
    • 低优先级操作符处划分新行;换行时操作符应该也放下来,放在新行首;
    • 换行时建议一个完整的语句放在一行,不要根据字符数断行。

    正确示例:

    if ((temp_flag_var == TEST_FLAG)
    &&(((temp_counter_var - TEST_COUNT_BEGIN) % TEST_COUNT_MODULE) >= TEST_COUNT_THRESHOLD))
    {
        // process code
    }

    4、多个短语句(包括赋值语句)不允许写在同一行内 ,即一行只写一条语句

    错误示例:

    int a = 5; int b= 10; //不好的排版

    正确示例:

    int a = 5;
    int b= 10;

    5、if 、 for 、 do 、 while 、 case 、 switch 、 default 等语句独占一行

    执行语句必须用缩进风格写,属于if、for、do、while、case、switch、default等下一个缩进级别;

    一般写if、for、do、while等语句都会有成对出现的„{}‟,对此有如下建议可以参考:if、for、do、while等语句后的执行语句建议增加成对的“{}”;如果if/else配套语句中有一个分支有“{}”,那么另一个分支即使一行代码也建议增加“{}”;添加“{”的位置可以在if等语句后,也可以独立占下一行;独立占下一行时,可以和if在一个缩进级别,也可以在下一个缩进级别;但是如果if语句很长,或者已经有换行,建议“{”使用独占一行的写法。

    6、在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格 ; 进行非对等操作时,如果是关系密切的立即操作符(如-> > ),后不应加空格

    采用这种松散方式编写代码的目的是使代码更加清晰。

    在已经非常清晰的语句中没有必要再留空格,如括号内侧(即左括号后面和右括号前面)不需要加空格,多重括号间不必加空格,因为在C语言中括号已经是最清晰的标志了。在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。给操作符留空格时不要连续留两个以上空格。

    正确示例:

    1、逗号、分号只在后面加空格。

    int a, b, c;

    2、比较操作符, 赋值操作符"="、 "+=",算术操作符"+"、"%",逻辑操作符"&&"、"&",位域操作符"<<"、"^"等双目操作符的前后加空格。 

    if (current_time >= MAX_TIME_VALUE)
    a = b + c;
    a *= 2;
    a = b ^ 2;

    3、"!"、"~"、"++"、"--"、"&"(地址操作符)等单目操作符前后不加空格。

    *p = 'a'; // 内容操作"*"与内容之间
    flag = !is_empty; // 非操作"!"与内容之间
    p = &mem; // 地址操作"&" 与内容之间
    i++; 

     4、"->"、"."前后不加空格。

    p->id = pid; // "->"指针前后不加空格

    5、if、for、while、switch等与后面的括号间应加空格,使if等关键字更为突出、明显。

    if (a >= b && c > d)

    7、注释符(包括/**/、//)与注释内容之间要用一个空格进行分隔

    8、源程序中关系较为紧密的代码应尽可能相邻


    9、代码编辑编译 

    1、使用编译器的最高告警级别,理解所有的告警,通过修改代码而不是降低告警级别来消除所有告警

    编译器是你的朋友,如果它发出某个告警,这经常说明你的代码中存在潜在的问题。

    2、在产品软件(项目组)中,要统一编译开关、静态检查选项以及相应告警清除策略

    如果必须禁用某个告警,应尽可能单独局部禁用,并且编写一个清晰的注释,说明为什么屏蔽。某些语句经编译/静态检查产生告警,但如果你认为它是正确的,那么应通过某种手段去掉告警信息。

    4、本地构建工具(如 PC-Lint)的配置应该和持续集成的一致

    两者一致,避免经过本地构建的代码在持续集成上构建失败

    5、 使用版本控制(配置管理)系统,及时签入通过本地构建的代码,确保签入的代码不会影响构建成功

    及时签入代码降低集成难度。

    6、要小心地使用编辑器提供的块拷贝功能编程

    以上为自我总结,感兴趣的同志,推荐阅读全文,也就60余页。

    展开全文
  • ❤️五万字《C语言动漫教程》❤️

    万次阅读 多人点赞 2021-07-01 13:44:49
    六万字最全C语言动漫式教程,本文连载直到卡死
     
    
    🙉饭不食,水不饮,题必须刷🙉

    C语言免费动漫教程,和我一起打卡!
    🌞《光天化日学C语言》🌞

    LeetCode 太难?先看简单题!
    🧡《C语言入门100例》🧡

    数据结构难?不存在的!
    🌳《数据结构入门》🌳

    LeetCode 太简单?算法学起来!
    🌌《夜深人静写算法》🌌

    一、开端

    在这里插入图片描述

    • C语言是一种高级语言,运行效率仅次于汇编,支持跨平台,所以被广泛的应用于软件开发、系统开发、嵌入式系统、游戏开发等场景。

    二、一个C语言程序

    在这里插入图片描述

    1、编程环境

    • ( 1 ) (1) (1) 百度搜索 “c语言在线编译”,如图四-1-1所示:

    图四-1-1

    • ( 2 ) (2) (2) 任意选择一个在线编译工具,我选择的是菜鸟工具,如图四-1-2所示:
      图四-1-2

    2、写代码

    在这里插入图片描述

    • 先给出代码,然后根据行尾的标号,一行一行进行解释;
    #include <stdio.h>               // (1)
    int main()                       // (2)
    {
       /* 我的第一个 C 程序 */       // (3)
       printf("Hello, World! \n");   // (4)
       return 0;                     // (5)
    }
    

    这段代码只做了一件事情,就是向屏幕上输出一行字:Hello, World!
    ( 1 ) (1) (1) stdio.h是一个头文件 (标准输入输出头文件) , #include是一个预处理命令,用来引入头文件。当编译器遇到 printf()函数时,如果没有找到 stdio.h头文件,就会发生编译错误。
    ( 2 ) (2) (2) main()作为这个程序的入口函数,代码都是从这个函数开始执行的。
    ( 3 ) (3) (3)/**/包围起来的代表注释,是给人看到,不进行代码的解析和执行。
    ( 4 ) (4) (4) printf代表将内容输出到控制台上。其中\n代表换行符。
    ( 5 ) (5) (5) 作为函数的返回值。
    在这里插入图片描述

    • 你可能对 头文件、预处理命令、函数、换行符、返回值 这些都没有概念,没有关系,刚开始我们不去理解这些概念,你只需要知道:通过改一些代码以后,能够看到想要看到的结果 就行。

    3、修改代码

    • 我们把 Hello, World 改成 光天化日学C语言 后,再来看看效果:
    #include <stdio.h>                // (1)
    int main()                        // (2)
    {
       /* 我的第一个 C 程序 */        // (3)
       printf("光天化日学C语言! \n"); // (4)
       return 0;                      // (5)
    }
    

    三、编译运行

    1、编译

    • 编译就是把高级语言变成计算机可以识别的二进制语言,因为计算机只认识 1 和 0,你需要把一大堆复杂的语言词法、语法全部转换成 0 和 1。

    2、运行

    • 运行就是执行可执行程序啦。就是我们通常 Windows 上的双击 exe 干的事情。

    在这里插入图片描述


    四、为什么要搭建本地环境

    在这里插入图片描述

    • 1)联网:在线编译环境毕竟涉及到联网,如果没有网的情况下,我们就不能写代码了,真是听者伤心,闻者流泪啊;
    • 2)定制化:写代码是一辈子的事情,界面当然要搞得赏心悦目才能持久,本地环境可以配置字体和背景,支持定制化,觉得什么界面好看就配成什么样的;
    • 3)代码补全:字体高亮,代码补全 这些好用的功能,能够帮助你减少很多不必要编码错误;
    • 4)多文件:当代码量比较大以后,涉及到多个文件时,在线编译环境就无能为力了;

    五、下载 Dev C++

    • Dev C++ 是一个轻量级的 C/C++ 集成编译环境,正因为是轻量级,所以还是有很多不太好用的地方,不过不用担心,对于教学来说已经足够了。
    • 相比 Visual Studio 20XX 来说,安装快了不少,所以我打算用这个工具来进行后续文章的讲解。
    • 可以选择以下任何一个链接进行下载,下载后解压出 DevCpp_v6.5.exe 即可。

    百度网盘下载

    CSDN下载

    六、安装 Dev C++

    1、语言选择

    • 双击 DevCpp 的 exe 文件,会跳出如下对话框,初学者建议直接用中文。如图五-1所示:

    图五-1

    2、我接受

    • 同意安装,如图五-2所示:

    图五-2

    3、下一步

    • 点击下一步,如图五-3所示:

    图五-3

    4、选择安装位置

    • 选择一个你钟意的安装路径,点击安装,如图五-4所示:

    在这里插入图片描述

    图五-4

    5、看他装完

    • 看他安装完,大概 7 秒左右,如图五-5-1所示:

    图五-5-1

    图五-5-2

    七、配置

    1、选择语言

    • 选择一个你钟意的语言,推荐用中文,强我国威,壮我河山!点击 Next,如图六-1所示:

    图六-1

    2、选择配色

    • 选择一个你看着舒服的配色方案,推荐 VS Code,如图六-1所示:

    在这里插入图片描述

    图六-1

    八、写一段代码

    1、新建文件

    • 点击界面左上角的 【新建】 按钮,选择【源代码】菜单栏,如图七-1所示;
      图七-1

    2、写代码

    • 把我们第一章中写过的代码,写到这个文件中。建议自己一行一行写哦,复制粘贴 和 自己敲出来的感觉是不一样的。
    #include <stdio.h>
    int main() {
        printf("光天化日写C语言!\n");
        return 0;
    }
    

    3、保存文件

    • 点击菜单栏的【保存】按钮,或者 Ctrl + S 快捷键保存文件。

    在这里插入图片描述

    图七-3

    4、编译运行

    • 点击菜单栏的【编译运行】或者 F11 按钮,就会跳出一个控制台,如图七-4所示:

    图七-4


    九、变量

    在这里插入图片描述

    1、变量的定义

    对于一个变量而言,有三部分组成:
      1)变量类型;
      2)变量名;
      3)变量地址;

    • 在C语言中,我们可以通过如下的方式定义一个变量:
        int Iloveyou;
    

    1)变量类型

    • int表示变量类型,是英文单词 Integer 的缩写,意思是整数。

    在这里插入图片描述

    2)变量名

    • Iloveyou表示变量名,也可以叫其它名字,例如:WhereIsHeroFromILoveYou1314等等。
    • 这个语句的含义是:在内存中找一块区域,命名为Iloveyou,用它来存放整数。
    • 需要注意的是,最后有一个分号,int Iloveyou表达了一个语句,要用分号来结束。需要注意的是,最后有一个分号,int Iloveyou表达了一个语句,要用分号来结束。

    在这里插入图片描述

    3)变量地址

    在这里插入图片描述

    2、变量的赋值

    • C语言中可以用以下语句把 520 520 520 这个整数存储到Iloveyou这个变量里:
        Iloveyou = 520;
    

    在这里插入图片描述

    • =在数学中叫 “等于号”,例如 1 + 1 = 2,但在C语言中,这个过程叫做变量的赋值,简称赋值。赋值是指把数据放到内存的过程。

    3、变量的初始化

    • 把上面的两个语句连起来,得到:
        int Iloveyou;
        Iloveyou = 520;
    
    • 当然,我们也可以写成如下形式:
        int Iloveyou = 520;
    

    在这里插入图片描述

    • 两段代码的执行结果相同,都是把Iloveyou的值变成 520 520 520

    在这里插入图片描述

    4、变量的由来

    • 如果我们需要,可以随时改变它的值,如下代码所示:
        int Iloveyou = 520;
        Iloveyou = 521;
        Iloveyou = 522;
        Iloveyou = 523;
    
    • 代码执行完毕以后,它的值以最后一次赋值为准,正因为可以不断修改,是可变的,所以才叫变量。

    • 简单总结一下就是:数据是放在内存中的,变量是给这块内存起的名字,有了变量就可以找到并使用这份数据。

    5、多变量的定义

    • 如果几个变量的类型一致,我们可以写在一行上进行定义,如下:
        int x, y, z = 5;
    
    • 这段代码代表一次性定义了三个整型类型的变量,并且将z初始化为 5,等价于如下代码:
        int x; 
        int y;
        int z = 5;
    

    6、变量间的赋值

    • 变量不能赋值给数字,但是变量可以赋值给变量。
        int a, b;
        520 = a;  // 错误
        a = b;    // 正确
    

    在这里插入图片描述

    【例题1】给出如下代码,求输出结果是什么。

    #include <stdio.h>
    int main()
    {
        int a = 1314, b = 520;
        b = a;
        a = b;
        printf("a=%d b=%d\n", a, b);
        return 0;
    }
    

    十、数据类型

    在这里插入图片描述

    • 接下来我们展开来讲一下变量类型,更加确切的讲,应该叫数据类型,C语言中有如下一些系统内置数据类型。

    1、内置数据类型

    • 从上面这个表,我们可以看到,有表示字符的,有表示整数的,也有表示浮点数的。

    在这里插入图片描述

    • 先来简单看下每种内置类型是如何进行定义的:
        char a = 'a';
        short b, c, d = 1314, e, f;
        int g = 5201314;
        long long h = 123456789;
        float i = 4.5;
        double j = 4.50000;
    

    2、数据的大小

    在这里插入图片描述

    • 字节是计算机中的一种基本单位,英文名为 Byte,计算机中所有的数据都是由字节组成的。
    • 我们通常在计算机中看到的文件单位 B 、K、M 、G、T 和字节的关系如下:

    • 一个字节在计算机里面是有 8 个位组成,一个位有 0 和 1 两种状态,所以一个字节能表示的状态数就是 2 8 = 256 2^8 = 256 28=256。如图四-2-1,代表的是一个字节的状态,白色代表0,灰色代表1,它的二进制表示就是 ( 00001101 ) 2 (00001101)_2 (00001101)2
      图四-2-1

    3、整数的表示范围

    • 这样一来,上面提到的几种整数类型,能够表示的整数就显而易见了,假设字节数为 n n n,那么能够表示的整数个数就是能够表示的状态个数,即: 2 8 n 2^{8n} 28n
    • 由于我们需要表示负数 和 零,实际的每种整数数据类型能够表示的数字范围如下表所示:

    十一、变量名

    1、标识符

    • 定义变量时,我们使用了诸如 loveIloveyou这样的名字,为了表达变量的作用,这就叫 标识符,即 Identifier。
    • 标识符就是程序员自己起的名字,除了变量名,后面还会讲到函数名、常量名、宏名、结构体名等,它们都是标识符。

    2、关键字

    • 关键字(Keywords)是由C语言规定的具有特定意义的字符串,通常也称为保留字,例如 intcharlongintunsigned int等。
    • 程序自己定义的标识符不能与关键字相同,否则会出现错误。
    • 后续会对各个关键字进行一一讲解。

    3、命名规则

    • ( 1 ) (1) (1) 必须由字母、数字 或者下划线构成,如 _aaa123_都是合法的变量,?*a a#、都是非法的变量;
    • ( 2 ) (2) (2) 不能以数字开头,如123abc不是一个合法的变量名;
    • ( 3 ) (3) (3) 大小写敏感,即大小写看成不同,即oO不是同一个变量;
    • ( 4 ) (4) (4) 不能将变量名和C语言的语法保留关键字同名;
    • ( 5 ) (5) (5) C语言虽然不限制标识符的长度,但是它受到 编译器 和 操作系统 的限制。例如在某个编译器中规定标识符前 256 位有效,当两个标识符前 256 位相同时,则被认为是同一个标识符。
    • ( 6 ) (6) (6) 标识符命名时还是最好遵循 min-length-max-infomation 的原则,即以最小的长度表达最全的信息,不过这个是规范上的,语言层面是不会做过多的限制的。

    【例题2】给出一段程序,请回答这段程序的运行结果。

    #include <stdio.h>
    
    int main()
    {
    	int IloveYou = 0;
    	ILoveYou = 1314;
    	ILoveYou = ILoveYou;
    	ILoveYou = 520;
    	printf("%d\n", ILoveYou);
        return 0;
    }
    

    十二、概念简介

    1、输出的含义

    2、标准输出

    在这里插入图片描述

    在C语言中,有三个函数可以用来在屏幕上输出数据,它们分别是:
      1)puts() :只能输出字符串,并且输出结束后会自动换行;
      2)putchar() :只能输出单个字符;
      3)printf():可以输出各种类型的数据,作为最灵活、最复杂、最常用的输出函数,可以完全替代全面两者,所以是必须掌握的,今天我们就来全面了解一下这个函数。

    3、格式化

    在这里插入图片描述

    • 我们在进行输出的时候,对于小数而言,可能需要输出小数点后一位,亦或是两位,这个计算机自己是不知道规则的,需要写代码的人告诉它,这个告诉它如何输出的过程就被称为格式化。

    十三、格式化输出

    • printf前几个章节都有提及,这个函数的命名含义是:Print(打印)Format (格式) ,即 格式化输出。

    1、数据类型格式化

    1)整数

    #include <stdio.h>
    int main()
    {
        int a = 520;
        long long b = 1314;
    	printf("a is %d, b is %lld!\n", a, b);
    	return 0;
    }
    
    • 对于int而言,我们利用%d将要输出的内容进行格式化,然后输出,简单的理解就是把%d替换为对应的变量,%lld用于对long long类型的变量进行格式化,所以这段代码的输出为:
    a is 520, b is 1314!
    

    在这里插入图片描述

    2)浮点数

    #include <stdio.h>
    int main()
    {
    	float f = 1.2345;
    	double df = 123.45;
    	printf("f is %.3f, df is %.0lf\n", f, df);
    	return 0;
    }
    
    • 对于浮点数而言,我们利用%f来对单精度浮点数float进行格式化;用%lf来对双精度浮点数进行格式化,并且用.加 “数字” 来代表要输出的数精确到小数点后几位,这段代码的输出为:
    f is 1.235, df is 123
    

    • 另外,单精度 和 双精度 的区别就是双精度的精度更高一点,也就是能够表示的小数的范围更加精准,这个会在介绍浮点数的存储方式时详细介绍。

    3)字符

    #include <stdio.h>
    int main()
    {
        char ch = 'A';
        printf("%c\n", ch);
    	return 0;
    }
    
    • 对于字符而言,我们利用%c来进行格式化;C语言中的字符是用单引号引起来的,当然,字符这个概念扯得太远,会单独开一个章节来讲,具体可以参考 ASCII 码。
    • 顺便我们来解释一下一直出现但是我闭口不提的换行符\n,这个符号是一个转义符,它代表的不是两个字符(反斜杠\和字母n),而是换行的意思;
    • 这段代码的输出就是一个字符 A;
    A
    
    • 我们通过一个例题来理解这个换行符的含义;
      在这里插入图片描述

    【例题1】第1行输出1个1,第2行输出2个2,第3行输出3个3,第4行输出4个4。

    #include <stdio.h>
    int main()
    {
        printf("1\n");
        printf("22\n");
        printf("333\n");
        printf("4444\n");
    	return 0;
    }
    
    • 我们也可以用一条语句解决,如下:
    #include <stdio.h>
    int main()
    {
        printf("1\n22\n333\n4444\n");
    	return 0;
    }
    

    在这里插入图片描述

    4)字符串

    • 字符串,是由多个字符组合而成,用双引号引起来,这一章我不打算讲得太细,只需要知道用%s进行格式化的即可,代码如下:
    #include <stdio.h>
    int main()
    {
        char str[100] = "I love you!";
        printf("%s\n", str);
    	return 0;
    }
    
    • 这段代码,聪明的你应该很容易看懂啦!输出的就是:
    I love you!
    

    在这里插入图片描述

    • 作者:我了个擦,字体颜色都变了……

    2、对齐格式化

    • 我们发现,上文中所有的格式化,都有一个%和一个字母,事实上,在百分号和字母之间,还有一些其它的内容。

    主要包含如下内容:
      1)负号:如果有,则按照左对齐输出;
      2)数字:指定字段最小宽度,如果不足则用空格填充;
      3)小数点:用与将最小字段宽度和精度分开;
      4)精度:用于指定字符串重要打印的而最大字符数、浮点数小数点后的位数、整型最小输出的数字数目;

    在这里插入图片描述

    【例题2】给定如下一段代码,求它的输出内容。

    #include <stdio.h>
    int main()
    {
        double x = 520.1314;
    	int y = 520;
    	
        printf("[%10.5lf]\n", x);
    	printf("[%-10.5lf]\n", x);
    	
    	
    	printf("[%10.8d]\n", y);
    	printf("[%-10.8d]\n", y);
    
        return 0;
    }
    

    在这里插入图片描述

    • 输出答案如下:
    [ 520.13140]
    [520.13140 ]
    [  00000520]
    [00000520  ]
    
    • 我们发现,首先需要看小数点后面的部分,将要输出的内容实际要输出多少的长度确定下来,然后再看字段最小宽度,最后再来看左对齐还是右对齐。

    • 然后,我们来看看把不同类型的变量组合起来是什么效果;
    #include <stdio.h>
    int main()
    {
        char name[100] = "Zhou";
        int old = 18;
        double meters = 1.7;
        char spostfix = 's';
        printf("My name is %s, %d years old, %.2lf meter%c.\n", 
            name, old, meters, spostfix);
        return 0;
    }
    
    • 它的输出结果如下:
    My name is Zhou, 18 years old, 1.70 meters.
    

    在这里插入图片描述


    十四、输入概念简介

    1、输入的含义

    2、标准输入

    在C语言中,有三个函数可以用来在键盘上输入数据,它们分别是:
      1)gets() :用于输入一行字符串;
      2)getchar() :用于输入单个字符;
      3)scanf():可以输入各种类型的数据,作为最灵活、最复杂、最常用的输入函数,虽然无法完全替代前面两者,但是却是必须掌握的,今天我们就来全面了解一下这个函数。

    3、格式化

    • 我们在进行输入的时候,其实都是一个字符串,但是这个字符串被输入后有可能当成整数来用,也有可能还是字符串,这个计算机自己是不知道规则的,需要写代码的人告诉它,这个告诉它如何输入的过程就被称为格式化。

    十五、整数的格式化输入

    • scanf的函数的命名含义是:Scan(扫描)Format (格式) ,即 格式化输入。
    • 和输出一样,输入的时候,也根据数据类型的不同,分为 整数、浮点数、字符、字符串等等。
    • 但是这里会有很多问题,拿整数的输入为例,我们一个一个来看。

    1、单个数据的输入

    • 对于单个数据的输入,如下代码所示:
    #include <stdio.h>
    int main() 
    {
        int a;
        scanf("%d", &a);
        printf("%d\n", a);
        return 0;
    }
    
    • 这段代码的执行结果如下:
    13141314
    

    其中代表回车,即我们通过键盘输入1314,按下回车后,在屏幕上输出1314

    类比输出,我们发现,输入和输出的差别在于:
       ( 1 ) (1) (1) 函数名不同;
       ( 2 ) (2) (2) 输入少了换行符 \n
       ( 3 ) (3) (3) 输入多了取地址符&

    • 我们会在后面指针的章节来围绕对这个符号进行展开的。

    2、多个数据的输入

    • 类比单个数据的输入,我们来看看两个数据的输入:
    #include <stdio.h>
    int main() 
    {
        int a, b;
        scanf("%d", &a);
        scanf("%d", &b);
        printf("%d %d\n", a, b);
        return 0;
    }
    
    • 这段代码的执行结果如下:
    5201314520 1314
    

    其中代表回车,即我们通过键盘输入520,按下回车,再输入1314,按下回车后,在屏幕上输出520 1314

    • 这个很好理解,那么我们同样可以把输入放在一行上进行输入,类比输出的格式,如下:
    #include <stdio.h>
    int main() 
    {
        int a, b;
        scanf("%d %d", &a, &b);
        printf("%d %d\n", a, b);
        return 0;
    }
    
    • 这段代码的执行结果如下:
    520 1314520 1314
    

    其中代表回车,即我们通过键盘输入520空格1314,按下回车后,在屏幕上输出520 1314

    • 所以,多个数据的输入,我们可以放在一个scanf语句来完成。

    3、空格免疫

    • 然后我们来看下,对于输入的数据之间有一个空格和多个空格的情况,代码如下:
    #include <stdio.h>
    int main()
    {
        int a, b;
        scanf("%d %d", &a, &b);
        printf("%d %d\n", a, b);
        return 0;
    }
    
    520    1314520 1314
    

    其中代表回车,即我们通过键盘输入520n个空格1314,按下回车后,在屏幕上输出520 1314

    • 也就是说,虽然文中要求是1个空格,但是我们输入多个也不影响我们输入,再来看下一种情况:
    #include <stdio.h>
    int main()
    {
        int a, b;
        scanf("%d       %d", &a, &b);
        printf("%d %d\n", a, b);
        return 0;
    }
    
    520 1314520 1314
    

    其中代表回车,即我们通过键盘输入5201个空格1314,按下回车后,在屏幕上输出520 1314

    • 也就是说,虽然文中要求多个空格,但是我们输入1个也不影响我们输入。

    在这里插入图片描述

    4、回车结算

    • 通过以上的几个例子,我们发现,scanf()是以回车来结算一次输入的。
    • 用户每次按下回车键,计算机就会认为完成一次输入操作,scanf()开始读取用户输入的内容,并根据我们定义好的格式化内容从中提取有效数据,只要用户输入的内容和格式化内容匹配,就能够正确提取。

    十六、输入缓冲区

    • 在讲输入缓冲区之前,我们先来看个例子:
    #include <stdio.h>
    int main()
    {
        int a, b, c, d;
        scanf("%d %d %d %d", &a, &b, &c, &d);
        printf("%d %d %d %d\n", a, b, c, d);
        return 0;
    }
    
    • 接下里我们将围绕这段代码进行展开。
    1 2 3 41 2 3 4
    
    • 以上是我们的期望输入。

    1、少输入

    • 我们尝试少输入1个数,按下回车后,发现程序并没有任何的输出,当我们再次输入下一个数的时候,产生了正确的输出,如下:
    1 2 341 2 3 4
    

    2、多输入

    • 我们尝试多输入1个数,按下回车后,发现输出了前四个我们输入的数,如下:
    1 2 3 4 51 2 3 4
    

    3、再次尝试

    • 我们增加一行代码,就是在输出四个数以后,再调用一次 scanf(),如下:
    #include <stdio.h>
    int main()
    {
        int a, b, c, d, e;
        scanf("%d %d %d %d", &a, &b, &c, &d);
        printf("%d %d %d %d\n", a, b, c, d);
        scanf("%d", &e);
        printf("%d\n", e);
        return 0;
    }
    
    • 然后我们采用上述的一次性输入5个数的方式,如下:
    1 2 3 4 51 2 3 4
    5
    
    • 这时候,我们发现程序正常运行了。
    • 这是因为:我们从键盘输入的数据并没有直接交给scanf(),而是放入了输入缓冲区中,当我们按下回车键,scanf()才到输入缓冲区中读取数据。如果缓冲区中的数据符合 scanf()给定的格式要求,那么就读取结束;否则,继续等待用户输入,或者读取失败。
    • 关于输入缓冲区的内容,比较复杂,属于进阶内容,就不在这个章节继续展开啦。

    【例题1】给定一段代码,如下,并且给出一个输入,请问输出是什么。

    #include <stdio.h>
    int main()
    {
        int a = 9, b = 8, c = 7, d = 6, e = 5;
        scanf("%d %d %d %d", &a, &b, &c, &d);
        printf("%d %d %d %d\n", a, b, c, d);
        scanf("%d", &e);
        printf("%d\n", e);
        return 0;
    }
    
    • 输入如下:
    1 2b 3 4 5

    十七、其他数据类型的格式化输入

    • 其它数据类型,例如浮点数、字符、字符串的格式化参数类似 printf,如下:

    1、字符串的输入

    • 关于字符串,后面在讲完数组以后,还会着重讲,也有很多匹配算法是应用于字符串上的,也是一个很重要的内容,所以这里不作太多介绍,只需要记住,字符串输入时&可以不加,如下:
    #include <stdio.h>
    int main()
    {
    	char str[100]; 
        scanf("%s", str);    // (1)
        printf("%s\n", str);
        
        scanf("%s", &str);   // (2)
        printf("%s\n", str);
        return 0;
    }
    
    • ( 1 ) (1) (1) ( 2 ) (2) (2) 的方式都是可以的,但是我们一般采用 ( 1 ) (1) (1) 的方式;

    2、做个简单的游戏吧

    • 这是一个算命游戏,要求根据输入的姓名,得到这个人的算命信息。
    • 我们先来看看效果:

    在这里插入图片描述

    • 好啦,代码实现如下:
    #include <stdio.h>
    int main()
    {
    	char str[100]; 
    	int height;
    	
    	printf("请大侠输入姓名:");
        scanf("%s", str);
        
        printf("请大侠输入身高(cm):");
    	scanf("%d", &height);
    	
    	printf("%s大侠,身高%dcm,骨骼惊奇,是百年难得一遇的人才,只要好好学习C语言,日后必成大器!\n", str, height);
        return 0;
    }
    
    • 你学废了吗?评论区留下你的算命结果哦 ~~

    十八、何为进制

    • 进制也就是 进位计数制 的简称,是人为定义的带进位的计数方法。
    • 对于任何一种进制 —— X进制,表示每一个数位上的数运算时都是逢 X 进一位。
    • 例如:十进制是逢十进一,十六进制是逢十六进一,二进制就是逢二进一,八进制是逢八进一,以此类推,X进制就是 逢X进一。
    • 如图三-1所示,代表的则是十进制的进位演示:

    图三-1

    十九、常用进制

    1、二进制

    • 我们从定义出发:逢二进一。

    两只鞋子 = 1双鞋子;
    二个抓手 = 1双手;

    2、三进制

    • 同样,什么是逢三进一呢?

    3个月 = 1个季度;

    3、四进制

    • 好了接下来,你能举出四进制的例子吗?

    4个季度 = 1年

    5、十进制

    • 当然,现实生活中遇到的最多的数字都是十进制表示。例如:0、1、2、3、… 、9、10、…

    4、其它进制

    七进制:7天 = 1周;
    十二进制:12瓶啤酒 = 1打;
    二十四进制:24小时 = 1天;
    六十进制:60秒 = 1分钟;60分钟 = 1小时;

    二十、计算机中的进制

    • 在计算机中常用的进制有哪些呢?

    1、二进制

    • C语言中,我们如果想表示一个二进制数,可以用0b作为前缀,然后跟上01组成的数字,我们来看一段代码:
    #include <stdio.h>
    int main() {
    	int a = 0b101;
    	printf("%d\n", a);
    	return 0;
    }
    
    • 这段代码中,输出的结果如下:
    5
    
    • 因为%d代表输出的数是十进制,所以我们需要将二进制转换成十进制以后输出,0b101的数学表示如下: ( 101 ) 2 (101)_2 (101)2
    • 它在十进制下的值为 5。
    • 因为数字比较小,所以我们可以简单列出二进制和十进制的对应关系如下:
    进制
    二进制011011100101
    十进制012345
    • 也就是二进制下101对应于十进制下的 5。

    2、八进制

    • 讲八进制之前,我们还是先来看一段代码:
    #include <stdio.h>
    int main() {
    	int a = 0123;
    	printf("%d\n", a);
    	return 0;
    } 
    

    🙉饭不食,水不饮,题必须刷🙉

    还不会C语言,和我一起打卡!
    🌞《光天化日学C语言》🌞

    LeetCode 太难?上简单题!
    🧡《C语言入门100例》🧡

    LeetCode 太简单?大神盘他!
    🌌《夜深人静写算法》🌌

    • 那么,这段代码的输出值为多少呢?

    83
    
    • 为什么呢?参考二进制的表示法,八进制的表示法是前缀1个0,然后跟上0-7的数字;
    • 换言之,我们需要把 123这个八进制数转换成十进制后再输出。而转换结果就是83,由于这里数字较大,我们已经无法一个一个数出来了,所以需要进行进制转换,关于进制转换,在第四节进制转换初步里再来讲解。

    3、十六进制

    • 同样的,对于十六进制数,表示方式为:以0x或者0X作为前缀,跟上0-9a-fA-F的数字,其中大小写字母的含义相同,分别代表从1015的数字。如下表所示:
    小写字母大写字母代表数字
    aA10
    bB11
    cC12
    dD13
    eE14
    fF15
    • 我们看看这段代码的输出:
    #include <stdio.h>
    int main() {
    	int a = 0X123;
    	printf("%d\n", a);
    	return 0;
    } 
    
    • 对于这段代码,输出的是:
    291
    

    在这里插入图片描述

    二十一、进制转换初步

    1、X进制 转 十进制

    对于 X 进制的数来说,我们定义以下几个概念:
      【概念1】对于数字部分从右往左编号为 0 到 n n n,第 i i i 个数字位表示为 d i d_i di,这个数字就是 d n . . . d 1 d 0 d_{n}...d_1d_0 dn...d1d0
      【概念2】每个数字位有一个权值;
      【概念3】第 i i i 个数字位的权值为 X i X^i Xi

    • 基于以上几个概念, X进制 转 十进制的值为 每一位数字 和 它的权值的乘积的累加和,如下:
    • ∑ i = 0 n X i d i \sum_{i=0}^{n} X^id_i i=0nXidi
    • ∑ \sum 是个求和符号,不必惊慌!
    • 举个例子,对于上文提到的八进制的数0123,转换成十进制,只需要套用公式:
    • ∑ i = 0 n X i d i = ∑ i = 0 2 8 i d i = 8 2 × 1 + 8 1 × 2 + 8 0 × 3 = 64 + 16 + 3 = 83 \begin{aligned}\sum_{i=0}^{n} X^id_i &= \sum_{i=0}^{2} 8^id_i \\ &= 8^2 \times 1 + 8^1 \times 2 + 8^0 \times 3 \\ &= 64 + 16 + 3 \\ &= 83\end{aligned} i=0nXidi=i=028idi=82×1+81×2+80×3=64+16+3=83
    • 再如,上文提到的十六进制数0X123,转换成十进制,套用同样的公式,如下:
    • ∑ i = 0 n X i d i = ∑ i = 0 2 1 6 i d i = 1 6 2 × 1 + 1 6 1 × 2 + 1 6 0 × 3 = 256 + 32 + 3 = 291 \begin{aligned}\sum_{i=0}^{n} X^id_i &= \sum_{i=0}^{2} 16^id_i \\ &= 16^2 \times 1 + 16^1 \times 2 + 16^0 \times 3 \\ &= 256 + 32 + 3 \\ &= 291\end{aligned} i=0nXidi=i=0216idi=162×1+161×2+160×3=256+32+3=291

    2、十进制 转 X进制

    • 对于 十进制 转 X进制 的问题,我们可以这么来考虑:
    • 从 X进制 转 十进制 的原理可知,任何一个十进制数字都是由 X进制 的幂的倍数累加而成。所以,一个数一定有 X 0 X^0 X0 这部分,而这部分,可以通过原数除上 X X X 的余数得到。然后我们把原数除上 X X X 后得到的数,肯定又有 X 0 X^0 X0 的部分,就这样重复的试除,直到得到的商为 零 时结束,过程中的余数,逆序一下就是对应进制的数了。
    • 还是一上文的例子来说,对于291我们可以通过如下方式,转换成 十六进制。
    29116  ==========3
    1816  ===========2
    116  ============1
    
    • 而对于十进制的83,我们可以通过如下方式,转换成 八进制。
    838 ============3
    108 ============2
    18 =============1
    
    • 那么,等我们后面学习了循环语句以后,就可以教大家如何用计算机来实现进制转换了,目前阶段只需要了解下进制转换的基本原理即可。

    二十二、ASCII 码简介

    1、ASCII 码的定义

    • ASCII 码(即 American Standard Code for Information Interchange),翻译过来是美国信息交换标准代码。
    • 我们一般念成 ask 2 马。

    在这里插入图片描述

    2、ASCII 码的起源

    • 它是一套编码系统。
    • 由于计算机用 高电平 和 低电平 分别表示 1 和 0,所以,在计算机中所有的数据在存储和运算时都要使用二进制数表示,例如,像a-zA-Z这样的52个字母以及0-9的数字还有一些常用的符号(例如?*#@!@#$%^&*()等)在计算机中存储时也要使用二进制数来表示,具体用哪些二进制数字表示哪个符号,每个人都可以约定自己的一套规则,这就叫编码。
    • 即 一个数字 和 一个字符 的一一映射。
    • 为了通信而不造成混淆,所以需要所有人都使用相同的规则。

    3、ASCII 码的表示方式

    • 标准ASCII 码,使用 7 位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字09、标点符号,以及在英语中使用的特殊控制字符。
    • 简单的就可以认为:一个数字对应一个字符。具体如下表所示:

    二十三、ASCII 码的输出

    • ASCII 码对应的字符用单引号括起来,并且是可以按照两种方式来输出的,分别为:字符形式 和 整数形式。

    1、字符

    • 当成字符用的时候,用格式化输出%c来控制,如下:
    #include <stdio.h>
    int main() {
    	printf("%c\n", '0');
    	printf("%c\n", 'A');
    	printf("%c\n", 'a');
    	printf("%c\n", '$');
    	return 0;
    } 
    
    • 得到的输出结果如下:
    0
    A
    a
    $
    

    2、整数

    • 当成整数用的时候,用格式化输出%d来控制,如下:
    #include <stdio.h>
    int main() {
    	printf("%d\n", '0');
    	printf("%d\n", 'A');
    	printf("%d\n", 'a');
    	printf("%d\n", '$');
    	return 0;
    } 
    
    • 得到的输出结果如下:
    48
    65
    97
    36
    
    • 这是因为一个字符代表的是一个整数到符号的映射,它本质上还是一个整数,所以我们可以用整数的形式来输出。字符'0'的整数编码为48,字符'1'的整数编码为49,以此类推。

    二十四、ASCII 码的运算

    • 既然当成了整数,那么就可以进行简单的四则运算了。
    • 我们简单来看下下面这段代码:
    #include <stdio.h>
    int main() {
    	printf("%c\n", '0' + 5);
    	printf("%c\n", 'A' + 3);
    	printf("%c\n", 'a' + 5);
    	printf("%c\n", '$' + 1);
    	return 0;
    } 
    
    • 它的输出如下:
    5
    D
    f
    %
    
    • 字符加上一个数字,我们可以认为是对字符编码进行了一个对应数字的偏移,字符'0'向右偏移 5 个单位,就是字符'5';同样的,'A'向右偏移3个单位,就是字符'D'
    • 有加法当然也有减法,接下来让我们看个例题。

    【例题1】给出如下代码,给出它的输出结果。

    #include <stdio.h>
    int main() {
    	printf("%c\n", 'A' - 10);
    	return 0;
    }
    
    • 建议先想想,然后再敲代码看看结果,是否和你想的一致。

    二十五、ASCII 码的比较

    • ASCII 码既然可以和整数无缝切换,那么自然也可以进行比较了。
    • 通过上一节,我们了解到了 '0'加上1以后等于'1',那么顺理成章可以得出:'0' < '1'
    • 同样可以知道:'a' < 'b''X' < 'Y'
    • 那么,我们再来看个问题。

    【例题2】请问 'a' < 'A'还是 'a' > 'A'


    二十六、常量简介

    • C语言中的常量,主要分为以下几种类型:

    二十七、数值常量

    • 数值常量分为整数和浮点数,整数一般称为整型常量,浮点数则称为实型常量。

    1、整型常量

    • 整型常量分为二进制、八进制、十进制 和 十六进制。
    • 每个整型常量分为三部分:前缀部分、数字部分、后缀部分。
    • 如下表所示:
    • 关于前缀这部分,在 光天化日学C语言(06)- 进制转换入门 已经讲到过,就不再累述了。
    • 这里着重提一下后缀,uunsigned)代表无符号整数,llong)代表长整型,ll代表long long

    • 换言之,无符号整型就是非负整数。
    • 待时机成熟,我会对整数的存储结构进行一个非常详细的介绍。

    【例题1】说出以下整型常量中,哪些是非法的,为什么非法。

    1314
    520u
    0xFoooooL
    0XFeeeul
    018888
    0987UU
    0520
    0x4b
    1024llul
    30ll
    030ul
    

    2、实型常量

    • 实型常量又分为 小数形式 和 指数形式。

    1)小数形式

    • 小数形式由三部分组成:整数部分、小数点、小数部分。例如:
    	3.1415927
    	4.5f
    	.1314
    
    • 其中 f后缀代表 float,用于区分double
    • .1314等价于0.1314

    2)指数形式

    • 指数形式的典型格式为xey,如下:
    	1e9
    	5.2e000000
    	5.2e-1
    	1.1e2
    
    • 它表示的数值是:
    • x × 1 0 y x \times 10^{y} x×10y
    • 其中 y y y 代表的是数字10的指数部分,所以是支持负数的。

    二十八、字符常量

    • 字符常量可以是一个普通的字符、一个转义序列,或一个通用的字符。
    • 每个字符都对应一个 ASCII 码值。

    1)普通字符

    • 普通字符就是用单引号括引起来的单个字符,如下:
    	'a'
    	'Q'
    	'8'
    	'?'
    	'+'
    	' '
    

    2)转义字符

    • 转义字符是用引号引起来,并且内容为 斜杠 + 字符,例如我们之前遇到的用 '\n'代表换行,\t代表水平制表符(可理解为键盘上的 tab 键),'\\'代表一个反斜杠,等等;
    • 当然还可以用 '\ooo'来代替一个字符,其中一个数字o代表一个八进制数;也可以用 '\xhh'来代表一个字符,具体见如下代码:
    #include <stdio.h>
    int main() {
        char a = 65;
        char b = '\101';
        char c = '\x41';
        printf("%c %c %c\n", a, b, c);
        return 0;
    } 
    

    • 以上的代码输出结果为:
    A A A
    
    • 这是因为八进制下的101和十六进制的41在十进制下都是65,代表的是大写字母'A'的ASCII 码值。

    【例题1】请问如何输出一个单引号?

    二十九、字符串常量

    • 字符串常量,又称为字符串字面值,是括在双引号""中的。一个字符串包含类似于字符常量的字符:普通字符、转义序列。

    1、单个字符串常量

    #include <stdio.h>
    int main() {
        printf( "光天化日学\x43语言!\n" );
        return 0;
    } 
    
    • 我们可以用转义的'\x43'代表'C'和其它字符组合,变成一个字符串常量。以上代码输出为:
    光天化日学C语言!
    

    【例题2】如果我想要如下输出结果,请问,代码要怎么写?

    "光天化日学C语言!"
    

    2、字符串常量分行

    • 两个用""引起来的字符串,是可以无缝连接的,如下代码:
    #include <stdio.h>
    int main() {
    	printf( 
    		"光天化日学" 
    		"C语言!\n" 
    	);
    	return 0;
    } 
    
    • 这段代码的结果也是:
    光天化日学C语言!
    

    三十、符号常量

    1、#define

    • 利用 #define预处理器可以定义一个常量如下:
    #include <stdio.h>
    #define TIPS "光天化日学\x43语言!\n" 
    #define love 1314
    int main() {
        printf( TIPS );
        printf("%d\n", love);
        return 0;
    } 
    
    • 以上这段代码,会将所有TIPS都原文替换为"光天化日学\x43语言!\n";将所有love替换为1314

    2、const

    • const的用法也非常广泛,而且涉及到很多概念,这里只介绍最简单的用法,后面会开辟一个新的章节专门来讲它的用法。
    #include <stdio.h> 
    const int love = 1314;
    int main() {
    	printf( "%d\n", love );
    	return 0;
    }
    
    • 我们可以在普通变量定义前加上const,这样就代表它是个常量了,在整个运行过程中都不能被修改。

    【例题3】下面这段代码会发生什么情况,自己编程试一下吧。

    #include <stdio.h> 
    const int love = 1314;
    int main() {
        love = 520;
        printf( "%d\n", love );
        return 0;
    }
    

    三十一、算术运算符

    • 算术运算符主要包含以下几个:
    • 1)四则运算符,也就是数学上所说的加减乘除;
    • 2)取余符号;
    • 3)自增和自减。
    • 那么接下来让我们一个一个来看看吧。

    1、四则运算符

    • 数学上的加减乘除和C语言的加减乘除的含义类似,但是符号表示方法不尽相同,对比如下:
    /加法减法乘法除法
    数学+- × \times ×÷
    C语言+-*/

    1)加法

    • a + b代表两个操作数相加,代码如下:
    #include <stdio.h>
    int main() {
        int a = 1, b = 2;
        double c = 1.005, d = 1.995;
        printf("a + b = %d\n", a + b );
        printf("c + d = %.3lf\n", c + d);
        printf("a + c = %.3lf\n", a + c);
        return 0;
    }
    
    
    • 这段代码的输出为:
    a + b = 3
    c + d = 3.000
    a + c = 2.005
    

    2)减法

    • a - b代表从第一个操作数中减去第二个操作数,代码如下:
    #include <stdio.h>
    int main() {
        int a = 1, b = 2;
        double c = 1.005, d = 1.995;
        printf("a - b = %d\n", a - b );
        printf("c - d = %.3lf\n", c - d);
        printf("a - c = %.3lf\n", a - c);
        return 0;
    }
    
    • 这段代码的输出为:
    a - b = -1
    c - d = -0.990
    a - c = -0.005
    

    3)乘法

    • a * b代表两个操作数相乘,代码如下:
    #include <stdio.h>
    int main() {
        int a = 1, b = 2;
        double c = 1.005, d = 1.995;
        printf("a * b = %d\n",    a * b);
        printf("c * d = %.3lf\n", c * d);
        printf("a * c = %.3lf\n", a * c);
        return 0;
    }
    
    
    • 这段代码的输出为:
    a * b = 2
    c * d = 2.005
    a * c = 1.005
    

    4)除法

    不同类型的除数和被除数会导致不同类型的运算结果。
      1)当 除数 和 被除数 都是整数时,运算结果也是整数;
        1.a)如果能整除,结果就是它们相除的商;
        1.b)如果不能整除,那么就直接丢掉小数部分,只保留整数部分,即数学上的 取下整
      2)除数和被除数中有一个是小数,那么运算结果也是小数,并且是 double 类型的小数。

    • 我们来看一段代码:
    #include <stdio.h>
    int main() {
        int a = 6, b = 3, c = 4;
        double d = 4;
        printf("a / b = %d\n", a / b );
        printf("a / c = %d\n", a / c);
        printf("a / d = %.3lf\n", a / d);
        return 0;
    }
    
    • 输出结果如下:
    a / b = 2
    a / c = 1
    a / d = 1.500
    
    • a能被整除b,所以第一行输出它们的商,即 2
    • a不能被整除c,所以第二行输出它们相除的下整,即 1
    • ad中,d为浮点数,所以相除得到的也是浮点数;

    #include <stdio.h>
    int main() {
        int a = 5, b = 0;
        int c = a / b;
        return 0;
    }
    
    • 这里会触发一个异常,即 除零错。这种情况在 C语言中是不允许的,但是由于变量的值只有在运行时才会确定,编译器是没办法帮你把这个错误找出来的,平时写代码的时候一定要注意。

    2、取余符号

    • 取余,也就是求余数,使用的运算符是%。C语言中的取余运算只能针对整数,也就是说,%两边都必须是整数,不能出现小数,否则会出现编译错误。
    • 例如:5 % 3 = 27 % 2 = 1

    当然,余数可以是正数也可以是负数,由%左边的整数决定:
      1)如果%左边是正数,那么余数也是正数;
      2)如果%左边是负数,那么余数也是负数。

    • 我们继续来看一段代码:
    #include <stdio.h>
    int main()
    {
        printf(
            "9%%4=%d\n"
            "9%%-4=%d\n"
            "-9%%4=%d\n"
            "-9%%-4=%d\n",
            9%4, 
            9%-4, 
            -9%4, 
            -9%-4
        );
        return 0;
    }
    
    
    • 光天化日学C语言(08)- 常量 这一章中,我们提到的两个用""引起来的字符串是可以无缝连接的,所以这段代码里面四个字符串相当于一个。而%printf中是用来做格式化的,所以想要输出到屏幕上,需要用%%。于是,我们得到输出结果如下:
    9%4=1
    9%-4=1
    -9%4=-1
    -9%-4=-1
    
    • 印证了最后的符号是跟着左边的数走的。

    3、自增和自减

    • 自增和自减的情况类似,所以我们只介绍自增即可。

    	x = x + 1;
    
    • 我们也可以写成:
    	x++;
    
    • 当然,也可以写成:
    	++x;
    
    • 这两者的区别是什么呢?我们来看一段代码:
    #include <stdio.h>
    int main()
    {
        int x = 1;
        printf( "x = %d\n", x++ );
        printf( "x = %d\n", x );
        return 0;
    }
    
    • 输出结果是:
    x = 1
    x = 2
    
    • 这是因为x在自增前,就已经把值返回了,所以输出的是原值。我们再来看另一种情况:
    #include <stdio.h>
    int main()
    {
        int x = 1;
        printf( "x = %d\n", ++x );
        printf( "x = %d\n", x );
        return 0;
    }
    
    • 输出结果是:
    x = 2
    x = 2
    
    • 这是因为x先进行了自增,再把值返回,所以输出的是自增后的值。
    • 当然,自减也是同样的道理,大家可以自己写代码实践一下。
    展开全文
  • C语言

    2010-06-26 11:01:00
    因此,它的应用范围广泛,不仅仅是软件开发上,而且各类科研都需要用到C语言,具体应用比如单片机以及嵌入式系统开发。 [编辑本段]简介  C语言是Combined Language(组合语言)的中英混合简称。这句话头一次听说...
  • 初识c语言

    多人点赞 热门讨论 2021-05-27 06:57:34
    基本了解C语言的基础知识,对C语言有一个大概的认识。 本章重点: ·``什么是C语言 ·第一个C语言程序 ·数据类型 ·变量、常量 ·字符串+转义字符+注释 ·选择语句 ·循环语句 ·函数 ·数组 ·操作符 ·常见...
  • const int secret_code = 13; int code_entered; scanf("%d",&code_entered); while(code_entered !=secret_code) { scanf("%d",&code_entered); } printf("Congratulations!you are cured!\n");...
  • 在C语言中利用PCRE实现正则表达式

    千次阅读 2015-11-10 18:49:40
    在C语言中利用PCRE实现正则表达式 http://dev.jizhiinfo.net/?post=49(转载地址)   1. PCRE简介 2. 正则表达式定义 3. PCRE正则表达式的定义 4. PCRE的函数简介 5. 使用PCRE在C语言中实现正则表达式的...
  • C语言在程序中内存

    千次阅读 2015-10-15 16:03:43
    一、C语言程序的存储区域 C语言编写的程序经过编绎-链接...在C语言中,程序语句进行编译后,形成机器代码。执行程序的过程中,CPU的程序计数器指向代码段的每一条代码,并由处理器依次运行。 只读数据段(RO da
  • C语言中的ASSERT(断言)宏是嵌入式软件开发人员可以使用的最好的调试工具之一。虽然ASSERT功能强大,但我很少看到它被实施,并且一些使用它的案例中,它的实施要么是有瑕疵的要么是不正确的。以下一些技巧将不仅...
  • C语言经典面试题 与 C语言面试宝典

    万次阅读 多人点赞 2017-12-22 16:15:26
    函数外定义的静态变量——静态全局变量,该变量的作用域只能定义该变量的文件中,不能被其他文件通过 extern 引用。 2  内部链接属性   静态函数只能声明它的源文件中使用。 问题 2 : ...
  • 编者按:想想也学了将近2...今天阅读ARMLPC2103的例程的时候发现还有这么个东西也是C语言的关键.不相信,查了下书,发现真的有.才知道自己的孤陋寡闻,也明了了C语言的博大精深.所以网上找了一篇这样说明volatile用法
  • C语言实现2048

    万次阅读 多人点赞 2014-11-20 22:06:19
    本题目来自于一位去澳洲参加暑期交流的小朋友,为期一个多月的编程课程最后的Assignment就是用C语言在命令行中实现2048.具体要求:http://webapps.cse.unsw.edu.au/webcms2/course/index.php?cid=2360 请仔细看完...
  • C语言基础

    千次阅读 2016-04-10 18:20:20
    C 语言基础Object-C 语言是C语言的超集,意思就是我们可以将两种语言写同一个源代码文件中。Object-C语言结构的核心是建立在C语言之上的。所以学习语言的高级特性之前,掌握C语言基础是很重要的。这个模块为...
  • C语言字符

    千次阅读 2020-03-03 18:24:38
    实际开发中,程序员很少用单个字符来表示数据,字符的意义主要作为数组的时候可以形成一个字符串。本章节中,我们重点介绍字符与整数之间的关系和ASCII码的相关知识。 字符类型char,只能用单引号’ ‘来包围,...
  • 21: Conversion may lose significant digits — 转换时会丢失意义的数字 22: Conversion of near pointer not allowed — 不允许转换近指针 23: Could not find file xxx — 找不到XXX文件 24: Declaration ...
  • 定义变量的时候可以加上"属性"关键字 "属性"关键字指明变量的特殊意义 语法: property type var_name; 示例: void code() { auto char i; register int j; static long k; extern ...
  • VSCode配置C语言环境

    千次阅读 多人点赞 2021-04-02 17:52:21
    其中的很多功能可以大大地提高我们的学习与工作效率,现在本文介绍如何VSCode上配置C语言环境。 1.下载安装 VSCode下载官网:https://code.visualstudio.com/ 下载完成之后安装默认位置或者自定义位置 下面全部...
  • C语言是国际上广泛流行的高级语言,是B语言的基础上发展起来的, 1970年, 美国贝尔实验室的D.M.Ritchie设计了B 语言, 并用于编写了第一个UNIX操作系统。 1973年, D.M.Ritchie B语言的基础上设计出了C语言, 对...
  • C语言中register类型的变量有什么意义

    千次阅读 多人点赞 2017-03-21 08:37:08
    一般情况下,变量的值是存储内存中的,CPU 每次使用数据都要从内存中读取。如果有一些变量使用非常频繁,从内存中读取就会消耗很多时间,例如 for 循环中的增量控制: int i; for(i=0; i; i++){ // Some Code ...
  • 最近Android下jni 使用libusb,libusb返回值表示的意义,一直不知道是什么,特整理如下: /** Success (no error) */ LIBUSB_SUCCESS = 0, /** Input/output error */ LIBUSB_ERROR_IO = -1, /** Invalid ...
  • C语言关键字

    千次阅读 2016-04-19 11:14:44
    auto :声明自动变量 一般...作用:C程序是面向过程的,C代码中会出现大量的函数模块,每个函数都有其生命周期(也称作用域),函数生命周期中声明的变量通常叫做局部变量,也叫自动变量。例如: int fun(){ int
  • Clean-Code: 有意义的名字 最近看Clean-Code(代码整洁之道) 里面有些内容还是不错的,所以给大家分享下。   名副其实: 意思是说名称和意思要一致,更精确的说应该是看到名字就能想到意思。 对...
  • C语言内存学习

    2016-10-08 17:22:59
    C语言编程程序的内存如何布局  C语言程序连接过程中的特性和常见错误  C语言程序的运行方式  一:C语言程序的存储区域  由C语言代码(文本文件)形成可执行程序(二进制文件),需要经过编译-汇编-连接三个阶段。...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 27,702
精华内容 11,080
关键字:

code在c语言的意义

c语言 订阅