【讨论C++继承】

讨论C++继承

继承是面向对象程序设计中,使代码可以复用的重要手段,它允许程序员在保持原有类特性的基础上进行扩展。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

继承

定义

c++ 复制代码
class Person {
public:
    void print() {
        cout << "这是一个人类" << endl;
    }
protected:
    string name;
    int age;
};

class Student : public Person{
private:
    int stuId;
};

int main() {
    Student s;
    Person p;
    s.print();
    p.print();
    return 0;
}

上述代码,Person是基类,Student是派生类。

使用:来实现继承。

继承方式和访问限定符

基类/继承方式 public继承 protected继承 private继承
public成员 派生类的public成员 派生类的protected成员 派生类的private成员
protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见
  1. 基类使用private修饰的成员在派生类中无论以什么方式继承,都不可见。不可见是指还是会继承,只是不能直接使用,因为private修饰的成员在类外不可使用。
  2. protected修饰的成员不可在类外使用,但是存在继承关系的,派生类可以直接访问基类的成员。
  3. 使用关键字class时,默认的继承方式是private,使用struct时,默认的继承方式是public

基类和派生类的赋值转换

派生类对象可以直接赋值给基类的对象、指针、引用。

基类的对象不能赋值给派生类对象。

基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但必须是基类的指针指向派生类的对象时才安全。

c++ 复制代码
class Person {
public:
    void print() {
        cout << "这是一个人类" << endl;
    }
protected:
    string name;
    int age;
};

class Student : public Person{
private:
    int stuId;
};

int main() {
    Student s;
    Person* ps = &s;
    Person& rps = s;
    Person p = s;
    return 0;
}
  • 派生类对象赋值给基类对象,基类对象只能访问基类拥有的成员变量,不能访问派生类特有的成员变量。
  • 派生类赋值给基类对象时,不产生临时变量。

继承中的作用域

  1. 在继承体系中,基类和派生类的作用域是独立的。
  2. 当基类和派生类中有同名成员时,派生类成员将屏蔽基类同名成员的直接访问,称为隐藏或重定义。
  3. 成员函数只需要函数名相同即可达成隐藏。
c++ 复制代码
class Person {
protected:
    string name = "张三";
    int age = 10;
};

class Student : public Person{
public:
    void print() {
        cout << "姓名:" << name << endl;
        cout << "年龄:" << age << endl;
        cout << "学号:" << stuId << endl;
    }
private:
    int stuId;
    int age;
};

int main() {
    Student s;
    s.print();
    return 0;
}
  • 上述代码,Student类中的agePerson类中的age构成隐藏。Student对象使用自己类域中的age成员变量,因此是随机值。
  • 要想使用Person类中的age,需要Person::age
c++ 复制代码
class Student : public Person{
public:
    void print() {
        cout << "姓名:" << name << endl;
        cout << "年龄:" << Person::age << endl;
        cout << "学号:" << stuId << endl;
    }
private:
    int stuId;
    int age;
};

派生类的默认成员函数

  1. 派生类构造时必须调用基类的构造函数初始化基类的部分成员,如果基类没有默认构造函数,必须在派生类构造函数初始化列表中显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须调用基类的operator=完成基类的赋值。
  4. 派生类的析构函数会在调用完成后自动调用基类的析构函数清理基类成员。
  5. 派生类对象初始化先调用基类构造函数再调用派生类构造函数。
  6. 派生类对象析构时先调用派生类析构函数再调用基类析构函数。
c++ 复制代码
class Person {
public:
    Person(const char *name = "张三")
            : _name(name) {
        cout << "Person(name)" << endl;
    }

    Person(const Person &p)
            : _name(p._name) {
        cout << "Person(const Person &p)" << endl;
    }

    Person& operator=(const Person& p) {
        cout << "Person& operator=(const Person& p)" << endl;
        if(this != &p) {
            _name = p._name;
        }
        return *this;
    }

    ~Person() {
        cout << "~Person()" << endl;
    }

protected:
    string _name;
};

class Student : public Person {
public:
    Student(const char *name, int stuId = 110)
            : Person(name), _stdId(stuId) {
        cout << "Student(name, stuId)" << endl;
    }

    Student(const Student &s)
            : Person(s), _stdId(s._stdId) {
        cout << "Student(const Student &s)" << endl;
    }

    Student& operator=(const Student& s) {
        cout << "Person& operator=(const Person& p)" << endl;
        if(this != &s) {
            Person::operator=(s);
            _stdId = s._stdId;
        }
        return *this;
    }

    ~Student() {
        cout << "~Student()" << endl;
    }

private:
    int _stdId;
};

int main() {
    Student s1("李四", 111);
    cout << "====================" << endl;

    Student s2(s1);
    cout << "====================" << endl;

    Student s3("王五", 112);
    s2 = s3;
    cout << "====================" << endl;

    return 0;
}
  • 根据规则,派生类构造前应完成基类构造,因此在创建派生类对象,一定会初始化基类,析构时先析构派生类,再析构基类。

