c++随笔-6

本文详细介绍了C++中的封装、继承、多态以及虚函数的概念和使用。封装通过访问控制符限制成员的访问权限,保护数据安全。继承允许代码重用,子类可以访问基类的public和protected成员。多态通过虚函数实现,使得基类指针可以调用派生类的重写函数,增强了代码灵活性。同时,文章还讨论了抽象类和虚继承的作用,以及动态联编的概念。
摘要由CSDN通过智能技术生成

封装

class CProperty {  // 类中,成员函数在类里面或者累外边实现均可
public:
    void GetName(){GetObject();}
	void SetId(int id) { m_id = id; }
protected:   // 外边创建的对象不可访问
    void GetObject() { GetId();}
private:     // 外边创建的对象不可访问
    void GetId(){}

private:
	int m_id;
};

int main(int argc, char *argv[]) // 类外环境
{
    CProperty a;
    a.GetName();         // OK
    // a.GetId();        // NO
    // a.GetObject();    // NO
}
  • 封通过访问控制符实现,根据不同访问控制符类成员的访问权限不一样
  • private 类中可使用,子类中可用,在外边创建的对象不可调用
  • protected 类中可使用,子类中可用,在外边创建的对象不可调用
  • public 类中类外都可用
  • 类外不可访问属性可以通过提供 public 权限的借口实现
  • 友元可以打破这种访问权限约束

继承

// 单继承
class CBase {  // 函数的三种访问权限
public:
    void Eat1() {
        std::cout << "public: CBase Eat1" << std::endl;
    }
protected:
    void Eat2() {
        std::cout << "public: CBase Eat2" << std::endl;
    }
private:
    void Eat3() {
        std::cout << "public: CBase Eat3" << std::endl;
    }
};
// 三种不同访问权限的继承方式
class CDerive1 : public CBase{
public:
    void Invork() {  // 类中成员函数调用
        Eat1();
        Eat2();
        // Eat3();   // NO
    }
};

class CDerive2 : protected CBase{
public:
    void Invork() {  // 类中成员函数调用
        Eat1();
        Eat2();
        // Eat3();   // NO
    }
};

class CDerive3 : private CBase{
public:
    void Invork() {  // 类中成员函数调用
        Eat1();
        Eat2();
        // Eat3();   // NO
    }
};

int main(int argc, char *argv[]) // 类外环境
{
    CDerive1 d1;
    d1.Eat1();     // OK
    // d1.Eat2();  // NO
    // d1.Eat3();  // NO
    CDerive2 d2;
    // d2.Eat1();  // NO
    // d2.Eat2();  // NO
    // d2.Eat3();  // NO
    CDerive3 d3;
    // d3.Eat1();  // NO
    // d3.Eat2();  // NO
    // d3.Eat3();  // NO
}
类型及方式类外部子类本类
public可以访问可以访问可以访问
protected不可访问可以访问可以访问
private不可访问不可访问可以访问
类型及继承publicprotectedprivate
publicpublicprotectedprivate
protectedprotectedprotectedprivate
privateprivateprivateprivate
  • 继承可以减少代码冗余,提高代码片段复用率
  • 继承默认的继承权限是私有继承(private、protected、public),不同继承体现出的结果不一样
  • 派生类中成员可以访问基类的 public 和 protected 成员
  • 当 public 继承时,基类中 public 成员被子类以 public 继承,基类的 protected 成员被子类以 protected 继承
  • 当 protected 继承时,基类中无论是 public 还是 protected 的成员都被子类以 protected 继承
  • 当 private 继承时,基类类中 public 和 protected 成员都被子类以 private 继承
class CBase {
public:
};

class CDerive : public CBase {
public:

};

int main(int argc, char *argv[]) // 类外环境
{
    // 继承的初始化
    CBase a;     // 基类对象
    CDerive b;   // 派生类对象
    // CDerive b = a;   // 不能以基类初始化派生类对象
    CDerive c = *((CDerive*)(&a));  // 不能直接强制转换,需要通过指针强制转换,只能访问派生类成员
    CBase d = b;   // 派生类隐式转换为基类,只能访问基类成员

    CBase *e = new CDerive;   // 基类指针指向派生类,基类指针可以访问派生类有访问权限的成员,多态体现
}
// 多继承
class CBase {
public:
    void Eat1() {
        std::cout << "public: CBase Eat1" << std::endl;
    }
protected:
    void Eat2() {
        std::cout << "public: CBase Eat2" << std::endl;
    }
private:
    void Eat3() {
        std::cout << "public: CBase Eat3" << std::endl;
    }
};

