目录
1、空类里有哪些函数
默认构造函数、默认拷贝构造函数、默认析构函数、默认赋值运算符 这四个是我们通常大都知道的。但是除了这四个,还有两个,那就是取址运算符和 取址运算符 const
即总共有六个函数。
class Empty
{
public:
Empty(); // 缺省构造函数
Empty( const Empty& ); // 拷贝构造函数
~Empty(); // 析构函数
Empty& operator=( const Empty& ); // 赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&() const; // 取址运算符 const
};
但是,C++默认生成的函数,只有在被需要的时候,才会产生。即当我们定义一个类,而不创建类的对象时,就不会创建类的构造函数、析构函数等。
2、构造函数可以是虚函数吗?为什么
构造函数不能声明为虚函数,原因是:
1. 存储空间角度:虚函数对应一个vtable,vtable存储于对象的内存空间
若构造函数是虚的,则需要通过 vtable来调用,若对象还未实例化,即内存空间还没有,无法找到vtable
2、虚拟函数调用只需要“部分的”信息,即只需要知道函数接口,而不需要对象的具体类型。但是构建一个对象,却必须知道具体的类型信息。如果你调用一个虚拟构造函数,编译器怎么知道你想构建是继承树上的哪种类型呢?所以这在逻辑上是一个悖论。(在调用构造函数时,并不知道构造的是基类还是派生类,所以没办法判断调用哪个版本的构造函数。所以,这是个悖论。)
3、浅拷贝深拷贝
拷贝构造函数默认的是浅拷贝。当不涉及到堆内存时用浅拷贝完全可以,否则就需要深拷贝了。
浅拷贝相当于一个箱子有多个钥匙,但其中一个人打开箱子取走箱子里的东西时,其他人是不知道的。
深拷贝是有多个箱子每个箱子对应一个钥匙,但一个人取走他的钥匙对应的箱子里的东西时,不会对其他人产生影响。
在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。
总结:浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
3.浅拷贝带来问题的本质在于析构函数释放多次堆内存,使用std::shared_ptr,可以完美解决这个问题。
4、写时复制
浅拷贝与深拷贝的优缺点分别互为彼此的优缺点。有什么办法可以兼有二者的优点?
主要解决问题:
- 数据相同时只有一份内存。
- 不会出现多次释放问题。
写入时复制(COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这个过程对其他的调用者是透明的(transparently)。此作法的主要优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作是可以共享同一份资源。
5、智能指针有哪些,使用场景分别是
使用 C++ 的指针可以动态开辟存储空间,但若在使用完毕后忘记释放(或在释放之前,程序 throw 出错误,导致没有释放),导致该内存单元一直被占据直到程序结束,即发生了所谓的内存泄漏。
【注】内存泄漏是指堆内存的泄漏。堆,就是那些由 new 分配的内存块。
因此智能指针的作用就是为了保证使用堆上对象的时候,对象一定会被释放,但只能释放一次,并且释放后指向该对象的指针应该马上归 0。
C++11中推出了三种智能指针,unique_ptr、shared_ptr和weak_ptr,同时也将auto_ptr置为废弃
unique_ptr:专属所有权
我们大多数场景下用到的应该都是unique_ptr。unique_ptr,等于 boost 库中的 scoped_ptr,正如其名字所述,scoped_ptr 所指向的对象在作用域之外会自动得到析构。
unique_ptr代表的是专属所有权,即由unique_ptr管理的内存,只能被一个对象持有。
所以,unique_ptr不支持复制和赋值
性能
因为C++的zero cost abstraction的特点,unique_ptr在默认情况下和裸指针的大小是一样的。
所以内存上没有任何的额外消耗,性能是最优的。
使用场景1:忘记delete
unique_ptr一个最简单的使用场景是用于类属性。代码如下:
class Box{
public:
Box() : w(new Widget())
{}
~Box()
{
// 忘记delete w
}
private:
Widget* w;
};
如果因为一些原因,w必须建立在堆上。如果用裸指针管理w,那么需要在析构函数中delete w
;
这种写法虽然没什么问题,但是容易漏写delete语句,造成内存泄漏。
如果按照unique_ptr的写法,不用在析构函数手动delete属性,当对象析构时,属性w
将会自动释放内存。
使用场景2:异常安全
假如我们在一段代码中,需要创建一个对象,处理一些事情后返回,返回之前将对象销毁,如下所示:
void process()
{
Widget* w = new Widget();
w->do_something(); // 可能会发生异常
delete w;
}
在正常流程下,我们会在函数末尾delete创建的对象w,正常调用析构函数,释放内存。
但是如果w->do_something()发生了异常,那么delete w
将不会被执行。此时就会发生内存泄漏。
我们当然可以使用try...catch捕捉异常,在catch里面执行delete,但是这样代码上并不美观,也容易漏写。
如果我们用std::unique_ptr,那么这个问题就迎刃而解了。无论代码怎么抛异常,在unique_ptr离开函数作用域的时候,内存就将会自动释放。
shared_ptr:共享所有权
在使用shared_ptr之前应该考虑,是否真的需要使用shared_ptr, 而非unique_ptr。
shared_ptr代表的是共享所有权,即多个shared_ptr可以共享同一块内存。
因此,从语义上来看,shared_ptr是支持复制的。
shared_ptr内部是利用引用计数来实现内存的自动管理,每当复制一个shared_ptr,引用计数会+1。当一个shared_ptr离开作用域时,引用计数会-1。当引用计数为0的时候,则delete内存。
性能
- 内存占用高 shared_ptr的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。 因此相比于unique_ptr, shared_ptr的内存占用更高
- 原子操作性能低 考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。
- 使用移动优化性能 shared_ptr在性能上固然是低于unique_ptr。而通常情况,我们也可以尽量避免shared_ptr复制。 如果,一个shared_ptr需要将所有权共享给另外一个新的shared_ptr,而我们确定在之后的代码中都不再使用这个shared_ptr,那么这是一个非常鲜明的移动语义。 对于此种场景,我们尽量使用std::move,将shared_ptr转移给新的对象。因为移动不用增加引用计数,因此性能比复制更好。
使用场景
- shared_ptr通常使用在共享权不明的场景。有可能多个对象同时管理同一个内存时。
- 对象的延迟销毁。陈硕在《Linux多线程服务器端编程》中提到,当一个对象的析构非常耗时,甚至影响到了关键线程的速度。可以使用
BlockingQueue<std::shared_ptr<void>>
将对象转移到另外一个线程中释放,从而解放关键线程。 -
尽管 shared_ptr 功能强大,但不是任何时候都有必要使用 shared_ptr。当你不确定使用的指针是不是被分享所有权的时候,默认选 unique_ptr 独占式所有权,因为 unique_ptr 效率比 shared_ptr 高,不需要维护引用计数和背后的控制块。当确定要被分享的时候可以转换成 shared_ptr。
weak_ptr
weak_ptr是为了解决shared_ptr双向引用的问题。(解决环状引用问题,告诉 C++ 环上哪一个引用是最弱的,因此在一个环上把原来的某一个 shared_ptr 改成 weak_ptr 就可以了。)
class B;
struct A{
shared_ptr<B> b;
};
struct B{
shared_ptr<A> a;
};
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
pa和pb存在着循环引用,根据shared_ptr引用计数的原理,pa和pb都无法被正常的释放。
对于这种情况, 我们可以使用weak_ptr:
class B;
struct A{
shared_ptr<B> b;
};
struct B{
weak_ptr<A> a;
};
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
weak_ptr不会增加引用计数,因此可以打破shared_ptr的循环引用。
通常做法是parent类持有child的shared_ptr, child持有指向parent的weak_ptr。这样也更符合语义。
auto_ptr
auto_ptr主要是用来解决资源自动释放的问题,当auto_ptr对象生命周期结束时,其析构函数会将auto_ptr对象拥有的动态内存自动释放,即使发生异常,通过异常栈的展开过程也能将动态内存释放
为什么被废弃
auto_ptr采用copy语义来转移指针资源,转移指针资源的所有权的同时将原指针置为NULL,这跟通常理解的copy行为是不一致的(不会修改原数据),而这样的行为在有些场合下不是我们希望看到的。
这也就是用unique_ptr代替auto_ptr的原因,
本质上来说,就是unique_ptr禁用了copy,而用move替代。
auto_ptr<Obj> ptr1( new Obj() );
ptr1->FuncA();
auto_ptr<Obj> ptr2 = ptr1;
ptr2->FuncA();
ptr1->FuncA(); // 这句话会异常
为什么在把ptr1复制给ptr2之后ptr1再使用就异常了呢?
这也正是他被抛弃的主要原因。因为auto_ptr复制构造函数中把真是引用的内存指针进行的转移,也就是从ptr1转移给了ptr2,此时,ptr2引用了Obj内存地址,而ptr1引用的内存地址为空,此时再使用ptr1就异常了。
6、智能指针怎么实现的
智能指针其实就是模拟指针动作的类,所以智能指针类一般都会重载 -> 和 * 操作符。智能指针对象能在资源不再被使用的时候自动释放资源,也就是在智能指针对象析够的时候自动去释放其管理的资源。
- 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
- 智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
share_prt 简单设计和实现
设计需求:
1.该智能指针能接受各种类型的指针 -- 使用模板
2.智能指针需要知道该对象有多少个人在用他 -- 大家用的同一个计数器 int * num;
3.共同使用同一个内存 -- 数据指针 T * data;
ps:目前为线程不安全的实现,若要达到线程安全,需要使用一个共同的互斥变量。
#ifndef SHARE_PTR_H
#define SHARE_PTR_H
#include <stdio.h>
#include <iostream>
template<typename T>
class share_ptr{
private:
T * data;
int * num;
public:
/**
* @brief 构造函数,通过传入的对象指针进行初始化,
* 并将计数器置1
*/
share_ptr(T * t):data(t){
num = new int;
*num = 1;
}
/**
* @brief 析构函数,判断当前对象是否为最后一个,
* 是就delete,不是就计数减1
*/
~share_ptr(){
if(*num>1){
(*num)--;
}else{
delete data;
delete num;
data = NULL;
num = NULL;
}
}
/**
* @brief 拷贝构造函数,通过rhs的值赋值,
* 并将计数器加1
*/
share_ptr(share_ptr<T>& rhs){
data = rhs.data;
num = rhs.num;
(*num)++;
}
/**
* @brief 赋值,判断当前对象是否一致,
* 是则返回,不是则析构之前的,并用现在的赋值,
* 计数器加1
*/
share_ptr<T>& operator =( share_ptr<T>& rhs){
if( data == rhs.data){
return *this;
}else{
//判断本来指向的指针是否是最后一个,是的话,就delete掉,不是的话就*num--
if(*num == 1){
delete data;
delete num;
data = NULL;
num = NULL;
}else{ (*num)--; }
data = rhs.data;
num = rhs.num;
(*num)++;
}
return *this;
}
/**
* @brief 返回数据的引用
*/
T& operator *(){ return *data; }
/**
* @brief 返回数据的指针
*/
T* operator ->() { return data; }
/**
* @brief 获取当前有多少个共同使用者
*/
int count(){ return *num;}
};
#endif //SHARE_PTR_H
unique_ptr的实现
template<typename T>
class MyUniquePtr
{
public:
MyUniquePtr(T* ptr = nullptr)
:mPtr(ptr)
{}
~MyUniquePtr()
{
if(mPtr)
delete mPtr;
}
MyUniquePtr(MyUniquePtr &&p);
MyUniquePtr& operator=(MyUniquePtr &&p);
/* 不支持拷贝与赋值 */
MyUniquePtr(const MyUniquePtr &p) = delete;
MyUniquePtr& operator=(const MyUniquePtr &p) = delete;
T* operator*() const {return mPtr;}
T& operator->()const {return *mPtr;}
void reset(T* q = nullptr)
{
if(q != mPtr){
if(mPtr)
delete mPtr;
mPtr = q;
}
}
//u.release() u 放弃对指针的控制权,返回指针,并将 u 置为空
T* release()
{
T* res = mPtr;
mPtr = nullptr;
return res;
}
T* get() const {return mPtr;}
void swap(MyUniquePtr &p)
{
using std::swap;
swap(mPtr, p.mPtr);
}
private:
T* mPtr;
};
template<typename T>
MyUniquePtr<T>& MyUniquePtr<T>::operator=(MyUniquePtr &&p)
{
if(*this != p)
{
if(mPtr)
delete mPtr;
mPtr = p.mPtr;
p.mPtr = NULL;
}
return *this;
}
template<typename T>
MyUniquePtr<T> :: MyUniquePtr(MyUniquePtr &&p) : mPtr(p.mPtr)
{
p.mPtr == NULL;
}
7、队列的实现
队列(Queue),是一种线性存储结构。它有以下几个特点:
(01) 队列中数据是按照"先进先出(FIFO, First-In-First-Out)"方式进出队列的。
(02) 队列只允许在"队首"进行删除操作,而在"队尾"进行插入操作。
队列通常包括的两种操作:入队列 和 出队列。
下面是数组实现的队列,能存储任意类型的数据。
#ifndef ARRAY_QUEUE_HXX
#define ARRAY_QUEUE_HXX
#include <iostream>
using namespace std;
template<class T> class ArrayQueue{
public:
ArrayQueue();
~ArrayQueue();
void add(T t);
T front();
T pop();
int size();
int is_empty();
private:
T *arr;
int count;
};
// 创建“队列”,默认大小是12
template<class T>
ArrayQueue<T>::ArrayQueue()
{
arr = new T[12];
if (!arr)
{
cout<<"arr malloc error!"<<endl;
}
}
// 销毁“队列”
template<class T>
ArrayQueue<T>::~ArrayQueue()
{
if (arr)
{
delete[] arr;
arr = NULL;
}
}
// 将val添加到队列的末尾
template<class T>
void ArrayQueue<T>::add(T t)
{
arr[count++] = t;
}
// 返回“队列开头元素”
template<class T>
T ArrayQueue<T>::front()
{
return arr[0];
}
// 返回并删除“队列末尾的元素”
template<class T>
T ArrayQueue<T>::pop()
{
int i = 0;
T ret = arr[0];
count--;
while (i++<count)
arr[i-1] = arr[i];
return ret;
}
// 返回“队列”的大小
template<class T>
int ArrayQueue<T>::size()
{
return count;
}
// 返回“队列”是否为空
template<class T>
int ArrayQueue<T>::is_empty()
{
return count==0;
}
#endif
8、线程中的几种锁
在多处理器系统环境中需要保护资源避免由于并发带来的资源访问竞争导致的问题,就需要互斥访问,也就是需要引入锁的机制。只有获取了锁的进程才能访问资源。
当多个线程并发访问共享资源时,有可能产生并发访问的安全性问题,可能会导致共享资源被破坏,导致非预期的结果。比如C++ STL当中的vector,map等等都是非并发安全的容器。如果想要解决并发访问的安全性问题就需要引入线程同步机制。
线程间同步指的是:当有一个线程在对共享资源进行操作时,其他线程都不可以对这个资源进行操作,直到该线程完成操作。简单来说,就是线程之间需要达到协同一致。
一般线程间同步机制有:共享内存,信号量机制,锁机制,信号机制等等。其中对于锁的使用是最普遍的方式。
线程之间的锁有:互斥锁、条件锁、自旋锁、读写锁、递归锁。
互斥锁:
互斥锁的作用:
互斥锁是为实现保护共享资源而提出一种锁机制。采用互斥锁保护临界区,防止竞争条件出现。当某个线程无法获取互斥锁时,该线程会被挂起,当其他线程释放互斥锁后,操作系统会唤醒被挂起在这个锁上的线程,让其运行。
互斥锁的实现:
在Linux下互斥锁的实现是通过futex这个基础组件。
互斥锁加锁解锁开销很大,需要从用户态切换到内核态,上下文切换以及涉及缓存的更新等等。通常很多同步操作发生的时候并没有竞争的产生,此时上述开销就没有必要。考虑到这个因素,futex通过用户空间的共享内存以及原子操作,在共享的资源不存在竞争的时候,不会进行系统调用而是只有当竞争出现的情况下再进行系统调用陷入内核。进程或者线程在没有竞争的情况下可以立刻获取锁。具体来说,futex的优化方式如下:
futex将同步过程分为两个部分,一部分由内核完成,一部分由用户态完成;如果同步时没有竞争发生,那么完全在用户态处理;否则,进入内核态进行处理。减少系统调用的次数,来提高系统的性能是一种合理的优化方式。
互斥锁的使用场景:
-
解决线程安全问题,一次只能一个线程访问被保护的资源。
-
被保护资源需要睡眠,那么可以使用互斥锁。
自旋锁:
自旋锁的作用:
自旋锁也是为实现保护共享资源而提出一种锁机制。自旋锁不会引起调用线程阻塞,如果自旋锁已经被别的线程持有,调用线程就一直循环检测是否该自旋锁已经被释放。
自旋锁的特点:
- 线程不会阻塞,不会在内核态和用户态之间进行切换。
- 消耗 CPU: 因为自旋锁会不断的去检测是否可以获得锁,会一直处于这样的循环当中,这个逻辑的处理过程消耗的 CPU相对其实际功能来说是浪费的。
自旋锁的实现:
CAS(compare and swap) 是实现自旋锁的基础。CAS 的实现基于硬件平台的指令。
CAS涉及到三个操作数:
- 需要读写的内存值 value1
- 进行比较的值 value2
- 拟写入的新值 value3
当且仅当 value1 的值等于 value2时,CAS通过原子方式用新值value3来更新value1的值,否则不会执行任何操作。可以理解为线程会不停的执行一个while循环进行CAS操作,直到达成条件。
自旋锁的使用场景:
- 如果预计线程持有锁的时间比较短,相比使用互斥锁两次上下文切换的开销而言,自旋锁消耗的CPU更少的情况下,那么使用自旋锁比互斥锁更高效。
- 如果代码当中经常需要加锁但是实际情况下产生竞争的情况比较少此时可以使用自旋锁进行优化。
- 被保护的共享资源需要在中断上下文访问,就必须使用自旋锁。
9、vector 内存分配与回收机制
vector所有的内存相关问题都可以归结于它的内存增长策略。vector有一个特点就是:内存空间只会增长不会减少。
在调用push_back时,若当前容量已经不能够放入心得元素(capacity=size),那么vector会重新申请一块内存,把之前的内存里的元素拷贝到新的内存当中,然后把push_back的元素拷贝到新的内存中,最后要析构原有的vector并释放原有的内存。所以说这个过程的效率是极低的,为了避免频繁的分配内存,C++每次申请内存都会成倍的增长。
我们常用的操作clear()和erase(),实际上只是减少了size(),清除了数据,并不会减少capacity,所以内存空间没有减少。那么如何释放内存空间呢,正确的做法是swap()操作。
即先创建一个临时拷贝与原先的vector一致,值得注意的是,此时的拷贝 其容量是尽可能小的符合所需数据的。紧接着将该拷贝与原先的vector v进行 交换。好了此时,执行交换后,临时变量会被销毁,内存得到释放。此时的v即为原先 的临时拷贝,而交换后的临时拷贝则为容量非常大的vector(不过已经被销毁)