c++学习笔记--继承

一,继承

1,继承的概念

继承机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称⼦类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤,继承是类设计层次的复⽤。

2,继承的格式

下⾯我们看到Person是⽗类,也称作基类。Student是⼦类,也称作派⽣类。

3,继承父类成员访问方式的变化

1. ⽗类private成员在⼦类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指⽗类的私有成员还是被继承到了⼦类对象中,但是语法上限制⼦类对象不管在类⾥⾯还是类外⾯都不能去访问它。

2. ⽗类private成员在⼦类中是不能被访问,如果⽗类成员不想在类外直接被访问,但需要在⼦类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

3. 实际上⾯的表格我们进⾏⼀下总结会发现,⽗类的私有成员在⼦类都是不可⻅。⽗类的其他成员在⼦类的访问⽅式 == Min(成员在⽗类的访问限定符,继承⽅式),public > protected > private。

4. 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显示的写出继承⽅式。

5. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤protetced/private继承,因为protetced/private继承下来的成员都只能在⼦类的类⾥⾯使⽤,实际中扩展维护性不强。

4,继承类模板

namespace bit
{
    template<class T>
    class stack : public std::vector<T>
    { 
    public:
        void push(const T& x)
        {
        //push_back(x);//会出错
        // ⽗类是类模板时,需要指定⼀下类域,
        // 否则编译报错:error C3861: “push_back”: 找不到标识符
        // 因为stack<int>实例化时,也实例化vector<int>了
        // 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
        vector<T>::push_back(x);
        }
    };
}

二,父类和子类对象赋值兼容转换

