2. C言語との違い

ここでは、オブジェクト指向的な要素以外での、C言語からの追加・改良点について説明していきます。

2.1 コメント

C言語からの拡張

行末までを無視するコメント // が導入された。
// の後に続く同じ行上のものはすべてコメントとなる。

Cでのコメントは /* */ でしたが、これに加えて // もコメントして使えます。

. C言語でも // をコメントして使うことができたのに、どうして?これまでの演習でも使っていたはずです。
A. 最近の処理系では、Cモードでのコンパイル時に、// をコメントとして使えますが、ANSI Cの定義に組み込まれるようになったのはC99の規格からです(C89の規格では//は定義されていません)。よって、処理系によっては使えない場合があるのでCプログラミングでは注意しましょう(C++では大丈夫です)。

2.2 変数宣言と定数

2.2.1 変数宣言

C言語との違い

Cでは関数内の変数の宣言は、ブロックの先頭で行わなければならなかった。C++ではブロック中の宣言は文の一種となり、任意の箇所で行える。

List 2-1に例を示します。関数内で宣言された変数のスコープは,宣言の位置から宣言が行われたブロックの末尾までである。for(int i = 0; i < a; i++)のようにfor文の中(初期指定の部分)でも宣言できます。for文で宣言した変数のスコープは、for文中で有効となりますが、ただし、処理系によってはスコープの範囲が、forブロックの外でも有効な場合があるので注意が必要です。

List 2-1
#include <stdio.h> 

int main(void)
 {
   int a;
       
   printf("Enter a -> ");
   scanf("%d",&a);
   int i, sum = 0;   // C言語ではこの位置で宣言できない
   for( i = 0; i < a; i++){ 
      sum += i;
   }
   printf("%d\n",sum);
   
   return 0;
}

余談
実はこの変数宣言の方法もANSI CのC99の規格から対応するようになりました。後で説明する inline関数や bool型等にも対応しています。とりあえずここではC89との違いという意味で説明していきます。


練習問題2.1
List2-1をCとC++でコンパイルして、違い(Cでのコンパイルではエラーが出ること)を確かめよ。
(Visual Studio 2010 統合開発環境を用いたコンパイル方法)

2.2.2 const

下記のように const を使って定数変数を宣言することによりマクロの代わりとして使うことができます。

List 2-2
int main(void)
{
   const int max = 100;
   int x[max];  // C言語ではエラー
   
   return 0;
}

C言語からの拡張

整数型(int, longなど)の const 宣言はマクロの代わりとなる。
宣言時には必ず初期化しなければならない。

. こんな面倒なことをしなくても #define文を使えばいいのでは?
A. const は、#define と比べると構造体やクラスにも適用できるなどの柔軟性もありますが、#define よりも特に優れている点は、スコープを持つことです。例えばList2-2の場合maxはmain関数以外からはアクセスできません。では、関数の外で宣言された場合はどうなるのでしょうか?この場合は内部リンケージを持つことになり、そのソースファイルのみ有効であり、他のソースファイルからは隠されて見えなくなります。もう一つ、constが優れている点は、コンパイル時に型のチェックが行われるという点です。
. スコープって何ですか?
A. もう一度C言語を復習してください…、と言いたいところですが、スコープ(scope,有効範囲)とは,宣言された変数を直接アクセスできるプログラム内の範囲のことです。

constはスコープを持ち、コンパイル時に型のチェックができたりなど、#defineよりもバグの少ないプログラミングが可能となります。
よって、C++プログラミングでは#define文は使わずにconstを使うようにする。

2.3 関数

2.3.1 関数プロトタイプ

関数プロトタイプは、C言語でも導入されており演習でも学びました。C++でも違いはありませんが、C言語では関数プロトタイプ宣言がない場合は戻り値をintとしてコンパイル可能でしたが、C++では関数プロトタイプ宣言が必須となっています。

C言語との違い

C++では関数を呼び出す際、必ず関数プロトタイプ宣言が必要である。

List2-3のプログラムはCではコンパイルできますが(警告はでるかも)、C++ではコンパイルエラーとなります。コメントをはずすか、int sum の関数を main関数の前に持ってくるとエラーがなくなります。

List 2-3
#include <stdio.h> 

/* int sum(int a, int b); //C++ではこの文が必要となる */

int main(void)
{
   int a, b, c;

   a = 1; b = 2;
   c = sum(a,b);

   printf("%d\n",c);
	
   return 0;
}

int sum(int a, int b)
{
   return a+b;
}

