Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3285172
  • 博文数量: 346
  • 博客积分: 10189
  • 博客等级: 上将
  • 技术积分: 3125
  • 用 户 组: 普通用户
  • 注册时间: 2008-08-05 19:46
文章分类

全部博文(346)

文章存档

2013年(35)

2011年(35)

2010年(76)

2009年(48)

2008年(152)

分类: C/C++

2008-09-03 16:18:18

4 函数设计

函数是C++/C程序的基本功能单元,其重要性不言而喻。函数设计的细微缺点很容易导致该函数被错用,所以光使函数的功能正确是不够的。本章重点论述函数的接口设计和内部实现的一些规则。

函数接口的两个要素是参数和返回值。C语言中,函数的参数和返回值的传递方式有两种:值传递(pass by value)和指针传递(pass by pointer)。C++ 语言中多了引用传递(pass by reference)。由于引用传递的性质象指针传递,而使用方式却象值传递,初学者常常迷惑不解,容易引起混乱,请先阅读6.6节“引用与指针的比较”。

4.1 参数的规则

       【规则4.1-1      参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字,如果函数没有参数,则用void填充;例如:

void SetValue(int nWidth, int nHeight); // 良好的风格

void SetValue(int, int);               // 不良的风格

float GetValue(void);                  // 良好的风格

float GetValue();                      // 不良的风格

       【规则4.1-2      参数命名要恰当,顺序要合理;

例如编写字符串拷贝函数StringCopy,它有两个参数,如果把参数   名字起为str1str2,例如:

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

那么我们很难搞清楚究竟是把str1拷贝到str2中,还是刚好倒过来,可以把参数名字起得更有意义,如叫strSourcestrDestination。这样从名字上就可以看出应该把strSource拷贝到strDestination。还有一个问题,这两个参数那一个该在前那一个该在后?参数的顺序要遵循程序员的习惯。一般地,应将目的参数放在前面,源参数放在后面。如果将函数声明为:

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

别人在使用时可能会不假思索地写成如下形式:

char str[20];

StringCopy(str, Hello World);  // 参数顺序颠倒 

       【规则4.1-3      如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。例如:

void StringCopy(char *strDestinationconst char *strSource);

       【规则4.1-4      如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率;

       【建议4.1-1      避免函数有太多的参数,参数个数尽量控制在5个以内。如果参数太多,在使用时容易将参数类型或顺序搞错;

       【建议4.1-2      尽量不要使用类型和数目不确定的参数;

C标准库函数printf是采用不确定参数的典型代表,其原型为:

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

这种风格的函数在编译时丧失了严格的类型安全检查。

4.2 返回值的规则

       【规则4.2-1      不要省略返回值的类型;

C语言中,凡不加类型说明的函数,一律自动按整型处理,这样做不会有什么好处,却容易被误解为void类型;

C++语言有很严格的类型安全检查,不允许上述情况发生。由于C++程序可以调用C函数,为了避免混乱,规定任何C++/ C函数都必须有类型。如果函数没有返回值,那么应声明为void类型

       【规则4.2-2      函数名字与返回值类型在语义上不可冲突;

违反这条规则的典型代表是C标准库函数getchar

例如:

char c;

c = getchar();

if (c == EOF)

按照getchar名字的意思,将变量c声明为char类型是很自然的事情。但不幸的是getchar的确不是char类型,而是int类型,其原型如下:

int getchar(void);

由于cchar类型,取值范围是[-128127],如果宏EOF的值在char的取值范围之外,那么if语句将总是失败,这种“危险”人们一般哪里料得到!导致本例错误的责任并不在用户,是函数getchar误导了使用者

       【规则4.2-3      不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return语句返回;

       【建议4.2-1      有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值;

例如字符串拷贝函数strcpy的原型:

char *strcpy(char *strDestconst char *strSrc);

strcpy函数将strSrc拷贝至输出参数strDest中,同时函数的返回值又是strDest。这样做并非多此一举,可以获得如下灵活性:

char str[20];

int  nLength = strlen( strcpy(str, Hello World) );

       【建议4.2-2      如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出错;

 

对于建议4.2-2,如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传递”可以提高效率,而有些场合只能用“值传递”而不能用“引用传递”,否则会出错,例如:

class String

{

    // 赋值函数

    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什么也得不到,流下了隐患。

4.3 函数内部实现的规则

不同功能的函数其内部实现各不相同,看起来似乎无法就“内部实现”达成一致的观点。但根据经验,我们可以在函数体的“入口处”和“出口处”从严把关,从而提高函数的质量。

 

       【规则4.3-1      在函数体的“入口处”,对参数的有效性进行检查;

很多程序错误是由非法参数引起的,我们应该充分理解并正确使用“断言”(assert)来防止此类错误。详见4.5节“使用断言”

       【规则4.3-2      在函数体的“出口处”,对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的变量不存在构造函数与析构函数,虽然该“临时变量的语法”不会提高多少效率,但是程序更加简洁易读。

4.4 其它建议

       【建议4.4-1      函数的功能要单一,不要设计多用途的函数;

       【建议4.4-2      函数体的规模要小,尽量控制在150行代码之内;

       【建议4.4-3      尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在C/C++语言中,函数的static局部变量是函数的“记忆”存储器。建议尽量少用static局部变量,除非必需。

       【建议4.4-4      不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等;

       【建议4.4-5      用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误情况。

4.5 使用断言

程序一般分为Debug版本和Release版本,Debug版本用于内部调试,Release版本发行给用户使用。

断言assert是仅在Debug版本起作用的宏,它用于检查“不应该”发生的情况。示例4.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;

}

示例4.5 复制不重叠的内存块

 

assert不是一个仓促拼凑起来的宏。为了不在程序的Debug版本和Release版本引起差别,assert不应该产生任何副作用。所以assert不是函数,而是宏。程序员可以把assert看成一个在任何系统状态下都可以安全使用的无害测试手段。如果程序在assert处终止了,并不是说含有该assert的函数有错误,而是调用者出了差错,assert可以帮助我们找到发生错误的原因。 

 

       【规则4.5-1      使用断言捕捉不应该发生的非法情况,不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的;

       【规则4.5-2      在函数的入口处,使用断言检查参数的有效性(合法性);

       【建议4.5-1      在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查;

       【建议4.5-2      一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。

4.6 引用与指针的比较

引用是C++中的概念,初学者容易把引用和指针混淆一起。一下程序中,nm的一个引用(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。由于ki的引用,所以i的值也变成了6

    int i = 5;

    int j = 6;

    int &k = i;

    k = j;  // ki的值都变成了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的引用,xn是同一个东西,改变x等于改变n,所以n的值成为10

    void Func3(int &x)

{

    x = x + 10;

}

int n = 0;

    Func3(n);

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

 

对比上述三个示例程序,会发现“引用传递”的性质象“指针传递”,而书写方式象“值传递”。

 


5 附录

5.1 变量类型定义

 

 

 

bool(BOOL)

b开头

bIsParent

byte(BYTE)

by开头

byFlag

short(SHORT)

n开头

nFileLen

int(INT)

n开头

nStepCount

long(LONG)

l开头

lSize

char(CHAR)

ch开头

chCount

unsigned short(WORD)

w开头

wLength

unsigned long(DWORD)

dw开头

dwBroad

void(VOID)

v开头

vVariant

0结尾的字符串

sz开头

szFileName

LPCSTR(LPCTSTR)

str开头

strString

HANDLE(HINSTANCE)

h开头

hHandle

struct

blk开头

blkTemplate

BYTE*

pb开头

pbValue

WORD*

pw开头

pwValue

LONG*

pl开头

plValue

 
阅读(1632) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~

chinaunix网友2009-10-28 20:57:18

你好,我想进入软件开发 学习c++ 麻烦你指教一下, 我的qq 532915863 就是c++应该学习哪些呢?