写点什么

C++ 第九章 内存模型和名称空间

用户头像
Dreamer
关注
发布于: 2020 年 11 月 02 日
C++ 第九章 内存模型和名称空间

以下内容为本人在阅读 《C++ primer 》第六版的第九章时的,整理的一些重点,只为自己巩固所学,也加深对 C++的理解。


1. 编译


C++鼓励程序员将组件函数放在单独的文件中,单独的编译这些文件,然后再将这些文件链接成可执行的程序。通常的编译器是既可以编译程序,也可以管理链接器的。


为避免在程序开发中把同一程序放入多个文件的问题,引入了 #include 来处理这种情况。因此程序分为 3 个部分


  • 1. 头文件: 包含结构声明和使用这些结构的函数原型

  • 2. 源代码文件:包含与结构有关的函数代码

  • 3. 包含调用与结构相关的函数的代码


注意,不要将函数定义或变量声明放在头文件中,这种做法对于简单的情况可行,但通常会引来麻烦。例如,可能引起重复定义而报错。以下是头文件常包含的内容:


  • 函数原型

  • \#define 或 const 定义的符号常量

  • 结构声明

  • 类声明

  • 模版声明

  • inline 函数


“ ” 还是 < >包含头文件的问题


  • '' c++ 编译器将在存储头文件的主题系统中查找

  • < >首先在当前的工作目录下或源代码下查找,如果没有找到,就在标准位置查找


还要注意一点不要\#include 源代码文件


牢记规则:在同一个文件中只能将同一个头文件包含一次。很可能不知情的时候多次包含。解决的办法是使用编译器预处理指令\#ifdefine


最后关于编译的一个点就是确保使用同一个编译器来编译自己的程序,否则很容易链接失败。


2. 存储持续性,作用域和链接性


![总体的理解](/Users/dreamer365898376/Desktop/CSDNBlogs/CPP/CPPPrimer/chapter9/存储数据的方案.png)


  • 作用域:名称在文件内多大的范围内可见

  • 链接性:名称如何在不同的单元内共享


auto 在 C++11 中的用法上自动推断


froob(int n)

{

auto float ford; // ford has automatix storage

...

}

自动变量和栈


深入的了解下典型的 C+编译器中如何实现自动变量的原理。常用方法:留出一段内存,并视为栈,以管理变量的增减,新增的变量放在原数据之上,程序使用完后,释放这段内存。通过两个指针来控制,一个指针指向栈底,另一个指向栈顶。


寄存器变量


关键字 register 由 c 引入,建议编译器使用 CPU 寄存器来存储自动变量:


register int count_fast; // regiser for a register variable

目的是提高变量的访问速度。C++11 中这种提示作用只能用于自动变量。书中关于这里的解释不是很明白。


静态持续变量


静态变量提供了三种链接性


  • 外部链接性:可在其他文件中访问

  • 内部链接性:只能在当前文件中访问

  • 无链接性:只能在当前函数或者代码块中访问


共同点: 在整个代码期间存在


程序在将分配固定的内存块来存储所有的静态变量,这些变量在整个程序的运行期间一直存在。如果没有显示的初始化静态变量,编译器将把它设置为 0.默认情况下,静态数组和结构的每个元素或成员的所有味都设置为 0


下面举例三种静态变量


int global = 1000; //static duration, external linkage

static int one_file = 50; // static duration, internal linkage

int main()

{

...

}

void dunct(int n)

{

static int count = 0; //static duration, no linkage

int llama = 0;

}

上述的代码中, 所有的静态持续变量(global, one_file, count)都在整个程序的执行期间存在,count 为作用域为局部,无链接性.与 llam 相同,两者的区别是,count 在即使函数 dunct()没有执行的时候,就存在内存中,one_file 在这个文件的函数中都可使用,global 可以在程序的其他文件中使用。


再次总结下,static 在局部声明时,表明的是无链接性的静态变量,表示的是存储持续性。在代码块外声明的时候,static 表示的是内部链接性,变量已经是静态持续性了,有人称这种用法为关键字重载。


引入命名空间前对 5 种不同的存储特性总结如下:


|存储描述|持续性 | 作用域 | 链接性 | 如何声明 |

