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

第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 linkage
    int 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++11

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关键字时会被替换:

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 new
    p1 = new chaff;        // place structure in heap
    p3 = new int[20];      // place int array in heap

    // now , the two forms of placement new
    p2 = new (buffer1) chaff;    //place structure in buffer1
    p4 = 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::fetch
    cin >> ::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声明,将其作用域设置为局部而不是全局

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aiky哇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值