「C++ プログラミングの原則と実践」ノート 第 17 章 ベクターおよび無料ストレージ

この章と次の 4 章では、C++ 標準ライブラリ (STL と呼ばれることが多い) のコンテナ部分とアルゴリズム部分について説明します。この章と次の 2 章では、最も一般的に使用され便利な STL コンテナであるベクターの設計と実装に焦点を当てます。

17.1 はじめに

C++ 標準ライブラリで最も便利なコンテナは ですvectorvector指定されたタイプの要素のシーケンスを提供します。標準ライブラリはvector、便利で柔軟、(時間と空間) 効率が高く、静的にタイプセーフな要素コンテナーです。

この章と次の 2 章では、基本的な言語機能に基づいて構築する方法を示しvector、便利な概念とプログラミング手法を示し、C++ 言語機能を通じてそれらを表現します。私たちの実装で遭遇する言語機能とプログラミング技術vectorは汎用的であり、広く使用されています。

私たちは、一連の段階的により複雑な実装を通じてvector標準ライブラリにアプローチしますvector

17.2 ベクトルの基本

まずvector非常に単純な使用法を考えてみましょう。

vector<double> age(4); // a vector with 4 elements of type double
age[0]=0.33;
age[1]=22.0;
age[2]=27.2;
age[3]=54.2;

このコードは、double型の 4 つの要素を持つベクトルを作成し、値 0.33、22.0、27.2、および 54.2 を 4 つの要素に割り当てます。これら 4 つの要素には 0、1、2、3 の番号が付けられます。C++ 標準ライブラリ コンテナ内の要素の番号付けは常に 0 から始まります。ベクトル内の要素の数はsizeと呼ばれ、要素番号 (インデックス) の範囲は 0 から size-1 までです。それは次の図で表すことができますage

ベクトル図

この「グラフィックデザイン」はどのようにしてコンピュータのメモリに実装できるのでしょうか? vector明らかに、サイズを格納するデータ メンバーと要素を格納するデータ メンバーを取るクラスを定義する必要があります。しかし、可変数の要素のセットをどのように表現するのでしょうか? 最初の要素のメモリ アドレスが必要です。C++では、アドレスを格納できるデータ型をポインタと呼びます

vectorしたがって、クラスの独自の最初のバージョンを定義できます。

// a very simplified vector of doubles
class vector {
    
    
    int sz;        // the size
    double* elem;  // pointer to the first element (of type double)
public:
    vector(int s); // constructor: allocate s doubles,
                   // let elem point to them
                   // store s in sz
    int size() const {
    
     return sz; }  // the current size
};

設計する前にvector、まず「ポインタ」とそれに密接に関連する「配列」の概念を学びます。これは、C++ における「メモリ」の概念の鍵です。

17.3 メモリ、アドレス、およびポインタ

コンピュータのメモリは一連のバイトであり、バイトには 0 から始まる番号が付けられます。メモリの場所を示すために使用される番号はアドレスと呼ばれますアドレスは基本的には整数です。たとえば、1 MB (= 2 10 KB = 2 20 B) のメモリは、次のグラフとして視覚化できます。

メモリ図

それぞれの四角はバイト(記憶単位)を表しており、そのバイトの番号(アドレス)は、0、1、2、…、2 20 -1となります。

メモリ内のすべてのオブジェクトにはアドレスがあります。例えば:

int var = 17;

このステートメントは、 「size」(通常は 4 バイト) のメモリ空間varを に割り当てint、値 17 をこのメモリに置きます。アドレスを保持するオブジェクトはポインターと呼ばれ構文的にはT*Tポインターからポインター」となります。例えば:

int* ptr = &var;  // ptr holds the address of var

演算子のアドレスは、&オブジェクトのアドレスを取得するために使用されます。ここに へのポインタがptrあり、その値はのアドレスです。アドレスが 4096 であるとすると、 4096 の値は次のようになります。varvarvarptr

ポインタ図

int注: 1 つが4 バイトを占めると仮定すると、var実際には 4096 ~ 4099 個の連続した 4 バイトを占め、オブジェクトのアドレスは最初のバイトのアドレスになります。

逆参照演算子は、*ポインターが指すオブジェクトにアクセスするために使用されます例えば:

cout << "pi==" << pi << "; contents of pi==" << *pi << "\n";

考えられる出力は次のとおりです

pi==0x649ff8b4; contents of pi==17

piの値は、コンパイラがメモリ内の変数にvar割り当てるアドレスによって異なります (プログラムが実行されるたびに変化します)。ポインタの値(アドレス)は通常16進数で表現されます。

逆参照演算子は、代入演算子の左側 (piポインター自体、*piおよびポインターが指すオブジェクト) で使用することもできます。

*pi = 27;  // OK: you can assign 27 to the int pointed to by pi

ポインターの値は整数として出力できますが、ポインターは整数ではないためint、この 2 つを混同しないように注意してください。

int i = pi;  // error: can't assign an int* to an int
pi = 7;     // error: can't assign an int to an int*

異なる型のポインタを混在させることもできません。たとえば、へのcharポインタは以下を指すことはできませんint

char* pc = pi;  // error: can't assign an int* to a char*
pi = pc;        // error: can't assign a char* to an int*

