C语言断言assert-从源码解析到熟练使用_c assert,撸了郭霖大神写的Framework源码笔记

#include <assert.h>
#include <stdio.h>

int main()
{
int a;
char str[50];

printf(“请输入一个整数值: “);
scanf(”%d”, &a);
assert(a >= 10);
printf(“输入的整数是: %d\n”, a);

printf(“请输入字符串: “);
scanf(”%s”, str);
assert(str != NULL);
printf(“输入的字符串是: %s\n”, str);

return(0);
}

让我们在交互模式下编译并运行上面的程序,如下所示:

请输入一个整数值: 11
输入的整数是: 11
请输入字符串: runoob
输入的字符串是: runoob

C 库宏 - assert() 源码

assert() 源码很简单,包括assert.h和assert.c

assert.h的源码

/* assert.h - ANSI standard assert functions header */

/* Copyright 1992 Wind River Systems, Inc. */

/*
modification history

01e,13nov92,smb fixed assert macro to not generate warnings
01d,22sep92,rrr added support for c++
01c,20jul92,smb added __assert extern.
01b,04jul92,jcf cleaned up.
01a,03jul92,smb written.
*/

#ifdef __cplusplus
extern “C” {
#endif

#include “types/vxANSI.h”

#undef assert
#ifdef NDEBUG
#define assert(ignore) ((void) 0)
#else /* turn debugging on */

#define _ASSERT_STR(z) _ASSERT_TMP(z)
#define _ASSERT_TMP(z) #z

#if defined(__STDC__) || defined(__cplusplus)
extern void __assert (const char *msg);
#else
extern void __assert ();
#endif

#define assert(test) ((void)
((test) ? ((void) 0) :
__assert("Assertion failed: “#test”, file "
__FILE__ “, line “_ASSERT_STR(__LINE__)”\n”)))
#endif /* NDEBUG */

#ifdef __cplusplus
}
#endif

assert.c的源码

/* assert.c - ANSI standard assert function */

/* Copyright 1992 Wind River Systems, Inc. */

/*
modification history

01c,24oct92,smb removed some redundant documentation.
01b,20sep92,smb documentation additions.
01a,20jul92,smb written.
*/

/*
DESCRIPTION

INCLUDE FILES: stdio.h, stdlib.h, assert.h

SEE ALSO: American National Standard X3.159-1989
NOMANUAL
*/

#include “vxWorks.h”
#include “assert.h”
#include “stdio.h”
#include “stdlib.h”

/******************************************************************************
*
* __assert - function called by the assert macro.
*
* INCLUDE: stdio.h assert.h
*
* RETURNS: never returns
* NOMANUAL
*/
void __assert
(
const char *msg /* message string */
)
{
fdprintf(2, “%s\n”, CHAR_FROM_CONST (msg)); /* print msg to error stream */
abort();
}

主要的代码如下

#define assert(test) ((void)
((test) ? ((void) 0) :
__assert("Assertion failed: “#test”, file "
__FILE__ “, line “_ASSERT_STR(__LINE__)”\n”)))

可以看出assert是一个宏定义,在这个宏定义中调用了一个函数 __assert()。

assert(expression)中expression为真时,不发生什么;当expression为假时则调用__assert()函数。

下面我们看看__assert()函数的具体实现

void __assert
(
const char *msg /* message string */
)
{
fdprintf(2, “%s\n”, CHAR_FROM_CONST (msg)); /* print msg to error stream */
abort();
}

__assert()函数调用了fdprintf()函数和abort()函数。fdprintf()函数用来打印信息,abort()函数用来终止程序执行。

/*******************************************************************************
*
* fdprintf - write a formatted string to a file descriptor
*
* This routine writes a formatted string to a specified file descriptor. Its
* function and syntax are otherwise identical to printf().
*
* RETURNS: The number of characters output, or ERROR if there is an error
* during output.
*
* SEE ALSO: printf()
*
* VARARGS2
*/

int fdprintf
(
int fd, /* file descriptor to write to */
const char * fmt, /* format string to write */
… /* optional arguments to format */
)
{
va_list vaList; /* traverses argument list */
int nChars;

va_start (vaList, fmt);
nChars = fioFormatV (fmt, vaList, printbuf, fd);
va_end (vaList);

return (nChars);
}

void abort (void)
{
raise (SIGABRT);
exit (EXIT_FAILURE);
}

/*******************************************************************************
*
* raise - send a signal to the caller’s task
*
* This routine sends the signal to the task invoking the call.
*
* RETURNS: OK (0), or ERROR (-1) if the signal number or task ID is invalid.
*
* ERRNO: EINVAL
*/

