【嵌入式】C語言高級編程-長度爲0的數組(05)

00. 目錄

01. 什麼是零長度數組

零長度數組就是長度爲0的數組。

ANSI C 標準規定:定義一個數組時,數組的長度必須是一個常數,即數組的長度在編譯的時候是確定的。在ANSI C 中定義一個數組的方法如下:

類型 數組名[數組元素個數];

int array[10];

C99 新標準規定:可以定義一個變長數組。

int len;
scanf("%d", &len);
int array[len];

也就是說,數組的長度在編譯時是未確定的,在程序運行的時候才確定,甚至可以由用戶指定大小。比如,我們可以定義一個數組,然後在程序運行時才指定這個數組的大小,還可以通過輸入數據來初始化數組。

程序示例

#include <stdio.h>

int main(void)
{
    int len;
    int i = 0;

    printf("please input a length: ");
    scanf("%d", &len);

    int a[len];

    for (i = 0; i < len; i++)
    {
        a[i] = i + 1;
    }

    for (i = 0; i < len; i++)
    {
        printf("a[%d] = %d\n", i, a[i]);
    }

    return 0;
}

執行結果

deng@itcast:~/tmp$ gcc 7.c  
deng@itcast:~/tmp$ ./a.out  
please input a length: 10
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4
a[4] = 5
a[5] = 6
a[6] = 7
a[7] = 8
a[8] = 9
a[9] = 10

在這個程序中,我們定義一個變量 len,作爲數組的長度。程序運行後,我們可以通過輸入指定數組的長度並初始化,最後再將數組的元素輸出來。

我們在程序中定義一個零長度數組,你會發現除了 GCC 編譯器,在其它編譯環境下可能就編譯通不過或者有警告信息。零長度數組的定義如下:

#include <stdio.h>


int main(void)
{
    //定義長度爲零的數組
    int a[0];

    return 0;
}

零長度數組有一個奇特的地方,就是它不佔用內存存儲空間。我們使用 sizeof 關鍵字來查看一下零長度數組在內存中所佔存儲空間的大小。

程序示例

#include <stdio.h>


int main(void)
{
    int a[0];

    printf("sizeof(a): %lu\n", sizeof(a));

    return 0;
}

執行結果

deng@itcast:~/tmp$ gcc 7.c  
deng@itcast:~/tmp$ ./a.out  
sizeof(a): 0

我們定義一個零長度數組,使用 sizeof 查看其大小可以看到:零長度數組在內存中不佔用空間,大小爲0。

零長度數組一般單獨使用的機會很少,它常常作爲結構體的一個成員,構成一個變長結構體

程序示例

#include <stdio.h>

struct student
{
    int id;
    char sex;
    int a[0];
};


int main(void)
{
    int a[0];

    printf("sizeof(struct): %lu\n", sizeof(struct student));

    return 0;
}

執行結果

deng@itcast:~/tmp$ gcc 7.c  
deng@itcast:~/tmp$ ./a.out  
sizeof(struct): 8

零長度數組在結構體中同樣不佔用存儲空間,所以 student結構體的大小爲8。

02. 零長度數組應用

零長度數組經常以變長結構體的形式,在某些特殊的應用場合,被程序員使用。在一個變長結構體中,零長度數組不佔用結構體的存儲空間,但是我們可以通過使用結構體的成員 a 去訪問內存,非常方便。

程序示例

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

struct student
{
    int id;
    char sex;
    char a[0];
};


int main(void)
{
    struct student *s = NULL;

    s = malloc(sizeof(struct student) + 20);
    if (NULL == s)
    {
        printf("malloc failed..\n");
        return 1;
    }
    memset(s, 0, sizeof(struct student) + 20);

    s->id = 1;
    s->sex = 'M';
    strcpy(s->a, "hello world");

    printf("id: %d sex: %c a: %s\n", s->id, s->sex, s->a);

    free(s);

    return 0;
}

