【C++まとめと抜粋 0x02】メモリ管理(1):生ポインタ


序文

長い間、C++ は C 言語のポインターと配列のサポートを継承してきました. 初期の C++ では、
この方法でポインターまたは配列を定義することができました.

int n;
int *p = &n;
int digits[10] = {
    
    5,5,5,1,2,3,4,5,6,7};

しかし、最新の C++ では、これらの「生のポインターまたは配列」をより頻繁に回避する傾向があります。
代わりに、新しいスマート ポインター (例: unique_ptr) とコンテナー クラス (例: ベクトル、リスト) を使用してください。それらを使用することには多くの明らかな利点があります。

  • より多くの完全な機能を備えたインターフェイスがあります
  • 開発者の意図をより明確に表現する能力
  • それらのデストラクタは自動的にリソースを解放します

もちろん、これは「生のポインタ」が完全に放棄されたことを意味するわけではありません。
「生のポインター」に基づいたライブラリやフレームワークを使用する場合、「生のポインター」を使用するコードを読む場合でも、生のポインターを処理する必要があります。

したがって、この記事は、次の内容を含む「生のポインター」から開始します。

オブジェクトへのポインター

ポインタとアドレス

メモリは、コンピューターで最も重要なものかもしれません. 新しい変数を作成してプログラムを実行するときは、メモリを扱うことになります. ポインタは、メモリを操作および管理するためのツールです. 多くの人はポインタは複雑だと考えていますが、実際にはポインタは単なる数値であり (ポインタの種類に関係なく)、メモリ アドレスを格納するだけです。

より詳細なステートメントは次のとおりです。

  • ポインターは、別のオブジェクトのアドレスを保持するオブジェクトです
  • すべてのポインターには型があり、それが指すオブジェクトの型を表します。
int *pi;                  // a "pointer to int"
unsigned long *pul;       // a "pointer to unsigned long"

したがって、任意のオブジェクト x があり、このオブジェクトのアドレスを取得したい場合は、&x を使用して x のアドレスを取得します。つまり、x が T 型の場合、&x は「T へのポインター」型になります。

int i;
unsigned long ul;           // object T

int *pi = &i;
unsigned long *pul = &ul;   // pointer to T

上記の 2 点を理解すると、ポインターは単なる数値なので、ポインターの値を変更できるかどうかは容易に想像できます。実際、ほとんどの場合、それは可能です. ポインターは、ライフサイクルのさまざまな段階でさまざまなオブジェクトを指すことができます. 一部の例外については、後で紹介します.
ここに画像の説明を挿入
たとえば、上の図のポインター P は、オブジェクト a をポイントする場合もあれば、オブジェクト b をポイントする場合もあります。P のポイントを次のように変更できます。

int a = 1, b = 2;
int *p = &a;

p = &b;

ヌルポインタ

また、何も指さないポインターもあり、ヌルポインターと呼ばれます。

int *p1 = NULL;       // traditional C
int *p2 = 0;          // traditional C++
int *p3 = nullptr;    // modern C++

上記の p1、p2、p3 はすべて null ポインターですが、構文は時期によって異なります.3 つの書き込み方法で目的を達成できますが、最初に選択するのは nullptr です。

NULL はもともと C 言語に由来するため、C 言語では NULL は 0 または 0L として定義されます。しかし、どの定義であっても、NULL は作成されたばかりの時点では型を持ち、何も指していません: int は整数です。これにより、オーバーロードされた関数を使用すると、予期しない状況が発生する可能性があります。

void f(int i);
void f(char *s);

f(NULL)			// call f(int), not f(char *)

上記の例では、NULL は整数データ型であるため、オーバーロードされた関数を呼び出すときは、int が char * よりも優先されます。したがって、nullptr は C++11 で導入されました。これは、整数ではなく一意の型であり、任意のポインター型に変換できます。

逆参照

次のトピックは逆参照です。ポインター変数 p がある場合、*p を使用して、p が指すオブジェクトを取得できます。

int i = 13;
unsigned long ul = 42;           
int *pi = &i;
unsigned long *pul = &ul;

*pi = 14;                         //把 i 的值变成 14
*pul += 2;                        //把 ul 的值加 2

もちろん、未定義の動作である null ポインターの逆参照を避けるように注意する必要があります。どんな悪いことが起こるかはわかりませんが、良いことではないことは確かです。

ポインタの寿命

一般に、ポインタの寿命は、それが指すオブジェクトの寿命とは何の関係もありません。

void f(int *p){
    
    
	return;
}

int i = 10;
f(&i);

上記の例では、 f 関数が呼び出されるたびに、新しい p インスタンスが再作成されます. 関数が呼び出された後、このインスタンスは破棄され、そのライフサイクルはこの呼び出しのみです. しかし、関数呼び出しの前に、p が指すオブジェクト i は既に存在し、呼び出しが終了した後も i はまだ存在しています。
オブジェクトがポインターよりも長く存続する場合、問題はありません。しかし、ポインターの存続期間がオブジェクトの存続期間よりも長い場合、いくつかの予期しない状況が発生します。

