当前位置: 首页 > news >正文

数组与数组名到底该如何理解?

文章目录

    • 前言
    • 数组的概念
    • 数组的类型名
    • 数组名的转换
    • 左值数组与右值数组
    • 对转换后的数组名的探究

前言

前言:为了探究数组,本人查阅了许多资料,但尽管这样,由于笔者才疏学浅,不敢说把数组的本质讲清楚,只是对前辈的一些观点进行学习,而总结的个人认为能够接受的看法。
大部分观点来源于于为老师的博客
链接: https://www.cnblogs.com/ywsoftware/category/692379.html

数组的概念

我们不妨先探究探究数组的概念。
An array type describes a contiguously allocated nonempty set of objects with a particular member object type, called the element type. Array types are characterized by their element type and by the number of elements in the array. An array type is said to be derived from its element type, and if its element type is T, the array type is sometimes called “array of T”. The construction of an array type from an element type is called “array type derivation”.–出自C99标准

译文
【数组类型】描述具有特定成员对象类型(称为元素类型)的连续分配的非空对象集。【数组类型】由它们的元素类型和数组中元素的数量来表征。【数组类型】是从它的元素类型派生而来的,如果它的元素类型是T,那么【数组类型】有时被称为“T的数组”。从元素类型到【数组类型】的构造称为“数组类型派生”。

根据以上解释,我们可以得到一个结论, 数组是一种对象,其对象类型就叫数组类型。这跟指针类型一样都属于派生类型,这也是我们经常看到数组类型和指针类型有许多相似的地方的原因。

ps:大多数人对数组类型的意识不清晰,不过也的确有些书并没有将数组作为一个类型去阐述,也许原因就在于此吧。

根据这个结论,可能大多数人都会有以下疑问:

数组的类型名

疑问一:我们知道整型类型名是int,字符类型名是char,浮点数类型名是float,既然存在数组类型,那么数组类型名是什么呢?

根据类型名可用来定义变量,看看数组变量的定义——int arr[10];。
我们常常习惯性把arr[10]整体,看作定义出来的对象,这是其实是大错特错的,首先[]是操作符,本身就具有语法功能,并且其在某情况下,arr[10]里的中括号的意思就是下标引用操作符,而且还产生了越界,它连数组的元素都算不上。最重要的一点,如果arr[10]是对象,那么int就是对象的类型?这是非常荒谬的!

仔细想一想,【定义】其实是给一个某个特定的符号赋予某种含义,【定义一个变量】就是把某个符号定义为一个变量,这样的符号我们就叫做【标识符】,而数组名作为数组对象的标识符,其性质和其他类型的对象无异;
当这个数组对象的概念明白了,该对象类型名也不难理解了,其实就是int [10],这里我们也可以对同为派生类型的指针类型名作类比:
整形指针int * arr,类型名是int *;数组指针int (arr)[10],类型名为int ()[10];函数指针void (p)(),类型名为void ()();
这样看,我们对 int arr[10]中数组变量arr的类型为int [10]就不足为奇了。

数组名的转换

疑问二:还是int arr[10];这个例子,既然arr就是数组对象,那么对arr调用printf函数打印出来的理论上来说应该是数组的内容,但实际上却是数组首元素的地址,这是怎么回事呢?
因为在C语言中也并没有根本没有数组的值的概念,那么谈何打印数组的值呢。有人说数组列表就是数组的值,这是其实很牵强,我更愿意讲数组列表称为数组的表现形式。既然数组的值不存在那么总要拿一个值来弥补空缺吧?所以,数组首元素的地址应运而生,成为了天选之子!但凡设计数组的值的问题都被转换为数组首元素地址的问题,而这也是有的人认为数组名就是指针的原因,这实际上是把等效关系误解为等价关系,等价是相同事物的不同表现形式,而等效是不同事物的相同效果,这特殊情境下数组名发生转换只能称其与指针等效,而并非等价。

这个特殊性不仅仅反映在对arr调用printf函数中,还反映在同样与数组的值相关的表达式的计算中,这也就是C/C++标准中数组与指针转换条款产生的原因,在这个条款中,数组名不被转换为对象的值,而是一个地址。

Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type “array of type” is converted to an expression with type “pointer to type” that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined.

译文:除非是sizeof操作符或一元&操作符的操作数,或者是用于初始化数组的字符串字面量,具有【数组类型】类型的表达式将被转换为具有“指向类型的指针”类型的表达式,该表达式指向数组对象的初始元素,而不是左值。如果数组对象具有寄存器存储类,则行为未定义

