基本事項
次のページから、C言語の解説に入りますが、このページで言及されることを知っている前提で、話を進めていきます。
さっと読んで、よく知らない内容があった場合は読んでおくことをおすすめします。
学べる事
学ぶこと-前半:
graph TD
A[プログラム実行] --> B[関数]
B --> C[変数]
B --> D[main関数]
C --> E[データ型]
C --> F[初期化]
E --> G[整数型]
E --> H[浮動小数点型]
E --> I[文字型]
D --> J[引数]
D --> K[返り値]
後半:
graph TD
L[プリプロセッサ] --> M[#include]
M --> N[標準ライブラリ]
N --> O["stdio.h"]
O --> P[printf]
O --> Q[scanf]
O --> R[fgets]
O --> S[putchar]
O --> T[getchar]
データ型
C言語には、データ型(型)というものが存在しています。
主に、ある値(データ)がどのような種類か、またそのサイズなどを知るために使用されています。
例えば、132なら整数。10.2なら小数。'a'なら文字のようなものだと分かります。
すべてを挙げることはできませんが、以下は主なデータ型の例です。
(サイズはプラットフォームによって異なることがあります)
| データ型 | 種類 | サイズ | 範囲 |
|---|---|---|---|
| int | 整数 | 32bit | -2,147,483,648 ~ 2,147,483,647 (-2^31 ~ 2^31-1) |
| unsigned int | 整数 | 32bit | 0 ~ 4,294,967,295 (0 ~ 2^32-1) |
| short | 整数 | 16bit | -32,768 ~ 32,767 (-2^15 ~ 2^15-1) |
| unsigned short | 整数 | 16bit | 0 ~ 65535 (0 ~ 2^16-1) |
| long long int | 整数 | 64bit | -2^63 ~ 2^63-1 |
| char | 文字 | 8bit | -128 ~ 127(多くの場合) |
| float | 浮動小数点数 | 32bit | 3.4E +/- 38 (7 桁) |
| double | 浮動小数点数 | 64bit | 1.7E +/- 308 (15 桁) |
これらを暗記する必要はありません!
正確には、暗記しなくてもどのような値をもつデータ型かを理解できるほどコードに慣れて貰えると嬉しいです。
上の表には普段あまり使わないものもあるので、違いを理解してそのようなデータ型が存在することを認識してもらうだけで結構です。
リテラル
C言語のプログラム内に値を直書きしたとき、これらは リテラル と呼ばれます。
プログラム中に、例えば100と書いたとき、これは 整数リテラル で、基本的にint型になります。
また、12.3と書いたとき、これは 浮動小数点リテラル で自動的にdouble型になります。
いろいろ書きましたが、結局は「データの種類を表すのがデータ型である」と理解してもらえれば結構です。
ただ、コンパイラはプログラム中に直書きされた57という数字を見つけたとき、
「int型で4バイト(注1 32bit)の整数があるじゃん」
と認識するということですね。
なぜ、プログラム中に直書きした12.3がよりサイズの小さいfloatではなくdoubleになるのでしょうか?
現代のCPUでは、floatもdoubleも計算の速度はほぼ同じです。
普通のプログラムでは明示しない限り、より精度の高い小数を扱えるならdoubleの方が良いと考えられています。
これは、Rustという言語でも同じで、浮動小数点リテラルはf32(32bitの浮動小数点数)ではなくf64(64bitの浮動小数点数)になります。
あえて、floatを使う例を挙げるなら、特に一つの変数に細かい精度が重要でない、大量に変数が必要となるシミュレーションや機械学習などのプログラムは32bit以下の精度の浮動小数点数が使われていることが多いです!
補足ですが、floatのリテラルを使いたければ、12.3fと書くことができます。
変数の宣言
C言語では、変数 というものがあります。
int main() {
// データ型 識別子;
int x;
}
このCコードで、xは変数です。
また、これは変数xを 宣言 しています。
あえて、「変数は箱である~」のような陳腐な表現を避けて説明します。
詳しくは今後話しますが、あなたがこれを読んでいるコンピューターにはメモリ(主記憶装置, RAM)というものが積まれていますね。
変数の宣言がされると、データが配置できる特定のメモリの場所から、データ型から分かるサイズの量(この例では、intで4バイト)、この変数名xと結びつけて使うよ、ということになります。
ですので、この変数xを通してint型の4バイト分を使用することができるということですね!
(実際には、使われていない無駄な変数が宣言されないようになったり、より高速化するためにCPUの記憶装置(レジスタ)のみにデータが配置されることがあります)
ですが、メモリのある場所を使うということを宣言しただけです。
本当に、この変数の中身が何が入っているか分からないのです。
これは不定の値なので、プログラムでは基本的に危険です。
以下のように安全な値にしてあげたいですね!
int main() {
// データ型 識別子;
int x;
// 識別子 代入演算子 式;
x = 0;
}
代入
変数に値を入れることを、代入 といいます。
それらは代入演算子=を用いて行われます。
x = 0だと、xに0を代入(書き込み)するということになります。
また、この中身の分からない変数xのような、不定な変数 の中身に値を代入し、どのような値か分かるようにすることを初期化 といいます。
識別子
補足すると、上記コードのコメントに書かれている 識別子(Identifier) は変数名と読み替えてもらってもこの例では構いません。
これは、ユーザーがC言語のキーワード(e.g. if, while, return, ...)に被らない範囲で、自由に定義できる名前のようなものです。
コンパイラがプログラムの文脈の中で、特定の識別子が他のものと同じであることを、判別するために 識別子 という名前になっています。
ここでは、宣言したxと、0という値が代入されているxが同じものであると識別されるということです。
私たちが書くのは、ただの文字列に過ぎませんが、コンパイラはそれを意味づけるのです。
ですので、代入においては、コンパイラがある変数の識別子を知っていればその情報を元に処理をしますし、知らなければ宣言されていない変数としてエラーを出すということです。
また、先ほどのコードは以下のように書くことができます。
int main() {
// データ型 識別子 代入演算子 式;
int x = 0;
}
少し纏まりましたね!
実は、識別子は多くのプログラミング言語で、数字から始めることができません。
(もちろん、C言語もその仲間です。)
もしもあなたがプログラミング言語が好きで仕方が無く、向上心があるのであれば、コンパイラ(言語処理系)の実装をしてみると良いかもしれません。
なぜ、多くの言語で識別子が数字から始めることができないのかが分かるはずです。
ネタバレ: 面倒だから (多くの場合)
関数
C言語において、 関数 はコンピューターが実行する命令や、手続き(プロシージャ)が書かれたルーチンを表します。
すなわち、「特定の処理をひとまとめにして名前をつけたもの」と言うことができます。よく「料理のレシピ」に例えられます。例えば、「カレーを作る」というレシピがあれば、その手順に従うことで誰でもカレーを作ることができますね。
関数も同じように、「これこれをしたら、こうなる」という処理の手順(手続き)が書かれていて、その手順に名前が付けられています。関数を使うことで、同じ処理を何度も書く必要がなくなり、プログラムがすっきりして読みやすくなります。
(すべて関数が、読みやすくするために、また何度も書かないでよくするために存在するという訳ではありません)
main関数
関数はmain関数とそれ以外のものに分けることもできます。
main関数は、mainという名前が付けられた以下のような特別な関数です。
int main() {
return 0;
}
ここで、main関数はプログラムの最初に実行される(注2)、エントリポイント で、プログラムの中心となる メインルーチン になります。
ですので、main()でない関数はすべて、メインルーチンから呼び出されるサブルーチン になるということです。
特に、そのサブルーチンは、プログラマーが定義する ユーザー定義関数 と、C言語に元から用意されている関数を (標準)ライブラリ関数 といいます。
ユーザー定義関数のコード例は以下のようなものです。
// データ型 識別子(データ型 変数名, …)
int add_one(int a) {
return a + 1;
}
これは add_one という名前の関数です。この関数は、int 型の数を受け取って、それに 1 を足した結果を返します。
関数の構成要素
関数名: add_one のように、関数の名前です。
引数: 関数に渡す値です。add_one 関数では int a が引数で、呼び出し元からの整数を受け取ります。
戻り値: 関数が返す値です。add_one 関数では、a + 1 の計算結果が戻り値です。
データ型: int add_one(int a) の最初の int は、この関数が返す値のデータ型(この場合は整数)を表します。
関数名
関数において、関数名も識別子です。
呼び出したい関数と実際に定義されている関数はもちろん同じものでないといけないですね。
コンパイラは、呼ばれている関数が本当に定義されていて、どのようなものかを特定したいのです。
もちろん識別子なので、先ほどの変数の識別子のように、数字から始めることはできませんし、キーワードと被ってはいけないということです。
引数
// データ型 識別子(データ型 変数名, …)
int sum(int x, int y) {
return x + y;
}
上記は関数の宣言で、最初のintと書かれたデータ型は、この関数が返す値のデータ型を表しています。
また、()内に書かれた(int x, int y)は引数(仮引数)というものです。
このコードでは、仮引数としてint型の変数xとyを 仮に定義 しています。
これは、intの変数として、別の関数から呼び出されることを仮定して 記述しています。
ですので、このsum()を呼び出すときには、必ずint型の値を二つ入れる必要があります。
まとめると、sum()は、intの変数を二つとり、その変数の値を合計してintを返す関数ということです。
関数呼び出し
先ほどのコードを 関数呼び出し してみましょう。
int sum(int x, int y) {
return x + y;
}
int main() {
int result = sum(1, 2); // 3
printf("%d\n", result); // 表示!
return 0;
}
このコードで、sum()は1と2を受け取ります。
sum()内で仮に定義された、xとyとが足されて呼び出し元に返却されるという処理が、引数として渡された1と2に適用されるので、3と評価されます。
もちろん、私たちが最初に実行した、Hello, World!プログラムで使用したprintf()も関数であり、関数呼び出しをしているということです。
後で詳しく見ていきますが、printf()内では文字列が最初の引数にくることを仮定して定義されているため、呼び出す際には"Hello, World!" などと()に文字列を入れる必要があるということです。
(正確には、printf()は引数にフォーマットしたい文字列と可変長引数が仮に定義されている)
注意!
int add_one(int a) { // 呼び出し元の x と同じ変数名にする必要はない
return a + 1;
}
int main() {
int x = 0;
int y;
// 0 + 1 の結果がyに代入される
y = add_one(x); // add_one() を使うためには、intの値を入れる必要がある (e.g. x, 0, 123)
printf("%d", y); //Output: 1
return 0;
}
上記コードでは、xを0で初期化し、宣言したyに値を代入しています。
また、add_one()は先ほど定義したものを呼び出しています。
呼び出し元では、add_one()の関数定義で宣言した仮引数aと同じ名前にする必要はありません。
なぜなら、ある関数の宣言された引数や変数の識別子は、その関数内でしか使うことができませんし、他の関数の識別子が、宣言したものと勝手に衝突するようなことは、あってほしくないですね。
ですが、add_one()はint型の変数を使って1を足すという処理をしているので、呼び出し元で同じint型の変数を引数として呼び出さ なければならない ということです。
別の例を見てみましょう!
返り値
// データ型 識別子 (引数)
double multiple(double left, double right) {
return left * right; // 掛け算して返す
}
この関数の宣言で、return ~; は~の部分に、呼ばれた元の関数にどのような値を戻すか を記述します。
これを、返り値(戻り値) と言います。
先ほど言いましたが、この関数の宣言で、頭に書かれるデータ型(double)は、この関数が返すデータ型を表しています。
つまり、return で返す返り値と、関数の宣言で返すと書いたデータ型は一致させなければなりません。
もし、関数の宣言でintを返すと書いているのに、関数の宣言で浮動小数点数(小数)を返そうとしていたらびっくりですね!
// ↓int型を返す関数であると宣言
int return_int() {
return 1.1; // double型...? (呼び出し元は、int型が返ってくると思っている)
}
ですので、呼び出す側も、返す側も同様にデータ型は一致させる必要があります。
このコードはコンパイラによって、警告が出ると思います。
これは、大きな文法上の問題は無いが、問題のあるコードの記述に対して行われるものです。
最近のコンパイラは親切なことが多いので、エラーや警告のメッセージを読んで理解するようにしましょう!
補足
補足ですが、returnが実行された時点でその関数は終了し、呼び出し元に処理が戻されます。(main関数でも同様)
int routine() {
return 0;
//--- main関数であっても、ここから下のコードは実行されない
printf("表示して!"); // ※表示されない
return 123; // return の数に制限はありません(このコードでは実行されませんが)
}
このコードでは、returnの下に処理が書かれています。
returnが実行されていなければ、表示して!と標準出力されるはずですね。
ですが、条件分岐していようとなんだろうと、結局はreturnが実行された時点でその関数は終了します。
(void)って何?
main()の定義にint main(void) {と書いてあるプログラムを今後見るかもしれません。
(void) は関数の引数が何もないことを表します。
まず、C言語の古い言語仕様(C90など)には、引数がない場合、voidを書くことが推奨されていました。
最近のCコンパイラもvoidがなくてもコンパイルエラーにはなりません。
かなり古いコンパイラを使うという場合でなければ、今ではお好みということです。
課題 1
int型の仮引数を1つ宣言し、それを二倍して、呼び出し元に返却するdouble_int()を実装してください。
Hello, World! 2
最初のHello, World!のコードに戻ってみましょう!
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
ここで、main()はintの0を返却するメインルーチンです。
0を返却する理由は、プログラムが正常に終了したことを示す標準的な方法とされているからです。
これは、OSがこのプログラムを実行したとき、main関数の戻り値をプログラムの終了コードとして利用することに由来します。
多くのOSでは、0を返すと「正常終了」を意味し、0以外の値は「異常終了」を示すことが多いです。
ちょっとしたお約束のようなものですね!
プログラムの最初には、printf()を用いて"Hello, World!"を 標準出力 しています。
これについては、少し後で説明します。
基本的には、実行したプログラム(e.g. Windowsならコマンドプロンプトなど)に内容が引き継がれ、それに表示してもらうことになっています。
ですので、この場合では"Hello, World!"が表示されますね!
プリプロセッサ指令
#includeについて説明していませんでした。
このような#から始まる記述を、 プリプロセッサ指令 といいます。
#includeは、主に、関数の宣言、マクロ、構造体定義、定数などを他のソースファイルから取り込むために使われます。
すなわち、他のプログラムで宣言された内容を、自分のプログラムで使用できる(呼び出せる)ということです。
なぜ、これが必要かというと、printf() は #include <~> をしている、stdio.h に含まれているからということですね。
標準ライブラリ
#include <stdio.h> は、C言語の規格に合わせて実装された、便利な関数が纏めて実装されている、
標準ライブラリ の"stdio.h"を使用しています。
stdio.hの"std"は、Standard(標準)の略で、"io"は Input/Output(入出力) を表しています。
ですので、これをインクルードする(含める)ことで、標準ライブラリの入出力関数を扱うことができます。
いつか詳しく話すかもしれませんが、I/O(Input/Output) はコンピューターの仕事で、プログラムからはそれにお願いをしなければなりません。
実行しているプログラムからOSに行うお願いを、System call(システムコール) などと言います。
では、あえてstdio.hを使わずにHello, World!してみましょう。
void main() {
// 出力する文字列
const char message[] = "Hello, World!\n";
const long message_length = sizeof(message) - 1;
// writeシステムコールを呼び出す
asm volatile (
"mov $1, %%rax\n" // write のシステムコール番号 (1)
"mov $1, %%rdi\n" // 標準出力 (ファイルディスクリプタ 1)
"mov %0, %%rsi\n" // 書き込む文字列のアドレス
"mov %1, %%rdx\n" // 書き込む文字列の長さ
"syscall\n" // システムコールを呼び出す
:
: "r"(message), "r"(message_length) // 入力オペランド
: "%rax", "%rdi", "%rsi", "%rdx" // 破壊されるレジスタ
);
// exit システムコールを呼び出す
asm volatile (
"mov $60, %%rax\n" // exit のシステムコール番号 (60)
"mov $0, %%rdi\n" // 終了コード (0)
"syscall\n" // システムコールを呼び出す
:
:
: "%rax", "%rdi" // 破壊されるレジスタ
);
}
あまり考えたくないコードですね...
これはx86-64のLinuxでしか動作しません(注3)。すなわち、
- システムコールを直接呼び出しているため、Windowsなどには存在しない
writeのシステムコール番号を使用しています。 - CPUがx86-64アーキテクチャに対応していることも必要です。(e.g. AndroidスマホのAArch64やAArch32のCPUでは動きません)
- OSが64ビットモードで動作していることも必要で、32bitのLinux(x86)では動きません。
ですので、異なる仕様間で便利にI/Oを用いるプログラミングをするために、stdio.hが存在しているということです。
これらの異なる仕様をすべて吸収するために標準ライブラリを書いてくれた方々に感謝しなければいけませんね!
標準入出力関数とは
標準入力/標準出力とは、プログラムに明示せずとも標準の入力先/出力先になっているものです。
プログラムで実行された標準入力・標準出力は実行されたソフトウェアに引き継がるようになっています。
例:
- コマンドプロンプト(cmd.exe)で私たちがコンパイルした、"Hello, World!"を 標準出力 するプログラムを実行する。
- 実行中のcmd.exeに標準出力が委ねられ、標準出力の内容を表示しようとする。
- ディスプレイに表示!
なので、printf()を使って、標準出力をしてもその先は実行環境任せということですね!
stdio.h
C言語のstdio.hは、標準入出力ライブラリで、多くの便利な関数を提供しています。
以下に主な関数を説明します。
今は上の二つあたりを知っておくだけで大丈夫です。
また、難解な引数や返り値の定義も今は無視してください!
-
printf (const char* format, ...)
概要: 標準出力にフォーマットした文字列を出力します。
返り値:int
使用例:printf("Hello, %s!\n", "World"); -
scanf (const char* format, ...)
概要: 標準入力からフォーマットに従ってデータを読み込みます。
返り値:int
使用例:scanf("%d", &number); -
fgets (char* str, int count, FILE* stream)
概要: バッファサイズを指定して安全に文字列を読み込みます。
返り値:* char
使用例:fgets(buffer, sizeof(buffer), stdin); -
putchar (int ch) / getchar ()
概要: 単一の文字を出力または入力します。
返り値:int
使用例:putchar('A');/char c = getchar();
まとめ
- データ型とリテラル:
- C言語には、値の種類とそのメモリサイズを決定するデータ型があり、整数型、浮動小数点型、文字型などが存在します。プログラム中に直書きされた値(リテラル)は、型が自動的に設定されます。
- 変数の宣言と初期化:
- 変数を宣言して使用するには、データ型と変数名(識別子)を指定します。初期化しておくことで予期せぬエラーを防ぐことができます。
- 関数の宣言:
- C言語では、処理手順をまとめた関数を使用します。関数には引数と返り値があり、特定のデータ型を持つ値を返します。プログラムのエントリポイントであるmain関数を中心に構成されます。
- プリプロセッサ指令:
#includeを用いることで、外部の標準ライブラリやヘッダファイルから関数などを利用できます。stdio.hのような標準ライブラリには入出力処理に必要な関数が含まれています。
次のページでは、変数の宣言を具体的に学んでいきましょう!
課題 2
- C言語の演算子の種類について調べてください。
printf()やscanf()で使用される、C言語のフォーマット文字列について調べてください。- 以下のコードの未実装な部分を埋めるように、変数の内容を入れ替えて標準出力するプログラムを書いてください。(
ヒント: 変数の宣言が必要です)
#include <stdio.h>
int main() {
int a = 1;
int b = 5;
printf("a: %d, b: %d", a, b); // a: 1, b: 5 が表示される
// 未実装!
printf("a: %d, b: %d", a, b); // a: 5, b: 1 が表示されるはず
}
- 一つ上の課題を関数に切り出し、二つの
int変数を入れ替えて出力する関数にしてください。
#include <stdio.h>
int swap(int x, int y) {
// 未実装!
display(x, y); // 表示!
return 0;
}
int display(int x, int y) { // ただint型の二つの値を受け取り、表示する
printf("x: %d, y: %d", x, y);
}
int main() {
int x = 1;
int y = 5;
display(x, y);
swap(a, b);
}
注釈
注1
8bit = 1オクテット を 1バイト と表現しています。
C言語標準では、そもそも1バイトはオクテットに限らないですが、ここでは 1バイト としておきます。
注2
実際のC言語仕様には、main関数が必ず最初に実行されるわけではありません。
組み込みシステムなどのOSを使用しない環境、すなわちfreestandingな環境ではmain関数から始める必要はありません。
逆に、OSを使用するhostedな環境では、main関数がエントリポイントになります。
注3
面倒なコンパイラオプションを避け、みなさんがそのまま実行できるように、あえて hosted なC言語(main関数)を使用しているため、exit システムコールを呼ぶ必要はまったくありません。