内存分为:全局静态存储区,代码区,常量存储区,堆,栈。其中全局静态存储区存放全局变量和所有静态变量(类成员和外部定义的静态变量)。一切函数实现存放在代码区,栈存放函数体内部的局部变量,函数参数,返回值。
成员变量:
静态成员变量存在全局静态存储区,不占用对象内存。
非静态成员变量存储于对象内存,还要加上对齐的字节。
成员函数:
非静态成员函数:函数参数列表中有默认的指向对象指针的参数,调用时会把this指针传入,所以在函数内部可以访问对象的成员变量和函数。
静态成员函数:没有this指针参数,所以静态函数内部只能调用类的静态成员变量以及静态成员函数。
虚函数:函数实现依旧存于代码区,但是函数地址存在于虚函数表(存放于全局静态存储区)中,对象中会存在虚表指针指向虚函数表。
综上:对象内存布局为:虚函数表(或无)–>非静态成员变量–>对齐字节,表示存储位置的顺序。这里的虚函数表实际上是虚表指针,只有虚表指针存在内存中,以下同理。
而对于非虚函数,编译器在编译时期会把调用函数的地方直接换成函数地址。如:
class B
{
public:
void f() { cout << "B::f" << endl; }
}
int main()
{
B* b = NULL;
b->f(); //B::f 在编译器这段代码被换成f()函数在代码区的地址。
return 0;
}
编译器查找名称是由内而外的,先查找子类没有该函数定义就查找基类,如果是多继承,而且多个基类恰好有同名函数定义,那么调用就会出错,可以通过指定基类作用域的方式来调用。
class A {
public:
void func()
{
cout << "a";
}
};
class A1 {
public:
void func()
{
cout << "a1";
}
};
class B :public A, public A1 {
};
int main(int argc, char** argv)
{
B b;
b.func();//报错
b.A::func();
b.A1::func();
system("pause");
}
对于单继承来说对象内存分布为:基类虚函数表(或无)–>基类成员变量–>派生类成员变量–>对齐字节。
继承之后如果基类有虚函数表,那么子类的虚函数地址就保存在基类虚函数表中,子类中不单独设计虚函数表。
(1)下面看多继承的例子。
class B
{
public:
virtual void f() {cout << "B::f" << endl; }
virtual void g1(){cout<< "B::g""<<endl;}
int a;
};
class C {
public:
virtual void f() { cout << "C::f" << endl; }
virtual void g2(){cout<< "C::g""<<endl;}
int a;
};
class D :public B, public C
{
public:
void f() { cout << "D::f" << endl; }
virtual void g(){cout<< "D::g""<<endl;}
};
D d;
d.a;//访问存在二义性,是B还是C的a。只能用作用域运算符显性指定。d.B::a;相当于d对象中的A部分。
子类对象中内存为:虚函数表1->基类1成员变量->虚函数表2->基类2成员变量->子类的成员变量。也就是说子类中有两个虚表指针分别指向两个父类的虚函数表。对于子类单独定义的虚函数会保存在第一个虚函数表中,这里的虚函数表存储的先后位置是在类声明时决定的class D: public B, public C所以第一个虚函数表是B类的虚函数表。
如果在D类中没有覆盖f()方法的话,虚函数表如下
D::vftable@B
&B::f |
---|
&B::g1 |
&D::g |
D::vftable@C
&C::f |
---|
&C::g2 |
当子类中覆盖了f()方法时,虚函数表:
D::vftable@B
&D::f |
---|
&B::g1 |
&D::g |
D::vftable@C
&D::f |
---|
&C::g2 |
也就是会用子类的函数地址替换掉所有重名父类的函数地址,这也是实现多态的关键。这样的话父类指针用子类对象实例化后调用虚函数,就只能调用子类中的实现,因为在子类对象的虚函数表中父类虚函数地址都被覆盖掉了。
如果两个基类中存在同名的成员函数或同名成员变量,而且派生类没有重新覆盖,那么派生类对象在访问基类的同名成员函数/变量时就存在二义性,到底调用哪个基类呢?当然覆盖的话就不会出现了。
(2)菱形继承
B,C类均继承A类,而Dlei多继承于B,C就形成菱形继承。
class A {
public:
virtual void f() { cout << "A::f" << endl; }
int a;
};
class B: public A
{
public:
virtual void f() {cout << "B::f" << endl; }
int a;
};
class C : public A {
public:
virtual void f() { cout << "C::f" << endl; }
};
class D :public B, public C
{
public:
virtual void f() {
cout << "D::f" << endl;
}
};
int main()
{
D d;
d.f();
return 0;
}
内存分布: A虚表–>A成员变量–>B成员变量 -->A虚表–>A成员变量–>C成员变量 -->D成员变量
所以不管继承多少层都只保存最顶端基类的虚函数表,但多路继承时每路对象都保存一份基类对象内存,造成重复存储,这是多路继承的问题,单路继承就没有这个问题。
同时菱形继承也具备上述二义性。
(3)解决方案-- 虚继承
在继承方式前加上virtual表示虚继承,每个虚继承的派生类都会有一个虚基表指针vbptr,指向虚基表,虚基表中存放的是基类成员变量的相对内存地址。也就是把原来存放基类变量的地方换成了vbptr。
虚继承时基类的内存存放在派生类内存之后。所以分析多层继承关系时先分析一般继承的内存:基类内存在对象开始位置;分析虚继承:积累内存在对象内存结束位置,多层虚继承时对象内存从顶向下分析。
class Base
{
public:
Base (int a = 1):base(a){}
void fun0(){cout << base << endl;}
int base;
};
class Base1: virtual public Base
{
public:
Base1 (int a = 2):base1(a){}
void fun1(){cout << base1 << endl;}
int base1;
};
class Base2:virtual public Base
{
public:
Base2 (int a = 3):base2(a){}
void fun2(){cout << base2 << endl;}
int base2;
};
class Derive: virtual public Base1, virtual public Base2
{
public:
Derive (int value = 4):derive (value){}
void fun3(){cout << derive << endl;}
int derive;
};
如果D一般继承B,C那么
参考:https://www.cnblogs.com/longcnblogs/archive/2017/10/09/7642951.html
因为基类的private成员变量和成员函数只能在当前类中使用,其派生类无法使用,所以只讨论基类public,protected成员在继承后的访问权限。使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
public继承后,基类成员的访问权限不变。
protected继承,成员访问均变为protected。基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected.可 以看出保护成员限定符是因继承才出现的 。
private继承,成员访问均变为private。
继承方式 | 基类public成员 | 基类protected成员 | 基类private成员 |
---|---|---|---|
public继承 | 不变 | 不变 | 不可见 |
protected继承 | protected | protected | 不可见 |
private继承 | private | private | 不可见 |
静态成员的继承:
因为静态成员变量存放于静态存储区,所以派生类可以继承但要受到上述访问权限的约束,而且派生类和基类的成员变量共用同一块内存,这在使用静态成员做引用计数时要注意。一般类中用于引用计数的静态成员要设置为private,以防派生类对其进行修改。
静态成员函数同理,派生类和基类的成员函数地址相同,shou访问权限约束。
class A {
public:
static void func()
{
cout << "a";
}
};
class B :public A {
};
int main(int argc, char** argv)
{
B b;
cout << B::func << endl;
cout << A::func << endl;//两者地址相同。注意只有非静态成员函数取地址加&。
system("pause");
}