目录
1、初识继承
2、继承的定义
3、派生类与基类之间的赋值
3.1 指向派生类的基类指针/引用
4、继承中的隐藏关系
5、成员函数的隐藏
6、派生类的默认成员函数
6.1 构造函数
6.2 拷贝构造
6.3 赋值重载
6.4 析构函数
7、继承和友元
8、继承与静态成员
9、菱形继承
9.1 虚拟继承(virtual)
10、继承和组合
结语:
前言:
在C++中,继承一般是作用于类的,继承的逻辑和函数复用的逻辑有些相似,比如一个类B继承了类A,那么可以在类B中直接访问类A的成员(私有成员除外),通常把类B叫做派生类(子类),而类A叫做基类(父类),并且抽象的把类A看作是类B中的一部分。
首先介绍一下protected(保护域),他的作用和private相似,即不能在类外进行对protected域内进行访问,但是类内可以随意访问,他跟private的区别在于派生类可以访问基类的protected域,但是派生类不能访问基类的private域。
体现继承的代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class A
{
public:
void funA()
{
cout << "funA()" << endl;
}
protected:
int a=1;
};
class B:public A//继承类A的写法,public表示继承A的公有部分和保护部分
{
public:
void funB()
{
cout << "funB()" << endl;
cout << a << endl;//可以打印类A中成员变量a的值
funA();//类A的成员函数也可以直接调用
}
protected:
int b;
};
int main()
{
B b1;
b1.funB();
return 0;
}
运行结果:
从结果可以看到,能够直接在类B中进行对类A成员的访问,仿佛类A中的成员就保存在类B中,也叫做类B“继承”了类A的内容。并且可以在主函数中通过变量b1去调用类A和类B的public区域,但是protected区域的内容不能在主函数中调用。
继承示意图:
上述代码的继承示意图如下:
在继承中,有三种继承方式:public、protected、private。这三种方式刚好与访问限定符用的同一关键字。
首先基类的private域无论采用哪种继承方式都不能在派生类中访问。其次三种继承方式有一个大小关系:public>protected>private。具体示意图如下:
这张表提供的信息就是如果是protected继承,则派生类访问基类的public成员需要用protected的访问权限去访问,虽然在派生类内都可以访问得到,但是在类外就有区别了,比如上述代码的类A函数funA,如果用的是protected继承,则主函数不能直接通过b1调用funA。
所以一般用的继承方式都是public,因为该继承方式不会修改对基类成员的访问权限。
内置类型支持不同类型的直接赋值,又称隐式类型的转换,比如:double b = 3.3,int a = b。同样派生类可以赋值给到基类对象、基类指针、基类引用,可以理解为这是一个“权限缩小”的过程,因为派生类原本就继承了基类,因此这个过程只会将派生类中的基类部分拷贝过去,又称切片。
代码体现派生类赋值给基类:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class A
{
public:
int a;
};
class B :public A
{
public:
int b;
};
int main()
{
B b1;
b1.a = 10;
A a1 = b1;//派生类赋值给基类
cout << a1.a << endl;
return 0;
}
运行结果:
如果把一个派生类的地址给到一个基类型指针,那么该指针维护的区域是派生类中基类部分,引用亦是如此。
示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class A
{
public:
int a;
};
class B :public A
{
public:
int b;
};
int main()
{
B b1;
A* pb = &b1;//pb的类型是基类
pb->a = 100;
cout << b1.a << endl;
A& rb = b1;//rb的类型是基类
rb.a = 1212;
cout << b1.a << endl;
return 0;
}
如果一个作用域中定义了两个同名变量,那么就会发生重定义的错误。如果派生类和基类中成员变量也存在同名,则不会报错,可以理解为派生类和基类处于两个不同的作用域,但是两个成员变量构成隐藏关系。隐藏关系导致访问派生类的成员变量会自动屏蔽基类的成员变量,若想访问基类变量则可以用:基类::成员变量名。
体现隐藏关系的代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class A
{
public:
int a = 30;
};
class B :public A
{
public:
int a = 20;
void func()
{
a++;//访问的也是B中的a
}
};
int main()
{
B b1;
b1.func();
cout << b1.a << endl;//访问的是B中的a
cout << b1.A::a << endl;//指定访问A中的a
return 0;
}
运行结果:
函数隐藏的条件是只要派生类和基类中的成员函数名相同就构成隐藏关系。
示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class A
{
public:
void func()
{
cout << "funcA" << endl;
}
};
class B :public A
{
public:
void func(int i)
{
cout << "funcB->"<
运行结果:
这里A类中的func和B类中的func看起来像是构成了函数重载,但是由于派生类和基类是处于两个不同的作用域,因此不会构成函数重载,然后由于派生类同名成员屏蔽基类同名成员,所以调用的默认是派生类的成员。
派生类会自动生成6个默认成员函数(前提是我们不写构造函数),常用的分别是构造函数、拷贝构造、赋值重载、析构函数(取地址重载很少用到就不算进去了)。
派生类中的基类部分的成员初始化必须要通过基类的构造函数进行,不能在派生类中完成对基类成员的初始化。
体现派生类自动调用基类的构造函数代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class A
{
public:
A(int i = 12)
:a(i)
{
cout << "A(int i = 12)" << endl;
}
int a;
};
class B :public A
{
public:
B(int x,int y)
:b(y)
//,A(x)//可以显示调用A构造函数,如果我们不调用,编译器也会自动调用
{
cout << "B(int x,int y)" << endl;
}
int b;
};
int main()
{
B b1(10,20);
cout << b1.a << endl;
cout << b1.b << endl;
return 0;
}
运行结果:
从结果可以看到,初始化的逻辑是先调用基类的构造函数进行对基类成员的初始化,然后再调用派生类的构造函数对派生类自己的成员进行初始化。
当一个派生类对象拷贝构造另一个派生类对象时,会先将基类部分拷贝过去,然后再拷贝派生类部分,并且基类部分的拷贝必须要调用基类自己的拷贝构造。
拷贝构造代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class A
{
public:
A(int i = 12)//构造函数
:a(i)
{}
A(const A& a1)//A类的拷贝构造
:a(a1.a)
{
cout << "A(const A& a1)" << endl;
}
int a;
};
class B :public A
{
public:
B(int x, int y)//构造函数
:b(y)
{}
B(const B& b1)//B类的拷贝构造
:b(b1.b)
,A(b1)//调用A的拷贝构造完成基类部分成员的拷贝
{
cout << "B(const B& b1)" << endl;
}
int b;
};
int main()
{
B b1(10, 20);
B b2 = b1;//b1拷贝给b2
cout << b2.a << endl;
cout << b2.b << endl;
return 0;
}
运行结果:
派生类对象之间的赋值逻辑也同上,即基类部分的赋值要调用基类部分自己的赋值重载。
赋值重载代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class A
{
public:
A(int i)//构造函数
:a(i)
{}
A& operator=(const A& a1)//赋值重载
{
a = a1.a;
return *this;
}
int a;
};
class B :public A
{
public:
B(int x, int y)//构造函数
:b(y)
,A(x)
{}
B& operator=(const B& b1)
{
if (this != &b1)
{
A::operator=(b1);//调用A类的赋值重载,需要指定类A调用,否则会无限递归
b = b1.b;
}
return *this;
}
int b;
};
int main()
{
B b1(10,20);
B b2(19,30);
b1 = b2;
cout << b1.a << endl;
cout << b1.b << endl;
return 0;
}
运行结果:
析构函数名通常都是:~+类名,但是在继承中,派生类和基类的析构函数名都会被统一处理成destructor,如此一来派生类和基类的析构函数就构成了隐藏关系,隐藏关系会让调用派生类成员时屏蔽基类的成员,所以如果要调用基类的析构函数必须要指定调用:基类名::基类析构函数。
析构顺序示意图:
可以发现析构的时候先析构派生类再析构基类,并且在调用派生类的析构函数时,编译器会自动的调用基类的析构函数,所以总结下来就是虽然派生类和基类的析构函数构成隐藏关系,但是也不需要我们手动调基类析构函数。
析构函数示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
};
class B :public A
{
public:
~B()
{
cout << "~B()" << endl;
}
};
int main()
{
B b1;
return 0;
}
运行结果:
友元关系不能被继承,虽然派生类继承了基类,但是基类的友元函数不能访问派生类中的私有和保护成员。因此若要从友元函数内访问派生类的成员,则要在派生类里再声明一个友元函数。
示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class B;
class A
{
public:
friend void friend1(const A& a1, const B& b1);
protected:
int a = 2;
};
class B :public A
{
public:
friend void friend1(const A& a1, const B& b1);//再次声明
protected:
int b = 23;
};
void friend1(const A& a1, const B& b1)
{
cout << a1.a << endl;
cout << b1.b << endl;
}
int main()
{
A a1;
B b1;
friend1(a1, b1);
return 0;
}
运行结果:
在基类定义的静态成员,则所有继承该基类的派生类都能看到该静态成员,并且他们是共用该成员。
示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class Person
{
public:
Person()
{
++_count;
}
protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
int main()
{
Student s1;
Student s2;
Person s3;
Graduate s4;
cout << " 人数 :" << Person::_count << endl;
Graduate::_count = 0;//从任意一个类中对_count归零后,其他类看到的也是0
cout << " 人数 :" << Person::_count << endl;
}
运行结果:
上文所提到的继承都是一个派生类对应一个基类,然而一个派生类也可以继承多个基类,甚至一个派生类继承的多个基类,这些基类又继承了其他的基类,这种复杂的结构称之为菱形继承。
菱形继承示意图如下:
从上图中看不出菱形继承存在什么问题,实则菱形继承存在数据二义性的问题,也就是派生类B会有两份基类D的成员内容,因为A继承了D的成员,然后B又继承了一份D的成员,最后B继承A和B的同时会继承A和B中的D成员内容。
具体示意图如下:
菱形继承的代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class D
{
public:
int d;
};
class C:public D
{
int c;
};
class A :public D
{
int a;
};
class B :public A,public C
{
int b;
};
int main()
{
B b1;
b1.d;//此时访问不到d,因为不知道访问的是A中的d还是C中的d
//只能指定访问
b1.A::d;
b1.C::d;
return 0;
}
对重复继承的基类使用虚拟继承就可以解决数据二义性的问题,即对上述代码中的类A和类B使用虚拟继承类D,这样一来D的成员d就只会存在一份。虚拟继承的用法:在继承方式前面加上关键字--virtual。
虚拟继承示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class D
{
public:
int d;
};
class C:virtual public D//虚拟继承D
{
int c;
};
class A :virtual public D//虚拟继承D
{
int a;
};
class B :public A,public C
{
int b;
};
int main()
{
B b1;
b1.A::d=1;
b1.C::d=2;
cout << b1.d << endl;//选取最后一次修改的值
return 0;
}
运行结果:
继承与组合的区别在于:继承的耦合性比组合要高。一般情况下我们写代码追求低耦合、高内聚。例如继承中的派生类和基类之间就处于一种高耦合的状态,本身派生类和基类就是两个类,但是如果修改派生类中基类部分成员的名字,则派生类有可能也需要修改。而组合就不会发生这种情况。(可以把高耦合理解为若改一处代码,则其他的代码也要跟着改动)
继承与组合区别图如下:
当然,继承也有继承的优势,有些场景下就适合用继承,只不过类与类之间的关系也可以用组合的形式表达。
以上就是关于继承的讲解,继承作为C++的三大特性之一自然是非常重要的。最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞+关注+收藏!如果有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!