Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 
連載:C言語の最新事情を知る(1)

連載:C言語の最新事情を知る(1)

C99の仕様

2014年1月30日

長い歴史を持ちながら、依然として人気の高いC言語。その最新仕様の情報にキャッチアップするための連載スタート。今回は1999年に策定された「C99」を取り上げる。

花井 志生
  • このエントリーをはてなブックマークに追加

 C言語(以降、単にC)はDennis Ritchieによって1969~1973年の間にベル研にて開発されたプログラミング言語である。長い歴史を持つと共に非常にポピュラーな言語で、プログラマーでCを知らない人はまずいないと言っていいだろう。プログラミング言語のシェアを調査しているTIOBEでも、ここ最近は常に1、2位を占めている。

 Cの言語仕様は今から25年近く前である1989年に初めて規格化され、これは一般に「ANSI-C」と呼ばれている。ANSI-Cは長らくCの言語仕様のスタンダードの位置を占め、世の中の大半のプログラマーは、このANSI-Cに慣れ親しんでいることだろう。しかし、実はCの言語仕様はその後もゆっくりと進化し続けている。

 本連載では新しくなったCの言語仕様について紹介していく。初回となる本記事では、1999年に策定された「C99」について取り上げ、最新の仕様である「C11」については次回で取り上げることにしよう。

Cの歴史

 Cには当初明確な言語仕様が無く、Dennis RitchieとBrian Kernighanが著したいわゆる「K&R本」を基に多くの処理系の実装が行われた。1989年、ANSIによって言語仕様の規格化が行われ、これは「ANSI-C」あるいは「C89」と呼ばれている。K&R本もANSI-Cに合わせて改訂され、第2版が出版された。この本は「Cのバイブル」ともいえる存在で、現在でも入手が可能だ。ANSI-Cでは仕様の規格化だけでなく、重要な言語仕様の改訂(主だったものとしては、関数プロトタイプ宣言、関数の引数宣言の新しい形式、void型の導入)が行われた。Cの仕様はこの後、10年間変化しなかった。これは、それだけANSI-Cがよく練られたものだったことを示しているといえるだろう。

 ANSI-Cの後もCの規格は改訂されており、これらは「C99」(1999年)、「C11」(2011年)と呼ばれている。本連載ではこれらの仕様の内容をかいつまんで紹介する。なお、本連載の内容は、ISO/IEC 9899:2011(C11)の仕様書をベースにしている。本記事では、それぞれの項目に対して仕様書の場所を章、節番号で付記してあるので、詳細に興味を持たれた方は、仕様書を参照していただきたい*1

C99の仕様

C99の概要

 C99の仕様は、1999年にISOで規格化された(ISO/IEC 9899:1999)。ANSI-C以降、Cの仕様は停滞した状態が続いていたため、C99以降の仕様について知らない人も多いのではないかと思われる。C99による変更は以下のとおり、多岐に渡る大きなものとなっている。

  • 予約語追加
  • ヘッダーファイル追加
  • プリプロセッサの拡張
  • 字句追加
  • 配列の拡張
  • 整数型の拡張
  • 複素数型の導入
  • 文法の拡張
  • ライブラリの拡張

 仕様策定から10年以上経過し、C99の仕様をサポートした処理系も増えてきた*2。今回はその中から主だったものを見てみよう。

bool型(C99:§7.16、C11:§7.18)

 Cにはこれまではbool型が標準では用意されていなかったため、あらゆるプログラマーが独自にbool型を実装してきた。bool型の実装に当たってはマクロ、定数、enumを使ったものなどさまざまなものが考えられ、異なったbool型の実装を持ったアプリケーション、ライブラリを同居させる場合、さまざまな配慮が必要となったが、C99ではようやくbool型が標準の機能として追加された。

 C99におけるbool型は、「_Bool」という名前になっている。この名前は言語標準の型としては奇妙だが、これはすでに「bool」という名前を使用しているアプリケーションとの互換性に配慮した結果だと思われる。もしもbool、true、falseという名前を使用していないアプリケーションが対象であれば、stdbool.hヘッダーファイルを読み込むことで、bool、true、falseという名前が使用できるようになる(リスト1)。

C
#include <stdio.h>
#include <stdbool.h>  // stdbool.hヘッダーファイルをインクルード
 
