您的位置:首页 > 编程语言 > C语言/C++

《C++ Primer Plus》(第6版)中文版—学习笔记—内存模型和名称空间

2020-08-05 05:05 302 查看

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

书中课后练习将在https://github.com/linlll/CppPrimePlus发布

单独编译

将函数等定义放置在独立的文件中可以,提高代码的复用率。

头文件常常包含的内容

  1. 函数原型
  2. 使用
    #define
    const
    定义的符号常量
  3. 结构声明
  4. 类声明
  5. 模板声明
  6. 内联函数

一般头文件对应的源文件都是用来写函数定义的。

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

之前我们学习过C++通过三种不同的方案存储数据,而在C++11中是四种,线程存储持续性是新增的一种。

  • 自动存储持续性:在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码时,它们使用的内存被释放。C++有两种存储持续性为自动的变量。
  • 静态存储持续性:在函数定义外定义的变量和使用关键字
    static
    定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。C++有3种存储持续性为静态的变量。
  • 线程存储持续性(C++):当前,多核处理器很常见,这些CPU可同时处理多个执行任务。这让程序能够将计算放在可并行处理的不同线程中。如果变量是使用关键字
    thread_local
    声明的,则其生命周期与所属的线程一样长。这里不讨论
  • 动态存储持续性:用
    new
    运算符分配的内存将一直存在,直到使用
    delete
    运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储或堆。

作用域和链接

作用域描述了名称在文件(翻译单元)的多大范围可见。链接性描述了名称如何在不同单元间共享。链接性为外部的名称可在文件见共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量的名称没有链接性,因为它们不能共享。

  1. 作用域为全局的变量在定义位置到文件结尾之间都可用。
  2. 自动变量的作用域为局部,静态变量的作用域是全局还是局部取决于它是如何被定义的。

自动存储持续性

在默认情况下,在函数声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。自动变量只在代码块中有效,允许不同的函数拥有同名的变量,随着程序运行到代码块的结尾,这个自动变量就会被销毁。

int main
{
int a;
{
int b;
}
}

这段代码中的变量

a
在中间的代码块可见,而变量
b
不能够被最外面的那个代码块使用。

在C++11中,

auto
是用来自动判断类型的关键字,但是在C语言和以前的C++版本中,
auto
是用来显示地指出变量为自动存储。而在C++11中,这样就不在合法了。

那么典型的C++编译器如何实现自动变量有助于更深入地了解自动变量。

由于自动变量的数目随函数的开始和结束而增减,因此程序必须在运行时对自动变量进行管理。常用的方法是留出一段内存,并将其视为栈,以管理变量的增减。之所以被称为栈,是由于新数据被象征性地放在原有数据的上面(也就是说,在相邻的内存单元中,而不是在同一个内存单元中),在程序使用完后,将其从栈中删除。栈的默认长度取决于实现,但编译器通常提供改变栈长度的选项。程序使用两个指针来跟踪栈,一个指针指向栈底——栈的开始位置,另一个指针指向堆顶——下一个可用内存单元。当函数被调用时,其自动变量将被加入到栈中,栈顶指针指向变量后面的下一个可用的内存单元。函数结束时,栈顶指针被重置为函数被调用前的值,从而释放新变量使用的内存。

register int count_fast;

这串代码定义了一个寄存器变量,但是在C++11中,

register
关键字的作用被抹除了,只是用来显式地表示自动变量。

静态持续变量

在默认情况下,静态数组和结构将每个元素或成员的所有位都设置为0。

...
int global = 1000;
static int one_file = 50;
int main()
{
...
}
void func1(int n)
{
static int count = 0;
int llama = 0;
...
}
void func2(int q)
{
...
}

上面的代码总共展示了三种不同的静态变量。

五种变量存储方式

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

静态持续性、外部链接性

静态持续性、外部链接性的变量通常被称为外部变量,作用域为整个文件。一般变量时满足单定义规则,也就是说变量只能够定义一次,对于全局变量(也叫外部变量),C++中有两种方式来定义,一种时定义声明,还是有一种是引用声明,如下

double a;
extern double a;

extern
关键字在引用声明的时候不能够初始化,否则的话,这样的声明就会为定义,会分配储存空间。

静态持续性、内部链接性

通过

static
关键字来实现,这样声明的变量在所属的文件中有效。

// file1.cpp
int a;
int main()
{...}

// file2.cpp
static int a;
int func()
{...}

由于单定义规则,上面的代码是否可行呢?答案是可行,因为

static
关键字表明file2.cpp中的变量
a
是静态的,而file1.cpp是自动变量,当运行file2.cpp中的程序是,静态变量会将同名的自动变量隐藏,不会影响自动变量的值,原文中的程序也说明了两个变量的地址是不一样的。

