コードで見るC言語とC++の7つの違い
- 作成日: 2021-10-24
- 更新日: 2023-12-24
- カテゴリ: C言語
コードで見るC言語とC++の7つの違い
C言語とC++は似ている言語ですが、その違いについては色々ややこしいところがあります。
この記事ではC言語とC++の違いを具体的なコードで解説します。
C++はC言語のスーパーセットで上位互換性がある言語だと言われています。
しかし実際には細かい仕様の違い、あるいは決定的な機能の欠落などがあり、完全なスーパーセットではありません。
具体的なコードでこの2つの言語の違いを見ることで、両者の言語について深く知ることができます。
具体的には↓の7つの点について見ていきます。
- クラスの有無
- 例外の有無
- STLの有無
- VLAの違い
- キャストの違い
- 関数の引数とvoid
_Generic
の有無
それではC言語とC++の違いについて解説していきます。
クラスの有無
まずクラスの有無についてです。
C言語にはクラスはありませんが、C++にはクラスがあります。
C++はマルチパラダイム、つまり何でも取り入れる言語なので、オブジェクト指向も取り入れています。
クラスはオブジェクト指向ではなくてはならないものです。
そのためC++ではクラスを書くことができます。
いっぽう、C言語はオブジェクト指向ではありません。
オブジェクト指向なコードを書くことはできますが、言語レベルでオブジェクト指向の機能を備えているわけではありません。
そのためクラスもないです。
C言語にはクラスはなく、C++にはクラスがあります。
クラスを使いたい場合はC++を選択するといいでしょう。
クラスとは?
クラスとはデータとそれを操作する関数を1つにまとめたものを言います。
データとそれに関する関数をまとめることでコードが書きやすくなります。
またオブジェクト指向的なコード、たとえば継承やポリモーフィズムなどのコードを書くことも可能になります。
たとえばCar
という車のクラスがあったとします。
このCar
というクラスはspeed
という車のスピードを表す変数を持っています。
そしてクラスの関数(メソッド)にspeed_up()
という関数を加えます。
speed_up()
を呼び出すとCar
が持っているspeed
の値が変わります。
このようにクラスを使うとデータと関数を1つにまとめることができます。
C++のクラスの書き方
C++のクラスの書き方について見てみましょう。
C++ではクラスは↓のように書きます。
#include <iostream>
// 車を表現するクラス
class Car {
public:
int speed = 0; // 車のスピードを表す変数
// クラスの関数(メソッド)
void speed_up() {
speed += 2;
}
};
int main() {
auto car = Car();
car.speed_up();
std::cout << "speed: " << car.speed << std::endl;
// speed: 2
return 0;
}
↑のようにC++でクラスを書くときはclass
というキーワードを書いてからクラス名を書きます。
そして波カッコを書いてその中にクラスの持つデータである変数や、関数などを書きます。
C++のクラスではpublic
というキーワードを書くとそれ以降の変数や関数をパブリックなものにします。
↑のコードはGCC 10.0.0以降のコンパイラでコンパイルしています。
C言語でクラスみたいなことをする
C言語でもクラス的なオブジェクト指向のコードを書くことはできます。
これは構造体と関数を使います。
↓のコードを見てください。
#include <stdio.h>
typedef struct {
int speed; // 車のスピードを表す変数
} Car;
// Carの関数
void car_speed_up(Car *this) {
this->speed += 2;
}
int main(void) {
Car car = {0};
car_speed_up(&car);
printf("speed: %d\n", car.speed);
// speed: 2
return 0;
}
↑のように構造体と関数を使うと、C++のクラスのようなコードを書くことができます。
こういった設計によりC言語でオブジェクト指向的なコードを書くことも可能です。
例外の有無
C言語には例外はありませんが、C++には例外があります。
C++はC言語の機能を拡張した言語です。
そのため例外機構が追加で実装されました。
具体的にはthrow
で始まるキーワードを使うとC++で例外を投げることができます。
C言語に例外はなく、C++には例外がありますが、C++の例外について見ていきたいと思います。
例外とは?
例外とは、エラーが発生したときにコードをジャンプする仕組みのことを言います。
エラーが発生したときにコードをジャンプすることで、エラー処理を簡潔に書くことができます。
たとえば深い階層の関数でエラーが発生したときに例外を発生させれば、浅い階層の関数でそのエラーを補足することができます。
例外は現代的なプログラミング言語では実装されていることが多い機能です。
C++の例外の書き方
C++で例外を書くには↓のようにします。
#include <iostream>
#include <stdexcept>
int main(void) {
try {
throw std::runtime_error("failed"); // 例外を投げる
} catch (const std::runtime_error &e) { // 例外を補足する
std::cerr << e.what() << std::endl; // エラー内容を出力する
}
return 0;
}
throw
というキーワードを使うと例外を投げることができます。
例外をキャッチするにはtry - catch
文を使います。
↑の例ではstd::runtime_error
という例外を投げています。
エラーメッセージは「failed
」で、それをstd::cerr
というエラー出力に出力しています。
STLの有無
C++にはSTL(Standard Template Library)があります。
C言語にはありません。
STLは多くの開発者がC++を使う理由の1つです。
なぜならSTLを通して配列、リスト、ハッシュマップといった基本的なデータ構造を扱うことができるからです。
C言語ではこれらのデータ構造を使う場合は外部ライブラリを使うか自作する必要があります。
しかしC++のSTLを使えばこれらのデータ構造やアルゴリズムは簡単に使うことができます。
STLはC++の優位性の1つと言えます。
STL無しの開発は考えられないという開発者も多いのではないのでしょうか。
C++のSTLの書き方
C++のSTLであるvector
を使うコードは↓になります。
vector
は可変長配列のSTLです。
#include <vector>
#include <iostream>
int main() {
std::vector<int> v {1, 2, 3};
v.push_back(4);
for (auto &el : v) {
std::cout << el << std::endl;
}
return 0;
}
↑のコードでは、int
型のvecotr
を定義しています。
vector
のv
の初期値は1, 2, 3
の3要素です。
そのv
にpush_back()
で末尾に4
の値を追加しています。
最後にv
をfor
文で回してv
の要素を出力しています。
↑のコードをコンパイルして実行すると↓のような結果になります。
1
2
3
4
VLAの違い
C言語とC++ではVLA(Variable-Length Array)という可変長配列を扱うことができます。
これは静的な配列の要素数を実行時に動的に決められるというものです。
VLA
はC言語で先に実装されました。
その後、C++では実装されていない期間がありましたが、遅れて実装されました。
これにより互換性が維持されています。
しかし、C言語のVLAは宣言と同時に初期化できないのに対し、C++のVLAは宣言と同時に初期化できるなど細かい違いがあります。
VLAの初期化
VLAの変数の初期化をコードで見てみます。
#include <stdio.h>
int main(void) {
int n = 4;
int ary[n] = {0}; // C++では通る。C言語では通らない
printf("%d\n", sizeof(ary));
return 0;
}
↑のコードの場合ary
というのがVLAです。
ary
の要素数は変数n
で実行時に決定されています。
このためコンパイラはary
をVLAと認識します。
↑のコードはC++コンパイラでは通りますが、C言語コンパイラでは通りません。
理由はVLAを= {0}
で初期化しているからです。
ary[n] = {0};
というコードをary[n];
というコードに変更すると、C言語のコンパイラでも通るようになります。
キャストの違い
C言語とC++のキャストの実装は違っています。
理由は不明ですが、後発のC++が互換性を何らかの理由で無視したようです。
これによりC言語のキャストを使ったコードがC++のコンパイラでエラーになるということがあります。
具体的にはvoid
ポインタの暗黙的キャストです。
これはC言語では通りますが、C++ではエラーになります。
キャストはC系の言語では基本的な機能ですが、C言語とC++では実装が違っています。
キャストとは?
キャストは、ある型の変数を別の型の変数に変換することを言います。
たとえばint
型の変数をdouble
型の変数に変換することもキャストです。
キャストは型のある言語で用いられる機能です。
なぜかというとキャストできないと異なる型同士で値を移せないからです。
具体的にはキャストは↓の2つがあります。
- 明示的キャスト
- 暗黙的キャスト
明示的キャストはコードで明示的に行うキャストのことを言います。
いっぽう、暗黙的キャストとは明示的にキャストを行わずに、ひっそりと行われるキャストのことを言います。
voidポインタの暗黙的キャスト
C言語ではvoid
型のポインタの暗黙的キャストは通りますが、C++では通らずにエラーになります。
C++でコードを通すには明示的キャストを行う必要があります。
たとえば↓のコードを見てください。
#include <stdio.h>
int main(void) {
void *a = NULL;
int *b = a; // C言語では通る。C++では通らない
return 0;
}
↑のコードではa
というvoid
型のポインタをint
型のポインタに暗黙的にキャストしています。
これはC言語では通りますが、C++ではエラーになります。
たとえばGCCでは↓のようなエラーになります。
error: invalid conversion from ‘void*’ to ‘int*’ [-fpermissive]
C++で↑のコードを通すようにするにはstatic_cast
などを使ってa
を明示的にキャストする必要があります。
このキャストの違いはC++と互換性のあるC言語を書く場合にコストになります。
C++と互換性のあるC言語のコードを書くにはC言語でも明示的キャストを行わなければいけません。
関数の引数の宣言
関数の引数の宣言もC言語とC++では違っています。
これも理由は不明ですが、後発のC++がC言語の仕様を無視したようです。
具体的には関数の引数を空にした場合です。
この場合、C言語では引数は可変長引数になりますが、C++では引数が何もないという意味になります。
これはC言語とC++の細かい違いの1つです。
C++では空の引数は引数無し
↓のコードですが、関数func
を定義しています。
void func() {
}
int main(void) {
func(1, 2); // C言語では通る。C++では通らない
return 0;
}
↑のコードの場合、C言語では関数func
は可変長引数を持つ関数になります。
しかしC++では引数が何もないという意味になります。
そのため↑のコードはC言語のコンパイラでは通りますが、C++のコンパイラでは通りません。
この仕様の違いのため、C言語のコードで↑のような関数を作った場合、C++との互換性が無くなってしまうことになります。
`_Generic`の有無
C言語ではジェネリックなコードを書く場合_Generic
というマクロを使います。
しかしC++ではこの_Generic
マクロは実装されていません。
C++に実装されていない理由は不明ですが、おそらくC++にはテンプレートと言うジェネリックな機能がすでに存在するため、導入していないのではないかと思われます。
C言語の_Generic
はC11
で追加されました。
C言語で_Generic
を使う場合、その段階でC++との互換性が失われます。
ですので注意が必要です。
`_Generic`とは?
_Generic
はC言語でジェネリックなコードを書くためのマクロです。
ジェネリックとは、型を抽象化して扱うことを言います。
たとえばint
型の引数を持つ関数a
と、double
型の引数を持つ関数b
があります。
その場合、ジェネリックで引数の型を抽象化したc
という関数を作ります。
c
は内部でa
とb
を呼び出します。
こうすることでc
と言う関数にint
もdouble
も渡せるようになります。
ジェネリック・プログラミングは非常に便利なので、C言語にも導入されました。
そのためC言語でもジェネリックなコードを書くことが可能です。
C言語の_Genericの書き方
C言語で_Generic
なコードを書くには↓のようにします。
#include <stdio.h>
int add_int(int a, int b) {
return a + b;
}
double add_double(double a, double b) {
return a + b;
}
#define add(a, b) _Generic((a), \
int: add_int, \
double: add_double \
)(a, b) \
int main(void) {
int i = add(1, 2);
printf("%d\n", i); // 3
double d = add(1.2, 2.3);
printf("%f\n", d); // 3.500000
return 0;
}
↑の場合、ジェネリックな関数マクロはadd()
です。
add()
は引数a
の型を判別して、add_int
とadd_double
を呼び分けます。
add
の呼び出し側ではadd()
を使ってint
もdouble
も計算することができます。
C++でジェネリックなコードを書く
C++でジェネリックなコードを書くにはテンプレート機能を使います。
たとえば↓のようにです。
#include <iostream>
template<typename T>
T add(T a, T b) {
return a + b;
}
int main() {
auto i = add<int>(1, 2);
std::cout << i << std::endl;
auto d = add<double>(1.2, 2.3);
std::cout << d << std::endl;
return 0;
}
↑の場合、add()
というのがテンプレート関数です。
この関数はadd<int>
と指定されたときに返り値と引数の型をint
にします。
このようにテンプレート関数を定義することで、ジェネリックなプログラミングが可能になります。
C++はC言語のスーパーセットと言えるか?
C++はC言語のスーパーセットと言えるのでしょうか?
スーパーセットとは、上位互換性があることをいいます。
つまりC++コンパイラでC言語のコードをコンパイルできることを指します。
この記事を見てもわかるように、残念ながら完璧なスーパーセットとは言えないのが現状です。
両者とも歴史のある言語なので、実装の過程で細かい違いが生まれてきています。
そのためC++コンパイラでC言語のコードをコンパイルするには、C言語の開発者がC++のコンパイラを意識してコードを書く必要があります。
たとえば_Generic
の使用を制限するなどです。
ただC言語側で気を使えばC++コンパイラでもコンパイルできるC言語のコードを書くことは可能なので、そういう意味ではC++はC言語のスーパーセットと言えます。
ライブラリ開発などでは実際にそのような開発が行われることが多いです。
おわりに
今回はC言語とC++の違いをコードで見てみました。
C言語はC++とけっこう違うところがありますが、C言語側で気をつければC++のコンパイラでもC言語をコンパイル可能です。
またC++を学ぶ際にC言語から始める場合は、これらの違いを意識しておくといいかもしれません。
🦝 < C言語とC++どっちが好き?
🐭 < どっちも好き!