関数プロトタイプによりコンパイラによるチェックが行えるようになった。
関数プロトタイプ宣言は一般的にはヘッダファイルで宣言します。

また、C言語では、関数で仮引数を受け取らない場合は

int f1(void);

のようにvoidを指定する必要がありましたがC++では、

int f1();

のように書くこともできます。int f1(void)としてもよいです。
C言語で int f1() は引数のチェックを行わないという意味になりますが、これをC++では

int f1(...);

のように ... を用いて表します。例えば、

int f1( int x, ... );

は、2番目以降の引数に関しては型や個数が可変であることを示しています。
以上をまとめると以下のようになります。

C言語との違い

関数および関数プロトタイプにおいて、引数を受け取らないことや、チェックを行わないことを指定する記述法が異なる。

C言語 C++
引数なし void f(void); void f(); または void f(void);
引数のチェックを行わない void f(); voidf (...);

2.3.2 デフォルト引数

C言語からの拡張

C++では、関数の引数にデフォルト値を指定できるようになった。

つまり、関数呼び出し時に、引数を省略した場合、自動的に関数に値を渡すデフォルト値を指定することができます。具体的にはList2-4-1に示すように指定します。この場合は、yとzの引数を省略した場合にデフォルト値として0が与えられます。

List 2-4-1
#include <stdio.h> 

void disp(int x, int y = 0, int z = 0)
{
   printf("%d %d %d\n", x, y, z);
}

int main(void)
{
   disp(1,2,3); // 1 2 3 と出力される
   disp(1,2); // 1 2 0 と出力される
   disp(1);  // 1 0 0 と出力される
	
   return 0;
}

デフォルト引数は、後ろ側(右側)の引数から順に指定しなければならず、途中の引数をとばすことはできません。つまり

void disp(int x = 0; int y; int z = 0)

のようにはできません。

関数のプロトタイプ宣言と、関数の中身が分かれている場合にはList2-4-2のようにプロトタイプ宣言にのみ記述します。

List 2-4-2
#include <stdio.h> 

void disp(int x, int y = 0, int z = 0) ;

int main(void)
{
   disp(1,2,3); // 1 2 3 と出力される
   disp(1,2); // 1 2 0 と出力される
   disp(1);  // 1 0 0 と出力される
	
   return 0;
}

void disp(int x, int y, int z) 
{ 
   printf("%d %d %d\n", x, y, z); 
}

2.3.3 インライン関数

C言語からの拡張

C++では、マクロと同じような働きをもつ、inline関数が使えるようになった。

インライン関数は、以下のようにinline指定子を最初に指定すればよい。

inline double square(double x)
{
   return (x*x);
}

これにより、C++コンパイラがマクロと同じように展開してくれるので、オーバーヘッドを気にしなくてよく通常の関数よりも速く実行することができます。

インライン関数を使う際にはいくつかの注意点があるので気をつけましょう。
  • 通常の関数は外部リンケージをもちますが、インライン関数は内部リンケージをもちます。よって複数のソースファイルで使用する場合には、ヘッダファイルに記述する必要があります。
  • インライン関数は、前方参照しかできない(後方参照ができない)ため、必ず呼び出しより前で定義する必要があります。
  • インライン関数による条件が満たされない場合、コンパイラは通常の関数として扱います。条件は処理系により異なりますが、以下のような例を含む関数はインライン化されず通常の関数として扱われます。
    • if以外(do, for, whileなど)の制御を含む、流れが複雑な関数
    • 再帰呼び出し
    • static変数を含む関数
. #define文を使えばいいのでは?
A. #define文を使う場合は、以下のような問題点があります。
  • 複数行にわたる関数を記述する場合,\ 記号を使わなければならなく記述がめんどくさい 
  • 引数に--i などの副作用のある式を書くと、期待通りの結果が得られない
  • 型のチェックをしてくれない

よって、constのセクションでも述べたようにC++では#define文は使わないようにしましょう。


練習問題2.2
#define square(x) ((x)*(x))
を用いて--xを引数として渡した際の副作用について調べよ

2.3.4 関数のオーバーロード(多重定義)

C言語からの拡張

Cでは、同じ関数名の関数は1つしか作成できなかったが、C++では、異なる関数に同じ名前を与えることが可能となった。これをオーバーロードまたは多重定義と呼びます。

以下の例は、絶対値を返す関数absが多重定義されています。

List 2-5
#include <stdio.h> 

//intの絶対値を返す関数
int abs(int x)
{
  return( (x < 0) ? -x : x );
}

//longの絶対値を返す関数
long abs(long x)
{
   return( (x < 0L) ? -x : x);
}

//doubleの絶対値を返す関数
double abs(double x)
{
   return( (x < 0.0) ? -x : x);
}

int main(void)
{
   printf("%d \n", abs(-8));
   printf("%ld \n", abs(14L));
   printf("%f \n", abs(-33.5));
   return 0;
}
以下のような場合は多重定義できないので注意が必要です
  • 戻り値のみが異なる場合
    例 : int func(int x); と float func(int x);
  • type type& (参照型type&は次節で説明します)
    例 : int func(int x); と int func(int &x);
  • 以下のような引数を特定しない関数
    例 : int func(int x, ...); と int func( int x, double y);

実際に使用する際に曖昧さのない場合に多重定義ができます。

関数のオーバーロードの他に、演算子に対してもオーバーロードが可能ですが、これに関しては後々(C++中級編)説明します。

2.4 参照

2.4.1 参照型

C++では、C言語でのポインタと似たものとして、参照(reference)が導入されました。参照とは、変数の別名として動作する暗黙的なポインタを表します。つまり、変数に別名をつけることができます。例を以下に示します。

List 2-6
#include <stdio.h> 

int main(void)
{
   int x;
   int &ref_x = x;   //xの別名としてref_xを定義

   ref_x = 10;
   printf("x = %d\n", x);
   printf("ref_x =%d\n", ref_x);
   return 0;
}

参照型の変数は、List2-6のように、型の後ろに & を指定することによって宣言します。
List2-6をポインタを用いて記述するとList2-7のようになります。

List 2-7
#include <stdio.h> 

int main(void)
{
   int x;
   int *ref_x = &x;   //xのアドレスをさすポインタref_xを定義

   *ref_x = 10;
   printf("x = %d\n", x);
   printf("*ref_x =%d\n", *ref_x);
   return 0;
}

List2-6の方がすっきりした記述になっているのがわかります。

C言語からの拡張

C++では、変数に別名をつけることができる参照が導入された。ただし参照はポインタと異なり、宣言と同時に初期化する必要があり、初期化後に参照先を変更することはできない。

参照における初期化と代入はまったく意味が異なる。
という点に注意しましょう。

. 別名を定義して何の役に立つの?
A. 実際には、次節で述べる参照引数や、参照を返す関数で使います。特に参照を返す関数では、後々出てくる演算子のオーバーロードにおいて、代入文の左辺で関数を使用する際に必要となります。

2.4.2 参照引数


練習問題2.3
下記のプログラムを参考に、2つの値を交換する関数swapを作成せよ。
#include <stdio.h> 

/*** この部分を作成
void swap

*****/

int main(void)
{
   int a = 1;
   int b = 2;

   swap(&a, &b);
   printf("a = %d\n", a);
   printf("b = %d\n", b);

   return 0;
}

C言語では、関数の引数は値渡しによって行われます。すなわち、関数は実引数のコピーを受け取るので、関数内で引数の変更は、呼び出し側には反映されません。変更内容を呼び出し側に反映させるためには、関数の引数にポインタ*を用い、呼び出す際に値を渡す実引数のアドレス&を渡すことにより、実現することができました。
しかし、参照型の引数を用いると練習問題2.2のプログラムはList2-8のように簡潔に記述できます。
List 2-8
#include <stdio.h> 

void swap(int &x, int &y)
{
   int temp;

   temp = x;
   x = y;
   y = temp;
} 

int main(void) 
{
   int a = 1; 
   int b = 2; 

   swap(a, b); 
   printf("a = %d\n", a); 
   printf("b = %d\n", b); 
   return 0; 
} 

2.4.3 参照を返す関数

参照は、変数だけではなく、関数からも参照を返すことができます。例を以下に示します。

List 2-9
#include <stdio.h> 

int &func(void);
int x;

int main(void)
{
   func() = 7;
   printf("x = %d\n",x);
   printf("func() = %d\n",func());
   
   return 0;
}

int &func(void)
{
   return x;
}

ここでは、関数funcはint型の参照を返すように宣言されています。したがってreturn x; では変数xの値を返すのではなくxへの参照を返すことになります。つまり、func()はxの別名となります。よって、func() = 7; はxに7を代入していることと同じことになります。次の2つのprintf文でもfunc()はxの別名ですから同じ値を出力することになります。

参照を返す関数を用いることで代入文の左辺で関数を使用することができます。

ここではまだ、その効果はわからないかもしれませんが、授業の中級編、上級編あたりでその効果がわかると思います。


