C++ day13 内存模型(一)存储持续性、作用域

本文讨论内存方案,会讲很多存储类别,部分内容和C是一样的或者相近,比如作用域,存储持续性,链接,但是也新增了很多东西,比如名称空间,定位new运算符。

这里提供三篇之前的博客,也是讲内存和存储方案的:
内存管理,存储类别,链接(一)

内存管理,存储类别,链接(二)

动态结构,存储类型,数组替代品

C++在内存中存储数据也是很灵活的,你可以选择数据在内存的滞留时间,即存储的持续性或者说是生命周期;还可以控制程序的哪些部分可以访问数据,哪些部分不能访问,也可以用名称空间来控制访问权;还可以用new即时地在运行阶段动态分配内存;定位new运算符是一个新的运算符,也可以用来动态分配内存。

单独编译(组成一个大型程序的每一个翻译单元(一般是源代码文件),单独编译)

听说过混合编译,指的是多种语言一起,比如Python代码可以和C,C++混合编译 ,但是这个单独编译不是混合编译的对立面。

而是指每个翻译单元(比如可以是源代码文件)的单独,一般大型程序都由多个源代码文件组成,这些文件之间还会共享一些数据,这时候就涉及每个文件的单独编译。

后面说翻译单元,一般就是指一个文件哈。

注意C中说了,翻译单元是一个术语看这篇文章,一般是说一个源代码文件以及他所包含的所有文件,但他不一定是指一个文件哈!!因为文件并不是计算机组织信息的唯一方式

C++和C都是鼓励程序员把组件函数都放在独立文件的,不希望你和稀泥垒高墙似的堆在一个源文件里。因为堆在一起的话,可读性不好,组件之间的关系啥的不容易看清,且没法控制访问权。

反正你写在多个文件里,每个文件单独编译后的机器代码会被连接器linker链接为一个可执行程序

但是注意,只有同一个编译器生成的所有函数代码能够保证链接成功,你不要拿不同编译器编译一个程序的不同函数,这样很有可能会链接失败,无法把所有的翻译单元的机器代码链接为一个完整可用的程序。这是因为不同编译器对函数的名称修饰的实现很可能是不同的,前面说过名称修饰了哈,就是编译代码的时候,编译器会为每一个函数名“加密”,比喻说法,本质是把返回类型,参数类型数量都编码,和函数名一起变成一个编码,不同编译器可能用不同方式编码,所以相互可能不认识,就找不到想要的函数模块的机器代码了。

这可是编译内部的原理哦,比较底层的呢,是不是很激动又钻的深一点了呢?原来编译会给每个函数名编码(名称修饰),会把每个翻译单元单独编译为一个代码块,然后链接得到大的程序。

分开放置代码的好处是:更好地组织程序,可以使得大型程序的管理更加便捷。如果你修改了某一个文件,编译器可以只对这一个文件单独重新编译,然后和其他文件的编译后代码链接即可,而无需大家全部回炉重造。这样编译器省事儿,编译速度自然就快

利用头文件,更好地组织程序的同时不添加更多麻烦(和OOP异曲同工)

但是如果你简单地把每个函数放在一个单独文件里,也会造成很多新的麻烦。比如好几个函数都要用同一个结构体,你总不能把这个结构体复制好几遍到这几个函所在的文件吧,那你后面万一要修改结构体,肯定记不得要改好几处。所以这时候,为了把函数分开放,但同时也不自找麻烦,我们就要充分利用头文件了。

把共享的结构体定义,函数原型,一股脑放到头文件里,然后大家需要用的就#include,一句代码万事大吉,要改一起改。
头文件常常包含的
在这里插入图片描述
而所有的源代码文件里放函数定义以及调用函数的代码。

其实OOP也这么组织大型程序:用一个文件包含了用户自定义类型的定义,即函数原型(相当于这里的头文件);用另一个文件写这些方法/函数的定义代码。以前用C写ADT的时候接触过的。

示例 组织程序

这一点我要好好学习一下,再也不写在一个文件里了,以前用python也容易出现程序的组织问题,导致我采用复制函数定义的愚笨方法糊弄解决。

头文件

//coordin.h
#ifndef COORDIN_H_
/*
用预处理器编译指令#ifndef防止重复包含该头文件
(其实实质上没有阻止编译器第二次包含,只是让它在第二次甚至更多次重复包含时,没有真的替换文本)
一般定义头文件里的这个名称时,用大写的文件名加上_H_,这样基本不会和程序的其他变量重名
如果以前没有用#define 定义名称COORDIN_H_才执行#ifndef和#endif之间的语句
把#define用于名称时,不需要后面的替换体,就可完成名称的定义
所以名称是空宏
*/
#define COORDIN_H_

struct rect
{
    double x;
    double y;
};
struct polar
{
    double distance;
    double angle;
};
const double RAD_TO_DEG = 57.29577951;
polar rect_to_polar(rect);
rect polar_to_rect(polar);
void showPolar(polar);
void showRect(rect);
#endif // COORDIN_H_
//main.cpp
//主程序,即main()函数
// 双引号告诉编译器在当前工作目录或者源代码目录中找头文件
#include "coordin.h"

