【深度C++】之“数组”

0. 数组的概念

参考【深度C++】之“类型与变量”我们知道,数组是一种复合类型。

数组是一种存放多个类型相同对象的容器。对象本身没有名字,通过其所在数组中的位置访问。

我们来理解一下定义:

  1. 类型相同,意味着不可以一会儿是int,一会儿是double
  2. 对象,意味着引用类型不可以放入数组,但是指针可以
  3. 位置,意味着访问数组的方式

下面就是一个数组:

int arr[10];  // 含有10个整数的数组

关于数组相关的内容,我们需要知道:

  1. 数组的声明
  2. 数组的初始化
  3. 拷贝和赋值数组
  4. 访问数组的元素
  5. 理解字符数组
  6. 数组与指针
  7. 数组与引用
  8. 多维数组(数组与数组)
  9. 数组的遍历(4种方式)

1. 数组的声明

前例:

int arr[10];  // 含有10个整数的数组

中括号中的内容是数组的维度,维度必须是编译时已知的,因此维度必须是一个常量表达式

2. 数组的初始化

默认情况下,数组的元素执行默认初始化,即有可能出现未定义的值。

可以使用初始值列表进行数组的初始化,此时可以省略数组的维度。

const unsigned sz = 3;  // 常量

// 含有三个元素的数组,元素值是0,1,2
int ial[sz] = {0, 1, 2};

// 可以省略不写
int a2[] = {0, 1, 2};

// 初始值列表的数量可以小于数组维度
// 此时剩余元素进行值初始化。
int a3[5] = {0, 1, 2};

上述示例需要注意,当初始值列表中元素数量少于数组的维度时,剩余元素进行值初始化

3. 拷贝和赋值数组

不允许拷贝和赋值数组。

int a[] = {1, 2, 3};
int a2[] = a;  // ERROR!

int a3[] = {4, 5, 6};
a = a3;  // ERROR!

4. 访问数组的元素

通过数组的概念,我们了解到通过位置来访问数组元素。

位置通常也被称作索引下标

数组索引从0开始,其类型为size_t类型,不必是一个常量表达式。

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

cout << a[0];  // 输出1

size_t i = 1;
cout << a[1];  // 输出2

最好再访问数组的时候,检查下标是否在合理的范围之内。如果你确定不会超越数组范围,则可以不用检查。

如上述示例,我若访问a[4],这显然是有问题的,但是编译器不会帮你检查

5. 理解字符数组

在数组的知识领域里,字符数组是比较特殊的一类数组。

char s[] = {'h', 'e', 'l', 'l', 'o', '\0'};

我们可以使用字符串字面值初始化数组:

char s[] = "Hello";

注意,此时数组的维度是6,字符串会默认添加一个空字符\0

cout << sizeof(s) / sizeof(char);  // 输出6

要想正确的使用字符数组,一定要时刻考虑'\0',不论是定义还是使用。

6. 数组与指针

因为指针是对象,所以可以放入数组…

(╯‵□′)╯︵┻━┻

6.1 包含指针的数组和指向数组的指针

int arr[10];

// 包含10个整型指针的数组
int *ptrs[10];

// 指向包含10个整数的数组的指针
int (*ptr_arr)[10] = &arr;

6.2 先右后左,由内向外

怎么理解上面的定义?答:用计算机的思维。

先看ptrs。①从变量名ptrs开始,先右后左,先遇到中括号[],此时判定它是一个数组。②是一个什么样的数组呢?不能往右了,我们向左,看到了*,此时判定它是一个包含10个指针的数组。③什么样的指针呢?继续往左,看到了int,此时判定它是一个包含10个整型指针的数组。

再看ptr_arr。①从变量名开始,先右后左,不能右了,向左,看到了*,此时判定它是一个指针。②是一个什么样的指针呢?由内向外,跳出括号,先右后左,看到了[],此时判定它是一个指向数组的指针。③什么样的数组呢?不能往右了,我们向左,看到了int,此时判定它是一个指向包含10个整数的数组的指针。

这里比较复杂的原因是复合类型的复合类型。判定一个复合类型需要一个基础类型,因此会有很多定语修饰,这也是我们每走一步就提问“什么样”的原因。

6.3 编译器会将数组转换成指针

除了我们在定义数组和指针上,二者有交集,在使用数组时,指针和数组也不分家。

