为什么要使用bzero而不是memset?

187
在我上学期修读的系统编程课中,我们必须使用C语言实现基本的客户端/服务器。当初始化结构体(如sock_addr_in)或字符缓冲区(用于在客户端和服务器之间发送数据)时,教授指示我们仅使用bzero而不是memset来初始化它们。他从未解释过原因,我很好奇是否有有效的理由。
我在这里看到:http://fdiv.net/2009/01/14/memset-vs-bzero-ultimate-showdown bzero由于只会清零内存,所以更高效,因此不需要执行memset可能会执行的任何其他检查。然而,这似乎仍然不是绝对不使用memset清零内存的理由。 bzero被认为是过时的,并且不是标准的C函数。根据手册,出于这个原因,memsetbzero更受欢迎。那么为什么仍然希望使用bzero而不是memset?仅仅是为了提高效率,还是有其他原因?同样,memset相对于bzero的优势是什么,使其成为新程序的默认首选选项?

35
为什么使用bzero而不是memset?- 不要这样做。 memset是标准的,而bzero则不是。 - user529758
34
bzero是BSD风格的函数(),而memset()则为ansi-c风格。如今,bzero()可能会被实现成宏。让你的教授刮脸并读一些书。效率是一个伪命题。系统调用或上下文切换很容易就会耗费数万个时钟周期,而对缓冲区的一次遍历则以总线速度进行。如果你想要优化网络程序:尽量减少系统调用的次数(通过读取/写入更大的数据块)。 - wildplasser
9
memset 函数因为 "有一些额外的检查" 可能会稍微不那么高效这一想法视为过早优化是正确的:无论您省略一个或两个 CPU 指令可能获得多少性能提升,在危及代码可移植性的情况下都不值得。bzero 已经过时,这已足够理由不使用它。 - Sergey Kalinichenko
5
通常情况下,您可以添加一个初始化器= {0}而无需调用任何函数。当 C 语言在本世纪初停止要求提前声明局部变量时,这变得更加容易。尽管如此,有些真正陈旧的纸质文献仍然停留在上个世纪深处。 - MSalters
显示剩余6条评论
9个回答

173

我认为没有理由更喜欢bzero而不是memset

memset是标准的C函数,而bzero从未成为C标准函数。这样做的原因可能是因为您可以使用memset函数完全实现相同的功能。

关于效率,像gcc这样的编译器使用内置实现memset,当检测到常量0时,切换到特定的实现方式。如果禁用内置,则glibc也会执行相同操作。


谢谢。这很有道理。我相当确定在这种情况下应该始终使用memset,但是为什么我们没有使用它感到困惑。感谢您的澄清,并再次确认我的想法。 - PseudoPsyche
1
我曾经遇到过许多bzero实现出现问题的情况。在非对齐数组上,它会超出提供的长度并清零更多字节。自从转换为memset后,我再也没有遇到这样的问题了。 - rustyx
1
不要忘记使用 memset_s,如果您想确保编译器不会悄悄地优化掉对“擦除”内存的调用,以实现某些安全相关目的(例如清空包含敏感信息的内存区域,如明文密码)。 - Christopher Schultz

72
我猜你使用过(或者你的老师受到)W. Richard Stevens 的《UNIX网络编程》。即使是最新版,他经常使用 bzero 而不是 memset 。这本书非常受欢迎,我认为它已经成为网络编程中的一种惯用语,这就是为什么你仍然会看到它被使用。
我建议使用 memset ,因为 bzero 已经被弃用并且减少了可移植性。我怀疑您使用其中一个而不是另一个不会获得任何真正的收益。

5
没错,你说得对。我们这门课确实没有必需教材,但我再次查看了教学大纲,发现《UNIX网络编程》确实被列为可选资源。谢谢。 - PseudoPsyche
11
实际情况比这更糟。它在 POSIX.1-2001 中被弃用,而在 POSIX.1-2008 中被 _删除_。 - paxdiablo
13
引用W. Richard Stevens的《UNIX网络编程》第三版第8页的内容:“事实上,TCPv3的作者在第一次印刷中在10个地方交换了memset的第二个和第三个参数。C编译器无法捕获这个错误,因为两个位置都是相同的……这是一个错误,可以通过使用bzero来避免,因为如果使用函数原型,交换bzero的两个参数将始终被C编译器捕获。” 但是,正如paxdiablo指出的那样,bzero已经被弃用了。 - Aaron Newton
@AaronNewton,你应该将这个加到Michael的回答中,因为它证实了他所说的。 - Synetech
1
回复:降低可移植性:使用polyfill非常简单,而你所得到的是减少不必要的人为错误可能性,这需要长期的精神警觉成本,而这些成本可以更好地用于发现其他逻辑问题。 - mtraceur

63

bzero() 相对于 memset() 来说,唯一的优势在于它减少了出错的可能性。

我曾经遇到过类似这样的错误:

