写点什么

C++ 学习 ------cassert 头文件的作用与源码学习

作者:桑榆
  • 2022 年 9 月 01 日
    广东
  • 本文字数:3153 字

    阅读完需:约 10 分钟

引言

cassert 是对 assert.h 头文件的封装,里面定义了一个 assert 函数,可以用于异常判断,那么它的使用方式及实现原理是怎么样的呢?我们一起来学习一下。

cassert 的学习

一个小例子

我们通过下面这个例子来学习 cassert 头文件的使用:

#include <iostream>//#define NDEBUG#include <cassert>
void printNum(int* num){ assert(num != nullptr); std::cout << "num:" << *num << std::endl;}
int main(){ int a = 333; int *b = nullptr, *c = nullptr;
b = &a;
printNum(b); printNum(c);
return 0;}
复制代码

使用 g++编译执行:g++ -g test.cpp -o test && ./test得到结果:

num:333test: test.cpp:14: void printNum(int*): Assertion `num != nullptr' failed.Aborted (core dumped)
复制代码

可见,在出现异常的位置打印了对应的可执行文件名、源文件名、函数信息以及 assert 判断失败的原因,然后出现了主动 aborted 的报错。

第二次我们在#include <cassert>之前增加定义#define NDEBUG,再进行编译执行,得到如下结果:

num:333Segmentation fault (core dumped)
# 出现段错误,使用gdb进行调试gdb ./test
# 结果如下:(gdb) rStarting program: /home/sangyu/WorkSpace/Code/C++/CPPStudy/CHeaderFile/0_assert.h/testnum:333
Program received signal SIGSEGV, Segmentation fault.0x00005555555549a0 in printNum (num=0x0) at test.cpp:1515 std::cout << "num:" << *num << std::endl;(gdb) bt#0 0x00005555555549a0 in printNum (num=0x0) at test.cpp:15#1 0x0000555555554a12 in main () at test.cpp:25(gdb) display num1: num = (int *) 0x0
复制代码

这一次只是出现了段错误,并没有对应的报错信息打印,这是因为在#include <cassert>之前增加定义#define NDEBUG,可以屏蔽后续 assert 的使用。通过 gdb 调试结果,我们可以看到报错出现的对应行号,以及对应指针 num 为 0,这样也能找到问题的根本原因。

cassert 的源码实现

参考文件

http://www.aospxref.com/android-12.0.0_r3/xref/bionic/libc/include/assert.h

43  #include <sys/cdefs.h>44  45  #undef assert46  #undef __assert_no_op47  48  /** Internal implementation detail. Do not use. */49  #define __assert_no_op __BIONIC_CAST(static_cast, void, 0)50  51  #ifdef NDEBUG52  # define assert(e) __assert_no_op53  #else54  # if defined(__cplusplus) || __STDC_VERSION__ >= 199901L55  #  define assert(e) ((e) ? __assert_no_op : __assert2(__FILE__, __LINE__, __PRETTY_FUNCTION__, #e))56  # else57  /**58   * assert() aborts the program after logging an error message, if the59   * expression evaluates to false.60   *61   * On Android, the error goes to both stderr and logcat.62   */63  #  define assert(e) ((e) ? __assert_no_op : __assert(__FILE__, __LINE__, #e))64  # endif65  #endif66  67  #if !defined(__cplusplus) && __STDC_VERSION__ >= 201112L68  # undef static_assert69  # define static_assert _Static_assert70  #endif71  72  __BEGIN_DECLS73  74  /**75   * __assert() is called by assert() on failure. Most users want assert()76   * instead, but this can be useful for reporting other failures.77   */78  void __assert(const char* __file, int __line, const char* __msg) __noreturn;79  80  /**81   * __assert2() is called by assert() on failure. Most users want assert()82   * instead, but this can be useful for reporting other failures.83   */84  void __assert2(const char* __file, int __line, const char* __function, const char* __msg) __noreturn;85  86  __END_DECLS
复制代码

可以看到,在 include 这个头文件之前,如果定义 NDEBUG,那么 assert 宏将被定义为

# define assert(e) __assert_no_op,追踪一下这个的实现

#define __assert_no_op __BIONIC_CAST(static_cast, void, 0)继续追踪

http://www.aospxref.com/android-12.0.0_r3/xref/bionic/libc/include/sys/cdefs.h

57  #if defined(__cplusplus)//定义了CPP,所以走这里,即static_cast<void>(0)其实是一句没有意义的语句,即什么都不做58  #define __BIONIC_CAST(_k,_t,_v) (_k<_t>(_v))59  #else60  #define __BIONIC_CAST(_k,_t,_v) ((_t) (_v))61  #endif
复制代码

那么正常走的话又会是怎么样呢?通过源码可以看到如果

defined(__cplusplus) || __STDC_VERSION__ >= 199901L,那么就会将 assert 宏定义为

# define assert(e) ((e) ? __assert_no_op : __assert2(__FILE__, __LINE__, __PRETTY_FUNCTION__, #e))即满足条件不做任何处理,不满足就调用__assert2函数

或者最后走到# define assert(e) ((e) ? __assert_no_op : __assert(__FILE__, __LINE__, #e))即调用__assert函数

__assert2 与__assert 函数

31  #include <assert.h>32  33  #include <async_safe/log.h>34  35  void __assert(const char* file, int line, const char* failed_expression) {36    async_safe_fatal("%s:%d: assertion "%s" failed", file, line, failed_expression);37  }38  39  void __assert2(const char* file, int line, const char* function, const char* failed_expression) {40    async_safe_fatal("%s:%d: %s: assertion "%s" failed", file, line, function, failed_expression);41  }
复制代码

通过源码来看,实际上这两个函数都是调用了函数async_safe_fatal()来打印报错信息:

  • __FILE__是当前的文件名

  • __LINE__是当前的报错函数

  • __PRETTY_FUNCTION__是当前的函数名,可以获取更为详细的函数信息

  • #e 是将后面的参数进行字符串化操作,即"e"这样看和我们之前看到的报错打印信息对应上:

test: test.cpp:14: void printNum(int*): Assertion `num != nullptr' failed.
复制代码

