目录
继承
继承方式
切片(切割)
重定义(隐藏)
继承的6个默认成员函数
继承与友元,静态成员
菱形继承
菱形继承的冗余和二义性
继承和组合
继承
什么是继承?
是代码复用的一种手段。
语法:
父类也叫基类(Base)
子类也叫派生类(Derived)
class Person
{
private:
char sex;
};
class Student :public Person
{
private:
string _name;
string _tel;
};
继承方式
分为3大类,public继承,protected 继承,private继承
不可见就是无论在类外还是在类内都不能使用该继承而来的成员变量。
切片(切割)
将子类赋值给父类是因为数据的类型不同,会发生类型转换,该类型转换就是切片(切割)
class Base
{
public:
Base(int a = 1)
{
_a = a;
}
void print()
{
cout << _a << endl;
}
int _a;
};
class Derived :public Base
{
public:
Derived(int b = 2)
{
_b = b;
}
int _b;
};
int main()
{
Derived b(3);
Base a;
//子类可以赋值给父类的对象,指针和引用
a = b;
Base* c = &b;
Base& d = b;
//父类不能赋值给子类对像
//b = a;
//父类指针可以强转为子类指针但使用强转后的父类指针可能出现越界问题
Derived* f = (Derived*)c;
//越界
f->_b;
return 0;
}
如何实现子类到父类的赋值的?
重定义(隐藏)
父类和子类中有同名的成员或同名的函数,调用子类中的同名成员是就会屏蔽父类中的同名成员。
注意:只要同名就构成屏蔽(重定义)
class Base
{
public:
Base(int a = 1)
{
_a = a;
}
void print()
{
cout << _a << endl;
}
int _a;
};
class Derived :public Base
{
public:
Derived(int b = 2)
{
_b = b;
}
void print()//重定义的成员函数
{
cout << _a << endl;
cout << _b << endl;
}
double _a = 1.1;//重定义的成员变量
int _b;
};
重定义的本质:基类和派生类的作用域不同,重定义的本质就是如果构成重定义,默认情况下就不会去基类中去查找同名的成员。
如何去访问基类的同名成员?
以上知识默认情况下,当然也可以指定类域去访问基类的同名,这样就可以访问基类的同名成员了。
Base a;
a.print();
Derived b;
b.print();
b.Base::print();//指定类域去访问同名基类成员
class Base
{
public:
Base(int a = 1)
{
_a = a;
}
void print()
{
cout << _a << endl;
}
int _a;
};
class Derived :public Base
{
public:
Derived(int b = 2)
{
_b = b;
}
void print(int b)//重定义的成员函数
{
cout << _a << endl;
cout << _b << endl;
}
double _a = 1.1;//重定义的成员变量
int _b;
};
基类中pirnt成员函数是否与派生类中的print成员函数构成函数重载?
答案是否,因为函数重载的条件是,同一作用域,函数名相同,参数列表不同。
因为基类的print和派生列的print不在同一作用域,所以两个print不构成函数重载。
继承的6个默认成员函数
1.派生类的初始化也需要初始化基类的成员,所以需要调用基类的构造函数,如果基类没有默认构造函数的话,那么就需要在初始化列表中显示调用基类的默认构造函数。
class Base
{
public:
//此时基类没有默认构造函数
Base(int a)
{
_a = a;
}
void print()
{
cout << _a << endl;
}
int _a;
};
class Derived :public Base
{
public:
Derived(int b = 2)
:Base(1)//基类没有默认构造函数,必须显示调用基类的构造函数
{
_b = b;
}
void print(int b)//重定义的成员函数
{
cout << _a << endl;
cout << _b << endl;
}
double _a = 1.1;//重定义的成员变量
int _b;
};
所以完成派生类的初始化时,需要调用基类的构造函数,基类和派生类构造函数的调用顺序是先调用基类的在调用派生类的。
为什么?
因为派生类的初始化可能用到基类的成员。
比如,以下这个例子,如果不先调用基类的构造函数,那么派生类的成员就无法正常完成初始化
class Base
{
public:
//此时基类没有默认构造函数
Base(int a)
{
_a = a;
}
void print()
{
cout << _a << endl;
}
int _a;
};
class Derived :public Base
{
public:
Derived(int b = 2)
:Base(1)//显示调用基类的构造函数
,_b(_a)//用基类的成员初始化
{
;
}
void print(int b)//重定义的成员函数
{
cout << _a << endl;
cout << _b << endl;
}
int _b;
};
2.析构函数是先派生类自己的析构函数,然后再调用自动调用基类的析构函数(这里是基类中的析构函数,不单单是指默认析构函数)。
这里的自动调用是编译器的工作,意思就是我们不用显示调用,编译器会完成。
为什么是这样的顺序呢?
因为在派生类析构的时候可能调用基类的成员,如果提前析构了,那么就无法找到该变量。
class Derived :public Base
{
public:
Derived(int b = 2)
:Base(1)//显示调用基类的构造函数
,_b(_a)//用基类的成员初始化
{
;
}
void print(int b)//重定义的成员函数
{
cout << _a << endl;
cout << _b << endl;
}
~Derived()
{
Base::~Base();//先调用基类的析构
cout << _a << endl;//但后续需要用到基类的成员
cout << "~Derived()" << endl;
}
int _b;
};
由以上的运行结果可知无论是否显示调用基类的析构函数,编译器都会自动调用基类的析构函数一次,有以前的知识可知,有的类不能调用两次析构函数,所以不要显示调用基类的析构函数。
析构函数因为重写(后续会谈),而重写需要函数同名,很明显表面上基类和派生类的析构不同名,而编译器会将两个析构函数都处理为destructor()函数,这样就同名了。如果不加virtual修饰,那么基类和派生类的析构函数就构成重定义(隐藏),基类的析构加上virtual就构成重写。
3.完成派生类的拷贝构造,赋值重载都需要调用基类中相应的函数,完成基类中拷贝构造或赋值。
继承与友元,静态成员
友元是不能继承的。
在整个继承体系中只有一个静态成员。
菱形继承
菱形继承的冗余和二义性
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;
};
由图可知D中有两个_a有冗余,并且如果不指定类域的话不知道_a是继承的B的_a,还是继承的C的_a。
这就是菱形继承的冗余和二义性。
菱形D类在内存中的结构
如何解决菱形继承中的冗余和二义性?
使用虚继承,
class A
{
public:
int _a;
};
class B :virtual public A//虚继承A
{
public:
int _b;
};
class C :virtual public A//虚继承A
{
public:
int _c;
};
class D :public B, public C
{
public:
int _d;
};
虚继承的解决原理就是 在B类和C类中有个指针指向续基表,虚基表中存的是偏移量,偏移量就是相应类的_a在D类的偏移量。
class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "peter";
}
继承和组合
//继承
class A
{
public:
int _a;
};
class B :public A
{
public:
int _b;
};
//组合
class A
{
public:
int _a;
};
class B
{
public:
A a;
int _b;
};
public继承就是一种 is-a的关系,就比如人与黄种人的关系,黄种人是一种人。
组合就是一种 has-a 的关系,就比如汽车与轮胎的关系,轮胎是汽车的组成之一。
优先使用组合,后考虑继承。
1.继承是一种白箱复用,基类的细节派生类可见,封装性低;基类和派生类依赖性强,耦合度高。
2.组合是一种黑箱复用,依赖性弱,耦合度低。