|-------|-------|-------|-------|-------|

| 自动 | 自动 | 代码块 | 无 | 代码块内 |

|寄存器 | 自动 | 代码块 | 无 | 代码块内,关键字 register|

|静态,无链接性| 静态 | 代码块 | 无 | 代码块内,关键字 static|

|静态, 外部链接性 | 静态|文件 | 外部 | 不在任何函数内 |

|静态, 内部链接性| 静态 | 文件 | 内部 | 不在任何函数内, 关键字 static|


静态变量初始化


除默认的零初始化外,静态变量可以进行常量表达式初始化和*动态初始化*,在 C++中,指针变量初始化为响应的内部实现,结构成员是零初始化。


零初始化和常量初始化为静态初始化,在编译器处理文件时进行,动态初始化为编译后初始化。

真实的处理哦过程如下,


  • 首先,所有静态变量被零初始化,不管程序员是否现实的初始化

  • 如果使用常量表达式初始化,且编译器仅根据文件内容就可计算出表达式,将执行常量表达式初始化

  • 最后,如果没有足够的信息,变量将动态初始化。实例如下


#include <cmath>

int x; // zero-initialization

int y = 5; //constant-expression initialization

long z = 13 * 13; //constant-expression initialization

const double pi = 4 * atan(1.0) //dynamic initialization

2.4 静态持续性,外部链接性


链接性为外部的变量称为外部变量,存储持续性为静态,作用域为整个文件,外部变量在定义后的任何函数中都可以使用。


这里需要说明一下的是单定义原则(One Defination Rule),即变量只能定义一次。为满足这种需求,C++提供两种声明方式


  • 定义声明(defining declaratuin)简称定义(define), 为变量分配存储空间

  • 引用声明(reference declararion)简称声明(declaration),因为它引用已有的变量


引用声明中使用关键字 extern,且不进行初始化,否则声明为定义,导致分配内存空间。


double up; //definition, uo is 0

extern int blem; //blem is define elsewhere

extern char gr = 'z'; // definition because initialized

如果在多个文件中使用外部变量,只需要在一个文件中包含定义该变量的定义,但在使用该变量的其他所有文件中,都必须使用 extern 声明它。


//file01.cpp

extern int cats = 20; //definition because of initialization

int dogs = 22; // also a definition

int fleas; // also a definition

//file02.cpp

//use cats and dogs from file01.cpp

extern int cats; //not definitions because they use extern

extern int dogs; // and have no initialization

//file03.cpp

//use cats and dogs form file01.cpp

extern int cats;

extern int dogs;

extern int fleas;

在文件 file01.cpp 中 extern 关键字不是必不可少的,即使省略,效果也相同。


还有一点 关于单定义原则需要说明的是,单定义原则上并不是意味不能有多个变量的名称相同。在不同的函数中声明的同名变量彼此独立,分别有自己的地址。局部变量可能会隐藏同名的全局变量


对于使用同名函数的问题,c++引入了:: 作用域解析运算符,来特别指明所调用的变量所在的命名空间,这种方法更加安全,在命令空间的部分会更详细的说明。


2.5 静态持续性,内部链接性


当在不同的文件中使用相同的名称来表示变量时,可以使用如下的方式


//file1

int errors = 20; //external declration

...

---------------------------------------

//file2

static int errors = 5; //known to file2 only

void froobish()

{

cout << erros; // uses errors in file2

}

以上没有违反单定义原则,static 指明该处定义的变量只在文件内部使用。


外部变量的使用目的是在多个文件的你不同部分中共享数据,链接性为内部的静态变量在同一文件中多个函数中共享数据。


2.6 静态存储持续性,无链接性


无链接性的局部变量,该变量只在代码块内使用,但在该代码块不处于活动状态的时候任然存在,两次函数调用之间,静态局部变量的值不变。


2.7 说明符和限定符


存储说明符分类如下:


  • auto :c++11 自动推断

  • register:寄存器变量

  • static

  • extern

  • threadlocal :同一个生命中不能使用多个说明符,threadlocal 除外,可以和 extern 或 static 结合

  • mutable: 与 const 对应


  1. cv-限定符

