今日は88ページの文字定数と前置増分演算子と前置減分演算子から始めたいと思います
88ページ・前置増分演算子と前置減分演算子
今日は前置増分演算子と前置減分演算子からです
了解です!「まず計算してから代入(参照)される」という、前置ならではの動きを確認しましょう。
前置増分 (++i) と 前置減分 (--i)
これらは、変数の値を1増やしたり減らしたりした「後」の値が、その式の値として使われます。
#include <stdio.h>
int main() {
int a = 5;
int b = 5;
// 前置増分: aを1増やしてから、その値をxに代入
int x = ++a;
// 前置減分: bを1減らしてから、その値をyに代入
int y = --b;
printf("a: %d, x: %d\n", a, x); // 両方 6
printf("b: %d, y: %d\n", b, y); // 両方 4
return 0;
}
ここがポイント
++aは、a = a + 1;を実行した直後のaを使うイメージです。- よく比較される後置 (
a++) との違いは、「いつプラスされるか」のタイミングにあります。
そう、私も知りたいのは前置と後置の1増えるタイミングがいったいどこなのかっていうのが知りたいです
さっき提示してくれたプログラムの「int x = ++a;」
例えばこの文だったらどのタイミングでインクリメントされてるの?代入前?
結論から言うと、「代入前」です!
処理の順番を細かく分解すると、以下のようになっています。
- まず、変数
aの値が +1 される(インクリメント実行) - 新しくなった
aの値を読み出す - その値を
xに 代入 する
ですので、int x = ++a; が終わった直後は、x も a も同じ値になります。
これに対して、後置の x = a++; だと「代入が先、インクリメントが後」という逆の動きになります。
後置の場合は代入を行ってからになるんだよね?どのタイミングで1増えるかをちゃんと知っておきたいです
厳密に言うと、C言語のルール(副作用完了点)に基づいた以下の順序で動いています。
1. 後置演算子 (x = a++;) のタイミング
「代入してから増える」と教わることが多いですが、正確には「元の値をキープしておき、それを使って代入し、直後に変数を増やす」という流れです。
- 値をメモする:
aの現在の値(例:5)を一時的にどこかに保存する。 - インクリメント:
aの値を 1 増やす(ここでaは 6 になる)。 - 代入: 最初にメモしておいた「元の値(5)」を
xに代入する。
結果: x は 5、a は 6 になります。
2. 前置演算子 (x = ++a;) のタイミング
こちらはシンプルに「先に変化を完了させる」動きです。
- インクリメント:
aの値を 1 増やす(ここでaは 6 になる)。 - 代入: 増えた後の
aの値(6)をxに代入する。
結果: x は 6、a は 6 になります。
まとめ:いつ「1」増えるのか?
どちらも 「その行(セミコロン ; に到達するまで)の処理が終わる前」 には、変数 a 自体は必ず増えています。
- 前置 (
++a): 代入の 前 に増える。 - 後置 (
a++): 代入用の値を確定させた 直後(代入とほぼ同時、または直前)に増えるが、代入には 増える前の値 が使われる。
その書き方だと、後置の場合どこかに数値を保存しておかないといけないから、変数a以外にメモリ使って保存してるってこと?
その通りです!非常に鋭い洞察ですね。
CPUやメモリの内部的な動きとしては、仰る通り「増やす前の値を一時的に保存しておく場所(レジスタやスタック)」を余計に使用しています。
概念的な動作を比較すると、以下のようになります。
前置 (++a) のイメージ
無駄がありません。
aを直接書き換えて 6 にする。- その
a(6) をxに渡す。
後置 (a++) のイメージ
一工程増えます。
aの値 5 を「一時保存用メモリ」にコピーする。 ← ここでメモリを使う!aを書き換えて 6 にする。- 「一時保存用メモリ」にある 5 を
xに渡す。
プログラミング的な観点
- 計算効率:
最近のコンパイラは非常に賢いので、x = a++;のように結果を代入せず、単にa++;とだけ書いた場合は、前置と同じくらい効率的に動くよう自動で最適化してくれます。 - C++などでの影響:
今はint型なので微々たる差ですが、将来的に C++ などの言語で「巨大なデータ(クラス)」を扱うようになると、この「一時コピー」のコストが無視できなくなるため、「特に理由がなければ前置 (++i) を使う」という習慣を持つプログラマも多いです。
「コンピュータが裏でこっそり作業用のメモ帳(メモリ)を使っている」というイメージ、バッチリ合っています。
そうだったんだね。前に書くか後に書くかだけじゃなく、処理そのものに影響が出てくるんだね
そうなんです!単なる「書き方の好み」ではなく、コンピュータの作業手順(アルゴリズム)そのものが変わっている、というのが面白いところですよね。
これを知っておくと、将来的に複雑なコードを読んだときも「ここで値が保持されているから、この計算結果になるんだな」と正確に読み解けるようになります。
演習4-9
まずputcha関数を使うことはわかったので一度書いてみます
#include <stdio.h>
int main(void) {
int no;
printf("正の整数を入力せよ:");
scanf("%d",&no);
while(no-- > 0){
putchar('+');
putchar('-');
}
return 0;
}
うわ、これだと+-が5回で10個表示されちゃう。
もう少し考えないと
まずは「no-- > 0」という条件式を使って、学習したばかりの後置減分演算子をさっそく実戦投入されているのが素晴らしいです。
実行結果を確認してみると、入力した数に対して「出力される文字の総数」に注目すると、改善のヒントが見つかるかもしれません。
例えば、3 と入力したときに、期待される動作は +-+(3文字)ですが、現在のコードだとどうなるでしょうか……?
納得いくまで、ぜひいろいろな数字を入れて試行錯誤してみてください!
偶数と奇数で表示させる文字を変えてみたよ。奇数の時は+偶数の時はー
#include <stdio.h>
int main(void) {
int no;
printf("正の整数を入力せよ:");
scanf("%d",&no);
while(no-- > 0){
if(no % 2 ==1){
putchar('+');
}else{
putchar('-');
}
}
return 0;
}
あーーこれだと入力値が偶数か奇数かで最初に表示される文字が+とーが逆になってしまう。・・・。入力された値じゃなくループのカウント数を使えばいいかも
#include <stdio.h>
int main(void) {
int no;
printf("正の整数を入力せよ:");
scanf("%d",&no);
int i = 0;
while(no-- > 0){
if(++i % 2 ==1){
putchar('+');
}else{
putchar('-');
}
}
return 0;
}素晴らしいです!正解です。
++i(前置増分) を使って、「1回目は1(奇数)なので +」「2回目は2(偶数)なので -」というロジックを完璧に組み立てられましたね。
先ほど学んだ「前置ならその場で増えて、その増えた値が計算に使われる」という性質が、++i % 2 の部分で実に見事に活かされています。
このコードなら:
noに3を入れたら:1回目(+),2回目(-),3回目(+)で止まる。noに0以下の値を入れたら:whileの条件が最初から偽になり、何も表示されない。
という、問題の要求をすべて満たしています。
whileの条件で0より大きい場合だけ表示になってるから0以下は何も表示しないという条件にもあってるよね?
その通りです!完璧な理解です。
while(no-- > 0) という条件式が、非常にスマートに2つの役割をこなしています。
- 0以下を弾く: 最初に
noが0や-5だった場合、no > 0が最初から「偽」になるので、whileの中身(putcharなど)は一度も実行されません。 - 回数を制御する: 正の数が入力されたときは、1回処理するごとに
noが減っていくので、ちょうど入力された回数分だけ繰り返して止まります。
「問題文の条件を、ループの入り口で自然にクリアしている」という、無駄のない綺麗なロジックですね。
カウント数 i の初期化を1にしたら後置 i++でもなりたちますか?
結論から言うと、成り立ちます!
ですが、if文の条件式の書き方に少し注意が必要です。比較してみましょう。
i = 1 で後置 i++ を使う場合
int i = 1;
while(no-- > 0){
if(i++ % 2 == 1){ // 先に1で判定してから、iが2に増える
putchar('+');
} else {
putchar('-');
}
}この場合、最初のループは 1 % 2 == 1 が判定に使われるので、無事に + が出ます。
「判定に使われる値」の推移は 1, 2, 3, 4… となるので、期待通り交互に表示されます。
もし i = 1 で前置 ++i を使った場合(ズレる例)
int i = 1;
while(no-- > 0){
if(++i % 2 == 1){ // 先に2に増やしてから判定に使う
putchar('+');
} else {
putchar('-');
}
}これだと、最初の判定が 2 % 2(偶数)から始まってしまうため、- から表示されてしまいます。
まとめ
- 初期値 0 + 前置
++i: 最初の判定は 1 - 初期値 1 + 後置
i++: 最初の判定は 1
どちらも「最初の判定を 1(奇数)にする」という目的は同じなので、組み合わせ次第でどちらでも正解になります。
「初期値」と「演算子の位置」のコンビネーションで結果が変わる、という感覚が掴めてきましたね!
演習4-10
これはさっきのプログラムを利用したら簡単でした
#include <stdio.h>
int main(void) {
int no;
printf("正の整数を入力せよ:");
scanf("%d",&no);
while(no-- > 0){
putchar('*');
putchar('\n');
}
return 0;
}
正解です!完璧ですね。
putchar('\n'); を使って、1つ出すたびに改行する。まさに「縦に連続して表示」という要件をストレートに満たした、非常に読みやすいコードです。
演習4-9で学んだ「no-- > 0 で回数制御と0以下除外を同時に行う」テクニックも、しっかり自分のものにされているのがわかります。
演習4-11
これは以前よくやった、新しい変数に記憶していくという手法ですね。
#include <stdio.h>
int main(void) {
int no;
do{
printf("正の整数を入力せよ:");
scanf("%d",&no);
if(no <= 0)
puts("\a正でない数字を入力しないでください");
}while(no <= 0);
int moto = no;
printf("%dを逆から読むと",moto);
while(no > 0){
printf("%d",no % 10);
no /= 10;
}
puts("です");
return 0;
}
正解です!完璧な修正ですね。
新しく int moto = no; という変数を用意して、値をコピーしておく。この「元の値をバックアップしておく」という考え方は、実際の開発現場でも非常によく使われる重要なテクニックです。
一つ質問があります。この変数に取っておくという手法ですが、noを変数に代入するタイミングが非常に大事ではないですか?
その通りです!タイミングがすべてと言っても過言ではありません。
プログラムは上から下へ一行ずつ実行されるので、「データが壊れる(書き換わる)前」にバックアップを取る必要があります。
変数の値を保存するときは、「その値が確定し、かつ書き換えられる直前」という「一瞬の隙」を狙って代入する必要があります。
この「データの生存期間(いつ値が入り、いつ消えるか)」を意識できるようになったのは、プログラミングのレベルが一段上がった証拠ですよ!
あとですが、{}の中で宣言した変数は{}を出ると消えちゃうって習いましたよね?だからあえて外に書いたんですが。なので保存しておく変数(moto)の宣言はdoの中などでしてはいけませんよね?
非常に重要なルールをしっかり守られていますね。
C言語には「スコープ(変数の有効範囲)」というルールがあり、{ }(ブロック)の中で宣言した変数は、そのブロックを出ると消えてしまいます。
もし do { ... } while (...); の中で int moto = no; を宣言してしまうと、その後の printf では「moto って誰?」とコンパイラに怒られてしまいます。
int no;の宣言と一緒にint moto;だけをdo{}の外でしておけばdoの中でscanfの直後に代入しても大丈夫じゃないですか?
int no;
int moto; // 外で宣言しておく
do {
// ... scanf ...
moto = no; // 中で代入する
} while (no <= 0);
// ここでも moto は消えずに使える!その方法でも全く問題なく動作します。変数の「宣言(箱を作る)」と「代入(中身を入れる)」を分けて考えれば、宣言さえ外で済ませておけば、中で何度中身を書き換えても、ブロック({ })を出た後にその値を持ち出すことができます。
ちなみに、最近のC言語(C99規格以降)では、今回のコードのように「使う直前で宣言する」書き方が推奨されています。昔のC言語は「関数の先頭で全部宣言しなきゃいけない」という厳しいルールがありましたが、今の書き方のほうが「あ、ここでバックアップを取ったんだな」と意図が伝わりやすくて良いですね。
今回のケースで、あえてループの外(直後)で宣言と代入を同時に行うメリットを挙げるとすれば、「正しい入力(正の整数)が確定した瞬間の値だけを保存する」という意図がより明確になる、という点くらいです。
どちらも正解ですが、「宣言さえ外にあれば、中での変更は有効」という理解は、今後の複雑なループ処理を書く際にとても役立ちます!
演習4-12
これはヒントもあるしサクッととけそうです!
#include <stdio.h>
int main(void) {
int no;
do{
printf("正の整数を入力せよ:");
scanf("%d",&no);
if(no <= 0)
puts("\a正でない数字を入力しないでください");
}while(no <= 0);
int moto = no;
int i = 0;
printf("%dは",moto);
while(no > 0){
no /= 10;
++i;
}
printf("%d桁です\n",i);
return 0;
}正解です!完璧ですね。
while ループの回数を数えるために、専用の変数 i を用意して ++i でカウントアップする。そして、ループが終わった後にその i を表示する。論理的に非常にスッキリとした素晴らしいコードです。
今回のポイントを整理すると:
- バックアップ:
int moto = no;で元の値をしっかり保護している。 - カウント:
whileループの中でno /= 10するたびに、桁数カウンタiを増やしている。 - 終了条件:
noが0になった瞬間にループを抜けるので、ちょうど桁数分だけiが増える。
この「回数を数える変数(カウンタ)」を用意してループを回す手法は、今後あらゆるプログラムで登場する超重要テクニックです!
正直このプログラムで一番むずかしいのは no /= 10が桁を表すことになるっていう考え方です。思いつかないもん。でもそれは最初から使っていいヒントだったのでサクッとできました。
その感覚、すごく正しいです!「10で割っていくことが桁を数えることになる」というのは、数学的な発想というか、プログラミング特有の「定番のパターン(定石)」なんですよね。
日常生活では「123は、1、2、3…3桁だ」と目で見て数えますが、コンピュータには「123」という一つの塊の数字しか見えていません。それを、
123 / 10 = 12(1回割れた!1桁ある)12 / 10 = 1(2回割れた!2桁ある)1 / 10 = 0(3回割れた!3桁ある、終了!)
という風に、「一の位を削り落としながらカウントする」というアルゴリズムに翻訳する必要があるんです。
これは「思いつく」というより、一度経験して「あ、こういうやり方があるんだ!」と引き出しにしまっておく知識に近いので、今回でしっかり自分の武器になったはずですよ。
