继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。(被继承的类称为基类)继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
通过直接在类后面继承,可以在stu和tea实例化的对象中看到person的成员(已经成为stu和tea的成员了)
继承方式的组合
谁的权限小,就取谁。
总结:
1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它。
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过 最好显示的写出继承方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
基类和派生类对象赋值转换
派生类对象可以赋值给基类的对象 (T)基类的指针 (T*) 基类的引用(T&)。叫切片或者切割。把派生类中父类那部分切来赋值过去。
基类对象不能赋值给派生类对象。
1.直接赋值
底层发生的行为:
1. 拷贝基类部分:编译器仅复制派生类对象中的基类子对象部分(即 Base::x),忽略派生类新增成员(如 Derived::y)。
2. 对象切片(Slicing):派生类的额外信息(如 y)被“切掉”,仅保留基类部分。
3. 虚函数表(vtable)的影响: 如果基类有虚函数,派生类重写了虚函数(构成多态):
class Base {
public:
virtual void foo() { cout << "Base"; }
};
class Derived : public Base {
public:
void foo() override { cout << "Derived"; }
};
Derived d;
Base b = d; // 切片后,b 的虚函数表指向 Base::foo(非多态行为!)
b.foo(); // 输出 "Base"(非 "Derived")
派生类的虚函数表指针会被重置为基类的虚函数表,失去多态性。
而传指针或引用,派生类虚函数表不会被重置,支持多态。
class Animal {
public:
virtual void speak() { cout << "Animal sound"; }
int _animal=1;
};
class Dog : public Animal {
public:
void speak() override { cout << "Woof!"; }
int _dog=2;
};
Dog dog;
Animal* animalPtr = &dog; // 基类指针指向派生类对象
// 通过指针给基类赋值,就是将dog的虚函数表传给了animalPtr,animalPtr可以访问Dog类里的所有虚函数
从对象的角度看,animalPtr就是Dog的对象,
anim和animalPtr是同类型的,本应该共用同一张虚函数表,但是animalPtr与dog是同一张虚函数表(并且成员变量dog也传给了animalPtr),说明animalPtr与dog是一样的对象,这样赋值有什么意义呢?
其实作用并不是给Animal类型的变量赋值,而是给Dog类型的变量一个通行证,让它能通过基类至上而下去找对应的派生类。
这样所有继承了同一个基类的派生类实例化出的对象都可以通过基类这个统一的接口去调用自己的派生类,实现不同的行为,显示了C++是一门面对对象的语言。
总结
将派生类指针赋值给基类指针的意义在于:
1. 实现多态:通过虚函数表动态调用实际对象的函数。
2. 统一接口:基类抽象共性,派生类定制差异。
3. 灵活扩展:支持未来新增派生类,最小化代码修改。
再回来看
直接赋值,就真的是给Animal类型的变量赋值了!!!
继承中的作用域
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类(child)和父类 (parent) 中有同名成员变量,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问 this->person::age), (在成员函数外,使用 派生类::基类::基类成员 访问,例如:t1.person::age)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员变量。
派生类的6个默认成员函数
“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类 中,这几个成员函数是如何生成的呢?
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
看看前面的知识和构造函数
class person
{
public:
int _age=19;
const char* _name;
void fun(int i) {};
person(int age = 0,const char* name = "GuYu")
:_age(age)
,_name(name)
{
cout << "person()" << endl;
}
~person()
{
cout << "~person()" << endl;
}
void operator=(const person& p)
{
this->_age = p._age;
this->_name = p._name;
}
private:
int i;
protected:
int b;
};
class stu : person // class默认为私有-->private person
{
public:
void fun() {};
stu(int no=21)
:_no(no)
{
cout << "stu()" << endl;
}
~stu()
{
cout << "~stu()" << endl;
}
private:
//stu() //当构造函数私有,该类不可继承,也无法在类外创建变量
// :_no(2)
//{ }
int _no;
};
class tea :public person // 将person里的成员作为tea的共有成员
{
public:
void fun() {};
int _age;
tea(int jobn=1, int age1=18,int age2=20,const char*name="GY")
:_jobn(jobn)
, _age(age1)
,person(age2,name) // 可以不用写,如果person有默认构造函数,且不想赋值 ///初始化列表阶段显示调用
{
cout << "tea()" << endl;
}
tea(const tea& t)
:_jobn(t._jobn)
, _age(t._age)
, person(t) //-->引用赋值 // 发生了切片或切割
//, person(t.person::_age,t.person::_name) // -->传值赋值也可以
{}
void operator=(const tea& t)
{
person::operator=(t); // 发生切片
_age = t._age;
_name = t._name;
}
~tea() // 子类析构函数~tea()与父类析构函数~person()构成隐藏(重写),因为他们的名字会被编译器统一处理成destructor()
{
cout << "~tea()" << endl;
}
private:
int _jobn;
};
}
void test4()
{
person p1;
stu s1;
// s1._no;
// s1.age; // 不可访问 默认是私有继承的
tea t1;
t1._age; // public person 继承,为公有
// t1.b; // t1的保护成员,不能访问 只能在派生类中访问
p1 = t1; // 子类通过切割赋值给父类
person* p2 = &t1;
person& p3 = t1;
// p1 = s1; // p1传给s1的成员在s1里面是私有的,不可访问
// t1 = p1; 不允许父类给子类赋值
tea* t2 = (tea*)p2; //强转类型可以赋值
t1._age = 1; // 当子类与父类有同名成员,子类成员会将父类成员隐藏
cout << t1.person::_age << endl; //想要通过子类访问父类的同名元素,需要指定父类命名空间
// person 和 stu中都有fun,关系是重定义(隐藏),默认是子类中的fun()
s1.fun();
t1.fun();
t1.person::fun(5); // 虽然重定义了,也可以通过子类访问父类fun,
}
void test5()
{
tea t1; // 调用顺序:person._age-->person._name-->tea._age-->tea.jobn-->~person-->~tea
cout << t1._age << " " << t1._name << " " << t1.person::_age << " " << t1.person::_name << endl;
// 先按基类的声明顺序,再派生类的声明顺序。
}
继承与友元与静态
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
无虚函数的菱形继承(继承成员变量)
第一种:非虚拟下的多重继承
class A
{
public:
A(){cout << "A()" << endl;}
char _a;};
class B:public A
{
public:
B(){cout << "B()" << endl;}
int _b;};
class C:public A
{
public:
C(){cout << "C()" << endl;}
int _c;};
class D:public B,public C
{
public:
D(){cout << "D()" << endl;}
int _d;};
void test8()
{
D d;
cout << sizeof(d) << endl; // 20个字节,_a _a _b _c _d
d.B::_a = 1;
d.C::_a = 2; // -->数据冗余 可以看出菱形继承有数据冗余和二义性的问题,在d中a的成员有2份
// d._a = 3; // 不明确 -->二义性
d._b = 3;
d._c = 4;
d._d = 5;
}
看一下内存分布
一目了然。
第二种:虚拟多重继承
class A2
{
public:
A2()
{cout << "A2()" << endl;}
~A2() { cout << "~A2()" << endl; }
char _a;};
class B2: virtual public A2
{
public:
B2()
{cout << "B2()" << endl;}
~B2() { cout << "~B2()" << endl; }
int _b;};
class C2: virtual public A2
{
public:
C2()
{cout << "C2()" << endl;}
~C2() { cout << "~C2()" << endl; }
int _c;};
class D2:public B2,public C2
{
public:
D2()
{cout << "D2()" << endl;}
~D2() { cout << "~D2()" << endl; }
int _d;};
void test9()
{
B2 b2;
C2 c2;
D2 d;
cout << sizeof(d) << endl;
d.B2::_a = 1;
d.C2::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
}
我们先从b2看一下是如何虚拟继承的
思考1:虚基表的第一行有什么作用呢?(为什么将偏移量放在第2行)(this指针)我也还没找到规律(其中包含优化问题)
思考2:为什么b2的内存是9个字节(不满足内存对齐)(直到结束也是9个字节)
c2也是同理
接下来看看d的内存分布
在d的内存中存放了2个虚基类偏移表,分别是b2,c2的虚基表,并且偏移量发生了变化,但都指向共有成员变量a。
当创建b变量时,它的构造和析构顺序是A2() B2() C2() D2() ~D2() ~C2() ~B2 ~A2()
在这里,A2()只调用了一次,是由D2()直接调用的,D2()的构造函数发生了隐式扩展。
D2::D2() {
A2::A2(); // 直接调用虚基类 A2 的构造函数(关键!)
B2::B2(); // 调用 B2 的构造函数(跳过其中的 A2 构造)
C2::C2(); // 调用 C2 的构造函数(跳过其中的 A2 构造)
// D2 自身的代码
cout << "D2()" << endl;
}
在构造a的时候,会将B2 和 C2的_vbptr(虚基类指针)初始化,使其指向共有的部分a。
虚拟继承的优势
减小了内存的开辟
解释: d和d2中变量 char arr[1000] , int _b , int _c , int _d 。
组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。 优先使用对象组合,而不是类继承 。 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复,因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。 实际尽量多去用组合。
组合的耦合度低,代码维护性好。有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
class E2
{
public:
E2()
{
//cout << "D2()" << endl;
}
A2 a; // 每一个 E2 e 对象都有一个A2 a对象,
int _e;
private:
};
void test11()
{
E2 e;
// e._a // 没有该成员
e.a._a; // 必须通过 a 访问
}