目录
菱形继承:也称为钻石继承,是将多继承与单继承结合到一起的一种继承方式
一、定义
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类,继承体现了面向对象程序设计的层次结构,继承体现了由简单到复杂的认知过程,以前接触的都是函数复用,继承是设计层次的复用。
要实现继承方式,一定先要有一个基类——>基类就是各种对象高度抽象(将子类的共性提供出)
C以public的方式继承B
class B
{
private:
int _b;
};
class C : public B
{
private:
int _c;
};
二、继承权限与访问权限
1)private成员无论以什么方式继承到子类,在子类中都不允许对该成员进行访问(指从基类继承下来到子类的private类型成员)
2)如果基类成员继承到子类后,希望它能够被子类访问,而不能被类外访问则定义为protected类型(保护成员限定符就是因为继承才产生的)
3)使用关键字class默认继承方式为private,使用struct默认继承方式为public
4)在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承
三、基类与派生类对象赋值转换
默认在public继承方式的大前提下(public——is-a 即可以将一个子类对象看作为一个基类对象)
1)子类对象可以赋值给基类对象
动物类派生出来的狗 猫 他们都是动物,但动物不一定是猫、不一定是狗
首先明确 B类中含有 _b,而C类中含有_b,_c 这里可以类比一种切割的方法,把派生类中父亲的那部分切割下来赋值过去。反过来就会报错
2)基类的指针和引用可以赋值为子类的指针和引用
D为子类,b为基类中成员,当子类指针指向基类对象时,pd指针的另一部分_d无法被接受这是不安全的!!!
class B
{
public:
B(int b)
: _b(b)
{}
private:
int _b;
};
class C : public B C以public的方式继承B
{
public:
C(int c, int b)
: _c(c)
, B(b)
{}
private:
int _c;
};
int main()
{
B b(10); // 基类
C c(20, 0); // 子类
// c = b; // 基类给子类赋值 false
b = c; // 1、子类对象给基类赋值
B& br = c; // 2、基类的引用指向子类
// C& cr = b; // 子类的引用指向基类 false
B* ptr1 = &c; // 3、基类的指针指向子类
//C* ptr2 = &b; // 子类的指针指向基类 false
return 0;
}
四、继承中的作用域
1、在继承体系中基类与派生类都有其自己独立的作用域
2、当子类与父类中存在同名成员时,子类成员将屏蔽父亲对对同名成员的直接访问(同名隐藏)
优先调用子类自己的成员
3、如果要在子类中直接访问基类成员,可以使用 ::作用域限定符来告知编译器。
4、注意在实际中在继承体系里面最好不要定义同名的成员
class father
{
public:
void Print()
{
cout << "hello, I am father!!!" << endl;
}
};
class child : public father
{
public:
void Print()
{
cout << "hi, I am child!!!" << endl;
}
};
int main()
{
child d;
d.Print(); // 子类默认优先调用自己的Print
d.father::Print(); // 告诉编译器这里调用的是father的Print
return 0;
}
陷阱1 看如下代码,俩个Print函数只有形参不同,这是函数重载吗?
答案不是,函数重载限定于俩个函数在同一个作用域中,而下面俩个Print一个在father的作用域中,一个在child的作用域中,并且继承体系中基类和派生类都有其各自的作用域。
重载:在相同作用域中,几个函数仅有参数列表不同(参数的类型、个数、次序不同),则这几个函数构成重载。
class father
{
public:
void Print()
{
cout << "hello, I am father!!!" << endl;
}
};
class child : public father
{
public:
void Print(int a)
{
cout << "hi, I am child!!!" << endl;
}
};
五、派生类的默认成员函数
1)子类的构造函数
1、如果基类 带有参数并且非全缺省参数 的构造函数,你就必须自己实现自己的构造,并且在初始化列表位置显示调用基类的构造方法
class B
{
public:
B(int b)
: _b(b) // 基类有构造函数
{}
private:
int _b;
};
class C : public B
{
public:
C(int c, int b) // 子类必须给出构造函数并在初始化列表调用基类构造
: _c(c)
, B(b) // 这里调用基类的构造
{}
private:
int _c;
};
2. 如果基类没有显示定义任何构造方法(或者为无参或全缺省的构造方法),子类可以根据自己选择性实现构造
class B
{
public:
B(int b = 0) // 基类为全缺省或者为无参构造函数就可以不需要显示调用基类构造
: _b(b)
{}
private:
int _b;
};
class C : public B
{
public:
C(int c)
: _c(c)
{}
private:
int _c;
};
2. 子类的拷贝构造函数
1). 基类拷贝构造如果没有实现,则子类的拷贝构造可以实现也可以不用实现
class B
{
public:
B(int b)
: _b(b)
{
cout << "B()" << endl;
}
protected:
int _b;
};
class D : public B
{
public:
D(int d, int b)
: _d(d)
, B(b)
{
cout << "D()" << endl;
}
private:
int _d;
};
int main()
{
D d1(1, 2);
D d2(d1);
return 0;
}
2.) 如果基类和子类的拷贝构造都定义了,子类的构造方法必须在其初始化列表的位置显式基类的拷贝构造
class B
{
public:
B(int b)
: _b(b)
{
cout << "B()" << endl;
}
B(const B& b) // 基类的拷贝构造函数
: _b(b._b)
{}
protected:
int _b;
};
class D : public B
{
public:
D(int d, int b)
: _d(d)
, B(b)
{
cout << "D()" << endl;
}
D(const D& d) // 定义拷贝构造并在初始化列表显示调用基类的拷贝构造
: B(d) // 调用基类的拷贝构造, 使用子类对象给基类对象赋值
, _d(d._d)
{}
private:
int _d;
};
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
1、先调用基类的赋值运算符重载完成对基类继承下来的成员的赋值
B::operator=(d);
2、再处理子类新增的元素赋值
_d = d._d;
class B
{
public:
B(int b)
: _b(b)
{
cout << "B()" << endl;
}
B(const B& b) // 基类的拷贝构造函数
: _b(b._b)
{}
B& operator=(const B& b)
{
if (this != &b)
{
_b = b._b;
}
return *this;
}
protected:
int _b;
};
class D : public B
{
public:
D(int d, int b)
: _d(d)
, B(b)
{
cout << "D()" << endl;
}
D(const D& d)
: B(d) // 使用子类对象给基类对象赋值
, _d(d._d)
{}
D& operator=(const D& d)
{
if (this != &d)
{
//1、先调用基类的赋值运算符重载完成对基类继承下来的成员的赋值
B::operator=(d);
// 2、再处理子类新增的元素赋值
_d = d._d;
}
return *this;
}
private:
int _d;
};
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
1)子类先进行析构
2)处理到子类析构的末尾时
3)跳转到基类的析构函数
4)执行完基类的析构函数后回到子类的析构函数末尾
5. 派生类对象初始化先调用基类构造再调派生类构造。
哪个对象先调用构造,就先进入哪个对象的构造主体中
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
哪个对象先调用析构,先进入哪个对象的析构主体
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
六、友元关系不能继承
子类继承下来的友元无法访问子类中的private与protected成员
class B
{
friend void MyPrint();
public:
B(int b = 0)
: _b(b)
{}
private:
int _b;
};
class C : public B
{
public:
C(int c)
: _c(c)
{}
private:
int _c;
};
void MyPrint() //该函数为基类的友元函数
{
B b;
cout << b._b << endl; // 创建一个基类对象,可以被访问
C c;
cout << c._c << endl; 报错// 创建一个子类对象,子类对象在该函数中不可被访问
}
七、继承与静态成员
基类定义了一个static静态成员,则整个继承体系中只有一个这样的成员
在基类B中定义一个static成员变量,通过C、D分别继承B,最终发现三者的_count地址值都为同一个
回顾一下static在类中使用:
该static不存在任何一个类中,它为所有类对象所共享,在这里就是为所有的继承体系类所共享,在这里即为只存在于静态区(所以通过sizeof查看C的大小为8,基类继承的_b 和 子类新增的_c)
static在类中的使用http://t.csdn.cn/P9raS
class B
{
public:
B(int b = 0)
: _b(b)
{
++_count;
}
static int _count;
private:
int _b;
};
int B::_count = 10;
class C : public B
{
public:
C(int c)
: _c(c)
{
++_count;
}
private:
int _c;
};
class D : public B
{
public:
D(int c)
: _d(c)
{
++_count;
}
private:
int _d;
};
int main()
{
cout << sizeof(C) << endl;
cout << &B::_count << " " << &C::_count << " "<< &D::_count << endl;
// 0084C000 0084C000 0084C000
return 0;
}
八、复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
class Person
{
public:
Person(string sex, int age)
: _sex(sex)
, _age(age)
{
cout << "Person()" << endl;
}
protected: // 基类使用protected方式定义成员变量,可以使其在子类中能够被访问到
string _sex;
int _age;
};
class Student : public Person // Student 使用public方式继承Person
{
public:
Student(string name, string sex, int age)
: _name(name)
, Person(sex, age)
{
cout << "Student()" << endl;
}
private:
string _name;
};
多继承:一个子类有俩个或以上直接父类时称这个继承关系为多继承
class Student
{
public:
Student(string StudentID)
: _StudentID(StudentID)
{
cout << "Student()" << endl;
}
private:
string _StudentID;
};
class Teacher
{
public:
Teacher(string TeacherID)
: _TeacherID(TeacherID)
{
cout << "Teacher()" << endl;
}
private:
string _TeacherID;
};
class Assistent : public Student, public Teacher
{
public:
Assistent(string position, string StudentID, string TeacherID)
: _position(position)
, Student(StudentID)
, Teacher(TeacherID)
{
cout << "Assistent()" << endl;
}
private:
string _position;
};
菱形继承:也称为钻石继承,是将多继承与单继承结合到一起的一种继承方式
菱形继承的问题就是会存在数据冗余和二义性,在Assistant的对象中存在俩份Person成员
class Person
{
public:
Person(string sex, int age)
: _sex(sex)
, _age(age)
{
cout << "Person()" << endl;
}
protected:
string _sex;
int _age;
};
class Student : public Person
{
public:
Student(string name, string sex, int age)
: _name(name)
, Person(sex, age)
{
cout << "Student()" << endl;
}
protected:
string _name;
};
class Teacher : public Person
{
public:
Teacher(string name, string sex, int age)
: _name(name)
, Person(sex, age)
{
cout << "Teacher()" << endl;
}
protected:
string _name;
};
class Assistent : public Student, public Teacher
{
private:
string _course;
};
模型研究
class B
{
public:
int _b;
};
class C1 : public B
{
public:
int _c1;
};
class C2 : public B
{
public:
int _c2;
};
class D : public C1, public C2
{
public:
int _d;
};
int main()
{
cout << sizeof(D) << endl; 答案为20
return 0;
}
在对上述各个类中的变量进行赋值时,产生了如上所述的数据二义问题
为什么sizeof(D)的值为20,这就是原因,将上图内存形式转化为图像模型即为:
菱形继承缺陷分析与解决
对于上述二义性问题:D对象从C1中继承下来一个_b,又从C2中继承下来一个_b,那么d对象中就会存在俩个_b,这时如果在d对象中访问_b,编译器就不知道访问的是哪一个_b
解决:
访问明确化(通过作用域限定符来告知编译器访问的是那个基类对象中的成员)
d.C1::_b = 0;
d.C2::_b = 1;
然而这种方式只是简单的为了达到编译的目的,在对象的空间存储中,_b仍然存在于俩个继承体系中。如何彻底的解决这个元素二次开辟的问题呢?
虚拟继承
何为虚拟继承?就是在继承权限前加上一个virtual关键字
class child : virtual public father
class B
{
public:
int _b;
};
class C : virtual public B // 采用virtual继承
{
public:
int _c;
};
int main()
{
cout << sizeof(C) << endl; // 12
C c;
c._b = 1;
c._c = 2;
}
基类部分在下,子类部分在上
发现:只有俩个int类型的成员变量最终却打印出来了12字节的类的大小,多出来这四个字节是什么呢?
这四个字节其实就是一个地址,它指向了一块虚基表(偏移量表格)
编译器会在c对象构造时,完成对前四个字节内容的初始化工作,初始化虚基表并使该地址指向虚基表。
如何通过使用虚拟继承解决菱形继承二义性问题:
class B
{
public:
int _b;
};
class C1 : virtual public B C1虚拟继承自B
{
public:
int _c1;
};
class C2 : virtual public B C2虚拟继承自B
{
public:
int _c2;
};
class D : public C1, public C2
{
public:
int _d;
};
int main()
{
cout << sizeof(D) << endl; 24
D d;
d._b = 1;
d._c1 = 2;
d._c2 = 3;
d._d = 4;
return 0;
}
如上图所示模型