为什么(非常量)全局变量是邪恶的
本节阅读量:如果您想问一位资深程序员关于良好编程实践的一条建议,经过一番思考,最可能的答案是“避免全局变量!”。有很好的理由:全局变量是历史上被滥用最多的概念之一。尽管它们在小型项目中似乎无害,但在大型项目中通常会有问题。
新手程序员经常使用许多全局变量,因为它们很容易使用,特别是当涉及到对不同函数的许多调用时(通过函数参数传递数据是一件痛苦的事情)。当然这通常是一个坏主意。许多开发人员认为应该完全避免非常量的全局变量!
但在我们讨论原因之前,我们应该澄清一下。当开发人员告诉您全局变量是邪恶的时候,通常不是在谈论所有的全局变量。主要讨论的是非常量的全局变量。
为什么(非常量)全局变量是邪恶的
到目前为止,非常量全局变量是危险的最大原因是,它们的值可以被调用的任何函数更改,并且程序员没有简单的方法得知这发生在哪里。考虑以下程序:
|
|
请注意,main函数将变量g_mode设置为1,然后调用doSomething() 。除非明确知道 doSomething() 的实现细节,否则阅读代码的读者不知道它修改了变量g_mode的值!因此,main() 的其余部分不能像预期的那样工作。
简而言之,全局变量使程序的状态不可预测。每个函数调用都有潜在的危险,没有简单的方法知道哪些函数调用是危险的,哪些不是!局部变量更安全,因为其他函数不能直接影响它们。
还有许多其他的理由不使用非常量全局变量。
对于全局变量,通常会发现如下所示的代码:
|
|
调试后,您确定程序工作不正常,因为g_mode的值为3,而不是4。怎么修理它?现在您需要找到所有可能将g_mode设置为3的地方,并跟踪它是如何设置的。这可能是在一段完全不相关的代码中!
声明局部变量的一个原则,是尽可能靠近它们的使用位置,因为这样做可以最大限度地减少,您为了查看变量作用所需要查看的代码量。全局变量在频谱的另一端——因为它们可以在任何地方访问,所以您可能必须查看整个程序才能理解它们的用法。在小型程序中,这可能不是问题。但在大型程序中,这样极大提高排查问题难度。
例如,您可能会发现在程序中引用了442次g_mode。除非g_mode有很好的文档记录,否则您可能必须仔细检查g_mode的每个相关代码,以了解它在不同的情况下是如何使用的,它的有效值是什么,以及它的整体功能是什么。
全局变量也会降低程序的模块化程度和灵活性。只使用其参数并且没有副作用的函数是完全模块化的。模块化有助于理解程序的功能以及可重用性。全局变量显著降低了模块性。
特别是,避免将全局变量用于重要的“决策点”变量(例如,您将在条件语句中使用的变量,如上例中的变量g_mode)。如果变量值发生了变更,这可能会影响程序的关键行为。
最佳实践
尽可能使用局部变量而不是全局变量。
全局变量的初始化顺序问题
在执行main函数之前,静态变量(包括全局变量)的初始化作为程序启动的一部分发生。这分两个阶段进行。
第一阶段称为静态初始化(static initialization)。在静态初始化阶段,具有constexpr初始值设定项(包括字面值)的全局变量被初始化为对应的值。此外,没有初始值设定项的全局变量被零初始化。
第二个阶段称为动态初始化(dynamic initialization)。这个阶段更加复杂和微妙,但其要点是初始化具有非constexpr初始值设定项的全局变量。
下面是非constexpr初始值设定项的示例:
|
|
在单个文件中,对于每个阶段,全局变量通常按定义顺序初始化(对于动态初始化阶段,此规则有一些例外)。考虑到这一点,您需要小心,不要让变量依赖于其他变量的初始化值,这些变量直到最后才会初始化。例如:
|
|
这将打印:
|
|
更重要的是,C++没有定义不同文件之间的初始化顺序。给定两个文件a.cpp和b.cpp,任何一个都可以首先初始化其全局变量。这意味着,如果a.cpp中的变量依赖于b.cpp中的值,则这些变量尚未初始化的可能性为50%。
警告
全局变量的动态初始化在C++中引起了许多问题。尽可能避免动态初始化。
那么,使用非常量全局变量的必须的理由是什么呢?
没有太多必须的理由。在大多数情况下,有其他方法可以解决问题,从而避免使用非常量全局变量。但在某些情况下,明智地使用非常量全局变量实际上可以降低程序的复杂性,并且在这些罕见的情况下,它们的使用可能比替代方案更好。
一个很好的例子是日志文件,您可以在其中打印错误或调试信息。将其定义为全局变量是有意义的,因为您可能在程序中只有一个日志,并且它可能会在程序中的任何地方使用。
值得一提的是,std::cout和std::cin对象被实现为全局变量(在std命名空间内)。
根据经验法则,全局变量的任何使用都应该至少满足以下两个标准:变量在程序中表示的东西应该只有一个,并且它的使用应该在整个程序中无处不在。
许多新程序员错误地认为某些东西可以作为全局实现,因为现在只需要一个。例如,您可能会认为,因为您正在实现单人游戏,所以您只需要一个玩家。但当你想添加多人模式(对战或热区)时,会发生什么呢?
保护自己免受全局变量破坏
如果您确实很好地使用了非常量全局变量,那么一些有用的建议将最大限度地减少您可能遇到的麻烦。这个建议不仅适用于非常量全局变量,而且可以帮助处理所有全局变量。
首先,用“g”或“g_”作为所有非命名空间全局变量的前缀,或者将它们放在命名空间中,以减少命名冲突的可能性。
例如,不编写下面类型的程序:
|
|
而编写如下的:
|
|
其次,与其允许直接访问全局变量,不如“封装”变量。确保只能从声明变量的文件中访问该变量,例如,通过使变量成为静态或常量,然后提供外部全局“访问函数”来使用该变量。这些功能可以确保保持正确的使用(例如,进行输入验证、范围检查等)。此外,如果您决定更改底层实现(例如,变量修改名称),则只需更新访问函数,而不是修改使用全局变量的每段代码。
例如,不编写下面类型的程序:
|
|
而编写如下的:
|
|
一个提醒
默认情况下,常量 全局变量具有内部链接,gravity变量不需要声明是 static。
第三,当编写使用全局变量的独立函数时,不要在函数体中直接使用变量。请将其作为参数传入。这样,如果函数在某些情况下需要使用不同的值,则可以简单地改变参数。这有助于维护模块化。
例如,不编写下面类型的程序:
|
|
而编写如下的:
|
|
一个笑话
全局变量的最佳命名前缀是什么?
答案://
