問題
以下の3つの関数のうち、コンパイルエラーにならないものはどれか、また理由を述べよ
void do_something1(char* dest, char const* src)
{
src = NULL;
}
void do_something2(char* dest, char const* src)
{
*src = 'a';
}
void do_something3(char* dest, char* const src)
{
src = NULL;
}
いままで変数はメモリー上に確保されると言ってきました。ところでメモリーと言っても実際にはCPUのキャッシュ領域(L1,L2,L3キャッシュ)、物理メモリー、HDDやSSDに置く仮想メモリーなどがあり、
これらをOSが管理してくれているお陰で、プログラマーは(高速化を意識しない限り)これらを1つのメモリーとして扱えているわけです。
GPGPUとかやるとOS管理領域のメモリーとGPU側のメモリーを別個に管理しないといけないのでものすご~く面倒なのです。一回一回メモリー転送するコードを書いたり。
で、そのひとまとめの空間には仮想的に住所が割り振られます。
言い換えるとすべての変数には住所があるわけです(関数だって住所がある)。
では早速その住所(アドレス)を取得してみましょう。
#include <stdio.h>
int main()
{
int p = 20;
printf(
"変数p: %d\n"
"変数pのアドレス: %p\n",
p, &p
);
}
変数に入っている数値と変数のアドレスは無関係です、念のため。
変数のアドレスは「&」を変数名の前につけると取得できます。便利。
なお、printfで表示するときは%pを使います。いや、たいていの処理系なら%xとか%dでもちゃんと表示してくれるだろうけどさ。
実行例はこんなかんじかな。
変数p: 20 変数pのアドレス: 0038FBA4
このアドレスは大抵実行するごとに変わります。
みなさま、大変ながらくお待たせいたしました。ようやくポインタのお話です。まあ既に何回もチラチラ出てきてるわけですが。
ポインタとは、矢印のことで、矢印であるからには、根本と指示す先が有ります。以上です。
・・・うそです。まあ大体今の説明でいいんだけど、例とか上げつつ実際に見てみましょう。でも今の矢印に例えるのは、C言語のポインタの理解にはものすごく役立つので覚えておいてください。
どのくらい大事かていうと、冷蔵庫の卵を入れるとこくらいには重要です。
int a = 20;
int *p;
p = &a;
printf("%d, %p, %d", a, p, *p);
3行目はさっきの変数のアドレスを取得するやつですね。こうすることでint*型の変数、pはaを指します。図を書いたほうがわかりやすいかな。
ポインタが指し示す先を取得するには変数名の前に「*」をつけます。「int *p;」の「*」とは意味が違います。なお
int *p; int* p;
は同値ですが、
int* p1, p2;
とした時、p2の型は「int型」です。そのため、「int* p」ではなく「int *p」と書くことがあります。
int *p1, *p2; // '*'がそれぞれの変数に適用されることが見た目で分かる
もっとも
typedef int *int_ptr; int_ptr pi, p2;
とすればどちらともint*型になりますが。C言語ってよぐわがんね。
ポインタは、なにかを指すことで初めて意味を持つから派生型、とかいう言い方をするのですが、
ポインタと言っても、他の型から派生するという違いこそあるものの、所詮ただの型であり、型なんだからポインタ型の変数もポインタ型の値もあるわけです。
int型から派生したからint*型だったわけで、char型から派生すればchar*型になりますし、その他も以下略です。派生型ですから派生元の型しか差せません。
なお、どんな型でもさせるポインタ型というのもあり、void*型がそれです。ただし、使用時はキャストして元の型をコンパイラーに教える必要があります。
といったように内部的にchar*やint*があるわけではなく、単にコンパイラーが何から派生したか覚えてくれている、というだけのことなのです。
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <errno.h>
//機能:標準入力を数字に変換する。
//引数:戻り値の最大値,戻り値の最小値
//戻り値:入力した数字、エラー時はINT_MIN, EOFのときはEOF
int get_integer_num(const int max, const int min)
{
char s[100];
if (fgets(s, 100, stdin) == NULL) {
// エラーの原因がEOFか切り分け
if (feof(stdin))
return EOF;
// 改行文字が入力を受けた配列にない場合、入力ストリームにごみがある
size_t i;
for(i = 0; i < 100 && '\0' == s[i]; i++); // strlenもどき
if(s[i - 1] != '\n')
while(getchar() != '\n'); // 入力ストリームを掃除
return INT_MIN;
}
if (s[0] == '\n')
return INT_MIN;
errno = 0;
const long t = strtol(s, NULL, 10);
if (0 != errno || t < min || max < t)
return INT_MIN;
return (int)t;
}
int compare_int(const void *a, const void *b)
{
return *static_cast<int*>(a) - *static_cast<int*>(b);
}
int main()
{
const int num1 = get_integer_num(INT_MAX, INT_MIN);
if (num1 == INT_MIN)
return -1;
const int num2 = get_integer_num(INT_MAX, INT_MIN);
if (num2 == INT_MIN)
return -1;
const int result = compare_int(&num1, &num2);
if (result > 0)
puts("num1のほうが大きい");
else if (result == 0)
puts("num1とnum2は等しい");
else
puts("num2のほうが大きい");
return 0;
}
さて、ポインタは宣言した時は普通の変数がそうであるように、変数の値に何が入っているかわかりません。つまり、どこを指しているかわからないポインタ、というわけです。
それで、どこも指していないことが保証されているポインタというのがあり、NULLポインタと呼ばれます。
C言語はアメリカで開発されたので読みは英語の「ナル」です。「ヌル」ではありません。ぬるぽ。
void *hoge_ptr1 = NULL; //#define NULL 0 となっている場合(Cコンパイラーならほぼ例外なく) auto tmp1 = NULL;//int
改めて、初期化です。
プログラマがその変数に何が入っているかわかるようにすることを初期化といいます。
ポインタは矢印に例えられますが、矢印と、指示す先があるわけです。
したがって、矢印(どこを指すか、というアドレス)と矢印の先(さされている確保されたメモリー空間)の双方がプログラマーにとって既知である必要があります。矢印をNULLにするだけでは不十分です。
以前constとは定数だと言いました。これがポインタを絡めるとどうなるか見て行きましょう。
ポインタを矢印にたとえるならばそれに対する指し示す先、つまり実体領域があるのでした。つまりconstはその双方につけることができます。
ここがややこしいところなのです。以下「矢」、「先」と略します。書き込み可能を「○」、書き込み不可 (const) を「×」とします。
int main(void){
int hoge1 = 0;
int hoge2 = 1;
}
int *hoge1_p1 = &hoge1;//'hoge_p1'はintへのポインタ *hoge1_p1 = 2;//OK hoge1_p1 = &hoge2;//OK
const int *hoge1_p2 = &hoge1;//'hoge_p2'はint定数へのポインタ int const *hoge1_p3 = &hoge1;//'hoge_p3'はint constへのポインタ *hoge1_p2 = 3;//エラー *hoge1_p3 = 4;//エラー hoge1_p2 = &hoge2;//OK hoge1_p3 = &hoge2;//OK
int * const hoge1_p4 = &hoge1;//'hoge1_p4'はintへのポインタ定数 *hoge1_p4 = 5;//OK hoge1_p4 = &hoge2;//error
const int * const hoge1_p5 = &hoge1;//'hoge1_p5'はint定数へのポインタ int const * const hoge1_p6 = &hoge1;//'hoge1_p6'はintへのポインタ定数 *hoge1_p5 = 3;//error *hoge1_p6 = 4;//error hoge1_p5 = &hoge2;//error hoge1_p6 = &hoge2;//error
基本型の前にconstを書こうがあとに書こうが意味は変わりませんし型も変わりません。'*'の前か後ろかを見てください。
参考サイト
POINTER | 「配列とポインタの完全制覇」
http://kmaebashi.com/programmer/pointer.html
C言語の const とポインタについて調べてみた | ふにょいサイト
http://hunyoi.com/?p=215
以下の3つの関数のうち、コンパイルエラーにならないものはどれか、また理由を述べよ
void do_something1(char* dest, char const* src)
{
src = NULL;
}
void do_something2(char* dest, char const* src)
{
*src = 'a';
}
void do_something3(char* dest, char* const src)
{
src = NULL;
}