因为编译器一般会把数组转换成指向首元素的指针

int arr[] = {1, 2, 3};
cout << *arr;  // 输出1

上面的代码利用真正的指针实现:

int arr[] = {1, 2, 3};
int *p = &arr[0];
cout << *p;  // 输出1

这样,我们就可以做一些特殊的操作了。

int arr[] = {1, 2, 3};
auto arr2(arr);  // arr2的类型,是指针
cout << sizeof(arr2);  // 输出8
cout << *(arr2 + 2);  // 输出3

7. 数组与引用

引用不是对象,所以没有包含引用的数组,只有数组的引用:

int arr[10];
int (&r_arr)[10] = arr;

理解这个的方法,还是“先右后左,由内向外”。

那聪明的你告诉我,这个是啥子:

int *(&arry)[10] = ptrs;

8. 多维数组(数组与数组)

C++中并没有多维数组的概念,所谓的多维数组,不过是数组的数组(自己和自己复合)。

定义多维数组,需要多个维度。

// mat是一个数组,维度为3,它的内容是
// 另一类数组,维度是3的整形数组。
int mat[3][3];

依旧可以使用初始值列表进行初始化:

int mat0[3][3] = {{1, 2, 3},
                  {2, 3, 6},
                  {9, 3, 2}};

// 可以省略维度的具体信息
int mat2[3][3] = {1, 2, 3, 2, 3, 6, 9, 3, 2};

// 这样初始化每行的首元素,其余元素值初始化
int mat3[3][3] = {{1}, {2}, {9}};

// 注意和上面不等价,此处初始化了第一行的元素
int mat3[3][3] = {1, 2, 9};

9. 数组的遍历

对数组说,最常见的操作就是遍历。

9.1 通过下标遍历

const int sz = 10;
int arr[sz] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (int i = 0; i < sz; ++i) {
    cout << arr[i];
}

这种方法得提前知道数组的维度,不然容易出现越界。我们可以通过sizeof(arr) / sizeof(int)得到。

int arr[] = {0, 1, 2, 3, 4};
const int sz = sizeof(arr) / sizeof(int);
for (int i = 0; i < sz; ++i) {
    cout << arr[i];
}

9.2 范围for语句

int mat[3][3] = {{1, 2, 3},
                 {2, 3, 6},
                 {9, 3, 2}};

for (auto &row : mat) {
    for (auto el : row) {
        cout << el;
    }
    cout << endl;
}

注意:范围for语句除了最内层的for,其余的外层for必须使用引用。否则会将row转换为每行的首元素的地址,编译无法通过。

9.3 指针视角

int mat[3][3] = {{1, 2, 3},
                 {2, 3, 6},
                 {9, 3, 2}};

for (int (*row)[3] = mat; row != mat + 3; ++row) {
    for (int *col = *row; col != *row + 3; ++col) {
        cout << *col;
    }
    cout << endl;
}

row是一个指向包含三个整形元素数组的指针,解引用指针row将得到包含三个整形元素的数组,但是数组和指针存在天然等价关系,因此*row就是指向包含三个整形元素的数组的首地址,也是一个类型为int *的指针,可以赋值给*col,通过col去访问。

这里的mat + 3也是通过指针的视角去看待。mat是指向一个包含三个数组的数组的首地址,每次对mat加1,将跳到第二行。

9.4 迭代器视角begin & end

上面的方法是通过mat + 3*row + 3得到了尾后指针的效果,但是极容易出错。

可以使用C++11新引入的标准库函数beginend

int mat[3][3] = {{1, 2, 3},
                 {2, 3, 6},
                 {9, 3, 2}};

for (auto p = std::begin(mat); p != std::end(mat); ++p) {
    for (auto q = std::begin(*p); q != std::end(*p); ++q) {
        cout << *q;
    }
    cout << endl;
}

这样的操作就和迭代器有些相似了。

10. 总结

数组是C++中另外一种复合类型,它是一种存放多个类型相同对象的容器,通过位置访问其中的对象。

使用数组要注意数组的声明、初始化,如何访问数组的元素,数组下标从0开始,不可以直接拷贝和赋值数组。

数组本身是一种复合类型,还可以和其他复合类型如指针、引用甚至是数组本身进行复合。

对数组的遍历可以用四种方式,最难理解的是指针的视角。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值