constが2つある場合のポインタ変数
2024/08/29
C言語を長いこと使っていないのでいろいろ忘れている。
前回の Exercise にこういう行があった。
static const struct device *const async_adapter;
const
が 2つある場合、変数への代入ができない方と値の書き換えができない方があった気がする。
const char[]
char c[] = "abc";
これはchar
型で初期値abc\0
を持つ配列だ。
配列の場合、単にc
だけにするとc[0]
のアドレス値を指す。
c
はポインタ変数ではないので、c[0]
にchar
型のデータを代入はできてもc
にchar
ポインタ型の変数を代入することはできない。
同一型で同一サイズの変数なら代入で書くことによりmemcpy()
になるかと思ったが、そうはならんようだ。
そういった事情?だから、c
のアドレスはその場所から変わらないことになる。
&c
も同じアドレスである。
ポインタ変数と間違えないようにコンパイルエラーにしてもいいんじゃないかという気はするが、
const
をつけると、c
のデータを書き換えるコードはコンパイルエラーになる。
const char c[] = "abc";
別のconst
が付かない変数にc
を代入してしまえば、コンパイラはconst
が付いていないからエラーにしない。
せいぜい、const
が付かない変数に代入する際に warning を出すくらいである。
たまに値を書き換えないのに仮引数でchar *
をとる関数がある。
文字列はconst char*
などが多いので引数で使うと warning が出る。
仕事でやっていると warning は全部除去するか、あるいは説明をしないといけないということがある。
そういうときはイライラするね(const
をつけなかった関数に)。
この例だとc[]
は書き換えないので、初期値付きで宣言しないとダメだろう。
かといってc[10]
みたいにしても代入できないので同じだ。
だったらこの変数の値は書き換えられないので、c
自体を RAM ではなく ROM に置いてしまってよいのではないかという気もする。
そうしてしまえば RAM は消費しないし、初期値のコピーも不要になるし。
const
配列を RAM に置くのか ROM に置くのかはよく知らないが、ROM に置かれるものと考えるのが良いだろう。
悩ましいなら、const
配列よりはconst
ポインタにした方がなんとなく無難な気がしているので、私はだいたいそう書くようにしている。
const char *
上に書いたが、書き換えない文字列なんかはconst char *
で十分だと思う。
const char *c = "abc";
ダブルクォーテーションで囲んだ文字列は「文字列リテラル」になる。
なので "abc"
は ROM に置かれるはず(個人の感想です)。
const char *
のポインタ変数は代入が可能である。
#define HLO "HELLO"
#define WLD "WORLD"
static void test1(void)
{
const char *c = HLO;
printf("c=%p\n", c);
c = WLD;
printf("c=%p\n", c);
}
この関数をコピーして違う名前(test2
)を割り当てて両方実行したが、それぞれHLO
のアドレスもWLD
のアドレスも同じところを指していた。
test1 --------------
a=0x55b0f7829004
a=0x55b0f7829010
test2 --------------
a=0x55b0f7829004
a=0x55b0f7829010
#define
は変数ではなくマクロなので、それぞれの場所に展開された(gcc)。
なので関数も違うし別のアドレスになるんじゃないかと思ったが、そうはならなかったのだ。
試しにtest2
の方でHLO
の代わりに直接"HELLO"
を使ってみたが、やはり同じアドレスになった。
この辺はコンパイラの最適化が仕事をしたのだろう。
char * const
const
が後ろに来ると、これは c
への代入ができなくなる。
こちらはコンパイルエラーだ。
static void test3(void)
{
char * const c = HLO;
printf("c=%p\n", c);
c = WLD; // エラー
}
その代わりと行ってはなんだが、中身への書き込みはできるようになる。
static void test3(void)
{
char *const c = HLO;
printf("c=%p\n", c);
c[0] = 'b';
printf("c=%p\n", c);
}
あくまでコンパイラ上ではできるというだけで、動くかどうかは別である。
上のコードはコンパイルエラーにはならないが、Linux 上で実行すると Segmentation Fault が発生する。
HLO
が ROM に置かれているので書き換えができないからだ。
const char * const
こうすると、ポインタ変数への代入もできないし、中身の書き換えもできない。
static void test4(void)
{
const char * const c = "HELLO";
printf("c=%p\n", c);
c = WLD; // エラー
c[0] = 'b'; // エラー
}
const char *
で十分と書いたが、c
を変更するつもりがないなら こっちの方がよいのかな。
const char const *
これはエラーにはならないが、うざいって言われる。
static void test5(void)
{
const char const * c = "HELLO";
printf("c=%p\n", c);
}
warning: duplicate ‘const’ declaration specifier [-Wduplicate-decl-specifier]
duplicate なのは後ろの const
である。
char const * const
これはconst char * const
と同じ扱い。
char const * const c = "HELLO";
なので無理に文字にするなら、const
の直後にあるものが const
の対象になるという感じか。
const *
がポインタ変数が指した中身で、 const c
がポインタ変数c
そのもの。
ただ、私は const char *
で済ませたい派で、スタックくらいだったらポインタ変数が可変になっていてもいいんじゃないのと思っている。
コンパイラの最適化任せ。
グローバル変数で変更したくないポインタ変数だったら仕方なくconst
を2つ付けるが、それでも最初にconst
を付けてしまいたい。
まあ、気分の問題と言ってしまえばそれまでだが。
const 以外の修飾子
const
の話はもう終わるとして、同じジャンル(修飾子)として volatile
と restrict
があるそうだ。
volatile
は覚えている。
これは最適化させないときに使うやつだ。メモリマップドI/Oでのレジスタアドレスが入ったポインタ変数などだ。
必要があって書き込むので最適化されると困る場合に使う。
restrict
は C99 で採用されたらしい。
これはコンパイラへの指示と同時に実装者の制約になっていて、restrict
なポインタ変数を使ってしか指している先のメモリにアクセスしないように実装しているので、コンパイラはそれを前提にしてギリギリまで最適化しても良いですよ、ということらしい。
memcpyの仮引数にも付いている。
memcpy
しているアドレスにそれ以外からアクセスすることがあってはいけないのでrestrict
を付けていても問題ないはず、みたいなところもあるのかな?
使いどころが難しいというか、restrict
があるのを思い出せない気がする。。。
ncs の C言語
ncs は C99 以降になっているのだろうか?
Language Standardsには C99 以降であればよいということが書かれていた。
CONFIG_CPP
でC++もサポートできるようだ。
STL は便利だけど、気軽に使っているとサイズが大きくなりそうだし、このくらいのマイコンであれば C言語の方がわかりやすいと思うので忘れよう。