トラック
/
C
C
/
シラバス
/
ポインター
ポイ

ポインター の C

{other: "%{count}個の演習"}

ポインターについて

ポインタとは、値が別の変数や定数のアドレスであるような、変数または定数のことです。 ポインタは、Cの強力であり、危険をはらむ可能性もある機能です。 ポインタを理解するのは難しいことがあります。

int i = 5;

変数に値を代入すると、次の2つの質問ができます。

誰ですか?

私はiです。

何を持っていますか?

私は整数の5を持っています。

さらに3つ目の質問もできます。

どこにいますか?

私は0x6dfed4にいます。

ちょっと待って、どういうこと?

変数や定数は、その値をメモリ上の場所に保存します。 そのメモリ上の場所は、変数または定数のアドレスと呼ばれます。

アドレス演算子と間接演算子(その1)

変数のアドレスを取得するには、アドレス演算子を使います。これは&です。

int i = 5;
int *i_address = &i;

ちょっと待って、i_addressの前にある*はどういう意味ですか?これは掛け算のことではないのですか?

型のあとに*、そのあとに識別子と書くのが、その型の変数へのポインタを宣言する方法です。 *は間接演算子と呼ばれます。ポインタがアドレスを保持している値に、間接的に言及するからです。

でも、なぜ別の変数のアドレスを保持するためだけに変数を作るのでしょうか?

そもそも、なぜ変数のアドレスが何であるかを気にするのでしょうか?

それは、変数が配列の中にあるとき、その場所が重要になるからです。

配列とポインタ

次のように配列を作ると

int my_ints[] = {1, 2};

my_intsの値は実はそのアドレスであり、これは偶然にも最初の要素のアドレスと同じです。 2番目の要素のアドレスは、最初の要素のアドレスに、そのシステムでのintのサイズを足したものになります。

これをよりよく確かめるには、次を実行してみてください

#include <stdio.h>

int main() {
    // my_int's value is its address
    int my_ints[] = {1, 2};
    
    // since my_ints is already an address
    // we don't need the address operator to make a pointer from it.
    // a pointer to my_ints holds the address of my_ints
    int *my_ptr = my_ints;
    
    // the address of my_ints and the value of my_ptr are the same
    printf("my_ints is %p. my_ptr is %p.\n", my_ints, my_ptr);
    
    // the address of the first element is the same as the values in the line above
    printf("&my_ints[0] is %p. my_ptr + 0 is %p.\n", &my_ints[0], my_ptr + 0);
    
    // the address of the second element has increased by the size of an int
    printf("&my_ints[1] is %p. my_ptr + 1 is %p.\n", &my_ints[1], my_ptr + 1); // 
    
    return 0;
}

ポインタ演算、つまり配列のインデックスが0から始まる理由

前の例は、なぜ配列のインデックスが1ではなく0から始まるのかを示しています。 配列へのインデックスは、インデックスに型のサイズを掛けて、配列の開始アドレスに足したものです。 最初の要素は配列の先頭と一致するので、最初の要素を得るには、型に0を掛けて開始アドレスに足します。 2番目の要素を得るには、型に1を掛けて開始アドレスに足します。 型が4バイトの長さなら、2番目の要素のアドレスは最初の要素より4バイト高くなります。

これが、ポインタがある型のものとして宣言される理由でもあります。 ポインタが、自分がどの型を指しているかを知らなければ、1を足すときに何バイト進めばよいかわからないでしょう。 ポインタに1を足すことは、必ずしもアドレスに1バイト足すことを意味しません。 それは、その型の長さをバイト数で1回足すことを意味します。

でも、まだあるんです! *の話がこれで終わったなんて、思っていませんよね?

間接演算子(その2)

アドレスが配列のインデックスでどう機能するかを理解したところで、あるアドレスに保存されている値を取得できなければ、何の役に立つのでしょうか? どうやってそれを取得するのでしょうか? あるアドレスに保存されている値を取得するには、間接演算子を使います。これは、思い出せるかもしれませんが、*です。 この状況では、間接演算子は参照外し演算子とも呼ばれることがあります。ポインタがアドレスを保持している値を参照外しするからです。 参照外しに使うときは参照外し演算子と呼ぶことで、ポインタを定義するときの間接演算子と区別できますが、どちらの用法でも*は技術的には間接演算子という名前です。

ちょっと待って! 冗談でしょう? *にはいったい何通りの用法があるのでしょうか?! 掛け算、ポインタの宣言、そして今度は参照外し! 紛らわしい! そもそも参照外しとは、いったい何なのでしょうか?

間接演算子を参照外しに使う方法を例で示します

#include <stdio.h>

