C语言中为何不能直接对一个数组对象赋值

资讯 1年前 (2022) 千度导航
10 0 0

今天在某个技术群里看到有一位朋友发出这么一个疑问,觉得非常有意思,而且也有必要分享出来同大家一起探讨一下。原话是这样的:“实在想不通,为何C/C++不能把数组跟结构体变量那样同等对待?直接给数组赋值,也不需要循环了。”

本人将阐述两个问题点:首先是为何C语言中不允许直接对一个数组对象赋值;其次是是否有其他方法来达成这个目的。因为要回答这个问题需要从当初C语言所设计的整套语法体系与设计哲学角度出发,所以三言两语无法说得清,故以此文进行阐明。各位还有其他见解的,欢迎在下方留言。

为何C语言中不允许直接对一个数组对象赋值

在1970年,Dennis Ritchie与Ken Thompson创建了C语言。该语言是基于B语言的基础上开发出来的。当时要解决的最大痛点就是各个硬件平台的可移植性,同时要尽可能地减少额外的性能损耗。

我们要知道,1970年代的计算机CPU主频才多少,内存才多少。像Dennis Ritchie与Ken Thompson所使用的由DEC生产制造的基于PDP-11处理器核心的计算机,它是一个16位系统,而其地址长度仅18位,因此内存容量最大只能支持到:2^18 = 256KB。所以可想而知,对于应用程序而言,像数组拷贝这种需求会不会多……

像做过嵌入式系统的朋友们应该更能明白,在对一个单片机编程时,数组一般用于啥场合。很显然,我们通常都是将数组用作为一个全局对象,而将数组作为函数内局部对象的场合很少。而需要对数组拷贝的场合就更少了~毕竟这是一个全局引用就能解决的事情。

所以在此历史背景下,C语言中的数组被设计为一个“指向它自身存储空间的常量指针”。下面,我们看以下代码来对数组和指针有个相互关联的认识。

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

int main(void)
{
    int arr[3] = { 1, 2, 3 };
    int* p = arr;
    
    if ((uintptr_t)&arr == (uintptr_t)arr) {
        // 这里会输出:Equal!
        puts("Equal!");
    }

    if ((uintptr_t)&p != (uintptr_t)p) {
        // 这里输出:Not equal!
        puts("Not equal!");
    }
}

我们从上述代码中可以看到,数组对象 arr 其地址跟它自身所引用的缓存首地址是同一个地址。这一点就跟C++中的引用语法很像。而指针 p 的地址跟它所指向的 arr 的地址显然不会相同,指针 p 有其自己独立的存储空间。

而另一方面,我们看到上述代码中指针 p 可直接指向 arr。此时,数组对象 arr 会被隐式转换为 int * 类型。因为上面提到过,为了节省存储空间,一般都会对同一个数组对象进行引用。有时为了要做数组元素的排序、搜索等操作,也会直接通过其他指针对象对该数组进行操作。正因为这些应用上的需求,使得C语言在设计时优先考虑能将数组隐式地转换为指针类型并赋值给某一指针对象,进而引出了 [ ] 操作符不仅能用于数组对象,而且也可用于指针对象。这就把数组与指针的元素操作语法体系给打通了。

所以无论是对于数组对象还是指针对象,我们既可以像数组一样通过 [ ] 操作符,又可以通过 * 操作符结合 +、- 操作符来访问相应位置的元素。我们看以下代码。

#include <stdio.h>

int main(void)
{
    int arr[3] = { 1, 2, 3 };
    int* p = &arr[1];

    // 使用[ ]操作符访问元素
    int a = arr[0];
    int b = p[0];

    // 使用 * 结合 +、 - 操作符访问元素
    a += *(arr + 1);    // 这里使用arr时,它被隐式转换为了 int * 类型
    b += *(p - 1);

    if (a == b) {
        // 输出:Equal!
        puts("Equal!");
    }
}

从上面代码中可以看出,p[i] 这种表达方式与 *(p + i) 是完全等同的,而前者这种表达明显更为简洁。

那假如我们将数组对象视作为一种复合类型可不可以呢?在工程上或许可以,但在语法体系上会显得格外另类。我们看上面的代码。假如 arr 不管在什么情况下均以 int [3] 类型的方式呈现,那么 arr[0] 则表征了访问 arr 的第0个元素。现在问题来了,[ ] 操作符使得把一个 int [N] 类型转换为了 int 类型。这在整个语法体系里就显得格外突兀。

然后网友会说,就把 int [3] 当成一个结构体来看待嘛~那我就要问了,&arr[0] 取的是一个临时变量的地址还是 arr 所引用的地址呢?倘若 int [3] 不与指针打通,我们是无法直接断言 &arr[0] 直接引用的是 arr 所处的存储空间。因为此时,&arr[0] 其实相当于:

int _tmp = arr[0];
int *p = &_tmp;

而将数组与指针的语法体系打通之后就很显然了,&arr[0] 相当于:

int *p = &*(arr + 0);

所以这么一来对数组元素的引用不会产生任何歧义性。

而现代化不少编程语言确实会将数组类型作为一种复合类型来看待,甚至有不少专门针对数组的封装类型,原因就是这些编程语言没有指针这个概念,它们不需要关心 arr[0] 拿到的是一个临时变量还是就是对 arr 自身的引用,反正就是对它进行读写即可。所以这是C语言跟其他高级编程语言之间的本质差异。

通过用结构体类型对数组封装以达到拷贝数组的目的

如果我们真想在某些情况下对数组进行拷贝赋值,那其实也是有办法的。我们可以直接将数组用一个结构体封装即可。大家可以看以下例子。

C语言中为何不能直接对一个数组对象赋值

通过断点设置我们可以看到,结构体对象s1中的数组元素与s2的元素完全一致。

版权声明:千度导航 发表于 2022年11月7日 00:26。
转载请注明:C语言中为何不能直接对一个数组对象赋值 | 千度百科

相关文章