参照: 「C プログラミング言語」の注記 第 5 章 ポインタと配列

17.3.1 sizeof 演算子

sizeofオブジェクトまたは型のサイズ (メモリのバイト単位) を取得する演算子:

void sizes(char ch, int i, int* pi) {
    
    
    cout << "the size of char is " << sizeof(char) << ' ' << sizeof(ch) << '\n';
    cout << "the size of int is " << sizeof(int) << ' ' << sizeof(i) << '\n';
    cout << "the size of int* is " << sizeof(int*) << ' ' << sizeof(pi) << '\n';
}

sizeof型名または式に使用できます。sizeof結果は、単位の正の整数sizeof(char)(1 として定義) になります。

それを試してみてください

C++ の基本型の長さを次の表に示します。

タイプ 長さ ポインタ型 ポインタの長さ
bool 1 bool* 4または8
char 1 char* 4または8
short 2 short* 4または8
int 2 または 4 int* 4または8
long 4または8 long* 4または8
long long 8 long long* 4または8
float 4 float* 4または8
double 8 double* 4または8

注記:

  • C++ 標準では、int型のサイズは少なくとも 2 バイトであり、マシンによっては 2 バイトまたは 4 バイトになる可能性があり、同様に、long型の長さは 4 バイトまたは 8 バイトになる可能性があります。詳細については、「基本型」セクションの「整数型」を参照してください。
  • int32_t標準ライブラリ ヘッダー ファイル <cstdint> は、 、int64_tなどの固定幅の整数型 (エイリアス) のセットを定義します。
  • ポインタ型の場合、32 ビット システムでは 4 バイト、64 ビット システムでは 8 バイトです (ポインタのサイズはメモリ アドレス空間のサイズにのみ関係し、メモリ アドレス空間のサイズとは関係ありません)。実際のタイプ)。

vectorどれくらいのメモリが必要ですか? 試す

cout << "the size of vector<int>(10) is " << sizeof(vector<int>(10)) << '\n';
cout << "the size of vector<int>(100) is " << sizeof(vector<int>(100)) << '\n';
cout << "the size of vector<int>(1000) is " << sizeof(vector<int>(1000)) << '\n';

として出力

the size of vector<int>(10) is 24
the size of vector<int>(100) is 24
the size of vector<int>(1000) is 24

この説明はこの章と次の章を読むと明らかになりますが、明らかにsizeofベクトル要素はカウントされません。

注:vectorオブジェクト自体には、サイズや要素へのポインターなどのメンバーのみが含まれており、要素はvectorオブジェクトの一部ではありません。そうでない場合、vectorオブジェクトのサイズは不確かになります。

17.4 空きストレージとポインタ

C++ プログラムのメモリ レイアウトを次の図に示します。

メモリレイアウト

メモリ空間全体は次の 4 つの部分に分割されます。

  • コードセグメント: テキスト セグメントとも呼ばれ、コードのコンパイルによって生成された機械命令が含まれます。
  • データ セグメント(データ セグメント): 静的ストレージ (静的ストレージ) とも呼ばれ、グローバル変数と静的変数が含まれます。
  • スタック(スタック): 自動ストレージ (自動ストレージ) とも呼ばれ、ローカル変数と関数パラメーターが含まれます。
  • ヒープ:空きストレージまたは動的メモリとも呼ばれnew演算子を通じて動的に割り当てることができるメモリ。

参考:Memory Layout of C Programs

注: スタック メモリ内の変数はスコープから外れるときに自動的に解放されますが、ヒープ メモリは手動で解放する必要があります。解放しないとメモリ リークが発生します。

例えば:

double* p = new double[4];  // allocate 4 doubles on the free store

次の図に示すように、このステートメントはdouble空きストレージに 4 つのメモリ スペース (32 バイト) を割り当て、最初のアドレス (850 と想定) へのポインタを返します。

無料のストレージ割り当て

17.4.1 空きストレージの割り当て

newオペレーターは空きストレージからメモリーを割り当て、最初のバイトのアドレス (作成するオブジェクトまたは配列の最初の要素へのポインター) を返します。オブジェクトの型が の場合T、返されるポインタの型は ですT*

new演算子は、単一のオブジェクトまたは配列にメモリを割り当てることができます。例えば:

int* pi = new int            // allocate one int
int* qi = new int[4];        // allocate 4 ints (an array of 4 ints)

double* pd = new double;     // allocate one double
double* qd = new double[n];  // allocate n doubles (an array of n doubles)

割り当てられる要素の数は可変であることに注意してください。これにより、実行時に割り当てるオブジェクトの数を指定できます。

17.4.2 ポインタによるアクセス

ポインターで逆参照演算子を使用することに加えて*、読み取りと書き込みの両方に使用できる添字演算子も使用できます[]例えば:

double* p = new double[4];  // allocate 4 doubles on the free store
double x = *p;              // read the (first) object pointed to by p
double y = p[2];            // read the 3rd object pointed to by p

*p = 7.7;                   // write to the (first) object pointed to by p
p[2] = 9.9;                 // write to the 3rd object pointed to by p

ポインターで使用される場合、[]演算子はメモリーを一連のオブジェクト (ポインター宣言で指定されたタイプ) として扱います。p[i]に相当します*(p+i)

[]注:演算子の使用は、ポインタが配列内の要素を指している場合にのみ意味があります。

