c++面试宝典

目录

一 多线程

二 指针

三  字符串

四  面向对象

五 基本用法

六 c++11

七  算法


c++面试必考多线程,内存(智能指针),常见算法,设计模式。

一 多线程

c++11提供了mutex和condition_variable,并没有提供临界区,信号量。(线程同步)

Mutex互斥量,C++ 11中使用 std::mutex类,必须包含 头文件。

(1)Mutex系列类(四种)
std::mutex,最基本的 Mutex 类。构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。lock()还是try_lock()都需要和unlock()配套使用。但性能损耗,推荐使用atomic操作数据类型
std::recursive_mutex,递归 Mutex 类。
std::time_mutex,定时 Mutex 类。

std::recursive_timed_mutex,定时递归 Mutex 类。

(2)Lock系列类(两种)----------这两个类相比使用std::mutex的优势在于不用配对使用,无需担心忘记调用unlock而导致的程序死锁
std::lock_guard,方便线程对互斥量上锁。除了构造函数外没有其他成员函数
std::unique_lock,除了lock_guard的功能外,提供了更多的成员函数,相对来说更灵活一些。这些成员函数包括lock,try_lock,try_lock_for,try_lock_until、unlock等

(3) 条件变量   

std::condition_variable 提供了两种 wait() 函数

无条件等待:

void wait (unique_lock& lck);

有条件等待

template 
void wait (unique_lock& lck, Predicate pred);

std::condition_variable::notify_one()   唤醒其中一个等待线程(不确定)

std::condition_variable::notify_all()   唤醒所有的等待(wait)线程。如果当前没有等待线程,则该函数什么也不做

(4)线程池实现原理

//ThreadPool.h

class ThreadPool{
public:
    //自定义void()的函数类型
    typedef functionTask;
    ThreadPool();
    ~ThreadPool();
public:
    size_t initnum;
    //线程数组
    vectorthreads ;
    //任务队列
    queuetask ;
    
    //互斥锁条件变量
    mutex _mutex ;
    condition_variable cond ;
    
    //线程池工作结束时为真
    bool done ;
    
    //队列是否为空
    bool isEmpty ;
    //队列是否为满
    bool isFull;

public:
    void addTask(const Task&f);
    void start(int num);
    void setSize(int num);
    void runTask();
    void finish();
};

//ThreadPool.cpp
#include"ThreadPool.h"
ThreadPool ::ThreadPool():done(false),isEmpty(true),isFull(false){
}

//设置池中初始线程数
void ThreadPool::setSize(int num){
        (*this).initnum = num ;
}

//添加任务
void ThreadPool::addTask(const Task&f)
{
    if(!done){
        //保护共享资源    
        unique_locklk(_mutex);
        
        //要是任务数量到了最大,就等待处理完再添加
        while(isFull){
            cond.wait(lk);
        }

        //给队列中添加任务
        task.push(f);
        
        if(task.size()==initnum)
            isFull = true;
        
        cout<<"Add a task"<lk(_mutex);
        
        //队列为空的话,就等待任务
        while(isEmpty){
            cond.wait(lk);
        }
        
        Task ta ;
        //转移控制快,将左值引用转换为右值引用
        ta = move(task.front());  
        task.pop();
        
        if(task.empty()){
            isEmpty = true ;    
        }    
        
        isFull =false ;
        ta();
        cond.notify_one();
    }
}