所以,数组名打印出来是数组首元素的地址是数组类型到指针类型转换的结果,转换为一个指向数组元素类型的指针类型,指向的是数组首元素地址,并且从一个左值转换成一个右值。经过转换后,数组名不再代表数组对象,而是一个代表数组首元素地址的符号地址,并且不是对象。特别指出的是, 数组到指针的转换规则只适用于表达式,只在这种条件下数组名才作为转换的结果代表数组首元素的地址,而当数组名作为数组对象定义的标识符、初始化器及作为sizeof、&的操作数时,它才代表数组对象本身,并不是地址。

左值数组与右值数组

我们可以看到,数组对象好像总是以左值的方式出现,那么数组对象一定是左值吗?
在将某函数参数声明为数组类型时,要使用空括号,省略数组的实际大小。因为数组作为函数形参时会转换成指针。并且后面C++也把对这些规定进行继承。具体来说,就是编译器会把这里的数组名解析成指向数组类型的指针类型,指向数组首元素地址,方括号里面的大小编译器会忽略,写不写都可以,最好不写,以免引起误解。

类似地,auto b = a;也会把b当成指针(原因是auto的类型推导采用函数参数的规则)。
这两个例子中的数组对象确实都是左值,在C89/90的转换条款也直接提出具有【数组类型】类型的左值将被转换为。。。并没有考虑数组右值的转换。但C99,关于转换的对象有不同的描述。

C89/90规定:

an lvalue that has type “array of type” is…

但C99却规定:

an expression that has type “array of tye” is…

C99中去掉了左值【lvalue】的词藻,为什么? 我们知道,数组名是一个不可修改的左值,但实际上,也存在右值数组。在C中,一个左值是具有对象类型或非void不完整类型的表达式,C的左值表达式排除了函数和函数调用,而C++因为增加了引用类型,因此返回引用的函数调用也属于左值表达式,就是说,非引用返回的函数调用都是右值,如果函数非引用返回中包含数组,情况会怎样?考虑下面的代码:

#include <stdio.h>

struct Test
{
    int a[10];

};

struct Test fun( struct Test* );

int main( void )

{
    struct Test T;

    int *p = fun( &T ).a;                         /* A */

    int (*q)[10] = &fun( &T ).a;                  /* B */

    printf( "%d", sizeof( fun( &T ).a ) );       /* C*/

    return 0;

}

struct Test fun( struct Test *T )

{
    return *T;

}

在这个例子里,fun( &T )的返回值是一个右值,fun( &T ).a就是一个右值数组,是一个右值表达式,但a本身是一个左值表达式,要注意这个区别。在C89/90中,由于规定左值数组才能进行数组到指针的转换,因此A中的fun( &T ).a不能在表达式中进行从数组类型到指针类型的转换,A中的fun( &T ).a是非法的,但C99在上述条款中不再限定左值表达式,即对这个转换不再区分左值还是右值数组,因此都是合法的;C中的fun( &T ).a是sizeof运算符的操作数,这种情况下fun( &T ).a并不进行数组到指针的转换,因此C在所有C/C++标准中都是合法的;B初看上去仍然有点诡异,&运算符不是已经作为例外排除了数组与指针的转换吗?为什么还是非法?其实B违反了另一条规定,&的操作数要求是左值,而fun( &T ).a是右值。C++继承了C99的观点,也允许右值数组的转换,其条款非常简单:

An lvalue or rvalue of type “array of N T” or “array of unknown bound of T” can be converted to an rvalue of type “pointer to T.” The result is a pointer to the first element of the array.

译文:类型为“(长度为)N T的数组”或“T边界未知的数组”的左值或右值可以转换为类型为“指向T的指针”的右值。结果是指向数组第一个元素的指针。

对转换后的数组名的探究

讨论完代表数组对象的数组名在转换为代表数组首元素地址的符号地址时,并非都是左值,讨论讨论数组首元素地址的符号地址这个右值是什么呢?根据之前的论述,通过数组名会得到一个数组首元素地址,首先数组首元素地址并不是常量,常量是有地址空间的,而数组首元素的地址本身没有地址空间对它存储,那么数组名转换成的是字面量咯?那也不一定,为什么呢?

请在C90的编译器中编译如下代码,注意不能是C99和C++的,因为C99和C++不再规定数组的初始化器必须在编译期间得到结果,会看不到效果:

int main( void )

{

    static int a[10], b[10];

    int c[10], d[10];

    int* e[] = { a, b };     /* A */

    int* f[] = { c, d };     /* B */

    return 0;

}

B为什么不能通过编译?是由于自动数组名并不是在编译期间得到结果。

在C中,常量表达式必须是编译期的,只在运行期不变的实体不是常量表达式,请看标准的摘录:

A constant expression can be evaluated during translation rather than runtime, and accordingly may be used in any place that a constant may be.

译文:常量表达式可以在翻译期间求值,而不是在运行时求值,因此可以在常量可能存在的任何地方使用。

