📚【C++从入门到放弃】指针与数组

,

— c++11入门费曼学习副产物,如有谬误,望佬友指正,不胜感激 —


:books: 其他c++姊妹帖

    ❀ 命名空间
    ❀ const与类型转换
    ✿ 指针与数组
    ❀ 内存模型与字符串底层
    ❀ 引用与移动语义


◢ 指针基础

在C/C++中,指针是一个指向一片空间并存储其地址的变量。
在之前的帖子中我们提到过:

一个数据变量通常在内存中占有一片空间,并有与之对应的标识符绑定到这个空间。

此外,函数等非数据变量虽然不占实例内存,但也有相应的入口代码地址。
那么对于指定对象,除了用标识符访问,我们还可以用指针来间接访问。

int  a = 1;
int* p = &a;          // & 取地址
std::cout << *p;      // * 解引用,即间接寻址取值,输出 1
  • * 在类型之后表示指向,在指针符号前则表示解引用。
  • &在类型之后表示引用(后面帖子再讲),在变量符号前则表示取地址符。

❖ 泛型指针基础

指针的类型也很重要,它表明了怎么对这个指针的值进行操作,例如,

对于int* p,int就是p指向的类型。

有时候我们暂时不确定指向的是什么类型,此时可以使用泛型类型指针void*表示类型未指定,等待确定类型指针赋值进来。

void fun(void* data);  // 可以传入任意类型的指针

虽然void*是泛型类型指针,但它只能承接地址值,不附带额外信息,因此不是所有指针都能当作void*传入。一般注意下面两种即可:

类型 不能转化原因
函数指针 有跳转信息
类成员指针 一些面向对象特性的信息

