腾讯面试题c++基础篇

腾讯面试题c++基础篇

C++虚函数和纯虚函数有什么区别,分别应用在什么场合?

首先纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。通俗来说,使用纯虚函数的类我们称为抽象类,该类不提供方法只提供接口,也可以很形象的理解为就是一个API。

纯虚函数的声明方法:vitual void A()=0;

虚函数则是为了实现多态,(多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作

STL中迭代器的作用,有指针为何还要迭代器(HR常问)

1、迭代器

Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。

由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展iterator。

2、迭代器和指针的区别

迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,重载了指针的一些操作
符,->、*、++、--等。迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内
全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高
级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,--等操作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用*取值后的值而不能直接输出
其自身。

3、迭代器产生原因

Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循
环遍历集合的效果。

前++和后++运算符怎么重载

当我们使用后置运算符时,编译器为这个辛灿提供一个值为0的实参。尽管从语法上来说后置函数可以使用这个额外的形参,但是在实际过程中通常不会这么做。这个形参唯一的作用就是区分前置版本和后置版本的函数,而不是真的要在实现后置版本时参与运算。”下面便是前++和后++的实现代码,请注意两种实现不同的区别,一个是返回引用,一个是返回临时对象

前缀式 operator++(),++后返回加之后的值
后缀式 operator++(int),++后返回加之前的值

#include 
using namespace std;
class Complex
{
public:
        Complex(float x, float y)
                :_x(x), _y(y) {}
        void display()
        {
                cout << "(x = " << _x << ", y = " << _y << ")" << endl;
        }
        // 一定要返回引用,因为++会改变操作数,而如果是临时对象,操作数据的值不会变
        // 前++
        Complex& operator++()
        {
                ++this->_x;
                ++this->_y;
                return *this;
        }
        // 后++,在参数中随便加一个类型,表示是后++,称为亚元
         const Complex operator++(int)
        {
                // 先保存一个*this的临时变量
                Complex tmp(*this);
                // 对this自身++
                this->_x++;
                this->_y++;
                // 返回临时的变量
                return tmp;
        }
private:
        float _x;
        float _y;
};

子类析构会调用父类的析构函数吗?执行顺序是什么?子类构造函数调用顺序?

析构函数调用的次序是先派生类的析构后基类的析构,也就是说在基类的的析构调用的时候,派生类的信息已经全部销毁了。

而定义一个对象时先调用基类的构造函数、然后调用派生类的构造函数;析构的时候恰好相反:先调用派生类的析构函数、然后调用基类的析构函数。

原因:

派生类构造函数中的某些初始化可能是基于基类的,所以规定构造在类层次的最根处开始,而在每一层,首先调用基类构造函数,然后调用成员(此处的成员只指各种类对象如QString a,不含基本类型变量如int n、指针变量如QString *a)对象构造函数(因为C++的成员变量是不会自动初始化的,只能使用初始化列表初始化(调用成员的构造函数,如果不在初始化列表中显式调用的话,则会隐式调用成员变量的默认构造函数,通过汇编可以发现)或在本层构造函数内初始化) 参考:http://www.cnblogs.com/lidabo...)。

如果没有显式调用基类的构造函数,会自动调用基类的无参构造函数。而如果基类只有带参数的构造函数,则会报错。不一定要显式的无参构造函数,可以显式调用基类带参数的构造函数。

#include
using namespace std;
class Base{
public:
    Base(int c){cout<< "基类带参构造函数" << c << endl;}
    ~Base(){cout<<"基类析构" << endl;}
};
class Derived:public Base{
public:
    Derived(int c):Base(c) // 显式调用基类构造函数
    {
        cout<< "派生类带参构造函数" << c << endl;
    }
    ~Derived(){cout<<"派生类析构" << endl;}
};
int main()
{
    int i = 9;
    Derived d1(i);
    return 0;
}

输出结果:

基类带参构造函数9

派生类带参构造函数9

派生类析构

基类析构

程序的内存分布

栈空间,堆空间,数据区,代码段

栈空间:局部变量,函数形参,自动变量。栈空间特点,先进后出,空间由系统管理;栈空间生命周期所在函数执行结束
后释放;栈空间保存的局部变量未初始化时,默认初始化为随机值。

堆空间:由malloc , calloc ,ralloc,这些好函数分配的控件位堆空间,堆空间特点:先进先出,由用户管理

数据区(静态区):又分为.bss段、.data段、常量区。其中.bss段保存的是未初始化的全局变量,当全局变量未初始化时,系统默认初始化为0,常量区保存的是常量,里面保存的值不能被修改,只能做读操作。.data段是保存已经初始化的全局变量以及被static修饰的变量(静态变量)。数据区的声明周期是整个程序执行完之后再释放。

代码段:保存的是代码。

拷贝构造函数与赋值函数的区别,等号在拷贝构造函数出现的时机

1、对象在创建时使用其他的对象初始化

Person p(q); //此时复制构造函数被用来创建实例p
Person p = q; //此时复制构造函数被用来在定义实例p时初始化p

2、对象作为函数的参数进行值传递时

f(p); //此时p作为函数的参数进行值传递,p入栈时会调用复制构造函数创建一个局部对象,与函数内的局部变量具有相同的作用域

需要注意的是,赋值并不会调用复制构造函数,赋值只是赋值运算符(重载)在起作用

p = q; //此时没有复制构造函数的调用!

简单来记的话就是,如果对象在声明的同时将另一个已存在的对象赋给它,就会调用复制构造函数;如果对象已经存在,然后将另一个已存在的对象赋给它,调用的就是赋值运算符(重载)

默认的复制构造函数和赋值运算符进行的都是"shallow copy",只是简单地复制字段,因此如果对象中含有动态分配的内存,就需要我们自己重写复制构造函数或者重载赋值运算符来实现"deep copy",确保数据的完整性和安全性。

什么时候需要重载赋值运算符。

当我们需要进行深拷贝的时候就需要重载赋值运算符,即当一个类需要申请资源的时候

深拷贝和浅拷贝

在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。下面举个深拷贝的例子。

#include 
using namespace std;
class CA
{
 public:
  CA(int b,char* cstr)
  {
   a=b;
   str=new char[b];
   strcpy(str,cstr);
  }
  CA(const CA& C)
  {
   a=C.a;
   str=new char[a]; //深拷贝
   if(str!=0)
    strcpy(str,C.str);
  }
  void Show()
  {
   cout<

考察虚函数调用的时机

B和C有A派生而来,D又是B和C集成而来,请问这样会什么问题

堆排序和栈内存的区别,他们的职责的区别

new和malloc的区别

(1)malloc和new都是在堆上开辟内存的
malloc只负责开辟内存,没有初始化功能,需要用户自己初始化;new不但开辟内存,还可以进行初始化,如new int(10);表示在堆上开辟了一个4字节的int整形内存,初始值是10,再如new int[10] ();表示在堆上开辟了一个包含10个整形元素的数组,初始值都为0。

(2)malloc是函数,开辟内存需要传入字节数,如malloc(100);表示在堆上开辟了100个字节的内存,返回void*,表示分配的堆内存的起始地址,因此malloc的返回值需要强转成指定类型的地址;new是运算符,开辟内存需要指定类型,返回指定类型的地址,因此不需要进行强转。

如堆上开辟int整形:

int *p1 = (int*)malloc(sizeof(int));   
=>  根据传入字节数开辟内存,没有初始化

int *p2 = new int(0); 
=>  根据指定类型int开辟一个整形内存,初始化为0

int *p3 = (int*)malloc(sizeof(int)*100);  
=>  开辟400个字节的内存,相当于包含100个整形元素的数组,没有初始化

int *p4 = new int[100]();  
=>  开辟400个字节的内存,100个元素的整形数组,元素都初始化为0  

(3)malloc开辟内存失败返回NULL,new开辟内存失败抛出bad_alloc类型的异常,需要捕获异常才能判断内存开辟成功或失败,new运算符其实是operator new函数的调用,它底层调用的也是malloc来开辟内存的,new它比malloc多的就是初始化功能,对于类类型来说,所谓初始化,就是调用相应的构造函数。

  try {
            int* p = new int[SIZE];
            // 其它代码
        } catch ( const bad_alloc& e ) {
            return -1;
        }

(4)malloc开辟的内存永远是通过free来释放的;而new单个元素内存,用的是delete,如果new[]数组,用的是delete[]来释放内存的

(5)malloc开辟内存只有一种方式,而new有四种分别是普通的new(内存开辟失败抛出bad_alloc异常), nothrow版本的new,const new以及定位new。

注意这里 : 如果问到malloc,还有可能问你memcpy等,realloc函数能不能在C++中使用,绝对不能,因为这些函数进行的都是内存值拷贝(也就是对象的浅拷贝),会发生浅拷贝这个严重的问题!

vector和list的区别

1.vector数据结构
vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。
因此能高效的进行随机存取,时间复杂度为o(1);
但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。
另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。

2.list数据结构
list是由双向链表实现的,因此内存空间是不连续的。
只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n);
但由于链表的特点,能高效地进行插入和删除。

3.vector和list的区别
vector和list对于迭代器的支持不同。
相同点在于,vector< int >::iterator和list< int >::iterator都重载了 “++ ”操作。
而不同点在于,在vector中,iterator支持 ”+“、”+=“,”<"等操作。而list中则不支持。
我们看一个简单的vector和list使用示例:

#include
#include
#include
using namespace std;
int main()
{
    vector v;
    list l;
    for(int i=0;i<8;i++) ////往v和l中分别添加元素
    {
        v.push_back(i);
        l.push_back(i);
    }
    cout<<"v[2]="<#include 
using namespace std;

class Father
{
public:
    Father(){cout<<"contructor Father!"<

在这里插入图片描述

#include 
using namespace std;

class Father
{
public:
    Father(){cout<<"contructor Father!"<

在这里插入图片描述

如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。

重写和重载的区别

(1)范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中。

(2)参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。

(3)virtual的区别:重写的基类必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。

C++实现多态的方法,即虚函数是怎么被实现的(虚函数表)?

虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。
为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
虚函数表的原理

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};
 
class B : public A {
public:
    virtual void vfunc1();
    void func1();
private:
    int m_data3;
};
 
class C: public B {
public:
    virtual void vfunc2();
    void func2();
private:
    int m_data1, m_data4;
};

腾讯面试题c++基础篇_第1张图片

public/private/protected哪些能被子类继承

腾讯面试题c++基础篇_第2张图片

函数的返回值是string和string * 有没有问题

C++ RVO机制

讲一下自己了解的设计模式(单例模式和工厂模式)

函数的重载、重定义、重写

回答:
1.重写( override):父类与子类之间的多态性。子类

重新定义父类中有相同名称和参数的虚函数。

2.重载( overload):指函数名相同,但是它的参数表列个

数或顺序,类型不同。但是不能靠返回类型来判断。

3.重定义( redefining)子类重新定义父类中有相同名称

的非虚函数(参数列表可以不同)。

类里面成员变量能不能用memset来进行设置? 会有什么问题呢?

回答:不能。这里说不可以,不是说真的不可以,而是说 真的别这样!有些情况下是可以用的,因为类只是一个 说明,对象也是这个类的一个具体化了的内存块,当你 memset—个对象时,它把这块对象内存初始化了,在不 影响内部结构的情况下是不会有问题的,这就是为什么 有时候使用memset—个对象时不会出错的原因。如果 类包含虚拟函数,则不能用memset来初始化类对象。

class GraphicsObject{
protected:
char *m_pcName;
int    m_iId;
//etc
public:
virtual void Draw() {}
virtual int Area() {}
char* Name() { return m_pcName;}
};

class Circle: public GraphicsObject{
void Draw() { /*draw something*/ }
int Area() { /*calculate Area*/ }
};

void main()
{
GraphicsObject *obj = new Circle; // 创建对象
memset((void *)obj,NULL,sizeof(Circle)); // memset to 0
obj->Name(); // 非虚函数调用没问题
obj->Draw(); // 在这里死得很难看
}

结 果我就不说了。因为每个包含虚函数的类对象都有一个指针指向虚函数表(vtbl)。这个指针被用于解决运行时以及动态类型强制转换时虚函数的调用问题。该指针是被隐藏的,对程序员来说,这个指针也是不可存取的。当进行memset操作时,这个指针的值也要被overwrite,这样一来,只要一调用虚函 数,程序便崩溃。这在很多由C转向C++的程序员来说,很容易犯这个错误,而且这个错误很难查。
为了避免这种情况,记住对于有虚拟函数的类对象,决不能使用 memset 来进行初始化操作。而是要用缺省的构造函数或其它的 init 例程来初始化成员变量。

智能指针详解

c++里面的四个智能指针: auto_ptr, unique_ptr,shared_ptr,weak_ptr,其中后三个是c++11支持,并且第一个已经被c++11弃用。

为什么要使用智能指针:我们知道c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。

指针失效问题如何解决?

C语言函数调用是怎么样一个过程?

大多数CPU上的程序实现使用栈来支持函数调用操作,栈被用来传递函数参数、存储返回信息、临时保存寄存器原有的值以备恢复以及用来存储局部变量。

函数调用操作所使用的栈部分叫做栈帧结构,每个函数调用都有属于自己的栈帧结构,栈帧结构由两个指针指定,帧指针(指向起始),栈指针(指向栈顶),函数对大多数数据的访问都是基于帧指针。
栈指针和帧指针一般都有专门的寄存器,通常使用ebp寄存器作为帧指针,使用esp寄存器做栈指针。

帧指针指向栈帧结构的头,存放着上一个栈帧的头部地址,栈指针指向栈顶。

C语言参数压栈顺序?

从右往左。

要回答这个问题,就不得不谈一谈printf()函数,printf函数的原型是:printf(const char* format,…)

没错,它是一个不定参函数,那么我们在实际使用中是怎么样知道它的参数个数呢?这就要靠format了,编译器通过format中的%占位符的个数来确定参数的个数。

现在我们假设参数的压栈顺序是从左到右的,这时,函数调用的时候,format最先进栈,之后是各个参数进栈,最后pc进栈,此时,由于format先进栈了,上面压着未知个数的参数,想要知道参数的个数,必须找到format,而要找到format,必须要知道参数的个数,这样就陷入了一个无法求解的死循环了!!

而如果把参数从右到左压栈,情况又是怎么样的?函数调用时,先把若干个参数都压入栈中,再压format,最后压pc,这样一来,栈顶指针加2便找到了format,通过format中的%占位符,取得后面参数的个数,从而正确取得所有参数。

C语言如何处理返回值?

返回变量值的时候,直接将局部变量的值传给了了寄存器eax,也就是说,函数返回以后,虽然局部变量已被释放,但是eax里面的还有一个值的拷贝。

返回指针的时候,用的是指令lea,这条指令的作用是,将[ebp-4](此单元对应的是变量local_data在栈上的数据存储位置)这个数据单元的地址传给eax寄存器,但是像这样在栈上开辟出来临时存储数据的单元,只要调用函数结束,就会释放掉里面的数据,因此虽然返回了一个指针,指针指向的数据却已经被系统销毁了,这就导致返回的指针指向不可预知的数据。

参考此文

无序的整数数组,使得奇数在前面,所有的偶数都在后面

C++11的特性

智力题:36匹马6个跑道无秒表选前三,最少跑几轮

应该是8次
(1)首先把36匹马分为6组,赛6场可以决出每组第一名来;
(2)每组第一名在进行一场比赛,得第一名的组,得第二名,得第三名的组继续,得第四名自,第五名,第六名的组都没必要参赛因为得第四名的组最好的名次就是第四名不可能排进前三了,第五名的组就更没机会了,所有得到将要进行下一轮比赛的为:
得第一名的组第一名,第二名,第三名,和得第二名的组第一名,第二名(第三名没知有必要因为得第二名的组最好成绩是第二名),得第三名的组第一名参赛(总共是6个)刚好赛一道场就可以排出前三名;所有总共是6+1+1=8场

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