C言語②:ポインタ

つまずきそうな小石を蹴飛ばしながら覚えるC言語(2)

この記事はまだ錯乱中です。


メモリは0と1の情報を格納する装置。情報はデータであるかもしれないし命令であるかもしれない。その物理的な仕組みはフリップフロップなどで検索すること。

メモリは情報を格納する物理的な場所に対して8ビット1バイトを1つの単位とし、単位ごとに番号を付けている。その番号をたどることで格納されている情報を読み込んだり、逆にメモリに書き込んだりする。

そんなような、情報を格納する物理的な場所に付いてる番号のことをアドレスという。

メモリが配列で、アドレスが配列のインデックスと見ることもできる。

登場人物(全部0と1)
データ:数値。これは座標であったり色であったり、文字に対応付けられたりする。
命令:CPUに可能なことに番号つけたもの。加減剰余のような計算機の能力。比較演算による制御。メモリなりレジスタなりのデータの読み込み、書き込みなど。CPUは既定の場所に0と1を置いておくと、それを命令と解釈して勝手に実行する。
アドレス:メモリ上の位置、格納領域ごとの番号。


ポインタ変数

アドレスを格納する変数。
変数の左側にアスタリスクを付けることで、その変数に格納された0と1はアドレスとみなされる。

ポインタ変数は変数でもあるので、そのへんの普通の変数としての機能も持つ。これはつまり変数の中身として格納されているアドレスを入れ替えることを意味する。

ポインタ変数はアドレスをたどった先のデータにアクセスする機能をもつ。これを関節参照という。

*と変数

アスタリスクは型の右に沿えても
変数の左に沿えても良いが、
Cの文法規則としては変数の左として認識される。

int* x;
int *y;

例えば以下のコードの上の例。
xはintポインタとみなされるがyはただのint型とみなされる。
下の例ではxもyもintポインタである。

int* x, y;
int *x, *y;

すなわち意識すべきはまず『変数』か『ポインタ変数』かである。

型と*

教科書を読むとだいたい*は変数に付いていることが強調される。

参考
詳説 Cポインタ 大型本 – 2013/12/26
Richard Reese (著), 菊池 彰 (翻訳)
プログラミング言語C 第2版 ANSI規格準拠
B.W.カーニハン (著), D.M.リッチー (著), 石田 晴久 (翻訳)

とはいえ*が型についてると考えた方が楽な場合もある。

例えばintが32bitだとして、これを格納するためにメモリ上の4単位(8ビット1byte×4)を使用すると考える場合。
ポインタ変数はメモリ上に指定int分確保できた領域を指すアドレスを格納する。
int型へのポインタという意味から見れば、型の方にアスタリスクを寄せた方が意味が通るように見える。

また、
キャストする場合

int x = 0;
int *y = (int*)x;

sizeofの場合

int x = sizeof(int*);

このあたりの文法規則は、
Microsoftの場合

キャスト演算子: ()
https://docs.microsoft.com/ja-jp/cpp/cpp/cast-operator-parens?view=msvc-170

式の概要
https://docs.microsoft.com/ja-jp/cpp/c-language/summary-of-expressions?view=msvc-170

宣言の概要
https://docs.microsoft.com/ja-jp/cpp/c-language/summary-of-declarations?view=msvc-170

unary-expression ( type-name ) cast-expression

cast-expression

cast-expression:
    unary expression
    (type-name)cast-expression

type-name:
    specifier-qualifier-list abstract-declarator opt

C 単項演算子(unary-expression)
https://docs.microsoft.com/ja-jp/cpp/c-language/c-unary-operators?view=msvc-170

unary-expression: 
 postfix-expression
 ++unary-expression
 --unary-expression
 unary-operator cast-expression
 sizeofunary-expression
 sizeof (type-name)
 unary-operator: & * + - ~ ! のいずれか


postfix-expression:
 primary-expression
 postfix-expression [ expression ]
 postfix-expression(argument-expression-listopt)
 postfix-expression . identifier
 postfix-expression -> identifier
 postfix-expression ++
 postfix-expression --
 ( type-name ) { initializer-list }
 ( type-name ) { initializer-list , }