int raise
(
int signo /* signal to send to caller’s task */
)
{

return (kill ((int) taskIdCurrent, signo));
}

/*******************************************************************************
*
* exit - exit a task (ANSI)
*
* This routine is called by a task to cease to exist as a task. It is
* called implicitly when the “main” routine of a spawned task is exited.
* The parameter will be stored in the WIND_TCB for
* possible use by the delete hooks, or post-mortem debugging.
*
* ERRNO: N/A
*
* SEE ALSO: taskDelete(),
* .I “American National Standard for Information Systems -”
* .I “Programming Language - C, ANSI X3.159-1989: Input/Output (stdlib.h),”
* .pG “Basic OS”
*/

void exit
(
int code /* code stored in TCB for delete hooks */
)
{
taskIdCurrent->exitCode = code; /* store the exit code */

taskLock (); /* LOCK PREEMPTION */

taskIdCurrent->options |= VX_UNBREAKABLE; /* mark as unbreakable */

if (taskBpHook != NULL) /* call the debugger hook */
(* taskBpHook) (taskIdCurrent); /* to remove all breakpoints */

taskUnlock (); /* UNLOCK PREEMPTION */

taskDestroy (0, TRUE, WAIT_FOREVER, FALSE); /* self destruct */
}

assert运用

对于断言,相信大家都不陌生,大多数编程语言也都有断言这一特性。简单地讲,断言就是对某种假设条件进行检查。在 C 语言中,断言被定义为宏的形式(assert(expression)),而不是函数,其原型定义在<assert.h>文件中。其中,assert 将通过检查表达式 expression 的值来决定是否需要终止执行程序。也就是说,如果表达式 expression 的值为假(即为 0),那么它将首先向标准错误流 stderr 打印一条出错信息,然后再通过调用 abort 函数终止程序运行;否则,assert 无任何作用。

默认情况下,assert 宏只有在 Debug 版本(内部调试版本)中才能够起作用,而在 Release 版本(发行版本)中将被忽略。当然,也可以通过定义宏或设置编译器参数等形式来在任何时候启用或者禁用断言检查(不建议这么做)。同样,在程序投入运行后,最终用户在遇到问题时也可以重新起用断言。这样可以快速发现并定位软件问题,同时对系统错误进行自动报警。对于在系统中隐藏很深,用其他手段极难发现的问题也可以通过断言进行定位,从而缩短软件问题定位时间,提高系统的可测性。

尽量利用断言来提高代码的可测试性

在讨论如何使用断言之前,先来看下面一段示例代码:

void *Memcpy(void *dest, const void *src, size_t len)
{
char *tmp_dest = (char *)dest;
char *tmp_src = (char *)src;
while(len --)
*tmp_dest ++ = *tmp_src ++;
return dest;
}

对于上面的 Memcpy 函数,毋庸置疑,它能够通过编译程序的检查成功编译。从表面上看,该函数并不存在其他任何问题,并且代码也非常干净。

但遗憾的是,在调用该函数时,如果不小心为 dest 与 src 参数错误地传入了 NULL 指针,那么问题就严重了。轻者在交付之前这个潜在的错误导致程序瘫痪,从而暴露出来。否则,如果将该程序打包发布出去,那么所造成的后果是无法估计的。

由此可见,不能够简单地认为“只要通过编译程序成功编译的就都是安全的程序”。当然,编译程序也很难检查出类似的潜在错误(如所传递的参数是否有效、潜在的算法错误等)。面对这类问题,一般首先想到的应该是使用最简单的if语句进行判断检查,如下面的示例代码所示:

void *Memcpy(void *dest, const void *src, size_t len)
{
if(dest == NULL)
{
fprintf(stderr,“dest is NULL\n”);
abort();
}
if(src == NULL)
{
fprintf(stderr,“src is NULL\n”);
abort();
}
char *tmp_dest = (char *)dest;
char *tmp_src = (char *)src;
while(len --)
*tmp_dest ++ = *tmp_src ++;
return dest;
}

现在,通过“if(destNULL)与if(data-srcNULL)”判断语句,只要在调用该函数的时候为 dest 与 src 参数错误地传入了NULL指针,这个函数就会检查出来并做出相应的处理,即先向标准错误流 stderr 打印一条出错信息,然后再调用 abort 函数终止程序运行。

从表面看来,上面的解决方案应该堪称完美。但是,随着函数参数或需要检查的表达式不断增多,这种检查测试代码将占据整个函数的大部分(这一点从上面的 Memcpy 函数中就不难看出)。这样代码看起来非常不简洁,甚至可以说很“糟糕”,而且也降低了函数的执行效率。

