跳到主要内容
C++ 继承入门:从概念定义到默认成员函数详解 | 极客日志
C++ 算法
C++ 继承入门:从概念定义到默认成员函数详解 继承是 C++ 面向对象的核心机制,用于代码复用与扩展。本文涵盖继承的基本定义、三种访问权限模式(public/protected/private)及其对成员可见性的影响。重点解析了派生类对象向基类的隐式转换(对象切片),以及同名成员在作用域中的隐藏规则。此外,详细阐述了构造、拷贝、赋值及析构函数的执行顺序:构造先父后子,析构先子后父。掌握这些规则能有效避免内存泄漏与访问错误,实现安全的类设计。
C++ 继承入门:从概念定义到默认成员函数详解
在面向对象编程中,实现多个类时总遇到'重复定义'的问题?比如 Student 和 Teacher 类都要写姓名、地址、身份认证函数,改一处就要两处同步更新——这就是没用到 C++ 的继承 机制。继承是面向对象复用代码的核心,能让子类直接'继承'父类的成员和方法,再扩展自己的专属功能。
一、继承的概念与定义
继承(Inheritance)机制是使代码复用的最重要手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法和属性,从而产生新的类,称为派生类 。
1. 继承的核心概念
父类(基类) :存放公共成员的类,例如 Person 类(包含姓名、地址、身份认证函数)。
子类(派生类) :继承父类并扩展专属成员的类,例如 Student(加学号)、Teacher(加职称)。
本质 :子类是父类的'扩展',能直接使用父类的公共/保护成员,无需重复定义。
2. 继承的定义格式
关键在于'继承方式 + 父类名',例如 class Student : public Person。
有了基础概念后,来看一段代码示例加深理解:
#include <iostream>
using namespace std;
class Person {
public :
void identity () { cout << "void identity()" << _name << endl; }
void age () { cout << _age << endl; }
protected :
string _name = "张三" ;
string _address;
string _tel = "123456" ;
private :
int _age = 18 ;
};
class Student : public Person {
public :
void {
cout << << _tel << endl;
();
}
:
_stuid;
};
: Person {
:
{ cout << << _tel << endl; }
:
string title;
};
{
Student s;
Teacher t;
s. ();
t. ();
s. ();
t. ();
;
}
study
()
"void study() "
age
protected
int
class
Teacher
public
public
void teaching ()
"void teaching() "
protected
int main ()
identity
identity
study
teaching
return
0
3. 继承方式与成员访问权限 父类成员在子类中的访问权限,取决于'父类的访问限定符'和'继承方式'。核心规则是:访问权限 = 两者中更严格的那个 (public > protected > private)。
父类成员类型 public 继承(推荐) protected 继承 private 继承 父类 public 成员 子类中为 public 子类中为 protected 子类中为 private 父类 protected 成员 子类中为 protected 子类中为 protected 子类中为 private 父类 private 成员 不可见(不可访问) 不可见(不可访问) 不可见(不可访问)
基类 private 成员在派生类中无论以什么方式继承都是不可见的。虽然它们被继承到了对象内存中,但语法上限制派生类无法访问。
如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为 protected 。可以看出保护成员限定符是因继承才出现的。
使用关键字 class 时默认的继承方式是 private ,使用 struct 时默认的继承方式是 public ,不过最好显式写出继承方式。
实际运用中一般使用 public 继承 ,几乎很少使用 protected/private 继承,因为后者继承下来的成员只能在派生类内部使用,扩展维护性不强。
这里拿之前的 Stack 实现来看,可以使用继承来实现:
#include <iostream>
#include <vector>
#include <list>
#include <deque>
using namespace std;
#define CONTAINER vector
namespace MyStack {
template <class T >
class stack : public std::CONTAINER<T> {
public :
void push (const T& x) {
CONTAINER<T>::push_back (x);
}
void pop () { CONTAINER<T>::pop_back (); }
const T& top () { return CONTAINER<T>::back (); }
bool empty () { return CONTAINER<T>::empty (); }
};
}
void Test2 () {
MyStack::stack<int > st;
st.push (1 );
st.push (2 );
st.push (3 );
while (!st.empty ()) {
cout << st.top () << " " ;
st.pop ();
}
}
int main () {
Test2 ();
return 0 ;
}
二、基类与派生类的转换:子类对象能当父类用吗? 这是继承的核心特性之一,简单说:子类对象能隐式转换成父类对象 / 指针 / 引用 ,反之不行。
public 继承的 派生类对象 可以赋值给 基类的指针/基类的引用 。这被称为对象切片 (Slicing),寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。
基类对象不能赋值给派生类对象。
基类的指针或者引用 可以通过强制类型转换赋值给派生类的指针或者引用 ,但必须确保基类的指针是指向派生类对象时才是安全的。在多态场景下,可使用 RTTI (dynamic_cast) 进行识别后进行安全转换。
class Person {
protected :
string _name;
string _sex;
int _age;
};
class Student : public Person {
public :
int _No;
};
void Test3 () {
Student st;
Person* ppe = &st;
Person& rpe = st;
Person pe = st;
}
为什么?
子类包含'父类部分 + 自己的部分',把子类当父类用,只会用到'父类部分',不会越界;但父类没有子类的成员,强行转换成子类会访问不存在的内容(比如学号),所以禁止。
三、继承中的作用域:同名成员会冲突吗? 父类和子类有独立的作用域,如果出现同名成员 (变量或函数),子类会隐藏 父类的同名成员,这就是**'隐藏规则'**,很容易踩坑。
在继承体系中基类和派生类都有独立的作用域。
派生类和基类有同名成员,派生类成员将屏蔽基类对同名成员的直接访问。
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏 (参数列表不重要)。
避坑提醒 :继承体系中,尽量不要定义同名成员。如果必须同名,访问时一定要加父类作用域。
1. 变量隐藏 class Person {
protected :
string _name = "张三" ;
int _num = 123456 ;
};
class Student : public Person {
public :
void Print () {
cout << "子类的_num:" << _num << endl;
cout << "父类的_num:" << Person::_num << endl;
}
protected :
int _num = 111 ;
};
int main () {
Student s;
s.Print ();
}
规则 :不管变量类型、参数,只要同名,子类就隐藏父类的。想访问父类的,必须用 父类名::成员名。
2. 函数隐藏 比变量隐藏更坑:只要函数名相同,不管参数列表,子类就隐藏父类的函数 。
class A {
public :
void fun () { cout << "func()" << endl; }
};
class B : public A {
public :
void fun (int i) { cout << "func(int i)" << i << endl; }
};
int main () {
B b;
b.fun (10 );
b.A::fun ();
return 0 ;
}
四、派生类的默认成员函数:构造、拷贝、析构怎么写? 子类和普通类一样,有 6 个默认成员函数,但子类的默认成员函数必须先处理父类的部分 。
核心规则 :子类的成员函数 = 父类成员的处理 + 子类成员的处理。
1. 构造函数:先调用父类构造,再初始化子类成员 派生类的构造函数必须调用基类的构造函数 初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表中显示调用。
class Person {
public :
Person (const char * name = "张三" ) :_name(name) {
cout << "Person()" << endl;
}
protected :
string _name;
};
class Student : public Person {
protected :
int _num;
string _address;
};
int main () {
Student s1;
return 0 ;
}
基类没有默认构造函数 的情况:(必须在子类构造的初始化列表中显式调用父类构造)
class Person {
public :
Person (const char * name) :_name(name) {
cout << "Person()" << endl;
}
protected :
string _name;
};
class Student : public Person {
public :
Student (const char * name = "张三" , int num = 18 , const char * address = "北京" )
: Person (name), _num(num), _address(address) {
cout << "Student()" << _name << endl;
}
protected :
int _num;
string _address;
};
int main () {
Student s1 ("李四" , 20 , "北京" ) ;
return 0 ;
}
关键顺序 :构造时 '先父后子' —— 父类先初始化,子类才能用父类的成员。
2. 拷贝构造:先拷贝父类,再拷贝子类 派生类的拷贝构造函数必须调用基类的拷贝构造 完成基类的拷贝初始化。
class Person {
public :
Person (const Person& p) :_name(p._name) {
cout << "Person(const Person& p)" << endl;
}
protected :
string _name;
};
class Student : public Person {
public :
Student (const Student& s)
: Person (s), _num(s._num), _address(s._address) {
}
protected :
int _num;
string _address;
};
int main () {
Student s1 ("李四" , 20 , "北京" ) ;
Student s2 (s1) ;
return 0 ;
}
3. 赋值重载:先赋值父类,再赋值子类 派生类的 operator= 必须调用基类的 operator= 完成基类的复制。需要注意的是派生类的 operator= 隐藏了基类的 operator= ,所以显示调用基类的 operator=,需要指定基类作用域 。
class Person {
public :
Person& operator =(const Person& p) {
if (this != &p) _name = p._name;
return *this ;
}
protected :
string _name;
};
class Student : public Person {
public :
Student& operator =(const Student& s) {
if (this != &s) {
Person::operator =(s);
_num = s._num;
_address = s._address;
}
return *this ;
}
protected :
int _num;
string _address;
};
int main () {
Student s1 ("李四" , 20 , "北京" ) ;
Student s3 ("王五" , 18 , "上海" ) ;
s1 = s3;
return 0 ;
}
4. 析构函数:先析构子类,再自动析构父类 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员 。这样才能保证派生类对象先清理派生类成员再清理基类成员。
class Person {
public :
~Person () { cout << "~Person()" << endl; }
protected :
string _name;
};
class Student : public Person {
public :
~Student () {
cout << "~Student()" << endl;
}
protected :
int _num;
string _address;
};
int main () {
Student s1 ("李四" , 20 , "北京" ) ;
return 0 ;
}
5. 实现一个不能被继承的类 方法一 :基类的构造函数私有 ,派生类的构造必须调用基类的构造函数,但是基类的构造函数私有化以后,派生类不可访问也就不能调用了,那么派生类就无法实例化出对象。
class Base {
public :
void func () { cout << "Base::func" << endl; }
protected :
int a = 1 ;
private :
Base () {}
};
class Derive : public Base { };
方法二 :C++11 新增了一个 final 关键字 ,final 修饰基类,派生类就不能继承了。
class Base final {
public :
void func5 () { cout << "Base::func5" << endl; }
protected :
int a = 1 ;
};
class Derive : public Base { };
继承的核心是'复用'与'扩展',吃透单继承的权限规则、对象转换、同名隐藏及默认函数逻辑,就能避开多数基础陷阱。总结如下:
用 public 继承实现代码复用,区分 public/protected/private 的访问权限。
记住**'子类能转父类,父类不能转子类'**,避免不安全转换。
同名成员会隐藏 ,访问时加父类作用域。
子类默认成员函数要先处理父类部分 ,尤其是构造和赋值重载。
构造顺序:先父后子 ,析构顺序:先子后父 。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online