C++
C++は、標準C言語のソースコードと互換性がありつつも、標準C言語にはない新たな機能が加えられたプログラム言語のひとつです。C++も標準C言語もシステムプログラミング言語です。システムプログラミング言語とは、オペレーティングシステム(OS)を開発することのできる高級言語という意味です。高級言語とはアセンブリ言語や機械語以外の、いわゆる普通のプログラミング言語です。
初心者むけの内容
目次
- はじめに (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)
- C言語/おわりに ※ 諸般の事情でリンクします。
上級者むけ
未分類
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関数では処理負担(関数の「オーバーヘッド」などという)を減らせる。(なお、オーバーロードとは別物なので、混同しないように注意。C++にはオーバーロードという用語もある。)
標準C言語の規格にもC99でinline関数は追加されているが、実装において、C++のinline関数 と 標準Cのinline 関数の挙動は、いくつか異なる。標準C言語にもdefine などのマクロの仕様がプリプロセッサとして定義されているが、inline 関数は、より関数に特化した仕組みになっている。
defineは本来の用途は、けっしてprintf など命令文のコードの置き換えではなく、本来は1つの定数だけを置き換えることを目的としているので、それ以外の用途に用いるとコンパイラの実装によっては予想外のエラーやバグを招くので、なるべくなら、関数のように命令文をプリプロセッサで置き換えたいなら、defineではなくinlineを用いるほうが安全であると考えられている。
具体的には、x++のようなインクリメント命令にdefineによる置換を適用した場合やそのような引数を与えた場合などに、 処理系またはコードによっては未定義動作になったり、あるいはx+1ではなくx+2あるいは他のもっと別の命令に置き換わってしまいかねない可能性がある [1]。なので関数的な命令の置換に対しては、defineマクロではなくinclude関数を使うのが安全であり望ましい。
inline関数の安全性の原理としては、defineが単語の単位でテキスト置き換えをする処理である一方、inline関数では関数の処理の内容を展開先に埋め込んでいるので、なのでdefineのようなテキスト置換に由来するようなバグはinlineでは生じないからである、と考えられている[2]。
しかし、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展開後のコード量が増えたことによってメモリの使用量が増えるなどの理由で、逆に処理速度が低下してしまう場合すらありえる[3]。
また、このような展開後コード量増加の問題があるため、inline関数によって高速化をするためにはinline関数ブロック内で定義された関数の内容は、十分に短いものでないとならない。(でなければ上述の理由で処理速度が低下する。)
コンパイラによっては、インライン展開した場合に処理速度が低下すると判断した場合は、インライン展開ではなく通常の関数として呼び出す。 また、関数をアドレス取得しようとすると、通常の関数になる[4]。
なお、クラス内で定義した一般の関数については、一説では自動的にインライン化されるという説がある[5]。
- ^ 『inlineとdefineの違い』質問日時:2009/04/01 2021年11月22日に確認.
- ^ ロベールの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日に確認.