bool isoctal(char c) {  // bool型の使用
  return '0' <= c && c < '8';
}
 
int main(int argc, char **argv) {
  char c = '3';
  if (isoctal(c)) {
    printf("%c is octal¥n", c);
  }
}
 
結果:
3 is octal
リスト1 bool型

可変長配列(C99:§6.7.5.2、C11:§6.7.6.2)

 C99には可変長配列が導入された。とはいっても初期サイズを実行時に指定できるようになっただけで、実行時に自由に伸縮できるわけではない。それでもこれは大きな改善といえるだろう(リスト2)。

C
#include <stdio.h>
 
size_t fsize3(int n) {
  char b[n + 3];
  // ……
  return sizeof b;
}
 
int main(int argc, char **argv) {
  printf("%ld¥n", fsize3(10));
}
 
結果:
13
リスト2 可変長配列

 リスト2のfsize3関数では、引数で受け取った「n + 3」の大きさのchar配列を生成し、その大きさを戻している。このため、実行結果にはchar配列のサイズである「13」が表示されている。

 つまりsizeof演算子の評価は、コンパイル時ではなく実行時に解決されるということだ。なお、C11からは可変長配列のサポートはオプションであることに注意しよう。可変長配列がサポートされない処理系では、「__STDC_NO_VLA__」というマクロが定義されるので(C11:§6.10.8.3)、これによって判別可能だ。

サイズ0配列(C99/C11:§6.7.2.1)

 ANSI-Cでは、Cの配列の長さの最小値は「1」である。このため、例えば頭に長さを持った可変長のデータ構造を、構造体を使って作成する場合、本当はサイズ「0」を指定したくても、リスト3のようにサイズ「1」を指定する必要があった。

C
typedef struct {
  size_t size;
  char data[1];
} Packet;
リスト3 データ長を頭に持ったデータ構造

2014/02/09 この箇所の文章を削除しました。詳しくは、本ページ最後の【更新履歴】の「誤1」をご参照ください。)

 C99では構造体の最後の要素が配列の場合に限って、サイズを省略できる(仕様では、「不完全配列(incomplete array)」とか、「長さ指定省略(unknown size)」と呼ばれている)。

C
typedef struct {
  size_t size;
  char data[];
} Packet;
リスト5 不完全配列を最後のメンバーに持った構造体

 この場合最後のメンバーである「data」の大きさは「0」となる2014/02/09 この箇所の文章を削除しました。詳しくは、本ページ最後の【更新履歴】の「誤2」をご参照ください)。

 なお、このような構造体は他の構造体のメンバーとして指定したり、配列の要素として指定したりできないことに仕様上はなっているが、GCCの場合は他の構造体の最後のメンバーに指定する分には問題無いようだ(リスト7)。

C
typedef struct {
  Packet p;    // ok
} Parent;
 
typedef struct {
  Packet p;    // エラー
  int i;
} Parent;
リスト7 GCCでは、構造体の末尾にサイズが不定の構造体をメンバーとして持てる

プリプロセッサ

C++コメント(C99/C11:§6.4.9)

 C99以前にも処理系で独自にサポートされていることが多かったC++コメントが、C99で標準化された。「//」以降は行末までコメントとして扱われる。ただし文字定数、文字列リテラル、コメントの内部では無効である。

関数名マクロ(C99/C11:§6.4.2.2)

 これまでソースコード内の現在の位置を示すマクロとして、ソースのファイル名を表す「__FILE__」と、行番号を表す「__LINE__」が使用できた。C99では新たに「__func__」というマクロが追加されている。これは現在の関数の名前を表す。リスト8は、現在実行中の関数名“myfunc”を表示する(識別子が小文字になっているのは、やはり既存アプリケーションへの配慮なのだろう)。

C
#include <stdio.h>
void myfunc(void) {
  printf("%s¥n", __func__);
}
 
結果:
myfunc
リスト8 __func__マクロ

指示初期化子(C99:§6.7.8、C11:§6.7.9)

 指示初期化子(designated initializer)を用いると、配列の特定の添字、構造体の特定のメンバーのみを初期化できる。

配列の指示初期化子

 配列の指示初期化子は「[n]」と指定する。リスト9に例を示す。

