交易系统开发技能之c++ 面试题

Posted BBinChina

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了交易系统开发技能之c++ 面试题相关的知识,希望对你有一定的参考价值。

概要:

C++当前在交易系统开发技能中还占主要核心位置,其核心特性在于保障低时延,以及提供系统处理能力。当然其也有缺点:写好c++真的很难,经常容易因为内存泄漏,操作越界等原因导致系统崩溃。程序的稳健性也是系统的考量目标之一,所以在掌握语言特性的本身,也可以学习其他新的语言,比如Rust。回到正题,本章内容主要列了关于c++面试的常见问题,除此之外,还希望大家掌握 STL、Modern C++、Boost库等。

Q1 空类实例化对象的sizeof

class Test 
;
Test t;
Test t2;

一个对象的大小大于等于所有非静态成员大小的总和,这里没有任何成员,那么是不是应该为0呢?

其实不然,如果值为0的话,那么t 与 t2 就应该是同个实例吧。其实每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器会往隐藏一个字节。

Q2 指针操作优先级 *p++

const char* p = "Test";

c++中, 运算符优先级高,*p++ 等同于 (p++), p++会返回旧值,§ 输出T后,p指向e字符

Q3 c-string作为map key时会有什么问题

以上代码定义了key为const char* 的map,其key的默认比较函数参数为两个 char 指针,而两个不同的指针可以指向同样的字符串内容,那么违背了key的唯一性,我们需要自定义比较函数,来处理C-字符串。

struct StrCmp 
	bool operator()(char const *a, char const *b) const 
		//是遍历c字符串比对
		return std::strcmp(a, b) < 0;
	

//
map<const char *, int , StrCmp> strMap;

Q4 虚函数表的Sizeof大小

class Test 
public:
 virtual ~Test() 
 
;

Test t;

可以看到Test类包含了虚函数,为了实现多态,编译器采用了虚函数表的方式,即t对象包含了一个指针,这个指针指向了虚函数表,即sizeof(t)统计了指针的大小(32位系统指针大小为 4, 64位系统指针大小为8)

Q5 new/new[]/malloc 对应 delete/delete[]/free

int *p = new int[10];
p++;
delete[] p;

以上代码是不安全的,p++ 将指针移动到指向p+1的地址,该地址并不是new[] 分配的,所以使用delete[] 不是合法行为。

再看以下代码:

int main() 
	int x = 5;
	delete &x;

x是一个栈变量,并不是通过new创建的,所以使用delete也是不合法行为。

又比如以下代码:

class Test ;
Test *p = new Test();
free(p);

因为p是通过new创建的,所以不能通过free释放,而应该通过delete,只有malloc分配的内存可有free关键字释放

Q6 统计对象的sizeof

class Test 
public:
	static int x;
	char c;
;
int Test::x = 3;
Test t;

在前文讲到过,sizeof统计的是对象非静态成员的大小,那么sizeof(t) 只计算了char c,即 sizeof(t) 输出 1个字节,因为Test已经不是一个空类了, 所以编译器没必要隐含一个字节。

Q7 类的默认构造函数

class Test 
public:
	Test(const Test& obj) 
	   
;
Test t;

以上代码不能正常编译,因为我们自定义了Test的构造函数,所以编译器不会创建默认构造函数,这个时候 t的实例化因为没有合适的构造函数而失败。

通常我们会自定义构造函数来组织编译器这种默认行为,默认构造函数会初始化成员变量,如果某成员无法初始化(没有默认构造函数时),会编译失败。

当需要自定义构造函数又需要默认构造函数时,可以这样定义:

Test() = default

Q8 类的构造函数

class Test
private:
	int *p;
	
public: 
	Test() 
		p = new int(10);
	
	~Test()
		delete p;
	

	//拷贝构造函数 Test t; Test t1(t);
	Test(const Test& obj) 
		this->p = obj.p;
	

	//赋值构造函数 Test t; Test t1 = t;
	Test& operator=(const Test& obj) 
		this->p = obj.p;
		return *this;
	

	//modern c++ 移动构造函数
	Test(Test &&obj) noexcept 
		this->p = obj.p;
		obj.p = nullptr;
	
;

