長さゼロの配列とは
名前が示すように、長さゼロの配列は長さがゼロの配列です。
ANSI C標準では、配列を定義する場合、配列の長さは定数でなければなりません。つまり、配列の長さはコンパイル時に決定されます。ANSI Cで配列を定義する方法は次のとおりです。
int a[10];
新しいC99標準では、可変長配列を定義できると規定されています。
int len;
int a[len];
つまり、配列の長さはコンパイル時に決定されず、プログラムの実行時にのみ決定され、サイズもユーザーが指定できます。たとえば、配列を定義し、プログラムの実行時に配列のサイズを指定したり、データを入力して配列を初期化したりできます。サンプルコードは次のとおりです。
int main(void)
{
int len;
printf("input array len:");
scanf("%d",&len);
int a[len];
for(int i=0;i<len;i++)
{
printf("a[%d]= ",i);
scanf("%d",&a[i]);
}
printf("a array print:\n");
for(int i=0;i<len;i++)
printf("a[%d] = %d\n",i,a[i]);
return 0;
}
このプログラムでは、配列の長さとして変数lenを定義します。プログラムの実行後、入力によって配列の長さを指定して初期化し、最後に配列の要素を出力できます。プログラムの実行結果は次のとおりです。
input array len:3
a[0]= 6
a[1]= 7
a[2]= 8
a array print:
a[0] = 6
a[1] = 7
a[2] = 8
GNU Cは可変長配列は面白くないと思うかもしれません。別の本当のハンマーを考えてみましょう:長さゼロの配列をサポートします。私ほど情け容赦がないコンパイラは他にありません!はい、プログラムで長さ0の配列を定義すると、GCCコンパイラに加えて、他のコンパイル環境ではコンパイルされないか、警告メッセージが表示されることがあります。長さゼロの配列の定義は次のとおりです。
int a[0];
長さがゼロの配列の奇妙な点は、メモリの記憶領域を消費しないことです。sizeofキーワードを使用して、メモリ内で長さゼロの配列が占めるストレージスペースのサイズをチェックします。コードは次のとおりです。
int buffer[0];
int main(void)
{
printf("%d\n", sizeof(buffer));
return 0;
}
このプログラムでは、長さ0の配列を定義し、sizeofを使用してそのサイズを表示します。長さ0の配列はメモリ内のスペースを占有せず、サイズは0です。
通常、長さゼロの配列は単独で使用される機会がほとんどなく、可変長構造体を形成するための構造体のメンバーとしてよく使用されます。
struct buffer{
int len;
int a[0];
};
int main(void)
{
printf("%d\n",sizeof(struct buffer));
return 0;
}
長さ0の配列は、構造内の記憶域も占有しないため、バッファー構造のサイズは4です。
長さゼロの配列の使用例
長さゼロの配列は可変長構造体の形式であることが多く、一部の特別なアプリケーションでプログラマーが使用します。可変長構造体では、長さ0の配列は構造体の記憶域を占有しませんが、構造体のメンバーaを使用してメモリにアクセスできるため、非常に便利です。可変長構造体の使用例は次のとおりです。
struct buffer{
int len;
int a[0];
};
int main(void)
{
struct buffer *buf;
buf = (struct buffer *)malloc \
(sizeof(struct buffer)+ 20);
buf->len = 20;
strcpy(buf->a, "hello wanglitao!\n");
puts(buf->a);
free(buf);
return 0;
}
このプログラムでは、メモリの一部に適用するためにmallocを使用します。サイズはsizeof(バッファ)+ 20で、サイズは24バイトです。このうち、4バイトは構造体ポインタbufが指す構造体型変数の格納に使用され、残りの20バイトは実際に使用するメモリ空間です。構造体のメンバーaを介してこのメモリに直接アクセスできます。
この柔軟な動的メモリの適用方法により、このバッファ構造で表されるメモリバッファはいつでも調整でき、大きくしたり小さくしたりできます。この機能は、場合によっては非常に役立ちます。たとえば、現在、多くのオンラインビデオサイトが、一般的なクリア、高解像度、ウルトラクリア、1080P、ブルーレイ、さらには4Kの複数の形式でのビデオ再生をサポートしています。ローカルプログラムが、デコードされたビデオデータをバッファするためにメモリ内のバッファを適用する必要がある場合、異なる再生フォーマットに必要なバッファサイズは異なります。4K規格に従ってメモリを申請すると、一般精細度ビデオを再生するときに、それほど大きなバッファは必要なくなり、メモリを無駄に浪費することになります。可変長構造を使用して、ユーザーの再生フォーマット設定に応じてさまざまなサイズのバッファーを柔軟に適用できるため、メモリスペースを大幅に節約できます。
カーネルでの長さがゼロの配列の使用
長さゼロの配列は通常、カーネルでは可変長構造体の形式で使用されます。今日は、LinuxカーネルのUSBドライバーを分析します。ネットワークカードドライバーでは、誰もが名前を知っているかもしれません:ソケットバッファー、つまり、ネットワークデータパケットの送信に使用されるソケットバッファー。同様に、USBドライバーにはURBと呼ばれる同様のものがあり、そのフルネームはUSBリクエストブロック、つまりUSBデータパケットの転送に使用されるUSBリクエストブロックです。
struct urb {
struct kref kref;
void *hcpriv;
atomic_t use_count;
atomic_t reject;
int unlinked;
struct list_head urb_list;
struct list_head anchor_list;
struct usb_anchor *anchor;
struct usb_device *dev;
struct usb_host_endpoint *ep;
unsigned int pipe;
unsigned int stream_id;
int status;
unsigned int transfer_flags;
void *transfer_buffer;
dma_addr_t transfer_dma;
struct scatterlist *sg;
int num_mapped_sgs;
int num_sgs;
u32 transfer_buffer_length;
u32 actual_length;
unsigned char *setup_packet;
dma_addr_t setup_dma;
int start_frame;
int number_of_packets;
int interval;
int error_count;
void *context;
usb_complete_t complete;
struct usb_iso_packet_descriptor iso_frame_desc[0];
};
この構造では、USBデータパケットの送信方向、送信アドレス、送信サイズ、送信モードが定義されています。これらの詳細については掘り下げず、最後のメンバーのみを調べます。
struct usb_iso_packet_descriptor iso_frame_desc[0];
URB構造の最後に、主にUSB同期伝送に使用される長さ0の配列を定義します。USBには4つの送信モードがあります。割り込み送信、制御送信、バッチ送信、同期送信です。USBデバイスごとに、転送速度と転送データのセキュリティに関する要件が異なり、使用される転送モードも異なります。USBカメラは、ビデオまたは画像のリアルタイム伝送に対する高い要件を備えており、データのフレーム損失を気にしません。フレームが失われたかどうかは関係なく、ダウンロードされます。そのため、USBカメラはUSB同期送信モードを採用しています。
現在、TaobaoのUSBカメラは、そのマニュアルを開き、一般的に複数の解像度をサポートしています。16* 16からHD 720Pまでの複数のフォーマット。異なる解像度のビデオ送信の場合、画像データのフレームでは、USB送信データパケットのサイズと数が異なります。さまざまなサイズのデータ転送要件に対応するようにUSBを設計する必要がありますが、他のUSB転送モードには影響しませんか?答えは、構造内のこの長さゼロの配列にあります。
ユーザーがビデオを送信するために異なる解像度を設定する場合、USBは1フレームのビデオデータを送信するために異なるサイズと数のデータパケットを使用する必要があります。長さゼロのアレイによって形成されるこの可変長構造は、この要件を満たすことができます。画像データのフレームのサイズに応じて、さまざまなサイズのデータ転送に対応するためのメモリ空間を柔軟に適用できます。ただし、この長さゼロの配列は構造のストレージスペースを占有しません。USBが他のモードで送信を使用する場合、影響はなく、この長さゼロの配列が存在しない可能性があります。だから、そのようなデザインは本当に素晴らしいと言わざるを得ません!
考える:長さゼロの配列の代わりにポインタを使用しないのはなぜですか?
さまざまな場面で、このような単語がよく見られることがあります。配列名が関数パラメーターとして渡される場合、それはポインターに相当します。ここで、この文と混同しないでください。配列名が関数パラメーターとして渡される場合、実際にはアドレスが渡されますが、配列名は決してポインターではなく、2つは同じものではありません。配列名は、連続メモリストレージスペースのアドレスを特徴付けるために使用され、ポインタは変数です。コンパイラが指す変数のアドレスを格納するには、別のメモリスペースを割り当てる必要があります。以下のこのプログラムを見てみましょう。
struct buffer1{
int len;
int a[0];
};
struct buffer2{
int len;
int *a;
};
int main(void)
{
printf("buffer1: %d\n", sizeof(struct buffer1));
printf("buffer2: %d\n", sizeof(struct buffer2));
return 0;
}
結果は次のとおりです。
buffer1:4
buffer2:8
ポインター変数の場合、コンパイラーはポインター変数用のストレージスペースを個別に割り当ててから、このストレージスペースに別の変数のアドレスを格納する必要があります。ポインターがこの変数を指すと言います。配列名。コンパイラーはそのための記憶域を割り当てません。これは、アドレスを表すために使用される、関数名のような単なる記号です。次に、別のプログラムを見てみましょう。
//hello.c
int array1[10] ={1,2,3,4,5,6,7,8,9};
int array2[0];
int *p = &array1[5];
int main(void)
{
return 0;
}
このプログラムでは、通常の配列、長さゼロの配列、およびポインター変数を定義します。このポインター変数pの値は、配列要素array1 [5]のアドレスです。これは、ポインターpがarraay1 [5]を指すことを意味します。次に、armクロスコンパイラを使用して、このプログラムをコンパイルおよび逆アセンブルします。
$ arm-linux-gnueabi-gcc hello.c -o a.out
$ arm-linux-gnueabi-objdump -D a.out
逆アセンブリによって生成されたアセンブリコードから、array1のアセンブリコードとポインタ変数pを見つけます。
00021024 <array1>:
21024: 00000001 andeq r0, r0, r1
21028: 00000002 andeq r0, r0, r2
2102c: 00000003 andeq r0, r0, r3
21030: 00000004 andeq r0, r0, r4
21034: 00000005 andeq r0, r0, r5
21038: 00000006 andeq r0, r0, r6
2103c: 00000007 andeq r0, r0, r7
21040: 00000008 andeq r0, r0, r8
21044: 00000009 andeq r0, r0, r9
21048: 00000000 andeq r0, r0, r0
0002104c <p>:
2104c: 00021038 andeq r1, r2, r8, lsr r0
Disassembly of section .bss:
00021050 <__bss_start>:
21050: 00000000 andeq r0, r0, r0
アセンブリコードから、長さ10の配列array1 [10]の場合、コンパイラーは0x21024--0x21048から40バイトの記憶域を割り当てましたが、配列名array1に個別に記憶域を割り当てていません。スペース、配列名array1は、40個の連続したストレージスペースの最初のアドレス、つまり配列要素array1 [0]のアドレスのみを表します。長さ0の配列であるarray2 [0]の場合、コンパイラーはストレージスペースを割り当てません。現時点では、array2はメモリ内のアドレスを表すために使用されるシンボルにすぎません。実行可能ファイルa.outを確認できますこのアドレス値を見つけるため。
$ readelf -s a.out
88: 00021024 40 OBJECT GLOBAL DEFAULT 23 array1
89: 00021054 0 NOTYPE GLOBAL DEFAULT 24 _bss_end__
90: 00021050 0 NOTYPE GLOBAL DEFAULT 23 _edata
91: 0002104c 4 OBJECT GLOBAL DEFAULT 23 p
92: 00010480 0 FUNC GLOBAL DEFAULT 14 _fini
93: 00021054 0 NOTYPE GLOBAL DEFAULT 24 __bss_end__
94: 0002101c 0 NOTYPE GLOBAL DEFAULT 23 __data_start_
96: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
97: 00021020 0 OBJECT GLOBAL HIDDEN 23 __dso_handle
98: 00010488 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used
99: 0001041c 96 FUNC GLOBAL DEFAULT 13 __libc_csu_init
100: 00021054 0 OBJECT GLOBAL DEFAULT 24 array2
101: 00021054 0 NOTYPE GLOBAL DEFAULT 24 _end
102: 000102d8 0 FUNC GLOBAL DEFAULT 13 _start
103: 00021054 0 NOTYPE GLOBAL DEFAULT 24 __end__
104: 00021050 0 NOTYPE GLOBAL DEFAULT 24 __bss_start
105: 00010400 28 FUNC GLOBAL DEFAULT 13 main
107: 00021050 0 OBJECT GLOBAL HIDDEN 23 __TMC_END__
110: 00010294 0 FUNC GLOBAL DEFAULT 11 _init
シンボルテーブルからわかるように、array2のアドレスは0x21054で、プログラムのbssセクションの後ろにあります。array2シンボルで示されるデフォルトのアドレスは、未使用のメモリ空間であり、コンパイラは配列名を格納するためのメモリ空間を割り当てません。これを見ると、配列名とポインタが同じではないことがわかり、関数名として使用する場合、配列名をアドレスとして使用することはできますが、同じにすることはできません。チョッパーは武器として使用できることもありますが、チョッパーが武器であるとは言えません。
なぜポインタを使用しないのかについては、それは非常に簡単です。ポインターを使用する場合、ポインター自体もストレージ領域を占有します。言うまでもなく、上記のUSBドライバーのケース分析によると、それは長さ0のアレイよりもはるかに賢くないことがわかります-構造定義に冗長性を引き起こしません。使い方もとても便利です。