17.4.3 範囲

ポインターの主な問題は、ポインターが自分が指している要素の数を「知らない」ことです (単一のオブジェクトを指しているのか、配列内の要素を指しているのかさえもわかりません)。例えば:

double* p = new double;        // allocate a double
double* q = new double[1000];  // allocate 1000 doubles
q[700] = 7.7;       // fine
q = p;              // let q point to the same as p
double d = q[700];  // out-of-range access!

このうち2 番目q[700](すなわち) は範囲外アクセスで、このメモリ位置は別のオブジェクトの一部である可能性があります。境界外アクセスは典型的な致命的なエラーです。「プログラムが不可解にクラッシュする」または「プログラムが間違った出力を行う」という形式の「致命的な」エラーです。特に迷惑なのが、外部からのアクセスです。境界外の読み取りでは、まったく無関係なオブジェクトに応じて「ランダムな」値が取得され、境界外の書き込みでは、オブジェクトに予期しない間違った値が与えられる可能性があります。p[700]p[700]

このような範囲外のアクセスが発生しないようにする必要があります。vector直接ではなくnew割り当てられたメモリを使用する理由の 1 つvectorは、そのサイズを把握して、境界外のアクセスを簡単に回避できるようにするためです。

17.4.4 初期化

ポインタとそれが指すオブジェクトの両方が初期化されていることを確認したいと考えています。考慮する:

double* p0;                    // uninitialized: likely trouble
double* p1 = new double;       // get (allocate) an uninitialized double
double* p2 = new double(5.5);  // get a double initialized to 5.5
double* p3 = new double[5];    // get (allocate) 5 uninitialized doubles

初期化されていないポインタは、上記のようなワイルド ポインタですp0その値はランダムであるため、初期化されていないポインターを使用すると、境界外アクセスが発生する必要があります。

組み込み型の場合、new割り当てられたメモリは初期化されていませp1()または を使用して{}初期化できますp2

new作成された配列もデフォルトでは初期化されません。初期化リストを指定できます (この場合、要素の数は省略できます)。

double* p4 = new double[5]{
    
    0, 1, 2, 3, 4};
double* p5 = new double[]{
    
    0, 1, 2, 3, 4};

注: 要素数が指定されているが、初期化リストの初期値の数が要素数より少ない場合、残りの要素はデフォルトで初期化されます。

カスタム型の場合、初期化をより細かく制御できます。型にデフォルトのコンストラクターがある場合、初期値が指定されていない場合はデフォルトのコンストラクターが使用されます

X* px1 = new X;      // one default-initialized X
X* px2 = new X[17];  // 17 default-initialized Xs

型にコンストラクターはあるが、デフォルトのコンストラクターがない場合は、明示的に初期化する必要があります

Y* py1 = new Y;      // error: no default constructor
Y* py2 = new Y(13);  // OK: initialized to Y(13)
Y* py3 = new Y[17];  // error: no default constructor
Y* py4 = new Y[17]{
    
    0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};

初期値リストは、配列の要素が少数の場合に便利です。

T注: このことから、カスタム型にデフォルトのコンストラクターがない場合、すべての要素を初期化できないため、それを使用して可変長の動的配列をnew T[n]作成できないことがわかります。n

17.4.5 Null ポインタ

ポインタを初期化するために使用できる値がない場合は、null ポインタが使用されますnullptr

double* p = nullptr;  // the null pointer

null ポインタの値は 0 で、通常はポインタが有効かどうか (たとえば、何らかのオブジェクトをポイントしているかどうか) をテストするために使用されます。たとえば、 は と同等if (p != nullptr)ですif (p)このテストは、初期化されていないポインターに対しては無意味であることに注意してください。

注記:

  • Null ポインターは、「見つからない」または「エラーが発生した」などの例外的な状況を示すために関数の戻り値で使用できます。
  • nullptrは C++11 で導入されたキーワードで、暗黙的に任意の型のポインターに変換できます。古いコードでは、ヌル ポインターを表すために0または を使用します。NULL

17.4.6 無料ストアリリース

コンピュータのメモリは有限であるため、空きストレージが新しい割り当てにメモリを再利用できるように、使用後にメモリを空きストレージに解放する必要があります。この再利用のためのメモリの解放は、大規模なプログラムや長時間実行されるプログラム (オペレーティング システム、組み込みシステムなど) にとって重要です。

use で割り当てられたメモリが解放されず、そのアドレスを保持するポインタの範囲外に出ると、メモリ リークnewが発生します(後でメモリを解放する機会がないため)。例えば:

double* calc(int res_size, int max) {
    
    
    double* p = new double[max];  // leaks memory
    double* res = new double[res_size];
    // use p to calculate results to be put in res
    return res;
}

double* r = calc(100,1000);

各呼び出しは、割り当てられた配列calc()によって占有されているメモリを「リーク」します(呼び出し元がそれを解放しない場合、メモリ リークも発生します)。pdoubler

メモリを解放して空きストレージに戻す演算子は、返されたポインタをdelete対象とします。new2 つの形式がありますdelete

  • delete pnew単一のオブジェクトによって割り当てられたメモリを解放します。
  • delete[] pnew配列によって割り当てられたメモリを解放します。

上記の例は次のように変更されます

