C++の策略を書き始める前に,基本的な知識が必要ですが,コンベオ式に精通するのではなく,少なくともこれらのルールを知っておく必要があります. 翻訳はこちらから
プログラマが自称してメモリについて何も知らないとすれば,彼は誇りをしているに違いない,と私はあなたに言うことができます. CやC++でプログラムを書くには,メモリに注意を払う必要があります.これは,メモリの配分が合理的であることがプログラムの効率性や性能に直接影響するだけでなく,より重要なのは,メモリを操作するときに不注意に問題が発生するということです.そして,多くの場合,これらの問題は,メモリ漏れ,例えば,指針の吊り下げのように,容易に検出されません.
C++はメモリを3つの論理領域に分けています: スタック, , 静的記憶領域です. そうなると,それらの間のオブジェクトをスタックオブジェクト,オブジェクト,静的オブジェクトと呼びます. では,これらの異なるメモリオブジェクトの違いは何ですか? スタックオブジェクトとオブジェクトの優劣は? スタックオブジェクトまたはオブジェクトの作成を禁止するにはどうすればよいですか? これらは今日のテーマです.
1 基本的な概念
は,一般的に局所変数やオブジェクトを格納するために用いられる.例えば,関数定義で次のように宣言するオブジェクト:
Type stack_object ;
stack_objectは,定義点から始まり,その関数が返される時に,生命が終了するオブジェクトである.
さらに,ほぼすべての仮オブジェクトはオブジェクトである.例えば,以下の関数定義:
Type fun(Type object);
この関数は少なくとも2つの仮オブジェクトを生成します. まず,パラメータは値で転送されるので,コピーコンストラクション関数を呼び出し,仮オブジェクトobject_copy1を生成します.関数の内部で使用されているのはobjectではなく,object_copy1です.自然,object_copy1はオブジェクトで,関数が返される時に解放されます.また,この関数は値が返されるので,返される値最適化 ((NRV) を考慮しない場合,仮オブジェクトobject_copy2も生成されます.この仮オブジェクトは,関数返された後,しばらくの間解放されます.例えば,以下のコードが関数に含まれています:
Type tt ,result ; //生成两个栈对象
tt = fun(tt); //函数返回时,生成的是一个临时对象object_copy2
上記の2番目の文の実行は,まず,funが返される時に一時的なオブジェクトobject_copy2を生成し,その後に付与演算子を呼び出すことで実行されます.
tt = object_copy2 ; //调用赋值运算符
分かりましたか? コンパイラが私たちの無意識で,私たちのために,これほど多くの臨時オブジェクトを生成しているのに,これらの臨時オブジェクトを生成する時間とスペースのコストは,非常に大きいかもしれません.
次に,stack を見てみましょう. stack は,フリーストレージ領域とも呼ばれ, プログラムが実行する過程で動的に分配されるので,その最大の特徴は動性である. C++ では,すべての stack オブジェクトの作成と破壊はプログラマーによって責任を負うので, 処理が不適切であれば,メモリの問題が発生します. stack オブジェクトを割り当てて,解放するのを忘れると,メモリ漏れが発生します.
では,C++では,スタックオブジェクトをどのように分配するのですか? 唯一の方法は,new ((もちろん,C式スタックメモリを,malloc 命令で取得することもできます) を使用することです.newを使用すると,スタックに1つのメモリが割り当てられ,スタックオブジェクトを指す指針が返されます.
静的記憶領域を見てみよう。すべての静的オブジェクト,全局オブジェクトは静的記憶領域に割り当てられている。全局オブジェクトについては,main () 函数を実行する前に割り当てられている。実際には,main () 函数内の表示コードを実行する前に,コンパイラによって生成された_main () 函数が呼び出される.そして_main () 函数はそのすべての全局オブジェクトの構成と初期化作業を行い,そして main () 函数終了前に,コンパイラによって生成されたexit関数が呼び出される.すべての全局オブジェクトを解放するため。例えば以下のコード:
void main(void)
{
... // 显式代码
}
// 实际上转化为这样:
void main(void)
{
_main(); //隐式代码,由编译器产生,用以构造所有全局对象
... // 显式代码
...
exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象
}
このことを知ると,いくつかのテクニックを導き出せます.例えば, main () 函数を実行する前に,ある準備作業をしたいと仮定すると,その準備作業をカスタム化された全局的なオブジェクトの構成関数に書き込むことができます.そうすると, main () 函数のエクスプレスコードが実行される前に,この全局的なオブジェクトの構成関数が呼び出され,期待された動作を実行し,目的を達成します. 静態記憶区内の全局的なオブジェクトを話していたので,局部静態オブジェクトはありますか?局部静態オブジェクトも通常,オブジェクトの象のように,関数で定義されます.ただし,その前には複数のstaticキーワードがあります.局部静態オブジェクトの寿命は,その関数が最初に呼び出されたときから,より正確には,その静態オブジェクトが最初に実行されたときから,その静態オブジェクトのステティックコードが生成され,そのプログラムが終了するまで,そのオブジェクトは破壊されます.
静的なオブジェクトは,クラスの一つの静的なメンバーとして存在する.この状況を考えると,より複雑な問題が発生します.
第”の問題は,クラスの静的メンバーオブジェクトの生命期であり,クラスの静的メンバーオブジェクトは,最初のクラスオブジェクトの生成とともに発生し,全プログラムの終了時に消滅する.つまり,このような状況が存在する,プログラムの中で,我々は,そのクラスに静的オブジェクトがメンバーとしてあるクラスを定義しているが,プログラムの実行過程で,もし我々がそのクラスオブジェクトのいずれかを作成しなかったならば,そのクラスに含まれる静的オブジェクトは生成されないだろう.また,もし複数のクラスオブジェクトが作成されたならば,これらのオブジェクトはすべてその静的オブジェクトメンバーを共有する.
2つ目の問題は,次の状況が起きたときです.
class Base
{
public:
static Type s_object ;
}
class Derived1 : public Base / / 公共继承
{
... // other data
}
class Derived2 : public Base / / 公共继承
{
... // other data
}
Base example ;
Derivde1 example1 ;
Derivde2 example2 ;
example.s_object = …… ;
example1.s_object = …… ;
example2.s_object = …… ;
上の3つの blackbody と表示された文は,s_object が同じオブジェクトにアクセスしていることに注意してください. 答えはイエスで,それらは同じオブジェクトを指しているようです. 聞こえるのはそうではないですよね? しかし,これは事実です. 簡単なコードを自分で書き,それを検証してください.
Derived1型オブジェクトを引用Base型でない関数を受け取る関数に渡すと切断が起こるとしたら,切断はどうなるのか考えてみましょう.これは,Derived1型オブジェクトのsubobjectを取り除き,Derived1が自定義した他のすべてのデータメンバーを無視して,このsubobjectを関数に渡すだけです (実際には,関数はこのsubobjectのコピーを使用しています).
すべての継承Baseクラスの派生クラスのオブジェクトには,Base型のsubobjectが含まれている (これは,Base型指針でDerived1オブジェクトの鍵を指すことができる,これは自然にも多様的な鍵である),そして,すべてのsubobjectとすべてのBase型オブジェクトは,同じs_objectオブジェクトを共有している.自然に,Baseクラスから派生した全体の継承システムクラスのインスタンスはすべて同じs_objectオブジェクトを共有している.上記のexample,example1,example2のオブジェクトレイアウトは,次の図のように示されている.
2 3種類のメモリオブジェクトの比較
オブジェクトの優点は,適切な時に自動的に生成され,適切な時に自動的に破壊され,プログラマーの心配は不要である.また,オブジェクトの作成速度は,一般的に,堆積オブジェクトよりも速い.なぜなら,堆積オブジェクトを割り当てる時,operator new操作が呼び出される.operator newは,メモリ空間検索アルゴリズムを使用する.この検索プロセスは,時間がかかる場合もあるが,オブジェクトを生成する場合は,それほど面倒ではない.それは,の頂点指針を移動するだけでよい.しかし,注意すべきことは,通常の空間容量は比較的小さいこと,通常は1MB2MBであり,体積が比較的大きいオブジェクトは,で割り当てるのに適さないことである.
スタックオブジェクトは,その生成時刻と破壊時刻をプログラマが精密に定義しなければならない.つまり,プログラマはスタックオブジェクトの生命に完全なコントロールを有している.例えば,複数の関数によってアクセスできるオブジェクトを作成する必要があるが,それを全局的にしたくない場合,スタックオブジェクトを作成して,各関数間でこのスタックオブジェクトの指針を転送して,そのオブジェクトの共有を実現する場合は,スタックオブジェクトの作成は間違いなく良い選択である.また,スタックの容量は,空間に比べて非常に大きい.実際には,物理メモリが不十分である場合,新しいスタックオブジェクトの生成が必要な場合,通常,実行時にエラーが生じません.代わりに,システムは仮想メモリを使用して実際の物理メモリを拡張します.
静的なオブジェクトについて考えてみましょう.
まず,グローバルオブジェクト. グローバルオブジェクトは,クラス間通信と関数間通信の最も簡単な方法を提供していますが,その方法は優雅ではありません. 一般的に,完全にオブジェクト指向の言語では,グローバルオブジェクトは存在しません.例えばC#では,グローバルオブジェクトは不安全で高度な結合を意味し,プログラムでグローバルオブジェクトを過剰に使用すると,プログラムの強度,安定性,保守性,および可用性が大きく低下します.
次に,クラスの静的なメンバーです.上記のように,基層とその派生クラスのすべてのオブジェクトが,この静的なメンバーオブジェクトを共有しているので,これらのクラスまたはこれらのクラスオブジェクト間のデータ共有または通信が必要な場合,このような静的なメンバーは,間違いなく良い選択です.
次に静的な局所オブジェクトがあり,主にそのオブジェクトが関数を繰り返し呼び出す間の中間状態を保存するために使用されます.その中でも最も顕著な例は,帰帰帰関数です.帰帰帰関数は自らを呼び出す関数であることを私たちは知っています.帰帰関数に非静的局所オブジェクトを定義すると,帰帰帰次数がかなり大きい場合,発生する費用も大きいのです.これは,非静的局所オブジェクトがオブジェクトであるため,毎回帰帰呼び出しは,そのようなオブジェクトを生成し,毎回返却は,このオブジェクトを解放します.そして,そのようなオブジェクトは,現在の呼び出し層に限定され,より深い嵌入層とより浅い外露層には見えないのです.各局所オブジェクトには独自のオブジェクトとパラメータがあります.
還元関数設計では,static オブジェクトをnonstatic 局所オブジェクト (すなわちオブジェクト) に代用して使用することができる.これは,各リクエスト呼び出しと返却時に生成および解放されるnonstatic オブジェクトの費用を削減するだけでなく,static オブジェクトはリクエスト呼び出しの中間状態を保存し,各呼び出し層でアクセスできます.
3 の被験者による意外な収穫
前にも紹介した通り,オブジェクトは適切な時に作成され,適切な時に自動的に解放される,つまりオブジェクトには自動管理機能がある. では,オブジェクトが自動的に解放されるのは,何ですか? まず,その生命期間の終わりに; 二つ目は,その関数に異常が発生したときに. あなたは,これは正常で,大したことないと言うかもしれません. はい,大したことないです. しかし,もう少し深く掘り下げれば,意外な収穫があるかもしれません.
オブジェクトは,自動的に解き放たれると,それ自身の解剖関数を呼び出す.もし,オブジェクトにリソースを封じ込め,そしてオブジェクトの解剖関数でリソースを解放する動作を実行すると,資源の漏洩の確率を大幅に減らすことができる,なぜならオブジェクトは,その関数に異常が発生しても,リソースを自動的に解き放つことができるからです.実際のプロセスは,こうです:関数が異常を投げるとき, stack_unw (((inding stack roll back)) と呼ばれることが起こります,つまり,スタックが展開します.オブジェクトは,自然にの中に存在しているため,スタックの回転の過程で,オブジェクトの解剖関数が実行され,その封じ込めのリソースを解放されます.
4 スタックオブジェクトの生成は禁止
上記のように,ある種のスタックオブジェクトの生成を禁止すると, スタック内でのみ生成されるリソースパッケージングクラスを自分で作成して, 異常な状況で自動的にパッケージングされたリソースを解放できます.
スタックオブジェクトの生成は,new操作を使用するだけで,new操作を禁止することはできません.さらに,new操作を実行すると,newオペレーターが呼び出され,newオペレーターはリロードできます.方法があります,newオペレーターをプライベートにする,対称性のために,deleteオペレーターもリロードできます.今,あなたは,オブジェクトを作成するには,newを呼び出す必要はありませんか?
#include <stdlib.h> //需要用到C式内存分配函数
class Resource ; //代表需要被封装的资源类
class NoHashObject
{
private:
Resource* ptr ;//指向被封装的资源
... ... //其它数据成员
void* operator new(size_t size) //非严格实现,仅作示意之用
{
return malloc(size) ;
}
void operator delete(void* pp) //非严格实现,仅作示意之用
{
free(pp) ;
}
public:
NoHashObject()
{
//此处可以获得需要封装的资源,并让ptr指针指向该资源
ptr = new Resource() ;
}
~NoHashObject()
{
delete ptr ; //释放封装的资源
}
};
“NoHashObject”は,次のコードを書くと, スタックオブジェクトを禁止するクラスになります.
NoHashObject* fp = new NoHashObject() ; //コンパイルエラー!
delete fp ;
上記のコードはコンパイルエラーを生成する. さて,あなたは,スタックオブジェクトを禁止するクラスを設計する方法を知っているので,あなたは私と同じ疑問を持っているかもしれません. NoHashObjectの定義が変更できない場合,スタックオブジェクトのそのタイプを生成することはできませんか?
void main(void)
{
char* temp = new char[sizeof(NoHashObject)] ;
//强制类型转换,现在ptr是一个指向NoHashObject对象的指针
NoHashObject* obj_ptr = (NoHashObject*)temp ;
temp = NULL ; //防止通过temp指针修改NoHashObject对象
//再一次强制类型转换,让rp指针指向堆中NoHashObject对象的ptr成员
Resource* rp = (Resource*)obj_ptr ;
//初始化obj_ptr指向的NoHashObject对象的ptr成员
rp = new Resource() ;
//现在可以通过使用obj_ptr指针使用堆中的NoHashObject对象成员了
... ...
delete rp ;//释放资源
temp = (char*)obj_ptr ;
obj_ptr = NULL ;//防止悬挂指针产生
delete [] temp ;//释放NoHashObject对象所占的堆空间。
}
上記の実装は厄介で,実用的にはほとんど使われない,しかし,それを理解することは,C++のメモリオブジェクトを理解するのに有益であるため,私はそれを書き出しました.上記の強制型変換の多くについて,その根本的なことは何ですか?
特定のメモリのデータは不変で,タイプは私たちが着ている眼鏡です.私たちが1つの眼鏡を身に着けると,メモリ内のデータを対応するタイプで解釈します.したがって,異なる解釈は異なる情報を得る.
強制型変換とは,実際に別のメガネを入れ,同じメモリデータを見直すことです.
また,異なるコンパイラがオブジェクトのメンバーデータの配置を異なるように配置することがあります.例えば,ほとんどのコンパイラはNoHashObjectのptrポインタメンバーをオブジェクトのスペースの最初の4バイトに配置します.これは,次の文の変換動作が私たちが期待するように実行されることを保証します.
Resource* rp = (Resource*)obj_ptr ;
しかし,必ずしもすべてのコンパイラがそうではない.
特定の種類のスタックオブジェクトの生成を禁止できるなら, クラスを設計して, オブジェクトの生成を禁止することはできますか? もちろんできます.
5 の生成を禁止する
前述したように,オブジェクトを作成する際には,の適切なサイズのスペースを取るためにのポインタを移動し,このスペースで直接対応するコンストラクション関数を呼び出してオブジェクトを作成します.関数が返ってくると,その解剖関数を使用してこのオブジェクトを解放し,それからのメモリを回収するためにポインタを調整します. このプロセスは, new/delete操作を必要としません,したがって, new/delete操作をプライベートに設定することは目的を達成できません.
そうは言えますし,私もそうするつもりです。しかし,その前に,はっきりと考えるべきことがあります.つまり,もし,コンストラクション関数をプライベートに設定すると,newで直接スタックオブジェクトを生成することもできません.なぜなら,newは,オブジェクトにスペースを割り当てた後に,そのコンストラクション関数も呼び出すからです。だから,私は,分解関数のみをプライベートに設定するつもりです。さらに,分解関数をプライベートに設定すると,のオブジェクト生成を制限する以外に,他の影響があるのでしょうか?はい,これは継承を制限するものです。
クラスが基底クラスを意図しない場合,通常採用される方法は,その解構関数を private として宣言することです.
オブジェクトを制限し,継承を制限しないために,解構関数をprotectedと宣言して,両方がうまくいく. 以下のようなコードで示します.
class NoStackObject
{
protected:
~NoStackObject() { }
public:
void destroy()
{
delete this ;//调用保护析构函数
}
};
このNoStackObjectは,NoStackObjectのクラスで,NoStackObjectのクラスで,
NoStackObject* hash_ptr = new NoStackObject() ;
… … // hash_ptr を指すオブジェクトを操作する
hash_ptr->destroy() ; 面白いことに,newでオブジェクトを作成したのに,deleteで削除するのではなく,destroyで削除する. ユーザは,この奇妙な使い方に慣れていないことは明らかです. だから,私はコンストラクションの関数も,privateまたはprotectedに設定することを決定しました. これは,前に回避しようとした問題に戻ります. newを使わずに,どのようにオブジェクトを生成するか.
class NoStackObject
{
protected:
NoStackObject() { }
~NoStackObject() { }
public:
static NoStackObject* creatInstance()
{
return new NoStackObject() ;//调用保护的构造函数
}
void destroy()
{
delete this ;//调用保护的析构函数
}
};
このNoStackObjectは,NoStackObjectのクラスを使用して,
NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
… … // hash_ptr を指すオブジェクトを操作する
hash_ptr->destroy() ;
hash_ptr = NULL ; // 懸掛ポインタの使用を防止する
作成されたオブジェクトと解放されたオブジェクトの操作は一致している.
多くのCまたはC++プログラマは,ゴミ回収はダイナミックメモリを自分で管理するより確実に低効率であると考え,ゴミ回収は必ずプログラムをそこで停止させるだろうと考え,そして,自己制御のメモリ管理であれば,配分と解放時間は安定し,プログラムを停止させないだろう.最後に,多くのC/C++プログラマは,C/C++でゴミ回収の仕組みを実現することができないと強く信じている.これらの誤った考えは,ゴミ回収のアルゴリズムを理解していないために作り出されたものである.
実際,ゴミ回収の仕組みは遅くないし,動的メモリ分配よりも効率的だ.分担して解放できないので,分担する時には,堆積から新しいメモリを常に取得するだけで,堆積の頂部を移動する指針は十分だ.解放の過程は省略され,自然にもスピードが加速する.現代のゴミ回収のアルゴリズムは,増量収集のアルゴリズムは,ゴミ回収のプロセスを分段的に行かせて,プログラムの動作を妨げないようにしている.従来の動的メモリ管理のアルゴリズムは,適切な時間にメモリを収集する作業も行っているので,ゴミ回収よりも優れているわけではない.
ゴミ回収のアルゴリズムは,通常,現在使用されている可能性のあるすべてのメモリブロックをスキャンしてマークし,既に割り当てられているすべてのメモリから,マークされていないメモリを回収することを基礎とする.C/C++では,ゴミ回収を実現できないという考えは,通常,使用され得るすべてのメモリブロックを正しくスキャンできないという考えに基づいている.しかし,不可能に見えるものは,実際には実現されることは複雑ではない.まずは,メモリデータをスキャンすることで,堆積物に動的に割り当てられたメモリを指すポインタは,簡単に識別され,識別エラーがあった場合,ポインタは,ポインタでないデータにのみポインタされ,ポインタは,ポインタでないデータにポインタされることはありません.こうして,ゴミ回収のプロセスは,誤ったメモリを削除するだけで,誤ったメモリを削除しません.
ゴミ回収の時には,bss段,data段,そして現在使用されているスペースをスキャンして,動的メモリ指針である可能性のある量を見つけ,引用されたメモリをリクエストスキャンして,現在使用されているすべての動的メモリを取得できます.
もし,あなたのプロジェクトに良いゴミ収集器を実装すれば,メモリ管理の速度を上げ,あるいはメモリ消費を減らすことも可能です.興味があれば,ゴミ収集に関するオンラインの論文や実装されたライブラリを検索して,視野を広げることはプログラマーにとって特に重要です.
撮影した写真:HK Zhang
#include<stdio.h>
int*fun(){
int k = 12;
return &k;
}
int main(){
int *p = fun();
printf("%d\n", *p);
getchar();
return 0;
}
ウェブのページをクリックすると,アクセスできるだけでなく,変更することもできます.ただ,アクセスする確率は不明です. 局所変数のアドレスは,プログラム自体のスタック内にあり,権限変数が終了した後に,その局所変数のメモリアドレスを他の変数に与えなければ,その値は存在し続けます.しかし,このメモリアドレスを変更した場合,比較的危険です.このメモリアドレスは,プログラムの他の変数に与えられ,指針で強制的に変更した場合,プログラムクラッシュを引き起こす可能性があります.