void ThreadPool::start(int num){
    
    setSize(num);
    
    for(int i=0;i

//Test.cpp

#include
#include"ThreadPool.h"
void func(int i){
    cout<<"task finish"<<"------>"< }
int main()
{

    ThreadPool p ;
    p.start(N);
    int i=0;

    while(1){
        i++;
        //调整线程之间cpu调度,可以去掉
       this_thread::sleep_for(chrono::seconds(1));
        auto task = bind(func,i);
        p.addTask(task);
    }

    p.finish();
    return 0;
}
 

二 指针

0 智能指针

shared_ptr,每次创建类的新对象时,初始化指针并将引用计数count=1,拷贝构造函数count++,赋值运算符重载左操作数所指对象的引用计数count--,右操作数所指对象的引用计算count++

实现原理:引入2个类,一个引用计数类Counter,一个指针类SmartPtr

注意事项:1 不能将原始指针直接赋值给shared_ptr,不要将同一个原始指针初始化不同的shared_ptr,不要循环引用,不要delete  get函数获取的原始指针,删除器的使用

weak_ptr,配合shared_ptr使用,观测权,无引用计数

与shared_ptr不同,unique_ptr没有定义类似make_shared的操作,因此只可以使用new来分配内存,并且由于unique_ptr不可拷贝和赋值,初始化unique_ptr必须使用直接初始化的方式。

  1. unique_ptr up1(new int()); //okay,直接初始化

  2. unique_ptr up2 = new int(); //error! 构造函数是explicit

  3. unique_ptr up3(up1); //error! 不允许拷贝

  4. int *p = new int;
    unique_ptr p1(p);//原始指针拷贝,编译不报错,不推荐使用
    unique_ptr p2(p);

  5. 警惕智能指针作为参数。  const  reference是智能指针做参数的底线。

unique_ptr   同一时刻只能有一个unique_ptr指向给定对象。当unique_ptr被销毁时,它所指向的对象也会被销毁。因此不允许多个unique_ptr指向同一个对象,所以不允许拷贝与赋值

unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权

1 new int[12]生成一个大小为12 的int数组,而new int(12)是生成一个初始值为12的int变量。

对于基本类型数组来说,delete a和delete [ ] a效果是一样的,如果a是一个用户自己定义的结构类型数组,只能使用delete [ ] a。

2下面的代码会输出:

int main()
{    
    int a[4]={1,2,3,4};
    int *ptr=(int *)(&a+1);
    printf("%d",*(ptr-1));
}
解析:最终结果会输出4。考察数组和指针, 指针加一的能力由指针指向的类型决定。&a和a都指的是数组首元素的地址,不同的是,a就是a+0,*(a+0)也就是a[0],而&a+1相当于对a[]类型的数组类型的指针加一,此时指针加到数组的末尾。ptr接受之后,ptr指向最后一个元素后面的那个位置,而ptr的类型是int*,因此,ptr-1之后指向数组最后一个元素4。

3 有下面一段程序,则下列不合法的是

int f1(float);
int f2(char);
int f3(float);
int f4(float);
int (*pf)(float)
A:int (*p)(float)=&f1             B:pf=&f4             C:pf=&f2                  D:pf=f3
解析:C错误,原因是函数参数类型不匹配。函数指针声明的方法:  返回值类型   (*指针变量名)([参数列表])
根据定义,则int (*pf)(float)                           int (*p)(float)=&f1     ,pf和p都是函数指针。对于函数地址的获取,可以是函数名,也可以是函数名前加取地址符&。

4 以下程序段执行后结果是:

#include
void main()
{
    short *p,*q;
    short arr[15]={0};
    p=q=arr;
    p++;
    printf("%d",p-q);
    printf("%d",(char*)p-(char*)q);
    printf("%d",sizeof(arr)/sizeof(*arr));
}
解析:short占两个字节,指针加一的能力由指针指向的类型决定,所以p++与q的偏移量就是2个字节。short arr[15]={0}是初始化所有数组元素为0, 指针相减的结果是指针地址的偏移量除以指针每次移位的大小。
    p-q:偏移量为2个字节,每次移动2个字节,所以p-q=1

            (char*)p-(char*)q:偏移量还是2个字节,但是每次指针移位按照char*移位,也就是每次移动1个字节,所以(char*)p-(char*)q=2;

    对数组名取sizeof()得到的是整个数组的大小,所以sizeof(arr)=15*2=30,;*arr指向数组的第一个元素,所以sizeof(*arr)=2,sizeof(arr)/sizeof(*arr)=15

5 下列程序执行结果是:

#include
using namespace std;
class TestClass
{
    char x;
public:
    TestClass()
    {
        cout<<'A';
    }
    TestClass(char a)
    {
        cout<     }
    ~TestClass()
    {
        cout<<'B';
    }
};
int  main()
{
    TestClass p1,*p2;
    p2=new TestClass ('X');
    delete p2;
    return 0;
}

解析: 类指针的声明不会调用构造函数,但是指向一个类的实例(new)的时候会调用构造函数。类的声明会调用类的构造函数。
    TestClass p1,*p2;     //只会为p1调用默认构造函数

            p2=new TestClass ('X');       //为p2调用构造函数

    delete p2;             //释放p2指向的内存空间,为p2指向的实例调用析构函数

    return 0;              //程序结束,调用析构函数释放p1指向的内存空间

           最后输出AXBB。

6 下面到底哪个是数组指针,哪个是指针数组呢:
A)
int *p1[10];
B)
int (*p2)[10];

