注: このブログはブロガーによって一字一句書かれており、簡単ではありませんが、オリジナリティを尊重してください。
プログラムのメモリ分割モデル
1) メモリパーティション
1.1 実行する前に
作成した C プログラムを実行する場合、最初のステップはプログラムをコンパイルすることです。
- 前処理: マクロ定義の展開、ヘッダー ファイルの展開、条件付きコンパイル、ここでは構文チェックは行われません
- コンパイル: 構文をチェックし、前処理されたファイルをアセンブリ ファイルにコンパイルします。
- アセンブリ: アセンブリ ファイルをターゲット ファイル (バイナリ ファイル) に生成します。
- リンク: オブジェクト ファイルを実行可能プログラムにリンクします。
実行可能ファイルをコンパイルして生成した後、linux
undersize
コマンドを使用して、実行可能バイナリ ファイルの基本的な状況を表示できます。 a> a>
上の図からわかるように、プログラムが実行される前、つまりプログラムがメモリにロードされる前 、実行可能プログラム内 3 つの情報はコード領域 (text)、データ領域 (data)、および初期化されていないデータ領域 (bss) に分割されています (データと bss を直接組み合わせて、それをスタティック エリアまたはグローバル エリアと呼ぶ人もいます)。
-
コード領域
には、CPU
によって実行される機械命令が格納されます。通常、コード領域は共有可能です (つまり、他の実行プログラムがコード領域を呼び出すことができます)。コード領域を共有可能にする目的は、頻繁に実行されるプログラムの場合、メモリ内に必要なコードのコピーが 1 つだけになるようにすることです。通常、コード領域は読み取り専用ですが、読み取り専用にする理由は、プログラムが誤って命令を変更することを防ぐためです。さらに、コード領域には、ローカル変数に関する関連情報も計画されています。 -
グローバル初期化データ領域/静的データ領域 (データセクション)
この領域には、プログラム内で明示的に初期化されるグローバル変数および初期化された静的変数 (グローバル静的変数とローカル静的変数を含む) が含まれます。変数)と定数データ(文字列定数など)。 -
初期化されていないデータ領域 (bss 領域とも呼ばれます)
には、初期化されていないグローバル変数と初期化されていない静的変数が保存されます。未初期化データ領域内のデータは、プログラムの実行が開始される前に、カーネルによって0
または空 (NULL
) に初期化されます。
一般に、プログラムのソースコードはコンパイル後、主にプログラム命令(コード領域)とプログラムデータ(データ領域)の 2 種類のセグメントに分かれます。コード セグメントはプログラム命令に属し、データ フィールド セグメントと .bss セグメントはプログラム データに属します。
では、なぜプログラム命令とプログラムデータを分離するのでしょうか?
- プログラムが
load
メモリに移動された後、データとコードを 2 つのメモリ領域にそれぞれマッピングできます。データ領域はプロセスに対して読み書き可能、命令領域はプログラムに対して読み取り専用であるため、分割後、プログラムの命令領域とデータ領域をそれぞれ読み取り、書き込み、または読み取り専用に設定できます。これにより、プログラム命令が意図的または非意図的に変更されるのを防ぎます。 - システム内で同一のプログラムが複数実行されている場合、これらのプログラムによって実行される命令はすべて同じであるため、メモリにプログラム命令のコピーを保存するだけでよく、各プログラム内のデータのみが保存されます。実行中 それはただ違います、これは多くのメモリを節約することができます。たとえば、前の
Windows Internet Explorer 7.0
が実行された後は、112844KB
のメモリを占有する必要があり、そのプライベート部分のデータは約15944KB
になります。また、プログラム内でそのようなプロセスが数百個も実行されている場合、共有方法によりメモリを大幅に節約できると考えられます。96900KB
1.2 走行後
プログラムがメモリにロードされる前にコード領域とグローバル領域 (データと bss) のサイズは固定されています。プログラムの実行中は変更できません。変更してください。次に、実行可能プログラムを実行すると、オペレーティング システムが物理ハードディスク プログラムをメモリにロードします。コード領域 (テキスト)、データ領域 (データ) に加えて、未初期化データ領域 (bss)、スタック領域、ヒープ領域も追加されます。
-
コード領域 (テキスト セグメント)
は、実行可能ファイルのコード セグメントをロードします。すべての実行可能コードはコード領域にロードされます。このメモリは操作中に変更できません。 -
初期化されていないデータ領域 (BSS)
は、実行可能ファイルBSS
セクションをロードします。場所はデータ セクションから離れていても近くても構いません。保存されます。データ セグメント内のデータ (初期化されていないグローバル データ、初期化されていない静的データ) のライフ サイクルは、プログラム実行プロセス全体です。 -
グローバル初期化データ領域/静的データ領域 (
data segment
)
は、実行可能ファイルのデータ セグメントをロードし、データ セグメントに格納します (グローバル初期化、静的初期化データ、リテラル定数 (読み取り専用)) には、プログラム実行プロセス全体のライフサイクルがあります。 -
スタック領域 (スタック)
スタックは先入れ後出しのメモリ構造であり、関数のパラメータ値、戻り値、ローカル変数など。ローカル変数はプログラム実行中にリアルタイムでロードと解放が行われるため、スタック領域の申請と解放がローカル変数のライフサイクルとなります。 -
ヒープ領域 (ヒープ)
ヒープは大きなコンテナです。その容量はスタックの容量よりもはるかに大きくなりますが、先入れ後出しの機能はありません。スタックのように順序付けします。動的メモリ割り当てに使用されます。ヒープは、メモリ内のBSS
領域とスタック領域の間に配置されます。通常、プログラマによって割り当ておよび解放されますが、プログラマが解放しない場合は、プログラム終了時にオペレーティング システムによってリサイクルされます。
タイプ | 範囲 | ライフサイクル | ストレージの場所 |
---|---|---|---|
auto 変数 |
一对{} 内 |
現在の関数 | スタック領域 |
static ローカル変数 |
一对{} 内 |
プログラム実施期間全体 | は data セクションで初期化され、 BSS セクション では初期化されません。 |
extern 変数 |
プログラム全体 | プログラム実施期間全体 | は data セクションで初期化され、 BSS セクション では初期化されません。 |
static グローバル変数 |
現行ファイル | プログラム実施期間全体 | は data セクションで初期化され、 BSS セクション では初期化されません。 |
extern 関数 |
プログラム全体 | プログラム実施期間全体 | コードエリア |
static 関数 |
現行ファイル | プログラム実施期間全体 | コードエリア |
register 変数 |
一对{} 内 |
現在の関数 | CPU 実行時にレジスタに保存されます |
文字列定数 | 現行ファイル | プログラム実施期間全体 | data 一部 |
要約:
スタック領域
1. 先入れ後出し (後入れ先出し)
2. コンパイラがデータの開発とリリースを管理 a>
3. 容量には制限があるため、大量のデータをスタック領域
ヒープ領域
に開かないでください。スタック領域よりもはるかに大きい
2. プログラマーは手動でデータを開き (malloc)、手動でデータを解放します (無料)
2) パーティションモデル
2.1 スタック領域
メモリ管理はシステムによって実行されます。主に関数のパラメータとローカル変数を格納します。関数の実行が完了すると、システムはユーザーの管理なしに自動的にスタック領域メモリを解放します。
スタック領域に関する注意: ローカル変数のアドレスは返さないでください。ローカル変数は関数本体の実行後に解放されます。再度操作すると、違法な操作であり、結果は不明です。
例 1:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int* func03()
{
int a = 10; // 栈上创建的变量,当函数结束后会被释放
return &a;
}
void test13()
{
int* p = func03();
// 因为func03调用结束后,变量a早已被释放,再去操作这块内存就属于非法操作
printf("*p = %d\n", *p); // 第一次打印结果为10,是出于编译器的保护,编译器会认为你误操作
printf("*p = %d\n", *p); // 第二次打印结果就不是10了
}
int main()
{
test13();
system("pause");
return EXIT_SUCCESS;
}
例 2:
char* getString()
{
char str[] = "hello cdtaogang";
return str;
}
void test14()
{
char* p = NULL;
p = getString();
printf("p = %s\n", p);
}
int main()
{
test14();
system("pause");
return EXIT_SUCCESS;
}
2.2 ヒープ領域
はプログラマによって手動で適用され、手動で解放されます。手動で解放しない場合は、プログラムの終了後にシステムによってリサイクルされます。ライフ サイクルは、プログラムの実行期間全体です。 malloc
または new
を使用してヒープを適用します。
ヒープ領域の使用:
例:
int* getSpace()
{
int* p = malloc(sizeof(int) * 5);
for (int i = 0; i < 5; i++)
{
p[i] = 100 + i;
}
return p;
}
void test15()
{
int* p = getSpace();
for (int i = 0; i < 5; i++)
{
printf("p[%d] = %d\n", i, p[i]);
}
// 释放堆区数据
free(p);
p = NULL; // 这一步是避免成为野指针
}
int main()
{
test15();
system("pause");
return EXIT_SUCCESS;
}
ヒープ領域に関する注意: 呼び出し側関数ではヌル ポインタによるメモリの割り当てが行われますが、呼び出される関数ではピア ポインタを使用した割り当て (変更) は失敗します。
例:
void allocateSpace(char* pp)
{
char* temp = malloc(100);
memset(temp, 0, 100);
strcpy(temp, "hello cdtaogang");
pp = temp;
}
void test16()
{
char* p = NULL;
allocateSpace(p);
printf("p = %s\n", p);
}
int main()
{
test16();
system("pause");
return EXIT_SUCCESS;
}
解決策: 高レベル ポインタを使用して低レベル ポインタを変更する
例:
void allocateSpace2(char** pp)
{
char* temp = malloc(100);
memset(temp, 0, 100);
strcpy(temp, "hello cdtaogang");
*pp = temp;
}
void test17()
{
char* p = NULL;
allocateSpace2(&p);
printf("p = %s\n", p);
}
2.3 グローバル/スタティックエリア
グローバル静的領域内の変数にはメモリ領域が割り当てられ、コンパイル段階で初期化されます。このメモリはプログラムの実行中に存在します。主にグローバル変数と静的変数<を保存します。 a i=4> と定数。
注:
(1) 静的ストレージ領域内の変数が明示的に初期化されていない場合、コンパイラは自動的にデフォルトのデータ領域を使用するため、初期化されたデータ領域と初期化されていないデータ領域の間に区別はありません。初期化は静的記憶領域で実行されます。つまり、静的記憶領域には初期化されていない変数はありません。
(2) グローバル静的記憶領域の定数は定数変数と文字列定数に分かれており、一度初期化すると変更することはできません。静的ストレージの定数変数はグローバル変数であり、ローカル定数変数とは異なります。違いは、ローカル定数変数はスタックに格納され、実際にはポインタまたは参照を通じて間接的に変更できるのに対し、グローバル定数変数は静的定数に格納されることです。領域であり、間接的に変更することはできません。
(3) 文字列定数はグローバル/静的記憶領域の定数領域に格納されます。
- 静的変数
サンプル コード: は 1 回だけ初期化され、メモリはコンパイル フェーズ中に割り当てられ、内部リンク属性であり、現在のファイルでのみ使用できます。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 静态变量
static int a = 10; // 特点:只初始化一次,在编译阶段就分配内存,属于内部链接属性,只能在当前文件中使用
void test18() // 局部静态变量,作用域只能在当前test18中
{
// a 和 b的生命周期是一样的
static int b = 20;
}
int main()
{
g_a = 2000; // error g_a默认为内部链接属性,在文件外是无法访问g_a的
system("pause");
return EXIT_SUCCESS;
}
- グローバル変数
サンプル コード:C
言語では、キーワードは外部変数であるグローバル変数extern
の前に隠されます。リンクのプロパティ
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 静态变量
static int a = 10; // 特点:只初始化一次,在编译阶段就分配内存,属于内部链接属性,只能在当前文件中使用
void test18() // 局部静态变量,作用域只能在当前test18中
{
// a 和 b的生命周期是一样的
static int b = 20;
}
// 全局变量
extern int c = 100; //在C语言下 全局变量前都隐藏加了关键字 extern,属于外部链接属性
void test19()
{
extern int g_b;//告诉编译器 g_b是外部链接属性变量,下面在使用这个变量时候不要报错
printf("g_b = %d\n", g_b);
}
int main()
{
//g_a = 2000; // error g_a默认为内部链接属性,在文件外是无法访问g_a的
test19();
system("pause");
return EXIT_SUCCESS;
}
- 絶え間ない
サンプル コード:const
によって変更されたグローバル変数は、構文が合格した場合でも、実行時に定数領域によって保護され、実行に失敗します。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//1、const修饰的全局变量,即使语法通过,但是运行时候受到常量区的保护,运行失败
const int a1 = 10; //放在常量区
void test20()
{
//a1 = 100; //直接修改 失败
// 间接修改,语法通过,但运行失败
int* p = &a1;
*p = 100;
printf("%d\n", a1);
}
int main()
{
test20();
system("pause");
return EXIT_SUCCESS;
}
サンプル コード:const
変更されたローカル変数。データはスタック領域に格納されます。C
は言語では擬似定数と呼ばれ、定数領域で保護されていません
//2、const修饰的局部变量
void test21()
{
const int b = 10; // 数据存放在栈区,C语言下称为伪常量
// b = 100; // 直接修改失败的
// 间接修改成功,分配到栈上,没有常量区保护
int* p = &b;
*p = 100;
printf("b = %d\n", b);
//int a[b]; // 伪常量是不可以初始化数组的
}
int main()
{
test21();
system("pause");
return EXIT_SUCCESS;
}
サンプル コード:文字列定数は共有できますが、文字列定数の変更は許可されません
//3、字符串常量
void test22()
{
char* p1 = "hello cdtaogang";
char* p2 = "hello cdtaogang";
char* p3 = "hello cdtaogang";
// 字符串常量是可以共享的
printf("%d\n", p1);
printf("%d\n", p2);
printf("%d\n", p3);
printf("%d\n", &"hello cdtaogang");
p1[0] = 'b'; // 不允许修改字符串常量
printf("%c\n", p1[0]);
}
int main()
{
test22();
system("pause");
return EXIT_SUCCESS;
}
文字列定数は変更可能ですか?文字列定数の最適化:
ANSI C では、文字列定数が変更された場合、結果は未定義であると規定されています。 ANSI C では、コンパイラ実装者による文字列の処理方法は規定されていません。例:
1. 文字列定数を変更できるコンパイラもあれば、文字列定数を変更できないコンパイラもあります。
2. コンパイラによっては、複数の同一の文字列定数を 1 つとして扱います (この最適化はスペースを節約するために文字列定数に現れる場合があります)。また、一部のコンパイラはこの最適化を実行しません。最適化を実行すると、1 つの文字列定数を変更すると他の文字列定数も変更される可能性があり、結果は不明です。
したがって、文字列定数は変更しないようにしてください。
文字列定数のアドレスは同じですか?
TC2.0 では、同じファイル文字列定数アドレスが異なります。
VS2013 以降では、文字列定数アドレスは同じファイルでも異なるファイルでも同じです。
Dev C++ と QT は、同じファイル内では同じですが、ファイルが異なると異なります。
2.4 概要
メモリ パーティションを理解するC/C++
場合、データ領域、ヒープ、スタック、静的領域、定数領域、グローバル領域、文字列定数領域、リテラル定数領域、コード領域など、初心者は混乱します。ここで、上記のパーティション間の関係を明確にしてみます。
データ領域には、 ヒープ、スタック、グローバル/静的ストレージ領域が含まれます。
グローバル/静的ストレージ領域には、 定数領域、グローバル領域、静的領域が含まれます。
定数領域には、 文字列定数領域と定数変数領域が含まれます。
コード領域: プログラムのコンパイル済みバイナリ コードを格納します。これはアドレス指定できない領域です。
実際には、C/C++ にはコード領域とデータ領域の 2 つのメモリ パーティションしか存在しないと言えます。
3) 関数呼び出しモデル
3.1 マクロ機能
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MYADD(x, y) x+y
// 运算保证完整性
#define MYADD2(x, y) ((x)+(y))
// 在预编译阶段做了宏替换
// 宏函数注意:保证运算的完整性
// 宏函数使用场景:将频繁短小的函数,封装为宏函数
// 优点:以空间换时间(入栈和出栈的时间)
void test23()
{
int a = 10;
int b = 20;
printf("a + b = %d\n", MYADD(a, b)); // x+y == a+b == 10+20 = 30
printf("a + b = %d\n", MYADD(a, b) * 20); // x+y*20 == a+b*20 == 10+20*20 == 410
// 运算保证完整性
printf("a + b = %d\n", MYADD2(a, b)); // 30
printf("a + b = %d\n", MYADD2(a, b) * 20); // 600
}
int main()
{
test23();
system("pause");
return EXIT_SUCCESS;
}
3.2 関数呼び出し処理
スタック (stack
) は、現代のコンピュータ プログラムにおいて最も重要な概念の 1 つです。ほとんどすべてのプログラムがスタックを使用します。スタックがなければ、関数もローカル変数も存在しません。つまり、今日私たちが目にするすべてのコンピューターに対応する言語は存在しませんでした。スタックがなぜそれほど重要なのかを説明する前に、まずスタックの従来の定義を理解しましょう。
古典的なコンピュータ サイエンスでは、スタックは特別なコンテナとして定義されます。ユーザーはデータをスタックにプッシュするか (プッシュ、push
)、スタックにデータをプッシュできます。スタックはポップされます (ポップ、pop
) が、スタック コンテナーはルールに従う必要があります: 最初にスタックにプッシュされたデータが最後にポップされます (First In Last Out,FILO
)
従来のオペレーティング システムでは、スタックは常に下方向に成長します。プッシュ操作ではスタックの先頭のアドレスが減少し、ポップ操作ではスタックの先頭のアドレスが増加します。
スタックはプログラムの実行において非常に重要な役割を果たします。最も重要なことは、スタックには、関数呼び出しのために保持する必要がある情報が保持されていることです。これは、スタック フレーム (Stack Frame
) またはアクティビティ レコード (Activate Record
) と呼ばれることがよくあります。 . 関数 呼び出しプロセスに必要な情報には、通常、次の側面が含まれます。
- 関数の戻りアドレス。
- 関数パラメータ。
- ローカル変数。
- 保存されたコンテキスト: 関数呼び出しの前後で変更しない必要があるレジスタが含まれます。
以下のコードから、次の関数の呼び出しプロセスを分析します。
int func(int a,int b){
int t_a = a;
int t_b = b;
return t_a + t_b;
}
int main(){
int ret = 0;
ret = func(10, 20);
return EXIT_SUCCESS;
}
考え方 1:a、b
変数をスタックにプッシュするとき、左から右にプッシュするべきですか、それとも右から左にプッシュするべきですか?
考え方 2: a、b
変数は main
関数 (主要な呼び出し関数) によって管理および解放されるのか、または func
関数(関数と呼ばれる)管理リリース?
3.3 呼び出し規約
さて、関数呼び出しのプロセスは大体理解できましたが、この間、関数の呼び出し元と呼び出される側が関数呼び出しについて一貫した理解を持っているという現象が起こります。関数のは、固定された方法でスタックにプッシュされた特定の関数に基づいています。そうしないと、関数は正しく実行されません。
関数の呼び出し元がパラメータを渡すときに最初に a
パラメータをプッシュし、次に b
パラメータをプッシュすると、呼び出される関数は最初にプッシュされる プッシュされるのは b
で、後からプッシュされるのは a
です。その後、呼び出された関数が a,b
とすると逆になります。
したがって、関数の呼び出し元と呼び出し先は、関数の呼び出し方法について明確に合意する必要があります。両者が同じ合意に従う場合にのみ、関数を正しく呼び出すことができます。呼び出し。このような規則は「呼び出し規則」 と呼ばれ、呼び出し規則には通常次の側面が含まれます。
関数パラメータを渡す順序と方法
関数を渡す方法はたくさんありますが、最も一般的なのはスタックを使用する方法です。関数の呼び出し元はパラメーターをスタックにプッシュし、関数自体がスタックからパラメーターを取り出します。複数のパラメーターを持つ関数の場合、呼び出し規約により、関数の呼び出し元がパラメーターをスタックにプッシュする順序 (左から右、または右から左) が決まります。一部の呼び出し規則では、パフォーマンスを向上させるために、レジスタを使用してパラメータを渡すこともできます。
スタック メンテナンス メソッド
関数がパラメータをスタックにプッシュした後、関数本体が呼び出されます。その後、スタックにプッシュされたすべてのパラメータをポップアウトする必要があるため、スタックをスタック内に保持できること、関数呼び出しの前後で一貫性があること。このポップアップ作業は、関数の呼び出し元または関数自体によって実行できます。
リンク時に呼び出し規約を区別するために、呼び出し規約は関数自体の名前を変更します。呼び出し規約が異なれば、名前変更方法も異なります。
実際、c
言語には複数の呼び出し規則があり、デフォルトは cdecl
です。呼び出し規則を明示的に指定しないものはどれも規則 デフォルトでは、関数はすべて cdecl
規則です。たとえば、上記の func
関数の宣言の場合、完全な記述は次のようになります。
int _cdecl func(int a,int b);
注: _cdecl
は標準キーワードではないため、コンパイラによって書き方が異なる場合があります。たとえば、gcc
は存在しません_cdecl
ではなく、__attribute__((cdecl))
を使用してください。
呼び出し規約 | 離脱パーティー | パラメータの受け渡し | 名前の変更 |
---|---|---|---|
cdecl |
関数の呼び出し元 | パラメータを右から左にスタックにプッシュします | アンダースコア + 関数名 |
stdcall |
機能そのもの | パラメータを右から左にスタックにプッシュします | アンダースコア + 関数名 + @ + パラメータのバイト数 |
fastcall |
機能そのもの | 最初の 2 つのパラメータはレジスタに渡され、残りのパラメータはスタックに渡されます。 | @+関数名+@+パラメータのバイト数 |
pascal |
機能そのもの | パラメータを左から右にスタックにプッシュします | さらに複雑な場合は、関連ドキュメントを参照してください |
3.4 関数変数伝達解析
簡単な例:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void B()
{
}
void A()
{
int b; // 在函数A、B中可以使用,但在main函数中使用不了
B();
}
int main()
{
int a; // 在main函数和A、B函数中都可以使用(传参)
A();
system("pause");
return EXIT_SUCCESS;
}
4) スタックの成長方向とメモリの格納方向
4.1 スタックの成長方向
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 栈的生长方向
void test24()
{
int a = 10; // 栈底-高地址
int b = 20;
int c = 30;
int d = 40; // 栈顶-低地址
printf("%d\n", &a);
printf("%d\n", &b);
printf("%d\n", &c);
printf("%d\n", &d);
}
int main()
{
test24();
system("pause");
return EXIT_SUCCESS;
}
4.2 メモリの格納方向
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 内存存放方向
void test25()
{
int a = 0x11223344;
char* p = &a;
// 小端结果,大端相反
printf("%x\n", *p); // 44 - 低位字节 - 低地址
printf("%x\n", *(p+1)); // 33 - 相对44 高位字节 - 高地址
printf("%x\n", *(p + 2)); // 22
printf("%x\n", *(p + 3)); // 11
}
int main()
{
test25();
system("pause");
return EXIT_SUCCESS;
}