C++内存分区
(1)栈区:存储函数内部的非静态局部变量、函数参数等,栈内存由编译器自动分配与释放。栈向低地址扩展,分配的内存是连续的。进栈的首先是函数调用语句的下一条可执行语句的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。
(2)堆区:用于存放进程运行时动态分配的内存,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。堆向高地址扩展,操作系统用链表来管理空闲内存地址,堆中内容只能通过指针间接访问。当对象足够大或者对象需要在某个特定的时刻进行构造或者析构时使用堆。
(3)全局区/静态存储区:存放全局变量和静态变量,程序运行结束操作系统自动释放。在C语言中,未初始化的放在.bss段中,已初始化的放在.data段中,C++中不再区分了。
(4)常量区:存放字符串常量等,程序运行结束操作系统自动释放。
(5)代码区(.text段):存放程序的执行代码(CPU执行的机器指令),代码段是共享和只读的。
拷贝构造函数的使用时机
- 使用已经创建好的对象初始化新对象
- 以值传递的方式给函数传参
- 以值传递方式返回局部对象
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age) {
m_age = age;
}
// 拷贝构造函数
Person(const Person& p)
{
m_age = p.m_age;
cout << "Person(const Person& p);" << endl;
}
public:
int m_age;
};
// 1.使用已经创建好的对象初始化新对象
void test01()
{
Person p1(18);
Person p2(p1); // Person(const Person& p);
}
// 2.以值传递的方式给函数传参
void doWork(Person p) {}
void test02()
{
Person p(18);
doWork(p); // Person(const Person& p);
}
// 3.以值传递方式返回局部对象
Person doWork2()
{
Person p(18);
return p;
}
void test03()
{
Person p = doWork2(); // Person(const Person& p);
}
int main()
{
test01();
test02();
test03();
system("pause");
return 0;
}
深拷贝和浅拷贝
- 浅拷贝:简单的赋值拷贝操作
- 深拷贝:在堆区重新申请空间进行拷贝
#include <iostream>
using namespace std;
class Person {
public:
Person() {
m_age = 0;
m_height = nullptr;
}
Person(int age, int height) {
m_age = age;
m_height = new int(height);
}
Person(const Person& p) {
m_age = p.m_age;
// 编译器默认实现的浅拷贝
//m_height = p.m_height;
// 深拷贝,浅拷贝会重复释放堆区内存
m_height = new int(*p.m_height);
}
~Person() {
// 释放堆区开辟的数据
if (m_height != nullptr)
{
delete m_height;
m_height = nullptr;
}
}
public:
int m_age;
int* m_height;
};
只在堆区/栈区创建对象
- 只在堆区生成对象
将析构函数设置为私有。原因:编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的可访问性。若析构函数不可访问,则不能在栈上创建对象。 - 只在栈区生成对象
重载new操作符,设置为私有权限。原因:在堆上生成对象,需要使用new操作符,将其设置为私有权限,就不能在堆上生成对象。
#include <iostream>
using namespace std;
class Person {
public:
Person() :_id(0) {}
Person(int id) :_id(id) {}
private:
// 析构函数
~Person() {}
// new操作符
static void* operator new(size_t size) {
Person* p = (Person*)malloc(size);
return p;
}
int _id;
};
int main()
{
// 栈区无法生成对象
//Person p;
// 堆区无法生成对象
//Person* p = new Person;
system("pause");
return 0;
}
this指针
为什么引入this指针
只有非静态数据成员属于对象,静态数据成员、静态函数、非静态函数都不属于对象,因此非静态成员函数只会一份实例,多个同类型对象会共用一块代码。由于每个对象都有独一无二的地址,因此对象在调用非静态成员函数时,非静态成员函数需要知道是谁在调用它。
this指针的特点
- this指针隐含在非静态成员函数中
- this指针指向被调用的成员函数所属的对象
- this指针的本质是指针常量,即指针的指向不可修改
- this是一个右值,不能对this指针取地址
this指针的使用
- 当非静态成员函数形参与类的数据成员相同时,用this指针来区分
- 实现对象的链式引用,在类的非静态成员函数中返回对象本身
delete this合法性
- 合法,但必须保证this指针所指的对象是通过new操作符分配的,必须保证this指针之后不再使用。
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
// 1.当非静态成员函数形参与类的数据成员相同时,用this
this->age = age;
}
Person& PersonAddPerson(Person& p)
{
this->age += p.age;
// 2.实现对象的链式引用,在类的非静态成员函数中返回对象本身
return *this;
}
public:
int age;
};
int main() {
Person p1(10);
Person p2(100);
// 链式编程思想
p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
cout << "p2.age = " << p2.age << endl; // 130
system("pause");
return 0;
}
常函数与常对象
常函数
- 常函数内不可以修改数据成员
- 声明数据成员时加关键字mutable后,在常函数中依然可以修改
常对象
- 常对象只能调用常函数
- 常对象可以访问数据成员,但是不能修改,但常对象可以修改关键字mutable修饰数据成员
#include <iostream>
using namespace std;
class Person {
public:
Person() :m_A(0), m_B(0) {}
// this指针:const Person * const this;
// 常函数
void const_func() const
{
// 常函数内不可以修改数据成员
//this->mA = 100;
// 常函数内可以修改mutable关键字修饰的数据成员
this->m_B = 100;
}
void func() {
m_A = 10000;
}
public:
int m_A;
mutable int m_B;
};
int main()
{
// 常对象可以访问成员变量,但不能修改
const Person person;
cout << person.m_A << endl;
//person.m_A = 100;
// 常对象可以修改mutable关键字修饰成员变量
person.m_B = 100;
// 常对象只能调用常函数
person.const_func();
// 常对象不能调用普通的成员函数
//person.func();
system("pause");
return 0;
}
静态成员变量与静态成员函数
静态成员变量
- 静态成员变量属于类,所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
#include <iostream>
using namespace std;
class Person
{
public:
// 静态成员变量
static int m_A;
// 普通成员变量
int m_C;
private:
// 静态成员变量也可以有访问权限的
static int m_B;
};
// 类内声明,类外初始化
int Person::m_A = 10;
int Person::m_B = 10;
int main()
{
// 静态成员变量两种访问方式
// 1、通过对象进行访问
Person p1;
p1.m_A = 100;
cout << "p1.m_A = " << p1.m_A << endl; // 100
Person p2;
p2.m_A = 200;
cout << "p1.m_A = " << p1.m_A << endl; // 200
cout << "p2.m_A = " << p2.m_A << endl; // 200
// 2、通过类名进行访问
cout << "m_A = " << Person::m_A << endl;
//cout << "m_B = " << Person::m_B << endl; // 类外访问不到私有静态成员变量
//cout << "m_C = " << Person::m_C << endl; // 非静态成员必须通过对象进行访问!
system("pause");
return 0;
}
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量,不能直接访问普通成员变量
- 静态成员函数没有this指针,不能直接访问普通变量成员
#include <iostream>
using namespace std;
class Person
{
public:
// 静态成员函数
static void func()
{
// 静态成员函数可以访问静态成员变量
m_A = 100;
// 静态成员函数不可以访问非静态成员变量
//m_B = 100;
}
// 静态成员变量
static int m_A;
// 非静态成员变量
int m_B;
private:
// 静态成员函数也是有访问权限的
static void func2() {}
};
// 类内声明,类外初始化
int Person::m_A = 10;
int main()
{
// 静态成员函数两种访问方式
// 1、通过对象访问
Person p1;
p1.func();
// 2、通过类名访问
Person::func();
//Person::func2(); // 私有权限访问不到
system("pause");
return 0;
}
空对象
C++规定空类对象大小至少为1字节,可以通过对象的内存地址区分各对象。
#include <iostream>
using namespace std;
class Empty {};
int main()
{
Empty e;
cout << sizeof(e) << endl; // 1
system("pause");
return 0;
}
C++的空类会有六个默认的函数:默认构造函数、 默认拷贝构造函数、 默认析构函数、 默认赋值运算符、取址运算符、取址运算符的const版本。
class Empty
{
public:
// 默认构造函数
Empty();
// 默认拷贝构造函数
Empty(const Empty&);
// 默认析构函数
~Empty();
// 默认赋值运算符
Empty& operator=(const Empty&);
// 取址运算符
Empty* operator&();
// 取址运算符的const版本
const Empty* operator&() const;
};
类对象作为类成员的构造与析构
- 先调用对象成员的构造函数,再调用本类构造函数
- 先调用本类的析构函数,再调用对象成员析构函数
#include <iostream>
using namespace std;
class Phone
{
public:
Phone(string name) {
m_PhoneName = name;
cout << "Phone构造函数调用!" << endl;
}
~Phone() {
cout << "Phone析构函数调用!" << endl;
}
public:
string m_PhoneName;
};
class Person
{
public:
// 隐式转换:Phone m_phone = Phone(pname);
Person(string name, string pName) :m_Name(name), m_Phone(pName) {
cout << "Person构造函数调用!" << endl;
}
~Person() {
cout << "Person析构函数调用!" << endl;
}
public:
string m_Name;
Phone m_Phone;
};
int main()
{
{
Person p("张三", "苹果");
}
system("pause");
return 0;
}
内联函数
内联函数:把内联函数里面的内容写在调用内联函数处,即不用执行进入函数的步骤,直接执行函数体。
- 在类中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。
- 内联函数对于编译器而言只是一个建议,编译器不一定会接受这种建议,即使没有声明内联函数,编译器可能也会内联一些小的简单的函数。
- 虚函数可以是内联函数,内联是在编译期内联,而虚函数的多态性体现运行期,当虚函数表现多态性的时候不能内联。
初始化列表
初始化列表用途
当类中出现下列成员时,只用构造函数的函数体无法完成变量的初始化
- 引用成员变量
- const成员变量
class Test
{
public:
Test(int ref) :m_const(100), m_ref(ref) {}
public:
// 常量
const int m_const;
// 引用
int& m_ref;
};
初始化列表好处
对象的创建分两步:
- 数据成员初始化
- 执行被调用构造函数体内的动作
// 在构造函数的函数体中“初始化”成员变量
#include <iostream>
using namespace std;
class Point
{
public:
Point() : m_x(0), m_y(0), m_z(0) {
cout << "默认构造函数," << "(" << m_x << "," << m_y << "," << m_z << ")" << endl;
}
Point(int x, int y, int z) : m_x(x), m_y(y), m_z(z) {
cout << "有参构造函数," << "(" << m_x << "," << m_y << "," << m_z << ")" << endl;
}
Point& operator= (const Point& p) {
m_x = p.m_x;
m_y = p.m_y;
m_z = p.m_z;
cout << "赋值运算符," << "(" << m_x << "," << m_y << "," << m_z << ")" << endl;
return *this;
}
public:
int m_x;
int m_y;
int m_z;
};
class Test
{
public:
// 在构造函数的函数体中“初始化”成员变量
Test(int a, Point p)
{
m_a = a;
m_p = p;
}
// 在初始化列表中初始化成员变量
//Test(int a, Point p) :m_a(a), m_p(p) {}
private:
int m_a;
Point m_p;
};
int main()
{
Point p(100, 200, 300);
Test t(0, p);
system("pause");
return 0;
}
在构造函数的函数体中“初始化”成员变量,对于Test类中的Point类型的数据成员,有两次成员函数的调用:默认构造函数,赋值运算符。在初始化列表中初始化成员变量,只会调用拷贝构造函数。
成员变量的初始化顺序
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
#include <iostream>
using namespace std;
class Test
{
public:
Test(int val) :m_a(val), m_b(m_a) {}
void print() {
cout << m_a << " " << m_b << endl;
}
private:
int m_b;
int m_a;
};
int main()
{
Test t(100);
t.print(); // 100 -858993460
system("pause");
return 0;
}
不要通过初始化列表初始化静态成员变量
静态成员变量最好类内声明,类外初始化。静态成员变量属于类,所有对象共享同一份数据。如果在类的内部进行定义,在建立多个对象时会多次声明和定义该变量的存储位置,在名字空间和作用域相同的情况下会导致重名的问题。
友元
全局函数作友元
#include <iostream>
using namespace std;
class Building
{
// 友元全局函数
friend void goodfriend(Building* building);
public:
Building() {
m_Sittingroom = "客厅";
m_Bedroom = "卧室";
}
string m_Sittingroom;
private:
string m_Bedroom;
};
void goodfriend(Building* building) {
cout << building->m_Sittingroom << endl;
cout << building->m_Bedroom << endl;
}
void stranger(Building* building) {
cout << building->m_Sittingroom << endl;
// 无法访问私有成员
//cout << building->m_Bedroom << endl;
}
类作友元
- 友元不可继承
- 友元是单向的,类A是类B的友元类,但类B不一定是类A的友元类
- 友元不具有传递性,类A是类B的友元类,类B是类C的友元类,但类A不一定是类C的友元类
#include <iostream>
using namespace std;
class Building
{
// 友元类
friend class Goodfriend;
public:
Building() {
m_Sittingroom = "客厅";
m_Bedroom = "卧室";
}
string m_Sittingroom;
private:
string m_Bedroom;
};
class Goodfriend
{
public:
void test(Building* building) {
cout << building->m_Sittingroom << endl;
cout << building->m_Bedroom << endl;
}
};
class Stranger
{
public:
void test(Building* building) {
cout << building->m_Sittingroom << endl;
// 无法访问私有成员
//cout << building->m_Bedroom << endl;
}
};
成员函数作友元
#include <iostream>
using namespace std;
// 前向声明
class Building;
class Person
{
public:
void test1(Building* building);
void test2(Building* building);
};
class Building
{
friend void Person::test1(Building* building);
public:
Building() {
m_Sittingroom = "客厅";
m_Bedroom = "卧室";
}
string m_Sittingroom;
private:
string m_Bedroom;
};
void Person::test1(Building* building)
{
cout << building->m_Sittingroom << endl;
cout << building->m_Bedroom << endl;
}
void Person::test2(Building* building) {
cout << building->m_Sittingroom << endl;
// 无法访问私有成员
//cout << building->m_Bedroom << endl;
}
函数重载与运算符重载
函数重载与运算符重载称为静态多态,表示函数地址早绑定,在编译阶段就确定地址。
函数重载
函数重载的条件
- 同一作用域下
- 函数名称相同
- 函数参数的类型不同或个数不同或顺序不同
函数的返回值不可以作为函数重载的条件!函数重载碰到函数默认参数产生二义性!
#include <iostream>
using namespace std;
// 引用作为重载条件
void func(int& a)
{
cout << "func (int &a) 调用 " << endl;
}
void func(const int& a)
{
cout << "func (const int &a) 调用 " << endl;
}
// 函数重载碰到函数默认参数
void func2(int a, int b = 10)
{
cout << "func2(int a, int b = 10) 调用" << endl;
}
void func2(int a)
{
cout << "func2(int a) 调用" << endl;
}
int main()
{
int a = 10;
func(a); // void func(int& a)
func(10); // void func(const int& a)
// 函数重载碰到函数默认参数产生二义性
// func2(10);
system("pause");
return 0;
}
运算符重载
运算符重载的目的是扩展C++中提供的运算符的适用范围,使之能作用于自定义的数据类型。运算符重载的实质是函数重载,可以重载为普通成员函数,也可以重载为成员函数。
运算符重载注意事项:
- 重载运算符(),[] ,->, =的时候,重载运算符的函数必须声明为类的成员函数
- 重载运算符<<,>>的时候,只能通过全局函数配合友元函数进行重载
- 不要重载&&和||运算符,因为无法实现短路原则
左移运算符重载
- 不用类的成员函数来重载左移运算符,因为无法实现cout在左侧
- ostream对象只能有一个,要使用引用的形式传递参数
#include <iostream>
using namespace std;
class Person {
// 友元
friend ostream& operator<<(ostream& cout, Person& p);
public:
Person(int a, int b) :m_A(a), m_B(b) {}
private:
int m_A;
int m_B;
};
ostream& operator<<(ostream& out, Person& p) {
out << "m_a:" << p.m_A << " m_b:" << p.m_B;
return out;
}
递增运算符重载
- 后置递增运算符重载需要添加一个占位参数,用来区分前置递增运算符
- 前置递增以引用方式返回,后置递增以值传递方式返回
#include <iostream>
using namespace std;
class MyInteger {
// 友元
friend ostream& operator<<(ostream& out, MyInteger myint);
public:
MyInteger() :m_Num(0) {}
// 前置递增运算符
MyInteger& operator++() {
m_Num++;
return *this;
}
// 后置递增运算符
MyInteger operator++(int) {
MyInteger temp = *this;
m_Num++;
return temp;
}
private:
int m_Num;
};
// 左移运算符重载
ostream& operator<<(ostream& out, MyInteger myint) {
out << myint.m_Num;
return out;
}
// 前置递增运算符测试
void test01() {
MyInteger myInt;
cout << ++myInt << endl; // 1
cout << myInt << endl; // 1
}
// 后置递增运算符测试
void test02() {
MyInteger myInt;
cout << myInt++ << endl; // 0
cout << myInt << endl; // 1
}
函数调用运算符重载(仿函数)
#include <iostream>
using namespace std;
class MyPrint
{
public:
void operator()(string text)
{
cout << text << endl;
}
};
int main()
{
MyPrint myFunc;
myFunc("hello world");
// 使用匿名对象
MyPrint()("hello world");
system("pause");
return 0;
}
继承
类成员的权限
- 公共权限(public):类内、类外都可以访问。
- 保护权限(protected):类内可以访问,类外不可以访问,子类可以访问父类的保护内容。
- 私有权限(private):类内可以访问。类外不可以访问,子类不可以访问父类的私有内容。
继承方式
- 公有继承(public)
父类中的public权限成员到子类中是public权限,父类中的protected权限成员到子类中是protected权限,父类中的private权限成员不可访问。 - 保护继承(protected)
父类中的public权限成员到子类中是protected权限,父类中的protected权限成员到子类中是protected权限,父类中的private权限成员不可访问。 - 私有继承(private)
父类中的public权限成员到子类中是private权限,父类中的protested权限成员到子类中是private权限,父类中的private权限成员不可访问。
注意:子类会继承父类的私有成员,只是被编译器隐藏,无法访问
#include <iostream>
using namespace std;
class Base
{
public:
int m_A; // 4bytes
float m_B; // 4bytes
private:
double m_C; // 8bytes
};
class Son :public Base
{
public:
int m_D; // 4bytes
};
int main()
{
// 内存字节对齐:4 4 8 4 4
cout << sizeof(Son) << endl; // 24
system("pause");
return 0;
}
继承下的构造和析构
- 子类创建对象时,先调用父类的构造函数,然后再调用自身的构造,析构顺序与构造顺序相反
- 子类会继承父类的成员属性和成员函数,但子类不会继承父类构造函数和析构函数
#include <iostream>
using namespace std;
class Base
{
public:
Base() { cout << "父类构造函数" << endl; }
~Base() { cout << "父类析构函数" << endl; }
};
class Son : public Base
{
public:
Son() { cout << "子类构造函数" << endl; }
~Son() { cout << "子类析构函数" << endl; }
};
int main()
{
{
Son s;
}
system("pause");
return 0;
}
继承中的同名处理
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类出现与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数,如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
#include <iostream>
using namespace std;
class Base
{
public:
Base() :m_A(100) {}
void func() { cout << "Base - func()调用" << endl; }
void func(int a) { cout << "Base - func(int a)调用" << endl; }
public:
int m_A;
};
class Son : public Base {
public:
Son() :m_A(200) {}
void func() { cout << "Son - func()调用" << endl; }
public:
int m_A;
};
int main()
{
Son s;
// 1.同名成员变量
// 访问子类同名属性成员 直接访问即可
cout << "Son下的m_A = " << s.m_A << endl; // 200
// 访问父类同名属性成员 需要加作用域
cout << "Base下的m_A = " << s.Base::m_A << endl; // 100
// 2.同名成员函数
// 访问子类同名属性函数 直接访问即可
s.func();
// 访问子类同名属性函数 需要加作用域
s.Base::func();
// 注意:函数重载也没用,需要加父类的作用域
//s.func(10);
s.Base::func(10);
system("pause");
return 0;
}
同名静态成员处理方式和非静态处理方式一样,可以通过对象和通过类名进行访问
#include <iostream>
using namespace std;
class Base {
public:
// 静态成员函数
static void func() { cout << "Base - static void func()" << endl; }
static void func(int a) { cout << "Base - static void func(int a)" << endl; }
// 静态成员变量
static int m_A;
};
// 类内声明,类外初始化
int Base::m_A = 100;
class Son : public Base {
public:
// 静态成员函数
static void func() { cout << "Son - static void func()" << endl; }
// 静态成员变量
static int m_A;
};
// 类内声明,类外初始化
int Son::m_A = 200;
// 1.同名静态成员变量
void test01()
{
Son s;
// (1)通过对象访问
cout << "Son 下 m_A = " << s.m_A << endl;
cout << "Base 下 m_A = " << s.Base::m_A << endl;
// (2)通过类名访问
cout << "Son 下 m_A = " << Son::m_A << endl;
// Son:: 通过类名的方式
// Base:: 访问父类的数据
cout << "Base 下 m_A = " << Son::Base::m_A << endl;
}
// 2.同名静态成员函数
void test02()
{
Son s;
// (1)通过对象访问
s.func();
s.Base::func();
// (2)通过类名访问
Son::func();
Son::Base::func();
// 注意:函数重载也没用,需要加父类的作用域
//Son::func(100);
Son::Base::func(100);
}
int main() {
test01();
test02();
system("pause");
return 0;
}
多继承
多继承:子类继承多个父类
#include <iostream>
using namespace std;
class Base1 {
public:
Base1() : m_A(100) {}
public:
int m_A;
};
class Base2 {
public:
Base2() : m_A(200) {}
public:
int m_A;
};
class Son : public Base2, public Base1
{
public:
Son() : m_C(300), m_D(400) {}
public:
int m_C;
int m_D;
};
int main()
{
Son s;
// 当父类中出现同名成员,需要加作用域区分
cout << s.Base1::m_A << endl;
cout << s.Base2::m_A << endl;
system("pause");
return 0;
}
菱形继承
菱形继承:两个子类继承于同一个父类,同时又有另外一个类多继承于两个子类。
菱形继承带来的问题:
- 羊驼类继承了两份动物类中的数据,只需要一份即可
- 从不同父类继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题
#include <iostream>
using namespace std;
// 动物类
class Animal
{
public:
int m_Age;
};
// 羊类
class Sheep : public Animal {};
// 驼类
class Tuo : public Animal {};
// 羊驼类
class SheepTuo : public Sheep, public Tuo {};
int main()
{
SheepTuo st;
st.Sheep::m_Age = 100;
st.Tuo::m_Age = 200;
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl; // 100
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl; // 200
//cout << "st.m_Age = " << st.m_Age << endl; // 对m_Age的访问不明确
system("pause");
return 0;
}
为了解决多继承时命名冲突和冗余数据的问题,C++提出了虚继承这个概念,虚继承可以使得在派生类中只保留一份间接基类的成员。虚继承的目的是让某个类做出声明,承诺愿意共享它的基类,这个被共享的基类就称为虚基类。
#include <iostream>
using namespace std;
// 虚基类
class Animal
{
public:
int m_Age;
};
// 利用虚继承解决菱形继承的问题
// 羊类
class Sheep : virtual public Animal {};
// 驼类
class Tuo : virtual public Animal {};
// 羊驼类
class SheepTuo : public Sheep, public Tuo {};
int main() {
SheepTuo st;
st.Sheep::m_Age = 100;
st.Tuo::m_Age = 200;
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl; //200
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl; //200
// 这份数据只有一份了
cout << "st.m_Age = " << st.m_Age << endl; // 200
system("pause");
return 0;
}
多态
动态多态的函数地址晚绑定,运行阶段确定函数地址。函数返回值类型、函数名、参数列表完全一致称为重写。
多态使用条件
- 有继承关系
- 子类重写父类中的虚函数
- 父类指针或引用指向子类对象
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void speak() { cout << "动物在说话" << endl; }
};
class Cat :public Animal
{
public:
// virtual关键字可写可不写
void speak() { cout << "小猫在说话" << endl; }
};
class Dog :public Animal
{
public:
void speak() { cout << "小狗在说话" << endl; }
};
void DoSpeak(Animal& animal)
{
animal.speak();
}
int main()
{
Animal animal;
DoSpeak(animal); // 动物在说话
Cat cat;
DoSpeak(cat); // 小猫在说话
Dog dog;
DoSpeak(dog); // 小狗在说话
system("pause");
return 0;
}
final关键字
C++ 中增加了 final 关键字来限制某个类不能被继承,或者某个虚函数不能被重写。如果使用 final 修饰函数,只能修饰虚函数,并且要把final关键字放到类或者函数的后面。
// final修饰虚函数
class Base
{
public:
virtual void test()
{
cout << "Base::test()";
}
};
class Child : public Base
{
public:
void test() final
{
cout << "Child::test()";
}
};
class GrandChild : public Child
{
public:
// 不允许重写
void test()
{
cout << "GrandChild::test()";
}
};
// final修饰类
class Base {};
class Child final : public Base {};
// 不允许继承
class GrandChild : public Child {};
override关键字
确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确表明将会重写基类的虚函数,这样就可以保证重写的虚函数的正确性,也提高了代码的可读性,关键字要写到虚函数后面。
class Base
{
public:
virtual void test()
{
cout << "Base::test()";
}
};
class Child : public Base
{
public:
void test() override
{
cout << "Child::test()";
}
};
class GrandChild : public Child
{
public:
void test() override
{
cout << "GrandChild::test()";
}
};
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
- 如果父类中出现了一个纯虚函数,则变为抽象类,抽象类不可实例对象
- 如果父类为抽象类,子类继承父类后,必须实现父类所有的纯虚函数,否则子类也为抽象类,也无法实例对象
- 但纯虚析构函数例外,因为子类不会继承父类的析构函数!
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。解决方式:将父类中的析构函数改为虚析构函数或者纯虚析构函数。纯虚析构函数需要提供函数的实现,而一般纯虚函数不能有实现。
#include <iostream>
using namespace std;
class Animal {
public:
Animal() :m_Name(new string) {}
Animal(string name) {
m_Name = new string(name);
}
virtual void Speak() { cout << "动物在说话!" << endl; }
// 虚析构函数
virtual ~Animal()
{
cout << "Animal虚析构函数调用!" << endl;
if (this->m_Name != nullptr) {
delete m_Name;
m_Name = nullptr;
}
}
public:
string* m_Name;
};
class Cat : public Animal {
public:
Cat(string name) {
m_Name = new string(name);
}
// 重写父类虚函数
virtual void Speak() {
cout << *m_Name << "猫在说话!" << endl;
}
// 析构函数
// 如果不给基类增加一个虚析构函数则不执行
~Cat()
{
cout << "Cat析构函数调用!" << endl;
if (this->m_Name != nullptr) {
delete m_Name;
m_Name = nullptr;
}
}
public:
string* m_Name;
};
int main()
{
Animal* animal = new Cat("Tom");
animal->Speak();
delete animal;
system("pause");
return 0;
}
为什么C++默认的析构函数不是虚函数?
因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。
构造函数能否是虚函数?
不能,因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针。
参考:https://www.bilibili.com/video/BV1et411b73Z
参考:https://github.com/twomonkeyclub/BackEnd/tree/master/