继承的概念及定义
继承也是面向对象的三大特性之一,是为了代码能够复用的重要手段,它使得我们在原有的类特性的基础上进行扩展,产生新的功能,这样的类我们成为派生类,而原有的类则叫做基类。继承就和我们以前的函数复用一样,只是这次复用的是属于设计层次上的。
定义格式
例如:
class Person {
public:
string _name = "mingzi";
};
class Student :public Person{
public:
void print() {
cout << "name" << _name << endl;
cout << "stid" << _stid << endl;
}
private:
int _stid = 202238;
};
int main() {
Student stdt;
stdt.print();
}
此时Student和Person就成为了父子类关系,Person被称为基类,也叫做父类,Students被称为派生类,也叫子类,而public则叫做继承方法。上面的代码我们可以发现子类复用了父类的成员,并且我们在子类里可以使用父类的成员变量,这就是因为用的是public的继承方式 继承方式和访问限定符都是一样的,分别为public,protected,private三种
继承基类成员访问方式的变化
类成员/继承方法 | public继承 | protected继承 | private继承 |
---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 | 基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 | 基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
此时我们可以发现,不管是什么类型的成员还是继承方式,最后子类获得的都是范围最小的那一种即public>protected>private,protect叫做保护成员限定符,是因继承才出现的。简单来讲,private和不可见的区别是,派生类不可见是子类无法使用父类的private成员,而private是属于类内可以使用,但类外无法使用。而protected,父类的protected成员子类可以使用,但类外无法使用。 不过我们实际运用中一般都是public
基类和派生类对象赋值转换
int main(){
Person p;
Student s;
p=s;
s=p;
Person* ptr=&s;
Person& ref=s;
return 0;
}
我们把子类对象赋值给父类对象/指针/引用的行为叫做切割,天然行为,不存在类型转换(没有const临时变量)。形象的来讲因为子类中有全部的父类成员,把多余(属于自己子类)的部分给切除,就可以把子类内的成员依次给父类赋值了
继承中的作用域
基类和派生类都有独立的作用域,当子类和父类中有同名成员时,子类成员会隐藏父类成员,这种情况叫隐藏,也叫重定义
class Person {
public:
string _name = "mingzi";
int _stid=111;
};
class Student :public Person{
public:
void print() {
cout << "name" << _name << endl;
cout << "stid" << _stid << endl;
}
private:
int _stid = 202238;
};
int main() {
Student stdt;
stdt.print();
}
此时将会打印202238,因为子类的成员函数会优先调用自己的成员变量,将父类的隐藏(注:尽量不要重名,但是在虚函数中又不一样了,后面多态会讲)
class A{
public:
void fun(){
cout << "func" <<endl;
}
};
class B : public A{
public:
void fun(int i){}
};
int main(){
B b;
b.fun(10);
b.A::fun();
return 0;
}
此时A类和B类的两个fun函数构成隐藏关系(只要函数名相同,不管参数怎么样,就是隐藏关系),继承中函数名相同就是隐藏值得注意的是,重载的条件是在同一个作用域中。
派生类的默认成员函数
子类的构造函数——我们不写,编译器默认生成,此时 1.继承的父类成员作为一个整体——调用父类的默认构造函数初始化 2.自己的自定义类型成员 ——调用它的默认构造函数 3.自己的内置类型成员 ——不处理(除非声明时给了缺省值)
子类的拷贝构造函数也是同理——我们不写,编译器默认生成,此时 1、继承的父类成员作为一个整体 ——调用父类的拷贝构造 2、自己的自定义类型成员 —— 调用它的拷贝构造 3、自己的内置类型成员 —— 值拷贝
子类的拷贝赋值函数也是同理——我们不写,编译器默认生成
子类析构函数 – 我们不写,编译器默认生成 ——此时 1、继承的父类成员作为一个整体 – 调用父类的析构函数 2、自己的自定义类型成员 – 调用它的析构函数 3、自己的内置类型成员 – 不处理 子类析构函数和父类析构函数构成隐藏关系 因为编译器会对析构函数名做特殊处理,所有类的析构函数名都会被处理成统一名字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)
:Person(name)
, _id(id)
, _address(address)
{}
Student(const Student& s)
:_id(s._id)
, _address(s._address)
, Person(s)
{}
Student& operator=(const Student& s){
if (this != &s){
_id = s._id;
_address = s._address;
Person::operator=(s);
}
return *this;
}
~Student(){
}
private:
int _id;
string _address;
};
int main(){
Student s1("张三", 1, "西安市");
Student s2(s1);
Student s3("张思", 2, "北京市");
s1 = s3;
}
当父类的构造函数是无参或者给了全缺省的默认构造函数时,子类的构造函数也可以是默认构造函数(也可以不是),来调用父类的默认构造函数。但是当父类不是默认构造函数时(子类也不能使用默认构造函数),子类的构造函数必须对父类一个整体,进行初始化(如:Person(name),就像缺省值一样) ??声明的顺序,才是初始化顺序,所以子类的初始化列表里(列表里出现的顺序不重要,重要的是声明的顺序),无论父类Person在哪个位置,都是先初始化父类的 PS:这里比较啰嗦,代码里的注释没看懂的话可以看这。
拷贝构造:?值得注意的是,这里address用的是自定义类型的拷贝构造,即string的拷贝构造,因为在析构时并没有发生崩溃,说明是深拷贝
继承与友元
Display函数声明为友元,所以Display函数可以使用类里的成员变量,但对子类来说,友元不能继承,即基类友元不能访问子类私有和保护成员
class Student;
class Person{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name;
};
class Student : public Person{
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum;
};
void Display(const Person& p, const Student& s){
cout << p._name << endl;
cout << s._stuNum << endl;
}
void main(){
Person p;
Student s;
Display(p, s);
}
继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,子类不会有这样的static的成员,并且我们可以用类名去访问静态成员(由该类所有对象共享),类名::变量名
?复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称整个继承关系为单继承 多继承:一个子类有两个或者以上父类时整个继承关系称为多继承 菱形继承:时多继承的一种特殊情况
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 x;
x.A::_a=0
x.B::_a=0;
那这种情况下我们应该如何去解决这样的问题呢? 首先我们知道了D类中用拥有两个_a的成员变量,那么此时我们可以通过调试,使用内存可以看到他们的数据,首先我们先取d的地址 我们可以看到d内存和监视的地址是相同的 我们可以发现在B类和C类空间中都各有一份_a成员 此时,我们可以通过virtual,虚继承去解决二义性和数据冗余
class B : virtual public A
class C : virtual public A
class D : virtual public B, public C
int main(){
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
d._a = 0;
}
此时我们在到内存中查看
这时候我们可以发现BC空间中没有了_a的数据,在D类的空间中出现了2,但BC中产生了两个地址
我们进入到两个地址时,发现了14和0c两个数据 那么这是什么呢? 实际上这里两个值叫做偏移量,0x14=20,0x0c=12,这时候我们可以进行计算,D空间地址-B空间地址=20,D空间地址-C空间地址=12,所以我们可以通过存储偏移量,这样我们就可以只存1份_a了。 上面两个表叫做虚基表,我们通过B和C的两个指针指向一张表,指针叫做虚基表指针 但实际上因为我们需要格外增加指针寻找变量,所以效率降低了,更复杂
B b=d;
B* p=&d;
B& r=&d;
总结
在多继承中我们可以感受到C++的复杂性,所以我们一般不建议设计出多继承,一定不要有菱形继承 我们应该优先使用组合,而不是类继承,继承中基类的内部细节对子类可见,继承一定成都破坏了基类的封装,使得基类和派生类耦合度很高 所以我们尽量使用组合,降低耦合度,组合被叫做黑箱复用,内部不可见。而继承叫白箱复用 最后感谢各位看到这里噢!!喜欢的可以点个赞噢!
|