【C++】继承(1)
目录
1. 继承的概念及定义
2. 继承类模版
3. 基类和派生类间的转换
3.1 对象赋值
派生类对象-->基类对象(合法,隐式进行)
基类对象-->派生类对象(危险,禁止使用)
3.2 类型转换
向上转换:派生类指针/引用-->基类指针/引用
向下转换:基类指针/引用-->派生类指针/引用
4. 隐藏关系
练习
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 T>
class Mystack : public std::vector<T>
{
public:void push(const T& x){//push_back(x); error! 编译报错:error C3861: “push_back”: 找不到标识符vector<T>::push_back(x); //right!基类是类模板时,需要指定⼀下类域 this->push_back(x); //两种写法等效 this的类型依赖T,延迟到第二阶段模板实例化时,再去基类中查找该成员 }void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};int main()
{Mystack<int> 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 通过基类域限定符访问被隐藏的基类_areturn 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;
};
实际开发中应尽量避免派生类与基类成员同名,若必须同名,访问基类成员时务必用基类名::显式指定,避免歧义。