C++C++面试C/C++

C++研发工程师笔试题/面试题(1-10)

2018-07-19  本文已影响50人  编程半岛

1. (1) 简述智能指针的原理;(2)c++中常用的智能指针有哪些?(3)实现一个简单的智能指针。

  1. 简述智能指针的原理:智能指针是一种资源管理类,这个类在构造函数中传入一个原始指针,在析构函数中释放传入的指针。智能指针都是栈上的对象,所以当函数(或程序)结束时,会自动释放。
  2. c++中常用的智能指针:在C++11中的<memory>中有unique_ptrshared_ptrweak_ptr
    unique_ptr:同一时刻只能由唯一的unique_ptr指向给定对象,不支持拷贝操作赋值操作
    shared_ptr:可以多个指针指向相同的对象,通过引用计数机制,支持拷贝操作赋值操作每使用一次,内部引用计数加1,析构一次,引用计数减1,当计数为0时,释放所指的堆空间
    weak_ptr:弱引用。 引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr。顾名思义,weak_ptr是一个弱引用,只引用,不计数。如果一块内存被shared_ptrweak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针.、。
  3. 实现一个简单的智能指针:
    下面是一个基于引用计数的智能指针的实现,需要实现构造,析构,拷贝构造,=操作符重载,重载*和->操作符。
#include <iostream>

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<T>& 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<char> cp1(new char('a'));
    cout << cp1.count() << endl;

    SmartPointer<char> cp2(cp1);
    cout << cp1.count() << endl;
    cout << cp2.count() << endl;

    SmartPointer<char> 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 <iostream>  
#include <memory>  
using namespace std;  
  
class B;  
class A  
{  
public:  // 为了省去一些步骤这里 数据成员也声明为public  
    shared_ptr<B> pb;  

    ~A()  
    {  
        cout << "kill A\n";  
    }  
};  
  
class B  
{  
public:  
    shared_ptr<A> pa;  
    ~B()  
    {  
        cout <<"kill B\n";  
    }  
};  
  
int main(int argc, char** argv)  
{  
    shared_ptr<A> sa(new A());  
    shared_ptr<B> sb(new B());  
    if(sa && sb)  
    {  
        sa->pb=sb;  
        sb->pa=sa;  
    }  
    cout<<"sa use count:"<<sa.use_count()<<endl;  
    return 0;  
}

上面的代码运行结果为: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 <iostream>  
    #include <memory>  
    using namespace std;  
      
    class B;  
    class A  
    {  
    public:// 为了省去一些步骤这里 数据成员也声明为public  
        weak_ptr<B> pb;  
        //shared_ptr<B> pb;  
        void doSomthing()  
        {  
            shared_ptr<B> pp = pb.lock();  
            if(pp)//通过lock()方法来判断它所管理的资源是否被释放  
            {  
                cout<<"sb use count:"<<pp.use_count()<<endl;  
            }  
        }  
      
        ~A()  
        {  
            cout << "kill A\n";  
        }  
    };  
      
    class B  
    {  
    public:  
        //weak_ptr<A> pa;  
        shared_ptr<A> pa;  
        ~B()  
        {  
            cout <<"kill B\n";  
        }  
    };  
      
    int main(int argc, char** argv)  
    {  
        shared_ptr<A> sa(new A());  
        shared_ptr<B> sb(new B());  
        if(sa && sb)  
        {  
            sa->pb=sb;  
            sb->pa=sa;  
        }  
        sa->doSomthing();  
        cout<<"sb use count:"<<sb.use_count()<<endl;  
        return 0;  
    }  

weak_ptr除了对所管理对象的基本访问功能(通过get()函数)外,还有两个常用的功能函数:expired()用于检测所管理的对象是否已经释放;lock()用于获取所管理的对象的强引用指针。不能直接通过weak_ptr来访问资源。那么如何通过weak_ptr来间接访问资源呢?答案是:在需要访问资源的时候weak_ptr为你生成一个shared_ptrshared_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 <iostream>
#include <mutex>

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()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。

那么如何限制类对象只能在堆或者栈上建立呢?下面分别进行讨论。

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,然后提供一个publicstatic函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。代码如下,类似于单例模式:

#include <iostream>

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()函数释放内存。

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有什么区别?

  1. 编译器处理阶段不同

    define宏在预处理阶段展开, const常量在编译阶段使用

  2. 类型安全检查不同

    defined宏没有类型,不做类型检查,只做简单的展开
    const常量有类型,在编译阶段会执行类型检查

  3. 存储方式不同

    define定义的常量在替换后运行过程中会不断地占用内存,在内存中有若干份copy
    而const定义的常量存储在数据段,只有一份copy,效率更高

  4. 能否调试

    define定义的常量不能被调试
    const常量可以

  5. 效率不同

    编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。

参考链接
https://www.nowcoder.com/questionTerminal/a60c01a7c4ab473e81218ed0b333b4e6

8. define和inline有什么区别?

  1. 宏define在预处理阶段完成;inline在编译阶段
  2. 类型安全检查
    inline函数是函数,要做类型检查;宏定义则不用
  3. 替换方式
    define字符串替换;inline是指嵌入代码,在编译过程中不单独产生代码,在调用函数的地方不是跳转,而是把代码直接写到那里去,对于短小的函数比较实用,且安全可靠。
  4. inline函数是否展开由编译器决定,有时候当函数太大时,编译器可能选择不展开相应的函数.

参考链接
https://www.nowcoder.com/questionTerminal/2f04608344924b929d6a09dc00166d3b
http://www.cnblogs.com/fengkang1008/p/4746157.html

9. malloc和new有什么区别?

  1. malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。但它们都可用于申请动态内存和释放内存。

  2. 对于非内部数据类型的对象而言,用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free,因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,和一个能完成清理与释放内存工作的运算符delete。

  3. new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void*指针。new delete在实现上其实调用了malloc,free函数。

  4. new 建立的是一个对象;malloc分配的是一块内存。

参考链接
https://www.nowcoder.com/questionTerminal/84c6de43ca954bbbb5581d9cfbb60431
http://www.cnblogs.com/webary/p/4754522.html

10. C++中static关键字作用有哪些?

  1. 隐藏性:当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性static可以用作函数和变量的前缀,对于函数来讲static修饰后,该函数只能在本文件中访问,其他文件不能访问,即该函数具有隐藏性
  2. static的第二个作用是保持变量内容的持久性:存储在静态存储区的变量会在程序刚开始运行时就完成初始化,也是唯一一次初始化。

共有两种变量存储在静态存储区全局变量静态存储区变量。和全局变量比起来,静态存储区的变量可以控制可见范围,即静态存储区的变量具有隐藏性

  1. static的第三个作用是:static变量的默认初始化为0
  1. 不能将静态成员函数定义为虚函数(静态成员函数没有this指针)
  2. 静态成员变量存储在静态存储区,所以必须要初始化。(必须手动初始化,否则编译时一般不会报错,但链接是会报错)
  3. 静态成员变量在定义或说明时前面加static,在初始化处不需要加static
    下面为定义和初始化一个静态成员变量的示例代码:
#include <iostream>

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

上一篇下一篇

猜你喜欢

热点阅读