面对上面的问题,或许可以利用 C 的预处理程序有条件地包含或不包含相应的检查部分进行解决,如下面的代码所示:

void *MemCopy(void *dest, const void *src, size_t len)
{
#ifdef DEBUG
if(dest == NULL)
{
fprintf(stderr,“dest is NULL\n”);
abort();
}
if(src == NULL)
{
fprintf(stderr,“src is NULL\n”);
abort();
}
#endif
char *tmp_dest = (char *)dest;
char *tmp_src = (char *)src;
while(len --)
*tmp_dest ++ = *tmp_src ++;
return dest;
}

这样,通过条件编译“#ifdef DEBUG”来同时维护同一程序的两个版本(内部调试版本与发行版本),即在程序编写过程中,编译其内部调试版本,利用其提供的测试检查代码为程序自动查错。而在程序编完之后,再编译成发行版本。

上面的解决方案尽管通过条件编译“#ifdef DEBUG”能产生很好的结果,也完全符合我们的程序设计要求,但是仔细观察会发现,这样的测试检查代码显得并不那么友好,当一个函数里这种条件编译语句很多时,代码会显得有些浮肿,甚至有些糟糕。

因此,对于上面的这种情况,多数程序员都会选择将所有的调试代码隐藏在断言 assert 宏中。其实,assert 宏也只不过是使用条件编译“#ifdef”对部分代码进行替换,利用 assert 宏,将会使代码变得更加简洁,如下面的示例代码所示:

void *MemCopy(void *dest, const void *src, size_t len)
{
assert(dest != NULL && src !=NULL);
char *tmp_dest = (char *)dest;
char *tmp_src = (char *)src;
while(len --)
*tmp_dest ++ = *tmp_src ++;
return dest;
}

现在,通过“assert(dest !=NULL&&src !=NULL)”语句既完成程序的测试检查功能(即只要在调用该函数的时候为 dest 与 src 参数错误传入 NULL 指针时都会引发 assert),与此同时,对 MemCopy 函数的代码量也进行了大幅度瘦身,不得不说这是一个两全其美的好办法。

实际上,在编程中我们经常会出于某种目的(如把 assert 宏定义成当发生错误时不是中止调用程序的执行,而是在发生错误的位置转入调试程序,又或者是允许用户选择让程序继续运行等)需要对 assert 宏进行重新定义。

但值得注意的是,不管断言宏最终是用什么样的方式进行定义,其所定义宏的主要目的都是要使用它来对传递给相应函数的参数进行确认检查。如果违背了这条宏定义原则,那么所定义的宏将会偏离方向,失去宏定义本身的意义。与此同时,为不影响标准 assert 宏的使用,最好使用其他的名字。例如,下面的示例代码就展示了用户如何重定义自己的宏 ASSERT:

/*使用断言测试*/
#ifdef DEBUG
/*处理函数原型*/
void Assert(char * filename, unsigned int lineno);
#define ASSERT(condition)
if(condition)
NULL;
else
Assert(__FILE__ , __LINE__)
/*不使用断言测试*/
#else
#define ASSERT(condition) NULL
#endif
void Assert(char * filename, unsigned int lineno)
{
fflush(stdout);
fprintf(stderr,“\nAssert failed: %s, line %u\n”,filename, lineno);
fflush(stderr);
abort();
}

如果定义了 DEBUG,ASSERT 将被扩展为一个if语句,否则执行“#define ASSERT(condition) NULL”替换成 NULL。

这里需要注意的是,因为在编写 C 语言代码时,在每个语句后面加一个分号“;”已经成为一种约定俗成的习惯,因此很有可能会在“Assert(FILELINE)”调用语句之后习惯性地加上一个分号。实际上并不需要这个分号,因为用户在调用 ASSERT 宏时,已经给出了一个分号。面对这种问题,我们可以使用“do{}while(0)”结构进行处理,如下面的代码所示:

#define ASSERT(condition)
do{
if(condition)
NULL;
else
Assert(__FILE__ , __LINE__);
}while(0)
现在,将不再为分号“;”而担心了,调用示例如下:
void Test(unsigned char *str)
{
ASSERT(str != NULL);
/*函数处理代码*/
}
int main(void)
{
Test(NULL);
return 0;
}

很显然,因为调用语句“Test(NULL)”为参数 str 错误传入一个 NULL 指针的原因,所以 ASSERT 宏会自动检测到这个错误,同时根据宏 FILELINE 所提供的文件名和行号参数在标准错误输出设备 stderr 上打印一条错误消息,然后调用 abort 函数中止程序的执行。运行结果如图 1 所示。

img
图 1 调用自定义 ASSERT 宏的运行结果

如果这时候将自定义 ASSERT 宏替换成标准 assert 宏结果会是怎样的呢?如下面的示例代码所示:

void Test(unsigned char *str)
{
assert(str != NULL);
/*函数处理代码*/
}

毋庸置疑,标准 assert 宏同样会自动检测到这个 NULL 指针错误。与此同时,标准 assert 宏除给出以上信息之外,还能够显示出已经失败的测试条件。运行结果如图 2 所示。

img
图 2 调用标准 assert 宏的运行结果

从上面的示例中不难发现,对标准的 assert 宏来说,自定义的 ASSERT 宏将具有更大的灵活性,可以根据自己的需要打印输出不同的信息,同时也可以对不同类型的错误或者警告信息使用不同的断言,这也是在工程代码中经常使用的做法。当然,如果没有什么特殊需求,还是建议使用标准 assert 宏。

尽量在函数中使用断言来检查参数的合法性

在函数中使用断言来检查参数的合法性是断言最主要的应用场景之一,它主要体现在如下 3 个方面:

  1. 在代码执行之前或者在函数的入口处,使用断言来检查参数的合法性,这称为前置条件断言。
  2. 在代码执行之后或者在函数的出口处,使用断言来检查参数是否被正确地执行,这称为后置条件断言。
  3. 在代码执行前后或者在函数的入出口处,使用断言来检查参数是否发生了变化,这称为前后不变断言。

例如,在上面的 Memcpy 函数中,除了可以通过“assert(dest !=NULL&&src!=NULL);”语句在函数的入口处检查 dest 与 src 参数是否传入 NULL 指针之外,还可以通过“assert(tmp_dest>=tmp_src+len||tmp_src>=tmp_dest+len);”语句检查两个内存块是否发生重叠。如下面的示例代码所示:

void *Memcpy(void *dest, const void *src, size_t len)
{
assert(dest!=NULL && src!=NULL);
char *tmp_dest = (char *)dest;
char *tmp_src = (char *)src;
/*检查内存块是否重叠*/
assert(tmp_dest>=tmp_src+len||tmp_src>=tmp_dest+len);
while(len --)
*tmp_dest ++ = *tmp_src ++;
return dest;
}

除此之外,建议每一个 assert 宏只检验一个条件,这样做的好处就是当断言失败时,便于程序排错。试想一下,如果在一个断言中同时检验多个条件,当断言失败时,我们将很难直观地判断哪个条件失败。因此,下面的断言代码应该更好一些,尽管这样显得有些多此一举:

assert(dest!=NULL);
assert(src!=NULL);

最后,建议 assert 宏后面的语句应该空一行,以形成逻辑和视觉上的一致感,让代码有一种视觉上的美感。同时为复杂的断言添加必要的注释,可澄清断言含义并减少不必要的误用。

避免在断言表达式中使用改变环境的语句

默认情况下,因为 assert 宏只有在 Debug 版本中才能起作用,而在 Release 版本中将被忽略。因此,在程序设计中应该避免在断言表达式中使用改变环境的语句。如下面的示例代码所示:

int Test(int i)
{
assert(i++);
return i;
}
int main(void)
{
int i=1;
printf(“%d\n”,Test(i));
return 0;
}

对于上面的示例代码,由于“assert(i++)”语句的原因,将导致不同的编译版本产生不同的结果。如果是在 Debug 版本中,因为这里向变量 i 所赋的初始值为 1,所以在执行“assert(i++)”语句的时候将通过条件检查,进而继续执行“i++”,最后输出的结果值为 2;如果是在 Release 版本中,函数中的断言语句“assert(i++)”将被忽略掉,这样表达式“i++”将得不到执行,从而导致输出的结果值还是 1。

因此,应该避免在断言表达式中使用类似“i++”这样改变环境的语句,使用如下代码进行替换:

int Test(int i)
{
assert(i);
i++;
return i;
}

现在,无论是 Debug 版本,还是 Release 版本的输出结果都将为 2。

避免使用断言去检查程序错误

在对断言的使用中,一定要遵循这样一条规定:对来自系统内部的可靠的数据使用断言,对于外部不可靠数据不能够使用断言,而应该使用错误处理代码。换句话说,断言是用来处理不应该发生的非法情况,而对于可能会发生且必须处理的情况应该使用错误处理代码,而不是断言。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
[外链图片转存中…(img-qE4dZPga-1712959784554)]
[外链图片转存中…(img-X8igFGgX-1712959784555)]
[外链图片转存中…(img-ox21r6TW-1712959784556)]
[外链图片转存中…(img-yAmyKA75-1712959784556)]
[外链图片转存中…(img-QK6WJYcC-1712959784557)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-ohLxYMAl-1712959784557)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 25
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值