您的位置:首页 > 编程语言 > C语言/C++

C++继承、多重继承与虚基类 继承、多重继承时对象内存分布

2013-03-14 14:19 344 查看

类对象内存模型

类是对属性和行为的封装,在类的对象中也有属性(成员变量)和行为(成员函数),反映到类对象的内存模型中,也就是内存中应该有对象的成员变量和成员函数。在同一个类的所有对象的成员函数,并不是每个对象有一份成员函数,而是共有一份相同的成员函数。例如下面一个类:

[cpp]
view plaincopyprint?

class Base { //行为 public: void f001(void){}; void f002(void){}; //属性 public: double m_fMenber1; int m_nMember2; };

class Base
{
//行为
public:
void f001(void){};
void f002(void){};
//属性
public:
double m_fMenber1;
int m_nMember2;
};


[cpp]
view plaincopyprint?

typedef void (Base::*CLASS_FUNC)(void); void main() { Base base; cout<<"类对象地址"<<&base<<endl; cout<<"类成员地址"<<&(base.m_fMenber1)<<endl; cout<<"类成员地址"<<&(base.m_nMember2)<<endl; CLASS_FUNC pFunc=&Base::f001; unsigned* tmp=(unsigned*)&pFunc; cout<<"类成员函数地址"<<*tmp<<endl; pFunc=&Base::f002; tmp=(unsigned*)&pFunc; cout<<"类成员函数地址"<<*tmp<<endl; }

typedef void (Base::*CLASS_FUNC)(void);
void main()
{
Base base;
cout<<"类对象地址"<<&base<<endl;
cout<<"类成员地址"<<&(base.m_fMenber1)<<endl;
cout<<"类成员地址"<<&(base.m_nMember2)<<endl;

CLASS_FUNC pFunc=&Base::f001;
unsigned* tmp=(unsigned*)&pFunc;
cout<<"类成员函数地址"<<*tmp<<endl;

pFunc=&Base::f002;
tmp=(unsigned*)&pFunc;
cout<<"类成员函数地址"<<*tmp<<endl;
}




仔细分析输出结果,对象的第一个成员变量得到地址跟整个对象的地址相同,表明在对象内存模型中排第一位的是第一个成员变量,对象中的其他成员变量按照类声明中的顺序依次排列;

那成员函数呢?发现为一个奇怪的内存地址。这是因为在同一个类的所有对象的成员函数,并不是每个对象有一份成员函数,而是共有一份相同的成员函数,如下图所示:



参考文献:
/article/7790842.html

继承

protected还有另一重要性质:
派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象的protected成员没有特殊访问权限。例如,假定Bulk_item定义了一个成员函数,接受一个Bulk_item对象的引用和一个Item_base对象的引用,该函数可以访问自己对象的protected成员以及Bulk_item形参的protected成员,但是,它不能访问Item_base形参的protected成员。
void Bulk_item::memfcn(const Bulk_item& d,const Item_base &b)
{
//attempt to use protected member
double ret=price;//OK :uses this->price;
ret=d.price;//OK:uses price from a Bulk_item object
ret=b.price;//error:no access to price from an Item_base
}
class类默认继承方式 是private,struct类默认的继承方式是public
如果基类以public公有方式被继承,则基类的所有公有成员都会成为派生类的公有成员,受保护的基类成员成为派生类的受保护成员
如果基类以private私有被继承,则基类的所有公有成员都会成为派生类的私有成员。基类的受保护成员成为派生类的私有成员。
如果基类以protected受保护方式被继承,那么基类的所有公有和受保护成员都会变成派生类的受保护成员。

不管基类以何种方式被继承,基类的私有成员,仍然保有其私有性,被派生的子类不能访问基类的私有成员。派生访问标号将控制派生类的用户对从基类继承而来的成员的访问。

覆盖和隐藏基类成员变量或成员函数
1、基类的成员变量或函数被覆盖:如果派生类覆盖了基类中的成员函数或成员变量,则当派生类的对象调用该函数或变量时是调用的派生类中的版本,当用基类对象调用该函数或变量时是调用的基类中的版本。

2、隐藏基类成员函数的情况:如果在派生类中定义了一个与基类同名的函数,不管这个函数的参数列表是不是与基类中的函数相同,则这个同名的函数就会把基类中的所有这个同名的函数的所有重载版本都隐藏了,这时并不是在派生类中重载基类的同名成员函数,而是隐藏,比如类A中有函数f(int i,intj)和f(int i)两个版本,当在从A派生出的类B中定义了基类的f()函数版本时,这时基类中的f(int i)和f(int i,intj)就被隐藏了,也就是由类B创建的对象比如为
m,不能直接访问类A中的f(int i)版本,即使用语句m.f(2)时会发生错误。
3.怎样使用派生类的对象访问基类中被派生类覆盖或隐藏了的函数或变量:
3.1方法1使用作用域运算符::,在使用对象调用基类中的函数或变量时使用作用域运算符即语名m.A::f(2),这时就能访问基类中的函数或变量版本。注意,访问基类中被派生类覆盖 了的成员变量只能用这种方法
3.2方法2使用using:该方法只适用于被隐藏或覆盖的基类函数,在派生类的类定义中使用语句 using把基类的名字包含进来,比如using A::f;就是将基类中的函数f()的所有重载版本包含进来,重载版本被包含到子类之后,这些重载的函数版本就相当于是子类的一部分,这时就可以用派生类的对象直接调用被派生类隐藏了的基类版本,比如m.f(2),但是使用这种语句还是没法调用 基类在派生类中被覆盖了的基类的函数,比如m.f()调用
的是派生烦躁 定义的函数f,要调用 被覆盖的基类中的版本要使用语句m.A::f()才行。

分清什么是隐藏什么是覆盖,举例说明
class A
{
public:
void fun(){cout<<a<<ends<<d<<endl;}
void fun(int ){cout<<a<<endl;}
private:
int a;
double d;
};
class B:public A
{
public:
void fun(){cout<<b<<ends<<d<<endl;}//这个函数把父类中的所有fun函数都给隐藏了,且把函数 //void fun{cout<<a<<ends<<d<<endl;}给覆盖了
private:
int b;
double d;//这里子类的d把父类的d给覆盖 了
};

派生类以私有方式被继承时改变基类中的公有成员为公有的方法:(派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松)
1、使用::作用域运算符,不提倡用这种方法,在派生类的public后面用作用域运算符把基类的公有成员包含进来,这样基类的成员就会成为派生类中的公有成员了,注意如果是函数的话后面不能加括号如A::f;如果f是函数的话不能有括号
2、使用using语句,现在一般用这种方法,也是在派生类的public使用using 把基类成员包含进行,如using A::f.

在继承中,基类的构造函数构建对象的基类部分,派生类的构造函数构建对象的派生类部分。
2. 当创建派生类对象时先用派生类的构造函数调用基类的构造函数构建基类,然后再执行派生类构造函数构造派生类。
即先构造基类再构造派生类的顺序。执行析构函数的顺序与此相反。
3. 调用基类 带参数的构造函数的方法:在派生类的 构造函数中使用初始化列表的形式就可以调用基类带参数的构造函
数初始化基类成员,如 B():A(int i){},类 B 是类 A 的派生类。
4. 派生类的 构造函数调用基类的构造函数的方法为:
4.1 如果派生类没有 显示用初始化列表调用基类的构造函数时,这时就会用派生类的构造函数调用基类的默认构造
函数,构造完基类后,才会执行派生类的构造函数函数体,以保证先执行基类构造函数再执行派生类构造函数
的顺序,如果基类没有默认构造函数就会出错。
4.2 如果派生类用显示的初始化列表调用基类的构造函数时,这时就会检测派生类的初始化列表,当检测到显示调
用基类的构造函数时,就调用基类的构造函数构造基类,然后再构造派生类,以保证先执行基类构造函数再执
行派生类 构造函数的顺序.

6如果在基类中没有定义 默认构造函数,但定义了 其他构造函数版本,这时派生类中定义了几个构造函数的不同版本,这时只要派生类有一个 构造函数没有显示调用基类中定义的构造函数版本就会发生错误,因为编译器会首先检查派生类构造函数调用基类构造函数的匹配情况,如果发 现不匹配就会出错,即使没有创建 任何类的对象都会出错,而不管这个派生类的对象有没有调用派生类的这个构造函。数比如:基类有一个 构造函数版本A(int i)而没有定义
默认
构造函数,派生类 B,有这几个版本的构造函数 B():A(4){},B(int i):A(5){},再有语句 B(int i, int j){}没有显示调用
基类定义的 构造函数而是调用基类的默认构造函数,如果创建了 B m 和语句 B m(1)时都会提 示没有可用的基类默认
构造函数可用的错误,虽然这时类 B 的对象 m 没有调用派生类 B 的带有两个形参的构造函数,但同样会出错。

7. 同样的道理,如果基类中定义了默认构造函数,却没有其他版本的构造函数,而这时派生类 却显示调用了基类构造函数的 其他版本,这时就会出错,不管你有没有创建类的对象,因为编译器会先在创建对象前就检查构造函数的匹
配问题。
8.派生类只能 初始化他的直接基类。比如类 C 是类 B 的子类,而类 B 又是类 A 的子类,这时 class C:publicB{public:B():A(){} };将会出错,该语句 试图显示调用类B 的基类类 A 的构造函数,这时会出 现类A 不是类 C 的基类的错误.

9.继承中的复制构造函数和构造函数一样,基类的 复制构造函数复制基类部分,派生类的 复制构造函数复制派生类部
分。
10.派生类 复制构造函数调用基类复制构造函数的方法为:A(const A& m):B(m){}其中 B 是基类,A 是派生类。
11.如果在派生类中定义了 复制构造函数而没有用初始化列表显示调用基类的复制构造函数,这时不管基类是 否定义了复制构造函数,这时出 现派生类对象的复制初始化情况时就将调用基类中的默认构造函数初始化基类的成员变量,注意是 默认构造函数不是默认复制构造函数,如果基类没有 默认构造函数就会出错。也就是说派生类的 复制构造函数的默认隐藏形式是 B(const B& j):A(){}这里 B 是 A 的派生类,也就是说如果不
显示用初始化列表形式调用基类的复制构告函数时,默认情况下是用初始化列表的形式调用的是基类的默认构造函数。
12.当在派生类中定义了 复制构造函数且显示调用了基类的复制构造函数,而基类 却没有定义基类的复制构造函数时,这时出 现派生类对象的复制初始化情况就将调用基类中的默认复制构造函数初始化基类部分,调用派生类的 复制构造函数初始化派生类部分,因为复制构造函数只有一种形式,即 A(const A& m){},比如当出 现调用时A(const A&
m):B(m){}如果这时基类 B 没有定义 复制构造函数,则该语句将会调用派生类 A 的默认复制构造函数。
12.如果基类定义了 复制构造函数,而派生类没有定义时,则会调用基类的复制构造函数初始化基类部分,调用派生类的默认复制构造函数初始化派生类部分。
在构造函数和析构函数中没有多态,即里面调用虚函数时不再起多态的作用。

如果派生类中覆盖了基类中的成员变量或函数,则当声明一个基类指针指向派生类对象时,这个基类指针只能访问基类中的成员变量或函数。命名:基类B和派生类D都定义了函数f,则B*p;D m;p=&m;m.f()将调用基类中的函数f()而不会调用派生类中的函数f();
如果基类指针指向派生类对象,则当对其进行增减运算时,它将指向它所认为的基类的下一个对象,而不会指向派生类的下一个对象,因此,应该座充地这种指针进行的增减操作是无效 的。
如果基类含有虚函数,则当声明了一个基类的指针时,当基类指针指向不同的派生类时,它就会调用相应派生类中虚函数版本,这种调用 方法是在运行时决定的,例如在类B中声明了虚函数,C,D,E都是从B继承而来且都实现了自己的虚函数版本,那么当定义了一个B类的指针P时,当P指向子类C时就会调用 子类C中定义的虚函数,当P指向子类D时就会调用 子类D中定义的虚函数,当P指向子类E时就会调用
子类E中定义的虚函数。
如果基类定义了一个虚函数,但派生类中却定义了一个虚函数的重载版本,则派生类的这个版本就会把基类的虚函数隐藏掉,当使用基类指针调用该函数时只能调用基类的虚函数,而不能调用派生类的重载版本,当用派生类的对象调用基类的虚函数时就会出现错误 了,因为基类的虚函数被派生类的重载版本隐藏了。
当基类的虚函数带有默认,则派生 类中对基类虚函数的重定义也必须有相同数量的形参,但形参可以有默认值也可以没有,如果派生类中的形参数量和基类中的不一样多,则是对基类的虚函数的重载,对虚函数的重定义也就意味着,当用指向派生类的基类指针调用 该虚时就会调用基类中的虚函数版本。如果虚函数形参有默认值 ,那么派生类中的虚函数的形参不论有无默认值,当用指针调用 派生类中的虚函数时就会被基类的默认值覆盖,即派生类的默认值不起作用。但用派生类的对象调用
该函数时,就不会出现这种情况。
纯虚函数声明形式为virtual 类型 函数名(参数列表)=0;注意后面的等于0;
如果类至少有一个纯虚函数,则这个类就是抽象的。
如果基类只是声明虚函数而不定义虚函数则此虚函数是纯虚函数,任何派生类都必须实现纯虚函数的自己的版本,如果不实现纯虚函数那么该类也是抽象类。
抽象类不能有对象,抽象类只能用作其它类的基类,因为抽象类中的一个或多个函数没有定义,所以不能用抽象类声明对象。但仍然可以用抽象类声明一个指针,这个指针指向派生类对象。如果派生类中未定义虚函数,则会使用基类中定义的函数。

包括虚函数的类被称为多态类。C++使用虚函数支持多态性。
在子类中重定义虚函数时,虚函数必须有与基类虚函数的声明完全相同的参数类型和数量,这和重载是不同的,如果不相同,则是函数重载,就失去了虚函数的本质。虚函数必须是类的成员函数,不过可以是另一个类的友元。

虚函数虚拟特性是以层次结构的方式来继承的,例如C从B派生而且C中重定义了B中的虚函数,而D双从C派生且未重定义B中的虚函数,这时声明一个基类指针P,当P指向在D,并调用D中的虚函数时,由于D中未重定义虚函数他会调用基类中的虚函数版本,这时他会调用类C中的虚函数而不是类B中的虚函数,因为类C比类B更接近于类D.

参考文献:
/article/9587190.html

转换与继承

引用和指针的转换

每个派生类对象包含一个基类部分,这意味着可以像使用基类对象一样在派生类对象上执行操作。因为派生类对象也是基类对象,所以存在从派生类型引用到基类型引用的自动转换,即可以将派生类对象的引用转换为基类子对象的引用。对指针也类似,但没有从基类引用到派生类引用的转换。具体的转换要通过dynamic_cast,如下所示
dynamic_cast
强制转换运算符:该转换符用于将一个指向派生类的基类指针或引用转换为派生类的指针或引用,注意 dynamic_cast 转换符只能用于含有虚函数的类,其表达式为 dynamic_cast<类型>(表达式),其中的类型是 指把表达式要转换成的目标类型,比如含有虚函数的基类 B 和从基类 B 派生出的派生类 D,则 B *pb; D *pd, md;pb=&md; pd=dynamic<D*>(pb); 最后一条语句表示把指向派生类
D 的基类指针 pb 转换为派生类 D 的指针,然后将这个指针赋给派生类 D 的指针 pd,有人可能会觉得这样做没有意义,既然指针 pd 要指向派生类为什么不pd=&md;这样做 更直接呢?有些时候我们需要强制转换,比如如 果指向派生类的基类指针 B 想访问派生类 D 中的除虚函数之外的成员时就需要把该指针转换为指向派生类 D 的指针,以达到访问派生类 D 中特有的成员的目的,比如派生类 D 中含有特有的成员函数 g(),这时可以这样来访问该成员 dynamic_cast<D*>(pb)->g();因为dynamic_cast
转换后的结果是一个指向派生类的指针,所以可以这样访问 派生类中特有的成员。但是该语句不 影响原来的指针的类型,即基类指针 pb 仍然是指向基类 B 的。如果单独使用该指针仍然不能访问派生类中特有的成员。一般情况下不 推见这样使用dynamic_cast 转换符,因为 dynamic_cast 的转换并不会总是成功的,具体情况在后面 介绍。

dynamic_cast 转换操作符在执行类型转换时首先将检查能否成功转换,如果能成功转换则转换之,如果 转换失败,如果是指针则反回一个 0 值,如果是转换的是引用,则抛出一个 bad_cast 异常,所以在使用 dynamic_cast 转换之间应使用 if 语句对其 转换成功与否进行测试,比如 pd=dynamic_cast<D*>(pb); if(pd){…}else{…},或者这 样测试 if(dynamic_cast<D*>(pb)){…}else{…}。

对象的转换

对基类对象进行初始化或赋值,实际上是在调用函数:初始化时调用复制构造函数,赋值时调用赋值操作符。
举例如用派生类对象对基类对象初始化时:
1.先将派生类对象转换为基类引用。
2.将该引用作为实参传递给复制构造函数
3那些操作符使用派生类的基类部分分别对调用复制构造函数的基类对象的成员进行初始化
4.一旦执行完毕,对象就包含了派生类对象的基类部分的副本,派生类的派生部分将被忽略掉!

引用转换与转换对象的区别:
引用转换是将派生类对象转换为基类类型引用,一个是用派生类对象对基类对象进行初始化或赋值。

多重继承与虚基类

1.C++允许一个派生尖从多个基类继承,这种继承方式称为多重继承,当从多个基类继承时每个基类之间用逗号隔开,比如class A:public B,public C{};就表示派生类A从基类B和C继承而来。

2.多重继承的构造函数和析构函数:多重继承中初始化的次序是按继承的次序来调用构造函数的,比如class A:public B,public C{};那么在定义类A的对象A m时将首先由类A的构造函数调用类B的构造函数初始化B,然后调用类C的构造函数初始化C,最后再初始化对象A.这与类A的初始化列表顺序无关!

3.多重继承中的二义性问题:

3.1.成员名重复:比如类A从类B和C继承而来,而类B和C中都包含有一个名字为f的成员函数,这时派生类A创建一个对象,比如A m;语句m.f()将调用类B中的f函数还是类C中的f函数呢?

3.2多个基本副本:比如类C和B都从类D继承而来,这时class A:public B,pulibc C{};类A从类C和类B同时继承而来,这时类A中就有两个类D的副本,一个是由类B继承而来的,一个是由类C继承而来的,当类A的对象比如 A m;要访问类D中的成员函数f时,语句m.f()就会出现二义性,这个f函数是调用的类B继承来的f还是访问类C继承来的函数f呢

3.3在3.2的情况下,还有另外一种情况,语句class A:public B,public C{};因为类A首先使用类B的构造 函数调用共同基类D的构造函数构造第一个类D的副本,然后再使用类C的构造 函数调用 共同基类D的构造 函数构造第二个类D的副本。类A对象m总共有两个共享基类D的副本,这时如果类D中有一个公共成员变量d,则语句m.B::d和m.D::d都是访问的同一变量,类B和类D都共享同一个副本,既如果有语句m.D::d=3则m.B::d也将是3.这时m.C::d的值不受影响而是原来的值。为什么会这样呢?因为类A的对象m总共只有两个类D的副本,所以类A的对象m就会从A继承来的两个直接基类B和C中,把从共同基类D中最先构造的第一个副本作为
类A的副本,即类B构造的D的副本。因为class A:public B,public C{};最先使用B的构造 函数调用 共同基类D创建D的第一个副本,所以类B和类D共享同一个副本。

3.4解决方法:对于第1和第2种情况都可以使用作用域运算符::来限定要访问的类名来解决二义性。但对于第二种情况一般不允许出现两个基业的副本,这时可以傅虚基类来解决这个问题,一旦定义了虚基类,就只会有一个基类的副本。

4虚基类:方法是使用virtual关键字,比如class B:public virtual D{},class C:public virtual D{};类B类C以虚基类的方式 从类D继承,这样的话从类B和类C同时继承的类就会只创见一个类D的副本,比如class A:public B,public C{};这时类A的对象就只会有一个类D的副本,类A类B类C类D四个类都共享一个类D的副本,比如类D有一个公有成员变量d,则m.d和m.B::d, m.C::d,m.D::d都将访问的是同一个变量。这样类A的对象调用类D中的成员
时就不会出现二义性了。

5.虚基类的构造函数:比如class B:public virtual D{};class C:public virtual D{};class A:public B,public C{};这时当创建类A的对象A m时初始化虚基类D将会使用类A的构造 函数直接调用 虚基类D的构造函数初始化虚基类部分,而不会使用类B或者类C的构造函数调用虚基类的构造 函数初始化虚基类部分,这样就保证了只有一个虚基类的副本。但是当创建一个类B和类C的对象时仍然会使用类B和类C中的构造函数调用
虚基本的构造 函数初始化虚基类
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: