C言語ワークショップ
C言語ワークショップに来てくれてありがとうございます! このワークショップではこのページにかかれている内容をもとに説明を行います。またオンラインでのコードチェックを行える環境を整えているのでこちらで確認してみてください!
採点サーバ: https://cworkshop.s.ihavenojob.work
入出力
プログラムを作成するうえで入出力はとても重要な役割を果たします。
このワークショップでは基本的にprintf
とscanf
しか用いないため授業の内容と被ってしまうかもしれませんが復習していきましょう。
入出力の種類
入出力には様々なものがあるのでいくつか紹介します。ちなみに()の中には C 言語からそれらを扱う場合に必要なキーワードを書いています。
出力には次のようなものが挙げられます。
- 画面への文字の表示
- 画面への図形の表示(curses?)
- ファイルの保存 (fopen)
- ネットワークへの送信 (socket)
- etc...
入力には次のようなものが挙げられます。
- キーボードからの文字入力
- マウスからの座標入力
- ファイルの読み込み
- ネットワークからの受信 (socket)
- etc...
これらの入出力を自在に操ることで世の中のプログラムは動いています。
printf
printfは画面に文字を表示するための関数です。
使用例:
int main(){
printf("Hello World\n");
}
変数の出力
整数型のint
はprintfの文字列の中に%d
を入れることで表示することができます。
int main(){
int a = 10;
printf("a = %d\n", a);
}
他にも浮動小数点の出力には%f
,文字列の出力には%s
など他にもいくつか使用できます。
入力
標準入力から入力を受けとります。キーボードで入力を行い、Enter を押したところまでを取得してくれます。また、%d
などの表現を用いることで整数などへの型変換も行ってくれます。
数値を受け取ってその倍の値を出力するプログラムの例:
int main(){
int n;
scanf("%d", &n);
printf("%d\n", n);
}
入力演習
制御文
プログラムを書くうえで条件によって表示する内容を切り替えたり、似たような処理を何度も行うことが必要になります。 そこで必要な法文について学んでい行きましょう。
今回出てくるキーワードは次のようになっています。
- if 文
- while 文
- do-while 文
- for 文
if 文
If 文を用いるとプログラム中にとある条件に応じて分岐を行うことができます。
if
次の例では printf の内容が表示されます。
int main(){
if (4 > 3) {
printf("4 は 3 より大きいです。\n");
}
}
次の例では何も表示されません
int main(){
if (1 == 2) {
printf("1 と 2 は等しいです。\n"); // !!!表示されない!!!
}
}
else
条件に当てはまらなかった場合について書きたい場合は次のようにもかけますが、面倒ですよね
int main(){
if (1 == 2) {
printf("1 と 2 は等しいです。\n"); // !!!表示されない!!!
}
if (1 != 2) {
printf("1 と 2 は等しくありません。\n"); // 出力される
}
}
そのような場面でelse
を使います。
int main(){
if (1 == 2) {
printf("1 と 2 は等しいです。\n"); // !!!表示されない!!!
} else {
printf("1 と 2 は等しくありません。\n"); // 出力される
}
}
このように書くことで if の条件に当てはまらなかった場合について記述することができます。これは見た目が良いだけでなくどちらか片方だけを変えてしまったときに、あとからとても気が付きにくいバグになってしまうことを予防することにも繋がりますね。
else if
次のような書き方で複数の条件について確認することができます。
int main(){
if (1 == 2) {
printf("1 と 2 は等しいです。\n"); // !!!表示されない!!!
} else if (1 == 3) {
printf("1 と 3 は等しいです。\n"); // !!!表示されない!!!
} else {
printf("1 と 2, 1 と 3 は等しくありません。\n"); // 出力される
}
}
入力の値で分岐
int main(){
int input;
scanf("%d", &input); //値を入力
if (input == 1) {
printf("入力は1です!\n");
} else if (input == 2) {
printf("入力は2です!\n");
} else {
printf("入力は1でも2でもありませんでした\n");
}
}
条件の取り扱い
C 言語では条件の表現が少し特殊です。条件は数値として表され、0 は偽、それ以外は真を表すようになっています。そのため、次のような条件は常に真となります。
int main(){
if (1) {
printf("真\n"); // !!!表示される!!!
} else {
printf("偽\n");
}
}
今後このような表現を見かけることもあるかと思うのでその時は条件が 0 かそれ以外かで表現されていることを思い出しましょう。
余談 else if は存在しない
C 言語の規約にはelse if
という構文は存在しません。説明にもあった通りつける構文なのになぜでしょう?
if 文は次のような定義がされています。
if (条件) 文
// もしくは
if (条件) 文 else 文
つまりこのように見えていたelse if
文は
int main(){
if (...) {
...
} else if (...) {
...
} else {
...
}
}
実はこのような解釈がされているのです。
int main(){
if (...) {
...
} else {
if (...) {
...
} else {
...
}
}
}
実用上は特に気にすることはないので余談としていますが面白くないでしょうか?
参考:Stackoverflow: Does an else if statement exist?
ループ
ループはプログラミングの基本です。面倒な反復タスクは人間よりもコンピュータにやらせたほうがよっぽど効率的だからです。それでは C 言語での使い方を見ていきましょう。
ループを用いたプログラムを作成するとたまに無限ループに陥ってプログラムが止まらなくなります。その際に特定のキーを押すことでプログラムを強制的に止めることができます。
- Mac:
control
キーを押しながらc
- Windows:
Ctrl
キーを押しながらc
- Linux: 機種に合わせてどちらか
while 文
while 文は条件が真の場合繰り返すといった構文になります。
10 回"C 言語ワークショップ"と出力するプログラムは次のようにかけます。
int main() {
int count = 0;
while (count < 10) {
printf("C言語ワークショップ %d", count);
count++;
}
}
新しい内容がいくつか増えていますが安心してください。次のようなステップでループは実行されています。
int count = 0;
はcount
という変数を用意し、0 で初期化していますwhile (count < 10)
でcount が 10 未満である場合に繰り返すようにしています- "C 言語ワークショップ"と一緒にすのループのカウントが表示されます
count++;
でcount
変数に 1 を加算しています
このように書くことで 10 回繰り返すということが達成できます。
do-while 文
do-while 文は一度内容を実行したあとでループを行うか確認するような法文となっています。 一度は実行しなければいけないループや一度実行したあとでしか条件がわからない場合に使用されるケースが見られます。要するに条件を確認するタイミングの違いです。
先程の例と同じものを書いています。
int main() {
int count = 0;
do {
printf("C言語ワークショップ %d", count);
count++;
} while (count < 10);
}
for 文
for 文は最も多く使われるようなループの方法ですが、少し書き方がややこしくなっています。先程の例を書き直してみましょう。
int main() {
for (int count = 0; count < 10; count++) {
printf("C言語ワークショップ %d", count);
};
}
少し分かりづらいですよね。 解説すると次のようになっています。
for(ループに用いる変数の初期化; ループの条件; ループの変数更新)
これまで while で行っていた内容をひとまとめにしたものですね。
余談 -ループ変数の宣言はは for の外側か内側のどちらで行うべきか-
授業資料で for を見ると次のように書かれているのではないでしょうか?
int main() {
int count; //これ
for (count = 0; count < 10; count++) {
printf("C言語ワークショップ %d", count);
};
}
この書き方と今回の資料で用いた書き方のどちらが良いかという話です。
少し深い話にはなってしまいますがプログラミング言語の多くにはスコープという概念が存在します。ある変数がどこから見れるかということです。
int global; // グローバルスコープ(プログラムのどこからでも見れる)
int main() {
int local; // ローカル変数(main関数(現在の関数)から見れる)
if(1) {
int a; // ifのスコープの変数(ifの中だけで見れる)
}
}
スコープの広いグローバル変数などを用いるとコードが読みにくくなり、バグの原因が分かりにくくなるとされています。参考:【C 言語】なぜグローバル変数は使わない方が良いのか?
なので、基本的にはスコープは狭い範囲にできると良いとされています。
もう一度 for を見てみましょう。
int main() {
for (int count = 0; count < 10; count++) {
// count が見れる
printf("C言語ワークショップ %d", count);
};
// countが見れない
}
int main() {
int count;
for (count = 0; count < 10; count++) {
// countが見れる
printf("C言語ワークショップ %d", count);
};
// !!!countが見れる!!!
}
前者の宣言の仕方ではfor の中でのみ変数を見ることができます。スコープを狭めるほどよいという考えで行くのであれば前者のほうが良いのではないでしょうか?
個人的には基本的にこの資料で扱っている書き方を使っています。
演習
配列
配列は変数のいくつも並んだものを指します。よく、並んだ箱、タンスや引き出し、建物の階ごとの部屋のように説明されています。プログラミングにおいてはユーザーから受け取った複数のデータを格納するときやループと一緒に用いられます。
それでは使い方を見てみましょう。
数値の配列
配列には様々なものを入れることが想定できますがまずは単純な数値から考えましょう。
宣言
整数の 10 個入る配列は次のように定義できます。
int array[10];
解説です。
int
は配列の中身の型array
は配列の名前(ここは何でも大丈夫です。a
でもb
でもmy_awesome_array
でも OK)[10]
は 10 要素入ることを指します。
配列の初期化
整数[1,2,3]が入った配列の初期化。
int array[3] = {1,2,3};
ちなみに初期化に使う要素数に合わせて自動で配列の長さを決めてほしい場合は[]
の中身を省略できます。
int array[] = {1,2,3}; // 配列の長さ3
この方法は初期化のときのみ行えます。
配列に値を代入
プログラムの動作中に配列の内容を変えるには次のような方法で行います。
int array[3];
...
array[1] = 10;
このように書くことで2要素目の値が10
になります。このときアクセスに用いる[]
の中の値を添字と言います。
C 言語は配列を0 番目から数えます。1 要素めを指したい場合は[0]
、10 要素めを指したい場合は[9]
となります。
どうしてこのようなズレがあるかについて疑問を持った場合は、次回のポインタの解説を聞いてください。
ある程度納得がいくでしょう。
値を取り出す
プログラムの動作中に配列の内容を読み取るには次のような方法で行います。
int array[3];
int a;
...
a = array[1];
これで変数a
に配列の 2 要素目がの値が代入されます。
printf に入れる場合は次のようになります。
int array[3];
...
printf("%d\n",array[1]);
配列の中身をすべて出力
これにはループを使います。2 通りの表示方法を書いておきます。
すべての要素を改行して出力。
int array[5] = {1,2,3,4,5};
for (int i = 0; i < 5; i++) {
printf("%d\n");
};
一行にすべての要素をスペース区切りで表示する方法です。ループの外で改行を入れることで実現。(出力に一つ余分なスペースが入ってしまっているが、興味がある人はこれを見つけて消すことに挑戦してみてほしい。)
int array[5] = {1,2,3,4,5};
for (int i = 0; i < 5; i++) {
printf("%d ");
};
printf("\n");
サンプルプログラム -ユーザーからの入力を受け取って格納する-
ユーザーから 5 個入力を受け取って合計を出力。
#include<stdio.h>
int main(){
int inputs[5];
for (int i = 0; i < 5; i++) {
int n;
scanf("%d",&n);
inputs[i] = n;
}
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += inputs[i];
}
printf("合計 = %d\n", sum);
}
文字列
整数を入れた配列について見てきましたが、文字について考えてみると、文字を並べたものと言うのは日常的にもよく使いますよね。
C言語では文字の列、文字列を"abcde"
と表記します。
文字列の初期化
このように文字列を初期化することができます。
char hello[12] = "Hello World";
上の例で11文字の文字列に対してで12要素の配列を用いていることがわかります。なぜでしょう?
C言語では文字列の終わりに0
が入っています。この0
は文字列の終了を表しているのでその1文字分余分に確保する必要があるのです。
ちなみにこの0をを別の値で上書きしてしまった場合は文字列として扱った場合にどこまで読めば良いのかわからずに、プログラムがクラッシュしてしまうなんてこともあります。
文字列の表示
出力に使う場合にはprintfの%s
を用います。
char str[] = "Cワークショップ!";
printf("%s\n", str);
演習
デバッグ
プログラミングをするときには、実際に機能を書く時間よりもデバッグをする時間のほうが長くかかるなんてことはよくあります。 そこで、デバッグに役立つ様々な手法をここで共有します。
printデバッグ
プログラムの様々な場所にprintfを入れてどこまで動いたか、望んだ通りの挙動をしているか確認する方法です。単純ですがとても効果的な方法です。
例:1+2+...+5のプログラム
このようなプログラムを書いたとしましょう。
#include <stdio.h>
int main(){
int sum = 0;
for (int i = 1; i < 5; i++) {
sum += i;
}
printf("%d\n", sum); // 10
}
答えは15になるはずですが、10になってしまっていますね。
ではprintfをループ内に入れて確認してみましょう。
#include <stdio.h>
int main(){
int sum = 0;
for (int i = 1; i < 5; i++) {
sum += i;
+ printf("i=%d, sum=%d\n", i, sum);
}
printf("%d\n", sum); // 10
}
出力を見ると次のようになっているはずです。
1, 1
2, 3
3, 6
4, 10
10
5回目が入っていないようですね。これは不等号の書き間違いのようなのでこのように修正すると動くでしょう。
#include <stdio.h>
int main(){
int sum = 0;
- for (int i = 1; i < 5; i++) {
+ for (int i = 1; i <= 5; i++) {
sum += i;
printf("i=%d, sum=%d\n", i, sum);
}
printf("%d\n", sum); // 15
}
サニタイザー
サニタイザーとはコンパイラについている機能でプログラム中で明らかにおかしいことをした場合に知らせてくれる機能です。
使用方法
hello.c
をgccでコンパイルする際にオプションをつけることで有効化できます。通常このようにコンパイルするところを、
$ gcc hello.c
このように変更します。
$ gcc hello.c -fsanitize=undefined -g
これで有効になります。
使用例 -配列の要素外アクセス-
次のプログラムで配列の最後の要素を出力しようとしてもうまく行きません。
#include <stdio.h>
int main(){
int array[3] = {1,2,3};
printf("%d\n", array[3]); // ???
}
サニタイザを有効にして確認してみると、このような出力が出ます。
test.c:5:25: runtime error: index 3 out of bounds for type 'int [3]'
test.c:5:5: runtime error: load of address 0x7ffe370d7ac0 with insufficient space for an object of type 'int'
0x7ffe370d7ac0: note: pointer points here
03 00 00 00 01 00 00 00 00 00 00 00 0e d1 03 b0 b9 7f 00 00 d8 7b 0d 37 fe 7f 00 00 46 11 40 00
^
1
何やら難しいことが書いてありますが、一行目に着目してみると、
index 3 out of bounds for type 'int [3]'
と書いてあります。 配列の添字は0から数えるので0,1,2しかないですね。しかし3にアクセスしようとしているよ!という内容のエラーです。 読み方さえわかってしまえばありがたいエラーメッセージですね。
アサーション
この機能は紹介だけに留めます。 アサーションはプログラム中に現在この条件を満たしていますか?ということを確認する機能です。
使用例 -入力が正であることを確認-
10 を入力値で割るプログラムで受け取った入力が 0 ではないことを確認したい。
#include <stdio.h>
#include <assert.h>
int main(){
int n;
scanf("%d",&n);
assert(n!=0);
printf("%d\n", 10 / n);
}
2 を入力した場合
$ ./a.out
2
5
0 を入力した場合
$ ./a.out
0
a.out: test.c:7: main: Assertion `n!=0' failed.
zsh: IOT instruction (core dumped) ./a.out
clang-format
フォーマットが整っていないプログラムは書き手にとっても読み手にとっても分かりづらいだけでなく、バグを探すことを難しくしてしまいます。そこで、フォーマッタを使ってみましょう。
clang-format のインストール
MacOS
- brew のインストール
brew install clang-format
の実行
Windows
- WSL 上で Linux と同様の手順を行う
Linux
sudo apt update && sudo apt install -y clang-format
の実行
使用例
こんなコードが
#include <stdio.h>
int main(){
int array[
5
] ={1,2,3,4,5};
for
(int i; i<5;
i++){printf("%d\n",array[i]);
}}
これを実行するだけで
$ clang-format hello.c -i
こうなります!
#include <stdio.h>
int main() {
int array[5] = {1, 2, 3, 4, 5};
for (int i; i < 5; i++) {
printf("%d\n", array[i]);
}
}
だいぶ読みやすくなったのではないでしょうか?
演習
関数
関数は多くのプログラムで使われているので見ていきましょう
数学のような関数
数学ではxを二乗するという関数を書くときは次のように書くと思います。
$$ f(x) = x^2 $$
この関数を使って他の式に代入することもあるかと思います。
$$ g(x) = \sin(f(x)) $$
このような書き方をC言語でも行うことができます。2乗するという関数を書いてみましょう。
int square(int x) {
int t = x * x;
return t;
}
数学的な書き方との違いとしては次のようなところが考えられます。
- 入出力に型がある
- 関数の中で変数を使っている
- 明示的に何を返すか書かなくてはいけない
このように関数を表現できます。
これをmainから呼び出してみましょう。
int square(int x) {
int t = x * x;
return t;
}
int main() {
int s = square(3);
printf("%d\n", s); // 9が出力される
}
サブルーチンとしての関数
C言語では数学のような使い方だけでなく処理のまとまりとして関数を書くことがあります。
void sub() {
printf("sub!\n");
}
int main() {
printf("main!\n");
sub();
}
このように書くことでsubの処理を呼び出すことができます。
main!
sub!
ここで実装されているsub
関数に入出力がありません。入力は空のためわかりやすいですが、戻り値がないことはvoid
で表現しています。
関数
関数は多くのプログラムで使われているので見ていきましょう
ポインタ
ポインタは、メモリアドレスを格納するための変数です。メモリアドレスは、コンピュータのメモリ上の位置を示します。
値のコピー
変数を、別の変数に代入したら、元の変数の値がコピーされます。以下のコードを実行してみましょう。
#include <stdio.h>
int main()
{
int n;
int m;
n = 100;
m = n;
n = 200;
printf("n = %d, m = %d\n", n, m);
}
このコードを実行すると、n = 200, m = 100
と表示されます。n
に 100
を代入した後、m
に n
を代入しています。その後、n
に 200
を代入していますが、m
には影響がありません。 m
に n
を代入したときに、n
の値がコピーされます。
コンピューターのメモリは、変数の箱が並んでいる数直線のようにイメージすることができます。そして、それぞれの変数に対してメモリのアドレス (番地) が存在します。 先程のプログラムを図にすると以下のようになります。(以下の例では n
のアドレスを 102番地、 m
のアドレスを 103番地 としています。)
値の参照
ポインタ変数を使うと、変数のアドレスを格納することができ、変数のメモリアドレスを使ったプログラミングをすることができます。ポインタ変数の宣言には、変数の宣言時に変数名の前に *
(アスタリスク) をつけます。変数のアドレスを取得するには、変数名の前に &
をつけます。 ポインタ変数に格納されているアドレスに格納されている値を参照するには、変数名の前に *
をつけます。以下のコードを書いて実行して、書き方と挙動を確認してみましょう。
#include <stdio.h>
int main()
{
int n;
int *p;
n = 100;
p = &n;
n = 200;
printf("n = %d, *p = %d\n", n, *p);
}
このコードを実行すると、n = 200, *p = 200
と表示されます。以下の図に先ほどのプログラムのイメージ図を示してます。 n
に 100
を代入した後、p
に n
のアドレスを代入しています。 その後、n
に 200
を代入します。printf
の *p
は、p
が参照しているアドレスにある値を示します (ここでは 102番地の値)。そのため、*p
は n
の値を参照してるため、 200
になります。
printf
でメモリアドレスを表示
printf
でメモリアドレスを表示する場合、 %p
を使います。先程のプログラムに、printf
でポインタ変数 p
に格納されているアドレスと p
のアドレスと n
のアドレスを表示する処理を追加したプログラムを示します。
#include <stdio.h>
int main()
{
int n;
int *p;
n = 100;
p = &n;
n = 200;
printf("n = %d, *p = %d\n", n, *p);
printf("p = %p, &p = %p, &n = %p\n", p, &p, &n);
}
このコードを実行すると、以下のように表示されます。アドレスは 16 進数で表示されます。 p
には、 n
のアドレスが格納されているのがわかります。また、p
と n
のアドレス(つまり &p
と &n
) は別の変数であるため、異なるアドレスであることがわかります。
メモリアドレスは実行するたびに変わります。
n = 200, *p = 200
p = 0x7fff8bf26a2c, &p = 0x7fff8bf26a30, &n = 0x7fff8bf26a2c
図での例では、以下の用に表示されます。(わかりやすく、10進数で表示しています。)
n = 200, *p = 200
p = 102, &p = 103, &n = 102
Swap 関数
Swap 関数は、2 つの変数の値を交換する関数です。
以下のコードを書いたら、実行する前に 1回目の printf
と 2回目の printf
の出力を予想してみましょう。
#include <stdio.h>
void swap(int a, int b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
}
int main()
{
int n = 100;
int m = 200;
printf("n = %d, m = %d\n", n, m);
swap(n, m);
printf("n = %d, m = %d\n", n, m);
}
このコードを実行すると、以下のようになります。
n = 100, m = 200
n = 100, m = 200
この swap
関数は値を入れ替えることができません。関数を呼び出すと、引数の変数は元の変数とは別の場所にメモリが確保され、元の値をその場所にコピーするからです。そのため、関数内で引数の値を変更しても、元の変数には影響がありません。以下に先程のプログラムのイメージ図を示します。
参照 Swap 関数
Swap 関数は、2 つの変数の値を交換する関数です。しかし、 Swap 関数の引数を普通の変数にした場合、値がコピーされるため、正しく値を交換することはできませんでした。
次は、 Swap 関数の 2 つの引数をポインタ変数にしてみましょう。以下のコードを書いたら、実行する前に 1回目の printf
と 2回目の printf
の出力を予想してみましょう。
#include <stdio.h>
void swap(int *a, int *b)
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int n = 100;
int m = 200;
printf("n = %d, m = %d\n", n, m);
swap(&n, &m);
printf("n = %d, m = %d\n", n, m);
}
このコードを実行すると、以下のようになります。
n = 100, m = 200
n = 200, m = 100
ポインタ変数を使い、main 関数の変数 n
、m
のアドレスを swap 関数に渡し、swap 関数でそのアドレスを参照することで、値を交換することができました。以下に先程のプログラムのイメージ図を示してます。
ポインタと配列
C 言語で、配列はメモリ上で連続して配置されます。以下のプログラムを実行して、配列の要素のアドレスを表示してみましょう。
#include <stdio.h>
int main(){
int arr[] = {10, 11, 12};
printf("%d, %d, %d\n", arr[0], arr[1], arr[2]);
printf("%p, %p, %p\n", &arr[0], &arr[1], &arr[2]);
printf("%p\n", arr);
printf("%p\n", &arr);
}
実行結果は以下のようになります。配列型の変数は、 &arr[0]
(先頭の要素のアドレス) と arr
(arr
に格納されているアドレス) と &arr
(arr
のアドレス) が同じ値であることがわかります。(配列の各要素のアドレスは、4byteずつ連続しています。これは、メモリのアドレスが 1 byte ずつに割り当てられており、 int 型の変数の大きさは 4 byte であるためです。)
10, 11, 12
0x7ffff6cf7b48, 0x7ffff6cf7b4c, 0x7ffff6cf7b50
0x7ffff6cf7b48
0x7ffff6cf7b48
以下にイメージ図を示します。
このイメージ図では、以下のように表示されます。(わかりやすく 10 進数で表示しています。 int 型の変数の大きさは 4 byte ですが、わかりやすくするために、アドレスは 1 byte ずつに割り当てています。)
10, 11, 12
102, 103, 104
102
102
アドレスに数値を足す
ポインタを使って、配列の要素にアクセスすることができます。以下のプログラムを実行して、ポインタを使って配列の要素にアクセスする方法を確認してみましょう。
#include <stdio.h>
int main(){
int arr[] = {10, 11, 12};
int *p = arr;
printf("%d, %d, %d\n", arr[0], arr[1], arr[2]);
printf("%p, %p, %p\n", &arr[0], &arr[1], &arr[2]);
printf("%p\n", arr);
printf("%p\n", &arr);
printf("%d, %d, %d\n", *p, *(p + 1), *(p + 2));
printf("%p, %p, %p\n", p, p + 1, p + 2);
printf("%d, %d, %d\n", p[0], p[1], p[2]);
}
実行結果は以下のようになります。p
は arr
の先頭要素のアドレスを指しています。p + 1
は p
のアドレスに 1 を足したアドレスを指します。*(p + 1)
は p + 1
のアドレスに格納されている値を取得します。
10, 11, 12
0x7ffe73b706d8, 0x7ffe73b706dc, 0x7ffe73b706e0
0x7ffe73b706d8
0x7ffe73b706d8
10, 11, 12
0x7ffe73b706d8, 0x7ffe73b706dc, 0x7ffe73b706e0
10, 11, 12
以下にイメージ図を示します。
イメージ図では、以下のように表示されます。(わかりやすく 10 進数で表示しています。 int 型の変数の大きさは 4 byte ですが、わかりやすくするために、アドレスは 1 byte ずつに割り当てています。)
10, 11, 12
102, 103, 104
102
102
10, 11, 12
102, 103, 104
10, 11, 12
文字列
文字列(string)は、文字(character)の並びです。 C 言語では、文字列は文字の配列として表現されます。
文字列は、"
(ダブルクォーテーション) で囲む必要があります。以下に、文字列を扱うプログラミング例を示します。str1
と str2
は、同じ文字列を表しています。初期化するときに、str1
は "
を使って初期化されていますが、str2
は文字の配列を使って初期化されています。文字列の最後には文字列の最後を意味する、'\0'
(ヌル文字) が必要です (ヌル文字のように、連続したデータの最後尾を意味する特殊なデータのことを番兵(ばんぺい)といいます)。そのため、char 型の配列の長さより 1 小さいサイズまで文字列を格納できます。 "
で囲まれた文字列は、コンパイラによって自動的にヌル文字が追加されます。
#include <stdio.h>
int main(){
char str1[] = "TUAT";
char str2[] = {'T', 'U', 'A', 'T', '\0'};
printf("%s\n", str1);
printf("%s\n", str2);
}
実行結果は以下のようになります。
TUAT
TUAT
以下にイメージ図を示します。char 型の変数が配列として連続して配置されています。文字列の最後には、ヌル文字が追加されています。配列の各要素には一文字ずつ ASCII コードが割り当てられています。ヌル文字は 0
です。 ASCII コード表はこちらを参照してください。
文字列の長さ
文字列の長さを取得するには、strlen
関数を使用します。 strlen
関数は、文字列の長さを返します。 #include <string.h>
が必要です。
以下に、最大15文字の入力された文字列の長さを表示するプログラム例を示します。
#include <stdio.h>
#include <string.h>
int main(){
char str[16];
int length;
scanf("%s", str);
length = strlen(str);
printf("length = %d\n", length);
}
tuat
と入力すると、以下のように表示されます。
tuat
length = 4
012345678901234
と入力すると、以下のように表示されます。
012345678901234
length = 15
以下にイメージ図を示します。文字列の最後には、ヌル文字が追加されています。
strlen
関数は、char 型の配列で、ヌル文字 (\0
) までの文字をカウントしてくれます。
文字列の比較
文字列の比較とは、2つの文字列が同じかどうかを判定することです。C言語で文字列の比較を行うには、strcmp
関数を使います。同じ文字列であれば、0
が返ります。異なる文字列であれば、辞書順で比較した際、どちらが先かによって、正の値か負の値が返ります。以下に 入力された文字列がTUAT
と一致するかどうかを判定するプログラムを示します。
#include <stdio.h>
#include <string.h>
int main(){
char tuatStr[] = "TUAT";
char inputstr[16];
int result;
printf("Input TUAT\n");
scanf("%s", inputstr);
result = strcmp(tuatStr, inputstr);
if(result == 0){
printf("Correct\n");
}else{
printf("Incorrect\n");
}
printf("result = %d\n", result);
}
TUAT
と入力すると、以下のように表示されます。
Input TUAT
TUAT
Correct
result = 0
同じ文字列であるため、 result
は 0
になります。
tuat
と入力すると、以下のように表示されます。
Input TUAT
tuat
Incorrect
result = -32
TUAT
と tuat
は異なる文字列で、 tuat
は辞書順で TUAT
よりも後ろであるため、result
は負の値になります。
MCC
と入力すると、以下のように表示されます。
Input TUAT
MMC
Incorrect
result = 7
TUAT
と MCC
は異なる文字列で、 MCC
は辞書順で TUAT
よりも前であるため、result
は正の値になります。
文字列の連結
文字列の連結とは、複数の文字列を一つの文字列に結合することです。C言語においては、文字列の連結を行うためにstrcat
関数があります。 strcat
関数は、2つの文字列を連結して、1つの文字列にします。以下に、strcat
関数を使って文字列を連結するプログラムを示します。
#include <stdio.h>
#include <string.h>
int main(){
char str1[16] = "Hello ";
char str2[16];
scanf("%s", str2);
strcat(str1, str2);
printf("%s\n", str1);
}
World
と入力すると、以下のように表示されます。
World
Hello World
イメージ図を示します。str1
と str2
は、それぞれの文字列を表しています。str1
は、Hello
という文字列で初期化されています。str2
に入力された文字列が連結されて、str1
に格納されます。
文字列のコピー
文字列をコピーするには、strcpy
関数を使います。
#include <stdio.h>
#include <string.h>
int main(){
char str1[16] = "Hello ";
char str2[16] = "World";
strcpy(str1, str2);
printf("%s\n", str1);
}
実行すると、以下のように表示されます。
World
strcpy
関数は、str2
の文字列をstr1
にコピーします。str1
の文字列はHello
からWorld
に変わります。
以下にイメージ図を示します。str1
とstr2
は、それぞれ別のメモリ領域に配置されています。strcpy
関数を使うことで、str2
の文字列をstr1
にコピーします。
構造体
構造体とは、複数の異なるデータをまとめて扱うデータ構造です。構造体を使うことで、複数のデータを一つのデータとして扱うことができます。
以下に人の情報をまとめた構造体 Person
という構造体名を宣言する例を示します。
struct Person{
char name[16];
int age;
double height;
};
Person
には、 name
、 age
、 height
という変数が含まれています。これらのように構造体の中に含まれる変数をメンバ(メンバ変数)と呼びます。変数の箱の中に変数の箱が入ってるのをイメージしてください。
以下に、構造体を使って人の情報を表示するプログラムを示します。構造体のメンバにアクセスするには、.
(ドット(直接メンバ参照演算子)) を使います。
#include <stdio.h>
#include <string.h>
struct Person{
char name[16];
int age;
double height;
};
int main(){
struct Person person;
person.age = 24;
person.height = 180.5;
strcpy(person.name, "daniel");
printf("Name: %s\n", person.name);
printf("Age: %d\n", person.age);
printf("Height: %.1f\n", person.height);
}
実行すると、以下のように表示されます。
Name: daniel
Age: 20
Height: 180.5
構造体は、異なるデータ型をまとめて扱うことができます。上記の例では、name
は文字列、age
は整数、height
は浮動小数点数を扱っています。
構造体の配列
構造体の配列を作ることもできます。以下に構造体の配列を作る例を示します。
#include <stdio.h>
#include <string.h>
struct Person{
char name[16];
int age;
double height;
};
int main(){
struct Person people[3];
strcpy(people[0].name, "daniel");
people[0].age = 24;
people[0].height = 180.5;
strcpy(people[1].name, "sugawa");
people[1].age = 22;
people[1].height = 164.4;
strcpy(people[2].name, "genchan");
people[2].age = 21;
people[2].height = 170.3;
for(int i = 0; i < 3; i++){
printf("Name: %s\n", people[i].name);
printf("Age: %d\n", people[i].age);
printf("Height: %.1f\n", people[i].height);
}
}
実行すると、以下のように表示されます。
Name: daniel
Age: 24
Height: 180.5
Name: sugawa
Age: 22
Height: 164.4
Name: genchan
Age: 21
Height: 170.3
構造体のポインタ変数
構造体のポインタ変数も使うことができます。構造体のポインタ変数も宣言時には、普通のポインタ変数の宣言と同じように、*
を変数名の前につけます。また、構造体のアドレスを取得するのにも、普通のポインタ変数と同様に &
を変数名の前につけます。
#include <stdio.h>
#include <string.h>
struct Person{
char name[16];
int age;
double height;
};
int main(){
struct Person person;
struct Person *p;
person.age = 24;
person.height = 180.5;
strcpy(person.name, "daniel");
p = &person;
printf("Name: %s\n", (*p).name);
printf("Age: %d\n", (*p).age);
printf("Height: %.1f\n", (*p).height);
}
実行すると、以下のように表示されます。
Name: daniel
Age: 20
Height: 180.5
アロー演算子
構造体のポインタ変数を使う場合、(*p).name
のように、*
を使って構造体のメンバにアクセスすることができます。C 言語では、構造体ポインタが示すアドレスにある構造体のメンバにアクセスするための演算子として、->
(アロー演算子)が用意されています。アロー演算子を使うと、(*p).name
は p->name
と書くことができます。
上記のプログラムをアロー演算子を使って書き換えると以下のようになります。
#include <stdio.h>
#include <string.h>
struct Person{
char name[16];
int age;
double height;
};
int main(){
struct Person person;
struct Person *p;
person.age = 24;
person.height = 180.5;
strcpy(person.name, "daniel");
p = &person;
printf("Name: %s\n", p->name);
printf("Age: %d\n", p->age);
printf("Height: %.1f\n", p->height);
}
実行すると、以下のように表示されます。
Name: daniel
Age: 20
Height: 180.5
メモリの動的管理
配列は、要素数が固定されているため、要素数を変更することができません。しかし、配列の長さ10個だけ必要なとき、100個だけ必要なとき、1000個だけ必要なとき...があると思います。予め1000個の長さの配列を作っても、10個だけしか使わなければ9990個は無駄になります。このような場面には、malloc
関数を用いて動的にメモリを確保します。メモリを確保する際、変数のサイズを考慮する必要があります。変数のサイズの取得は sizeof
演算子を用いて行います。
malloc
関数は引数で指定した分、メモリを確保し、確保したメモリのアドレスを返します。malloc
関数はヘッダファイル stdlib.h
に定義されています。 malloc
関数で確保したメモリは、使用後に free
関数で解放する必要があります。
次に malloc
関数の使用例を示します。 入力された長さの int 配列を作成し、0から順に代入し、出力するプログラムです。動的配列は、ポインタ変数を用いて宣言します。 malloc
関数で確保したメモリを使用後、 free
関数で解放しています。動的配列は malloc
関数でメモリを確保するまで、アクセスできません。また、free
関数でメモリを解放したら、アクセスすることができなくなります。#include <stdlib.h>
を忘れないでください。
#include <stdio.h>
#include <stdlib.h>
int main() {
int length;
int *arr;
scanf("%d", &length);
// int のサイズを length 個分メモリ確保
arr = (int *)malloc(sizeof(int) * length);
for (int i = 0; i < length; i++) {
arr[i] = i;
}
for (int i = 0; i < length; i++) {
printf("%d\n", arr[i]);
}
free(arr);
}
3
と入力した場合、次のように出力されます。
0
1
2
10
と入力した場合、次のように出力されます。
0
1
2
3
4
5
6
7
8
9
メモリを確保したときのメモリのイメージ図を以下に示します。malloc関数では、指定したサイズのメモリを確保し、確保したメモリのアドレスを返します。ポインタ変数を配列のようにインデックスを指定すれば、確保したメモリにアクセスできます。
構造体の動的メモリ確保
構造体の動的メモリ確保は、構造体のサイズを sizeof
で取得し、malloc
関数で確保します。
以下のコードを実行してみましょう。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Person{
char name[16];
int age;
double height;
};
int main(){
struct Person *person;
person = (struct Person *)malloc(sizeof(struct Person));
person->age = 24;
person->height = 180.5;
strcpy(person->name, "daniel");
printf("Name: %s\n", person->name);
printf("Age: %d\n", person->age);
printf("Height: %.1f\n", person->height);
free(person);
}
メモリを確保したときのメモリのイメージ図を以下に示します。malloc関数では、指定したサイズのメモリを確保し、確保したメモリのアドレスを返します。ポインタの構造体であるため、アロー演算子を使ってメンバにアクセスしましょう。
ファイル入出力
C 言語のプログラムでは、ファイルの作成や読み込み、書き込みを行うことができます。ファイルを書き込むことで、プログラムの実行結果を保存したり、ファイルからデータを読み込んで処理を行うことができます。
ファイルの読み込み
ファイルの読み込みには、fopen
関数を使用します。fopen
関数は、ファイルを開いてファイルポインタを返します。第一引数にファイル名、第二引数にファイルのモードを指定します。モードには、読み込みモード "r"
、書き込みモード "w"
、追記モード "a"
などがあります。ファイルポインタは、ファイルの読み書き位置を管理するための構造体です。ファイルポインタから、文字列を読み込むためには fscanf
関数を使用します。以下に、ファイルを読み込むプログラムの例を示します。
#include <stdio.h>
int main()
{
FILE *fp;
char str[256];
fp = fopen("input.txt", "r");
fscanf(fp, "%s", str);
printf("%s\n", str);
fclose(fp);
}
実行する前に、input.txt
というファイルを作成し、適当な文字列を書き込んでおいてください。プログラムを実行すると、ファイルから読み込んだ文字列が表示されます。
例として、input.txt
に以下の文字列を書き込んだ場合、プログラムの実行結果は以下のようになります。
Hello,TUAT!
Hello,TUAT!
ファイルの書き込み
ファイルの書き込みには、fopen
関数を使用します。fopen
関数は、ファイルを開いてファイルポインタを返します。第一引数にファイル名、第二引数にファイルのモードを指定します。モードには、読み込みモード "r"
、書き込みモード "w"
、追記モード "a"
などがあります。ファイルポインタは、ファイルの読み書き位置を管理するための構造体です。ファイルポインタに、文字列を書き込むためには fprintf
関数を使用します。以下に、ファイルに書き込むプログラムの例を示します。
#include <stdio.h>
int main()
{
FILE *fp;
fp = fopen("output.txt", "w");
fprintf(fp, "Hello,TUAT!\n");
fclose(fp);
}
プログラムを実行すると、output.txt
というファイルが作成され、Hello,TUAT!
という文字列が書き込まれます。
ファイルの追記
ファイルの追記には、fopen
関数を使用します。fopen
関数は、ファイルを開いてファイルポインタを返します。第一引数にファイル名、第二引数にファイルのモードを指定します。モードには、読み込みモード "r"
、書き込みモード "w"
、追記モード "a"
などがあります。ファイルポインタは、ファイルの読み書き位置を管理するための構造体です。ファイルポインタに、文字列を書き込むためには fprintf
関数を使用します。以下に、ファイルに追記するプログラムの例を示します。
#include <stdio.h>
int main()
{
FILE *fp;
fp = fopen("output.txt", "a");
fprintf(fp, "Hello,TUAT!\n");
fclose(fp);
}
プログラムを実行すると、output.txt
というファイルに、Hello,TUAT!
という文字列が追記されます。
ファイルモードが "w"
の場合、ファイルが存在する場合は上書きされます。ファイルモードが "a"
の場合、ファイルが存在する場合は末尾に追記されます。