【C++进阶】继承——代码+图文讲透继承:新手快速上手面向对象编程
文章目录
- 引言
- 一、定义
- 1. 访问限定符
- 2. 继承方式
- 3. 继承类模板
- 二、基类和派生类间的转换
- 三、继承中的作用域
- 小练习
- 四、派生类的默认成员函数
- 1. 构造函数
- 2. 拷贝构造函数
- 3. 拷贝赋值运算符
- 4. 析构函数
- 5. 不能被继承的类
- 五、多继承
- 1、多继承
- 2、菱形继承
- 4、虚继承
- 拓展
- 1、继承与友元
- 2、继承与静态成员
引言
- 继承(
Inheritance) 是面向对象编程(OOP)中最核心、最经典的三大特性之一;它解决了相同的方法在不同的对象中冗余实现的问题,是面向对象编程中使代码可复用的重要手段;- 在底层:子类对象的内部真实的包含一个基类对象;
一、定义
以上是一个派生类(子类)继承基类(父类)的基本格式,其中需要注意两个点:
- 访问限定符
- 继承方式
1. 访问限定符
访问限定符就是对类中的方法、成员变量的访问域限定;即public、protected、private;
- public表示外部(包含派生类类)可以访问;
- protected表示外部不可访问、派生类可以访问(给派生类保留的内部接口)
- private则表示只有类内部可以访问;
2. 继承方式
三种继承方式:public、protected、private
- public表示完全继承基类、类外可以访问基类的public限定的成员、派生类可以访问protected修饰的成员(常用)
- protected表示派生类可访问基类的成员,类外不可访问、并且可以继续继承给下一层派生类(基本不用)
- private表示派生类可访问基类的成员,类外不可访问、不能继续继承给下一层派生类
写法:
class Derived : public Base {};
class Derived : protected Base {};
class Derived : private Base {};
总之: 继承方式主要是对外部(包含下一层派生类)进行限制;派生类内部都可以访问基类的public/protected限定的成员;
| 基类成员 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | ❌ 不可访问 | ❌ 不可访问 | ❌ 不可访问 |
根据以上表格的总结,可以发现基类的private成员派生类中都不可访问;基类的public/protected限定的成员在派生类中的访问方式=Min(访问限定符,继承方式) 来计算;
不显示写继承方式时,struct声明的类默认继承方式为public,class默认为private;
3. 继承类模板
当基类是模板类时,派生类中复用基类中的方法时需要指定类域;否则编译报错
- 由于编译器对于模板类是进行按需实例化的,只有当我们传入具体的类型时,编译器才知道模板类的具体类型;为了提高效率;只有当我们调用时才会进行实例化;
- 指定类域的三种方法:
- 显式加 this->
- 在派生类模板里 using 声明
- 在调用函数前指定,如vector
::方法
#define CONTAINER std::vector //宏替换基类名称,为了方便修改基类
template<class T>
class stack:public CONTAINER<T>
{
public:
void push(const T& val)
{
//push_back(val);//此时基类并未实例化,编译器以为push_back是当前类的方法,最后报错找不到push_back
CONTAINER<T>::push_back(val);//指定类域,使用push时编译器对基类的push_back实例化
}
const T& top()
{
return CONTAINER<T>::back();
}
void pop()
{
this->pop_back();
}
bool empty()
{
return CONTAINER<T>::empty();
}
};
int main()
{
stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while(!st.empty())
{
cout << st.top() << " ";
st.pop();
}
return 0;
}
二、基类和派生类间的转换
- 通过
public继承的派生类对象可以赋值给基类的指针/引用,本质上就是将派生类中内嵌的基类对象切割出来,基类的指针/引用指向这部分; - 基类对象不能赋值给派生类指针/引用
class Base
{
public:
string _gender;
};
class Derived :public Base
{
private:
int _no;
};
int main()
{
Derived d1;
Base* p = &d1;
Base& ref = d1;
return 0;
}
运行结果:

指针p指向Derived派生类对象d1内的Base基类对象,引用ref也是基类对象的别名;可以通过这种方法更具体的操作派生类对象内的基类对象;

三、继承中的作用域
- 在C++中有三个作用域:类域、命名空间域、全局域;每个类都有独立的作用域,不同的类可以定义相同名称的成员,在继承中,派生类对象调用同名成员时,则会采用就近原则,调用当前派生类的成员,隐藏基类的成员;
- 只要函数名相同,即使返回值、形参不同,也构成隐藏;默认隐藏掉基类的同名函数
- 可以通过指定基类类域的方式调用基类的成员(基类::基类成员)
class base
{
public:
void print()
{
cout << "base print()" << endl;
cout << _gender << endl;
}
public:
string _gender="男";
int _no=123;
};
class derived :public base
{
public:
void print()
{
cout << "derived print()" << endl;
cout << _gender << endl;
cout << _no << endl; //默认使用当前类成员变量,隐藏基类成员变量,
cout << base::_no << endl; //指定类域,使用基类成员变量
}
private:
int _no=321;
};
int main()
{
derived d1;
d1.print(); //默认使用当前对象成员函数,隐藏base基类成员函数
d1.base::print();//指定类域,调用基类对象成员函数
}
小练习
下⾯程序的编译运行结果是什么()
A. 编译报错 B. 运行报错 C. 正常运行
class base
{
public:
void print()
{
cout << "base print()" << endl;
}
public:
string _gender="男";
int _no=123;
};
class derived :public base
{
public:
void print(int i)
{
cout << "derived print()" << endl;
}
private:
int _no=321;
};
int main()
{
derived d1;
d1.print(10);
d1.print();
}
解析:
派生类中定义了同名函数,基类中所有同名函数将会被隐藏(无论参数是否不同);函数重载 ≠ 继承重载;故:d1.print()编译报错
四、派生类的默认成员函数
派生类的每一个默认成员函数,都会“先处理基类部分,再处理派生类自身部分”。
1. 构造函数
- 派生类的构造函数必须调用基类的构造函数来构造基类对象;
- 当基类的默认构造函数不存在时(显示写了其他构造函数,默认构造则不会自动生成),需要在派生类的构造函数的初始化列表中调用基类的构造函数;
class Base {
public:
Base(int x) {}
};
class Derived : public Base {
public:
Derived()
:Base(10) //必须在初始化列表阶段调用基类构造
{}
};
2. 拷贝构造函数
- 派生类使用拷贝构造时,必须先调用调用基类的拷贝构造,完成基类的拷贝初始化;
- 不一定要手写出基类的拷贝构造语句,在默认的拷贝构造时也会发生;
class Base
{
public:
Base(const Base& b)
:_gender(b._gender)
{ }
public:
string _gender;
};
class Derived :public Base
{
public:
Derived(const Derived& d)
:Base(d) //先对基类拷贝初始化
,_no(d._no)
{ }
private:
int _no;
};
派生类
Deirved的拷贝构造中初始化列表中使用基类的拷贝构造初始化时;传入的是派生类对象,接收的参数是基类对象的引用,等同于const Base& b=d,这期间发生了切割;
3. 拷贝赋值运算符
- 派生类在调用
operator=时,需要先调用基类的operator=对基类的成员先完成赋值 - 派生类在调用基类的
operator=时,注意在前面加上基类的类域,否则构成隐藏,会自动隐藏基类的operator=,递归调用派生类的operator=造成栈溢出;
class Base
{
public:
Base& operator=(const Base& b)
{
_gender = b._gender;
return *this;
}
public:
string _gender;
};
class Derived :public Base
{
public:
Derived& operator=(const Derived& d)
{
Base::operator=(d); //显示指定基类的类域
_no = d._no;
return *this;
}
private:
int _no;
};
4. 析构函数
- 派生类的析构函数中不需要再调用基类的析构,因为派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序;
- 编译器会将析构函数名处理成
destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。 -
构造:先基类 → 后派生
析构:先派生 → 后基类
5. 不能被继承的类
- 将当前类的默认构造函数限定为私有,这个类就不能被继承
- C++11中在基类的后面加上
final(如:class Base final{……}),这个基类就不能被继承了
五、多继承
1、多继承
- 一个派生类继承2个及以上的直接基类时,就是多继承;
- 多继承的派生类对象在内存中:先继承的基类在前面,后继承的基类随其次,派生类的成员在最后;
- 多继承有些场景会存在二义性,比如:派生类的两个基类中有同名成员;需要指定具体基类的类域
class A { public: int x; };
class B { public: int x; };
class C : public A, public B {};
C c;
c.x; //存在二义性
c.A::x; //指定基类类域
c.B::x;