C
int a[] = {
  [9] = 0
};
リスト9 配列の指示初期化子

 リスト9は指示初期化子を用いて、9番目の要素を「0」としている。この場合、配列は次のように初期化される。

  • 配列のサイズが未知(a[])の場合、指示初期化子で指定された添字の最大のもの(9)を基に決定される。つまりサイズは「10」となる
  • 指示初期化子が指定されなかった添字の領域は、staticに配列が宣言されたときと同じように初期化される。つまりint型の場合は、「0」に初期化され、ポインターならnullポインターに初期化される

 指示初期化子は複数指定することも可能だ。また、指示初期化子でない通常の初期化子と同時に使用することもできる(リスト10)。

C
int a[] = {
  [3] = 0,
  1,
  [8] = 0,
  2
};
リスト10 配列における指示初期化子と通常の初期化子の混在

 リスト10は、3番目の要素を「0」、4番目の要素を「1」に、8番目の要素を「0」、9番目の要素を「2」に初期化し、それ以外の添字の領域は「0」に初期化する。

 なお指示初期化子を使わずにauto変数(ローカル変数)を宣言したときの挙動は、以前と同様に未初期化となるので注意する必要がある(リスト11)。

C
void foo(void) {
  int a[10]; // 配列aの中身は初期化されない
}
リスト11 指示初期化子を使わずにローカル変数を定義すると、その要素の初期化は行われない
構造体、共用体の指示初期化子

 構造体、共用体の指示初期化子には「.メンバー名」が使える(リスト12)。

C
typedef struct {
  const int x;
  const int y;
  const int width;
  const int height;
} Rectangle;
 
int main(int argc, char **argv) {
  Rectangle r = { .x = 0 };
  // ……
}
リスト12 構造体における指示初期化子

 リスト12は、構造体の初期化に「.x = 0」を指定している。これにより、xには「0」が、残りのメンバーには「0」が設定される。このように構造体のメンバーの任意のメンバー名にピリオド(.)を前置きすることで、指定されたメンバーを初期化できる。配列のときと同様、明示的に初期化を指定しなかったメンバーは、staticに構造体が宣言されたときと同じように初期化される。

 配列の場合と同じように途中のメンバーも指定可能だし、指示初期化子の後に通常の初期化子を指定することも可能だ(リスト13)。

C
Rectangle r = {
  .width = 100,
  200
};
 
printf("%d, %d, %d, %d¥n", r.x, r.y, r.width, r.height);
 
結果:
0, 0, 100, 200
リスト13 構造体の初期化で指示初期化子と通常の初期化子を混在させる

 リスト13では「.width」を明示しているので、widthは「100」に初期化される。xとyは指定が無いので「0」に初期化される。最後に指定された「200」は、widthの次のメンバーであるheightに使用される。

変数宣言(C99/C11:§6.8.2)

 ANSI-Cまでは、関数内におけるローカル変数の宣言は、ブロック(※2015/10/19修正)の先頭で行う必要があった(リスト14)。

C
#include <stdio.h>
#include <assert.h>
 
int sum(int count, int a[]) {
  assert(count >= 0);
 
  int sum = 0; // ANSI-Cではエラー
  for (int i = 0; i < count; ++i) {  // ANSI-Cではエラー
    sum += a[i];
  }
  return sum;
}
 
int main(int argc, char **argv) {
  int data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
  printf("sum = %d¥n", sum(sizeof(data) / sizeof(data[0]), data));
}
リスト14 変数の宣言位置

 リスト14は、ANSI-Cまでは文法エラーとなる。ANSI-Cまでは、変数の宣言はブロックの先頭に置く必要があったからだ。C99以降では文法が以下のように変更されている。

文法
compound-statement:
  { block-item-list(opt) }
 
block-item-list:
  block-item
  block-item-list block-item
 
block-item:
  declaration
  statement
リスト15 C99におけるブロック(複合文)の文法

 「compound-statement」がブロックを表す文法要素だが、「block-item」の定義を見ると分かるとおり、文と宣言とを任意の順序で記述できることが分かる。リスト14では、ブロックの途中に変数宣言が出現する他、for文の中で変数が宣言されている。これは仕様では§6.8.5(C99/C11)で規定されており、for文でのみ変数宣言を置くことが可能となっている(リスト16)。