int *g(){
    
    
	int i = 0;    // i lifetime begins
	return &i;    
}                 // i lifetime ends

int *pi = g();    // pi points to dead i

上記の例では、ポインター pi は、ダングリング ポインターと呼ばれるデッド オブジェクトを指しており、*pi へのアクセスも未定義の動作です。

配列内のポインタ

まず、最も単純な固定長配列を定義します。

char x[N];     // 长度为 N 的 char 型数组

表面的には、C++ の配列は他の言語の配列と変わりませんが、最初に気付くのは、配列の長さを N で初期化する場合、N は整数定数 const int でなければならないということです。配列に割り当てるメモリの量を決定するために使用される評価可能な量。

次に、0 から N-1 までのインデックスが付けられた配列要素にランダムにアクセスできます。

int k;

x[0] = 'a';
x[k] = 'c';

配列内のポインター計算

ポインター pc を使用して配列内の要素にアクセスすることもできます。++pc は、配列要素のサイズに関係なく、配列の次の要素を指すようにします。このようにして、配列全体を反復処理することもできます。

char x[N];
char *pc = &x[0];

*pc = 'a';      // same as : x[0] = 'a'

++pc;           // pc now points to x[1]
*pc = 'c';      // same as : x[1] = 'c'

int in[5] = {
    
    1,2,3,4,5};
for(int *p = in; p < x + 5; ++p){
    
       // step through the array
	~~~
}

配列をトラバースする上記のコードは次のように示されます。最初のポインターは x の最初の要素を指し、次に 1 つずつ後方に移動し始めます。ただし、ループが終了すると、ポインターは配列の外側の最初の要素を指しますここに画像の説明を挿入
。 (点線で)。このとき、メモリ内の配列の格納場所の次の場所を指し、そこからの書き込みまたは読み取りは、予期しない動作を引き起こす可能性があります。

上記の「配列要素のサイズに関係なく」というのは、実際の意味は、配列内のポインターを加算または減算すると、単位化されているように見え、常に配列要素の単位であり、実際に占有されているメモリユニットの数は関係ありません。以下の例を参照してください。

int i,j;
int x[5];
int *p = &x[i], *q = &x[j];

int n = q - p;
int m = j - i;
if(n == n) {
    
     ~~~ } // always true

ここで、ポインター間のこの減算は、2 つのポインターが同じ配列内の要素を指し、それらの結果が int 値である場合にのみ有効であることに注意することが特に重要です。

配列の添え字とポインタ

上記の例の x を、x[0] の位置を指すポインターと直接見なすことができます。

int *pi = x;   // same as : pi = &x[0]
*x = 4;        // same as : x[0] = 4

もう一度拡張すると、実際には、配列添字 [] は実際には配列操作ではなくポインター操作を表します。

x[i]   // same as : *(x + i)

for(int i = 0; i < n; ++i){
    
    
	sum += x[i];
	sum += *(x + i);   // equivalent but not recommended
}

そうは言っても、配列は実際には単なるポインタだと思うかもしれません。しかし、実際にはそうではありません。コンパイラは配列 x を int へのポインタとして扱うため、x をポインタに直接割り当てることができます。これを「崩壊」と呼びます。

この減衰は一時的なもので、代入ステートメントが終了するまでしか持続しません。double 値と int 値を加算してから double に代入できるように、コンパイラは計算時に int 値を double として扱いますが、値が実際に double に変換されるという意味ではありません。

double d;
int i;
~~~
d += i;     ==      {
    
     
				      double t = static_cast<double>(i)
					  d += t;
					}  // t在此处结束生命周期

同様に、コンパイラでは、x は一時的に &x[0] に減衰します。

ただし、配列が関数パラメーターとして使用される場合、実際にはポインターであり、次の例の 3 つの書き込み方法は同じであることに注意してください。

void foo(int *x){
    
    
	cout << sizeof(x);           // sizeof(x) = sizeof(int *) 	
}
void foo(int x[]){
    
    
	cout << sizeof(x);           // sizeof(x) = sizeof(int *)
}
void foo(int x[10]){
    
    
	cout << sizeof(x);           // sizeof(x) = sizeof(int *)
}

size_t と ptrdiff_t

標準ライブラリには、バイト単位のオブジェクトであるパラメーターを使用したり、値を返したりする関数が多数あります。たとえば、次のようになります。

T *p = malloc(N);   // N 是需要被分配的字节数
memcpy(dst,src,N);  // N 是需要从src拷贝到dst的字节数

ここで N の型は何ですか? 最も簡単に思いつくのは INT です。
これらの関数は、任意のオブジェクトを処理するときに使用できるようにする必要があるため、N は任意のオブジェクトのサイズを表すことができる必要があります。そのため、設計者は新しいタイプ size_t を特別に設計しました。実際には、ターゲット オブジェクトのサイズを示す組み込みの sizeof() 関数があります。

