C++面试知识点总结

一、多态性有哪些?

(静态和动态,然后分别叙述一下虚函数和函数重载)

多态指相同的对象收到不同的消息或者不同的对象收到相同的消息时产生的不同的实现动作。

C++支持两种多态:编译时多态(静态)、运行时多态(动态)

编译时多态

就是在编译期确定的一种多态。在C++中主要体现在函数模板,这里需要注意,函数重载和多态无关,很多地方把函数重载误认为是编译多态,这是错误的。

1.	#include <iostream>  
2.	using namespace std;  
3.	  
4.	template <typename T>  
5.	T add(T a, T b)  
6.	{  
7.	     t c = a+b;  
8.	     return c;  
9.	}  
10.	  
11.	int main()  
12.	{  
13.	     int a1 = 1;  
14.	     int b1 = 2;  
15.	     int c1 = add(a1,b1);  
16.	     cout<<"c1:"<<c1<<endl;  
17.	       
18.	     double a2 = 2.0;  
19.	     double b2 = 4.0;  
20.	     double c2 = add(a2,b2);  
21.	     cout<<"c2:"<<c2<<endl;  
22.	}  

上例中,我们定义了一个函数模板,用来计算两个数的和。这两个数的数据类型在使用时才知道,main函数中调用同一个函数分别计算了两个int值和两个double值的和,这就体现了多态,在编译期,编译器根据一定的最佳匹配算法来确定函数模板的参数类型到底是什么,这就体现了编译期的多态性。

运行时多态性

C++运行时多态性主要是通过虚函数来实现的。体现在具有继承关系的父类和子类之间,子类重新定义父类的成员函数称为覆盖或者重写,而虚函数允许子类重新定义父类的成员函数,即重写父类的成员函数。

下面举例说明一下:

1.	#include <iostream>  
2.	using namespace std;  
3.	  
4.	class A{  
5.	public:  
6.	     void f1()  
7.	     {  
8.	          cout<<"A::f1()"<<endl;  
9.	     }  
10.	     virtual void f2()  
11.	     {  
12.	          cout<<"A::f2()"<<endl;  
13.	     }  
14.	};  
15.	  
16.	class B:public A  
17.	{  
18.	public:  
19.	     //覆盖  
20.	     void f1()  
21.	     {  
22.	          cout<<"B::f1()"<<endl;  
23.	     }  
24.	     //重写  
25.	     virtual void f2()  
26.	     {  
27.	          cout<<"B::f2()"<<endl;  
28.	     }  
29.	};  
30.	  
31.	int main()  
32.	{  
33.	     A* p = new B();  
34.	     B* q = new B();  
35.	     p->f1();          //调用A::f1()  
36.	     p->f2();          //调用B::f2(),体现多态性
37.	     q->f1();          //调用B::f1()  
38.	     q->f2();          //调用B::f2()  
39.	     return 0;  
40.	}  

说说例2中关于体现多态性的问题,我们在父类即A类中定义了一个虚函数f2()——由关键字virtual修饰。既然是虚函数,允许子类重写这个函数,于是我们在子类即B类中重写了函数f2()。之后我们在main函数中定义了一个A类指针p,请注意,虽然定义的是一个父类指针,但是它指向的却是一个子类的对象(new B()),然后我们用这个父类的指针调用f2(),从结果来看,实际上调用的是子类的f2(),并不是父类的f2(),这就体现了多态性。虽然p是父类指针,但是它指向的是子类对象,而且调用的又是虚函数,那么在运行期,就会找到动态绑定到父类指针上的子类对象,然后查找子类的虚函数表,找到函数f2()的入口地址,从而调用子类的f2()函数,这就是运行期多态。

