印象笔记同步分享:《程序员面试宝典》学习记录7
整个C++程序设计全面围绕面向对象的方式进行。类的继承特性是C++的一个非常重要的机制。继承特性可以使一个新类获得其父类的操作和数据结构,程序员只需在新类中增加原有类没有的成分。
在面试过程中,各大企业会考量你对虚函数、纯虚函数、私有继承、多重继承等知识点的掌握程度
1、以下代码的输出结果是什么?
#include<iostream>using namespace std;
class A
{
protected:
int m_data;
public:
A(int data = 0)
{
m_data = data;
}
int GetData()
{
return doGetData();
}
virtual int doGetData()
{
return m_data;
}
};
class B : public A
{
protected:
int m_data;
public:
B(int data = 1)
{
m_data = data;
}
int doGetData()
{
return m_data;
}
};
class C : public B
{
protected:
int m_data;
public:
C(int data = 2)
{
m_data = data;
}
};
int main ()
{
C c(10);
cout << c.GetData() <<endl;
C中未定义,故调用B中的,但是B中也未定义,故调用A中的GetData(),因为A中的doGetData()是虚函数,所以调用B类中的doGetData(),而B类的doGetData()返回B::m_data, 故输出 1。
cout << c.A::GetData() <<endl;
因为A中的doGetData()是虚函数,所以调用B类中的doGetData(),而B类的doGetData()返回B::m_data,故输出 1。
cout << c.B::GetData() <<endl;
肯定是B类的返回值 1 了。
cout << c.C::GetData() <<endl;
C类中未重定义GetData(),故调用从B继承来的GetData(),但是B类也未定义,所以调用A中的GetData(),因为A中的doGetData()是虚函数,所以调用B类的doGetData(),股输出为1
cout << c.doGetData() <<endl;
B类的返回值 1 了。
cout << c.A::doGetData() <<endl;
因为直接调用了A的doGetData() ,所以输出0。
cout << c.B::doGetData() <<endl;
调用了B的doGetData(),所以输出 1。
cout << c.C::doGetData() <<endl;
调用了B的doGetData(),所以输出 1。
return 0;
}
总结:这里要注意存在一个就近调用,如果父类存在相关接口则优先调用父类接口,如果父类也不存在相关接口则调用祖父辈接口。
考点2:虚函数覆盖虚函数
以下代码输出结果是什么?
#include<iostream>using namespace std;
class A
{
public:
void virtual f()
{
cout<<"A"<<endl;
}
};
class B : public A
{
public:
void virtual f()
{
cout<<"B"<<endl;
}
};
int main ()
{
A* pa=new A();
pa->f(); 这个很明显A
B* pb=(B*)pa;
pb->f(); 这个强制将pa复制到pb,所以pb指向A
delete pa,pb; 删除pa,pb所指向的地址,但是pa、pb指针并没有删除,悬浮指针
pa=new B();
pa->f(); B
pb=(B*)pa;
pb->f(); B
return 0;
}
考点1:公有继承和私有继承的区别
公有继承(public)
公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
私有继承(private)
私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。(私有继承使父类中的函数转化为私有)
保护继承(protected)
保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。
public protected private
共有继承 public protected 不可见
私有继承 private private 不可见
保护继承 protected protected 不可见
考点2:保护继承和私有继承后,子类对象想访问父类成员
公有继承:子类对象可以直接访问父类的public的成员
保护继承:继承之后的类相对于父类是独立的,不能直接访问父类成员,其类对象,在公共场合无法使用基类成员,也只能通过自己的成员函数来访问父类的protected和public成员。
私有继承:继承之后也不能直接访问父类成员,只能通过子类的成员函数来访问父类的protected和public成员。
#include
class Animal
{
public:
Animal(){}
void eat(){cout << "eat
";}
};
class Giraffe:protected Animal
{
Giraffe(){}
void StrechNeck(double)
{cout << "strechneck
";}
void take()
{
eat(); //ok
}
};
void main()
{
Giraffe girl;
girl.eat(); 错误 保护继承不能直接访问父类成员
girl.take(); 正确 保护继承只能通过子类的成员函数来访问父类成员
girl.StretchNeck(); 正确 保护继承只能通过子类的成员函数来访问父类成员
}
考点3:派生类的三种继承深入了解
#include <iostream>#include <stdio.h>
class Parent
{
public:
Parent(int var = -1)
{
m_nPub = var;
m_nPtd = var;
m_nPrt = var;
}
public:
int m_nPub;
protected:
int m_nPtd;
private:
int m_nPrt;
};
class Child1:public Parent
{
public:
int GetPub(){return m_nPub;};
int GetPtd(){return m_nPtd;};
int GetPrt(){return m_nPrt;}; 错误 父类私有变量不能被子类访问
};
class Child2:protected Parent
{
public:
int GetPub(){return m_nPub;};
int GetPtd(){return m_nPtd;};
int GetPrt(){return m_nPrt;}; 错误 父类私有变量不能被子类访问
};
class Child3:private Parent
{
public:
int GetPub(){return m_nPub;};
int GetPtd(){return m_nPtd;};
int GetPrt(){return m_nPrt;}; 错误 父类私有变量不能被子类访问
};
int main()
{
Child1 cd1;
Child2 cd2;
Child3 cd3;
int nVar = 0;
//公有继承
cd1.m_nPud = nVar; 正确公有继承访问并改变公有变量
cd1.m_nPtd = nVar; 错误公有继承m_nPtd可以被继承访问但是不能被修改
nVar = cd1.GetPtd(); 正确公有继承通过函数访问父类的公有变量
//保护继承
cd2.m_nPtd = nVar; 错误 保护继承 保护继承不能直接访问父类的成员
nVar = cd2.GetPtd(); 正确 保护继承 通过函数来访问父类成员
//私有继承
cd3.m_nPub = nVar; 错误 是有继承 不能直接修改父类的公有变量
nVar = cd3.GetPtd(); 正确 可以通过函数访问父类的保护变量
return 0;
}
考点1:理解虚方法(虚函数)
每个对象里有虚表指针,指向虚表,虚表里存放了虚函数的地址,虚函数表是顺序存放虚函数地址的,不需要用到链表。所以类中的每一个对象都有一个链表来存虚方法地址,那就是虚表。
虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定后该对象应该调用哪一个虚函数,典型的情况下,这个信息具有一种被称为vptr虚函数指针的指针形式,vptr指向一个被称为vtbl的虚函数表函数指针数组,每一个虚函数都关联到vtbl,当一个对象调用了虚函数,实际的被调用函数通过下面步骤确定,找到对象的vptr指向的vtbl,之后在vtbl中寻找合适的函数指针。
虚拟函数使用的缺点
优点讲了一大堆,现在谈一下缺点,虚函数最主要的缺点是执行效率较低,看一看虚拟函数引发的多态性的实现过程,你就能体会到其中的原因,另外就是由于要携带额外的信息(VPTR),所以导致类多占的内存空间也会比较大,对象也是一样的
考点2:虚函数、虚函数表、虚函数指针的联系
每一个具有虚函数的类都有一个虚函数表VTABLE,里面按在类中声明的虚函数的顺序存放着虚函数的地址,这个虚函数表VTABLE是这个类的所有对象所共有的,也就是说无论用户声明了多少个类对象,但是这个VTABLE虚函数表只有一个。
在每个具有虚函数的类的对象里面都有一个VPTR虚函数指针,这个指针指向VTABLE的首地址,每个类的对象都有这么一种指针。
考点3:虚函数的继承
1)空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同);
2)一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的;
3)类对象的大小=各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和+ vfptr指针(多继承下可能不止一个)+vbptr指针(多继承下可能不止一个)+编译器额外增加的字节。
4)当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vPtr指向虚函数表VTable;
#include<iostream>#include<memory.h>#include<assert.h>
using namespace std;
class A
{
char k[3]; 所占的大小为3
public:
virtual void aa(){}; 虚指针大小为4
};
class B : public virtual A
{
char j[3];
public:
virtual void bb(){};
};
class C : public virtual B
{
char i[3];
public:
virtual void cc(){};
};
int main(int argc, char *argv[])
{
cout << "sizeof(A): " << sizeof(A) << endl; 大小为4(char)+4(虚表)=8
cout << "sizeof(B): " << sizeof(B) << endl; 大小为8(A副本)+4(char)+4(虚表)=16
cout << "sizeof(C): " << sizeof(C) << endl; 大小为16(B副本)+4(char)+4(虚表)=24
return 0;
}
考点3:什么是虚继承?它和一般的继承有什么不同?有什么用
虚拟继承是多重继承中特有的概念。虚拟基类是为了解决多重继承而出现的,可以节省内存空间
请看下图:
在图 1中,类D接触自类B和类C,而类B和类C都继承自类A,因此出现了图 2所示的情况。
在图 2中,类D中会出现两次A。为了节省内存空间,可以将B、C对A的继承定义为虚拟继承,而A成了虚拟基类。最后形成了图 3。
代码如下:
class A;
class B : public virtual A;
class C : public virtual A;
class D : public B,public C;
考点4:区分虚函数继承和虚继承
虚拟继承是多重继承中特有的概念,是为解决多重继承的。用虚继承可以节省内存空间
虚函数是面向对象多态性的主要方式,通过继承基类中的虚函数在子类中重载实现不同操做。继承的虚函数在子类中不需要加virtual,默认就是虚函数。可以被它的子类覆盖。
考点4:区分虚继承和直接继承
#include <stdio.h>
class A {
public:
int a;
}; sizeof(A)=4
class B : virtual public A {
public:
int b; sizeof(B)=4(虚表)+4(A副本)+4(自己变量)=12
};
class C : virtual public B { sizeof(c)= 12(B副本)+4(虚表) = 16 如果这里改为直接继承,那么sizeof(c)=12
};
int main() {
printf("%d
", sizeof(C));
return 0;
}
再举一个例子:
#include <stdio.h>
class A {
public:
int a;
}; sizeof(A) = 4
class B : virtual public A {
}; sizeof(B) =4+4=8
class C : virtual public A { sizeof(C) =4+4=8
};
class D : public B, public C{ sizeof(D)=8+8-4=12 这里需要注意要减去4 因为B和C同时继承A,属于只需要保存一个A的副本就好了 sizeof(D)=4(A的副本)+4(B的虚表)+4(C的虚表)=12
};
int main() {
printf("%d
", sizeof(D));
return 0;
}
再举一个例子:含有普通继承
class A
{
};
class B
{
char ch;
virtual void func0() { }
};
class C
{
char ch1;
char ch2;
virtual void func() { }
virtual void func1() { }
};
class D: public A, public C
{
int d;
virtual void func() { }
virtual void func1() { }
};
class E: public B, public C
{
int e;
virtual void func0() { }
virtual void func1() { }
};
int main(void)
{
cout<<"A="<<sizeof(A)<<endl; result=1 空类所占空间的大小为1
cout<<"B="<<sizeof(B)<<endl; result=8 1+4 对其 8
cout<<"C="<<sizeof(C)<<endl; result=8 1+1+4 对其 8
cout<<"D="<<sizeof(D)<<endl; result=12 C的副本+D本身=12
cout<<"E="<<sizeof(E)<<endl; result=20 B的副本+C的副本+E本身=20
return 0;
}
这里需要区分一下:①不没有继承的时候,存在虚函数则需要加上虚指针,如果有多个也只需要加上一个,因为只有一个虚指针;②对于普通继承,类D和类E中自己的虚函数,大小为0,因为他没有虚表③对于虚继承中,派生类中存在一个或多个虚函数的时候,它本身就有一个虚表,指向自己的虚表,所以要加4
再举一个例子:含有虚继承
class CommonBase
{
int co; 4
};
class Base1: virtual public CommonBase 4副本+4虚指针+4自身+4=16
{
public:
virtual void print1() { }
virtual void print2() { }
private:
int b1;
};
class Base2: virtual public CommonBase 同理16
{
public:
virtual void dump1() { }
virtual void dump2() { }
private:
int b2;
};
class Derived: public Base1, public Base2 16+16-4+4=32
{
public:
void print2() { }
void dump2() { }
private:
int d;
};
class Derived size(32):
+---
| +--- (base class Base1)
| | {vfptr}
| | {vbptr}
| | b1
| +---
| +--- (base class Base2)
| | {vfptr}
| | {vbptr}
| | b2
| +---
| d
+---
+--- (virtual base CommonBase)
| co
+---
再举一个例子:
class A
{
public:
virtual void aa() { }
virtual void aa2() { }