何故C言語で配列をdefineする必要があるか
組み込み向けプログラミングでは、今でもC言語でプログラムが書かれることが主流です。組み込み向け機器では、WiFiやBluetoothなどでデバイスのアドレスなどの情報を保持することがあります。そのとき、例えばアドレスを保持する配列を初期化する際に以下のようなことができると便利です。
uint8_t addr[ADDR_SIZE]
memcpy(addr, ADDR_NONE, ADDR_SIZE)
Real Time OSであるZephyr OSのソースコードを読んでいる時に、これを実装する方法を発見し、この方法に関してネットで探してもあまり情報が見当たらなかったので、ここで共有します。
配列をdefineする方法
結論から書くと、以下のようにすればよいです。
#include <string.h>
#include <stdio.h>
#define ARRAY_LENGTH 3
#define INVALID_ARRAY ((char[]){'r', 'e', 'i'})
int main(){
char array[ARRAY_LENGTH];
memcpy(array, INVALID_ARRAY, ARRAY_LENGTH);
for(int i=0;i<ARRAY_LENGTH;i++){
printf("%c ", array[i]);
}
printf("\n");
}
これで問題なく動作します。ポイントは、char[]
でキャストしている点です。
@SaitoAtsushiさんに、これはキャストではなく、compount literalsであると教えていただきました。ご指摘いただきありがとうございます。内容を修正しました。
C99の規格の6.5.2.5 Compound literalsのセクションにおいて、以下のように記述されています。
4 A postfix expression that consists of a parenthesized type name followed by a braceenclosed list of initializers is a compound literal. It provides an unnamed object whose value is given by the initializer list 84).
84)Note that this differs from a cast expression. For example, a cast specifies a conversion to scalar types or void only, and the result of a cast expression is not an lvalue.
要約すると、(char[]){'r','e','i'}
のように、括弧で囲まれた型名に続く形で、中括弧の中に初期値をリスト形式で記載すると、それはcompound literalとなります。さらに、これはキャストとは異なるものであり、キャストはスカラー型およびvoidにしか変換することができず、キャストした結果を左辺値(lvalue)にはすることができません(特定のアドレスに値が保持されません)。逆に言えば、compound literalであれば、左辺値(lvalue)であるため、特定のアドレスに値が保持されます。
今回の例であれは、((char[]){'r', 'e', 'i'})
は一旦特定のアドレスに値が保持されるため、memcpyでarrayに内容を移すことができます。
ダメな例: char*
でキャストする。
上記の例では、compound literalを使用してうまく動作しましたが、char*
でキャストするとうまく動きません。
#include <string.h>
#include <stdio.h>
#define ARRAY_LENGTH 3
#define INVALID_ARRAY ((char*){'r', 'e', 'i'})
int main(){
char array[ARRAY_LENGTH];
memcpy(array, INVALID_ARRAY, ARRAY_LENGTH);
for(int i=0;i<ARRAY_LENGTH;i++){
printf("%c ", array[i]);
}
printf("\n");
}
コンパイルは通りますが、以下のようなwarningが表示されます。
main.c:9:19: warning: incompatible integer to pointer conversion initializing 'char *' with an expression of type 'int' [-Wint-conversion]
memcpy(array, INVALID_ARRAY, ARRAY_LENGTH);
^~~~~~~~~~~~~
main.c:5:32: note: expanded from macro 'INVALID_ARRAY'
#define INVALID_ARRAY ((char*){'r', 'e', 'i'})
^~~
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/secure/_string.h:63:33: note: expanded from macro 'memcpy'
__builtin___memcpy_chk (dest, __VA_ARGS__, __darwin_obsz0 (dest))
^~~~~~~~~~~
main.c:9:19: warning: excess elements in scalar initializer [-Wexcess-initializers]
memcpy(array, INVALID_ARRAY, ARRAY_LENGTH);
^~~~~~~~~~~~~
main.c:5:37: note: expanded from macro 'INVALID_ARRAY'
#define INVALID_ARRAY ((char*){'r', 'e', 'i'})
^~~
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/secure/_string.h:63:33: note: expanded from macro 'memcpy'
__builtin___memcpy_chk (dest, __VA_ARGS__, __darwin_obsz0 (dest))
^~~~~~~~~~~
2 warnings generated.
そして、生成された実行ファイルを実行しても、Segmentation faultが表示されます。
Segmentation fault: 11
何故このようなことが起こるのでしょうか?
前節で説明したように、(char*)でキャストされた値はスカラー値およびvoidにしかなりません。そのため、この値はmemsetの引数に求められるvoidポインタに、さらにキャストすることはできず、warningが出ます。
さらに、char *str = "rei";
とchar[] str = "rei"
の違いを考えるとわかってきます。前者では、"rei"
というイミュータブルな変数がアドレス上に確保され、それに対するポインタがstr
に格納されます。従って、"rei"
という値はイミュータブルとなり、変更できません。一方で、後者の場合は、str
という配列が確保され、"rei"
で初期化されます。
そのため、char*
でキャストするには、あらかじめイミュータブルな変数が確保されている必要があり、((char*){'r', 'e', 'i'})
ではエラーが生じてしまいます。
Zephyr OSでの記述方法
参考になったZephyrのコードを転記しておきます(関連する部分のみ)。
/** Length in bytes of a standard Bluetooth address */
#define BT_ADDR_SIZE 6
/** Bluetooth Device Address */
typedef struct {
uint8_t val[BT_ADDR_SIZE];
} bt_addr_t;
/** Bluetooth LE Device Address */
typedef struct {
uint8_t type;
bt_addr_t a;
} bt_addr_le_t;
/** Bluetooth LE device "none" address, not a valid address */
#define BT_ADDR_LE_NONE ((bt_addr_le_t[]) { { 0, \
{ { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff } } } })
単純な配列ではなく、構造体になっているので少しややこしくなっていますが、本質的な部分は同じです。
##追記##
以前のバージョンでは、上記のように書かれていましたが、少なくとも最新のバージョン(3.3.0以降)では、以下のように変数参照になっています。メモリ効率が悪かったからでしょうか。
@tenmyoさんに教えていただきました。ありがとうございます。
extern const bt_addr_t bt_addr_any;
extern const bt_addr_t bt_addr_none;
extern const bt_addr_le_t bt_addr_le_any;
extern const bt_addr_le_t bt_addr_le_none;
#define BT_ADDR_ANY (&bt_addr_any)
#define BT_ADDR_NONE (&bt_addr_none)
#define BT_ADDR_LE_ANY (&bt_addr_le_any)
#define BT_ADDR_LE_NONE (&bt_addr_le_none)
最後に
誤っている点があれば、コメント欄で教えていただけますと幸いです。