接下来我们再来看看p->f1();这句话,从运行结果来看,调用的是父类的f1()函数,这里是为什么没有体现多态呢?原因很简单,因为f1()不是虚函数,所以根本就没有多态性,虽然子类和父类都有f1()函数,但是子类仅仅是覆盖或者说是隐藏了父类的f1()函数,注意这里不是重写,是覆盖。而p是父类指针,所以只能调用父类的f1()函数。而指针q的类型为子类指针,所以q->f1();调用子类的函数f1(),q->f2();调用子类的函数f2();

C++纯虚函数 

定义:纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法,基类中实现纯虚函数的方法是在函数原型后面加“=0”。例如:

1.  virtual void f() = 0;  

为什么要引入纯虚函数

1、为了使用多态特性,我们必须在基类中定义虚拟函数。 

2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。 为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtualReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。纯虚函数永远不会被调用,它们主要用来统一管理子类对象

二、动态绑定怎么实现?

(问一下基类与派生类指针和引用的转换问题)

1.为每一个包含虚函数的类设置一个虚表(VTABLE)每当创建一个包含有虚函数的类或从包含虚函数的类派生一个类时,编译器就会为这个类创建一个VTABLE。在VTABLE中,编译器放置了这个类中,或者它的基类中所有已经声明为 virtual的函数的地址。如果在这个派生类中没有对基类中声明为 virtual 的函数进行重新定义,编译器就使用基类的这个虚函数的地址。而且所有VTABLE中虚函数地址的顺序是完全相同的。

2.初始化虚指针(VPTR)然后编译器在这个类的各个对象中放置VPTR。VPTR在对象的相同的位置(通常都在对象的开头)。VPTR必须被初始化为指向相应的VTABLE。

3.为虚函数调用插入代码 当通过基类的指针调用派生类的虚函数时,编译器将在调用处插入相应的代码,以实现通过VPTR找到VTABLE,并根据VTABLE中存储的正确的虚函数地址,访问到正确的函数。

 为了支持c++的多态性,才用了动态绑定和静态绑定。理解他们的区别有助于更好的理解多态性,以及在编程的过程中避免犯错误。

需要理解四个名词:
1、对象的静态类型:对象在声明时采用的类型。是在编译期确定的。
2、对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。
关于对象的静态类型和动态类型,看一个示例:

1.	class B  
2.	{  
3.	}  
4.	class C : public B  
5.	{  
6.	}  
7.	class D : public B  
8.	{  
9.	}  
10.	D* pD = new D();//pD的静态类型是它声明的类型D*,动态类型也是D*  
11.	B* pB = pD;//pB的静态类型是它声明的类型B*,动态类型是pB所指向的对象pD的类型D*  
12.	C* pC = new C();  
13.	pB = pC;//pB的动态类型是可以更改的,现在它的动态类型是C*  

3、静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。
4、动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。

1.	class B  
2.	{  
3.	    void DoSomething();  
4.	    virtual void vfun();  
5.	}  
6.	class C : public B  
7.	{  
8.	    void DoSomething();//首先说明一下,这个子类重新定义了父类的no-virtual函数,这是一个不好的设计,会导致名称遮掩;这里只是为了说明动态绑定和静态绑定才这样使用。  
9.	    virtual void vfun();  
10.	}  
11.	class D : public B  
12.	{  
13.	    void DoSomething();  
14.	    virtual void vfun();  
15.	}  
16.	D* pD = new D();  
17.	B* pB = pD;  

让我们看一下,pD->DoSomething()和pB->DoSomething()调用的是同一个函数吗?
不是的,虽然pD和pB都指向同一个对象。因为函数DoSomething是一个no-virtual函数,它是静态绑定的,也就是编译器会在编译期根据对象的静态类型来选择函数。pD的静态类型是D*,那么编译器在处理pD->DoSomething()的时候会将它指向D::DoSomething()。同理,pB的静态类型是B*,那pB->DoSomething()调用的就是B::DoSomething()。

