C++之指针扫盲

1,094 阅读8分钟

前言

指针对于学习C/C++的人来说是一道必须迈过去的坎,就像学习九阳神功必须要打通任督二脉一样的道理。虽然说随着智能指针的普及,很少需要程序员再手动操作原始指针, 但是如果你连原始指针的都没学好,那你怎么可能用好智能指针呢?

无论是原始指针还是智能指针,要想用好它就一定要做到知其然,知其所以然

因为本文阅读对象是有了一定指针基础的童鞋,所以如果你对指针如果是处于一无所知的状态的话,建议先去温习下指针的基础知识,不然可能读起来会打击你求知的欲望。

指针为什么要有类型

是为了指针运算和取值。

当使用指针取值的时候需要知道怎么取值,比如按照多少个字节去取值,这是需要确定才能取到正确的值的,要知道用多少个字节去取就得知道指针的类型是什么。

我们知道指针的运算增加或者减少1意味着需要偏移指针所表示的类型的大小个字节数,比如说一个int字节的指针增加1,表示偏移4个字节(一般情况下int都是4个字节),所以这也是需要知道指针的类型。

指针和数组

本来从字面上来说指针和数组是八竿子打不着的,它们理应是井水不犯河水的,怎么就扯上了呢?我们经常听说数组指针、指针数组,这些都是什么意思呢?他们到底是指针还是数组呢?下面将一一为你解答。

指针数组,首先它是一个数组,数组里面的每个元素都是一个指针,例如比如int *p[4] 就是一个指针数组,因为运算符[]的优先级运算符*的优先级高,所以p优先和[]组成数组,然后*和类型int组合成数组元素的类型。 例如以下程序就是一个指针数组的示例:

main.c
#include <stdio.h>
int main(){
    char *str[3] = {
        "我是数组1",
        "我是数组2",
        "我是数组3"
    };
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

数组指针,首先它是一个指针,这个指针所指向的对象是数组,比如这个指针是p,那么通过解引用*p获得内容就是一个数组,例如int (*p)[4],主意带上括号, 通常数组指针也作为一个二维数组来使用。

二级指针

所谓的二级指针其实就是一个指向指针的指针,例如int **p就是一个二级指针,它内部存放的对象是一个指针,通过一次解引用获得的是内存存放的指针的地址,需要再次对这个内部的指针进行解引用才能获取到 这个真是内容的值。

理解起来有点绕,那么这个拗口的二级指针有什么作用呢?二级指针在C++中可能用的不多,但是在C中是经常使用的一把利器,它通常作为一个函数的参数,起到在函数内部对一个指针进行初始化的作用, 比如经典的音视频处理工具FFmpeg中就大量使用了二级指针。 以下例子展示如何通过二级指针对指针形式赋值:

main.cpp
void initP(int **p){
    *p = new int(10);
}

int main() {
    int *p = nullptr; // 一个空的指针
    initP(&p); // 通过二级指针初始化指针p
    std::cout << "*p的值:" << *p << endl;
    delete p;
    return 0;
}

可能在这里就有人和当初笔者刚接触C语言一样迷惑了,难道不能通过给函数传递一级指针给指针初始化吗?这是不行的,这是因为值传递的缘故,像深入探讨的童鞋们可以写个例子打印下实参的具体地址对比下研究下其背后的原理。

指针与多态绑定

我们都知道C++是一门面向对象的设计语言,支持多态就是它的一个重要特性之一,在学习C++类的相关知识的时候老师就告诉我们:*在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。*也就是 说使用通过父类的指针或引用就能按照实参的实际类型是父类还是子类调用不同的虚函数。

例如如以下代码:

main.cpp
class Base{
public:
    virtual void print() const{
        std::cout << "base print" << endl;
    }

    virtual ~Base(){

    }
};

class Child:public Base{
public:
    void print() const override{
        std::cout << "Child print" << endl;
    }
};

void testPrint(const Base &base){
    base.print();
}

int main() {
    Base a = Child();
    testPrint(a);// 打印Base print
    Child b = Child(); // 注意,不能写成Base b = Child(),否则打印的是Base的print
    testPrint(b); // 打印Child print
    Base *c = new Child(); // 指针,动态类型与静态类型不一致
    testPrint(*c); // 打印Child print

    Base &&r = Child(); // 表达式是右值引用,动态类型与静态类型不一致
    testPrint(r); // 打印Child print
    return 0;
}

为什么在上面的程序中变量a的实际类型是Child,但是函数testPrint内部调用的却是父类的打印方法呢?不是说引用会触发多态吗?函数testPrint也是通过引用传递的呀, 真是百思不得其jie呀。

要解开这个疑惑就得了解下静态类型和动态类型的知识了。静态类型在编译时总是已知的,首先静态类型是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型,动态类型直到运行时才可知。 如果变量在定义时表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致的,也就是声明时所指的类型,否则的话静态类型可能与动态类型不一致。

那么有了静态类型与动态类型的概念之后再结合注释看上面的示例代码是不是就有一种拨开云雾见青天的感觉了呢?

函数指针

函数指针顾名思义就是指向函数的指针,它的定义:函数指针是指向函数的指针变量。因此“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。

其声明方式是:

返回值类型 (*函数名) (参数)  

函数指针的一个重要用途就是作为函数的参数,用于在函数内部进行指针函数的调用,一般用作回调函数,比如在创建一个POSIX线程的就需要传递一个函数指针用于指明该线程做点什么事情。

以下代码展示了一个简单的函数指针的使用方法:

void testFunc(int a,int b,void (*func)(int c,int d) ){
    // do something
    func(a,b);
}

void callback(int a,int b){
    
}

int main() {
    testFunc(1,2,callback);
    return 0;
}

类成员指针

这里类成员指针表示的是指向类的某个对象的非静态成员的指针,而不是表示类成员变量的指针,首先需要区分好这是两个不同的概念,如果不能好好区分这两个概念的童鞋,需要再好好思考一下。

成员指针的类型囊括了类的类型以及成员的类型。当初始化一个这样的指针时,我们令其指向类的某个成员,但是不指定该成员所属的对象;直到使用成员指针时,才提供成员所属的对象。

和其他指针一样,在声明成员指针时我们也使用*来表示当前声明的名字是一个指针。与普通指针不同的是,成员指针还必须包含成员所属的类。下面是一个使用的示例:

class Person{
public:
    virtual void print() const{
        std::cout << "base print" << endl;
    }

    virtual ~Person(){

    }

public:
    string lastName;
    string firstName;
};

int main() {
    string Person::*p; // 声明了一个类成员指针
    p = &Person::firstName; // 成员变量的指针指向了Peron的name
    Person person;
    person.*p = "hello"; // 使用成员指针
    p = &Person::lastName; // 成员变量的指针指向了Peron的lastName
    person.*p = "world"; // 使用成员指针
    std::cout << person.firstName << " " << person.lastName << std::endl;
    return 0;
}

至于这个成员指针有什么用处,给笔者的感觉就是重新命了一个别名的感觉,甚至有点脱裤子放屁?但是存在即合理,不是没有用处,只是笔者见过那种场景而已吧。。。

除了有指向类成员变量的指针外还有指向类成员函数的指针,这里就不多展开探讨了!!!

关注我,一起进步,人生不止coding!!! 公粽号:思想觉悟