double* calc(int res_size, int max) {
    
    
    // the caller is responsible for the memory allocated for res
    double* p = new double[max];
    double* res = new double[res_size];
    // use p to calculate results to be put in res
    delete[] p;  // we don't need that memory anymore: free it
    return res;
}

double* r = calc(100,1000);
// use r
delete[] r;  // we don't need that memory anymore: free it

この例は、空きストレージを使用する主な理由の 1 つを示しています。関数内でオブジェクトを作成し、それを呼び出し元に返すことができます (値のコピーを避けるため)。

注記:

  • カスタム タイプの場合は、オブジェクト/要素のデストラクターが自動的に呼び出されますdeletedelete[]
  • 関数がnew呼び出し元から返されたポインターを返す場合、そのメモリを解放するのは呼び出し元の責任です。解放しないとメモリ リークが発生します。
  • C++11 ではスマート ポインターが導入されshared_ptrunique_ptrヘッダー ファイル <memory> で定義されたメモリを自動的に解放できます (セクション 19.5.4 を参照)。
  • C++17 では、コピー排除(コピー省略)/戻り値最適化 (戻り値最適化、RVO) 機能が導入されています。特定の状況下では、関数の戻り値が値型であっても、値のコピーは発生しません。

オブジェクトを 2 回削除すると、重大なエラーになります。例えば:

int* p = new int(5);
delete p;  // fine: p points to an object created by new
// ... no use of p here ...
delete p;  // error: p points to memory owned by the free-store manager

2回目には、指定されたメモリを所有していなくなり、空きストレージ マネージャーがそのメモリを再利用して他のオブジェクトに割り当てた可能性があり、(プログラムの他の部分が所有する) オブジェクトを削除するとプログラム エラーが発生しますdelete pp

NULL ポインターを削除しても何も行われないため、NULL ポインターを削除しても無害です。

実際、コンパイラは、メモリの一部が不要になったことを認識し、人間の介入なしにそれを再利用することができます。これは自動ガベージ コレクション(ガベージ コレクション、GC) と呼ばれます。ただし、自動ガベージ コレクションは無料ではなく、すべてのアプリケーションに最適なわけでもありません。C++ では、(new空きストレージを使用して) プログラマー自身が「ゴミ」を処理しなければなりません。

注記:

  • Java や Python などの言語には、メモリを自動的に管理できるガベージ コレクターが組み込まれています。
  • C++ では、標準ライブラリ コンテナーやスマート ポインターなどのツールを使用して、ガベージ コレクションを簡素化できます。

17.5 デストラクター

vectorセクション 17.2 のコンストラクターは、フリーストア割り当てを使用して実装できるようになりました。

vector(int s) :sz(s), elem(new double[s]{
    
    0}) {
    
    }

コンストラクターはすべてを 0 に割り当ててs初期double化します。

ただし、vectorコンストラクターで使用してnew割り当てられたメモリが解放されないため、メモリ リークが発生します。考慮する:

void f(int n) {
    
    
    vector v(n);  // allocate n doubles
    // ... use v ...
}

関数f()が戻ったときにv、空きストレージに割り当てられたメモリが解放されませんでした。vector1 つを定義しclean_up()て呼び出すことができます。

void f2(int n) {
    
    
    vector v(n);   // allocate n doubles
    // ... use v ...
    v.clean_up();  // clean_up() deletes elem
}

これは可能です。しかし、無料ストレージに関する最も一般的な問題の 1 つは、人々が忘れてしまうことですdeleteclean_up()同じ問題: 人々はそれを呼び出すことを忘れる可能性があります。より良いアプローチは、コンストラクターの逆を行う関数、つまりデストラクターをコンパイラーに認識させることです(セクション 9.4.2 で説明されているように、イニシャライザーの呼び出し忘れを避けるためにコンストラクターを使用します)。コンストラクターはクラスのオブジェクトが作成されるときに暗黙的に呼び出され、デストラクターはオブジェクトがスコープ外に出るときに暗黙的に呼び出されますコンストラクターはオブジェクトが適切に作成および初期化されることを保証しますが、デストラクターはオブジェクトが適切に破棄されることを保証します(所有されている「リソース」を解放します)。

クラスTのデストラクターには という名前が付けられます~T例えば:

// destructor: free memory
~vector() {
    
    
    delete[] elem;
}

これにより、次のように書くことが可能になります。

void f3(int n) {
    
    
    double* p = new double[n];  // allocate n doubles
    vector v(n);  // the vector allocates n doubles
    // ... use p and v ...
    delete[ ] p;  // deallocate p's doubles
}  // vector automatically cleans up after v

明らかに、delete[]非常に面倒で間違いが発生しやすくなります。これを使用すると、割り当てられたメモリをvector使用する必要がなく、忘れずに解放する必要があります。それはすでに行われており、さらに良くなりました。newdeletevector

注:new単一のオブジェクトを作成する場合は、代わりにスマート ポインターを使用できます。

リソース (空きストレージ、ファイル、バッファ、スレッド、ロックなど) を持つすべてのクラスにはデストラクターが必要です。リソースはコンストラクターで取得され、リソースはデストラクターで解放されます。 vector空きストアメモリを処理するコンストラクタ/デストラクタは典型的な例です。

