【高质量C/C++编程】—— 6. 函数设计
函数是C/C++程序的基本功能单元,其重要性不言而喻。函数设计的细微缺点很容易导致该函数被错用,所以光使用函数的功能是远远不够的。
函数接口的两个要素是参数和返回值。C语言中参数和返回值的传递方式有两种:值传递 和 指针传递。C++中多了一个引用传递,由于引用传递的性质和指针传递很像,但是使用方式和值传递很像,初学者容易对其混淆,不懂的同学们可以先阅读本章“第6段引用与指针的比较”。
规则:
void填充const,防止指针在函数体中被意外修改const &方式传递,这样可以省去临时对象的构造和析构过程,从而提高效率void SetValue(int width, int height); // 良好的风格
void SetValue(int, int); // 不良的风格
float GetValue(void); // 良好的风格
float GetValue(); //不良的风格
void StringCopy(char* str1, char* str2); // 不良的风格,参数名意义不足
void StringCopy(char* destination, char* source); // 不良风格,用来输入的指针source没有用const修饰
void StringCopy(char* destination, const char* source); // 良好的风格
// 类ClassA
class ClassA {... ...}; // 不良的风格
void CopyObject(const ClassA& a); // 良好的风格
规则:
- C语言中不加返回值类型的函数,统一按整型处理。容易让用户错误的认为是
void类型- C++中有严格的类型检查,理论上不用担心无返回值的函数,但是C++中可以使用C语言,这就产生了很多危险
void test(void); // 良好的风格,返回类型是void
test(void); // 不良的风格,返回值类型实际上是int
- 违反这条规则的是C标准库函数
int getchar(void);,按名字的意思返回的是char类型,但实际上返回的是int类型。- 在我们实现函数时,不要使用这种与返回值类型冲突的命名方式。
int GetInt(void); // 良好的风格
double GetInt(void); // 不良的风格,命名与实际返回值不同
return返回
- 在
getchar函数的正常值是char类型字符的ASCII码值,而发生错误时会返回EOF(-1),它将正常值和错误标志放在一起返回。- 在我们实现函数时,应将正常值放在参数中带回,而返回值只返回错误标志
int GetChar(void); // 不良的风格,错误标志与正常值一起返回
int GetChar(char* ch); // 良好的风格,错误标志由返回值返回,正常值由参数ch指针带回
建议:
char* strcpy(char* dest, const char* src); // 拷贝函数原型
// 引用类型返回非局部变量
int& GetInt(int* num)
{
*num += 20;
return *num;
}
// 错误的返回
int& GetInt(void)
{
int num = 10;
return num; // 错误,返回值为局部变量
}
不同功能的函数其内部实现各不相同,但是我们可以在函数的入口处和出口处严格把关,从而提高函数质量
规则:
很多程序错误是由非法参数引起的,我们应该正确理解
assert断言(本章第5段有详细描述)
// 对数组进行处理
int test(int* arr, int size)
{
// 断言对arr数组和size数组长度进行有效性检查
assert(arr != NULL && size >= 0);
// 对数组长度为0进行检查
if (size == 0)
{
return 0;
}
... ...
return 1;
}
- 要搞清楚返回值究竟是值、指针还是引用
return语句不可返回指向栈内存的指针或引用,在函数结束时内存空间会自动销毁- 如果返回值是一个对象,要考虑return语句的效率
int* test1(void)
{
int num = 10;
return # // 返回指向栈内存的指针
}
class ClassA {... ...};
int& GetClassA(ClassA& a)
{
return a; // 引用类型返回对象,提高效率
}
建议:
在C/C++语言中,带有记忆功能的函数是由
static修饰的局部变量来存储记忆值。我们尽量减少使用static局部变量
// 带有记忆功能的函数
int GetInt(int num)
{
static int temp = 1; // 静态局部变量存储记忆值
temp += num;
num = temp;
return num;
}
程序一般分为Debug版本和Release版本,Debug版本用于内部调试,Release版本发行给用户使用。
断言assert是仅在Debug版本起作用的宏,它用于检查不应该发生的情况,如果运行过程中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;
}
assert不是一个仓促拼凑起来的宏,为了不在程序的Debug版本和Release版本引起差别,assert不应该产生任何副作用。所以assert不是函数,而是宏。程序员可以把assert看成一个在任何系统状态下都可以安全使用的无害测试手段。如果程序在assert处终止了,并不是说该assert的函数有错误,而是调用者出现了差错,assert可以帮助我们找到发生错误的原因。
很少有比跟踪到程序的断言,却不知道该断言的作用更让人更沮丧的事情了。你花了很多时间,不是为了排除错误,而只是为了弄清楚这个错误到底是什么。有时候,程序员偶尔还会设计出错误的断言。所以如果搞不清楚断言检查的是什么,就很难判断错误是出现在程序中,还是出现在断言中。幸运的是这个问题很好解决,只要加上清晰的注释即可。这本是显而易见的事情,可是很少有程序员这样做。难以理解的断言常常被程序员忽略,甚至删除,这是非常危险的。
规则:
// 对数组进行处理
int test(int* arr, int size)
{
// 数组arr为NULL或数组长度为负数是非法情况,需要断言处理
assert(arr != NULL && size >= 0);
// 数组长度为0属于错误情况,需要处理
if (size == 0)
{
return 0;
}
... ...
return 1;
}
建议:
在C++的概念中,初学者容易把指针和引用混淆在一起。
引用的一些规则:
- 引用被创建的同时必须初始化,而指针任何时候都可以初始化
- 没有对
NULL或nullptr的引用,必须引用合法的存储单元- 一旦引用被初始化,就不能改变引用的关系
void test(void)
{
int a = 0;
int b = 0;
int& ra = a; // 引用
int* pb = &b; // 指针
std::cout << ra << std::endl; // 引用类型变量可以当作变量本身使用
std::cout << *pb << std::endl; // 指针类型必须显示解引用使用
int& raa = ra; // 没有二级引用,引用直接当变量名使用
int** ppb = &pb; // 有二级指针,指针有严格的内外层之分
}
// 指针参数
int AddOfPionter(int* a, int* b)
{
return *a + *b;
}
// 引用参数
int AddOfLead(int& a, int& b)
{
return a + b;
}
int main(void)
{
int a = 10;
int b = 100;
AddOfPionter(&a, &b); // 传递指针
AddOfLead(a, b); // 传递引用
return 0;
}