第九章 内存模型和名称空间

本文介绍了C++中的内存管理概念,包括存储持续性、作用域、链接性等,并探讨了自动、静态和动态存储的特点。此外,还讨论了名称空间的概念及其在避免命名冲突中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • C++在内存中存储数据方面提供了多种选择。可以选择数据保存在内存中的时间长度(存储持续性)以及程序的哪一部分可以访问数据(作用域和链接)等。可以使用new来动态分配内存,而布局new操作符提供了这种技术的一个变种。
  • C++名称空间是另一种控制访问权的方式。
单独编译
  • C++鼓励程序员将组件函数放在独立的文件中。可以单独编译这些文件,然后将它们链接成可执行的程序。通常,C++编译器既编译程序,也管理链接器。
  • 如果只修改了一个文件,则可以只重新编译该文件,然后将它与其他文件的编译版本链接,使得大程序的管理更便捷。
  • 头文件中常包含的内容:
    1. 函数原型
    2. 使用#define或const 定义的符号常量
    3. 结构声明
    4. 类声明
    5. 模板声明
    6. 内联函数
  • 不要将函数定义或变量声明放到头文件中,这样通常会有麻烦。可能在多次引用头文件的时候造成同一程序中包含同一个函数的多个定义。除非函数时内联的,否则将出错。
  • 将结构声明放在头文件中时可以的,因为它们不创建变量,而只是在源代码文件中声明结构变量时,告诉编译器如何创建该结构变量。
  • 同样,模板声明不是将被编译的代码,它们指示编译器如何生成与源代码中的函数调用相匹配的函数定义。
  • 被声明为const的数据和内联函数有特殊的链接属性,因此可以放在头文件中。
  • 注意,在包含头文件时,我们使用"coordin.h",而不是<coodin.h>。如果头文件包含字尖括号中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;但如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录(取决于编译器)。如果没有找到则将在标准位置查找。因此在包含自己的头文件时,应使用引号而不是尖括号。