#include <cstddef>
using namespace std;

size_t n = sizeof(widget);

異なるプラットフォームでは、size_t の定義が若干異なる場合がありますが、2 つの一般原則があります。

  • オブジェクトのサイズを負にすることはできないため、size_t は符号なし整数です。
  • size_t は int の場合もあれば、long または long long の場合もあります。ターゲット マシンで最大のオブジェクトを表すことができる必要があります。

戻り値としての size_t の別の例を見てください。

size_t strlen(char const *);

sizeof(array) は、配列が占めるバイト サイズを返します。また、配列に含まれる要素の最大数も示します (各要素のサイズは少なくとも 1 です)。

size_t は正の数でなければならないことは上で述べましたが、配列内の 2 つのポインターを減算した結果は必ずしも正の数ではないため、それをどのように表現するか: 明らかに、ここで p - q = -3、デザイナーは
ここに画像の説明を挿入
新しい2 つのポインターの加算と減算の結果を表すために使用される型 ptrdiff_t は、通常、符号付き整数です。

size_t と ptrdiff_t の 1 つは符号なしの数値を表し、もう 1 つは符号付きの数値を表します. 符号付きの数値を符号なしの数値と比較すると、コンパイラは符号付きの数値を符号なしの数値に変換しますが、予期しないバグが発生する可能性があります:

char buffer[64];
char const *field_end = strchr(field,',');
ptrdiff_t length = field_end - field;
if(lenght < sizeof(buffer)){
    
             // 无符号数和有符号数进行了比较
	~~~
}

const とポインタ

const がポインターに遭遇すると、多くのプログラマーはしばしば混乱します. このセクションでは、const がポインターにどのように影響するかを要約します.

T *pから始めましょう:

const T *p;            // (1)
T const *p;            // (2)
T *const p;            // (3)
const T *const p;      // (4)
T const *const p;      // (5)

それぞれどういう意味ですか?

(1) と (2) の場合、* 記号の左側に const があり、このとき p は「定数 T へのポインタ」を意味します。これは、T が定数であることを意味します。T の値を変更することはできませんが、ポインターのポイントを変更することはできます。

T x,y;
p = &x;   // OK : can modify p
*p = y;   // NO : can not modify T referenced by *p

(3)については * 記号の右側に const があり、このとき p は「T を指すポインタ定数」、つまり p は T のことしか考えられず、T オブジェクトを変更できることを意味します。

T x,y;
p = &x;   // NO : can not modify p
*p = y;   // OK : can modify T referenced by *p

次に、(4)(5) については、T も p も変更できないことは明らかです。これは、定数 T を指す定数ポインター p です。

次に、導入する必要があるのは constexpr です。constexpr と const は同等ではありません。constexpr は常にポインター定数を参照します。

char constexpr *p;
~~~
char const     *p;  // not equivalent
char *const     p;  // equivalent

最後に、ポインターの型変換について説明します。覚えておくべき例を 1 つだけ示します。

T *p;
T const *pc;

pc = p    // OK
p = pc    // NO : lost const 

上記の例では、T オブジェクトが const によって変更されるときに、p もそれを指している場合、p を介して定数 T を変更できますが、これは許可されていません。

ポインタ型変換

C 言語の黎明期には、異なる型のポインター間の予期しない変換が原因で多くのバグが発生しました. この変換では、通常、コンパイラーは警告を出すだけでしたが、C++ では、この変換は許可されません. コンパイラーはエラーをスローします.

gadget *pg;
widget *pw;  // gadget and widget are distinct types
~~~
pg = pw;
pw = pg;     // warning in C, error in C++

もちろん、reinterpret_cast を使用してコンパイラをシャットダウンし、強制的に変換を完了させることはできますが、これはこの変換が安全であることを意味しません。

pg = reinterpret_cast<gadget *>(pw); 

したがって、安全な型変換は次のとおりです。

  • 基本クラスへのポインターに変換された派生クラスへのポインター
  • 任意の型のポインターを void * に直接割り当てることができます

最初のポイントは、詳しく説明する必要はありません。
2 番目の点に関して、C 言語の一部の関数 (malloc や free など) は、任意のオブジェクトへのポインターを操作するように設計されており、C および C++ は、任意のデータ型を指すことができるポインターを表すために void * を提供します。

void *malloc(size_t n);
void free(void *p);

上記で、任意の型のポインターを void * に直接割り当てることができると述べましたが、その逆は当てはまらないことに注意してください。void ポインターであるため、プログラムはそれを介して操作 (アクセス、代入、加算、減算) を実行できませんが、型に型を含めることはできません。

要約する

この記事では「生ポインタ」の使い方を紹介していますが、スマートポインタを理解するためにはかなりの事前知識が必要なので、筆者はこれらの事前知識を紹介した上でスマートポインタを更新していきます。

おすすめ

転載: blog.csdn.net/qq_35595611/article/details/126433137