primary-expression:
 identifier
 constant
 string-literal
 ( expression )
 generic-selection


specifier-qualifier-list:
 type-specifier specifier-qualifier-list opt
 type-qualifier specifier-qualifier-list opt
 alignment-specifier specifier-qualifier-list opt

type-specifier

type-specifier:
 voidcharshortint
 __int81
 __int161
 __int321
 __int641
 longfloatdoublesignedunsigned_Bool_Complex
 atomic-type-specifier
 struct-or-union-specifierenum-specifiertypedef-name

type-qualifier:

type-qualifier:
 constrestrictvolatile
 _Atomic

abstract-declarator

abstract-declarator:
 pointer
 pointer opt direct-abstract-declarator
pointer:
 *type-qualifier-list opt
 *type-qualifier-list opt pointer

つまり

問題となるのは(type-name)部分が(int*)になっても良いのかという話であるが、type-nameは型指定(intとか)および型修飾(constとか)およびポインタに置換され、opt付きは省略可能であることを意味するから、最終的には(int*)は多分大丈夫なんだろうと推察できる。

関数とか配列とかと*

以下はintポインタを戻り値として返す関数。

//宣言
int *func();

以下は関数ポインタ。
intを返す関数へのポインタ。

int (*func)();

以下はintポインタを返す関数へのポインタ。

int *(*func)();

以下はポインタの配列。
詳細は後述。

int *arr[10];

以下は配列のポインタ。
詳細は後述。

int (*arr)[10];

constと*

以下はポインタに格納されたアドレスの先のデータを変更不能とする。
後述、関節参照を参考

const int *x;
int const *x;

この場合、ポインタに格納されたアドレス自体は変更できる。

const int *x;
int y = 0;
x = &y;

ポインタに格納されたアドレスを変更不能とするには以下。

int* const x;

以下はアドレスもアドレス先の値も変更不能とする。

const int* const x;
int const * const x;

(const int* const)へのポインタ変数

const int* const *x;

結論

変数の左に付くアスタリスクは最優先。
また型に付く(付いて見える)こともある。
関数や配列の左にもアスタリスクは付くと考える。
(でないと特に関数ポインタは間違う)
const1つの時は変数左にアスタリスクがあるかないかで2パターンを区別。
const2つのアスタリスク1つは両方const。
const2つのアスタリスク2つは両方constへのポインタ。

関節参照

アスタリスク付きの変数に対する操作は、アドレスの場所に置いてあるデータへの操作となる。すなわち変数に格納されている値はアドレスとみなされる。

*x = 0;

アドレスが格納してある変数にアスタリスク無しでアクセスした場合、追跡していたデータに二度とアクセスできなくなる可能性がある。

x = 0;

ちなみにアドレス0はヌルポ。
int型とintポインタ型は異なるとみなされるが、0だけは代入可能。

アドレスが格納してある変数を+1することは配列の次の要素にアクセスすることと同じ操作である。

x += 1;
x++;

アドレス演算子

変数の左側にアンパサンドを付けることで、その変数が確保しているメモリ領域のアドレスを返す。

&x

int型変数のアドレスをintポインタに代入。

int x = 0;
int *y = &x;

単項演算としてのアンパサンドが重複、連続することはない。
単純な&&は論理積。
また&(&x)のように、アドレスのアドレスはとれない。

しかしポインタ変数のアドレスを取得したり、
配列のポインタのアドレスを取得したりはありうる。

言語的には
&の対象オペランドは関数指示子かオブジェクトを指す左辺値でなければならない。
&の結果はオブジェクトおよび関数へのポインタとなる。これは右辺値である。ゆえに左辺値をとる&演算に連続適用はできない。

オブジェクト
読み取りおよび書き込みができるメモリ領域。
左辺値
オブジェクトを指し示す式。必ずしもオブジェクトへの書き込みを保証しない。配列やconstはオブジェクトの変更を許可しない左辺値。
右辺値
読み取りのみできるもの。式の右辺に立つもの。というより右辺にしか立てないもの。

なので以下のような配列に対する適用はありえる。
詳細後述。

int arr[3]={1,2,3};
int (*c)[3] = &arr;