文法
for ( declaration expression(opt) ; expression(opt) ) statement
リスト16 for文の文法

 ここで宣言された変数は、for文のブロック内でだけ有効となる(リスト17)。

C
for (int i = 0; i < count; ++i) {
  sum += a[i];
}
printf("i = %d¥n", i); // エラー(変数未定義)
リスト17 for文で宣言した変数はfor文のブロック内でのみ使用可能

複合リテラル(C99/C11:§6.5.2.5)

 構造体や配列(ただし可変長配列を除く)をリテラルとして生成できるようになった。これは指示初期化子と併用すると、非常に強力な機能となる。文法はリスト18のいずれかになる。

文法
( type-name ) { initializer-list }
( type-name ) { initializer-list , }
リスト18 複合リテラルの文法

 例を見てみよう。

C
typedef struct {
  int x;
  int y;
} Point;
 
Point center(const Point *p1, const Point *p2) {
  return (Point) {
    .x = (p1->x + p2->x) / 2,
    .y = (p1->y + p2->y) / 2
  };
}
 
int main(int argc, char **argv) {
  Point p = center(&(Point){1, 2}, &(Point){2, 3});
  printf("(%d, %d)¥n", p.x, p.y);
}
リスト19 複合リテラル

center関数では戻り値を返すのに、また、main関数ではcenter関数に渡す引数を生成するのに複合リテラルを使用している。

 リスト19では、Pointという型で2次元上の点を表現し、centerという2つの点の中点を算出する関数を作成している。center関数はreturnで返すPointを生成するのに複合リテラルを使用している。このように「(型名) {初期化リスト}」で任意の構造体、サイズ指定の無い配列を生成することが可能だ。また、center関数を呼び出す側でも複合リテラルを利用している(リスト20)。

C
Point p = center(&(Point){1, 2}, &(Point){2, 3});
リスト20 関数呼び出しで複合リテラルを使用

 このように複合リテラルを用いると大幅に記述を簡略化できることが分かるが、ブロック内で使用した場合、auto記憶域上に確保されることに十分に注意が必要だ。オブジェクトがヒープ上に確保されるような一般的なオブジェクト指向言語の感覚でリスト21のようなコードを書くと、当然動作は保証されない。

C
// 間違い
Point *center(const Point *p1, const Point *p2) {
  return &(Point) {
    .x = (p1->x + p2->x) / 2,
    .y = (p1->y + p2->y) / 2
  };
}
リスト21 center関数が終了した時点で、戻り値とした複合リテラルが格納されている領域は無効となる

 この場合、複合リテラルで生成されるPointは、center関数のauto記憶域(通常はスタック)に確保されるため、center関数を抜けたところで無効になり、ポインターが指す場所は無効な記憶域となる。Cが持つこうしたスリリングさは記述が簡単になっても相変わらずなので、常に気を配る必要がある。

 なお、配列も複合リテラルで生成可能だ(リスト22)。

C
int sum(size_t size, const int ary[]) {
  int s = 0;
  for (size_t i = 0; i < size; ++i) {
    s += ary[i];
  }
 
  return s;
}
 
int main(int argc, char **argv) {
  int s = sum(3, (const int []) {1, 2, 3});  // 1
  printf("%d¥n", s);
}
リスト22 配列を複合リテラルで生成する

 1では複合リテラルを用いて配列を渡している。なお仕様上は配列を複合リテラルで生成する場合には、unknown size、つまりサイズ指定の無いものしか使用できないことになっている。つまり「(const int[3]) {1}」は間違いということになる(GCCでは「-Wall -pedantic -std=c99」を指定しても問題なくコンパイルでき、警告も表示されなかった)。

ライブラリ

 C99ではライブラリも変更されている。ここではその中から主だったものを紹介しよう。

