完整教程:【C++】继承(1)
目录
1. 继承的概念及定义
继承是面向对象编程的三大特性之一,它允许一个类(称为派生类 / 子类)继承另一个类(称为基类 / 父类)的属性和行为,从而实现代码复用和建立类之间的层次关系。
继承的语法
派生类通过 : 指定继承关系,语法如下:
class 派生类名 : 继承方式 基类名 { //派生类的成员 };
public继承是C++中最常用的继承方式,其核心规则是:
- 基类的public成员:在子类仍然保持public属性(子类对象可直接访问);
- 基类的protected成员:在子类中仍然保持protected属性(仅子类内部可访问,子类对象不可直接访问);
- 基类的private成员:在子类中完全不可见(即使子类内部也不能直接访问,仅基类自己的成员函数可访问)。
Student和Teacher作为Person的特殊类型,通过继承直接复用了Person的属性(姓名、电话等)和行为(identity身份认证),无需在子类中重复定义这些共性内容,仅需扩展各自的特有属性(如Student的_stuid学号,Teacher的title职称)和行为(如study()学习、teaching授课)。
class Person { public: // 进入校园/图书馆/实验室刷二维码等身份认证 void identity() { cout << "void identity()" << _name << endl; cout << _age << endl; } protected: string _name = "张三"; //姓名 string _address; //地址 string _tel; //电话 private: int _age = 18; //年龄 }; //派生类(子类) class Student : public Person { public: void study() //学习 { // ... //cout << _age << endl;//父类的私有成员在子类中不可见,仅父类自己的成员函数可访问,子类不可直接访问 cout << _tel << endl; //受保护成员,子类内部可访问 } protected: int _stuid; //学号 }; //派生类(子类) class Teacher : public Person { public: void teaching() //授课 { //... } protected: string title; //职称 }; int main() { Student s; Teacher t; s.identity(); t.identity(); return 0; }
不同继承方式下派生类的访问权限如下:
| 基类成员类型 | public继承 | protected继承 | private继承 |
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
public继承是实际开发中最常用的继承方式,几乎很少使用protected/private继承,也不提倡使用。
注意:基类的private成员无论哪种继承方式,派生类都无法直接访问(需要通过基类的piblic/protected成员函数间接访问)。
在C++中,用class定义类时,默认的继承方式是private,用struct定义类时,默认的继承方式是public,不过,为了让代码更清晰、易读,建议显示写出继承方式。
基类Person中各成员的原始权限:
class Person
{
public:
void Print() { cout << _name << endl; } // public成员(函数)
protected:
string _name; // protected成员(变量)
private:
int _age; // private成员(变量)
};
举例说明不同继承关系下的成员访问权限的变化:
1. public继承
class Student : public Person { public: void Test() { Print(); //合法:子类成员函数可访问基类public成员 _name = "张三"; //合法:子类成员函数可访问基类protected成员 // _age = 18; //错误:基类private成员不可见 } protected: int _stunum; }; int main() { Student s; s.Print(); //合法:子类对象可访问继承的public成员 // s._name = "李四"; //错误:protected成员子类对象不可访问 return 0; }2. protected继承
class Student : protected Person { public: void Test() { Print(); //合法:子类成员函数可访问protected成员 _name = "张三"; //合法:子类成员函数可访问protected成员 // _age = 18; //错误:基类private成员不可见 } protected: int _stunum; }; int main() { Student s; // s.Print(); //错误:Print()在子类中是protected,子类对象不可访问 return 0; }3. private继承
class Student : private Person { public: void Test() { Print(); //合法:子类成员函数可访问private成员 _name = "张三"; //合法:子类成员函数可访问private成员 // _age = 18; //错误:基类private成员不可见 } protected: int _stunum; }; // 孙子类(继承Student) class Graduate : public Student { public: void Test2() { // Print(); //错误:Print()在Student中是private,孙子类不可访问 // _name = "李四"; //错误:_name在Student中是private,孙子类不可访问 } }; int main() { Student s; // s.Print(); //错误:Print()在子类中是private,子类对象不可访问 return 0; }
private与protected的区别
private和protected的核心区别体现在是否允许派生类访问,protected平衡了封装和继承的需需求,允许子类访问但限制外部访问。
private确保了类的内部实现不被外部访问,但继承时,如果基类的某些成员需要被子类使用但又不想暴露给外部,private就不够了,这时候protected就派上用场了。
比如假设设计一个Animal基类,其中“体重”是需要被子类(Dog)使用的属性,但不希望外部代码随意直接修改动物的体重,这时就可以使用protected。
class Animal { protected: int _weight; //体重:允许派生类访问,不允许外部直接修改 public: //外部只能通过接口间接修改,保证合法性(比如体重不能为负) void SetWeight(int w) { if (w > 0) _weight = w; } }; class Dog : public Animal { public: //派生类可以使用_weight计算食量(复用基类成员_weight) int CalculateFood() { return _weight * 6; //假设每公斤体重每天吃6g食物 } };如果_weight用private,Dog类无法访问_weight,就无法实现CalculateFood() ;如果用public,外部可以直接写dog._weight = -100,破坏数据合法性,protected完美解决了这个矛盾。
- private成员:仅在当前类的内部可见(类自己的成员函数可访问,任何外部代码包括派生类都不可直接访问)。
- protected成员:在当前类内部和其派生类内部可见(类自己的成员函数、派生类的成员函数可访问,但类的外部对象不可直接访问)
2. 继承类模版
MyStack<T>继承自std::vector<T>,复用了vector的底层存储能力。
template
class Mystack : public std::vector
{
public:
void push(const T& x)
{
//push_back(x); error! 编译报错:error C3861: “push_back”: 找不到标识符
vector::push_back(x); //right!基类是类模板时,需要指定⼀下类域
this->push_back(x); //两种写法等效 this的类型依赖T,延迟到第二阶段模板实例化时,再去基类中查找该成员
}
void pop()
{
vector::pop_back();
}
const T& top()
{
return vector::back();
}
bool empty()
{
return vector::empty();
}
};
int main()
{
Mystack st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
return 0;
}
模版类继承时,访问基类成员需使用this->成员名或基类名<T>::成员名,避免编译器因模版延迟实例化导致的查找错误。
类模版的编译分为两个阶段:
- 第一阶段:仅做“语法层面的检查”,不会实例化参数T相关成员。
- 第二阶段:模版实例化时,编译器才会根据实际传入的模版参数实例化。
上面程序派生类模版Mystack<T>继承基类模版std::vector<T>,基类成员是依赖模版参数T的,在第一阶段时,模版未实例化,编译器无法确定push_back的归属,所以需要显式指定告诉编译器这个成员依赖于模版参数,明确成员来源,否则会报“找不到标识符”的错误。
虽然代码能够正常工作,但不推荐使用public继承实现栈,public继承会暴露基类所有的public成员,用户可以直接调用st.insert()、st.erase()、st[ ]等接口,从而破坏栈的特性。推荐使用私有继承或组合:
- 私有继承:将std::vector作为私有基类,此时基类的public成员在派生类中变为private,外部无法访问。
- 组合:将td::vector作为stack的私有成员(“has-a关系”,栈有一个vector)(更推荐的方式)
3. 基类和派生类间的转换
3.1 对象赋值
对象赋值的本质是两个独立对象之间的内容复制,需遵循“派生类包含基类,基类不包含派生类”的内存结构逻辑,仅有一种场景合法且常用。
派生类对象-->基类对象(合法,隐式进行)
将派生类对象中的基类部分(如_name、 _age)复制到基类对象中,派生类特有的成员(如_stuid)会被“切片丢弃”,此过程称为对象切片。
本质:派生类对象s-->隐式类型转换为Person临时对象(切片,丢弃_stuid)-->将临时对象拷贝给基类对象p。
class Person //基类
{
public:
string _name;
int _age;
};
class Student : public Person //派生类
{
public:
int _stuid;
};
int main()
{
Student s;
s._name = "张三";
s._age = 18;
s._stuid = 1001;
//派生类对象-->基类对象(会发生“对象切片”)
Person p;
p = s; //隐式类型转换:派生类对象s隐式类型转换为Person临时对象(切片丢弃_stuid),再将临时对象拷贝给基类对象p
//p._stuid; //错误:p是Person对象,没有_stuid成员
return 0;
}
基类对象-->派生类对象(危险,禁止使用)
基类对象的内存中不包含派生类的特有成员,强制赋值时只能复制基类部分到派生类中,而派生类的特有成员会是随机值(未初始化),访问这些成员会触发未定义行为,即使通过static_cast强制转换,也会导致严重问题。
3.2 类型转换
在C++中,基类和派生类之间的转换(也称为“类型转换”),核心围绕“向上转换”(派生类->派生类->基类)和“向下转换”(基类->派生类)两者的安全性和使用场景有显著差别。
向上转换:派生类指针/引用-->基类指针/引用
隐式转换,完全安全,是继承场景中最常用的转换。
因为派生类对象包含基类对象,派生类指针/引用指向的内存区域,必然包含基类的完整数据。转换后,基类指针/引用仅“聚焦”于内存中的基类部分,不会越界或访问非法数据,所以安全。

class Person //基类 { public: string _name; int _age; }; class Student : public Person //派生类 { public: int _stuid; }; Student s; Student* s_ptr = &s; Student& s_ref = s; //隐式向上转换 Person* p_ptr = s_ptr; // 基类指针指向派生类对象的基类部分 Person& p_ref = s_ref; // 基类引用绑定派生类对象的基类部分 p_ptr->_name = "王五"; // 正确:访问基类成员 // p_ptr->_stuid; // 错误:基类指针无法解读派生类特有成员
向下转换:基类指针/引用-->派生类指针/引用
这是不安全的,且不能隐式转换,必须显式使用static_cast或dynamic_cast转换。
因为基类对象不包含派生类的特有成员(如Person没有_stuid),若强制转换,访问派生类特有成员会导致未定义行为,所以不安全。
两种显式转换方式:
- static_cast:编译时转换,不做运行时检查,风险较高。
- dynamic_cast:运行时转换,仅适用于多态类(包含虚函数的类),会检查转换的有效性,更安全。
4. 隐藏关系
继承中,隐藏关系指的是:派生类中定义了与基类同名的成员(变量或函数)时,派生类的成员会“隐藏”基类的同名成员——即默认情况下,在派生类的作用域内,直接访问该同名成员时,优先访问派生类自己的成员,基类的同名成员会被“屏蔽”,必须通过基类域限定符(::)才能访问。
隐藏的前提:作用域不同+名称相同
隐藏、重载、重写的区别:
- 重载:同一作用域内,同名函数的参数列表不同(个数 / 类型 / 顺序);
- 重写:派生类与基类的虚函数,参数列表、返回值、cv 限定符完全相同(多态的核心);
- 隐藏:不同作用域(基类 vs 派生类),同名成员(变量或函数,函数参数可同可不同);
同名变量的隐藏:无论变量类型是否相同,基类的变量都会被隐藏。
class Base
{
public:
int _a = 10; //基类的变量_a
};
class Derived : public Base
{
public:
double _a = 20.5; //派生类的变量_a(与基类同名,隐藏基类的_a)
};
int main()
{
Derived d;
cout << d._a << endl; // 输出 20.5 默认访问派生类的_a(基类的被隐藏)
cout << d.Base::_a << endl; // 输出 10 通过基类域限定符访问被隐藏的基类_a
return 0;
}
同名函数的隐藏:无论函数的参数列表是否相同,基类的同名函数都会被隐藏。
class Base
{
public:
void show(int x)
{
cout << "Base::show(int): " << x << endl; //基类的int版本
}
};
class Derived : public Base
{
public:
void show(double x)
{
cout << "Derived::show(double): " << x << endl; //派生类的double版本
}
};
int main()
{
Derived d;
d.show(10); // 实际运行:输出 Derived::show(double): 10(int→double隐式转换)cout对double类型输出有简化显示的默认行为
d.show(3.14); // 输出 Derived::show(double): 3.14(直接匹配double)
d.Base::show(10); // 输出 Base::show(int): 10(显式访问基类被隐藏的函数)
return 0;
}
练习
1. 下面程序中A类和B类中的两个fun构成什么关系?
A. 重载 B. 隐藏 C. 没关系
派生类中定义与基类同名的函数(无论参数是否相同),会隐藏基类的同名函数。这里B::fun(int i)隐藏了A::fun(),因此构成隐藏关系。
2. 下面程序的运行结果是什么?
A. 编译报错 B. 运行报错 C. 正常运行
由于B::fun(int i)隐藏了A::fun(),编译器在B的作用域内,发现B只有func(int),但调用时没有传参,参数不匹配,因此编译阶段直接报错。
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();
return 0;
};
实际开发中应尽量避免派生类与基类成员同名,若必须同名,访问基类成员时务必用基类名::显式指定,避免歧义。

浙公网安备 33010602011771号