最后我们看看async_safe_fatal的实现:

//http://www.aospxref.com/android-12.0.0_r3/xref/bionic/libc/async_safe/include/async_safe/log.h
45  // Formats a message to the log (priority 'fatal'), then aborts.46  // Implemented as a macro so that async_safe_fatal isn't on the stack when we crash:47  // we appear to go straight from the caller to abort, saving an uninteresting stack48  // frame.49  #define async_safe_fatal(...) \50   do { \51   async_safe_fatal_no_abort(__VA_ARGS__); \52   abort(); \53   } while (0) \
//http://www.aospxref.com/android-12.0.0_r3/xref/bionic/libc/async_safe/async_safe_log.cpp573  void async_safe_fatal_no_abort(const char* fmt, ...) {574   va_list args;575   va_start(args, fmt);576   async_safe_fatal_va_list(nullptr, fmt, args);577   va_end(args);578  }
534  int async_safe_format_log_va_list(int priority, const char* tag, const char* format, va_list args) {535   ErrnoRestorer errno_restorer;536   char buffer[1024];537   BufferOutputStream os(buffer, sizeof(buffer));538   out_vformat(os, format, args);539   return async_safe_write_log(priority, tag, buffer);//后续还有多个函数调用,与操作系统相关540  }
复制代码

调用async_safe_fatal_no_abort打印信息后主动调用了 abort,后续函数调用写入标准输出的部分我们就不再进行跟踪了。

综上,我们分析了 cassert 的源码,搞明白了其用法及原理,你明白了吗?

发布于: 2022 年 09 月 01 日阅读数: 24
用户头像

桑榆

关注

北海虽赊,扶摇可接;东隅已逝,桑榆非晚! 2020.02.29 加入

Android手机厂商-相机软件系统工程师 爬山/徒步/Coding

评论

发布
暂无评论
C++学习------cassert头文件的作用与源码学习_c++_桑榆_InfoQ写作社区