stdint.hヘッダーファイル(C99:§7.18、C11:§7.20)

 Cはint型のbit(ビット)幅を規定しない。これにより、その処理系で最も効率的に処理できるbit幅をintに割り当てることで、同じソースコードでも、その処理系で最適な実行効率を得ることが可能となっている。しかしこれはもろ刃の剣であり、たまたま使っていた処理系のintが32bitだったからと、intが32bitであることを前提としたコードを書いてしまうと、他のプラットフォームとの互換性が失われてしまう2014/03/20 この箇所の文章を修正しました。詳しくは、本ページ最後の【更新履歴】の「誤3」をご参照ください)

 また、アルゴリズムによっては、特定のbit幅が必要となることも多い(例えば特定の符号化処理など)。そういう場合、これまではプログラマーあるいは処理系がbit幅を規定した「INT32」のような型名をtypedefしたヘッダーファイルを用意し(リスト23)、intが32bitでない処理系に移植する際には、このヘッダーファイルを修正するという方法で対処するのが一般的だった。stdint.hヘッダーファイルは、これを標準として取り込んだものだ。

C
typedef int INT32;
リスト23 以前は、移植性を高めるために、処理系依存な部分を分離したヘッダーファイルを用意していた

 このヘッダーファイルでは表1に示す型を定義する。

表記解説処理系でのサポート
intN_t
uintN_t
指定されたbit幅を持つ整数型 オプション int8_t: 8bit幅の符号付き整数型
int_leastN_t
uint_leastN_t
最低でも指定されたbit幅を持つ整数型 N=8/16/32/64は必須(それ以外はオプション) int_least8_t: 最低でも8bit幅の符号付き整数型
uint_least16_t: 最低でも16bit幅の符号無し整数型
int_fastN_t
uint_fastN_t
最低でも指定されたbit幅を持つ最も高速に処理できる整数型 N=8/16/32/64は必須(それ以外はオプション) int_fast8_t: 最低でも8bit幅の高速に処理可能な符号付き整数型
uintptr_t
intptr_t
ポインターも精度落ちなく保持できる整数型 オプション  
uintmax_t
intmax_t
最大のbit幅を持つ整数型 必須  
表1 stdint.hヘッダーファイルで定義される整数型(それぞれ1行目は符号付き、2行目は符号無し)

 指定されたbit幅を持った型だけでなく、最低でも指定されたbit幅を持った型、最低でも指定されたbit幅を持ち、かつ高速に処理できる型が定義されていることが特徴だ。さらにintN_tは仕様ではオプションなのに、int_leastN_tや、int_fastN_tの、「N」としては「8」「16」「32」「64」(つまり例えば「int_least8_t」~「int_least64_t」)が必須となっている点も面白い点といえる。つまり移植性を考慮した場合、intN_tよりも、int_leastN_tや、int_fastN_tの方が望ましいということになる。これはCPUアーキテクチャによっては、狭いbit幅でのメモリアクセスが遅くなるケースがあることに配慮した結果だろう。stdint.hヘッダーファイルでは、これらの型に対応した最大、最小値も定義されている。

表記解説
INT[N]_MIN Nビットで表現される最小の値(符号付き) INT16_MIN = -32768
INT[N]_MAX Nビットで表現される最大の値(符号付き) INT16_MAX = 32767
UNT[N]_MAX Nビットで表現される最大の値(符号無し) UINT16_MAX = 65535
INT_LEAST[N]_MIN 最低でも指定されたbit幅を持つ、整数型に格納できる値の最小値(符号付き) INT_LEAST16_MIN = -32768以下の値(処理系依存)
INT_LEAST[N]_MAX 最低でも指定されたbit幅を持つ、整数に格納できる値の最大値(符号付き) INT_LEAST16_MAX = 32767以上の値(処理系依存)
UINT_LEAST[N]_MAX 最低でも指定されたbit幅を持つ、整数に格納できる値の最大値(符号無し) UINT_LEAST16_MAX = 65535以上の値(処理系依存)
INT_FAST[N]_MIN 最低でも指定されたbit幅を持つ、最も高速に処理できる整数型に格納できる値の最小値(符号付き) INT_FAST16_MIN = -32768以下の値(処理系依存)
INT_FAST[N]_MAX 最低でも指定されたbit幅を持つ、最も高速に処理できる整数型に格納できる値の最大値(符号付き) INT_FAST16_MAX = 32767以上の値(処理系依存)
UINT_FAST[N]_MAX 最低でも指定されたbit幅を持つ、最も高速に処理できる整数型に格納できる値の最大値(符号無し) UINT_FAST16_MAX = 65535以上の値(処理系依存)
INTPTR_MIN ポインターを格納可能な整数型で格納可能な整数の最小値(符号付き)  
INTPTR_MAX ポインターを格納可能な整数型で格納可能な整数の最大値(符号付き)  
UINTPTR_MAX ポインターを格納可能な整数型で格納可能な整数の最大値(符号無し)  
INTMAX_MIN 最大のbit幅を持つ整数型に格納可能な整数の最小値(符号付き)  
INTMAX_MAX 最大のbit幅を持つ整数型に格納可能な整数の最大値(符号付き)  
UINTMAX_MAX 最大のbit幅を持つ整数型に格納可能な整数の最大値(符号無し)  
表2 stdint.hヘッダーファイルで定義される最大値、最小値

 これらを用いることでbit幅を明示的に指定したプログラミングが可能となる。また、この仕様が広まれば、こうした型や最大、最小値定義が処理系ごとに異なることが原因で起きていた、移植時の煩わしい作業が不要となるだろう。

