【高质量C/C++】6.函数设计

【高质量C/C++编程】—— 6. 函数设计

  函数是C/C++程序的基本功能单元,其重要性不言而喻。函数设计的细微缺点很容易导致该函数被错用,所以光使用函数的功能是远远不够的。

  函数接口的两个要素是参数和返回值。C语言中参数和返回值的传递方式有两种:值传递 和 指针传递。C++中多了一个引用传递,由于引用传递的性质和指针传递很像,但是使用方式和值传递很像,初学者容易对其混淆,不懂的同学们可以先阅读本章“第6段引用与指针的比较”。

一、参数的规则

规则

  1. 在函数声明中,参数的书写要完整,不要贪图省事只写参数类型而省略参数名字
  2. 如果函数没有参数,则用void填充
  3. 参数名字要有意义,顺序要恰当,一般遵从程序员的习惯
  4. 如果参数是指针,且仅作输入作用,则应在类型前加const,防止指针在函数体中被意外修改
  5. 如果参数是以值传递的方式传递对象,则使用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);						// 良好的风格

二、返回值的规则

规则

  1. 不要省略返回值的类型
  1. C语言中不加返回值类型的函数,统一按整型处理。容易让用户错误的认为是void类型
  2. C++中有严格的类型检查,理论上不用担心无返回值的函数,但是C++中可以使用C语言,这就产生了很多危险
void test(void);	// 良好的风格,返回类型是void
test(void);			// 不良的风格,返回值类型实际上是int
  1. 函数名字与返回值类型在语义上不可冲突
  1. 违反这条规则的是C标准库函数int getchar(void);,按名字的意思返回的是char类型,但实际上返回的是int类型。
  2. 在我们实现函数时,不要使用这种与返回值类型冲突的命名方式。
int GetInt(void);		// 良好的风格
double GetInt(void);	// 不良的风格,命名与实际返回值不同
  1. 不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return返回
  1. getchar函数的正常值是char类型字符的ASCII码值,而发生错误时会返回EOF(-1),它将正常值和错误标志放在一起返回。
  2. 在我们实现函数时,应将正常值放在参数中带回,而返回值只返回错误标志
int GetChar(void);		// 不良的风格,错误标志与正常值一起返回
int GetChar(char* ch);	// 良好的风格,错误标志由返回值返回,正常值由参数ch指针带回

建议

  1. 有些函数不需要返回值,但是为了增加灵活性,如支持链式表达,可以增加返回值
  2. 如果函数的返回值不是局部变量,可以使用引用类型返回以提高效率,若是局部变量就不可以使用引用类型返回
char* strcpy(char* dest, const char* src);	// 拷贝函数原型

// 引用类型返回非局部变量
int& GetInt(int* num)
{
    *num += 20;
    return *num;
}

// 错误的返回
int& GetInt(void)
{
    int num = 10;
    return num;		// 错误,返回值为局部变量
}

三、函数体实现规则

不同功能的函数其内部实现各不相同,但是我们可以在函数的入口处和出口处严格把关,从而提高函数质量

规则

  1. 在函数的入口处对参数的有效性进行检查

很多程序错误是由非法参数引起的,我们应该正确理解assert断言(本章第5段有详细描述)

// 对数组进行处理
int test(int* arr, int size)
{
    // 断言对arr数组和size数组长度进行有效性检查
    assert(arr != NULL && size >= 0);
    
    // 对数组长度为0进行检查
    if (size == 0)
    {
        return 0;
    }
   
    ... ...
        
    return 1;
}
  1. 在函数的出口处对return语句的正确性和效率进行检查
  1. 要搞清楚返回值究竟是值、指针还是引用
  2. return语句不可返回指向栈内存的指针或引用,在函数结束时内存空间会自动销毁
  3. 如果返回值是一个对象,要考虑return语句的效率
int* test1(void)
{
    int num = 10;
    return #	// 返回指向栈内存的指针
}

class ClassA {... ...};
int& GetClassA(ClassA& a)
{
    return a;		// 引用类型返回对象,提高效率
}

四、其他建议

建议

  1. 函数功能要单一,不要设计多用途的函数
  2. 函数体规模要小,尽量控制在50行代码之内
  3. 尽量避免函数带有记忆功能,相同输入参数应当产出相同的输出,带有记忆功能的函数其行为可能是难以预测的,不便于理解和维护。

在C/C++语言中,带有记忆功能的函数是由static修饰的局部变量来存储记忆值。我们尽量减少使用static局部变量

// 带有记忆功能的函数
int GetInt(int num)
{
    static int temp = 1;		// 静态局部变量存储记忆值
    temp += num;
    num = temp;
    return num;
}
  1. 不仅要检查输入参数的有效性,还要检查通过其他途径进入函数体内的变量的有效性,如全局变量,文件句柄等
  2. 用于处理错误的返回值一定要清除,让使用者不容易忽视或误解错误情况

五、使用断言

程序一般分为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可以帮助我们找到发生错误的原因。

很少有比跟踪到程序的断言,却不知道该断言的作用更让人更沮丧的事情了。你花了很多时间,不是为了排除错误,而只是为了弄清楚这个错误到底是什么。有时候,程序员偶尔还会设计出错误的断言。所以如果搞不清楚断言检查的是什么,就很难判断错误是出现在程序中,还是出现在断言中。幸运的是这个问题很好解决,只要加上清晰的注释即可。这本是显而易见的事情,可是很少有程序员这样做。难以理解的断言常常被程序员忽略,甚至删除,这是非常危险的。

规则

  1. 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在且一定要处理的
  2. 在函数的入口处检查参数的合法性
// 对数组进行处理
int test(int* arr, int size)
{
    // 数组arr为NULL或数组长度为负数是非法情况,需要断言处理
    assert(arr != NULL && size >= 0);
    
    // 数组长度为0属于错误情况,需要处理
    if (size == 0)
    {
        return 0;
    }
   
    ... ...
        
    return 1;
}

建议

  1. 在编写函数时需要进行反复考察,自问“我打算做哪些假定?”,一旦确定了假定,就使用断言对假定进行处理
  2. 一般教科书都鼓励程序员进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果不可能发生的事情发生了,需要进行断言处理进行报警。

六、引用与指针的比较

在C++的概念中,初学者容易把指针和引用混淆在一起。

  • 指针:指针变量的创建需要开辟内存,需要显示解引用才能访问指向内存的内容
  • 引用:引用类型的底层使用了指针实现,实际也要开辟内存。在使用时,可以直接通过变量访问内存内容,相当于对变量起了别名。

引用的一些规则:

  1. 引用被创建的同时必须初始化,而指针任何时候都可以初始化
  2. 没有对NULLnullptr的引用,必须引用合法的存储单元
  3. 一旦引用被初始化,就不能改变引用的关系
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;
}

猜你喜欢

转载自blog.csdn.net/weixin_52811588/article/details/127122349
今日推荐