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)mallocsizeof( node ) ); // 申请新结点 
 ifNULL == 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) 
{ 
 ifNULL == S) //检验栈 
 { 
  printf("There no node in stack!"); 
  return NULL; 
 } 
 
 LinkStack p = NULL; 
 p = (LinkStack)mallocsizeof( node ) ); // 申请新结点 
 
 ifNULL == p) 
 { 
  printf("Fail to malloc a new node.n"); 
  return S; 
 } 
 ifNULL == 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; 
 
 ifNULL == 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 ); //创建空栈 
 
 whileNULL != S->next ) //S 出栈入S1 
 { 
  n = Pop( S ); 
  Push( S1, n.data ); 
 } 
 Push( S1, data ); //新结点入栈 
 
 whileNULL != 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 charconst &v1,const charconst &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

声明:1、本内容转载于网络,版权归原作者所有!2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!