因为Test类包含了指针的成员变量,如果使用编译器默认的构造函数,会引发c++中的深拷贝、浅拷贝问题,所以需要我们自定义赋值、拷贝构造函数来屏蔽编译器默认的构造函数。
深拷贝和浅拷贝主要是针对类中的指针和动态分配的空间来说的,浅拷贝只是简单的复制指针的地址,并不是实例化对象,在没有自定义赋值构造函数情况下:
Test t; Test t1(t); 构造两个实例化对象 t跟t1时, t1的p指针与t的指针地址一致(浅拷贝复制指针值),当t析构时会释放t的指针p,而t1进行析构时,因为t1.p指向的地址内存已经被t释放了,造成了double free的情况。

Modern c++ 引入了左值、右值的概念,但存在有所有权要转移情况下,优化临时变量的创建,我们可以定义移动构造函数。
拷贝构造函数里 使用了&符号,其表示的是一个左值引用,可使用&&来表示右值,右值表示所引用的对象将要被销毁,该对象没有其他的用户,可以被当前使用者窃取数据,类似于rust所讲的所有权转移。这里的函数使用了noexcept关键字,表示当前函数不会抛出异常,即通知标准库不需要对异常处理做额外的工作,可以提升效率。

因为是窃取obj的数据,而obj表示即将销毁的对象,那么它会调用析构函数,同样会释放obj的p指针,所以这里将其值设置为nullptr,避免窃取过来的内存被obj给释放。

如果不需要自定义构造函数又不想让编译器默认生成构造函数时,我们可以使用delete关键字

class Test
private:
	int *p;
	
public: 
	Test() 
		p = new int(10);
	
	~Test()
		delete p;
	
	
	Test(const Test& obj) delete;
	
	Test& operator=(const Test& obj) = delete;
;

Q9 构造函数、析构函数是否可抛出异常

因为构造函数不像其他的函数有返回值(比如返回错误码),而当需要在构造函数做资源准备等一些逻辑操作时,可以抛出异常来处理实例化的错误情况。当然了,也可以像windows api一样,通过getlasterror来查看是否有错误,可使用threalocal的存储技术。

析构函数在编程规范上不建议抛出异常,而是在析构函数里将所有的错误情况处理完再正常退出实例,保证实例的生命周期是完整的。

Q10 虚函数表

class B 
public:
	virtual void f() 
	virtual void g() 
;

class D : public B 
public:
	virtual void h() 
	void f() 
;

为了实现多态(存在虚函数时),类会维护虚函数表,表内保存的是当前类的虚函数。
B的虚函数表:

&B::f
&B::g

D的虚函数表:

&B::g
&D::f
&D::h

Q11 子类实例化对象的虚函数调用

class Base
public:
	virtual void f() 
		cout<< "Base::f()" <<endl;
	
;

class Derived : public Base 
private:
	void f() 
		cout<< "Derived::f()" <<endl;	
	
;

int main() 
	Base* p = new Derived();
	p->f();

以上代码将输出"Derived::f()", 尽管Derived的f函数为private,但是在编译时,编译器检测到p类型为Base,而Base的f函数是public的,所有p->f可以调用,而当在运行时,会根据实例化对象的虚函数表查找到真正函数的地址,从而调用的是Derived的f函数。

Q12 多线程环境下实现单例模式

1、从c++11 开始,当第一个线程开始初始化一个本地静态变量时,其他线程会阻塞直到初始化完成,所以我们可以使用以下方式实现单例

static Singleton* getSingletonInstance() 
	//第一个访问的线程初始化instance时,其他线程阻塞,静态变量只进行一次初始化,所以完成单列化
	static Singleton instance;
	return &instance;

2、如果是旧版本地化,可以采用call_once来制定函数只会被调用一次

class Singleton 
public:
	static Singleton& getInstance() 
		//获取实例时,只有首次调用的线程进行一次调用,其他线程阻塞
		//call_once需要once_flag变量配合,相当于锁的概念
		std::call_once(m_onceFlag, []m_instance.reset(new Singleton()););
		return *(m_instance.get());
	
	
	//可以通过assert检测 count是否为1
	static int getCount() 
		return count;
	
private:
	singleton() 
		cout<<"Constructor called"<<endl;
		++count;
	
	//屏蔽掉赋值、拷贝构造函数
	Singleton(const Singleton& that) = delete;
	Singleton& operator=(const Singleton& that) = delete;

	int x;
	static std::once_flag m_onceFlag;
	static std::unique_ptr<Singleton> m_instance;
	static int count;
