• 《c++ Primer Plus 第6版》读书笔记(4)


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

    本章内容包括:

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

    9.1 单独编译

    C++也允许将组件函数放在独立的文件中。可以打拿督编译这些文件,然后将它们连接成可执行的程序。

    C++编译器既编译程序,也负责链接器。

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

    请不要将函数定义或变量声明放到头文件中。如果头文件中包含一个函数定义,有两个文件进行了include,那么一个程序中将包含同一个函数的两个定义(除非函数是inline)。

    头文件中常包含的内容包括:

    • 函数原型
    • 使用#define和const定义的符号常量
    • 结构声明
    • 类声明
    • 模板声明
    • 内联函数

    被声明为const的数据和内联函数有特殊的链接属性,因此可以放在头文件中,不会引起问题。

    另外,在包含头文件时,如果文件名被包含在尖括号,比如,那么编译器将会在存储标准头文件的文件系统中查找;如果包含在双引号中,编译器会首先查找当前的工作目录或源代码目录,如果未找到,再去标准位置查找。因此包含自己的头文件的时候,应该使用引号而不是尖括号。

    #include指令用来管理头文件,不应该使用include包含源代码文件。

    在同一个文件中只能将同一头文件包含一次,但是很可能在不知情的状况下将头文件包含多次。使用预处理编译器指令#ifndef可以解决问题。

    1. #ifndef COORDIN_H_
    2. #define COORDIN_H_
    3. // place include file contents here
    4. #endif

    编译器首次遇到该文件时,名称COORDIN_H_(一般用文件名加下划线)没有定义,此时编译器将会查看#ifndef和#endif之间的内容,并读取到定义#define一行。如果在同一文件中另一位置发现已经定义了COORDIN_H_,那么编译器就会跳到#endif后面的一行上。

    使用这种方法并不能防止编译器将文件包含两次,只是会忽略掉第一次包含之外的所有内容。

    C++每个编译器都会用自己的方式进行名称修饰,不同编译器创建的二进制模块可能会无法正确链接。所以在链接编译模块时,请确保所有对象文件或库都是由同一个编译器生成的。

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

    C++使用三种(C++11为四种)不同的方式来存储数据,区别在于数据保留在内存中的时间。

    • 自动存储连续性:在函数定义中声明的变量(包括函数参数)的存储连续性是自动的。函数执行时创建,完成时释放。C++有两种存储持续性为自动的变量。
    • 静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量,存储持续性都为静态,在程序整个运行过程中都会存在。C++有三种存储持续性为静态的变量。
    • 线性存储持续性(C++11):多核处理使CPU同时处理多个执行任务,如果变量是使用关键字thread_local声明的,那么其生命周期与所属的线程一样长。
    • 动态存储持续性:使用new运算符分配内存将会一直存在,直到使用delete运算符将其释放或者程序结束为止。这种内存的存储持续性为动态,有时候会被称为自由存储或者堆存储。

    9.2.1 作用域和链接

    作用域描述了名称在文件(翻译单元)的多大范围内可见。比如函数中定义的变量在该函数中可见。

    链接性描述了名称如何在不同单元间共享。链接性为外部的名称可在文件之间共享,链接性为内部的名称只能有一个文件中的函数共享。自动变量的名称没有链接性,因为不能共享。

    C++变量作用域有很多种。

    作用域为局部的变量只在定义它的代码块(花括号括起来)中可用;作用域为全局(文件作用域)的变量在定义位置到文件结尾之间都可用;在名称空间中声明的变量的作用域为整个名称空间。

    9.2.2 自动存储持续性

    在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有磁链性。所以在不同函数中声明的相同名称的变量是独立的。

    如果在代码块中声明了变量,在该代码块中又定义了新代码块,声明了相同的变量,那么新的定义会隐藏旧的定义,新定义可见,就定义暂时不可见,程序离开里代码块后,旧定义重新可见。

    在旧版的C中,旧版本中的关键字auto,是于显示指出变量为自动存储的,但是用的人很少,所以在新版本C中用新的含义代替了老的使用方法。

    自动变量和栈

    C编译器运行时会对自动变量进行管理,留出一段内存,将其视为栈,用来管理变量的增减。

    寄存器变量

    关键字register最初是由C语言引入的,他建议编译器使用CPU寄存器来存储自动变量。

            register int count_fast;

    但是现在的register已经没有了作用,和以前的auto关键字作用是相同的。

    9.2.3 静态持续变量

    C++为静态存储持续性变量提供了3种链接性:

    • 外部链接性:可在其他文件中访问
    • 内部链接性:只能在当前文件中访问
    • 无链接性:只能在当前函数或代码中访问

    静态变量名称的意义在于,静态变量在整个程序运行期间数目是不变的,因此程序不需要使用特殊的装置(比如栈)来进行管理。

    编译器会将没有显式初始化的静态变量设置为0。

    进行三种静态变量举例:

    1. ...
    2. int global = 100; // static, external linkage
    3. static int one_fle = 50; // static, internal linkage
    4. int main()
    5. {
    6. ...
    7. }
    8. void funct1(int n)
    9. {
    10. static int count = 0; // static , no linkage
    11. int llama = 0;
    12. ...
    13. }
    14. void funct2(int q)
    15. {
    16. ...
    17. }

    三种静态变量在整个程序执行期间都存在。

    • count作用域为局部,没有磁链性,所以只能在funct1中使用。但是和llama不一样的是,count在程序最开始运行时就已经存在在内存了
    • global作用域为整个文件,链接性为外部,在程序的其他文件中可以使用
    • one_file作用域为整个文件,磁链性为内部,只能在当前文件中使用

    5种变量存储方式

    存储描述持续性作用域磁链性如何声明
    自动自动代码块在代码块中
    寄存器自动代码块在代码块中,使用register
    静态,无磁链性静态代码块在代码块中,使用关键字static
    静态,外部磁链性静态文件外部不在任何函数内
    静态,内部磁链性静态文件内部不在任何函数内,使用关键字static

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

    C++存在“单定义规则”,即变量只能有一次定义。

    所以C++提供了两种变量声明:一种是定义声明,会给变量分配存储空间;一种是引用声明,不会分配空间,只是引用已有变量。

    引用声明使用extern进行定义:

    1. double up; // 定义声明
    2. extern int blem; // 引用声明
    3. extern char gr = 'z'; // 定义声明,因为包含了初始化
    4. // 如果要在其他文件中使用另一个文件定义的变量,使用extern
    5. // file01.cpp
    6. int process_status = 0;
    7. int main()
    8. {
    9. ...
    10. }
    11. // file02.cpp
    12. extern int process_status;
    13. int work()
    14. {
    15. std::cout << ::process_status << endl;
    16. ...
    17. int process_status = 1;
    18. std::cout << process_status << endl;
    19. }

    说明,定义与全局变量同名的局部变量后,局部变量将会隐藏全局变量。

    C++提供了作用域解析运算符(::)。放在变量名前面时,表示该变量为全局版本变量。

    外部存储尤其适用于表示常量数据。

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

    将static作用于作用域为整个当前文件的变量时,磁链性为内部。

    如果在一个文件中创建一个全局静态变量,另一个文件中想要创建一个同名变量,那么仅仅省略extern是不够的,这样会违反C++的单定义规则。

    正确的做法是声明一个static变量,这样静态变量会隐藏常规外部变量。这样做,声明的变量磁链性为内部,不会违法单定义规则。

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

    使用static作用于代码块内的变量,就可以创建无磁链性的局部变量。

    变量会在函数运行之前存在,在函数运行结束之后也不会被回收。因此在两次函数调用之间,静态局部变量的值将会保持不变。

    另外如果定义时进行初始化,只会在程序启动时进行一次初始化,之后再调用函数时,不会再次进行初始化。

    9.2.7 说明符和限定符

    存储说明符:

    • auto(C++11中不再是说明符)
    • register(C++11显式指出是自动类型变量)
    • static
    • extern
    • thread_local(C++11新增)
    • mutable

    cv-限定符:

    • const
    • volatile

    关键字volatile表示,即使程序代码没有对内存单元进行修改,值也可能会发生变化。比如硬件修改内容、两个程序共享数据等等。该关键字作用是为了改善编译器的优化能力。

    关键字mutable表示,即使结构或类变量为const,其某个成员也可以被修改。

    1. struct data
    2. {
    3. char name[30];
    4. mutable int accesses;
    5. ...
    6. };
    7. const data veep = {"aiky",0,...};
    8. strcpy{veep.name, "john"}; //not allowed
    9. veep.accesses++; // allowed

    关键字const。在默认情况下全局变量的链接性为外部的,但是const全局变量的链接性为内部的。会和static一样。

    可以把const变量放到头文件中,由不同的文件包含。这时由于内部连接性,所以每个文件都会有自己的一组常量,而不是所有文件都共享一组常量。

    如果希望某个常量的链接性为外部,那么可使用extern关键字:

    extern const int states = 50;    // definition with external linkage

    此时常量为外部链接性定义。

    9.2.8 函数和链接性

    函数和变量一样也有链接性,但是范围比较少。

    由于C++和C默认不能再函数中定义另外一个函数,所以所有函数的存储持续性都自动为静态。

    链接性上,函数默认链接性为外部,可以在文件之间共享。也可以使用extern关键字来指出函数为另一文件中定义,可选。

    也可以使用static将函数的链接性默认为内部链接。此时意味着该函数只在当前文件中可见,其他文件中可以定义同名函数。

    函数大多只能有一个定义,内联函数除外,所以内联函数可以放在头文件之中。

    9.2.9 语言链接性

    C语言中,一个名称只对应一个函数,spiff翻译成_spiff。

    但是C++中,一个名称可能对应多个函数,spiff(int)可能翻译成_spiff_i,spiff(double,double)可能翻译成_spiff_d_d。

    那么此时如果在C++程序寻找C中的函数,就找不到了。

    为了解决这种问题,可以使用函数原型来指出要使用的约定。

    1. extern "C" void spiff(int); // use C protocol for name look-up
    2. extern void spoff(int); // use C++ protocol for name look-up
    3. extern "C++" void spaff(int); // use C++ protocol for name look-up

    C和C++的链接性是C++标准指定的说明符,但实现可提供其他语言链接性的说明符。

    9.2.10 存储方案和动态分配

    C++为变量分配内存的5种方案是不适用于动态内存分配的。C++使用new,C使用malloc分配的内存为动态内存分配。

    动态内存由new和delete控制,因此可以在一个函数中分配内存,在另一个函数中释放。

    通常情况下,编译器使用三块独立的内存:一块用于静态变量(可再细分),一块用于自动变量,一块用于动态存储。

    使用new运算符

    C++98提供了内置类型的内存分配和初始化方法。

    1. int * pi = new int (6); // *pii set to 6
    2. double *pd = new double (99.9); // *pd set to 99.9

    C++11提供了常规结构或者数组的初始化。

    1. struct where { double x; double y ; double z;};
    2. where * one = new where {2.5,5.3,7.2}; // C++11
    3. int * ar = new int [4] {2,4,6,7}; // C++11
    4. int * pin = new int {6}; // C++11

    如果使用new失败,找不到请求的内存量。在之前C++会返回空指针,但现在会引发异常std::bad_alloc。

    在C++内部中,new会调用void * operator new(std::size_t); 而new []会调用void * operator new[](std::size_t);这些函数被称为分配函数。delete会调用 void operator delete(void *); 而delete[]会调用 void operator delete[] (void *);这些函数被称为释放函数。

    在代码中调用new和delete关键字时会被替换:

    1. int * pi = new int ;
    2. // 会被转化为
    3. int * pi = new(sizeof(int));
    4. int * pa = new int[40];
    5. // 会被转化为
    6. int * pa = new(40 * sizeof(int));
    7. delete pi;
    8. // 会被转化为
    9. delete (pi);

    C++将这些函数称为可替换,可以根据需求,对其进行定制和替换函数。

    通常,new会在堆中找到一个符合条件的内存块。new运算符还有另一种变体,被称为定位new运算符。

    使用定位特性,需要包含头文件。举例:

    1. #include
    2. struct staff
    3. {
    4. char dross[20];
    5. int slag;
    6. };
    7. char buffer1[50];
    8. char buffer2[500];
    9. int main()
    10. {
    11. chaff *p1, *p2;
    12. int *p3, *p4;
    13. // first ,the regular forms of new
    14. p1 = new chaff; // place structure in heap
    15. p3 = new int[20]; // place int array in heap
    16. // now , the two forms of placement new
    17. p2 = new (buffer1) chaff; //place structure in buffer1
    18. p4 = new (buffer2) int[20]; // place int array in buffer2
    19. ...
    20. }

    示例中,使用两个静态数组来为new运算符提供内存空间。从buffer1中分配空间给chaff;从buffer2中分配空间给int数组。

    但是不能够使用delete进行内存释放,因为buffer是静态内存,delete只能够用于new分配的堆内存。

    但如果buffer是使用new来创建的,就可以通过使用delete来释放了。

    定位new运算符的另一种用法是,初始化后,将信息放在特定的硬件地址位置。

    标准定位new会调用一个接收两个参数的new()函数:

    1. int * p2 = new int; // -> new(sizeof(int))
    2. int * p2 = new (buffer) int; // -> new(sizeof(int), buffer)
    3. int * p3 = new (buffer) int[40]; // -> new(40*sizeof(int), buffer)

    9.3 名称空间

    随着项目越来越大,名称相互冲突的可能性也在增加。

    使用多个厂商的类库时,可能会导致名称冲突。这种冲突被命名为名称空间问题。

    C++提供了名称空间工具,方便更好的控制作用域。

    9.3.1 传统的C++名称空间

    声明区域:是可以在其中进行声明的区域。

    潜在作用域:从声明点开始,到声明区域的结尾。潜在作用域比声明区域小。

    9.3.2 新的名称空间特性

    C++新增功能,通过定义一种新的声明区域来创建命名的名称空间。

    名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。

    1. namespace Jack {
    2. double pail;
    3. void fetch();
    4. int pal;
    5. struct Well { ... };
    6. }
    7. namespace Jil{
    8. double bucket(double n){ ... }
    9. double fetch;
    10. int pal;
    11. struct Hil {...};
    12. }

    另一种名称空间——全局名称空间。对应于文件级声明区域。

    名称空间是开放的,可以将新的名称加入到已有的名称空间中。也可以在一个文件中,使用名称空间Jil提供函数原型,然后在另一个文件中使用名称空间为其提供定义。

    需要有一种方法来访问给定名称空间的名称,最简单的方式是,通过作用域解析运算符::

    比如:

            Jack::pail = 12.34;

            Jill::Hill mole;

    未被装饰的名称被称为未限定名称;包含名称空间的名称被称为限定名称。

    using声明和编译指令

    using声明能够使特定标识符可用,using编译指令能够使整个名称空间可用。

    1. // using声明 --------------
    2. // file1
    3. namespace Jill{
    4. double bucket(double n) { ... }
    5. double fetch;
    6. struct Hill { ... };
    7. }
    8. // file2
    9. char fetch;
    10. int main()
    11. {
    12. using Jill::fetch;
    13. cin >> fetch ; //read a value into Jill::fetch
    14. cin >> ::fetch; //read a value into global fetch
    15. }
    16. // using编译指令-------------
    17. using namespace Jack; // make all the names in Jack available

    using编译指令不能同时使用,会导致二义性。

    通常情况下,使用using声明会比编译指令更加安全。

    名称空间的其他特性

    可以将名称空间进行嵌套

    1. namespace elements
    2. {
    3. namesapce fire
    4. {
    5. int flame;
    6. ...
    7. }
    8. float water;
    9. }
    10. // 此时以下命令都是有效的
    11. using namespace elements::fire;
    12. elements::fire::flame = 10;
    13. // 在命名空间内也是可以包含using编译指令和using声明的
    14. namespace myth
    15. {
    16. using Jill::fetch;
    17. using namespace elements;
    18. using std::cin;
    19. }

    using编译指令是可以传递的,如果A op B and B op C,那么A op C

    未命名的名称空间

    通过省略名称空间的名称来创建未命名的名称空间。

    1. namespace // unnamed namespace
    2. {
    3. int ice;
    4. int bandycoot;
    5. }

    不能够在未命名名称空间所属文件之外的其他文件中,使用该名称空间中的名称。可以作为static的替代品。

    9.3.4 名称空间及其前途

    当前的一些指导原则:

    • 不使用外部全局变量,使用已命名的名称空间中声明的变量
    • 不使用静态全局变量,使用已命名的名称空间中声明的变量
    • 开发的函数库或类库,将其放在一个名称空间中。
    • 仅将using编译指令作为权宜之计
    • 不要在头文件中使用using编译指令。非要使用可以放在include之后
    • 导入名称时,首选使用作用域解析运算符或是using声明的方法
    • 对于using声明,将其作用域设置为局部而不是全局

  • 相关阅读:
    【贪心算法】贪心算法任务调度具体应用详解与示例
    【Linux】ls命令
    日语基础复习 Day 15
    数据结构——链表
    深度剖析集成学习Xgboost
    k8s 1.28版本:使用StorageClass动态创建PV,SelfLink 问题修复
    用DIV+CSS技术设计的数码购物商城网站(web前端网页制作课作业)
    Linux 信号集 及其 部分函数
    热门新游 2024 植物大战僵尸杂交版 Mac 版本下载安装详细教程
    会话技术—cookie&Session
  • 原文地址:https://blog.csdn.net/qq_35423190/article/details/126803299