zoukankan      html  css  js  c++  java
  • 高质量C /C编程指南第6章 函数方案

    第6章 函数方案

    函数是C /C序次的底子成效单元,其严峻性不问可知。函数方案的纤细缺陷很任意招致该函数被错用,所以光使函数的成效切确是不敷的。本章重点论述函数的接口方案和外部完成的一些规矩。

    函数接口的两个要素是参数和前去值。C言语中,函数的参数和前去值的通报要拥有两种:值通报(pass by value)和指针通报(pass by pointer)。C 言语中多了援用通报(pass by reference)。因为援用通报的性质象指针通报,而应用方法却象值通报,初学者经常困惑不解,任意惹起紊乱,请先阅读6.6节“援用与指针的比较”。

    6.1 参数的规矩

    l         【规矩6-1-1参数的誊录要无缺,不要贪图省事只写参数的类型而省略参数名字。假定函数没有参数,则用void填充。

    比方:

    void SetValue(int width, int height);   // 良好的气魄气度

    void SetValue(int, int);            // 不良的气魄气度

    float GetValue(void);    // 良好的气魄气度

    float GetValue();       // 不良的气魄气度

     

    l         【规矩6-1-2参数命名要适当,按首要公道。

    比方编写字符串拷贝函数StringCopy,它有两个参数。假定把参数名字起为str1和str2,例如

    void StringCopy(char *str1, char *str2);

    那么我们很难搞清晰终究是把str1拷贝到str2中,仍是正好倒已往。

    可以把参数名字起得更存心义,如叫strSource和strDestination。如许从名字上就可以看出应该把strSource拷贝到strDestination。

    另有一个成效,这两个参数那一个该在前那一个该在后?参数的按首要遵照序次员的习气。通俗地,应将目的参数放在前面,源参数放在前面。

    假定将函数声明为:

    void StringCopy(char *strSource, char *strDestination);

    别人在应用时可以大概会不假思量地写成如下体式格局:

    char str[20];

    StringCopy(str, “Hello World”);   // 参数按次倒置

     

    l         【规矩6-1-3假定参数是指针,且仅作输入用,则应在类型前加const,以进攻该指针在函数体内被意外修改。

    比方:

    void StringCopy(char *strDestination,const char *strSource);

     

    l         【规矩6-1-4假定输入参数以值通报的方法通报东西,则宜改用“const &”方法来通报,如许可以省去临时东西的结构和析构进程,从而进步服从。

     

    ²        【发起6-1-1进攻函数有太多的参数,参数个数尽管即便节制在5个以内。假定参数太多,在应用时任意将参数类型或按次搞错。

     

    ²        【发起6-1-2尽管即便不要应用类型和数目不确定的参数。

    C尺度库函数printf是给与不确定参数的模范代表,其原型为:

    int printf(const chat *format[, argument]…);

    这种气魄气度的函数在编译时损失了严格的类型安静查抄。

    6.2 前去值的规矩

    l         【规矩6-2-1不要省略前去值的类型。

    C言语中,凡不加类型声明的函数,不合自动按整型措置。如许做不会有什么长处,却任意被误解为void类型。

    C 言语有很严格的类型安静查抄,不允许上述状况发生发火。因为C 序次可以调用C函数,为了进攻紊乱,划定规矩任何C / C函数都必需有类型。假定函数没有前去值,那么回声明为void类型。

     

    l         【规矩6-2-2函数名字与前去值类型在语义上弗成争论。

    违犯这条规矩的模范代表是C尺度库函数getchar。

    比方:

    char c;

    c = getchar();

    if (c == EOF)


    根据getchar名字的意思,将变量c声明为char类型是很自然的工作。但不幸的是getchar确实不是char类型,而是int类型,其原型如下:

            int getchar(void);

    因为c是char类型,取值规模是[-128,127],假定宏EOF的值在char的取值规模之外,那么if语句将老是失败,这种“危害”人们通俗那处料失失!招致本例错误的责任并不在用户,是函数getchar误导了应用者。

     

    l         【规矩6-2-3不要将正常值和错误标志混在一起前去。正常值用输入参数取得,而错误标志用return语句前去。

    回想上例,C尺度库函数的方案者为什么要将getchar声明为令人含糊的int类型呢?他会那么傻吗?

    在正常状况下,getchar确实前去单个字符。但假定getchar碰到文件完毕标志或发生发火读错误,它必需前去一个标志EOF。为了区别于正常的字符,只好将EOF定义为正数(往往为负1)。是以函数getchar就成了int类型。

    我们在志向任务中,经常会碰到上述令工资难的成效。为了进攻出现误解,我们应该将正常值和错误标志分隔。即:正常值用输入参数取得,而错误标志用return语句前去。

    函数getchar可以改写成 BOOL GetChar(char *c);

    虽然gechar比GetChar火速,比方 putchar(getchar()); 可是假定getchar用错了,它的火速性又有什么用呢?

     

    ²        【发起6-2-1有光阴函数原来不需求前去值,但为了添加火速性如支持链式表达,可以附加前去值。

    比方字符串拷贝函数strcpy的原型:

    char *strcpy(char *strDest,const char *strSrc);

    strcpy函数将strSrc拷贝至输入参数strDest中,同时函数的前去值又是strDest。如许做并非添枝加叶,可以取得如下火速性:

        char str[20];

        int  length = strlen( strcpy(str, “Hello World”) );

     

    ²        【发起6-2-2假定函数的前去值是一个东西,有些场合用“援用通报”替代“值通报”可以进步服从。而有些场合只能用“值通报”而不克不及用“援用通报”,不然会出错。

    比方:

    >

    {…

        // 赋值函数

        String & operate=(const String &other);   

    // 相加函数,假定没有friend润饰则只许有一个右侧参数

    friend    String   operate ( const String &s1, const String &s2);

    private:

        char *m_data;

    }

     

           String的赋值函数operate = 的完成如下:

    String & String::operate=(const String &other)

    {

        if (this == &other)

            return *this;

        delete m_data;

        m_data = new char[strlen(other.data) 1];

        strcpy(m_data, other.data);

        return *this;    // 前去的是 *this的援用,无需拷贝进程

    }

     

    关于赋值函数,理当用“援用通报”的方法前去String东西。假定用“值通报”的方法,虽然成效仍然切确,但因为return语句要把 *this拷贝到保存前去值的外部存储单元之中,添加了不需求的开支,降低了赋值函数的服从。比方:

      String a,b,c;

      …

      a = b;     // 假定用“值通报”,将发生发火一次 *this 拷贝

      a = b = c;   // 假定用“值通报”,将发生发火两次 *this 拷贝

     

           String的相加函数operate 的完成如下:

    String  operate (const String &s1, const String &s2)  

    {

        String temp;

        delete temp.data;    // temp.data是仅含‘\0’的字符串

            temp.data = new char[strlen(s1.data) strlen(s2.data) 1];

            strcpy(temp.data, s1.data);

            strcat(temp.data, s2.data);

            return temp;

        }

     

    关于相加函数,理当用“值通报”的方法前去String东西。假定改用“援用通报”,那么函数前去值是一个指向部分东西temp的“援用”。因为temp在函数完毕时被自动废弃,将招致前去的“援用”有效。比方:

        c = a b;

    此时 a b 并不前去盼愿值,c什么也得不到,流下了隐患。

    6.3 函数外部完成的规矩

    差别成效的函数其外部完成各不相同,看起来似乎无法就“外部完成”杀青不合的不雅看法。但根据经历,我们可以在函数体的“出口处”和“出口处”从严把关,从而进步函数的质量。

     

    l         【规矩6-3-1在函数体的“出口处”,对参数的有效性举办查抄。

    良多序次错误是由合法参数惹起的,我们应该充足清晰并切确应用“断言”(assert)来进攻此类错误。详见6.5节“应用断言”。

     

    l         【规矩6-3-2在函数体的“出口处”,对return语句的切确性和服从举办查抄。

         假定函数有前去值,那么函数的“出口处”是return语句。我们不要轻视return语句。假定return语句写得不好,函数要么出错,要么服从低下。

    垂青工作如下:

    (1)return语句弗成前去指向“栈内存”的“指针”年夜体“援用”,因为该内存在函数体完毕时被自动废弃。比方

        char * Func(void)

        {

            char str[] = “hello world”;    // str的内存位于栈上

            …

            return str;     // 将招致错误

        }

    (2)要搞清晰前去的终究是“值”、“指针”仍是“援用”。

    (3)假定函数前去值是一个东西,要思量return语句的服从。比方   

                  return String(s1 s2);

    这是临时东西的语法,表现“确立一个临时东西并前去它”。不要感觉它与“先确立一个部分东西temp并前去它的效果”是等价的,如

    String temp(s1 s2);

    return temp;

    素质不然,上述代码将发生发火三件事。首先,temp东西被确立,同时完成初始化;然后拷贝结构函数把temp拷贝到保存前去值的外部存储单元中;末了,temp在函数完毕时被废弃(调用析构函数)。可是“确立一个临时东西并前去它”的进程是差别的,编译器间接把临时东西确立并初始化在外部存储单元中,省去了拷贝和析构的化费,进步了服从。

    近似地,我们不要将 

    return int(x y); // 确立一个临时变量并前去它

    写成

    int temp = x y;

    return temp;

    因为外部数据类型如int,float,double的变量不存在结构函数与析构函数,虽然该“临时变量的语法”不会进步多少服从,可是序次加倍简练易读。

    6.4 别的发起

    ²        【发起6-4-1函数的成效要单一,不要方案多用途的函数。

    ²        【发起6-4-2函数体的规模要小,尽管即便节制在50行代码之内。

    ²        【发起6-4-3尽管即便进攻函数带有“记忆”成效。相同的输入理当发生发火相同的输入。

    带有“记忆”成效的函数,其行为可以大概是弗成预测的,因为它的行为可以大概取决于某种“记忆形态”。如许的函数既不易清晰又倒霉于测试和维护。在C/C 言语中,函数的static部分变量是函数的“记忆”存储器。发起尽管即便罕用static部分变量,除非必需。

    ²        【发起6-4-4不光需查抄输入参数的有效性,还要查抄经过别的途径进入函数体内的变量的有效性,比方全局变量、文件句柄等。

    ²        【发起6-4-5用于出错措置的前去值必定要清晰,让应用者不任意轻忽或误解错误状况。

    6.5 应用断言

    序次通俗分为Debug版本和Release版本,Debug版本用于外部调试,Release版本发行给用户应用。

    断言assert是仅在Debug版本起感染的宏,它用于查抄“不应该”发生发火的状况。示例6-5是一个内存复制函数。在运转进程中,假定assert的参数为假,那么序次就会间断(通俗地还会出现提示对话,声明在什么中央引发了assert)。

     

             void  *memcpy(void *pvTo, const void *pvFrom, size_t size)

    {

            assert((pvTo != NULL) && (pvFrom != NULL));     // 应用断言

            byte *pbTo = (byte *) pvTo;     // 进攻变化pvTo的地址

            byte *pbFrom = (byte *) pvFrom; // 进攻变化pvFrom的地址

            while(size -- > 0 )

                *pbTo = *pbFrom ;

            return pvTo;

    }

    示例6-5 复制不堆叠的内存块

     

    assert不是一个匆促拼集起来的宏。为了不在序次的Debug版本和Release版本惹起差别,assert不应该发生发火任何副感染。所以assert不是函数,而是宏。序次员可以把assert当作一个在任何零碎形态下都可以安静应用的无害测试技巧。假定序次在assert处中止了,并不是说含有该assert的函数有错误,而是调用者出了舛错,assert可以帮助我们找到发生发火错误的缘故起因。

    很少有比跟踪到序次的断言,却不晓得该断言的感染更让人懊丧的事了。你化了良多光阴,不是为了拂拭错误,而只是为了弄清晰这个错误终究是什么。有的光阴,序次员偶然还会方案出有错误的断言。所以假定搞不清晰断言查抄的是什么,就很难坚决错误是出而今序次中,仍是出而今断言中。幸运的是这个成效很好管理,只需加上明了的注解即可。这本是不言而喻的工作,可是很少有序次员如许做。这比如一集体私家在丛林里,看到树上钉着一块“危害”的年夜牌子。但危害终究是什么?树要倒?有废井?有野兽?除非报告人们“危害”是什么,不然这个警告牌难以起到积极有效的感染。难以清晰的断言经常被序次员疏忽,乃至被删除。[Maguire, p8-p30]

     

    l         【规矩6-5-1应用断言捕获不应该发生发火的合法状况。不要搅浑合法状况与错误状况之间的区别,后者是必定存在的并且是必定要作出措置的。

    l         【规矩6-5-2】在函数的出口处,应用断言查抄参数的有效性(公道性)。

    l         【发起6-5-1在编写函数时,要举办频频的调查,并临时问:“我方案做哪些假定?”一旦确定了的假定,就要应用断言对假定举办查抄。

    l         【发起6-5-2通俗教科书都鼓动序次员们举办防错方案,但要记着这种编程气魄气度可以大概会隐瞒错误。当举办防错方案时,假定“弗成以大概发生发火”的工作确实发生发火了,则要应用断言举办报警。

    6.6 援用与指针的比较

    援用是C 中的不雅看法,初学者任意把援用和指针搅浑一起。一下序次中,n是m的一个援用(reference),m是被援用物(referent)。

        int m;

        int &n = m;

    n相称于m的别名(外号),对n的任何操作便是对m的操作。例若有人名叫王小毛,他的外号是“三毛”。说“三毛”怎样怎样的,实在便是对王小毛说长道短。所以n既不是m的拷贝,也不是指向m的指针,实在n便是m它自己。

    援用的一些规矩如下:

    (1)援用被确立的同时必需被初始化(指针则可以在任何光阴被初始化)。

    (2)不克不及有NULL援用,援用必需与公道的存储单元计议关连(指针则可所以NULL)。

    (3)一旦援用被初始化,就不克不及变化援用的关连(指针则可以随时变化所指的东西)。

        以下示例序次中,k被初始化为i的援用。语句k = j并不克不及将k修改成为j的援用,只是把k的值转酿成为6。因为k是i的援用,所以i的值也酿成了6。

        int i = 5;

        int j = 6;

        int &k = i;

        k = j;    // k和i的值都酿成了6;

        上面的序次看起来象在玩翰墨游戏,没有表现出援用的价钱。援用的严峻成效是通报函数的参数和前去值。C 言语中,函数的参数和前去值的通报要拥有三种:值通报、指针通报和援用通报。

        以下是“值通报”的示例序次。因为Func1函数体内的x是外部变量n的一份拷贝,变化x的值不会影响n, 所以n的值仍然是0。

        void Func1(int x)

    {

        x = x 10;

    }


    int n = 0;

        Func1(n);

        cout << “n = ” << n << endl;    // n = 0

       

    以下是“指针通报”的示例序次。因为Func2函数体内的x是指向外部变量n的指针,变化该指针的内容将招致n的值变化,所以n的值成为10。

        void Func2(int *x)

    {

        (* x) = (* x) 10;

    }


    int n = 0;

        Func2(&n);

        cout << “n = ” << n << endl;        // n = 10

     

        以下是“援用通报”的示例序次。因为Func3函数体内的x是外部变量n的援用,x和n是不合个东西,变化x等于变化n,所以n的值成为10。

        void Func3(int &x)

    {

        x = x 10;

    }


    int n = 0;

        Func3(n);

        cout << “n = ” << n << endl;      // n = 10

     

        比较上述三个示例序次,会发明“援用通报”的性质象“指针通报”,而誊录方法象“值通报”。志向上“援用”可以做的任何工作“指针”也都可以大概做,为什么还要“援用”这东西?

    谜底是“用适当的东西做恰到长处的任务”。

        指针可以大概毫无约束地操作内存中的怎样东西,尽管指针成效强年夜,可是很是危害。就象一把刀,它可以用来砍树、裁纸、修指甲、剃头等等,谁敢如许用?

    假定确实只需求借用一下某个东西的“别名”,那么就用“援用”,而不要用“指针”,以免发生发火意外。比如说,某人需求一份证明,原来在文件上盖上公章的印子就行了,假定把取公章的钥匙交给他,那么他就取得了不应有的权柄。



    版权声明: 原创作品,允许转载,转载时请务必以超链接体式格局标明文章 原始因由 、作者信息和本声明。不然将追查法律责任。

  • 相关阅读:
    软工网络15团队作业4——Alpha阶段敏捷冲刺
    (转) linux目录结构详细介绍
    ActiveMQ使用记录
    .NET4.5中WCF中默认生成的basicHttpsBinding的研究
    StackExchange.Redis的使用
    微信/QQ机器人的实现
    EntityFramework中的datetime2异常的解决
    在Web API中使用Swagger-UI开源组件(一个深坑的解决)
    (转)使用Migrations更新数据库结构(Code First )
    WebApi中帮助页Description的中文显示
  • 原文地址:https://www.cnblogs.com/zgqjymx/p/1974606.html
Copyright © 2011-2022 走看看