class CDerive1 : public CBase {  // protected 继承只有在派生类中可访问,通过派生类继承的派生类不能访问
public:
    void Eat4() {
        std::cout << "public: CDerive1 Eat3" << std::endl;
    }
};

class CDerive2 : public CDerive1 {  // 一般使用最多的继承方式是 public 继承
public:
};

class CBase2 {
public:
    void Eat5() {
        std::cout << "public: CDerive3 Eat3" << std::endl;
    }
};

class CDerive4 : public CDerive2, public CBase2 {  // 继承多个类
public:
};

int main(int argc, char *argv[]) // 类外环境
{
    CDerive1 d1;
    d1.Eat1();
    CDerive2 d2;   // 不能访问 protected 和 private 的成员
    d2.Eat1();
    d2.Eat4();
    CDerive4 d4;   // 继承是 public 控制符,所有基类的 public 成员都被继承
    d4.Eat1();
    d4.Eat4();
    d4.Eat5();
}
// 菱形继承
class CBase {
public:
    int m_id;
};

class CDerive1 : public CBase {
public:
};

class CDerive2 : public CBase {
public:
};

class CDerive3 : public CDerive1, public CDerive2 {
public:
    void SetId(int id) {
        // m_id = id;   // 此处不成立,编译器找到了两个 m_id,不能决定用哪个
        CDerive1::m_id = id;   // 添加访问作用域解决冲突
        CDerive2::m_id = id;
    }
};
  • 注意实际设计类继承关系时尽量不要出现菱形继承,多继承时很容易产生命名冲突
class CBase {  // static
public:
    static int GetId() { return m_id; }
    static void SetId(int id) { m_id = id; }
    static int m_size;
private:
    static int m_id;
};

int CBase::m_size = 4;
int CBase::m_id = 10;

class CDerive : public CBase {
public:

};

int main(int argc, char *argv[]) // 类外环境
{
    std::cout << "CBase::m_size : " << CBase::m_size << std::endl;
    std::cout << "CDerive::m_size : " << CDerive::m_size << std::endl;
    std::cout << "CBase::GetId() : " << CBase::GetId() << std::endl;
    std::cout << "CDerive::GetId() : " << CDerive::GetId() << std::endl;
    CBase::m_size = 8;
    std::cout << "CBase::m_size : " << CBase::m_size << std::endl;
    std::cout << "CDerive::m_size : " << CDerive::m_size << std::endl;
    CDerive::m_size = 12;
    std::cout << "CBase::m_size : " << CBase::m_size << std::endl;
    std::cout << "CDerive::m_size : " << CDerive::m_size << std::endl;
    std::cout << "CBase::m_size address : " << &CBase::m_size << std::endl;
    std::cout << "CDerive::m_size address : " << &CDerive::m_size << std::endl;
    CBase::SetId(20);
    std::cout << "CBase::GetId() : " << CBase::GetId() << std::endl;
    std::cout << "CDerive::GetId() : " << CDerive::GetId() << std::endl;
    CDerive::SetId(30);
    std::cout << "CBase::GetId() : " << CBase::GetId() << std::endl;
    std::cout << "CDerive::GetId() : " << CDerive::GetId() << std::endl;
}

输出结果:
CBase::m_size : 4
CDerive::m_size : 4
CBase::GetId() : 10
CDerive::GetId() : 10
CBase::m_size : 8
CDerive::m_size : 8
CBase::m_size : 12
CDerive::m_size : 12
CBase::m_size address : 0x404010
CDerive::m_size address : 0x404010
CBase::GetId() : 20
CDerive::GetId() : 20
CBase::GetId() : 30
CDerive::GetId() : 30

  • 基类的 static 变量和函数在派生类中依然可用,同样受访问控制约束
  • 对 static 变量来说,派生类和基类中共用内存空间
  • 不能继承构造函数,派生类类不能像使用成员函数一样调用基类的构造函数(注意并不是说不能调用基类构造函数)
  • 派生类的构造函数执行任务前,必须先调用基类构造函数(派生类可以指定调用基类的某个构造函数,如果不指定,默认调用无参构造函数),初始化基类成员
  • 不能继承析构函数,可以在析构中调用基类的析构函数
  • 继承发生时,构造顺序由基类到派生类,析构顺序由派生类到基类
  • 一般将析构声明为 virtual ~CBase(){} 形式,这样基类对象释放时自动调用子类析构函数

