C++基础——内存模型和名称空间

C++基础——内存模型和名称空间

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

存储持续性是指数据保留在内存中的时间。

不考虑并发,则只有三种:

  • 自动变量
  • 静态变量
  • 动态变量

作用域和链接

作用域描述了名称(变量名、函数名等)在文件内多大范围内可见。

通常局部变量的作用就是代码块括起来的语句。全局变量的作用域为变量定义位置到程序结束位置。

静态变量的作用域取决于定义方式。

链接描述了名称如何在不同单元之间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能在定义的文件中共享,自动变量的名称没有链接性,它们不能被共享。

一定要区分作用域和链接性,作用域仅仅是空间性的,链接具有空间性和时间性。

静态变量

静态变量最困难的地方,就是要考虑其链接性,也就是共享性。

对于下面三个静态变量:

int global;
static int one_file;
int main(){
    ...
}
void func(){
    static int var;
}

假设此文件名为file.cpp

global变量可以在另一个cpp文件中被使用,比如这个文件名为another_file.cpp

extern int global;

当然这个外部变量的声明也可以放在头文件中。

就可以使用file.cpp中的变量global

而对于one_file变量,则不能在其他cpp文件中被引用,只能在file.cpp被共享被使用,也就是说,static将这个变量限制在了文件file.cpp,或者说,其作用域仅在这一个cpp文件中。

前面这两个静态变量还比较容易理解,第三个静态变量就有些复杂了。

var变量是func函数的静态变量,这也就是说,它只能被这个函数使用和共享,即作用域为这个函数,乍一看可能和自动变量没有什么区别,差别就在于静态和共享

所谓静态,指这个变量在程序开始运行时就存在于内存中,即便还没有执行到func函数,而自动变量只有在执行到func函数的时候才会放入内存。

从静态就能够看出来,static静态变量虽然作用域是这个函数,但是它似乎与这个函数有一定的独立性。实际上在内存中,static静态变量是存放在静态存储区而不是栈中。

什么是共享呢?为了方便解释,先举一个例子:

void func(int value){
    static int var = 0;
    var += value;
}
int main(){
    func(1);
    func(2)
}

上面的代码执行完成后var的值为多少呢?

如果是自动变量,第一次执行完var1,第二次为2

但是对于静态变量,它是能被该函数的不同调用共享的,则第一次执行完成后var1,但是执行完成后var并没有被销毁,而是始终保存在静态存储区,当我们执行func(2)时,static int var;不会再生成新的静态变量,而是会跳过这个语句,因为编译器在静态存储区找到了同名的静态变量,因此编译器会直接从静态存储区取得静态变量的当前值,也就是1,据此得到的计算结果为3,结果依旧不会被销毁,还是保存在静态存储区。

下面是在程序运行期间静态变量var和函数func存在的时间简图:

在这里插入图片描述

图4-1 函数中的静态变量

静态变量初始值都为0,如果是数组,则元素全部置为0,只有在第一次遇到静态变量时才开始初始化,程序开始时并不会进行初始化。

全局/静态变量在编译器编译过程中就已经分配好静态存储区,执行时直接由操作系统映射到内存中。具体的步骤可以学习计算机组成原理编译原理

通过下面这个图或许更好理解。

对于上面的程序,在程序运行时,静态存储区已经生成了静态变量:

在这里插入图片描述

图4-2 静态存储区

程序运行时,如果遇到需要访问静态变量的语句,就回到静态存储区取得该变量进行计算:

在这里插入图片描述

图4-3 静态变量工作原理简单介绍

在类中的静态变量机制和函数中的静态变量非常类似,等之后讲到类会再提一次。

很巧妙的一点是,由于static静态变量一定有自己的作用域(最大为整个文件),所以我们在不同文件中可以定义同名的静态变量,这两个静态变量虽然都存在于静态存储区,名称相同,但是编译器会认为这是两个不同的变量。

而全局变量不同,它生来就贯穿于程序运行的始终,当在一个项目中时,不同的cpp文件会被链接到一起,如果在某个文件中被定义了,其他cpp文件就不能再定义,否则链接后会报错——重定义。

此时extern的作用显示出来了,因为extern int global;本身是一个声明,int global;是一个定义,不允许全局变量的重定义,但是可以多次声明。

这也是为什么最好不要将全局变量的定义写入头文件,因为当多个cpp文件包含这个头文件时,会出现重定义错误。

说明符和限定符

存储说明符
  • auto
  • register
  • static
  • extern
  • thread_local
  • mutable

其中有一部分已经讲过,同一个声明中不能使用多个说明符,但是thread_local除外,它可以和staticextern结合使用。

其他的部分等之后有时间再细说,这里先埋个坑吧(因为有一些在我这个阶段用处不大)。

限定符const

前面讲到static的时候,提到了链接性。

由于全局变量的外部链接性,它不能够放在头文件中,因为如果头文件被多个cpp文件引用,会出现重定义错误。

但是cosnt却可以放在头文件中声明,原因就是const自带内部链接性,或者说,const声明前面自带static。因此当某个文件引用这个头文件时,const变量被限制在这个cpp文件中,两个文件中的相同的const变量是不同的。

如果希望这个cosnt成为具有外部链接性的常量,则需要在所有cpp文件中使用extern声明,并且只能有一个cpp文件对其进行初始化,或者在头文件中使用extern声明。这和普通的全局变量是不同的,一般的全局变量最原始声明不需要extern

