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,描述了信息在翻譯單元的多大範圍內可見)

變量的作用域

變量的作用域有多種, 局部,全局(即文件作用域),函數原型作用域, 類作用域,名稱空間作用域。

其中全局作用域是名稱空間作用域的特例

  • 局部:只在聲明他的代碼塊中可用

  • 全局:從定義位置到文件結尾可用

  • 函數原型:包含參數列表的括號內可用,所以是否在原型中聲明變量並不重要

  • 在類中聲明的成員的作用域是整個類

  • 在名稱空間聲明的變量的作用域是整個名稱空間

函數的作用域

函數的作用域只有兩種:類作用域和名稱空間作用域(包含了全局作用域)。

函數不可以有局部作用域,因爲那樣的話,就不能被別的函數調用,那還有啥用,此外,也不允許在代碼塊內部定義函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章