静态持续性、无链接性

这种变量一般都是在代码块中被声明定义,例如一个函数中,例如下述代码

void strcount(const char *str)
{
using namespace std;
static int total = 0;
int count = 0;
...
}

在这个函数中我们定义了一个静态持续性和无链接性的静态变量,这个变量进行了初始化,则程序会在启动的时候进行一次初始化。以后再调用函数的时候,将不会想自动变量那样给这个变量初始化了。

说明符和限定符

存储说明符

  • auto
  • register
  • static
  • extern
  • thread_local
  • mutable

cv-限定符

  • const
  • volatile

我们先来讲一下cv-限定符,我们了解const,const变量不能够被修改。那么什么是volatile呢?关键字volatile表明,即使程序代码没有堆内存单元进行修改,其值也可能发生变化。例如编译器有这样的优化,假设编译器发现程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在这两次使用之间不会变化。而使用了volatile,编译器就不会对此进行优化。

个人猜测用于采集硬件数据很有效 。

我们再来看看mutable

struct data
{
char name[30];
mutable int access;
...
}
...

const data veep = {"LINJIA", 0, ...};
veep.access++;

mutable就像解除const的限制一样。

我们再来看看const与静态变量之间的关系吧

之前提过,默认情况下,全局变量的链接性为外部,但是const变量的链接性为内部的,而extern可以覆盖默认的内部链接性。

const int a;		// internal linkage
extern const int b;	// external linkage

函数和持续性

函数也有链接性,当我们编写了一个程序,使用了多文件形式,一般而言,一个文件中的函数都是外部链接性的,也就是可以在文件间共享,但是我们如果希望函数的链接性为内部,也就是在一个文件中使用,我们可以在函数类型前面加上static关键字

static int private(){}

语言持续性

请看原文

存储方案和动态分配

动态内存可以使用new和delete运算符来控制,我们知道,如果使用new申请了内存,没有通过delete进行释放内存的话,那块内存将永远存在(在程序运行时),会带来内存溢出的情况。

初始化

// 指针初始化
int *pi = new int(6);
double *pd = new double(9.99);
// 结构初始化
struct where {double x; double y; double z;};
where *one = new where {2.5, 1.0, 3.0};
int *ar = new int[4] {2, 4, 6, 8};
// 列表初始化
int *pin = new int {6};
double *pdo = new double {9.99};
void * operator new(std::size_t);
void * operator new[] (std::size_t);
void operator delete(void *);
void operator delete[] (void *);

int *pi = new(sizeof(int));			// int *pi = new int;

int *pa = new(40 * sizeof(int));	// int *pa = new int[40];

delete (pi);						// delete pi;

这样的函数称为可替换的,也就是说,只要你愿意,就可以为new和delete提供替换函数,进行定制。

定位new运算符

请看以下代码

// newplace.cpp -- using placement new
#include <iostream>
#include <new> // for placement new
const int BUF = 512;
const int N = 5;
char buffer[BUF];      // chunk of memory
int main()
{
using namespace std;

double *pd1, *pd2;
int i;
cout << "Calling new and placement new:\n";
pd1 = new double[N];           // use heap
pd2 = new (buffer) double[N];  // use buffer array
for (i = 0; i < N; i++)
pd2[i] = pd1[i] = 1000 + 20.0 * i;
cout << "Memory addresses:\n" << "  heap: " << pd1
<< "  static: " <<  (void *) buffer  <<endl;
cout << "Memory contents:\n";
for (i = 0; i < N; i++)
{
cout << pd1[i] << " at " << &pd1[i] << "; ";
cout << pd2[i] << " at " << &pd2[i] << endl;
}

cout << "\nCalling new and placement new a second time:\n";
double *pd3, *pd4;
pd3= new double[N];            // find new address
pd4 = new (buffer) double[N];  // overwrite old data
for (i = 0; i < N; i++)
pd4[i] = pd3[i] = 1000 + 40.0 * i;
cout << "Memory contents:\n";
for (i = 0; i < N; i++)
{
cout << pd3[i] << " at " << &pd3[i] << "; ";
cout << pd4[i] << " at " << &pd4[i] << endl;
}

cout << "\nCalling new and placement new a third time:\n";
delete [] pd1;
pd1= new double[N];
pd2 = new (buffer + N * sizeof(double)) double[N];
for (i = 0; i < N; i++)
pd2[i] = pd1[i] = 1000 + 60.0 * i;
cout << "Memory contents:\n";
for (i = 0; i < N; i++)
{
cout << pd1[i] << " at " << &pd1[i] << "; ";
cout << pd2[i] << " at " << &pd2[i] << endl;
}
delete [] pd1;
delete [] pd3;
// cin.get();
return 0;
}

