C++

编辑 / C/C++ / 发布于2020-07-21 / 更新于2023-03-16 / 阅读 109

virtural继承

class AAAA { int  a; };
class BBBB :public virtual AAAA { int b; char a; };
class CCCC :public virtual AAAA { int c; };
class DDDD :public BBBB, public CCCC { int d; };

内存分布:

AAAA

1>class AAAA	size(4):
1>	+---
1> 0	| a
1>	+---

BBBB

1>class BBBB	size(20):
1>	+---
1> 0	| {vbptr}   虚基类指针,占8位
1> 8	| b
1>  	| <alignment member> (size=4)
1>	+---
1>	+--- (virtual base AAAA)    虚基类中的a,占4位
1>16	| a
1>	+---
1>BBBB::$vbtable@:  虚基类表,偏移16的位置为虚基类AAAA
1> 0	| 0
1> 1	| 16 (BBBBd(BBBB+0)AAAA)
1>vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
1>            AAAA      16       0       4 0

CCCC

1>class CCCC	size(20):
1>	+---
1> 0	| {vbptr}
1> 8	| c
1>  	| <alignment member> (size=4)
1>	+---
1>	+--- (virtual base AAAA)
1>16	| a
1>	+---
1>CCCC::$vbtable@:
1> 0	| 0
1> 1	| 16 (CCCCd(CCCC+0)AAAA)
1>vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
1>            AAAA      16       0       4 0

DDDD

1>class DDDD	size(44):
1>	+---
1> 0	| +--- (base class BBBB)
1> 0	| | {vbptr}
1> 8	| | b
1>  	| | <alignment member> (size=4)
1>  	| | <alignment member> (size=4)
1>	| +---
1>16	| +--- (base class CCCC)
1>16	| | {vbptr}
1>24	| | c
1>  	| | <alignment member> (size=4)
1>  	| | <alignment member> (size=4)
1>	| +---
1>32	| d
1>  	| <alignment member> (size=4)
1>	+---
1>	+--- (virtual base AAAA)
1>40	| a
1>	+---
1>DDDD::$vbtable@BBBB@:
1> 0	| 0
1> 1	| 40 (DDDDd(BBBB+0)AAAA)
1>DDDD::$vbtable@CCCC@:
1> 0	| 0
1> 1	| 24 (DDDDd(CCCC+0)AAAA)
1>vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
1>            AAAA      40       0       4 0

virtual 虚函数

虚函数是指一个类中你希望重写(override)的成员函数,当你用一个基类指针或引用,指向一个继承类对象的时候,调用一个虚函数时, 实际调用的是继承类的版本。

class Animal {
public :
    void  Print()
    { 
        cout << "Animal" << endl; 
    }
};
class Dog: public Animal
{
public:
    void  Print() 
    {
        cout << "Dog" << endl;
    }
private:
};
int main(int argc, char** argv)
{
    Animal* d = new Dog();
    d->Print();
}

没有使用virtual时,函数的输出的是Animal

class Animal {
public :
    virtual void  Print()
    { 
        cout << "Animal" << endl; 
    }
};
class Dog: public Animal
{
public:
    void  Print() 
    {
        cout << "Dog" << endl;
    }
private:
};
int main(int argc, char** argv)
{
    Animal* d = new Dog();	
    d->Print();
}

内存布局:

1>class Animal	size(8):
1>	+---
1> 0	| {vfptr}   虚表指针
1>	+---
1>Animal::$vftable@:    虚函数表
1>	| &Animal_meta
1>	|  0
1> 0	| &Animal::Print    可能有多个虚函数
1>Animal::Print this adjustor: 0

1>class Dog	size(8):
1>	+---
1> 0	| +--- (base class Animal)
1> 0	| | {vfptr}
1>	| +---
1>	+---
1>Dog::$vftable@:
1>	| &Dog_meta
1>	|  0
1> 0	| &Dog::Print
1>Dog::Print this adjustor: 0

使用virtual后,函数的输出是Dog

没有使用virtual之前,C++对成员函数使用静态绑定,函数的调用地址在运行前就已经固定。不考虑指针指向的内容,对于什么类型的指针就调用什么类型的函数。
对于虚函数,每个派生类对象都有一个指向虚函数表的指针,访问任何虚函数都是间接通过这个指针进行的,之所以要用虚函数表,是因为调用一个虚函数的哪个版本是在运行过程(调用时指针所指的对象)才能确定的(动态绑定)。

这也可以叫运行时多态。

{% note warning %}
因此特性,若基类的析构函数是non-virtual的,容易造成局部销毁,导致资源泄漏。非常危险。
{% endnote %}

编译期多态:对模板参数而言,多态是通过模板具现化和函数重载解析实现的。以不同的模板参数具现化导致调用不同的函数,这就是所谓的编译期多态。

class Dog
{
public:
    void shout(){ cout << "Dog"<<endl; }
};
class Cat
{
public:
    void shout(){ cout << "Cat"<<endl; }
};

template <typename T>
void  Animal(T & t)
{
    t.shout();
}

int main()
{
    Dog dog;
    Cat cat; 
    animalShout(dog);
    animalShout(cat);   
}

在编译之前,函数模板中t.shout()调用的是哪个接口并不确定。在编译期间,编译器推断出模板参数,因此确定调用的shout是哪个具体类型的接口。不同的推断结果调用不同的函数,这就是编译器多态。

virtural继承下的虚函数

class Base {
public:
    int x;
    virtual void func() {  };
    virtual void func1() {  };
    virtual void func2() {  };
};
class Derive:public virtual Base {
public:
    int y;
    virtual void func1() {  };
    virtual void func2() {  };
    virtual void func3() {  };
};