继承和友元

友元关系不能被继承。

c++ 复制代码
class Student;
class Person {
public:
    friend void print(const Person& p, const Student& s);
public:
    Person(const char *name = "张三")
            : _name(name) {
        cout << "Person(name)" << endl;
    }

    Person(const Person &p)
            : _name(p._name) {
        cout << "Person(const Person &p)" << endl;
    }

    Person& operator=(const Person& p) {
        cout << "Person& operator=(const Person& p)" << endl;
        if(this != &p) {
            _name = p._name;
        }
        return *this;
    }

    ~Person() {
        cout << "~Person()" << endl;
    }

protected:
    string _name;
};

class Student : public Person {
public:
    Student(const char *name, int stuId = 110)
            : Person(name), _stdId(stuId) {
        cout << "Student(name, stuId)" << endl;
    }

    Student(const Student &s)
            : Person(s), _stdId(s._stdId) {
        cout << "Student(const Student &s)" << endl;
    }

    Student& operator=(const Student& s) {
        cout << "Person& operator=(const Person& p)" << endl;
        if(this != &s) {
            Person::operator=(s);
            _stdId = s._stdId;
        }
        return *this;
    }

    ~Student() {
        cout << "~Student()" << endl;
    }

protected:
    int _stdId;
};

void print(const Person& p, const Student& s) {
    cout << p._name << endl;
    cout << s._name << s._stdId << endl;
}

int main() {
    Student s("李四", 111);
    print(s, s);
    return 0;
}
  • 上述代码会报错,error: '_stdId' is a protected member of 'Student'。证明友元函数没有被继承,因为Student类使用privateprotected修饰的成员变量不能在类外使用。

继承和静态成员

基类定义的static静态成员,整个继承体系中只有一个这样的成员。

c++ 复制代码
class Person {
public:
    Person() {
        ++n;
    }
public:
    static int n;
};
int Person::n = 0;
class Student : public Person {
};


int main() {
    Student s;
    cout << Person::n << endl;
    s.n = 10;
    cout << Person::n << endl;

    return 0;
}

菱形继承

  • 单继承:一个派生类只有一个直接基类。Java只支持单继承。
  • 多继承:一个派生类继承于多个基类。C++支持多继承。
  • 菱形继承:它是多继承的一个特殊情况。

菱形继承所带来的问题是,最初继承的类内成员会存在两份,即数据冗余和二义性问题。

c++ 复制代码
class A {
public:
    int _a;
};

class B : public A{
public:
    int _b;
};

class C : public A {
public:
    int _c;
};

class D : public B, public C {
public:
    int _d;
};


int main() {
    D d;

    return 0;
}
  • 想要直接通过d对象去修改_a是不被允许的,因为编译器不知道想要修改的是B类和C类中哪个类的_a
  • 只能通过d.B::a = 10这种指定类域的方式来修改或赋值。这种方式可以解决二义性问题,但无法解决数据冗余问题。

虚拟继承

  • 使用virtual关键字建立虚拟继承,可以解决数据冗余和二义性问题。
  • 当不使用虚拟继承时,内存空间是这样的。
c++ 复制代码
class A {
public:
    int _a;
};

class B : public A{
public:
    int _b;
};

class C : public A {
public:
    int _c;
};

class D : public B, public C {
public:
    int _d;
};


int main() {
    D d;
    d.B::_a = 1;
    d.C::_a = 9;
    d._b = 2;
    d._c = 3;
    d._d = 4;

    return 0;
}
  • 使用虚拟继承,_a就只存在一份,可以直接赋值。
c++ 复制代码
class A {
public:
    int _a;
};

class B : virtual public A{
public:
    int _b;
};

class C : virtual public A {
public:
    int _c;
};

class D : public B, public C {
public:
    int _d;
};


int main() {
    D d;
    d.B::_a = 1;
    d.C::_a = 9;
    d._b = 2;
    d._c = 3;
    d._d = 4;

    return 0;
}
  • clang编译器通过BC的两个指针,指向一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存在偏移量。通过偏移量可以找到A
相关推荐
H1001 分钟前
Kotlin中对空的很多处理
android·开发语言·kotlin
松仔log2 分钟前
Kotlin基础——异步和并发
android·开发语言·kotlin
史嘉庆18 分钟前
【C++ | 继承】|概念、方式、特性、作用域、6类默认函数
c++
菜鸟赵大宝1 小时前
【C++】C++深拷贝与浅拷贝
java·jvm·c++
多方通行81 小时前
UE5材质之HLSL:深度
开发语言·算法·ue5·图形渲染·材质
MessiGo1 小时前
Qt 实战(7)元对象系统 | 7.1、简介
java·开发语言·qt
苏呆仔1 小时前
如何使用PHP根据输入文字动态调整图片尺寸?
android·开发语言·php
二进制人工智能1 小时前
【C++设计模式】(一)面向对象编程的八大原则
c++·设计模式
程序猿不脱发21 小时前
【设计模式】装饰者模式里Java实现
java·开发语言