見出し画像

C言語教室 第35回 constの闇 と 第34回の解答

前回の教室で const のポインタ型については、後回しにしていました。今回はそれを説明します。

まず C言語の const とは、コンパイラが代入を見つける(初期化は代入ではありません)とエラーとするものです。const を付けたからと言って、その値が書き換えできないように保護されているメモリに格納される訳ではありません。最適化により文字通り定数に展開されコードに埋め込まれることはありますが、いろいろな理由で値が変わることが無いわけではありません。const の影響は、意図せずに値が書き換えられないようにするためというより、値が変わらないという前提に立つことができるので、より強力な最適化が出来るようになることにあります。

const が変数の「値」に対して指定している時はわかりやすいのですが、これがポインタ型に対して指定していると、その const が何を指しているのかが少しばかり分かりにくいことになります。前回の課題を例に使うので、まだ課題を解いていない方は、できればここから先は読まずに、まず自分の頭で考えてください。


ということで、少しだけ余談を挟みます。#define による定数の定義は文字列的な処理なので、値は「リテラル」に展開されコードに「埋め込まれ」ます。

#define STR "abcdefg"
char s[] = STR;

リテラルは本来は読み込み専用なので、const の話が出てこなくても、s[0]を変更することはそもそもできない筈なのですが、実際には書き換えてしまっても特にエラーが出ないこともあります(多くのlinuxの実装)。その意味ではリテラルは静的変数と同じように振る舞うのです。※リテラルとは、コードに直接埋め込まれるような値です。

#include <stdio.h>

void main() {
  char s[] = "abcdefg";
  s[0] = 'A';
  printf("%s\n", s);
}

これを実行すると linux では以下の出力が得られます。

Abcdefg

ちなみにWindows上のMinGWの場合だと以下のエラーが「実行時」に出ます(抜粋)。

プログラム 'a.exe' の実行に失敗しました: ファイルにウイルスまたは望ましくない可
能性のあるソフトウェアが含まれているため、操作は正常に完了しませんでした。発生
場所 行:1 文字:1 .\a.exe

なお a.exe があるディレクトリをセキュリティ設定で除外しておかないと、当たり前ですが、ウィルスチェックソフトにとって未知のプログラムなので、いちいち警告が出て面倒です。

ということで、リテラルを指すポインタは、そもそも「const にしておけ」という話でもあります。


さて、本題に戻ります。リテラルを書き換える話にならないように、ちょっと余計なコードを足しておきます。

[const.c]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main() {
  char *s = (char*)malloc(sizeof(char)*4);
  strcpy(s, "abc");
  const char *p = s;
  char * const q = s;

  /* ここをいろいろ書き換える */
  printf("%s\n", s);
  free(s);
}

このコードのコメントになっている行を p[0] = 'A'; (*p = 'A'; でも同じ)という式に置き換えると、

const.c:11:8: error: assignment of read-only location '*p'
11|   p[0] = 'A';
  |        ^

というエラーが出て、コンパイルできません。もちろんsを使った代入は出来るので、s[0] = 'A'; と書けば文字列を書き換えられます。pを使って変更できなくなるだけです。p自身は

p = q;

などと代入することができるので、書き換えてはいけない対象を変えることはできます。constが後ろにあるケースでは

q[0] = 'A';

はエラーとならず、'A'を代入して書き換えることができますが、

q = s;

とすると、

const.c:11:5: error: assignment of read-only variable 'q'
11 |   q = s;
   |     ^

というエラーになります。つまりポインタの値であるアドレスが変更できなくなっているのです。なお、ポインタも指す先も書き換えられないようにするには、

const char * const r = s;

と書きます。

白いところは変えられるがグレーのところはダメ

さて関数のプロトタイプ宣言にも、const はとてもよく登場します。

引き数で渡される変数は、呼び出しの時に「コピー」されるので、その変数が const であろうとなかろうと呼び出し側の変数の値が変わることはありません。これがポインタ型の場合「ポインタの値」であるアドレスがコピーされるので、ポインタ自身が const であるかどうかは関係ないのです。ところが「ポインタの指す先」が const である場合は注意が必要です。

[arg.c]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void func(const char* s) {
  *s = 'A';
  printf("%s\n", s);
}

void main() {
  char *q = (char*)malloc(sizeof(char)*4);
  strcpy(q, "abc");
  const char* p = q;
  func(q);
  free(q);
}

このように関数の側が const char* の場合は、呼び出す時に使うポインタの型が char* でも const char* でも構いません(pでもqでもfunc()を呼べる)。関数側が const char* なので、このポインタの指す先が関数内で変更されないことを期待することになります(なぜ期待と書いているかは後で説明します)。

さて、funcの引き数の型が char* であったらどうなるでしょう(関数の引き数をvoid func(char* s)と変えた場合)。呼び出し側の型がchar*であれば何の問題もありません(qで呼び出した場合)。これをconst char* である p で呼び出そうとすると、

.\arg.c: In function 'main':
.\arg.c:14:8: warning: passing argument 1 of 'func' discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]
9 |   func(q);
  |        ^
.\arg.c:5:17: note: expected 'char *' but argument is of type 'const char '
3 | void func(char p) {
  |           ~~~~~~^

というメッセージが出て、コンパイルできません。これは「このポインタはconst なんで、const の無い char * を引き数としている関数を呼ぶのはダメだからね」と怒られるわけです。さて、怒られたので関数の型をfunc(const char* s)とすれば、このエラーは無くなるのですが、この関数の中で、渡されたポインタをキャストしてしまえば、何の問題もなく渡したポインタの指す先を書き換えることは出来ます。

void func(const char* s)  {
  char *r = (char*)s;
  *r = 'A';
  printf("%s\n", r);
}

という訳で、引数の型が const char* だから絶対安心かと言うと、そんなことはありません。ですからC言語のconstは面倒なだけで気休めに過ぎない側面はあるのですが、コンパイラはconstを参考に最適化をするので、キチンとconstを書くのはとても意味があります。なお restrict を付けてもこのコードはエラーになりませんが、厳密な意味では要件に違反するので動作は未定義になります。

続 restrictキーワード


さて、前回の課題はここまでの説明で解答になっている筈です。今回の課題は以前にもAyumiKatayamaさんが、

【C言語】ポインタ変数に「const」を付けよう!

で触れられていたのですが、あらためて

C言語教室 第34回 - 型修飾子あれこれ(回答)

に回答を頂きました。もう「わかっているよ」という内容ではありますが、ありがとうございます。私の説明でわからないときは、こちらの記事のほうがシンプルな説明になっていると思います。


さて、今回の課題ですが、もうひとつ const が使えるところがあるので、そちらを課題にします。

課題

関数の戻り値を const にした例を示し、その効果を考察しなさい。

なおC++ではconstな(メンバ)関数というのが出てくるので、さらにややこしいことになります。


次回は積み残している enum と bool を取り上げる予定です。

ヘッダ画像は、いらすとや さんより
https://www.irasutoya.com/2013/10/blog-post_3451.html

#C言語 #プログラミング講座 #ポインタ #引き数 #リテラル

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