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

说明

看《C++ Primer Plus》时整理的学习笔记,部分内容完全摘抄自《C++ Primer Plus》(第6版)中文版,Stephen Prata 著,张海龙 袁国忠译,人民邮电出版社。只做学习记录用途。

本章介绍 C++ 的内存模型和名称空间,包括数据的存储持续性、作用域和链接性,以及定位 new 运算符。

9.1 单独编译

C++ 鼓励程序员将组件函数放在独立的文件中,可以单独编译这些文件,然后将它们链接成可执行的程序。(通常,C++ 编译器既编译程序,也管理链接器。)如果只修改了一个文件,则可以只重新编译该文件,然后将它与其他文件的编译版本链接,大多数集成开发环境(如 Microsoft Visual C++Apple Xcode)都提供了这一功能,减少了人为管理的工作量。

9.1.1 程序组织策略

以下是一种非常有效且常用的程序组织策略,它将整个程序分为三个部分:

  • 头文件:包含结构声明和使用这些结构的函数的原型
  • 源代码文件:包含定义与结构有关的函数的代码。
  • 源代码文件:包含调用与结构有关的函数的代码。

在编译时,C++ 预处理器会将源代码文件中的 #include 指令替换成头文件的内容。源代码文件和它所包含的所有头文件被编译器看成一个包含以上所有信息的单独文件,该文件被称为翻译单元(translation unit)。描述一个具有文件作用域的变量时,它的实际可见范围是整个翻译单元。如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的头文件。下图简要地说明了在 UNIX 系统中,将含 1 个头文件 coordin.h 与 2 个源代码文件 file1.cppfile2.cpp 的程序编译成一个 out 可执行程序的过程。

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