“[]”的优先级比“*”要高。p1 先与“[]”结合,构成一个数组的定义,数组名为p1,int *修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含10 个指向int 类型数据的指针,即指针数组。至于p2 就更好理解了,在这里“()”的优先级比“[]”高,“*”号和p2 构成一个指针的定义,指针变量名为p2,int 修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚p2 是一个指针,它指向一个包含10 个int 类型数据的数组,即数组指针

7  struct Test
{
   int Num;
   char *pcName;
   short sDate;
   char cha[2];
   short sBa[4];
}*p;

假设p 的值为0x100000。如下表表达式的值分别为多少?
   p + 0x1 = 0x___ ?
   (unsigned long)p + 0x1 = 0x___?
   (unsigned int*)p + 0x1 = 0x___?

指针变量与一个整数相加减并不是用指针变量里的地址直接加减这个整数。这个整数的单位不是byte 而是元素的个数。所以:p + 0x1 的值为0x100000+sizof(Test)*0x1。至于此结构体的大小为20byte,前面的章节已经详细讲解过。所以p +0x1 的值为:0x100014。

(unsigned long)p + 0x1 的值呢?这里涉及到强制转换,将指针变量p 保存的值强制转换成无符号的长整型数。任何数值一旦被强制转换,其类型就改变了。所以这个表达式其实就是一个无符号的长整型数加上另一个整数。所以其值为:0x100001。

(unsigned int*)p + 0x1 的值呢?这里的p 被强制转换成一个指向无符号整型的指针。所以其值为:0x100000+sizof(unsigned int)*0x1,等于0x100004。

8  开发c代码时,经常见到如下类型的结构体定义:

typedef struct list_t
{
    struct list_t *next;
    struct list_t *prev;
    char data[0];
}list_t;
则,最后一行char data [0]的作用是:
 A :方便管理内存缓冲区     B:减少内存碎片化    C:标示结构体结束     D:没有作用

解析:最后的char data [ 0 ]是一个柔性数组,只能放在结构体的末尾,是声明一个长度为0的数组,就可以使得这个结构体是变长的。对于编译器来说,此时长度为0并不占用空间,因为数组名本身不占空间,它只是一个偏移量,数组名这个符号本身代表了一个不可修改的地址常量,是紧跟在结构体后面的地址,对于这个数组的大小,可以进行动态分配内存,以使得整个结构体的大小是可变的,而且编译器会支持对于数组data的越界访问,这种声明方法可以很巧妙地实现数组扩展。它的作用其实就是方便管理内存缓冲区,减少内存碎片化,而不是标示结构体的结束。

9  关于以下代码中的变量在内存中的存储位置描述不正确的是()
int a=0;
class someClass
{
    int b;
    static int c;
};
 
int main()
{
    int d=0;
    someClass *p=new someClass();
    return 0;
}
A:b存在堆区      B:c存在堆区          C:d存在栈区          D:a存在全局变量区
解析:栈区:由编译器自动分配释放,存储为函数运行而分配的局部变量、函数参数、返回数据、返回地址等

            堆区:有程序员分配释放,new malloc之类的,如果程序员不释放,程序运行结束时由操作系统回收

            全局区:存放全局变量、静态数据、常量,程序结束后由系统释放

           文字常量区:存放常量字符串,程序结束后由系统释放

   程序代码区:存放函数体的二进制代码
           a:全局变量,存在全局变量区      b:成员变量,存放在堆区      c:静态成员变量,存放在全局区     d:局部变量,存放在栈区    p:方法变量,p本身存在栈区,但是指               向堆区内存空间

10 new/delete 和malloc/free区别

1.malloc分配的空间在堆上而new在自由存储区,自由存储区是专门为new操作定义的一个区。//堆是用来动态分配空间的。栈用来存储函数内部声明的变量。

2.malloc是c库里面的一个函数,分配成功的时候返回void指针,需要我们强制转换,而new是关键字,可以为任意数据类型分配内存空间,返回对象类型的指针

3.new请求失败抛出异常,而malloc请求失败返回null

4.new时不需要指定内存大小,但是malloc需要指定类型和大小(int*)malloc(15)这样

5.new/delete需要调用构造和析构函数,但是malloc/free不用调用,实际上new的底层是用malloc写的