練習問題2.4
List2-9のプログラムをC言語でも利用できるように参照ではなくポインタを使用したプログラムに変更せよ

2.5 new と delete (メモリの動的確保)

C言語では、メモリを割り当てるときにはmalloc()関数を使用し、割り当てたメモリを開放するときはfree()関数を使用しました。C++では、newとdeleteを使うことによって簡潔にメモリの動的確保を行うことができます。newとdeleteは、ライブラリ関数ではなく、演算子としてC++の言語仕様に組み込まれています。

例えば、int型の変数および配列の確保および開放をC言語では

int *p1, *p2;

/* 領域の確保 */
p1 = malloc(1*sizeof(int));
p2 = malloc(5*sizeof(int));

/* 領域の解放 */
free(p1);
free(p2);

のようにmallocとfreeを用いて行ってきました。C++では、

int *p1, *p2;

/* 領域の確保 */
p1 = new int;
p2 = new int[5];

/* 領域の解放 */
delete p1;
delete [] p2;

のように簡潔に記述することができます。
配列の割当にはnew int[5]のように[]にサイズを指定します。

C言語からの拡張

C++では、動的なメモリの確保・解放にnewおよびdelete演算子を使うことにより、ライブラリの呼び出しではなく言語レベルで実現できるようになった。

. なぜmalloc()ではなくnew, deleteを使う必要があるの?
A. new, deleteは、malloc()よりも簡潔に記述できるという点以外にも下記のような利点があります。
  • newは指定の型のオブジェクト(変数)を格納するためのメモリを自動的に割り当てるため、これまでのように必要なバイト数を調べるためにsizeofなどを使わなくてもよい。これによりエラーの可能性が少なくなります。
  • newは指定した型のポインタを自動的に返します。これまでのように、明示的に型のキャストを行う必要がありません。newはこれから獲得しようとするオブジェクト(変数)の型を知っているということになります。
  • new, deleteの使用時に、コンストラクタ、デストラクタを呼び出すことができる。(後々説明します)
  • new, delete演算子は、演算子のオーバーロードができるので、独自の割当システムを作成することができる。(後々説明します)

3つ目と4つ目の利点はまだわからないと思いますが、後で理解できます。
C++では、malloc(), free()を使わずにnew, delete演算子を使いましょう。

new演算子では、

int *a = new int(5); //初期値を5とする

のように( )で初期値を指定することもできます。

2.6 bool型

C言語からの拡張

C++では、ブール値であるtrue(真)とfalse(偽)を表す型 bool型が定義された。
bool型は、trueとfalseの値のみ格納することができる。

C++では、関係演算子と論理演算子の結果はbool型の値であり、条件文の結果はブール値にならなければなりません。しかし、C言語では真が非ゼロであり、偽がゼロとなっており、C++でも、非ゼロは真に、ゼロは偽に自動的に変換されるため、あまり意識する必要はありません。また、整数式でブール値を使用すると、真は1に、偽は0に変換されます。

2.7 キーワード(予約語)

C++では、Cで定義されているキーワードをすべてサポートしており、さらに独自のキーワードが追加されています。以下にC++でのキーワードを示します。これらのキーワードは、プログラマが変数や関数名などの用途に使用することはできません。

C++のキーワード
asm const_cast explicit int register switch union auto
continue extern long reinterpret_cast template unsigned bool default
false mutable return this using break delete float
namespace short throw virtual case do for new
signed true void catch double friend operator sizeof
try volatile char dynamic_cast goto private static typedef
wchar_t class else if protected static_cast typeid while
const enum inline public struct typename

2.8 スコープ解決演算子

C++では、スコープ解決演算子 :: が導入されました。使用例をList2-10に示します。

List 2-10
#include <stdio.h> 

int x = 10;

int main(void)
{
   int x;

   x = 20;
   printf("x = %d\n", ::x); // グローバル変数xの表示
   printf("x = %d\n", x);  //main関数内のxの表示
   
   return 0;
}

List2-10を実行すると、はじめにグローバル変数xの値10、次にmain関数内のローカル変数xの値20が表示されます。
C言語では、main関数内の変数xのみを扱うことができ、広域的なファイルスコープを持つxにアクセスすることはできませんでした。C++では、::x のようにスコープ解決演算子を用いることにより関数外で宣言されたファイルスコープを持つ変数(オブジェクト)にアクセスすることができます。
スコープ演算子は、xx::yyのように2項形式の形で使われます。このことについては、クラスと関係が深いので、次のクラスのところで説明します。