;

int Singleton::count = 0;
std::once_flag Singleton::m_onceFlag;
std::unique_ptr<Singleton> m_instance;

Q13 实现智能指针

智能指针采用引用计数方式,增加一处引用时 计数+1, 减少一处引用时,计数-1.
当计数为0时,表示内存可被释放

引用计数类

class RefCounter 
	//控制引用计数的构造只有智能指针类内部构造
	RefCounter(const RefCounter&)=delete;
	RefCounter& operator=(const RefCounter&)=delete;
private:
	//计数值
	int _counter;
public:
	RefCounter() 
		_counter = 0;
	

	int get() 
		return _counter;
	
	//i++
	void operator++() 
		_counter++;
	
	//++i
	void operator++(int) 
		_counter++;
	
	void operator--()
		_counter--;
	
	void operator--(int)
		_counter--;
	

智能指针模板

template <typename T>
class SmartPtr 
	RefCounter* counter;
	T* obj;

public:
	SmartPtr(T* raw) 
		counter = new RefCounter();
		obj = raw;
		if(obj) (*counter)++;
	
	SmartPtr(const SmartPtr& that) 
		counter = that.counter;
		(*counter)++;
		obj = that.obj;
 	
 	SmartPtr& operator=(const SmartPtr& that) 
		//检测自我赋值情况
		if(this != that) 
			//减少当前对象的引用计数
			(*counter)--;
			//如果当前对象的引用计数为0, 那么可以进行释放了
			if (counter->get() == 0) 
				delete counter;
				delete obj;
			
			counter = that.counter;
			(*counter)++;
			obj = that.obj;
		
		return *this;
	

	int use_count() 
		return counter->get();
	

	~SmartPtr() 
		(*counter)--;
		if (counter->get() == 0) 
			delete counter;
			delete obj;
		
	
;

之前也实现过一篇c++的引用计数
引用计数类

Q14 dynamic_cast进行动态转换

dynamic_cast时在运行时检查转换是否安全,当从子类指针向父类指针转换时,会检测是否可以转换,当转换失败时,如果是指针,那么返回nullptr,如果是引用,那么抛出异常。

Q15 CRTP奇异递归模板,实现静态多态分发

CRTP(Curiously Recurring Template Pattern) ,是一种c++模板编程中的常用模式,其形式是将派生类作为基类的模板参数,其一般形式如下:

template <class T>
class Base ...;

class X : public Base<X> ...;

使用CRTP的目的,看以下代码的演变:

//实现多态,需要调用do_work时,需要通过具体实例化对象的虚函数表来调用对应的函数do_work

class interface 
public:
	virtual void process() = 0;
;

class Impl : public Interface 
public:
	virtual void process() ...
;

void do_work(Interface* obj) 
	obj->process();

Interface* obj = new Impl();
do_work(obj);

//使用CRTP来实现静态动态分发,即在编译时可以确认调用的do_work函数,而不需要再通过虚函数表的方式来动态调用对应的do_work函数

template <typename Impl>
class Interface 
public:
	void process() 
		impl().process();
	
private:
	Impl& impl() 
		//实现静态分发
		return *static_cast<Impl*>(this);
	
;

class Impl : public Interface<Impl> 
public:
	void process() 
		...
	
;

void do_work(Interface<Impl>* obj) 
	obj->process();


Interface<Impl>* obj = new Impl();
do_work(obj);

可以看到从定义虚函数到静态多态分发的演化,相当于静态调用函数,优化虚函数表查找调用的性能。

Q16 placement new的用法及用途

placement new 时new操作符的重载,通常的new 指令执行了两个动作:
1、分配内存
2、在分配的内存上构造对象

而 placement new将以上两个动作分离出来,意味着我们可以先分配内存,再通过在分配好的内存上构造对象,通常用于内存池场景:内存池管理着预先分配的内存,从内存池获取到要求大小的内存后,再执行placement new构造对象,当对象需要销毁时,我们可以将回收的内存重新放回内存池供其他对象构建使用,而避免了频繁动态分配内存的操作

class Test;
//预先分配内存
char *buf = new char[10 * sizeof(Test)];
//在分配好的内存上构造对象
Test * tp = new (buf)Test();

Q17 左值右值

左值右值是C++11后的特性,在前文也讲到过左值右值,让我们看一段代码:

class Test;