6.new和malloc可以被重载但是malloc不行

7.new是关键字,语言编译器支持,而malloc是库函数,需要头文件支持

11 堆和自由存储区的区别

在C++中,内存区分为5个区,分别是堆、栈、自由存储区、全局/静态存储区、常量存储区

从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。

堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。

12 指针和引用的区别

1.指针可以为空但是引用不行。

2.引用必须要一开始的时候进行初始化,但是指针不用。

3.引用是实际上是一个常量的指针。一旦初始化了一个对象,就不能随意更改。但是指针可以。

4.指针的sizeof由编译器决定,引用的sizeof得不到引用的大小空间,而是引用类型所占的空间。

13 父类指针指向子类数组

class B{

virtual ~B(){}

};

B* pb = new D[3];        //B为父类,D为子类
delete []pb;

解析:delete就是释放堆上对象,它删除一个对象的时候,从指针pb开始,到sizeof(B)结束,记住,这时候是sizeof(B),调用B的析构函数,B的析构函数是虚函数,根据多态性,就先调用D的析构函数,再调用B的析构函数。

删除1个对象或者数组的第1个对象都没有问题,但删除第2个对象就麻烦了,delete第2个对象,就是从2*sizeof(B)开始删除,但是sizeof(B) != sizeof(D),于是截断D对象,结果不能正确调用析构函数,发生错误。
 

三  字符串

1 char a[] ={"abc"} ;  或  char[] = "abc";   //\0由计算机自动加上

2从键盘读入一个字符串
   cin >> string1 ;
   当从输入流中读入一个字符串时, 将读入以空白字符分隔的字符串。
   如当用户键入 Harry Borter时, 只有Harry读入到name。
   为了完整输入用户信息, 可以使用getline函数。
   getline(cin,name);    //   读入回车之前的所有字符,并将所读入的字符串存储到变量name中。
 

3 strlen求字符串长度是不包括\0的,sizeof包括\0

4 除了char型的数组cout<<数组名 输出内容
其他的类型数组cout<<数组名  输出地址

char chartest[256] = "asdhfjka";
int inttest[256] = { 1, 2, 3, 4, 5, 6 };
cout << chartest << endl;        //字符串:asdhfjka
cout << inttest;                       //地址:00B3F1EC

5 char *Example[] = { "tgqaertg", "sadgf", "fg","ggg" };
这里,Example是指针数组。

c++面试宝典_第1张图片

6 int main()
{
   char a[5]={'A','B','C','D'};
   char (*p3)[5] = &a;
   char (*p4)[5] = a;
   return 0;
}
p3 和p4 都是数组指针,指向的是整个数组。&a 是整个数组的首地址,a是数组首元素的首地址,其值相同但意义不同。在C 语言里,赋值符号“=”号两边的数据类型必须是相同的,如果不同需要显示或隐式的类型转换。p3 这个定义的“=”号两边的数据类型完全一致,而p4 这个定义的“=”号两边的数据类型就不一致了。左边的类型是指向整个数组的指针,右边的数据类型是指向单个字符的指针。在Visual C++6.0 上给出如下警告:
   warning C4047: 'initializing' : 'char (*)[5]' differs in levels of indirection from 'char *'。
还好,这里虽然给出了警告,但由于&a 和a 的值一样,而变量作为右值时编译器只是取变量的值,所以运行并没有什么问题。不过我仍然警告你别这么用。

7 关于下列程序,说法正确的是

void   fun()
{
    char b[2]={0};
    strcpy(b,"aaaa");
}
A:Debug版本崩溃,Release版本正常                              B:Debug版本正常,Release版本崩溃
C:  Debug版本崩溃,Release版本崩溃                              D:  Debug版本正常,Release版本正常 解析:在Debug模式中,会有assert断言保护,所以会崩溃。而在Release模式中,会删掉assert断言保护,所以会正常运行。但是我在VS2010下运行两个版本都正常运行了......这就尴尬了....但是这两种模式的区别还是要知道的。
 

四  面向对象

1 如下代码,result变量的输出结果是多少?

#include
using namespace std;
int i=1;
class MyCls
{
    public:
    MyCls():m_nFor(m_nThd),m_nSec(i++),m_nFir(i++),m_nThd(i++)
    {
        m_nThd=i;
    }
    void echo()
    {
        cout<<"result"<     }
    private:
    int m_nFir;
    int m_nSec;
    int m_nThd;
    int &m_nFor;
};
 
