1.构造与析构
1.1类成员初始化方式
赋值初始化:在函数体内进行赋值初始化
列表初始化:在冒号后使用初始化列表进行初始化
Student(string in_name, int in_age):name(in_name),age(in_age) {}
区别:列表初始化效率更高,因为C++中赋值的操作会产生临时对象,如果赋值的是另外的类,还需要调用构造函数。而列表初始化时纯粹的初始化,可能调用拷贝构造函数。
1.2必须使用成员列表初始化的情况
初始化引用成员,初始化常量成员(前两个都是本身就需要在定义的时候初始化,而成员列表初始化是不需要等到构造函数为类对象申请完所有内存再进行初始化的,相当于变量一定义就初始化了,而不是先定义再初始化),调用基类的有参数的构造函数,调用成员类有参数的构造函数。
class Test{
public:
int& a;
Test(int& b) : a(b){}
}
1.3构造与析构的执行顺序
构造函数执行顺序:基类的构造函数-派生类的成员类对象的构造函数(组合的情况),派生类的构造函数。
析构函数执行顺序:派生类析构函数-派生类成员类对象的析构函数-基类的析构函数。
1.4构造函数的作用
构造函数主要用来在创建对象时完成对对象属性的一些初始化等操作, 当创建对象时, 对象会自动调用它的构造函数。一般来说, 构造函数有以下三个方面的作用:
给创建的对象建立一个标识符;为对象数据成员开辟内存空间;完成对象数据成员的初始化。
1.5类什么时候会析构
对象的生命周期结束,
使用delete删除指向对象的指针或者删除指向对象的基类类型指针,
组合情况,外部类对象被析构,其成员变量的类也被析构
2.拷贝构造函数
2.1深拷贝与浅拷贝
深拷贝:开辟一块新内存地址用于存放复制的对象
浅拷贝:引用对象,并不开辟新的内存空间
C++默认的是浅拷贝,所以一般需要写一个拷贝构造函数定义深拷贝,这样的话避免在拷贝对象时,一个对象析构了,另一个对象仍然指向析构的空间,变成了野指针。
2.2如何禁止自动生成拷贝构造函数
一般需要手动重写拷贝构造函数和拷贝赋值函数,为了避免被调用,可以设为private,但是类的成员函数和友元函数还是能够调用。这时可以顶一个base基类,在基类中将拷贝构造函数和拷贝赋值函数设置成private,派生类中的编译器就不会自动生成这两个函数了。
2.3拷贝构造函数必须引用传参不能值传参
拷贝构造函数用来初始化一个非引用类类型的对象,如果传值的方式进行传参,那么在传入时需要拷贝传进来的实参——调用拷贝构造函数,而拷贝构造函数又需要传递的参数,就会一直递归。
3.继承、多态
3.1类与类之间的关系
包含关系:一个类的成员变量是其他类的对象。(组合)
使用关系:通过类之间的成员函数互相联系,定义友元或通过参数传递实现。
继承关系:子类包括了父类的属性和方法。
3.2多态性
C++支持两种多态,编译时(静态)多态和运行时(动态)多态。
编译时多态:函数重载,模板,在编译时就确定调用函数的类型
运行时多态:函数覆盖,虚函数
3.3重载、隐藏、重写(覆盖)
3.3.1重载、隐藏、重写(覆盖)三者的区别
重载:是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型(统一为void,否则报错)。
隐藏:是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。(可不看)
重写(覆盖):是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。
3.3.2重载为什么改变参数就可以实现调用不同的函数?
因为C++在编译的时候会对函数进行重命名,保证函数名的唯一性,而重载函数的参数不同,就会被命名为不同的函数名。
3.3.3构造函数可以被重载么?析构函数呢?
构造函数可以被重载,因为构造函数可以有多个且可以带参数。
析构函数不可以被重载,因为析构函数只能有一个,且不能带参数。
3.3.4继承与组合的优缺点
继承:继承就是 is a的关系,如student is a person。继承的优点是,子类可以覆盖父类的方法来方便对父类进行扩展。缺点是:父类的内部细节对子类的可见的,父类方法做出修改子类方法也需要进行修改,是一种高耦合的方式。
组合:当前对象只能通过包含的其他对象调用其对象的方法,具体方法不可见。修改包含对象的代码不需要修改当前对象,是低耦合的。缺点是容易产生过多的对象,对接口需要仔细定义。
3.3.5访问权限与继承方式
访问权限:
Public:类内可以访问,类外也可以访问
Private:类内可以访问,类外不能访问
Protected:类内可以访问,派生类可以访问,类外不能访问
继承方式(取更小的范围):
Public:基类成员在派生类中的访问权限不变
Private:基类所有成员在派生类中都会变为private
Protected:基类的私有成员在派生类中仍为私有,其他的在派生类中都为protected
4.虚函数
抽象类与纯虚函数
抽象类就是一个接口,包含纯虚函数的类都是抽象类
Virtual <类型><函数名><参数表> = 0;
抽象类不能实例化为对象,只能作为基类为派生类服务。派生类必须实现所有纯虚函数,否自也是抽象类。
4.1 虚函数与纯虚函数的区别
虚函数既有定义又有实现,纯虚函数只有定义不能有实现。
含有虚函数的类可以实例化,含有纯虚函数的类不能实例化,只能作为接口被继承。
4.2 哪些函数不能是虚函数
构造函数
内联函数,内联函数在编译阶段进行函数体替换操作,而虚函数则要在运行期间进行类型确定。
静态函数,静态函数不属于对象,属于类,静态成员函数没有this指针(虚函数的vptr指针需要this指针来访问),设置为虚函数没有意义。
友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数一说。
普通函数,普通函数不属于类的成员函数,不具有继承特性。
4.3 构造函数为什么不能是虚函数
存储的角度:虚构函数对应一个指向虚函数表的指针,而这个指针需要存储在对象的内存空间。如果构造函数是虚函数,就需要调用该指针,而构造函数未执行完,对象的内存空间还未建立。
从使用的角度:虚函数是用来在信息不完全的情况下,使重载的函数得到相应的调用。而构造函数本身就是要被调用的,不需要通过父类的指针或引用去调用。
4.4 析构函数为什么必须是虚函数
为了防止内存泄漏。如果基类的析构函数不为虚函数,那么在删除指向派生类实例对象的基类指针时,只会析构基类的析构函数,无法触发动态绑定,派生类的析构函数不会执行。
4.5 构造函数和析构函数可以调用虚函数吗
不提倡调用虚函数,因为不会触发动态绑定。
构造函数中,如果调用虚函数。因为父类的构造函数先执行,而构造函数中有虚函数,子类还未创建,所以C++不会进行动态绑定。
析构函数中,如果调用虚函数,父类的析构函数执行时,子类已经析构,这时候虚函数也无法绑定到子类的方法上,没有什么意义。
所以即使调用了虚函数,也是自身类型定义的版本,而和虚函数的本质相违背。
4.6 虚函数的代价
带有虚函数的类,需要产生一个虚函数表(保存指向虚成员函数的指针)
带有虚函数的类的每一个对象,都会包含一个指向虚函数表的指针
5.其他
5.1空类的大小是多少,为什么
C++空类的大小不为零,因为C++要求对于类的每一个实例都必须有独一无二的地址,编译器会自动为空类分配一个字节的大小(不同的编译器设置不一样)。如果有虚函数,则每个对象会有一个vptr指针指向虚函数表。
5.2类对象的大小包含那些部分
类的非静态成员变量大小(静态成员变量和成员函数不占据类的空间大小,成员函数的地址在编译时就确定了,所有对象共享一份成员变量,通过this指针进行绑定)
内存对齐产生的额外空间的分配
虚函数的vptr指针大小
若是派生类,还需包括继承基类的成员变量部分的大小
5.3 如何计算一个类子类的个数
类中设计静态成员变量count作为计数器
类定义结束后初始化count
类的构造函数中,进行count+1
类的拷贝构造函数中,进行count+1
类的复制构造函数中,进行count+1
类的析构函数中,进行count-1