C/C++ compound type array

数组

数组和vector类似,但是数组是定长的,大小不变,而vector是可变的,可以把vector看成可变长度的数组。
如果不清楚元素的确切个数,使用vector。

定义和初始化

数组的维度必须是确定的,在编译时就知道,即是一个常量表达式(值不会改变并且在编译时就已知)。
默认情况下,数组的元素被默认初始化。和内置类型的变量一样,如果在数组内部定义了某种内置类型的数组,那么默认初始化得到的数组含有未定义的值。

显式的初始化数组

  1. 可以忽略维度,编译器会根据初始值推测出来;
  2. 如果指明维度,初始值的数量不应该超出指定的大小;
  3. 如果维度比提供的初始值大,其它的元素被初始化成默认值(值初始化)。

数组不支持直接拷贝和直接赋值

不能将数组或的内容拷贝给其他数组作为初始值,也不能用数组为其他数组赋值。

数组和vector

不允许使用一个数组为另一个数组赋初值,也不允许使用vector初始化数组。但是支持使用数组来初始化vector

1
2
int arr[] = {1, 2, 3};
vector<int> vi(begin(arr), end(arr));

复杂数组的声明

对于数组声明的理解,从变量名开始,先往右,再往左读。比如:

1
2
3
4
5
6
int arr[10];    //数组arr,有10个元素,每个元素是int类型
int *ptrs[10]; //数组ptrs,有10个int*类型元素,
int &refs[10]; //错误
int (*Parray)[10] = &arr; //数组的指针Parray,指向一个有10个int类型的数组
int (&arrRef)[10] = arr; //数组的引用arrRef,引用一个10个int类型的数组
int *(&array)[10] = ptrs; //数组的引用array,引用一个10个int*类型的数组

访问数组元素

  1. 范围for 语句。
  2. 下标运算符。下标的类型是size_t类型。它是一种机器相关的无符号类型,足够大能够表示内存中任意对象的大小。定义在<stddef.c>或者<cstddef>头文件中。

数组和指针

数组名和指针

  1. 一般情况下,可以使用取地址符获取某个对象的指针。数组元素也是对象,使用下标运算符得到数组指定位置的对象,使用取地址符就能得到指向该元素的指针。([1]3.5.3)
  2. 数组的另一个特性是,在大多用到数组名字的时候,编译器都会自动的将它转换成一个指向数组首元素的指针,而且数组名是一个常量指针([1]3.5.3)。或者说数组作为右值时,编译器会自动将数组转化成数组首元素的地址([2)]。
  3. 在C中,只有两种操作不把数组名字当做常量指针,当数组名字用作sizeof&的操作对象时。当sizeof的输入是数组名时,返回的是数组的长度(字节为单位)。而&对数组名取地址时,返回的是指向数组的指针。

C++复合类型指针中介绍了指针的算术运算。指针的算术运算有一个要求,就是指针必须指向数组中某一个元素。如下所示:

1
2
3
4
5
6
7
8
const size_t sz = 5;
int arr[sz] = {1, 2, 3, 4, 5};
int *ip;
ip = arr; //这个不是数组的赋值。事实上,这是指针的赋值。
int *ip2 = ip+4;

int *p = arr + sz;
int *p2 = arr + 10;

上述代码中,给arr加上sz时,编译器自动的将它转换成指向数组arr中首元素的指针。执行加法操作,其实就是指针的算术运算,最后指向arr的第五个元素之后的位置。如果超出了这个位置,就会出错,编译器不会检查这种错误。

下标引用

  1. 对于内置数组来说,执行下标运算,相当于先将数组转换为首元素的指针,然后执行数组的算术运算。如下所示:
    1
    2
    3
    4
    5
    int ia[] = {0, 2, 4, 6, 8};
    int i = ia[2]; //这行代码其实相当于以下两行代码

    int *p = ia;
    i = *(p+2);

