1 C++ 中 直接class 类名 和 include 类的头文件的区别?

Class Student;只是声明存在这么一个类,但是通过这个声明无法得到任何关于此类的具体信息。这样你可以在其他使用到的地方声明一个该类型的指针或引用(#include 头文件)。其他的什么也用不了。
#include "student.h"则是将整个该头文件与使用到的地方关联起来。

  • 1、使用class 类名一般是为了去除编译依赖,减少编译消耗的时间。

在一些大的工程中,可能会包含几十个基础类,免不了之间会互相引用(不满足继承关系,而是组合关系)。也就是需要互相声明。好了,这时候会带来一些混乱。如果处理得不好,会搞得一团糟。
编码时,我们一般会尽量避免include头文件,而是采用声明class XXX。但有时候还是必须用Include头文件,那么,两者的划分在于什么呢?
我们首先要明白为什么要用声明取代头文件包含。是为了避免无必要的重编译(在头文件发生变更时)。工程较大,低速机,或基础类经常变更(不合理的设计吧),编译速度还是会在意的,另外,更为重要的是,采用声明可降低代码(class)之间的藕合度,这也是面向对象设计的一大原则(一般原则:尽量少include
a. 头文件中尽量少include,如果可以简单申明Class Student;解决,那最好。减少没有必要的include;
b. 实现文件中也要尽量少include,不要include没有用到的头文件。

  • 2、那什么时候可以只是简单声明class Student呢?

简单的说:不需要知道Student的内存布局的用法都可以(静态成员除外),也就是讲如果是指针或引用方式的都行。
比如:
Student* m_student; // 指针占4个字节长
Student& test(Student* stu) { return *m_student; }

  • 3、什么时候不能简单声明class clsOld,必须include呢?

不满足2描述的的场景下:
比如:Student m_student; // 不知道占据大小,必须要通过它的具体声明来计算
原因很简单,想想你要计算sizeof(m_student),但连m_student的size都不知道,编译器显然会无能为力。
特殊情况:int test() { return Student ::m_sInt; }
静态成员调用,想来应该是不需要知道内存布局的,但因为需要知道m_sInt是属于Student命名空间 的,如果只声明Class Student;显然是不足以说明的,所以必须包含头文件。
综上所述:
1:如果有共同相关依赖(必须include)的类,比如A,B都依赖D可以放在一起,然后直接Include “d”类的使用者只需关心与本类暴露出的相关类型,内部用到的类型不用去管(不用自已去include d)。这样给出的class,调用者才更好用(不用去看代码查找,是不是还需要包含其它头文件)。
2:如果A类依赖D B类不依赖D,可以把它们分开两个头文件。各自Include。这样可避免当D发生变化时, 避免不必要重编译。
3:类中尽量采用指针或引用方式调用其它类,这样就可以只声明class xxx了。并且这也符合资源最优 利用,更利于使用多态。?? 不太认同??

个人心得:如果仅仅是声明class 类名,就只能声明这个类的指针或引用,你想解引用都不行,就相当于存储了指针。但include了的话,指针就可以解引用。
函数参数,以及指针,引用的声明定义,只需要前置声明就够了
对象的定义,声明,需要类型的全部信息
某些情况 inlcude 头文件 会报错,必须只前置声明????
同时include 和类的前置声明。
error::expected class name
class MediaSubtitleRenderCallbackWrapper: virtual public android::RefBase
只类的前置声明。—- compile ok
在C++中,报错:allocating an object of abstract class type “xxxx”
原因:一般是因为该类继承的抽象类中,有未实现的抽象函数。
vendor/realtek/hardware/interfaces/media/1.0/default/RtkMediaPlayer.cpp:731:64: error: no matching constructor for initialization of 'vendor::realtek::media::V1_0::implementation::RtkMediaPlayer::MediaSubtitleRenderCallbackListener'

2 构造函数和析构函数

2.1 构造顺序

  1. 构造函数:从类层次的最根处开始(先父类,再子类)。每一层,先调用基类的构造函数,再调用成员对象的构造函数(按照成员对象在类中的声明次序来)。
    1. 按声明顺序调用基类和成员构造函数。
    2. 如果类派生自虚拟基类,则会将对象的虚拟基指针初始化。
    3. 如果类具有或继承了虚函数,则会将对象的虚函数指针初始化。 虚函数指针指向类中的虚函数表,确保虚函数正确地调用绑定代码。
    4. 它执行自己函数体中的所有代码。
  2. 析构函数:构造函数的相反次序。
    1. 构造函数主体中的代码将展开。
    2. 基类和成员对象将被销毁,顺序与声明顺序相反。
    3. 如果是非委托构造函数,所有完全构造的基类对象和成员均将被销毁。 但是,对象本身不是完全构造的,因此析构函数不会运行。

2.2 构造函数不能为虚函数

官方说法:虚函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。 要创建一个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构造函数不应该被定义为虚函数。
简单讲就是没有意义。虚函数的作用在于通过子类的指针或引用来调用父类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用。
网上普遍解释:虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
用什么方式实现虚函数是编译器的事情,使用Vtable只是大多数编译器采用的一种手段,并不代表编译器实现不了虚构造函数,编译器之所以不支持虚构造函数主要原因就是没有必要,所以正好这种实现方式也不支持,巧合而已。

2.3 析构函数可以为虚函数

对象已经创建,虚表指针存放析构函数的地址,基类与派生类都含有析构虚函数,创建基类与子类对象,都含有各类的虚表指针,当写通用函数时,运行根据传入对象的类型确定析构函数的地址,然后调用该析构函数。 但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。

2.4 派生类构造函数不能直接初始化类中的基类成员

因为它们在基类中可能是私有的,不能访问赋值。要想初始化这些基类成员,必须通过基类的构造函数才能完成。
调用基类构造函数 语法形式:
派生类构造函数名(形参列表):基类名(形参1),基类名(形参2),····
{
······ // 初始化派生类成员变量
}
派生类中的数据成员有基类中的数据成员和新增加的数据成员构成,如果新增加的数据成员中还包含有对象成员,则这些新增加的数据成员还会间接的包含这些对象中的成员,因此如果要对派生类中的成员进行初始化,就应该对以上这些数据成员进行初始化。
5、<类名>(<参数表>)构造函数与类同名,且没有返回值类型,可以是私有的。既可以在类外定义,也可以作为内联函数在类内定义。构造函数允许函数重载,提供初始化类对象的不同方法。
当我们想利用单例模式的时候,就可以把类的构造函数声明成private的,这样就能保证外界不能实例多个对象出来了。
如: 单例模式。