参考
S・P・ハービソン3世とG・L・スティール・ジュニアのCリファレンスマニュアル 単行本 – 2015/4/1
サムエル・P. ハービソン,3世 (著), ガイ・L. スティール,ジュニア (著), Samuel P. Harbison,3 (原著), & 2 その他p246くらい
プログラミング言語C 第2版 ANSI規格準拠 B.W.カーニハン (著), D.M.リッチー (著), 石田 晴久 (翻訳)p250くらい

配列とポインタ

arrと&arrと&arr[0]

arrの定義により動作は異なる。
通常の型の配列か、配列の配列か、ポインタの配列か、配列のポインタか。
また、宣言か、代入されるのか、読み取るのか、によっても動作は異なる。

通常の型の配列の宣言

 int arr[3]={1,2,3};

である時。arrと&arr[0]は同じ表現。
int配列のint要素のアドレスを返す。

    int arr[3]={1,2,3};
    //aとbは同一のアドレスを格納する
    int *a = arr;
    int *b = &arr[0];
    //x1とx2は同一の関節参照を通じて同一の要素を得る
    int x1 = *arr;
    int x2 = arr[0];
    //y1とy2は同一の関節参照を通じて同一の要素を得る
    int y1 = *(arr+1);
    int y2 = arr[1];

    printf("%d\n",  x1);//=>1
    printf("%d\n",  x2);//=>1
    printf("%d\n",  y1);//=>2
    printf("%d\n",  y2);//=>2

    //以下3つは無効
    //&arrはint(*)[3], 要素数3のint配列のポインタ
    //int c = &arr;
    //int *c = &arr;
    //int **c = &arr;

    //要素数3のint配列のポインタ
    int (*c)[3] = &arr;
    int z1 = c[0][0];
    int z2 = c[0][1];
    printf("%d\n",  z1);//=>1
    printf("%d\n",  z2);//=>2

*arr[index];

配列表現はその表現自体アスタリスクを伴う。
ゆえに*arr[]は演算子を2つ伴っているような状態。
すなわち多重配列である。
代入にせよ読み取りにせよそういう扱いをする必要がある。

宣言時
*arr[]は、特に型を伴ってポインタの配列と読む。
例えば int *arr[] はintポインタ型の配列。
これは多重配列でもあり、特にジャグ配列に相当する。
詳細は後述、多重配列を参照。

配列名arr

配列型は変更不可能な左辺値である。
以下のような表現は全部無効。

arr++;
&arr++;
&arr[0]++;

言語的には
配列名は配列が格納する型へのポインタを返す。
そのポインタは配列が格納する最初の要素へのポインタである。
これは単項演算と同レベルの規則である。
であるため、前述arrがint型の配列の時、
arrと&arr[0]は同じ表現。

&arr;

arrが配列である時
&arrは読み取り時、配列のポインタとして解釈される。
これは前述の配列名の規則の例外条項である。
詳細は後述、多重配列を参照。

    int arr[3]={1,2,3};    
    int (*brr)[3] = &arr;
    int x1 = brr[0][0];
    int x2 = brr[0][1];
    printf("%d\n",  x1);//=>1
    printf("%d\n",  x2);//=>2

*pt = arr;

ポインタ変数と配列は読み取り操作に対して似たような反応をするが、
代入操作で決定的な違いが出る。

以下は有効

int *pt = arr;
int z0 = *pt;//arr[0]
pt++;
int z1 = *pt;//arr[1]
pt++;
int z2 = *pt;//arr[2];

ポインタを受ける関数にほりこむこともできる。

//関数の実行
func(arr);
func(&arr[0]);

//配列の一部を渡す例
func(arr+1);
func(&arr[1]);

関数の定義内における仮引数として

void func(int arr[]){}
void func(int *arr){}

は同一。
以下例題。K&R p124

//K&R p124
static char allocbuf[ALLOCSIZE];//確保されたメモリ領域
static char *allocp = allocbuf;//その現在位置(を示すポインタ)