執行結果

deng@itcast:~/tmp$ gcc 7.c  
deng@itcast:~/tmp$ ./a.out  
id: 1 sex: M a: hello world

在這個程序中,我們使用 malloc 申請一片內存,大小爲 sizeof(buffer) + 20,即28個字節大小。其中8個字節用來存儲結構體指針 student 指向的結構體類型變量,另外20個字節空間,纔是我們真正使用的內存空間。我們可以通過結構體成員 a,直接訪問這片內存。

通過這種靈活的動態內存申請方式,這個 student結構體表示的一片內存緩衝區,就可以隨時調整,可大可小。這個特性,在一些場合非常有用。比如,現在很多在線視頻網站,都支持多種格式的視頻播放:普清、高清、超清、1080P、藍光甚至4K。如果我們本地程序需要在內存中申請一個 buffer 用來緩存解碼後的視頻數據,那麼,不同的播放格式,需要的 buffer 大小是不一樣的。如果我們按照 4K 的標準去申請內存,那麼當播放普清視頻時,就用不了這麼大的緩衝區,白白浪費內存。而使用變長結構體,我們就可以根據用戶的播放格式設置,靈活地申請不同大小的 buffer,大大節省了內存空間。

03. 內核中的零長度數組

零長度數組在內核中,一般以變長結構體的形式使用。今天我們就分析一下 Linux 內核中的 USB 驅動。在網卡驅動中,大家可能都比較熟悉一個名字:套接字緩衝區,即 socket buffer,用來傳輸網絡數據包。同樣,在 USB 驅動中,也有一個類似的東西,叫 URB,其全名爲 USB request block,即 USB 請求塊,用來傳輸 USB 數據包。

linux-headers-5.4.0-33/include/linux/usb.h

struct urb {
    /* private: usb core and host controller only fields in the urb */
    struct kref kref;       /* reference count of the URB */
    int unlinked;           /* unlink error code */
    void *hcpriv;           /* private data for host controller */
    atomic_t use_count;     /* concurrent submissions counter */
    atomic_t reject;        /* submissions will fail */

    /* public: documented fields in the urb that can be used by drivers */
    struct list_head urb_list;  /* list head for use by the urb's
                     * current owner */
    struct list_head anchor_list;   /* the URB may be anchored */
    struct usb_anchor *anchor;
    struct usb_device *dev;     /* (in) pointer to associated device */
    struct usb_host_endpoint *ep;   /* (internal) pointer to endpoint */
    unsigned int pipe;      /* (in) pipe information */
    unsigned int stream_id;     /* (in) stream ID */
    int status;         /* (return) non-ISO status */
    unsigned int transfer_flags;    /* (in) URB_SHORT_NOT_OK | ...*/
    void *transfer_buffer;      /* (in) associated data buffer */
    dma_addr_t transfer_dma;    /* (in) dma addr for transfer_buffer */
    struct scatterlist *sg;     /* (in) scatter gather buffer list */
    int num_mapped_sgs;     /* (internal) mapped sg entries */
    int num_sgs;            /* (in) number of entries in the sg list */
    u32 transfer_buffer_length; /* (in) data buffer length */
    u32 actual_length;      /* (return) actual transfer length */
    unsigned char *setup_packet;    /* (in) setup packet (control only) */
    dma_addr_t setup_dma;       /* (in) dma addr for setup_packet */
    int start_frame;        /* (modify) start frame (ISO) */
    int number_of_packets;      /* (in) number of ISO packets */
    int interval;           /* (modify) transfer interval
                     * (INT/ISO) */
    int error_count;        /* (return) number of ISO errors */
    void *context;          /* (in) context for completion */
    usb_complete_t complete;    /* (in) completion routine */
    struct usb_iso_packet_descriptor iso_frame_desc[0];
                    /* (in) ISO ONLY */
};

在這個結構體內定義了 USB 數據包的傳輸方向、傳輸地址、傳輸大小、傳輸模式等。這些細節我們不深究,我們只看最後一個成員:

struct usb_iso_packet_descriptor iso_frame_desc[0];

在 URB 結構體的最後,定義一個零長度數組,主要用於 USB 的同步傳輸。USB 有4種傳輸模式:中斷傳輸、控制傳輸、批量傳輸和同步傳輸。不同的 USB 設備對傳輸速度、傳輸數據安全性的要求不同,所採用的傳輸模式是不同的。USB 攝像頭對視頻或圖像的傳輸實時性要求較高,對數據的丟幀不是很在意,丟一幀無所謂 ,接着往下傳。所以 USB 攝像頭採用的是 USB 同步傳輸模式。

現在淘寶上的 USB 攝像頭,打開它的說明書,一般會支持多種分辨率:從16*16到高清720P多種格式。不同分辨率的視頻傳輸,對於一幀圖像數據,對 USB 的傳輸數據包的大小和個數需求是不一樣的。那USB到底該如何設計,去適配這種不同大小的數據傳輸要求,但又不影響 USB 的其它傳輸模式呢?答案就在結構體內的這個零長度數組上。

當用戶設置不同的分辨率傳輸視頻,USB 就需要使用不同大小和個數的數據包來傳輸一幀視頻數據。通過零長度數組構成的這個變長結構體就可以滿足這個要求。可以根據一幀圖像數據的大小,靈活地去申請內存空間,滿足不同大小的數據傳輸。但這個零長度數組又不佔用結構體的存儲空間,當 USB 使用其它模式傳輸時,不受任何影響,完全可以當這個零長度數組不存在。所以,不得不說,這樣的設計真是妙!

04. 指針可以代替零長度數組?

大家在各種場合,可能常常會看到這樣的字眼:數組名在作爲函數參數進行參數傳遞時,就相當於是一個指針。在這裏,我們千萬別被這句話迷惑了:數組名在作爲函數參數傳遞時,確實傳遞的是一個地址,但數組名絕不是指針,兩者不是同一個東西。數組名用來表徵一塊連續內存存儲空間的地址,而指針是一個變量,編譯器要給它單獨再分配一個內存空間,用來存放它指向的變量的地址。我們看下面這個程序。
程序示例

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

struct s1
{
    int len;
    int a[0];
};

struct s2
{
    int len;
    int *a;
};

int main(void)
{
    printf("sizeof(s1): %lu\n", sizeof(struct s1));
    printf("sizeof(s2): %lu\n", sizeof(struct s2));

    return 0;
}

執行結果

deng@itcast:~/tmp$ ./a.out  
sizeof(s1): 4
sizeof(s2): 16

對於一個指針變量,編譯器要爲這個指針變量單獨分配一個存儲空間,然後在這個存儲空間上存放另一個變量的地址,我們就說這個指針指向這個變量。而數組名,編譯器不會再給其分配一個存儲空間的,它僅僅是一個符號,跟函數名一樣,用來表示一個地址。

程序示例

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


int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

int b[0];

int *p = &a[5];

int main(void)
{

    return 0;
}

在這個程序中,我們分別定義一個普通數組、一個零長度數組和一個指針變量。其中這個指針變量 p 的值爲 a[5] 這個數組元素的地址,也就是說指針 p 指向 a[5]。我們接着對這個程序使用 arm 交叉編譯器進行編譯,並進行反彙編。

deng@itcast:~/tmp$ arm-linux-gcc 7.c -o a.out
deng@itcast:~/tmp$ arm-linux-objdump -D a.out > a.dis
deng@itcast:~/tmp$ 

從反彙編生成的彙編代碼中,我們找到 array1 和指針變量 p 的彙編代碼。


