高质量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

 

    比较上述三个示例法式,会创造“援用通报”的性子象“指针通报”,而誊写方式象“值通报”。理想上“援用”可以做的任何任务“指针”也都可以或许做,为什么还要“援用”这器械?

答案是“用得当的对象做恰到长处的任务”。

    指针可以或许毫无束缚地把持内存中的怎样器械,只管指针成效强大,但长短常损伤。就象一把刀,它可以用来砍树、裁纸、修指甲、理发等等,谁敢如许用?

倘使简直只需求借用一下某个对象的“别号”,那么就用“援用”,而不要用“指针”,以免发买卖外。比如说,某人需求一份证实,本来在文件上盖上公章的印子就行了,倘使把取公章的钥匙交给他,那么他就获得了不该有的权利。



版权声明: 原创作品,答理转载,转载时请务必以超链接形式标明文章 原始出处 、作者信息和本声明。不然将究查功令责任。

posted @ 2011-03-07 17:03  蓝色的天空III  阅读(176)  评论(0编辑  收藏  举报