关于面向对象的面试知识点整理(一)
例1: C++ 中的空类默认产生哪些类成员函数?
对于一个空类,编译器默认产生4个成员函数:默认构造函数、 析构函数、 拷贝构造函数和赋值函数。
例2: structure是否可以拥有constructor / destructor及成员函数?如果可以, 那么structure和class还有区别么?
区别是class中变量默认是private, struct中的变量默认是public。struct可以有构造函数,析构函数,之间也可以继承,等等。 C++中的struct其实和class意义一样, 唯一不同的就是struct里面默认的访问控制是public, class中默认的访问控制是 private。 C++中存在struct关键字的唯一意义就是为了让C程序员们有个归属感, 是为了让C++编译器兼容以前用C开发的项目。
例3: 现有以下代码, 则编译时会产生错误的是?
struct Test
{
Test(int) {
}
Test() {
}
~Test() {
}
void fun() {
}
};
int main()
{
Test a(1);
a.fun();
Test b();
b.fun();
return 0;
}
Test b()
这个语法等同于声明了一个函数,函数名为b, 返回值为Test, 传入参数为空。但是实际上,代码作者是希望声明一个类型为Test,变量名为b的变量,应该写成Test b;
, 但程序中这个错误在编译时是检测不出来的。出错的是b.fun()
,它是编译不过去的。
例4: 下面程序的打印出的结果是什么?
#include <iostream>
using namespace std;
class Base
{
public:
Base(int i) : m_j(i), m_i(m_j) {
}
Base() : m_j(0), m_i(m_j) {
}
~Base() {
}
int get_i() const {
return m_i; }
int get_j() const {
return m_j; }
private:
int m_i;
int m_j;
};
int main()
{
Base obj(98);
cout << obj.get_i() << endl;
cout << obj.get_j() << endl;
return 0;
}
本题想要得到的结果是"98,98"。 但是成员变量的声明是先m_i, 然后是m_j;初始化列表的初始化变量顺序是根据成员变量的声明顺序来执行的, 因此m_i会被赋予一个随机值。更改一下成员变量的声明顺序可以得到预想的结果。 如果要得到 "98,98"的输出结果,程序需要修改如下
int m_j;
int m_i;
补充变量未初始化的值
- 未初始化的全局数组为0;
- 未初始化的局部数组为随机值;
- 初始化部分的全局数组与局部数组,初始化部分为初始化值,未初始化部分都为0;(不管全局还是局部)
- 全局变量未初始化:
#include <iostream>
using namespace std;
int i;
char c;
float f;
double d;
int* p;
int main()
{
cout << i << endl; // 0
cout << c << endl; // '\0'
cout << f << endl; // 0
cout << d << endl; // 0
cout << p << endl; // 0
return 0;
}
- 局部变量未初始化为随机数。
#include <iostream>
using namespace std;
int main()
{
int i;
char c;
float f;
double d;
int* p;
cout << i << endl;
cout << c << endl;
cout << f << endl;
cout << d << endl;
cout << p << endl;
return 0;
}
-
类的成员变量未初始化的值
-
对象在全局作用域或为静态局部对象时,类的内置成员变量被初始化为0.
-
对象在局部作用域定义时,类的内置成员变量不被初始化为0。
-
对于类类型成员按照其自身的默认构造函数进行初始化。
如果类显式提供了带参数的构造函数,则编译器不会再为其生成空参数的构造函数。这时候就不能用空参数来定义类类型变量
class A{ public: int value; A (int i):value(i){ } }; int main(){ A a; // error return 0; }
-
参考代码:
#include <iostream>
using namespace std;
class A
{
public:
int i;
float f;
double d;
char c;
int* p;
void print() {
cout << i << endl;
cout << c << endl;
cout << f << endl;
cout << d << endl;
cout << p << endl;
cout << "-------------------" << endl;
}
};
A a0;
int main()
{
a0.print();
A a;
a.print();
return 0;
}
例5: MFC类库中,CObject类的重要性不言自明。在CObject的定义中,我们看到一个有趣的现象,即CObject的析构函数是虚拟的。为什么MFC的编写者认为虚拟的析构函数是必要的?
#include <iostream>
using namespace std;
class Base
{
public:
Base() {
cout << "Base" << endl; }
~Base() {
cout << "~Base" << endl; }
};
class Derived : public Base
{
public:
Derived() {
cout << "Derived" << endl; }
~Derived() {
cout << "~Derived" << endl; }
};
int main()
{
Base *b = new Derived();
delete b;
return 0;
}
我们将一个父类的指针指向子类的对象,当我们使用delete b
去释放b指向的内存,会调用父类的析构函数(静态联编),但是却不会调用子类的析构函数。
如果子类对象在构造函数内申请了一块堆内存,最后根据上述情况则会造成内存泄漏,我们在 ~Base() { cout << "~Base" << endl; }
前加上 virtual
, delete b
会先调用子类析构,再调用父类析构,自然就把子类申请的内存释放掉啦!
补充知识: C++静态联编和动态联编
根据: https://blog.csdn.net/erlian1992/article/details/44262843 所修改
联编是指一个程序自身彼此关联的一个过程。 按照联编所进行的阶段不同,可分为静态联编和动态联编两种。
静态联编: 静态联编是指在程序编译链接阶段进行联编。这种联编又称为早期联编,这是因为这种联编工作是在程序运行之前完成的。
编译时所进行的联编又称为静态束定。束定是指确定所调用的函数与执行该函数代码之间的关系。 其优点是效率高,但灵活性差。
下面来看一个静态联编的程序例题:
#include <iostream>
using namespace std;
class Point
{
public:
Point(double i,double j)//基类的构造函数
{
x=i;y=j; }
double Area() const//定义的常成员函数
{
return 0.0; }
private:
double x,y;
};
class Rectangle:public Point//公有继承的派生类
{
public:
Rectangle(double i,double j,double k,double l);//声明派生类的构造函数
double Area() const
{
return w*h; }
private:
double w,h;
};
Rectangle::Rectangle(double i,double j,double k,double l):Point(i,j)//派生类构造函数的函数体
{
w=k;
h=l;
}
void fun(Point &s)//定义的类外函数
{
cout<<s.Area()<<endl;
}
int main()
{
Rectangle rec(3.5,15.2,5.0,28.0);//定义的派生类的带参数的对象
fun(rec);//调用函数fun()
}
输出的结果为:0
程序分析:从输出的结果来看,该程序执行了Point::Area()的结果。派生类Rectangle的对象rec作为函数fun()的实参,而该函数的形参是类Point()的对象的引用s。在程序编译阶段,对象引用s所执行的Area()操作被联编到Point类的函数上。因此在执行函数fun()中的s.Area()操作时,返回值为0。
通过对象指针进行的普通成员函数的调用,仅仅与指针的类型有关,而与此刻指针正指向什么对象无关。要想实现当指针指向不同对象时执行不同的操作,就必须将基类中相应的成员函数定义为虚函数,进行动态联编。
动态联编
动态联编是指在程序运行时进行的联编,这种联编又称为晚期联编。 动态联编的优点是灵活性强,但效率低。
动态联编要求在运行时解决程序中的函数调用与执行该函数代码间的关系,又称为动态束定。在上述的例题中,由于是静态联编,函数fun()中的参数s所引用的对象被联编到类Point上。那么实行动态联编,则s所引用的对象将被联编到类Rectangle上。由此可见,对于同一个对象的引用,采用不同的联编方式将会被联编到不同类的对象上,即不同联编可以选择不同的实现,这便是多态性。实际上是对于函数fun()的参数的多态性选择。联编是一种重要的多态性选择。
实现动态联编需要同时满足以下三个条件:
- 必须把动态联编的行为定义为类的虚函数。
- 类之间应满足子类型关系,通常表现为一个类从另一个类公有派生而来。
- 必须先使用基类指针指向子类型的对象,然后直接或者间接使用基类指针调用虚函数。
上述的例题可以改为:
#include <iostream>
using namespace std;
class Point
{
public:
Point(double i,double j)//基类的构造函数
{
x=i;y=j; }
virtual double Area() const//定义的虚函数
{
return 0.0; }
private:
double x,y;
};
class Rectangle:public Point//公有继承的派生类
{
public:
Rectangle(double i,double j,double k,double l);//声明派生类的构造函数
virtual double Area() const//派生类的虚函数
{
return w*h; }
private:
double w,h;
};
Rectangle::Rectangle(double i,double j,double k,double l):Point(i,j)//派生类构造函数的函数体
{
w=k;
h=l;
}
void fun(Point &s)//定义的类外函数
{
cout<<s.Area()<<endl;
}
int main()
{
Rectangle rec(3.5,15.2,5.0,28.0);//定义的派生类的带参数的对象
fun(rec);//调用函数fun()
}
输出的结果为:140.
程序分析:该程序中说明了虚函数,fun()函数的对象引用参数s被动态联编,该函数体内调用的Area()函数是在运行中确定的,而不是在编译时确定的,因此在运行时,实参rec为类rectangle的对象,于是Area()函数被确定为类Rectangle的Area()函数。
例6: 析构函数可以为virtual型,构造函数则不能。那么为什么构造函数不能为虚呢?
虚函数采用一种虚调用的办法。虚调用是一种可以在只有部分信息的情况下工作的机制,特别允许我们调用一个只知道接口而不知道其准确对象类型的函数。但是如果要创建一个对象,你势必要知道对象的准确类型,因此构造函数不能为虚。
例7: 如果虚函数是非常有效的,我们是否可以把每个函数都声明为虚函数?
不行,这是因为虚函数是有代价的:由于每个虚函数的对象都必须维护一个虚表,因此在使用虚函数的时候都会产生个系统开销。如果仅是个很小的类,且不想派生其他
类,那么根本没必要使用虚函数。
例8: 析构函数可以是内联函数吗?
析构函数可以是内联函数。
例9: 重载和覆盖有什么不同?
override是指派生类重写基类的虚函数, overload约定成俗地被翻译为 “重载",是指编写一个与已有函数同名,但是参数表不同的函数。 例如一个函数既可以接收整型数作为参数,也可以接收浮点数作为参数。 重载不是一 种面向对象的编程, 而只是一种语法规则, 重载与多态没有什么直接关系。
例10: 在C++中, 你不应该从以下哪个抛出异常?
A. Constructor (构造函数)
B. Destructor (析构函数)
C. Virtual function (虚方法)
D. None of the above (以上答案都不对)
构造函数中抛出异常是有一定必要的,试想如下情况:
构造函数中 有两次new操作,第一次成功了,返回了有效的内存,而第二次失败,此时因为对象构造尚未完成,析构函数是不会调用的,也就是delete语句没有被执行,第一次new出的内存就悬在那儿了(发生内存泄露), 所以异常处理程序可以将其暴露出来。
构造函数中遇到异常是不会调用析构函数的, 一个对象的父对象的构造函数执行完毕,不能称之为构造完成,对象构造是不可分割的,要么完全成功, 要么完全失败,C++保证这一点。 对于成员变量,C++遵循这样的规则,即会从异常的发生点按照成员变量的初始化的逆序释放成员。 举例来说,有如下初始化列表:
A:: A() : ml(), m2(), m3(), m4(), m5() () {
};
假定m3的初始化过程中抛出异常,则会按照m2,ml的顺序调用这两个成员的析构函数。 在{}之间发生的未捕捉异常,最终会导致在栈的开解时析构所有的数据成员。
处理这样的问题, 使用智能指针是最好的, 这是因为auto_ptr
成员是一个对象而不是指针。 换句话说,只要不使用原始的指针,那么就不必担心构造函数抛出异常而导致资源泄露。
所以在C++中,资源泄露的问题一般都用RAII(资源获取即初始化)的办法:把需要打开/关闭的资源用简单的对象封装起来(这种封装可以同时有多种用处,比如隐藏底层API细节,以利于移植)。 这可以省去很多的麻烦。
如果不用RAII,即使当前构造函数里获取的东西在析构函数里都释放了,如果某天对类有改动,要新增加一种资源,构造函数里一般能适当地获取,但记不记得要在析构函数里相应地释放呢?失误的比例很大。 如果考虑到构造函数里抛出异常,就更复杂了。 随着项目的不断扩大和时间的推移,这些细节不可能都记得住,而且,有可能会由别人来实施这样的改动。
- C++中通知对象构造失败的唯一方法,就是在构造函数抛出异常
- 当对象发生部分构造异常时,已经构造完毕的子对象将会逆序地被析构(即异常发生点前面的对象; 而还没有开始构建的子对象将不会被构造了(即异常发生点后面的对象, 当然它也就没有析构过程了; 还有正在构建的子对象和对象自己本身将停止继续构建(即出现异常的对象),并且它的析构是不会被执行的。
析构函数抛异常
Effective C++建议,析构函数尽可能地不要抛出异常。 设想如果对象出了异常, 现在异常处理模块为了维护系统对象数据的一致性,避免资源泄漏,有责任释放这个对象的资源,调用对象的析构函数,可现在假如析构过程又再出现异常,那么请问由谁来保证这个对象的资源释放呢?而且这新出现的异常又由谁来处理呢?不要忘记前面的一个异常目前都还没有处理结束,因此陷入了一个矛盾之中,或者说处于无限的递归嵌套之中。 所以C++标准就做出了这种假设。