本記事について
この記事には 前編 および 中編 があります。そちらを読んだ前提で進んでいきます。
前回 1.06 から 1.10 までを扱ったため、今回の内容は 1.10 から 1.15 までとなっています。
前回までも書いたように、問題があれば本記事は削除します。
目次
- 1.00.はじめに
- 1.01.出力とコメント | EX1
- 1.02.プログラムの書き方とエラー | EX2
- 1.03.四則演算と優先順位 | EX3
- 1.04.変数と型 | EX4
- 1.05.プログラムの実行順序と入力 | EX5
- 1.06.if文・比較演算子・論理演算子 | EX6
- 1.07.条件式の結果とbool型 | EX7
- 1.08.変数のスコープ | EX8
- 1.09.複合代入演算子 | EX9
- 1.10.while文 | EX10
↓ここから
- 1.11.for文・break・continue | EX11
- 1.12.文字列と文字 | EX12
- 1.13.配列 | EX13
- 1.14.STLの関数 | EX14
- 1.15.関数 | EX15
L - 1.11.for文・break・continue
キーポイント
- __for文は__繰り返し処理でよくあるパターンをwhile文より短く書くための構文
- 「初期化」→「条件式」→「処理」→「更新」→「条件式」→「処理」→...という順で実行され、条件式が真のとき繰り返し続ける
for (初期化; 条件式; 更新) {
処理
}
- N回の繰り返し処理は次のfor文で書くのが一般的
for (int i = 0; i < N; i++) {
処理
}
- breakを使うとループを途中で抜けられる
- continueを使うと後の処理を飛ばして次のループへ行ける
for文
for文は「N回処理する」というような繰り返し処理でよくあるパターンをwhile文より短く書くための構文です。
3回繰り返すプログラムをwhile文とfor文で書くと次のようになります。
#include <stdio.h>
int main(void) {
int j = 0;
while (j < 3) {
printf("Hello while: %d\n", j);
j++;
}
for (int i = 0; i < 3; i++) {
printf("Hello for: %d\n", i);
}
}
Hello while: 0
Hello while: 1
Hello while: 2
Hello for: 0
Hello for: 1
Hello for: 2
for文は次のように書き、条件式が真のとき処理を繰り返し続けます。
for (初期化; 条件式; 更新) {
処理
}
※コードテストではなく自分の環境でプログラムを書いている人の中に、もしかしたら上のコードだと動かないという人もいるかもしれません。その場合は、
int i;
for (i = 0; i < 3; i++) {
printf("Hello for: %d\n", i);
}
とすると動くかもしれません。1
forとwhileの対応関係
for文とwhile文の対応関係を見てみましょう。
int j = 0; // 初期化
while (j < 3 /* 条件式 */ ) {
printf("Hello while: %d\n", j); // 処理
j++; //更新
}
for (int i = 0 /* 初期化 */ ; i < 3 /* 条件式 */ ; i++ /* 更新 */) {
printf("Hello for: %d\n", i); // 処理
}
処理される順序はwhile文で書いたときと全く同じで、「初期化」→「条件式」→「処理」→「更新」→「条件式」→「処理」→...という順で実行されます。
while文とfor文は機能面ではほとんど差がありませんが、次の「N回の繰り返し処理」等、for文で簡潔に書ける処理はfor文で書くのが一般的です。
N回の繰り返し処理
for文を使うとき、ほとんどは「N回の繰り返し処理」のパターンです。
動作の細かい部分がわからない人は、とりあえずこのパターンを覚えるところから始めましょう。
for (int i = 0; i < N; i++) {
処理
}
for文を使うときのコツ
「N回の繰り返し処理」のfor文を使うときは、「初期化」「条件式」「更新」の細かい動作を考えないようにしましょう。
よりおおざっぱに、for文はiが1ずつ増えながらN回処理を繰り返す機能と考えた方が、for文を使うプログラムを書きやすくなります。
また、ループでどう書けばよいかわからなくなったときは、__まずループを使わずにプログラムを書いてみて、その後ループで書き直す__という方法が有効です。
また、コンテストで「N人のchokudaiさんがいて...」のような問題文が出たときも N = 1, 2 くらいの時のプログラムを考えてみて、それからループに持っていくというのも有効だと思います。
以下の2つのプログラムはその例です。
printf("hello for: 0\n");
printf("hello for: 1\n");
printf("hello for: 2\n");
for (int i = 0; i < 3; i++) {
printf("hello for: %d\n", i);
}
breakとcontinue
while文とfor文を制御する命令として、__break__と__continue__があります。
break
breakはループを途中で抜けるための命令です。
breakを使ったプログラムの例です。
#include <stdio.h>
int main(void) {
// breakがなければこのループは i == 4 まで繰り返す
for (int i = 0; i < 5; i++) {
if (i == 3) {
printf("ぬける\n");
break; // i == 3 の時点でループから抜ける
}
printf("%d\n", i);
}
printf("終了\n");
}
0
1
2
ぬける
終了
if文で i == 3
が真になったとき、break;
の命令を実行することでforループを抜け、終了
と出力しています。
continue
continueは後の処理をとばして次のループへ行くための命令です。
continueを使ったプログラムの例です。
#include <stdio.h>
int main(void) {
for (int i = 0; i < 5; i++) {
if (i == 3) {
printf("とばす\n");
continue; // i == 3 のとき これより後の処理をとばす
}
printf("%d\n", i);
}
printf("終了\n");
}
0
1
2
とばす
4
終了
上のプログラムでは、if文で i == 3
が真になったとき、continue;
の命令を実行することでcontinueより下の部分を飛ばし、次のループに入ります。
カードゲームで例えるなら、continueが強制ターンエンド、breakがゲームエンドのようなものでしょうか。
細かい話
細かい話なので、飛ばして問題を解いても良いです。
for文とwhile文の違い
for文とwhile文の違いは主に2点あります。「カウンタ変数のスコープ」と「continueをしたときの動作」です。
カウンタ変数のスコープ
for文のカウンタ変数はwhile文よりスコープが狭くなります。
次の例では、for文のカウンタ変数であるi
がスコープの範囲外で使われているため、コンパイルエラーが発生しています。
#include <stdio.h>
int main(void) {
int j = 0; // jのスコープはmain関数の終わりまで
while (j < 3) {
printf("Hello while\n");
j++;
}
for (int i = 0; i < 3; i++) { // iのスコープはforの終わりまで
printf("Hello for\n");
}
printf("%d\n", j);
printf("%d\n", i);
}
./Main.c: In function ‘main’:
./Main.c:17:18: error: ‘i’ undeclared (first use in this function)
17 | printf("%d\n", i);
| ^
./Main.c:17:18: note: each undeclared identifier is reported only once for each function it appears in
while文で使っている変数j
のスコープはmain関数の終わりまでですが、 for文の「初期化」の場所で宣言している変数i
のスコープは、for文の{ }
中だけになります。
continueをしたときの動作
while文でcontinueをすると更新処理を飛ばしてしまう事があるので、注意が必要です。
例を見てみましょう。
#include <stdio.h>
int main(void) {
for (int i = 0; i < 5; i++) {
if (i == 3) {
printf("forとばす\n");
continue;
}
printf("for%d\n", i);
}
printf("for終了\n");
int j = 0;
while(j < 5) {
if (j == 3) {
printf("whileとばす\n");
continue;
}
printf("while%d\n", j);
j++;
}
printf("while終了\n");
}
for0
for1
for2
forとばす
for4
for終了
while0
while1
while2
whileとばす
whileとばす
whileとばす
(無限に続く)
for文のcontinueは先に説明したとおりですが、while文では無限ループになってしまっています。
j == 3
のとき、continue;
より後を飛ばして次のループに行っていますが、よく見てみるとj++
の処理も飛ばされてしまっています。
そのため、変数j
の値は永遠に3
のままとなり、無限ループになってしまいます。
省略したfor文
for文の「初期化」「条件式」「更新」の部分は、必要が無い場合省略できます。
次のように書いた場合、for文の動作はwhile文と完全に同じものになります。
int i = 0;
for (; i < n;) {
i++;
}
「条件式」の部分を省略した場合、true
と書いたときと同じ動作になります。
次のプログラムは無限ループになります。
for (int i = 0; ; i++) {
printf("%d\n", i);
}
{ }
の省略
if文と同様に、for文やwhile文の処理が一文のみの場合も{ }
を省略できます。
#include <stdio.h>
int main(void) {
for (int i = 0; i < 3; i++)
printf("Hello!\n");
}
for文のネスト
if文と同様、for文とwhile文もネストさせることができます。そのような書き方は__多重ループ__と呼ばれます。
#include <stdio.h>
int main(void) {
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
printf("i:%d, j:%d\n", i, j);
}
}
}
問題
以下のリンク先に問題文が載っています。
"+"
, "-"
, "*"
, "/"
は文字列として受け取る必要があります。 文字列の比較をしたい場合は、string.h をインクルードして strcmp
という関数を使うなどしてみるといいかと思います。
#include <stdio.h>
#include <string.h>
int main(void) {
int N, A;
scanf("%d%d", &N, &A);
// ここにプログラムを追記
}
解答例
必ず自分で問題に挑戦してみてから見てください。
解答例
#include <stdio.h>
#include <string.h>
int main(void) {
int N, A;
scanf("%d%d", &N, &A);
for (int i = 0; i < N; i++) {
char op[5];
int x;
scanf("%s%d", op, &x);
if (strcmp(op, "+") == 0) {
A += x;
}
else if (strcmp(op, "-") == 0) {
A -= x;
}
else if (strcmp(op, "*") == 0) {
A *= x;
}
else if (strcmp(op, "/") == 0 && x != 0) {
A /= x;
}
else {
printf("error\n");
break;
}
printf("%d:%d\n", i + 1, A);
}
}
M - 1.12.文字列と文字
キーポイント
- 文字を扱うには__char型__を使う
- 文字列を扱うには__char型の配列__を使う
-
strlen(文字列変数)
で文字列の長さを取得できる -
文字列変数[i]
でi文字目にアクセスできる -
文字列変数[i]
のi
を__添え字__という - 添字は0から始まる
- 添字の値が正しい範囲内に無くてもエラーが出てくれない
文字(char型)
何度も出てきましたが、「文字型」というものがあります。それは__char型__です。
char型は一文字のデータしか保持することができません。
文字列を表すために" "
で囲ったように、char型を表すためには' '
で囲います。変換指定子は%c
です。
#include <stdio.h>
int main(void) {
char c = 'a';
printf("%c\n", c); // a
}
a
追記: C言語ではシングルクォートで囲まれた文字 (文字リテラル) の型はintらしいです。(C++ではcharだったのでCもそうだと思っていました。)
文字列
abc
やhello
のように、文字が順番に並んでいるもののことを文字列といいます。
何度か説明したように、C言語で文字列を扱うにはchar型の配列を使います。配列については「N - 1.13.配列」で扱いますが、ここでも簡単な説明をします。変数を宣言するときに[]
をつけることで配列として宣言することができます。
変換指定子は%s
です。配列は扱いが特殊で、scanf()
をするときに、&
をつけてはいけません。
#include <stdio.h>
int main(void) {
char str1[10];
scanf("%s", str1);
char str2[] = ", world!";
printf("%s%s\n", str1, str2);
}
Hello
Hello, world!
文字列の長さ
文字列の長さ(文字数)はstrlen(文字列変数)
で取得できます。
strlen()
は、string.hをインクルードすると使うことができます。
#include <stdio.h>
#include <string.h>
int main(void) {
char str[] = "Hello";
printf("%d\n", strlen(str));
}
5
i番目の文字
次のように書くとi文字目が取得できます。
文字列[i]
このi
のことを添字と言います。
文字列[i]の型
文字列[i]
で取得されるデータはchar型です。
#include <stdio.h>
int main(void) {
char str[] = "AAA";
char c = str[1]; // char型の値が得られる
printf("%c\n", c); // A
}
A
添字は0から始まる
添字は0から始まることに注意しましょう。
#include <stdio.h>
int main(void) {
char str[] = "hello";
printf("%c\n", str[0]); // h
printf("%c\n", str[4]); // o
}
h
o
str[0]
で1文字目、str[4]
で5文字目を取得しています。
"hello"
という文字列の場合、添字の値と文字の対応は次の表のとおりです。
添字 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
文字 | h | e | l | l | o |
ループのカウンタ変数を0から始めるのと同じように、添字が0から始まることにも慣れていきましょう。
文字列の書き換えと比較
文字列の一部を書き換えるときや比較をするときはchar型を使う必要があります。
#include <stdio.h>
int main(void) {
char str[] = "Hello";
str[0] = 'M'; // char型の'M'
printf("%s\n", str); // Mello
if(str[1] == 'e'){
printf("AtCoder\n");
}
}
Mello
AtCoder
文字列処理関数
それでは文字列に文字列を代入するにはどうすればいいのでしょうか?
直感的には、次のように書けばできそうです。
#include <stdio.h>
int main(void) {
char str[] = "Hello";
str = "World!";
printf("%s\n", str); // World! と出てほしい
}
しかし、このコードはコンパイルエラーになります。C言語ではこういった数値のときのような書き方は文字列には使えないのです。
ということで、文字列の処理をしたいときにはstring.hをインクルードすることで使える__文字列処理関数__が必要になります。さきほど紹介したstrlen()
関数もその中の一つです。
文字列の代入
文字列に文字列を代入したいときにはstrcpy()
を使います。
cpy つまり copy という名前の通り、
strcpy(a, b)
と書くと文字列bを文字列aにコピーする、つまり文字列aに文字列bを代入することができます。
#include <stdio.h>
#include <string.h>
int main(void) {
char str[10] = "Hello";
strcpy(str, "World!");
printf("%s\n", str);
}
World!
注意点として、元の文字列よりも長い文字列を代入した時にも大丈夫なように文字列の宣言の時に[]
に大きめの数字を入れるようにしてください。
文字列の連結
2つの文字列を連結したいときには、strcat()
を使います。
cat は concatenation(連結) という意味で、
strcat(a, b)
と書くと文字列bを文字列aに連結することができます。
#include <stdio.h>
#include <string.h>
int main(void) {
char str[20] = "Hello";
strcat(str, ", World!");
printf("%s\n", str);
}
Hello, World!
この関数を使うときも、連結した後の文字列が入りきるように[]
に大きめの数字を入れるようにしてください。
文字列の比較
2つの文字列の比較がしたいときは、strcmp()
をつかいます。
cmp は comparison(比較) という意味で、
strcmp(a, b)
と書くと文字列aと文字列bの大きさを比較することができます。
この「大きさ」というのは辞書順によって判断されます。(aよりzのほうが後ろに来そうですよね?その順番です)
- a>b の場合は正の数
- a=b の場合は0
- a<b の場合は負の数
が出力されます
#include <stdio.h>
#include <string.h>
int main(void) {
char str1[] = "ABCDE";
char str2[] = "BCDE";
int n = strcmp(str1, str2); // "ABCDE" < "BCDE"
printf("%d\n", n);
}
-1
正の数、負の数とは書きましたが、実際には0なのかどうか、つまり文字列が等しいのかどうかを判定するために使うことが多いです。
応用
ループ構文との組み合わせ
文字列はループ構文を組み合わせることで様々な処理が記述できるようになります。
次のプログラムでは、入力された文字に何文字'O'
が含まれているかを数えています。
#include <stdio.h>
#include <string.h>
int main(void) {
char str[10];
scanf("%s", str);
int n = strlen(str), count = 0;
for(int i = 0; i < n; i++){
if(str[i] == 'O'){
count++;
}
}
printf("%d\n", count);
}
LOOOOL
4
注意点
範囲外エラー
添字の値が正しい範囲内に無いと実行時エラーになって__くれません!__
次のプログラムは"hello"
という5文字の文字列(有効な添字の値は0~4)に対し、[10]
で存在しない文字数目にアクセスしようとしています。
#include <stdio.h>
int main(void) {
char str[] = "hello";
printf("%c\n", str[10]);
}
このような操作をした場合、エラーが出てくれないうえに、何が起きてしまうのかわかりません。添え字の範囲には、最大限注意しましょう。
全角文字の扱い
string型やchar型は全角文字を正しく扱えません。
#include <stdio.h>
int main(void) {
char str[] = "こんにちは";
printf("%c\n", str[0]);
char c = 'あ';
printf("%c\n", c);
}
�
�
どちらの出力も文字化けしてしまっています。
うまく処理する方法もありますが、それらの扱いは半角文字に比べかなり面倒な部分があり、競技プログラミングで要求されることもあまりないかと思います。そのため、ここでの説明は省きます。
細かい話
char型の変数への入力
char型の変数に入力すると一文字ずつ取り出すことができます。
#include <stdio.h>
int main(void) {
char a, b;
scanf("%c%c", &a, &b);
printf("%c\n%c\n", a, b);
}
OK
O
K
エスケープシーケンス
「改行」などの特殊な文字をプログラム中で表現する場合、__エスケープシーケンス__を利用します。今までもつかってきた\n
などがそうです。
基本的には\n
だけ覚えておけば良いですが、他にも代表的なエスケープシーケンスとして以下のものがあります。
エスケープシーケンス | 説明 |
---|---|
\" |
二重引用符 "
|
\' |
引用符 '
|
\\ |
バックスラッシュ(または半角円記号) \
|
\t |
タブ(水平タブ) |
\r |
復帰(一部の実行環境では改行に用いる) |
行単位での入力
scanf()
を使うと空白や改行区切りの入力を簡単に扱えますが、空白で区切らずに行単位で入力を受け取りたいこともあります。
その場合はgets()
を使います。
次のプログラムは2行の入力を行単位で受け取っています。
#include <stdio.h>
int main(void) {
char s[20], t[20];
gets(s); // 変数sで入力を一行受け取る
gets(t); // 変数tで入力を一行受け取る
printf("一行目 %s\n", s);
printf("二行目 %s\n", t);
}
I have a pen.
I have an apple.
一行目 I have a pen.
二行目 I have an apple.
追記: gets関数はひどい脆弱性を抱えているらしいです。入力がしっかりと指定されている競技プログラミングでそこまで問題になることもないかとは思いますが、競技でないプログラミングでは使わないほうがよさそうです。
問題
以下のリンク先に問題文が載っています。
#include <stdio.h>
#include <string.h>
int main(void) {
char S[200];
scanf("%s", S);
// ここにプログラムを追記
}
解答例
必ず自分で問題に挑戦してみてから見てください。
解答例
#include <stdio.h>
#include <string.h>
int main(void) {
char S[200];
scanf("%s", S);
// 計算結果を保持する変数
int answer = 1;
int len = strlen(S);
for (int i = 0; i < len; i++) {
if(S[i] == '+'){
answer++;
}
if(S[i] == '-'){
answer--;
}
}
printf("%d\n", answer);
}
N - 1.13.配列
キーポイント
- __配列__は様々なデータの列を扱うことができる機能
-
型名 配列変数名[要素数]
と書くと指定した要素数で配列変数を宣言できる -
型名 配列変数名[] = { 要素1, 要素2, ... };
で配列を宣言と同時に初期化できる -
配列変数[i]
でi番目の要素にアクセスできる - 配列の要素数を直接知る方法はない
- 配列でN個の入力を受け取るときは、十分大きな要素数で初期化した後、for文の中で
[]
を使って1ずつ受け取る
int v[100];
for(int i = 0; i < N; i++){
scanf("%d", &v[i]);
}
- 配列とfor文を組み合わせると、大量のデータを扱うプログラムを簡潔に書ける
- 配列の添字のルールは文字列と同じ
配列と文字列
文字列は「文字の列」を扱うための機能でした。
__配列__は文字だけでなく、様々なデータの列を扱うことができる非常に重要な機能です。
文字列と配列は使い方もある程度同じです。
次のプログラムは、「'a'
, 'b'
, 'c'
, 'd'
」という文字の列と、「25
,100
,64
」という整数の列を扱っています。
#include <stdio.h>
#include <string.h>
int main(void){
char str[] = "abcd"; // 'a', 'b', 'c', 'd' という文字(char)の列
printf("%c\n", str[0]); // 1つ目である'a'を出力
printf("%d\n", strlen(str)); // 文字列の長さである4を出力
int data[] = { 25, 100, 64 }; // 25, 100, 64 という整数(int)の列
printf("%d\n", data[0]); // 1つめである25を出力
printf("%d\n", sizeof(data)/sizeof(int)); // 配列の長さである3を出力
}
a
4
25
3
配列変数の宣言
配列変数は次の形式で宣言します。
型名 配列変数名[要素数];
int data[10]
と書いた場合、int型のデータ列を扱う要素数10の配列変数dataが宣言されます。
配列変数への代入
配列変数に値を代入する方法として
配列変数a = 配列変数b;
のようなものが思い浮かぶかもしれませんが、文字列のときにも説明したようにこの書き方はできません。(コンパイルエラーになります)
そのため、
型名 配列変数名[] = { 要素1, 要素2, ... };
のように書いて初期化の段階で値を入れるか、i番目の要素に要素iを代入する ということを繰り返していくことになります。
i番目の要素
文字列と同様に、ほかの配列も[i]
を使ってi番目の要素へアクセスできます。
配列変数[i]
添字は0から始まります。
int data[] = { 25, 100, 64 };
という配列変数dataの場合、添字の値と文字の対応は次の表のとおりです。
添字 | 0 | 1 | 2 |
---|---|---|---|
要素 | 25 | 100 | 64 |
配列の要素数
文字列のときはstrlen()
を使って要素数(長さ)を取得できましたが、ほかの配列にはそのような便利関数はありません。
そこで、sizeof()
というものを利用します。
sizeof(配列変数)
と書くと、配列のサイズがわかります。
え!?じゃあそれでいいじゃん! と思った方もいるかもしれませんが、配列のサイズというのはメモリを専有しているサイズのことです。つまり、要素数のことではありません。
int型が4byteだとすると、int data[] = { 25, 100, 64 };
というのはint 型が3つであるため全体で12byteになります。
よって、その場合sizeof(data)
とすると12と返ってきます。
ここで、sizeof(型)
と書くと、型のサイズもわかります。
よって、int型が4byteだとするとsizeof(int)
と書くと4と返ってきます。
これらを組み合わせて、
int size = sizeof(配列変数) / sizeof(型);
と書くことで、要素数を取得することができます。
例えば
int data[] = { 25, 100, 64 };
int size = sizeof(data)/sizeof(int);
と書くとsize
が3になるわけです
配列と変数
配列は「複数の変数を一度に宣言する方法」のように使うことができます。
次のプログラムは、3つの整数を入力から受け取り、それらの合計を出力します。
#include <stdio.h>
int main(void){
int a1, a2, a3;
scanf("%d%d%d", &a1, &a2, &a3);
printf("%d\n", a1 + a2 + a3);
}
次のプログラムは上のプログラムとほとんど同じ意味です。
#include <stdio.h>
int main(void){
// 3個の入力を受け取れるように 3要素の配列を宣言
int data[3];
// []を使って1つずつ入力
scanf("%d%d%d", &data[0], &data[1], &data[2]);
// 和を出力
printf("%d\n", data[0] + data[1] + data[2]);
}
配列変数の初期化
配列変数は次の形式で初期化します。
型名 配列変数名[] = { 要素1, 要素2, ... };
このように書いた場合、{ }
の中の要素で(その要素数で)初期化されて宣言ます。
また、
型名 配列変数名[100] = { 要素1, 要素2, ... };
のように書くこともできます。
この場合、要素数は[]
の中の数字で初期化されます。要素の値は{ }
の中の要素で初期化されて、指定した要素数のほうが書いた要素の数より大きかった場合は残りの部分はすべて0
で初期化されます。(指定した要素数より多くの要素を書いてはいけません)
つまり、
data[10] = { 3, 1, 4, 1, 5, 9 };
と書くと
data[] = { 3, 1, 4, 1, 5, 9, 0, 0, 0, 0};
とほとんど同じ意味です。
これを利用して、
data[10] = { };
のように書いて全ての要素を0
で初期化することもできます。
配列変数への入力
配列変数で入力を受け取るには、十分な大きさで配列を初期化した後、[i]
を使って一つずつ受け取っていく必要があります。
// 3個の入力を受け取れるように 3要素の配列を宣言
int data[3];
// []を使って1つずつ入力
scanf("%d%d%d", &data[0], &data[1], &data[2]);
for文を使った入力
入力を配列変数で受け取る場合、for文を使って入力処理を書くのが一般的です。
#include <stdio.h>
int main(void){
// 100要素の配列を宣言
int data[100];
// 100個の入力を受け取る
for(int i = 0; i < 100; i++){
scanf("%d", &data[i]);
}
}
入力の個数が多いときでも、for文を使えば簡潔に入力処理を書くことができます。
配列の使い所
配列とfor文を組み合わせると、大量のデータを扱うプログラムを書くことができます。
次の例題を見て下さい。
例題
$N$人の数学と英語のテストの点数が与えられます。
それぞれの人について、数学と英語の点数の合計点を計算してください。
制約
$0 \le N \le 1000$
N
1人目の数学の点数 2人目の数学の点数 ... N人目の数学の点数
1人目の英語の点数 2人目の英語の点数 ... N人目の英語の点数
1人目の数学と英語の合計点
2人目の数学と英語の合計点
.
.
.
N人目の数学と英語の合計点
3
20 100 30
100 5 40
120
105
70
解答例
$N$が小さい場合、配列を使わずにint型の変数だけでこの問題を解くことも可能です。
しかし、$N$が大きい場合は配列を使わずに書くのは非常に大変になります。
例えば $N=1000$ だったとき、変数を1000個宣言しなければなりません。
配列とfor文を使えば、$N$の大きさに関わらず簡潔に処理を書くことができます。
#include <stdio.h>
int main(void){
int N;
scanf("%d", &N);
int math[1500]; // 数学の点数データ
int english[1500]; // 英語の点数データ
// 数学の点数データを受け取る
for (int i = 0; i < N; i++) {
scanf("%d", &math[i]);
}
// 英語の点数データを受け取る
for (int i = 0; i < N; i++) {
scanf("%d", &english[i]);
}
// 合計点を出力
for (int i = 0; i < N; i++) {
printf("%d\n", math[i] + english[i]);
}
}
注意点
範囲外エラー
文字列と同様に、配列も添字の値が正しい範囲内に無いときも実行時エラーに__なりません!__
添え字の範囲には、最大限注意しましょう。
大きすぎる配列
配列の要素数が多すぎると、実行時エラーになることがあります。
どの程度までの大きさの配列が確保できるかは実行環境によります。
AtCoderの問題の回答として提出してこのエラーが出た場合、REまたはMLEと表示されます。
問題
以下のリンク先に問題文が載っています。
#include <stdio.h>
int main(void){
int N;
scanf("%d", &N);
}
解答例
必ず自分で問題に挑戦してみてから見てください。
解答例
#include <stdio.h>
int main(void){
int N;
scanf("%d", &N);
int A[1050];
for (int i = 0; i < N; i++) {
scanf("%d", &A[i]);
}
int sum = 0;
for (int i = 0; i < N; i++) {
sum += A[i];
}
int mean = sum / N;
for (int i = 0; i < N; i++) {
if(A[i] > mean) {
printf("%d\n", A[i] - mean);
}
else {
printf("%d\n", mean - A[i]);
}
}
}
配列とループ構文がわかっていれば、AtCoderのコンテストに参加するのに最低限の知識を持っていると言えます。
ぜひリアルタイムでコンテストに挑戦してみてください。
O - 1.14.標準ライブラリの関数(原題:STLの関数)
キーポイント
- __関数__を使うとプログラムのまとまった機能を簡単に使うことができる
- C言語で用意されている、関数等がまとまっているもののことを__標準ライブラリ__という
-
関数名(引数1, 引数2, ...)
で関数を呼び出せる - 関数に
( )
で渡す値のことを__引数__という - 関数の計算結果のことを__返り値__または__戻り値__という
- 引数と返り値は型のルールが決まっており、間違えるとコンパイルエラーになる
標準ライブラリの関数
__関数__を使うとプログラムのまとまった機能を簡単に使うことができます。
「プログラミングの関数」と「数学の関数」は似た部分もありますが、基本的には別物であることに注意してください。
関数の使い方
「変数の絶対値を出力するプログラム」を例として見てみましょう。
関数を使わないで書く場合、次のようになります。
#include <stdio.h>
int main(void){
int a = 10, b = -5;
int ansa, ansb;
if(a >= 0){
ansa = a;
}else{
ansa = -a;
}
if(b >= 0){
ansb = b;
}else{
ansb = -b;
}
printf("%d %d", ansa, ansb);
}
10 5
「abs関数」を使えば次のように書くことができます。
(stdlib.h をインクルードすると使えます)
#include <stdio.h>
#include <stdlib.h>
int main(void){
int a = 10, b = -5;
int ansa = abs(a);
int ansb = abs(b);
printf("%d %d", ansa, ansb);
}
10 5
標準ライブラリとは
C言語ではabs
の他にも様々な関数が用意されており、多くの機能を自分でプログラムを書くこと無く利用できます。
C言語で用意されている、関数等がまとまっているもののことを__標準ライブラリ__といいます。
関数を自分で作る事もできます。それについては「1.15.関数」で説明します。
関数の呼び出し方
関数を使うことを__関数呼び出し__と言います。
関数呼び出しの記法は以下のとおりです。
関数名(引数1, 引数2, ...)
__引数(ひきすう)__とは、関数に渡す値のことです。abs(a)
では変数aがそれに当たります。引数の数は関数によって異なります。
関数の計算結果の値のことを__返り値(かえりち)__または__戻り値(もどりち)__と言います。
int ansa = abs(a);
ではabs関数の返り値をansaに代入しています。
追記: JIS (Japanese Industrial Standards = 日本産業規格) では返却値という用語が当てられているそうです。
引数と返り値の型
引数と返り値は関数によって型のルールが決まっており、間違うとコンパイルエラーになります。
標準ライブラリの関数の使い方を間違えると、非常に長いコンパイルエラー文が出ることがあります。冷静に対処しましょう。
関数の例
標準ライブラリの関数の中からいくつかの関数を紹介します。
abs関数
引数の絶対値を計算し,結果をint型で返します。stdlib.h をインクルードすることで使えます。
よく似た関数に、labs関数、llabs関数、fabs関数などがあります。(型の違いです)
sqrt関数
引数の平方根を計算し、結果をdouble型で返します。math.h をインクルードすることで使えます。
よく似た関数に、sqrtf関数、sqrtl関数などがあります。
配列を引数にする関数
qsort関数
データ列を順番に並び替えることをソートといいます。
qsort関数を使うと、配列の要素を小さい順などに並び替えることができます。stdlib.h をインクルードすることで使えます。
第1引数: 整列したい配列
第2引数: 配列の要素数
第3引数: 配列の要素のサイズ
第4引数: 比較関数
戻り値: なし
この関数は少し特殊で、どんな順番にソートしたいのかを指定するための比較関数を実装しておく必要があります。
比較関数のルールは以下の通りです。
- 第 1 引数を第 2 引数より前にしたい場合: 0 より小さい値を返す
- 第 1 引数と第 2 引数のどちらが前でもいい場合: 0 を返す
- 第 1 引数を第 2 引数よりも後ろにしたい場合: 0 より大きい値を返す
#include <stdio.h>
#include <stdlib.h>
int comp(const void *a, const void *b){
if(*(int*)a > *(int*)b) return 1;
if(*(int*)a < *(int*)b) return -1;
return 0;
}
int main(void){
int data[] = {2, 5, 2, 1};
qsort(data, 4, sizeof(int), comp);
for(int i = 0; i < 4; i++){
printf("%d\n", data[i]);
}
}
1
2
2
5
配列の要素を小さい順(昇順)に並び替えることができました。
比較関数の部分など難しいと思いますが、競技プログラミングでは配列のソートを必要とする場面がかなり多いので頑張って調べるなどしましょう。
問題
以下のリンク先に問題文が載っています。
#include <stdio.h>
int main(void){
int A, B, C;
scanf("%d%d%d", &A, &B, &C);
}
解答例
必ず自分で問題に挑戦してみてから見てください。
解答例
#include <stdio.h>
#include <stdlib.h>
int comp(const void *a, const void *b){
if(*(int*)a > *(int*)b) return 1;
if(*(int*)a < *(int*)b) return -1;
return 0;
}
int main(void){
int A, B, C;
scanf("%d%d%d", &A, &B, &C);
int data[3] = {A, B, C};
qsort(data, 3, sizeof(int), comp);
int ans = data[2] - data[0];
printf("%d\n", ans);
}
せっかくなのでソートを使ってみましたが、特に使う必要はありません。
P - 1.15.関数
キーポイント
- 関数を作成することを関数を定義すると言う
- 関数の返り値は__return文__を使って
return 返り値;
で指定する - 関数の返り値が無い場合は返り値の型に__void__を指定し、return文は
return;
と書く - 関数の引数が不要な場合は定義と呼び出しで
()
だけを書く - 処理がreturn文の行に到達した時点で関数の処理は終了する
- 返り値がある関数で返り値の指定を忘れた場合、どんな値が返ってくるかは決まっていない
- 引数に渡された値は基本的にコピーされる
関数
関数を作成することを関数を__定義__すると言います。
qsortを紹介した時には、比較関数を定義しました。
2つの値のうち、小さいほうを取得する関数を定義してみます。
#include <stdio.h>
int my_min(int x, int y) {
if (x < y) {
return x;
}
else {
return y;
}
}
int main(void){
int ans = my_min(10, 5);
printf("%d\n", ans);
}
5
関数の定義
関数の定義はmain関数より前で行います。
関数定義の記法は次のとおりです。
返り値の型 関数名(引数1の型 引数1の名前, 引数2の型 引数2の名前, ...) {
処理
}
前の節で見たとおり、引数は「関数に渡す値」、返り値は「関数の結果の値」のことです。
my_min関数はint型の引数を2つ受け取り、計算結果としてint型の値を返すので、定義は次のようになります。
int my_min(int x, int y) {
}
呼び出し方は標準ライブラリの関数と同様です。
次のように呼び出した場合、引数xに10が代入され、引数yに5が代入されます。
my_min(10, 5);
返り値の指定
関数の返り値は、__return文__で指定します。
return 返り値;
my_min関数では2つの引数x
,y
のうち小さい方を返すので、次のように書きます。
if (x < y) {
return x;
}
else {
return y;
}
返り値が無い関数
関数の返り値は無いこともあります。その場合は返り値の型にvoid
を指定します。
次のプログラムのhello関数は、引数の文字列の始めに"Hello, "
をつけて出力します。
返り値は必要ないのでvoid型を指定しています。
#include <stdio.h>
void hello(char str[]){
printf("Hello, %s\n", str);
return;
}
int main(void){
hello("Tom");
hello("C");
}
Hello, Tom
Hello, C
返り値がvoid型である関数のreturn文は次のように書きます。
return;
引数が無い関数
関数の引数が不要な場合は、定義では(void)
を書き、呼び出しでは()
だけを書きます。
#include <stdio.h>
// 整数の入力を受け取って返す関数
// 引数なし
int input(void) {
int x;
scanf("%d", &x);
return x;
}
int main(void){
int num = input(); // 引数なし
printf("%d\n", num + 5);
}
10
15
関数を自分で定義する理由
関数を自分で定義する理由はいくつかありますが、その中から3つを紹介します。
- プログラムの重複が避けられる
- 処理のかたまりに名前をつけられる
- 再帰関数が書ける
大規模なプログラムを書く際は1.と2.による恩恵が大きいです。
競技プログラミングでは3.のために関数を定義することが多いです。詳しくは「2.04.再帰」で説明します。2
注意点
return文の動作
処理がreturn文の行に到達した時点で関数の処理は終了します。
一つの関数の中にreturn文が複数あっても問題ありませんが、return文より後に書いた処理は実行されないことに注意しましょう。
次のプログラムでは、"Hello, "を
出力した次の行でreturn;
と書いているため、その後の処理は実行されません。
#include <stdio.h>
void hello(void){
printf("Hello, ");
return; // この行までしか実行されない
printf("world!");
return;
}
int main(void){
hello();
}
Hello,
返り値の指定忘れ
返り値がある関数でreturn文を忘れると、どんな値が返ってくるかは決まっていません。
追記: 正確には、返り値の指定されていない関数の返り値を使おうとした時の動作が未定義らしいです。
次のプログラムのmy_min関数は、if文の中が実行されない場合に返り値を指定していません。
#include <stdio.h>
int my_min(int x, int y) {
if (x < y) {
return x;
}
// x >= y のときのreturn忘れ
}
int main(void){
int ans = my_min(10, 5);
printf("%d\n", ans);
}
0
このプログラムでは0
が返ってきますが、0
でない値が返ってくることもあります。
返り値がある関数では、どのような引数のパターンでも必ずreturnするように注意しましょう。
引数はコピーされる
他の変数に値を代入したとき同様に、基本的に引数に渡した値はコピーされます。
次のプログラムのadd5関数は関数内で引数に5
を足していますが、呼び出し元の変数numの値は変化しません。
#include <stdio.h>
// 引数の値に5を足して出力する関数
void add5(int x) {
x += 5;
printf("%d\n", x);
return;
}
int main(void){
int num = 10;
add5(num); // numの値は引数xにコピーされる
printf("%d\n", num);
}
15
10
よくわからない人は1.04.変数と型の「変数はコピーされる」を読んでください。
なお、配列を引数にした場合は別です。
###関数が呼び出せる範囲
関数は宣言した行より後でしか呼び出せません。
次のプログラムでは、hello関数を定義した行より前でhello関数を呼び出しているため、コンパイルエラーが発生しています。
#include <stdio.h>
int main(void){
hello();
}
void hello(void){
printf("hello!\n");
return;
}
......
なんでコンパイルエラーでないの!?
コンパイラさんがとても優秀だとコンパイルエラーがでなくて普通に実行できてしまうこともあるようです。しかしさすがにこれに期待してmain関数より後に書くのは危険だと思うので、前に書くようにするか、後述する「プロトタイプ宣言」を利用しましょう。
細かい話
細かい話なので、飛ばして問題を解いても良いです。
プロトタイプ宣言
__プロトタイプ宣言__をすれば関数を定義する前に呼び出すことができます。
プロトタイプ宣言とは、「関数の名前」と「引数と返り値の型」だけを先に宣言することです。
#include <stdio.h>
// プロトタイプ宣言
void hello(void);
int main(void){
// プロトタイプ宣言をしたので呼び出せる
hello();
}
void hello(void){
printf("hello!\n");
return;
}
hello!
プロトタイプ宣言の記法は次のようになります。
返り値の型 関数名(引数1の型 引数1の名前, 引数2の型 引数2の名前, ...);
引数名の省略
引数の名前は省略することもできます。
例えば、my_min関数のプロトタイプ宣言は次のように書くこともできます。
int my_min(int, int);
returnの省略
返り値がない場合、関数の末尾ではreturnを省略できます。
void hello(char str[]){
printf("Hello, %s!\n", str);
}
main関数
はじめのプログラムから出てきていた__main関数__も関数の一つです。
ただし、main関数は特別扱いされていて、returnを省略した場合は必ず0
が返るようになっています。
#include <stdio.h>
int main(void){
printf("Hello, world!\n");
// return 0; と書くのと同じ
}
Hello, world!
変数のスコープ
関数内の変数のスコープは、呼び出し元とは完全に分けられています。
次のプログラムでは、test関数とmain関数の両方でnum
という名前の変数を定義していますが、その2つは完全に別の変数として扱われます。
#include <stdio.h>
void test(void){
// test関数のスコープで変数numを宣言
int num = 5;
printf("%d\n", num); // 5
return;
}
int main(void){
// main関数のスコープで変数numを宣言
int num = 10;
test();
printf("%d\n", num); // 10
}
5
10
問題
以下のリンク先に問題文が載っています。
#include <stdio.h>
// 1人のテストの点数を表す配列から合計点を計算して返す関数
// 引数 scores: scores[i]にi番目のテストの点数が入っている
// 引数 N: テストの個数
// 返り値: 1人のテストの合計点
int sum(int scores[], int N) {
// ここにプログラムを追記
}
// 3人の合計点からプレゼントの予算を計算して出力する関数
// 引数 sum_a: A君のテストの合計点
// 引数 sum_b: B君のテストの合計点
// 引数 sum_c: C君のテストの合計点
// 返り値: なし
void output(int sum_a, int sum_b, int sum_c) {
// ここにプログラムを追記
}
// -------------------
// ここから先は変更しない
// -------------------
// N個の入力を受け取って配列に入れて返す関数
// 引数 scores: 入力を受け取る配列
// 引数 N: 入力を受け取る個数
// 返り値: なし
void input(int scores[], int N) {
for (int i = 0; i < N; i++) {
scanf("%d", &scores[i]);
}
return;
}
int main(void){
// 科目の数Nを受け取る
int N;
scanf("%d", &N);
// それぞれのテストの点数を受け取る
int A[200], B[200], C[200];
input(A, N);
input(B, N);
input(C, N);
// それぞれの合計点を計算
int sum_A = sum(A, N);
int sum_B = sum(B, N);
int sum_C = sum(C, N);
// プレゼントの予算を出力
output(sum_A, sum_B, sum_C);
}
解答例
必ず自分で問題に挑戦してみてから見てください。
解答例
#include <stdio.h>
// 1人のテストの点数を表す配列から合計点を計算して返す関数
// 引数 scores: scores[i]にi番目のテストの点数が入っている
// 引数 N: テストの個数
// 返り値: 1人のテストの合計点
int sum(int scores[], int N) {
int s = 0;
for (int i = 0; i < N; i++) {
s += scores[i];
}
return s;
}
// 3人の合計点からプレゼントの予算を計算して出力する関数
// 引数 sum_a: A君のテストの合計点
// 引数 sum_b: B君のテストの合計点
// 引数 sum_c: C君のテストの合計点
// 返り値: なし
void output(int sum_a, int sum_b, int sum_c) {
printf("%d\n", sum_a * sum_b * sum_c);
}
// -------------------
// ここから先は変更しない
// -------------------
// N個の入力を受け取って配列に入れて返す関数
// 引数 scores: 入力を受け取る配列
// 引数 N: 入力を受け取る個数
// 返り値: なし
void input(int scores[], int N) {
for (int i = 0; i < N; i++) {
scanf("%d", &scores[i]);
}
return;
}
int main(void){
// 科目の数Nを受け取る
int N;
scanf("%d", &N);
// それぞれのテストの点数を受け取る
int A[200], B[200], C[200];
input(A, N);
input(B, N);
input(C, N);
// それぞれの合計点を計算
int sum_A = sum(A, N);
int sum_B = sum(B, N);
int sum_C = sum(C, N);
// プレゼントの予算を出力
output(sum_A, sum_B, sum_C);
}
終わりに
ここまで読んでいただきありがとうございました。これで APG4b 第一章 は修了となります。そして、それと同時にこの記事も完結となります。
あなたは理論上は全ての「計算処理」を行うプログラムを書ける程の知識を身に着けました。これから競プロにはまってAtCoder Beginner Contestなどに出てみてくれたらうれしいです。
競プロはおもしろそうだけど何をしたらわからない!という方は、AtCoder Beginners Selectionにチャレンジしてみましょう!
C言語での解説はQiitaの記事があったのでここには書きません。
はじめに書いたように、問題があれば本記事は削除します。その点はよろしくお願いいたします。
また、この記事は競技プログラミング入門のために説明をかなりかみ砕いて書いており、筆者もC言語をあまり知っていないので、ある程度知識がついた人はしっかりと自分で調べなおしてみることをお勧めします。