38 对c++中的smart 四个智能指针:,,,的理解
C++里面的四个智能指针: , , , 其中后三个是c++11支持,并且第一个已经被11弃用。
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
采用所有权模式。
auto_ptr p1 (new string ("I reigned lonely as a cloud.”));
auto_ptr p2;
p2 = p1; //auto_ptr不会报错.
此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以的缺点是:存在潜在的内存崩溃问题!
实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用”)特别有用。
采用所有权模式。
unique_ptr p3 (new string ("auto")); //#4
unique_ptr p4; //#5
p4 = p3;//此时会报错!!
编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,比更安全。
另外还有更聪明的地方:当程序试图将一个 赋值给另一个时,如果源 是个临时右值,编译器允许这么做;如果源 将存在一段时间,编译器将禁止这么做,比如:
unique_ptr pu1(new string ("hello world"));
unique_ptr pu2;
pu2 = pu1; // #1 not allowed
unique_ptr pu3;
pu3 = unique_ptr(new string ("You")); // #2 allowed
其中#1留下悬挂的(pu1),这可能导致危害。而#2不会留下悬挂的,因为它调用 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明, 优于允许两种赋值的 。
「注意」:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个赋给另一个。例如:
unique_ptr ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入, ,来构造。当我们调用()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
是为了解决 在对象所有权上的局限性( 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
成员函数:
返回引用计数的个数
返回是否是独占所有权( 为 1)
swap 交换两个 对象(即交换所拥有的对象)
reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 (new int(1)); sp 与 sp.get()是等价的
是一种不控制对象生命周期的智能指针, 它指向一个 管理的对象. 进行该对象的内存管理的是那个强引用的 . 只是提供了对管理对象的一个访问手段。 设计的目的是为配合 而引入的一种智能指针来协助 工作, 它只可以从一个 或另一个 对象构造, 它的构造和析构不会引起引用记数的增加或减少。是用来解决相互引用时的死锁问题,如果说两个相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和之间可以相互转化,可以直接赋值给它,它可以通过调用lock函数来获得。
class B;
class A
{
public:
shared_ptr pb_;
~A()
{
cout<<"A delete
";
}
};
class B
{
public:
shared_ptr pa_;
~B()
{
cout<<"B delete
";
}
};
void fun()
{
shared_ptr pb(new B());
shared_ptr pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main()
{
fun();
return 0;
}
可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为就可以了,我们把类A里面的 pb_; 改为 pb_; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。
「注意」:不能通过直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个,应该先把它转化为,如: p = pa->pb_.lock(); p->print();
39 说说强制类型转换运算符
「」
「」
「」
「」
使用
try {
Circle& ref_circle = dynamic_cast(ref_shape);
}
catch (bad_cast b) {
cout << "Caught: " << b.what();
}
40 谈谈你对拷贝构造函数和赋值运算符的认识
拷贝构造函数和赋值运算符重载有以下两个不同之处:
「注意」:当有类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认 的。
41 在C++中,使用申请的内存能否通过释放?使用new申请的内存能否用free?
不能, /free主要为了兼容C,new和 完全可以取代 /free的。 /free的操作对象都是必须明确大小的。而且不能用在动态类上。new 和会自动进行类型检查和大小,/free不能执行构造函数与析构函数,所以动态对象它是不行的。当然从理论上说使用申请的内存是可以通过释放的。不过一般不这样写的。而且也不能保证每个C++的运行时都能正常。
42 用C++设计一个不能被继承的类
template class A
{
friend T;
private:
A() {}
~A() {}
};
class B : virtual public A
{
public:
B() {}
~B() {}
};
class C : virtual public B
{
public:
C() {}
~C() {}
};
void main( void )
{
B b;
//C c;
return;
}
「注意」:构造函数是继承实现的关键,每次子类对象构造时,首先调用的是父类的构造函数,然后才 是自己的。
43 C++自己实现一个类
#include
#include
using namespace std;
class String{
public:
// 默认构造函数
String(const char *str = nullptr);
// 拷贝构造函数
String(const String &str);
// 析构函数
~String();
// 字符串赋值函数
String& operator=(const String &str);
private:
char *m_data;
int m_size;
};
// 构造函数
String::String(const char *str)
{
if(str == nullptr) // 加分点:对m_data加NULL 判断
{
m_data = new char[1]; // 得分点:对空字符串自动申请存放结束标志''的
m_data[0] = '';
m_size = 0;
}
else
{
m_size = strlen(str);
m_data = new char[m_size + 1];
strcpy(m_data, str);
}
}
// 拷贝构造函数
String::String(const String &str) // 得分点:输入参数为const型
{
m_size = str.m_size;
m_data = new char[m_size + 1]; //加分点:对m_data加NULL 判断
strcpy(m_data, str.m_data);
}
// 析构函数
String::~String()
{
delete[] m_data;
}
// 字符串赋值函数
String& String::operator=(const String &str) // 得分点:输入参数为const
{
if(this == &str) //得分点:检查自赋值
return *this;
delete[] m_data; //得分点:释放原有的内存资源
m_size = strlen(str.m_data);
m_data = new char[m_size + 1]; //加分点:对m_data加NULL 判断
strcpy(m_data, str.m_data);
return *this; //得分点:返回本对象的引用
}
44 访问基类的私有虚函数
写出以下程序的输出结果:
#include
class A
{
virtual void g()
{
cout << "A::g" << endl;
}
private:
virtual void f()
{
cout << "A::f" << endl;
}
};
class B : public A
{
void g()
{
cout << "B::g" << endl;
}
virtual void h()
{
cout << "B::h" << endl;
}
};
typedef void( *Fun )( void );
void main()
{
B b;
Fun pFun;
for(int i = 0 ; i < 3; i++)
{
pFun = ( Fun )*( ( int* ) * ( int* )( &b ) + i );
pFun();
}
}
输出结果:
B::g A::f B::h
「注意」:考察了面试者对虚函数的理解程度。一个对虚函数不了解的人很难正确的做出本题。在学习面向对象的多态性时一定要深刻理解虚函数表的工作原理。
45 对虚函数和多态的理解
多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了关键字的函数,在子类中重写时候不需要加也是虚函数。
虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。
46 简述类成员函数的重写、重载和隐藏的区别
(1)重写和重载主要有以下几点不同。
(2)隐藏和重写、重载有以下几点不同。
「注意」:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完 全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。
47 链表和数组有什么区别
「注意」:在选择数组或链表数据结构时,一定要根据实际需要进行选择。数组便于查询,链表便于插 入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的存储空间。
48 用两个栈实现一个队列的功能
typedef struct node
{
int data;
node *next;
}node,*LinkStack;
//创建空栈:
LinkStack CreateNULLStack( LinkStack &S)
{
S = (LinkStack)malloc( sizeof( node ) ); // 申请新结点
if( NULL == S)
{
printf("Fail to malloc a new node.n");
return NULL;
}
S->data = 0; //初始化新结点
S->next = NULL;
return S;
}
//栈的插入函数:
LinkStack Push( LinkStack &S, int data)
{
if( NULL == S) //检验栈
{
printf("There no node in stack!");
return NULL;
}
LinkStack p = NULL;
p = (LinkStack)malloc( sizeof( node ) ); // 申请新结点
if( NULL == p)
{
printf("Fail to malloc a new node.n");
return S;
}
if( NULL == S->next)
{
p->next = NULL;
}
else
{
p->next = S->next;
}
p->data = data; //初始化新结点
S->next = p; //插入新结点
return S;
}
//出栈函数:
node Pop( LinkStack &S)
{
node temp;
temp.data = 0;
temp.next = NULL;
if( NULL == S) //检验栈
{
printf("There no node in stack!");
return temp;
}
temp = *S;
if( S->next == NULL )
{
printf("The stack is NULL,can't pop!n");
return temp;
}
LinkStack p = S ->next; //节点出栈
S->next = S->next->next;
temp = *p;
free( p );
p = NULL;
return temp;
}
//双栈实现队列的入队函数:
LinkStack StackToQueuPush( LinkStack &S, int data)
{
node n;
LinkStack S1 = NULL;
CreateNULLStack( S1 ); //创建空栈
while( NULL != S->next ) //S 出栈入S1
{
n = Pop( S );
Push( S1, n.data );
}
Push( S1, data ); //新结点入栈
while( NULL != S1->next ) //S1 出栈入S
{
n = Pop( S1 );
Push( S, n.data );
}
return S;
}
「注意」:用两个栈能够实现一个队列的功能,那用两个队列能否实现一个队列的功能呢?结果是否定 的,因为栈是先进后出,将两个栈连在一起,就是先进先出。而队列是现先进先出,无论多少个连在一 起都是先进先出,而无法实现先进后出。
49 模板函数和模板类的特例化
「引入原因」
编写单一的模板,它能适应多种类型的需求,使每种类型都具有相同的功能,但对于某种特定类型,如果要实现其特有的功能,单一模板就无法做到,这时就需要模板特例化
「定义」对单一模板提供的一个特殊实例,它将一个或多个模板参数绑定到特定的类型或值上
(1)模板函数特例化
必须为原函数模板的每个模板参数都提供实参,且使用关键字后跟一个空尖括号对,表明将原模板的所有模板参数提供实参,举例如下:
template //模板函数
int compare(const T &v1,const T &v2)
{
if(v1 > v2) return -1;
if(v2 > v1) return 1;
return 0;
}
//模板特例化,满足针对字符串特定的比较,要提供所有实参,这里只有一个T
template
int compare(const char* const &v1,const char* const &v2)
{
return strcmp(p1,p2);
}
「本质」特例化的本质是实例化一个模板,而非重载它。特例化不影响参数匹配。参数匹配都以最佳匹配为原则。例如,此处如果是(3,5),则调用普通的模板,若为(“hi”,”haha”)则调用特例化版本(因为这个cosnt char*相对于T,更匹配实参类型),注意二者函数体的语句不一样了,实现不同功能。
「注意」模板及其特例化版本应该声明在同一个头文件中,且所有同名模板的声明应该放在前面,后面放特例化版本。
(2)类模板特例化
原理类似函数模板,不过在类中,我们可以对模板进行特例化,也可以对类进行部分特例化。对类进行特例化时,仍然用表示是一个特例化版本,例如:
template
class hash
{
size_t operator()(sales_data& s);
//里面所有T都换成特例化类型版本sales_data
//按照最佳匹配原则,若T != sales_data,就用普通类模板,否则,就使用含有特定功能的特例化版本。
};
「类模板的部分特例化」
不必为所有模板参数提供实参,可以指定一部分而非所有模板参数,一个类模板的部分特例化本身仍是一个模板,使用它时还必须为其特例化版本中未指定的模板参数提供实参(特例化时类名一定要和原来的模板相同,只是参数类型不同,按最佳匹配原则,哪个最匹配,就用相应的模板)
「特例化类中的部分成员」
可以特例化类中的部分成员函数而不是整个类,举个例子:
template
class Foo
{
void Bar();
void Barst(T a)();
};
template
void Foo::Bar()
{
//进行int类型的特例化处理
cout << "我是int型特例化" << endl;
}
Foo fs;
Foo fi;//使用特例化
fs.Bar();//使用的是普通模板,即Foo::Bar()
fi.Bar();//特例化版本,执行Foo::Bar()
//Foo::Bar()和Foo::Bar()功能不同
50 为什么析构函数一般写成虚函数
由于类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。所以将析构函数声明为虚函数是十分必要的。在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,要将基类的析构函数声明为虚函数。举个例子:
#include
using namespace std;
class Parent{
public:
Parent(){
cout << "Parent construct function" << endl;
};
~Parent(){
cout << "Parent destructor function" <<endl;
}
};
class Son : public Parent{
public:
Son(){
cout << "Son construct function" << endl;
};
~Son(){
cout << "Son destructor function" <<endl;
}
};
int main()
{
Parent* p = new Son();
delete p;
p = NULL;
return 0;
}
//运行结果:
//Parent construct function
//Son construct function
//Parent destructor function
———END———
限 时 特 惠: 本站每日持续更新海量各大内部创业教程,永久会员只需109元,全站资源免费下载 点击查看详情
站 长 微 信: nanadh666