【独学するコンピュータサイエンス】配列とポインタの知識11
2018年の秋からサーベイしてきた配列とポインタを中心とした話題についてまとめていきます。
1. 文字列とポインタ
char * 型の変数を複数並べたデータを扱うと文字列リテラルへのポインターの配列となり、文字列を扱うことができる
配列の各要素に対して文字列を格納したい場合には、配列をchar*型(char型へのポインタ型)として定義すればよいです
たとえば
ps *[3]
ps は char * 型の要素数 3 の(一次元)配列
C++なら要素数を可変長にしたければSTLのvectorもつかえます
vector
psはchar *型のvector(C++のSTL、可変長配列)
2. ポインタとconst修飾子
const は型を修飾するものです。では int * p; において const はどこに記述するのでしょう。三箇所あり、一箇所ずつ記述したものが以下です。
const int * p;
int * const p;
int const * p;
配列の宣言の読み方を思い出してください。それとあまり変わりません。ただ const は記述直後の型を修飾する事は常に頭に置いてください。
各宣言を順に読み解きます。
const int * p; について。
識別子を見ます。「 p は、」
右を見ます。セミコロンで終わっています。
これまでの日本語を繋げます。「 p は、」
左を見ます。 * があります。「ポインター」
左を見ます。 int があります。「 int 型の」
左を見ます。 const があります。「読み込み専用の」
識別子より右の日本語と、識別子より左の日本語を左順に繋げたものとを、一つに合わせます。
「 p はポインターです。型は int です。そしてそれは読み込み専用です」
整えます。
「 p は int * 型変数です。int 型は読み込み専用です」
「 int 型は」に気を付けてください。const は直後の型を修飾します。ですから int * 型ではなく int 型が読み込み専用になります。これを const を交えて書くと「 p は const int * 型の変数です」となります。 const は直後の型である int だけを修飾するので int は const 修飾されますがポインターを示す * は const 修飾されません。つまり以下の様になります。
int i[] = { 1, 2, 3 };
const int * p = i;
p += 1; /* 良い。p は const int * 型、const 修飾は int だけの為。
そして i[ 1 ] のアドレスを指す事になる /
p = 10; / 不可。p は const int 型の為エラーとなる */
分かり難いでしょうが宣言を一つ一つ丁寧に読み解くしかありません。
次は int * cont p; を説明します。
識別子を見ます。「 p は、」
右を見ます。セミコロンで終わっています。
これまでの日本語を繋げます。「 p は、」
左を見ます。 const があります。「読み込み専用の」
左を見ます。 * があります。「ポインター」
左を見ます。 int があります。「 int 型の」
左を見ます。何もありません。
識別子から右の日本語と、識別子から左の日本語は左順に繋げたものとを、一つに合わせます。
「 p は読み込み専用です。そしてポインターで、型は int です」
整えます。
「 p は読み込み専用の変数です。型は int * です」
これも分かり難いと思います。 p が読み込み専用とは、p の型を考えます。p は int * 型です。従って p の変数そのものが書き込み不可になったのです。例を示します。
int i[] = { 1, 2, 3 };
int * const p = i;
p += 1; /* 不可。p は int * const 型の為 /
p = 10; / 良い。p は int 型の為。
p += 1; の行を無効にすれば i[ 0 ] は 10 になる */
最後の int const * p; は const int * p; と同じです。
訳が分からなくなって来ているかも知れません。ですがもう一つ必要な説明があります。
const int * p; とした場合、p は書き込み可能で、p は書き込み不可です。
int * const p; とした場合、p は書き込み不可で、p は書き込み可能です。
では p も *p も書き込み不可にする場合はどうするのかと疑問が湧いて来ます。答えは単純でこの二つを組み合わせるのです。つまり
const int * const p;
とすれば望みが叶います。最初の const は int を読み込み専用にし、次の const は int * 型である p 自体を読み込み専用にします。
標準関数 puts のプロトタイプ宣言を確認します。
int puts( const char * s );
もうこの意味はお分かりですね。 s は const char * 型で、読み込み専用なのは char です。 s は char * 型ですから s += 1; といった記述は許可され、 *s は const 修飾された char 型ですので *s = 'A'; といった記述はエラーとなります。よってポインターを引数にする場合、この関数ではその内容を書き換えないと宣言する為に const を用います。関数を使う側はその宣言や定義を見れば「内容が書き換わらない事が保障されている」と把握出来ます。
関数の引数にポインターを指定する場合、内容を書き換えない意思表示として const 修飾子を使用すべきです。もし誤ってその関数内で内容を書き換えた場合、コンパイル・エラーが出て実行されないので安全です。ポインターの指し示す内容を書き換える時に限り const を付けない様にします。今後はこの規則に則って本書のプログラムも記述します。
3. プログラムにとって重要なこと
プログラムに於いて最重要な事が意図した動作をするのは云うまでもありません。そして次に大事なのが保守性の高さであり、その内の一つがプログラムの見易さです。
この空白やタブ、改行はプログラムの見易さに影響する例を示したのが以上のソースとなります。もうお分かりですね。プログラムは見易く記述しましょう。本書のプログラムを見て少しずつ覚えてください。
4. バグとは?
バグとはプログラム上の誤りのことをいいます。
それより問題なのが論理的なバグ、即ち文法上問題なくても意図した処理の流れにプログラムが組まれていない場合です。これはコンパイラーには分からないのでコンパイルされ、実行ファイルも作成されます。そして動作させてみると間違った反応を示します。この修正をするには何が問題なのかを追求しなければなりません。
コンパイラーがエラーを出さないバグの修正の方が遥かに困難です。今は分かり易い例でしたが実際にはもっと複雑になってきます。
この様な事があっても注釈がしっかり付記されていれば原因を突き止めるのが容易になる場合は多々あります。バグが出ないようにプログラムを組むのはとても大切ですが、もしバグが発生しても修正を容易にする為に、重要なところや難解な部分は正確な注釈を記述するよう心掛けましょう。誤った注釈はバグの元となり厳禁です。
5. 直値(リテラル)
昔から「直値は使用するな」といわれています。
リテラルとは、コンピュータプログラムのソースコードなどで、特定のデータ型による値を直接表記する際の書式。また、そのような書式に従って記載された値のことをいいます。
文字列リテラルの特徴は文字列リテラルの最後に文字列終端記号(\0)が自動的に格納される事です。
6. 直値と文字列リテラルの特徴
プログラムで書換え不可能な領域に格納されるという事です。規格の記述では「プログラムの開始から終了まで書き換わらない」となっています。直感的には、
hensu = "ABC";やhoge = 24;といった値は変数ではないので、書き換え不可となると考えるといいかもしれません。
プログラムの鉄則に直値を極力記述しないというのがあり、以前説明した EOF に通じるものです。
直値をプログラム内に組み込むと、プログラムの仕様変更があった場合全ての直値を調べ、変更の必要性を確認、必要なら変更、といった手順を踏まなければなりません。これではプログラムを変更した時に変更し忘れや余計な変更をしてしまったりと、新たなバグを発生させ兼ねません。
そこで広域で使用する直値はヘッダー・ファイルに収め、それをインクルードして使用します。これなら変更箇所はヘッダー・ファイルを調べて対応するだけで済みます。
7. 文字と文字列はちがう
文字列ではなく単一の文字です。文字は(一重の)引用符、シングル・クォーテーションで囲んだ一文字が相当します。
(文字列は""ダブルクォーテーションでしたね)'A'がそれで、整数文字定数 (integer character constant)や文字定数と呼びます。
注意して欲しいのは文字列ではなく文字である事。そして文字の実態は int 型の数値である事。この数値は文字コードそのものです。環境によってその文字コードは異なりますが、'A'と記述すればその環境に於ける A の文字コードの int 型の値になります。引用符の中身は一文字の場合のみ動作が決められています。もし二文字以上記述した場合はコンパイラーによってその挙動が決められています。つまり 'ab' や 'あ' といった記述は規格違反ではありませんがその結果はコンパイラーにより異なります。コンパイラーに依らない記述をするのなら引用符の中身は一文字にするのが賢明です。もし複数の文字を記述してもそれはあくまでも数値として扱われます。
文字は数値ですから 'B' - 'A' と云った演算も可能です。但しその結果の値は環境によって異なります。その結果を元に何らかの処理をすると環境依存になってしまうので気を付けて下さい。もっとも日本語を JIS やシフト JIS にて扱う時点で環境依存になっています。
一つだけ規格で定められている事があります。十進数記号に関し '0' が一番文字コードが小さく、その後一加算するごとに '1' 、 '2' 、…、 '9' となっています。従って '0' + 1 の値は '1' と同じになります。
8. 関数の戻り値を変数に格納する方法が 変数名 = 関数名;
なので、こういうのもありです
res = printf("%d","ABCDE");
また、関数そのものが関数の戻り値と同等であることは次のプログラムで確認出来ます。
9. データ型が異なる場合
x と y の型が異なれば通常の算術変換、つまり小さい型は大きい型へと変換されます
10. xまたはyが実数の場合
x または y が実数の場合は両者が等しい事を単純に比較するのは危険です。以前説明した様に丸め誤差が発生するからです。実数を比較する時には整数に型変換してから行うか、それが出来ない状況なら一定の範囲内の値である事を確認する等丸め誤差を考慮する必要があります。実数比較を浮動小数点演算の詳細を知らずに行うと非常に発見し難いバグに繋がります。
11. if文の入れ子の深さ
if 文の中に if 文を入れ、その中に更に if 文を入れ、と多くの if 文を入れる事が出来ます。これを入れ子の深さと呼びます。if 文に限らず入れ子の深さに制限はありますがその前に人間の方が理解不能になります。この様な C 言語の制限は別項にて説明するとして、if 文の深さはせめて四つ以内、なるべくなら二つ以内を目安にしてください。これより深いものは分かり難くなります。
つづきはまた今度書きますね