除了优先级外,下标运算和间接访问(解引用)完全一样。

  1. 而且只要指针指向的是数组中的元素或者数组中尾元素的下一个位置,都可以执行下标操作。当指针指向数组尾后元素时,该位置不可访问!但是可以访问其他位置。

    1
    2
    3
    int *p = &ia[2];
    int j = p[1];
    int k = p[-2];

  2. 和标准库stringvector下标操作的不同之处在于,内置数组的下标操作中,,下标可以是负的,而标准string,vector中下标必须是无符号类型。

数组!=指针

需要注意的是,数组和指针并不是等价的!考虑以下代码:

1
2
int a[5];
int *b;

ab不能互换使用,即使它们都可以使用指针运算,可以进行间接访问和下标引用操作。它们并不一样。
定义一个数组时,编译器会根据声明指定的元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量,指向这段空间的起始位置。定义一个指针变量时,编译器只为指针本身保留内存空间。所以,进行了上述定义以后,*a合法,而*b不合法。

C++中的数组和指针

  1. 使用decltype关键字对数组进行解析时,不会将数组转换成指针,它会将识别出数组类型。使用auto分析数组类型时会推断出指针类型。
  2. 指针也是迭代器。通过数组名字或者数组首元素地址都可以得到指向数组第一个元素的指针。
  3. beginend获取数组首元素指针和尾后指针。尾后指针不能执行解引用和递增操作。

C风格字符串

C风格字符串不是一种类型,而是一种约定俗成的写法。按照约定,C风格字符串存放在数组中,并且以空字符'\0'结束。关于C风格字符串更多的内容可以查看

字符数组

  1. 使用列表初始化。

    1
    2
    char str2[] = {'h', 'e', 'l', 'l', 'o'};
    char str3[] = {'h', 'e', 'l', 'l', 'o', '\0'};

  2. 使用字符串字面值初始化。

    1
    2
    char str1[] = "helloworld";
    //编译器会隐式的在最后加一个"\0",sizeof(str)会计算这个"\0", strlen(str)不会

这种方式是字符数组初始化的简便写法。

  1. 指针和C风格字符串
    1
    char *messages= "hello world";

对于以上三种方式来说,方式1和2是等价的,这种方式中的"helloworld"存放在栈中。而第三种有些特殊,在第三种方式中,"hello world"是一个字符串字面值常量,存放在数据区的字符串常量部分[6,7,8,9]。事实上,它是一个常量字符数组,是一个不可修改的左值[9],把它作为右值时,会进行类型转换将左值转换成右值,即使用常量字符数组首字符的地址进行初始化。

C风格字符串操作函数

C语言标准库<string.h>或者C++版本的<string.h>提供了以下的字符串操作函数,它们的参数必须是指向以空字符结束的字符数组的指针。在函数内存不会验证这些字符串参数是否满足要求。

  • strlen(p),返回p指向的字符串的长度,不包括空字符
  • strcmp(p1, p2),p1==p2,返回0,p1>p2,返回正值,否则返回负值。
  • strcat(p1, p2),p2拼接到p1,返回p1
  • sctcpy(p1, p2),p2拷贝到p1,返回p1

有一点需要注意的是,p2必须能够容纳下拼接后或者拷贝后的字符串,编译器不会进行检查,这需要由程序员自己进行检查。

C风格字符串的比较

两个C风格字符串的比较,其实比较的是指针而不是字符串本身。如下所示:

1
2
3
char str1[] = "hello";
char str2[] = "world";
if(str1 < str2) //这行代码比较的不是两个字符串,而是两个指针。

string和C风格字符串的相互转换

  1. 允许使用以空字符结束的字符数组初始化string对象或者为string对象赋值。
  2. string对象的加法运算中允许使用空字符结束的字符数组作为其运算对象,不能两个都是。
  3. string对象的复合赋值运算中允许使用以空字符结束的字符数组作为其右侧运算对象。

string转换成C风格字符串