int main()
{
    MyCls oCls;
    oCls.echo();
    return 0;
}
 解析:首先要明白, 变量初始化的顺序是其声明的顺序,跟初始化列表中的顺序无关,所以变量的初始化顺序m_nFir(i++),m_nSec(i++),m_nThd(i++),m_nFor(m_nThd),i初始化为1,所以经过初始化列表后的m_nFir=1,m_nSec=2,m_nThd=3,m_nFor是m_nThd的一个引用,并且此时i的值为4,执行构造函数中的赋值语句后,m_nThd=4,此时m_nFor是m_nThd的一个引用,也是4。result=1+2+4+4=11。

2  已知下列的class层次,其中的每一个类都定义有一个default constructor和一个virtual destructor,则下面执行dynamic_cast会失败的是()

class X{...};
    class A{...};
    class B:public A{...};
    class C:public B{...};
    class D:public X,public C{...};
A:  A *pa=new D;  X *px=dynamic_cast(pa)                        B:  D *pd=new D;  A *pa=dynamic_cast(pd)
C:  B *pb=new B;  D *pd=dynamic_cast(pb)                     D:  A *pa=new C;  C *pc=dynamic_cast(pa)
解析:dynamic_cast<>用于C++类继承多态间的转换,分为子类向基类的向上转换和基类向子类的向下转换。其中,子类向基类的向上转换不需要借助任何其他特殊的方法,只需要将子类的指针或者引用赋给基类的指针或者引用即可。而向下转换时要特别注意,dynamic_cast<>操作符安全的将基类类型的引用或者指针转换为派生类的引用或者指针,dynamic_cast再将基类cast到子类的时候,基类必须有虚函数,因为dynamic_cast运行时需要检查RTTI信息,只有带虚函数的类运行时才会检查RTTI。

           例:假如继承关系如下:C继承B,B继承A

  则:A *a=new B;   //new了一个B,向上转换为A,但是其本质还是B

  B *b=dynamic_cast a;     //把基类A向下转换为子类B,因为a的本质还是B,所以把B转换为B正确

  C *c=dynamic_casta;     //把基类A向下转换为子类C,因为a的本质是B,而C是B的子类,把本质是B的东西转换为子类C,所以是错误的。  由上可知,本题答案为C。

3  下面程序段包含了四个函数,其中具有隐含this指针的是

int f1();
class T
{
public :
    static int f2();
private:
    friend int f3();
protected:
    int f4();
};
解析: 只有非静态类成员才有this指针。友元函数不是类成员函数,所以没有this指针。

4  类A是类B的友元,类C是类A的公有派生类,忽略特殊情况,下列说法正确的是()

A: 类B是类A的友元         B:类C不是类B的友元            C:类C是类B的友元           D:类B不是类A的友元

解析:友元关系是单向的,不对称,不可继承。所以BD正确
 

5  在32位编译器下sizeof(p)为()

class P
{
private:
    int ival;
public:
    P();
    ~P();
    int GetVal()
    {
        return ival;
    }
    virtual int SetVal(int val)
    {
        ival=val;
    }
};
解析: 1、类的大小为类的非静态成员数据的类型大小之和,也就是说静态类成员数据不做考虑
            2、普通成员函数与类的sizeof()无关

            3、虚函数由于要维护虚函数表所以要占据一个指针的大小,也就是4字节

            4、类的总大小也要遵守字节对齐原则

    本题,4+4=8字节

6  头文件已经正常包含,一下代码在vs上编译和运行的结果是:

class A 
{
    public:
    void test()
    {
        printf("test A");
    }
};
int main()
{
    A *pA =NULL;
    pA->test();
}
解析: 对于非虚成员函数,C++是静态绑定的,在编译时就已经确定了,即使pA为NULL,但是已经声明了类型就知道pA有个test函数,且test函数里没有用到成员变量,单单有个打印语句是可以运行成功的。

7 三种继承方式:
            使用private继承,父类的protected和public属性在子类中变为private;
           使用protected继承,父类的protected和public属性在子类中变为protected;
           使用public继承,父类中的protected和public属性不发生改变; 

8 当一个类A 中没有声命任何成员变量与成员函数,这时sizeof(A)=1

9 空对象是否报错

class A

{

public:

int m_iA1;

void print()

{

cout << "A" << endl;

}

};