void foo(Test& t);
void foo(Test&& t);

Test t;

Test getTest() 
	Test t;
	return t;


int main() 
	foo(t);
	foo(getTest());

左值是一个持久化的对象,如栈对象t,所以调用foo(t)时,执行的是void foo(Test& t)

右值表示即将销毁的对象,比如getTest返回值是一个临时变量,如果调用foo(getTest())时,执行的是void foo(Test&& t)。

Q18 move语义

在Q17的左值、右值概念上,c++11提供了std::move这个方法来将左值参数无条件的转换为右值,即得到右值引用 &&,通常用于实现移动构造函数(Q8)、移动赋值函数,实现数据的窃取转移,优化临时变量的创建开销,被窃取后的对象不可用。

Q8里实现了移动构造函数

class Test
	//modern c++ 移动构造函数
	Test(Test &&obj) noexcept 
		this->p = obj.p;
		obj.p = nullptr;
	
;

Test t1(std::move(getTest()))

Q19 复制省略 copy elision

class Test 
public:
	Test()
	~Test()
	Test(const Test&)
;

Test foo() 
	return Test();


int main() 
	Test t = foo();

在C++11 之前,调用foot()返回构造的Test对象时,会生产临时类对象变量,需要调用拷贝构造函数来将构造t,即这里调用了两次(栈对象拷贝给临时变量,临时变量拷贝给t)。
copy elision时通过编译器优化,让返回的对象直接在t上构造。

Q20 std::shared_ptr是线程安全的吗?

为了减少内存泄漏问题,c++使用了智能指针的方式来协助开发者管理分配的内存。
在Q13的时候,我们实现了自己的智能指针,而shared_ptr同样采用的引用计数的方式,多线程环境下,引用计数为其race data。
1、当多线程操作的是同一个shared_ptr对象时:

//多线程使用同一sp对象
std::thread td([&sp]()
);

//函数指针
void fn(shared_ptr<T>& sp) 
	//调用复制构造函数
	sp = other_sp;

复制构造函数的操作:sp原先的引用计数值减一,other_sp的引用计数值加一,这两个动作不符合原子性,所以多线程操作同一对象时会存在问题。

2、当多线程操作的是不同shared_ptr对象时:

//值传递,让多线程使用的是不同的shared_ptr对象
std::thread td([sp]()
);

//函数指针
void fn(shared_ptr<T> sp) 
	//调用复制构造函数
	sp = other_sp;

因为是不同的shared_pt,管理的是自己不同引用计数。

Q21 策略模式

template<class T, class Allocator = std::allocator<T>> class vector;

在stl源码中经常看到容器的定义如上,模板参数Allocator采用策略模式使vector类有多种内存分配释放的方式,如果没有设置内存分配策略的话,将采用stl默认分配类。

Q22 std::list 与 std::vector遍历元素时的性能对比

list采用链表、vector采用数组的方式管理元素。数组的优势在于元素在内存上是连续存储的。
Cpu会有prefetch的预加载,并不是每次读数据才去内存里加载,根据空间局部性实现,即当前访问的数据和指令可能与目前正在使用的数据和指令在地址空间上相邻或者相近。当访问vector数据时,cpu可根据cache line大小预先加载连续的内存数据,所以其遍历性能比list优秀,而list不是缓存友好的,因为其数据通常指向的内存不连续,所以无法通过预加载的方式优化访问速度,即常说的cache miss

Q23 stl容器是线程安全吗?

当并发线程访问同一stl容器对象的const 成员函数时,才是线程安全的,const 函数表示不改变成员变量

Q24 类成员变量的初始化顺序

class Test 
	int y, x;
public:
	Test(int i) : x(i), y(x + 1) 
	
;

类成员的初始化顺序由其在类中的声明决定,即先 初始化 y 再 初始化 x。
而构造的初始化列表先让x初始化, 再使用x+1初始化y,但会因为初始化y时,x内存还未分配导致程序执行时出现不合法行为。

<

以上是关于交易系统开发技能之c++ 面试题的主要内容,如果未能解决你的问题,请参考以下文章

交易系统开发技能及面试之低延迟编程技术

交易系统开发技能及面试之低延迟编程技术

交易系统开发技能及面试之低延迟编程技术

交易系统开发技能及面试之TechCoding

交易系统开发技能及面试之TechCoding

交易系统开发技能及面试之TechCoding