1
2
string s("hello world!");
const char *str = s.c_str();

多维数组

多维数组指的是数组的数组。多维数组的定义方式如下:

1
2
int ia[3][4];   //大小为3的数组,每个元素是一个大小为4的数组
int arr[10][20][30]; //大小为10的数组,每个元素是大小为20的数组,每个数组的元素又是一个大小为30的数组。

存储顺序

按照从右到左的维度顺序依次进行存储,实际上它们存储在了线性空间内。比如对于一个数组int array[3][6],它在内存中的存储顺序其实是:
a[0][0], a[0][1], a[0][2], a[0][3], a[0][4], a[0][5], a[1][0], a[1][1], a[1][2], a[1][3], a[1][4], a[1][5], a[2][0], a[2][1], a[2][2], a[2][3], a[2][4], a[2][5]。

多维数组的初始化

使用花括号括起来的一组值初始化多维数组。

  1. 指定所有元素的值。

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

  2. 初始化一部分,其余的进行值初始化

    1
    int a[3][4] = {0, 1, 2, 3};

只对a[0]进行了初始化。

  1. 初始化每一行的部分元素。
    1
    int a[3][4] = {{0}, {1}, {2}, {3}};

只对每一行的第一个值进行了初始化。

多维数组数组名和指针

一维数组的数组名是一个指针,它的类型是指向元素类型的指针。多维数组也一样,多维数组的数组名也是一个指针,只不过它的类型是指向数组的指针。比如:

1
int matrix[3][10];

数组名matrix实际上是指向数组首元素的一个指针,数组首元素是一个int [10]类型的数组,所以matrix是一个指向int [10]类型数组的指针,解引用操作*matrix得到一个int [10]类型的数组,这个东西其实也是一个数组名,相当于一个指针,指向一个int类型。*(*(matrix+1)+2)其实就是matrix[1][2]

多维数组的下标

如果要访问多维数组中的某个元素,必须指定所有维度的下标。即:

1
2
3
int matrix[3][10];
//要访问第2行的第16个元素,使用下标[1][15]。
matrix[1][15];

但是,实际上,下标和间接引用是等价的,在多维数组中也一样。数组名matrix可以当成一个指针,matrix +1实际上指向第二个int [10]的数组,*(matrix+1)是一个int [10]的数组。

指向数组的指针

在下面的代码中,pa是一个int指针,p是一个int [10]类型的指针,指向matrix的第一行的10个元素。

1
2
int array[10], *pa = vector;
int matrix[3][10], (*p)[10] = matrix;

指针数组

我们可以创建指针的数组。例如:

1
int *api[10];

创建了一个数组,数组有10个元素,每个元素都是一个int*类型的指针。可以创建一个char*类型的指针数组:

1
2
3
4
5
6
7
8
9
const char *keyword[] = {
"do",
"while",
"if",
"for",
"return",
"switch",
NULL
}

可以参考下面示意图:
char_pointer_array

数组和左值

数组本身是一个不可修改的左值。

字符串字面值和左值

字符串字面值常量是一个常量,不可修改的左值,它以数组的形式存储。

为什么数组不支持赋值

  1. 不支持数组的拷贝是为了避免不必要的复制开销,数组复制将会导致连续的内存读和写。
  2. 为什么用指针代替数组,不是因为他们太像了,而是避免赋值的开销,因为c里面只有值传递,如果对数组采用pass by value,会有很大的开销。

array,&array&array[0]

array&array[0]是一样的,它们和&array有什么区别呢?[3]

1
2
3
4
int array[] = {1, 2, 3, 4,5};
printf("array=%p, &array=%p\n", array, &array);

printf("array+1=%p: &array+1=%p\n", array+1, &array+1);

程序的输出如下:

1
2
array=0x7ffc50c541c0: &array=0x7ffc50c541c0
array+1=0x7ffc50c541c4: &array+1=0x7ffc50c541d4