函数和链接性

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

一般来说,函数的链接性是外部的,可以在文件中共享。它的extern是可选的说明符。可以通过static将其链接性改为内部,此时必须在函数声明和函数定义中同时使用static

函数的定义(实现)只能在某一个cpp文件中完成,因此实现不放在头文件中。但是内联函数不受影响,即内联函数的定义可以放在头文件中,此时同一个内联函数的定义必须全部相同。

动态分配

在内存中,程序有三块独立的内存:静态存储区,自动变量区和动态存储区。

动态分配的变量被存放在动态存储区(堆),它不是LIFO(Last In First Out)的,而是根据newdelete的调用实现内存地址的获取和消除。

定位new运算符

new通常的使用方法就是从堆中获取一块内存空间,但是它还有另一个功能,就是指定使用位置,使用时需要包含头文件<new>

比如:

char buffer[500];

int main(){
    int *p1, *p2;
    p1 = new int[10];
    p2 = new (buffer) int[20];
    
    return 0;
}

p1指向系统分配的地址,而p2指向buffer的位置,即在buffer的位置来保存分配的int[20]

此时我们不能通过delete来释放这部分空间,因为它指向的不是动态存储区(堆)。

还要注意的是,我们使用new定位的时候,编译器不会跟踪哪些内存单元被使用,也不会查找未被使用的内存块。

比如:

char buffer[512];

...
int main(){
    ...
    double *pd;
    pd = new (buffer) double[5];
    ...
    pd = new (buffer + 5 * sizeof(double)) double[5]; 
    ...
    return 0;
}

上面的例子中,当我们再次使用定位new运算符时,需要提供一个从buffer开头算起的偏移量,此时才能保证不会覆盖之前的内存空间。

实际上,对于new的不同使用方法就是函数重载:

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

定位new运算符在我这个阶段用处并不是很大,如果不明白可以先跳过。

名称空间

在C++中,当名称变多,或者使用不同类库时,就可能发生名称冲突,此时,名称空间就非常有用。

名称空间的引入提供了一个声明名称的区域,一个名称空间中的名称不会与另一个名称空间的相同名称发生冲突,同时允许程序的其他部分使用该名称空间声明的东西。

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

除了用户定义的名称空间外,还有另一个默认的名称空间——全局名称空间。前面提到的全局变量其实默认就是位于这个名称空间之下。

如果在某个名称空间中提供了函数原型,我们可以在另一个文件中提供函数定义,比如:

namespace Jack{
    void fetch();
}

则我们可以在另一个文件中完成定义:

namespace Jack{
    void fetch(){
        ...
    }
}

访问名称空间中的名称最简单的方法就是通过作用域解析运算符::,这个运算符在类中还会被提及。

比如:

Jack::pail = 12.34;
Jill::fetch();

using声明和using编译指令

先说一下using编译指令,这个是我们最常用的,通过using编译指令可以直接使某个名称空间中的所有名称都可用,比如:

using namespace std;

这是在C++中最常用的了。

一般来说,不允许同时使用两个using声明

using jack::pal;
using jill::pal;
pal = 4;

上面这种方式是错误的。

当我们在使用using编译指令指定名称空间后,如果在它作用范围内的某个位置中定义了重名变量,则局部名称会隐藏名称空间名,比如:

namespace Jill{
    double bucket(double n){...}
    double fetch;
}
char fetch;
int main(){
    using namespace Jill;
    double fetch;
    cin >> fetch;							// #1
    cin >> ::fetch;							// #2
    cin >> Jill::fetch;						// #3
    ...
}

#1使用的是main中的变量fetch#2使用的是全局变量fetch,而#3使用的是Jill名称空间中的fetch

在上面的例子中,如果不使用using编译指令using namespace Jill;而使用using声明:

int main(){
    double fetch;
    cin >> fetch;							// #1
    cin >> ::fetch;							// #2
    cin >> Jill::fetch;						// #3
    ...
}

则会报错。

因为使用using声明时,就好像声明了相应的名称一样,如果某个名称已经声明,则不能用using声明导入相同的名称。

而使用using编译指令时,将进行名称解析,如果某个名称再次被声明,如果没有指定名称空间名,则优先使用局部名称。

名称空间特性

名称空间可以嵌套:

namespace elements{
    namespace fire{
        int flame;
        ...
    }
    ...
}

则可以通过:

using namespace elements::fire;

来使用fire名称空间。

也可以在名称空间中使用using编译指令和using声明:

namespace myth{
    using Jill:fetch;
    using namespace elements;
    using std::cout;
}

此时Jill::fetch现在位于myth名称空间中,因此可以通过:

std::cin >> myth::fetch;

来使用,也可以直接通过Jill::fetch来使用,效果相同。

另外using编译指令是可传递的,在上面的myth名称空间中包含了elements名称空间,则:

using namespace myth;

就相当于:

using namespace myth;
using namespace elements;

也可以通过=来指定名称空间的别名:

namespace my_very_favorite_things{...};

namespace mvft = my_very_favorite_things;

C++允许使用未命名的名称空间,此时相当于在名称空间之后使用了using编译指令,由于没有名称,这种名称空间不能在其他文件中被使用,因此相当于对其中的名称添加了static

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值