漫谈:C语言 C++ 数组的迷惑与混乱 数组参数究竟是什么

2024-04-15 10:36

本文主要是介绍漫谈:C语言 C++ 数组的迷惑与混乱 数组参数究竟是什么,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

初级代码游戏的专栏介绍与文章目录-CSDN博客

我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。

这些代码大部分以Linux为目标但部分代码是纯C++的,可以在任何平台上使用。


目录

数组名究竟是什么

数组用作函数参数究竟发生了什么

传递数组给指针参数究竟发生了什么

总结:数组长度压根不存在

顺路:为什么delete和delete[]不能用错


        C语言设计很失败,起码数组的设计很失败。

        看看这是什么:

int a[5];

        “这是一个int数组,共有5个元素”——回答正确。

int a[5][3];

        “这是一个int二维数组,第一维为5,第二维为3,一共有5X3=15个元素”——不难对吧。

        再深挖一下细节:

int a[5][3];a是什么类型?
a+0是什么类型?
*a是什么类型?

        “a是二维数组,a+0是一维数组,*a是int”——你确定吗?

数组名究竟是什么

        我们知道数组名就是数组的首地址——这个说法有没有问题?既然是首地址,为什么不是一个指针?但是数组名确实可以当作指针来用,比如“a+0”就是指针运算。

        有点晕吗?我们用代码来研究一下这些东西。

        作为前提,我们要知道这两个东西:

  • sizeof() 一个类型或对象的字节大小
  • typeid().name() 一个类型或对象的类型名称(C++)

        以下代码为VS2022的C++代码(64位编译):

#include <stdio.h>
#include <typeinfo>int main()
{int a[5][3];int* p;printf("sizeof(int)       : %2zd : typeid : %s\n", sizeof(int), typeid(int).name());printf("sizeof(p)         : %2zd : typeid : %s\n", sizeof(p), typeid(p).name());printf("sizeof(a)         : %2zd : typeid : %s\n", sizeof(a), typeid(a).name());printf("sizeof(*a)        : %2zd : typeid : %s\n", sizeof(*a), typeid(*a).name());printf("sizeof(a[0])      : %2zd : typeid : %s\n", sizeof(a[0]), typeid(a[0]).name());printf("sizeof(*a[0])     : %2zd : typeid : %s\n", sizeof(*a[0]), typeid(*a[0]).name());printf("sizeof(a+0)       : %2zd : typeid : %s\n", sizeof(a + 0), typeid(a + 0).name());printf("sizeof(*(a+0))    : %2zd : typeid : %s\n", sizeof(*(a + 0)), typeid(*(a + 0)).name());printf("sizeof(a[0][0])   : %2zd : typeid : %s\n", sizeof(a[0][0]), typeid(a[0][0]).name());printf("sizeof(a[0]+0)    : %2zd : typeid : %s\n", sizeof(a[0] + 0), typeid(a[0] + 0).name());printf("sizeof(*(a[0]+0)) : %2zd : typeid : %s\n", sizeof(*(a[0] + 0)), typeid(*(a[0] + 0)).name());
}

        急性子的先看一下结果:

sizeof(int)       :  4 : typeid : int
sizeof(p)         :  8 : typeid : int * __ptr64
sizeof(a)         : 60 : typeid : int [5][3]
sizeof(*a)        : 12 : typeid : int [3]
sizeof(a[0])      : 12 : typeid : int [3]
sizeof(*a[0])     :  4 : typeid : int
sizeof(a+0)       :  8 : typeid : int (* __ptr64)[3]
sizeof(*(a+0))    : 12 : typeid : int [3]
sizeof(a[0][0])   :  4 : typeid : int
sizeof(a[0]+0)    :  8 : typeid : int * __ptr64
sizeof(*(a[0]+0)) :  4 : typeid : int

        看一下结果应该很清楚了。

        由于是64位编译,指针类型的长度都是8,int是4,数组则是int大小乘以元素总数。

        “*a”是什么?数组名作为对象,代表数组,数组名作为指针,则代表指向数组元素的指针。因为这里a是二维数组,所以a作为指针指向的是一维数组。

        数组名什么时候代表数组、什么时候代表指针呢?当数据名做加减的时候就变成了指针。所以“a”和“a+0”是完全不同的东西,一个是“int [5][3]”,一个是“int[3] *”(非正确写法,示意)。

        上面的示例代码输出仔细搞懂,关于数组你就差不多不会迷惑了。

数组用作函数参数究竟发生了什么

        直接上代码(VS2022,64位):

#include <stdio.h>
#include <typeinfo>void f(int x[6][3])
{printf("sizeof(x)         : %2zd : typeid : %s\n", sizeof(x), typeid(x).name());
}
void f2(int x[][3])
{printf("sizeof(x)         : %2zd : typeid : %s\n", sizeof(x), typeid(x).name());
}
void f3(int x[5][2])
{printf("sizeof(x)         : %2zd : typeid : %s\n", sizeof(x), typeid(x).name());
}
void f4(int x[5][])//无法编译,最后一维不能省略
{printf("sizeof(x)         : %2zd : typeid : %s\n", sizeof(x), typeid(x).name());
}
int main()
{int a[5][3];f(a);f2(a);f3(a);//无法编译,参数类型不匹配
}

        有两处不能编译:

        “void f4(int x[5][])”,不允许省略数组最后一维的大小。但是为什么可以省略第一维?这其实暗示了数组名和指针的关系:因为当作指针用的,所以第一维的长度其实无关紧要(所以才可以随便数组越界啊!!!!)。

        “f3(a);”,错误提示:“无法将参数 1 从“int [5][3]”转换为“int [][2]”。 这个信息明确告诉我们,第一维的长度被忽略了,这也能够解释为什么对“void f(int x[6][3])”的调用没有出错,因为第一维的长度根本就被忽略了。

        删除不能编译的代码,运行程序,输出如下:

sizeof(x)         :  8 : typeid : int (* __ptr64)[3]
sizeof(x)         :  8 : typeid : int (* __ptr64)[3]

        哈哈,函数参数其实是个指针!

        但是出错信息用的是数组表示法啊,写编译器的人自己把自己都绕进去了?

传递数组给指针参数究竟发生了什么

#include <stdio.h>
#include <typeinfo>void f(int* x)
{printf("sizeof(x)         : %2zd : typeid : %s\n", sizeof(x), typeid(x).name());
}
void f2(int(*x)[3])
{printf("sizeof(x)         : %2zd : typeid : %s\n", sizeof(x), typeid(x).name());
}
void f3(int* x[3])
{printf("sizeof(x)         : %2zd : typeid : %s\n", sizeof(x), typeid(x).name());
}
int main()
{int a[5][3];f(a[0] + 0);f(a + 0 + 0);//无法将参数 1 从“int (*)[3]”转换为“int *”f((a + 0) + 0);//无法将参数 1 从“int (*)[3]”转换为“int *”f2(a);f3(a);//无法将参数 1 从“int [5][3]”转换为“int *[]”
}

        这个代码有三处不能编译。

        前两处错误信息相同。“二维数组的数组名a做加减就成了指向一维数组的指针,一维数组的指针再做加减不就是指向int的指针吗?”……啊,我再想想……是我想错了啊,原来“指向一维数组的指针”做加减还是“指向一维数组的指针”,指针加减不会改变指针类型,数组名变成指针的时候脱去了一层,再做加减不会继续脱了。

        第三处明显不对的,这样写这是为了显示“int(*x)[3]”和“int* x[3]”的区别,前一个是指向数组的指针,后一个是指针数组,区别要细细品味一下。

        删除不能编译的代码,执行结果如下:

sizeof(x)         :  8 : typeid : int * __ptr64
sizeof(x)         :  8 : typeid : int (* __ptr64)[3]

        输出的是f和f2的参数类型,因为定义的就是指针,所以显示出来没什么区别。

总结:数组长度压根不存在

  •         数组名确实就是指向数组元素的指针。
  •         不仅如此,作为函数的参数,数组被解释为指向数组元素的指针,数组长度(对于多维数组,就是第一维的长度)被完全无视!
  •         C语言压跟没想过控制数组越界。
  •         除了sizeof(数组名)会判断一下数组长度,数组长度也就是初始化的时候用一下。

顺路:为什么delete和delete[]不能用错

        其实new[]申请的内存多了一个整数(假设是4个字节),放数组长度,new[]返回的值是申请的内存地址+4,delete[]知道传递进来的地址前面还有四个字节,可以根据这四个字节来对所有元素逐个执行析构函数,而delete不知道这一点,delete只会释放一个元素。虽然不执行析构函数大部分情况下不会立即导致问题,但是,最终需要释放的内存地址不一样啊(delete[]需要-4),所以用错的话内存管理系统一定要崩溃的。


(这里是结束)

这篇关于漫谈:C语言 C++ 数组的迷惑与混乱 数组参数究竟是什么的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/905581

相关文章

使用Go语言开发一个命令行文件管理工具

《使用Go语言开发一个命令行文件管理工具》这篇文章主要为大家详细介绍了如何使用Go语言开发一款命令行文件管理工具,支持批量重命名,删除,创建,移动文件,需要的小伙伴可以了解下... 目录一、工具功能一览二、核心代码解析1. 主程序结构2. 批量重命名3. 批量删除4. 创建文件/目录5. 批量移动三、如何安

C++一个数组赋值给另一个数组方式

《C++一个数组赋值给另一个数组方式》文章介绍了三种在C++中将一个数组赋值给另一个数组的方法:使用循环逐个元素赋值、使用标准库函数std::copy或std::memcpy以及使用标准库容器,每种方... 目录C++一个数组赋值给另一个数组循环遍历赋值使用标准库中的函数 std::copy 或 std::

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

Go语言中三种容器类型的数据结构详解

《Go语言中三种容器类型的数据结构详解》在Go语言中,有三种主要的容器类型用于存储和操作集合数据:本文主要介绍三者的使用与区别,感兴趣的小伙伴可以跟随小编一起学习一下... 目录基本概念1. 数组(Array)2. 切片(Slice)3. 映射(Map)对比总结注意事项基本概念在 Go 语言中,有三种主要

Java通过反射获取方法参数名的方式小结

《Java通过反射获取方法参数名的方式小结》这篇文章主要为大家详细介绍了Java如何通过反射获取方法参数名的方式,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1、前言2、解决方式方式2.1: 添加编译参数配置 -parameters方式2.2: 使用Spring的内部工具类 -

c++中std::placeholders的使用方法

《c++中std::placeholders的使用方法》std::placeholders是C++标准库中的一个工具,用于在函数对象绑定时创建占位符,本文就来详细的介绍一下,具有一定的参考价值,感兴... 目录1. 基本概念2. 使用场景3. 示例示例 1:部分参数绑定示例 2:参数重排序4. 注意事项5.