memset(someobject, size_of_object, 0);    // clear object
编译器不会报错(虽然在某些编译器上增加一些警告级别可能会),因此内存不会被清除。由于这并没有破坏物体 - 它只是让它保持原样 - 所以有很大的机会,这个错误可能不会表现出明显的问题。 bzero()不是标准函数,这是一个小烦恼。(顺便说一下,我不会惊讶如果我的程序中大部分函数调用都是非标准的;事实上编写这些函数是我的工作之一)。
在回答中的另一个评论中,Aaron Newton引用了Unix Network Programming, Volume 1, 3rd Edition by Stevens, et al.,Section 1.2中的以下内容(重点加粗):
bzero不是ANSI C函数。它来源于早期的Berkely网络代码。尽管如此,我们在整个文本中使用它,而不是ANSI C memset函数,因为bzero更容易记住(只有两个参数),而memset有三个参数。几乎每个支持套接字API的供应商也提供bzero,如果没有,我们在我们的unp.h头文件中提供宏定义”。
“事实上,《TCPv3》 [TCP/IP Illustrated, Volume 3 - Stevens 1996] 的作者在第一版中的10个地方错误地交换了memset的第二个和第三个参数。 C编译器无法捕获此错误,因为两个参数类型相同(实际上,第二个参数是int,而第三个参数是size_t,通常是unsigned int,但指定的值0和16仍然接受为其他类型的参数)。调用memset仍然可以工作,因为只有少数套接字函数需要将Internet套接字地址结构的最后8个字节设置为0。尽管如此,这是一个错误,可以通过使用bzero来避免,因为如果使用函数原型,交换两个参数到bzero始终会被C编译器捕获。”
我认为大多数对memset()的调用都是为了将内存清零,那么为什么不使用专门针对该用例的API呢? bzero()可能存在的缺点是编译器更可能优化memcpy(),因为它是标准的,所以它们可能被写成识别它。但请记住,正确的代码仍然比经过优化的不正确的代码要好。在大多数情况下,使用bzero()不会对程序性能造成明显影响,并且bzero()可以是一个宏或内联函数,它扩展为memcpy()

7
我认为这是一个可以在教室外进行的论述 - 我曾在生产代码中见过这个错误。我认为这是一个容易犯的错误。我还猜想,绝大多数的memset()调用只是将一块内存清零,这也是支持使用bzero()的另一个论点。那么,bzero()中的“b”代表什么? - Michael Burr
11
"memset"函数的参数顺序与常见的“缓冲区,缓冲区大小”相反,这使得它在我看来特别容易出错。 - jamesdlin
在Pascal中,他们通过称之为“fillchar”并使用char来避免这种情况。大多数C/C++编译器都会选择它。这让我想知道为什么编译器不会说“您正在传递一个32/64位指针,而期望的是一个字节”,并将您牢牢地踢出编译器错误。 - Móż
3
@Gewure 的第二个和第三个参数顺序不正确;引用的函数调用实际上 _什么也没做_。 - Ichthyo
1
@PseudoPsyche 我既希望人们仔细地阅读手册,但我也认识到每个接口的不规则性和大脑需要跟踪的相关可能性都是一种成本。在 memset 的情况下,调用 memset 时传递值-然后大小而不是大小-然后值的认知需要经常使用或排练,这包括始终检查“等等,我是否正在使用其中一个特殊函数?”然后才进入更普遍使用和加强的认知流程,即在缓冲区指针之后立即传递缓冲区大小的认知。 - mtraceur
显示剩余6条评论

5
您可以随心所欲地定制。 :-)
#ifndef bzero
#define bzero(d,n) memset((d),0,(n))
#endif

注意:
1.原始的bzero函数没有返回值,memset函数返回一个无类型指针(d)。这个问题可以通过在定义中添加对void的强制类型转换来解决。
2.#ifndef bzero不能防止隐藏原始函数即使它存在。它测试一个宏的存在。这可能会引起很多混乱。
3.无法为宏创建函数指针。当通过函数指针使用bzero函数时,这将不起作用。

1
这有什么问题,@Leeor?你对宏有普遍的反感吗?还是你不喜欢这个宏可能会与函数混淆(甚至隐藏它)的事实? - Palec
2
@Palec,后者。将重新定义隐藏为宏可能会导致很多混淆。使用此代码的另一个程序员认为他正在使用一件事情,而不知不觉地被迫使用另一件事情。这是一个定时炸弹。 - Leeor
1
经过再次思考,我同意这确实是一个糟糕的解决方案。除此之外,我还发现了一个技术原因:当通过函数指针使用 bzero 时,它将无法正常工作。 - Palec
1
你真的应该给你的宏定义取一个不同于bzero的名字。这太糟糕了。 - Dan Bechard
1
缺陷在一旁,这个答案确实很有用。此时此刻,它是众多答案中第一个积极指出bzero可以如何轻松定义的答案 - 答案中给出的宏是一个不完美的源级填充,但仍然是一个填充,你可以按照这种填充方法进行逻辑改进,在没有bzero的平台上定义一个名为bzero的函数。(同样,你也可以决定实现一个具有不同名称的函数或宏,以避免bzero的不可移植性,这是最接近暗示的最高排名答案。) - mtraceur

