构造函数
类的实例化,必须通过构造函数。
通过类名实例化对象,会默认调用构造函数和析构函数;
Base a; // 自动调用构造函数去实例化对象
Base b; // 程序结束也会自动调用析构函数,先进后出
调用顺序
通过类指针和new去实例化对象,仅定义指针则不调用构造函数、仅new则仅调用构造函数、只有手动new和delete才会调用构造函数和析构函数(不排顺序,即写即操作)。
先基类,后派生类。
Base *a; // 不调用任何函数
Base *b = new Base(); // 调用构造函数
Base *c = new Base(); // 调用构造函数
delete c; // 调用析构函数(马上)
虚函数
基于虚函数表的底层实现
不能写成虚函数:
- 友元函数,它不是类的成员函数
- 全局函数
- 静态成员函数,它没有this指针
- 构造函数,拷贝构造函数,以及赋值运算符重载(可以但是一般不建议作为虚函数)
虚函数表
在函数的定义前加virtual,则该类的内存会多一个指向虚函数表的指针(4位或8位)。实例化对象过程中,如果对象存在虚函数,那么编译器就会生成一个指向虚函数表的指针,所有的虚函数都存在于这个表中,虚函数表就可以理解为一个数组,每个单元用来存放虚函数的地址。这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。
虚函数表一般在对象的最前面。
重写虚函数后会将重写内容覆盖原虚函数表,否则直接继承基类的表,若既有重写又重新定义了新的虚函数,则先覆盖,再将新的虚函数接在继承来的表后面,具体如下:
class Base {
public:
virtual void a() {cout << "Base a()" << endl; }
virtual void b() {cout << "Base b()" << endl; }
virtual void c() {cout << "Base c()" << endl; }
};
class Derive : public Base {
public:
virtual void b() {cout << "Derive b()" << endl; }
};
int main() {
// 基类指针指向派生类
Base *p = new Derive();
p->a();
p->b();
p->c();
delete p;
}
如果是多继承,则继承类有多个虚函数表,重写的虚函数覆盖在继承的表上,新定义的虚函数则写在第一个表的后面,具体如下:
class Base1 {
public:
virtual void a() {cout << "Base1 a()" << endl; }
virtual void b() {cout << "Base1 b()" << endl; }
virtual void c() {cout << "Base1 c()" << endl; }
};
class Base2 {
public:
virtual void d() {cout << "Base2 d()" << endl; }
virtual void e() {cout << "Base2 e()" << endl; }
};
class Derive : public Base1, public Base2 {
public:
virtual void a() {cout << "Derive a()" << endl; }
virtual void d() {cout << "Derive d()" << endl; }
virtual void f() {cout << "Derive f()" << endl; }
};
创建时机
对于虚函数表来说,在编译的过程中编译器就为含有虚函数的类创建了虚函数表,并且编译器会在构造函数中插入一段代码,这段代码用来给虚函数指针赋值。因此虚函数表是在编译的过程中创建。
对于虚函数指针来说,由于虚函数指针是基于对象的,所以对象在实例化的时候,虚函数指针就会创建,所以是在运行时创建。由于在实例化对象的时候会调用到构造函数,所以就会执行虚函数指针的赋值代码,从而将虚函数表的地址赋值给虚函数指针。
// 静态联编:在编译时所进行的这种联编又称静态束定,在编译时就解决了程序中的操作调用与执行该操作代码间的关系。
// 动态联编:编译程序在编译阶段并不能确切知道将要调用的函数,只有在程序运行时才能确定将要调用的函数,为此要确切知道该调用的函数,
// 要求联编工作要在程序运行时进行,这种在程序运行时进行联编工作被称为动态联编。
class A{
public:
int x;
A(){
memset(this, 0, sizeof(x)); // 将this对象中的成员初始化为0
cout << "构造函数" << endl;
}
A(const A& a) {
memcpy(this, &a, sizeof(A)); // 直接拷贝内存中的内容
cout << "拷贝构造函数" << endl;
}
virtual void virfunc() {
cout << "虚函数func" << endl;
}
void func() {
cout << "func函数" << endl;
}
virtual ~A() {
cout << "析构函数" << endl;
}
};
A a; // 静态编连
a.virfunc(); // 可以调用该虚函数,说明是静态编连,构造函数里将虚函数指针置空,没有实质改变该虚函数的指向
A *a = new A(); // 动态编连
a->virfunc(); // 无法编译通过,说明是动态编连,虚函数指针置空后无法再调用到该虚函数
/* 所以说,实现多态的时候尽量还是用 指针或者引用 去实例化对象(记得要手动delete和置空该指针) */
纯虚函数
class Base {
public:
virtual void func() = 0; // 纯虚函数
};
在成员函数(必须为虚函数)的形参列表后面写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。纯虚函数是一定要被继承的,否则它存在没有任何意义。