前言
提示:这里可以添加本文要记录的大概内容:
面向对象有三大基本概念:封装、继承、多态。今天就来拷打继承。
提示:以下是本篇文章正文内容,下面案例可供参考
一、初步拷打继承
继承:能够让不同的类联系在一起构成的一种层次关系。被继承的类称之为基类,继承的类称之为派生类。派生类又称之为子类。基类称为父类。继承方法有三种,分别为public(公有)继承,protected(保护)继承,private(私有)继承,又因为访问限定符有三种,形成了九种情况。我们要记住,private成员在派生类中不可见即不能使用,(父类的私有成员子类不能使用,无论用什么方法继承)。public继承基本上满足的大多数场景
这是继承的语法B能继承A的成员,具体场景看上面表格
class A
{
public:
void printf_()
{
cout << _a << endl;
}
protected:
int _a = 9;
};
//class B :private A
//class B :protected A // 可以
class B :public A // 没有问题
{
public:
void _printf()
{
cout << _a << endl;
cout << _b << endl;
}
protected:
int _b = 10;
};
int main()
{
A a;
B b;
a.printf_();
b.printf_();
return 0;
}
二、使用步骤
1.基类和派生类对象赋值转换
我们知道, int i = 0; double j = i ; 这种情况会发生类型转换。即生成一个临时变量,给j 。那么在继承中 有两个对象A(基类)和B(派生类),A=B(派生类在赋值给基类的时候,会自动将基类没有的给切出去,基类有的给赋值过去,也叫切片,并且中间不会产生临时变量), B=A(错误,基类是不能够给派生类的。这是由于基类有的派生类一定有,而派生类有的基类不一定有,如果基类的成员赋值给派生类,那么派生类当中基类没有的该怎么办,所以是不可取的)。
class A
{
public:
void printf_()
{
cout << _a << endl;
}
protected:
int _a = 9;
};
class B :public A
{
public:
void _printf()
{
cout << _a << endl;
cout << _b << endl;
}
protected:
int _b = 10;
};
int main()
{
A a;
B b;
a.printf_();
b.printf_();
int i = 1;
double j = i;
// double& e = i; 中间有临时变量。临时变量具有常性
const double& e = i;
a = b;// (向上转换)
// b = a;// (向下转换)
B& _a = b;
return 0;
}
2.继承中的作用域
在继承体系中,基类和派生类都有自己独立的作用域。
基类和派生类在不同的作用域可以有同名的成员,同名的函数,在这种情况下,派生类同名成员将屏蔽父类对同名成员的直接访问,这叫做重定义也可以叫隐藏。(要想访问基类同名的成员函数,可以使用 基类::基类成员 进行显示访问)
成员函数的重定义,只需要函数名相同就构成。(一般不会用重定义)
语法上,现在派生类中查找成员,派生类没有 再去基类查找。(子可以调父,父不可以调子)
有时候会把重载和继承混淆,记住一点:重载在一个作用域,重定义不在同一个作用域
class A
{
public:
void fun()
{
cout << "A fun" << endl;
}
void printf_()
{
cout << "A a= " << _a << endl;
}
protected:
int _a = 9;
};
class B :public A // 没有问题
{
public:
int fun(int _a) // 仅仅函数名相同构成隐藏
{
cout << "B fun" << endl;
return 0;
}
void _printf()
{
cout << "B a=" << _a << endl;
cout << "B(A::_a) a=" << A::_a << endl; // 调用基类中的a
cout << "B b=" << _b << endl;
}
protected:
int _a = 11; // 对变量进行重定义
int _b = 10;
};
int main()
{
A a;
B b;
b._printf();
b.printf_();
a.printf_();
b.fun(2);
a.fun();
b.A::fun();
return 0;
}
3.派生类中的默认成员函数
6个默认成员函数,这个就比较熟悉了,
C++有规定:派生类必须调用基类的构造函数,初始化派生类的成员
派生类不能在显示的构造函数列表中初始化基类。
析构函数是自动调用
调用构造函数的时候保证先父后子,调用析构函数的时候保证先子后父(先进先出原则)
派生类的operator=必须要调用基类的operator=完成基类的复制
class A
{
public:
A(int a = 9)
:_a(a)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl; // C++有规定:派生类必须调用基类的构造函数,初始化派生类的成员
}
protected:
int _a;
};
class B :public A
{
public:
protected:
int _a = 11; // 对变量进行重定义
int _b = 10;
};
class C :public A
{
public:
C(int a = 12, int c = 14)
:_c(c)
// ,_a(a) //派生类不能在显示的构造函数列表中初始化基类。
,A(a) //语法:初始化 _a 这里是A先初始化,初始化顺序和初始化列表没有关系跟声明顺序有关系
{
cout << "C()" << endl;
}
~C()
{
cout << "~C()" << endl;
}
C(const C& c)
:_c(c._c)
//,_a(c._a) //拷贝构造也一样
, A(c) //子类对象可以传给父类指针或者引用,指针或者引用会进行切割(切片),如果不写对于父类会默认调用父类的构造函数
{}
C& operator=(const C& c)
{
if (this != &c)
{
_a = c._a;
_c = c._c;
}
return *this;
}
protected:
int _c;
};
int main()
{
C c;
C c_(c);
C c1(2, 3);
c = c1;
return 0;
}
4.友元和继承
友元关系不能继承,基类友元不能访问派生类私有保护成员
5. 继承的静态成员
基类定义了一个static静态成员,这个静态成员同时属于基类和派生类,在派生类中不会单独拷贝一份,继承的是使用权。使用的是同一个地址,同时属于父类和派生类,在派生类中不会单独拷贝一份。
3.菱形继承 (大坑)
3.1初窥门径
C++中继承方式有两种,一个是单继承:一个子类只有一个父类时称这种继承关系为单继承。多继承:一个子类有两个或者以上的父类直接继承称为多继承,而菱形继承是多继承的一种特殊情况。
通过这个图片,很容易就可以理解菱形继承的概念,那么问题来了,最后一个D继承的是B与C而这两个类里都有一个共同的A那么A中的成员怎么算,这不就相当于有两个A中的成员变量了。
3.2虚拟继承
菱形继承出了一个大坑,为了填坑,引出了虚拟继承,在一个函数腰部的位置加上一个virtual( class B : virtual public A,class C: virtual public A)这就是虚继承。它的作用:B,C两个类在D中只有一个A。解决出现两个A导致数据冗余和二义性。
看好下面代码,我后面会根据下面代码进行调试观看和分析
带着问题走:什么是菱形继承?菱形继承的坑是什么?什么是菱形虚拟继承?菱形虚拟继承在底层是怎么体现的。
class A
{
public:
int _a;
};
class B :virtual public A
{
public:
int _b;
};
class C :virtual public A
{
public:
int _c;
};
class D :public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
d._a = 0;
return 0;
}
这里我们模拟了两个不同的情况,一个是没有virtual的菱形继承,和一个有virtual的虚拟继承,并且在内存中进行观察。通过观察发现,菱形继承有两个A类成员也就是_a,分别放在B类和C类里面。
对比着下面的图进行理解,在虚拟继承中 _a只有一个并且在最下面,而B和C的地址里面存了两个参数,一个是变量,一个是地址我们称它为指向地址,在内存中找到指向地址,发现指向地址里面存的是数,B是20,C是12,在反过来看内存1发现0x010FFC38地址到0x010FFC4C直接间隔20,0x010FFC40到0x010FFC4C直接间隔16,也就是存放的是距离A的偏移量(相对距离),用来找公共的A。我们看内存2与内存3发现第一个是0那是进行预留是无论D定义多少对象都可以在这里面存放偏移量。这个表也叫虚基表专门用来存放偏移量的。如果在创建变量d1 d2等等等,会再次开辟一个空间,但B类的首地址存放的一定是 00 69 7b 40而C类的首地址存的一定是 00 69 7b 48,当然不同的编译器包括重新调试的时候地址是不一样的这个应该知道。但首地址存放的地址一定是一样的。