17.5.1生成されるデストラクタ破壊順序

クラスでデストラクターが定義されていない場合、コンパイラーは自動的にデストラクター (空の関数本体に相当) を生成します。

カスタム デストラクターであっても、生成されたデストラクターであっても、関数本体の実行後、コンパイラーはデータ メンバーのデストラクターと基本クラスのデストラクターも順番に呼び出しますこれは、「デストラクターが呼び出されるべきである」という保証が実装される方法です。

注: このルールは、破壊の順序と構築の順序が逆になることを保証します。

  • 構築順序:基本クラス→メンバー(宣言順)→self
  • 破棄順序: self → member (宣言の逆順による) → 基本クラス

例えば:

struct Customer {
    
    
    string name;
    vector<string> addresses;
};

void some_fct() {
    
    
    Customer fred;
    // initialize fred
    // use fred
}

を終了するとsome_fct()fredが破棄され、その (自動生成された) デストラクタが呼び出され、次に とのデストラクタが呼び出されますnameaddresses

すべてのルールは次のように要約できます。オブジェクトが破棄されると (スコープ外に出る、通過するdeleteなど)、デストラクターが呼び出されます。

17.5.2デストラクタと空きストレージ仮想デストラクタ

空きストレージとクラス階層を組み合わせた例を考えてみましょう。

Shape* fct() {
    
    
    Text tt(Point(200, 200), "Annemarie");
    // ...
    Shape* p = new Text(Point(100, 100), "Nicholas");
    return p;
}

void f() {
    
    
    Shape* q = fct();
    // ...
    delete q;
}

関数ではfct()Textオブジェクトはttを離れるfct()ときに破棄され、ttそのメンバーと基本クラスShapeのデストラクターが呼び出されます。ただし、によって作成されたオブジェクトがfct()return fromの場合、呼び出し元の関数は を指すかどうかを知りません。では、デストラクターはどのように呼び出されるでしょうか?newTextf()qTextdelete qText

セクション 14.2.1 では、問題の核心である仮想デストラクターShapeがあるという事実を省略しました。実行されると、型が調べられて、デストラクターを呼び出す必要があるかどうかが確認され、必要であれば、デストラクターが呼び出されます。したがって、デストラクターは と呼ばれますこれは仮想関数であり、仮想関数呼び出しメカニズムに従って、ここでは派生クラスのデストラクターが実際に呼び出されます(その後、自動的に呼び出されます)。仮想関数でない場合は呼び出されず、そのメンバーは適切に破棄されません。delete qdeleteqdelete qShape~Shape()~Shape()~Text()~Shape()~Shape()~Text()Textstring

経験則:クラスが基本クラスとして機能する場合、仮想デストラクターが必要です。そうしないと、基本クラス ポインターによって作成された派生クラス オブジェクトがdelete渡されたときにnew、派生クラスのデストラクターが呼び出されず、派生クラス オブジェクトが正しく破棄されません。

それを試してみてください

17.6 要素へのアクセス

ベクトル要素の読み取りと書き込みのために、単純なメンバー関数が提供されていget()ますset()

double get(int n) {
    
     return elem[n]; }       // access: read
void set(int n, double v) {
    
     elem[n] = v; }  // access: write

ここでは、セクション 17.4.2 で説明されているように、要素にアクセスするためのポインターelemで演算子が使用されます[]

次のように使用できるようになりましたvector

vector v(5);
for (int i = 0; i < v.size(); ++i) {
    
    
    v.set(i, 1.1*i);
    cout << "v[" << i << "]==" << v.get(i) << '\n';
}

get()とを使用したコードは、一般的に使用される添字表記と比較するとset()醜いです。セクション 18.4 では演​​算子vectorを定義します[]

現在のバージョンvector: Simple Vectors v1

17.7 クラスオブジェクトへのポインタ

「ポインタ」の概念は汎用的なもので、メモリ内の任意のオブジェクトを指すことができます。たとえば、vector使用できるポインタは次のとおりです。

vector* f(int s) {
    
    
    vector* p = new vector(s);  // allocate a vector on free store
    // fill *p
    return p;
}

void ff() {
    
    
    vector* q = f(4);
    // use *q
    delete q;  // free vector on free store
}

覚えておいてください: コンストラクターの外側では、コンストラクターが作成したオブジェクトのことを忘れる機会がnew生じます。deleteオブジェクトを削除するための適切な戦略 (例: ) がない限り、オブジェクトをコンストラクターに配置し、デストラクターに配置するようVector_refにしてください。newdelete

オブジェクト名を指定すると、.演算子を使用してメンバーにアクセスできます。

vector v(4);
int x = v.size();
double d = v.get(3);

同様に、オブジェクト ポインターを指定すると、->演算子を使用してメンバーにアクセスできます。

vector* p = new vector(4);
int x = p–>size();
double d = p–>get(3);

p->mに相当します(*p).m

.多くの場合、メンバー アクセス演算子->と呼ばれます

注記:

  • NULL ポインターまたは削除されたポインターを介してオブジェクト メンバーにアクセスすると、不正なメモリ アクセスとなり、プログラム クラッシュ (コアダンプ) が発生します。
  • データ メンバーにアクセスしないメンバー関数は、NULL ポインターを介して呼び出すことができます。

