面向对象三大特点:封装、继承、多态。封装为了信息隐藏,继承(is-a)为了代码复用,多态则是为了接口复用,即一个接口,多种实现。有继承,就会有派生。类定义时,代码实现者一般会根据实际需要定义好各个成员的访问权限,继承体系也有明确的权限控制。
继承,根据父类的个数和继承层次体系可分为单一继承、多重继承、菱形继承等,多重继承可能会遇到二义性问题,问题表现形式是明明代码中定义类编译通过,一运行就报错ambiguous,解决方法就是使用域运算符或者虚继承。根据继承方式可分为虚继承、非虚继承。根据是否为虚继承,基类又有是否为虚基类的概念。
在声明类时定义成员函数,根据函数的定义方式,可将函数定义为普通成员函数、虚函数、纯虚函数。多态的机制则是依赖继承和虚函数来实现的,而纯虚函数是在实现接口(抽象类),而非普通类的时候使用。
组合(has-a)也是代码复用的重要思想,23种经典设计模式中就有很多模式使用了组合的概念,巧妙地给出了在工程方面组织代码架构的思想。实际项目开发中,可以结合ORM(Object-Relation-Mapping)实现现实和类设计的抽象。
友元,则是继承/组合关系里边的一种特殊概念。在一个类A中,将类B声明为友元类,则类B可以访问类A的私有成员和保护成员。另外,也可以将函数声明为友元函数。虽然使用友元可以简化类的设计,但这种方式也破坏了类的封装,一般情况下不提倡频繁使用。
这些基础概念非常重要,具体细节可以参考C++经典书籍,如C++ Prime、C++ Prime Plus、C++编程思想、Effective系列等,这里不作详细介绍,仅对以下容易混淆的问题进行讨论。
- C++中结构体struct能否被继承?
- 类在什么情况下不能被继承?
- 子类可以继承父类的私有成员变量吗?
- 构造函数和析构函数能不能被继承?
- 析构函数为什么会再自动调用父类的析构函数?
- C++实现一个不被继承的类
- 构造函数和析构函数可以是虚函数吗?为什么?
C++中结构体struct能否被继承?
在C++中结构和类没有太大的区别,或者说区别在于结构中成员默认为public访问,类中为private访问方式。
实际上,类与结构的唯一区别在于:在默认状态下,结构的所有成员均是公有的,而类的所有成员是私有的。除此之外,类与结构是等价的,也就是说,一个结构定义了一个类的类型。
C++结构具有如下特性:
- 结构可以包含各种类型成员,也可以加入private,protected,public等修饰符
- 简单类型成员
- 函数
- 函数指针
- 其他结构类型的成员
- 其他类的对象成员
- 本结构类型的指针
- 结构可以继承,甚至可以多继承,虚继承,多态,重载运算符,定义友元函数,友元类等。
类在什么情况下不能被继承?
1、如果类被final修饰,那么此类不可以被继承。
2、如果类中只有private的构造函数,那么此类不可以被继承。(比如单例模式,但这种模式影响了类的实例化)
其原因在于:
(1)一个类一定会有构造函数,如果不写,那就是默认的无参构造函数,如果写,就只有所写的构造函数。
(2)子类的构造函数一定会调用父类的构造函数,但是如果父类中只有私有的构造函数,那么子类就无法调用父类,就会有问题。
3.通过友元和虚继承机制构造一个类,可以实现不能继承,但可以正常实例化的功能。(下面会有介绍)
子类可以继承父类的私有成员变量吗?
- 关于私有成员变量
无论父类中的成员变量是私有的、公有的、还是其它类型的,子类都会拥有父类中的这些成员变量。但是父类中的私有成员变量,无法在子类中直接访问,必须通过从父类中继承得到的protected、public方法(如getter、setter方法)来访问。所谓的private、public、protected关键字都是控制访问权限的,跟能不能继承没有关系。
- 关于静态成员变量
无论父类中的成员变量是静态的、还是非静态的,子类都会拥有父类中的这些成员变量。
- 关于被子类覆盖的成员变量
无论父类中的成员变量是否被子类覆盖,子类都会拥有父类中的这些成员变量。
构造函数和析构函数能不能被继承?
继承的时候,派生类对象中包含整个完整的基类子对象。但是在派生类中,有一些方法应该不是一般的继承,比如 构造、析构、拷贝构造等(就是那些在用户未定义时,由系统自动生成默认方法的那些成员方法),这些方法应该是逐级调用的关系,而不是 “继承” 关系,个人目前这么理解。
构造函数和析构函数都不能被继承。
1. 构造函数不能为 virtual,构造函数不能继承;
2. 如果子类不显式调用父类的构造函数,编译器会自动调用父类的【无参构造函数】;
3. 继承构造函数(Inheriting constructors)
- C++11 才支持;
- 实质是编译器自动生成代码,通过调用父类构造函数来实现,不是真正意义上的【继承】,仅仅是为了减少代码书写量(参考 《C++ Primer》)。
constructor
构造方法是用来初始化类的对象,与父类的其他成员不同,它不能被子类继承。因此,在创建子类对象时,为了初始化从父类继承来的数据成员,系统需要调用其父类的构造方法。
如果没有显式的构造函数,编译器会给一个默认的构造函数,并且该默认的构造函数仅仅在没有显式的声明构造函数情况下创建。
构造原则如下:
- 如果子类没有定义构造方法,则调用父类的无参数构造方法
- 如果子类定义了构造方法,不论无参数还是带参数,在创建子类的对象的时候,首先执行父类无参数的构造方法,然后执行自己的构造方法。
- 在创建子类对象的时候,如果子类的构造函数没有显式调用父类的构造函数,则会调用父类的默认无参构造函数。
- 在创建子类对象时候,如果子类的构造函数没有显式调用父类的构造函数,且父类自己提供了无参构造函数,则会调用父类自己的无参构造函数。
- 在创建子类对象时候,如果子类的构造函数没有显式调用父类的构造函数且父类之定义了自己的有参构造函数,则会出错(如果父类只有有参数的构造方法,则子类必须显式调用此带参构造方法)。
- 如果子类调用父类带参数的构造方法,需要初始化父类成员对象的方法。
注意:子类只能在构造对象时才能默认(或者用初始化列表中显式调用“特定父类构造函数”)调用父类的构造函数,在构造完成后不能像调用父类成员函数一样调用父类构造函数,这样保证了父类构造函数只调用一次的原则。
deconstructor
析构函数的作用是在对象撤销之前,进行必要的清理工作。
在派生时,派生类是不能继承基类的析构函数的,也需要通过派生类的析构函数去调用基类的析构函数。在派生类中可以根据需要定义自己的析构函数,用来地派生类中所增加的成员进行清理工作。基类的清理工作仍然由基类的析构函数负责。
调用的顺序与构造函数正好相反:先执行派生类自己的析构函数,对派生类新增加的成员进行清理,然后调用子对象的析构函数,对子对象进行清理,最后调用基类的析构函数,对基类进行清理。
析构函数为什么会再自动调用父类的析构函数?
原帖: http://bbs.youkuaiyun.com/topics/380022416
里面的讨论基本上已经给出答案了。
派生类的析构函数在执行完后,会自动执行基类的析构函数,这个是编译器强制规定的,没有为什么,甚至你在析构函数里调用return都不会立即返回到调用处,而是会先按顺序把析构函数全部调用完。
以下是从stackoverflow上找到的回答,引用了RTTI,解释的也更专业一点。
https://stackoverflow.com/questions/3261694/why-base-class-destructor-virtual-is-called-when-a-derived-class-object-is-del
C++实现一个不被继承的类
在C#中定义了关键字sealed,被sealed修饰的类不能够被继承。在Java中同样也有关键字final表示一个类不能被继承。C++11提供final关键字使得类不能够被继承。
如何自己实现一个不被继承的类?
一、单例模式下的不被继承的类
常规的解法是,把类的构造和析构函数都设置为private即可,然后公有派生;这是因为子类访问不到父类私有属性。这使用了C++构造派生类实例时会自动先调用父类构造函数的特性,同样析构的顺序和构造的顺序相反。
从继承的特性来说,派生类继承其基类的私有成员,但是不可访问;派生类的构造要先调基类的构造函数构造基类然后在调自己的构造函数构造自己;我们可以将基类的构造函数写在private下,那么基类此时就是一个不被继承的类;但是这样基类是不可被继承,但同时他也不会正常实例化对象,因为不可访问其私有成员;这时我们可以借助单例模式来实现,通过静态方法实现返回一个对象,但是这种实例化的方式不符合我们正常类的实例化;所以此方法不是最优解,我们期望实现一个类可以不被继承但不影响其实例化对象;
最直观的解决方法就是将其构造函数声明为私有的,这样就可以阻止子类构造对象了。但是这样的话,就无法构造本身的对象了,就无法利用了。既然这样,我们又可以想定义一个静态方法来构造类和释放类。整个过程就是实现单例模式的过程。
#include<iostream>
using namespace std;
class A
{
public:
static A * Construct(int n)
{
A *pa = new A;
pa->num = n;
cout<<"num is:"<<pa->num<<endl;
return pa;
}
static void Destruct(A * pIntance)
{
delete pIntance;
pIntance = NULL;
}
private:
A(){}
~A(){}
public:
int num;
};
int main()
{
A *f = A::Construct(9);
cout<<f->num<<endl;
A::Destruct(f);
return 0;
}
按照理论分析,这样做确实可以做到防止被继承。
注意:又有一个新的问题,对,就是只能在堆上创建,无法再栈上实现这个类。
这就是私有的构造函数的局限性。
我们只能得到位于栈上或堆上的实例(通过类内的static方法创建实例)!如果在堆和栈都能创建实例呢?
二、使用友元和虚基类机制实现不被继承的类
利用友元不能被继承的特性,可以实现这样的类。
主要思想,设计一个模板辅助类Base,将构造函数声明为私有的;再设计一个不能继承的类FinalClass,,将FinalClass 作为Base的友元类。
将FinalClass虚继承Base。代码如下,
include <iostream>
using namespace std;
template <typename T>
class Base
{
friend T;
private:
Base()
{
cout << "base" << endl;
}
~Base(){}
};
class FinalClass : virtual public Base<FinalClass>
{
//一定注意 必须是虚继承
public:
FinalClass()
{
cout << "FinalClass()" << endl;
}
};
class C:public FinalClass
{
public:
C(){} //继承时报错,无法通过编译
};
int main()
{
FinalClass b; //B类无法被继承
//C c;
return 0;
}
原理如下:
类Base的构造函数和析构函数因为是私有的,只有Base类的友元可以访问,FinalClass类在继承时将模板的参数设置为了FinalClass类,所以构造FinalClass类对象时,可以直接访问父类(Base)的构造函数。
为什么必须是虚继承呢?
虚继承的功能是:当出现了菱形继承体系的时候,使用虚继承可以防止二义性,即子孙类不会继承多个原始祖先类。这有什么用呢?
那么虚继承如何解决这种二义性的呢?从具有虚基类的类继承的类在初始化时进行了特殊处理,在虚派生中,由最低层次的派生类的构造函数初始化虚基类。
结合上面的代码来解释:C类在调用构造函数时,不会先调用FinalClass的构造函数,而是直接调用Base的构造函数,C不是Base的友元类,所以无法访问。这样的话C就不能继承FinalClass。
注:c++11的已经加入了final关键字,直接在类后面加上final关键字,就可以防止该类被继承。
非模板的例子
要正常的使用该类,它的构造函数和析构函数必然是public访问权限。我们为它增加一个虚继承的父类,它是父类的friend(友元),父类的构造函数是private。
class A;
class final
{
friend class A;//class关键字不可省略,否则在g++中不能编译通过
final()
{}
};
class A : virtual public final
{
public:
A()
{}
};
class B : public A
{
public:
B()
{
}
};
class A 是final的friend,所以A可以调用final的构造函数。
因为A虚继承了final,往后所有继承A的子类,都必须自己实例化final,以保证final在对象中的唯一性
所以,B继承A后,需要自己调用虚继承的父类final的构造函数。显然,B不是final的友元,不能访问private中的构造函数。
(注意,将上面的final的构造函数换成了析构函数~final()后,在VS2013中能通过编译,读者想想为什么呢?)
为什么上述设计就可以呢?原因如下:
- 虚基类的特点
虚基类构造函数的参数必须由最新派生出来的类负责初始化(即使不是直接继承)
虚基类的构造函数先于非虚基类的构造函数执行
- 友元的特点
友元关系是单向的,即B是A的友元(朋友),但是A不一定是B的友元(朋友);
友元关系是不可传递的,比如B是A的友元,A是C的友元,则B是C的友元,这一理论不成立;
友元关系是不可继承的,B是A的友元,C继承B,但是C不是A的友元;
如果B可以被继承,那么C类的对象的内存布局应该如下图所示:
由于虚基类的构造要依靠最终子类,那么C要先构造A,但是A的构造函数在私有下,外部不可访问,并且A与C之间不存在友元关系,所以C无法构造A,所以B是一个不可被继承的类,但是可以和普通类一样实例化。
参考:
C++虚基类构造函数详解(调用顺序)之一 - 记忆斑驳的时光 - 博客园
C++ 虚继承对基类构造函数调用顺序的影响_yuanchunsi的博客-优快云博客