【重学c++primer】第四章第三节 深入浅出:从数组到指针

数组到指针

例子:

int a[3] = {1,2,3};
auto b = a; // decay to pointer, 类型的退化/转化

大多数时候, 数组都会转化为指针, 还是有些情况不会
比如之前提到的decltype(a), 此时就只会返回`int[3];
还比如:sizeof(a)

这种转换是一种隐式的转换, 会丢失一部分信息
比如上面的例子, a的类型是int[3], 但是b的类型是int*, 其中丢失的信息就是3, 3的含义就是元素的个数为3
还可以使用引用来避免类型转换, 例如:auto& b = a;
此时b的类型为int(&)[3]

一般这种隐式的类型转换不会带来什么影响,但是还是有一些地方需要注意
比如说:

// main.cpp
extern int b[4];    // 数组的声明

// source.cpp
int b[4] = {1,2,3,4};   // 没问题,只要有include就是正确的

// main.cpp
extern int* a;  // 声明一个指针

int mian()
{
    cout << a[0];   // error
    return 0;
}

// source.cpp
int a[4] = {1,2,3,4};   // 定义一个指针

上述的第二个例子企图使用数组会隐式转换成指针来完成数组的声明,这样在source.cpp文件中,即使更改了数组的下标,在main.cpp中也不需要更改,可以小偷懒

但是会报错,猜猜是链接错误,编译错误,还是运行时错误?
可能有人认为是编译错误, 但是实际上是运行期的错误
可以编译,链接,但是一运行,就会段错误
为什么呢?
其实是因为数组和指针还是有本质的区别

  1. 肯定不是编译器错误, 编译其实解决的对象是翻译单元,也就是对应的文件,单看任何一个文件都是没有问题的
  2. 这也不是一个链接错误,这是比较迷惑人的地方,现在让我们来看看链接后的情况
    )

可以看到都有一个a, 但是再进一步注意到,这两个a都没有任何的类型信息(U和D都不是类型信息)

目前的解释是:类型是编译期的概念,链接不需要这个信息,那么c++和其他语言可以更好的链接,比如和c语言链接

因为链接的时候把类型的信息丢掉,因此链接的时候不会报错
接下来,我们这样做
cout << a
在这里插入图片描述

cout会输出16进制的数呢?
答案很显然,指针!
因此a被视为了一个指针

再来看

// source.cpp
#include <iostream>

using namespace std;

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

void func()
{
    cout << a;
}


// main.cpp
#include <iostream>

using namespace std;

void func();

extern int* a;

int main()
{
    func();
    cout << a;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上面的地址是func打印的,下面的地址是main打印的,两者是完全不一样的地址
那么哪一个是对的?
基本可以猜到,上面是对的,更符合操作系统总线位数
在编译source.cpp的时候,编译器可以清楚的看到a是一个数组,因此会转换成指向这个数组首元素的指着,然后将首元素的地址打印出来
而main.cpp,也认为a是一个指针(因为输出了16进制数),那么遇到指针,就会输出指针指向的元素的地址
但是为什么是输出2很多个0然后一个1呢?

解释如下:

首先,在source.cpp文件中:
int a[5] = {1,2,3,4,5};
首元素为int类型,值为1,由于int占4个字节,因此逻辑表示为:0000 0001
第二个元素为int类型,值为2,由于int占4个字节,因此逻辑表示为:0000 0002
同时因为大小端的原因且数组是连续存放的,因此物理存储为
01 00 00 00 02 00 00 00
然后,由于在main.cpp文件里,a被视为一个指针,在64位操作系统下,指针占8个字节,因此读取出来的数组为:01 00 00 00 02 00 00 00
输出到终端(从物理存放到逻辑表示,也就是抛开大小端的影响)的情况就为:
00 00 00 02 00 00 00 01
再因为,开头的0不输出,那么就是2 00 00 00 01

故main.cpp输出如上图

因此,如果你不通过隐式的类型转换,而是选择强行的把数组看成一个指针的话,数据是错的
那么回过头来修改之前的demo

在main.cpp文件里,要声明为extern int a[]此时程序运行才符合逻辑
这里还有一点点需要注意的,就是中括号里没有给出元素的个数

一般内建数组都是要给出的,除非你是想让编译器自己推算,但是编译器推算的时候,会根据数组的初始化情况来推算
但是其实作为数组的声明是可以不用的,而这种形式在c++中有一个专门的术语叫:Unknown Bounded Array

当然,还有一种术语也能描述这种声明:incomplicit type(不看extern)
当然,还有其他的不完整类型,比如类, 这种类型需要在其他文件共享文件的定义

除此之外, 给定一个数组,我们还能获得一个指向数组开头和结尾的指针

int a[3] = {1,2,3};
auto p = &(a[0]);   // 获得指向数组开头的指针
auto p2 = a;        // 获得指向数组开头的指针
auto p3 = (a + 3);  // 获得指向数组结尾的指针
auto p4 = &(a[3]);  // 获得指向数组结尾的指针

// 标准库的做法
std::(c)begin, std::(c)end  // c表示const
cout << std::begin(a);  // 获得指向数组开头的指针
cout << std::end(a);    // 获得指向数组结尾的指针

前面提到, 数组在大多数情况会隐式类型转换成指针,这种类型的隐式转换会带来一些不好的情况,比如:

int a[3] = {1,2,3};
auto b = a;
cout << std::begin(b); // error
cout << std::end(b);    // error

begin和end是获得数组的指针,但是这在类型转换的过程中,b不再是一个数组,因此无法获得指针,那么就会报错

再进一步,前面有个数组声明的例子:

extern int a[];
int main()
{
    cout << std::begin(a);  // error

    return 0;
}

原因还是一样的。
那么,有一点疑惑:
既然我们能用

  • &(a[0])
  • a
  • (a + n)
  • &(a[n])
    这些方法获取数组的开头/结尾指针, 为什么还要引入std::begin/end呢?

实际上是因为std::begin/end可以应用在不同的类型上面, 而不单单是内建数组,例如:vector等

指针的运算

指针的加法

int a[3] = {1,2,3};
auto p = a;
p = p + 1;  // 此时指向了第二个元素

指针加上一个常数,其含义为:指针向后移动元素的字节个数
例如上面的例子, p就移动了4个字节, 因为int占4个字节(至少我的电脑int占4字节)
减法同理

指针的比较:

auto p2 = a + 3;
if (p == p2)
{
    cout << "equal";
}
else if (p < p2)
{
    cout << "less";
}
else if (p > p2)
{
    cout << "greater";
}

指针支持大于、小于、等于、小于等于、大于等于、不等于的比较运算,但是不建议使用大于、小于、大于等于、小于等于
原因在于你无法保证两个指针是指向同一个数组
你肯定会反驳,哎呀,我自己整的两个指针,我自己当然知道是指向同一个数组啦,我想用就用

那么…

  1. 你用的指针是你同事提供的呢?
  2. 你的同事写出了两个指针的大于判断,那你岂不是还要往前判断一下这两个指针是不是指向了同一个数组,你肯定会想骂人

因此,非常的不建议

指针的求距离

int a[3] = {1,2,3};
auto p = a;
auto p2 = a + 3;

cout << p2 - p; // 3

指针的相减,含义为:两个指针的地址值之差,再除以元素的字节个数,得到两个指针之间的距离

指针的解引用

int a[3] = {1,2,3};
auto p = a;
cout << *(p + 1);
cout << *p;

指针的索引

int a[3] = {1,2,3};
auto p = a;
cout << p[1];   // 指针索引
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值