C++面试题总结

1、说一下C++和C的区别?

设计思想上:
C++是面向对象的语言,而C是面向过程的结构化编程语言
语法上:
C++具有封装、继承和多态三种特性
C++相比C,增加多许多类型安全的功能,比如强制类型转换、
C++支持范式编程,比如模板类、函数模板等

2、C++中的拷贝构造函数传参为什么传引用?

如果是传值的话,就会在拷贝构造函数内调用拷贝构造函数,会永无休止的递归从而导致栈溢出。因此C++不允许传值,会直接编译出错。

3、赋值运算符重载函数中为什么传引用?

如果不是引用而是实例,会调用一次拷贝构造函数,把参数声明为引用可以避免这个无谓的消耗,提高效率。同时,我们在赋值运算符内不会改变传入的参数,所以加上const。

4、请你回答一下什么时候析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数?

在实现多态时,当一个类被作为基类并且该基类对派生类的对象进行操作,在析构时防止只析构基类而不析构派生类的状况发生。把基类的析构函数设计为虚函数可以在基类的指针指向派生类对象时,用基类的指针删除派生类对象,避免内存泄漏。

C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数所以C++的默认析构函数不是虚函数

5、请你说一说多态实现的原理?

编译器发现一个类中有虚函数,生成具体对象时保证了虚函数表的指针存在于对象实例中最前面的位置,生成虚函数表vtable。虚函数表的各表项为指向对应虚函数的指针。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行vptr与vtable的关联代码,将vptr指向相应的vtable,将类与此类的vtable联系起来。另外在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的this指针,这样依靠此this指针即可得到正确的vtable,如此才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。

6、说一说c++中四种cast转换

C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
①const_cast
用于将const变量转为非const
②static_cast
用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
③dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
向上转换:指的是子类向基类的转换
向下转换:指的是基类向子类的转换
它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
④reinterpret_cast
几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
⑤为什么不使用C的强制转换?
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

7、请说一下C/C++ 中指针和引用的区别?

①指针有自己的一块空间,而引用只是一个别名;
②使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
③指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;
④作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;
⑤可以有const指针,但是没有const引用;
⑥指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
⑦指针可以有多级指针(**p),而引用至于一级;
⑧指针和引用使用++运算符的意义不一样;
⑨如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

8、请你来说一说C++函数栈空间的最大值

默认是1M,不过可以调整

9、请你说说C语言参数压栈顺序?

从右到左

10、请你说说C++如何处理返回值?

生成一个临时变量,把它的引用作为函数参数传入函数内。

11、请你回答一下malloc与new区别

1、malloc需要给定申请内存的大小,返回的指针需要强转。
2、new会调用构造函数,不用指定内存大小,返回的指针不用强转。
3、malloc与free是C/C++语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
4、对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。
5、既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,结果也会导致程序出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。
6、new会调用构造函数,malloc不会;
7、new可以被重载,malloc不能。
问题2:malloc(0)返回值
答:如果请求的长度为0,则标准C语言函数malloc返回一个null指针或不能用于访问对象的非null指针,该指针能被free安全使用。

12、请你说说虚函数表具体是怎样实现运行时多态的?

子类若重写父类虚函数,虚函数表中,该函数的地址会被替换,对于存在虚函数的类的对象,在VS中,对象的对象模型的头部存放指向虚函数表的指针,通过该机制实现多态。

13、请你回答一下静态函数和虚函数的区别

静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销

14、请你说一说重载和覆盖和shadow

重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写
shadow:在父子类中,只要出现了重名标识符(函数成员,数据成员),就会构成,shadow。在子类中如果想访问被shadow的成员,加上父类的命名空间

15、请你来说一下C++中类成员的访问权限

C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员

16、请你来说一下C++中struct和class的区别

①在C++中,可以用struct和class定义类,都可以继承。区别在于:structural的默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private。
②另外,class还可以定义模板类形参,比如template <class T, int i>。

③C++中的struct可以定义函数

④C++中可以直接使用struct定义的变量,而C语言中要加上struct 变量才可以使用

17、请你回答一下C++类内可以定义引用数据成员吗?

可以,必须通过成员函数初始化列表初始化。

18、深拷贝、深深的赋值