内存布局

1>class Base	size(16):
1>	+---
1> 0	| {vfptr}
1> 8	| x
1>  	| <alignment member> (size=4)
1>	+---
1>Base::$vftable@:
1>	| &Base_meta
1>	|  0
1> 0	| &Base::func
1> 1	| &Base::func1
1> 2	| &Base::func2
1>Base::func this adjustor: 0
1>Base::func1 this adjustor: 0
1>Base::func2 this adjustor: 0
1>class Derive	size(40):
1>	+---
1> 0	| {vfptr}
1> 8	| {vbptr}
1>16	| y
1>  	| <alignment member> (size=4)
1>	+---
1>	+--- (virtual base Base)
1>24	| {vfptr}
1>32	| x
1>  	| <alignment member> (size=4)
1>	+---
1>Derive::$vftable@Derive@:
1>	| &Derive_meta
1>	|  0
1> 0	| &Derive::func3
1>Derive::$vbtable@:
1> 0	| -8
1> 1	| 16 (Derived(Derive+8)Base)
1>Derive::$vftable@Base@:
1>	| -24
1> 0	| &Base::func
1> 1	| &Derive::func1
1> 2	| &Derive::func2
1>Derive::func1 this adjustor: 24
1>Derive::func2 this adjustor: 24
1>Derive::func3 this adjustor: 0
1>vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
1>            Base      24       8       4 0

const

  • const在指针左边时,指针指向的内容是const。

  • const在指针右边时,指针是const指针

  • 当const在函数名前面的时候修饰的是函数返回值。

  • 在函数名后面表示是常成员函数,该函数不能修改对象内的任何成员的值,如果需要修改,要在变量前面加mutable关键字。

  • 事实上,在某种条件下,const仍可能被修改,可见Effective C++ P22。

explicit

explicit关键字的作用就是防止类构造函数的隐式自动转换,只能用于修饰只有一个参数的类构造函数,与默认的implicit(隐式)相对。构造函数只有一个参数时, 那么在编译的时候就会有一个缺省的转换操作:将该构造函数对应数据类型的数据转换为该类对象。

class Demo { public: Demo(param) };
Demo d1(param);
Demo d2 = param;    //可行,相当于Demo d1(param);

class Demo { public: explicit Demo(param) };
Demo d1(param);
Demo d2 = param;    //不可行,explicit关键字禁止了这种隐式转换

static

在c++的类中,对于static成员变量是否是private数据并没有影响,因为设定static成员变量初值时,不受任何存取权限的束缚。在VS2019中对于类内的静态成员变量是由访问控制的,对于private的静态成员变量,只能在初始化时在类外操作。
static成员变量需要进行初始化操作。初始化时要带上类型名,并且不要带static。static成员变量是在初始化(而不是在类声明时候)才定义出来的。如果没有初始化操作,会产生链接错误。例如 ~ typename classname::membername; ~

lambda

语法形式如下:
[capture](parameters)->return-type

capture:

  • 空。没有使用任何函数对象参数。
  • = 。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
  • &。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。
  • this。函数体内可以使用Lambda所在类中的成员变量。
  • a。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。
  • &a。将a按引用进行传递。
  • a, &b。将a按值进行传递,b按引用进行传递。
  • = ,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递。
  • &, a, b。除a和b按值进行传递外,其他参数都按引用进行传递。

Others

  1. C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。

  2. 类内成员变量总是以其声明顺序被初始化,

  3. C++对于不同编译单元(源文件)的non-local static(文件内,所有函数以外的定义的对象)对象的初始化顺序无明确定义。为了避免这个问题,可以通过函数调用的方式(返回一个local static对象),将non-local static对象改为函数内部的static对象。

  4. 构造函数最好使用成员初值列赋值。

     Construct():value1(),value2(),value3(){}
    

    成员初值列赋值时,赋值顺序由class中成员的生命顺序决定,不是由初始化列表中的顺序决定。初始化列表的赋值在构造函数内的代码执行前执行。

  5. C++编译器会自动给空类创建构造函数、赋值函数和析构函数。(当这些函数被调用时)

     class Empty{};
    

    等价于

     class Empty
     {
         public:
             Empty(){...}
             Empty(const Empty& rhs){...}
             ~Empty(){...}
    
             Empty& operator=(const Empty& rhs){...}
     }
    

    想要阻止这一自动创建,可以将这些函数声明为private,并且故意不实现他们。

  6. 函数中通过值传递传递对象时,若一个派生类被视为基类传递,基类的构造函数会被调用,派生的特化性质被切掉。

  7. 函数调用,名称查找法则以及实参取决值查找规则。

  8. 用户可以全特化std内容template,但不允许添加任何新的其他东西,在调用时,优先选择特化版本。 见Effective C++ P107。

  9. 在使用依赖于模板参数的嵌套从属类型时,要在它前面放置关键字typename (在使用嵌套从属类型时,这个名称被假设为非类型。在前面加上typename告诉C++,这个是类型),但不能在基类列或者成员初始列前加typename。

  10. C++往往拒接在继承的模板化基类内寻找继承来的名称(方法),因为模板化基类又可能被特化,而特化版本的基类可能不含有此名称。必须要使用时可以在基类函数调用前加上this-> 或者BaseClass<TemplateName>:: 或在使用前添加using BaseClass<TemplateName>::FuncName;

  11. 函数返回局部变量时,返回的并不是这个局部变量,而是这个局部变量的一个拷贝。

C