• 【高质量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;
    }
    
  • 相关阅读:
    API接口获取商品评论
    python中的闭包和装饰器的使用
    python+vue+elementui毕业设计选题系统
    优秀公共DNS服务器推荐
    TCP协议灵魂之问
    阿里三面:CAP和BASE理论了解么?可以结合实际案例说下?
    Turtlebot3-burger入门教程#foxy版#-雷达测试
    面试经典刷题)挑战一周刷完150道-Python版本-第2天(22个题)
    学点设计模式,盘点Spring等源码中与设计模式的那些事,给所有的设计模式来个大总结
    HBase海量数据高效入仓解决方案
  • 原文地址:https://blog.csdn.net/weixin_52811588/article/details/127122349