①深拷贝:

#include <iostream>
#include <string.h>
using namespace std;
class DataStr
{
public:
    DataStr()                          //构造器
    {
        cout<<"constructor"<<endl;
        _str = new char[100];
        strcpy(_str,"C++ is the best language in the World");
    }
    
    DataStr(const DataStr & another)  //深拷贝构造器
    {
        cout<<"copy constructor"<<endl;
        _str = new char[strlen(another._str)+1]; //创建新的堆空间,分配内存 注意strlen求的长度
        strcpy(_str,another._str);               //将传入的another对象内容拷贝一份给新的
    }
    
    ~DataStr()                    //析构器
    {
        cout<<"destructor"<<endl;
        delete []_str;
    }
    
    void dis()
    {
        cout<<_str<<endl;
    }
    
private:
    char *_str;
};
int main()
{
    DataStr ds;
    ds.dis();
    
    DataStr ds2 = ds; //调用深拷贝构造器
    return 0;
}

②深深的赋值

mystring & operator =(const mystring & another)
{
    if(this == &another)
          return *this;            //实现自赋值
    
    delete []this->_str;        //解决内存泄露,先销毁掉原来的空间
    
    int len = strlen(another._str);//解决重析构double free问题
    _str = new char [len+1];
    strcpy(_str,another._str);
    
    return *this;   //实现连续赋值
}

19、this 指针

this指针就是指向调用对象的指针。系统在创建对象时,默认生成的指向当前对象的指针。这样作的目的,就是为了带来使用上的方便。

this 指针的使用规则

1、指向当前对象,可用于用所有的成员函数,但不能应用于初始化列表。

2、this 是以隐含参数的形式传入,而非成员的一部分,所以不会影响 sizeof(obj)对象的大小。

3、this 指针本身是不能更改指向的,即,是 const 类型修饰的。

this 作用

1、指向当前对象,避免入参与成员名相同

2、支持基于返回 this 引用的多重串联调用的函数(连续赋值)。

20、派生类构造顺序

由于子类中, 包含了两部分内容, 一部分来自, 父类, 一部分来自, 子类。 父类的部分, 要由调用父类的构造器来完成, 子类的部分, 在子类的构造器中来完成始化。 子类中,有内嵌的子对象(内嵌子对象就是类中的数据成员是其他类的一个对象)也需要构造。

子类会调用父类构造器,只有在父类含有标配的情况下才会调用,否则只能在子类中显示的调用。(标配就是不需要提供参数的构造器,也就是无参构造器或参数列表都有默认参数的情况),内嵌子对象也一样。
显示调用如下:
注意:基类的构造器和内嵌子对象只能放在参数列表中,而且参数列表中的是内嵌子对象,是放的对象,不是类,而且基类和内嵌子对象顺序只能是下面这样

派生类名::派生类名(总参列表)
    :基类名(参数表),内嵌子对象(参数表)
{
    派生类新增成员的初始化语句; //也可出现地参数列表中
}

21、虚函数

虚函数必须是类的成员函数否则编译不过
类的静态成员函数不能为虚函数否则编译不过
构造函数不能为虚函数否则编译不过,它将在执行期间被构造, 而执行期则需要对象已经建立, 构造函数所完成的工作就是为了建立合适的对象, 因此在没有构建好的对象上不可能执行多态

22、什么是RAII资源管理?

即资源获取就是初始化,利用对象生命周期来控制程序资源,简单来说就是通过局部对象来处理一些资源问题

23、实现单例模式

class Singleon
{
private:
    Singleon()
    {
        cout << "Singleon()" << endl;
    }
    static Singleon*instrance;
public:
    static Singleon* GetSingleon()
    {
        if (NULL == instrance)
        {
            instrance = new Singleon();
            cout << "对象创建成功" << endl;
        }
        else
        {
            cout << "对象已经创建成功,无须再建" << endl;
        }
        return instrance;
    }
    static Singleon* Destroy()
    {
        delete instrance;
        instrance = NULL;
    }
};
Singleon* Singleon::instrance =  NULL;

24、实现string代码

mystring.h

#define MYSTRING_H
#include <iostream>
#include <string.h>

