C++ 继承机制详解:同名成员隐藏与默认函数
继承是面向对象编程(OOP)中实现代码复用的核心手段。它允许我们在保持基类特性的基础上扩展新的方法和属性,从而生成派生类。这种机制体现了由简单到复杂的层次结构,是类设计层面的复用方式。
什么是继承?
假设我们有两个类 Student 和 Teacher,它们都有姓名、地址、电话等公共成员,也有各自独有的成员。如果分别定义,会导致大量重复代码。通过引入一个 Person 基类,将公共部分提取出来,派生类只需关注差异部分,即可避免冗余。
class Person {
public:
void identity() {
cout << "identity()" << _name << endl;
}
protected:
string _name = "张三";
int _age = 18;
string _address;
string _tel;
};
class Student : public Person {
public:
void study() {
cout << "study()" << endl;
}
protected:
int _stuid;
};
继承的定义与访问权限
在 C++ 中,基类(父类)和派生类(子类)的关系通过继承方式定义。主要有三种:public、protected、private。
继承后的访问权限变化遵循以下规则:
- 基类成员的访问权限在派生类中不会变宽,只会变窄或保持不变。
- 使用
class关键字时,默认继承方式是private;使用struct时,默认是public。建议显式写出继承方式。 - 实际开发中通常使用
public继承。protected或private继承会导致成员只能在派生类内部使用,降低了扩展性和维护性。
此外,继承不仅限于普通类,还可以继承类模板。例如用继承实现 stack 容器适配器:
template<class T>
class stack : public std::vector<T> {
public:
void push(const T& x) {
// 基类是类模板时,需要指定作用域,否则编译报错
vector<T>::push_back(x);
}
void pop() {
vector<T>::pop_back();
}
const T& top() {
return vector<T>::back();
}
bool empty() {
return vector<T>::empty();
}
};
基类与派生类的转换
继承关系类似于严格的父子财产关系:
- 向上转换:派生类对象可以赋值给基类的指针或引用。这被称为'切片',即只保留派生类中属于基类的部分。
- 向下转换:基类对象不能直接赋值给派生类对象。若需转换,必须确保基类指针实际指向的是派生类对象,且最好使用多态类型配合
dynamic_cast进行安全转换。
class Person {
protected:
string _name;
int _age;
};
class Student : public Person {
public:
int _No;
};
int main() {
Student sobj;
Person* pp = &sobj; // 合法:向上转换
Person& rp = sobj; // 合法:向上转换
Person pobj = sobj; // 合法:调用拷贝构造
// sobj = pobj; // 非法:基类对象不能赋值给派生类对象
return 0;
}
继承的作用域:隐藏与重载
这是初学者最容易混淆的地方。
重载规则:同一作用域下,函数名相同但参数不同,构成重载。 隐藏规则:派生类和基类有独立的作用域。如果派生类定义了与基类同名的成员,派生类成员将屏蔽基类成员的直接访问。
如果是成员函数的隐藏,只要函数名相同就构成隐藏,无论参数是否相同。
class Person {
protected:
string _name = "小李子";
int _num = 111;
};
class Student : public Person {
public:
void Print() {
cout << "姓名:" << _name << endl;
cout << "身份证号:" << Person::_num << endl; // 显式访问基类成员
cout << "学号:" << _num << endl; // 访问派生类成员
}
protected:
int _num = 999;
};
示例分析:
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::fun(int),正常
b.fun(); // 编译错误!B 中隐藏了 A::fun(),无法找到无参版本
return 0;
}
在上面的例子中,A 和 B 的 fun 不构成重载,而是隐藏。因为它们在两个不同的作用域中。调用 b.fun() 时,编译器只在 B 的作用域查找,找不到无参版本,导致编译报错。
派生类的默认成员函数
派生类会自动生成默认成员函数,但有一些特殊规则需要注意:
- 构造函数:派生类构造函数必须调用基类构造函数初始化基类部分。如果基类没有默认构造函数,必须在初始化列表中显式调用。
- 拷贝构造:派生类拷贝构造需调用基类拷贝构造完成基类部分的复制。
- 赋值运算符:派生类
operator=会隐藏基类的operator=。如需调用基类版本,需指定作用域Person::operator=(s)。 - 析构函数:析构顺序是先派生类后基类。如果基类析构函数未声明为
virtual,派生类析构函数会与基类析构函数构成隐藏关系,而非重写。这在多态场景下可能导致资源泄漏,因此建议基类析构函数设为virtual。
class Person {
public:
Person(const char* name = "peter") : _name(name) {
cout << "Person()" << endl;
}
Person(const Person& p) : _name(p._name) {
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p) {
cout << "Person operator=(const Person& p)" << endl;
if (this != &p) _name = p._name;
return *this;
}
~Person() {
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person {
public:
Student(const char* name, int num) : Person(name), _num(num) {
cout << "Student()" << endl;
}
Student(const Student& s) : Person(s), _num(s._num) {
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s) {
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s) {
Person::operator=(s); // 显式调用基类赋值
_num = s._num;
}
return *this;
}
~Student() {
cout << "~Student()" << endl;
}
protected:
int _num;
};
理解这些规则有助于编写更健壮、易维护的 C++ 代码。在实际项目中,尽量避免在继承体系中定义同名成员,以减少混淆。