int main() {
    // define the variable
    int my_ints[] = {1, 2};
    
    // define the pointer from the address of the variable
    int *my_ptr = my_ints;
    
    // prints: my_ints[0] is 1. *(my_ptr + 0) is 1.
    printf("my_ints[0] is %u. *(my_ptr + 0) is %u.\n", my_ints[0], *(my_ptr + 0));
    
    // prints: my_ints[1] is 2. *(my_ptr + 1) is 2.
    printf("my_ints[1] is %u. *(my_ptr + 1) is %u.\n", my_ints[1], *(my_ptr + 1));
    
    return 0;
}

ポインタ演算を使うとき、括弧(())を使うと意図の曖昧さを減らすのに役立ちます。 次の例は、プログラマーが意図したとおりに出力されないかもしれません

#include <stdio.h>

int main() {
    int my_ints[] = {1, 42};
    int *my_ptr = my_ints;
    
    // prints: my_ints[0] is 1. *++my_ptr is 42.
    printf("my_ints[0] is %u. *++my_ptr is %u.\n", my_ints[0], *++my_ptr);
    
    // prints: my_ints[1] is 43. ++*my_ptr++ is 43.
    printf("my_ints[1] is %u. ++*my_ptr++ is %u.\n", my_ints[1], ++*my_ptr);

    return 0;
}

*と++はどちらも同じ優先順位ですが、右から左に評価されます。 最初の行では、my_ptrがまず2番目の要素にインクリメントされ、それが参照外しされます。 2番目の行では、最初の行で2番目の要素にインクリメントされたmy_ptrが、まず参照外しされ、次にその参照外しされた値がインクリメントされます。 この行を上の例に追加すると

// prints: my_ints[1] is 43. *++my_ptr is (some garbage value).
printf("my_ints[1] is %u. *++my_ptr is %u.\n", my_ints[1], *++my_ptr);

その行では、ポインタを配列の外までインクリメントしてしまっています。 配列をはみ出して、メモリ上の次の4バイトを指しています。ポインタはそれをintとして解釈しますが、実際にはそれらのバイトは別の変数や複数の変数に属しているかもしれません。 それらの4バイトはdoubleの一部かもしれないし、4つのcharかもしれません。 問題は、ポインタでそれらを見るべきですらないということです。

こうすることもできました

// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);

配列のインデックスはポインタで実装されているので、配列をはみ出す同じ危険が当てはまります。

そして、文字列はcharの配列なので、ポインタは文字列にも使えます。 文字列は、しばしば次のようにポインタの構文で宣言されます

char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";

ただ、1つ質問があります。 なぜアスタリスクを識別子の隣に置くのでしょうか? その型へのポインタなのだから、型の隣に置かないのでしょうか? なぜなら、これは

int* my_ptr1, my_ptr2_that_is_really_an_int;

intへのポインタを2つ宣言するのではなく、intへのポインタを1つとintを1つ宣言するからです。 intへのポインタを2つ宣言するなら、次のようになります

int *my_ptr1, *my_ptr2;

なので、曖昧さを減らすために*を識別子の近くに置くのがよいでしょう。 あるいは、1行につき1つの識別子だけを宣言するのもよいでしょう。 そして、*の話にまだ飽きていないなら、ようこそ次の世界へ

ポインタへのポインタ

文字列の配列が次のように表されるのをよく見かけます

#include <stdio.h>

void print_strings(char **passed_in_strings) {
    printf("%s\n", *passed_in_strings);
    printf("%s\n", *(passed_in_strings + 1));
}

int main() {
    char *my_strings[] = {"One string", "Two string"};
    print_strings(my_strings);
    return 0;
}

明らかではないかもしれませんが、文字列の配列は、charの配列の配列へのポインタとして宣言され、初期化されています。 charの配列の配列がprint_strings()に渡されると、シグネチャはそれをcharへのポインタのポインタとして記述します。

では、char **が本当にcharの配列の配列へのポインタの配列へのポインタであって、単一のcharへのポインタのポインタではないと、どうやってわかるのでしょうか?

私たちにはわかりません。

これは、ポインタの使用を紛らわしくしうる数多くのことの1つです。 次のコードはコンパイルが通るかもしれません

#include <stdio.h>

void print_strings(char **passed_in_strings) {
    // prints: A (followed by gibberish)
    printf("%s\n", *passed_in_strings);
    // prints gibberish
    printf("%s\n", *(passed_in_strings + 1));
}

int main() {
    char my_char = 'A';
    char *my_ptr = &my_char;
    char **my_ptr_to_ptr = &my_ptr;
    print_strings(my_ptr_to_ptr);
    return 0;
}

しかし、print_strings()は意図したとおりに使われていません。 別のシグネチャを使っても助けにはなりません。シグネチャをこれに変えても