int main()

{

A *pObjectA=NULL;

pObjectA->print();

return 0; 

}

*a没有初始化,结果成功输出“A”。

分析:成员函数在代码段,成员变量在数据段,地址为在类对象地址基础上累加。

此处的print成员函数没有用到成员变量,与类外独立函数无异。调用的时候不涉及数据段,不会崩溃。

如果成员函数print中使用到了成员变量,情况会是怎样呢?

class A

{

public:

int m_iA1;

char m_chA2;

void print()

{

m_iA1 = 1;

cout << "A" << endl;

}

};

int main()

{

A *pObjectA=NULL;

pObjectA->print();

return 0; 

}

编译执行后,崩在了print函数中的红色标记语句。

分析:此处的print成员函数用到成员变量m_iA1,我们可以看到,&pObjectA=0x00000000,&m_iA1=0x00000000,&m_iA2=0x00000004

​访问了非法地址,崩溃了

尝试给pObjectA分配空间后,&pObjectA=0x003b6060,&m_iA1=0x003b6060,&m_iA2=0x003b6064,

​这是合法的数据段地址,访问成功。

试试static静态变量

class A

{

public:

static int m_iSA4;

static void print()

{

m_iSA4 = 4;

cout << "A" << endl;

}

};

int A::m_iSA4 = 0;

int main()

{

A *pObjectA=NULL;

pObjectA->print();

return 0; 

}

运行成功!静态变量独立于类外部分配好了合法地址,访问成功。

试试继承

class B

{

int m_iB1;

public:

virtual void print()

{

cout << "B" << endl;

}

};

class A:public B

{

    int m_iA1;

    char m_chA2;

public:

    void print()

    {

        cout << "A" << endl;

    }

};

int main()

{

    A *pObjectA=NULL;

    pObjectA->print();

    return 0; 

}

运行之后崩在在main函数的红色标记语句。

分析:虽然A类的print函数没有用到显式的成员变量,但是类A继承于类B,print()是继承于B的虚函数,调用A的print函数时,用到了虚表,虚表地址在类对象数据段最开头,此时访问了非法地址。

可以看到成员变量地址:

​给pObjectA分配空间之后再看

​可以看到,在继承类对象的地址空间中的顺序为:虚表指针、基类成员变量、继承类成员变量
 

10 C和C++区别

a   C++是面向对象的语言,而C是面向过程的。面向过程就是分析解决问题的步骤,然后把这些步骤一步一步进行实现,注重对过程的分析。面向对象就是把数据和方法进行封装起来,把构成问题的事分为各个对象,建立对象的目的不仅是为了解决问题还为了描述对象发生的行为。

b   面向过程的方法用函数进行数据操作,把函数和数据分开。面向对象将函数和数据封装成一个整体,一个类。更加容易维护和实现代码复用。封装,继承,多态

11 多态

多态就是一个接口,多种形态。

编译时的多态可以通过函数重载实现,是静态多态。而运行时候的多态通过虚函数(函数覆盖)实现,是动态多态,具体使用引用对象类型调用函数,在运行时时实现动态绑定。底层实现通过虚函数机制。

多态实现的3个条件:

  • 有继承关系
  • 有虚函数覆写(override)
  • 基类的指针或引用指向继承类

重载,覆盖和隐藏的区别

a  重载在同一个类里面,函数名字相同,但是传递的参数不同。

b  子类复写父类里面的函数,如果父类里面是虚函数,子类默认是虚函数。而且要求函数名,传递的参数都相同,返回的方法也一致。如果不是虚函数,那就是隐藏。

12 虚函数机制

同一个类多个对象,具有相同的虚函数表。(同一类对象的虚函数都是一样的,存放在代码区,不必每个对象都重复存放一份)

c++面试宝典_第2张图片

 一般多继承中,继承类的类对象模型中会在对象地址起始位置连续放置与基类个数想当的虚表指针,分别指向不同基类的虚函数表,对于继承类中的虚函数放置在第一个基类虚函数表里,继承类中有覆写基类虚函数的在虚函数表中替换基类虚函数。

父类和子类(有virtual)分别有自己的虚函数表,在编译时就会创建虚函数表。同一个类的不同对象公共一个虚函数表,因为函数存放在代码区。

c++面试宝典_第3张图片

13 拷贝构造函数使用条件:

1)建立新对象,并用同类对象初始化时;