头文件管理
  • 在同一个文件中只能将同一个头文件包含一次。
  • 使用基于预处理器编译指令#ifndef(即if not difined)的技术可以避免多次包含同一个头文件。
  • 这种方法并不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容。
		#ifnedf COORDIN_H_
		```
		#endif
		// 意味着仅当以前没有使用预处理编译指令#define定义名称COORDIN_H_时,才处理#ifndef和#endif之间的语句。
		// 通常,使用#define语句来创建符号常量。	如:#define MAXIMUM 4096
		// 但只要#define用于名称,就足以完成该名称的定义。 如:#define COORDIN_H_
存储持续性、作用域和链接性
  • C++ 使用3种不同的方案来存储数据,这些方案的区别在数据保留在内存中的时间。
    1. 自动存储持续性:在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。C++有两种存储持续性为自动的变量。
    2. 静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存储。C++有3种存储持续性为静态的变量。
    3. 动态存储持续性:用new操作符分配的内存将一直存在,知道使用delete操作符将其释放或程序结束为止。这种内存的存储持续性为动态,有时又称自由存储(free store)。
作用域和链接
  • 作用域(scope)描述了名称在文件(翻译单元)的多大范围内可见。
  • 链接性(linkage)描述了名称如何在不同单元内共享。
  • 链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。
  • 自动变量的名称没有链接性,因为它们不能共享。
  • C++变量的作用域有多种:
    1. 作用域为局部的变量只在定义它的代码块中可用。
    2. 作用域为全局(也叫文件作用域)的变量在定义位置到文件结尾之间都可用。
    3. 自动变量的作用域为局部,静态变量的作用域取决于它是如何被定义的。
    4. 在函数原型作用域(function prototype scope)中使用的名称只在包含参数列表的括号内可用。
    5. 在类中声明的成员的作用域是整个类。
    6. 在名称空间中声明的变量的作用域为整个名称空间。
  • C++函数的作用域可以是整个类或整个名称空间(包括全局的),但不能是局部的(因为不能在代码块内定义函数,如果函数的作用域为局部,则只对它自己可见,不能被其他函数调用。这样的函数将无法运行。)
自动存储持续性
  • 默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。
  • 可以使用C/C++关键字auto来显式指出存储类别。
堆栈
  • 由于自动变量的数目随函数的开始和结束而增减,因此程序必须在运行时对自动变量进行管理。常用的方法是留出一段内存,并将其视为堆栈。
  • 之所以被称为堆栈,是由于新数据被象征性地放在原有数据的上面,当程序使用完后,将其从堆栈中删除。
  • 程序使用两个指针来跟踪堆栈,一个指针指向栈底——堆栈的开始位置,另一个指针指向堆顶——下一个可用内存单元。当函数被调用时,其自动变量将加入到堆栈中,栈顶指针指向变量后面的下一个可用内存单元。函数结束时,栈顶指针被重置为函数被调用前的值,从而释放新变量使用的内存。
  • 堆栈是LIFO(后进先出)。这种设计简化了参数传递。函数调用将其参数的值放在栈顶,然后重新设定栈顶指针。被调用的函数根据其形参描述来确定每个参数的地址。
寄存器变量
  • 寄存器变量是另一种形式的自动变量,其存储持续性为自动,作用域为局部,但没有链接性。使用关键字register提醒编译器,用户希望它通过使用CPU寄存器而不是堆栈来处理特定变量,从而提供对变量的快速访问。
  • CPU访问寄存器的值的速度比访问堆栈中内存快。
  • 如果变量被存储在寄存器中,则没有内存地址,因此不能将地址操作符用于寄存器变量。
  • 简单来说,常规局部变量、使用auto声明的局部变量以及使用register声明的局部变量的存储性都是自动的,作用域都是局部的,也都没有链接性。
  • 声明局部变量时,如果没有使用说明符,则与使用auto声明变量等效。
静态持续变量
  • C++为静态存储持续性变量提供了3中链接性:外部链接性、内部链接性和无链接性。这三种链接性都在整个程序执行期间存在。
  • 编译器将分配固定的内存块来存储所有的静态变量,而不是使用堆栈。
  • 要创建链接性为外部的静态持续性变量,必须在代码块的外面声明它;要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用static限定符;要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static限定符。
  • 静态持续变量由两个初始化特征:1)未被初始化的静态变量的所有位都被设置为0;2)只能使用常量表达式来初始化静态变量。
五种变量存储方式
存储描述持续性作用域链接性如何声明
自动自动代码块在代码块中(可使用关键字auto)
寄存器自动代码块在代码块中,使用关键字register
静态、无链接性静态代码块在代码块中,使用关键字static
静态、外部链接性静态文件外部在函数外面
静态、内部链接性静态文件内部在函数外面,使用关键字static
静态持续性、外部链接性
  • 链接性为外部的变量同城简称为外部变量,存储持续性为静态,作用域为整个文件。
  • 外部变量是在函数外部定义的,因此对所有函数而言都是外部的。
  • 如果定义了与外部变量同名的自动变量,则当程序执行其所属函数时,该自动变量将隐藏同名的外部变量。
  • 使用关键字extern可以重新引用声明以前定义过的外部变量。在引用声明中指定的类型应与定义声明中相同;不能在引用声明中初始化变量。
  • 仅当声明将为变量分配存储空间时(即定义声明),才能在声明中初始化变量。
  • 如果定义与全局变量同名的局部变量后,局部变量将隐藏全局变量。C++可以使用作用域解析操作符(::)放在变量前时表示使用变量的全局版本。
静态持续性、内部链接性
  • 将static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。
  • 在多文件程序中,链接性为内部的变量只能在其所属的文件中使用;常规外部变量都具有外部链接性,可以在其他文件中使用。
  • 对于外部链接性变量,有且只有一个文件中包含了该变量的外部定义。其他文件要使用该变量,必须在引用声明中使用关键字extern。
  • 在多个文件程序中,可以且只能在一个文件中定义一个外部变量。使用该变量的其他文件必须使用关键字extern声明它。
  • 应使用外部变量在多文件程序的不同部分之间共享数据;
  • 应使用链接性为内部的静态变量在同一个文件中的多个函数之间共享数据(名称空间提供了另外一种共享数据的方式,C++标准指出,使用static来创建内部链接性的方法将逐步被淘汰)。
  • 如果将作用域为整个文件的变量变成静态的,就不用担心其名称与其他文件中的作用域为整个文件的变量发生冲突。
静态存储持续性、无链接性
  • 将static限定符用于在代码块中定义的变量是无链接性的局部变量,且存储持续性为静态。
  • 虽然此种变量只在该代码块中可用,但它在该代码块不处于活动状态时仍然存在。因此在两次函数调用之间,静态局部变量的值将保持不变。
说明符和限定符
存储说明符(storage class specifier)
  • auto:声明自动变量。
  • register:声明寄存器存储类型。
  • static:被用在作用域为整个文件的声明中时,表示内部链接性;被用在局部声明中,表示局部变量的存储持续性为静态的。
  • extern:引用声明,即声明引用在其他地方定义的变量。
  • mutable:即使结构或类变量为const,其某个成员也可以被修改。
cv限定符(cv-qualifier)
  • const:初始化之后,程序便不能再对它进行修改。
  • volatile:即使程序代码没有对内存单元进行修改,其值也可能发生变化。作用是为了改善编译器的优化能力。
const
  • 在C++(而不是C)中,const对默认存储类型稍有影响。在默认情况下全局变量的链接性为外部的,但const全局变量的链接性是内部的,跟static一样。
函数和链接性
  • C/C++不允许在一个函数中定义另一个函数,因此所有函数的存储持续性都自动为静态的,即在整个程序执行期间都一直存在。
  • 在默认情况下,函数的链接性为外部的,即可以在文件间共享。
  • 可以在函数原型中使用关键字extern来指出函数是在另一个文件中定义的。
  • 可以使用关键字static将函数的链接性设置为内部的,使之只能在一个文件中使用。必须在原型和函数定义中使用该关键字。
  • C++单定义规则,即对于每个非内联函数,程序中只能包含一个定义。对于链接性在外部的函数来说,这意味着在多文件程序中,只能有一个文件包含该函数的定义,但使用该函数的每个文件都应包含其函数原型。
  • 内联函数不受单定义规则的约束,这允许程序员能够将内联函数的定义放在头文件中。
C++查找函数
  • 假如在程序的某个文件中调用一个函数。如果函数原型是静态的,则编译器将只在该文件中查找函数定义;否则,编译器(包含链接程序)将在所有的程序文件中查找。如果找到两个定义,编译器将报错,因为每个外部函数只能有一个定义。如果在程序文件中没有找到,编译器将在库中搜索。这意味着如果顶一个与库函数同名的函数,编译器将使用程序员使用的版本,而不是库函数。
语言链接性(language linking)
  • 链接程序要求每个不同的函数都有不同的符号名。在C语言中,一个名词只对应一个函数。但在C++中,同一个名词可能对应多个函数,必须将这些函数翻译为不同的符号名称。因此,C++编译器执行名称矫正或名称修饰,为重载函数生成不同的符号名称。
存储方案和动态分配
  • 使用new分配的内存被称为动态内存,它们是用操作符new和delete控制,而不是由作用域和链接性规则控制。因此可以在一个内存中分配动态内存,而在另一个函数中释放。
  • 与自动内存不同,动态内存不是LIFO,其分配和释放顺序要取决于new和delete在何时以何种方式被使用。
  • 通常,编译器使用3块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,另外一块用于动态存储。
  • 虽然存储方案概念不适用于动态内存,但适用于用来跟踪动态内存的自动和静态指针变量
布局new操作符
  • 通常,new负责在堆(heap)中找到一个足以能够满足要求的内存块。
  • 布局(placement)new操作符是new操作符的变体,能够指定要使用的位置。可以使用这种特性来设置其内存管理规程或处理需要通过特定地址进行访问的硬件。
  • 要使用布局new特性,首先需要包含头文件new,它提供了这种版本的new操作符的原型;然后将new操作符用于提供了所需地址的参数。除需要指定参数外,句法与常规new操作符相同。具体地说,使用布局new操作符时,变量后面可以有方括号,也可以没有。
名称空间
  • C++中,名称可以是变量、函数、结构、枚举、类以及类和结构的成员。
传统的C++名称空间
  • 声明区域(declaration region):是可以在其中进行声明的区域。
  • 潜在作用域(potential scope):变量的潜在作用域从声明点开始,到其声明区域的结尾。因此潜在作用域比声明区域小,这是由于变量必须定以后才能使用。
  • 变量并非在其潜在作用域内的任何位置都可见。变量对程序而言可见的范围被称为作用域(scope)。
  • C++关于全局变量和局部变量的规则定义了一种名称空间层次。每个声明区域都可以声明名称,这些名称独立于在其他声明区域中声明的名称。
  • 在一个函数中声明的局部变量不会与在另一个函数中声明的局部变量冲突。
新的名称空间特性
  • C++可以通过定义一种新的声明区域来创建命名的名称空间,目的之一是提供一个声明名称的区域。
  • 一个名称空间中的名称不会与另一个名称空间的相同名称发生冲突,同事允许程序的其他部分使用该名称空间中声明的东西。
  • 使用关键字namespace创建名称空间。
  • 名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。
  • 除了用户定义的名称空间外,还存在另一个名称空间——全局名称空间(global namespace),它对应于文件级声明区域。
  • 通过作用域解析操作符(::)来访问给定名称空间中的名称。
  • 未被装饰的名称被称为未限定的名称(unqualified name);包含名称空间的名称被称为限定的名称(qualified name)。
using声明和using编译指令
  • C++提供了两种机制(using声明和using编译指令)来简化对名称空间中名称的使用。
  • using声明使特定的标识符可以用,using编译指令使整个名称空间可用。
  • using声明由被限定的名称和它前面的关键字using组成
    using Jill::fetch;
  • using声明将特定的名称添加到它所属的声明区域中。
  • 在函数的外面使用using声明时,将把名称添加到全局名称空间中。
  • using声明使一个名称可用,而using编译指令使所有的名称都可用。
  • using编译指令由名称空间名和它前面的关键字using namespace组成,它使名称空间中的所有名称都可用,而不需要使用作用域解析操作符。
    using namespace Jack;
  • 在全局声明区域中使用using编译指令,将使该名称空间的名称全局可用。
  • 在一个函数中使用using编译指令,将使其中的名称在该函数中可用。
  • 使用using编译指令导入一个名称空间中所有的名称与使用多个using声明是不一样的,而更像是大量使用作用域解析操作符。
  • 使用using声明时,如同声明了相应的名称。如果某个名称已经在函数中声明了,则不能用using声明导入相同的名称。
  • 使用using编译指令时,将进行名称解析,如果在包含using声明和名称空间本身的最小声明区域中声明了名称。
  • 如果名称空间是全局的,使用using编译指令导入一个已经在函数种声明的名称,则局部名称将隐藏名称空间名,就像隐藏同名的全局变量一样。
  • 假设名称空间和声明空间定义了相同的名称。如果试图使用using声明将名称空间的名称导入该声明区域,则这两个名称发生冲突,从而出错。如果使用using编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间版本。
  • 一般,使用using声明比使用using编译指令更安全,这是由于它只导入指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。
嵌套式名称空间
	namespace elements
	{
		namespace fire
		{
			int flame;
			…
		}
		float water;
	}
名称空间及前途
  • 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量。
  • 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
  • 如果开发了一个函数库或类库,将其放在一个名称空间中。
  • 仅当编译指令using作为一种将旧代码转换为使用名称空间的权宜之计。
  • 不要在头文件中使用using编译指令。
  • 导入名称时,首选使用作用域解析操作符或using声明的方法。
  • 对于using声明,首选将其作用域设置为局部而不是全局。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值