文字,文字列,配列,ポインタ

データ型の基本

文字 … 計算機中では,数値と同義

C言語では,文字をchar型で表現することが慣例です.

文字 (char型) は,計算機中では単なる数値なので,'A'とは,数値の65を簡単に書く手段でしかありません.

例えば:

  • c = 65」 と 「c = 'A'」 は同じ意味

  • c = 51」 と 「c = '3'」 も同じ意味

  • c = 3」 と 「c = '3'」 は別物(つまり,'3'3は等しくない)

文字と数を対応付ける様々な対応表があります.例えば,ASCIIコードJISコードなどが有名です.

C言語では, ASCIIコードを前提としています.

$ man ascii    # → 対応表を見ることができる

従って,C言語では,65と書く代わりに,'A'と書けます.この方がプログラマにも分かりやすいですね.

Listing 1 文字と数字
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
char c1 = 'A';
char c2 = 65;     // コンパイル時に Warning が出るが,今回は無視してよい.
                  // 詳細は,教科書K&R p.55 を参照.

printf("c1 = [%c]\n", c1);
printf("c2 = [%c]\n", c2);
                  // 同じ"文字"がカッコ内に表示されるはずである.

printf("c1 = [%d]\n", c1);
printf("c2 = [%d]\n", c2);
                  // 同じ"数値"がカッコ内に表示されるはずである.

/* ここでは,ソースコードの一部のみを示している.C言語で書かれたプログラム
   としてコンパイルするためには,自分で書き加える必要がある.*/

まずは,C言語プログラム作成の復習をしておきましょう.

練習:

Listing 1を,C言語で書かれたプログラムのソースコードとしてEmacs で完成させ,Gcc でコンパイルし,実行してください.手順を忘れてしまったという人は,プログラミング演習 FAQを見てください.必要に応じて,教育用計算機のページ からたどれる3. エディタ / Emacs6. Cコンパイラ / gccも,参照してください.

文字列 … 文字を連ねたもの(文字の配列)

C言語では,文字列の終わりを表すために,文字定数'\0'(ナル文字; K&R p46) を付けます.

文字'\0'を表すASCIIコード(数値)は0です.(言うまでもなく,'\0''0'は異なります.)

Listing 2 "ABC" という文字列を配列 s の初期値とする宣言の例
//     文字数+1
//     |
char s[4] = {'A', 'B', 'C', '\0'}; // 0でなく'\0'と書くことで'文字'であることを強調
//        = {65, 66, 67, 0};       // こうも書ける
//        = "ABC";                 // こちらが楽
// char s[] = "ABC";               // 初期値を与えるならば,左辺を省略することもできる

このときのs[]"ABC"の関係をメモリの配置で示すと,以下のようになります.

../_images/p01-pointer-table1.svg

Figure 1 char s[] = "ABC" のメモリ配置

ここでは,sの開始アドレスを2000としていますが,これは,例のために決めた値で,実行環境によって異なることに注意してください.これを見ると,"ABC"は,以下のようにとらえることができます.

"ABC"

メモリ中に4つの数をあらかじめ用意して,その先頭のメモリの番地(アドレス)を表す式.

s

"ABC"の先頭アドレス(2000)を表すラベル.

つまり,この例では,"ABC"sは,2000と同義です.ここで注意して欲しいのは,

  • 文字列"A"と 文字'A'は別物である

ということです.以下の点において両者は異なります.

  • 'A'1つの整数(65) だが,"A"'A''\0'2つの要素を持つ配列

  • 'A'整数だが,"A"はアドレス(例では 2000)

"ABC"は,単なるアドレス(ポインタ)なので,以下のような記法が許されます.

"ABC"[0]     // == 'A' == 65
 *("ABC"+2)  // == 'C' == 67

配列 … データの並びの先頭に名前を付けたもの

以下のように配列を宣言したときに,

char s[] = "ABC";
../_images/p01-pointer-table2.svg

Figure 2 char s[] = "ABC" のメモリ配置2