由于不同 C++ 编译器对函数的名称修饰方式不同,因此由不同编译器创建的二进制模块(对象代码文件,如上图中的 file1.ofile2.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

在头文件中,可以包含以下内容:

  • 使用 #defineconst 定义的符号常量
  • 结构声明,它们并不创建变量,只是告诉编译器当需要创建它们时应该如何创建。
  • 类声明,同结构声明一样,它们并不创建类,只是告诉编译器当需要创建它们时应该如何创建。
  • 模板定义,它们不是将被编译的代码,只是被用来指示编译器如何生成与源代码中的函数调用相匹配的函数定义。
  • 常规函数原型
  • 内联函数定义

不要将常规函数定义(非函数模板、非内联函数)或常规变量声明(非 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 的(后进先出),即最后加入到栈中的变量首先被弹出。这种设计简化了参数传递,函数调用时将其参数的值放在栈顶,然后重新设置栈顶指针,被调用的函数根据其形参描述来确定每个参数的地址。

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

函数 fib() 被调用时,传递一个 2 字节的 int 和一个 4 字节的 long,这些值被加入到栈中。当 fib() 开始执行时,它将名称 realtell 同这两个值关联起来。当 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,这增加了创建常量表达式的方式。最后,在程序执行时将进行动态初始化。上述程序中,xyzupi 首先被零初始化,然后编译器计算常量表达式的值对 yzu 进行常量表达式初始化,但要初始化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 可与 staticextern 结合使用,其他五个说明符不能同时用于同一个声明

  • 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 的警告信息,如下图所示。

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

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())分配的内存,被称为动态内存。动态内存由运算符 newdelete 控制,而不是由作用域和链接性规则控制。动态内存的分配和释放顺序取决于 newdelete 在何时以何种方式被使用,因此,可以在一个函数中分配动态内存,而在另一个函数中将其释放。通常,编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,另外一块用于动态存储

//文件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 分配出来的堆内存,下面例子中的 buffer1buffer2 都属于静态内存,不能用 delete 释放,若 buffer1buffer2 是通过常规 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 * 进行强制转换,以使得 buffer1buffer2 的地址能够正常输出,否者它们将输出字符串。定位 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 名称空间

在 C++ 中,名称可以是变量函数结构枚举以及类和结构的成员,当随着项目的增大,名称相互冲突的可能性也将增加,使用多个厂商的类库时,可能导致名称冲突,C++标准提供了名称空间工具,以便更好地控制名称的作用域。

9.4.1 传统的 C++ 名称空间

声明区域(declaration region):声明区域是可以在其中进行声明的区域。对于在函数外面声明的全局变量,其声明区域为其声明所在的文件;对于在函数中声明的变量,其声明区域为其声明所在的代码块。

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

潜在作用域(potential scope):变量的潜在作用域从声明点开始,到其声明区域的结尾。潜在作用域比声明区域小。

作用域(scope):变量对程序而言的可见范围被称为作用域。变量并非在其潜在作用域内的任何位置都是可见的,它可能被另一个在嵌套声明区域中声明的同名变量隐藏,例如在函数中声明的局部变量将隐藏在同一个文件中声明的同名全局变量。

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

C++ 关于全局变量和局部变量的规则定义了一种名称空间层次,每个声明区域都可以声明名称,这些名称独立于在其他声明区域中声明的名称,因此在一个函数中声明的局部变量不会与在另一个函数中声明的局部变量发生冲突,这就是传统的 C++ 名称空间。

9.4.2 新增的 C++ 名称空间

C++ 可使用关键字 namespace 创建自定义名称的名称空间,任何名称空间中的名称都不会与其他名称空间的相同名称发生冲突。

  • 名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中,因此,在默认情况下,名称空间中名称的链接性为外部的(除非它是 const 常量)。
  • 名称空间是开放的,即可以把名称加入到已有的名称空间中,通常在头文件中写自定义名称空间中的函数原型,然后在源代码文件中将函数定义加入到自定义名称空间。
  • 除了用户定义的名称空间外,还存在另一个名称空间——全局名称空间(global namespace)。它对应于文件级声明区域,因此前述外部链接性的静态变量与内部链接性的静态变量都位于全局名称空间中。
//自定义名称空间
namespace Jack {
    double pail;
    void func();
}
namespace Jill {
    double pail;
    void func();
}

//访问指定名称
Jack::pail = 12.34;
Jill::pail = 45.36;
Jack::func();
Jill::func();

访问给定名称空间中名称的最简单的方式是使用作用域解析运算符 ::,使用名称空间自身名称来进行限定。未被装饰的名称(如 pail)被称为未限定的名称(unqualified name);包含名称空间的名称(如 Jack::pail)被称为限定的名称

9.4.3 using 声明和 using 编译指令

C++ 提供了两种机制来简化对名称空间中名称的使用,使用其中任一种机制后,便可直接通过未限定的名称对指定名称进行访问,而不需要使用作用域解析运算符。using 声明使特定的标识符可用,using 编译指令使整个名称空间可用。

using 声明:它将特定的名称添加到它所属的声明区域中,使这个特定的名称变得可用。如下例子中使用 using 声明将 Jack::pail 导入全局名称空间,将 Jill::pail 导入局部声明区域。完成这些声明后,便可以使用名称 pail 代替它们的限定形式,和其他局部变量一样,下面 fund() 函数中的 pail(限定名称为 Jill::pail)将覆盖全局的同名变量 pail(限定名称为 Jack::pail)。当包含 using 声明的最小声明区域中已经声明了和名称空间中相同的名称时,若仍使用 using 声明导入该名称空间的同名名称,则这两个名称将会发生冲突,编译器会报错。当使用 using 声明导入函数时,由于只有名称没有函数特征标以及返回类型,若函数被重载,则该函数的所有版本都将被导入

//自定义名称空间
namespace Jack {
    double pail;
    void func();
}
namespace Jill {
    double pail;
    void func();
}

//将名称导入全局名称空间
using Jack::pail;

//将名称导入局部名称空间
void fund()
{
    using Jill::pail;
    ...
    cout << pail;     //使用的是Jill::pail
}

using 编译指令:它由名称空间名和它前面的关键字 using namespace 组成,它使名称空间中的所有名称都可用,而不需要使用作用域解析运算符。在全局声明区域中使用 using 编译指令,将使该名称空间的名称全局可用;在函数或代码块中使用 using 编译指令,将使其中的名称在该函数或代码块中可用。与 using 声明不同的是,using 编译指令会进行名称解析,在一些时候名称空间的变量会被同区域声明的同名变量隐藏,不会出现名称冲突的报错。但在另一些情况下,使用 using 编译指令仍会出现名称冲突的报错。这在本人的另一篇博客 C++ using 编译指令与名称冲突 中有详细讨论,具体总结如下:

  • 若仅存在同名全局变量,不存在同名局部变量,使用 using 编译指令后,在作用域的重合区域使用变量时一定会引发名称冲突。
  • 若仅存在同名局部变量,不存在同名全局变量,使用 using 编译指令将会进行名称解析,不会引发名称冲突,但在代码块中,同名局部变量将隐藏名称空间中的变量。
  • 若不同名称空间中存在同名变量,不存在同名全局变量以及同名局部变量,使用 using 编译指令后,在作用域的重合区域使用变量时一定会引发名称冲突。
  • 若名称空间中的变量、同名全局变量、同名局部局部变量三者同时存在,using 编译指令的使用位置不会影响名称解析的结果,且不会引发名称冲突,这正是 using 编译指令进行名称解析的效果。

一般而言,使用 using 声明比使用 using 编译指令更安全,因为它只导入指定的名称,若出现名称冲突就会报错。using 编译指令将导入所有名称,一方面由于名称解析的特性,导致编译器在某些情况下不会报名称冲突的错误,这增加了程序员误用变量的可能性;另一方面,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。

9.4.4 嵌套的名称空间

可以将名称空间进行嵌套,有以下两种嵌套方式:

//嵌套方式一:名称空间声明里面含有名称空间声明
namespace elements {
    namespace fire {
        int flame;
    }
    float water;
}

//嵌套方式二:名称空间声明里面含有using编译指令或using声明
namespace myth {
    using namespace elements;
    using Jill::pail;
    using std::cout;
    using std::cin;
}

使用上面的嵌套声明后,Jill::pail 已经被引入到名称空间 myth 中,在 myth 中其名称可以为未限定的 pail,若要访问 Jill::pail,有以下几种方式:

//访问方式一
std::cout << Jill::pail;

//访问方式二
std::cout << myth::pail;

//访问方式三:若无冲突的局部变量
using namespace myth;
cout << pail;

using 编译指令是可传递的(如果 A op BB op C 可以得到 A op C,则说操作 op 是可传递的,例如 > 运算符是可传递的,因为 A > BB > C 可以得到 A > C),using 编译指令的可传递性表现如下,using namespace myth 将同时导入名称空间 myth 与名称空间 elements 中的所有名称。

//导入myth名称空间
using namespace myth;

//上述编译指令的等价形式
using namespace myth;
using namespace elements;

可以使用 namespace 创建名称空间的别名,以简化嵌套名称空间的使用,以下两种方式等效,其中第二种方式将使 EF 成为 elements::fire 的别名。

//方式一:常规方式访问fire名称空间中的名称
std::cout << elements::fire::flame;
std::cout << myth::fire::flame;

//方式二:使用别名进行简化
namespace EF = elements::fire;
std::cout << EF::flame;

9.4.5 未命名的名称空间

可以通过省略名称空间的名称来创建未命名的名称空间,这就像后面跟着 using 编译指令一样,在该名称空间中声明的名称的潜在作用域为:从声明点到该声明区域末尾。由于这种名称空间没有名称,因此不能显式地使用 using 编译指令或 using 声明来使它在其他位置都可用,即不能在未命名名称空间所属文件之外的其他文件中,使用该名称空间中的名称。未命名名称空间中的变量可以作为内部链接性静态变量(static 全局变量)的替代品,以下两种形式等价。

//形式一:static全局变量
static int counts = 23;
int main()
{
    std::cout << counts; //结果为23
    
    return 0;
}

//形式二:未命名名称空间
namespace {
    int counts = 23;
}
int main()
{
    std::cout << counts; //结果为23
    
    return 0;
}

9.4.6 使用名称空间的指导原则

以下是一些使用名称空间的指导原则:

  • 通常在头文件中写自定义名称空间中的函数原型、结构定义、常量,然后在源代码文件中将函数定义加入到自定义名称空间。
  • 在导入名称空间中的名称时,尽量使用作用域解析运算符 ::using 声明的方法,少用 using 编译指令。
  • 使用 using 声明时,首选将其作用域设置为局部而不是全局。
  • 使用 using 编译指令时,应将其放在源文件中,且放在所有预处理指令 #include 之后,而不应将其放在头文件中,否则头文件的包含顺序可能会影响 using 编译指令名称解析的行为。
  • 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量或 static 全局变量。
  • 如果开发了一个函数库或类库,尽量将其放在一个名称空间中,以减小名称冲突的可能性。
posted @ 2022-11-06 22:30  木三百川  阅读(310)  评论(0编辑  收藏  举报