• 『C++』基础知识点


    一、基础知识

    1、C++编译流程

    以Unix系统编译中间文件为说明:

    .cpp—(编译预处理)—>.ii—(编译)—>.s—(汇编)—>.o—(ld,连接)—>.out

    2、#include

    作用于编译预处理阶段,将被include文件抄送在include所在位置,并会在相应位置写出调用栈,生成中间文件.ii,该中间文件可读

    include文件加引号表示先从当前目录寻找索引,加尖括号表示从编译器指定根目录索引,Unix默认为"~//usr/include"目录

    3、定义、声明、头文件

    .h头文件中只应存放三种代码:

    函数声明:没有大括号,形如void fun()

    变量声明:extern 变量名

    class、结构体定义

    extern表示声明一个全局变量

    声明只是提示编译器,存在这个东西,并没有定义出实体,不定义直接调用会报错。

    4、标准头文件结构

    #ifndef HEADER_FLAG

    #define HEADER_FLAG

    /*头文件*/

    #endif

    这是为了防止多次include同一个头文件时,每次都抄送到预编译文件中,造成文件过大、循环导入或者结构体定义重复以致报错(声明重复问题不大)。

    5、默认参数

    在声明中写默认参数,不在定义中给默认参数。

    6、调用函数过程

    本地变量进入堆栈(未必初始化)

    函数参数进入堆栈

    返回地址计入堆栈

    返回值进入寄存器(运行函数)

    pop掉参数

    返回值进入堆栈(返回地址,所以要pop掉参数,堆栈先进后出)

    7、内联函数

    在编译阶段优化,省略上一小结中复杂的堆栈操作,效果如下,

    汇编(伪)优化如下,

    注意,inline 函数名实际是一个声明,而非定义所以不需要额外声明。实际上以空间换时间(编译会将函数插入调用位置),编译器如果发现函数递归或者过于巨大,可能会拒绝inline操作,函数较小可能被自动inline,建议就是小函数inline(2-3行),超过20行的就不要inline了。

    相比于宏,inline可以做类型检查,给出debug提示,下图中C++会提示double的f(a)和%d不匹配,C会直接给出一个奇怪的值,

     8、const

    初始化之后不可修改,值得注意的是下图这种,指针和const,到底是地址(指针)还是地址中的内容(对象)是const

    重点在于const和*的位置顺序,下述代码中2、3两句等价,

    且const变量不能传给其他非const的指针(因为这样有可能造成修改),

    函数和const

    函数虚参加const表示函数内部不可修改该变量,对输入无要求

    return const 对接收函数返回的类型无要求

    class和const

    const 对象,此时我们不能保证class方法是否修改成员变量,又不能限制函数不能使用(class就没有意义了)

    const 对象 or 成员变量是const,要求成员变量必须有初始值,因为事后无法赋值

    main文件(编译时可以感知类声明文件),类声明文件,类定义文件:声明、定义(两个位置都需要添加)函数时后面添加const关键字如,int fun() const。

    下图运行结果为"f() const",

    实际构成了重载,

    void f(A* this)

    void f(const A* this)

     9、字符串

    char *s = "Hello World";  // 将代码段的字符串地址直接付给指针,所以后面尝试修改会报错(代码段不可修改),

                 // 应该在开头改写为const char *s

    char s[] = "Hello World";  // 数组被写入堆栈,将代码段的字符串拷贝到堆栈

    10、引用

    char& r = c;  // 引用可以做左值

    相当于给c取了一个别名,此时c、r绑定到同一实体。

    int x;int y;

    int& a = x;

    int& b = y;

    a = b;  // 等价x=y

    注意,引用无法取地址,即 int&* r 的写法是错误的,不过相对的,int*& p 是没问题的,指针可以被引用。

    class的成员变量是引用时

    此时只能使用initializer list的方式初始化引用对应的变量,如果在{}中使用m_y=a则表示将a复制给m_y对应的变量。

    函数返回引用时

    return一个全局变量,

    这个引用表示变量,不表示值,所以最后一句表示赋值。

    11、中间结果

    相当于Python中的“_”,i*3这样的结果会作为const int类型临时保存。

    二、class入门

    1、变量

    Field,成员变量,作用域为class的对象,类的函数中可以直接使用;class本身不能拥有变量,理解为声明一个变量(函数和变量不同,函数属于class而不是对象);

    parameters,函数参数;

    local variables,本地变量,作用域为本函数;

     后两者完全相同,本地存储,出来作用域则不存在该变量。

    关键字 this:一个指针,为当前对象的指针(指该次调用成员函数的对象的指针),

    经由指针this区分调用成员函数的不同实例,其原理如下:使用'实例对象.成员函数'来调用等价于直接调用该函数并将对象指针作为首个参数输入,即:成员函数(this),原理和python一致,成员函数实际上有一个默认存在的参数输入,接收实例指针

    2、构造和析构

    在C++中,class实例化时成员变量不会初始化,仅仅寻找到一块足够大的地址(java会清空地址内数据)。VS会在debug时为未初始化空间填充0xcd,用于排查(0xcd0xcd在国标码中为‘烫’)。

    constructor:构造函数,初始化对象时自动执行(相当于python中的__init__)

    函数名和class名相同

    没有返回类型

    destructor:析构函数,退出对象所在scope时自动执行

    函数名为'~'加构造函数名

    没有返回类型

    不可以有参数

    有关‘{}’,表示scope,如下代码中,进入‘{’后会执行Tree的构造函数,退出‘}’前,本scope内资源回收,会自动执行析构函数

    数组、结构体、使用构建函数的class初始化方式对比:

    Y经由构建函数Y()间接将f、i赋值,顺便一提,数组b后面未指定元素会被初始化为0

    default constructor:无参数构建函数,见下右的第二行会报错,因为构建y2有两个元素,而第二个元素会调用default constructor,但实际上constructor需要参数,所以会报错:

    :

    3、scope和存储空间

    编译器在‘{}’开始的位置会分配好空间,而在运行到相关定义时才会真正的运行构造函数。

    如下图,某些情况下编译会出错,因为一旦goto成功,则x1不会被构建,相应的退出‘{}’时,析构函数执行会失败。

    4、动态分配空间

    new:制造对象,类似malloc;分配空间、调用构造函数(对于class),返回地址

    使用一张表,记录下每次申请的内存大小和对应的地址

    delete:收回空间,类似free;析构对象(对于class)、回收空间;它有两种用法,如下:

    delete p :普通用法

    delete[] p :一般来说new p[]时,需要使用这个,会将所有对象的析构函数分别调用,否则回收内存正常,但只调用指针直接指向的对象的析构

    5、访问控制

    public:任何人可以访问

    private:成员函数可以访问 ,注意对class来讲,同一个class不同对象可以互相访问私有变量,如下代码,p[0]是可以访问b的私有变量的

    friends:声名一个函数/class等,使之可以访问自己(本class的任何实例)的私有变量

    下面代码涉及两个知识点:1、friends声明在class内部;2、结构体可以前向声明(开头的X),用于在结构体Y定义中占位。

     

    protected:自己及子类可以访问

    6、struct vs class

    未指定访问控制属性的变量、函数,class默认为private,struct默认public

    7、初始化list

    初始化后才执行构造函数(大括号中语句)

    在大括号中赋值的话会先默认初始化变量,然后赋值;初始化list的方式直接用目标值初始化变量

    8、成员函数和inline

    在class内部给出了body的成员函数,视为内联函数。

    三、父类子类

    1、组合和继承

    组合:已有类作为新的类的成员

    继承:改造类,class B: public A {},意为B类为A类子类 

         父类的private,在子类中存在,但是不能直接访问(需要使用父类的public方法),需要使用protected声明。

    另一点值得注意的是,由于构造函数不可以直接调用, 调用父类的构造函数方式需要使用初始化list方法,而且必须最先构造父类(如果父类构造函数有参数),构造先父后子,析构先子后父:

    2、覆盖(override)、重载(overload)、隐藏

    overload

    在同一作用域中,函数名相同,参数列表不同,返回值可同可不同的函数,编译器会根据传入参数决定调用哪个函数,注意仅返回值不同不能构成overload关系。

    override

    又叫覆盖,是指不在同一个作用域中(分别在父类和子类中),函数名,参数个数,参数类型,返回值类型都相同,并且父类函数必须有virtual关键字的函数,就构成了重写(协变除外)。协变:协变也是一种重写,只是父类和子类中的函数的返回值不同,父类的函数返回父类的指针或者引用,子类函数返回子类的指针或者引用。

    virtual:子类的同名同参函数之间有联系(继承树中某一个函数是virtual的,子类的该方法都是virtual的)。

    重定义

    又叫隐藏,是指在不同的作用域中(分别在父类和子类中),函数名相同,不能构成重写的都是重定义(重定义的不光是函数,还可以是成员变量),隐藏和覆盖不同,被隐藏的父类成员可以通过子类.父类::成员的方式调用。

    3、向上造型upcasting

    子类对象可以被传给父类对象指针,如下图所示,

    这是由于C++的class类似于C的结构体,实际上是一个指针指向一块有特定内容排列顺序的内存,子类只会在父类的内存规划上向后扩充,不会更改父类已经规划好的部分。如果有子类方法隐藏了父类方法,向上造型后会隐藏失效,此时的对象指针仅能识别父类原有的模块。

    类似地,也有向下造型,不过可能会出错。

    Employee是Manager的父类

    4、多态

    本小节摘抄自文章:C++ 多态的实现及原理

    想要理解多态,需要区分函数和虚函数的区别(内存上的位置差异),并要理解向上造型的概念,了解了前面两点,就了解了动态绑定、静态绑定的区别,对于多态产生的种种现象就能够从机理上给出自己的解释。

    virtual虚函数内存机制

    上面提到过,virtual是让子类与父类之间的同名函数有联系,这就是多态性,实现动态绑定。

    任何类若是有虚函数就会比正常类大一点,所有有virtual的类的对象里面最头上会自动加上一个隐藏的,不让我知道的指针,它指向一张表,注意,该表对于同一个class的不同对象是同一个,不同class(指父类子类)的表不同。这张表叫做vtable(虚表),vtable里是所有virtual函数的地址,对于下面代码,

    class Shape {
    public:
        Shape();
        virtual  ~Shape();
        virtual void render();
        void move(const pos&);
        virtual void resize();
    protected:
        pos center;
    };
    

     其内存分布如下:

    我们看一下其子类的内存分布:

    class Ellipse : public Shape{
    public:
        Ellipse (float majr, float minr);
        virtual void render();
    
    protected:
        float major_axis;
        float minor_axis;
    };
    

     

    这里的resize沿用了shape的成员函数。

    多态实现逻辑

    看如下代码,

    #include "stdafx.h"
    #include <iostream> 
    #include <stdlib.h>
    using namespace std; 
    
    class Father
    {
    public:
        void Face()
        {
            cout << "Father's face" << endl;
        }
    
        void Say()
        {
            cout << "Father say hello" << endl;
        }
    };
    
    
    class Son:public Father
    {
    public:     
        void Say()
        {
            cout << "Son say hello" << endl;
        }
    };
    
    void main()
    {
        Son son;
        Father *pFather=&son; // 隐式类型转换
        pFather->Say();
    }
    

    输出的结果为:

    我们在main()函数中首先定义了一个Son类的对象son,接着定义了一个指向Father类的指针变量pFather,然后利用该变量调用pFather->Say().估计很多人往往将这种情况和c++的多态性搞混淆,认为son实际上是Son类的对象,应该是调用Son类的Say,输出"Son say hello",然而结果却不是.

    从编译的角度来看:

    c++编译器在编译的时候,要确定每个对象调用的函数(非虚函数)的地址,这称为早期绑定,当我们将Son类的对象son的地址赋给pFather时,c++编译器进行了类型转换,此时c++编译器认为变量pFather保存的就是Father对象的地址,当在main函数中执行pFather->Say(),调用的当然就是Father对象的Say函数

    从内存角度看:

        

    Son类对象的内存模型如上图

    我们构造Son类的对象时,首先要调用Father类的构造函数去构造Father类的对象,然后才调用Son类的构造函数完成自身部分的构造,从而拼接出一个完整的Son类对象。当我们将Son类对象转换为Father类型时,该对象就被认为是原对象整个内存模型的上半部分,也就是上图中“Father的对象所占内存”,那么当我们利用类型转换后的对象指针去调用它的方法时,当然也就是调用它所在的内存中的方法,因此,输出“Father Say hello”,也就顺理成章了。

    正如很多人那么认为,在上面的代码中,我们知道pFather实际上指向的是Son类的对象,我们希望输出的结果是son类的Say方法,那么想到达到这种结果,就要用到虚函数了。

    前面输出的结果是因为编译器在编译的时候,就已经确定了对象调用的函数的地址,要解决这个问题就要使用晚绑定,当编译器使用晚绑定时候,就会在运行时再去确定对象的类型以及正确的调用函数,而要让编译器采用晚绑定,就要在基类中声明函数时使用virtual关键字,这样的函数我们就称之为虚函数,一旦某个函数在基类中声明为virtual,那么在所有的派生类中该函数都是virtual,而不需要再显式地声明为virtual。

    代码稍微改动一下,看一下运行结果

    #include "stdafx.h"
    #include <iostream> 
    #include <stdlib.h>
    using namespace std; 
    
    class Father
    {
    public:
        void Face()
        {
            cout << "Father's face" << endl;
        }
    
        virtual void Say()
        {
            cout << "Father say hello" << endl;
        }
    };
    
    
    class Son:public Father
    {
    public:     
        void Say()
        {
            cout << "Son say hello" << endl;
        }
    };
    
    void main()
    {
        Son son;
        Father *pFather=&son; // 隐式类型转换
        pFather->Say();
    }
    

    我们发现结果是"Son say hello"也就是根据对象的类型调用了正确的函数,那么当我们将Say()声明为virtual时,背后发生了什么。

    编译器在编译的时候,发现Father类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表(即 vtable),该表是一个一维数组,在这个数组中存放每个虚函数的地址,

    那么如何定位虚表呢?编译器另外还为每个对象提供了一个虚表指针(即vptr),这个指针指向了对象所属类的虚表,在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向了所属类的虚表,从而在调用虚函数的时候,能够找到正确的函数,对于第二段代码程序,由于pFather实际指向的对象类型是Son,因此vptr指向的Son类的vtable,当调用pFather->Son()时,根据虚表中的函数地址找到的就是Son类的Say()函数.

    正是由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的,换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数,那么虚表指针是在什么时候,或者什么地方初始化呢?

    答案是在构造函数中进行虚表的创建和虚表指针的初始化,在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否还有继承者,它初始化父类对象的虚表指针,该虚表指针指向父类的虚表,当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。

  • 相关阅读:
    Hadoop学习之编译eclipse插件
    js堆栈溢出错误
    java——推断日期是否在今天之前
    AlertDialog.Builder中的setMultiChoiceItems中的事件处理
    Qemu之Network Device全虚拟方案二:虚拟网卡的创建
    【Android Tricks 6】ViewPager首页与尾页的滑动动作响应
    JFinal开发web项目出现故障小记
    HDU-4407-Sum(容斥原理)
    自己动手写CPU之第五阶段(3)——MIPS指令集中的逻辑、移位与空指令
    待字闺中之巧妙排序分析:
  • 原文地址:https://www.cnblogs.com/hellcat/p/9669998.html
Copyright © 2020-2023  润新知