見出し画像

C言語 間違い探し 答え編

こちらの記事の答えである。



そもそも、いったい何を間違ったのか

再掲すると、例外を引き起こしたのはこのコードである。

#include <stdio.h>
int main()
{
        const static char* name[] =
        {
                "aaa"
                "bbb"
                "ccc"
        };

        printf("%s\n", name[0]);
        printf("%s\n", name[1]);
        printf("%s\n", name[2]);
        return 0;
}

私が間違ったのは「,」忘れである。
ここ、この「,」たち。

これを付け忘れている。
しかも、この「,」忘れがコンパイルエラーにならないというのである。
Visual Studioのバグでね?
初めはそれさえ疑った。
だってね、たいていの「,」は忘れるとコンパイルエラーになりますやん。

これだって

func(a b c);

これだって

enum {
    AAA
    BBB
    CCC
};

いやいや、これにしたって

const static int number[] =
{
    1
    2
    3
};

全部、コンパイルエラーになる。

なんでこれだけがコンパイルエラーになりまへんのん?

const static char* name[] =
{
    "aaa"
    "bbb"
    "ccc"
};

なので調べてみた

カーニハンC

索引の次の項目を読んでみたが、関連する記載はなかった。

  • 文字定数

  • 文字列

  • 文字列定数


カーニハンC 第2版(ANSI規格準拠)

ああ。ありました。

文字定数はコンパイル時に連結することもできる。
 "hello,"  " world"

 "hello, world"
と同じである。これはいくつかのソース行にまたがる長い文字列を扱うときに便利であろう。

プログラミング言語C
2.3 定数

なるほど。
この仕様か。

だから、このコード

const static char* name[] =
{
    "aaa"
    "bbb"
    "ccc"
};

これは次のようにコンパイルされたわけだ。

const static char* name[] =
{
    "aaabbbccc"
};

この場合、
 name[0] = "aaabbbccc"
となるが
 name[1]
 name[2]
は、ない。
ないというのは、ないんである。
0に設定されているのではなく、存在そのものがない。

const static char* name[3];

ではなくて

const static char* name[1];

ということだ。
配列1つ分しか領域は確保されていないので、
 name[1]
 name[2]
はオーバーランということになる。
領域侵犯である。

clang は(null)を表示して
Visual Studio は例外になった。
不正ポインタの挙動はマチマチである。
言語仕様としても規定されていない。

Visual Studioさん、すみません。
言いがかりでした。
これは言語仕様でした。


JIS (延長戦)

ついでなのでJIS も確認してみた。
実はこちらを先に確認していたんだが、こちらはこちらで軽く酩酊状態に陥る。

意味規則 翻訳フェーズ(6)において,隣り合う単純文字列リテラル字句又はワイド文字列リテラル字句で指定される多バイト文字の並びは,連結して一つの多バイト文字の並びになる。

JIS
6.4.5 文字列リテラル

早速、クラクラしてきた。正確に書こうとしてまどろっこしい文章になってるんだが、簡単に書くとこうなる。

隣り合う文字列は,連結して一つの文字列になる。

JISX:3010 6.4.5 文字列リテラル

単純文字列リテラル字句、とか
ワイド文字列リテラル字句、とか
多バイト文字、とか
そんな風に称するから、それは正確さを求めてのことなんだろうけど、肝心の焦点がぼやけそうである。

さらに、次のような例もある。

例 隣り合った単純文字列リテラルの組
  "\x12" "3"
は,隣り合う文字列リテラルの連結の前に逆斜線表記が実行文字集合の一つの要素に変換されるので,値が'\x12'と値'3'の二つの文字を含む一つの単純文字列リテラルを作る。

JISX:3010 6.4.5 文字列リテラル

どうでもいいが、「\」を「\」と、全角で書くのは何故だろう。
フォントによっては読みにくいから?
確かに
 \x
と書くと縦線のように見えなくもないのであって、電子データであればコピーすればすむものの印刷するとなると目で見てタイピングせねばならず、となると読み間違いにくい表記をめざす気持ちはわからないではないんだが、それにしてもなんだかなぁと思わずにはいられない。

ついでに「逆斜線表記」というのもなんだかなぁ、である。「エスケープシーケンス」でいいんじゃね?
そんなに必死になって訳さなくても。

この際だからついでに言うと
「逆斜線表記が実行文字集合の一つの要素に変換されるので」
とあるが、あとに続く文章では変換されていないので何を言っているのかわかりにくい。

私流に書き直すとこうなる。

例 隣り合った文字列
  "\x12" "3"
は,バックスラッシュをエスケープシーケンスで変換した後で連結するので,値'0x12'と値'0x33'の2バイトの文字列を作る。

Ayumi変換

なるほど、そういうことであったか。
文字列 "\x12" というのは

16進数で「12」という変換にする

というエスケープシーケンスである。「12」の数値は「00~FF」までを自由に選択できて、16進数文字列を連続することができる。この「連続できる」というのが肝である。結果

"\x123"

と書いても

0x12、0x33

という数値には展開されない。
なので次のようになる。

  • "\x01" → 0x01

  • "\x012" → 0x12

  • "\x01" "2" → 0x01 0x32

なんで "\x123" ではないのかというと、"\x123" は clang ではエラーになったからである。JIS には

16進逆斜線表記は非16進文字によってだけ終了するので,表現'\x123'はただ1文字を含む単純文字定数を示す

JISX:3010 6.4.4.4 文字定数 

とあるので、これまた微妙ではあるがこういうことはC言語ではよくある。JIS(ISO)に対して100点満点のコンパイラというのはなかなか難しかろうと思われる。

ついでにもう1つ。
翻訳フェーズ(6)ってなんだというと、次に書いてある。

5.1.1.2 翻訳フェーズ
(6) 隣接する文字列リテラル字句同士を連結する。

JISX:3010 

ちなみに他のフェーズも列挙するとこうなる。

(1) 文字コード解釈。3文字表記変換。
(2)「\改行」カット
(3)コメントカット
(4)プリプロセッサ展開
(5)エスケープシーケンス展開
(6)隣接文字列連結
(7)コンパイル
(8)リンク

隣接文字列の連結というのは「(7)コンパイル」に含まれるのではなくって独立しているんである。コメントカットやプリプロセッサ展開、エスケープシーケンス展開と同列だ。この連結処理を終えてからコンパイルするわけで、コンパイルエラーになりようがない。


余談ですが

カーニハンCを読んでいると、他にも「ん? 」と思うことが………。

最後に,\の後に復改が続くと無視される。

プログラミング言語C 
付録A:C参照マニュアル 
2.5 文字列

え?
そうなん?
これまた初見である。
なのでやってみた。

#include <stdio.h>
int main()                                                        {
        printf("こういう長い文章を printf で出力しようとすると\
ひたすら長い1行になってコードが見にくくって\
しかたがない。\
とはいうものの、いちいち printf を分けるのも\
面倒じゃない。ちょっと改行して\
コードを見やすくしたいんです。\n");
        return 0;
}

実行結果

こういう長い文章を printf で出力しようとするとひたすら長い1行にな ってコードが見にくくってしかたがない。とはいうものの、いちいち printf を分けるのも面倒じゃない。ちょっと改行してコードを見やすくしたいんです。

ちなみにこの「\改行」をカットする処理は「翻訳フェーズ(2)」にあたる。

ふぅ~。

この記事が気に入ったらサポートをしてみませんか?