s"ABC"(2000番地) に付けられたラベル(定数)であると捉えることができます.つまり,以下のことが成り立ちます.

s[0] == "ABC"[0] == 2000[0] == 65 == 'A'  // どの値も同じ

sは,単なるラベルですから,2000 自体がどこかに(メモリのContentsとして)保存されている訳ではありません.

ポインタ … メモリの番地(アドレス)を保存する変数

今度は,配列と同じように扱われることが多いポインタについて見てみます.配列の場合とほぼ同等と思われる宣言は,以下の通りです:

char *p = "ABC";

これについて,メモリの内容を図(左)で表すと,

../_images/p01-pointer-table3.svg

Figure 3 char *p = "ABC"; (左) と char s[] = "ABC"; (右) の比較

となります.配列の場合(右)と,同じように見えますが,配列とは,内部表現が違いますね.変数は,アドレスに付いたラベルであると考えると,

  • pは,アドレスを保存する変数で,2004番地にラベル付けされており,pの値は,2000 である.

  • sは,2000番地にラベル付けされているが,単なる定数なので,s自体を変更できない.

つまり,p = p + 1は可能ですが,s = s + 1はできません.s[0] = s[0] + 1は当然 OKです.言い換えると,s[]演算子付けて,その指すメモリの中身を使った時に,はじめて意味を持つ定数です.

ポインタと配列

ポインタに関連する演算子 (* と &)

以下の宣言について,メモリの図を書いてみよう.

Listing 3 ポインタ,配列,変数の比較
1
2
3
char *p  = "ABC";
char s[] = "DEF";
int  a   = 3;

例えば,以下のFigure 4のようになります:

../_images/p01-pointer-table4.svg

Figure 4 ポインタ,配列,変数の比較

このとき,変数の参照,方法には,以下の3つがあることをおさえておきましょう.

  • そのまま書く →a

  • *を付ける →*a

  • &を付ける →&a

では,それぞれについての意味をおさらいします.

Figure 4は,以降の説明で何度も参照します.あらかじめ,右クリック→画像を新しいタブで表示,タブをウィンドウ化,をしておくと見やすいでしょう.)

(1) そのまま書く

つまり,変数aaとそのまま書いて参照した場合です.この場合は,そのラベル(アドレス)にあるメモリ内容をそのまま返します.

表記

解釈

a

3

int

aが持つ値を返す

p

2000

char*

pが持つ値を返す == 2000

s

2008

char[4]

4つ全部を一度に返せないので,代わりに先頭アドレスを返す(&と同じ効果を持つ)

ただし,配列sは,特別で,以下の性質があります.

  • C言語の式は,1つの値を返すことしかできないのに対して,配列は,複数の値を持つため,全部を一度に値として返せない.

  • そこで,配列に関しては,代わりに先頭(ラベル)のアドレス (2008) を返す.

  • これは,"ABC"2000を返していたのと同じ理由.

sは,図の例でいうと,{68, 69, 70, 00}の4つの整数からなる配列を値として持っています.この 4つの値をsの値として返すことができれば,apの挙動と統一が取れ,スッキリします.しかし,C言語では,式は,1つの数を返すことになっているので,{68, 69, 70, 00}を返すことはできません.(構造体は,ちょっと違う動きをしますが,ここでは説明しません)したがって,sの先頭アドレスである 2008 を代表として返すようになっているのです.

この性質のために,ポインタと配列が同等に見えます.つまり,p(=2000)もs(=2008) もアドレス(ポインタ) で,それに対して*演算子や[]演算子を適用することで,中身(ポインタの指す先)を取り出すからです.実は,s[1]は,*(s + 1)を分かり易く配列的に書いたに過ぎません.(K&R P.120)

(2) *を付ける

変数の値が示すアドレスに入っている値を返します.つまり,「(1) そのまま書く」で返って来る値 (3, 2000, 2008)をアドレスとみなして, そのメモリの中身を返すというわけです.

表記

解釈

*a

3

-

