首页 > 技术文章 > 【原创】浅谈指针(四)

jisuanjizhishizatan 2021-10-05 11:56 原文

前几篇文章的链接:
浅谈指针(一)https://www.cnblogs.com/jisuanjizhishizatan/p/15365167.html
浅谈指针(二)https://www.cnblogs.com/jisuanjizhishizatan/p/15365823.html
浅谈指针(三)https://www.cnblogs.com/jisuanjizhishizatan/p/15367297.html

前言

“浅谈指针”系列的大概是最后一篇了吧。如果以后我再发现一些其他用法,也会开几篇新文章,不过短时间内应该就不会更新了吧。

从何说起呢?上次我们把free和delete讲完,delete中如果是delete一个数组,那么需要这样写:
delete []p;

这里使用了空的方括号。那么,C++里,还有什么地方使用空方括号呢?我们一起探究一下。

空方括号[]的使用

初始化数组

假设我们要开发一个日历软件,其中必定要保存每个月的天数。这些数值最好以常量数组的形式保存,我们可以这样写:
const int Days[]={0,31,28,31,30,31,30,31,31,30,31,30,31};

确实,我们可以往那个方括号中写上13,但是我们也可以不写,这时候,编译器会自动确定元素个数。

函数参数

我们这次编写一个函数,形式为print(array,size),用于输出array数组的内容。

void print(int array[],int size){
    for(int i=0;i<size;i++){
        cout<<array[i];
    }
}

在函数的参数之中,我们使用空的方括号来说明这是一个数组。如果你在方括号中手动定义元素个数,例如void print(int array[10],int size),元素个数10也会被编译器无视。
这和本文的主题——指针有何关系呢?我们来看看。
实际上,C++不支持把数组当作参数传递。看似我们print函数传递的是数组,实际上,传递的是“指向数组首元素的指针”。也就是说,我们的参数int array[]和参数int *array是等价的。当往参数传递数组时,数组会被退化为指针。

有人会想,我们可以直接使用sizeof输出,就不需要调用方指定size参数了。很可惜,这种写法是错误的。

#include<iostream>
using namespace std;
void print(int array[]){
    for(int i=0;i<sizeof(array);i++){
        cout<<array[i];
    }
}
int main(){
    int a[5]={1,2,3,4,5};
    print(a);
}

输出结果一定不是12345。因为,array在函数中,已经不再是数组了,而是指针,因此array中保存的是地址。在32位系统上,sizeof(array)是4。在64位系统上,sizeof(array)是8。不管怎样,输出的一定不是我们期望的结果。
因此,当我们开发此类函数的时候,一定要注意sizeof的问题。当然,我们可以把数组作为结构体的成员进行传递,但是这样做速度将会减慢(因为要复制整个数组元素而不是一个地址)。

类和指针

构造函数和析构函数

在阅读文章之前,想必大家都对C++的类,构造函数和析构函数有所了解。
关于构造函数和析构函数,可以看我之前的一篇文章:https://www.cnblogs.com/jisuanjizhishizatan/p/15313713.html
构造函数和析构函数的定义:

构造函数就是在一个类被建立的时候自动执行的函数。
析构函数就是在一个类被销毁的时候自动执行的函数。

new和delete

当执行new的时候,分配一个类的内存,会自动执行构造函数。
当执行delete的时候,释放类的内存,就会自动执行析构函数。
我们看如下代码:

#include<bits/stdc++.h>
using namespace std;
class A{
public:
  int a;
  A(){
    cout<<"created"<<endl;
  }
};
A *test;
int main(){
  cout<<"main"<<endl;
  test=new A;
}

一般来说,在类声明的时候就调用构造函数了,test是全局变量,因此一定在main之前执行,输出created。
但是,由于test是指针,直到new的时候,test才被分配内存,因此在main之后输出。
析构函数的原理类似。

malloc

如果把上面代码的new换为malloc,程序会输出什么?答案是只输出main。
有人可能会很奇怪,不就是把new换成malloc吗?其实,只有new可以自动调用构造函数,malloc不会调用构造函数,因此,C++中一般更常用new而不是malloc。

强制转型问题

上文提到了malloc。那么我就顺便说点关于malloc强制转型的问题吧。在C语言中,由于没有new,只能用malloc分配内存。例如下面的语句:
p=malloc(100*sizeof(int));

malloc的返回值是void*。在C语言里,任何数都可以赋值给void*,void*也可以赋值给任何指针类型。因此,上面的代码理所应当这样写。如果使用强制转换,就显得没有必要:
p=(int*)malloc(100*sizeof(int));

并且《征服C指针》这样写:

C语言默认把没有定义的函数的返回值解释为int。假设忘记写了#include<stdlib.h>,又对malloc的返回值作了强制转型,C编译器可能不会发出警告。那些运气好,现在还能跑起来的程序,如果迁移到了指针的大小完全不同的机器上去,应该就跑不起来了。

C语言中,一般不对返回值作强制转型。然而C++就不一样了。我们尝试执行不带强制转型的malloc,结果是:
error: invalid conversion from 'void*' to 'int*' [-fpermissive]

C++不允许把void*的指针赋值给其他类型。因此,如果要使用malloc,必须使用强制转型。相比较而言,还是new来的方便。
顺便提一句,NULL在stdio.h有些时候定义为(void*)0(当然更常见的是直接定义为0,没有void*),因此这种情况下就不能把NULL赋值给其他指针类型。对此C++规定把NULL直接定义为常量0,并专门拓展了nullptr。

nullptr

nullptr和C语言的NULL完全等价。

NULL的问题

假设有两个重载函数

void f(int i);
void f(int* i);

f(NULL);

执行最后一行的时候,由于0可以解释为指针也可以解释为整数,两个重载都可以匹配,导致出错。

nullptr

nullptr完全可以代替NULL赋值给指针,表示空地址。但是,不能把nullptr赋值给普通整数,
int n=nullptr;是非法的。
如果上面重载例子中把nullptr传入f的参数,结果会调用指针版本的f,这样不容易造成二义性。

完。

推荐阅读