2022-03-05 22:26阅读: 77评论: 0推荐: 0

C++有用的基础知识

有用的基础知识

基础不牢,地动山摇

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

  • 一个类的实例化过程往往是发生了如下过程:
    1. 若存在基类,则基类先初始化。
    2. 若存在成员变量,则按照定义先后的顺序初始化。
  • 对象的析构顺序则取之相反,是从外到内、从后到前发生。
  • 如代码所示,定义了4个类,A、B、C、Base,其中C继承自Base,A、B、Base基本只输出基本构造、析构与赋值信息,定义如下
查看类A、B、Base的定义代码
copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
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
copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
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
copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
#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.右值引用

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

  • 变量与变量名:

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

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

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

    • 变量初始化与赋值

      copy
      • 1
      • 2
      • 3
      • 4
      • 5
      void fun() { int b=1; b=2; }
      copy
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      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
copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
#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

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

本文作者:oniisan

本文链接:https://www.cnblogs.com/oniisan/p/basicCPP.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Oniisan_Rui  阅读(77)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起