看《C++ Primer Plus》时整理的学习笔记,部分内容完全摘抄自《C++ Primer Plus》(第6版)中文版,Stephen Prata 著,张海龙 袁国忠译,人民邮电出版社。只做学习记录用途。
-
9.1 单独编译
- 9.1.1 程序组织策略
- 9.1.2 头文件
- 9.1.3 源代码文件
-
9.2 存储持续性、作用域和链接性
- 9.2.1 存储持续性种类
- 9.2.2 作用域种类
- 9.2.3 链接性种类
- 9.2.4 自动存储持续性变量
- 9.2.5 静态存储持续性变量
- 9.2.6 外部链接性的静态变量
- 9.2.7 内部链接性的静态变量
- 9.2.8 无链接性的静态变量
- 9.2.9 存储说明符和 cv 限定符
- 9.2.10 函数链接性
- 9.2.11 语言链接性
-
9.3 定位 new 运算符
- 9.3.1 动态存储持续性
- 9.3.2 常规 new 运算符的使用
- 9.3.3 定位 new 运算符的使用
-
9.4 名称空间
- 9.4.1 传统的 C++ 名称空间
- 9.4.2 新增的 C++ 名称空间
- 9.4.3 using 声明和 using 编译指令
- 9.4.4 嵌套的名称空间
- 9.4.5 未命名的名称空间
- 9.4.6 名称空间的使用方法
本章介绍 C++ 的内存模型和名称空间,包括数据的存储持续性、作用域和链接性,以及定位 new
运算符。
9.1 单独编译
C++ 鼓励程序员将组件函数放在独立的文件中,可以单独编译这些文件,然后将它们链接成可执行的程序。(通常,C++ 编译器既编译程序,也管理链接器。)如果只修改了一个文件,则可以只重新编译该文件,然后将它与其他文件的编译版本链接,大多数集成开发环境(如 Microsoft Visual C++ 和 Apple Xcode)都提供了这一功能,减少了人为管理的工作量。
9.1.1 程序组织策略
以下是一种非常有效且常用的程序组织策略,它将整个程序分为三个部分:
- 头文件:包含结构声明和使用这些结构的函数的原型。
- 源代码文件:包含定义与结构有关的函数的代码。
- 源代码文件:包含调用与结构有关的函数的代码。
在编译时,C++ 预处理器会将源代码文件中的 #include
指令替换成头文件的内容。源代码文件和它所包含的所有头文件被编译器看成一个包含以上所有信息的单独文件,该文件被称为翻译单元(translation unit)。描述一个具有文件作用域的变量时,它的实际可见范围是整个翻译单元。如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的头文件。下图简要地说明了在 UNIX 系统中,将含 1 个头文件 coordin.h
与 2 个源代码文件 file1.cpp
、file2.cpp
的程序编译成一个 out
可执行程序的过程。
由于不同 C++ 编译器对函数的名称修饰方式不同,因此由不同编译器创建的二进制模块(对象代码文件,如上图中的 file1.o
、file2.o
)很可能无法正确地链接,因为两个编译器将为同一个函数生成不同的名称修饰。这时,可使用同一个编译器重新编译所有源代码文件,来消除链接错误。
9.1.2 头文件
在同一个文件中只能将同一个头文件包含一次,否则可能会出现重复定义的问题。一般在头文件中使用预处理器编译指令 #ifndef
(即 if not defined)来避免多次包含同一个头文件。编译器首次遇到该文件时,名称 COORDIN_H_
没有定义(加上下划线以获得一个在其他地方不太可能被定义的名称),这时编译器将查看 #ifndef
和 #endif
之间的内容,并通过 #define
定义名称 COORDIN_H_
。如果在同一个文件中遇到其他包含 coordin.h
的代码,编译器将知道 COORDIN_H_
已经被定义了,从而跳到 #endif
后面的一行。但这种方法并不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容。
#ifndef COORDIN_H_
#define COORDIN_H_
//头文件内容
...
#endif
在头文件中,可以包含以下内容:
- 使用
#define
或const
定义的符号常量。 - 结构声明,它们并不创建变量,只是告诉编译器当需要创建它们时应该如何创建。
- 类声明,同结构声明一样,它们并不创建类,只是告诉编译器当需要创建它们时应该如何创建。
- 模板定义,它们不是将被编译的代码,只是被用来指示编译器如何生成与源代码中的函数调用相匹配的函数定义。
- 常规函数原型。
- 内联函数定义。
不要将常规函数定义(非函数模板、非内联函数)或常规变量声明(非 const
变量、非 static
变量)放到头文件中,否则当同一个程序的两个源文件都包含该头文件时,可能会出现重复定义的问题。
9.1.3 源代码文件
在源代码文件开头处,通常会使用 #include
预编译指令包含所需的头文件,有以下两种包含方式:
-
使用尖括号
<>
包含,例如#include <iostream>
,如果文件名包含在尖括号中,则 C++ 编译器将在存储标准头文件的主机系统的文件系统中查找,一般用来包含系统自带的头文件或标准头文件。 -
使用双引号
""
包含,例如#include "coordin.h"
,如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录(或其它目录,这取决于编译器以及用户设置),如果没有在那里找到头文件,则将在标准位置查找,一般用来包含用户自定义的头文件。
不要在源代码文件中包含其它源代码文件,这可能出现重复定义的问题。在源代码文件中,一般包含头文件中常规函数原型所对应的函数定义(声明与定义相分离的策略,声明位于头文件中,定义位于源代码文件中)、类声明中成员函数的定义、全局变量声明等。
9.2 存储持续性、作用域和链接性
不同的 C++ 存储方式是通过存储持续性、作用域和链接性来描述的,下表总结了引入名称空间之前使用的存储特性。
存储描述 | 持续性 | 作用域 | 链接性 | 声明方式 |
---|---|---|---|---|
常规自动变量 | 自动存储持续性 | 代码块 | 无 | 在代码块中 |
寄存器自动变量 | 自动存储持续性 | 代码块 | 无 | 在代码块中,使用关键字 register
|
外部链接性的静态变量 | 静态存储持续性 | 翻译单元 | 外部 | 不在任何函数内,分为定义声明和引用声明 |
内部链接性的静态变量 | 静态存储持续性 | 翻译单元 | 内部 | 不在任何函数内,使用关键字 static
|
无链接性的静态变量 | 静态存储持续性 | 代码块 | 无 | 在代码块中,使用关键字 static
|
下面对这些存储特性进行逐一介绍。
9.2.1 存储持续性种类
C++ 使用三种(C++11 中是四种)不同的方案来存储数据,这些方案的区别就在于数据保留在内存中的时间,即存储持续性。
- 自动存储持续性:在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。
-
静态存储持续性:在函数定义外部定义的变量和使用关键字
static
定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。 -
动态存储持续性:用
new
运算符分配的内存将一直存在,直到使用delete
运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储(free store)或堆(heap)。 -
线程存储持续性(C++11):当前,多核处理器很常见,这些 CPU 可同时处理多个执行任务。这让程序能够将计算放在可并行处理的不同线程中。如果变量是使用关键字
thread_local
声明的,则其生命周期与所属的线程一样长。
9.2.2 作用域种类
作用域(scope)描述了名称在文件(翻译单元)的多大范围内可见。C++ 变量的作用域有多种:
- 局部作用域:作用域为局部的变量只能在声明它的代码块(由一对花括号括起来的多条语句)中使用,不能在其它地方使用。所有自动变量的作用域都是局部的,静态变量的作用域是全局还是局部取决于它是如何被声明的。例如:函数体内声明的常规变量、函数形参、无链接性的静态变量。
- 全局作用域:作用域为全局的变量在其声明位置到文件结尾之间都可以用,全局作用域也称为文件作用域。例如在文件中函数定义之前定义的变量(外部链接性的静态变量、内部链接性的静态变量)。
-
函数原型作用域:在函数原型作用域中使用的名称只在包含参数列表的括号内可用。C++11 中可在原型括号后面使用
decltype
关键字推断返回类型,但这实际上并没有使用参数的值,只用它们来做了类型推断。 - 类作用域:在类中声明的成员的作用域为整个类,它们又有三种不同的属性:公有、私有和继承,这将在后续章节介绍。
- 名称空间作用域:在名称空间中声明的变量的作用域为整个名称空间,全局作用域是名称空间作用域的特例。
C++ 函数的作用域可以是类作用域或名称空间作用域(包括全局作用域),但不能是局部作用域。
9.2.3 链接性种类
链接性(linkage)描述了名称如何在不同单元间共享。有以下三种链接性:
- 外部链接性:链接性为外部的名称可在文件间共享。
- 内部链接性:链接性为内部的名称只能由一个文件中的函数共享。
- 无链接性:自动变量的名称没有链接性,因为它们不能共享。
9.2.4 自动存储持续性变量
自动变量的初始化:在默认情况下,在函数或代码块中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性,只有在定义它们的函数中才能使用它们,当函数结束时,这些变量都将消失。可以使用任何在声明时其值为已知的表达式来初始化自动变量,若在声明时未进行初始化,则其值是未知的。
int w; //未被初始化,其值未知
int x = 5; //被数字字面常量初始化
int y = 2*x; //被可计算值的表达式初始化
int z = INT_MAX - 1; //被常量表达式初始化
自动变量的内存管理:自动变量的数目随函数的开始和结束而增减,程序常用的方法是留出一段内存,并将其视为栈,以管理变量的增减。
- 栈的默认长度取决于实现,但编译器通常提供改变栈长度的选项,Microsoft Visual Studio 默认大小为 1 MB。
- 栈的虚拟内存是连续的,但物理内存不一定连续,程序使用两个指针来跟踪栈,一个指针指向栈底(栈的开始位置),另一个指针指向栈顶(栈的下一个可用内存单元)。
- 当函数被调用时,其中的自动变量将被加入到栈中,栈顶指针指向变量后面的下一个可用的内存单元。当函数结束时,栈顶指针被重置为函数被调用前的值,从而释放新变量使用的内存。
- 栈是 LIFO 的(后进先出),即最后加入到栈中的变量首先被弹出。这种设计简化了参数传递,函数调用时将其参数的值放在栈顶,然后重新设置栈顶指针,被调用的函数根据其形参描述来确定每个参数的地址。
函数 fib()
被调用时,传递一个 2 字节的 int
和一个 4 字节的 long
,这些值被加入到栈中。当 fib()
开始执行时,它将名称 real
和 tell
同这两个值关联起来。当 fib()
结束时,栈顶指针重新指向以前的位置。新值没有被删除,但不再被标记,它们所占据的空间将被下一个将值加入到栈中的函数调用所使用。(上图做了简化,实际上函数调用可能传递其它信息,比如返回地址,深入学习可查看函数调用时的汇编代码)
自动变量的隐藏:如下例子所示,在函数内的代码块中,新的同名自动变量 value
隐藏了代码块外部的 value
变量,当程序离开该代码块时,原来的 value
变量又重新可见。
int main()
{
//自动变量1
int value = 1;
//输出结果为0x0080FDC8
cout << &value << endl;
//用花括号括起来的代码块
{
//自动变量2
int value = 2;
//输出结果为0x0080FDBC
cout << &value << endl;
}
//输出结果为0x0080FDC8
cout << &value << endl;
return 0;
}
auto
关键字:在 C++11 之前,关键字 auto
被用来显式地指出变量为局部自动存储,且只能被用于默认为自动存储的变量;在 C++11 中,关键字 auto
被用来做自动类型推断。
//C++11之前,显式指明x为局部自动存储
auto double x = 53.0;
//C++11中,用于自动类型推断
auto x = 53.0;
register
关键字:在 C++11 之前,关键字 register
被用来建议编译器使用 CPU 寄存器来存储自动变量,提示编译器这种变量用得很多,可对其做特殊处理(寄存器变量);在 C++11 中,关键字 register
被用来显式地指出变量是局部自动存储,且只能被用于原本就是自动存储的变量,这与 auto
以前的用法完全相同,使用它的唯一原因是,指出一个自动变量,这个自动变量可能与外部变量同名。
//C++11之前,建议编译器用寄存器存储x
register int x = 53;
//C++11中,显式指明x为局部自动存储
register int x = 53;
9.2.5 静态存储持续性变量
静态变量的种类:C++ 为静态存储持续性变量提供了 3 种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块中访问)。
- 要想创建外部链接性的静态变量,必须在代码块的外面声明它,如下代码片段中的
global_all_file
变量,可以在程序的其他文件中使用它; - 要想创建内部链接性的静态变量,必须在代码块的外面声明它并使用
static
关键字,如下代码片段中的global_one_file
变量,只能在包含static int global_one_file = 50;
语句的文件中使用它。 - 要想创建没有链接性的静态变量,必须在代码块的内部声明它并使用
static
关键字,如下代码片段中的local_one_function
变量,它的作用域为局部,只能在func()
函数中使用它,与自动变量不同的是,即使在func()
函数没有被执行时,它也留在内存中。
int global_all_file = 1000; //外部链接性的静态变量
static int global_one_file = 50; //内部链接性的静态变量
int main()
{
...
}
void func()
{
static int local_one_function = 10; //无链接性的静态变量
...
}
静态变量的内存管理:静态变量在整个程序执行期间一直存在,静态变量的数目在程序运行期间是不变的。程序不需要使用特殊的装置(如栈)来管理它们,编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。因此,与自动变量相比,它们的寿命更长。
静态变量的初始化:所有静态变量都有如下初始化特征:未被初始化的静态变量的所有位都被设置为 0,这种变量被称为零初始化的(zero-initialized),包括静态数组和结构。对于标量类型,零将被强制转换为合适的类型,例如空指针用 0 表示,但内部可能采用非零表示。除默认的零初始化外,还可对静态标量进行常量表达式初始化和动态初始化。零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译器处理文件(翻译单元)时初始化变量,动态初始化意味着变量将在编译后初始化。
#include <cmath>
int x; //零初始化
int y = 5; //常量表达式初始化
int z = 13 * 13; //常量表达式初始化
int u = 2 *sizeof(long) + 1; //常量表达式初始化
double pi = 4.0 * atan(1.0); //动态初始化
首先,所有静态变量都被零初始化,而不管程序员是否显式地初始化了它。接下来,如果使用常量表达式初始化了变量,且编译器仅根据当前翻译单元就可计算表达式,编译器将执行常量表达式初始化,必要时,编译器将执行简单计算,C++11 新增了关键字 constexpr
,这增加了创建常量表达式的方式。最后,在程序执行时将进行动态初始化。上述程序中,x
、y
、z
、u
和 pi
首先被零初始化,然后编译器计算常量表达式的值对 y
、z
和 u
进行常量表达式初始化,但要初始化pi
,必须调用函数 atan()
,这需要等到该函数链接且程序执行时。
9.2.6 外部链接性的静态变量
外部变量的使用:链接性为外部的变量通常简称为外部变量,它们的存储持续性为静态,作用域为整个文件,但也可以在同一项目的其他文件中使用它。外部变量的使用条件有两个:
- 一方面,在每个使用外部变量的文件中,都必须声明它。
- 另一方面,C++ 有单定义规则 "One Definition Rule",简称 ODR,该规则指出,变量只能有一次定义。
C++ 提供了两种变量声明方式,来满足这两个条件:
-
定义声明(defining declaration)或简称为定义(definition),它给变量分配存储空间。定义声明不使用关键字
extern
,或者在使用关键字extern
的同时对变量进行了人为初始化(可用此法来修改const
全局常量默认的内部链接性为外部链接性,见后面的 cv 限定符小节)。 -
引用声明(referencing declaration)或简称为声明(declaration),它引用已有的变量,不给变量分配存储空间。需要使用关键字
extern
且不能进行初始化,否则该声明将变为定义声明。
int x; //定义声明
extern int y = 0; //定义声明
extern int z; //引用声明,必须在其他文件中进行定义
在多个文件中使用外部变量时,必须且只能在一个文件中包含该变量的定义声明(满足第二个使用条件),在使用该变量的其他所有文件中,都必须使用关键字 extern
声明它,即包含该变量的引用声明(满足第一个使用条件)。
//文件file01.cpp
int dogs = 22; //定义声明
extern int cats = 40; //定义声明
//文件file02.cpp
extern int dogs; //引用声明
extern int cats; //引用声明
外部变量的隐藏:局部变量可能隐藏同名的全局变量,这并不违反单定义规则,虽然程序中可包含多个同名的变量的定义,但每个变量的实际作用域不同,作用域相同的变量没有违反单定义规则。定义与外部变量同名的局部变量后,局部变量将隐藏外部全局变量,但 C++ 提供了作用域解析运算符双冒号(::
),将它放在变量名前面,可使用该变量的全局版本。
//文件file01.cpp
int dogs = 22; //定义声明
//文件file02.cpp
extern int dogs; //引用声明
void local()
{
int dogs = 88;
cout << dogs << endl; //输出88
cout << ::dogs << endl; //输出22
}
int main()
{
...
}
9.2.7 内部链接性的静态变量
将 static
关键字用于作用域为整个文件的变量时,该变量的链接性将为内部的。链接性为内部的变量只能在其所属的文件中使用,无法在其他文件中使用,但外部变量都具有外部链接性,可以在其他文件中使用。
//文件file02.cpp
static int errors = 2; //内部链接性的静态变量,只能在其所属文件中使用
可使用外部变量在多文件程序的不同部分之间共享数据;可使用链接性为内部的静态变量在同一个文件中的多个函数之间共享数据(名称空间提供了另一种共享数据的方法)。另外,如果将作用域为整个文件的变量变为内部链接性的,就不必担心其名称与其他文件中的作用域为整个文件的变量发生冲突。因为此时若存在同名的外部变量,具有内部链接性的变量将完全隐藏同名外部变量,且无法通过 extern
关键字以及 ::
作用域解析运算符访问到同名外部变量。
//文件file01.cpp
int errors = 1; //外部链接性静态变量
//文件file02.cpp
static int errors = 2; //内部链接性静态变量
void func()
{
int errors = 3;
cout << errors << endl; //结果为3
cout << ::errors << endl; //结果为2
}
void fund()
{
extern int errors;
cout << errors << endl; //结果为2
cout << ::errors << endl; //结果为2
}
int main()
{
...
}
9.2.8 无链接性的静态变量
将 static
关键字用于在代码块中定义的局部变量时,该变量没有链接性,且将导致局部变量的存储持续性为静态的。这意味着虽然该变量只在该代码块中可用,但它在该代码块不处于活动状态时仍然存在。因此在两次函数调用之间,静态局部变量的值将保持不变。另外,如果初始化了静态局部变量,则程序只在启动时进行一次初始化,以后再次调用函数时,将不会像自动变量那样再次被初始化。
void func()
{
//初始化只进行一次
static int count = 0;
//每次调用时改变其值
count++;
//输出
cout << count << endl;
}
int main()
{
func(); //输出1
func(); //输出2
func(); //输出3
func(); //输出4
return 0;
}
9.2.9 存储说明符和 cv 限定符
C++ 关键字中包含以下六个存储说明符(storage class specifer),它们提供了有关存储的信息,除了 thread_local
可与 static
或 extern
结合使用,其他五个说明符不能同时用于同一个声明。
-
auto
关键字:在 C++11之前,可以在声明中使用关键字auto
指出变量为自动变量;在 C++11 中,auto
用于自动类型推断,已不再是存储说明符。 -
register
关键字:在 C++11 之前,关键字register
用于在声明中指示寄存器变量;在 C++11中,它只是显式地指出变量是局部自动存储。 -
static
关键字:关键字static
被用在作用域为整个文件的声明中时,表示内部链接性;被用于局部声明中时,表示局部变量的存储持续性是静态的,有人称之为关键字重载。 -
extern
关键字:关键字extern
表明是引用声明,即声明引用在其他地方定义的变量。 -
thread_local
关键字:关键字thread_local
指出变量的持续性与其所属线程的持续性相同,thread_local
变量之于线程,犹如常规静态变量之于整个程序。 -
mutable
关键字:关键字mutable
被用来指出,即使结构(或类)变量为const
,其某个成员也可以被修改。
//mutable变量不受const限制
struct mdata
{
int x;
mutable int y;
};
const mdata veep = {0, 0};
veep.x = 5; //不被允许
veep.y = 5; //可以正常运行
C++ 中常说的 cv 限定符是指 const
关键字和 volatile
关键字。关键字 volatile
表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化,例如:可以将一个指针指向某个硬件位置,其中包含了来自串行端口的时间或信息,在这种情况下,硬件(而不是程序)可能修改其中的内容,或者两个程序可能互相影响,共享数据,该关键字的作用是为了防止编译器进行相关的优化(若编译器发现程序在相邻的几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中,这种优化假设变量的值在这两次使用之间不会变化)。
关键字 const
表明,内存被初始化后,程序便不能再对它进行修改,除此之外,在 C++ 中,const
限定符对默认存储类型也稍有影响。在默认情况下全局变量的链接性为外部的,但 const
全局变量的链接性为内部的。因此,在 C++ 看来,全局定义 const
常量就像使用了 static
说明符一样:
//内部链接性的静态const常量,以下两种方式等效
const int x = 10;
static const int x = 10;
const
全局变量的这种特性意味着,可以将 const
常量的定义声明放在头文件中,只要在源代码文件中包含这个头文件,它们就可以获得同一组常量,此时每个定义声明都是其文件(翻译单元)所私有的,而不是所有文件共享同一组常量。若程序员希望某个 const
全局变量的链接性为外部的,可以在定义声明中增加 extern
关键字,来覆盖默认的内部链接性,此时就只能有一个文件包含定义声明,其他使用到该 const
常量的文件必须包含相应的 extern
引用声明,这个 const
常量将在多个文件之间共享。
//外部链接性的静态const常量
extern const int y = 10;
9.2.10 函数链接性
C++ 不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都是静态的,即在整个程序执行期间都一直存在。在默认情况下,函数的链接性为外部的,即可以在文件间共享。还可以使用关键字 static
将函数的链接性设置为内部的,使之只能在一个文件(翻译单元)中使用,必须同时在原型和定义中使用 static
关键字:
//链接性为内部的函数,只能在所在文件中使用
static int privateFunction(); //函数原型
//函数定义
static int privateFunction()
{
...
}
和变量一样,在定义内部链接性的函数的文件中,内部链接性函数定义将完全覆盖外部同名函数定义。单定义规则也适用于非内联函数,对于链接性为外部的函数来说,这意味着在多文件程序中,只能有一个文件(该文件可能是库文件)包含该函数的定义,但使用该函数的每个文件都应包含其函数原型(和外部变量不同的是,函数原型前可省略使用关键字 extern
)。内联函数则不受单定义规则的约束,可将内联函数定义写在头文件中,但 C++ 要求同一个函数的所有内联定义都必须相同。内部链接性的 static
函数定义也可写在头文件中,这样每个包含该头文件的翻译单元都将有各自的 static
函数,而不是共享同一个函数。
//文件file.cpp
#include <iostream>
#include <cmath>
double sqrt(double x) { return 0.0; }
int main()
{
using namespace std;
cout << sqrt(4.0) << endl; //结果为0
cout << ::sqrt(4.0) << endl; //结果为0
return 0;
}
在程序的某个文件中调用一个函数时,如果该文件中的函数原型指出该函数是静态的,则编译器将只在该文件中查找函数定义;否则,编译器(包括链接程序)将在所有的程序文件中查找,如果找到两个定义,编译器将发出错误消息,如果在程序文件中未找到,编译器将在库中搜索。这意味着如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本,而不是库函数。为养成良好的编程习惯,应尽量避免使用与标准库函数相同的函数名,上述程序在 Microsoft Visual Studio 2019 中的输出结果都为 0,但编译器会输出 C28251 的警告信息,如下图所示。
9.2.11 语言链接性
另一种形式的链接性——称为语言链接性(language linking)也对函数有影响,链接程序要求每个不同的函数都有不同的符号名。在 C 语言中,一个名称只对应一个函数,编译器可能将 spiff
这样的函数名翻译为 _spiff
,这种方法被称为 C 语言链接性(C language linking)。但在 C++ 中,由于函数重载,一个名称可能对应多个函数,编译器将执行名称修饰,可能将 spiff(int)
转换为 _spiff_i
,将 spiff(double, double)
转换为 _spiff_d_d
,这种方法被称为 C++ 语言链接性(C++ language linking)。因此,链接程序寻找与 C++ 函数调用匹配的函数时,使用的查询约定与 C 语言不同,若要在 C++ 程序中使用 C 库(静态库、动态库)中预编译的函数 spiff(int)
,应该使用如下函数原型来指出要使用的函数符号查询约定:
//使用C库中的预编译好的函数
extern "C" void spiff(int); //方式一
extern "C" //方式二
{
void spiff(int);
}
上面的两种方式都指出了使用 C 语言链接性来查找相应的函数,若要使用 C++ 语言链接性,可按如下方式指出:
//使用C++库中的预编译好的函数
void spiff(int); //方式一
extern void spiff(int); //方式二
extern "C++" void spiff(int); //方式三
extern "C++" //方式四
{
void spiff(int);
}
C 和 C++ 链接性是 C++ 标准指定的说明符,但实现可提供其他语言链接性说明符。
9.3 定位 new 运算符
9.3.1 动态存储持续性
使用 C++ 运算符 new
(或 C 函数 malloc()
)分配的内存,被称为动态内存。动态内存由运算符 new
和 delete
控制,而不是由作用域和链接性规则控制。动态内存的分配和释放顺序取决于 new
和 delete
在何时以何种方式被使用,因此,可以在一个函数中分配动态内存,而在另一个函数中将其释放。通常,编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,另外一块用于动态存储。
//文件file01.cpp
float * p_fees = new float[20];
//文件file02.cpp
extern float * p_fees;
虽然存储方案概念不适用于动态内存,但适用于用来跟踪动态内存的自动和静态指针变量。例如上述程序中由 new
分配的 80 个字节(假设 float
为 4 个字节)的内存将一直保留在内存中,直到使用 delete
运算符将其释放。但指针 p_fees
的存储持续性与其声明方式有关,若 p_fees
是自动变量,则当包含该申明的语句块执行完毕时,指针 p_fees
将消失,如果希望另一个函数能够使用这 80 个字节中的内容,则必须将其地址传递出去。若将 p_fees
声明为外部变量,则文件中位于该声明后面的所有函数都可以使用它,通过在另一个文件中使用它的引用声明,便可在其中使用该指针。
在程序结束时,由 new
分配的内存通常都将被系统释放,但在不那么健壮的操作系统中,在某些情况下,请求大型内存块将导致该代码块在程序结束不会被自动释放,最佳习惯是:使用 delete
来释放 new
分配的内存。
9.3.2 常规 new 运算符的使用
使用常规 new
运算符初始化动态分配的内存时,有以下几种方式:
//C++98风格,小括号初始化
int *pint = new int(6);
//C++11风格,大括号初始化
int *pint = new int{6};
//C++11大括号初始化可用于结构和数组
struct points {
double x;
double y;
double z;
};
points * ptrP = new points{1.1, 2.2, 3.3};
int * arr = new int[4]{2, 4, 6, 7};
常规 new
负责在堆(heap)中找到一个足以能够满足要求的内存块,当 new
找不到请求的内存量时,最初 C++ 会让 new
返回一个空指针,但现在将会抛出一个异常 std::bad_alloc
,这将在后续章节介绍。当使用 new
运算符时,通常会调用位于全局名称空间中的分配函数(alloction function),当使用 delete
运算符时,会调用对应的释放函数(deallocation function)。
//分配函数原型
void * operator new(std::size_t);
void * operator new[](std::size_t);
//释放函数原型
void operator delete(void *);
void operator delete[](void *);
其中 std::size_t
是一个typedef
,对应于合适的整型,这里只做简单的过程说明,实际上使用运算符 new
的语句也可包含给内存设定的初始值,会复杂一些。C++ 将这些函数称为可替换的(replaceable),可根据需要对其进行定制。例如,可定义作用域为类的替换函数,对其进行定制,以满足该类的内存分配需求。
int *pint = new int; //被转换为 int *pint = new(sizeof(int));
int * arr = new int[4];//被转换为 int * arr = new(4 * sizeof(int));
delete pint; //被转换为 delete(pint);
9.3.3 定位 new 运算符的使用
new
运算符还有另一种变体,被称为定位(placement)new
运算符,它能够让程序员指定要使用的位置,可使用这种特性来设置其内存管理规程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象。如下程序是一个使用定位 new
运算符的例子,有以下几点需注意:
- 使用定位
new
特性必须包含头文件new
,且在使用时需人为提供可用的内存地址。 - 定位
new
既可以用来创建数组,也可以用来创建结构等变量。 - 定位
new
运算符使用传递给它的地址,它不跟踪哪些内存单元已被使用,也不查找未使用的内存块,这将一些内存管理的负担交给了程序员,当在同一块大型内存区域内创建不同变量时,可能需要人为计算内存的偏移量大小,防止出现变量内存区域重叠的情况。 -
delete
只能用来释放常规new
分配出来的堆内存,下面例子中的buffer1
与buffer2
都属于静态内存,不能用delete
释放,若buffer1
或buffer2
是通过常规new
运算符分配出来的,则可以且必须用delete
进行释放。
#include <iostream>
#include <new>
struct person
{
char name[20];
int age;
};
char buffer1[50];
char buffer2[500];
int main()
{
using namespace std;
//常规new运算符,数据存储在堆上
person *p1 = new person;
int *p2 = new int[20];
//定位new运算符,数据存储在指定位置,这里为静态区
person *pp1 = new (buffer1) person;
int *pp2 = new (buffer2) int[20];
//显示地址(32位系统)
cout << (void *) buffer1 << endl; //结果为0x00AEC2D0
cout << (void *) buffer2 << endl; //结果为0x00AEC308
cout << p1 << endl; //结果为0x00EFF640
cout << p2 << endl; //结果为0x00EF6470
cout << pp1 << endl; //结果为0x00AEC2D0
cout << pp2 << endl; //结果为0x00AEC308
//释放动态堆内存
delete p1;
delete[] p2;
}
上面程序中使用 (void *)
对 char *
进行强制转换,以使得 buffer1
与 buffer2
的地址能够正常输出,否者它们将输出字符串。定位 new
运算符的原理也与此类似,它只是返回传递给它的地址,并将其强制转换为 void *
,以便能够赋给任何指针类型,将定位 new
运算符用于类对象时,情况将更复杂,这将在第 12 章介绍。C++ 允许程序员重载定位 new
函数,它至少需要接收两个参数,且其中第一个总是 std::size_t
,指定了请求的字节数。
int * p1 = new(buffer) int; //被转换为 int * p1 = new(sizeof(int),buffer);
int *arr = new(buffer) int[4];//被转换为 int *arr = new(4*sizeof(int),buffer)
9.4 名称空间
9.4.1 传统的 C++ 名称空间
且听下回分解
9.4.2 新增的 C++ 名称空间
且听下回分解
9.4.3 using 声明和 using 编译指令
且听下回分解
9.4.4 嵌套的名称空间
且听下回分解
9.4.5 未命名的名称空间
且听下回分解
9.4.6 名称空间的使用方法
且听下回分解