2)函数的参数为类的对象时;

3)函数的返回值为类的参数时。

14 深拷贝和浅拷贝

浅拷贝:又称值拷贝,将源对象的值拷贝到目标对象中去,本质上来说源对象和目标对象共用一份实体,只是所引用的变量名不同,地址其实还是相同的。

若是类中有指针成员,拷贝构造函数必须另开辟空间,防止多个对象共用同一份存储空间导致二次释放而崩溃

14 构造函数中调用虚函数

c++ primer 第四版中497页15.4.5构造函数和析构中的虚函数讲到,如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。

#include

#include

using namespace std;

class A

{

public:

    A(){

        cout << "A构造函数";

        Test();

    }

    ~A(){

        cout << "A析构函数";

        cout << "A::Test()" << endl;

    }

    virtual void Test(){

        cout << "A::Test()" << endl;

    }

};

class B :public A

{

public:

    B(){

          cout << "B构造函数";

        Test();

    }

    ~B(){

        cout << "B析构函数";

        Test();

    }

    virtual void Test(){

        cout << "B::Test()" << endl;

    }

};

int _tmain(int argc, _TCHAR* argv[]){

    A* pA = new B();

    cout << "动态调用:";

    pA->Test();

    delete pA;

    return 0;

}

1

2

3

4

5

6

//示例2运行结果

A构造函数A::Test()

B构造函数B::Test()

动态调用:B::Test()

A析构函数A::Test()

请按任意键继续. . .

15 构造函数初始化列表

三种情况必须使用初始化列表:

数据成员是对象,并且这个对象只有含参数的构造函数,没有无参数的构造函数

类中有引用,const成员

子类初始化父类私有成员

类对象的构造顺序显示,进入构造函数体后,进行的是计算,是对成员变量的赋值操作,赋值和初始化是不同的,这样就体现出了效率差异。如果不用成员初始化类表,那么类对自己的类成员分别进行的是一次隐式的默认构造函数的调用,和一次赋值操作符的调用,如果是类对象,这样做效率就得不到保障。

class   Test3{

private:

    int x,y;

public:

    void getData()    {

        cout<<"x="<

    }

};

int main(){

    Test3 test3;  //默认构造函数,栈方式调用默认构造函数

    test3.getData();

    return 0;

}

五 基本用法

1 下列程序段执行后,输出的结果为:

void  main()
{
    int a=1,b=0,c=-1,d=0;
    d=++a||++b&&++c;
    cout< }
解析: 短路原则:||操作符左操作数为真,即可停止判断,右操作数不再执行。因此本题最后结果a=2,b=0,c=-1,d=1。

2  以下代码输出是什么?

int a=1,b=32;
printf("%d,%d",a< 解析:这里考虑的是左移里一个比较特殊的情况,就是 当左移的位数超过该数值类型的最大位数时,编译器会用左移的位数去模类型的最大位数,然后按余数进行移位。
    所以a<  b为32超过最大位数32,所以有效移位数为b%32=0,也就是左移0位还是其本身,1

    而对于1<<32,会直接进行移位,有效移位数就是32,移位后为0。

 但是这个问题真正具体的话,在不同的编译器里会有不同的结果。
 

3  编译和执行下列c语言代码,输出结果是:

int main()
{
    char c='0';
    printf("%d  %d",sizeof(c),sizeof('0'));
    return 0;
}
解析:C语言:char a='0'    sizeof(a)=1     sizeof('0')=4       原因: C语言把 '0' 称为整形字符常量,被看成int类型,所以在32位机器上占4字节
    C++:char a='0'        sizeof(a)=1     sizeof('0')=1      原因:C++把‘0‘称为字符字面常量,被看成是char类型,所以占1字节

4  C++中,32位单精度浮点数能表示的十进制有效数字是多少位?

解析:单精度浮点数的有效位数是7位,双精度浮点型的有效位数是16位。

5  int a=5,则++(a++)的值是()

解析:++是一目运算符,只能用于变量,不能用于表达式,而(a++)是表达式,所以会编译错误。

6  下列程序的输出结果是

void f()
{
    static int i=15;
    i++;
    cout<<"i="< }
 
int main()
{
    for(int k=0;k<2;k++)
    {
        f();
    }
    cin.get();
    return 0;
}
解析: static修饰的变量只初始化一次,当下次执行到初始化语句时,直接跳过。所以输出16,17
 

7 C和C++中struct有什么区别?
C无Protection行为   不能定义函数

8 C++中的struct和class有什么区别?
【答案】从语法上讲,class和struct做类型定义时只有两点区别:
(1)默认继承权限。如果不明确指定,来自class的继承按照private继承处理,来自struct的继承按照public继承处理;
(2)成员的默认访问权限。class的成员默认是private权限,struct默认是public权限。 除了这两点,class和struct基本就是一个东西。语法上没有任何其它区别。

10构造函数声明为explicit,就可以避免隐式类型转换

11 STL线程安全

读是安全的,写操作要注意加锁

1.每次调用容器的成员函数的期间需要锁定。
2.每个容器返回迭代器的生存期需要锁定。
3.每个容器在调用算法的执行期需要锁定。

六 c++11

1 std::function,functor仿函数,函数指针的区别

std::function可调用普通函数、Lambda表达式、函数指针、类静态方法(非静态方法需用bind绑定),提供统一调用方式。

从功能上来说,function和函数指针没有差别。引入function主要有5点:

       1)泛型思想,与STL架构思想保持一致。

       2)function是一个对象,封装了数据和函数。而函数指针只能全局或局部变量。

       3)function可以支持多态

       4)functor是一个类,运算符重载(),一般轻量级代码,也可实现。

       5)函数指针非类型安全

