函数设计
函数是C++/C 程序的基本功能单元,其重要性不言而喻。函数设计的细微缺点很容易导致该函数被错用,所以光使函数的功能正确是不够的。
提高函数这个功能体我们要从构造函数的每一个细节入手,如函数名和参数命名规则,接口设计,传送参数的方式,函数的返回值,内部实现细节,内存分配等等。下面我们将一一考察。
1.关于命名
参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。
如果函数没有参数,则用void 填充。{笔记:容易忽视}
2.接口设计
参数命名要恰当,顺序要合理。一般地,应将目的参数放在前面,源参数放在后面。
如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。
例如:void StringCopy(char *strDestination,const char *strSource);
如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
避免函数有太多的参数,参数个数尽量控制在5个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。笔记:可以使用数组或结构体解决
尽量不要使用类型和数目不确定的参数,那样容易丧失了严格的类型安全检查。C 标准库函数printf 是采用不确定参数的典型代表,其原型为:int printf(const chat *format[, argument]…);
3.返回值的规则
(1)不要省略返回值的类型;
(2)函数名字与返回值类型在语义上不可冲突。违反这条规则的典型代表是C 标准库函数getchar。
(3)不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return 语句返回。为了避免出现误解,我们应该将正常值和错误标志分开。即:正常值用输出参数获得,而错误标志用return 语句返回。
(4)有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。
例如字符串拷贝函数strcpy 的原型:
char *strcpy(char *strDest,const char *strSrc);
strcpy 函数将strSrc 拷贝至输出参数strDest 中,同时函数的返回值又是strDest。这样做并非多此一举,可以获得如下灵活性:
char str[20];
int length = strlen( strcpy(str, “Hello World”) );
(5)如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出错。
4.函数内部实现的规则
不同功能的函数其内部实现各不相同,看起来似乎无法就“内部实现”达成一致的观点。但根据经验,我们可以在函数体的“入口处”和“出口处”从严把关,从而提高函数的质量。
【规则】在函数体的“入口处”,对参数的有效性进行检查。
很多程序错误是由非法参数引起的,我们应该充分理解并正确使用“断言”(assert)来防止此类错误。
【规则】在函数体的“出口处”,对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 的变量不存在构造函数与析构函数,虽然该“临时变量的语法”不会提高多少效率,但是程序更加简洁易读。
5.其它建议
【建议】函数的功能要单一
【建议】函数体的规模要小,尽量控制在50 行代码之内。
【建议】尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在C/C++语言中,函数的static 局部变量是函数的“记忆”存储器。建议尽量少用static 局部变量,除非必需。
【建议】不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。
【建议】用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误情况。
6. 引用与指针的比较
引用就是被创建对象的别名,指针是指向这个被创建对象的!
引用的一些规则如下:
(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++语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用”这东西?
答案是“用适当的工具做恰如其分的工作”。指针能够毫无约束地操作内存中的如何东西,尽管指针功能强大,但是非常危险。就象一把刀,它可以用来砍树、裁纸、修指甲、理发等等,谁敢这样用?如果的确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外。比如说,某人需要一份证明,本来在文件上盖上公章的印子就行了,如果把取公章的钥匙交给他,那么他就获得了不该有的权利。