17.8 型の混合: void* とキャスト

場合によっては、型システムの保護を無視しなければならないことがあります (たとえば、C++ 型を知らない他の言語と対話したり、静的にタイプ セーフになるように設計されていないレガシー コードと対話したりするなど)。この場合、次の 2 つのことが必要です。

  • どのようなオブジェクトが保存されているかわからないメモリへのポインタ
  • このポインターが指す型をコンパイラーに伝える操作 (文書化されていない)

typeの意味void*は、「コンパイラが型を知らないメモリへのポインタ」です。これは、実際の型がわからないコード間でアドレスを渡したいときに必要ですvoid*例には、コールバック関数の「アドレス」パラメータ (セクション 16.3.1)、および最下位レベルのメモリ アロケータ (new演算子の実装など) が含まれます。

void「戻り値がない」ことを意味するために使用され、voidその型のオブジェクトが存在しないため、void*ポインターを逆参照できません。任意の型のポインターをvoid*ポインターに割り当てることができます。それ以外の場合は、static_cast明示的な型変換(明示的な型変換) を使用するか、単に型変換(キャスト) と呼ばれる必要があります。例えば:

void* pv = new int;  // OK: int* converts to void*
void* pv2 = pv;      // copying is OK (copying is what void*s are for)
double* pd = pv;     // error: cannot convert void* to double*
*pv = 7;             // error: cannot dereference a void* (we don't know what type of object it points to)
pv[2] = 9;           // error: cannot subscript a void*
int* pi = static_cast<int*>(pv);  // OK: explicit conversion

C++ によって提供される明示的な型変換演算子:

  • static_cast: ある型を別の関連する型 ( intdoublevoid*、 などdouble*) に変換します。
  • dynamic_cast: 基本クラスのポインター/参照を派生クラスのポインター/参照に変換します (逆も同様)。変換が失敗した場合、ポインター型の場合は null ポインターが返され、std::bad_cast参照型の場合は例外がスローされます。
  • const_cast:const属性を削除します。
  • reinterpret_castint:となど、2 つの無関係な型の間で変換しますdouble*

絶対に必要な場合にのみ使用してくださいstatic_cast。 と とconst_castreinterpret_cast、最も危険な変換の 2 つです。reinterpret_cast使用するコードが移植可能であることを期待しないでください。

17.9 ポインタと参照

参照は、自動的に逆参照される不変のポインタ、またはオブジェクトのエイリアスと考えることができますポインターと参照の違い:

  • ポインタは初期化されていないか (ワイルド ポインタ)、オブジェクトを指していない可能性があります (null ポインタ)。参照は初期化する必要がありますが、「null 参照」は存在しません。
  • ポインタに値を割り当てると、ポインタ自体の値が変更され、他のオブジェクトを指すようになります。一方、参照は初期化後に他のオブジェクトを参照できず、参照に値を割り当てると、参照されるオブジェクトの値が変更されます。
  • ポインタは渡されるnew&取得され、使用*または[]逆参照されます。参照はオブジェクト名によって取得され、逆参照は必要ありません。

注記:

  • ポインターのプロパティはconst、ポインター自体と指すオブジェクトに分けられます。
タイプ 可変ポインタ 指すオブジェクトは可変です
T* はい はい
const T*( に相当T const*) はい いいえ
T* const いいえ はい
const T* const いいえ いいえ
  • 参照自体は不変であるため、constプロパティに関しては、T&と同等T* constconst T&と同等ですconst T* const

例えば:

int x = 10;
int* p = &x;    // you need & to get a pointer
*p = 7;         // use * to assign to x through p
int x2 = *p;    // read x through p
int* p2 = &x2;  // get a pointer to another int
p2 = p;         // p2 and p both point to x
p = &x2;        // make p point to another object

対応する引用の例は次のとおりです。

int y = 10;
int& r = y;    // the & is in the type, not in the initializer
r = 7;         // assign to y through r (no * needed)
int y2 = r;    // read y through r (no * needed)
int& r2 = y2;  // get a reference to another int
r2 = r;        // the value of y is assigned to y2
r = &y2;       // error: you can't change the value of a reference
               // (no assignment of an int* to an int&)

知らせ:

  • r2 = rと同等ですが*p2 = *p、 ではありませんp2 = p
  • r = &y2は型エラーであるため、参照自体に値を代入して別のオブジェクトを参照する操作はありません。

ポインタと参照はどちらもメモリ アドレスを使用して実装されますが、使用方法は少し異なります。

17.9.1 ポインタと参照パラメータ

変数の値を関数で計算した結果に変更したい場合、戻り値、ポインタパラメータ、参照パラメータの3つの選択肢があります。例えば:

int incr_v(int x) {
    
     return x+1; }  // compute a new value and return it
void incr_p(int* p) {
    
     ++*p; }      // pass a pointer (dereference it and increment the result)
void incr_r(int& r) {
    
     ++r; }       // pass a reference

各方法には独自の長所と短所があり、特定の機能とその用途に応じて決定する必要があります。ポインタ パラメータを使用すると、パラメータは変更できるが、null ポインタは判断する必要があることをプログラマに思い出させます。逆に、参照パラメータは「無害に見えます」(パラメータが変更されることがわかりません)。オブジェクトが参照されていることを確認できます。