根据上面程序的输出,array&array得到了一样的地址。但是它们并不是一样的!!!它们的地址相同,但是地址的类型不同。
对指针进行算术运算,将array&array都加一,我们却得到了不同的结果。事实上,array是指向数组第一个元素的指针,而&array是指向整个int [5]数组的指针。因此,根据指针运算规则,对地址array&array进行算术运算,得到了不同的结果。

数组和函数

数组形参

数组有两个特殊的性质:

  1. 不允许拷贝,因为不能拷贝数组,所以不能以值传递的方式使用数组参数。
  2. 在使用数组时通常会将其转换成指针。因为数组会被转换成指针,所以为函数传递数组时,实际上传递的是指向数组首元素的指针,这样子可以节约开销。

管理数组转换的指针

当传递给函数一个数组时,实参自动的转成指向数组首元素的指针,数组的大小对于函数的调用没有什么影响。因为数组是以指针的形式传递给函数的,所以函数其实是不知道数组的大小的,调用者应该为此提供一些额外的信息。通常有三种方式:

  1. 显示传递一个表示数组大小的形参
  2. 使用标记指定数组长度,要求数组本身包含一个结束标记,典型的例子是C风格字符串。
  3. 使用标准库规范,传递数组首元素和尾后元素的指针。可以使用beginend函数获得数组的首元素和尾后元素的指针。

数组形参和const

当函数不需要对数组进行写操作时,数组形参应该是指向const的指针。只有当函数确实需要改变数组元素值的时候,才把形参定义成指向非常量的指针。

数组引用形参

C++允许将变量定义成数组的引用,形参也可以是数组的引用。此时,引用形参绑定到数组上。

传递多维数组

C++其实没有真正的多维数组,多维数组其实就是数组的数组。把多维数组传递给函数时,传递的是指向数组首元素的指针。而多维数组是数组的数组,首元素本身就是一个数组,多维数组转换成指向数组的指针。数组第二维以及后面维度都是数组类型的一部分,不能省略。

函数返回值和数组

因为数组不能被拷贝,所以函数不能返回数组。不过可以返回数组的指针或者引用。

声明一个返回数组指针的函数

返回数组指针的函数形式如下所示:

1
Type (*function(parameter_list)) [dimension]

类似于其他数组的声明,Type表示元素的类型,dimension表示数组的大小,(*function(parameter_list))两端的括号必须在,否则函数的返回类型就是指针的数组。如下示例:

1
int (*func(int i))[10];

func带有参数,说明它是一个函数,前面带有解引用操作,说明可以对函数调用的结果执行解引用操作,括号右面说明这是一个维度为10的数组,括号左面是数组类型。

使用尾置返回类型

可以使用尾置返回类型,任何函数的定义都能使用尾置返回,但是这种形式一般用于比较复杂的返回类型,比如数组的指针或者数组的引用。形式如下:

1
auto func() -> int (*)[10];

使用decltype

可以使用decltype声明返回值类型。比如返回一个指针时,

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

decltype(odd) *func(int i)
{
return (i%2)? &odd: &even;
}

参考文献

1.《C++ Primer第五版》
2.https://stackoverflow.com/questions/1641957/is-an-array-name-a-pointer
3.https://www.geeksforgeeks.org/whats-difference-between-array-and-array-for-int-array5/
4.https://stackoverflow.com/questions/3437110/why-do-c-and-c-support-memberwise-assignment-of-arrays-within-structs-but-not/3439969
5.https://stackoverflow.com/questions/45656162/why-cant-a-modifiable-lvalue-have-an-array-type
6.https://stackoverflow.com/questions/1704407/what-is-the-difference-between-char-s-and-char-s
7.https://stackoverflow.com/questions/2938895/difference-between-char-a-string-char-p-string
8.http://c-faq.com/decl/strlitinit.html
9.https://stackoverflow.com/questions/10004511/why-are-string-literals-l-value-while-all-other-literals-are-r-value