Luna's blog 月亮月亮酱

面试题--C++基础

2018-12-16

1. 指针和引用的区别

  1. 引用不可以为空,当被创建的时候,必须初始化,而指针可以是空值,可以在任何时候被初始化。

  2. 可以有const指针,但是没有const引用;

  3. 指针可以有多级,但是引用只能是一级(int ** p;合法 而 int && a是不合法的)

  4. 指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;

  5. 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

  6. ”sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;

  7. 指针和引用的自增(++)运算意义不一样;

2. hash表的实现

采用了开链法实现哈希表,其中每个哈希节点有数据和next指针

template<class _Val>
struct _Hashtable_node
{
  _Hashtable_node* _M_next;
  _Val _M_val;
};

可以实现一个简化的哈希表,创建时需要指定大小,插入时用特定的哈希函数计算哈希值,如果位置没有元素,直接放入,有元素使用链表头插入。
对于这个问题,之后有时间需要补充一个哈希表的实现

3. C++控制内存泄漏的方式

  1. 监控new,重载new,日志
  2. 智能指针
  3. 内存泄漏检测工具(Valgrind)
  4. 定期检查内存结果,查看是否有大量相同内存

4. 为什么不要在构造函数或析构函数中调用虚函数

见effective C++ 第9条,派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。 同样,进入基类析构函数时,对象也是基类类型。在子类对象的基类子对象构造期间,调用的虚函数的版本是基类的而不是子类的。

#include<iostream>
using namespace std;
class Base
{
public:
	Base()
	{
		Fuction();
	}
	virtual void Fuction()
	{
		cout << "Base::Fuction" << endl;
	}
};
class A : public Base
{
public:
	A()
	{
		Fuction();
	}
	virtual void Fuction()
	{
		cout << "A::Fuction" << endl;
	}
};
int main()
{
	A a;
}

输出结果:

Base::Fuction
A::Fuction

可以看到子类在构造父类的时候调用的是父类的虚函数。

5. 动态绑定怎么实现

有一个基类,两个派生类,基类有一个virtual函数,两个派生类都覆盖了这个虚函数。现在有一个基类的指针或者引用,当该基类指针或者引用指向不同的派生类对象时,调用该虚函数,那么最终调用的是该被指向对象对应的派生类自己实现的虚函数。
C++中,通过基类的引用或指针调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。

6.浅拷贝和深拷贝

在对象拷贝过程中,如果没有自定义拷贝构造函数,系统会提供一个缺省的拷贝构造函数,缺省的拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型成员变量,调用其相应类型的拷贝构造函数。

当对象中含有指针时,需要重新定义拷贝构造函数进行深拷贝,否则两个对象的指针指向同一块空间,容易产生bug。

7. 在什么情况下系统会调用拷贝构造函数

  1. 用类的一个对象去初始化另一个对象时

  2. 当函数的形参是类的对象时(也就是值传递时),如果是引用传递则不会调用

  3. 当函数的返回值是类的对象或引用时

8. 为什么要进行内存对齐

  1. 平台原因(移植原因) 不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

  2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐,原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

9. 调用约定

  1. 调用协议常用场合 __stdcall:Windows API默认的函数调用协议。
    __cdecl:C/C++默认的函数调用协议。
    __fastcall:适用于对性能要求较高的场合。
  2. 函数参数入栈方式 __stdcall:函数参数由右向左入栈。
    __cdecl:函数参数由右向左入栈。
    __fastcall:从左开始不大于4字节的参数放入CPU的ECX和EDX寄存器,其余参数从右向左入栈。
    __fastcall在寄存器中放入不大于4字节的参数,故性能较高,适用于需要高性能的场合。
  3. 栈内数据清除方式 __stdcall:函数调用结束后由被调用函数清除栈内数据。
    __cdecl:函数调用结束后由函数调用者清除栈内数据。
    __fastcall:函数调用结束后由被调用函数清除栈内数据。

10. RTTI(Run-Time Type Information)

RTTI提供了以下两个非常有用的操作符:

  1. typeid操作符,返回指针和引用所指的实际类型。
  2. dynamic_cast操作符,将基类类型的指针或引用安全地转换为派生类型的指针或引用。

11. 定位new

定位 new(placement new)允许我们向 new 传递额外的参数。

new (palce_address) type
new (palce_address) type (initializers)
new (palce_address) type [size]
new (palce_address) type [size] { braced initializer list }
  • palce_address 是个指针
  • initializers 提供一个(可能为空的)以逗号分隔的初始值列表 以上代码的作用是,在palce_address所指向的内存区域上新建一个类type的对象,用initializers初始化该对象。在这个过程中,palce_address所指向的内存区域的内容改变了,并且palce_address指向了新参数的对象。

    12.如何定义一个只能在堆上或者栈上生成对象的类?

    1. 限制在堆上

    方法:将析构函数设置为私有

    原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。

  1. 限制在栈上

    方法:将 new 和 delete 重载为私有

    原因:在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。


Similar Posts

Content