したがって、答えは「関数の性質によって異なります」です。

  • 小さなオブジェクトの場合は、値渡しを使用します。
  • 「オブジェクトなし」(null ポインターで表される) が有効なパラメーターである関数の場合は、ポインター パラメーターを使用します (null ポインターを判断することを忘れないでください)。
  • それ以外の場合は、参照パラメータが使用されます。

セクション 8.5.6 も参照してください。

17.9.2 ポインタ、参照、および継承

セクション 14.4 で説明したように、派生クラスは、その共通の基本クラスが必要な場合 (インターフェイスの継承) に使用できます。ポインターまたは参照の場合、この考えは次のように表現されます。パブリック基本クラスの場合、B暗黙的に に変換でき暗黙的に に変換できます例えば:DD*B*D&B&

void rotate(Shape* s, int n);  // rotate *s n degrees

Shape* p = new Circle(Point(100, 100), 40);
Circle c(Point(200, 200), 50);
rotate(p, 35);
rotate(&c, 45);

参考のために、それは同様です:

void rotate(Shape& s, int n);  // rotate s n degrees

Shape& r = c;
rotate(r, 55);
rotate(*p, 65);
rotate(c, 75);

これは、ほとんどのオブジェクト指向プログラミング手法にとって不可欠です。

17.9.3 例: リンクされたリスト

最も一般的に使用され、便利なデータ構造の 1 つはリンクリストです。リンク リストは「リンク」(ノード) で構成され、各ノードはデータと次のノードへのポインタを保持します。これはポインターの典型的な使用法の 1 つです。

各ノードが先行ノード (先行ノード) と後続ノード (後続ノード) のポインターを持つリンク リストは、二重リンク リスト(二重リンク リスト) と呼ばれます。次に例を示します。

二重リンクリスト

ここで、 はnorse_godsヘッド ノードへのポインタです。

ノードが後続ノードへのポインターのみを持つリンク リストは、単一リンク リストと呼ばれます。次に例を示します。

単一リンクリスト

注記:

  • リンク リストのノードはメモリ内で順番に配置されていないため、配列のような添え字を介してアクセスすることはできず、ヘッド ノードから順番にのみトラバースできます。
  • 「C プログラミング言語」の注記 第 6 章の構造セクション 6.6 では、リンク リストについても紹介します。

次のようにノードを定義できます。

二重リンクリスト

上の図の北欧の神々のリンクされたリストは次のように作成できます。

Link* norse_gods = new Link("Thor", nullptr, nullptr);
norse_gods = new Link("Odin", nullptr, norse_gods);
norse_gods–>succ–>prev = norse_gods;
norse_gods = new Link("Freia", nullptr, norse_gods);
norse_gods–>succ–>prev = norse_gods;

ただし、このコードはかなりわかりにくいため、挿入操作を定義します。

// insert n before p (incomplete)
Link* insert(Link* p, Link* n) {
    
    
    n->succ = p;        // p comes after n
    p->prev->succ = n;  // n comes after what used to be p's predecessor
    n->prev = p->prev;  // p's predecessor becomes n's predecessor
    p->prev = n;        // n becomes p's predecessor
    return n;
}

pそれがノードを指しており、そのノードに先行ノードがある場合、関数は正常に動作しますポインターとリンク リストの構造について考えるとき、私たちは通常、コードの正しさを検証するために紙にボックスと矢印の図をいくつか描きます。あまり自慢せずに使ってください。

リンクされたリストの挿入操作

ただし、このバージョンはまたは- の場合にinsert()対応していないため、不完全ですnpp->prevnullptr

注: 簡単なアプローチは、最初に理想的なケースを検討し、->先行する。

Null ポインター テストを追加した後の正しいバージョンは次のとおりです。

// insert n before p; return n
Link* insert(Link* p, Link* n) {
    
    
    if (n == nullptr) return p;
    if (p == nullptr) return n;
    n->succ = p;            // p comes after n
    if (p->prev) p->prev->succ = n;  // n comes after what used to be p's predecessor
    n->prev = p->prev;      // p's predecessor becomes n's predecessor
    p->prev = n;            // n becomes p's predecessor
    return n;
}

pそれがヘッド ノード (すなわちp->prev == nullptr)である場合にinsert()も正常に動作することが確認でき、その時点でnそれが新しいヘッド ノードになります。

このように、リンク リストを作成するための前述のコードは次のように記述できます。

Link* norse_gods = new Link("Thor");
norse_gods = insert(norse_gods, new Link("Odin"));
norse_gods = insert(norse_gods, new Link("Freia"));

17.9.4 リンクリスト操作

以下に、便利なリンク リスト操作のセットを示します。

  • コンストラクタ
  • insert: ノードの前に挿入
  • add: ノードの後に​​挿入
  • erase: ノードの削除
  • find: 指定された値を持つノードを検索します
  • advance: n 番目の後続ノードを取得します

これらの操作の実装:リンク リスト操作

注記:

  • ノードがヘッド ノードの前に挿入された場合、またはヘッド ノードが削除された場合、呼び出し元はヘッド ポインターを更新する必要があります
  • erase(p)によってポインタが返されることが保証されていないため、リンクdeleteリストから削除できないノード一方、パラメータで渡されるため、呼び出し元はポインタにアクセスできる必要があります。したがって、呼び出し側は、必要に応じてリンク リストからノードを削除する必要があります。ppnewpdelete