*3 … 何があるか分からない(segv になることも)

*p

'A'

char

*2000 … 'A'

*s

'D'

char

*2008 … 'D'

整数a(= 3) をアドレスとみなして,*aとすると,3番地の中身を読みにいこうとします.通常,3番地の中身は,OSが読み出すことを禁止している領域ですから,*aを実行したとたんにOSによって,強制終了させられるでしょう.(Segmentation faultを経験したことがあるでしょう)

また,p[1]は,*(p + 1)を分かりやすく書換えたに過ぎません.以下のような等式が成り立ちます.

*(p + 1) == p[1] == *(2001) == 'B'
s[0] == *(s + 0) == *s == 'D'
"ABC"[2] == *("ABC" + 2) == *(2 + "ABC") == 2["ABC"] == 'C'

この記法を自由に行き来できますか?

(3) & を付ける

これは,その値が格納されているアドレス(変数のラベルの値)です.

表記

解釈

&a

2012

int*

3が格納されているアドレス

&p

2004

char**

2000が格納されているアドレス

&s[0]

2008

char*

68が格納されているアドレス

&s

2008

char (*)[4]

68, 69, 70, 00 が格納されているアドレス(&s[0]と同じ値だが型が違う)

「(1) そのまま書く」の所で,sをそのまま式に記述した場合は,2008 が返ってくると説明しました.また,2008 そのものがどこかに格納されているわけではないとも説明しました.そのため,&sは,sの値(2008)が格納されている場所?? … は,「なし」という混乱をしないでください.

sの値は,本来68, 69, 70, 00からなる配列という解釈であることに注意しましょう.従って,&sは,{68, 69, 70, 00}が格納されているアドレスになります.

また,s&s[0]は,同じ値のアドレスを指していますから,どちらの書き方でもいいのでしょうか?実は,ポインタとして扱う際の違いがあります.詳しくは次回の解説で説明することにします.

関数とポインタ

(教科書K&R 5.5節「文字ポインタと関数」, pp. 127-131 も参照のこと)

関数の呼び出しは,以下のように実行されます.

  1. 呼び出し側の実引数の値を評価(evaluate)

  2. 評価されたを関数の仮引数(変数)にコピー

  3. 関数本体を実行

  4. return 文で値を返す

C言語の関数は,値を受渡しすることで呼び出すだけ,です.これを値渡し (call by value) といいます(K&R 1.8節; pp.34-35).

関数を介して,何らかの変数の値を書き変えたい場合はどうすればいいのでしょうか?上記手順のSTEP 2で得られた,(コピーされた)仮引数の値を書き変えればいいのでしょうか?プログラミング言語によっては,参照渡し (call by reference) という機能により,実現することもできます.

C言語の関数には,参照渡し,はありません.そこで,参照渡しと同等の処理を実現するため,引数に与える値としてアドレスを渡して,ポインタとしての間接的な参照や代入を行います.

注釈

Fortran は,基本的に参照渡しとなるプログラミング言語です.その他,C++ や Pascal などの言語では,値渡しと参照渡しを使い分けることもできます.参考:Wikipedia - 引数#評価戦略

例えば,以下のプログラム例を見てください.

Listing 4 実引数として与えられた変数を書き変える関数(とそうならない関数)の例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void func1(int *x)
{
   *x = 100;
}

void func2(int x)
{
   x = 100;
}

void func3(int *x)
{
   x = 100;
}

int main(void)
{
   int value = 1;

   printf("value = %d\n", value);

   // func1(&value);
   // func2(value);
   // func3(&value);

   printf("value = %d\n", value);

   return 0;
}

QUIZ(参考)

  • func1()func2()func3()が,それぞれ何をしているか,説明できますか?

  • main()関数のコメントアウトを一部消して,func1(),func2(),func3()をそれぞれ実行した場合に,変数valueに何が起こるか,説明できますか?

同様のトピックとして,教科書K&Rの5.2節でswap()関数を利用した例も紹介されています.こちらも,十分に理解しておくようにしてください.