C++
C++は、オブジェクト指向プログラミングをサポートする汎用プログラミング言語です。C++は、C言語から派生して開発され、C言語の構文や機能を受け継いでいますが、より多くの機能を追加しています。
C++の主な特徴は以下の通りです:
- オブジェクト指向プログラミング:C++は、クラスやオブジェクトといったオブジェクト指向プログラミングの機能をサポートしています。
- ポインタ:C++は、C言語と同様にポインタを使用することができます。これにより、メモリを効率的に扱うことができます。
- テンプレート:C++は、テンプレートという機能を持っており、ジェネリックプログラミングを実現することができます。
- 例外処理:C++は、例外処理という機能を持っており、プログラムが実行中に発生したエラーを捕捉して処理することができます。
初心者むけの内容
目次
- はじめに
(2017-07-18) (コンパイラのインストールと動作確認、言語の特徴、など)
- 文法の基礎 (コンパイルから実行までの流れ、拡張子cpp、出力コマンドcout、実行ファイルa.out、オブジェクトファイル、など)
(※ 編集者へ: 記事『C言語』に、下記の単元とほぼ同様の単元があります。著作時の参考にしてください。)
中級〜上級者むけの内容
目次
- C++特有の概念
(2017-07-18) (オブジェクト指向、コンソール入出力 cout, cin など)
- C++のキーワード一覧
(2016-08-18)
- クラスの定義や継承
(2016-08-18)
- オブジェクトの配列とポインタ及び参照
(2016-08-18)
- 関数オーバーロード
- 演算子オーバーロード
- C++の入出力システム
- 仮想関数
- テンプレート
(2016-08-18)
- 例外処理
- 名前空間
(2016-08-18)
- 標準テンプレートライブラリ(STL)
(2016-08-18)
上級者むけ
未分類
uint8_t など
uint8_t という符号無し8bit整数の型があり、C99およびC++11から定義されていると言われている。実装されているコンパイラなら一般に、ヘッダ stdint.h をインクルードすることで使用できる。
1バイトが何ビットの処理系であるかに依存する可能性があるため、ネット上のIT評論などでは、処理系に依存するとされており、uint8_t などの使用には気をつけるべきとされている。
コード例
#include <stdio.h>
#include <stdint.h>
int main(void) {
int a = 3; // 比較用
uint8_t b = 7;
printf("この数は %d\n", b);
}
実行結果
この数は 7
念のため、Windows版とFedora 35 Linux の両方で、gcc と g++ でコンパイルしてみたが、いずれも上記の実行結果である。
一方、Windows7 の Visual Studio 2019 で上記コードを試したが、エラーにより実行できなかった(2022年1月25日にWindows7のVisual Studio 2019 で実験)。少なくとも標準設定のままでは Visual Studio では実行困難なようである。一方、特殊な型の変数宣言 uint8_t
などの項目を完全にコメントアウトして通常のint型だけを宣言してprintfでも通常 int 変数だけを表示するコードに改修すれば実行できたので、コンパイラの実装状況の問題だと思われる。ヘッダ #include <stdint.h>
自体は、エラーなく正常にインクルードできる。
さて、「8」という数字は、少なくともwindows7およびその時代のパソコンでは1バイトが8ビットとされているので、それに由来している。
8bitで表現できるのは 0~255 である。ためしに uint8_t b = 300;
をすると次行のprintfの計算結果は この数は 44
となる。300-256 = 44 だからである。
ためしに uint8_t b = -5;
をすると次行のprintfの計算結果は この数は 251
となる。251+5 = 256 だからである。
上記のそれぞれの例のように 256 の倍数になるたびに 0と見なされる型でもある。
uのない int8_t なら、符号付き整数(もちろん8bit)である。
コード例
#include <stdio.h>
#include <stdint.h>
int main(void) {
int a = 3; // 比較用
int8_t b = -5;
printf("この数は %d\n", b);
}
実行結果
この数は -5
なお 128 以上の数は、マイナスとして扱われる。
ためしに int8_t b = 200;
をすると次行のprintfの計算結果は この数は -56
となる。
なお int8_t b = 128;
をすると次行のprintfの計算結果は この数は -128
である(0ではない)。
- uint16_t など
同様に uint16_t や uint32_t 、 uを除いたint16_t や int32_t などが定義されているとされる。
ただし処理系によっては定義されていなかったり、挙動が異なる可能性がある。 ためしに windows7用のgcc で uint16_t によって上記コードの -5 をprintfで表示してみると 65531 になったが、しかし uint32_t では uをつけているにもかかわらずprintfで -5 が表示された。
属性構文
C++11以降、コンパイラに追加情報を送ることのできる属性構文が追加された。
C++11以前にも、Visual Studio や gcc など各コンパイラごとに類似の機能がそれぞれ別々にあったが、しかしC++規格としては統一されておらずにコンパイラごとに書式の異なる状況であった。
しかしC++11以降から、下記のような、いくつもの属性構文が国際規格に導入されていっているので、今後はC++規格にもとづく属性構文を用いることが望ましい。
fallthrough属性
属性構文のひとつの fallthrough
により、意図したフォールスルーである事をコンパイラに送ることができ、break忘れの警告を抑える事ができる。
コード例
//例 1ケタの素数を判定するプログラム
#include <stdio.h>
int main(void) {
printf("一桁の数値を入力してください。:");
int i;
scanf("%d", &i);
switch (i) {
case 2:
[[fallthrough]];
case 3:
[[fallthrough]];
case 5:
[[fallthrough]];
case 7:
printf("入力値は一桁の素数\n");
break;
default:
printf("入力値は一桁の素数ではない\n");
break;
}
}
このように、 case 節の最後に attribute
を追加する。
deprecated 属性
コンパイルに、廃止などの予定される古い関数が呼び出された時に、警告を発してコンパイルを止める事のできる deprecated 属性がc++14で導入された。
コード例
#include <iostream>
#include <stdio.h>
// 新しいほう
void sin_kansu() {
printf("goog morning\n");
}
// ↓古いほう
[[deprecated("hurui_kansuは廃止されます。新関数 sin_kansuを使ってください。")]]
void hurui_kansu() {
printf("hello\n");
}
int main()
{
hurui_kansu();
return 0;
}
このように、廃止される関数の直前に deprecated
をつける。
- 実行結果の例
g++ でコンパイルした場合、ファイル名が「at.cpp」なら、
at.cpp: 関数 ‘int main()’ 内: at.cpp:19:15: 警告: ‘void hurui_kansu()’ is deprecated: hurui_kansuは廃止されます。新関数 sin_kansuを使ってください。 [-Wdeprecated-declarations] 19 | hurui_kansu(); | ^ at.cpp:13:6: 備考: ここで宣言されています 13 | void hurui_kansu() { |
のように表示される。
なお、コード中からdeprecated
に相当する部分を除去すると、エラーはでなくなり、コンパイルは通るようになる。
#include <iostream>
#include <stdio.h>
// 新しいほう
void sin_kansu() {
printf("goog morning\n");
}
// ↓古いほう
// [[deprecated("hurui_kansuは廃止されます。新関数 sin_kansuを使ってください。")]]
void hurui_kansu() {
printf("hello\n");
}
int main()
{
hurui_kansu();
return 0;
}
- 実行結果
hello
maybe_unused 属性
未使用の要素(変数または関数またはクラスなど)を、意図的に残していることを明示して、未使用関係の警告を抑える。
コード例
#include <iostream>
#include <stdio.h>
int main()
{
int a = 5;
[[maybe_unused]] int b = 2; // これが未使用
printf("%d \n", a);
return 0;
}
- 実行結果
5
と表示される。
inline関数
C++ のinlineはプリプロセッサ(前処理指令)とは違い、コンパイル時に呼び出し先を置換える。 利点として、通常の関数と比べて、inline関数では関数呼び出しオーバーヘッドをなくすことが出来、更に共通部分式の括り出しなどの最適化の余地が生じる。
inline関数の安全性の原理としては、defineが単語の単位でテキスト置き換えをする処理である一方、inline関数では関数の処理の内容を展開先に埋め込んでいるので、なのでdefineのようなテキスト置換に由来するようなバグはinlineでは生じないからである、と考えられている[1]。
しかし、inline関数の書式は関数であるので、呼び出し方には関数としての制約を受ける。
とりあえず、コード例を示すと、下記のようになる。下記コードは gcc ではコンパイル失敗するので、gnu系コンパイラを用いる場合には g++ を使うこと。
#include <stdio.h>
int x = 70; // 関数で使うxの宣言
inline void func(int x) { x = 50; printf("%d\n", x + 1);}
int main(void) {
printf("%d\n", x + 1);
func(x);
}
実行結果
71 51
関数としてのコーディングの制約というのは、具体的に言うと、たとえば一般の関数では、関数中に使われる変数は、引数以外の変数でも定義済みでなければならないが、inline関数でも定義済みでないといけない。(一方、defineマクロだと、これは単に単語をコンパイル直前に置き換えたものをコンパイラに渡すだけなので、そのような制約は無い。)
しかしdefineはその自由度の高さゆえ、想定しないバグを起こしやすい。特に、単語を置き換える結果、コンパイラが見ているコードと、プログラマが見ているコードの文章がそもそも違うので、もしバグが混入した場合には、define を関数的に流用した場合にはバグを発見しづらくなる。
このような事情もあるので、どうしても関数呼び出しの処理負担を減らしたい場合、defineマクロではなくinline関数を使うのが良いとされる。
なお、gccでは、inline関数はmain関数の内部でも使える。ただし、通常の関数とは意味が異なり、やはり前処理指令の一種であるので、使い方には注意が必要である。
下記コードは gcc でもコンパイルできてしまう。g++ ではコンパイル失敗する。(※編集ミスなので、コードを要修正。)
#include <stdio.h>
int main(void) {
int x = 70; // 関数で使うxの宣言
printf("%d\n", x + 1);
inline void func() { x = 50; printf("%d\n", x + 1);}
func();
}
実行結果
71 51
inline関数を何度も呼び出す場合、コードの見た目は関数を用いたときのように短くなるが、しかし実際は呼び出し先の各所にマクロ的に展開をしてからコンパイルをしているので、なのでコードの見た目の長さ(短さ)と、実際にコンパイルで生成される実行ファイルとの間に、ファイルサイズの差異が生じている。
inline関数は名前こそ末尾に「関数」とついているものの、その実態は関数というよりも、あくまでマクロである。そしてinline関数は、マクロとしての呼び出しに失敗した場合、関数として呼び出しをする。
そのほか、inline関数の利用によって、ファイルサイズが大きくなるのが一般的である。(裏を返すと、通常の関数を用いると、ファイルサイズが小さくなるのが一般的であるともいえる。)
inline関数によってオーバーヘッドを減らすことで原理的には処理速度が上がる場合もあるので、用途としては、もしファイルサイズよりも処理速度を優先したい場合になら、inline関数の利用を検討する価値はある。
しかし実際はそう単純ではなく、inline展開後のコード量が増えたことによってメモリの使用量が増えるなどの理由で、逆に処理速度が低下してしまう場合すらありえる[2]。
また、このような展開後コード量増加の問題があるため、inline関数によって高速化をするためにはinline関数ブロック内で定義された関数の内容は、十分に短いものでないとならない。(でなければ上述の理由で処理速度が低下する。)
コンパイラによっては、インライン展開した場合に処理速度が低下すると判断した場合は、インライン展開ではなく通常の関数として呼び出す。 また、関数をアドレス取得しようとすると、通常の関数になる[3]。
なお、クラス内で定義した一般の関数については、一説では自動的にインライン化されるという説がある[4]。
範囲ベースfor
C++11から、C言語風のfor(;;)に加えて範囲ベースforが使えるようになりました。
- 範囲ベースのforの例
#include <iostream> using namespace std; int main(void){ int v[] = { 2, 3, 5, 7, 11}; for (auto i : v) cout << i << " "; cout << endl; for (auto&& i : v) i *= i; for (auto i : v) cout << i << " "; cout << endl; // cout << i << endl; ループ変数のスコープは for 文に閉じているので、これはエラー }
- 実行結果
2 3 5 7 11 4 9 25 49 121
- auto&& はユニバーサル参照で、左辺値式とするとコレクションの要素をmodifyすることが出来ます。
型推論
C++11から、キーワード auto が再定義され、型推論(type inference;しばしば ti と略される)を行えるようになりました。
auto i{0}; // int auto d{0.0}; // double auto f{0.0f}; // float auto* s{"abc"}; // char* auto fn() { // bool return true; }
decltype
C++11から、キーワード decltype が追加され、インスタンスから型情報を取出し、「型」として使うことが出来るようになりました。
auto i{0}; // int decltype(i) j{1}; // int decltype((i)) k{i}; // int& auto d{0.0}; // double decltype(d) e{1}; // double auto f{0.0f}; // float decltype(f) g{1}; // float auto* s{"abc"}; // char* decltype(*s) ch{' '}; // char
- 前述の auto との組合せばかりになってしまいましたが、auto でないインスタンスでも使うことが出来ます。
using を使った型定義
C++11から、キーワード using を使って型定義が出来るようになりました。
typedef int (*pfi)(int, int); using pfi = int (*)(int, int); template<typename T> using map = std::map<std::string, T>; map<int> int_map;
- using を使った型定義は typedef の機能のほか、テンプレートと組み合わせることが出来ます。
- リファレンスとの組合せ
int a = 3; int& r = a; auto x = r; // int decltype(r) y = r; // int& decltype(auto) z = r; // int&
decltype(auto)
は、C++14からの機能でした。
ラムダ式
C++11から、ラムダ式が使えるようになりました。
- ラムダ式の例
#include <iostream> using namespace std; int main(void) { struct { const char* opr; int (*pfi)(int, int); } tbl[] = { {"+", [](int x, int y) { return x + y; }}, {"-", [](int x, int y) { return x - y; }}, {"*", [](int x, int y) { return x * y; }}, {"/", [](int x, int y) { return x / y; }}, {"%", [](int x, int y) { return x % y; }}, }; for (auto pair : tbl) cout << "45 " << pair.opr << " 7 = " << pair.pfi(45, 7) << endl; }
- 実行結果
45 + 7 = 52 45 - 7 = 38 45 * 7 = 315 45 / 7 = 6 45 % 7 = 3
- この例は、C言語/ポインタ#関数へのポインターをラムダ式を使って再実装したものです。
- イテレーション部分には、範囲ベースforを使いました。
- auto を使って型推論すると、無名構造体型の変数も宣言できます。
nullptr
C++11から、キーワード nullptr が追加されました。
従来、char* p = NULL;
と書いていた様なところを、char* p = nullptr;
と書きます。
NULLは、マクロで値は 0 に定義されていました。 nullptr はキーワードで「空のポインター」を表します。 空のポインターなので、ポインター変数の初期化・代入や比較は出来ますが、他の型(例えば、int)は出来ません。
Uniform initialization syntax
C++11から、Uniform initialization syntax(統一初期化構文(仮))が使えるようになりました。
- 従来
int i = 3; int ary[] = { 2, 3, 5, 7 }; struct S { int a, b; } s = { 3, 4 }; std::vector<int> v { 2, 3, 5, 7 };
- と書いていたところを
int i { 3 }; int ary[] { 2, 3, 5, 7 }; struct S { int a, b; } s { 3, 4 }; std::vector<int> v { 2, 3, 5, 7 };
と書けます。
これによって
- 関数の戻り値の型の暗黙化
#include <iostream> using namespace std; struct X { int i; double d; }; X f() { return { 1, 3.14 }; } int main(void){ X x = f(); cout << "i = " << x.i << ", d = " << x.d << endl; }
- と出来ます。
- 従来は
X f() { return X{ 1, 3.14 }; }
- と型を明示する必要がありました。
脚註
- ^ ロベールのC++教室『第66章 インライン関数』Last update was done on 2000.7.27 2021年11月23日に確認.
- ^ 平山尚『ゲームプログラマになる前に覚えて起きたい技術』、秀和システム、P527
- ^ ロベールのC++教室『第66章 インライン関数』Last update was done on 2000.7.27 2021年11月23日に確認.
- ^ WisdomSoft『インライン関数』 2021年11月23日に確認.