JavaScriptとCの違い[2日目]

Pocket
LINEで送る

この記事はShiftallのプロジェクトに関わるメンバーが日替わりでブログを更新していくアドベントカレンダー企画の2日目です。その他の記事はこちらのリンクからご覧下さい。

アドベントカレンダー2018
https://blog.shiftall.net/ja/archives/tag/adventcalendar2018/

こんにちは、稲垣@Shiftallです。

最近はRaspberryPiやらGASやらからArduinoへやってくる人もいるようです (当者調べ)。そうするとプロセッサのパワーが落ちるのでスクリプト言語からC言語に乗り換える必要がでてきたりします。そこで、JavaScriptは知ってる、これからCを使ってみたい、という新世代プログラマへ贈る「JavaScriptとCの違い」。思い付くまま書いてみました。

文末のセミコロンが省略できない

まずは軽いところから。

JavaScriptには文末のセミコロンが自動的に挿入される機能があり、これを活用するべきかしないべきかで議論があるようです

Cにはそういう親切めかした機能はありません。文末にはセミコロン必須です。セミコロンが無いと次の行とくっつきます。

静的型付け

JavaScriptの変数宣言には型がありません。また、変数には何でも入れられます:

var x = 0;
x = "hoge";

これは変数には型がなく、オブジェクトが型情報を持っているためです。このような仕組みは動的型付け (ドウテキカタヅケ) と呼ばれます。スクリプト言語ではよく見る仕組みです。

一方でC言語は静的型付け (セイテキカタヅケ) の言語です。変数を宣言するときは型情報を明示し、その変数に他の型を入れることはできません:

int i = 0;   // 整数型
i = "hoge";  // コンパイルエラー

これはオブジェクトではなく変数が型情報を持っているわけです。もちろんプログラマがオブジェクトに型情報を含めることもできますが、言語としてそういう機能はありません。

ちなみに新しめのC++では型を明示せず推論させることが可能になっています。しかし依然として静的型付けであるため、別の型を入れることはやはりできません:

auto i = 0;  // 整数型に推論
i = "hoge";  // コンパイルエラー

関数宣言

JavaScriptでは変数を使うときだけ、使う前に変数の宣言が必要でした。

C言語では関数にも宣言ができます。というか関数を使うためには、使う前に定義しておくか宣言しておくかしなければなりません。うっかり先に使ってしまうと、コンパイラがデフォルトの宣言を挿入して解釈してくれるのですが、大抵間違っています。

void hoge(void);    // hogeの宣言
int fuga(int x, int y); // fugaの宣言。ちなみに仮引数の名前は省略可能です
void hoge() {       // 引数のvoidは書いても書かなくてもよし
    fuga(1, 2);
}
int fuga(int x, int y) {
    return x + y;
}

あと引数のない関数を宣言するときは引数の代わりにvoidと書かなければなりません。定義するときは書かなくても構いません。書いても大丈夫です。引数がないときはとりあえずvoidと書くようにしておくと無難かも知れません。

デフォルトでexport

ソースコードを分割するとき、JavaScriptにはexportimportがありました。exportしていないものはファイルの外からは見えません。

C言語では、関数を単純に定義するとexport扱いになります。関数をファイルの中だけに留めておく (ファイルスコープにする) ためにはstaticを付けて定義します。以下の例では、hogeはファイルの外からは見えません。mainは外から見えます:

static void hoge(void) {
}

int main(void) {
  hoge();
  return 0;
}

staticな関数の方が最適化がしやすくなるので、公開する必要のない関数はstaticにします。

変数についても同様で、関数の外で何も付けずに変数を作るとグローバル変数になります。ファイルスコープにするためにはstaticを付けて定義します。

importとか無い

JavaScriptでファイルの外の関数や変数を使うにはimportする必要がありました。

Cで変数や関数を使うときは先に定義か宣言が必要でした。この点、同じファイル内でも外でも扱いは変わりません。というわけで外の関数や変数を使うときでも単に宣言すればオッケーです (もちろん後でリンクする必要があります):

int main(void); // どこか別のファイルで定義されている

int func(void) {
    main();
}

単に宣言すると新しく変数が定義されてしまうような場合、externを明示することができます:

#include <stdio.h>
void func(void) {
  extern int foo;          // どこか別のファイルで定義されているグローバル変数
  printf("%d\n", foo);
}

ヘッダファイルには、こうした宣言がまとめて書かれているわけです。

プリプロセッサ

ヘッダファイルが出てきたのでプリプロセッサのことも書かないといけなくなりました。実はC言語のソースコードは二つの言語を使って書かれています。そのもう一つの言語がCプリプロセッサです。よく出てくる#include <stdio.h>とかいうのは、実はCではなくCプリプロセッサの命令なのです。プリプロセッサは、他のファイルを読み込んでソースコードに挿入したり、トークンを置換したりいろいろと楽しいことができます。

JavaScriptのimportには、プリプロセッサの#includeが近いかも知れません。でもやってることはけっこう違うし……インタプリタ言語とコンパイル言語の違いが出ている、ような気がします。

配列変数

JavaScriptでは配列はArrayクラスのオブジェクトでした。

C言語の配列はもっとプリミティブなものです。たとえばint a[8];という整数型の配列があるとき……:

  • aは配列の最初の要素のポインタ (int *型)
  • &aは配列のポインタ (int (*)[8]型)
  • sizeof aは配列の大きさ (バイト数)

aがポインタであるにも関わらずsizeof aが配列の大きさなのは気持ちわるい気もしますが、sizeofはメソッドなどではなく演算子なので、そういうものです。

構造体が変数に代入できる。コピーもできる

JavaScriptでは、変数に代入できるのはインスタンスへの参照でした。ですから以下のようなコードでObjectが増えることはありません:

function f() {
  var a = { x: 0, y: 1 };
  var b = a;        // bとaは同じものを参照している
  b.x = 2;
  alert("{" + a.x + "," + a.y + "},{" + b.x + "," + b.y + "}");
  // "{2,1},{2,1}"
}

C言語では構造体を変数に代入することができ、その名も構造体変数と言います (そのまんま)。構造体変数の間でコピーすることもできます:

#include <stdio.h>
struct foo {
  int x, y;
};
void f(void) {
  struct foo a = { 0, 1 };
  struct foo b = a;        // aのコピー。インスタンスが増える
  b.x = 2;
  printf("{%d,%d},{%d,%d}\n", a.x, a.y, b.x, b.y);
  // {0,1},{2,1}
}

JavaScriptのように一つの構造体を二つの変数で共有したいときはポインタを使います:

#include <stdio.h>
struct foo {
  int x, y;
};
void f(void) {
  struct foo a = { 0, 1 };
  struct foo *b = &a;      // aのポインタ
  b->x = 2;
  printf("{%d,%d},{%d,%d}\n", a.x, a.y, b->x, b->y);
  // {2,1},{2,1}
}

abで書き方が違うのが気になる場合、構造体変数cを実体として、abをポインタにするなどの方法が取れます。

構造体を関数の引数や返り値にすることもできます。ただしコピーが発生するので、大きな構造体はポインタ渡しにするべきです。

ブロックスコープがある

JavaScriptのスコープ (識別子の有効範囲) はファイルスコープと関数スコープだけなので、スコープを分けるために関数を作ることがありました。また関数の途中で変数を宣言したとしても、その変数は関数の最初で宣言したことになるのでした:

var name = "Charlie";
function f() {
  // 実質的にここに var name;と書かれている扱い
  alert(name); // undefined
  (function(){
    // 関数の中は別のスコープ
    var name = "Bob";
    alert(name); // Bob
  })();
  var name = "Alice";
  alert(name); // Alice
}

C言語では、{ }で括ったコードブロックがスコープになります。またブロックの途中で変数を宣言した場合、その変数のスコープは宣言したところからブロックの終わりまでになります。これは実質的にそこから新しい入れ子になったスコープが始まっていることになります:

#include <stdio.h>
void f(void) {
  char *name = "Alice";
  puts(name);  // Alice
  puts(name2); // コンパイルエラー
  {
    // ブロックの中は別のスコープ
    char *name = "Bob";
    puts(name);
  }
  char *name2 = "Charlie";
  puts(name2);  // Charlie
}

クロージャがない

JavaScriptでは、関数Aの中で関数Bを定義することができました。またそのとき関数Bの中から外の関数Aの中の変数を使うことができました。こうした仕組みはクロージャと呼ばれます:

function A() {
  var count = 0;
  function B() {
    return count++;     // 外の変数を使っている
  }
  return B;
}

Cでは関数の定義をネストさせることができませんので、クロージャもありません。

クロージャのようなことをプログラミングすることは可能ですが、こういうのはクロージャとは言わないでしょう (余談):

#include <stdlib.h>
struct B {
  int count;
};

struct B *A(void) {
  struct B *b = malloc(sizeof *b);
  b->count = 0;
  return b;
}

int B_func(struct B *b) {
  return b->count++;
}

void B_destroy(struct B *b) {
  free(b);
}

GCがない

GCというのはガベージコレクター、ごみ収集システムのことです。JavaScriptではオブジェクトが破棄されるのは「参照されなくなった後のいつか、GCに目をつけられたとき」でした。

C言語ではGCはありません。メモリを確保する方法が三種類あり、それぞれで寿命が異なります。

自動変数

関数の中などで普通に定義した変数は自動変数です。これはコードブロックが終わったときに問答無用で破棄されます。

文字列リテラル (非static) も同じ寿命で破棄されるので、以下のコードは無意味です:

char *func(void) {
  char *x = "hoge";
  return x;        // 破棄されるので無意味な値
}

破棄されては困る場合、コピーを作っていくか、あるいは必要な寿命をカバーするようなコードブロックの中で変数を定義するようにします。

構造体を返したい場合、構造体そのものを返すことで、上位のコードに構造体をコピーすることができます:

struct foo {
  int x, y;
};

struct foo func(void) {
  struct foo bar = { 0, 1 };
  return foo;              // OK
}

あるいは上位のコードブロックから構造体のポインタをもらってそこにコピーすることもできます:

struct foo {
  int x, y;
};

void func(struct foo *x) {
  struct foo bar = { 0, 1 };
  *x = bar;
}

自動変数の構造体のポインタを返してはいけません。ポインタを返した瞬間に構造体が破棄されてポインタが無意味になります。

静的変数・外部変数

staticを付けて定義した変数や、関数の外で定義した変数は破棄されません。プログラムが開始したときに初期化され、プログラムが終了するときまで存続します。

staticを付けた文字列リテラルは破棄されないので、以下のコードは意味があります:

char *func(void) {
  static char *x = "hoge";
  return x;        // 破棄されないので有効
}

ヒープ

mallocなどを使ってヒープから確保したメモリは自動では破棄されません。ユーザが自分で管理し、不要になったらfreeなどで破棄します。あるいはそのままプログラムを終了させます。

#include <string.h>
char *func(void) {
  char *x = strdup("hoge"); // ヒープを利用して文字列のコピーを作る
  return x;
}

文字列リテラルが書き換え可能

C言語の文字列リテラルは、規格上は書き換え可能です:

#include <stdio.h>
void func(void) {
  char *s = "Foo";
  puts(s);    // Foo
  s[0] = 'B';
  puts(s);    // Boo
}

しかし本当に書き換えが必要なのでなければ、constを付けて書き換え禁止にしておいた方が性能がよく、コードも見易くなるでしょう。

おしまい

JavaScriptとCは見た目が似ている部分があり、よく分からなくてもなんとなく書けてしまうかも知れません。でも実はけっこう違うんですね (変数宣言が関数の頭にまとまっちゃうのとかLispっぽいと思いますよね)。この記事で「Cはなんでこうなるんだ?」とか「これこれこうするべきだと言われたけど、なんで?」とかいう疑問が解決したら嬉しいです。

Happy hacking!