在使用全局变量方面有哪些利与弊?

308
在C/C++中,普遍认为全局变量是“不好的”,应尽可能避免使用。
撇开这个观点是否正确的问题,当使用全局变量时会出现哪些客观优缺点?

24
我会开始翻译,假设他想要讲一个笑话...“它们有多糟糕?” - Zach Scrivena
18
我认为这个问题非常有趣!自从软件开发开始以来,仍然面临着相同的陷阱,程序员经常还不知道使用全局变量、goto语句、短命名变量并不是问题所在。每天都会有人写出糟糕的代码而没有使用它们。+1 - Sylvain Rodrigue
86
我们怎么可能回答呢?他没有告诉我们他的教授认为他们有多糟糕。 :) - Steve Fallows
4
@Sylvain 我完全不同意。使用全局变量会增加环境的依赖性,这样就不能轻松地测试模块。它使得调试困难,因为您永远不知道谁在读取和写入变量。全局名称冲突也是一个问题。甚至不要让我开始单例模式,当它们不持有状态时,它们是有效的情况(即只用于控制应用程序中的一个实例),而任何其他用途都是对全局变量的委婉说法。单例模式仅适用于愿意将代码组织好并限制数据访问的人。每天都会出现糟糕的代码,而全局变量使情况更糟。 - Ruan Mendes
12
@Juan Mendes 我完全同意你的观点!我所谈论的问题是许多开发人员知道不应该使用全局变量,但他们并不知道为什么!因此,我看到了许多大型软件,每个函数都接收相同的包含100多个字段的超级结构 - 看妈妈,没有全局变量!这和所谓的“好方法”一样:它们在某些情况下是好方法,但在所有情况下都不是。使用它们可能会创建难以维护的代码。干杯。 - Sylvain Rodrigue
显示剩余7条评论
28个回答

332

全局变量的问题在于,由于每个函数都可以访问这些变量,因此越来越难以确定哪些函数实际上读取和写入这些变量。

为了理解应用程序的工作原理,你几乎必须考虑修改全局状态的每个函数。这是可以做到的,但随着应用程序的增长,这将变得越来越困难,甚至是完全无法做到的(或者至少是浪费时间)。

如果你不依赖全局变量,可以根据需要在不同函数之间传递状态。这样你就有更好的机会理解每个函数的功能,因为你不需要考虑全局状态。


13
这个答案非常好。结合这个“最小化变量作用域”的答案 http://stackoverflow.com/questions/357187/global-variables-when-are-they-acceptable/357361#357361 - bobobobo
33
将“应用程序”替换为“类”,将“全局状态”替换为“对象状态”,即可对不在类中使用成员变量(也称为字段)提出完全相同的论点。真正的答案是在适当的时候使用它们。 - Ian Goldby
4
几个(也许有些傻)问题: 1)如果你想知道哪些函数读写了这些变量,你可以在编辑器中使用“查找”功能来查找修改这些变量值的情况,对吗? 2)“那是可以做到的......完全浪费时间)。”你能举个例子吗? 3)“如果您不依赖全局变量,...则无需考虑全局状态”,我不明白这是一个优势。也许举个例子会对我有帮助。 - Andrei
3
@bobobobo,链接失效了,您能否提供一张截图给我们,作为一个10k+用户? - noɥʇʎԀʎzɐɹƆ
6
@noɥʇʎԀʎzɐɹƆ 这是给你的!https://i.imgur.com/RwRgJLZ.jpg - Mateen Ulhaq
显示剩余7条评论

129
重要的是记住总体目标:清晰易懂。
“不使用全局变量”规则存在是因为大多数情况下,全局变量会使代码含义更加模糊。
然而,像许多规则一样,人们记住了规则,但却忘记了规则的意图。
我曾经看到过一些程序,似乎通过传递大量的参数来避免使用全局变量,使得代码大小增加一倍以上。最终,使用全局变量会使阅读它的人更加清晰。原始程序员盲目地坚持遵守规则的字面意思,未能达到规则的目的。
所以,是的,全局变量通常是不好的。但是,如果你觉得在最终情况下,使用全局变量可以让程序员的意图更加清晰,那就去做吧。然而,请记得当你强制某人访问第二个代码片段(全局变量)以理解第一个代码片段时,自动降低了代码的清晰度。