2、菱形继承
- 菱形继承时多继承的一种特殊情况;
- 菱形继承会造成数据冗余和二义性问题,应尽量避免菱形继承的产生;
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 {};
示意图:

根据以上示意图可以清晰的发现:D声明的对象内存中会存在两个A对象,同一份数据存了两份造成空间的浪费;当我使用对象D访问成员变量a时,是访问基类B中继承的A类中的a成员还是基类C中继承的A类中的a呢?存在二义性; 当然,可以通过类域的方式访问A对象中的成员变量a,但是修改了对象B中A对象的成员变量之后C对象中A对象的成员变量不会改变,同样A对象的成员变量的值就不同,逻辑混乱,不符合实际;
4、虚继承
- 虚继承(
Virtual)主要就是为了解决菱形继承中的数据冗余和二义性问题的; - 在中间层对共同基类使用
virtual继承,底层的内存模型中冗余的数据就只会有一份; - 这个共同的基类就是
虚基类;由“最底层派生类”负责构造,中间的派生类将会忽略对这个类的初始化;
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 {};
示意图:

最后我们通过D对象访问成员变量a时就不会存在二义性;内存模型中也解决了数据冗余的问题;但是使用虚继承也是需要一定的代价:比如在对象中会多一个虚基类指针vptr、访问成员变量时也会多一次间接寻址、并且内存也不是平铺的结构了;
拓展
1、继承与友元
- 友元关系不会被继承:基类的友元函数不能访问派生类的私有和保护成员;
- 如果想要访问派生类的私有和保护成员需要在派生类中再声明一下友元函数;
- 派生类的友元不能访问基类的
protect/public成员
class Student;
class Person
{
public :
friend void Display_person(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
friend void Display_student(const Person& p, const Student& s);
protected :
int _stuNum; // 学号
};
void Display_person(const Person& p, const Student& s)
{
cout << p._name << endl;
//cout << s._stuNum << endl;//不能访问,因为没在派生类中声明友元
}
void Display_student(const Person& p, const Student& s)
{
//cout << p._name << endl; //不能访问,友元不能访问继承的基类的成员
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
Display_person(p, s);
Display_student(p, s);
return 0;
}
2、继承与静态成员
- 静态成员属于类而非对象,继承只影响访问路径,不复制实体
- 派生类和基类的静态成员同名时,会构成隐藏;自动隐藏基类的同名成员
class Person
{
public :
string _name; // 姓名
static int _no;
};
class Student : public Person
{
public:
int _stuNum; // 学号
};
int Person::_no = 0;
int main()
{
Person p;
Student s;
cout <<"&person._name: "<< &p._name << endl;
cout <<"&student._name:"<< &s._name << endl;
cout <<"&person._no: " << &p._no << endl;
cout <<"&student._no: " << &s._no << endl;
return 0;
}
运行结果:

运行结果可以看出:基类的静态成员变量_no与派生类中继承的_no存放在同一块内存,说明静态成员不参与继承;
本篇文章就到此结束了,如果本文对你有帮助,麻烦你 👍点赞 ⭐收藏 ❤️关注 吧~