4

对于memset函数,第二个参数是一个int类型,第三个参数是size_t类型。

void *memset(void *s, int c, size_t n);

通常情况下,第一个参数是 unsigned int 类型的。但如果第二个和第三个参数的值,比如说0 和 16 被错误地输入为 16 和 0,那么调用 memset 仍然可以正常工作,但不会有任何效果。这是因为要初始化的字节数被指定为 0

void bzero(void *s, size_t n)

使用bzero可以避免这样的错误,因为如果使用函数原型,将两个参数交换到bzero将始终被C编译器捕获。


1
如果你把这个调用看作是“将这个内存设置为这个值,大小为这个”,或者你有一个提供原型的IDE,甚至只是因为你知道自己在做什么,那么使用memset也可以避免这样的错误。 - paxdiablo
同意,但是这个函数是在没有这样智能的IDE支持的时候创建的。 - havish

4

想要提及一些关于bzero和memset参数的内容。请安装ltrace并比较其在底层执行的情况。 在Linux系统中的libc6(2.19-0ubuntu6.6)下,通过ltrace ./test123调用的结果完全相同:

long m[] = {0}; // generates a call to memset(0x7fffefa28238, '\0', 8)
int* p;
bzero(&p, 4);   // generates a call to memset(0x7fffefa28230, '\0', 4)

我听说,除非我在libc的深处或任何数量的内核/syscall接口中工作,否则就不需要担心它们。 我唯一需要担心的是调用是否满足缓冲区清零的要求。其他人已经提到了哪个优先于哪个,所以我就到这里吧。


这是因为某些版本的GCC会在看到bzero(ptr, n)时为memset(ptr, 0, n)发出代码,而它们无法将其转换为内联代码。 - zwol
@zwol 这实际上是一个宏。 - S.S. Anne
1
@S.S.Anne 我的电脑上的gcc 9.3可以自动进行这种转换,而无需使用系统头文件中的宏。extern void bzero(void *, size_t); void clear(void *p, size_t n) { bzero(p, n); }会产生一个对memset的调用。(包含stddef.h以获取size_t,但不要包含任何可能干扰的其他内容。) - zwol
@zwol,你已经在测试中验证了包含stddef.h不会导致定义bzero宏吗? - mtraceur
1
@mtraceur 是的。而且,我可以使用gcc内置的__SIZE_TYPE__在没有任何头文件的情况下重现这种效果。此外,stddef.h定义名为bzero的宏将是一种可怕的符合性违规行为。 - zwol
@zwol 谢谢你的阐明。 - mtraceur

4

您可能不应该使用 bzero,它实际上不是标准的C语言,而是一个POSIX的东西。

请注意那个词“was” - 它在POSIX.1-2001中被弃用,并在POSIX.1-2008中被移除,以便使用memset,因此最好使用标准的C函数。


标准 C 是什么意思?你的意思是它在标准 C 库中找不到吗? - Koray Tugay
@Koray,标准C指的是ISO标准,是的,bzero不属于其中。 - paxdiablo
不,我的意思是我不知道你所说的任何标准是什么。ISO标准是指标准C库吗?它随语言一起提供的那个?我们知道会有的最小库? - Koray Tugay
2
@Koray,ISO是负责C标准的标准组织,目前的标准是C11,早期的标准有C99和C89。他们制定了实现必须遵循的规则,以便被认为是C语言。因此,如果标准规定实现必须提供memset函数,那么它就会存在。否则,它就不是C语言。 - paxdiablo
在我看来,POSIX 在删除 bzero(3) 时犯了一个错误;正如 @MichaelBurr 在他的回答中已经指出的那样,它比 memset(3) 有更好的接口。然而,不在任何标准中并不重要对于用户代码。可以使用 bzero(3)(我鼓励这样做),如果实现缺少它,只需 inline void bzero(void *s, size_t n) { memset(s, 0, n); } 就足够了。 - alx - recommends codidact

2

是的,这是我在原帖中提到的一件事。实际上,我甚至链接到了那个确切的页面。结果发现,由于一些编译器优化,这似乎并没有太大的区别。有关更多详细信息,请参阅ouah的被接受答案。 - PseudoPsyche
7
这只是表明一个垃圾的memset实现很慢。在MacOS X和其他一些系统中,memset使用在启动时根据您正在使用的处理器设置的代码,充分利用向量寄存器,并且对于大型大小,它会聪明地使用预取指令以获得最后一点速度。 - gnasher729
1
指令越少并不意味着执行速度更快。实际上,优化通常会增加二进制文件的大小和指令数量,因为它包括循环展开、函数内联、循环对齐等操作... 查看任何优化良好的代码,你会发现它通常比糟糕的实现有更多的指令。 - phuclv

0

memset函数有3个参数,bzero函数有2个。 在内存受限的情况下,额外的参数会占用4个字节,并且大部分时间都会被用来将所有内容设置为0。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接