9
建议使用全局变量而不是传递变量,会导致你的代码不具备重用性,也不适合多线程环境下使用,因为这样容易造成不安全的情况。 - Ruan Mendes
32
在适当的情况下建议使用全局变量,这是编写更清晰、性能更高的代码的方法。"传递"需要不断进行栈动态内存分配,在应该是全局变量的情况下这样做显得愚蠢,例如用于接收套接字数据的全局缓冲区。举个例子,如果你有一个函数用于读取Winsock recv(),为什么要在每次调用时都不断创建和释放这个缓冲区呢?把它作为全局变量即可。因为多个线程也不会同时读取它。 - James
3
如果有人在传递100个变量,那么他们还没有学会什么是对象。使用对这个对象的引用最坏的情况就是传递一个指针。我认为这个规则不仅仅是清晰度,而且还涉及可测试性——使用非全局变量往往使测试变得更容易。 - UKMonkey
3
“如果有人在传递100个变量,那么他们就没有学会什么是对象。” 同意这种观点,但并非全世界都是面向对象的。我个人的例子是一份大型Fortran程序,约为1986年左右。作为一名刚毕业的员工,我“优化”了它,每次调用添加了约30个参数,消除了所有全局变量。后来意识到自己所做的事情时便撤销了这个改进。 - Tom West
2
谢谢你的回答。我看到很多“书呆子”称这个或那个为“邪恶”(我讨厌那个词),却并不真正理解背后的原因。没有一件事情适用于每一个情况。所以,每当称某物为“邪恶”时,必须强调:“在大多数情况下”。 - Silidrone
显示剩余2条评论

72

我的教授曾经说过类似这样的话:如果你正确使用全局变量,那么使用它们是可以的。但我不认为我擅长正确使用它们,所以我很少使用它们。


29
非常正确。它们就像“goto”语句,如果你不知道何时使用它们,那么就永远不要使用它们。 - David Holm
6
在我目前的公司,他们经常使用static全局变量,编程语言是C。由于限制在相对较小的翻译单元内,它们开始类似于C++对象的类变量。 - Vorac
1
@Vorac 静态变量不是全局变量,它们是局部变量。全局变量是程序中随处可用的变量(因此称为“全局”,显然)。不要与文件作用域变量混淆,这些变量在任何函数外声明。静态文件作用域变量不是全局变量。 - Lundin
1
为了纠正自己,程序生命周期,文件作用域变量。一旦将指向变量的指针传递到外部世界(这在自动变量中是不可能的),它们就变得非常全局。 - Vorac
如果您创建了一个全局变量,然后注释它在哪里使用以及哪个函数修改它,会怎样呢? - jamryu
显示剩余2条评论

52
全局变量对程序员造成的问题是,它扩大了使用全局变量的各个组件之间的组件耦合表面。这意味着随着使用全局变量的组件数量增加,交互复杂性也可能增加。这种增加的耦合通常使得在进行更改时更容易注入缺陷,并且使得诊断和纠正缺陷更加困难。这种增加的耦合还可以减少进行更改时的可用选项数量,并且可能增加更改所需的工作量,因为通常必须跟踪使用全局变量的各个模块以确定更改的后果。 封装的目的是减少耦合,使源代码更易于理解、更安全、更易于测试。当不使用全局变量时,使用单元测试要容易得多。
例如,如果您有一个简单的全局整数变量,被用作各种组件的状态机中的枚举指示器,然后通过添加新组件的新状态进行更改,您必须跟踪所有其他组件,以确保更改不会影响它们。可能存在问题的一个例子是,如果在各个位置使用了一个带有case语句的switch语句来测试枚举全局变量的值,并且恰好有一些switch语句没有一个default case来处理全局变量的意外值,那么应用程序就会出现未定义的行为。
另一方面,共享数据区域可能被用于包含一组全局参数,这些参数在整个应用程序中都被引用。这种方法通常用于具有小内存占用的嵌入式应用程序。
当在这些类型的应用程序中使用全局变量时,通常将写入数据区域的责任分配给单个组件,而所有其他组件将该区域视为const并从中读取,永远不会写入它。采用这种方法可以限制可能出现的问题。
全局变量需要解决的一些问题

当全局变量(例如结构体)的源代码被修改时,所有使用它的内容都必须重新编译,以便使用该变量的所有内容都知道其真实大小和内存模板。

