C++零基础到工程实战(5.2.6):函数与数组和数组引用

目录

前言

一、本节学习内容概要

[1.1 本节主要学习什么?](#1.1 本节主要学习什么?)

[1.2 本节核心结论](#1.2 本节核心结论)

[(1)普通数组本身可以用 sizeof 获取总大小](#(1)普通数组本身可以用 sizeof 获取总大小)

(2)数组作为普通函数参数后,会退化为指针

(3)数组引用可以保留数组完整类型

二、普通数组作为函数参数:数组会退化为指针

[2.1 main 函数中数组可以用 sizeof 获取总大小](#2.1 main 函数中数组可以用 sizeof 获取总大小)

[2.2 数组传入函数后会变成指针](#2.2 数组传入函数后会变成指针)

[2.3 int datas\[\]、int* datas、int datas10 在函数参数中等价](#2.3 int datas[]、int* datas、int datas[10] 在函数参数中等价)

[2.4 为什么函数内部无法通过 sizeof 获取数组大小?](#2.4 为什么函数内部无法通过 sizeof 获取数组大小?)

[三、普通数组传参时,一般要额外传入 size](#三、普通数组传参时,一般要额外传入 size)

[3.1 为什么要额外传 size?](#3.1 为什么要额外传 size?)

[3.2 更推荐的写法:sizeof(datas) / sizeof(datas0)](#3.2 更推荐的写法:sizeof(datas) / sizeof(datas[0]))

[3.3 为什么不能在函数内部再算 size?](#3.3 为什么不能在函数内部再算 size?)

[3.4 要点小结](#3.4 要点小结)

四、返回数组:普通数组不能直接返回,返回指针要保证生命周期

[4.1 函数能不能直接返回数组?](#4.1 函数能不能直接返回数组?)

[4.2 返回指针时,不能返回局部数组地址](#4.2 返回指针时,不能返回局部数组地址)

[4.3 为什么 return datas 可以?](#4.3 为什么 return datas 可以?)

[4.4 返回指针要确保指向空间仍然有效](#4.4 返回指针要确保指向空间仍然有效)

五、数组引用作为参数和返回值

[5.1 数组引用作为参数:可以保留数组大小](#5.1 数组引用作为参数:可以保留数组大小)

[5.2 数组引用为什么可以拿到 sizeof?](#5.2 数组引用为什么可以拿到 sizeof?)

(1)普通数组传参:

(2)数组引用传参:

[5.3 数组引用的限制](#5.3 数组引用的限制)

[5.4 数组引用作为返回值](#5.4 数组引用作为返回值)

六、完整代码示例

七、总结

[7.1 普通数组可以通过 sizeof 获取总大小](#7.1 普通数组可以通过 sizeof 获取总大小)

[7.2 数组作为普通函数参数时会退化为指针](#7.2 数组作为普通函数参数时会退化为指针)

[7.3 普通数组传参时,通常要额外传入 size](#7.3 普通数组传参时,通常要额外传入 size)

[7.4 不能返回函数内部的局部数组](#7.4 不能返回函数内部的局部数组)

[7.5 数组引用可以保留数组大小](#7.5 数组引用可以保留数组大小)

[7.6 返回数组引用可以先定义数组类型别名](#7.6 返回数组引用可以先定义数组类型别名)


前言

在前面几节中,我们已经学习了函数参数、返回值、指针、引用以及函数重载等内容。本节继续围绕函数展开,重点讲解一个非常容易混淆的问题:

数组作为函数参数时,到底传进去的是整个数组,还是数组首元素地址?

很多初学者会写出这样的代码:

cpp 复制代码
void TestBaseArr(int datas[10])
{
    cout << sizeof datas << endl;
}

然后以为 sizeof datas 可以得到整个数组大小。

但实际运行时,可能会发现:

cpp 复制代码
main 函数中 sizeof(datas) = 40
函数内部 sizeof(datas) = 8

这就说明:数组作为普通函数参数传递时,会退化为指针。

本节先只讲 基础数组和数组引用vector 作为参数和返回值的问题,放到下一章单独讲


一、本节学习内容概要

1.1 本节主要学习什么?

本节主要围绕下面几个问题展开:

  1. 数组作为函数参数为什么会变成指针?
  2. int datas\[\]、int* datas、int datas10 在函数参数中有什么区别?
  3. 为什么普通数组传参后,sizeof 无法得到数组总大小?
  4. 数组作为函数参数时,为什么通常要额外传入 size?
  5. 函数能不能返回数组?
  6. 数组引用 int (&datas)10 为什么可以保留数组大小?
  7. 数组引用作为返回值应该怎么写?

简单总结:

  1. 普通数组作为函数参数时,会退化为指针;
  2. 指针只能保存地址,不能保存数组长度;
  3. 如果想在函数内部知道数组大小,需要额外传 size;
  4. 如果想保留数组完整类型,可以使用数组引用;
  5. 数组引用可以通过 sizeof 得到整个数组大小。

1.2 本节核心结论

(1)普通数组本身可以用 sizeof 获取总大小

cpp 复制代码
int datas[10] = { 1,2,3,4,5 };

cout << sizeof datas << endl;

如果一个 int 占 4 字节,那么:

复制代码
sizeof datas = 10 * 4 = 40

(2)数组作为普通函数参数后,会退化为指针

cpp 复制代码
void TestBaseArr(int datas[])
{
    cout << sizeof datas << endl;
}

这里的 datas 已经不是完整数组了,而是一个指针。

在 64 位程序中,指针大小通常是:

复制代码
8 字节

所以函数内部输出可能是:

复制代码
8

(3)数组引用可以保留数组完整类型

cpp 复制代码
void TestBaseArrRef(int (&datas)[10])
{
    cout << sizeof datas << endl;
}

这里的**datas 是数组引用,不会退化为指针。**

因此:

复制代码
sizeof datas

仍然可以得到整个数组大小,也就是:

复制代码
40

二、普通数组作为函数参数:数组会退化为指针

2.1 main 函数中数组可以用 sizeof 获取总大小

先看一段基础代码:

cpp 复制代码
#include <iostream>
using namespace std;

int main()
{
    int datas[10] = { 1,2,3,4,5 };

    cout << sizeof datas << endl;

    return 0;
}

如果当前环境中:

复制代码
sizeof(int) = 4

那么:

复制代码
int datas[10]

一共占用:

复制代码
10 * 4 = 40 字节

所以输出结果是:

复制代码
40

这里要注意:

复制代码
datas 是一个真正的数组。

它不是指针。


2.2 数组传入函数后会变成指针

再看下面代码:

cpp 复制代码
#include <iostream>
using namespace std;

// 数组经过函数传参后,会退化为指针
void TestBaseArr(int datas[10])
{
    cout << sizeof datas << endl;
}

int main()
{
    int datas[10] = { 1,2,3,4,5 };

    cout << sizeof datas << endl;

    TestBaseArr(datas);

    return 0;
}

可能输出:

复制代码
40
8

这两个结果分别表示:

复制代码
main 中的 datas 是真正的数组,所以 sizeof(datas) = 40。
函数中的 datas 已经退化为指针,所以 sizeof(datas) = 8。
  1. 这里的 8 是因为在 64 位程序中,指针变量通常占 8 字节。
  2. 如果是 32 位程序,指针变量通常占 4 字节。

2.3 int datas\[\]、int* datas、int datas10 在函数参数中等价

在函数参数中,下面三种写法本质上是一样的:

cpp 复制代码
void TestBaseArr(int datas[])
{
}

void TestBaseArr(int* datas)
{
}

void TestBaseArr(int datas[10])
{
}

它们在函数参数位置都会被编译器理解成:

cpp 复制代码
int* datas

所以:

cpp 复制代码
int datas[]
int* datas
int datas[10]

放在函数参数中时,都不能真正保留数组大小。

也就是说,下面这个 10 只是形式上写了出来:

cpp 复制代码
void TestBaseArr(int datas[10])

并不会让函数内部真的知道数组长度是 10。


2.4 为什么函数内部无法通过 sizeof 获取数组大小?

原因很简单:

复制代码
数组传参后,函数内部拿到的是指针。

指针变量保存的是地址。

例如:

复制代码
int* p = datas;

p 里面保存的是数组首元素的地址。

但是指针本身并不知道:

  1. 这个数组有几个元素;
  2. 这个数组总共占多少字节;
  3. 这个数组在哪里结束。

所以函数内部写:

cpp 复制代码
sizeof datas

得到的是:

指针变量本身的大小,而不是数组总大小。

在 64 位程序中:

cpp 复制代码
sizeof(datas) = sizeof(int*) = 8

这就是为什么函数内部不能直接通过 sizeof 获取普通数组大小。


三、普通数组传参时,一般要额外传入 size

3.1 为什么要额外传 size?

  1. 既然数组传参后会变成指针,那么函数内部就不知道数组长度。
  2. 所以一般写函数时,会额外传入数组大小。

例如:

cpp 复制代码
int* TestBaseArr(int datas[], int size)
{
    cout << sizeof datas << endl;
    cout << "TestBaseArr size = " << size << endl;

    return datas;
}

调用时:

cpp 复制代码
int datas[10] = { 1,2,3,4,5 };

TestBaseArr(datas, sizeof(datas) / sizeof(int));

这里:

cpp 复制代码
sizeof(datas)

得到整个数组总字节数。

cpp 复制代码
sizeof(int)

得到单个元素的字节数。

所以:

cpp 复制代码
sizeof(datas) / sizeof(int)

就是数组元素个数。

如果 datasint[10]

复制代码
sizeof(datas) = 40
sizeof(int) = 4

那么:

复制代码
40 / 4 = 10

所以传入的 size 就是 10。


3.2 更推荐的写法:sizeof(datas) / sizeof(datas0)

实际写代码时,更推荐这样写:

cpp 复制代码
int size = sizeof(datas) / sizeof(datas[0]);

完整示例:

cpp 复制代码
#include <iostream>
using namespace std;

int* TestBaseArr(int datas[], int size)
{
    cout << "sizeof datas = " << sizeof datas << endl;
    cout << "TestBaseArr size = " << size << endl;

    return datas;
}

int main()
{
    int datas[10] = { 1,2,3,4,5 };

    cout << "main sizeof datas = " << sizeof datas << endl;

    TestBaseArr(datas, sizeof(datas) / sizeof(datas[0]));

    return 0;
}

可能输出:

bash 复制代码
main sizeof datas = 40
sizeof datas = 8
TestBaseArr size = 10

这里可以看出:

  1. main 中能算出数组大小;
  2. 函数内部不能靠 sizeof 算数组大小;
  3. 所以要把 size 单独传进去。

3.3 为什么不能在函数内部再算 size?

错误写法:

cpp 复制代码
void TestBaseArr(int datas[])
{
    int size = sizeof(datas) / sizeof(datas[0]);
    cout << size << endl;
}

很多初学者会以为这样可以算出数组长度。

但实际上不行。

因为函数内部:

复制代码
datas

已经是指针。

所以:

复制代码
sizeof(datas)

得到的是指针大小。

如果是 64 位程序:

复制代码
sizeof(datas) = 8

而:

复制代码
sizeof(datas[0]) = sizeof(int) = 4

所以:

复制代码
sizeof(datas) / sizeof(datas[0]) = 8 / 4 = 2

这显然不是数组真实长度 10。

因此一定要注意:

cpp 复制代码
sizeof(arr) / sizeof(arr[0])

这个写法只适用于数组还没有退化为指针的时候。


3.4 要点小结

main 函数中:

cpp 复制代码
int datas[10];
sizeof(datas);

这里的 datas 是真正的数组,所以 sizeof(datas) 计算的是整个数组大小。

如果数组有 10 个 int,每个 int 占 4 字节,那么结果就是:

复制代码
10 * 4 = 40

但是当数组作为函数参数传递时:

cpp 复制代码
void Test(int datas[])

这里的 datas[] 会退化成:

复制代码
int* datas

所以在函数内部:

复制代码
sizeof(datas)

计算的不再是整个数组大小,而是指针变量本身的大小。

在 64 位程序中,指针一般占 8 字节,因此结果是:

复制代码
8

所以:

cpp 复制代码
main 中 sizeof(datas) = 40
函数中 sizeof(datas) = 8

本质原因不是 sizeof datassizeof(datas) 写法不同,而是:


四、返回数组:普通数组不能直接返回,返回指针要保证生命周期

4.1 函数能不能直接返回数组?

C++ 中,基础数组不能像普通变量一样直接作为返回值返回。

例如下面这种写法是不允许的:

cpp 复制代码
int[10] Test()
{
}

基础数组本身不能直接作为函数返回值。

如果想返回数组相关内容,常见方式有几种:

  1. 返回指针;
  2. 返回数组引用
  3. 使用 std::array
  4. 使用 vector

本节先讲基础数组,所以这里只讨论指针和数组引用。


4.2 返回指针时,不能返回局部数组地址

看下面代码:

cpp 复制代码
int* TestBaseArr(int datas[], int size)
{
    cout << sizeof datas << endl;
    cout << "TestBaseArr size = " << size << endl;

    int arr[10]; // 局部数组,不能返回

    return datas;
}

这里有一个非常重要的点:

cpp 复制代码
int arr[10];

这个数组定义在函数内部****,属于局部变量

局部数组一般位于栈区**。**

当函数执行结束后,函数栈帧会被释放,这个局部数组的生命周期也结束了。

所以不能这样写:

cpp 复制代码
return arr;

因为**arr 离开函数后就被销毁了。**

如果返回它的地址,就相当于返回了一个已经失效的地址。

这类问题之前在"不能返回栈区变量地址"中也讲过,本质是一样的。

C++零基础到工程实战(5.2.3):栈区变量与函数返回指针、引用问题_c++ 函数返回指针-CSDN博客https://blog.csdn.net/m0_58954356/article/details/161251851?spm=1001.2014.3001.5502


4.3 为什么 return datas 可以?

在下面代码中:

cpp 复制代码
int* TestBaseArr(int datas[], int size)
{
    cout << sizeof datas << endl;
    cout << "TestBaseArr size = " << size << endl;

    int arr[10]; // 不能 return arr

    return datas;
}

这里返回的是:

cpp 复制代码
datas

datas 是从外部传进来的数组首元素地址。

例如:

cpp 复制代码
int datas[10] = { 1,2,3,4,5 };

int* re = TestBaseArr(datas, 10);

main 函数中的 datas 还没有销毁,所以函数返回它的地址是可以的。

但是要注意:

cpp 复制代码
return datas;

返回的是指针,不是整个数组。

也就是说,返回值类型是:

cpp 复制代码
int*

仍然不知道数组长度。

所以如果后续还要遍历这个返回结果,仍然需要知道数组大小。


4.4 返回指针要确保指向空间仍然有效

返回指针时,一定要考虑生命周期

可以返回的情况:

  1. 返回外部传入数组的地址;
  2. 返回全局数组的地址;
  3. 返回 static 静态数组的地址;
  4. 返回堆区动态申请空间的地址。

不能返回的情况:

复制代码
返回函数内部普通局部数组的地址。

错误示例:

cpp 复制代码
int* Test()
{
    int arr[10] = { 1,2,3 };

    return arr; // 错误:arr 离开函数后会销毁
}

这种代码即使编译器有时不立刻报错,运行时也属于危险行为。

正确理解:

复制代码
返回指针可以;
但是指针指向的空间必须在函数结束后仍然有效。

五、数组引用作为参数和返回值

5.1 数组引用作为参数:可以保留数组大小

如果不想让数组传参时退化为指针,可以使用****数组引用

写法如下:

cpp 复制代码
void TestBaseArrRef(int (&datas)[10])
{
    cout << sizeof datas << endl;
}

这里的重点是:

cpp 复制代码
int (&datas)[10]

它表示:

  1. datas 是一个引用;
  2. 引用的是一个 int10 类型的数组。

注意括号不能省略。

复制代码
int (&datas)[10]

和下面这个不是一回事:

复制代码
int& datas[10]

后者表示"引用数组",而 C++ 中不存在真正的引用数组这种用法。

所以数组引用必须写成:

复制代码
int (&数组名)[数组长度]

5.2 数组引用为什么可以拿到 sizeof?

因为数组引用没有发生数组退化。

例如:

cpp 复制代码
#include <iostream>
using namespace std;

void TestBaseArrRef(int (&datas)[10])
{
    cout << "TestBaseArrRef: " << sizeof datas << endl;
}

int main()
{
    int datas[10] = { 1,2,3,4,5 };

    cout << "main sizeof datas = " << sizeof datas << endl;

    TestBaseArrRef(datas);

    return 0;
}

输出结果:

复制代码
main sizeof datas = 40
TestBaseArrRef: 40

这说明:

  1. main 中 datas 是 int10
  2. 函数参数 datas 也是 int10 的引用;
  3. 所以 sizeof datas 仍然得到整个数组大小。

这和普通数组传参不同。

(1)普通数组传参:

cpp 复制代码
void TestBaseArr(int datas[])

函数内部 datas 是指针。

(2)数组引用传参:

cpp 复制代码
void TestBaseArrRef(int (&datas)[10])

函数内部 datas 仍然代表原来的数组。


5.3 数组引用的限制

数组引用虽然可以保留数组大小,但是也有一个限制:

数组长度必须匹配。

例如:

cpp 复制代码
void TestBaseArrRef(int (&datas)[10])
{
}

这个函数只能接收:

复制代码
int[10]

不能接收:

复制代码
int[5]
int[20]

例如:

cpp 复制代码
int a[10];
int b[5];

TestBaseArrRef(a); // 正确
TestBaseArrRef(b); // 错误

因为 b 的类型是:

复制代码
int[5]

而函数需要的是:

复制代码
int[10]

所以类型不匹配。


5.4 数组引用作为返回值

如果想返回数组引用,写法会比较复杂。

所以通常可以先使用 using 给数组类型起一个别名

例如:

cpp 复制代码
using ArrType = int[10];

这句话表示:

复制代码
ArrType 等价于 int[10]

然后函数返回数组引用:

cpp 复制代码
ArrType& TestBaseArrRef(int (&datas)[10])
{
    cout << "TestBaseArrRef: "
         << sizeof datas << endl;

    return datas;
}

完整调用:

cpp 复制代码
int datas[10] = { 1,2,3,4,5 };

auto& re = TestBaseArrRef(datas);

cout << "size of return "
     << sizeof re << endl;

运行结果:

复制代码
TestBaseArrRef: 40
size of return 40

这里:

cpp 复制代码
auto& re

必须写引用。

如果写成:

cpp 复制代码
auto re = TestBaseArrRef(datas);

就可能发生数组退化或类型不符合预期。

所以返回数组引用时,建议写:

复制代码
auto& re = TestBaseArrRef(datas);

六、完整代码示例

cpp 复制代码
#include <iostream>
using namespace std;

// 函数与数组和引用
// 数组经过函数传参会退化为指针
// int datas[]
// int* datas
// int datas[10]
// 在函数参数中基本等价,都无法直接获得数组大小

int* TestBaseArr(int datas[], int size)
{
    cout << "sizeof datas = "
         << sizeof datas << endl;

    cout << "TestBaseArr size = "
         << size << endl;

    int arr[10]; // 局部数组,不能 return arr

    return datas; // 返回外部传入数组的首元素地址
}

// 数组引用作为参数和返回值
using ArrType = int[10];

ArrType& TestBaseArrRef(int (&datas)[10])
{
    cout << "TestBaseArrRef: "
         << sizeof datas << endl;

    return datas;
}

int main()
{
    int datas[10] = { 1,2,3,4,5 };

    cout << "main sizeof datas = "
         << sizeof datas << endl;

    auto re1 = TestBaseArr(
        datas,
        sizeof(datas) / sizeof(datas[0])
    );

    auto& re2 = TestBaseArrRef(datas);

    cout << "size of return "
         << sizeof re2 << endl;

    return 0;
}

可能输出:

复制代码
main sizeof datas = 40
sizeof datas = 8
TestBaseArr size = 10
TestBaseArrRef: 40
size of return 40

七、总结

本节主要学习了函数与数组、数组引用之间的关系。

7.1 普通数组可以通过 sizeof 获取总大小

cpp 复制代码
int datas[10];

sizeof(datas)

如果 int 占 4 字节,那么结果是:

复制代码
40

7.2 数组作为普通函数参数时会退化为指针

下面几种写法在函数参数中基本等价:

cpp 复制代码
void Test(int datas[]);
void Test(int* datas);
void Test(int datas[10]);

它们进入函数后,本质上都是:

cpp 复制代码
int* datas

所以:

复制代码
sizeof(datas)

得到的是指针大小,不是数组大小。

在 64 位程序中,通常是:

复制代码
8

7.3 普通数组传参时,通常要额外传入 size

因为函数内部无法知道数组长度,所以通常写成:

cpp 复制代码
void Test(int datas[], int size);

调用时:

cpp 复制代码
Test(datas, sizeof(datas) / sizeof(datas[0]));

7.4 不能返回函数内部的局部数组

错误写法:

cpp 复制代码
int* Test()
{
    int arr[10];

    return arr; // 错误
}

因为**arr 是局部数组,函数结束后空间会被销毁。**

返回指针时,一定要确保指针指向的空间生命周期有效。


7.5 数组引用可以保留数组大小

数组引用写法:

cpp 复制代码
void Test(int (&datas)[10]);

不会退化为指针,所以函数内部:

cpp 复制代码
sizeof(datas)

仍然可以得到整个数组大小。


7.6 返回数组引用可以先定义数组类型别名

cpp 复制代码
using ArrType = int[10];

ArrType& Test(int (&datas)[10])
{
    return datas;
}

调用时建议使用:

复制代码
auto& re = Test(datas);

这样可以保留数组引用类型。

最后一句话总结:

复制代码
普通数组传参会退化为指针,所以拿不到数组大小;
想知道大小,就额外传 size;
想保留完整数组类型,就使用数组引用。
相关推荐
郝学胜-神的一滴1 小时前
Qt 高级开发 020:水平布局手写代码实战
开发语言·c++·qt·系统架构·软件构建·用户界面
小欣加油1 小时前
leetcode2126 摧毁小行星
数据结构·c++·算法·leetcode·职场和发展
Mortalbreeze1 小时前
C++11 ---- 右值引用、值类型
开发语言·c++
少司府1 小时前
C++进阶:多态
c语言·开发语言·c++·多态·抽象类·虚函数·虚表指针
并不喜欢吃鱼1 小时前
从零开始 C++----- 十三【C++ 数据结构】哈希表从原理到手撕实现(开放定址 + 链地址全覆盖)
数据结构·c++·散列表
愿天垂怜1 小时前
【C++脚手架】etcd 的介绍与使用
java·linux·服务器·c语言·c++·中间件·etcd
小则又沐风a1 小时前
进程篇: 进程概念的补充(了解环境变量和虚拟地址空间)
linux·运维·服务器·c++
郝学胜-神的一滴1 小时前
[简化版 GAMES 101] 计算机图形学 11:频域·卷积·抗锯齿
c++·unity·图形渲染·opengl·three·unreal
valan liya1 小时前
C++ 继承
开发语言·c++