目录
前言
继承的概念
继承的定义
继承方式与权限
基类和派生类对象赋值转换
对象赋值
指针赋值
引用赋值
继承中的作用域
派生类的默认成员函数
构造函数
基类为默认构造(无参/全缺省构造)
基类为非默认构造函数
拷贝构造
析构函数
operator = 重载
继承与友元
继承与静态成员
单继承
多继承
菱形继承
菱形继承导致的问题以及解决方案
1.二义性
解决方案:
2.数据冗余
解决方案:
总结
继承在C++的学习中是对前面类的学习的补充和拓展,总而言之,这篇文章我们还是围绕类去学习。
在学习继承之前,我首先要强调一点:继承实质是代码复用。我们知道函数是可以被复用的,有些函数的功能比较常用,在其他函数中就可以直接调用完成相应任务的处理,避免写重复的代码。对于类而言,这样的情况也会出现,同样是为了避免写重复的代码,我们在构造类的时候,可以将重复的内容从其他的类那里继承下来,变为自己的。
一个类B要想继承一个类A,具体要怎么操作呢?
class A
{
};
class B :public A //B继承A
{
};
这里A类被称为基类(父类),B类称为派生类(子类),而中间的public则是继承方式。
格式:派生类 : 继承方式 基类
我们在学习类的时候了解到类的访问限定符:public、protected、private。
实际上继承也分三种方式:public、protected、private。和访问限定符的分类是对应着的。
到了这里,就涉及到了两者之间的相互影响了。毕竟你要继承一个类,总得清楚这个类本身的成员访问权限,以及你要怎样去继承这些成员。只有把这些都搞清楚,才能在后面避免使用权限越界。这里有关于两者之间的搭配效果:
小结:
1.当基类的成员权限是private时, 无论以怎样的继承方式,派生类都无法访问于基类的成员。
2.除基类的成员权限是private时,访问权限的最终结果按照继承权限和基类成员权限中的最小级来算。权限等级(由小到大):public > protected > private
3.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public。
4.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
派生类中能访问,就定义为protected。
代码测试:
#include
#include
using namespace std;
/**********************************************************/
class A
{
public:
A(const char* str="_strA")
:_strA(str)
{}
void f()
{
cout << _strA << endl;
}
protected:
string _strA;
};
/**********************************************************/
class B :public A
{
public:
B(const char* str="_strB")
:_strB(str)
{}
void test()
{
cout << _strA << endl; //因为是public继承,所以可以访问_strA。
}
protected:
string _strB;
};
/**********************************************************/
int main()
{
B b;
b.f();
b.test();
return 0;
}
由于继承的影响,派生类拥有基类所有的成员(除静态成员),因此我们完全可以理解把一个派生类赋值给基类的操作:把相同的成员变量赋值过去,不处理派生类所特有的成员变量。但是对于把基类赋值给派生类的操作逻辑上也行不通的,毕竟派生类所特有的成员变量基类没有,怎么赋值?
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用 (语法天然支持,没有类型转换,即没有临时变量的生成)
特别的,对于派生类赋值给基类的操作也叫切片或切割。
int main()
{
// A基类,B派生类
A a("皮皮蜥");
B b;
a = b;
b = a; //错误❌
return 0;
}
int main()
{
B b;
A* ptra;
ptra = &b;
return 0;
}
int main()
{
B b;
A& refa = b;
return 0;
}
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
#include
#include
using namespace std;
/**********************************************************/
class A
{
public:
A(const char* str="_strA")
:_strA(str)
{}
void f()
{
cout << _strA << endl;
}
protected:
string _strA;
};
/**********************************************************/
class B :public A
{
public:
B(const char* str="_strB")
:_strB(str)
{}
void f()
{
cout << _strB << endl;
}
protected:
string _strB;
};
/**********************************************************/
int main()
{
B b;
b.A::f();
b.f();
return 0;
}
上面的代码中A和B中都有一个函数 f() ,程序走到 b.f() 时,实际上调用的是B类中的函数,要想调用A中的 f() ,必须要显示说明:b.A::f() ,即类域A中的函数 f() 。
派生类的构造函数不仅仅要完成自己成员的初始化,而且要完成继承的基类成员的初始化。简单的说就是一个构造函数完成两个类的构造。
当基类的构造函数为默认构造函数时,派生类在构造时会默认调用基类的构造函数,不用显示调用。
//A的构造函数,为全缺省构造函数
A(const char* str="_strA")
:_strA(str)
{}
//B的构造函数
B(const char* strB = "_strB")//默认调用A()
:_strB(strB)
{}
此时派生类的构造函数的初始化列表中要显示调用基类的构造函数。
A(const char* str)
:_strA(str)
{}
B(const char* strA="_strA", const char* strB = "_strB")
:A(strA) //显示调用A()
,_strB(strB)
{}
注:只要基类的成员变量访问权限对于派生类不是public,就不能直接在派生类的初始化列表里用基类成员直接初始化。如:
B(const char* strA="_strA", const char* strB = "_strB")
:_strA(strA)//❌,错误写法
,_strB(strB)
{}
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
代码测试:
#include
#include
using namespace std;
class A
{
public:
A(const char* str="_strA")
:_strA(str)
{
cout << "A()" << endl;
}
A(const A& a)
:_strA(a._strA)
{
cout << "A(const A& a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
void f()
{
cout << _strA << endl;
}
protected:
string _strA;
};
class B :public A
{
public:
B(const char* strA="_strA", const char* strB = "_strB")
:A(strA)
,_strB(strB)
{
cout << "B()" << endl;
}
B(const B& b)
:A(b)
,_strB(b._strB)
{
cout << "B(const B& b)" << endl;
}
~B()
{
cout << "~B()" << endl;
}
void f()
{
cout << _strB << endl;
}
protected:
string _strB;
};
int main()
{
B b1;
B b2(b1);
return 0;
}
与构造函数相对应,派生类的析构函数是先调用自己的析构函数,处理自己的成员变量,之后再调用基类的析构函数,处理基类的成员变量。
可以看出构造的时候是先基类后派生类,析构的时候是先派生类后基类,刚好对应。
派生类的operator=必须要调用基类的operator=完成基类的复制。
代码测试:
A& operator=(const A& a)
{
if (this != &a)
{
_strA = a._strA;
cout << "A& operator=(const A& a)" << endl;
}
return *this;
}
B& operator=(const B& b)
{
if (this != &b)
{
A::operator=(b);//显示调用A的
_strB = b._strB;
cout << "B& operator=(const B& b)" << endl;
}
return *this;
}
友元关系不能继承!!
当基类中有友元函数时,派生类是不能把这个友元函数继承下来的。如果这个友元函数还想要访问派生类的成员变量或成员函数,必须要在派生类里重新声明一下友元函数。
#include
#include
using namespace std;
class B;//先声明B类存在,避免友元函数向上找不到B
class A
{
friend void print(const A& a, const B& b);
public:
/.../
protected:
string _strA;
};
class B :public A
{
friend void print(const A& a, const B& b);
public:
/.../
protected:
string _strB;
};
//友元函数
void print(const A& a, const B& b)
{
cout << a._strA << endl << b._strB << endl;
}
int main()
{
B b;
A a;
print(a, b);
return 0;
}
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。
代码测试:
class A
{
public:
A()
{
cout << "A()" << endl;
num++;
}
void show()
{
cout << num << endl;
}
protected:
static int num;
};
int A::num = 0;
class B :public A
{
public:
B()
{
cout << "B()" << endl;
num++;
}
};
class C :public B
{
public:
C()
{
cout << "C()" << endl;
num++;
}
};
int main()
{
A a;
a.show();
B b;
b.show();
C c;
c.show();
return 0;
}
每次调用A的构造函数都使num自增1
由结果可知每次的num都是最初的那个,否则每次的show的结果应该都是1。
一个派生类只有一个直接基类时称这个继承关系为单继承
菱形继承实际上是多继承的一种特殊情况。
为了方便测试,我们暂时把各个类的成员变量权限设为public。
class A
{
public:
A(const int& a=1)
:_a(a)
{}
int _a;
};
class B :public A
{
public:
B(const int& a=1, const int& b=2)
:A(a)
,_b(b)
{}
int _b;
};
class C :public A
{
public:
C(const int& a = 11, const int& c = 3)
:A(a)
, _c(c)
{}
int _c;
};
class D :public B, public C
{
public:
D(const int& a=1, const int& b=2, const int& c=3, const int& d=4)
:B(1,b)
,C(11,c)
,_d(d)
{}
int _d;
};
int main()
{
D d;
d._a = 1;
return 0;
}
实际在运行后会出编译错误,原因在于指向不明确,毕竟有两个_a。这时就需要显示指明对象了。
从上面的代码不难看出,_a这个数据出现了两次,造成了数据的冗余。
五个整型变量的大小(_a、_a、_b、_c、_d) ,_a刚好重复导致。
B和C在继承A的时候使用虚拟继承,作用在于B和C共有的A只出现一次,且不在B或C中了,单独在内存中,且属于D。
代码:
class B :virtual public A
{
public:
B(const int& a=1, const int& b=2)
:A(a)
,_b(b)
{}
int _b;
};
class C :virtual public A
{
public:
C(const int& a = 11, const int& c = 3)
:A(a)
, _c(c)
{}
int _c;
};
上图可以观察到A确实不在B或C中了,只存在D中!
但是B和C中除了有02 00 00 00和03 00 00 00这样的数字,还有e0 9b 0f 00和e8
9b 0f 00这样的指针,这些指针到底是干嘛的呢?
这里就要引入虚基类、虚基表和虚基表指针的概念了。
因为B和C是虚拟继承A,因此A又叫做虚基类。而现在A内存上不属于B和C了,但是逻辑上还是属于的,因此我们通过B和C也要能够找到A才对,这时就需要一个指针来帮助寻找了,这个指针就叫做虚基表指针,指向的空间为虚基表,其存放有偏移量,可根据偏移量来查找虚基类的位置。
我们通过内存来查看一下:
此时的d的大小为24字节,刚好是4个整形变量(_a、_b、_c、_d)和2个虚基表指针的大小,说明数据冗余的问题得到了解决。
事实上,此时的二义性也得到了解决,因为无论是通过谁去访问的_a,都只有一个_a去访问,只不过B和C是通过虚基表指针来访问,D就可以直接访问,对最终的结果没影响。
C++支持的多继承会出现菱形继承的现象,使整个继承过程变得相对麻烦了一点,且理解困难。因此在使用继承的时候,能不用多继承就不用。
类的复用除了继承之外,还有组合。就继承而言,对于基类和派生类的关联性我们能够感受得到是很强的,也就是耦合性太高,不利于代码的维护。而组合就类似于不同类之间的功能借用,类与类之间并不清楚内部的构造,只知道有一些功能的接口,这样类与类之间的耦合性大大降低了。因此继承和组合的使用要看实际情况来选择性地使用。