如果有多个组件可以修改全局变量,则可能会遇到不一致的数据存在于全局变量中的问题。在多线程应用程序中,您可能需要添加某种锁定或关键区域,以提供一种方式,使只有一个线程可以同时修改全局变量,并且当线程正在修改变量时,所有更改都已完成并提交,然后其他线程才能查询变量或修改它。

调试使用全局变量的多线程应用程序可能更加困难。您可能会遇到竞态条件,这可能会导致难以复制的缺陷。通过全局变量进行通信的几个组件,特别是在多线程应用程序中,了解哪个组件何时以及如何更改变量,可能非常难以理解。

全局变量的使用可能会导致名称冲突问题。与全局变量同名的局部变量会隐藏全局变量。在使用C编程语言时,还会遇到命名约定的问题。解决方法是将系统划分为子系统,并以相同的前三个字母开头为特定子系统的全局变量命名(请参阅解决Objective-C中名称空间冲突)。C++提供了命名空间,而在C中,可以创建一个全局可见的结构体,其中成员是各种数据项和指向数据和函数的指针,这些数据和函数在文件中作为静态提供,因此仅具有文件可见性,只能通过全局可见的结构体引用。
在某些情况下,原始应用程序意图可能会改变,以便修改为全局变量,为单个线程提供状态并允许运行多个重复线程。例如,设计用于单个用户的简单应用程序使用全局变量来设置状态,然后管理层要求添加REST 接口,以允许远程应用程序充当虚拟用户。所以现在你需要复制全局变量及其状态信息,以使每个远程应用程序的虚拟用户和单个用户都有自己独特的一组全局变量。 使用 C++ namespace 和 C 的 struct 技术 对于C++编程语言,namespace指令有助于减少名称冲突的可能性。 namespaceclass和各种访问关键字(privateprotectedpublic)一起提供了大部分封装变量所需的工具。然而,C编程语言没有提供此指令。这个stackoverflow帖子,Namespaces in C ,提供了一些C语言技术。
一个有用的技术是拥有一个单一的内存驻留数据区,该数据区被定义为具有全局可见性的struct,并且在这个struct中有指向各种全局变量和函数的指针。使用static关键字将全局变量的实际定义给予文件范围。如果您使用const关键字指示哪些是只读的,编译器可以帮助您强制执行只读访问。
使用struct技术也可以封装全局变量,使其成为一种包或组件,恰好是一个全局变量。通过拥有这种类型的组件,更容易管理影响全局变量和使用全局变量的功能的更改。

然而,虽然namespacestruct技术可以帮助管理名称冲突,但全局变量引入的组件间耦合问题仍然存在,特别是在现代多线程应用程序中。


你的编程语言应该有一个代码规则来阻止你过度使用类耦合。 - Christian Findlay

42

全局变量只有在没有其他选择时才应使用。是的,这包括单例模式。90%的情况下,引入全局变量是为了节省传递参数的成本。然后多线程/单元测试/维护编码发生,你就会遇到问题。

所以,在90%的情况下,全局变量都是不好的。异常情况可能不会在你的大学年代看到。我能想到的一个例外是处理固有全局对象,例如中断表。像DB连接这样的东西似乎是全局的,但实际上并不是。


3
在我上大学时,唯一一个我看到的例外是图形回调函数。在XWindows中,鼠标回调没有void*数据参数,因此无法传递任意程序状态块...(虽然这也不比全局变量好多少...) - Brian Postow
12
+1 表示“像数据库连接这样的东西似乎是全局的,但实际上并不是”。 - R.. GitHub STOP HELPING ICE
2
中断表不是全局的,每个处理器都有一个 - 但是每个处理器还有一个程序实例,所以它 "抵消了"。 - user253751
2
有没有人能够告诉我为什么数据库连接不是全局的(以及什么是一个好的替代方案)?我一直认为连接是全局变量可以接受的罕见情况之一。 - Floella

23

是的,但只有当你停止使用全局变量的代码并开始编写使用该代码的其他内容时,才会产生全局变量的成本。但成本仍然存在。

换句话说,这是一种长期间接成本,因此大多数人认为它并不糟糕。


23

全局变量和你对它们的使用一样糟糕,没有更少。