接尾辞形式は++増分前の値を返し、接頭辞形式は++増分後の値を返すことに注意してください。例えば:

int a = 1;
int b = ++a;  // a = 2, b = 2
int c = a++;  // a = 3, c = 2

17.9.5 リンクリストの応用

演習として、2 つのリンク リストを作成してみましょう。

Link* norse_gods = new Link("Thor");
norse_gods = insert(norse_gods, new Link("Odin"));
norse_gods = insert(norse_gods, new Link("Zeus"));
norse_gods = insert(norse_gods, new Link("Freia"));

Link* greek_gods = new Link("Hera");
greek_gods = insert(greek_gods, new Link("Athena"));
greek_gods = insert(greek_gods, new Link("Mars"));
greek_gods = insert(greek_gods, new Link("Poseidon"));

「残念なことに」私たちは 2 つの間違いを犯しました: ゼウスは北欧の神ではなくギリシャの神でした; そしてギリシャの戦争の神はマルスではなくアレスでした (マルスは彼のラテン語/ローマ名でした)。以下を修正できます:

Link* p = find(greek_gods, "Mars");
if (p) p–>value = "Ares";

find()返品防止には細心の注意を払っておりますので予めご了承くださいnullptrこの例ではありえませんが、実際のプログラムではポインタを返す関数に対してヌルポインタを判定する必要があります

注: これら 2 つのステートメントは次のように省略できます。if (Link* p = find(greek_gods, "Mars")) p->value = "Ares";

同様に、Zeus を正しい位置に移動できます。

Link* p = find(norse_gods, "Zeus");
if (p) {
    
    
    erase(p);
    insert(greek_gods, p);
}

このコードには 2 つの微妙なバグがあることに注意してください。

  • ポイントされたノード (ヘッド ノードなど)を削除する場合はnorse_gods、更新する必要がありますnorse_gods。更新しないと、間違ったノードをポイントすることになります。
  • greek_godsポイントされたノード (ヘッド ノード) の前にノードが挿入された場合は、そのノードも更新する必要がありますgreek_gods
if (Link* p = find(norse_gods, "Zeus")) {
    
    
    if (p == norse_gods) norse_gods = p->succ;
    erase(p);
    greek_gods = insert(greek_gods, p);
}

注: これらのバグは両方とも、ヘッド ノードの更新に関連しています。この種の問題を解決するには、「仮想ヘッド ノード」を使用します。つまり、ヘッド ノードとして固定ノードを作成し、他のノードは仮想ヘッド ノードの後継ノードになります。これにより、リンクリストの演算においてヘッドノードの特別な処理を回避でき、コードロジックを簡素化することができる。

最後に、これら 2 つのリンクされたリストを出力します。

{ Freia, Odin, Thor }
{ Zeus, Poseidon, Ares, Athena, Hera }

リンクリストの応用

注: この本のコードは、new作成されたノードによって占有されているメモリを解放しません。ループを使用してdelete各ノードを順番に走査する必要があります。

void destroy(Link* p) {
    
    
    while (p) {
    
    
        Link* next = p->succ;
        delete p;
        p = next;
    }
}

17.10 this ポインタ

現在のリンク リスト操作関数はすべて、Link*最初のパラメーターとして 1 を受け入れ、このオブジェクト内のデータにアクセスします。通常、このような関数はメンバー関数として実装します。

二重リンクリスト v2

prevポインターをsuccプライベートとして宣言し、constメンバー関数previous()next()それにアクセスするためのvalue関数を提供しますが、それは単なるデータであるためパブリックのままです (ゲッターとセッターのペアを提供し、他のチェック ロジックを提供しないことは、メンバーをパブリックとして直接宣言することと同等です)。

メンバー関数では、現在のオブジェクト (つまり、メンバー関数を呼び出すオブジェクト) へのthisポインター現在のオブジェクトのメンバーにアクセスする場合は使用する必要はありませんたとえば、 では、はと同等です明示的な使用は、オブジェクト全体を参照する必要がある場合にのみ必要です(例:など)。thisLink::insert()this->prevprevthisn->succ = this;return *this;

注:Xクラスconstの非メンバー関数、コンストラクター、およびデストラクターでは、thisの型は であり、メンバー関数X*では、その型は ですconstconst X*

this割り当てはできませんのでご注意ください。

17.10.1 リンクリストの応用

新しいバージョンのリンク リスト アプリケーションの例を次に示します。

リンク リスト アプリケーション v2

insert()メンバー関数であっても自由関数であっても、この場合、違いは大きくありません (メンバー関数バージョンでは最初のパラメーターが関数の前に移動するだけ、つまり になりinsert(p, n)ますp->insert(n))。セクション 9.7.5 を参照してください。

ここにはまだリンク リスト クラスがなく、ノードが 1 つだけあることに注意してくださいLinkしたがって、呼び出し元は最初の要素へのポインター (つまり、ヘッド ポインター) を維持する必要があります。標準ライブラリは、listセクション 20.4 で説明されているクラスを提供します。

簡単な練習

ドリル17

エクササイズ

おすすめ

転載: blog.csdn.net/zzy979481894/article/details/130140557