对于常量表达式是这样,那么对于字面量其实也是,因为常量表达式的值其实就是一个字面量;
那么,数组首元素地址在编译期间就可以得到结果,那么它就是一个字面量,而数组名就可以成为常量表达式。

结论: C/C++中转换后的数组名,都不是常量。C中转换后的数组名,是否常量表达式要视其存储持续性而定,全局数组、静态数组名都具有静态存储连续性,都是常量表达式,而自动数组名并不具有静态存储持续性,而是自动存储持续性。在C++中,由于不再规定常量表达式必须是编译期的,因此C++的转换后数组名都是常量表达式

在数组名arr转换后,其实就可以将转换后的数组名arr和&arr[0]进行类比,在某种意义上讲,他们没有本质区别;

以上就是我对数组的总结,这里涉及到的数组类型到指针类型的转换与左值到右值的转换、函数类型到指针类型的转换一起是C/C++三条非常重要的转换规则。C++由于重载解析的需要,把这三条规则概念化了,统称为左值转换,但C由于无此需要,只提出了规则。符号是语言对计算机的高级抽象,但计算机并不认识符号,它只认识数值,因此一个符号要参加表达式计算必须先对其进行数值化,三条转换规则就是为了这个目的而存在的。
在这里插入图片描述

感谢您对这篇文章的阅读,若您对此有任何想法,欢迎下方留言讨论哦!

相关文章:

  • 计算机网络——随机接入
  • 【NLP开发】Python实现聊天机器人(微软Azure机器人服务)
  • MyBatis框架总结
  • 10.3国庆作业(UART实验)
  • 西瓜书研读——第五章 神经网络:感知机与多层网络
  • Docker实战:Docker安装Gitlab实用教程
  • 【python-Unet】计算机视觉~舌象舌头图片分割~机器学习(三)
  • 牛客网面试——数学类型3
  • 经典回顾 | 一种跨模态多媒体检索的新方法
  • 基于python+django框架+Mysql数据库的校园失物招领系统设计与实现
  • [ vulhub漏洞复现篇 ] Celery <4.0 Redis未授权访问+Pickle反序列化利用
  • 【ML15】浅谈神经网络 Nerual Network
  • 串口实验(10.3)
  • 猿创征文 | 使用Docker部署openGauss国产数据库
  • Python 常用内置函数
  • android百种动画侧滑库、步骤视图、TextView效果、社交、搜房、K线图等源码
  • CSS盒模型深入
  • express如何解决request entity too large问题
  • Js基础知识(四) - js运行原理与机制
  • Netty+SpringBoot+FastDFS+Html5实现聊天App(六)
  • PHP的类修饰符与访问修饰符
  • python_bomb----数据类型总结
  • Redux 中间件分析
  • Transformer-XL: Unleashing the Potential of Attention Models
  • ViewService——一种保证客户端与服务端同步的方法
  • 不上全站https的网站你们就等着被恶心死吧
  • 从零搭建Koa2 Server
  • 第2章 网络文档
  • 动态魔术使用DBMS_SQL
  • 高性能JavaScript阅读简记(三)
  • 规范化安全开发 KOA 手脚架
  • 快速体验 Sentinel 集群限流功能,只需简单几步
  • 王永庆:技术创新改变教育未来
  • 小程序button引导用户授权
  • [地铁译]使用SSD缓存应用数据——Moneta项目: 低成本优化的下一代EVCache ...
  • 如何正确理解,内页权重高于首页?
  • 曜石科技宣布获得千万级天使轮投资,全方面布局电竞产业链 ...
  • ​ 轻量应用服务器:亚马逊云科技打造全球领先的云计算解决方案
  • ​Z时代时尚SUV新宠:起亚赛图斯值不值得年轻人买?
  • ​第20课 在Android Native开发中加入新的C++类
  • ​云纳万物 · 数皆有言|2021 七牛云战略发布会启幕,邀您赴约
  • (1)STL算法之遍历容器
  • (13)[Xamarin.Android] 不同分辨率下的图片使用概论
  • (C语言)逆序输出字符串
  • (Spark3.2.0)Spark SQL 初探: 使用大数据分析2000万KF数据
  • (附源码)node.js知识分享网站 毕业设计 202038
  • (附源码)springboot金融新闻信息服务系统 毕业设计651450
  • (附源码)ssm教师工作量核算统计系统 毕业设计 162307
  • (教学思路 C#之类三)方法参数类型(ref、out、parmas)
  • (三)Hyperledger Fabric 1.1安装部署-chaincode测试
  • (十)【Jmeter】线程(Threads(Users))之jp@gc - Stepping Thread Group (deprecated)
  • (一)Spring Cloud 直击微服务作用、架构应用、hystrix降级
  • (中等) HDU 4370 0 or 1,建模+Dijkstra。
  • .NET 中 GetProcess 相关方法的性能
  • .NET 中使用 Mutex 进行跨越进程边界的同步