is-a
关系注:以下代码没实际意义,仅仅为了学习写的
公有派生将继承父类的所以数据成员和方法,Point
类来自上一篇博客:
class Point {
private:
int x;
int y;
friend ostream& operator<<(ostream& out, const Point &p);
friend Point operator*(const Point& p, int n);
friend Point operator*(int n, const Point& p);
public:
Point() { cout << "parent" << endl; x = 0; y = 0; }
Point(int x, int y) { cout << "parent agrv" << endl; this->x = x; this->y = y; }
~Point() { cout << "parent 析构" << endl; };
Point operator+(const Point& p) const;
Point operator*(const Point& p) const;
Point operator++(); // 前置++
Point operator++(int); // 后置++
void show() const;
};
class Child: public Point {
private:
float x;
float y;
public:
Child() { cout << "child" << endl; x = 2.0; y = 2.0; }
Child(float x, float y) { cout << "child agrv" << endl; this->x = x; this->y; }
Child(int x, int y, float fx, float fy) : Point(x, y) { cout << "child agrv" << endl; this->x = fx; this->y = fy; }
~Child() { cout << "child 析构" << endl; };
};
int main()
{
Child cp(1, 2, 5.0, 6.0);
long long* ptr = (long long*)&cp;
cout << hex;
cout << "0x" << sizeof(cp) << ": " << ptr << endl;
cout << dec;
float fx = *(float*)(ptr+1);
cout << "fx = " << fx << endl;
cout << cp << endl;
cp.show();
return 0;
}
输出结果如下:
parent agrv
child agrv
0x10: 0x7ffeefe5ad10
fx = 5
(1, 2)
(1, 2)
child 析构
parent 析构
注意点:
可以看到代码中我把 cp
的大小和地址打印出来了,我们调试看下:
根据输出知道 cp
的大小为 0x10
,地址为 0x7fffffffe250
,可以看到这里其实存储的就是类的数据成员,前8字节是基类的数据成员,后8字节是派生类的数据成员。
这里也验证了类实例其实只包含自己的数据成员(static const
除外),类方法是属于类的。
简单来说多态就:对象决定方法,其实就是重写基类方法
这里利用到了虚函数:virtual
关键字进行声明
这里对上面代码稍做修改:
class Point {
private:
int x;
int y;
friend ostream& operator<<(ostream& out, const Point &p);
friend Point operator*(const Point& p, int n);
friend Point operator*(int n, const Point& p);
public:
Point() { cout << "parent" << endl; x = 0; y = 0; }
Point(int x, int y) { cout << "parent agrv" << endl; this->x = x; this->y = y; }
virtual ~Point() { cout << "parent 析构" << endl; };
Point operator+(const Point& p) const;
Point operator*(const Point& p) const;
Point operator++(); // 前置++
Point operator++(int); // 后置++
virtual void show() const;
};
class Child: public Point {
private:
float x;
float y;
public:
Child() { cout << "child" << endl; x = 2.0; y = 2.0; }
Child(float x, float y) { cout << "child agrv" << endl; this->x = x; this->y; }
Child(int x, int y, float fx, float fy) : Point(x, y) { cout << "child agrv" << endl; this->x = fx; this->y = fy; }
~Child() { cout << "child 析构" << endl; };
virtual void show() const;
};
int main()
{
Point pp(10, 5);
Child cp(1, 2, 5.0, 6.0);
Point& p1 = pp;
Point& p2 = cp;
long long* ptr = (long long*)&cp;
cout << hex << "0x" << sizeof(cp) << ": " << ptr << dec << endl;
cout << cp << endl;
p1.show();
p2.show();
return 0;
}
void Point::show() const
{
cout << "(x, y) = (" << x << ", " << y << ")\n";
}
void Child::show() const
{
Point::show();
cout << "(fx, fy) = (" << x << ", " << y << ")\n";
}
输出如下:
parent agrv
parent agrv
child agrv
0x18: 0x7fff68a99bb0
(1, 2)
(x, y) = (10, 5)
(x, y) = (1, 2)
(fx, fy) = (5, 6)
child 析构
parent 析构
parent 析构
可以看到这里输出的 cp
的大小变成了 0x18
,这是咋回事呢?先来 IDA 里面看看:
可以看到最后调用 p2.show()
时有点奇怪,看下 cp
类的结构:
可以看到这里的类实际不仅保存了数据成员,还保存着一个虚指针,该虚指针指向虚表,这里我们在对代码改一改:
int main()
{
Child cp1(1, 2, 5.0, 6.0);
Child cp2(3, 4, 6.0, 7.0);
Point& p1 = cp1;
Point& p2 = cp2;
long long* ptr = (long long*)&cp1;
cout << hex << "0x" << sizeof(cp1) << ": " << ptr << dec << endl;
ptr = (long long*)&cp2;
cout << hex << "0x" << sizeof(cp2) << ": " << ptr << dec << endl;
p1.show();
p2.show();
return 0;
}
调试可以知道,不同对象实例指向的虚表是相同的,当然这也是当然的,因为之前说过,类方法属于类,而不属于类实例:
再改一改:
int main()
{
Point pp1(1, 2);
Child cp2(3, 4, 6.0, 7.0);
Point& p1 = pp1;
Point& p2 = cp2;
long long* ptr = (long long*)&pp1;
cout << hex << "0x" << sizeof(pp1) << ": " << ptr << dec << endl;
ptr = (long long*)&cp2;
cout << hex << "0x" << sizeof(cp2) << ": " << ptr << dec << endl;
p1.show();
p2.show();
return 0;
}
从调试结果知道,所有的虚函数都放在同一个虚表中,不同的类对象(有继承关系)指向的是不同的位置,比如这里的 Point
和 Child
指向的就是不同的位置:
这里只是有个印象,关于虚表的更多知识,在后面会进行解析
当类包含纯虚函数时,该类只能用作基类,不能创建实例对象
纯虚函数:virtual func() = 0;
,即函数声明最后加上 = 0
即可
todo
在上面,我们讲到了虚函数,其主要用来实现多态,当类中包含虚函数时,实例对象的头8字节则包含一个虚表指针。考虑如下代码:
#include <iostream>
using std::cout;
using std::endl;
class A {
private:
long x;
double y;
public:
A() { x = 10; y = 1.; }
virtual void foo() const;
virtual void bar() const;
};
int main()
{
A a;
cout << std::hex << &a << std::dec << endl;
return 0;
}
void A::foo() const {}
void A::bar() const {}
直接放进 IDA 看下:
可以看到构造函数中多了一段对 _vptr_A
赋值的代码,这其实是编译器做的:
虚表被放到了 data
段上,并且可以看到其实其前面还有 0x10 的数据,接下来看下实例对象:
可以看到实例对象的头8字节就是虚表指针,其指向的是虚表的对应函数的位置,并不是虚表头部,调试也可以看出来:
这里可以看到如果我们可以修改实例对象的虚表指针指向我们伪造的虚表,那么如果程序要调用虚表中的函数时就会去执行我们伪造的函数了。
那么什么时候执行虚函数会去找虚表呢?基类指针或引用操作派生对象
考虑如下代码:
#include <iostream>
#include <cstring>
using std::cout;
using std::endl;
class A {
private:
long x;
double y;
public:
A() { x = 10; y = 1.; }
virtual void foo() const;
virtual void bar() const;
~A() {}
};
class B : public A {
public:
virtual void foo() const;
void bar() const;
};
void hacker()
{
puts("Hacker");
}
int main()
{
long long* ptr = new long long[0x100];
ptr[0] = (long long)hacker;
ptr[1] = (long long)hacker;
A a;
cout << std::hex << &a << std::dec << endl;
a.bar();
A* pb = new B;
cout << std::hex << &pb << std::dec << endl;
*(long long*)pb = (long long)ptr;
pb->bar();
return 0;
}
void A::foo() const {}
void A::bar() const {}
void B::foo() const {}
void B::bar() const {}
输出:这里成功劫持了虚表
0x7ffe6d3fca60
0x7ffe6d3fca50
Hacker