其他分享
首页 > 其他分享> > cpp拾遗——类

cpp拾遗——类

作者:互联网

1. 继承和访问控制

1.1 访问控制的管道

public成员 protected成员 private成员
public继承 public , public = 子类内外皆可访问 protected , public = 只能子类内和父类内访问 private , public = 只能父类内访问
protected继承 public , protected = protected protected | protected = protected private | protected = 父类private
private继承 public | public = 子类private protected | private = 子类private private | private = 父类private

1.2 继承的本质

属性继承

若有一下继承

class T{
    public:
        int a;
};
class TT:public T{
    public:
        int a;
};

则 TT tt 的内存结构如下

方法继承

class T{
    public:
        void func() {}
};
class TT:public T{
    public:
        void func() {}
};

TT tt;
TT *ptt;

ptt = &tt;
ptt->func();
ptt->T::func();
tt.func();
tt.T::func();
  1. 将class中定义函数,重命名函数名,定义全局函数
class T{
    public:
        void func() {}
};
class TT:public T{
    public:
        void func() {}
};
// 相当于定义为
void T_func() {}
void TT_func() {}

2) 根据调用方法时,对象的类型,进行静态链编

TT tt;
TT *ptt;

ptt = &tt;
ptt->func();     // TT_func(ptt);
ptt->T::func();  // T_func(&ptt->t);
tt.func();       // TT_func(&tt);
tt.T::func();    // T_func(&tt.t);

2. 类的兼容性原则

派生类对象可以当基类对象使用。
包括以下情况:
1)子类对象可以当父类对象使用
2)子类对象可以直接赋值给父类对象
3)子类对象可以直接初始化父类对象
4)父类指针可以指向子类对象
5)父类引用可以直接引用子类对象

3. 构造析构的调用顺序

构造:先父亲,再朋友,再自己
析构:先自己,再朋友,再父亲

3.1 继承中的构造本质

class F{
    public:
        F() { }
        ~F() { }
};
class F2{
    public:
        F2() { }
        ~F2() { }
};
class T{
    public:
        T() { }
        ~T() { }
};
class TT:public T{
    public:
        TT() { }
        ~TT() { }
        F2 f2; // 朋友的构造顺序又声明时顺序决定
        F f;
};

cpp编译器会添加代码

        TT() { T(); F2(); F(); /* 构造自己 */ }
        ~TT() { /* 析构自己 */ ~F(); ~F2(); ~T(); }

4. 同名成员继承

  1. 覆盖父类成员
    当父类和子类的成员同名时,子类成员会覆盖父类成员,但通过子类对象加上域作用符仍然可以访问父类属性。
  2. 重载只存在于同个类中
    父类和子类的成员函数即使 函数名相同,参数不同,但不构成重载,而是覆盖。
    原因很明显cpp编译器在重新命名函数名时,由于 类名不同,导致被命名后的函数名不同。

5. 继承和static

父类的static成员 被 子类继承,子类对象可以正常访问。

6. 多继承的二义性


由于D继承B和C,所以D有两份A的属性,那么D访问A的属性时会出现二义性(即不知道访问的哪个内存)。


通过加上virtual关键字,D继承B和C时只会继承A的一份属性,解决二义性问题。

7. 多态

7.1 多态构成条件

父类指针指向子类对象,访问成员函数

  1. 非多态
    cpp在编译阶段,静态链编,由于指针是父类对象,所以调用父类函数

  2. 多态
    当父类函数用virtual修饰,则使用动态链编,运行阶段,根据指针指向的对象为子类对象,调用子类对象的函数

7.2 多态的应用

  1. 虚析构函数
    需求:希望用父类指针,释放子类对象
    若没有使用virtual,则 delete 父类指针,在编译阶段,编译成 调用父类对象的 析构函数,则导致内存泄漏。
    若使用virtual,则 delete 父类指针,在运行阶段,根据指针指向的是子类对象,则调用子类对象的析构函数,子类对象的析构函数已经被cpp编译器修改,增加了调用父类析构和朋友析构的代码,所以会析构完所有继承和组合的对象,所以没有内存泄漏。

7.3 多态的本质

由于现象是运行时确定调用函数,所以一定是用函数指针实现,
而多个函数都可以设为多态,所以一定是将函数地址组织为表。

cpp编译器为 使用了 virtual 关键字的类添加了 vptr指针,当子类继承时,子类也就继承了 vptr指针。
并将 多态函数地址构成一张表,当对象构造时,vptr指针指向本构造函数对应类的 多态表,
所以 子类的vptr 先在 父类构造函数中,被设置为 指向父类的多态函数表,然后在自己的构造函数中被设置为指向自己的函数表,

        T() {}
    127a:   f3 0f 1e fa             endbr64
    127e:   55                      push   %rbp
    127f:   48 89 e5                mov    %rsp,%rbp
    1282:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
    1286:   48 8d 15 d3 2a 00 00    lea    0x2ad3(%rip),%rdx        # 3d60 <_ZTV1T+0x10> # 设置vptr指针指向本类的多态函数表
    128d:   48 8b 45 f8             mov    -0x8(%rbp),%rax
    1291:   48 89 10                mov    %rdx,(%rax)


        TT() {}
    12c8:   f3 0f 1e fa             endbr64
    12cc:   55                      push   %rbp
    12cd:   48 89 e5                mov    %rsp,%rbp
    12d0:   48 83 ec 10             sub    $0x10,%rsp
    12d4:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
    12d8:   48 8b 45 f8             mov    -0x8(%rbp),%rax
    12dc:   48 89 c7                mov    %rax,%rdi
    12df:   e8 96 ff ff ff          callq  127a <_ZN1TC1Ev>   # 调用父类构造函数
    12e4:   48 8d 15 4d 2a 00 00    lea    0x2a4d(%rip),%rdx        # 3d38 <_ZTV2TT+0x10>  # 设置vptr指针指向自己的函数表
    12eb:   48 8b 45 f8             mov    -0x8(%rbp),%rax
    12ef:   48 89 10                mov    %rdx,(%rax)

调用多态时,根据当前vptr指针的值为基地址,根据多态函数声明顺序获得偏移量(编译阶段获得),计算得到调用函数的地址,并调用。

1919     pt->func();
1920     11b8:   48 8b 45 f0             mov    -0x10(%rbp),%rax
1921     11bc:   48 8b 00                mov    (%rax),%rax
1922     11bf:   48 8b 10                mov    (%rax),%rdx
1923     11c2:   48 8b 45 f0             mov    -0x10(%rbp),%rax
1924     11c6:   48 89 c7                mov    %rax,%rdi
1925     11c9:   ff d2                   callq  *%rdx
1926     pt->func2();
1927     11cb:   48 8b 45 f0             mov    -0x10(%rbp),%rax
1928     11cf:   48 8b 00                mov    (%rax),%rax
1929     11d2:   48 83 c0 08             add    $0x8,%rax
1930     11d6:   48 8b 10                mov    (%rax),%rdx
1931     11d9:   48 8b 45 f0             mov    -0x10(%rbp),%rax
1932     11dd:   48 89 c7                mov    %rax,%rdi
1933     11e0:   ff d2                   callq  *%rdx
1934     pt->func3();
1935     11e2:   48 8b 45 f0             mov    -0x10(%rbp),%rax
1936     11e6:   48 8b 00                mov    (%rax),%rax
1937     11e9:   48 83 c0 10             add    $0x10,%rax
1938     11ed:   48 8b 10                mov    (%rax),%rdx
1939     11f0:   48 8b 45 f0             mov    -0x10(%rbp),%rax
1940     11f4:   48 89 c7                mov    %rax,%rdi
1941     11f7:   ff d2                   callq  *%rdx

7.4 多态的缺点

由于多态调用是计算地址,并调用,所以效率低,不应该把所有函数都声明为多态。

8. 父类和子类指针步长

虽然多态让 父类指针 能表现子类特性,但由于 子类通常定义了 成员属性,所以父类指针步长和子类指针步长不同。

9. 纯虚函数和抽象类

纯虚函数
没有函数体,只用于为派生类提供接口的函数。

virtual void func() = 0;

抽象类
具有纯虚函数的类为抽象类,由于抽象类定义不完整,所以抽象类不能定义对象,只能定义指针,用于实现多态。

标签:48,子类,mov,拾遗,cpp,父类,public,rax
来源: https://www.cnblogs.com/yangxinrui/p/16313117.html