精华内容
下载资源
问答
  • 华为C语言编程规范(精华总结)

    万次阅读 多人点赞 2020-03-24 09:48:55
    物理层,无论使用什么样的目录或者名字空间等,需要把函数用一种标准的方法组织起来。例如:设计良好的目录结构、函数名字、文件组织等,这样可以方便查找。 1、一个函数仅完成一件功能 一个函数实现多个功能给开发...

    目录

    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余页。

    展开全文
  • storm基本使用心得精华

    千次阅读 2017-01-09 19:08:15
    storm基本使用与zookeepr的使用,由于Storm中Nimbus和Supervisor是无状态的,Nimbus会把topology写到到ZK当中,Supervisor会到ZK去读这些信息,实现了解耦 ;Storm如何确保消息的靠性

    1.利用mvn打好jar 提交到集群上

    storm jar stormTopoploy.jar [主函数名] [参数名]

    2.查看集群上运行的Topology

    storm list

    Topology_name        Status     Num_tasks  Num_workers  Uptime_secs

    -------------------------------------------------------------------

    wdTopology           ACTIVE     29         3            1157  


    3 kill 运行的Topology

    storm kill [Topology名字]

    2542 [main] INFO  b.s.u.StormBoundedExponentialBackoffRetry - The baseSleepTimeMs [2000] the maxSleepTimeMs [60000] the maxRetries [5]

    2599 [main] INFO  b.s.c.kill-topology - Killed topology: wdTopology


    二 本地运行Storm

    3226 [main] INFO  b.s.u.Utils - Using defaults.yaml from resources //默认读取本地的storm.yaml
    4753 [main] INFO  o.a.s.s.o.a.z.ZooKeeper - Client environment:zookeeper.version=3.4.6-1569965, built on 02/20/2014 09:09 GMT
    4753 [main] INFO  o.a.s.s.o.a.z.ZooKeeper - Client environment:host.name=172.18.59.82
    4753 [main] INFO  o.a.s.s.o.a.z.ZooKeeper - Client environment:java.version=1.8.0_101
    4753 [main] INFO  o.a.s.s.o.a.z.ZooKeeper - Client environment:java.vendor=Oracle Corporation
    4753 [main] INFO  o.a.s.s.o.a.z.ZooKeeper - Client environment:java.home=/Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home/jre


    //Storm 读取 zookeeper的信息

    Map<String, Object> cf = backtype.storm.utils.Utils.readStormConfig();

    String zkServers = cf.get(Config.STORM_ZOOKEEPER_SERVERS).toString();

    String zkPort = cf.get(Config.STORM_ZOOKEEPER_PORT).toString();
    System.out.println(zkServers+"----"+zkPort);
     
    
    //public static final String STORM_ZOOKEEPER_SERVERS = "storm.zookeeper.servers";
    //public static final String STORM_ZOOKEEPER_PORT = "storm.zookeeper.port";


    由于Storm中Nimbus和Supervisor是无状态的,Nimbus会把topology写到到ZK当中,Supervisor会到ZK去读这些信息,实现了解耦

    如下zookeeper存放storm的一些信息
    /-{storm-zk-root}           -- storm在zookeeper上的根
            |                            目录
            |
            |-/assignments            -- topology的任务分配信息
            |   |
            |   |-/{topology-id}      -- 这个下面保存的是每个
            |                            topology的assignments
            |                            信息包括: 对应的
            |                            nimbus上的代码目录,所有
            |                            task的启动时间,
            |                            每个task与机器、端口的映射
            |
            |-/tasks                  -- 所有的task
            |   |
            |   |-/{topology-id}      -- 这个目录下面id为
            |       |                    {topology-id}的topology
            |       |                    所对应的所有的task-id
            |       |
            |       |-/{task-id}      -- 这个文件里面保存的是这个
            |                            task对应的component-id:
            |                            可能是spout-id或者bolt-id
            |
            |-/storms                 -- 这个目录保存所有正在运行
            |   |                        的topology的id
            |   |
            |   |-/{topology-id}      -- 这个文件保存这个topology
            |                            的一些信息,包括topology的
            |                            名字,topology开始运行的时
            |                            间以及这个topology的状态
            |                            (具体看StormBase类)
            |
            |-/supervisors            -- 这个目录保存所有的supervisor
            |   |                        的心跳信息
            |   |
            |   |-/{supervisor-id}    -- 这个文件保存的是supervisor
            |                            的心跳信息包括:心跳时间,主
            |                            机名,这个supervisor上worker
            |                            的端口号运行时间
            |                            (具体看SupervisorInfo类)
            |
            |-/taskbeats              -- 所有task的心跳
            |   |
            |   |-/{topology-id}      -- 这个目录保存这个topology的所
            |       |                    有的task的心跳信息
            |       |
            |       |-/{task-id}      -- task的心跳信息,包括心跳的时
            |                            间,task运行时间以及一些统计
            |                            信息
            |
            |-/taskerrors             -- 所有task所产生的error信息
            |
            |-/{topology-id}      -- 这个目录保存这个topology下面
            |                    每个task的出错信息
            |
            |-/{task-id}      -- 这个task的出错信息


    Storm 问题


     ClassNotFoundException: kafka.api.OffsetRequest ,要注意strom-kafka是使用的kafka的低级api,因此也要引用kafka的包 导入就不报错了


    这个博客讲Storm觉得不错

    http://blog.csdn.net/tanggao1314/article/category/6326204


    Storm如何保证消息的完整性:

    Ack原理
      Storm中有个特殊的task名叫acker,他们负责跟踪spout发出的每一个Tuple的Tuple树(因为一个tuple通过spout发出了,经过每一个bolt处理后,会生成一个新的tuple发送出去)。当acker(框架自启动的task)发现一个Tuple树已经处理完成了,它会发送一个消息给产生这个Tuple的那个task。
    Acker的跟踪算法是Storm的主要突破之一,对任意大的一个Tuple树,它只需要恒定的20字节就可以进行跟踪。
    Acker跟踪算法的原理:acker对于每个spout-tuple保存一个ack-val的校验值,它的初始值是0,然后每发射一个Tuple或Ack一个Tuple时,这个Tuple的id就要跟这个校验值异或一下(两个操作数的位中,相同则结果为0,不同则结果为1),并且把得到的值更新为ack-val的新值。那么假设每个发射出去的Tuple都被ack了,那么最后ack-val的值就一定是0。Acker就根据ack-val是否为0来判断是否完全处理,如果为0则认为已完全处理

    要实现ack机制:
    1,spout发射tuple的时候指定messageId
    2,spout要重写BaseRichSpout的fail和ack方法
    3,spout对发射的tuple进行缓存(否则spout的fail方法收到acker发来的messsageId,spout也无法获取到发送失败的数据进行重发),看看系统提供的接口,只有msgId这个参数,这里的设计不合理,其实在系统里是有cache整个msg的,只给用户一个messageid,用户如何取得原来的msg貌似需要自己cache,然后用这个msgId去查询,太坑爹了
    3,spout根据messageId对于ack的tuple则从缓存队列中删除,对于fail的tuple可以选择重发。
    4,设置acker数至少大于0;Config.setNumAckers(conf, ackerParal);

    Storm的Bolt有BsicBolt和RichBolt:
      在BasicBolt中,BasicOutputCollector在emit数据的时候,会自动和输入的tuple相关联,而在execute方法结束的时候那个输入tuple会被自动ack。
      使用RichBolt需要在emit数据的时候,显示指定该数据的源tuple要加上第二个参数anchor tuple,以保持tracker链路,即collector.emit(oldTuple, newTuple);并且需要在execute执行成功后调用OutputCollector.ack(tuple), 当失败处理时,执行OutputCollector.fail(tuple);



    展开全文
  • 堆和栈的精华大总结

    万次阅读 多人点赞 2019-11-18 18:37:41
    并把这个字符串的地址赋给了str,就是因为String的”不可变”产生了很多临时变量,这也就是为什么建议用StringBuffer的原 因了,因为StringBuffer是可改变的。 下面是一些String相关的常见问题: String 中的...

    Java内存分配原理

    栈、堆、常量池虽同属Java内存分配时操作的区域,但其适用范围和功用却大不相同。

    一般Java在内存分配时会涉及到以下区域:

    ◆寄存器:我们在程序中无法控制

    ◆栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中

    ◆堆:存放用new产生的数据

    ◆静态域:存放在对象中用static定义的静态成员

    ◆常量池:存放常量

    ◆非RAM存储:硬盘等永久存储空间

    Java内存分配中的栈

    在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。
      
    当在一段代码块定义一个变量时,Java就在栈中 为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

    Java内存分配中的堆

    堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

    在堆中产生了一个数组或对象后,还可以 在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。  引用变量就相当于是 为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。引用变量就相当于是为数组或者对象起的一个名称。

    引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序 运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍 然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是 Java 比较占内存的原因。

    实际上,栈中的变量指向堆内存中的变量,这就是Java中的指针!


     
    常量池 (constant pool)

    常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:

    ◆类和接口的全限定名;

    ◆字段的名称和描述符;

    ◆方法和名称和描述符。

    虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和 floating point常量)和对其他类型,字段和方法的符号引用。

    对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的, 对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引 用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。
    在程序执行的时候,常量池 会储存在Method Area,而不是堆中。

    堆与栈

    Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、 anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存 大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态 分配内存,存取速度较慢。

    栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是 确定的,缺乏灵活性。栈中主要存放一些基本类型的变量数据(int, short, long, byte, float, double, boolean, char)和对象句柄(引用)。

    栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:

    1. int a = 3;   
    2. int b = 3;  

    编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。

    这时,如果再令 a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响 到b的值。

    要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。

    String是一个特殊的包装类数据。可以用:

    1. String str = new String("abc");   
    2. String str = "abc";  

    两种的形式来创建,第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对象。而第二种是先在栈中创建一个对String类的对象引用变量str,然后通过符号引用去字符串常量池 里找有没有"abc",如果没有,则将"abc"存放进字符串常量池 ,并令str指向”abc”,如果已经有”abc” 则直接令str指向“abc”。

    比较类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==,下面用例子说明上面的理论。

    String str1 = "abc";   
    String str2 = "abc";   
    System.out.println(str1==str2); //true  

    可以看出str1和str2是指向同一个对象的。

    String str1 =new String ("abc");   
    
    String str2 =new String ("abc");   
    
    System.out.println(str1==str2); // false  

    用new的方式是生成不同的对象。每一次生成一个。

    因此用第二种方式创建多个”abc”字符串,在内存中 其实只存在一个对象而已. 这种写法有利与节省内存空间. 同时它可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。

    另 一方面, 要注意: 我们在使用诸如String str = "abc";的格式定义类时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的 对象。只有通过new()方法才能保证每次都创建一个新的对象。
     
    由于String类的immutable性质,当String变量需要经常变换 其值时,应该考虑使用StringBuffer类,以提高程序效率。
     
    1. 首先String不属于8种基本数据类型,String是一个对象。因为对象的默认值是null,所以String的默认值也是null;但它又是一种特殊的对象,有其它对象没有的一些特性。

    2. new String()和new String(”")都是申明一个新的空字符串,是空串不是null;

    3. String str=”kvill”;String str=new String (”kvill”)的区别

    示例:

    String s0="kvill";   
    
    String s1="kvill";   
    
    String s2="kv" + "ill";   
    
    System.out.println( s0==s1 );   
    
    System.out.println( s0==s2 );  

    结果为:

    true 
    true

    首先,我们要知结果为道Java 会确保一个字符串常量只有一个拷贝。

    因为例子中的 s0和s1中的”kvill”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而”kv”和”ill”也都是字符串常量,当一个字 符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中” kvill”的一个引用。所以我们得出s0==s1==s2;用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。

    示例:

    String s0="kvill";   
    
    String s1=new String("kvill");   
    
    String s2="kv" + new String("ill");   
    
    System.out.println( s0==s1 );   
    
    System.out.println( s0==s2 );   
    
    System.out.println( s1==s2 );  

    结果为:

    false 
    false 
    false

    例2中s0还是常量池 中"kvill”的应用,s1因为无法在编译期确定,所以是运行时创建的新对象”kvill”的引用,s2因为有后半部分 new String(”ill”)所以也无法在编译期确定,所以也是一个新创建对象”kvill”的应用;明白了这些也就知道为何得出此结果了。

    4. String.intern()

    再补充介绍一点:存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的 intern()方法就是扩充常量池的 一个方法;当一个String实例str调用intern()方法时,Java 查找常量池中 是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常 量池中增加一个Unicode等于str的字符串并返回它的引用;看示例就清楚了

    示例:

    String s0= "kvill";   
    
    String s1=new String("kvill");   
    
    String s2=new String("kvill");   
    
    System.out.println( s0==s1 );   
    
    System.out.println( "**********" );   
    
    s1.intern();   
    
    s2=s2.intern(); //把常量池中"kvill"的引用赋给s2   
    
    System.out.println( s0==s1);   
    
    System.out.println( s0==s1.intern() );   
    
    System.out.println( s0==s2 );  

    结果为:

    false 
    false //虽然执行了s1.intern(),但它的返回值没有赋给s1 
    true //说明s1.intern()返回的是常量池中"kvill"的引用 
    true

    最后我再破除一个错误的理解:有人说,“使用 String.intern() 方法则可以将一个 String 类的保存到一个全局 String 表中 ,如果具有相同值的 Unicode 字符串已经在这个表中,那么该方法返回表中已有字符串的地址,如果在表中没有相同值的字符串,则将自己的地址注册到表中”如果我把他说的这个全局的 String 表理解为常量池的话,他的最后一句话,”如果在表中没有相同值的字符串,则将自己的地址注册到表中”是错的:

    示例:

    String s1=new String("kvill");   
    
    String s2=s1.intern();   
    
    System.out.println( s1==s1.intern() );   
    
    System.out.println( s1+" "+s2 );   
    
    System.out.println( s2==s1.intern() );  

    结果:

    false 
    kvill kvill 
    true

    在这个类中我们没有声名一个”kvill”常量,所以常量池中一开始是没有”kvill”的,当我们调用s1.intern()后就在常量池中新添加了一 个”kvill”常量,原来的不在常量池中的”kvill”仍然存在,也就不是“将自己的地址注册到常量池中”了。

    s1==s1.intern() 为false说明原来的”kvill”仍然存在;s2现在为常量池中”kvill”的地址,所以有s2==s1.intern()为true。

    5. 关于equals()和==:

    这个对于String简单来说就是比较两字符串的Unicode序列是否相当,如果相等返回true;而==是 比较两字符串的地址是否相同,也就是是否是同一个字符串的引用。

    6. 关于String是不可变的

    这一说又要说很多,大家只 要知道String的实例一旦生成就不会再改变了,比如说:String str=”kv”+”ill”+” “+”ans”; 就是有4个字符串常量,首先”kv”和”ill”生成了”kvill”存在内存中,然后”kvill”又和” ” 生成 “kvill “存在内存中,最后又和生成了”kvill ans”;并把这个字符串的地址赋给了str,就是因为String的”不可变”产生了很多临时变量,这也就是为什么建议用StringBuffer的原 因了,因为StringBuffer是可改变的。

    下面是一些String相关的常见问题:

    String中的final用法和理解

    final StringBuffer a = new StringBuffer("111");
    final StringBuffer b = new StringBuffer("222");
    a=b;//此句编译不通过
    final StringBuffer a = new StringBuffer("111");
    a.append("222");// 编译通过

    可见,final只对引用的"值"(即内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象 的变化,final是不负责的。

    String常量池问题的几个例子

    下面是几个常见例子的比较分析和理解:

    String a = "a1";   
    
    String b = "a" + 1;   
    
    System.out.println((a == b)); //result = true  
    
    String a = "atrue";   
    
    String b = "a" + "true";   
    
    System.out.println((a == b)); //result = true  
    
    String a = "a3.4";   
    
    String b = "a" + 3.4;   
    
    System.out.println((a == b)); //result = true 

    分析:JVM对于字符串常量的"+"号连接,将程序编译期,JVM就将常量字符串的"+"连接优化为连接后的值,拿"a" + 1来说,经编译器优化后在class中就已经是a1。在编译期其字符串常量的值就确定下来,故上面程序最终的结果都为true。

    String a = "ab";   
    
    String bb = "b";   
    
    String b = "a" + bb;   
    
    System.out.println((a == b)); //result = false 

    分析:JVM对于字符串引用,由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a" + bb无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给b。所以上面程序的结果也就为false。

    String a = "ab";   
    
    final String bb = "b";   
    
    String b = "a" + bb;   
    
    System.out.println((a == b)); //result = true 

    分析:和[3]中唯一不同的是bb字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量 池中或嵌入到它的字节码流中。所以此时的"a" + bb和"a" + "b"效果是一样的。故上面程序的结果为true。

    String a = "ab";   

    final String bb = getBB();   

    String b = "a" + bb;   

    System.out.println((a == b)); //result = false   

    private static String getBB() {  return "b";   } 

    分析:JVM对于字符串引用bb,它的值在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和"a"来动态连接并分配地址为b,故上面 程序的结果为false。

    通过上面4个例子可以得出得知:String  s  =  "a" + "b" + "c"; 就等价于String s = "abc";  

    String  a  =  "a";   
    String  b  =  "b";   
    String  c  =  "c";   
    String  s  =   a  +  b  +  c; 

    这个就不一样了,最终结果等于: 
     

    StringBuffer temp = new StringBuffer();     

    temp.append(a).append(b).append(c);     

    String s = temp.toString(); 

    由上面的分析结果,可就不难推断出String 采用连接运算符(+)效率低下原因分析,形如这样的代码:

    public class Test {  
        public static void main(String args[]) {  
            String s = null;  
            for(int i = 0; i < 100; i++) {  
            s += "a";  
            }  
        }  
    } 

    每做一次 + 就产生个StringBuilder对象,然后append后就扔掉。下次循环再到达时重新产生个StringBuilder对象,然后 append 字符串,如此循环直至结束。如果我们直接采用 StringBuilder 对象进行 append 的话,我们可以节省 N - 1 次创建和销毁对象的时间。所以对于在循环中要进行字符串连接的应用,一般都是用StringBuffer或StringBulider对象来进行 append操作。

    String对象的intern方法理解和分析:

    public class Test4 {  
    
        private static String a = "ab";   
    
        public static void main(String[] args){  
    
        String s1 = "a";  
    
        String s2 = "b";  
    
        String s = s1 + s2;  
    
        System.out.println(s == a);//false  
    
        System.out.println(s.intern() == a);//true    
    
        }  
    
    } 

    这里用到Java里面是一个常量池的问题。对于s1+s2操作,其实是在堆里面重新创建了一个新的对象,s保存的是这个新对象在堆空间的的内容,所 以s与a的值是不相等的。而当调用s.intern()方法,却可以返回s在常量池中的地址值,因为a的值存储在常量池中,故s.intern和a的值相等。

    总结

    栈中用来存放一些原始数据类型的局部变量数据和对象的引用(String,数组.对象等等)但不存放对象内容

    堆中存放使用new关键字创建的对象.

    字符串是一个特殊包装类,其引用是存放在栈里的,而对象内容必须根据创建方式不同定(常量池和堆).有的是编译期就已经创建好,存放在字符串常 量池中,而有的是运行时才被创建.使用new关键字,存放在堆中。

     

    展开全文
  • 精华Java问题总结

    万次阅读 多人点赞 2021-03-15 09:21:11
    当时在网上汇总了不知多少面试和基础题,弄了个精华总结。 1、一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致...

    当时在网上汇总了不知多少面试和基础题,弄了个精华总结。

    1、一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制?

    可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致。

    2、short s1= 1; s1= s1+1; 有没有错?

    s1+1运算时会自动提升类型,结果是int,赋值给s1时,将报告需要强转类型的错误。

    3、short s1= 1; s1 += 1;有没有错?

    +=是java规定的运算符,编译器会对它进行特殊处理,因此可以正确编译。

    4、使用final关键字修饰一个变量时,引用的内容一定不能变?

    使用final修饰变量时,是引用变量(也就是地址)不能变,引用变量所指向的对象中的内容还是可以改变的

    5、是否可以从static方法内对非static方法调用?为什么?

            不可以。因为非static方法是与对象关联的,必须创建一个对象,才可以在该对象上进行方法调用(对象.方法)。而static方法调用时不需要创建对象,可以直接调用。如果从一个static方法中发出对非static方法的调用,那个非static方法是关联到哪个对象上的呢?这个逻辑无法成立。

    6、Overload和Override的区别?

    重载Overload表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同(即参数个数或类型不同)。

    重写Override表示子类中的方法可以与父类中的方法的名称和参数完全相同,子类对象调用这个方法时,将调用子类中的定义方法,这就把父类中的方法覆盖了,这也是面向对象编程的多态性的一种表现。

    7、Overloaded的方法是否可以改变返回值的类型?

    如果几个重载Overloaded的方法的参数列表不一样,它们的返回者类型当然也可以不一样。

    如果两个方法的参数列表完全一样,不管返回值是否相同,都不允许。因为无法确定编程者倒底是想调用哪个方法了,因为他们被调用时看起来完全相同。

    8、接口是否可继承接口?抽象类是否可实现(implements)接口?抽象类是否可继承具体类(concreteclass)?抽象类中是否可以有静态的main方法?

            接口可以继承接口。抽象类可以实现(implements)接口,抽象类可以继承具体类。抽象类中可以有静态的main方法。

            记住抽象类与普通类的唯一区别就是不能创建实例对象和允许有abstract方法。

    9、Java中实现多态的机制是什么?

            靠的是父类(或接口定义)的引用变量可以指向子类(或具体实现类)的实例对象。

    而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,而不是引用变量的类型中定义的方法。

    10、abstractclass和interface有什么区别?

    抽象类可以有构造方法,接口中不能有构造方法。

    抽象类中可以有普通成员变量,接口中没有普通成员变量

    抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。

    抽象类中的抽象方法类型可以是public,protected,接口中的抽象方法只能是public类型的,并且默认为public abstract。

    7. 一个类可以实现多个接口,但只能继承一个抽象类。

    11、String s = "Hello";s = s + "world!";执行后,原始的String对象中的内容变了没有?

            没有。因为String被设计成不可变类,所以它的所有对象都是不可变对象。只是s不再指向旧的对象了。

    12、下面这条语句一共创建了多少个对象:String s="a"+"b"+"c"+"d";

    javac编译可以对字符串常量直接相加的表达式进行优化直接得出答案,不必要等到运行期再去进行加法运算处理

    这行代码被编译器在编译时优化后,相当于直接定义了一个”abcd”的字符串,所以,上面的代码应该只创建了一个String对象。

    13、final, finally, finalize的区别。

            final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。

            finally是异常处理语句结构的一部分,表示总是执行。

            finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法.

    14、error和exception有什么区别?

            error 表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处理这样的情况。                  exception表示程序还能够克服和恢复的问题

    15、Java 中堆和栈区别?
    栈常用于保存方法帧和局部变量,而对象总是在堆上分配。

    栈通常都比堆小,也不会在多个线程之间共享,而堆被整个 JVM 的所有线程共享。

            栈:在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,当在一段代码块定义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java 会自动释放掉为该变量分配的内存空间,该内存空间可以立即被另作它用。

            堆:堆内存用来存放由 new 创建的对象和数组,在堆中分配的内存,由 Java 虚拟机的自动垃圾回收器来管理。

    16、能将 int 强制转换为 byte 类型的变量吗?(引申到所有大类型转小类型)
            实际中,我们可以做强制转换,不会报错。

    但是存在大类型转小类型的通病: int 是 32 位的,而 byte 是 8 位的,如果强制转化,int 类型的高 24 位将会被丢弃,所以尽量不要这样做。

    17、hashCode有什么用?与 a.equals(b) 有什么关系?
            hashCode方法对应对象的 hash 值。它常用于基于 hash 的集合类,如 Hashtable、HashMap、LinkedHashMap等等。根            据 Java 规范,两个使用 equal() 方法来判断相等的对象,必须具有相同的 hash code。

    18、垃圾回收的优点和原理。

            垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。回收机制有分代复制垃圾回收和标记垃圾回收,增量垃圾回收。

    原理:可以给对象添加一个被引用的计数器,就可以判断是否已经是无引用对象。但是难以解决循环引用问题。

    如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。则在 Java 堆当中的两块内存依然保持着互相引用无法回收。

    可达性分析法:通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。 

    19,java中会存在内存泄漏吗

    内存泄漏:指一个不再被程序使用的对象或变量一直被占据在内存中。java中有垃圾回收机制,它可以保证当对象不再被引用的时候,对象将自动被垃圾回收器从内存中清除掉。

            由于Java使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引用,只要它们和根进程不可达,那么GC也是可以回收它们的。

    java中的内存泄露的情况:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露.

    尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。通俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是java中可能出现内存泄露的情况,例如,缓存系统,我们加载了一个对象放在缓存中(例如放在一个全局map对象中),然后一直不再使用它,这个对象一直被缓存引用,但却不再被使用。

     

     

    21、线程和进程有什么区别?

    进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位

    线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。

    不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。

    22、如何在Java中实现线程?

    继承Thread类
    class Handler extends Thread{
        public void run(){
           //方法重写
        }
        public static void main(String[] args){
            Thread thread = new Handler();//创建线程对象
            thread.start();//启动线程
        }
    }
    
    实现Runnable接口
    class Handler implements Runnable{
        public void run(){
           //方法实现
        }
        public static void main(String[] args){
            Handler handler = new Handler();
            Thread thread = new Thread(handler);//创建线程对象
            thread.start();//启动线程
        }
    }
    

    23、Java 关键字volatile 与 synchronized 作用与区别?

        1,volatile
        它所修饰的变量不保留拷贝,直接访问主内存中的。
    在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。

    为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变 量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。 

    一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。
        2,synchronized

    当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

    一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。

    二、当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

    三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有synchronized(this)同步代码块的访问将被阻塞。

    24、线程生命周期?

    新建一个线程时,它的状态是New。当我们调用线程的start()方法时,状态被改变为Runnable。线程调度器会为Runnable线程池中的线程分配CPU时间并且将它们的状态改变为Running。其他的线程状态还有Waiting,Blocked 和Dead

    25、死锁?

    指多个进程在运行过程中因争夺资源而造成的一种僵局,当处于这种状态时,若无外力作用,它们都将无法再向前推进。

    原因可归结为两点:竞争资源、 进程间推进顺序非法
    产生死锁的必要条件:

    互斥条件:在一段时间内某资源仅为一进程所占用。
    请求和保持条件:对已获得的资源保持不放。
    不剥夺条件:已获得的资源只能由自己释放。
    环路等待条件:存在一个进程--资源的环形链。

    26、什么是线程池? 为什么要使用它?

    创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。

    线程池实现了线程重复利用,节约了时间和资源。

    // Java线程池的完整构造函数
    public ThreadPoolExecutor(
      int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
      int maximumPoolSize, // 线程数的上限
      long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长,
                                         // 超过这个时间,多余的线程会被回收。
      BlockingQueue<Runnable> workQueue, // 任务的排队队列
      ThreadFactory threadFactory, // 新线程的产生方式
      RejectedExecutionHandler handler) // 拒绝策略

    27、反射?

    JAVA反射机制是在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

    28、JDK 、 JRE 、JVM有什么区别和联系?

    JVM : Java 虚拟机。能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作。所以说,jvm 是 Java 能够跨平台的核心。

    JRE :Java 运行时环境。包含两个部分,jvm和 Java 的一些基本类库。

    JDK :Java 开发工具包。jdk 是整个 Java 开发的核心,它集成了 jre 和一些好用的小工具。

    这三者的关系是:一层层的嵌套关系。JDK>JRE>JVM。

    29、深拷贝浅拷贝

          数据类型分为两种基础类型和引用类型:

    基础类型:像Number、String、Boolean等这种为基本类型
    引用类型:Object和Array

    浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,修改其中任意的值,另一个值会随之变化

    深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变。

    30、JVM内存分为哪几部分?各个部分的作用是什么?

    JVM内存区域分为五个部分,分别是堆,方法区,虚拟机栈,本地方法栈,程序计数器。


    1)堆。

    堆是Java对象的存储区域,任何用new字段分配的Java对象实例和数组。
    2)方法区。

    它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,方法区,从JDK1.8永久代被移除。
    3)虚拟机栈。

    虚拟机栈中执行每个方法的时候,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
    4)本地方法栈。

    与虚拟机栈发挥的作用相似,相比于虚拟机栈为Java方法服务,本地方法栈为虚拟机使用的Native方法服务,执行每个本地方法的时候,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
    5)程序计数器。

    指示Java虚拟机下一条需要执行的字节码指令。

    总:其中方法区和堆被JVM中多个线程共享,比如类的静态常量就被存放在方法区,供类对象之间共享,虚拟机栈,本地方法栈,pc寄存器是每个线程独立拥有的,不会与其他线程共享。

    31、为什么会出现4.0-3.6=0.40000001这种现象?

    2进制的小数无法精确的表达10进制小数,在计算10进制小数的过程中要先转换为2进制进行计算,这个过程中出现误差。

    32、“==”比较的是什么?

    “==”两边是对象,比较地址。

    “==”两边是基本类型,比较数值。

    展开全文
  • #region 使用静态文件和目录浏览 app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider("D:/MyStaticFiles"),//文件路径,一定是绝对路径 RequestPath = "/StaticFiles",//...
  • 用于表示操作后将引起一定后果的情况;也用于表示由于系统原因而造成的负向结果</view> </view> </view> <view class="icon-box"> <icon class="icon-box-img" type="warn" size="93...
  • Android精华汇总

    千次阅读 2016-03-15 23:20:42
    Android资源文件 - 使用资源存储字符串 颜色 尺寸 整型 布尔值 数组 Android系统架构解析 显而易见的,根据上图自底向上我们知道Android系统架构包含5大部分:Linux Kernel(Linux内核) 、Libraries(库)、Androi
  • oracle 11g r2 grid是干什么的 从11g开始,Oracle把...在oracle 11g r2中要使用ASM,是不是一定要安装Grid Infrastructure?Grid Infrastructure 简称GI 在oracle 10g中需要安装ASM 和集群软件 但在11g oracle
  • kafka只能使用作为"常规"的消息系统,在一定程度上,尚未确保消息的发送与接收绝对可靠(比如,消息重发,消息发送丢失等)   2、Websit activity tracking   kafka可以作为"网站活性跟踪"的最佳工具;可以将网页/用户...
  • 复习Java的精华总结

    万次阅读 多人点赞 2021-03-14 11:02:51
    输入 java.util.Scanner 是 Java5 的新特征,我们...使用方法如下: //对应类型用对应的方法接收 String stringx=s.next(); String stringy=s.nextLine(); int intx=s.nextInt(); long longx=s.nextLong();
  • C#编程精华总结

    千次阅读 多人点赞 2018-03-25 16:23:24
    C#学习总结C#简介:1、C#是一种最新的、面向对象的编程语言2、C#使得程序员可以在Microsoft开发的最新的.NET平台上快速地编写Windows应用程序3、而且Microsoft .NET提供了...,专门与.NET Framework一起使用而设计的...
  • int getStatusCode() 我们接下来就要讲到HttpGet,HttpPost两个类中解决无参数和带参数的问题,和连接池技术问题 看如下博客,知道Get,Post请求的区别(也就是为什么我们将HttpGet,HttpPost两个类) get请求和...
  • 「冰河技术」部分精华文章目录汇总

    千次阅读 多人点赞 2020-06-15 01:09:55
    Java8新特性:一张图带你领略Java8有哪些新特性 Java8新特性:Java8为什么要引入Lambda表达式?原来如此! Java8新特性:Lambda表达式基础语法,都在这儿了!! Java8新特性:Lambda表达式典型案例,你想要的的都在...
  • wifi破解方法总结(精华

    万次阅读 多人点赞 2021-09-27 23:42:04
     以下常见路由器默认密码(一些新手新买的路由器一般嫌麻烦都不会修改密码哦)  品牌:ViKing用户名:adsl 密码:adsl1234  品牌:实达默认IP:192.168.10.1版本:  用户名:admin 密码:conexant  版本:...
  • 但其中有很多框架的设计并不中立,用这些框架去开发某些特定应用或许还行,如果放到一个更大范围的应用类型中,就会发现无法满足要求,这样的框架通用性不足,开发者一定要谨慎使用。 讲良心话,Next.js 真的是一...
  • hibernate精华总结

    千次阅读 2018-05-07 21:52:07
    hibernate所有的精华都沉淀在面向对象这个思想上,这也是它最出彩的地方。 一、关于多表: 多表是一种关系的表达,具体的有一对多,多对多这两种。 1、一对多关系,如订单和客户,一个订单只能属于一个客户,但一个...
  • // 超过一定次数,核心等于1,交替使用Thread.Sleep(0)和Thread.Yield方法 // 再超过一定次数,Thread.Sleep(1) // Sleep(0)实际上调用SleepEx系统函数 // Yield()调用SwitchToThread的系统函数 } // 锁保护...
  • linux精华

    千次阅读 2010-07-08 11:18:00
    字符,但可能在文件名中产生,如unix下的NFS文件系统在Mac系统上使用 1.解决的方法,把NFS文件系统在挂到不过滤'/'字符的系统 下删除含特殊文件名的文件。 2.也可将错误文件名的目录其它文件移走,ls -id ...
  • 论坛精华

    万次阅读 2008-01-01 17:11:00
    [转]ASP.NET四种页面导航方式之比较与选择 在ASP.NET应用中,Web表单之间的导航有多种方式:用超级链接,用Response.Redirect,... 一、超级链接 从一个表单进入另一个表单最简单的方式是使用HTML超级链接控件。在We
  • Eclipse的Debug调试技巧大全(精华版)

    万次阅读 多人点赞 2017-04-24 16:13:17
    Eclipse的Debug调试技巧大全(精华版)
  • 三天研读《中兴电路设计规范》精华总结

    万次阅读 多人点赞 2020-05-16 18:25:52
    本博客将简述中兴通讯股份有限公司在原理图设计中需要注意的一些事项,其中包含了中兴设计开发部积累的大量硬件开发知识和经验,可以作为学习使用。硬件工程师可以学习并掌握检查条目的内容以及对条目的详细说明,...
  • Java知识——精华总结

    万次阅读 多人点赞 2017-03-05 14:55:54
    Java知识——精华总结
  • C++总结精华

    千次阅读 多人点赞 2018-05-31 20:15:35
    起床、洗脸、刷牙、出门都是你的一个行为,你只需要写出这些行为的方法,具体执行交给某个人去执行,执行顺序也不一定,对比起来,你就可以发现面向对象的优越性。 面向对象三大特性 封装 继承 ...
  • 《程序员面试宝典》精华 编程语言部分 正所谓取其精华,去其糟粕。本文谨记录下《程序员面试宝典》一些关键的知识点、易错点,对于一些虽然重要但书中没有解释清楚的地方不做记录。当然这里的糟粕只是指不那么...
  • 各路大神写的代码精华
  • WebLogic管理精华

    千次阅读 2006-07-07 17:58:00
    WebLogic管理精华Ø 1 日常管理1.1 WebLogic Platform 8.1 永不过期的开发版license 下载地址:http://dev2dev.bea.com.cn/bbs/servlet/D2DServlet/download/81-8992-44196-240/license.bea 使用方式:替换c:bea...
  • AWS精华笔记

    千次阅读 2015-07-29 09:19:56
    (AWS更新较快,最近想再重新跟一跟,看到这个资料不错,先收下了。) 常用的AWS服務(本文取自「優福網-AWS教學手冊」) AWS雲服務. AWS網址 http://aws.amazon.com/ AWS是Amazon Web Services的...
  • 《思考致富》精华

    千次阅读 2014-05-08 17:10:52
    “两个人的智慧加起来,一定会产生第三种看不见的无形力量,我们可以把这种力量比喻第三个人的智慧。”当两个人的智慧协调一致时,每一个人大脑的精神能量部分会形成一股吸引力,从而形成了智囊团的精神,从而可以...
  • 运维精华面试题

    万次阅读 2020-05-24 15:23:18
    1.常见的Linux发行版本都有什么?你最擅长哪一个?它的官网网站是什么?说明你擅长哪一块? 常见的Linux发行版本有Redhat、Centos、Debian、Ubuntu、Suse 最擅长Redhat和Centos Redhat官网:www.redhat.com Centos...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 35,151
精华内容 14,060
关键字:

为什么一定要使用精华