* const :内存初始化后,就无法改变

* volatile: 用于程序代码没有对内存修改时,内存也可能改变,


  1. mutable

指出 const 结构的某个成员也可以改变

struct data

{

char name[30[;

mutable int accesses;

...

}

  1. const


c++ 中 const 限定符对默认的 存储类型有影响。默认情况下,全局变量的链接性为外部的,但是全局的 const 定义同 static 说明符一样


const int fingers = 10; // same as static int fingers

int main()

{

...

}

需要为某个文件使用一组定义,然而在其他的文件中使用另一组定义的时候,由于 const 是外部定义的,内部链接属性的变量,可以在所有的文件中使用相同的声明,内部链接性意味着每个文件中都有一组属于自己的常量,而不是共享。


当在需要某个常量的链接属性为外部时,需要使用 extern 关键字来覆盖默认的内部链接性


extern const int status = 10; //definition with external linkage

在所有需要使用该常量的文件中使用 extern 来声明它。与常规的变量不同,定义常规变量时,不必使用 extern 关键字,在此时必修使用 const.并且,由于单个 const 在多个文件中共享,只有一个文件对其初始化。


2.8 函数和链接性


对比变量存在链接性,函数也有链接性。


C++不允许在一个函数定义中定义另外一个函数。因此所有的函数的存储持续性为自动静态的,在整个程序的执行期间都存在。默认的情况下,函数的链接性为外部的,可以在文件之间共享。实际上,可以使用函数原型中的关键字 extern 来指出另一个文件中定义的函数。要让程序在另一个文件中查找函数,该文件必须作为**程序的组成部分编译或者是由链接程序搜索为的库文件。还可以使用 static 关键字将函数的链接性设定为内部的,只在单个文件中使用。必须同时在函数原型和函数定义中使用关键字。


static int private(double x);

...

static int private(double x

{

...

}

定义静态函数的文件中,静态函数将覆盖外部定义,即使外部定义了同名的函数,该文件将使用此静态函数。


单定义规则也适用于非 inline 函数。对于每个非 inline 函数,程序中只包含一个定义。对于链接性为外部的函数来说,这意味着在多个文件程序中,只能定义一次,但使用该函数的每个文件都应包含其函数原型。


inline 函数不受这项规则的约束,但是 C++要求同一个函数的所有 inline 定义必须相同。


在程序运行的过程中,C++寻找函数的定义,遵循以下的规则:


  • 如果该文件的函数类型指出是静态的,编译器只在该文件内查找

  • 未指明函数为静态,编译器在所有的文件中查找。如果找到了两个定义将报错

  • 如果程序文件中没有找到,编译器将在库中搜索。如果定义一个与库函数同名的函数,编译器将使用程序员定义的版本。


2.9 语言链接性


对于在 c++和 c 相互调用的问题。由于 c 中,一个名称对应一个函数,c++中,同一个名称对应多个函数。相互调用的适应会出现问题。解决方案是使用关键字 extern.


extern "c" void spiff(int); // use c protocal for name look-up

extern void spoff(int); // use c++ protocol foir name look-up

extern "c++" void spaff(int) // use c++ protocal for name look-up

2.10 存储分配和动态分配


之前说的 5 种分配方式不适用于 C++运算符 new, (或 C 函数 malloc()分配的内存),动态分配由 new, delete 控制,不受作用域和链接规则的控制。可以在一个函数中分配内存,在另外一个函数中释放内存。通常情况下,编译器中有三块独立的内存,静态变量(可再次细分),自动变量和动态变量。


虽然存储方案不适用于动态内存,但适用于跟踪动态内存的自动和静态指针变量。例如:


float *p_fees = new float [20];

由 new 分配 80 个字节(假设一个 float 4 个字节)的内存一直存在内存中,直到使用 delete 运算将其释放。但当包含该声明的语句执行完毕后,pfees 指针将消失。如果希望另一个函数中使用这 80 个字节的内容,需要把地址传递或返回给该函数。如果将 pfees 的链接性声明为外部的,该文件中声明的所有函数都可以使用。


1.使用 new 初始化

初始化动态分配的变量.


//初始化内置标量类型的方式:

int pi = new int (6); // *pi set to 6

double pd = new double (99.99); // pd set to 99.99

//初始化常规结构体

struck where

{

double x;

double y;

double z;

};

where * one = new where {2.5, 5.3, 7.2};

int * ar = new int [4] {2, 4, 6, 7};

2.new 失败

new 找不到请求的内存量,c++之前的设定是返回空指针,现在的操作是**引发异常 std::bad_alloc.

关于异常的文件会在后面的文章中总结。


3.new: 运算符,函数和替换函数


//new, new []

void * operator new(std::size_t); //use by new

void * operator new [](std::size_t); // use by new[]

//delete , delete []

void operator delete (void *);

void operator delete [](void *);

4.定位 new 运算符

通常 new 在堆(heap)中找到一个足以满足要求的内存块,还有一种用法(placement)new 元算符,可以指定要使用的位置。使用 placement new 需要包含头文件 new.这种情况下的 new 不能用 delete.


5.placement new 的其他形式

调用一个接受两个参数的 new()


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

int * p2 = new(buffer) int; //invokes new(sizeof(int), buffer)

int p3 = new(buffer) int[40]; //incokes new(40sizeof(int), buffer)

palcement new 不可替换,但是可以重载。


3.命名空间


3.1 传统的 C++命名空间


需要复习的名称概念。


  • 声明区域(declaration region).声明在函数外的微全局变量,,声明区域为整个文件,声明在函数内的,声明区域为该代码块

  • 潜在作用域(potential scope).从声明点开始,到声明区域结尾。


3.2 新的名称空间特性


C++提供一种新的声明区域来创建命名的名称空间。

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


除了用户定义的名称空间外,还存在一个全局命名空间(global space),它对应文件层级。前面说的全局变量现在被描述为全局名称空间中。


任何名称空间内的名称都不会和其他名称空间的中名称冲突。名称空间是开放的,即可以把名称加入已有的名称空间中,


//假设已经定义过 Jill

namespace Jill

{

char goose(const char );}


对于访问给定名称空间中的名称,最简单的方法是,通过作用域解析运算符::.不带装饰的名称称为未限定的名称,包含命名空间的名称称为限定的名称


  1. using 声明和 using 编译指令


  • using 声明使特定的标识符可用

  • using 编译使整个命名空间可用


using Jill::fetch; //a using delcaration

using 声明的变量和函数也会覆盖同名的全局变量和函数


using namespace Jill; // make all names in Jill available

使用上述的两种用法会增加命名冲突的可能性。更推荐的用法是用命名空间的来解析的方式。


Jill::pal = 3;

Jack::pal = 4;

  1. using 编译指令和 using 声明比较

使用 using 编译指令,像是大量的使用作用域解析运算

使用 using 声明,像声明了相应的名称。如果使用 using 编译指令将名称空间中的名称导入该声明区域,则局部版本将隐藏名称空间版本


  1. 名称空间的其他特性


//using 嵌套使用

namespace elements

{

namespace fire

{

int flame;

...

}

float water;

}

---------------------------------

//using 编译和声明混合使用

namespace myth

{

using Jill::fetch;

using namespace elemets;

using std::coput;

using std::cin;

}

using 编译命令有传递性,例如上例中 using namespace myth

using namespce myth

using namespace elements

作用相同。

还有一个特别的用法,可以对名称空间创建别名

  1. 为命名的名称空间


namespace

{

int ice;

int bandcoot;

}

使用这种定义类似与静态的全局变量

3.3 名称空间的使用建议


  • 使用已命名的名称空间中的变量,而不是使用全局变量

  • 使用已命名的名称空间中的变量,而不是使用静态全局变量

  • 开发的库放入一个命名空间内

  • 仅把使用 using 作为将旧代码转换的权宜之计

  • 不要在头文件中使用 using 编译命令

  • 导入名称式,首选作用域解析运算符或 using 声明方法

  • 对 using 声明,首选设置为局部



用户头像

Dreamer

关注

一个不想做搜索的NLPer不是一个好的CVer 2019.12.18 加入

还未添加个人简介

评论

发布
暂无评论
C++ 第九章 内存模型和名称空间