:pushpin: void* 赋值给另一个void*不涉及类型转换,是安全的(好像废话

void* 指向的对象类型未知,直接使用void* 指针是十分危险的。

❖ 泛型指针的转换

void*应该要先转换为其他指针再使用,且必须是显式转换。

在这个帖子中提到了使用static_cast将void* 转为其他类型的指针,例如:

void* p = malloc(1);	// 指向1字节大小内存的void*指针
char* c = static_cast<char*>(p);	// 转为char*使用

帖子中同时也提到了:

用户可能自己写错导致转换的类型不可用

也就是可能会出现void* 原来的类型不是char*,但转成了char*来使用,这会导致出现非预期的错误。
c++认为void*转换是程序员的责任,因此不会对该转换编译报错。

void*向其他类型的隐式转换是不允许的 (但在c中是允许的):

int* p = (void*)0;	// 报错,void*不能隐式转换成int*

❖ 空指针

void*不能隐式向其他类型指针进行转换,因此c++中NULL不能像C中那样可以采用void* 0的形式实现,常见实现为整数0。

但即便如此,在c++中下述空指针的写法也是不推荐的:

int* p = NULL;	// c++不推荐这样的写法
int* p = nullptr;	// 这是推荐的写法

因为c++中有函数重载机制,即根据传入参数类型的不同执行不同的代码。而由于上述,NULL在c++中会首先被视为0,然后才可能推理成指针,如果用NULL表示空指针,可能会造成误解。

替代地,c++给出nullptr专门用于指代空指针,是更推荐的空指针表示用法。

◢ 静态数组基础

指数组大小不会变化的数组。

c++允许直接定义静态数组。(动态数组后面再讲罢)

int arr[3];	// 定义非静态/全局的数组未初始化,会有垃圾值在其中
int arr[]{1,2,3};	// 定义数组并初始化为1,2,3,自动设置元素数量为3

int arr[3]{1};	// 元素数量为3,但只显式定义了第一个元素为1,剩余元素会自动置零值
int arr[3]{1,0,0};	// 等价上行

int arr[3]{};	// 元素数量为3,但未指定值,会全部初始化为零值
int arr[3]{0,0,0};	// 等价上行

:pushpin: 零值:各个类型不同,如int为0,char为‘\0’。

  • 对于int arr[3]{};
    arr是数组名,取值后的地址指向数组开头,即arr和&arr[0]指向同一个地址,值是一样的,但在类型上略有不同。

  • &arr[0]
    是头元素的地址,是int*类型,指向的空间大小仅限一个元素。

  • arr
    值是数组的起始地址,是int[3]类型,其大小是指代整个数组所有元素的大小。

区别如下:

sizeof(&arr[0]);	//值为8,一个指针类型的大小为8
sizeof(arr);	// 值为12,即3个int的长度

那么此时就出现了一个问题,
arr赋值到其他变量如果复制整个数组,开销会很大,而目的可能只是多加个标识符而已。

C/C++因此引入数组类型退化机制,在大多数表达式场景中退化成其元素指针。

❖ 数组类型的退化

为防止混淆,在此将与数组相关的类型梳理一遍:

  • 数组类型:int arr[]中的arr数组名的类型,非指针,只是会退化成元素类型指针。
  • 数组指针:int (*a)[],是指针,指向一个元素类型为int的数组。
  • 指针数组:int *a[],是数组,元素类型是int*。

上面提到的“大多数表达式”的退化场景,指的是需要读取数组内容数据的场景,如:

  • 函数传参:读取数组内容到函数参数,开销大,退化为数据指针。

    void fun(int* data);
    
    int arr[]{1,2,3};
    fun(arr);	// int[3]类型的数组传入函数会退化为int*指针类型
    

    有人可能问,那我这么写呢:

    void fun(int data[3]);
    

    这样写是没用的,参数传值依旧不会传整个数组,编译器仍将其解释为期待一个int*的数据。

  • 表达式取值:

    int* p = arr+1;
    

    双目+运算符会拿到两边的值,arr被取值时退化成int*,式子等同于:

    int* p = &arr[0]+1;	// int*和int相加指向下一个int*,即获得下一个元素的地址
    

    再举例赋值运算符:

    int* p = arr;
    

    =读取arr的值拷贝给p,因此退化成int*,再将地址值拷贝过去。

退化机制存在,没有进化机制,即例如下述代码是会报错的:

int arr1[3]{};
int *p = arr1;
int arr2[3] = p;	// int*不能赋值给int[3]

这也解释了为什么void fun(int data[3]);仍会视为接收一个int*类型的数据。

那什么场景是不读取数组内容的呢?常见的有三种:

  • 编译期运算符,例如很常见的sizeof

    sizeof(arr);
    

    其作用是编译期获取数组类型的大小,没有涉及运行时值的读取,不会退化。

    但另一种类似的字符串长度读取:

    strlen(arr);
    

    是根据运行时数组中的数据读取直至’\0’的出现计算返回的,与内容相关,因此会退化。

  • 字符常量初始化

    constexpr char arr[] = "hello";
    

    在c++中"hello"被视为const char[6]类型,等价于{'h','e','l','l','o','\0'}。

    不同于其他类型数组,字符串常量数组可以在赋值给同类型数组时不会退化,直接拷贝入arr数组所在空间中,这是编译器对字符串常量的特殊适配。

    相比其他类型进行类似操作是不可行的:

    int arr1[] = arr2;	// 报错,数组类型不能直接赋值给数组类型
    

    因为arr2会退化为指针,而指针不能进化为数组类型赋值给arr1。

    当然,如果接收字符串常量数组的不是同类型数组,那么也会退化:

    const char *p = "hello";	// "hello"退化为const char*类型后拷贝值到p
    
  • 取地址&

    &arr会获得一个指向数组的指针,即int (*)[3]类型(注意不是int* [ ]类型),取数组的地址不涉及其内的值,因此也不会退化。

或许有人注意到,我上面提到的一句话:

即int (*)[3]类型(注意不是int* [ ]类型)

[ ]中前者有数字,后者却没有,[ ]在类型声明写不写数字有区别吗?是有的。

看下述比较:

void fun1(int data[3]);	// 这里的3无意义
void fun2(int data[]);	// 跟上式等价

void fun3(int (*data)[3]);	// 这里的3是类型的一部分
void fun4(int (*data)[]);	// []不写数字,则仅可以接受int (*)[]类型,不会自动推导(c++20起可以接受推导)

在前面两个式子中,因为数组类型退化机制的存在,[ ]中写了数字也被视为int*类型,因此数字无意义。

而在后面两个式子中,因为data是数组指针,本身就是指针自然不会退化,指向一个int[3]类型的数组,此时3也是类型的一部分。

那么问题就来了,如果函数传参是多维数组怎么办?

❖ 多维数组类型的传参

看下面的代码:

void fun(int data[1][2]);	// 看似接收了一个1x2的int二维数组

但是就如前面提到的,传值不会直接传整个数组,必须先退化成指针,传指针的开销小。

在c/c++中二维数组是由一维数组存储的,比如元素data[2][1],在第一行第一列,那么其在一维数组中的存储位置为2x行宽+1,即行下标x行宽+列下标。

也就是二维数组可以看作是按行连续存储的数组,你可以视为每个元素是一个行数组。

那么int data[1][2]的退化思路就很明显了,其等价于int (*data)[2],原式子中第一个[ ]中的数字无意义,第二个数字有意义,即:

void fun1(int data[1][2]);
void fun2(int (*data)[2]);	// 等价于上式
void fun3(int data[][2]);	// 等价于上式

void fun4(int data[1][]);	// 报错
void fun5(int data[][]);	// 报错

因此,我们可以这么写来表示可以接收任意行宽的列宽为2的二维数组:

void fun1(int data[][2], int rows);

那要是希望两个维度都不写死,但int data[][]是不行的,要怎么写呢?

上面我们说过:

在c/c++中二维数组是由一维数组存储的

那实际上我们完全可以把二维数组当作一维int*类型传入,在函数内自行计算行下标x行宽+列下标即可。

void fun(int* arr, int rows, int cols);

这样这个函数就可以接收任意行和任意列规格的二维数组了。

但在使用上,仍然需要注意一下,

当我们创建了一个二维数组,直接传入上述fun时,这个二维数组只会退化到int (*)[3],不是我们期望的int*:

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

fun(arr,3,2);	// 报错
fun(&arr[0][0],3,2);	// 正确用法

综上,内置数组特别在多维数组方面麻烦多多,在可以的情况下用标准库vector替代会好很多(以后讲到STL再说罢)

13 Likes

从入门到入睡

真技术贴,点赞

睡我膝枕 :zany_face:

来了来了

指针坏!w

1 Like

这句话我觉得应该讲得更严谨一点,因为事实上并不会这么做

好厉害!

那这样

arr赋值到其他变量如果复制整个数组

1 Like

应该是const char *

1 Like

ok,忘了加const,不过不是*,在语义分析阶段会先当成[6],当然后续大部分场景下会退化为*,但这里要讲的是退化,所以还是写[6]

1 Like

还没入门就放弃 :bili_017:

靠我之前都没意识到这个问题,我想了想写了个例子加深我自己的理解

#include <cstddef>

template <size_t N>
constexpr int processString(const char (&str)[N]) {
    return N;
}

int main() {
    constexpr int a = processString("abc"); // ok
    auto c = "123";
    constexpr int b = processString(c); // error
}
1 Like

捉个虫:这个例子中的声明 void fun4(int (*data)[]); 实际上是合法的。其中,data 是一个指向未知长度的 int 数组的指针。与此同时,类型 T (*)[N](指向具有已知长度 N 的 T 数组的指针)可以隐式转换为 T (*)[]。示例如下:

void fun4(int (*data)[]);

extern int a1[];
extern int a2[5];
extern int a3[10];

int main() {
  fun4(&a1);
  fun4(&a2);
  fun4(&a3);
}
1 Like

感谢佬友捉虫!
函数声明确实是合法的,稍后我会更改。

有一点也提一嘴,

因为学的c++11,在之前的测试中发现c++11并不能达到下述的表述

如图报错均为无法推导到对应类型:


即必须类型一模一样,无法推导,写帖子的时候我记成在声明报错了 :rofl:

我注意到可能是你使用的是c++20标准,在11/17中应该还没有这个特性。

以上,再次感谢

1 Like

ao 是记混了,多维的声明该填数字的地方不填会报错,一维的声明是要一模一样就行不报错。

我说呢我记得在哪看到过声明有报错的 :melting_face:

c++的泛型不像 go 的 interface存储了 type 信息吗?

其实他这个泛型应该打个引号,C语言里通常会使用这个做泛型,C++的泛型是另外的东西
不过我也有在别的地方看到泛型指针这个说法

差不多,实际上我写的是泛型指针和泛型类型指针,连起来的。单论泛型应该是模板那边的事

void*不像go的any类型,那玩意附带有类型信息,这边就是裸地址,因此没有类似go的安全断言,是否安全全看程序员功力