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

本章内容包括:

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

9.1 单独编译

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

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

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

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

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

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

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

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

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

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

#ifndef COORDIN_H_
#define COORDIN_H_// place include file contents here#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。

进行三种静态变量举例:

...
int global = 100;        // static, external linkage
static int one_fle = 50;    // static, internal linkage
int main()
{
...
}void funct1(int n)
{static int count = 0;     // static , no linkageint llama = 0;
...
}void funct2(int q)
{
...
}

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

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

5种变量存储方式

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

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

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

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

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

double up;         // 定义声明
extern int blem;    // 引用声明
extern char gr = 'z';    // 定义声明,因为包含了初始化// 如果要在其他文件中使用另一个文件定义的变量,使用extern// file01.cpp
int process_status = 0;
int main()
{...
}// file02.cpp
extern int process_status;
int work()
{std::cout << ::process_status << endl;...int process_status = 1;std::cout << process_status << endl;}

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

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,其某个成员也可以被修改。

struct data
{char name[30];mutable int accesses;...
};const data veep = {"aiky",0,...};strcpy{veep.name, "john"};    //not allowed
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中的函数,就找不到了。

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

extern "C" void spiff(int);    // use C protocol for name look-up
extern void spoff(int);        // use C++ protocol for name look-up
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提供了内置类型的内存分配和初始化方法。

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

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

struct where { double x; double y ; double z;};
where * one = new where {2.5,5.3,7.2};        // C++11
int * ar = new int [4] {2,4,6,7};        // C++11int * 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关键字时会被替换:

int * pi = new int ;
// 会被转化为
int * pi = new(sizeof(int));int * pa = new int[40];
// 会被转化为
int * pa = new(40 * sizeof(int));delete pi;
// 会被转化为
delete (pi);

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

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

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

#include <new>
struct staff
{char dross[20];int slag;
};char buffer1[50];
char buffer2[500];
int main()
{chaff *p1, *p2;int *p3, *p4;// first ,the regular forms of newp1 = new chaff;        // place structure in heapp3 = new int[20];      // place int array in heap// now , the two forms of placement newp2 = new (buffer1) chaff;    //place structure in buffer1p4 = new (buffer2) int[20];    // place int array in buffer2
...
}

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

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

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

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

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

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

9.3 名称空间

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

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

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

9.3.1 传统的C++名称空间

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

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

9.3.2 新的名称空间特性

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

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

namespace Jack {double pail;     void fetch();int pal;struct Well { ... };
}namespace Jil{double bucket(double n){ ... }double fetch;int pal;struct Hil {...};
}

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

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

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

比如:

Jack::pail = 12.34;

Jill::Hill mole;

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

using声明和编译指令

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

// using声明 --------------
// file1
namespace Jill{double bucket(double n) { ... }double fetch;struct Hill { ... };
}// file2
char fetch;
int main()
{using Jill::fetch;cin >> fetch ;    //read a value into Jill::fetchcin >> ::fetch;    //read a value into global fetch
}// using编译指令-------------
using namespace Jack;    // make all the names in Jack available

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

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

名称空间的其他特性

可以将名称空间进行嵌套

namespace elements
{namesapce fire{int flame;...}float water;
}// 此时以下命令都是有效的
using namespace elements::fire;elements::fire::flame = 10;// 在命名空间内也是可以包含using编译指令和using声明的
namespace myth
{using Jill::fetch;using namespace elements;using std::cin;
}

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

未命名的名称空间

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

namespace            // unnamed namespace
{int ice;int bandycoot;
}

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

9.3.4 名称空间及其前途

当前的一些指导原则:

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