00011024 <a>:
   11024:   00000001    andeq   r0, r0, r1
   11028:   00000002    andeq   r0, r0, r2
   1102c:   00000003    andeq   r0, r0, r3
   11030:   00000004    andeq   r0, r0, r4
   11034:   00000005    andeq   r0, r0, r5
   11038:   00000006    andeq   r0, r0, r6
   1103c:   00000007    andeq   r0, r0, r7
   11040:   00000008    andeq   r0, r0, r8
   11044:   00000009    andeq   r0, r0, r9
   11048:   00000000    andeq   r0, r0, r0

0001104c <p>:
   1104c:   00011038    andeq   r1, r1, r8, lsr r0

Disassembly of section .bss:

從彙編代碼中,可以看到,對於長度爲10的數組 a[10],編譯器給它分配了從 0x11024–0x11048 一共40個字節的存儲空間,但並沒有給數組名 a單獨分配存儲空間,數組名 a僅僅表示這40個連續存儲空間的首地址,即數組元素 a[0] 的地址。而對於 a[0] 這個零長度數組,編譯器並沒有給它分配存儲空間,此時的 a僅僅是一個符號,用來表示內存中的某個地址,我們可以通過查看可執行文件 a.out 的符號表來找到這個地址值。

    78: 000082d4     0 FUNC    GLOBAL DEFAULT   12 _start
    79: 000082bc     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
    80: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    81: 00000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
    82: 00008408     0 FUNC    GLOBAL DEFAULT   13 _fini
    83: 0001104c     4 OBJECT  GLOBAL DEFAULT   22 p
    84: 00008410     4 OBJECT  GLOBAL DEFAULT   14 _IO_stdin_used
    85: 0001101c     0 NOTYPE  GLOBAL DEFAULT   22 __data_start
    86: 00011050     0 NOTYPE  GLOBAL DEFAULT  ABS __bss_start__
    87: 0000841c     0 NOTYPE  GLOBAL DEFAULT  ABS __exidx_end
    88: 00011024    40 OBJECT  GLOBAL DEFAULT   22 a
    89: 00011020     0 OBJECT  GLOBAL HIDDEN    22 __dso_handle
    90: 00011054     0 NOTYPE  GLOBAL DEFAULT  ABS __end__
    91: 0000839c   104 FUNC    GLOBAL DEFAULT   12 __libc_csu_init
    92: 00011054     0 NOTYPE  GLOBAL DEFAULT  ABS __bss_end__
    93: 00011050     0 NOTYPE  GLOBAL DEFAULT  ABS __bss_start
    94: 00011054     0 NOTYPE  GLOBAL DEFAULT  ABS _bss_end__
    95: 00011054     0 OBJECT  GLOBAL DEFAULT   23 b
    96: 00011054     0 NOTYPE  GLOBAL DEFAULT  ABS _end
    97: 00011050     0 NOTYPE  GLOBAL DEFAULT  ABS _edata
    98: 00008414     0 NOTYPE  GLOBAL DEFAULT  ABS __exidx_start
    99: 00008380    28 FUNC    GLOBAL DEFAULT   12 main
   100: 00008290     0 FUNC    GLOBAL DEFAULT   10 _init

從符號表裏可以看到,b 的地址爲 0x11054,在程序 bss 段的後面。b符號表示的默認地址是一片未使用的內存空間,僅此而已,編譯器絕不會單獨再給其分配一個內存空間來存儲數組名。看到這裏,也許你就明白了:數組名和指針並不是一回事,數組名雖然在作爲函數參數時,可以當一個地址使用,但是兩者不能劃等號。菜刀有時候可以當武器用,但是你不能說菜刀就是武器。

至於爲什麼不用指針,很簡單。使用指針的話,指針本身也會佔用存儲空間不說,根據上面的 USB 驅動的案例分析,你會發現,它遠遠沒有零長度數組用得巧妙——不會對結構體定義造成冗餘,而且使用起來也很方便。

05. 附錄

參考:C語言嵌入式Linux高級編程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章