继承?什么是继承?在生活中,我们可以听到继承人这样的专有名词,那么C++中的继承是什么呢?我们来看看继承的概念~
继承是一种机制,它允许一个类(称为子类或派生类)获取另一个类(称为基类或父类)的属性和方法。这就像是在现实生活中,孩子会继承父母的某些特征和行为。
想象一下,我们想要描述一个包含多种动物的世界。在这个世界中,有狗、猫、鸟等多种动物。这些动物之间有一些共同的特征和行为,比如它们都有名字和年龄,它们都会吃和睡觉。但是,它们也有一些独特的特征和行为,比如狗会叫,鸟会飞~
为了简化描述并避免重复,我们可以引入一个概念叫做“继承”。
基类:动物(Animal)
首先,我们定义一个基类,叫做“动物”(Animal)。这个基类代表了所有动物共有的特征和行为。它包含两个基本的属性:名字(name)和年龄(age)。同时,它还定义了两个基本的行为:吃(eat)和睡觉(sleep)。
子类:狗(Dog)、鸟(Bird)
接着,我们定义几个子类,分别叫做“狗”(Dog)、“鸟”(Bird)。这些子类都是基类“动物”(Animal)的特例,也就是说,它们都是动物的一种。
通过继承,我们可以很容易地描述不同种类的动物,同时避免重复描述它们共有的特征和行为。这样,我们的描述更加简洁、清晰,也更容易理解和维护。
接下来我们使用代码进行实现:
#include<iostream>
using namespace std;
//基类Animal
class Animal
{
public:
string _name;
int _age;
void eat() {
cout << _name << " is eating." << endl;
}
void sleep() {
cout << _name << " is sleeping." << endl;
}
};
//子类
class Dog :public Animal
{
public:
void bark()
{
cout << _name << " is barking." << endl;
}
};
class Bird : public Animal {
public:
void fly()
{
cout << _name << " is flying." << endl;
}
};
在这个程序中,通过继承,可以很容易地扩展类体系,添加新的动物类型,而不需要重复编写已经存在的代码。继承使得代码更加模块化和易于维护,同时也提高了代码的可复用性。继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段~
在上面的代码,可能大家对继承的定义还不太清楚,我们接下来就来掀开它的神秘面纱~
上面的代码定义了一个简单的类继承体系,其中包含一个基类(或者叫父类)Animal
和两个派生类(或者叫子类)Dog
(狗)和Bird
(鸟),每个类都有其特定的属性和方法~
继承方式和访问限定符都有三种:
不同的继承方式和不同的访问限定符组合决定了派生类(子类)如何访问基类(父类)的成员(属性和方法),我们来看看下面这张图~
接下来我们来进行简单的使用:
//基类Animal
class Animal
{
public:
string _name;
int _age;
void eat() {
cout << _name << " is eating." << endl;
}
void sleep() {
cout << _name << " is sleeping." << endl;
}
};
//子类
class Dog : Animal//class关键字,继承方式为private
{
public:
void bark()
{
cout << _name << " is barking." << endl;
}
};
struct Bird : Animal //struct关键字,继承方式为public
{
public:
void fly()
{
cout << _name << " is flying." << endl;
}
};
int main()
{
Dog d;
d._name = "DD";//err
d._age = 6;//err
d.eat();//err
//class 关键字,不显示写继承方式默认为private
//除基类的私有成员外,其他成员在派生类中的访问方式
//取决于成员在基类中的访问限定符和继承方式的最小值(Min),这里是private
d.bark();
Bird b;
b._name = "BB";
b._age = 3;
b.eat();
b.fly();
return 0;
}
在面向对象编程中,关于继承与多态性,我们常遇到这样的情境:
一个由public继承而来的派生类对象,能够轻松地赋值给一个基类类型的指针或引用。这一操作,形象地被称为“切片”或“切割”。其寓意在于,当我们从派生类中“切割”出基类部分时,基类指针或引用便指向了这块被“切割”出来的基类“片段”。
然而,事情并非总是双向的:
• 我们无法将一个基类对象直接赋值给一个派生类对象,这在逻辑上是不允许的。
但转换思维,我们或许可以尝试另一种方式:
• 通过强制类型转换,基类指针或引用可以被赋予派生类指针或引用的“外衣”。但这样的操作隐藏着风险,唯有当基类指针确实指向一个派生类对象时,这样的转换才是安全的。幸运的是,当基类具备多态性时,我们可以借助RTTI(Run-Time Type Information,即运行时类型信息)中的dynamic_cast
工具,进行类型识别,从而确保转换的安全性。(小贴士:关于dynamic_cast
的深入讲解,我们将留待后续的类型转换章节博客中详细展开,此处仅作简要提及)
//基类Animal
class Animal
{
public:
string _name;
int _age;
void eat() {
cout << _name << " is eating." << endl;
}
void sleep() {
cout << _name << " is sleeping." << endl;
}
};
//子类
class Dog : public Animal
{
public:
void bark()
{
cout << _name << " is barking." << endl;
}
};
struct Bird : public Animal
{
public:
void fly()
{
cout << _name << " is flying." << endl;
}
};
int main()
{
Dog d;
d._name = "DD";
d._age = 6;
d.eat();
d.bark();
Bird b;
b._name = "BB";
b._age = 3;
b.eat();
b.fly();
//1.派生类对象可以赋值给基类
Animal a1;
a1 = d;
a1.eat();
// a1.bark();//err 父类没有这个成员函数
Animal a2;
a2 = b;
a2.eat();
// a2.fly();//err 父类没有这个成员函数
//2.派生类对象可以赋值给基类的指针/引用
Animal* aa1 = &d;
Animal& aa2 = d;
Dog* dp = &d;
cout << aa1 << endl;
cout << dp << endl;//这也就是同一个地址
//3.基类对象不可以赋值给派生类
// d = a1;//这里编译报错
return 0;
}
我们首先来看看隐藏规则:
1、在继承体系中,基类和派生类各自拥有独立的作用域。 2、当派生类与基类中存在同名成员时,派生类的成员会屏蔽基类中对同名成员的直接访问。这种情况被称为隐藏。 (在派生类的成员函数中,可以通过使用“基类::基类成员”的方式来显式访问被隐藏的基类成员。) 3、需要特别注意的是,如果发生的是成员函数的隐藏,那么只要函数名相同(也就是说参数即使不同),就构成了隐藏。 4、在实际应用中,为了避免潜在的混淆和错误,建议在继承体系中尽量避免定义同名的成员。 5、与我们的函数重载进行区分,一个类中函数名相同,但是函数参数类型或者个数不同就构成函数重载,而函数隐藏只要函数名相同就构成~
接下来,看看实际的例子:
//基类Animal
class Animal
{
public:
string _name;
int _age;
void eat()
{
cout << " Animal " << _name << " is eating." << endl;
}
void sleep()
{
cout << " Animal " << _name << " is sleeping." << endl;
}
};
//子类
class Dog : public Animal
{
public:
void bark()
{
cout << " Dog " << _name << " is barking." << endl;
}
void bark(int a)
{
cout << a << " Dog " << _name << " is barking." << endl;
}
//一个类中函数名相同,但是函数参数类型或者个数不同就构成函数重载
//隐藏只要函数名相同就构成
void eat()
{
cout << " Dog " << _name << " is eating." << endl;
}
};
struct Bird : public Animal
{
public:
void fly()
{
cout << " Bird " << _name << " is flying." << endl;
}
};
int main()
{
Dog d;
d._name = "DD";
d._age = 6;
d.bark();
d.bark(3);//函数重载
d.eat();//函数隐藏——调用派生类,而不是基类
d.Animal::eat();//使用“基类::基类成员”的方式来显式访问被隐藏的基类成员
d.sleep();//调用基类的
return 0;
}
接下来,我们来看看两道有趣的题目:
class A
{
public:
void func()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void func(int i)
{
cout << "func(int i)" <<i<<endl;
}
};
int main()
{
B b;
b.func(10);
b.func();
return 0;
};
1》上面A和B类中的两个func构成什么关系()
A. 重载 B. 隐藏 C.没关系
2》 下面程序的编译运行结果是什么()
A. 编译报错 B. 运行报错 C. 正常运行
答案揭晓:
正确答案:B A 解析:事实上,func函数名相同就构成了隐藏,所以第一题选择B 而构成隐藏就只能直接访问子类的成员函数func(int a),但是子类的成员函数func(int a)必须有参数才可以正常调用,所以这里b.func()编译就会出问题,第二题答案选择A
在C++中,若我们未显式定义,编译器会自动为类生成以下六个默认成员函数。那么,在派生类中,这些成员函数是如何生成和工作的呢(这里主要讲解四个常用默认成员函数)?
operator=
):派生类的赋值运算符在对象赋值时,必须调用基类的赋值运算符来完成基类成员的更新。值得注意的是,派生类的赋值运算符会隐藏基类的赋值运算符,因此调用基类赋值运算符时,需显式指定基类作用域(例如,BaseClass::operator=(other);
)。
我们首先来看看构造和析构:
//基类Animal
class Animal
{
public:
string _name;
int _age;
//基类构造函数
Animal(string name, int age):_name(name),_age(age)
{
cout << "基类Animal构造" << endl;
}
void eat()
{
cout << " Animal " << _name << " is eating." << endl;
}
void sleep()
{
cout << " Animal " << _name << " is sleeping." << endl;
}
//基类析构函数
~Animal()
{
cout << "~Animal" << endl;
}
};
//派生类
class Dog : public Animal
{
public:
int _id;
//派生类构造函数
//Dog(string name,int age,int id):_name(name),_age(age),_id(id)//err 这种写法是错误的
//基类没有提供默认构造函数(即无参构造函数),则派生类构造函数需在初始化列表中显式调用基类的某个构造函数
//正确写法
Dog(string name, int age, int id) :Animal(name, age), _id(id)
{
cout << "派生类Dog构造" << endl;
}
void bark()
{
cout << " Dog " << _name << " is barking." << endl;
}
//派生类析构函数
~Dog()
{
cout << "~Dog()" << endl;
}
};
class Bird : public Animal
{
public:
void fly()
{
cout << " Bird " << _name << " is flying." << endl;
}
};
int main()
{
Dog d("DD", 6, 1);
//先初始化基类,再初始化派生类
//先析构派生类,再析构基类
return 0;
}
接下来,看看拷贝构造和赋值运算符重载:
#include<iostream>
using namespace std;
//基类Animal
class Animal
{
public:
string _name;
int _age;
//基类构造函数
Animal(string name, int age):_name(name),_age(age)
{
cout << "基类Animal构造" << endl;
}
//基类拷贝构造
Animal(const Animal& a):_name(a._name),_age(a._age)
{
cout << "Animal拷贝构造" << endl;
}
//基类赋值运算符重载
Animal& operator=(const Animal& a)
{
//不是本身才进行赋值
if (this != &a)
{
_name = a._name;
_age = a._age;
}
cout << "Animal 赋值运算符重载" << endl;
return *this;
}
void eat()
{
cout << " Animal " << _name << " is eating." << endl;
}
void sleep()
{
cout << " Animal " << _name << " is sleeping." << endl;
}
//基类析构函数
~Animal()
{
cout << "~Animal" << endl;
}
};
//派生类
class Dog : public Animal
{
public:
int _id;
//派生类构造函数
//Dog(string name,int age,int id):_name(name),_age(age),_id(id)//err 这种写法是错误的
//基类没有提供默认构造函数(即无参构造函数),则派生类构造函数需在初始化列表中显式调用基类的某个构造函数
//正确写法
Dog(string name, int age, int id) :Animal(name, age), _id(id)
{
cout << "派生类Dog构造" << endl;
}
//派生类拷贝构造
//这里派生类和基类之间的转换就发生了大作用
Dog(const Dog& d) :Animal(d), _id(d._id)
{
cout << "Dog 拷贝构造" << endl;
}
//派生类赋值运算符重载
Dog& operator=(const Dog& d)
{
//派生类的赋值运算符在对象赋值时,必须调用基类的赋值运算符来完成基类成员的更新
// 派生类的赋值运算符会隐藏基类的赋值运算符
// 因此调用基类赋值运算符时,需显式指定基类作用域
if (this != &d)
{
Animal::operator=(d);//显式指定基类作用域
_id = d._id;
}
cout << "Dog 赋值运算符重载" << endl;
return *this;
}
void bark()
{
cout << " Dog " << _name << " is barking." << endl;
}
//派生类析构函数
~Dog()
{
cout << "~Dog()" << endl;
}
};
int main()
{
Dog d1("DD", 6, 1);
//先初始化基类,再初始化派生类
Dog d2 = d1;//调用拷贝构造
Dog d3("DD3", 8, 2);
d3 = d1;
//先析构派生类,再析构基类
return 0;
}
在C++里,有时候我们不想让别人基于我们的类创建新的子类,这就像我们不希望别人随意改动我们设计好的玩具一样。为了实现这个目的,我们可以把类设置为“最终类”,也就是不能被继承的类。
怎么做呢?
方法1:将基类的构造函数设为私有,本意是防止外部创建对象,但并非有效阻止继承的手段。因为派生类在构造时仍需调用基类构造函数,私有构造函数会导致编译错误,且这种错误发生在尝试实例化派生类时,而非继承时。此外,该方法未能明确表达“不可继承”的意图。(这个方法事实上卡了语法的Bug) 方法2:C++11引入的
final
关键字,是专为阻止类被继承而设计的。声明为final
的类无法被其他类继承,编译器将直接报错。此方法既清晰又有效,直接解决了类不应被继承的问题,且没有引入不必要的复杂性或潜在错误。因此,推荐使用final
关键字来实现不可继承的类。
为什么要这么做呢?
主要是为了保护我们的设计,防止别人不小心或者故意地破坏它。这样,我们就可以确保我们的类按照我们预期的方式工作,不会出现意外的行为或者错误。
使用举例
//实现一个不能被继承的类
// C++11的方法——声明为final的类无法被其他类继承,编译器将直接报错
class Base final
{
public:
void func1() { cout << "Base::func1" << endl; }
protected:
int a = 1;
private:
// C++98的方法——构造函数私有
/*Base()
{}*/
};
class Derive :public Base
{
void func2() { cout << "Derive::func2" << endl; }
protected:
int b = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
友元关系在C++中不遵循继承规则,即基类的友元不能自动访问派生类的私有和保护成员。即使基类与某类建立了友元关系,该关系也不会传递给基类的派生类。因此,派生类的成员访问权限对基类的友元类是受限的,如果我们想解决这个问题,可以让友元关系在派生类也是存在的~
例:
#include<iostream>
using namespace std;
class B;//前向声明B类,说明有这个类
class A
{
public:
friend void show(const A& a, const B& b);
A(int a) :_a(a)
{
}
protected:
int _a;
};
class B :public A
{
public:
friend void show(const A& a, const B& b);
//让友元关系在派生类也存在
B(int a, int b) :A(a), _b(b)
{
}
protected:
int _b;
};
void show(const A& a, const B& b)
{
cout << "show()" << endl;
cout << a._a << endl;
cout << b._b << endl;//基类的友元不能自动访问派生类的私有和保护成员
//解决方法:让友元关系在派生类也是存在的
}
int main()
{
B b(1, 2);
show(b, b);
return 0;
}
在面向对象编程中,若基类定义了一个静态成员,那么在整个继承体系中,这个静态成员将只有一个唯一的实例存在,无论从这个基类派生出多少个子类,这些子类都将共享这个唯一的静态成员实例。
例:
class Base
{
public:
int _a;
static int _count;//基类有一个静态成员变量
};
int Base::_count = 0; // 静态成员变量的定义和初始化
class Derived1 : public Base
{
};
class Derived2 : public Base
{
};
int main() {
Base b1, b2;
Derived1 d1;
Derived2 d2;
b1._count++; // 访问并修改静态成员
d1._count++; // 同样访问的是Base::count,因为静态成员在继承体系中是共享的
std::cout << "Base count: " << b1._count << std::endl; // 输出2
std::cout << "Base count: " << b2._count << std::endl; // 输出2
std::cout << "Derived1 count: " << d1._count << std::endl; // 输出2,因为访问的是同一个静态成员
std::cout << "Derived2 count: " << d1._count << std::endl; // 输出2,因为访问的是同一个静态成员
//证明:打印地址是一样的
//静态成员在继承体系中是共享的
cout << &b1._count << endl;
cout << &b2._count << endl;
cout << &d1._count << endl;
cout << &d2._count << endl << endl;
//非静态成员在继承体系中不共享同一份,地址不一样
cout << &b1._a << endl;
cout << &d1._a << endl;
return 0;
}
继承模型
定义:当一个派生类只有一个直接基类时,这种继承关系被称为单继承。
特点:
例:
class Base
{
public:
int _a;
};
class Derived1 : public Base //只有一个基类
{
};
定义:当一个派生类有两个或更多直接基类时,这种继承关系被称为多继承。
特点:
class Base1
{
public:
int _a;
};
class Base2
{
public:
int _b;
};
class Derived1 : public Base1,public Base2//一个派生类有两个或更多直接基类——多继承
{
};
class Derived2 : public Base1//单继承
{
};
内存中的模型: 在多继承中,对象的内存布局通常按照基类继承的顺序来排列。这意味着,如果派生类从A和B两个基类继承,且A在B之前被继承,那么对象在内存中的布局将首先是A基类的成员,然后是B基类的成员,最后是派生类自己的成员。
多继承中指针偏移问题?
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;
}
结合上面的代码,下面说法正确的是( )
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
正确答案:C
解析: 这个问题就与我们提到的“切割”有关系,同时我们需要考虑内存模型。Base1和Base2虽然都是Derive父类,但在子类内存模型中,其位置不同,所以p1和p2所指子类的位置也不相同,因此p1!=p2。 由于Base1对象是第一个被继承的父类类型,所有其地址与子类对象的地址Derive所指位置都为子类对象的起始位置,因此p1==p3,所以C正确 画图理解:
举一反三:
#include<iostream>
#include<vector>
using namespace std;
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base2, public Base1 { public: int _d; };
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
cout << p1 << endl;
cout << p2 << endl;
cout << p3 << endl;
return 0;
}
结合上面的代码,下面说法正确的是( )
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 == p3
正确答案:D 这一段代码由于Base2对象是第一个被继承的父类类型,所有其地址与子类对象的地址Derive所指位置都为子类对象的起始位置,因此p2==p3,所以C正确 所以我们需要注意子类继承父类的顺序
定义:菱形继承是多继承的一种特殊情况,其中两个基类都从同一个公共基类继承,而这两个基类又共同被一个派生类继承。
特点:
问题: 菱形继承的问题主要体现在数据冗余和二义性上。数据冗余是因为公共基类的成员在派生类中会有多份拷贝,这可能导致不必要的内存开销和潜在的数据不一致问题。二义性则是因为派生类可能通过不同的基类路径访问到公共基类的同名成员,这可能导致编译错误或运行时错误。
比如我们来看看下面的代码就是一个菱形继承,存在二义性和数据冗余的问题!我们可以解决二义性问题,但是数据冗余解决不了
//菱形继承
class A
{
public:
int _a;
};
class B :public A
{
public:
int _b;
};
class C :public A
{
public:
int _c;
};
class D :public B, public C
{
public:
int _d;
};
int main()
{
D d;
//d._a = 2;//err D::_a不明确 —— 存在二义性
//解决方法——显示指定访问哪一个基类,但是数据冗余问题无法解决
d.B::_a = 2;
d.C::_a = 3;
//在两个类分别拷贝了一次,依然存在数据冗余
cout << d.B::_a << endl;
cout << d.C::_a << endl;
return 0;
}
上面的代码,类A派生出类B和类C,类D继承自类B和类C。此时,类A中的成员变量和成员函数在类D中就会存在两份拷贝,一份来自A→B→D路径,另一份来自A→C→D路径。
画图理解:
解决方案: 为了避免菱形继承的问题,一些编程语言(如Java)直接不支持多继承。在其他支持多继承的语言中(如C++),可以通过虚继承(virtual inheritance)来解决菱形继承带来的二义性问题。虚继承会确保公共基类在派生类中只有一份拷贝,从而避免了数据冗余和二义性。
实践建议: 尽管多继承在某些情况下可能提供方便,但由于其复杂性和潜在的问题(特别是菱形继承),在实践中通常建议避免使用多继承。相反,可以通过组合(composition)或接口(interface)来实现类似的功能,这些技术通常更加灵活且易于维护。
定义:虚继承是C++中一种特殊的继承方式,用于解决多重继承中的菱形继承问题。
问题背景:在菱形继承结构中,一个基类通过多个路径被同一个派生类继承,可能导致基类成员在派生类中存在多个副本,引发数据冗余和二义性。
解决方案:通过虚继承,无论通过多少条路径,确保派生类只继承基类的一个副本。
语法:在继承声明中使用virtual
关键字。
//虚继承
class A
{
public:
int _a;
};
class B :virtual public A
{
public:
int _b;
};
class C :virtual public A
{
public:
int _c;
};
class D :public B, public C
{
public:
int _d;
};
int main()
{
D d;
d._a = 1;//解决了二义性和数据冗余问题
d.B::_a = 2;
d.C::_a = 3;
//通过虚继承,无论通过多少条路径,确保派生类只继承基类的一个副本
cout << d._a << endl;
cout << d.C::_a << endl;
return 0;
}
优点:
缺点:
总之,C++中的虚继承是一种用于处理多重继承中菱形继承问题的有效机制,它通过确保基类只有一个副本来避免数据冗余和二义性。
接下来思考,如果继承关系像下面这个样子,这是不是菱形继承呢?如果是,我们怎么使用虚继承解决这个问题呢?
答案是这也是菱形继承,因为B和C中分别拷贝了一份A类,也就造成了二义性和数据冗余,我们只需要将B和C使用关键字设置为虚继承就可以很好的解决这个问题~
组合例子:
Tire(轮胎)和Car(车)更符合has-a的关系
class Tire
{
protected:
string _brand = "MM"; // 品牌
size_t _size = 17; // 尺⼨
};
class Car
{
protected:
string _colour = "黑色"; //颜色
Tire _t1; // 轮胎
Tire _t2; // 轮胎
Tire _t3; // 轮胎
Tire _t4; // 轮胎
};
#include<iostream>
#include<vector>
using namespace std;
namespace xiaodu
{
//template<class T>
//class vector
//{ };
// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public std::vector<T>//继承类模板
{
public:
void push(const T& x)
{
// 基类是类模板时,需要指定⼀下类域
// 否则编译报错:error C3861: “push_back”: 找不到标识符
// 因为stack<int>实例化时,也实例化vector<int>了——继承关系
// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
vector<T>::push_back(x);//指定类域
//push_back(x);
}
void pop()
{
vector<T>::pop_back();
}
const T& top()
{
return vector<T>::back();
}
bool empty()
{
return vector<T>::empty();
}
};
}
int main()
{
xiaodu::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
return 0;
}
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有