本文討論內存方案,會講很多存儲類別,部分內容和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,描述了信息在翻譯單元的多大範圍內可見)
變量的作用域
變量的作用域有多種, 局部,全局(即文件作用域),函數原型作用域, 類作用域,名稱空間作用域。
其中全局作用域是名稱空間作用域的特例
-
局部:只在聲明他的代碼塊中可用
-
全局:從定義位置到文件結尾可用
-
函數原型:包含參數列表的括號內可用,所以是否在原型中聲明變量並不重要
-
在類中聲明的成員的作用域是整個類
-
在名稱空間聲明的變量的作用域是整個名稱空間
函數的作用域
函數的作用域只有兩種:類作用域和名稱空間作用域(包含了全局作用域)。
函數不可以有局部作用域,因爲那樣的話,就不能被別的函數調用,那還有啥用,此外,也不允許在代碼塊內部定義函數。