1. (1) 简述智能指针的原理;(2)c++中常用的智能指针有哪些?(3)实现一个简单的智能指针。
- 简述智能指针的原理:智能指针是一种资源管理类,这个类在构造函数中传入一个原始指针,在析构函数中释放传入的指针。智能指针都是栈上的对象,所以当函数(或程序)结束时,会自动释放。
- c++中常用的智能指针:在C++11中的
中有unique_ptr
、shared_ptr
、weak_ptr
unique_ptr
:同一时刻只能由唯一的unique_ptr
指向给定对象,不支持拷贝操作和赋值操作。
shared_ptr
:可以多个指针指向相同的对象,通过引用计数机制,支持拷贝操作和赋值操作。每使用一次,内部引用计数加1,析构一次,引用计数减1,当计数为0时,释放所指的堆空间。
weak_ptr
:弱引用。 引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr
。顾名思义,weak_ptr
是一个弱引用,只引用,不计数。如果一块内存被shared_ptr
和weak_ptr
同时引用,当所有shared_ptr
析构了之后,不管还有没有weak_ptr
引用该内存,内存也会被释放。所以weak_ptr
不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr
是否为空指针.、。 - 实现一个简单的智能指针:
下面是一个基于引用计数的智能指针的实现,需要实现构造,析构,拷贝构造,=操作符重载,重载*和->操作符。
#include
template < typename T >
class SmartPointer
{
private:
T* _ptr;
size_t* _count;
void destory()
{
if( *_count == 0 )
{
delete _ptr;
delete _count;
}
}
void releaseCount()
{
if( _ptr )
{
(*_count)--;
destory();
}
}
public:
SmartPointer(T* p=0) : _ptr(p), _count(new size_t)
{
if( p )
{
*_count = 1;
}
else
{
*_count = 0;
}
}
SmartPointer(const SmartPointer& obj)
{
if(this != &obj)
{
_ptr = obj._ptr;
_count = obj._count;
++(*_count);
}
}
SmartPointer& operator=(const SmartPointer& obj)
{
if( _ptr == obj._ptr )
{
return *this;
}
releaseCount();
_ptr = obj._ptr;
_count = obj._count;
++(*_count);
return *this;
}
T& operator* ()
{
if( _ptr )
{
return *_ptr;
}
}
T* operator-> ()
{
if( _ptr )
{
return _ptr;
}
}
size_t count()
{
return *_count;
}
~SmartPointer()
{
destory();
}
};
using namespace std;
int main()
{
SmartPointer cp1(new char('a'));
cout << cp1.count() << endl;
SmartPointer cp2(cp1);
cout << cp1.count() << endl;
cout << cp2.count() << endl;
SmartPointer cp3;
cp3 = cp2;
cout << cp1.count() << endl;
cout << cp2.count() << endl;
cout << cp3.count() << endl;
return 0;
}
参考链接:
https://www.nowcoder.com/ta/nine-chapter/review?tpId=1&tqId=10787&query=&asc=true&order=&page=6
https://blog.csdn.net/worldwindjp/article/details/18843087
https://www.cnblogs.com/wxquare/p/4759020.html
2. 如何处理循环引用问题?
循环引用造成的原因:两个对象互相使用一个shared_ptr
成员变量指向对方的会造成循环引用。 如下代码
#include
#include
using namespace std;
class B;
class A
{
public: // 为了省去一些步骤这里 数据成员也声明为public
shared_ptr pb;
~A()
{
cout << "kill A\n";
}
};
class B
{
public:
shared_ptr pa;
~B()
{
cout <<"kill B\n";
}
};
int main(int argc, char** argv)
{
shared_ptr sa(new A());
shared_ptr sb(new B());
if(sa && sb)
{
sa->pb=sb;
sb->pa=sa;
}
cout<<"sa use count:"<
上面的代码运行结果为:sa use count:2, 注意此时sa,sb都没有释放,产生了内存泄露问题!!!
即A内部有指向B,B内部有指向A,这样对于A,B必定是在A析构后B才析构,对于B,A必定是在B析构后才析构A,这就是循环引用问题,违反常规,导致内存泄露。
一般来讲,解除这种循环引用有下面有三种可行的方法(参考):
1. 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
2. 当A的生存期超过B的生存期的时候,B改为使用一个普通指针指向A。
3. 使用弱引用的智能指针打破这种循环引用。
虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。我们一般使用第三种方法:弱引用的智能指针weak_ptr
。
强引用和弱引用:
一个强引用当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放)。share_ptr
就是强引用。相对而言,弱引用当引用的对象活着的时候不一定存在。仅仅是当它存在的时候的一个引用。弱引用并不修改该对象的引用计数,这意味这弱引用它并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。
使用weak_ptr来打破循环引用
代码如下:
#include
#include
using namespace std;
class B;
class A
{
public:// 为了省去一些步骤这里 数据成员也声明为public
weak_ptr pb;
//shared_ptr pb;
void doSomthing()
{
shared_ptr pp = pb.lock();
if(pp)//通过lock()方法来判断它所管理的资源是否被释放
{
cout<<"sb use count:"< pa;
shared_ptr pa;
~B()
{
cout <<"kill B\n";
}
};
int main(int argc, char** argv)
{
shared_ptr sa(new A());
shared_ptr sb(new B());
if(sa && sb)
{
sa->pb=sb;
sb->pa=sa;
}
sa->doSomthing();
cout<<"sb use count:"<
weak_ptr
除了对所管理对象的基本访问功能(通过get()函数)外,还有两个常用的功能函数:expired()
用于检测所管理的对象是否已经释放;lock()
用于获取所管理的对象的强引用指针。不能直接通过weak_ptr
来访问资源。那么如何通过weak_ptr
来间接访问资源呢?答案是:在需要访问资源的时候weak_ptr
为你生成一个shared_ptr
,shared_ptr
能够保证在shared_ptr
没有被释放之前,其所管理的资源是不会被释放的。创建shared_ptr
的方法就是lock()
方法。
参考链接:
https://www.nowcoder.com/ta/nine-chapter/review?page=7
https://blog.csdn.net/xtzmm1215/article/details/45868835
http://zhoumf1214.blog.163.com/blog/static/5241940201221942041379/
3. 请实现一个单例模式的类,要求线程安全。
用懒汉模式的下的单例模式+双检锁机制实现。
#include
#include
using namespace std;
class Singlenton
{
private:
Singlenton(){}
Singlenton(const Singlenton& obj);
Singlenton& operator=(const Singlenton& obj);
static Singlenton* m_instance;
public:
static Singlenton* getInstance()
{
if( m_instance == NULL )
{
Lock(); // 借助其他类实现
if( m_instance == NULL )
{
Singlenton* temp = new Singlenton();
m_instance = temp;
}
UnLock();
}
return m_instance;
}
};
Singlenton* Singlenton::m_instance = NULL;
int main()
{
Singlenton* s1 = Singlenton::getInstance();
Singlenton* s2 = Singlenton::getInstance();
if( s1 == s2 )
{
cout << "yes" << endl;
}
else
{
cout << "no" << endl;
}
return 0;
}
参考链接:
https://www.nowcoder.com/ta/nine-chapter/review?query=&asc=true&order=&page=8
https://www.cnblogs.com/myd620/p/6133420.html
http://www.cnblogs.com/ccdev/archive/2012/12/19/2825355.html
4. 如何定义一个只能在堆上(栈上)生成对象的类?
在C++中,类的对象建立分为两种,一种是静态建立,如A a
;另一种是动态建立,如A* ptr=new A
;这两种方式是有区别的。
静态建立一个类对象,是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。
动态建立类对象,是使用new
运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()
函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。
那么如何限制类对象只能在堆或者栈上建立呢?下面分别进行讨论。
-
只能建立在堆上
类对象只能建立在堆上,就是不能静态建立类对象,即不能直接调用类的构造函数。
容易想到将构造函数设为私有。在构造函数私有之后,无法在类外部调用构造函数来构造类对象,只能使用new运算符来建立对象。然而,前面已经说过,new运算符的执行过程分为两步,C++提供new运算符的重载,其实是只允许重载operator new()函数,而operator()函数用于分配内存,无法提供构造功能。因此,这种方法不可以。
当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,情况会是怎样的呢?比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。
因此,将析构函数设为私有,类对象就无法建立在栈上了。代码如下:
class A
{
public:
A(){}
void destory(){delete this;}
private:
~A(){}
};
试着使用A a;
来建立对象,编译报错,提示析构函数无法访问。这样就只能使用new
操作符来建立对象,构造函数是公有的,可以直接调用。类中必须提供一个destory
函数,来进行内存空间的释放。*类对象使用完成后,必须调用 destory
函数。
上述方法的一个缺点就是,无法解决继承问题。如果A作为其它类的基类,则析构函数通常要设为 virtual ,然后在子类重写,以实现多态。因此析构函数不能设为 private 。还好C++提供了第三种访问控制, protected
。将析构函数设为 protected
可以有效解决这个问题,类外无法访问protected
成员,子类则可以访问。
另一个问题是,类的使用很不方便,使用 new
建立对象,却使用destory
函数释放对象,而不是使用delete
。(使用delete
会报错,因为delete
对象的指针,会调用对象的析构函数,而析构函数类外不可访问)这种使用方式比较怪异。为了统一,可以将构造函数设为protected
,然后提供一个public
的static
函数来完成构造,这样不使用new
,而是使用一个函数来构造,使用一个函数来析构。代码如下,类似于单例模式:
#include
using namespace std;
class A
{
protected:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A() " << endl;
}
public:
static A* create()
{
return new A();
}
void destory()
{
delete this;
}
};
int main()
{
A* a = A::create();
a->destory();
return 0;
}
这样,调用create()
函数在堆上创建类A对象,调用destory()
函数释放内存。
-
只能建立在栈上
只有使用
new
运算符,对象才会建立在堆上,因此,只要禁用new
运算符就可以实现类对象只能建立在栈上。将operator new()
设为私有即可。代码如下:
class A
{
private:
void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的
void operator delete(void* ptr){} // 重载了new就需要重载delete
public:
A(){}
~A(){}
};
参考链接:
https://www.nowcoder.com/ta/nine-chapter/review?query=&asc=true&order=&page=9
https://blog.csdn.net/szchtx/article/details/12000867#
5. 下面的结构体大小分别是多大(假设32位机器)?
struct A {
char a;
char b;
char c;
};
struct B {
int a;
char b;
short c;
};
struct C {
char b;
int a;
short c;
};
#pragma pack(2)
struct D {
char b;
int a;
short c;
};
结构体的大小问题在求解的时候要注意对齐:
A:对齐值为:1 。大小为:3
B:对齐值为:4 。 大小为:4+4 = 8(第一个4为int,第二个4为char 和 short ,要空余1个)
C:对齐值为:4。大小为:4+4+4 = 12(第一个为char ,空余3个,第二个为int ,第三个为char 空余3个)
D:指定对齐值为:2(使用了#pragma pack(2)
) 。大小为2+4+2 = 8。(第一个char,空余1个,第二个为int ,4个,第3个位char,空余1个)
参考链接
https://www.nowcoder.com/questionTerminal/0482d89ea4d34032ab89a72807aa4abf
6. 引用和指针有什么区别?
1.指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;
而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。
2.指针可以有多级,但是引用只能是一级;
3.指针的值可以为空,也可能指向一个不确定的内存空间,
但是引用的值不能为空,并且引用在定义的时候必须初始化为特定对象;(因此引用更安全)
4.指针的值在初始化后可以改变,即指向其它的存储单元,
而引用在进行初始化后就不会再改变引用对象了;
5.sizeof引用得到的是所指向的变量(对象)的大小,
而sizeof指针得到的是指针本身的大小;
6.指针和引用的自增(++)运算意义不一样
参考链接
https://www.nowcoder.com/questionTerminal/61987bb9e369427282eb50f9d753fb42
http://www.cnblogs.com/webary/p/4754522.html
7. const和define有什么区别?
-
编译器处理阶段不同:
define宏在预处理阶段展开, const常量在编译阶段使用
-
类型安全检查不同
defined宏没有类型,不做类型检查,只做简单的展开
const常量有类型,在编译阶段会执行类型检查 -
存储方式不同
define定义的常量在替换后运行过程中会不断地占用内存,在内存中有若干份copy,
而const定义的常量存储在数据段,只有一份copy,效率更高。 -
能否调试
define定义的常量不能被调试,
const常量可以。 -
效率不同
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
参考链接
https://www.nowcoder.com/questionTerminal/a60c01a7c4ab473e81218ed0b333b4e6
8. define和inline有什么区别?
- 宏define在预处理阶段完成;inline在编译阶段
- 类型安全检查
inline函数是函数,要做类型检查;宏定义则不用 - 替换方式
define字符串替换;inline是指嵌入代码,在编译过程中不单独产生代码,在调用函数的地方不是跳转,而是把代码直接写到那里去,对于短小的函数比较实用,且安全可靠。 - inline函数是否展开由编译器决定,有时候当函数太大时,编译器可能选择不展开相应的函数.
参考链接
https://www.nowcoder.com/questionTerminal/2f04608344924b929d6a09dc00166d3b
http://www.cnblogs.com/fengkang1008/p/4746157.html
9. malloc和new有什么区别?
malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。但它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free,因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,和一个能完成清理与释放内存工作的运算符delete。
new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void*指针。new delete在实现上其实调用了malloc,free函数。
new 建立的是一个对象;malloc分配的是一块内存。
参考链接
https://www.nowcoder.com/questionTerminal/84c6de43ca954bbbb5581d9cfbb60431
http://www.cnblogs.com/webary/p/4754522.html
10. C++中static关键字作用有哪些?
- 在C语言中关键字
static
有下面三个作用:
- 隐藏性:当同时编译多个文件时,所有未加
static
前缀的全局变量和函数都具有全局可见性。static
可以用作函数和变量的前缀,对于函数来讲,static
修饰后,该函数只能在本文件中访问,其他文件不能访问,即该函数具有隐藏性; -
static
的第二个作用是保持变量内容的持久性:存储在静态存储区的变量会在程序刚开始运行时就完成初始化,也是唯一一次初始化。
共有两种变量存储在静态存储区:全局变量和静态存储区变量。和全局变量比起来,静态存储区的变量可以控制可见范围,即静态存储区的变量具有隐藏性。
-
static
的第三个作用是:static变量的默认初始化为0。
- 除此之外,在C++中的新作用有:
- 不能将静态成员函数定义为虚函数(静态成员函数没有this指针)
- 静态成员变量存储在静态存储区,所以必须要初始化。(必须手动初始化,否则编译时一般不会报错,但链接是会报错)
- 静态成员变量在定义或说明时前面加
static
,在初始化处不需要加static
。
下面为定义和初始化一个静态成员变量的示例代码:
#include
using namespace std;
class Test
{
private:
static int m; // 静态成员函数的声明
public:
Test()
{
}
int get()
{
return m;
}
};
int Test::m = 0; // 静态成员函数的初始化,前面不需要加static
int main()
{
Test t;
cout << t.get() << endl;
return 0;
}
参考链接
https://www.nowcoder.com/questionTerminal/3dcc1dc72db540a4911f17252b84fb7f
http://www.cnblogs.com/webary/p/4754522.html