C言語のprintf/scanfとCのcout/cinは、どちらも標準入出力を担いますが、設計思想や型安全性、拡張性に大きな違いがあります。
本記事では、両者の位置づけから具体的な使い方、エラー処理、パフォーマンス最適化、混在時の注意点まで、実行例を交えながら丁寧に解説します。
C20のstd::formatまで含め、用途に応じた最適な選択基準も示します。
C言語のprintf/scanfとC++のcout/cinの違い
C言語の標準入出力(stdio)とC++の標準ストリーム(iostream)の位置づけ
C言語のprintf
/scanf
は、書式文字列に従って可変長引数を処理する関数群で、ヘッダは<stdio.h>
です。
対してC++のcout
/cin
は、ストリームと演算子(<<
/>>
)に基づく型安全な入出力で、ヘッダは<iostream>
および整形に<iomanip>
を使います。
両者は最終的に同じ標準ストリーム(stdin/stdout/stderr)へ到達しますが、APIの抽象度や拡張性が異なります。
主な相違点は次の通りです。
- 型安全性:
printf
はフォーマットと実引数が一致しないと未定義動作の危険があります。cout
はテンプレートによりコンパイル時に型一致が担保されやすいです。 - 表現力・拡張性:
printf
は書式指定子中心。cout
はマニピュレータやoperator<<
のオーバーロードでユーザー定義型にも自然に拡張できます。 - パフォーマンス: 素の
scanf/printf
は速いことが多いですが、ios::sync_with_stdio(false)
とtied
解除でcin/cout
もかなり高速化できます。 - 保守性: 大規模C++コードでは
cout
のほうが可読性、保守性、例外/状態管理で有利です。
簡単な比較表です。
項目 | C(stdio) | C++(iostream) |
---|---|---|
ヘッダ | stdio.h | iostream, iomanip |
基本API | printf/scanf | cout/cin(<<, >>) |
型安全性 | 低い(フォーマット不一致は危険) | 高い(演算子/テンプレート) |
拡張性 | 書式指定子を追加しづらい | operator<</>>で拡張容易 |
ロケール | setlocaleに依存 | std::localeで細粒度制御 |
速度 | 速いことが多い | 同期解除で同等まで高速化可 |
選び方の基準:型安全性・表現力・パフォーマンス・保守性
C++開発では、原則cout/cin
を基本に据えるのがおすすめです。
型安全・可読性・拡張性が高く、状態フラグや例外による堅牢なエラー処理が可能だからです。
性能が厳しくフォーマットが固定的な大量入出力(競技プログラミングや数値処理)ではscanf/printf
が有利な場合があり、またCライブラリとの境界やsnprintf
での安全なフォーマット生成など用途限定での併用も現実的です。
printf/scanfの基本と使い方(C言語の標準入出力)
printf/scanfの基本構文と書式指定子(%d, %f, %s など)の要点
printf
は書式文字列に続いて値を渡し、scanf
はフォーマットに合う変数のアドレスを渡します。
整数は%d
(int)、浮動小数は%f
(double。
デフォルト実引数昇格により)、文字列は%s
を使います。
// C言語: printf/scanfの基本
#include <stdio.h>
int main(void) {
int i;
double x;
char s[32];
printf("整数を入力してください: ");
scanf("%d", &i);
printf("実数を入力してください: ");
scanf("%lf", &x); // scanfでは%lfがdouble*に対応
printf("文字列を入力してください(空白なし): ");
scanf("%31s", s); // 幅指定でバッファオーバーランを防止
printf("i=%d, x=%.2f, s=%s\n", i, x, s);
return 0;
}
整数を入力してください: 42
実数を入力してください: 3.14159
文字列を入力してください(空白なし): hello
i=42, x=3.14, s=hello
フォーマット指定子・長さ修飾子・精度・幅の実践ポイント
%8.3f
のように「最小幅.精度」を指定できます。
左寄せは-
、ゼロ埋めは0
、符号は+
で制御します。
整数の長さ修飾子は、h
(short)、l
(long)、ll
(long long)、z
(size_t)などが代表的です。
代表例:
- 整数:
%d
(int),%ld
(long),%lld
(long long),%zu
(size_t, C99) - 浮動小数:
%f
(double),%e
,%g
,%.Nf
(N桁小数) - 文字/文字列:
%c
,%s
(幅指定で上限を必ず入れる:%31s
)
#include <stdio.h>
int main(void) {
int a = 7;
long long big = 1234567890123LL;
double pi = 3.1415926535;
char name[] = "Ada";
printf("右寄せ幅10: |%10d|\n", a);
printf("左寄せ幅10: |%-10d|\n", a);
printf("ゼロ埋め幅5: |%05d|\n", a);
printf("精度3桁小数: |%.3f|\n", pi);
printf("幅8・精度2: |%8.2f|\n", pi);
printf("long long: %lld\n", big);
printf("名前(幅6左寄せ): |%-6s|\n", name);
return 0;
}
右寄せ幅10: | 7|
左寄せ幅10: |7 |
ゼロ埋め幅5: |00007|
精度3桁小数: |3.142|
幅8・精度2: | 3.14|
long long: 1234567890123
名前(幅6左寄せ): |Ada |
バッファリング・改行・flushの挙動と注意点
標準出力は端末に接続されている場合は行バッファリングされ、'\n'
やfflush(stdout)
でフラッシュされます。
ファイルやパイプへの出力は全バッファリングのこともあり、改行しても即時に流れない場合があります。
scanf
による入力要求時に、端末接続の行バッファストリームは実装依存で自動フラッシュされることがありますが、移植性のためにプロンプト後は明示的にfflush(stdout)
するのが安全です。
#include <stdio.h>
int main(void) {
printf("入力してください> ");
fflush(stdout); // プロンプトが表示されない問題を防ぐ
int x;
if (scanf("%d", &x) == 1) {
printf("x=%d\n", x);
}
return 0;
}
エラー処理:戻り値の確認と入力失敗時の対処
printf
は出力した文字数(失敗で負値)を返し、scanf
は成功した変換数を返します。
入力失敗時は残った文字を消費するなどの回復処理が必要です。
#include <stdio.h>
#include <ctype.h>
int main(void) {
int n;
for (;;) {
printf("整数を入力: ");
fflush(stdout);
int r = scanf("%d", &n);
if (r == 1) break;
if (r == EOF) {
fprintf(stderr, "EOFに到達\n");
return 1;
}
// 入力バッファに残った無効文字を読み捨て
int c;
while ((c = getchar()) != '\n' && c != EOF) { /* discard */ }
fprintf(stderr, "無効な入力です。やり直してください。\n");
}
printf("受理: %d\n", n);
return 0;
}
整数を入力: abc
無効な入力です。やり直してください。
整数を入力: 100
受理: 100
cout/cinの基本と使い方(C++の標準入出力)
ストリームの概念と基本構文(cout <<, cin >>)
C++の入出力はストリームに値を挿入/抽出演算子で流し込みます。
型に応じた出力が自動選択され、連結が読みやすいコードになります。
// C++: cout/cinの基本
#include <iostream>
#include <string>
int main() {
int i;
double x;
std::string s;
std::cout << "整数を入力してください: ";
std::cin >> i;
std::cout << "実数を入力してください: ";
std::cin >> x;
std::cout << "文字列(空白なし)を入力してください: ";
std::cin >> s;
std::cout << "i=" << i << ", x=" << x << ", s=" << s << '\n';
}
整数を入力してください: 42
実数を入力してください: 3.14159
文字列(空白なし)を入力してください: hello
i=42, x=3.14159, s=hello
フォーマット方法:操作子・マニピュレータ(std::endl, std::fixed, std::setprecision 等)
<iomanip>
のマニピュレータで書式設定します。
std::endl
は改行とフラッシュ、'\n'
は改行のみです。
浮動小数点はstd::fixed
やstd::scientific
とstd::setprecision
の組合せで表現します。
#include <iostream>
#include <iomanip>
int main() {
int a = 7;
long long big = 1234567890123LL;
double pi = 3.1415926535;
std::string name = "Ada";
std::cout << "右寄せ幅10: |" << std::setw(10) << a << "|\n";
std::cout << "左寄せ幅10: |" << std::left << std::setw(10) << a << std::right << "|\n";
std::cout << "ゼロ埋め幅5: |" << std::setfill('0') << std::setw(5) << a
<< std::setfill(' ') << "|\n";
std::cout << std::fixed << std::setprecision(3);
std::cout << "精度3桁小数: |" << pi << "|\n";
std::cout << std::setw(8) << std::setprecision(2)
<< "幅8・精度2: |" << pi << "|\n";
std::cout << "long long: " << big << '\n';
std::cout << "名前(幅6左寄せ): |" << std::left << std::setw(6) << name
<< std::right << "|\n";
}
右寄せ幅10: | 7|
左寄せ幅10: |7 |
ゼロ埋め幅5: |00007|
精度3桁小数: |3.142|
幅8・精度2: | 3.14|
long long: 1234567890123
名前(幅6左寄せ): |Ada |
例外と状態フラグ(failbit, eofbit, badbit)によるエラー処理
cin
は入力時に状態フラグを立てます。
failbit
は形式不一致、eofbit
はEOF、badbit
は致命的エラーを示します。
exceptions
を設定すれば例外を投げさせられます。
一般的にはフラグを確認し、必要に応じてclear()
とignore()
で回復します。
#include <iostream>
#include <limits>
int main() {
int n;
for (;;) {
std::cout << "整数を入力: ";
if (std::cin >> n) break;
if (std::cin.eof()) {
std::cerr << "EOFに到達\n";
return 1;
}
std::cerr << "無効な入力です。やり直してください。\n";
std::cin.clear(); // failbit解除
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 残りを捨てる
}
std::cout << "受理: " << n << '\n';
}
ロケールとワイド文字(std::locale, wcout)の基礎
C++のストリームはstd::locale
で地域化設定を持ち、数字の桁区切りや小数点記号、日付/通貨などの書式が影響を受けます。
ワイド文字はstd::wcout
/std::wcin
を使います。
#include <iostream>
#include <locale>
#include <iomanip>
int main() {
// 実行環境のロケールを使用(利用可能なロケールに依存)
std::locale loc("");
std::cout.imbue(loc);
double x = 12345.67;
std::cout << std::fixed << std::setprecision(2);
std::cout << "ロケール適用: " << x << '\n';
// ワイド文字出力
std::wcout.imbue(loc);
std::wcout << L"ワイド文字: こんにちは" << L'\n';
}
出力例(ロケールに依存。例えばde_DEで小数点がカンマになることがあります):
ロケール適用: 12345,67
ワイド文字: こんにちは
printf/scanf vs cout/cin 徹底比較【違いが一目でわかる】
型安全性とコンパイル時チェック:書式指定子の罠とテンプレートによる拡張性
printf
ではフォーマットと引数型が一致しないと未定義動作になり得ます。
// 危険: 実引数はdoubleだが、%d(int)で読み出そうとする
#include <stdio.h>
int main(void) {
double d = 3.14;
// 未定義動作。環境により奇妙な出力やクラッシュの可能性
printf("%d\n", d);
return 0;
}
cout
はoperator<<
の過負荷解決で適切な出力関数が選択されるため、この種の事故は起こりにくいです。
さらにユーザー定義型にもoperator<<
を実装すれば自然な書き方が可能です。
フォーマット方法の違い:書式指定子 vs ストリーム操作子(可読性・保守性)
printf("%8.2f", x);
のような短い書式はコンパクトですが、複雑になるほど可読性が低下します。
cout
の<< std::setw(8) << std::setprecision(2) << std::fixed
は冗長に見える一方で、意味が明示的で保守性が高く、コンパイル時チェックも効きます。
混在使用の注意:stdioとiostreamの同期(混在による表示順ズレの回避)
C++では既定でiostream
はstdio
と同期していますが、std::ios::sync_with_stdio(false)
で同期を切ると混在時に順序が乱れることがあります。
混在するなら同期を維持するか、両方で明示フラッシュを行いましょう。
#include <iostream>
#include <cstdio>
int main() {
// 同期を切ると順序が崩れる可能性
std::ios::sync_with_stdio(false);
std::cout << "cout 1";
printf(" printf 1");
std::cout << " cout 2\n";
std::cout.flush();
fflush(stdout);
}
出力は環境により順序が前後する可能性があります。
混在する場合は同期を切らない、もしくは片方に統一するのが無難です。
国際化・ロケール・小数点記号の扱い
printf
はsetlocale(LC_ALL, "...")
の影響を受けますが、Cはストリーム単位でimbue
でき、より細やかな制御が可能です。
国際化対応が必要な場合、Cのロケールシステムが便利です。
一方、printf
でもロケール依存の書式指定(%'.2f
のような拡張)が実装依存で存在することがありますが、移植性は低めです。
// C: setlocaleでロケールを切り替える例
#include <stdio.h>
#include <locale.h>
int main(void) {
setlocale(LC_ALL, "");
printf("小数: %.2f\n", 12345.67);
}
ユーザー定義型の対応と拡張(operator<< / >> の実装可否)
C++ではユーザー定義型にoperator<<
やoperator>>
を提供するだけで直感的な入出力が可能になります。
#include <iostream>
#include <string>
struct Point {
int x, y;
};
// 出力の拡張
std::ostream& operator<<(std::ostream& os, const Point& p) {
return os << '(' << p.x << ", " << p.y << ')';
}
// 入力の拡張
std::istream& operator>>(std::istream& is, Point& p) {
// 例: "10 20" のような形式
return is >> p.x >> p.y;
}
int main() {
Point p;
std::cout << "点を入力 (x y): ";
std::cin >> p;
std::cout << "受理: " << p << '\n';
}
点を入力 (x y): 3 5
受理: (3, 5)
Cのprintf/scanf
でユーザー定義型を直接扱うには、自前の変換関数を経由して文字列化/解析するなどの追加手間が必要です。
パフォーマンス最適化と同期設定(C++ iostream高速化)
iostream高速化:std::ios::sync_with_stdio(false) と cin.tie(nullptr)
標準ではCと同期しているためオーバーヘッドがあります。
競プロや大量I/Oでは起動直後に以下を設定します。
#include <bits/stdc++.h> // 競プロ向け。通常は<iomanip><iostream>など個別に
int main() {
std::ios::sync_with_stdio(false); // C stdioとの同期を解除
std::cin.tie(nullptr); // cinとcoutの結び付けを解除(自動flush抑制)
int n; long long sum = 0, x;
if (!(std::cin >> n)) return 0;
for (int i = 0; i < n; ++i) {
std::cin >> x;
sum += x;
}
std::cout << sum << '\n';
}
これによりscanf/printf
に近い速度まで向上することが多いです(ただし混在は避ける)。
std::endlと’\n’の使い分け(暗黙flushのコスト)
std::endl
は改行+flush
でコストが高く、ループ内で多用すると著しく遅くなります。
基本は'\n'
を使い、必要な箇所だけstd::flush
やstd::endl
で明示的にフラッシュしましょう。
大量入出力(競プロ/数値処理)での実践:scanf/printfの速度特性との比較
一般に大規模I/Oでは以下の傾向です。
scanf/printf
は高速でオーバーヘッドが小さい。cin/cout
は同期解除+tie解除でほぼ同等まで高速化可能。- さらに速い専用パーサ(自作バッファ、
getchar_unlocked
など)もありますが移植性が犠牲になります。
フォーマットが単純、解析が明快ならscanf/printf
、型安全や拡張性重視ならcin/cout
で十分です。
バッファリング・flush・改行のベストプラクティス
- プロンプト後は明示フラッシュ(
fflush(stdout)
/std::cout << std::flush
)。 - ループ内の改行は
'\n'
を使用。 - ファイルI/Oでは明示的フラッシュは最小限にし、スコープ終了時の自動クローズ/フラッシュに任せる。
iostream
とstdio
の混在は避ける。どうしても混在する場合は同期を維持する。
よくある入出力の落とし穴と対策+使い分け指針【まとめ】
文字列入力の罠:空白を含む入力(cin >> と getline の併用、scanfの%sの注意)
cin >> s
やscanf("%s")
は空白で切れます。
空白を含む行を読みたい場合は、C++ならstd::getline
、Cならfgets
を使い、その後で必要な変換を行います。
cin >>
の直後にgetline
を使うと、直前の改行が残っていて空行を読み取る問題が起きるため、std::ws
やignore
で残留改行を消費します。
#include <iostream>
#include <string>
int main() {
std::string name, line;
std::cout << "名前(単語): ";
std::cin >> name;
std::cout << "説明(行全体): ";
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 改行を捨てる
std::getline(std::cin, line);
std::cout << "name=" << name << ", desc=" << line << '\n';
}
Cのscanf("%s", buf)
は幅指定必須(例: "%31s"
)です。
空白を含むならfgets(buf, sizeof buf, stdin)
を使い、末尾改行を取り除いてから解析します。
残留改行・空白の扱いとクリア(ignore, ws, バッファ消費)
std::ws
は先頭の空白(改行含む)をスキップします。
1文字消費ならcin.get()
、大量に捨てるならignore(max, '\n')
が有効です。
Cではgetchar()
ループで改行まで捨てるのが定番です。
// wsの例
#include <iostream>
#include <string>
int main() {
int n;
std::string line;
std::cin >> n;
std::getline(std::cin >> std::ws, line); // 直前の空白類をスキップして行読み
std::cout << "n=" << n << ", line=" << line << '\n';
}
数値変換エラーの検出と回復(cin.fail() の処理/scanfの戻り値確認)
C++はif (!(cin >> n)) { cin.clear(); cin.ignore(...); }
のパターン、Cはif (scanf("%d", &n) != 1) { バッファ破棄 }
のパターンを確実に行います。
これにより無限ループや同じエラーの繰り返しを防げます。
浮動小数点の表示精度・丸め(setprecision と %f/%lf の違い)
printf
では、実引数の浮動小数点は自動昇格により常にdouble
で渡されるため、出力は%f
を使います。%lf
はprintf
では非標準/同義扱いの実装が多いですが、標準では%f
でdoubleです。scanf
では、%f
はfloat*
、%lf
はdouble*
を要求します。ここを取り違えると未定義動作です。- C++では
std::setprecision(N)
で有効桁数の指定、std::fixed
と組み合わせると小数点以下の桁数に切り替わります。
// C: scanfの%f/%lfの違い
#include <stdio.h>
int main(void) {
float a;
double b;
scanf("%f", &a); // float* に%f
scanf("%lf", &b); // double* に%lf
printf("a=%.3f b=%.3f\n", a, b); // printfは%fでdoubleを出力
}
// C++: 表示精度
#include <iostream>
#include <iomanip>
int main() {
double x = 1.0/3.0;
std::cout << std::setprecision(5) << x << '\n'; // 有効桁5
std::cout << std::fixed << std::setprecision(5) << x << '\n'; // 小数5桁
}
0.33333
0.33333
(前者は値によって指数表記になる場合があります。)
どちらを使うべきか:C++ではcout/cinを基本に、必要に応じてprintf併用
- 通常のC++アプリ、ユーザー定義型、ロケール対応、例外/状態管理が必要:
cout/cin
推奨。 - 競技プログラミングや膨大なI/Oで固定フォーマット:
scanf/printf
、またはcin/cout
の同期解除+tie解除。 - 文字列フォーマットを生成して別APIに渡す:
std::format
(C++20)やsnprintf
。
補足:C++20のstd::formatとcoutの併用による可読性と性能の両立
std::format
はPython風のフォーマットで、型安全かつ高速な整形が可能です。
整形後はcout
に流せます。
ログや複雑な書式ではprintf
より読みやすく、cout
より短く書けることがあります。
// コンパイル: -std=c++20 など
#include <iostream>
#include <format>
#include <string>
int main() {
int id = 42;
double score = 95.5;
std::string name = "Ada";
std::string msg = std::format("id={:04d}, name={:^8}, score={:.1f}\n", id, name, score);
std::cout << msg;
}
id=0042, name= Ada , score=95.5
std::format
は型安全で、桁埋め・配置・精度などを簡潔に表現できます。
まとめ
本記事では、Cのprintf/scanf
とC++のcout/cin
の位置づけ、使い方、エラー処理、ロケール、ユーザー定義型対応、混在使用時の注意、そしてパフォーマンス最適化までを実例とともに解説しました。
要点は次の通りです。
- C++開発では型安全・拡張性・可読性の観点から
cout/cin
を基本とするのが推奨です。状態フラグや例外で堅牢なエラー処理が可能です。 - 大量I/Oでは
std::ios::sync_with_stdio(false)
とcin.tie(nullptr)
で高速化し、std::endl
の乱用を避けます。極限の速度が必要ならscanf/printf
も選択肢です。 printf/scanf
は強力ですが、フォーマット不一致や幅指定忘れ(%s
の危険)に注意し、戻り値チェックとバッファ破棄で回復する習慣をつけましょう。- 国際化やユーザー定義型対応ではC++のストリームと
std::locale
、operator<< / >>
が有利です。 - C++20の
std::format
は、cout
と組み合わせることで読みやすく、型安全な整形出力を提供します。
用途と要件(型安全・表現力・性能・移植性)に応じて最適な手段を選び、必要に応じて併用することで、安全で読みやすく、高性能な入出力を実現できます。