本章内容包括:
- 单独编译
- 存储持续性、作用域和链接性
- 定位new运算符
- 名称空间
9.1 单独编译
- C++允许甚至鼓励程序员将组件放在独立的文件中。可以单独编译这些文件,然后将它们链接成可执行程序。C++既编译程序,也管理链接器。如果只修改了一个文件,则可以只重新编译该文件,然后将它与其他文件的编译版本链接。
- 将程序分成三个部分:头文件(包含结构声明和使用这些结构的函数的原型)、源代码文件(包含与结构有关的函数的代码)、源代码文件(包含调用与结构相关的函数的代码)
- 不要将函数定义或变量声明放在头文件中。如果在头文件包含一个函数定义,然后在其他两个文件(属于同一个程序)中包含该头文件,则同一个程序中将包含同一个函数的两个定义,除非函数是内联的,否则将出错。
- 头文件包含的内容:函数原型、使用#define或const定义的符号常量、结构声明、类声明、模板声明、内联函数。
- 被声明为const的数据和内联函数有特殊的链接属性,因此可以将其放在头文件中。
9.1.1 头文件管理
- 在同一个文件中只能将同一个头文件包含一次。有一种标准的C/C++技术可以避免多次包含同一个头文件。它是基于预处理器编译指令#ifndef的。
下面的代码片段意味着仅当以前没有使用预处理器编译指令#define定义名称COORDIN_H_时,才处理#ifndef和#endif之间的语句:
#ifndef COORDIN_H_
#define COORDIN_H_
//place include file contents here
...
#endif
//这种方法并不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容
9.2 存储连续性、作用域和链接性
- 自动存储持续性:在**函数定义中声明的变量(包括函数参数)**的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。
- 静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。
- 线程存储持续性:如果变量是使用thread_local声明的,则其声明周期与所属的线程一样长。
- 动态存储持续性:用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储或堆。
9.2.1 作用域和链接
- 作用域描述了名称在文件的多大范围内可见。
- 链接性描述了名称如何在不同单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量没有链接性,因为它们不能共享。
- 在函数原型作用域中使用的名称只在包含参数列表的括号内可用。在类中声明的成员的作用域为整个类。在名称空间中声明的变量的作用域为整个名称空间。
9.2.2 自动存储持续性
- 使用C++11中的auto:在C++11中,关键字auto用于自动类型判断。由于只能将关键字auto用于默认为自动的变量,因此程序员几乎不使用它。它的主要用途是指出当前变量为局部自动变量。
- 自动变量的初始化:可以使用任何在声明时其值为已知的表达式来初始化自动变量。
- 自动变量和栈:栈是先进后出的,
- 寄存器变量:使用CPU寄存器来存储自动变量,使用register关键字,这旨在提高变量的访问速度。关键字register显示地提出变量是自动的。使用它的唯一原因是,指出程序员想使用一个自动变量,这个变量名称可能与外部变量相同。
9.2.3 静态存储变量
- C++为静态存储变量提供了三种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块访问)
- 静态变量在整个程序执行期间一直存在。
- 如果没有显示地初始化静态变量,编译器将把它设置为0。
- 创建链接性为外部的静态持续变量,必须在代码块的外部声明它;创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用static限定符;要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static限定符。
- 关键字static的两种用法:用于局部声明,以指出变量是无链接性的静态变量时,static表示的时存储连续性;用于代码块外的声明时,static表示内部链接性,而变量是静态持续性。
- 静态变量的初始化:默认零初始化、常量表达式初始化和动态初始化。零初始化和常量表达式初始化被统称为静态初始化,意味着在编译器处理文件时初始化变量。动态初始化意味着变量将在编译后初始化。
9.2.4 静态持续性、外部链接性
- 外部变量也称全局变量。
9.2.4.1 单定义规则
- 变量只能有一次定义,C++有两种变量声明:一种是定义声明,它给变量分配存储空间;另一种是引用声明,它不给变量分配存储空间,因为它引用已有的变量。
- 引用声明使用关键字extern,且不进行初始化,否则声明为定义,导致分配内存空间。
- 如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义,但在使用该变量的其他所有文件中,都必须使用关键字extern声明它。
- 如果在函数中声明了一个与外部变量同名的变量,这种声明将被视为一个自动变量的定义,自动变量将隐藏同名的全局变量。
- C++提供了作用域解析运算符(:😃。放在变量名前面时,该运算符表示使用变量的全局版本。
9.2.5 静态持续性、内部链接性
- 如果文件定义了一个静态外部变量,其名称与另一个文件中声明的常规外部变量相同,则在该文件中,静态变量将隐藏常规外部变量。这没有违反单定义规则,因为关键字static指出变量的链接性为内部,因此并非要提供外部定义。
9.2.6 静态存储持续性、无链接性
- 将static限定符用于在代码块中定义的变量。在代码块中使用static时,将导致局部变量的存储持续性为静态的。
- 静态局部变量在所属代码块不处于活跃状态时仍然存在。因此在两次函数调用之间,静态变量的值将保持不变。8
- 如果初始化了静态局部变量,则程序只在启动时进行一次初始化。
9.2.7 说明符和限定符
- 存储说明符:auto; register; static; extern; thread_local; mutable
- 在同一个声明中不能使用多个说明符,但thread_local除外,它可与static或extern结合使用。
9.2.7.1 cv-限定符
- 关键字volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。假设编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在这两次使用之间不会发生变化。如果不将变量声明为volatile,则编译器将进行这种优化;将变量声明为volatile,相当于告诉编译器不要进行这种优化。
9.2.7.2 mutable
- 可以用mutable来指出,即使结构(或类)变量为const,其某个成员也可以被修改。
9.2.7.3 再谈const
- const全局变量的链接性为内部的,因此可以在所有文件中使用相同的声明。
- 可以使用extern关键字来覆盖默认的内部链接性。
- 单个const在多个文件之间共享,只有一个文件可对其进行初始化。
9.2.8 函数和链接性
- C++不允许在同一个函数中定义另一个函数,因此所有函数的存储连续性都自动为静态的,即在整个程序执行期间都一直存在。
- 在默认情况下,函数的链接性为外部的,即可以在文件间共享。
- 可以在函数原型中使用关键字extern指出函数是在另一个文件定义的。还可以使用关键字static将函数的链接性设置为内部的,必须在原型和函数定义中使用该关键字。
- 静态函数将覆盖外部定义。
- 单定义规则也适用于非内联函数,程序只包含一个定义。对于链接性为外部的函数来说,在多文件程序中,只能有一个文件(可能是库文件)包含该函数的定义,但使用该函数的每个文件都应包含其函数原型。内联函数不受这项规则约束,这允许程序员能够将内联函数的定义放在头文件中。C++要求同一个函数的所有内联定义都必须相同。
9.2.9 语言链接性
- 链接程序要求每个不同的函数都有不同的符号名。
- 可以使用函数原型来指出要使用的约定
extern "C" void spiff(int);//use C protocol
extern void spoff(int);//use C++ protocol
extern "C++" void spaff(int);//use C++ protocol
9.2.10 存储方案和动态分配
- 动态内存由new和delete控制,而不是由作用域和链接性控制。因此,可以在一个函数中分配动态内存,而在另一个函数中释放。
- 动态内存不是先入后出,它分配和释放顺序取决于new和delete在何时以何种方式被使用。通常编译器使用三块独立的内存:一块用于静态变量,一块用于自动变量,另外一块用于动态存储。
- 存储方案适用于用来跟踪动态内存的自动和静态指针变量。
float * p_fees = new float [20];
由new分配的80个字节的内存将一直保留在内存中,直到使用delete运算符将其释放。
9.2.10.1 使用new运算符初始化
- 如果要初始化动态分配的变量:
如果要为内置的标量类型分配存储空间并初始化,可在类型名后面加上初始值,并将其用括号括起:int *pi=new int(6);
这种括号语法也适用于有合适构造函数的类。
如果要初始化常规结构或数组,需要使用大括号的列表初始化:
struct where {double x; double y; double z;};
where* one = new where {2.5, 5.3, 7.2};
在C++中,还可以使用列表初始化单值变量。
9.2.10.2 new失败时
让new返回空指针;引发异常std::bad_alloc。
9.2.10.3 new:运算符、函数和替换函数
- 运算符new和new[]分别调用函数:
void* operator new(std::size_t); void operator new[ ](std::size_t);
这些函数被称为分配函数,它们位于全局名称空间中。同样,也有由delete和delete[]调用的释放函数:void operator delete(void*); void operator delete[] (void*);
它们使用运算符重载。std::size_t是一个tyoedef,对应于合适的整型。 - C++ 将这些函数称为可替换的。可定义作用域为类的替换函数,并对其进行定制,以满足该类的内存分配需求
9.2.10.4 定位new运算符
- new负责在堆找到一个足以满足要求的内存块。new运算符还有一种变体被称为定位new运算符,能够指定要使用的位置。
- 要使用定位new特性,需要包含头文件new。
#include<new>
struct chaff
{
char dross[20];
int slag;
}
char buffer1[50];
char buffer2[500];
int main()
{
chaff* p1,*p2;
int* p3,*p4;
p1=new chaff;
p3=new int[20];
p2= new (buffer1)chaff;
p4= new (buffer2)int[20];
}
9.3 名称空间
9.3.1 传统的C++名称空间
- 声明区域:可以在其中进行声明的区域。可以在函数外面声明全局变量,这种变量的声明区域是其声明所在的文件;对于在函数中声明的变量,其声明区域是其声明所在的代码块。
- 潜在作用域:变量的潜在作用域从声明点开始,到其声明区域的结尾。变量并非在其潜在作用域内的任何位置都可见,它可能被另一个在嵌套声明区域中声明的同名变量隐藏。变量对程序而言可见的范围被称为作用域。
9.3.2 新的名称空间特性
- C++通过定义一种新的声明区域来创建命名的名称空间,这样做目的是提供一个声明名称的区域。一个名称空间中的名称不会于另外一个名称空间的相同名称起冲突,同时允许程序的其他部分使用该名称空间中声明的东西。
namespace Jack{
double pail;
void fetch();
int pal;
struct Well{...};
}
namespace Jill{
double bucket(double n){...}
double fetch;
int pal;
struct Hill{...};
}
- 名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。在默认情况下,在名称空间中声明的名称的链接性是外部的。
- 全局名称空间:全局变量位于全局名称空间中。
- 任何名称空间中的名称都不会于其他名称空间中的名称发生冲突。
- 未被装饰的名称称为未限定的名称;包含名称空间的名称称为限定的名称。
9.3.2.1 using声明和using编译指令
- using声明使特定的标识符可用;using编译指令使整个名称空间可用。
- using声明将特定的名称添加到它所属的声明区域中:
using Jill::fetch;
完成该声明后,就可以使用名称fetch来代替Jill::fetch。 - 由于using声明将名称添加到局部声明区域中,在函数内部避免了将另一个局部变量也命名为fetch。fetch将覆盖同名的全局变量。
- 在函数外面使用using声明时,将把名称添加到全局名称空间中。
using编译指令和using声明之比较
- 如果某个名称已经在函数中声明了,则不能用using声明导入相同的名称。使用using编译指令时,进行名称解析。
namespace Jill{
double bucket(double n){...}
double fetch;
int pal;
struct Hill{...};
}
int main()
{
using namespace Jill;
Hill Thrill;
double water =bucket(2);
double fetch;
cin>>fetch;//read a value into the local fetch
cin>>::fetch;//read a value into the global fetch
cin>>::Jill::fetch;//read a value into Jill::fetch
...
}
int foom()
{
Hill top;//ERROR
Jill::Hill crest;//VALID
}
在main()中,名称Jill::fetch被放在局部名称空间中,但它作用域不是局部的(使用using编译指令),因此不会覆盖全局的fetch。局部声明的fetch将隐藏Jill::fetch和全局fetch.
- 虽然函数中的using编译指令将名称空间的名称视为在函数之外声明的,但它不会使得该文件中的其他函数能够使用这些名称。
- 一般说来,使用using声明比使用using编译指令更安全。如果该名称与局部名称发生冲突,编译器将发出提示。名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确地知道添加了哪些名称。
名称空间的其他特性
- 可以将名称空间声明进行嵌套:
namespace elements
{
namespace fire
{
int flame;
...
}
float water;
flame指的是elements:🔥:flame。同样,可以使用using编译指令使内部的名称可用:using namespace elements::fire;
- 也可以在名称空间中使用using编译指令和usng声明。
namespace myth
{
using Jill::fetch;
using namespace elements;
using std::cout;
using std::cin;
}
using namespace myth;
和using namespace myth; using namespace elements;
等价。
未命名的名称空间
- 可以通过省略名称空间的名称来创建未命名的名称空间:
namespace
{
int ice;
int bandycoot;
}
由于这种名称空间没有名称,因此不能显示地使用using编译指令或using声明来使它在其他位置都可用。即不能在未命名名称空间所属文件之外的其他文件中使用该名称空间中的名称。这提供了链接性为内部的静态变量的替代品。
9.3.3 名称空间及其前途
- 使用已命名的名称空间中声明的变量,而不是使用外部全局变量。
- 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
- 如果开发了一个函数库或类库,将其放在一个名称空间中。
- 仅将编译指令using作为一种将旧代码转换为使用名称空间的权宜之计。
- 不要在头文件使用using编译指令。
- 导入名称时,首选使用作用域解析运算符或using声明的方法。
- 对于using声明,首先将其作用域设置为局部而不是全局。