初始化列表
特点
Person(int age, int height): m_age(age), m_height(height) { }
初始化列表与默认参数配合使用
Person(int age = 0, int height = 0): m_age(age), m_height(height) { }
如果函数的声明和实现是分离的
- 初始化列表只能写在函数的实现中
- 默认参数只能写在函数的声明中
父类的构造函数
- 子类的构造函数默认会调用父类的无参构造函数
- 如果子类的构造函数显示地调用了父类的有惨构造函数,就不会再去默认调用父类的无参构造函数
- 如果父类缺少无参构造函数,子类的构造函数必须显式调用父类的有参构造函数
继承体系下的构造函数示例
struct Person {
int m_age;
Person(): Person(0) {}
Person(int age): m_age(age) { }
};
struct Student: Person {
int m_no;
Student(): Student(0, 0) {
}
Student(int age, int no): Person(age), m_no(no) {
}
};
父类指针、子类指针
- 父类指针可以指向子类对象,是安全的(继承方式必须是public)
- 子类指针指父类对象是不安全的
class Person {
public:
int m_age;
};
class Student : public Person {
public:
int m_score;
};
void test() {
Person *person = new Student();
// 可以访问m_score, m_age; 安全
person->m_age = 10;
Student *stu = (Student*)new Person();
// 访问不到 m_score, 不安全
stu->m_score = 10;
}
多态
- 默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态
特性
- 同一个操作作用于不同的对象,可以有不同的解释,产生不同的执行结果
- 在运行时,可以识别出真正的对象类型,调用对应子类中的函数
多态的要素
- 有继承关系
- 子类重写父类的成员函数(override)
- 父类指针指向子类对象
- 利用父类调用重写的成员函数
多态分为两类
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态个动态多态的区别
- 静态多态的函数地址早绑定-编译阶段确定函数地址
- 动态多态的函数地址晚绑定-运行阶段确定函数地址
class Animal {
public:
virtual void run() {
cout << "Animal::run()" << endl;
}
};
class Dog: public Animal {
public:
void run() override {
cout << "Dog::run()" << endl;
}
};
class ErHa: public Dog {
public:
void run() override {
cout << "ErHa::run()" << endl;
}
};
void test() {
Dog *dog = new Dog();
dog->run();
Animal * animal0 = new Dog();
animal0->run();
Animal * animal1 = new ErHa();
animal1->run();
}
虚函数
- C++中的多态通过虚函数(virtual function)来实现
- 虚函数:被virtual修饰的成员函数
- 只要在父类中声明为虚函数,子类中重写的函数也自动变成虚函数(也就是说子类可以省略virtual关键字)
虚表
- 虚函数的实现原理是虚表,这个虚表里面存储着最终需要调用的函数地址,这个虚表也叫虚函数表
class Animal {
public:
virtual void run() {
cout << "Animal::run()" << endl;
}
virtual void speak() {
cout <<"Animal::speak()" << endl;
}
};
class Cat: public Animal {
public:
int m_life;
Cat(): m_life(0) {}
void run() override {
cout << "Cat::run()" << endl;
}
void speak() override {
// 先执行父类的成员函数
Animal::speak();、
// 再执行自己的一些操作
cout << "Cat::Speak()" << endl;
}
};
虚表的x86环境图
- 由上图可知,所有的Cat对象只对应一份虚表。
- 具有虚函数的类,在对象创建时,会在其内存布局的时候,拿出4个字节的内存,用来存储该对象对应的函数虚表地址(vfptr: 虚函数指针),x86环境中,一个虚函数地址占用4个字节。
虚表的汇编分析
// 调用Cat::run
// 取出cat指针变量里面存储的地址值
// eax里面存放的是cat对象的地址
mov eax, dword ptr [cat]
// 取出Cat对象的前面4个字节给edx
// edx里面存储的是虚表的地址
mov edx, dword ptr [eax]
// 取出虚表中的前面4个字节给eax
// eax存放的就是Cat::run的函数地址
mov eax, dword ptr [edx]
call eax
// 调用Cat::speak
// 取出cat指针变量里面存储的地址值
// eax里面存放的是cat对象的地址
mov eax, dword ptr [cat]
// 取出Cat对象的前面4个字节给edx
// edx里面存储的是虚表的地址
mov edx, dword ptr [eax]
// 取出虚表中的后面4个字节给eax
// eax存放的就是Cat::speak的函数地址
mov eax, dword ptr [edx]
call eax
虚析构函数
- 含有虚函数的类,应该将析构函数声明为虚函数(虚析构函数)
- delete 父类指针时,才会调用子类的析构函数,保证析构的完整性
- 虚析构或纯纯虚析构就是用来解决父类指针释放子类对象
- 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
- 拥有纯虚析构函数的类属于抽象类
class Animal {
public:
virtual void run() {
cout << "Animal::run()" << endl;
}
virtual void speak() {
cout <<"Animal::speak()" << endl;
}
// 虚析构
virtual ~Animal() {
}
// 纯虚析构
virtual ~Animal() = 0;
};
class Cat: public Animal {
public:
int m_life;
Cat(): m_life(0) {}
void run() override {
cout << "Cat::run()" << endl;
}
void speak() override {
// 先执行父类的成员函数
Animal::speak();
// 再执行自己的一些操作
cout << "Cat::Speak()" << endl;
}
~Cat() {
cout << "Cat::~Cat()" << endl;
}
};
纯虚函数
- 定义:没有函数体且初始化为0的虚函数,用来定义接口规范
- 抽象类
- 含有虚函数的类,不可以实例化
- 抽象类也可以包含非纯虚函数
- 如果父类是抽象类,子类没有完全实现纯虚函数,那么这个子类依然是抽象类
class Animal {
public:
virtual void run() = 0;
virtual void speak() = 0;
virtual ~Animal() { }
};