程序的健壮性

程序的健壮性


使用断言assert

断言assert是尽在debug版本起作用的宏, 它用于检查”不应该”发生的情况.

void* memcpy(void *pTo, const void* pFrom, size_t size)
{
    assert((pTo != NULL) && (pFrom != NULL);   //  使用断言
    byte* ptTo = (byte*)pTo;  //防止改变pTo的地址
    byte* ptFrom = (byte*)pFrom;  //防止改变pFrom的地址
    while (size-- > 0) {
        *ptTo++ = *btFrom++;
    }
    return pvTo;
}

在运行过程中, 如果assert的参数为假, 那么程序就会终止(一般还会出现提示对话, 说明在什么地方引发了assert).

使用断言assert的规则:

  1. 使用断言捕捉不应该发生的非法情况, 不要混淆非法情况和错误情况之间的区别, 后者是必然存在的, 并且是一定要做出处理的.
  2. 在函数的入口处, 使用断言检查参数的有效性.
  3. 在编写函数时, 要进行反复的考查, 并且自问: ” 我打算作那些设定?”, 一旦确定了假设, 就要使用断言对嘉定进行检查.
  4. 一般教科书都鼓励程序员们进行防错设计, 但要记住这种编码风格可能会隐瞒错误. 当进行防错设计时, 如果”不可能发生”的事情的确发生了, 则要使用断言进行报警.

if 语句:

布尔变量和零值的比较

if (flag)  OK
if (flag == TRUE)    if (flag == 1)   //  不良风格

整形变量和零值比较

if (value == 0)   OK
if (value)  //  不良风格

浮点变量和零值的比较
浮点变量都有精度限制, 所以避免将浮点变量用 ==或!=与数字比较, 应该设法转换成 >= 或者<=.

if (x == 0.0)   //  隐含错误的比较
if ((x>=-EPSINON) && (x<=EPSINON))   OK

指针变量和零值的比较

if (NULL == p)  OK
if (p == 0)      //  让人误解p是整形变量

使用const提高函数的健壮性

const更大的魅力是可以修饰函数的参数, 返回值, 甚至是函数的定义体.
1 用const修饰函数的参数

输出的参数, 不论是什么数据类型, 也不论是采用”指针传递”还是”引用传递”都不能加const修饰, 否则会该参数会失去输出功能.
const只能修饰输入参数
输入参数采用”指针传递”, 那么加const修饰可以防止意外地改动该指针, 起到保护作用.
如果输入参数是”值传递”, 由于函数将自动产生临时变量用于复制该参数, 该参数本来就无需保护, 所以不要加const.

为了提高效率, 可以将函数声明改动为void Fun(A &a), 因为”引用传递”仅借用一下参数的别名而已, 不需要产生临时变量.
但是这样会存在一个问题, “引用传递”有可能改变参数, 这不是我们期望的, 加上const修饰符就可以解决这个问题,
因此最后函数的声明会变成void Fun(const A &a);

2 用const修饰函数的返回值

如果给以”指针传递”方式的函数返回值加const修饰符, 那么返回的返回值(即指针)内容不能被修改,
该返回值只能被赋给加const修饰的同类型的指针.

const char* getString(void);
以下语句:char* str = getString();   // 编译错误
正确的写法是:const char* str = getString();

函数返回值采用”值传递”方式, 由于函数会把返回值复制到外部临时的存储单元中, 加const修饰符没有任何价值.
如:

不要把 int getInt(void)写成 const int getInt(void)

如果返回值不是内部数据类型, 将函数A getA(void)写成const A getA(void)会提高效率, 其中A是用户自定义类型. 小心, 一定要搞清楚函数究竟是想返回一个对象的”拷贝”还是仅返回”别名”就可以了, 否则程序会出错.

函数return语句不要返回指向”栈内存”的指针或者引用. 因为该内存在函数体结束后被自动销毁.

指针参数是如何传递参数的

如果函数的参数是一个指针, 那么不要指望用该指针去申请动态内存.

如:

void getMemory(char* p, int  num) 
{
    p = (char*)malloc(sizeof(char)*num);
    // p是实参的一个副本, 该行代码p申请了新的内存, 只是把p所指的内存地址改变了, 但是原来的实参丝毫未变
}
void test(void) 
{
     char* str = NULL;
     getMemory(str, 100);  
     //  str仍为NULL, 每执行一次getMemory就会造成一次内存泄漏, 因为没有free释放.
     strcpy(str, "hello");
}

编译器总是要为函数的每个参数制作临时副本, 指针p的副本是_p, 编译器使_p = p. 如果函数体的程序修改了_p的内容, 就导致了参数p的内容作相应的修改, 这就是指针可以用作输出的原因.
如果非要使用指针参数去申请内存, 那么就应该改用”二级指针”:

void getMemory(char** p, int num)
{
    *p = (char*)malloc(sizeof(char)*num);
}
void test(void) 
{
    char* str = NULL;
    getMemory(&str, 100);  //  此处是&str
    strcpy(str, "hello");
    free(str);  // free释放
}

当然也可以使用函数的返回值来传递动态内存:

char* getMemory(int num)
{
    char* p = (char*)malloc(sizeof(char)*num);
    return p;
}
void test(void) 
{
    char* str = getMemory(100); 
    strcpy(str, "hello");
    free(str);  // free释放
}

return

不要使用return返回”栈内存”的指针, 因为该内存在函数结束后自动消亡了.
如:

char* getMemory(void)
{
    char p[] = "hello world";  //
    return p;   //  编译器将提出警告
}
void test(void) 
{
    char* str = getMemory();   //str的内容是垃圾
    strcpy(str, "hello");
}

如果:

char* getString(void)
{
    char *p = "hello world";  //
    return p; 
}
void test(void) 
{
    char* str = getString();   
}

虽然不会出错, getString设计概念是错误的, 因为getString的”hello world”是字符串常量, 位于静态存储区, 在程序生命周期内恒定不变. 无论什么时候调用, 返回的始终是一个”只读”的内存块.

循环语句的效率

在C/C++中, for循环使用最高, while次之, do语句很少用.
循环语句的几个建议:

  1. 在多重循环中, 如果可能应当把最长的循环放在最内层, 最短的循环放在最外层, 以减少CPU跨切循环层的次数.
  2. 如果循环体内存存在逻辑判断, 并且循环次数很大, 宜将逻辑判断移到循环体的外面.

如: (以下如果N较小, 效率差别不大)

for (i=0; iif (condition)  
    //  多执行了N-1次判断, 总是进行逻辑判断, 打断了循环"流水线"作业, 使得编译器不能对循环进行优化处理, 降低了效率.
        doSomething();
    else {
        doOtherSomething();
    }
}
if (condition) {  // 效率高, 但程序不简洁
    for (i=0; idoSomething():
    }
}
else {
    for (i=0; idoSomething():
    }
}

for语句循环控制变量:

不可再for循环体中修改循环变量, 防止for循环失去控制.
建议for语句的循环控制变量的取值采用”半开半闭区间”写法.

欢迎关注微信
程序的健壮性_第1张图片

你可能感兴趣的:(C语言)