— c++11入门费曼学习副产物,如有谬误,望佬友指正,不胜感激 —
其他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*传入。一般注意下面两种即可:
| 类型 | 不能转化原因 | ||
|---|---|---|---|
| 函数指针 | 有跳转信息 | ||
| 类成员指针 | 一些面向对象特性的信息 |
![]()
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}; // 等价上行
零值:各个类型不同,如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*类型的数据。
那什么场景是不读取数组内容的呢?常见的有三种:
-
编译期运算符,例如很常见的
sizeofsizeof(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再说罢)
