第7週 ポインタ
本日の内容
前回の復習
アドレス演算子/間接演算子
ポインタ型
mallocとfree
ダブルポインタと二次元配列
ポインタを引数にとる関数
前回の復習
code:triangle.c
typedef struct {
double x;
double y;
} Point;
typedef struct {
Point a;
Point b;
Point c;
} Triple;
typedef enum {Red, Orange, Yellow, Blue, Cyan, Green, Brown, Gray} Color;
typedef struct {
Triple tri;
Color color;
} Triangle;
double distance(Point, Point);
int is_triangle(Triple);
double calc_area(Triple);
Point make_point();
Triple make_triangle();
void print_triangle(Triple);
Triangle generate();
int main() {
srand(time(NULL));
for(int i=0; i<16; i++) {
Triangle tri = generate();
print_triangle(tri.tri);
printf("area: %lf\n", calc_area(tri.tri));
printf("color: %d\n", tri.color);
}
}
double distance(Point a, Point b) {
double dx = a.x - b.x, dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
int is_triangle(Triple t) {
double a, b, c;
a = distance(t.b, t.c);
b = distance(t.c, t.a);
c = distance(t.a, t.b);
return (a+b>c)&&(b+c>a)&&(c+a>b);
}
double calc_area(Triple d) {
if(!is_triangle(d)) {
return -1;
}
Point a = d.a, b = d.b , c = d.c;
double res = (a.x - c.x) * (b.y - c.y) - (b.x - c.x) * (a.y - c.y);
return fabs(res / 2.);
}
Point make_point() {
Point res;
res.x = rand() % 100;
res.y = rand() % 100;
return res;
}
Triple make_triangle() {
Triple tri;
double area = calc_area(tri);
while(area <= 0 || 100 < area) {
tri.a = make_point();
tri.b = make_point();
tri.c = make_point();
area = calc_area(tri);
}
return tri;
}
void print_triangle(Triple tri) {
if(!is_triangle(tri)) {
puts("tri is not a triangle");
return;
}
printf("(x1, y1): (%lf, %lf)\n", tri.a.x, tri.a.y);
printf("(x2, y2): (%lf, %lf)\n", tri.b.x, tri.b.y);
printf("(x3, y3): (%lf, %lf)\n", tri.c.x, tri.c.y);
}
Triangle generate() {
static int color;
Triple tri = make_triangle();
Triangle res = {tri, (Color)color};
color++;
if(color > 7) color = 0;
return res;
}
アドレス演算子/間接演算子
アドレス演算子
プログラム実行時,変数はメモリ空間の一部として存在しています.
変数がメモリ空間のどの位置(番地)に存在しているのかを表したものをアドレスといい,アドレス演算子(&: アンパサンド)を用いて変数のアドレスを求めることができます.
table: アドレス演算子
&a 変数aのアドレス
ここで,&aのように変数のアドレスを表す値を,変数のメモリ内での位置を指し示していることからポインタといいます.
間接演算子(デリファレンス)
間接演算子(*: アスタリスク)を用いて変数に間接的にアクセスすることができます.
table: 間接演算子
*b 変数へのポインタbが指す変数
変数aのアドレスがbであると分かっているとき,*b (bが指す変数)は変数aそのものとなります.
このことから,「*bはaのエイリアスである」というような言い方をします.エイリアスはここでは,別名やあだ名といった意味です.
ポインタ型
ポインタ型について
ポインタを格納するような変数のデータ型としてポインタ型があります.
ポインタ型はそのポインタが指す変数の型に*を付けたものが型名となります.
ここで使用する*は間接演算子で用いた*とは完全に別物であることに注意してください.
*は型のすぐ横に記述しても問題ありませんが,ここでは半角スペースを開けて表記する事とします.
ポインタ型のフォーマット指定子は,ポインタが指す変数の型に関わらず%pです.
table: ポインタ型の例
型名 ポインタが指す変数の型 有効範囲(32bitCPU)
int * int型 0x00000000~0xFFFFFFFF (8byte分)
float * float型 0x00000000~0xFFFFFFFF (8byte分)
double * double型 0x00000000~0xFFFFFFFF (8byte分)
char * char型 0x00000000~0xFFFFFFFF (8byte分)
struct 識別子 * ユーザ定義型 0x00000000~0xFFFFFFFF (8byte分)
ポインタ型変数とアドレス演算子・間接演算子
実際にポインタ型とアドレス演算子・間接演算子を使用したプログラムを例として挙げます.
code: pointer.c
int main(void) {
int *a, b; // bはポインタ型でない!!
b = 10;
a = &b; // a に b のアドレスを代入する
printf("bの値は%dです\n", *a); // => bの値は10です
printf("a: %p\n", a); // => a: 0061FF18 (個人差アリ)
printf("&b: %p\n", &b); // => &b: 0061FF18 (個人差アリ)
*a = 100; // *a(bのエイリアス)に100を代入する
printf("bの値は%dです\n", b); // => bの値は100です
return 0;
}
ここで,ポインタ型変数を宣言する際にint *a, b;のようにすると,変数bの型はポインタ型ではなくint型になります.
実は変数bの型をポインタ型として宣言するにはint *a, *b;のように記述する必要があるのです.
これがかなり混乱を招く仕様ではあるのですが,これはこういうものだと理解しておくのが良いと思います.
しかし,あくまでも*はポインタ型である事を表していることに注意してください(間接演算子と混同しないように).
また,変数のエイリアスに値を代入することは変数に値を代入することに等しいため,プログラム内で*aに100を代入すると変数bの値も100に更新されます.
NULLとNULLポインタ
ポインタがどの変数も指していないような状態にする際に使われるのがNULLであり,どの変数も指していないようなポインタの事をNULLポインタといいます.
NULLポインタを参照し,それが指す変数への読み書きを行おうとすると,セグメンテーション違反が発生し,プログラムは正常に動作しません.
code: null_sample.c
int main(void) {
int *i = NULL;
int j = 100;
if (i != NULL)
printf("i: %d\n", *i); // => 出力されない
else
puts("iはNULLポインタです"); // => 出力される
i = &j;
printf("i: %d\n", *i); // => i: 100
return 0;
}
このように,マクロNULLを利用することで何も指していないポインタを作成したり,比較することで予めエラーが発生する事を防いだりすることが出来ます.
また,NULLは(void *)0で定義されているため,代わりに0を用いてNULLポインタの作成や比較を行う事も可能ですが,マクロを用いた方が理解し易いコードとなるためおすすめできません.
このプログラム中のif文の条件式として,単にiと記述するだけでも同じ動作を実現することができます(if (i)).
このような書き方はよく見かける(気がする)ので,覚えておきましょう.
構造体(ユーザ定義型)へのポインタとアロー演算子
ここで,構造体変数へのポインタ型変数から,構造体変数のメンバへアクセスする際にアロー演算子を用いる必要があることに注意してください.
table: アロー演算子
-> student->name studentが指す構造体変数のメンバnameへアクセスする
code: pointer_structure_sample.c
typedef struct _score {
int japanese;
int math;
int science;
} score;
typedef struct _student {
int id;
score grade;
} student;
int main(void) {
student takashi = {1, "タカシ", {90, 95, 87}};
student hanako = {2, "ハナコ", {95, 100, 92}};
student *sp;
sp = &hanako;
printf("%sのIDは%dです\n", takashi.name, takashi.id); // => タカシのIDは1です
printf("%sのIDは%dです\n", sp->name, sp->id); // => ハナコのIDは2です
printf("%sの数学の点数は%dです\n", takashi.name, takashi.grade.math); // => タカシの数学の点数は95です
printf("%sの数学の点数は%dです\n", sp->name, sp->grade.math); // => ハナコの数学の点数は100です
sp->id = 3;
sp->grade.japanese -= 10;
printf("%sのIDは%dです\n", hanako.name, hanako.id); // => ハナコのIDは3です
printf("%sの国語の点数は%dです\n", hanako.name, hanako.grade.japanese); // => ハナコの国語の点数は85です
printf("%sの理科の点数は%dです\n", (*sp).name, (*sp).grade.science); // => ハナコの理科の点数は92です
return 0;
}
このプログラムでは,構造体scoreをメンバとして持つ構造体studentを定義し,構造体studentの変数としてtakashiとhanakoを宣言しています.
また,構造体studentのポインタ型変数としてspが宣言されており,構造体studentの変数hanakoのアドレスを代入しています.
構造体変数のメンバへのアクセスが.を用いているのに対して,構造体変数のポインタ型からその構造体変数のメンバへアクセスする際には->が用いられていることを理解できれば,なぜsp->grade.mathが->と.を使って書かれているかが説明できます.
spは構造体studentのポインタ型変数であるため,ポインタが指している変数のメンバへアクセスするためには->を用いる必要がありますが,間接的にアクセスされた変数のメンバgradeは構造体scoreの(ポインタ型でない)変数であるため,そのメンバmathへアクセスするためには.を用いればよいのです.
また,他のデータ型と同様に,*を用いることで間接的に変数へアクセスすることもできます.
ポインタと一次元配列
さて,プログラム実行時に変数がメモリ空間の一部として存在していることは説明しました.
では,一次元配列はメモリ空間内でどのように存在しているのでしょうか.
code: array_memory.c
int main(void) {
printf("d0: %p\n", &d0); // => d0: 0061FF14 (個人差アリ) printf("d1: %p\n", &d1); // => d1 0061FF18 (個人差アリ) printf("d2: %p\n", &d2); // => d2 0061FF1C (個人差アリ) return 0;
}
このプログラムでは,アドレス演算子を用いて配列の各要素のアドレスを表示しており,結果から配列の隣合う要素同士のアドレスの差が全て4になっていることが分かります.
これは,int型のデータサイズが4byte(メモリアドレス4つ分)であり,配列は連続したメモリ領域を利用しているためです.
では,他のデータ型で試すとどうなるのでしょうか?
答えは簡単.そのデータ型のサイズが何byteかによって配列の隣り合う要素同士のアドレスの差が変わります.
もし,これについて興味があるなら以下のプログラムを試してみてください.
code: data_size.c
int main(void) {
printf("%d\n", ((int) &a1 - (int) &a0)); // => データ1つ分のサイズ(byte) return 0;
}
本題に戻ります.
このように,C言語では連続したメモリ領域を配列として用いています.
つまり配列int a[10]のような配列を用意すると,a[0]を先頭とした連続する4byte × 10のサイズのメモリ領域が用意されるという訳です.
では,int a[10]において,aは一体何を意味するのでしょうか?
答えは,配列aの先頭アドレスです.
配列では先頭から何番目の要素であるかを[](角括弧)を用いて指定することで,配列の要素へアクセスしています.
code: pointer_array.c
int main(void) {
int *b;
for (int i = 0; i < 10; i++)
b = a; // aは配列の先頭アドレスであるから,int型のポインタ型変数に代入可能
for (int i = 0; i < 10; i++)
printf("b%d = %d\n", i, bi); // => 配列aの要素とまったく同じ printf("a0 = %d\n", a0); // => a0 = 100 printf("a9 = %d\n", a9); // => a9 = 1000 return 0;
}
このように,配列の要素のデータ型のポインタ型変数に配列の先頭アドレスを代入することによって,ポインタ型変数を元の配列と同じように使用することができます.
配列のある要素を指すポインタ変数に対して,整数nを加算することは,その要素 + n番目の要素へのポインタを求めること
つまり,ポインタ型変数に整数nを加算した値は,ポインタ型変数が指すアドレス + ポインタ型変数が指す変数のデータ型のサイズ * n となるわけです.
code: move_pointer.c
int main(void) {
char hello_world[] = "Hello, World!!";
char *cp;
int len = strlen(hello_world);
// 配列hello_worldに格納されている文字列を表示する
for (cp = hello_world; *cp != '\0'; cp++)
printf("%c", *cp);
puts("");
cp = hello_world;
for (int i = 0; i < len; i++) {
if (hello_worldi == *(hello_world+i)) printf("hello_world%dと*(hello_world+%d)は等価です(%c)", i, i, *(hello_world+i)); if (hello_worldi == *(cp+i)) printf("\thello_world%dと*(cp+%d)は等価です(%c)", i, i, *(cp+i)); puts("");
}
cp = hello_world + (len - 1);
// 配列hello_worldに格納されている文字列を逆にして表示する
for (int i = 0; i < len; i++)
printf("%c", *(cp-i));
return 0;
}
以下余談
code: pointer_digression.c
int main(void) {
printf("%d\n", &a1-&a0); // => 1 return 0;
}
このように,配列の要素を指すポインタ変数同士の差を計算すると,その配列のインデックスの差と等しくなります.
関数へのポインタ
関数にはエントリポイントがあり,エントリポイントを指すような関数ポインタを用いることで,間接参照で関数を呼び出すことが可能となります.
table: 関数ポインタの宣言
宣言方法 使い方の例 例の説明
型 (* 変数名)(仮引数) void (* funcp)(int, int) void型で,int型の引数を2つもつような関数へのポインタ変数
ここで,優先順位の関係から変数名と*を()で囲う必要があることに注意してください.
code: callback_function.c
int callback1(int a, int b) {
return a+b;
}
int callback2(int a, int b) {
return a-b;
}
void func(int a, int b, int (*callback)(int, int)) {
int c = (*callback)(a, b);
printf("%d\n", c);
}
int main(void) {
int (*funcp)(int, int);
funcp = callback1; // callback1は,関数callbackのエントリポイントである
func(5, 2, funcp); // => 7
func(5, 2, callback2); // => 3
return 0;
}
mallocとfree
malloc関数
変数や配列を宣言している場合にメモリの領域は静的に確保され,プログラム終了時まで保持されますが,malloc関数を用いることによって動的に確保することが可能です.
malloc関数では,確保したいメモリのサイズを引数に指定することによってメモリ領域が確保され,その領域へのポインタが返り値として返されます.
この際,メモリが何らかの理由で確保できなかった場合,malloc関数はNULLを返します.
free関数
C言語では,動的に確保したメモリ領域で不要になった領域を自動で開放するガーベージコレクションという機能が備わっていません.
そのため,malloc関数で動的に確保したメモリ領域で不要となったものは解放する必要があります.
free関数では,動的に確保したメモリ領域へのポインタを引数として指定することによってその領域のメモリが解放されます.
code: malloc_free.c
int main(void) {
int *p;
int n;
printf("input: ");
scanf("%d", &n);
// メモリの確保
p = (int *)malloc(n * sizeof(int));
for (int i = 0; i < n; i++)
for (int i = 1; i <= n; i++)
// メモリの解放
free(p);
return 0;
}
ちなみに,指定されたサイズのメモリ領域 × 指定された数だけメモリ領域を確保するようなcalloc関数や,動的に確保したメモリ領域を拡大することができるrealloc関数というものもあるので,興味があれば調べてみてください.
余談 - 危険なコード
code: pointer_dangerous_code.c
int *safe_func() {
int *a = (int *)malloc(sizeof(int));
*a = 100;
return a;
}
int *dangerous_func() {
int a = 100;
return &a;
}
int main(void) {
int *secured = safe_func();
int *released = dangerous_func();
free(secured);
return 0;
}
上のプログラムは一見動作するように思えますが,実は非常に危険です.
関数safe_func内ではmalloc関数を用いてメモリを確保してからそのアドレスを返していますが,関数dangerous_func内では単に宣言しただけの変数のアドレスを返しています.
これの何が問題かといいますと,実は関数内で宣言した変数のメモリは関数終了とともに解放されてしまいます.
そのため,一応動作はするものの(コンパイラのバージョンによってはエラーがでて実行できません),返却されたポインタの中身がいつ確保されて書き変わってしまうかが分からないという状態になります.
以上のことから,変数や配列を作成するような関数を定義する場合は,必ずmalloc関数を用いてメモリ確保を行うようにしましょう.
ダブルポインタと二次元配列
ポインタ型のポインタ型を,二重のポインタという意味をこめてダブルポインタといいます.
さて,一次元配列はその先頭の要素のアドレスを格納したポインタによって代替が可能でしたが,二次元配列はどうでしょうか.
int **dp;のようにint型のポインタのポインタ,つまりint型のダブルポインタdpを宣言して,配列int a[3][4]の先頭アドレスを代入してみましょう(dp = a).
一見,これは動作するように思うかもしれませんが,実はこれはセグメンテーション違反となります.
実は二次元配列は,配列を要素とする配列であり,int a[3][4]においてaはint[4](4要素の配列)のポインタの配列の先頭アドレスなのです.
https://gyazo.com/32eda040aa90ffdb21385d60b411331b
よって,二次元配列とダブルポインタは似ているようで違うものなので注意しましょう.
code: pointer_twodimarray.c
int main(void) {
int a34 = {{0, 1, 2, 3}, {10, 11, 12, 13}, {20, 21, 22, 23}}; int **dp = (int **)malloc(3 * sizeof(int *));
// dp = a; => セグメンテーション違反
for (int i = 0; i < 3; i++) {
dpi = (int *)malloc(4 * sizeof(int)); }
for (int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++)
puts("");
}
for (int i = 0; i < 3; i++)
free(dp);
return 0;
}
余談 - 配列へのポインタ
code: array_pointer.c
int main(void) {
int a34 = {{0, 1, 2, 3}, {10, 11, 12, 13}, {20, 21, 22, 23}}; int (*ap2)[];
ap = a;
ap2 = a;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++)
printf("%2d ", apij); // ap2ijを参照することはできません(配列のサイズを定義していないため) puts("");
}
for (int i = 0; i < 4; i++)
puts("");
for (int i = 0; i < 4; i++)
}
ポインタを引数にとる関数
通常の関数では,引数に変更を加えても元の値は変更されません.
しかし,これは関数内で行われた変更が取り消された訳ではなく,そもそも関数内で扱っていた引数が元の変数とは完全に別物(アドレスが異なる)ためです.
ポインタを引数にとる関数では,間接的に変数へアクセスすることによって変更を加えることができます.
変数の値を引数として渡すことを値渡しと呼びましたが,このように変数のアドレスを渡すことを参照渡しといいます.
code: pointer_argument.c
void func1(int i) {
i *= 100;
}
void func2(int *p) {
*p *= 100;
}
for (int i = 0; i < 3; i++)
}
int main(void) {
int i = 1;
func1(i);
printf("%d\n", i); // => 1
func2(&i);
printf("%d\n", i); // => 100
func3(a);
for (int i = 0; i < 3; i++)
printf("%d ", ai); // => 各要素が10倍された値が表示される return 0;
}
ここで,配列を引数としてとった関数内で元の配列に変更を加えることができたのは,配列の先頭アドレスを渡すことで,引数が元の配列のエイリアスとなるためです.
練習問題
問題1
前回の練習問題で作成したプログラムに,構造体Triangleを参照渡しで受け取り,重心の座標が(0, 0)となるように平行移動させるような関数centering_triangleを追加し,その動作を確認するようなプログラムを記述してください.
問題2
問題1で作成したプログラムに次の2つの関数を追加し,その動作を確認するようなプログラムを作成してください.
指定した数だけ要素を持ったTriangle型配列を生成して返すような関数generate_triangle_array
与えられたTriangle型配列の要素を,三角形の面積が大きい順番で並べ替える関数sort_triangle_array
(ヒント1)
math.hで定義されている関数を用いることで,べき乗や平方根の計算を行うことが容易となります.
(ヒント2)
stdlib.hで定義されている関数qsort関数を用いることによって,配列を指定した比較方法で昇順に並び変えることが
できます.
qsort関数の定義
void qsort(void *base, size_t num, size_t size, int (*compare)(const void*, const void*))
table: qsort関数の引数
引数 説明
base ソートする配列の先頭要素へのポインタ
num 配列の要素数
size 配列の要素1つ分のメモリサイズ
compare 比較に用いる関数
compare関数は,比較する値(配列の要素)へのポインタを引数とし,第一引数として与えられたポインタが指す値が先になるならば負の値を,第二引数として与えられたポインタが指す値が先になるならば正の値を返すような関数である.
なお,0を返した場合はどちらが先でもよいとする.
compare関数の使い方の例として,構造体studentの配列をid順に昇順ソートするようなプログラムを以下に示す.
code: howtouse_qsort.c
typedef struct _student {
int id;
} student;
int compare_student(const void *l, const void *r) {
return ((student *)l)->id - ((student *)r)->id;
}
int main(void) {
student students5 = {{1, "Ichiro"}, {3, "Saburo"}, {2, "Ziro"}, {5, "Goro"}, {4, "Shiro"}}; qsort(students, 5, sizeof(student), compare_student);
for (int i = 0; i < 5; i++)
printf("%d %s\n", (students+i)->id, (students+i)->name);
return 0;
}