//確保されたメモリ領域からn文字分使用できるなら
//確保されたメモリ領域上の現在位置を返す
char *alloc(int n)
{
  //メモリ領域に空きがある
  //配列名は配列の先頭アドレス
  if(n<=allocbuf+ALLOCSIZE-allocp)
  {
    allocp += n;
    return allocp - n;
  }
  else//メモリ領域は空いてない
  {
    //ヌルポ
    return 0;
  }
}

//指定されたメモリ領域を解放する
//確保されたメモリ領域上の現在位置を指定された位置に動かす
void afree(char *p)
{
  if(allocbuf <= p && p < allocbuf + ALLOCSIZE)
  {
    allocp = p;
  }
}

多重配列

以下は要素数10のint配列へのポインタ。
要素数10のint配列の先頭アドレスを格納する。
List<int[]>みたいの。

//宣言
int (*arr)[10];//n行10列
//不正な宣言
//要素数0のint配列のポインタ。
//配列は要素数0では初期化できない
//int (*arr)[0];

//正常な宣言
//int (*arr)[10];


    //int配列へのポインタ
    int (*arr)[3];
    int brr[]={1,2,3};
    
    //arr = brr;//エラー brr==int*
    arr = &brr;//&brr==int (*)[3] : brrはint配列へのポインタ
      
    int *x0 = arr[0];//brr[]
    int y0 = x0[0];//brr[0]
    int y1 = x0[1];//brr[1]
    printf("%d\n", *x0);//=>1(x0のアドレス上の値)   
    printf("%d\n",  x0);//=>x0のアドレス
    printf("%d\n",  y0);//=>1    
    printf("%d\n",  y1);//=>2

    int *x1 = arr[1];//???[] 存在しないint配列    
    int y2 = x1[0];//???[0]
    int y3 = x1[1];//???[1]
    printf("%d\n", *x1);//=>6422476 存在しないint配列[0]の値 
    printf("%d\n",  x1);//=>6422252 存在しないint配列のアドレス  
    printf("%d\n",  y2);//=>6422476 存在しないint配列[0]の値 
    printf("%d\n",  y3);//=>1981336768 存在しないint配列[1]の値  

    int x2 = arr[0][0];//brr[0]
    int x3 = arr[0][1];//brr[1]
    int x4 = arr[0][2];//brr[2]
    int x5 = arr[0][3];//brr[3]
    int x6 = arr[1][0];
    int x7 = arr[1][1];    
    printf("%d\n", x2);//=>1
    printf("%d\n", x3);//=>2
    printf("%d\n", x4);//=>3
    printf("%d\n", x5);//=>6422476 brr[3]
    printf("%d\n", x6);//=>6422476 存在しないint配列[0]の値 
    printf("%d\n", x7);//=>6422476 存在しないint配列[1]の値  

以下はintポインタの配列、かつ配列の要素数10。
配列のポインタであり、ジャグ配列に対応する。

int *arr[10];//10行n列
//正常な宣言
//int *arr[10];

    //つまりこう
    int a = 1;    int b = 2;    int c = 3;
    int *arr[3]={&a,&b,&c};//あるいはint *arr[]={&a,&b,&c};
    int x1 = *arr[0];
    int x2 = (*arr)[0];
    int x3 = *arr[1];
    int x4 = (*arr)[1];
    int x5 = *arr[2];
    int x6 = (*arr)[2];
        
    printf("%d\n", x1);//=>1//関節参照
    printf("%d\n", x2);//=>1
    printf("%d\n", x3);//=>2//関節参照
    printf("%d\n", x4);//=>1981336768
    printf("%d\n", x5);//=>3//関節参照
    printf("%d\n", x6);//=>-1572729299    

    //また
    int d[] = {4,5,6};
    int *brr[3]={&a,&b,d};
    int x7 = *brr[0];
    int x8 = (*brr)[0];
    int x9 = *brr[1];
    int x10 = (*brr)[1];
    int x11 = *brr[2];
    int x12 = (*brr)[2];        
    printf("%d\n", x7);//=>1//関節参照
    printf("%d\n", x8);//=>1   
    printf("%d\n", x9);//=>2//関節参照
    printf("%d\n", x10);//=>1981336768
    printf("%d\n", x11);//=>4//関節参照
    printf("%d\n", x12);//=>4



この記事が気に入ったらサポートをしてみませんか?