int main()
{
    rect r = {1, 1};
    showRect(r);
    polar p = rect_to_polar(r);
    showPolar(p);
    rect r1 = polar_to_rect(p);
    showRect(r1);
    return 0;
}
//file1.cpp
//这里放函数定义
#include <cmath>
#include "coordin.h"
#include <iostream>

polar rect_to_polar(rect r)
{
    polar p;

    p.distance = sqrt(r.x * r.x + r.y * r.y);
    p.angle = atan2(r.y, r.x);

    return p;
}

rect polar_to_rect(polar p)
{
    rect r;

    r.x = p.distance * cos(p.angle);
    r.y = p.distance * sin(p.angle);
    return r;
}

void showPolar(polar p)
{
    std::cout << "distance: " << p.distance
              << " angle: " << p.angle << std::endl;
}
void showRect(rect r)
{
    std::cout << "horizontal coordinate: " << r.x
              << " vertical coordinate: " << r.y << std::endl;
}

正确的输出

horizontal coordinate: 1 vertical coordinate: 1
distance: 1.41421 angle: 0.785398
horizontal coordinate: 1 vertical coordinate: 1

存储持续性(duration)

C++的存储方式要用存储持续性,作用域和连接性三个概念来描述。

回顾已经知道的知识,以前学的知识没有涉及到名称空间,本文将介绍名称空间对存储方式的影响(主要是影响了访问权)。

注意多线程是把一个程序,或者一个进程分割为多个计算模块,用多个线程实现这些模块的并行运算。
在这里插入图片描述

栈 (编译器对自动变量的实现机制)

自动变量都在函数内部,所以在函数调用的过程中,自动变量会不断增减,所以程序在运行时需要对自动变量的存储进行管理。怎么管呢?由于自动变量不断增多和减少,变化很多,所以栈这种数据结构很适合用来完成自动变量的存储和管理。

栈就像是一个桶,只在一段添加或者删除数据。它是不断把数据堆叠,堆叠,堆叠,新数据堆叠在旧数据的上方,所以名字是stack,堆叠的意思。

我之前在想,可不可以用队列的方式来使用存储自动变量的内存,想了想,觉得大概用是可以的,只是相比于栈,麻烦了一点,毕竟栈只有一端开口(即可以添加和删除),都够这个需求使用了,又何必用两端开口的更灵活强大的队列呢。
而且自动变量还是用先进后出感觉好点。但我没想通先进先出是否会有硬伤。

栈的具体实现,使用了两个指针。
一个指针指向栈底,程序执行的整个期间一直不变,具体指向哪里是编译器指定;
另一个指针指向的当然就是栈顶了,是下一个要存进来的数据的起始地址。
两个指针共同指出了栈的长度。

静态变量(长寿型选手,零初始化)

由于静态变量的数目不会在程序执行期间变化,所以不需要像自动变量那样,用栈的方式使用内存存储他们。而是直接分配固定长度的内存,存起来就好了。当然静态变量使用的内存和自动变量使用的内存是分开的,因为自动变量要用的那段内存是用栈的方式管理。

零初始化,zero-initilized, 指的是所有静态变量没初始化的话,就会被编译器初始化为0.

静态变量的三种链接

注意count虽然在函数内部才能用,但是就算函数执行完了,他还是在内存中,不像自动变量那样被销毁了。
在这里插入图片描述

static 关键字重载(含义取决于上下文)

即他的含义需要取决于上下文

  • 在声明无链接的静态变量时,static强调的是存储持续性
  • 声明内部链接的静态变量时,static又强调的是链接性

这么说auto也用了关键字重载,哈哈

静态变量的初始化(静态初始化 VS 动态初始化)

这两种初始化都是针对静态变量的哈,不要以为动态初始化不能初始化静态变量。

之前了解到静态和动态的区分点是编译,即编译时做的事,比如声明变量等,就是静态的;
编译后,即运行程序时,再声明变量或者分配内存,就是动态的。不要觉得运行时不能分配内存或者声明变量哦,因为你只要写了代码,比如new写的代码,编译为机器码,执行的时候他就会去分配呀。不是只有编译器才可以做那些。

静态初始化分为零初始化常量表达式初始化。即在编译翻译单元时,执行初始化。

动态初始化是在编译后初始化。

三种初始化静态变量的方式的顺序:零初始化—常量表达式初始化(如果可以)—动态初始化(如果需要)。

在这里插入图片描述

特别喜欢这种总结图
在这里插入图片描述

函数的存储持续性都是静态的

即程序的所有函数在程序运行期间都一直存在,不会像变量那样还有自动存储持续性,毕竟你又不可能在一个函数中嵌套定义另一个函数。

作用域(scope,描述了信息在翻译单元的多大范围内可见)

变量的作用域

变量的作用域有多种, 局部,全局(即文件作用域),函数原型作用域, 类作用域,名称空间作用域。

其中全局作用域是名称空间作用域的特例

  • 局部:只在声明他的代码块中可用

  • 全局:从定义位置到文件结尾可用

  • 函数原型:包含参数列表的括号内可用,所以是否在原型中声明变量并不重要

  • 在类中声明的成员的作用域是整个类

  • 在名称空间声明的变量的作用域是整个名称空间

函数的作用域

函数的作用域只有两种:类作用域和名称空间作用域(包含了全局作用域)。

函数不可以有局部作用域,因为那样的话,就不能被别的函数调用,那还有啥用,此外,也不允许在代码块内部定义函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值