virtual

class CBase {   // virtual
public:
    virtual void Print() { // 修饰函数,此时会默认生成类的虚函数表,子类可以重写此函数
        std::cout << "CBase virtual Print" << std::endl;
    }

    virtual void PrintSize(int) {
        std::cout << "CDerive virtual PrintSize" << sizeof (CBase) << std::endl;
    }
};

class CDerive : public CBase {
public:
	// 重写
	// 1. 必须继承关系
    // 2. 函数必须一模一样(返回值、函数名、形参类型)
    // 3. 基类函数必须有 virtual 关键字
    // 4. 重写函数的访问控制符权限不能大于被重写的函数访问权限

    virtual void Print() override { // 重写基类函数 virtual & override 关键字可省略,默认存在
        std::cout << "CDerive virtual Print" << std::endl;
    }

	// 隐藏
    // 1. 必须继承关系
    // 2. 必须函数名字相同
    // 3. 参数不同时,不论有无virtual关键字,基类的函数将被隐藏
    // 4. 参数相同,但是基类函数有无 virtual 关键字基类对应的函数被隐藏

    void PrintSize() {  // 隐藏了基类的同名函数
        std::cout << "CDerive virtual PrintSize" << sizeof (CDerive) << std::endl;
    }
};

int main(int argc, char *argv[])
{
    CBase a;
    a.Print();
    a.PrintSize(1);  // 调用派生类的同名函数
    CDerive b;
    b.Print();
    b.PrintSize();
    CBase *p = new CDerive;
    p->Print();
    p->PrintSize(1);
}

输出结果:
CBase virtual Print
CDerive virtual PrintSize8
CDerive virtual Print
CDerive virtual PrintSize8
CDerive virtual Print
CDerive virtual PrintSize8

  • virtual 关键字修饰函数时,可以在派生类中对此函数进行重写
  • 继承关系中重写和隐藏不相同,注意区分
  • virtual 关键字修饰函数时,类中默认生成虚函数列表,可以通过对比类的大小看出来
class CBase {};  // 注意空类大小不为 0

class CBase1 {
public:
    virtual void Print1() { // virtual
        std::cout << "CBase1 virtual Print1" << std::endl;
    }
    virtual void Print2() { // virtual
        std::cout << "CBase1 virtual Print2" << std::endl;
    }

    // 此类的 sizeof 计算为 4   +    8     +     4;
    //                   m_a  虚函数指针vptr 内存对齐的扩展字节
    int m_a;
};

class CBase2 : public CBase1 {  //  默认添加虚函数列表
public:
    virtual void Print1() { // virtual
        std::cout << "CBase2 virtual Print1" << std::endl;
    }
    virtual void Print2() { // virtual
        std::cout << "CBase2 virtual Print2" << std::endl;
    }

    // 此类的 sizeof 计算为 4   +    8     +     4;
    //                   m_a  虚函数指针vptr 内存对齐的扩展字节
};

int main(int argc, char *argv[])
{
    std::cout << "sizeof (CBase) : " << sizeof (CBase) << std::endl;
    std::cout << "sizeof (CBase1) : " << sizeof (CBase1) << std::endl;
    std::cout << "sizeof (CBase2) : " << sizeof (CBase2) << std::endl;
    // 证明虚函数指针和虚函数表存在,虚函数指针在类的起始位置
    CBase2 b2;
    using Func = void (*)(void);
    std::cout << "vtable address = " << (int*)&b2 << "\t" << "value = " << *((int*)&b2) << std::endl;

    Func f1 = (Func)*((int*)*((int*)&b2) + 0);
    f1();
    Func f2 = (Func)*((int*)*((int*)&b2) + 2);
    f2();

    CBase1 b1;
    using Func = void (*)(void);
    std::cout << "vtable address = " << (int*)&b1 << "\t" << "value = " << *((int*)&b1) << std::endl;

    Func f3 = (Func)*((int*)*((int*)&b1) + 0);
    f3();
    Func f4 = (Func)*((int*)*((int*)&b1) + 2);
    f4();
    return 0;
}

输出结果:
sizeof (CBase) : 1
sizeof (CBase1) : 16
sizeof (CBase2) : 16
vtable address = 0x65fdf0 value = 4216320
CBase2 virtual Print1
CBase2 virtual Print2
vtable address = 0x65fde0 value = 4216288
CBase1 virtual Print1
CBase1 virtual Print2

  • 虚函数表是编译器自动生成的一块内存空间(类中有 virtual 修饰的函数),存储每一个虚函数的地址
  • 当类中存在虚函数时,每个对象都有一个指向虚函数的指针
  • 定义派生类对象时,vptr 指针先指向基类的虚函数表,基类构造完成后,派生类的 vptr 才指向本身的虚函数表