void print_strings(char *passed_in_strings[]) {
    // code snipped
}

コンパイルは通り、やはり意味不明なものが出力されるかもしれません。 しかし、単一のcharへのポインタのポインタを渡すことには、実質まったく価値がありません。 たいてい、char **を見かけたら、それはcharの配列の配列へのポインタの配列へのポインタを表します。

私たちは、関数に渡されたポインタを見てきました。 しかし、なぜ関数にポインタを渡すのでしょうか?

関数にポインタを渡す

Cでは、引数は値渡しで関数に渡されます。 つまり、値は引数からコピーされ、そのコピーはその関数だけのものになります。 関数が終わると、コピーされた値もなくなります。

関数の中で値を変えようとするとどうなるか見てみましょう

#include <stdio.h>

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    //prints:  In 'swap', a is 4 and b is 3.
    printf("In 'swap', a is %d and b is %d.\n", a, b);
}

int main() {
    int a = 3, b = 4;
    swap(a, b);
    // prints: After 'swap', a is 3 and b is 4
    printf("After 'swap', a is %d and b is %d.\n", a, b);
    return 0;
}

これは、変数の名前が同じでも、swap()の中のaとbはmain()の中のaとbとは別物だからです。 それらはaとbの値の_コピー_です。 意図したとおりに動くようにswapを変えられるでしょうか?

できます! aとbのアドレスをポインタとして渡せばよいのです。 でも、関数はポインタをコピーするのだから、それで何の役に立つのでしょうか? コピーされたアドレスもやはり同じアドレスなので、役に立つのです。 そのアドレスを参照外しすることで、main()で定義されたままの元の値にアクセスできます。

#include <stdio.h>

// pass in pointers to a and b
void swap(int *a, int *b) {
    // set temp to value at a's dereferenced address
    int temp = *a;

    // set a's value to value at b's dereferenced address
    *a = *b;

    // set b's value to temp
    *b = temp;

    // prints: In 'swap', a is 4 and b is 3.
    printf("In 'swap', a is %d and b is %d.\n", *a, *b);
}

int main() {
    int a = 3, b = 4;
    swap(&a, &b);
    // prints: After 'swap', a is 4 and b is 3.
    printf("After 'swap', a is %d and b is %d.\n", a, b);
    return 0;
}

これでswapにはたくさんの*がありますが、意図したとおりに動くようになりました。

間接演算子は「〜にある値」と読むことができます。 つまり、*aはaにある値です。 これは文字列ではどの程度うまくいくのでしょうか?

charへのポインタ

配列変数の値が、その配列が始まるアドレスであることは知っています。 そして、文字列をcharの配列、つまりchar my_string[]としても、charへのポインタ、つまりchar *としても定義できることも知っています。 これらはどう違うのでしょうか?

このコードでは、char *の引数をdescribe_me()に渡しています。 adjective引数はcharの配列として定義され、関数はそれをcharへのポインタとして受け取ります。

#include <stdio.h>
#include <string.h>

// takes pointer to char
void describe_me(char *adjective){
    strcpy(adjective, "evil");
    // prints: I am evil.
    printf("I am %s.\n", adjective);
}

int main() {
    // defined as a char array
    char adjective[] = "good";
    describe_me(adjective);
    // prints: I am really evil.
    printf("I am really %s.\n", adjective);
    
    return 0;
}

adjectiveに持続的な変更を加えられており、定義したcharの配列がcharへのポインタとして受け取れることがわかります。

では、adjectiveを直接charへのポインタとして定義してみましょう。

#include <stdio.h>
#include <string.h>

void describe_me(char *adjective){
    // segmentation fault on this line when program is executed
    strcpy(adjective, "evil");
    printf("I am %s.\n", adjective);
}

int main() {
    char *adjective = "good";
    describe_me(adjective);
    printf("I am really %s.\n", adjective);
    
    return 0;
}

ポインタは強力で、同時に危険をはらむ可能性もあるのでしたね? しかし、なぜうまくいかないのでしょうか? 配列はポインタで、charの配列ではポインタがうまくいったはずです! 理由は、以前はadjectiveがcharの配列として定義されていて、これはmain()に属する変数だったからです。 今度は、adjectiveが文字列の_リテラル_として定義されています。つまり、それらのcharsは、プログラム全体に属する読み取り専用の静的メモリに書き込まれます。 どちらもポインタですが、指している先が違います。 リテラルの文字列値を変えようとするのは、5というリテラルの値を変えようとするようなものです。 それは許されません。 つまり、問題は本当はそれがポインタであることではありません。 問題は、それが何を指しているかです。

GitHubで編集 リンクは新しいウィンドウまたはタブで開きます