2 std::bind

 std::bind函数定义在头文件functional中,是一个函数模板,它就像一个函数适配器,接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。

std::bind绑定到虚函数时会表现出多态行为。

std::bind的函数参数默认使用的是拷贝, 如果需要使用引用,则需要配合std::ref。参数可以支持Placeholders::_1占位符。

3 lambda表达式 值传递,引用传递

4 左值/右值与左值引用/右值引用

int a=9;   

a左值,存储在内存中,可用&寻址,生命周期较长

9右值,存储在寄存器中,不一定能寻址,通常是个临时值,以常量形式存在,生命周期短。左值也能当成右值使用;例如int b=a;

左值引用声明时必须指向一个已经存在的内存地址;

例如 int &a = 3;    //错误,3存储在寄存器中

const int & a=3;   //正确,c++98/03标准允许常量左值引用操作右值和左值

右值引用

int && a=10;   //编译成功,非常量右值引用支持引用非常量右值

const int && a=10;   //成功,常量右值引用支持引用常量右值

int num = 100;

const int num2 = 100;

int &&a = num;   //失败,非常量右值引用不支持引用非常量左值

int &&a=num2;   //失败,非常量右值引用不支持引用常量左值。总之不支持引用左值

5 拷贝构造函数和移动构造函数

拷贝构造函数实现原理:为新对象复制一份和其他对象一样的数据,当类中有指针类型的成员变量时,拷贝构造函数需要以深拷贝方式复制该指针成员。

例如借书:拷贝构造函数步骤是,b拷贝一本新书给a,将原来的书再销毁

移动构造函数步骤是,b直接将书交给a(转移);

而move可以将左值转变为右值引用,减少临时对象的构造和析构,提高效率。

七  算法

链表逆序,

归并排序,

快速排序,

二叉树递归与非递归遍历,

kmp字串匹配

找出第k大的数:

方法1 :快速排序之后,取第k大的数,o(n*log n+k)

方法2 :利用快速排序的思想,从数组S中随机找出一个元素X,把数组分为两部分Sa和Sb。Sa中的元素大于等于X,Sb中元素小于X。这时有两种情况:

(a)  Sa中元素的个数小于k,则Sb中的第k-|Sa|个元素即为第k大数;

(b)  Sa中元素的个数大于等于k,则返回Sa中的第k大数。时间复杂度近似为O(n)
 

vector动态数组维护的是一个连续线性空间,支持随机访问。vector扩容时,新开辟capacity*2空间大小,并将原数据拷贝到新存储区,释放原空间。注意此时原数据地址已改变,指向原vector的迭代器都会失效

reserve只是当要开辟空间大于其原空间会开辟至需要的空间,而小于就不会更改其空间。

resize:若要开辟的空间的size大于其原来的size,那么resize之后要存放的数据就放在原size后的位置上。
若要开辟的空间小于原size则就保留前n个数据(之后的会自动的删除)

c++面试宝典_第4张图片

list,map,set,deque,stack底层实现原理

你可能感兴趣的:(c++,c++)