虚继承

// 菱形继承,使用虚继承解决变量冲突
class CBase {
public:
    int m_id;
};

class CDerive1 : virtual public CBase {
public:
};

class CDerive2 : virtual public CBase {
public:
};

class CDerive3 : public CDerive1, public CDerive2 {
public:
    void SetId(int id) {
        m_id = id;
    }
};

输出结果:

  • 为了解决多继承时的命名冲突和冗余数据等问题,C++提出虚继承概念,在派生类中只保留一份间接基类的成员
  • 虚继承的目的是让某个类做出声明,承诺愿意共享它的基类,被共享的基类就称为虚基类(CBase 类)
  • 不影响 CDerive1 类和 CDerive2 类的继承特性,只影响 CDerive3 类

多态

class CBase {   // 基类
public:
    virtual ~CBase(){}
    virtual void Print() {
        std::cout << "CBase Print" << std::endl;
    }
};

class CDerive1 : public CBase {  // 派生类
public:
    virtual void Print() {
        std::cout << "CDerive1 Print" << std::endl;
    }
};

class CDerive2 : public CBase { // 派生类
public:
    virtual void Print() {
        std::cout << "CDerive2 Print" << std::endl;
    }
};

void Print(CBase &obj) {    // 引用实现多态
    obj.Print();
}

void Print(CBase *obj) {    // 指针实现多态
    obj->Print();
}

int main(int argc, char *argv[])
{
    // 简单多态调用框架
    // 多态必须发生在具有继承关系的类中,使用不同派生类初始化基类指针或引用,产生不同的行为
    // 被基类指针或引用调用的函数必须是虚函数,且派生类重写了虚函数。

    CDerive1 d1;
    Print(d1);   // 引用调用,派生类对象直接传参
    Print(&d1);  // 指针调用
    CDerive2 d2;
    Print(d2);   // 引用调用,派生类对象直接传参
    Print(&d2);  // 指针调用

    CBase &ref = d1;
    Print(ref);   // 引用调用,使用基类对象类直接传参
    Print(&ref);  // 指针调用
    CBase *p1 = &d1;
    Print(ref);   // 引用调用,使用基类对象类直接传参
    Print(&ref);  // 指针调用

    CBase &ref2 = d2;
    Print(ref2);   // 引用调用,使用基类对象类直接传参
    Print(&ref2);  // 指针调用
    CBase *p2 = &d2;
    Print(ref2);   // 引用调用,使用基类对象类直接传参
    Print(&ref2);  // 指针调用
    return 0;
}

输出结果:
CDerive1 Print
CDerive1 Print
CDerive2 Print
CDerive2 Print
CDerive1 Print
CDerive1 Print
CDerive1 Print
CDerive1 Print
CDerive2 Print
CDerive2 Print
CDerive2 Print
CDerive2 Print


联编

  • 对函数的调用和实现进行映射链接称为联编,c++分为静态联编和动态联编
  • 在编译阶段能唯一确定好的程序的操作调用与执行该操作代码间的关系(静态联编或早绑定)
  • 在编译阶段不能确定,需要在运行时确定关系(动态联编或晚绑定)
  • 多态在运行时才确定需要调用的函数,属于动态联编,编译器对无 virtual 修饰的函数使用静态联编
  • 友元函数不是类成员,所以不能使用 virtual 修饰

抽象类

class CBase {   // 抽象类,完全由纯虚函数组成的类(无函数实现),派生类必须全部实现基类的纯虚函数
public:
    virtual ~CBase(){}
    virtual void Print() = 0;  // 纯虚函数,没有函数实现
};

class CDerive : public CBase {  // 派生类
public:
    virtual void Print() override {
        std::cout << "CDerive Print" << std::endl;
    }
};

int main(int argc, char *argv[])
{
    // CBase base; // 抽象类不能创建对象,抽象类只能作为基类来使用
    CBase *p = new CDerive;  // 使用多态
    p->Print();
    delete p;
    return 0;
}

输出结果:
CDerive Print

  • 抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,一般框架模块之间会设计彼此间的交互接口,使用抽象类很合适
  • 如果有虚函数的实现体,那就不是抽象类

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值