~public继承的⼦类对象可以赋值给⽗类的对象 / ⽗类的指针 / ⽗类的引⽤。这⾥有个形象的说法叫切⽚或者切割(如下图)。寓意把⼦类中⽗类那部分切来赋值过去。(这里并不是发生了类型转换

~⽗类对象不能赋值给⼦类对象。(不要记混了)

~⽗类的指针或者引⽤可以通过强制类型转换赋值给⼦类的指针或者引⽤。但是必须是⽗类的指针是指向⼦类对象时才是安全的。这⾥⽗类如果是多态类型,可以使⽤RTTI的dynamic_cast 来进⾏识别后进⾏安全转换。

三,继承中的作用域

1. 在继承体系中⽗类和⼦类都有独⽴的作⽤域。

2. ⼦类和⽗类中有同名成员(即不同的作用域),⼦类成员将屏蔽⽗类对同名成员的直接访问,这种情况叫隐藏。(在⼦类成员函数中,可以使⽤ ⽗类::⽗类成员 显⽰访问)(注意不要和重载记混,重载是在同一个作用域

3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。(不管形参

4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。

四,子类的默认成员函数

四个长见的默认成员函数:

1.默认构造函数

行为:

内置类型—>不确定

自定义类型—>调用自定义类型的默认构造

继承父类成员看做一个整体对象,要求调用父类的默认构造,如果⽗类没有默认的构造函数,则必须在⼦类构造函数的初始化列表阶段显⽰调⽤。

⼦类对象初始化先调⽤⽗类构造再调⼦类构造。

2.拷贝构造函数

⼦类的拷⻉构造函数必须调⽤⽗类的拷⻉构造完成⽗类的拷⻉初始化。(所有成员都要写在初始化列表里面

一般情况下默认生成的就够用了,如果子类有资源类的申请,才需要自己实现深拷贝。

3.赋值重载

⼦类的operator=必须要调⽤⽗类的operator=完成⽗类的复制。需要注意的是⼦类的operator=隐藏了⽗类的operator=,所以需要显⽰调⽤⽗类的operator=,需要指定⽗类作⽤域。

一般情况下默认生成的就够用了,如果子类有资源类的申请,才需要自己实现深拷贝。

4.析构

⼦类的析构函数会在被调⽤完成后⾃动调⽤⽗类的析构函数清理⽗类成员。因为这样才能保证⼦类对象先清理⼦类成员再清理⽗类成员的顺序。(所以不需要显示调用父类析构函数)

⼦类对象析构清理先调⽤⼦类析构再调⽗类的析构。

如果子类调用父类的析构函数时,要在前面指定父类类域(原因子类析构和父类析构构成隐藏,因为编译器对析构函数名特殊处理,处理成destructor() 

class Person
{
public :
    Person(const char* name = "peter")
    : _name(name )
    {
        cout<<"Person()" <<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 num)
    : Person(name)//这里初始化父类是相当于调用的是父类的构造函数
    , _num(num )
    {
        cout<<"Student()" <<endl;
    } 
    Student(const Student& s)
    : Person(s)//这里就运用的上面第二个标题的内容,赋值兼容转换
    , _num(s ._num)
    {
        cout<<"Student(const Student& s)" <<endl ;
    } 
    Student& operator = (const Student& s )
    {
        cout<<"Student& operator= (const Student& s)"<< endl;
        if (this != &s)
        {
            //构成隐藏,所以需要显⽰调⽤
            Person::operator =(s);
            _num = s ._num;
        } 
        return *this ;
    } 
    ~Student()
    {
        cout<<"~Student()" <<endl;
    }
protected :
    int _num ; //学号
};
int main()
{
    Student s1 ("jack", 18);
    Student s2 (s1);
    Student s3 ("rose", 17);
    s1 = s3 ;
    return 0;
}

五,继承与友元

友元关系不能继承,也就是说⽗类友元不能访问⼦类私有和保护成员 。
 

//声明前置
class Student;

class Person
{ 
public:
    friend void Display(const Person& p, const Student& s);
protected:
    string _name; // 姓名
};
class Student : public Person
{ 
protected:
    int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;
    cout << s._stuNum << endl;
} 
int main()
{
    Person p;
    Student s;
    // 编译报错:error C2248: “Student::_stuNum”: ⽆法访问 protected 成员
    // 解决⽅案:Display也变成Student 的友元即可
    Display(p, s);
    return 0;
}

六,继承与静态成员

⽗类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个⼦类,都只有⼀个static成员实例。

七,多继承及其菱形继承问题

1.继承模型

单继承:⼀个⼦类只有⼀个直接⽗类时称这个继承关系为单继承

多继承:⼀个⼦类有两个或以上直接⽗类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的⽗类在前⾯,后⾯继承的⽗类在后⾯,⼦类成员在放到最后⾯。

菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份(如下   图)。⽀持多继承就⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。

class Person
{ 
public:
    string _name; // 姓名
};
class Student : public Person
{ 
protected:
    int _num; //学号
};
class Teacher : public Person
{ 
protected:
    int _id; // 职⼯编号
};
class Assistant : public Student, public Teacher
{ 
protected:
    string _majorCourse; // 主修课程
};
int main()
{
    // 编译报错:error C2385: 对“_name”的访问不明确
    Assistant a;
    a._name = "peter";
    // 需要显⽰指定访问哪个⽗类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决
    a.Student::_name = "xxx";
    a.Teacher::_name = "yyy";
    return 0;
}

2.虚继承

class Person
{ 
public:
    string _name; // 姓名
};
// 使⽤虚继承Person类
class Student : virtual public Person
{ 
protected:
    int _num; //学号
};
// 使⽤虚继承Person类
class Teacher : virtual public Person
{
protected:
    int _id; // 职⼯编号
};
// 教授助理
class Assistant : public Student, public Teacher
{ 
protected:
    string _majorCourse; // 主修课程
};
int main()
{
    // 使⽤虚继承,可以解决数据冗余和⼆义性
    Assistant a;
    a._name = "peter";
    return 0;
}

注意:尽量不要造成菱形继承,如果使用虚继承会造成一些问题。

虚继承在构造函数的时候可能会出现一些问题如下面的程序

class Person
{ 
public:
    Person(const char* name)
        :_name(name) 
    {}
    string _name; // 姓名
};
// 使⽤虚继承Person类
class Student : virtual public Person
{ 
protected:
    Student(const char* name,int num = 0)
        :Person(name) 
        ,_num(num)
    {}
    int _num; //学号
};
// 使⽤虚继承Person类
class Teacher : virtual public Person
{
protected:
    Teacher (const char* name,int id = 0)
        :Person(name) 
        ,_id = id
    {}
    int _id; // 职⼯编号
};
// 教授助理
class Assistant : public Student, public Teacher
{ 
protected:
    Assistant(const char* name)
        :Teacher(name) 
        ,Student(name) 
        ,Person(name) //这里的构造函数只有这里生效,但前面必须要写
    {}
    string _majorCourse; // 主修课程
};

3.多继承中的指针偏移问题

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
    Derive d;
    Base1* p1 = &d;
    Base2* p2 = &d;
    Derive* p3 = &d;
    return 0;
}

上面程序中,在下面说法正确的是(c

A:p1 == p2 == p3              B:p1 < p2 < p3         C:p1 == p3 != p2        D:p1 != p2 != p3 

八,继承和组合

• public继承是⼀种is-a的关系。也就是说每个⼦类对象都是⼀个⽗类对象。

• 组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。

• 继承允许你根据⽗类的实现来定义⼦类的实现。这种通过⽣成⼦类的复⽤通常被称为⽩箱复⽤。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,⽗类的内部细节对⼦类可⻅。继承⼀定程度破坏了⽗类的封装,⽗类的改变,对⼦类有很⼤的影响。⼦类和⽗类间的依赖关系很强,耦合度⾼。

• 对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤,因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。

• 优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。

• 很多⼈说C++语法复杂,其实多继承就是⼀个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之⼀,后来的⼀些编程语⾔都没有多继承,如Java。

九,不能被继承的类

⽅法1:⽗类的构造函数私有,⼦类的构成必须调⽤⽗类的构造函数,但是⽗类的构成函数私有化以后,⼦类看不⻅就不能调⽤了,那么⼦类就⽆法实例化出对象。

⽅法2:C++11新增了⼀个final关键字,final修改⽗类,⼦类就不能继承了。(用法:final写在类的名字的后面)

  • 25
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值