让我们再来看一下,pD->vfun()和pB->vfun()调用的是同一个函数吗?
是的。因为vfun是一个虚函数,它动态绑定的,也就是说它绑定的是对象的动态类型,pB和pD虽然静态类型不同,但是他们同时指向一个对象,他们的动态类型是相同的,都是D*,所以,他们的调用的是同一个函数:D::vfun()。

上面都是针对对象指针的情况,对于引用(reference)的情况同样适用。

指针和引用的动态类型和静态类型可能会不一致,但是对象的动态类型和静态类型是一致的。

D D;
D.DoSomething()和D.vfun()永远调用的都是D::DoSomething()和D::vfun()。

至于那些是动态绑定,那些是静态绑定,有篇文章总结的非常好:
我总结了一句话:只有虚函数才使用的是动态绑定,其他的全部是静态绑定。目前我还没有发现不适用这句话的,如果有错误,希望你可以指出来。

特别需要注意的地方
当缺省参数和虚函数一起出现的时候情况有点复杂,极易出错。我们知道,虚函数是动态绑定的,但是为了执行效率,缺省参数是静态绑定的。

1.	class B  
2.	{  
3.	 virtual void vfun(int i = 10);  
4.	}  
5.	class D : public B  
6.	{  
7.	 virtual void vfun(int i = 20);  
8.	}  
9.	D* pD = new D();  
10.	B* pB = pD;  
11.	pD->vfun();  
12.	pB->vfun();  

有上面的分析可知pD->vfun()和pB->vfun()调用都是函数D::vfun(),但是他们的缺省参数是多少?
分析一下,缺省参数是静态绑定的,pD->vfun()时,pD的静态类型是D*,所以它的缺省参数应该是20;同理,pB->vfun()的缺省参数应该是10。编写代码验证了一下,正确。

对于这个特性,估计没有人会喜欢。所以,永远记住:

“绝不重新定义继承而来的缺省参数(Never redefine function’s inheriteddefault parameters value.)”

关于c++语言
目前我基本上都是在c++的子集“面向对象编程”下工作,对于更复杂的知识了解的还不是很多。即便如此,到目前为止编程时需要注意的东西已经很多,而且后面可能还会继续增多,这也许是很多人反对c++的原因。
c++是Google的四大官方语言之一。但是Google近几年确推出了go语言,而且定位是和c/c++相似。考虑这种情况,我认为可能是Google的程序员们深感c++的复杂,所以想开发一种c++的替代语言。有时间要了解一下go语言,看它在类似c++的问题上时如何取舍的。

三、类型转换有哪些?

(四种类型转换,分别举例说明)

1、  static_cast:

功能:完成编译器认可的隐式类型转换。

格式type1 a;

type2 b = staic_cast<type1>(a);将type1的类型转化为type2的类型;

使用范围:

(1)基本数据类型之间的转换,如int->double;

int a = 6;

double b = static_cast<int>(a);

(2)派生体系中向上转型:将派生类指针或引用转化为基类指针或引用(向上转型);

class base{       ….     }
class derived : public base{      ….     }
base *b;
derived *d = new derived();
b = static_cast<base *>(d);

2、  dynamic_cast

功能:执行派生类指针或引用与基类指针或引用之间的转换。

格式:

(1)      其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行运行时类型检查;

(2)      基类中要有虚函数,因为运行时类型检查的类型信息在虚函数表中,有虚函数才会有虚函数表;

(3)      可以实现向上转型和向下转型,前提是必须使用public或protected继承;

例子:

向上转型:
class base{       …      };
class derived : public base{      …      };
int main()
{
base *pb;
derived *pd = new derived();
pb = dynamic_cast<base *>(pd);
return 0;
}
向下转型:
class base{       virtualvoid func(){}           };
class derived : public base{      void func(){}      };
int main()
{
base *pb = new base();
derived *pd = dynamic_cast<derived *>(pb);//向下转型
return 0;
}

3、const_cast:

只能对指针或者引用去除或者添加const属性,对于变量直接类型不能使用const_cast;不能用于不同类型之间的转换,只能改变同种类型的const属性。

如:

const int a= 0;
int b = const_cast<int>(a);//不对的
const int *pi = &a;
int * pii = const_cast<int *>pi;//去除指针中的常量性,也可以添加指针的常量性;

const_cast的用法:

(1)常用于函数的形参是一个非const的引用,我想要穿进去一个const的引用,可以使用const_cast<Type&>para;去除实参的常量性,以便函数能够接受这个参数。

(2)一个const对象,我们想要调用该对象中的非const函数,可以使用const_cast去除对象的常量性;

4、reinterpret_cast:

从字面意思理解是一个“重新解释的类型转换”。也就是说对任意两个类型之间的变量我们都可以个使用reinterpret_cast在他们之间相互转换,无视类型信息。

不常使用。

四、操作符重载,具体如何去定义?

(让把操作符重载函数原型说一遍)

operator是C++的关键字,它和运算符一起使用,表示一个运算符函数,理解时应将operator=整体上视为一个函数名。

   这是C++扩展运算符功能的方法,虽然样子古怪,但也可以理解:一方面要使运算符的使用方法与其原来一致,另一方面扩展其功能只能通过函数的方式(c++中,“功能”都是由函数实现的)。

 1、为什么使用操作符重载?

对于系统的所有操作符,一般情况下,只支持基本数据类型和标准库中提供的class,对于用户自己定义的class,如果想支持基本操作,比如比较大小,判断是否相等,等等,则需要用户自己来定义关于这个操作符的具体实现。比如,判断两个人是否一样大,我们默认的规则是按照其年龄来比较,所以,在设计person 这个class的时候,我们需要考虑操作符==,而且,根据刚才的分析,比较的依据应该是age。 那么为什么叫重载呢?这是因为,在编译器实现的时候,已经为我们提供了这个操作符的基本数据类型实现版本,但是现在他的操作数变成了用户定义的数据类型class,所以,需要用户自己来提供该参数版本的实现。

2、如何声明一个重载的操作符?

A:  操作符重载实现为类成员函数
重载的操作符在类体中被声明,声明方式如同普通成员函数一样,只不过他的名字包含关键字operator,以及紧跟其后的一个c++预定义的操作符。
可以用如下的方式来声明一个预定义的==操作符:
class person{
private:
    int age;
    public:
    person(int a){
       this->age=a;
    }
   inline bool operator == (const person &ps) const;
};
实现方式如下:
inline bool person::operator==(const person &ps) const
{
     if (this->age==ps.age)
        return true;
     return false;
}
调用方式如下:
#include
using namespace std;
int main()
{
  person p1(10);
  person p2(20);
  if(p1==p2) c
cout<<”the age is equal!”< return 0;
}

这里,因为operator ==是class person的一个成员函数,所以对象p1,p2都可以调用该函数,上面的if语句中,相当于p1调用函数==,把p2作为该函数的一个参数传递给该函数,从而实现了两个对象的比较。

B:操作符重载实现为非类成员函数(全局函数)
对于 全局重载操作符,代表左操作数的参数必须被显式指定。例如:
#include
#include
using namespace std;
class person
{
public:
int age;
public:
};

bool operator==(person const &p1 ,person const & p2)
//满足要求,做操作数的类型被显示指定
{
if(p1.age==p2.age)
    return true;
return false;
}
int main()
{
person rose;
person jack;
rose.age=18;
jack.age=23;
if(rose==jack)
cout<<"ok"< return 0;
}

C:如何决定把一个操作符重载为类成员函数还是全局名字空间的成员呢
①如果一个重载操作符是类成员,那么只有当与他一起使用的左操作数是该类的对象时,该操作符才会被调用。如果该操作符的左操作数必须是其他的类型,则操作符必须被重载为全局名字空间的成员。
②C++要求赋值=,下标[],调用(), 和成员指向-> 操作符必须被定义为类成员操作符。任何把这些操作符定义为名字空间成员的定义都会被标记为编译时刻错误。
③如果有一个操作数是类类型如string类的情形那么对于对称操作符比如等于操作符最好定义为全局名字空间成员。

