一、继承的概念及定义
1.1 继承的概念
在没有接触继承之前我们要设计两个类student和teacher,student和teacher都有姓名/地址/电话/年龄等成员变量,都有identity身份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有一些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学生生独有的成员函数是学习,老师独有的成员函数是教授。
class student { public: void study() { //学习 } void identity() { //身份认证 } private: string _name; //姓名 string _address; //地址 string _tel; //电话 int _age; //年龄 int _stuid; //学号 }; class teacher { public: void teach() { //教授 } void identity() { //身份认证 } private: string _name; //姓名 string _address; //地址 string _tel; //电话 int _age; //年龄 int _title; //职工号 };
利用继承的方法我们可以将两个类中的公共部分提取出来封装成单独一个类person,再使teacher/student分别继承person。这样teacher/student中既有自己特有的成员变量(函数)也有公共的成员变量(函数),大大避免了代码的冗余。
#include<string.h> class person { public: void identity() { //身份认证 } string _name; //姓名 string _address; //地址 string _tel; //电话 int _age; //年龄 }; class student:public person //stuedent继承person类 { public: void study() { //学习 } private: int _stuid; //学号 }; class teacher:public person //teacher继承person类 { public: void teach() { //教授 } private: int _title; //职工号 };
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类叫做派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到到复杂的认知过程。以前我们接触的都是函数层面的复用,继承是类设计层次的复用。
1.2 继承的定义
1.2.1 定义格式
下面person是基类,也称作父类;student是派生类,也称作子类。
1.2.2 继承基类成员访问方式的变化
基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
基类private成员在派生类中不能访问,如果基类不想在类外直接访问,但还需要在派生类中可以访问,就定义成protected。可以看出保护成员限定符是因继承才出现的。
实际上面的表格我们总结一下就会发现,基类的私有成员在派生类中都是不可见的。基类的其他成员在派生类中的访问方式==min(成员在基类的访问限定符,继承方式)public>protected>private。
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显式地写出继承方式。
在实际运用中一般使用的都是public继承,几乎很少使用protected/private继承,也不提倡使用protected/private继承,因为protected/private继承下来的成员都只能在派生类中使用,实际中扩展维护性不强。
1.3 继承类模板
在 c++ 中,类模板可以被继承,这种方式称为 "继承类模板"。通过继承类模板,我们可以创建更具体或更特殊化的类,同时复用模板的通用逻辑。
继承类模板主要有两种场景:
- 普通类继承类模板的实例化版本
- 类模板继承另一个类模板
1.普通类继承类模板的实例化版本
当一个普通类继承自一个已经实例化的类模板时,需要指定模板参数:
// 定义一个类模板 template <typename t> class container { protected: t data; public: container(t val) : data(val) {} t get() const { return data; } void set(t val) { data = val; } }; // 普通类继承实例化的类模板 class intcontainer : public container<int> { public: intcontainer(int val) : container<int>(val) {} // 新增方法 void increment() { data++; } };
2.类模板继承另一个类模板
类模板可以继承另一个类模板,此时可以使用自身的模板参数作为基类模板的参数:
#include <iostream> // 基类模板 template <typename t> class base { protected: t value; public: base(t v) : value(v) {} void print() const { std::cout << "base value: " << value << std::endl; } }; // 派生类模板继承基类模板 template <typename t, typename u> class derived : public base<t> { private: u extra; public: // 注意初始化列表中需要显式指定基类模板 derived(t v, u e) : base<t>(v), extra(e) {} void show() const { // 访问基类成员时,可能需要使用this指针或base<t>::限定 std::cout << "derived value: " << this->value << ", extra: " << extra << std::endl; } }; int main() { derived<int, std::string> obj(42, "example"); obj.print(); // 调用基类方法 obj.show(); // 调用派生类方法 return 0; }
注意事项:
访问基类成员:在派生类模板中访问基类模板的成员时,可能需要使用
this->
指针或base<t>::
限定符,帮助编译器识别成员。模板参数传递:派生类模板可以将自身的模板参数传递给基类模板,也可以使用固定类型:
template <typename t> class derived : public base<double> { // 固定使用double类型 // ... };
二、基类和派生类间的转化
在 c++ 中,public 继承的派生类对象与基类之间存在三种常见的赋值 / 关联方式:派生类对象赋值给基类指针、派生类对象赋值给基类引用、派生类对象直接赋值给基类对象。这三种方式的本质和效果有显著区别,核心在于是否保留派生类的特性以及是否触发多态行为。
1.派生类对象赋值给基类指针(base* ptr = &derivedobj;)
- 本质:基类指针指向派生类对象的基类部分(但指针可以间接访问派生类的完整信息,配合虚函数实现多态)。
- 特性:
- 指针的静态类型是
base*
,但动态类型是derived*
(指向对象的实际类型)。 - 可以通过指针调用基类的成员(包括虚函数),若调用虚函数,会根据动态类型触发动态绑定(调用派生类的重写版本)。
- 不能直接访问派生类新增的成员(需显式类型转换,如
dynamic_cast
)。
- 指针的静态类型是
class base { public: virtual void func() { cout << "base::func()" << endl; } }; class derived : public base { public: void func() override { cout << "derived::func()" << endl; } void derivedfunc() { cout << "derived独有的函数" << endl; } }; int main() { derived d; base* ptr = &d; // 基类指针指向派生类对象 ptr->func(); // 调用derived::func()(动态绑定) // ptr->derivedfunc(); // 错误:不能直接访问派生类新增成员 return 0; }
这里需要注意的是:基类对象不能赋值给派生类对象。
2. 派生类对象赋值给基类引用(base& ref = derivedobj;)
- 本质:基类引用绑定到派生类对象的基类部分(引用是对象的别名,不产生新对象)。
- 特性:
- 引用的静态类型是
base&
,动态类型是derived&
。 - 行为与基类指针类似:可调用基类成员,调用虚函数时触发动态绑定。
- 不能直接访问派生类新增成员(需显式转换)。
- 引用的静态类型是
int main() { derived d; base& ref = d; // 基类引用绑定派生类对象 ref.func(); // 调用derived::func()(动态绑定) return 0; }
3. 派生类对象直接赋值给基类对象(base baseobj = derivedobj;)
- 本质:发生对象切片(object slicing)—— 仅将派生类对象中的基类部分复制到基类对象中,派生类独有的成员被 "切片" 丢弃。
- 特性:
- 生成一个全新的基类对象(与原派生类对象无关)。
- 该对象的静态类型和动态类型都是
base
,不触发多态(调用函数时仅使用基类的实现)。 - 无法访问派生类的任何特性(即使显式转换也不行,因为派生类部分已丢失)。
int main() { derived d; base baseobj = d; // 对象切片:仅复制基类部分 baseobj.func(); // 调用base::func()(无动态绑定) return 0; }
方式 | 操作本质 | 是否产生新对象 | 动态类型 | 是否支持多态(动态绑定) | 能否访问派生类新增成员 |
---|---|---|---|---|---|
派生类→基类指针 | 指针指向派生类的基类部分 | 否 | 派生类类型 | 是(调用虚函数时) | 否(需显式转换) |
派生类→基类引用 | 引用绑定派生类的基类部分 | 否 | 派生类类型 | 是(调用虚函数时) | 否(需显式转换) |
派生类→基类对象(直接赋值) | 复制基类部分,切片派生部分 | 是(生成基类对象) | 基类类型 | 否 | 否(已被切片丢弃) |
三、隐藏机制
3.1 继承中的隐藏机制
派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派生类成员函数中,要是需要访问基类中同名成员可以使用基类::基类成员显式访问)
class person { protected: int _num = 111; string _name = "⼩李⼦"; }; class student : public person { public: void print() { cout <<_num<<endl;//这里打印结果为999因为person中的_num被隐藏了 } protected: int _num = 999; }; int main() { student s1; s1.print(); return 0; };
需要注意的是对于成员函数来讲,只要函数名相同就构成了隐藏。也就是说,如果派生类与基类中的成员函数名相同,通过派生类的实例化对象调用该函数默认调用派生类中定义的重名函数,需要调用基类中的重名函数时需要指定类名:
class person { public: void print() { cout << "我是基类中的print函数" << endl; } }; class student : public person { public: void print() { cout <<"我是派生类中的print函数" << endl; } }; int main() { student s1; s1.print(); return 0; };
要是想显式调用基类中的print函数,需要指定person::print();如下示例:
int main() { student s1; s1.person::print(); return 0; };
这也从侧面说明,对于继承关系中的隐藏机制,基类中的重名的成员函数或者成员变量确确实实被派生类继承下来了,只是由于隐藏机制默认访问或调用的是派生类中的成员变量或函数,需要访问基类中的时需要指明类域。
3.2 重载与隐藏
这里我们试着想一想下面基类与派生类func函数之间的关系:
a:重载 b:隐藏
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); return 0; };
这里我们发现基类和派生类中都有一重名的func函数,根据上述提到的隐藏机制的形成条件基类的func函数与派生类中的func函数构成隐藏。
什么是重载?
在 c++ 中,它允许在同一个作用域内定义多个同名的函数或运算符,但这些同名的函数或运算符的参数列表(参数个数、参数类型或参数顺序)必须有所不同,返回值类型可以相同也可以不同。
根据概念我们就可以发现两者的区别,重载强调在同一作用域也就是同一个类中的同名函数之间构成重载,继承中的隐藏机制始终离不开基类与派生类两个作用域。
四、派生类的默认成员函数
4.1 构造函数
派生类的构造函数必须调用基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造 函数,则必须在派生类构造函数的初始化列表阶段显式调用。
默认构造函数是一种特殊的构造函数,它不需要任何参数,或者所有参数都有默认值。当创建类的对象时如果没有提供实参,编译器会自动调用默认构造函数。
class a { public: //这里基类没有默认构造函数需要传递参数 a(string _name) { name = _name; } string name; }; class b : public a { public: //派生类中需要显式调用基类构造函数初始化基类的成员变量 b(int _n1,string _name) :a(_name) ,n1(_n1) {} void print() { cout << name << n1 << std::endl; } private: int n1; }; int main() { b ss(18,"张三"); ss.print(); return 0; };
4.2 拷贝构造函数
派生类的拷贝构造函数必须调用基类的拷贝构造完成拷贝初始化。
在一般情况下,拷贝构造函数编译器自动生成的就够用了,但是如果类中有资源需要深拷贝时就需要我们手动实现拷贝构造函数。
当创建派生类对象时,先调用基类的构造函数,再调用派生类的构造函数(与析构函数的调用顺序完全相反)。
class a { public: //这里基类没有默认构造函数需要传递参数 a(string _name) { name = _name; } a(const a& _a) { name = _a.name; } string name; }; class b : public a { public: b(int _n1,string _name) :a(_name) ,n1(_n1) {} b(const b& _b) :a(_b)//显式调用基类的拷贝构造完成构造初始化 ,n1(_b.n1) {} void print() { cout << name << n1 << std::endl; } private: int n1; }; int main() { b ss(18,"张三"); b pp(ss); pp.print(); return 0; };
4.3 operator =
派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的 operator=隐藏了基类的operator=,所以需要指定基类作用域显式调用基类的operator=。
与前面的拷贝构造一样,在一般情况下,赋值运算符重载编译器自动生成的就够用了,但是如果类中有资源需要深拷贝时就需要我们手动实现赋值运算符重载。
这里我们也来演示一下:
class a { public: //这里基类没有默认构造函数需要传递参数 a(string _name) { name = _name; } a& operator=(a& _a) { if (this != &_a) { name = _a.name; } return *this; } string name; }; class b : public a { public: b(int _n1,string _name) :a(_name) ,n1(_n1) {} b& operator=(b& _b) { if (this != &_b) { //这里不能是operator(_b)因为构成隐藏会递归调用b中的operator=导致栈溢出 a::operator=(_b); n1 = _b.n1; } return *this; } void print() { cout << name << n1 << std::endl; } private: int n1; }; int main() { b ss(18,"张三"); b pp=ss; pp.print(); return 0; };
4.4 析构函数
当派生类对象销毁时,先调用派生类的析构函数,再调用基类的析构函数(与构造函数的调用顺序相反)。
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派 生类对象先清理派生类成员再清理基类成员的顺序。
一般来说,编译器自动生成的析构函数就已经够用了,如果有需要显式释放的资源,才需要自己实现:
#include <iostream> class base { public: ~base() { std::cout << "base destructor" << std::endl; } }; class derived : public base { public: ~derived() { std::cout << "derived destructor" << std::endl; } }; int main() { derived d; // 销毁时先调用 ~derived(),再调用 ~base() return 0; } // 输出: // derived destructor // base destructor
五、继承与友元
5.1 什么是友元
在 c++ 中,友元(friend) 是一种特殊的访问权限机制,它允许一个类或函数访问另一个类中的私有(private) 和保护(protected) 成员,即使它们不属于该类的成员。
通常友元具有以下三种形式:
1. 友元函数
普通函数可以被声明为某个类的友元,从而访问该类的私有成员。
class myclass { private: int secret; public: myclass(int s) : secret(s) {} // 声明友元函数 friend void printsecret(myclass obj); }; // 友元函数定义(无需加friend关键字) void printsecret(myclass obj) { // 可以直接访问私有成员 cout << "secret value: " << obj.secret << endl; }
2. 友元类
一个类可以被声明为另一个类的友元,此时友元类的所有成员函数都能访问该类的私有成员。
class a { private: int value; public: a(int v) : value(v) {} // 声明b为a的友元类 friend class b; }; class b { public: void showa(a a) { // b的成员函数可以访问a的私有成员 cout << "a's value: " << a.value << endl; } };
3. 类的成员函数作为友元
可以将一个类的特定成员函数声明为另一个类的友元(更精确的权限控制)。
class b; // 前向声明 class a { public: void showb(b b); }; class b { private: int data; public: b(int d) : data(d) {} // 仅将a的showb函数声明为友元 friend void a::showb(b b); }; // 实现a的showb函数 void a::showb(b b) { cout << "b's data: " << b.data << endl; }
5.2 继承中的友元关系
这里只需要记住一句话:友元关系不可以继承,也就是说基类的友元不能访问派生类的私有或者保护成员:
class student; class person { public: friend void display(const person& p, const student& s); protected: string _name; // 姓名 }; class student : public person { protected: int _stunum; // 学号 }; void display(const person& p, const student& s) { cout << p._name << endl; cout << s._stunum << endl; } int main() { person p; student s; // 编译报错:error c2248 : “student::_stunum” :⽆法访问protected成员 // 解决⽅案:display也变成student的友元即可 display(p, s); return 0; }
六、继承与静态成员
静态成员(变量或函数)属于整个类,而非某个具体对象,它被该类的所有实例以及所有派生类的实例共享。
在继承关系中,基类的静态成员不会被派生类复制,而是在整个继承体系中只存在一份副本。
派生类可以直接访问基类的静态成员,但需遵循访问权限控制(public
/protected
/private
):
- 基类的
public
静态成员:派生类可直接访问(通过类名或对象)。 - 基类的
protected
静态成员:派生类的成员函数中可访问,但类外不可直接访问。 - 基类的
private
静态成员:派生类无法访问(即使是成员函数中)。
#include <iostream> using namespace std; class base { public: static int public_static; // 公有静态成员 protected: static int protected_static; // 保护静态成员 private: static int private_static; // 私有静态成员 }; // 初始化基类静态成员 int base::public_static = 0; int base::protected_static = 0; int base::private_static = 0; class derived : public base { public: void print() { // 访问基类的public静态成员 cout << "public_static: " << public_static << endl; // 访问基类的protected静态成员(仅在派生类内部可访问) cout << "protected_static: " << protected_static << endl; // 错误:无法访问基类的private静态成员 // cout << "private_static: " << private_static << endl; } }; int main() { derived d; d.print(); // 正确:通过派生类对象调用,访问基类的public/protected静态成员 // 直接访问基类的public静态成员(通过基类名或派生类名) cout << base::public_static << endl; // 正确 cout << derived::public_static << endl; // 正确(派生类共享基类的静态成员) return 0; }
七、多继承与菱形继承
7.1 单继承与多继承
单继承指一个派生类只从一个基类继承成员的方式。这是最简单、最常用的继承形式,逻辑清晰,不易产生歧义。
多继承指一个派生类同时从多个基类继承成员的方式。它能让派生类整合多个不同基类的功能,但也可能带来复杂性。
多继承的语法格式:
class 基类1 { ... }; class 基类2 { ... }; class 派生类 : 继承方式 基类1, 继承方式 基类2, ... { // 派生类成员 };
7.1.1 多继承中的指针偏移
这里我们先来看一道题:
例题:下面说法正确的是( )
a. p1==p2==p3 b. p1<p2<p3 c.p1==p3!=p2 d.p1!=p2!=p3
class base1 { public: int _b1; }; class base2 { public: int _b2; }; class derive : public base1, public base2 { public: int _d; }; int main() { derive d; base1* p1 = &d; base2* p2 = &d; derive* p3 = &d; return 0; }
要解答这道题,我们首先来看指针p3,p3指向整个derive类:
再看p1与p2,因为p1比p2早声明所以p1<p2
接下来我们来考虑p1与p3的关系,我们发现p3与p1分别是派生类与基类的指针,并且p1指向的是第一个基类(最先声明的)。前面我们讲过派生类对象可以赋值给基类的指针/基类的引用,基类指针或引用指向的是派生类中切出来的基类那部分。如图:
所以正确结果是p2>p1==p3,也就是p1==p3!=p2选c。
7.2 菱形继承(钻石问题)
当两个基类继承自同一个间接基类,而派生类同时继承这两个基类时,会导致间接基类的成员在派生类中存在两份副本,引发歧义。
在这里student类与teacher类同时继承了person类,此时assistant又同时继承了student类与teacher类,这时会导致person类中的成员在assistant类中出现两次。当我们访问这类成员时编译器不知道我们访问的是student中person类的成员还是teacher中person类的成员:
如果出现这种情况,我们建议应该声明要访问的这类变量的类域让编译器知道我们要访问的是哪一个父类中的重名变量。
class person { public: string _name; // 姓名 }; class student : public person { protected: int _num; //学号 }; class teacher : public person { protected: int _id; // 职⼯编号 }; class assistant : public student, public teacher { protected: string _majorcourse; // 主修课程 }; int main() { // 编译报错:error c2385 :对“_name”的访问不明确 assistant a; a._name = "peter"; // 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决 a.student::_name = "xxx"; a.teacher::_name = "yyy"; return 0; }
7.2.1 虚继承
虚继承是面向对象编程中的一种技术,主要用于解决多继承时可能出现的菱形继承问题(也称为钻石问题)。
当一个派生类同时继承自两个基类,而这两个基类又共同继承自同一个间接基类时,就会形成菱形继承结构。这时,派生类会包含间接基类的两份副本,可能导致数据冗余和二义性。
虚继承的作用就是让派生类只保留间接基类的一份副本,从而解决上述问题。
在 c++ 中,通过在继承时使用virtual
关键字来实现虚继承,例如:
// 间接基类 class a { public: int x; }; // 虚继承自a class b : virtual public a { }; class c : virtual public a { }; // 继承自b和c,此时a只会有一份副本 class d : public b, public c { public: void func() { x = 10; // 不会产生二义性,因为a只有一份 } };
通过虚继承,类 d 中只会包含 a 的一个实例,避免了数据重复和访问冲突问题。这种技术在需要实现复杂的多继承结构时特别有用,例如在一些大型框架的类层次设计中。
八、继承与组合
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继方式中,基类的内部细节对派生类可见。继承⼀定程度破坏了基类的封装,基类的改变,对派生类有很⼤的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另⼀种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对 象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-boxreuse), 因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
// tire(轮胎)和car(⻋)更符合has - a的关系 class tire { protected: // 品牌 string _brand = "michelin"; // 尺⼨ size_t _size = 17; }; class car { protected: string _colour = "⽩⾊"; string _num = "陕abit00"; tire _t1; tire _t2; tire _t3; tire _t4; }; class bmw : public car { public: void drive() { cout << "好开操控" << endl; } }; // car和bmw / benz更符合is - a的关系 class benz : public car { public: void drive() { cout << "好坐舒适" << endl; } }; template<class t> class vector {}; // stack和vector的关系,既符合is - a,也符合has - a template<class t> class stack : public vector<t> {}; template<class t> class stack { public: vector<t> _v; }; int main() { return 0; }
优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太 那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的 关系既适合用继承(is-a)也适合组合(has-a),就用组合。
总结
到此这篇关于c++继承机制的文章就介绍到这了,更多相关c++继承机制内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论