一、继承方式介绍
在C++中,继承是面向对象编程的重要概念,通过继承可以创建一个新类,该类继承了另一个已存在的类的特性和行为。C++中的继承方式主要有三种:public(公有的)、protected(受保护的)和private(私有的)。这些继承方式决定了派生类中如何访问基类的成员。
1.1 访问权限说明
C++中类成员的访问权限从高到低依次为:
- public:成员在类内外都可以访问。
- protected:成员只能在类的成员函数中访问,且在派生类中可以访问基类的protected成员。
- private:成员只能在类的成员函数中访问,不能被类外或派生类访问。
1.2 继承方式对访问权限的影响
- 基类成员在派生类中的访问权限不得高于继承方式中指定的权限。
- 当继承方式为protected时,基类成员在派生类中的访问权限最高也为protected。
- 当继承方式为public时,基类成员在派生类中的访问权限将保持不变。
- 基类中的private成员在派生类中始终不能使用。
- 合理选择基类成员的访问权限。
- 希望基类成员被派生类继承并使用时,声明为public或protected。
- 不希望在派生类中使用的成员可以声明为private。
- private和protected继承方式的影响较复杂,一般使用public继承。
- private和protected继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂。
- 在派生类中,可以通过基类的公有成员函数间接访问基类的私有成员。
// 基类定义
class Base {
public:
int publicMember;
Base() : publicMember(0), protectedMember(0), privateMember(0) {}
void PublicFunction() {
// 可以访问所有成员
publicMember = 1;
protectedMember = 2;
privateMember = 3;
}
protected:
int protectedMember;
private:
int privateMember;
};
// 公有继承
class PublicDerived : public Base {
public:
void AccessBaseMembers() {
publicMember = 10; // 可以访问public成员
protectedMember = 20; // 可以访问protected成员
// privateMember = 30; // 不能访问private成员
}
};
// 受保护继承
class ProtectedDerived : protected Base {
public:
void AccessBaseMembers() {
publicMember = 10; // 可以访问public成员
protectedMember = 20; // 可以访问protected成员
// privateMember = 30; // 不能访问private成员
}
};
// 私有继承
class PrivateDerived : private Base {
public:
void AccessBaseMembers() {
publicMember = 10; // 可以访问public成员
protectedMember = 20; // 可以访问protected成员
// privateMember = 30; // 不能访问private成员
}
};
int main() {
PublicDerived pubDerived;
pubDerived.publicMember = 100; // 可以访问public成员
// pubDerived.protectedMember = 200; // 不能访问protected成员
ProtectedDerived protDerived;
// protDerived.publicMember = 100; // 不能访问public成员
// protDerived.protectedMember = 200; // 不能访问protected成员
PrivateDerived privDerived;
// privDerived.publicMember = 100; // 不能访问public成员
// privDerived.protectedMember = 200; // 不能访问protected成员
return 0;
}
- 使用using关键字改变访问权限
在派生类中,可以使用using关键字来改变基类成员的访问权限。注意,using只能改变基类中public和protected成员的访问权限,不能改变private成员的访问权限,因为private成员在派生类中是不可见的。class Derived : public Base { public: using Base::publicMember; // 改变public成员的访问权限为派生类中的public void AccessPublicMember() { publicMember = 10; // 现在可以访问基类的public成员 } };
二、继承的对象模型解释
2.1 创建派生类对象时的构造函数调用顺序
在创建派生类的对象时,首先会调用基类的构造函数,然后再调用派生类的构造函数。这是因为派生类需要继承基类的成员,并且在初始化时确保基类的构造过程完成。
2.2 销毁派生类对象时的析构函数调用顺序
在销毁派生类的对象时,先会调用派生类的析构函数,然后再调用基类的析构函数。这样确保对象从派生类向基类的方向进行清理操作。
class Base {
public:
Base(){
cout<<"基类构造函数"<<endl;
}
~Base(){
cout<<"基类析构函数"<<endl;
}
};
class Derived:public Base {
public:
Derived(){
cout<<"派生类构造函数"<<endl;
}
~Derived(){
cout<<"派生类析构函数"<<endl;
}
};
int main() {
Derived derived;
}
输出:
基类构造函数
派生类构造函数
派生类析构函数
基类析构函数
2.3 派生类对象的内存分配和this指针
创建派生类对象时,只会申请一次内存,派生类对象会包含基类对象的内存空间,并且派生类对象和基类对象共享相同的this指针。
2.4 派生类对象的初始化顺序
在创建派生类对象时,首先会初始化基类对象,然后再初始化派生类对象。
- 首先,会先初始化基类对象。这意味着基类的构造函数会被调用,基类的成员变量会被初始化。
- 然后,在基类对象初始化完成后,会初始化派生类对象。这时派生类的构造函数会被调用,派生类的成员变量会被初始化。
2.5 使用sizeof获取派生类对象的大小
对于派生类对象,使用sizeof运算符得到的大小包括了基类的所有成员(包括私有成员)以及派生类对象自己的成员。派生类只是无法看到和操作基类的private成员,但是还是包含在内存中。
2.6 继承方式的访问权限
在C++中,不同的继承方式(public、protected、private)在语法上处理访问权限,但在内存布局和对象模型方面没有区别。
void *operator new(size_t size) {
void *p = malloc(size);
cout << "申请到的内存地址为:" << p << ",大小为:" << size << endl;
return p;
}
void operator delete(void *p) {
if(p != nullptr){
cout << "释放内存" << endl;
free(p);
}
}
class Base {
public:
int a = 1;
protected:
int b = 2;
private:
int c = 3;
public:
Base() {
cout << "Base中this的地址是:" << this << endl;
cout << "Base中a的地址是:" << &a << endl;
cout << "Base中b的地址是:" << &b << endl;
cout << "Base中c的地址是:" << &c << endl;
}
void fun1() {
cout << "a=" << a << endl;
cout << "b=" << b << endl;
cout << "c=" << c << endl;
}
};
class Derived : public Base {
public:
int d = 4;
Derived() {
cout << "Derived中this的地址是:" << this << endl;
cout << "Derived中a的地址是:" << &a << endl;
cout << "Derived中b的地址是:" << &b << endl;
cout << "Derived中d的地址是:" << &d << endl;
}
void fun2() {
cout << "d=" << d << endl;
}
};
int main() {
cout << "Base占用内存大小是:" << sizeof(Base) << endl;
cout << "Derived占用内存大小是:" << sizeof(Derived) << endl;
Derived *derived = new Derived;
derived->fun1();
derived->fun2();
}
输出:
Base占用内存大小是:12
Derived占用内存大小是:16
申请到的内存地址为:0x600003ca8040,大小为:16
Base中this的地址是:0x600003ca8040
Base中a的地址是:0x600003ca8040
Base中b的地址是:0x600003ca8044
Base中c的地址是:0x600003ca8048
Derived中this的地址是:0x600003ca8040
Derived中a的地址是:0x600003ca8040
Derived中b的地址是:0x600003ca8044
Derived中d的地址是:0x600003ca804c
a=1
b=2
c=3
d=4
2.7 奇巧淫技
可以通过指针操作private变量,不推荐使用。
class Base {
private:
int c = 3;
public:
void show() {
cout << c << endl;
}
};
int main() {
Base *base = new Base;
base->show();//3
(*(int *)(base))= 4;
base->show();//4
}
三、派生类如何构造基类
- 创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。
- 如果没有指定基类构造函数,将使用基类的默认构造函数。
- 可以使用初始化列表指明要使用的基类构造函数。
- 基类构造函数负责初始化被继承的数据成员,派生类构造函数主要用于初始化新增的数据成员。
- 派生类的构造函数总是调用一个基类构造函数,包括拷贝构造函数。
class Base {
private:
int a;
public:
int b;
Base():a(0),b(0) {
cout<<"调用了Base的默认构造函数Base()"<<endl;
}
Base(int a, int b):a(a),b(b) {
cout<<"调用了Base的构造函数Base(int a, int b)"<<endl;
}
Base(const Base& base):a(base.a),b(base.b) {
cout<<"调用了Base的拷贝构造函数Base(const Base& base)"<<endl;
}
};
class Derived: public Base {
public:
int c;
Derived():c(0) {
cout<<"调用了Derived的默认构造函数Derived()"<<endl;
}
Derived(int a, int b, int c):Base(a,b),c(c) {
cout<<"调用了Derived的构造函数Derived(int a, int b, int c)"<<endl;
}
//尽管 Derived 和 Base 是不同的类型,但是由于派生类对象包含了基类对象的部分,所以可以在这种情况下进行对象之间的初始化和拷贝。
Derived(const Derived& derived):Base(derived),c(derived.c) {
cout<<"调用了Derived的拷贝构造函数Derived(const Derived& derived)"<<endl;
}
};
int main() {
Derived d1;
Derived d2(1,2,3);
Derived d3(d2);
}
输出:
调用了Base的默认构造函数Base()
调用了Derived的默认构造函数Derived()
调用了Base的构造函数Base(int a, int b)
调用了Derived的构造函数Derived(int a, int b, int c)
调用了Base的拷贝构造函数Base(const Base& base)
调用了Derived的拷贝构造函数Derived(const Derived& derived)
四、名字遮蔽与类作用域
4.1名字遮蔽和作用域
如果在派生类中的成员(变量或函数)与基类中的成员重名,派生类的成员将遮蔽(隐藏)基类的成员。这意味着当通过派生类对象或在派生类的成员函数中使用该成员时,将使用派生类的版本而不是基类的。
class Base {
public:
int a=1;
void show(){
cout<<"Base的show"<<endl;
}
};
class Derived: public Base {
public:
int a=2;
void show(){
cout<<"Derived的show"<<endl;
}
};
int main() {
Derived d;
cout<<d.a<<endl;
d.show();
}
输出:
2
Derived的show
4.2 遮蔽同名函数
注意,基类的成员函数和派生类的成员函数不会构成重载。如果派生类有与基类同名的函数,那么派生类中的函数将遮蔽基类中的所有同名函数。
class Base {
public:
int a=1;
void fun(){
cout<<"无参fun"<<endl;
}
void fun(int b){
cout<<"有参fun"<<endl;
}
};
class Derived: public Base {
public:
int a=2;
void fun(int b){
cout<<"派生类fun"<<endl;
}
};
int main() {
Derived d;
d.fun();//报错,因为派生类已经重写了fun,遮蔽了基类中所有的同名函数
d.fun(1);
}
4.3 类作用域和成员访问
每个类都有自己的作用域,在这个作用域内定义成员。在类的作用域之外,普通的成员只能通过对象(对象本身、对象指针或对象引用)来访问,而静态成员可以通过对象访问,也可以通过类名访问。
4.4 使用域解析符
通过在成员名前面加上类名和域解析符(::),可以访问特定作用域的成员。在没有继承关系的情况下,类名和域解析符可以省略。
4.5 继承关系中的作用域
在存在继承关系时,派生类的作用域会嵌套在基类的作用域中。如果成员在派生类的作用域中找到,就不会在基类的作用域中继续查找;如果在派生类作用域中未找到,则会在基类作用域中继续查找。
4.6 直接使用作用域成员
通过在成员名前面加上类名和域解析符,可以直接使用特定作用域的成员。
class Base {
public:
int a=1;
void show(){
cout<<"Base的show"<<endl;
}
};
class Derived: public Base {
public:
int a=2;
void show(){
cout<<"Derived的show"<<endl;
}
};
int main() {
Derived d;
cout<<d.Base::a<<endl;
d.Base::show();
cout<<d.Derived::a<<endl;
d.Derived::show();
}
输出:
1
Base的show
2
Derived的show
五、继承的特殊关系
-
派生类对象可以使用基类成员。
-
派生类对象可以赋值给基类对象,但会丢失非基类的成员。
-
基类指针可以指向派生类对象。
-
基类引用可以引用派生类对象。
#include <iostream> using namespace std; class Base { public: int Var=1; void fun() { cout << "父类Function" << endl; } }; class Derived : public Base { public: int Var=2; void fun() { cout << "子类Function" << endl; } }; int main() { Derived derivedObj; // 1. 使用基类成员 derivedObj.fun(); cout<<derivedObj.Var<<endl; // 2. 派生类对象赋值给基类对象 Base baseObj = derivedObj; // 非基类成员会被舍弃 baseObj.fun(); // 父类Function cout<<baseObj.Var<<endl; // 3. 基类指针指向派生类对象 Base* basePtr = &derivedObj; basePtr->fun(); // 父类Function cout<<basePtr->Var<<endl; // 4. 基类引用引用派生类对象 Base& baseRef = derivedObj; baseRef.fun(); // 父类Function cout<<baseRef.Var<<endl; return 0; }
输出:
子类Function 2 父类Function 1 父类Function 1 父类Function 1
- 注意:
-
基类指针或引用只能调用基类的方法,不能调用派生类的方法。
#include <iostream> class Base { public: void method() { std::cout << "Base Method" << std::endl; } }; class Derived : public Base { public: void method() { std::cout << "Derived Method" << std::endl; } }; int main() { Derived derivedObj; Base* basePtr = &derivedObj; basePtr->method(); // 调用基类的方法,而不是派生类的方法 return 0; }
-
可以使用派生类构造基类对象。
class Base { public: int value; Base(int val) : value(val) {} }; class Derived : public Base { public: Derived(int val) : Base(val * 2) {} }; int main() { Derived derivedObj(5); Base baseObj = derivedObj; // 使用派生类构造基类对象 return 0; }
-
如果函数的形参是基类,可以使用派生类作为实参。
#include <iostream> class Base { public: virtual void print() { std::cout << "Base" << std::endl; } }; class Derived : public Base { public: void print() override { std::cout << "Derived" << std::endl; } }; void foo(Base base) { base.print(); } int main() { Derived derivedObj; foo(derivedObj); // 使用派生类对象作为基类实参,输出:Base return 0; }
-
C++要求指针和引用类型与赋给的类型匹配,这一规则对继承来说是例外。但是,这种例外只 是单向的,不可以将基类对象和地址赋给派生类引用和指针(没有价值,没有讨论的必要)。
class Base { public: void method() {} }; class Derived : public Base { public: void anotherMethod() {} }; int main() { Base baseObj; Derived derivedObj; // 不允许将基类对象赋给派生类引用和指针 // Derived& dRef = baseObj; // 错误 // Derived* dPtr = &baseObj; // 错误 // 允许将派生类对象赋给基类引用和指针 Base& bRef = derivedObj; Base* bPtr = &derivedObj; return 0; }
-
六、多继承和虚继承(不推荐使用)
多继承是面向对象编程中的一个概念,它允许一个派生类从多个基类继承属性和方法。然而,多继承在实际使用中可能会引发一些问题,比如菱形继承问题,即某个派生类通过两个不同的路径继承同一个基类,从而导致二义性和数据冗余。虚继承是解决菱形继承问题的一种方式。
-
多继承的语法
class Base1 { public: int a=1; }; class Base2 { public: int b=2; }; class Derived : public Base1, public Base2 { public: int c=3; }; int main() { Derived derivedObj; std::cout << derivedObj.a << std::endl; std::cout << derivedObj.b << std::endl; std::cout << derivedObj.c << std::endl; return 0; }
-
多继承存在问题:菱形继承
- 数据冗余:类D中包含两个m_a
- 名称二义性:直接使用D中的m_a不知道是用的B中的还是C中的
class A { public: int a=1; }; class B : public A{ }; class C : public A{ }; class D:public B, public C{ }; int main() { D d;//多继承后,会有2个a,所以需要虚继承 cout <<sizeof(d)<< endl;//8 // cout <<d.a<< endl;//报错:a的访问不明确 //可以使用作用于限定符,可以看出地址不同 cout <<&(d.B::a)<< endl;//0x16aeeac14 cout <<&(d.C::a)<< endl;//0x16aeeac18 return 0; }
解决多继承存在的访问不明确这个问题,可以使用虚继承(虚继承中涉及到虚基类指针等东西,这里暂不介绍):
class A { public: int a=1; }; class B : virtual public A { }; class C : virtual public A{ }; class D:public B, public C{ }; int main() { D d;//虚继承后,只会有1个a cout <<d.a<< endl;//不报错 //可以看出地址相同 cout <<&(d.B::a)<< endl;//0x16f2eac10 cout <<&(d.C::a)<< endl;//0x16f2eac10 return 0; }
注意:
不提倡使用多继承,只有在比较简单和不出现二义性的情况时才使用多继承,能用单一继承解决的问题就不要使用多继承。
如果继承的层次很多、关系很复杂,程序的编写、调试和维护工作都会变得更加困难,由于这个原因,C++之后的很多面向对象的编程语言,例如 Java、C#、PHP 等,都不支持多继承。