C++核心编程 day06 运算符重载、类的继承
1. 关系运算符重载
这里主要给出关于关系运算符中==
和!=
的示例代码。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person
{
public:
Person(string name, int age)
{
this->name = name;
this->age = age;
}
bool operator==(Person &p)
{
return this->name == p.name && this->age == p.age;
}
bool operator!=(Person &p)
{
return !(this->name == p.name && this->age == p.age);
}
string name;
int age;
};
int main()
{
Person p1("Jerry", 20);
Person p2("Jerry", 20);
Person p3("Jerrry", 20);
if (p1 == p2)
{
cout << "p1 == p2" << endl;
}
if (p1 != p3)
{
cout << "p1 != p3" << endl;
}
system("pause");
return 0;
}
2. 函数调用运算符重载
当我们进行函数调用运算符()
的重载的时候,我们可以将类的对象名,进行调用的时候实际上一个仿函数,并不是一个真正的函数。本质上是一个函数对象。在函数调用运算符重载的时候,我们习惯调用的时候创建匿名对象进行调用。因为匿名对象的特点是当行执行完就进行释放。关于函数调用运算符重载的示例代码如下。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include <string>
class MyPrint1
{
public:
void operator()(string str)
{
cout << str << endl;
}
};
void MyPrint2(string str)
{
cout << str << endl;
}
void test01()
{
MyPrint1 mp1;
mp1("I love you"); // 仿函数,本质是一个对象 函数对象
MyPrint2("I love you"); // 普通函数
}
class MyAdd
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
void test02()
{
MyAdd add;
cout << add(3, 2) << endl;
cout << MyAdd()(4, 9) << endl; // 匿名函数对象, 特点:当前行执行完立即释放
}
int main()
{
test01();
test02();
system("pause");
return 0;
}
3. 强化训练----字符串类封装
在这里我们需要完成对一个字符串类的封装。那么一个字符串的类会提供什么功能呢?我们可以实现一下的一些方法:
- 字符串的构造函数,默认无参构造函数,无参构造函数构造出来的是一个空字符串。有参构造函数,根据一个字符串构造出字节的字符串。拷贝构造函数。
- 字符串的析构函数,能够对字符串的空间进行释放。
- 通过重载关系运算符
==
和!=
能够判断两个字符串是相等还是不相等。 - 通过重载中括号运算符
[]
能够使用下标对字符串的某个位置的字符进行更改和访问。 - 通过重载加法运算法符
+
能够实现字符串的拼接操作。 - 通过重载左移运算符
<<
和右移运算符>>
实现使用cout
和cin
对字符串进行输出和输入。 - 能够获取当前字符串的长度。
下面我们使用类的声明与实现分离来实现上述的功能。
MyString.h
#ifndef __MYSTRING_H__
#define __MYSTRING_H__
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class MyString
{
//左移运算符重载
friend ostream &operator<<(ostream &cout, MyString &str);
//右移运算符重载
friend istream &operator>>(istream &cin, MyString &str);
public:
//无参构造
MyString();
//有参构造
MyString(const char *str);
//拷贝构造
MyString(const MyString &str);
//赋值运算符重载=
MyString& operator=(const char *str);
MyString& operator=(const MyString &str);
//==运算符重载
bool operator==(const MyString &str);
//!=运算符重载
bool operator!=(const MyString &str);
//重载中括号运算符
char &operator[](int index);
//重载加法运算符
MyString operator+(MyString &str);
MyString operator+(const char *str);
// 字符串长度
int length();
//析构函数
~MyString();
private:
char *pStr; //维护堆区开辟的数组
int size; //字符串的长度
};
#endif
MyString.cpp
#include "MyString.h"
//无参构造
MyString::MyString()
{
//cout << "无参构造调用" << endl;
this->pStr = new char[1];
strcpy(this->pStr, "");
this->size = 0;
}
//有参构造
MyString::MyString(const char *str)
{
//cout << "有参构造调用" << endl;
this->pStr = new char[strlen(str) + 1];
strcpy(this->pStr, str);
this->size = strlen(str);
}
//拷贝构造
MyString::MyString(const MyString &str)
{
cout << "拷贝构造调用" << endl;
this->pStr = new char[strlen(str.pStr) + 1];
strcpy(this->pStr, str.pStr);
this->size = str.size;
}
//赋值运算符重载=
MyString& MyString::operator=(const char *str)
{
//判断原来是否在堆区上有内存,有就释放
if (this->pStr != NULL)
{
delete[] this->pStr;
this->pStr = NULL;
}
this->pStr = new char[strlen(str) + 1];
strcpy(this->pStr, str);
this->size = strlen(str);
return *this;
}
MyString& MyString::operator=(const MyString &str)
{
//判断堆区是否有内容,有就释放
if (this->pStr != NULL)
{
delete[] this->pStr;
this->pStr = NULL;
}
this->pStr = new char[strlen(str.pStr) + 1];
strcpy(this->pStr, str.pStr);
this->size = str.size;
return *this;
}
//==运算符重载
bool MyString::operator==(const MyString &str)
{
return strcmp(this->pStr, str.pStr) == 0 && this->size == str.size;
}
//!=运算符重载
bool MyString::operator!=(const MyString &str)
{
return !this->operator==(str);
}
//重载中括号运算符
char& MyString::operator[](int index)
{
if (index < this->size && index >= 0)
{
return this->pStr[index];
}
cout << "索引位置不合法" << endl;
exit(-1);
}
//重载加法运算符
MyString MyString::operator+(MyString &str)
{
char *tmp = new char[strlen(this->pStr) + strlen(str.pStr) + 1];
strcpy(tmp, this->pStr);
strcat(tmp, str.pStr);
MyString tmpStr = tmp;
delete[] tmp;
return tmpStr;
}
MyString MyString::operator+(const char *str)
{
char *tmp = new char[strlen(this->pStr) + strlen(str) + 1];
strcpy(tmp, this->pStr);
strcat(tmp, str);
MyString tmpStr = tmp;
delete[] tmp;
return tmpStr;
}
// 字符串长度
int MyString::length()
{
return this->size;
}
//析构函数
MyString::~MyString()
{
if (this->pStr != NULL)
{
//cout << "析构函数调用" << endl;
delete[] this->pStr;
this->pStr = NULL;
}
}
//左移运算符重载
ostream &operator<<(ostream &cout, MyString &str)
{
cout << str.pStr;
return cout;
}
//右移运算符重载
istream &operator>>(istream &cin, MyString &str)
{
// 原来在堆区有空间则释放掉
if (str.pStr != NULL)
{
delete[] str.pStr;
str.pStr = NULL;
}
char tmp[4096];
cin >> tmp;
str.pStr = new char[strlen(tmp) + 1];
strcpy(str.pStr, tmp);
str.size = strlen(tmp);
return cin;
}
接下来对上述实现的MyString
类进行测试,测试代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include "MyString.h"
int main()
{
MyString str1("hello");
MyString str2;
MyString str3(str1);
cout << "str1 = " << str1 << " ,字符串长度为: " << str1.length() << endl;
cout << "str2 = " << str2 << " ,字符串长度为: " << str2.length() << endl;
cout << "str3 = " << str3 << " ,字符串长度为: " << str3.length() << endl;
str2 = str3 = "Good Morning";
cout << "str2 = " << str2 << " ,字符串长度为: " << str2.length() << endl;
cout << "str3 = " << str3 << " ,字符串长度为: " << str3.length() << endl;
for (int i = 0; i < str2.length(); i++)
{
cout << str2[i] << ' ';
}
cout << endl;
//不合法的索引位置
//str2[13] = 'K';
str2[5] = 'K';
cout << "str2 = " << str2 << " ,字符串长度为: " << str2.length() << endl;
MyString str4 = str1 + str2;
MyString str5 = str1 + "Jerry";
cout << "str4 = " << str4 << " ,字符串长度为: " << str4.length() << endl;
cout << "str5 = " << str5 << " ,字符串长度为: " << str5.length() << endl;
MyString str6;
cin >> str6;
cout << "str6 = " << str6 << " ,字符串长度为: " << str6.length() << endl;
if (str2 != str3)
{
cout << "str2 != str3" << endl;
}
str2 = str3;
if (str2 == str3)
{
cout << "str2 == str3" << endl;
}
system("pause");
return 0;
}
4. 继承的基本语法
为什么要有继承呢?假如我们有一个网页,上面是公共的头部,中间左侧是公共的左侧列表,右边是该网页的主要内容,最下面是公共的底部。现在有几个甚至更多的相同的网页,则上述中公共的头部、公共的左侧列表、公共的底部则需要重复书写,代码重复严重。如下:
class News
{
public:
void header()
{
cout << "公共的头部" << endl;
}
void footer()
{
cout << "公共的底部" << endl;
}
void leftList()
{
cout << "公共的左侧列表" << endl;
}
void content()
{
cout << "新闻播报..." << endl;
}
};
class Sport
{
public:
void header()
{
cout << "公共的头部" << endl;
}
void footer()
{
cout << "公共的底部" << endl;
}
void leftList()
{
cout << "公共的左侧列表" << endl;
}
void content()
{
cout << "世界杯赛况" << endl;
}
};
这段代码是比较重复严重的,因此我们可以使用继承去解决。通过继承机制,可以实现已有数据类型的定义来定义新的数据类型,使新的类型中不仅拥有旧类型中的成员,还有新类型中的成员,很好的实现了代码的复用。
继承的语法是class 类名1 : 继承方式 类名2
。其中类名1
是新类型,类名2
是旧类型。我们把旧的类成为父类或者基类,把新的类成为子类或者派生类。关于上述的代码使用继承来实现如下:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
//class News
//{
//public:
// void header()
// {
// cout << "公共的头部" << endl;
// }
//
// void footer()
// {
// cout << "公共的底部" << endl;
// }
//
// void leftList()
// {
// cout << "公共的左侧列表" << endl;
// }
//
// void content()
// {
// cout << "新闻播报..." << endl;
// }
//};
//
//class Sport
//{
//public:
// void header()
// {
// cout << "公共的头部" << endl;
// }
//
// void footer()
// {
// cout << "公共的底部" << endl;
// }
//
// void leftList()
// {
// cout << "公共的左侧列表" << endl;
// }
//
// void content()
// {
// cout << "世界杯赛况" << endl;
// }
//};
//利用继承模拟网页
//继承优点:减少重复代码,提高代码复用性
class BasePage
{
public:
void header()
{
cout << "公共的头部" << endl;
}
void footer()
{
cout << "公共的底部" << endl;
}
void leftList()
{
cout << "公共的左侧列表" << endl;
}
};
//语法: class 子类 : 继承方式 父类
//News 子类 派生类
//BasePage 父类 基类
class News : public BasePage
{
public:
void content()
{
cout << "新闻播报..." << endl;
}
};
class Sport : public BasePage
{
public:
void content()
{
cout << "世界杯赛况" << endl;
}
};
int main()
{
News news;
cout << "新闻页面内容如下: " << endl;
news.header();
news.leftList();
news.content();
news.footer();
Sport sport;
cout << "体育页面内容如下: " << endl;
sport.header();
sport.leftList();
sport.content();
sport.footer();
system("pause");
return 0;
}
5. 继承方式
继承方式主要有三种,分别是public
(公有继承)、protected
(保护继承)和private
(私有继承)。
在公有继承之中,父类中的公共权限在子类中仍是公共权限;父类中的保护权限在子类中仍是保护权限;父类中的私有权限子类不能进行访问。
在保护继承中,父类中的公共权限在子类会变成保护权限;父类中的保护权限在子类中仍然是保护权限;父类中的私有权限子类不能进行访问。
在私有继承中,父类中的公共权限和保护权限在子类中都会变成私有权限;父类中的私有权限子类不能进行访问。
关于继承方式的示例代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
///公共继承//
class Base1
{
public:
int a;
protected:
int b;
private:
int c;
};
class Son1 : public Base1
{
public:
void func()
{
a = 100; //父类中公共权限子类中变为公共权限
b = 100; //父类中保护权限子类中变为保护权限
//c = 100; //父类中私有成员子类无法访问
}
};
void test01()
{
Son1 son1;
son1.a = 100; //在Son1中a是公共权限类外可以访问
//son1.b = 100; //在Son1中b是保护权限类外不可以访问
}
/保护继承//
class Base2
{
public:
int a;
protected:
int b;
private:
int c;
};
class Son2 : protected Base2
{
public:
void func()
{
a = 100; //父类中公共权限子类中变为保护权限
b = 100; //父类中保护权限子类中变为保护权限
//c = 100; //父类中私有成员子类无法访问
}
};
void test02()
{
Son2 son2;
//son2.a = 100; //子类中保护权限无法访问
//son2.b = 100; //子类中保护权限无法访问
//son2.c = 100; //父类中私有成员子类无法访问
}
/私有继承//
class Base3
{
public:
int a;
protected:
int b;
private:
int c;
};
class Son3 : private Base3
{
public:
void func()
{
a = 100; //父类中公共权限子类中变为私有权限
b = 100; //父类中保护权限子类中变为私有权限
//c = 100; //父类中私有成员子类无法访问
}
};
void test03()
{
Son3 son3;
//son3.a = 100; //在Son3中变为私有权限,类外访问不到
//son3.b = 100; //在Son3中变为私有权限,类外访问不到
}
int main()
{
test01();
test02();
test03();
system("pause");
return 0;
}
6. 继承中的对象模型
接下来我们需要讨论的是如果父类的某些属性被定义成了私有权限,那么子类进行继承,则该类的对象的大小占用多少个字节呢?例如下面的代码:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Base
{
public:
int m_A;
protected:
int m_B;
private:
int m_C; // 父类中私有属性,子类访问不到,是由编译器给隐藏了
};
class Son : public Base
{
public:
int m_D;
};
int main()
{
Son son;
cout << sizeof son << endl;
system("pause");
return 0;
}
那么上面代码中的son
占用多少个字节呢?父类Base
中有三个成员变量,Son
类中也有一个成员变量,所以该类占16个字节。而因为m_C
是父类中的私有权限,所有继承的时候会被编译器给隐藏起来,无法进行访问。我们可以使用VS中的开发者的工具去查看下类的布局。
首先打开VS安装路径中的Microsoft Visual Studio 12.0\Common7\Tools\Shortcuts
,在该路径中可以看到有个开发人员命令提示
。
打开该命令提示,切换到我们的编写的C语言的路径之中。
使用cl /d1 reportSingleClassLayout类名 文件名
就可以查看类的布局。比如现在查看Son
类的布局就输入cl /d1 reportSingleClassLayoutSon main.cpp
。
所有该对象的确是占16
字节的。
7. 继承中的构造与析构
在C++的继承中,如果一个类继承了另一个类,则创建对象的时候会先调用父类的构造函数,再调用本类自身的构造函数。若本类中有其他类的对象作为成员,则调用完父类的构造函数之后会调用其它类的构造函数,最后再调用自身的构造函数。其析构函数的调用过程是与其它类正好相反。当我们需要调用父类的某些含参构造方法的时候,这时候需要结合初始化列表的语法去显式地调用父类的构造函数。代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Base1
{
public:
Base1()
{
cout << "Base1的构造函数调用" << endl;
}
~Base1()
{
cout << "Base1的析构函数调用" << endl;
}
};
class Other
{
public:
Other()
{
cout << "Other的构造函数调用" << endl;
}
~Other()
{
cout << "Other的析构函数调用" << endl;
}
};
class Son1 : public Base1
{
public:
Son1()
{
cout << "Son1的构造函数调用" << endl;
}
~Son1()
{
cout << "Son1的析构函数调用" << endl;
}
Other other;
};
void test01()
{
Son1 son1; // 先调用父类的构造,再调用其他成员构造,再调用自身构造。析构的顺序与此顺序相反
}
class Base2
{
public:
Base2(int a)
{
this->m_A = a;
cout << "Base2的构造函数调用" << endl;
}
int m_A;
};
class Son2 : public Base2
{
public:
Son2(int a = 100) :Base2(a) // 利用初始化列表语法,显示调用父类中的的其他构造函数
{
cout << "Son2的构造函数调用" << endl;
}
};
void test02()
{
Son2 son2;
cout << son2.m_A << endl;
}
// 父类中构造、析构、拷贝构造、operator= 是不会被子类继承下去的
int main()
{
test01();
test02();
system("pause");
return 0;
}
8. 继承中的同名成员处理
当子类成员和父类成员同名的时候,子类依然会继承父类的同名成员。但是子类和父类同名,编译器会默认优先访问子类中的成员,也就是就近原则。而我们需要调用父类中的同名的成员,则需要借助作用域运算符::
才行。示例代码如下。而当我们重写了父类的某些成员函数,父类的成员函数会被编译器隐藏掉,也需要借助作用域运算符显式调用才行。
#define _CRT_SECURE_NOW_WARNINGS
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
this->m_A = 10;
}
void func()
{
cout << "Base中的func调用" << endl;
}
void func(int a)
{
cout << "Base中的func(int)调用" << endl;
}
int m_A;
};
class Son : public Base
{
public:
Son()
{
this->m_A = 20;
}
void func()
{
cout << "Son中的func调用" << endl;
}
int m_A;
};
void test01()
{
Son son1;
cout << "son1.m_A = " << son1.m_A << endl;
// 我们可以利用作用于来访问父类中的同名成员
cout << "Base中的m_A = " << son1.Base::m_A << endl;
}
void test02()
{
Son son1;
son1.func();
son1.Base::func(10);
// 当子类重新定义了父类中同名成员函数,子类的成员函数会隐藏掉父类所有重载版本的同名成员函数,可以利用作用域显式指定调用
}
int main()
{
test01();
test02();
system("pause");
return 0;
}
9. 继承中的同名静态成员处理
静态成员也是可以继承到子类之中的。如果静态成员变量或者函数的名字子类与父类是同名的,则其父类中的会被编译器隐藏掉,这时候需要借助作用域运算符才能实现访问或者调用。可以通过对象名加作用域去显示地调用,也可以通过类名加作用域去显示调用,示例代码如下。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Base
{
public:
static void func()
{
cout << "Base中的func调用" << endl;
}
static void func(int a)
{
cout << "Base中的func(int a)调用" << endl;
}
static int m_A;
};
int Base::m_A = 10;
class Son : public Base
{
public:
static void func()
{
cout << "Son中的func调用" << endl;
}
static int m_A;
};
int Son::m_A = 20;
void test01()
{
// 1. 通过对象调用
Son son;
cout << "m_A = " << son.m_A << endl;
cout << "Base中的m_A = " << son.Base::m_A << endl;
// 2.通过类名访问
cout << "m_A = " << Son::m_A << endl;
// 通过类名的方式访问父类作用域下的m_A静态成员变量
cout << "Base中的m_A = " << Son::Base::m_A << endl;
}
void test02()
{
// 1. 通过对象
Son son;
son.func();
son.Base::func();
// 2. 通过类名
Son::func();
// 当子类重定义父类中同名成员函数,子类的成员函数会隐藏掉父类中所有版本,需要加作用域调用
Son::Base::func(4);
}
int main()
{
test01();
test02();
system("pause");
return 0;
}
10. 多继承基本语法
在C++中提供了多继承。多继承的语法如下:
class 类名 : 继承方式 父类1, 继承方式 父类2, ...
{
}
多个类的继承中可能会导致成员函数名或者成员变量名等同名。那么怎么访问呢?此时也需要借助作用域运算符。多继承示例代码如下。
#define _CRT_SECUER_NO_WARNINGS
#include <iostream>
using namespace std;
class Base1
{
public:
Base1()
{
this->m_A = 10;
}
int m_A;
};
class Base2
{
public:
Base2()
{
this->m_A = 20;
}
int m_A;
};
// 多继承
class Son : public Base1, public Base2
{
public:
int m_C;
int m_D;
};
int main()
{
cout << "sizeof Son = " << sizeof(Son) << endl;
Son s;
// 当多继承的两个父类中有同名成员,需要加作用域区分
cout << s.Base1::m_A << endl;
cout << s.Base2::m_A << endl;
system("pause");
return 0;
}
11. 菱形继承问题及解决
有时候我们会遇到这样的一个情况,比如有一个动物类Animal
,其中有两个类羊类Sheep
和驼类Camel
都分别继承Animal
类。而我们知道这两个结合会产生羊驼,也就是最后一个类草泥马类Alpaca
。这样就会形成一个菱形的继承关系。这样的继承关系会导致我们的草泥马类中调用继承来的成员函数或者是成员变量都会产生歧义,也就是有了二义性。而且草泥马类中来自动物类的数据和函数都继承了两份,而我们只需要一份就行。
对于上述问题的解决,C++中采用了虚基类来解决,让羊类和驼类都虚继承于动物类。这样羊类和驼类中保存的是一个指针,这个指针在下面会细说。而草泥马中的数据就不会再重复。关于代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Animal
{
public:
int m_Age; // 年龄
};
// Animal称为虚基类
// 羊类
class Sheep : virtual public Animal {};
// 驼类
class Camel : virtual public Animal {};
// 羊驼类
class Alpaca : public Sheep, public Camel{};
void test01()
{
Alpaca alpaca;
alpaca.Sheep::m_Age = 10;
alpaca.Camel::m_Age = 20;
cout << "Sheep::m_Age = " << alpaca.Sheep::m_Age << endl;
cout << "Camel::m_Age = " << alpaca.Camel::m_Age << endl;
cout << "age = " << alpaca.m_Age << endl;
// 当发生虚继承后,Sheep和Camel类中继承了一个vbptr指针,虚基类指针,指向的是一个虚基类表vbtable
// 虚基类表中记录了偏移量,通过偏移量可以找到唯一的一个m_Age
}
void test02()
{
Alpaca alpaca;
alpaca.m_Age = 100;
// 通过Sheep找到偏移量
// *(int *)&alpaca 解引用到了虚基类表中
cout << *((int *)*(int *)&alpaca + 1) << endl;
cout << *((int *)*((int *)&alpaca + 1) + 1) << endl;
// 通过偏移量去访问m_Age
cout << ((Animal *)((char *)&alpaca + *((int *)*(int *)&alpaca + 1)))->m_Age << endl;
cout << *(int *)((char *)&alpaca + *((int *)*(int *)&alpaca + 1)) << endl;
cout << *(int *)((char *)((int *)&alpaca + 1) + *(((int *)*((int *)&alpaca + 1)) + 1)) << endl;
}
int main()
{
test01();
test02();
system("pause");
return 0;
}
我们来看一下关于草泥马类的内存模型,如下。
发生虚继承之后,Sheep
类和Camel
类中都继承了一个vbptr
指针,这个指针成为虚基类指针。虚基类指针指向vbtable
,也就是虚基类表。虚基类表中记录了偏移量,通过偏移量就可以找到唯一的成员变量。
在这个内存模型中,我们可以看到成员变量偏移0
个字节就是Sheep
类的虚基类指针;偏移4
字节就是Camel
类中的虚基类指针;偏移8
个字节就是Animal
类的基地址,也是存放m_Age
成员变量的地址。第一个虚基类指针指向的是虚基类表Alpaca::$vbtable@Sheep@
,从表中可以看出来只需要偏移8
个字节就是Animal
的基地址。第二个虚基类指针指向的是Alpaca::$vbtable@Camel@
,从表中可以看出只需要偏移4
个字节就是Animal
的基地址。
接下来我们来解释一下 ((Animal *)((char *)&alpaca + *((int *)*(int *)&alpaca + 1)))->m_Agel
、*(int *)((char *)&alpaca + *((int *)*(int *)&alpaca + 1))
、*(int *)((char *)((int *)&alpaca + 1) + *(((int *)*((int *)&alpaca + 1)) + 1))
究竟是什么意思。这三个都是访问的是成员变量m_Age
。
首先第一个 ((Animal *)((char *)&alpaca + *((int *)*(int *)&alpaca + 1)))->m_Agel
。&alpaca
是该对象的地址,由内存模型可以知道偏移0
个字节就是Sheep
类的虚基类指针,所以*(int *)&alpaca
就是表示虚基类表。而在Sheep
的虚基类表中可以看出来偏移8
个字节就是Animal
的基地址,所以(int *)*(int *)&alpaca + 1
就是虚基类表中的偏移量,也就是从&alpaca
偏移了8
字节,这个的结果就是8
。根据虚基类表中的偏移量,再加上我们的基地址就可以得到Animal
类的基地址。这样(char *)&alpaca + *((int *)*(int *)&alpaca + 1)
得到的就是Animal
的基地址,这里我们就可以有两种选择,第一个是强转类型为Animal
类的指针类型使用成员运算符->
就可以访问m_Age
。或者另一种方式是既然已经知道了m_Age
是int
类型,那么我们可以将该地址强制类型转换为int *
的类型再解引用访问的也是m_Age
。这也就是第二个的思路。
理解了前两个第三个也就不难理解了,第三个实际上是根据Camel
类的虚基类指针和虚基类表去进行访问的。