D:重载操作符具有以下限制

(1)      只有C++预定义的操作符集中的操作符才可以被重载;

C++允许重载的运算符
C++中绝大部分运算符都是可以被重载的。

不能重载的运算符只有5个:

.            (成员访问运算符)

.*           (成员指针访问运算符)

::            (域运算符)

sizeof   (长度运算符)

?:           (条件运算符)

前两个运算符不能重载是为了保证访问成员的功能不能被改变,域运算符合sizeof运算符的运算对象是类型而不是变量或一般表达式,不具备重载的特征。

(2)对于内置类型的操作符,它的预定义不能被改变,应不能为内置类型重载操作符,如,不能改变int型的操作符+的含义;

(3) 也不能为内置的数据类型定义其它的操作符;

(4) 只能重载类类型或枚举类型的操作符;

(5) 重载操作符不能改变它们的操作符优先级;

(6) 重载操作符不能改变操作数的个数;

(7) 除了对( )操作符外,对其他重载操作符提供缺省实参都是非法的;

实例1 重载operator():

1.	struct join_if_joinable  
2.	  {  
3.	    void operator()(thread& t)  
4.	    {  
5.	      if (t.joinable())  
6.	      {  
7.	        t.join();  
8.	      }  
9.	    }  
10.	  };  
11.	//use  
12.	join_if_joinable(thread1);  

五、内存对齐原则?

(原则叙述了一下并举例说明)

1、内存对齐的原因

1>、平台移植原因:不是所有的硬件平台都能任意访问任意地址上的数据,有些硬件平台只能在某些特定地址处读取特定的数据,否则会抛出硬件异常;
2>、性能原因:数据结构(尤其是栈)应尽可能的在自然边界对齐。原因在于,访问未对齐的内存,处理器需要进行两次访问,而访问对齐的内存,处理器只需要进行一次访问。

2、内存对齐的规则

在具体讲内存对齐的规则之前引入一个名词:对齐系数,也叫对齐模数,每个编译器都有自己默认的对齐系数,VC6.0默认为8。程序员可以根据需要进行修改,可通过预编译指令#pragma pack(n),n就是对齐系数,可以取1、2、4、8、16,具体对齐规则有三条,如下:
(1).数据成员的对齐规则:结构体(struct)(或者联合体(union))的数据成员,第一个数据成员放在偏移量为0的地方,以后每个数据成员按照#pragma pack(n)和数据成员中比较小的那个数对齐,也就是说,起始地址需要时这个数的倍数,具体下面会举例说明;
(2).结构体(struct)(或者联合体(union))整体对齐规则:整体的大小应该按照#pragma pack(n)和结构中最长的数据结构中,最大的那个进行,也就是,需要是这个数的倍数;

(3).如果#pragmapack(n)比结构中任何一个数据成员类型都大,则对齐系数不起任何作用。下面举例说明,环境:VS2013,32位操作系统

1.	#include <iostream>  
2.	using namespace std;  
3.	  
4.	struct S  
5.	{  
6.	    char a;  
7.	    int  b;  
8.	    short c;  
9.	 };  
10.	  
11.	struct T  
12.	{  
13.	    short c;  
14.	    char  a;  
15.	    int   b;  
16.	};  
17.	  
18.	int main()  
19.	{  
20.	    cout << "sizeof(S) is " << sizeof(S) << endl;  
21.	    cout << "sizeof(T) is " << sizeof(T) << endl;  
22.	    system("PAUSE");  
23.	    return 0;  
24.	}  

代码输出结果为:

sizeof(S) is 12

sizeof(T) is 8   

编译器默认对齐系数为8:

<
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值