stdio.hヘッダーファイル(C99:§7.19、C11:§7.21)

 stdio.hヘッダーファイルの変更で最も注目すべきものの1つが、snprintf関数だ(C99:§7.19.6.5、C11:§7.21.6.5)。もともと存在した「n」が付かないsprintf関数は、printf関数の結果を標準出力に出力する代わりにメモリ上に出力する大変便利な関数だが、ちょっとした不注意でバッファー領域の外に誤って書き込んでしまうバグを発生させやすいという問題があった。例えばリスト24のコードは、引数で渡されたp変数とi変数の値を連結した文字列を、ローカル変数buf内に生成するものだ。

C++
void foo(const char *p, int i) {
  char buf[16];
  sprintf(buf, "%s%d", p, i);
  // ……
}
リスト24 バッファオーバーランの危険性をはらんだコード

 引数pとiの値を連結した結果が15文字以内に収まるかどうかは引数次第だ。もしも収まらないとメモリ領域の破壊が起き、アプリケーションが異常動作、停止する原因となる。それだけではない。auto記憶域と、戻りアドレスを保管する記憶域が同じ処理系では、これはバッファオーバーラン脆弱性(ぜいじゃくせい)となる可能性がある。例えば引数pが外部から入力される値である場合、意図的に長い文字列を渡すことで、戻りアドレス部分を書き替えて、その先に悪意を持ったプログラムコードを配置すれば、そのプログラムが意図しないコードを実行させることが可能となるのだ。

 C99で追加されたsnprintf関数は、引数が追加されており、ここに出力先のバッファーの長さを指定できる(リスト25)。

C++
void foo(const char *p, int i) {
  char buf[16];
  snprintf(buf, sizeof(buf), "%s%d", p, i);
  // ……
}
リスト25 snprintf関数では出力先バッファーのサイズ指定が可能

 snprintf関数は、結果が第2引数で指定された長さを超える場合には、あふれた部分を切り捨てるので、意図しない引数を指定されたとしてもバッファーの領域外が破壊されることを防止できる。

まとめ

 今回はC99の仕様の中から、以下に挙げる主だったものを選んで紹介した。

  • bool型: trueかfalseかの2つの値を持つ型が標準に追加された
  • 可変長配列: 実行時にサイズを決定可能な配列を作成できるようになった
  • サイズ0配列: 構造体の最後の要素に限って配列のサイズを省略することで、サイズ0の配列を宣言できるようになった
  • C++コメント: 「//」で始まるC++コメントが使えるようになった
  • 関数名マクロ: 「__func__」というマクロが追加され、関数名を取得できるようになった
  • 指示初期化子: 構造体や配列で、特定の要素を指定して初期化できるようになった
  • 変数宣言: ブロックの先頭だけでなく、任意の位置で変数を宣言できるようになった
  • 複合リテラル: 構造体や配列をリテラルとして生成できるようになった
  • stdint.hヘッダーファイル: bit幅が規定された整数型が追加された
  • stdio.hヘッダーファイル: snprintf関数が追加され、書き込み先のバッファーのサイズを指定できるようになった

 これ以外にもC99にはさまざまな仕様が追加となっている。Cの新しい仕様の詳細に興味のある方は本文でも紹介したISOの仕様書を参照してほしい。

 今回はC99で追加された仕様について解説をした。次回はC11で追加された仕様について説明しよう。

【更新履歴】2014/02/09

 以下のような誤りがありました。お詫びして訂正させていただきます。

誤1

 このデータ用の領域を、malloc関数を使って確保するには、リスト4のように複雑な指定が必要となる。

C
// nはdata要素の個数
Packet *p = malloc(sizeof(Packet) + sizeof(char) * (n - 1));
リスト4 データ長を頭に持ったデータ構造用の領域をmalloc関数で確保(C99より前)
誤2

 この場合、最後のメンバーである「data」の大きさは「0」となるため、リスト6のような単純な式で領域を確保できる

C
Packet *p = malloc(sizeof(Packet) + sizeof(char) * n);
リスト6 データ長を頭に持ったデータ構造用の領域をmalloc関数で確保(C99)

 構造体は、最後のメンバの後にパディングを含むことがあり(trailing padding。C99、C11:§6.7.2.1)、sizeofは、このパディングを含んだサイズを返します(C99、C11:§6.5.3.4)。このため、「誤1」「誤2」の例では、このパディング分メモリを余計に確保してしまうことになります。より正確にはANSI-CでもC99でも以下のようにする必要があります。

C
Packet *p = malloc(offsetof(Packet, data) + sizeof(char) * n);
データ長を頭に持ったデータ構造用の領域をmalloc関数で確保(ANSI-C/C99)

【更新履歴】2014/03/20

 以下のような誤りがありました。お詫びして訂正させていただきます。

誤3

 同じソースコードでも、その処理系で最適な実行効率を得ることが可能となっている。しかしこれはもろ刃の剣であり、極端にいえば「intが8bit」の処理系もあり得るため(実際組み込み系用のコンパイラには、こういうものも存在する)、そうした処理系でも正しく動作するソースコードを書こうと意識すると、intで扱える値の範囲を「-128」~「127」に制限しなければならなくなってしまう。

正3

 同じソースコードでも、その処理系で最適な実行効率を得ることが可能となっている。しかしこれはもろ刃の剣であり、たまたま使っていた処理系のintが32bitだったからと、intが32bitであることを前提としたコードを書いてしまうと、他のプラットフォームとの互換性が失われてしまう。

 ANSI-Cでは、以下のような記載があり、

「コンパイラの実装においては、(絶対値において)ここに記載された値よりも大きい値でかつ同一符号の値を用いなければならない」
Their implementation-defined values shall be equal or greater in magnitude (absolute value) to those shown, with the same sign.

maximum value for an object of type int
INT_MAX +32767 // 2^15 − 1

 このことからintの最大値は少なくとも32767であることが規定されています。このためANSI-Cに準拠した処理系ではintは最低でも32767までの値を保持できなければなりません。

【更新履歴】2015/10/19

 以下のような誤りがありました。お詫びして訂正させていただきます。

誤4

 ANSI-Cまでは、関数内におけるローカル変数の宣言は、関数の先頭で行う必要があった(リスト14)。

正4

 ANSI-Cまでは、関数内におけるローカル変数の宣言は、ブロックの先頭で行う必要があった(リスト14)。

連載:C言語の最新事情を知る(1)
1. 【現在、表示中】≫ C99の仕様

長い歴史を持ちながら、依然として人気の高いC言語。その最新仕様の情報にキャッチアップするための連載スタート。今回は1999年に策定された「C99」を取り上げる。

連載:C言語の最新事情を知る(1)
2. C11の仕様-脆弱性対応に関連する機能強化点

C言語の最新仕様の情報にキャッチアップしよう。C11の仕様で強化された機能のうち、主に脆弱性対応に関連するものを紹介する。

連載:C言語の最新事情を知る(1)
3. C11の仕様-それ以外の主な機能強化点

C言語の最新仕様の情報にキャッチアップしよう。C11の仕様で強化された機能のうち、脆弱性対応以外の主要な強化点を紹介。

連載:C言語の最新事情を知る(1)
4. C99でリソース管理ライブラリを作ってみる

前回まではC99/C11の仕様について見てきた。今回は、従来のCプログラミングが、C99の仕様を活用することで、どのように変化するのかを取り上げる。

サイトからのお知らせ

Twitterでつぶやこう!