C++ Primer Plus “阅读”笔记(四)

这篇笔记这么慢主要是因为中间隔了一堆寒假作业。下一篇得开学之后写了,不知道会咕到什么时候233。

本篇内容包括:

  • 单独编译
  • 存储持续性、作用域和链接性
  • 定位(placementnew 运算符
  • 名称空间

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

9.1 单独编译

  • 一种非常有用的组织程序的策略:

    • 头文件:包含结构声明和使用这些结构的函数的原型。
    • 源代码文件:引用头文件,包含与结构有关的函数的代码。
    • 源代码文件:引用头文件,包含调用与结构相关的函数的代码。
  • 下面列出了头文件中常包含的内容

    • 函数原型。
    • 使用 #defineconst 定义的符号常量。
    • 结构声明。
    • 类声明。
    • 模板声明。
    • 内联函数。
  • 不要使用 #include 来包含源代码文件,这样做将导致多重声明

  • 防止头文件被多次包含:

    1
    2
    3
    4
    #ifndef Header
    /* details omitted */
    #define Header
    #endif
  • 虽然我们讨论的是根据文件进行单独编译,但为保持通用性,C++ 标准使用了术语翻译单元(translation unit),而不是文件;文件并不是计算机组织信息时的唯一方式

9.2 存储持续性、作用域和链接性

  • C++ 使用四种不同的方案来存储数据:

    • 自动存储持续性
    • 静态存储持续性
    • 线程存储持续性(C++11 引入)
    • 动态存储持续性(有时被称为自由存储(free store)或堆(heap))

9.2.1 作用域和链接

  • 链接性(linkage)描述了名称如何在不同单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量的名称没有链接性,因为它们不能共享
  • 注意,一个编译单元包括源文件内代码和它包含的库文件

9.2.2 自动存储持续性

  • 自动变量储存在栈中
  • 在 C++11 中,关键字 register 的作用只是显式地指出变量是自动的

9.2.3 静态持续变量

  • 由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的装置(如栈)来管理它们。编译器将分配固定的内存块来存储所有的静态变量。所有静态持续变量在整个程序执行期间都存在
  • 所有静态持续变量都是零初始化的(zero-initialized
  • C++ 也为静态存储持续性变量提供了3种链接性:
    • 外部链接性(可在其他文件中访问)
    • 内部链接性(只能在当前文件中访问)
    • 无链接性(只能在当前函数或代码块中访问)
  • 要想创建链接性为外部的静态持续变量,必须在代码块的外面声明它;要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用 static 限定符;要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用 static 限定符
    1
    2
    3
    4
    5
    6
    int global = 1000;         // static duration, external linkage
    static int one_file = 50; // static duration, internal linkage
    void foo() {
    static int ret = 0; // static duration, no linkage
    /* details omitted */
    }
  • 由上可知:用于局部声明,以指出变量是无链接性的静态变量时,static 表示的是存储持续性;而用于代码块外的声明时,static 表示内部链接性,而变量已经是静态持续性了。有人称之为关键字重载
  • 零初始化意味着将变量设置为零。对于标量类型,零将被强制转换为合适的类型。例如,在 C++ 代码中,空指针用0表示,但内部可能采用非零表示,因此指针变量将被初始化相应的内部表示。结构成员被零初始化,且填充位都被设置为零
  • 零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译器处理文件(翻译单元)时初始化变量。动态初始化意味着变量将在编译后初始化
  • C++11 新增了关键字 constexpr,这增加了创建常量表达式的方式。但本书不会更详细地介绍 C++11 新增的这项新功能(???

9.2.4 静态持续性、外部链接性

  • “单定义规则”(One Definition Rule(ODR)
    • 引用声明(referencing declaration
      • 引用声明使用关键字 extern,且不进行初始化;否则,声明为定义,导致分配存储空间
  • 作用域解析运算符(::
    • 放在变量名前面时,该运算符表示使用变量的全局版本
    • 从清晰和避免错误的角度说,相对于依赖于作用域规则,使用::是更好的选择,也更安全

9.2.5 静态持续性、内部链接性

  • 静态外部变量优先于常规外部变量:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // file1
    int foo = 20; // external declaration
    ...
    -----------------------------------------
    // file2
    static int foo = 5; // known to file2 only
    void bar() {
    std::cout << foo; // uses foo defined in file 2
    }

9.2.6 静态存储持续性、无链接性

  • 无链接性的局部变量
    • static 限定符修饰的且在代码块中定义的变量
    • 储存持续性为静态

9.2.7 说明符和限定符

  • 储存说明符:
    • auto(在 C++11 中不再是说明符)
      • C++11 之前:指出变量为自动变量
      • C++11 及之后:自动类型推断
    • register
      • C++11 之前:指示寄存器存储
      • C++11 及之后:显式地指出变量是自动的
    • static
      • 作用域为整个文件的声明:表示内部链接性
      • 局部声明:表示局部变量的存储持续性为静态的
    • extern
      • 引用声明
    • thread_local(C++11 新增的)
      • 变量的持续性与其所属线程的持续性相同
      • thread_local 变量之于线程,犹如常规静态变量之于整个程序
    • mutable
      • 即使结构(或类)变量为const,其某个成员也可以被修改
        1
        2
        3
        4
        5
        6
        7
        struct data {
        char name[30];
        mutable int accesses;
        };
        const data veep = {"Claybourne Clodde", 0};
        std::cout << veep.name;
        veep.accesses++;
  • cv-限定符
    • volatile
      • 即使程序代码没有对内存单元进行修改,其值也可能发生变化
        • e.g. 可以将一个指针指向某个硬件位置,其中包含了来自串行端口的时间或信息。在这种情况下,硬件(而不是程序)可能修改其中的内容
      • 这个声明是为了防止编译器进行一些程序员不希望出现的优化
    • const
      • 在默认情况下全局变量的链接性为外部的,但 const 全局变量的链接性为内部的。也就是说,全局 const 定义就像使用了 static 说明符一样
        • 因此,const 全局变量可以存在于头文件之中(多多考虑头文件被多个文件包含情况
      • 如果出于某种原因,程序员希望某个常量的链接性为外部的,则可以使用 extern 关键字来覆盖默认的内部链接性。此时,鉴于单个 const 在多个文件之间共享,因此只有一个文件可对其进行初始化
        1
        extern const int states = 50;

9.2.8 函数和链接性

  • 和变量一样,函数也有链接性
  • C++ 不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都自动为静态的
  • 在默认情况下,函数的链接性为外部的,即可以在文件间共享
  • 可以在函数原型中使用关键字 extern 来指出函数是在另一个文件中定义的
  • 可以使用关键字 static 将函数的链接性设置为内部的,使之只能在一个文件中使用。(必须同时在原型和函数定义中使用该关键字:)
  • 单定义规则也适用于非内联函数,因此对于每个非内联函数,程序只能包含一个定义。对链接性为外部的函数来说,这意味着在多文件程序中,只能有一个文件包含该函数的定义,但使用该函数的每个文件都应包含其函数原型
  • 内联函数不受这项规则的约束,这允许程序员能够将内联函数的定义放在头文件中。这样,包含了头文件的每个文件都有内联函数的定义。然而,C++ 要求同一个函数的所有内联定义都必须相同。

9.2.9 语言链接性

  • 同一个名称可能对应多个函数,因此,编译器执行名称矫正或名称修饰(参见第8章),为重载函数生成不同的符号名称。这被称为 C++语言链接(C++ language linkage)。
  • 如果要在 C++ 程序中使用 C 库中预编译的函数,可以用函数原型来指出要使用的约定:
    1
    2
    3
    extern "C" void foo(int);    // use C protocol for name look-up
    extern void foo(int); // use C++ protocol for name look-up
    extern "C++" void foo(int); // use C++ protocol for name look-up

9.2.10 存储方案和动态分配

  • 编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,另外一块用于动态存储。
  • 初始化动态分配的变量:
    • 内置的标量类型或者有合适构造函数的类,可用括号扩起
      1
      double *pi = new double(3.14);
    • 常规结构和数组,可用列表初始化(C++11)
      1
      2
      struct foo {int a, b;};
      foo *bar = new foo{1, 2};
      第一种也可使用列表初始化(C++11)
      1
      double *pi = new double{3.14};
  • new 失败时:在最初的10年中,C++ 在这种情况下让 new 返回空指针,但现在将引发异常 std::bad_alloc。第15章通过一些简单的示例演示了这两种方法的工作原理
  • new:运算符、函数和替换函数
    • newdelete 会分别调用如下函数(它们位于全局名称空间中)
      • 分配函数(alloction function
        1
        2
        void * operator new(std::size_t);   // used by new
        void * operator new[](std::size_t); // used by new[]
      • 释放函数(deallocation function
        1
        2
        void operator delete(void *);
        void operator delete[](void *);
      • 陈国师上课指出:delete 会调用析构函数,但 free 不会
    • C++ 将这些函数称为可替换的(replaceable),即可根据需要对其进行定制
  • 定位 new 运算符
    • new 负责在堆(heap)中找到一个足以能够满足要求的内存块。new 运算符还有另一种变体,被称为定位(placementnew 运算符,它让您能够指定要使用的位置。定位 new 运算符使用传递给它的地址,它不跟踪哪些内存单元已被使用,也不查找未使用的内存块。
    • e.g.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      #include <new>

      struct foo {
      char alex[20];
      int bob;
      };

      char buffer[50];

      int main() {
      foo *p1, *p2;
      p1 = new (buffer) foo;
      p2 = new (buffer + sizeof(foo)) foo;
      /* details omitted */
      return 0;
      }
    • buffer 指定的内存是静态内存,而 delete 只能用于这样的指针:指向常规 new 运算符分配的堆内存。也就是说,下面的语句将引发运行阶段错误
      1
      delete p1;
    • 定位 new 运算符的工作原理:基本上,它只是返回传递给它的地址,并将其强制转换为 void *,以便能够赋给任何指针类型。将定位 new 运算符用于类对象时,情况将更复杂,这将在第12章介绍。
      1
      2
      int *p3 = new (buffer) int [40];
      // invokes new(40 * sizeof(int), buffer)
    • C++允许程序员重载定位 new 函数。

9.3 名称空间

9.3.1 传统的 C++ 名称空间

  • 声明区域(declaration region):可以在其中进行声明的区域
  • 潜在作用域(potential scope):从声明点开始,到其声明区域的结尾
  • 作用域(scope):可以直接使用变量的区域(没被别的同名变量隐藏的潜在作用域)

9.3.2 新的名称空间特性

  • 名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)
  • 除了用户定义的名称空间外,还存在另一个名称空间——全局名称空间
    global namespace)。它对应于文件级声明区域,因此前面所说的全局变量现在
    被描述为位于全局名称空间中
  • 名称空间是开放的(open),即可以把名称加入到已有的名称空间中。例如,下面这条语句将名称 goose 添加到 Jill 中已有的名称列表中:
    1
    2
    3
    namespace Jill {
    char *goose(const char *);
    }
  • 未被装饰的名称称为未限定的名称(unqualified name),包含名称空间的名称称为限定的名称(qualified name)。
  • using 声明和 using 编译指令
    • using 声明将特定的名称添加到它所属的声明区域中。例如 main() 中的 using 声明 Jill::goosegoose 添加到 main() 定义的声明区域中
      1
      2
      3
      4
      5
      int main() {
      using Jill::goose;
      /* details omitted */
      return 0;
      }
      由于 using 声明将名称添加到局部声明区域中,因此这个示例避免了将另一个局部变量也命名为 goose
    • using 编译指令使所有的名称都可用。如:
      1
      using namespace std;
    • 不同之处:
      • 如果某个名称已经在函数中声明了,则不能用 using 声明导入相同的名称。
      • 如果使用 using 编译指令导入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名,就像隐藏同名的全局变量一样。
  • 名称空间的其他特性
    • 如果函数被重载,则一个 using 声明将导入所有的版本。
    • 可以将名称空间声明进行嵌套
    • 可以在名称空间中使用 using 编译指令和 using 声明
    • 可以给名称空间创建别名
      1
      2
      namespace foo { /* details omitted */ }
      namespace bar = foo;
    • 可以通过省略名称空间的名称来创建未命名的名称空间。这提供了链接性为内部的静态变量的替代品。例如下面两段代码等价
      1
      2
      static int counts;  // static storage, internal linkage
      int main() { /* details omitted */ }
      1
      2
      3
      4
      namespace {
      int counts; // static storage, internal linkage
      }
      int main() { /* details omitted */ }

9.3.4 名称空间及其前途

随着程序员逐渐熟悉名称空间,将出现统一的编程理念。下面是当前的一些指导原则。

  • 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量。
  • 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
  • 如果开发了一个函数库或类库,将其放在一个名称空间中。事实上,C++ 当前提倡将标准函数放在名称空间 std 中,这种做法扩展到了来自 C 语言中的函数。
  • 仅将编译指令 using 作为一种将旧代码转换为使用名称空间的权宜之计。
  • 不要在头文件中使用 using 编译指令。首先,这样做掩盖了要让哪些名称可用;另外,包含文件的顺序可能影响程序的行为。如果非要使用编译指令 using,应将其放在所有预处理器编译指令 #include 之后。
  • 导入名称时,首选使用作用域解析运算符或 using 声明的方法。
  • 对于 using 声明,首选将其作用域设置为局部而不是全局。

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×