will output:

Calling new and placement new:
Memory addresses:
heap: 0x1041920  static: 0x408040
Memory contents:
1000 at 0x1041920; 1000 at 0x408040
1020 at 0x1041928; 1020 at 0x408048
1040 at 0x1041930; 1040 at 0x408050
1060 at 0x1041938; 1060 at 0x408058
1080 at 0x1041940; 1080 at 0x408060

Calling new and placement new a second time:
Memory contents:
1000 at 0x1041c20; 1000 at 0x408040
1040 at 0x1041c28; 1040 at 0x408048
1080 at 0x1041c30; 1080 at 0x408050
1120 at 0x1041c38; 1120 at 0x408058
1160 at 0x1041c40; 1160 at 0x408060

Calling new and placement new a third time:
Memory contents:
1000 at 0x1041c80; 1000 at 0x408068
1060 at 0x1041c88; 1060 at 0x408070
1120 at 0x1041c90; 1120 at 0x408078
1180 at 0x1041c98; 1180 at 0x408080
1240 at 0x1041ca0; 1240 at 0x408088

定位new运算符需要偏移量。

名称空间

C++新增了名称空间的特性,使得一个名称空间的名称不会与另一个名称空间的名称发生冲突,C++将使用namespace关键字对名称空间进行创建,例如

namespace Jack
{
double a;
int b;
struct c {...};
void function() {...};
}
namespace Will
{
double a;
int b;
struct c {...};
void function() {...};
}
Jack::a = 0.2;
Will::b = 1;

即使两个名称空间中的名称都完全一样,但是由于名称空间的存在,这些同名的变量在创建的时候还是拥有不同的内存的。最后通过作用域解析运算符

::
来限定该名称。

名称空间可以时全局的,也可以位于另一个名称空间中,但是不能位于代码块中。因此,在默认情况下,名称空格键中声明的名称的链接性为外部的(除非它引用了常量)。

使用using声明和using编译指令的区别,顾名思义,using声明是一个声明,在使用using声明声明了一个变量的时候,如果你想再声明一个常规的同名变量时不允许的,例如

double a = 0.00;
int main()
{
using Jack::a;
double a;		// Error!!!,already have a local a
a = 1.00;		// Jack::a = 1.00;
::a = 2.00;		// global a = 2.00
}

using编译指令不同,如果使用using声明,并且我们想要使用一个名称空间的所有名称的时候,我们就需要对每个成员都使用using声明,但是using编译指令就容易多了,只需要

using namespace Jack
即可,与using声明不同的是,using编译指令只是指出了当前的声明区域中可以使用名称空间,也就是不要加
Jack::
作用域解析运算符了。

double a = 0.00;
int main()
{
using namespace Jack;
Jack::a;
double a;		// hides Jack::a
a = 1.00;		// Jack::a = 1.00;
::a = 2.00;		// global a = 2.00
Jack::a = 3.00;
}

与using声明不同的是,当同一个声明区域中,在声明了一个空间中的变量后再声明一个常规同名变量是被允许的,但是常规变量要将全局变量和空间变量都隐藏起来,当使用

::
Jack::
的时候才可以加以限定。

namespace Will
{
using Jack::a;
}

这样的代码是允许的,也就是在名称空间中使用using声明来使用Jack名称下的成员。这个时候,变量

a
是既位于空间Jack下,也位于空间Will下的,也就是说,
Jack::a = 0.00
Will::a = 0.00
都是可以的。

未命名的名称空间,就好比链接性为内部的 静态变量的替代品。

namespace
{
int a;
int b;
}

名称空间及其前途

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

  • 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量。
  • 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
  • 如果开发了一个函数库或类库,将其放在一个名称空间中。事实上,C++当前提倡将标准函数库放在名称空间std中,这种做法扩展到了来自C语言中的函数。例如,头文件math.h是与C语言兼容的,没有使用名称空间,但C++头文件cmath应将各种数学库函数放在名称空间std中。实际上,并非所有的编译器都完成了这种过渡。
  • 仅将编译指令using作为一种将旧代码转换为使用名称空间的权宜之计。
  • 不要再头文件中使用using编译指令。首先,这样做掩盖了要让哪种名称可用;另外,包含头文件的顺序可能影响程序的行为。如果非要使用编译指令using,应将其放在所有预处理器编译指令#include之后。
  • 导入名称后,首选使用作用域解析运算符或using声明的方法。
  • 对于using声明,首选使用作用域设置为局部而不是全局。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: