登堂入室C++之数组

数组简介

数组是我们编程中经常遇到的一个类型。多维数组是如何在内存中存储的呢?数组名到底代表什么呢?

数组是相同类型数据的一个集合,在内存中连续存储

数组可以有一维、二维、三维甚至更多维。多维的概念是存在于C/C++语言层面,对于编译后的汇编和二进制,只有一维的概念。

一维数组

一维数组使用如下形式进行定义:

data_type array_name[array_size]

比如

int scores[4]; 

定义一个数组,该数组的每个元素的类型的是int, 该数组一共有4个元素。

当然了,我们也可以在定义的时候给定初始值

int scores[4] = {90, 95, 99, 89};

这时候四个值在内存中的排布为:

1d-array.png

那如果我们没有给定足够的值呢?比如

int scores[4] = {90, 95};

这时候后面两个元素的值是啥?

其实这时候,后面两个值会被填充0,得到的值为

1d-array-pad.png

我们可以写一个简单的小程序分析一下

// maian.cpp
int main()
{
    int scores[4] = {90, 95};
    return 0;
}

然后用clang++ -S main.cpp生成汇编,我们看一下生成的汇编

1d-array-main.png

这里我们不会详细介绍x86汇编,只介绍一点我们需要用到的汇编的。

moveq src, dst 

也就是x86汇编里面,源在前面,目标在后面。

a(%register)

表示register的值加上a这个偏移量指向的地址。所以上面

movq %rax, -24(%rbp)

相当于*(rbp - 24) = rax

如果源是常数,那么直接使用一个符号加上这个常数就可以了,比如```90```就表示常数90。

如果我们把数组的大小变大一点,比如下面的代码

int main()
{
    int scores[40] = {90, 95};
    return 0;
}

反汇编我们可以看到

1d-array-main-2.jpg

那也许有的人会问,是不是默认就会清零?我们也可以验证一下,把代码修改为

int main()
{
    int scores[40];
    return 0;
}

反汇编我们可以看到

1d-array-main-3.jpg

从汇编代码我们可以看到是完全没有默认清零的。

最后值得一提的是,如果我们有初始值,也可以不指定数组大小,系统会根据初始值计算出大小。

int main()
{
    int scores[] = {95,98,99,100};
    return 0;
}

系统会自动根据初始值推断出scores数组大小为4。

二维和多维数组

二维数组可如如下定义

data_type array_name[size_1][size_2];

比如

int scores[2][4];

三维以及多维数组的定义类似

data_type array_name[size_1][size_2]...[size_n];

比如三维数组可以定义为

int scores[2][3][4];

跟一维数组一样,多维数组也可以在定义的时候给定初值

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

它在内存中的排布为

2d-array.png

我们给定下面一段代码

int main()
{
    int scores[2][2][3] = {
        {{1,2,3},{4,5,6}},
        {{7,8,9},{10,11,12}}
      };
    scores[1][1][0] =  95;
    
    return 0;
}

反汇编的代码如下

2d-array-as-1d.jpg

从上面反汇编我们可以很容易看出,编译之后并没有多维的概念,都是一维的,多维的概念只存在于语言层面。但是,这并不影响我们按照概念使用。从上面的汇编更多可以容易看出多维数组的值是怎么排布的。

可变大小的数组

可变大小数组也被称为运行时大小数组。不同于在定义的时候指定常量大小,可变大小数组可以根据参数来定义大小。

比如

void f(int size)
{
   int scores[size];
   ...
}

这样我们就可以在运行时根据需要传入需要的大小来分配数组。

可变大小数组是C99标准里面的,所以C语言程序如果标准设为C99以及以上是一定可以使用的。但是C++11之前是一定不支持可变大小数组的,C++11中提及数组大小的时候也说size是一个常量表达式,那么也就说明C++11从标准里面也是不支持可变大小数组的。但是GCC和Clang提供了扩展来支持可变大小数组,所以上面的代码使用GCC和Clang是可以编译的。

这里面就有两个问题需要我们注意:

  1. 可变参数数组是一个很好的特性,但是如果我们想要代码可移植性强,最好不要在C++中使用它。当然了,纯C程序可以放心使用;
  2. C++并不是C的严格超集,C中的一些东西在C++中是不能使用的。

数组元素的访问

常见的用数组名加上下标访问的方式我们已经见过了,就是类似

int a[3]  ={};
a[2] = 10;

但是在有的地方我们可以看见如下写法

2[a] = 11

这种写法是很不常见的,只是作为一种语法进行说明。更多的是a[-1]这种写法,其中a是一个指针。这里不详细说明,后面在聊指针和内存的时候会详细说明。

我们有下面一段代码

int main()
{
    int a[3] = {};
    a[2] = 10;
    1[a] = 11;
    
    return 0;
}

这段代码是完全可以编译和运行的,在执行到return 0;的时候,a[0]=0, a[1]=11, a[2]=10。从反汇编我们也可以看一下

array-ele-access.jpg

我们可以看到无论是a[2]还是1[a]最后都是基指(也就是rbp寄存器的值)加上一个偏移量。其实t[b]可以认为都是*(t+b),所以t和b哪一个是数组名,哪一个是偏移量并没有关系。

数组类型

对一个多维数组,每一维的类型到底是什么

为了解答这个问题,我们首先写一个打印类型的工具函数

template <typename T>
void print_type()
{
#if _MSC_VER
    const char *sig = __FUNCSIG__;
    printf("%s\n", sig);
    return;
#else
    constexpr int skip_begin = 23;
    constexpr int skip_end = 1;
    const char* sig = __PRETTY_FUNCTION__;
#endif
    char *result = new char[strlen(sig) - skip_begin - skip_end + 1]{};
    memcpy(result, sig+skip_begin, strlen(sig) - skip_begin - skip_end);

    printf("%s\n", result);
}

这个函数主要使用函数的签名中含有参数名的原理来提取我们的类型名,比如print_type<int>()可以得到

void print_type() [T = int]

这里面是不是就有类型int名在里面,我们去掉int前面的字符串和后面的]就可以得到最终的类型。这就是上面函数的原理。

手上没有Visual Studio,所以Windows版本并没有调试skip_begin和skip_end的值,需要的人可以自行调试。Clang版本是测试过的,值为上面的23和1,可以直接使用。

我们现在可以写个小程序打印不同维度的类型:

int main()
{
    int scores[2][3][4];
    
    print_type<decltype(&scores)>();
    print_type<decltype(scores)>();
    print_type<decltype(scores[0])>();
    print_type<decltype(scores[0][0])>();
    print_type<decltype(scores[0][0][0])>();
    
    return 0;
}

可以得到如下输出

int (*)[2][3][4]
int[2][3][4]
int (&)[3][4]
int (&)[4]
int &

也就是说

  • 对数组名取地址,得到的是一个指向数组类型的指针类型;
  • 数组的类型就是数组定义中去掉数组名之后得到的类型;
  • 数组的第一维元素是第二维和第三维组成的数组;
  • 数组的第二维元素是一个一维数组;
  • 数组的第三维元素是一个整形。

另外,数组的第一维、第二维、第三维都是常规的引用类型,所以在类型里面都有&。

这里有一个有意思的小问题就跟类型息息相关:v的地址假设是a,那么v+1的地址用a表示是多少?

比如我们假设&scores的地址值是a,那么&scores+1的地址值是多少?

首先我们需要知道,对于地址的加减法,v+1中1的量纲是sizeof(*v)。也就是1的大小用字节表示是v所指对象的大小。

&scores+1中的1用字节表示就是
\begin{aligned} sizeof(*\&scores)&=sizeof(scores)\\ &=2\times 3\times 4\times sizeof(int) \\ &= 96 \end{aligned}
所以&scores+1的地址用a表示就是a+96

同理我们可以计算scores + 1&scores[0] + 1, &scores[0][0]+1, &scores[0][0][0]+1的地址的值。

复合字面量

经常我们都会提到C++是C的超集,C中可以用的C++都可以。但是今天我们就可以遇到第二个在两门语言中表现不一样的:复合字面量。

我们上面的多维数组是规则的,那我们如果想要一个二维数组,它的每个维度的大小不一样,是可以实现的吗?

答案是当然可以,而且方法不止一种。

第一种常规的就是数组的成员是指针,然后让指针指向不同大小的内存;

第二种方法就是使用复合字面量。那什么事复合字面量呢?这也是一个C99标准里面的东西。简单来说

(int [2]){1,2}; 

就是一个复合字面量。

我们可以使用复合字面量得到一个大小不同的数组

int main()
{
    int (*scores[]) = {
        (int[]){1,2},
        (int[]){3,4,5},
        (int[]){6, 7, 8, 9}
    };
    
    return 0;
}

这部分代码,在Xcode中使用main.c是这样的

var-array-c.jpg

在main.cpp就成了

var-array-cpp.jpg

本质上这是一个C99的标准而不是一个C++的标准。在C++中的时候也需要谨慎谨慎再谨慎。

数组的分享就到这里了,还有一部分,比如数组名是左值还是右值,需要分享了对应的概念之后我们再回头来讨论。

更多文章及时发布在公众号“探知轩”,欢迎关注更及时看到文章。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容