在 C++ 中,类继承是面向对象编程的一个重要特性。它允许创建一个新类(派生类)从一个现有类(基类)继承属性和行为。
1、继承的基本概念及继承方式
基类(父类):是被继承的类,它包含了一些通用的属性和行为,可供派生类使用。
派生类(子类):是通过继承基类而创建的新类,它除了可以拥有自己特有的属性和行为外,还自动继承了基类的所有非私有成员(属性和方法)。
1、继承方式:
在 C++ 中有三种主要的继承方式:
- public(公有继承):这是最常用的继承方式。在公有继承下,基类的公有成员在派生类中仍然是公有成员,基类的保护成员在派生类中仍是保护成员,而基类的私有成员在派生类中不可直接访问(但可以通过基类的公有或保护成员函数间接访问)。
- protected(保护继承):基类的公有成员和保护成员在派生类中都变成保护成员。这种继承方式相对使用较少,它使得从派生类再派生出来的类可以访问这些原本在基类中的公有和保护成员,但派生类对象不能直接访问这些成员。
-
private(私有继承):基类的公有成员和保护成员在派生类中都变成私有成员。这意味着派生类对象本身可以访问这些从基类继承来的成员,和protected一样,派生类对象不能直接访问这些成员。
-
总的来说权限大小:私有<保护<公有。实际开发public继承使用最多。如果不考虑继承关系,protected成员和private成员都是一样的,类外不能访问。但是考虑到继承关系,两者就不一样了。基类的protected成员可以在派生类中访问,而基类中的private成员在派生类中不可以访问,私有成员变量是继承不了的。
-
如图所示:
类外访问私有或者保护成员,一般类内提供公共接口来实现。
2、using关键字(继承):
在 C++ 中,using
关键字结合继承可以用于调整成员在派生类中的访问权限,它可以在私有继承下把私有权限升级为保护权限,在公共和保护继承下把保护权限升级为公共权限。
下面仅演示一下在公共继承下把基类中的保护成员修改成为公共成员:
#include<iostream>
using namespace std;
class MyClass {
private:
int privateVar;
protected:
int protectedVar;
public:
int publicVar;
MyClass() :publicVar(10), protectedVar(20), privateVar(30)
{
}
void print()
{
cout <<" publicVar: " <<publicVar
<<" protectedVar " <<protectedVar
<<" privateVar " <<privateVar << endl;
}
};
class Text :public MyClass
{
public:
using MyClass::protectedVar;
//using MyClass::privateVar;
};
int main()
{
Text text;
text.protectedVar = 100;//类外访问保护成员
text.print();
}
运行结果如下:
可见我们类外使用了原基类继承的保护成员。
2、继承的对象模型
1、调用顺序
在创建派生类对象的时候,编译器先创建基类的构造函数,再创建派生类的构造函数。
而在销毁派生类对象的时候,他会先调用派生类的析构函数,再调用基类的析构函数。
让我们来验证一下,在前面代码的基础上做一点点改动,再多继承一个派生类:
#include<iostream>
using namespace std;
class MyClass {
private:
int privateVar;
protected:
int protectedVar;
public:
int publicVar;
MyClass() :publicVar(10), protectedVar(20), privateVar(30)
{
cout << "MyClass 构造函数调用!" << endl;
}
~MyClass()
{
cout << "MyClass 析构函数调用!" << endl;
}
};
class Text :public MyClass
{
public:
Text()
{
cout << "Text 构造函数调用 !" << endl;
}
~Text()
{
cout << "Text 析构函数调用 !" << endl;
}
};
class Texts :public Text
{
public:
Texts()
{
cout << "Texts 构造函数调用 !" << endl;
}
~Texts()
{
cout << "Texts 析构函数调用 !" << endl;
}
};
int main()
{
Texts a;
}
运行结果如下:
可见结果是符合预期的。
上述演示的结果只是表象,我们研究一下类继承背后的东西;
2、模型空间
创建派生类对象的时候,只会申请一次内存,派生类对象包含了基类对象的内存空间,两者this指针相同。
在创建派生类对象的时候,先初始化基类,再初始化派生类对象,
我们用一段代码分别打印出基类和派生类的成员地址:
#include<iostream>
using namespace std;
class MyClass {
private:
int privateVar;
protected:
int protectedVar;
public:
int publicVar;
MyClass() :publicVar(10), protectedVar(20), privateVar(30)
{
cout << "MyClass中的this 指针是: " << this << endl;
cout << "MyClass中的publicVar 地址是: " << &publicVar << endl;
cout << "MyClass中的protectedVar 地址是: " << &protectedVar << endl;
cout << "MyClass中的privateVar 地址是: " << &privateVar << endl;
//cout << "MyClass 构造函数调用!" << endl;
}
~MyClass()
{
//cout << "MyClass 析构函数调用!" << endl;
}
};
class Text :public MyClass
{
public:
Text()
{
cout << "Text中的this 指针是: " << this << endl;
cout << "Text中的publicVar 地址是: " << &publicVar << endl;
cout << "Text中的protectedVar 地址是: " << &protectedVar << endl;
//cout << "MyClass中的privateVar 地址是: " << &privateVar << endl;
//cout << "Text 构造函数调用 !" << endl;
}
~Text()
{
//cout << "Text 析构函数调用 !" << endl;
}
};
int main()
{
Text a;
}
程序运行结果如下:
程序显示基类中的成员和派生类中的成员指向同一块内存区域,Text中不能继承privateVar,他只是在派生类中隐藏了,实际上还是占据内存的。
所以当创建派生类对象的时候,系统会预先分配一块基类加派生类非继承成员的内存大小,是一块连续的内存空间。此时先调用基类的构造函数初始化初始化基类成员,再调用派生类的构造函数初始化派生类成员。销毁的时候反之调用。所以私有变量也是一同创建了的,只不过被隐藏了而已,这只是语法上的访问权限的处理。
3、“奇巧淫技 ”
“如果我们直接操作了内存,就可以直接打破C++中的语法限制。”
例如以下代码,我直接在类外修改不可访问的private变量;
#include<iostream>
using namespace std;
class MyClass {
private:
int privateVar;
protected:
int protectedVar;
public:
int publicVar;
void func()
{
cout<< " publicVar: " << publicVar
<< " protectedVar " << protectedVar
<< " privateVar " << privateVar ;
}
MyClass() :publicVar(10), protectedVar(20), privateVar(30)
{
}
~MyClass()
{
}
};
class Text :public MyClass
{
public:
void func1()
{
cout << " text : " << text << endl;
}
Text():text(40)
{
}
~Text()
{
}
private:
int text;
};
int main()
{
Text *a=new Text;
a->func();
a->func1();
memset(a, 0, sizeof(Text));
a->func();
a->func1();
delete a;
}
结果如下:
我们可以看到,直接修改内存中的数据,可以直接突破c++语法限制。
或者我们直接进行指针转换进行内存移动修改的操作,也是一样的(具体看你内存模型是如何具体分配的)
例如:
运行结果显示也是可以类外修改的:
当然这只是个技巧,知晓就行,实际开发并不赞成这么做。这么做可能会打破类的内存模型,并且不同的情况分配的内存模型也是不一样的。这就是指针的力量。
3、如何构造基类
类继承通常有个规范,基类的成员变量由基类的构造函数初始化,派生类新增的成员变量由派生类的构造函数初始化。
一方面因为派生类无法初始化基类的私有成员,可能会出现不初始化而带来的不必要麻烦,另一方面是为了方便继承,如果有多个派生类继承,就不需要每个派生类都来一次初始化。更常用的就是使用初始化列表的方式。
派生类的构造函数总是会调用基类的一个构造函数(包括拷贝构造),如果没有指定基类的构造函数,那么将调用基类默认的构造函数。
让我们来验证一下:
#include<iostream>
using namespace std;
class MyClass {
private:
int privateVar;
protected:
int protectedVar;
public:
int publicVar;
MyClass() : publicVar(10), protectedVar(20), privateVar(30)
{
cout << "Myclass 默认构造调用" << endl;
}
MyClass(int a,int b,int c) : publicVar(a), protectedVar(b), privateVar(c)
{
cout << "Myclass 有参构造调用" << endl;
}
MyClass(MyClass& a):publicVar(a.publicVar), protectedVar(a.protectedVar), privateVar(a.privateVar)
{
cout << "Myclass 拷贝构造调用" << endl;
}
~MyClass()
{
//cout << "Myclass 析构函数调用" << endl;
}
};
class Text :public MyClass
{
public:
Text():text(40)
{
cout << "Text 默认构造调用" << endl;
}
Text(int x) :text(x)
{
cout << "Text 有参构造调用" << endl;
}
Text(int a, int b, int c) :text(40), MyClass(a, b, c)
{
cout << "Text 无参构造调用" << endl;
}
Text(Text& a):text(a.text)
{
cout << "Text 拷贝构造调用(一参)" << endl;
}
Text(Text& a,MyClass&b) :text(a.text),MyClass(b)
{
cout << "Text 拷贝构造调用(多参)" << endl;
}
~Text()
{
//cout << "Text 析构函数调用" << endl;
}
private:
int text;
};
int main()
{
Text a;//Text默认,Myclass默认
Text b(80);//Text有参构造,Myclass默认
Text c(10, 20, 30);//Text 无参构造调用,Myclass 有参构造调用
Text d(a);//Text拷贝构造(一参),Myclass默认
MyClass e(10, 20, 30);//Myclass 有参构造调用
Text f(a,e);//Text 拷贝构造调用(多参), Myclass 拷贝构造调用
}
运行结果如下:
3、名字遮蔽与类作用域
如果派生类出现和基类同名的成员,通过派生类对象或者在派生类成员函数使用该成员时,将使用派生类新增的成员,而不是基类中的。
基类中的成员函数和派生类的成员函数不会构成重载,如果派生类有重名函数,那么就会遮蔽基类中的所有同名函数。
举个例子:
#include<iostream>
using namespace std;
class MyClass {
public:
void text()
{
cout << "Myclass text :" <<x<< endl;
}
private:
int x=0;
};
class Text :public MyClass
{
public:
void text()
{
cout << "Myclass text :" << x << endl;
}
private:
int x=1;
};
int main()
{
Text a;
a.text();
}
运行结果:
要解决这种模糊的情况,我们可以使用类作用域:
就像这样,加上类作用域名,就能清晰指定该调用哪个函数。
4、继承的特殊关系
1、可以把派生类对象赋值给基类对象,但是会舍弃非基类成员(调用默认的拷贝函数)。
2、基类的指针或者引用可以在不进行显示转换的情况下指向或引用派生类成员,但是他只能调用自己的方法,不能调用派生类的方法,除非使用虚函数:C++虚函数 。(这篇文章我之前有简述过,这里就不过多赘述。)
3、基类对象不能直接赋值给派生类。上文讲述过,由于内存布局的差异,理论上是不可以这么做的。
5、多继承与虚继承
多继承和单继承语法上没有什么区别,举个例子:
class a {
public:
int m_a=0;
};
class b
{
public:
int m_b=1;
};
class c :public a, public b
{
public:
int m_c = 2;
};
如果重名情况,我们加上类作用域名即可。我们再简述讨论一下他的特殊情况,菱形继承。如图:
我们可以看见class D继承了两个int m_a,出现了代码的冗余和名称二义性 。
为了解决这个问题,c++引入了虚继承,在上述中B、C两个类虚继承类A代码中,加上virtual关键字,就可以把他们变成同一个变量,内存地址是一样的。
6、结语
总之,C++ 类继承是强大且实用的特性,掌握他的继承方式以及对象模型的底层原理,能有效助力我们代码的复用与扩展。
以上就是本文关于C++类继承的简单论述,如有不当,还请多多指教。😸😸😸.