如果你正在创建一个完全封装的程序,你可以使用全局变量。 使用全局变量是个“罪”,但编程上的罪大部分都是哲学层面的。

如果你查看L.in.oleum,你会发现这种语言的变量仅为全局变量。 这是不可扩展的,因为所有的库都别无选择,只能使用全局变量。

也就是说,如果你有选择,并且可以忽略程序员的哲学,全局变量并不是那么糟糕。

如果正确使用,Gotos也不是坏事。

最大的“坏”问题是,如果你用错了,人们会尖叫,火星着陆器会坠毁,世界会爆炸...或者类似于这样的事情会发生。


22
我认为,淡化全局变量使用中的问题对于一个困惑的学生来说并不是个好主意。 - GEOCHET
4
设计哲学并不客观,一点也不。 仅仅因为大多数程序员不喜欢某些东西,并不意味着我们就永远不应该去探究它。 使用全局变量并不会导致世界末日,让他去尝试,付出努力(尽管他可能会遇到挑战),然后学习并成长。 - user54650
8
"富人是正确的。这个答案没有说明什么是/不是不好的(或者全局变量如何安全使用),只是说“它们并不像那样糟糕”。因此,它只是淡化了问题。" - jalf
4
我不同意全局变量只有“你自己想象中的那么糟糕”的说法。我认为,在我们大多数人生活、工作和编程的这个互联世界中,尤其是在多人开发的情况下,全局变量的一个主要问题是它们给了别人机会使你的代码变得糟糕。请注意,此处的“bad”指的是不良的编程实践和质量,而不是道德品质。 - gariepy
直到现在我还以为这个讨论是关于静态的 :D 好的,就这样吧...而且我的应用程序只有一两个全局变量,其中一个是Visual Studio自带的DEBUG和TRACE,我们通常不使用它们:D - Hassan Faghihi
如果使用得当,"goto" 也不是什么问题。赞! - Shark

20
如果你的代码可能会在最高法院审判期间接受严格审查,那么你需要确保避免全局变量。
请参阅本文: Bug呼吸测试仪代码反映了源代码审查的重要性
  

两项研究都发现了代码风格上的一些问题。评论人员关注的样式问题之一是广泛使用未受保护的全局变量。这被认为是不良形式,因为它增加了程序状态变得不一致或值被意外修改或覆盖的风险。研究人员还对代码中未始终保持小数精度表示出一些担忧。

天哪,我打赌那些开发人员后悔使用了全局变量!

6
这是我有一段时间以来最开心的笑声。这真正展示了为利润而进行的闭源开发的不好之处,也是全局变量失控的一个好例子! - Evil Spork
这里所表达的是,全局变量是被看作是有害的。但是这里并没有显示出全局变量在代码中真正造成了什么问题。SysTest表示,虽然该代码“不符合通常的软件设计最佳实践”,但仍然可以“可靠地产生一致的测试结果”。因此,并没有实际记录到全局变量带来的危害。就我看来,他们只是确认,“好吧,这些开发人员不遵循与主流世界相同的编码标准。” - LionKimbro

18
问题并不是它们“糟糕”,而是它们“危险”。它们有各自的优缺点,在一些情况下,它们是实现特定任务最有效或唯一的方式。然而,即使您采取措施始终正确地使用它们,它们也很容易被误用。
一些优点:
  • 可以从任何函数中访问。
  • 可以从多个线程中访问。
  • 直到程序结束才会失去作用域。
一些缺点:
  • 可以从任何函数中访问,无需明确将其作为参数拖入和/或记录。
  • 不是线程安全的。
  • 污染全局命名空间,可能导致名称冲突,除非采取措施防止发生这种情况。