《c++ Primer Plus 第6版》读书笔记(4)相关推荐

  1. Think in Java第四版 读书笔记9第15章 泛型

    Think in Java第四版 读书笔记9第15章 泛型 泛型:适用于很多很多的类型 与其他语言相比 Java的泛型可能有许多局限 但是它还是有很多优点的. 本章介绍java泛型的局限和优势以及ja ...

  2. 《Java编程思想》读书笔记

    前言:三年之前就买了<Java编程思想>这本书,但是到现在为止都还没有好好看过这本书,这次希望能够坚持通读完整本书并整理好自己的读书笔记,上一篇文章是记录的第十七章到第十八章的内容,这一次 ...

  3. 深入分析Java Web技术内幕读书笔记(一)浅析Web请求过程

    随着Web技术的快速发展,互联网的网络架构已经从传统的C/S架构转变为B/S架构,B/S架构相较于传统的C/S架构,有诸多优点,例如:提供了统一的操作方式,简化了用户的学习成本:便捷的开发方式大大提高 ...

  4. 《Java编程思想》读书笔记 第十三章 字符串

    <Java编程思想>读书笔记 第十三章 字符串 不可变String String对象是不可变的,每一个看起来会修改String值的方法,实际上都是创建一个全新的String对象,以及包含修 ...

  5. Think in Java第四版 读书笔记10 第16章 数组

    Think in Java第四版 读书笔记10 第16章 数组 数组和容器很像 但他们有一些差别 16.1 数组为什么特殊 数组与容器的区别主要在效率和存储类型 效率:数组是简单的线性序列 使得数组的 ...

  6. 鸟哥的linux私房菜-基础学习篇 读书笔记

    从事linux工作一年多,算是能够熟练运用linux服务器,但仍觉得自己对Linux的原理,理论缺乏空洞,潜下心来认真阅读尘封的鸟哥经典,知识点很全,收获颇多,实践与知识结合,知行合一,对linux开 ...

  7. Unreal Engine 4 学习总动员读书笔记

    Unreal Engine 4 学习总动员读书笔记 参考<Unreal Engine 4 学习总动员>,按照里面的录制的视频一步一步的操作,并将操作的流程和关键截图保存下来,方便后面的再次 ...

  8. 鸟哥的LINUX私房菜 基础学习篇 读书笔记 -- 第零章 计算机概论 (一)

    鸟哥的LINUX私房菜 基础学习篇 读书笔记 -- 第零章 计算机概论 (一) 第零章 计算机概论 0.1 电脑:辅助人脑的好工具 0.1.1 计算机硬件五大组成部分 0.1.2 一切设计的起点: C ...

  9. 深入分析Java Web技术内幕读书笔记(二)浅析DNS域名解析过程

    上一篇文章<浅析Web请求过程>讲述的是如何发起HTTP请求,对于请求发起过程中很重要的一个步骤--DNS解析过程的描述是一带而过,本篇文章将跟着DNS解析过程来分析域名是如何解析的. 一 ...

  10. 《Java核心技术卷一》读书笔记

    <Java核心技术卷一>读书笔记 对象与类 类 类是构造对象的模板.蓝图.由类构造对象的过程称为类的实例. 对象的数据叫做实例域,操作数据的过程叫做方法 对于每个特定的类实例(对象)都要一 ...

最新文章

  1. 何恺明、吴育昕最新成果:用组归一化替代批归一化
  2. 功能整合(二):轮播图(可控)、事件流
  3. C 整数反转
  4. Traceroute笔记
  5. .NET Core开发实战(第18课:日志框架:聊聊记日志的最佳姿势)--学习笔记(下)...
  6. code iban 是有什么组成_EAN-128码和Code-128码的区别
  7. 微软公司等数据结构+算法面试100题2010版全部出炉
  8. Invoke and BeginInvoke BeginInvoke和EndInvoke方法 (转)3
  9. php 如果判断是utf,php 判断网页是否是utf8编码的方法_PHP教程
  10. 迭代器,生成器,三元表达式,列表解析式
  11. mysql三台部署_使用三台主机部署LNMP
  12. java的编译原理_Javac编译原理 - Martiny的个人空间 - OSCHINA - 中文开源技术交流社区...
  13. Build Setting 之 Code Signing 详解
  14. VSCode 返回上一个光标 (上一个浏览位置)
  15. 【生活】驾照C1-科一手册
  16. 神奇黑八传奇再现金州 勇士大胜小牛光荣晋级
  17. android oreo 老机型,Android Oreo 通知新特性,这坑老夫先踩了
  18. 使用NFC模拟校园卡门禁功能 【Mac, Windows, Android, 手环】
  19. Mac新手必看教程 Mac系统基本设置 苹果电脑的基本操作
  20. 画论65 方薰《山静居画论》

热门文章

  1. HTML5+CSS3小实例:菜单栏图标悬停效果
  2. 学ajax要学php吗,javascript – Ajax新手学习(PHP JQuery)
  3. mysql 表结构的复制
  4. Win10 -- CUDA10 CUDNN 安装
  5. 博弈论夏普利值!提高机器学习可解释性的新方法!
  6. Spring三大核心思想之AOP(面向切面编程)
  7. 索尼M36h怎么刷第三方recovery?
  8. tensorflow 19: tflite 概念理解
  9. Android 集成Thinker 教程
  10. stm32 MPU6050 6轴姿态传感器的介绍与DMP的应用