宏定义的黑魔法——assert原理详解

Posted C_YCBX Py_YYDS

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了宏定义的黑魔法——assert原理详解相关的知识,希望对你有一定的参考价值。

assert在程序中的用法

assert 意为断言,顾名思义,可以将执行语句直接拦腰折断,我们先聊聊基本用法,再聊一聊需要注意在哪个时候使用。最后再深度剖析源代码实现。

怎么用?

基本用法:对前面的未知参数进行假设限定

#include<assert.h>
#include<stdlib.h>
#include<stdio.h>
int creat(int len)
    assert(len>0);	//一旦len不大于0则会导致程序中断
    int* ret = NULL;
    if((ret = malloc(sizeof(int)*len)==NULL)
        perror("malloc");
        exit(-1);
    
    return ret;


应该注意什么?

注意:assert的断言只能在debug时可用,一旦程序进入release发布状态,则assert将不会对程序进行阻断!assert实际上是一个很简单的宏定义,这个后面会解析。

为什么用?

由于assert用起来非常的简单,而且很直观,如果你有用过抛异常等等其他的处理方式,你会对assert感到非常舒服,因为它写起来相比其他的异常处理方式用法要简单太多太多,但很快也能发现它的局限性,assert在程序release后就无法发挥作用了,也就是说assert并不能增加你发布程序的健壮性!但是它能在调试的时候发挥巨大作用,能够很快的定位错误,进行更改,而且使用assert后代码可读性也大大增加。

​所以我个人认为,使用assert最大的原因在于可以让你写代码的逻辑连贯,而暂时不需要去考虑如何处理外界引发的错误,到了程序将要发布的阶段再根据断言对源代码进行一定的修改即可,如果你每次写一段代码就catch来考虑异常处理这样很难集中精神去干一件事。

什么时候使用?

根据为什么用,我们清楚了,assert适合我们在编写程序的时候进行逻辑断言,具体而言,是对外界未知条件的确定,从而使得程序逻辑严密。所以一般放在函数的开始部分用来对外界传入的参数进行限制,而如果是调用其他函数产生的错误,我们应该进行异常处理,而不是断言。

assert()的源代码解析

assert.h 文件完整代码:

以上代码仅仅作为展示,实际上这段代码中真正有用的部分就以下这段代码而已:

一、当定义了NDEGUB宏

首先我们看到预处理命令 #ifdef NDEGUG 这个 NDEBUG 宏,会在程序编译为release版本时编译器会自动定义 NDEBUG这个宏, 而debug版本不会定义,所以debug时只会执行 #else 里的语句,所以根据以上代码可知,一旦程序为release版本,则assert宏只会被展开为 ((void)0) 。示例如下:

void func(int len)
    assert(len>0); =>等价于 ((void)0);
    //而至于为什么要强转为void,因为void型,是不能用来赋值给其他任意类型
    //如:以下是会报错的
    int a = (void)0;

强转void避免了使用assert宏的返回值,实际上也不能使用它的返回值。

二、当未定义NDEBUG宏

而如果是debug版本则以上所用的 assert(len>0); 将转为以下代码:

基本的宏定义知识:在某个宏前面+’#’,则被替换时会在左右加上"",作为字符串。

void func()
    assert(len>0); => (void)((!!(len>0)) ||(_wassert(_CRT_WIDE(#len>0), _CRT_WIDE(__FILE__), (unsigned)(__LINE__)), 0) );

//简化整理得:(把与理解无关的宏定义去掉)
void func()
    (void)((!!(len>0))||(_wassert(#len,__FILE__,__LINE__),0));


我们最终把assert(expr);这个宏简化为了:

(void)( ( !!(expr) ) || ( _wassert( #expr,FILE,LINE ) , 0 ) );

我们以 || 为分隔,分为 Part1 和 Part2 来讲解

注意:这里的 expr 指代的是 assert(expr) 里面传进来的参数。

Part1!!(expr),即对表达式求两次反,如果expr为真,则!!(expr)为1;如果expr为假,则expr为0。两次求反,可以防止用户恶意传的值(非0、非1的值),!!(expr)要么为1,要么为0,不能为其他值了。

Part2(_wassert(表达式字符串, 文件名, 行号), 0),#expr中#是一个构串符,将宏参数转成一个字符串,__FILE__和__LINE__分别对应包含assert宏的文件名和assert宏在该文件中的行号。这里_wassert的声明如下:

    _ACRTIMP void __cdecl _wassert(
        _In_z_ wchar_t const* _Message,
        _In_z_ wchar_t const* _File,
        _In_   unsigned       _Line
        );

这函数的定义由运行时环境提供,没有源代码,知道下面几点就可以了。

(1)_wassert函数在part1为假,才会被调用;part1为真时,不会被调用。根据 || 操作符的短路特性。
(2)_wassert 函数和后面的0,构成一个逗号表达式,因此part2的返回值是0。因此 ( (part1) || (part2) ) 的返回值为0或1取决于part1。

三、assert执行过程总结

经过Part1和Part2的分析,我们清楚了assert宏的实现原理:

  1. 在debug下传入宏参数,展开为一个或表达式,而release下传入,则只会产生 ((void)0)
  2. 根据或表达式的判断结果,如果第一个判断结果为1,则或短路,不会再执行后面的语句了。
  3. 如果第一个判断结果为0,则继续执行下一段语句,下一段语句调用一个函数来中断程序的执行,并打印出相应的信息。

根据原理实现简单的assert()宏定义

一、宏定义实现

#ifdef NDEBUG   //当程序为发布状态时直接定义为下面这个空语句宏
    #define assert(expression) ((void)0)
#else

#define assert(expression) (void)( \\
        (!!(expression))||         \\
        (_assert(#expression,__FILE__,(unsigned)(__LINE__)),0)\\
        )

#endif

二、打印输出函数实现

除了打印以外,我们还需要把程序中断(退出),用exit退出即可。

void __cdecl _assert(//错误信息打印
        const char *_Message,
        const char * _File,
        unsigned       _Line)
    printf("Assertion failed!\\n");

    fputc('\\n',stdout);

    printf("File: %s, Line %d\\n",_File,_Line);

    fputc('\\n',stdout);

    printf("Expression: %s",_Message);
    exit(3);

三、最终效果

实现源码:

测试结果:

测试代码:

#include "assert.h"

int main() 
    int a = 32;
    int b = 322;
    assert(a==b);
    while (1);//一旦a==b为假,则会一直无限循环

执行结果:

以上是关于宏定义的黑魔法——assert原理详解的主要内容,如果未能解决你的问题,请参考以下文章

Objective C block背后的黑魔法

meta.Vectors 背后的黑魔法是啥?

Python都有哪些黑魔法?

iOS H5容器的一些探究:iOS 下的黑魔法 NSURLProtocol

OpenCV中的带参数宏定义CV_Assert()的作用

为啥在发布版本中断言宏的定义不能只是`#define assert(expression) 0`?