请注意,我列出的前两个优点和前两个缺点完全相同,只是措辞不同。这是因为全局变量的特性确实很有用,但使它们有用的特性正是它们所有问题的根源。
一些问题的潜在解决方案:
  • 考虑它们是否实际上是问题的最佳或最有效解决方案。如果存在任何更好的解决方案,请使用其他方法。
  • 将它们放入名称唯一的命名空间[C++]或单例结构[C,C++]中(一个很好的例子是“Globals”或“GlobalVars”),或使用全局变量的标准命名约定(例如“global_ [name]”或“g_module_varNameStyle”)(如评论中提到的underscore_d)。这将同时记录它们的使用(您可以通过搜索命名空间/结构名称找到使用全局变量的代码)并最小化全局命名空间的影响。
  • 对于访问全局变量的任何函数,请明确记录它读取和写入的哪些变量。这将使故障排除更容易。
  • 将全局变量放在独立的源文件中,并在相关头文件中声明它们为extern,以便将它们的使用限制在需要访问它们的编译单元中。如果你的代码依赖于大量全局变量,但每个编译单元只需要访问其中一小部分,则可以考虑将它们分类到多个源文件中,以便更容易地限制每个文件对全局变量的访问。
  • 建立一个机制来锁定和解锁它们,或者设计代码使尽可能少的函数需要实际修改全局变量。读取它们比写入它们更安全,尽管线程竞争仍可能在多线程程序中引起问题。
  • 基本上,最小化对它们的访问,并最大化名称唯一性。你要避免名称冲突,并尽可能少地有函数可能修改任何给定变量。
  • 它们是好是坏取决于如何使用它们。大多数人倾向于使用它们不良,因此对它们持谨慎态度。如果使用得当,它们可以成为一个巨大的福音;然而,如果使用不当,它们肯定会在最不期望的时间和方式咬你一口。

    一个好的方法是把它们看作不坏,但它们会导致糟糕的设计,并且可以将糟糕的设计效果成倍增加。


    即使你不打算使用它们,了解如何安全使用它们并选择不使用,也比因为不知道如何安全使用它们而不使用更好。如果你发现自己需要维护依赖全局变量的预先存在的代码,如果不知道如何正确使用它们,可能会遇到困难。


    2
    +1 的务实主义。单例模式通常只是添加样板代码以使实例化和重构成员,并且最终你会得到……全局变量,只是用不同的名称伪装而已。除了避免仅仅因为技术原因而犯全局变量之罪外,还有什么理由呢?命名空间作为屏障很好,但我发现一个简单的 g_module_varNameStyle 完全可读。明确一点,如果可以轻松避免使用全局变量,我就不会使用它们 - 关键词是“轻松”,因为自从我停止相信必须以任何代价避免它们 - 或者更确切地说是混淆它们 - 以来,我的编码体验更好了,我的代码也(惊人!)更整洁。 - underscore_d
    @underscore_d,这主要是为了更容易区分全局变量和局部变量,并且在搜索代码时更容易找到全局变量,以防止混淆变量是全局的还是局部的/参数/成员等。像你的标准命名约定一样也可以,只要保持一致即可。我会用标准命名约定来编辑我的答案,谢谢。 - Justin Time - Reinstate Monica
    1
    对于任何函数...请明确记录哪些变量 - 请记住这是一个传递关系。如果函数A调用函数B和C,则它会读取和写入由两个函数(以及直接在其主体中的变量)编写的变量。 - Caleth
    另一个陷阱:全局变量初始化的顺序。通常,全局变量不依赖于彼此的初始化,但有时确实如此。例如,在Golang中,它们通过推断正确的初始化顺序来解决这个问题。在其他语言中,这只是没有明确定义的。 - Nolan
    1
    另一个问题是:在某些语言(如C++)中,全局变量的初始化可能会导致程序启动或终止时出现故障,这可能很难诊断。 - Nolan

    17

    我会用另一个问题来回答这个问题:你是否使用单例模式?单例模式是坏的吗?

    因为(几乎所有)单例的使用都是对全局变量的美化。


    11
    我本来想发表一个聪明的评论,说“只有在称它们为全局变量而不是单例模式时才会出现问题”,但你比我先一步了。 - smo
    我仍在努力弄清楚单例模式是什么鬼 LOL。 - GeoffreyF67
    1
    @Geoffrey:这里有一些好的SO描述--https://dev59.com/7HVD5IYBdhLWcg3wWKHc,还有一些好的链接:https://dev59.com/7HVD5IYBdhLWcg3wWKHc。 - Gavin Miller
    10
    记录一下,单例指的是一个具有华丽设计模式名称的全局变量,以使其听起来合法。出于同样的原因,它同样糟糕。请注意,这里的“同样的原因”指的是与全局变量相关的缺点。 - R.. GitHub STOP HELPING ICE
    @GavinMiller,你是说如果使用单例模式的委婉说法——简单模式,这样就可以了吗? - Ruan Mendes

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