C++有用的基础知识

有用的基础知识

基础不牢,地动山摇

1.对象构造时执行的次序问题

  • 一个类的实例化过程往往是发生了如下过程:
    1. 若存在基类,则基类先初始化。
    2. 若存在成员变量,则按照定义先后的顺序初始化。
  • 对象的析构顺序则取之相反,是从外到内、从后到前发生。
  • 如代码所示,定义了4个类,A、B、C、Base,其中C继承自Base,A、B、Base基本只输出基本构造、析构与赋值信息,定义如下
查看类A、B、Base的定义代码
class Base
{
public:
    Base() { cout << "base construct\n"; };
    ~Base(){cout << "base destory\n";}
};
class A
{
public:
    A()
    {
        cout << "A construct\n";
    }
    A(const A &a)
    {
        cout << "A copy construct\n";
    }
    ~A(){cout << "A destory\n";}
    void operator=(const A &a)
    {
        cout << "A assign function\n";
    }
};
class B
{
public:
    B()
    {
        cout << "B construct\n";
    }
    B(const B &b)
    {
        cout << "B copy construct\n";
    }
    ~B(){cout << "B destory\n";}
    void operator=(const B &b)
    {
        cout << "B assign function\n";
    }
};
查看测试1
class C : public Base
{
public:
    B b;
    A a;
    C(){};
    ~C(){cout << "C destory\n";};
};
int main()
{
    C c;
    return 0;
}

运行结果:
base construct
B construct
A construct
C destory
A destory
B destory
base destory
- 在使用初始化列表时:
查看测试2 ```cpp class C : public Base { private: B _b; A_a; public: C(A& a,B& b):_a(a),Base(){ _b=b; }; ~C(){cout << "C destory\n";}; }; int main() { A a; // 1 B b; // 2 C c(a,b); return 0; } 结果如下: --注释掉的结果是 1 2 代码生成的 //A construct //B construct base construct B construct A copy construct B assign function C destory A destory B destory base destory //B destory //A destory ```
  • 根据结果有几点可以得出:
    1. 基类对象仍然是最先构建的。
    2. 初始化列表不能改变类成员初始化顺序。C类中成员 _b 永远在 _a 前面。
    3. 在初始化列表中初始化的话,会调用复制构造函数。
    4. 在函数体中初始化会再次调用赋值函数,且是最后执行。
    5. 当需要对成员变量初始化特定值时,优先使用初始化列表方法。避免先初始化后再赋值的资源浪费行为。
  • 注意:静态成员变量的初始化顺序和析构顺序是一个未定义的行为。
    • 静态成员变量不初始化则不会分配内存,无法访问。调用就会崩溃。
  • 从C++11开始,除 static 数据外,基本都允许在声明处初始化成员变量。但有以下特例:
    • 被常量表达式constexpr修饰的static可以在声明处初始化。
    • const static 整型可以声明处初始化。整型有short、int、long等)。
  • 在C++11以前,除枚举类型和 const static 整型外,不可以声明处初始化。
    • const non-static类型必须用初始化列表语法初始化。

2.虚函数探究

  • 虚函数是指被virtual关键字修饰的成员函数,作用是用来实现多态性。可为不同数据类型的实体提供统一的接口。
    • 虚函数的多态性具体的讲是将函数名动态绑定到函数入口地址(动态联编),而普通的成员函数在编译时就确定了(静态联编)。
  • 静态函数不可以声明为虚函数,同时也不能被constvolatile关键字修饰。
  • 构造函数也不可以声明为虚函数。同时除了inline|explicit之外,构造函数不允许使用其它任何关键字。
  • 只要一个类有可能会被其它类所继承, 就应该声明虚析构函数。
  • 虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。
  • 同一个类的各个实例化对象共用一个虚函数表。一个类继承多个基类时,有一个虚函数表,多个虚函数指针。
  • 虚函数运行过程
    • 当一个类实例化时,若存在虚成员函数,则会在构造函数中初始化一个虚表的指针vptr和对应的虚函数表vtable
      • 虚表存储着所有虚函数的函数指针,通过地址偏移来调用对应函数。vptr虚表地址在类地址的前8字节。
    • 当使用基类的指针或者引用指向派生类虚函数时,会调用指向或者引用的对象的类型。
      • 首先获取虚表地址vptr,在虚表中vtable得到需要的函数指针,最后进行调用。
  • 派生类会继承基类的虚表内容(各函数指针),当新增或者修改时修改该表即可,相同的虚函数的地址不会改变。
virtuallTest.cpp
#include <iostream>
#include <new>

using namespace std;
class A
{
private:
    int a=0;
public:
    virtual void speak()
    {
        cout << "A: hello!!\n";
    }
};

class B : public A
{
public:
    void speak()
    {
        cout << "B: hello--\n";
    }
};
class C : public A
{
};