using  namespace std;
class mystring
{
public:
    mystring (const char * p = nullptr);
    ~mystring ();
    mystring(const mystring & another);
    mystring& operator=(const mystring & another);
    bool operator < (const mystring & another);
    bool operator > (const mystring & another);
    bool operator == (const mystring & another);
    bool operator >= (const mystring & another);
    bool operator <= (const mystring & another);
    mystring operator + (const mystring & another);
    mystring& operator += (const mystring & another);

    friend ostream & operator << (ostream & co, const mystring& ms);
    friend istream  & operator >> (istream & ci,  mystring& ms);

    char operator[](int n);
    char at(int n);
    char * c_str();
private:
    char * _str;
};

#endif // MYSTRING_H

mystring.cpp

#include "mystring.h"


mystring::mystring (const char * p) //构造器
{
    if(p == nullptr)
    {
        _str = new char[1];
        *_str = '\0';
    }
    else
    {
        _str = new char [strlen(p)+1];
        strcpy(_str,p);
    }
}

mystring::~mystring ()//析构器
{
    delete[]_str;
}

mystring::mystring(const mystring & another)//拷贝构造器
{
    _str = new char [strlen(another._str) + 1];
    strcpy(_str,another._str);
}
mystring& mystring::operator=(const mystring & another)//赋值运算符重载
{
    if(this == &another)
    {
        return *this;
    }
    else
    {
        delete []this->_str;
        _str = new char[strlen(another._str)+1];
        strcpy(_str,another._str);
        return *this;
    }
}

bool mystring::operator < (const mystring & another)
{
    return strcmp(_str,another._str) < 0;
}

bool mystring::operator > (const mystring & another)
{
    return strcmp(_str,another._str) > 0;
}

bool mystring::operator == (const mystring & another)
{
    return strcmp(_str,another._str) == 0;
}

bool mystring::operator >= (const mystring & another)
{
    return strcmp(_str,another._str) >= 0;
}

bool mystring::operator <= (const mystring & another)
{
    return strcmp(_str,another._str) <= 0;
}

mystring mystring::operator + (const mystring & another)
{
    int len = strlen( _str) + strlen(another._str);
    mystring ms;
    delete []ms._str;
    ms._str = new char[len+1]{0};//这里的{0}一定要写,不然乱码
    strcat(strcat(ms._str,_str),another._str);

    return ms;
}

mystring& mystring::operator += (const mystring & another)
{
    int catlen = strlen(this->_str);
    int srclen = strlen(another._str);
    int len = catlen + srclen;
    this -> _str = static_cast<char *>(realloc(this->_str,len + 1));
    memset(this->_str + catlen, 0 , srclen);
    strcat(this->_str , another._str);
    return *this;
}

ostream & operator<< (ostream & co, const mystring& ms)
{
    co << ms._str << endl;
    return co;
}

istream & operator>> (istream & ci,  mystring& ms)
{
    delete [] ms._str;
    char c[1024] = {0};

    cin >> c;
    ms._str = new char[strlen(c)+1];
    strcpy(ms._str,c);

    return ci;
}

char mystring::operator[](int n)
{
    return _str[n];
}

char mystring::at(int n)
{
    return _str[n];
}

char * mystring::c_str()
{
    return _str;
}

25、什么是虚函数?什么是纯虚函数?

虚函数是允许被其子类重新定义的成员函数。

虚函数的声明:virtual returntype func(parameter);引入虚函数的目的是为了动态绑定;

纯虚函数声明:virtual returntype func(parameter)=0;引入纯虚函数是为了派生接口。(使派生类仅仅只是继承函数的接口)

26、基类为什么需要虚析构函数?

防止内存泄漏。想去借助父类指针去销毁子类对象的时候,不能去销毁子类对象。假如没有虚析构函数,释放一个由基类指针指向的派生类对象时,不会触发动态绑定,则只会调用基类的析构函数,不会调用派生类的。派生类中申请的空间则得不到释放导致内存泄漏。

27、当i是一个整数的时候i++和++i那个更快?它们的区别是什么?

几乎一样。i++返回的是i的值,++i返回的是i+1的值,即++i是一个确定的值,是一个可以修改的左值。

27、模板类、流对象

Last modification:February 8th, 2020 at 08:14 pm
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment