見出し画像

C言語_char型と文字コードとポインタ配列を理解するための備忘録

C言語のアルゴリズムを勉強していたところ、char型によるアルファベット同士の比較が出てきた。
こんな感じ

char *p = "KSOIDHEPZ";
int index;
if ('A' <= *p && *p<='Z')
  index=*p-'A';

一つ一つ確認してみる。

char *p = "KSOIDHEPZ";

これは、ポインタpを宣言して、そこにchar型の配列
{'K','S','O','I','D','H','E','P','Z'}
を格納する命令だ。
実はポインタには配列を格納することができる。
C言語初級者の私には目からうろこな事実。
実際、次のコードを実行したところ配列と同様の挙動をした。

for (int k=0;k<=7;k++)
  printf("%c\n",p[k]);

実行結果

K
S
O
I
D
H
E
P

つまりpは、p[]={'K','S','O'…}と宣言した配列と同じ挙動?をするわけだ。

'A' <= *p && *p<='Z'

私も混乱した。なぜなら、char型と配列を比較していると思ったからだ。一見するとそのように見えてしまうが、実は違う。
次のコードを実行して*pを調べてみた。

この時点で私はググりまくってアルファベットには文字コードで数字が割り当てられてる云々ということを把握していたのでこのコードをかけました。
アルファベットと数字の話はもう少し後にします。

printf("refer: int %d  char %c\n",*p,*p);

実行結果

refer: int 75  char K

みなさんおわかりかと思います。
実は*pは配列の先頭の要素だったのです。
つまり、pそのものはポインタのような役割を果たしているとわかります。ですが、腑に落ちないことが一つありました。
私の手元にある参考書は、次のコードでpの要素を出力していたのです。

for (i=0;i<8;){
  printf("%c", *p);
  p++;
}

なんと、配列に直接足し算してるではありませんか。
そこで、次はこのコードを使って検証してみました。

test.c

include <stdio.h>

int main(void){
    char *p = "aiueo";// ポインタで宣言    
    char a[] = "AIUEO";// []表記

    for (int i=0;i<5;i++){
        printf("%c ", *a);
        a++;
    }
    
    printf("\n");
    for (int k=0;k<5;k++){
        printf("%c ",*p);
        p++;
    }

}

[]で配列を格納した変数aとポインタ配列は同様に扱えるのかを確かめるコードです。

実行結果

test.cIn function 'main':
test.c:9:10: error: lvalue required as increment operand
         a++;
          ^~

ほうほう。ポインタで配列を宣言した場合と[]表記(a[]="AIUEO";のような表記)とでは挙動がやはり違う。
ということは、あのp++で要素を取り出す参考書のやり方はポインタから宣言した場合限定だ。

ちなみに、少し脱線するが、charの配列ではなくint型の配列だと警告みたいなのが出てきた。

include <stdio.h>

int main(void){
    int *p = {1,2,3,4,5};// ポインタで宣言

    for (int k=0;k<5;k++){
        printf("%d ",*p);
        p++;
    }

}

コンパイル時

test.c: In function 'main':
test.c:4:15: warning: initialization makes pointer from integer without a cast [-Wint-conversion]
     int *p = {1,2,3,4,5};// ・1;35m^
test.c:4:15: note: (near initialization for 'p')
test.c:4:17: warning: excess elements in scalar initializer
     int *p = {1,2,3,4,5};// ・1;35m^
test.c:4:17: note: (near initialization for 'p')
test.c:4:19: warning: excess elements in scalar initializer
     int *p = {1,2,3,4,5};// ・1;35m^
test.c:4:19: note: (near initialization for 'p')
test.c:4:21: warning: excess elements in scalar initializer
     int *p = {1,2,3,4,5};// ・1;35m^
test.c:4:21: note: (near initialization for 'p')
test.c:4:23: warning: excess elements in scalar initializer
     int *p = {1,2,3,4,5};// ・1;35m^
test.c:4:23: note: (near initialization for 'p')

あれれぇー、おっかしいなぁー。

ここで疑問。
なぜポインタ配列はインクリメントで要素を取り出せるのか。
すべての答えはこのサイトに載っていた。
(記事を書いてる途中で発見した。)
ついでに、int型のポインタ配列の正しい宣言の仕方も載っていた。最初にこのサイトに出会えれば…

さっくりいうと、
p[0] と *(p+1), p[1]と*(p+2), … は同じ。
もう少し詳しく言うと、
配列の要素1つにつきメモリ4byteを占めていて、
(これは型によって違う?)
*pはp[0]
*(p+1)は、自動的に*(p+1*4)
*(p+2)は、自動的に*(p+2*4) といった具合で
自動的に配列の次の要素にアクセスできるように変換してくれる。
うーん。わかるようでわからないような…
とりあえず、ポインタ配列は*(p+1)の形で次の要素にアクセスできることだけは覚えておこう。

index=*p-'A';

さて、これについて解説していきます。
解説と言っても私もこれをたったさっき知りました。
まず、indexに代入されるのは数字であり、int型です。
また、*p-'A'は単なる数値計算をしています。
*pはchar型、'A'もchar型です。
つまりchar型同士の引き算です。
前の方に書いたように、これには文字コードが関係しています。
char型は各アルファベットに数字を割り当てていて、
例えばK:75、Y:89、U:85といった具合。
そのため、char型の引き算はこの文字コードに割り当てられた数字を使っているのです。
とりあえず、*pがKなら、'A'の文字コードは65のため、
index=75-65=10となります。
この仕組みを利用すると、文字列を暗号化できたりするらしいです。
面白いですよね。

まとめ

・ポインタには配列を格納することができる。
・ポインタ配列はp++とすることで次の要素にアクセスできる
・char型で演算ができる。
・アルファベットには数字が割り当てられている。

感想

char型の文字コードは、
'A'<'B'<'C'<…のような大小関係になっているから、sort機能なんかに使えそう。
ポインタ配列についてある程度理解できるとポインタそのものへの理解もある程度深まるね。

この記事が参加している募集

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