typedef void (*Fun)(void);
int main()
{
    A *a = new A;
    B *b = new B;
    C *c = new C;
    // a ->A*指针; (int64_t*)a ->取前8位字节; *(int64_t*)a  ->前八个字节的指针解引用,虚指针
    // (int64_t*)*(int64_t*)a ->虚表前8位字节;*((int64_t*)*(int64_t*)a) ->虚表第一个项的指针解引用,第一个虚函数指针
    cout << "基类A的虚表地址:" << *(int64_t *)a << ", 第一个虚函数地址0x" << hex << *((int64_t *)*(int64_t *)a) << " \n";
    cout << "派生类B的虚表地址:" << *(int64_t *)b << ", 第一个虚函数地址0x" << hex << *((int64_t *)*(int64_t *)b) << " \n";
    cout << "派生类C的虚表地址:" << *(int64_t *)c << ", 第一个虚函数地址0x" << hex << *((int64_t *)*(int64_t *)c) << " \n";

    Fun fun = (Fun) * ((int64_t *)*(int64_t *)a);
    fun(); //
    A *spearker1 = (A *)b;
    spearker1->speak();

    delete a;
    delete b;
    delete c;
    return 0;
}
// 输出:
//     基类A的虚表地址:4330995984, 第一个虚函数地址0x10225b120 
//     派生类B的虚表地址:10225c138, 第一个虚函数地址0x10225b190 
//     派生类C的虚表地址:10225c168, 第一个虚函数地址0x10225b120 
//     A: hello!!
//     B: hello--

3.右值引用

变量、变量名、左值、右值、右值引用、移动构造、移动赋值

  • 变量与变量名:

    • 变量是内存中的一块区域,是可供程序操作的存储空间。这块区域的值(内容)一般是可以“变”的。(提供一座房子,但住谁不管)

    • 变量名是用来标识一块内存区域(变量)。变量名是不会被存储的,可以观察到,在对应汇编中是不存在变量名的。

    • 一般说变量,通常指两者的结合。也就是,一个提供具名的、可供程序操作的存储空间

    • 变量初始化与赋值

      void fun()
      {
          int b=1;
          b=2;
      }
      
      fun():
              push    rbp
              mov     rbp, rsp
      
              mov     DWORD PTR [rbp-4], 1
              mov     DWORD PTR [rbp-4], 2
      
              nop
              pop     rbp
              ret
      
  • 在C++中“对象”和“变量”一般可以互换使用,变量是左值,但存储内容是右值。

  • 当一个对象(变量)被用作右值时(包含纯右值和将亡值),用的是对象的值(内容);当对象被用作左值时,用的是对象的身份(在内存中的位置),一般可以修改其存储内容。

  • 不同运算符对元算对象要求也各不相同,有的要左值对象,有的要右值对象。返回结果也有差异,有的得到左值结果,有的是右值结果。

  • 一个原则是在需要右值的地方可以用左值替代,但是不能把右值当成左值(位置)使用。当一个左值被当成右值使用时,实际上用的是他的内容(值)。

  • 引用,是某个已存在变量的另一个名字。

  • 右值引用:也就是必须绑定到右值(纯右值,将亡值)的引用。通过 && 来获得右值引用。

    • 可绑定临时对象上或字面常量,延长其生命期。
    • 不能将一个右值引用绑定到一个右值类型的变量上。
  • 右值引用有两大作用:移动语义完美转发。能进一步优化性能。

    • 移动语义可减少不必要的复制,进而提升性能。如移动构造函数、移动赋值函数等。
    • 完美转发,是将一个或者多个实参连同类型不变地转发给其他函数,保持被转发实参的所有性质(是否const以及是左值还是右值)。forward函数可以实现。
  • 可以将一个const的左值引用绑定到右值上。

lrvalue.cpp
#include <iostream>
#include <string>
using namespace std;
class A
{
private:
    int *ptr;
public:
    A()
    {
        ptr = new int(-1);
    };
    ~A() { delete ptr; };
    // 复制构造
    A(const A &a) : ptr(new int(*a.ptr))
    {
        cout << "复制构造" << endl;
    }
    // 注意没有const  参数对象指针复制为空,即把该对象内存的拿来用,不用另外开辟新内存
    A(A &&a) : ptr(a.ptr)
    {
        a.ptr = nullptr;
        cout << "移动构造" << endl;
    }
    // 
    A &operator=(A &&a)
    {
        if (this == &a)
            return *this;
        // 注意要释放拥有的内存
        delete ptr;

        ptr = a.ptr;
        // 原对象a不能再使用了
        a.ptr = nullptr;
        cout<<"移动赋值"<<endl;
        return *this;
    }
};
int main()
{
    //右值引用  变量a是一个左值,可以取地址等各种操作
    //相当于延长了右值11的生命周期
    int &&a = 11;
    int *p = &a;
    *p += 1;           //也可以对其进行操作
    cout << a << endl; //输出 12

    //可以将一个const的左值引用绑定到右值上
    const int &b = 20;

    int m = 8;
    int n = 22;
    // int &&c = m;    //不允许将右值引用绑定到左值上

    // 可以将左值显示转换成对应的右值引用
    // 如move()函数之后应该只对m进行赋值或销毁操作
    int &&c = std::move(m);

    int &&d = std::move(n);
    m = std::move(d);
    cout << m << endl;

    // 普通构造
    A obj0;

    // 复制构造  会开辟新内存,并复制有关值
    A obj1(obj0);

    // std::move()强制将左值转换成右值
    // 使用移动构造后,不能再对obj0操作,因为内存已经给obj2了
    // 移动构造,不用申请新内存,也能减少不必要的复制
    A obj2(std::move(obj0));

    // A()是匿名对象,右值
    obj2 = A();

    return 0;
}
参考链接: https://www.cnblogs.com/catch/p/3500678.html

动手打一遍,才能记得住。 -- 鲁迅

posted @ 2022-03-05 22:26  Oniisan_Rui  阅读(99)  评论(0)    收藏  举报