C++基础闯关100题,你能闯多少?【2021超硬核大厂高频面试题】

前言

C++一直被称作永不过时的开发语言,游戏、服务器、人工智能等领域都必须用到他!

今天我整合了2021年100道大厂高频C++基础面试题,里面包含了C++很多基础知识点,干货满满。因内容较多,篇幅较长,所以会分成上下两篇讲解,强烈建议小伙伴们收藏!

下面我们一起来测验下, 大家一起看看能闯多少关?
在这里插入图片描述

1、C++内存分为哪几块?

  • ,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • ,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
  • 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
  • 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。

2、在main执行之前和之后执行的代码可能是什么?

main函数执行之前,主要就是初始化系统相关资源:

  • 设置栈指针

  • 初始化静态static变量和global全局变量,即.data段的内容

  • 将未初始化部分的全局变量赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL等等,即.bss段的内容

  • 全局对象初始化,在main之前调用构造函数,这是可能会执行前的一些代码

  • 将main函数的参数argc,argv等传递给main函数,然后才真正运行main函数

main函数执行之后

  • 全局对象的析构函数会在main函数之后执行

  • 可以用 atexit 注册一个函数,它会在main 之后执行

3、C++里面如何声明const void f(void)函数为C程序中的库函数?

在该函数前添加extern “C”声明。由于编译后的名字不同,C++程序不能直接调用C 函数。

4、指针和引用的区别是什么?

  • 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名

  • 指针可以有多级,引用只有一级

  • 指针可以为空,引用不能为NULL且在定义时必须初始化

  • 指针在初始化后可以改变指向,而引用在初始化之后不可再改变

  • sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小

  • 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。

  • 引用只是别名,不占用具体存储空间,只有声明没有定义;指针是具体变量,需要占用存储空间。

  • 引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。

  • 引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量。

  • 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。

void test(int *p)
{
    
    
  int a=1;
  p=&a;
  cout<<p<<" "<<*p<<endl;
}

int main(void)
{
    
    
    int *p=NULL;
    test(p);
    if(p==NULL)
    cout<<"指针p为NULL"<<endl;
    return 0;
}
//运行结果为:
//0x22ff44 1
//指针p为NULL
void testPTR(int* p) {
    
    
    int a = 12;
    p = &a;

}

void testREFF(int& p) {
    
    
    int a = 12;
    p = a;

}
void main()
{
    
    
    int a = 10;
    int* b = &a;
    testPTR(b);//改变指针指向,但是没改变指针的所指的内容
    cout << a << endl;// 10
    cout << *b << endl;// 10

    a = 10;
    testREFF(a);
    cout << a << endl;//12
}

5、一个C++源文件从文本到可执行文件经历的过程?

  • 预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件
  • 编译阶段:将经过预处理后的预编译文件转换成特定汇编代码(编译原理相关,词法分析、语法分析、语义分析等),生成汇编文件
  • 汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
  • 链接阶段:将多个目标文件及所需要的库打包连接成最终的可执行目标文件(或库文件以供其他程序使用)

6、堆和栈的区别是什么?

  • 申请方式不同:栈由系统自动分配;堆是自己申请和释放的。

  • 申请大小限制不同:栈顶和栈底是之前预设好的,栈是向栈底扩展,大小固定,可以通过ulimit -a查看,由ulimit -s修改;堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。

  • 申请效率不同:栈由系统分配,速度快,不会有碎片;堆由程序员分配,速度慢,且会有碎片。

7、说一说C与C++的内存分配方式?

  • 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,如全局变量,static变量。

  • 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

  • 从堆上分配(动态内存分配)程序在运行的时候用malloc或new申请任意多少的内存,程序员负责在何时用free或delete释放内存。动态内存的生存期自己决定,使用非常灵活。

8、区别以下指针类型?

int *p[10]
int (*p)[10]
int *p(int)
int (*p)(int)
  • int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。

  • int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。

  • int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。

  • int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。

9、new和malloc的区别?

  • new是C++运算符,malloc()是C/C++语言标准库函数

  • new会自动计算需分配的空间,malloc不行

  • new会调用构造函数,malloc不会

  • new是类型安全的,而malloc不是

int *p = new float[2]; //编译错误
int *p = (int*)malloc(2 * sizeof(double));//编译无错误
  • malloc/free要库文件支持,new/delete则不要
  • new返回指定类型指针,malloc返回void*指针,需要强制类型转换
  • new可以被重载,malloc不能

10、new和delete是如何实现的?

  • new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回指向新分配并构造后的的对象的指针

  • delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存

11、虚函数是什么以及其作用?

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

可以实现用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。

有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员,从而实现多态。

注意:构造函数不能为虚函数,但是析构函数可以为虚函数,并且虚析构函数可以防止父类指针销毁子类对象时不正常导致的内存泄漏。

12、C++中struct和class的区别?

相同点

  • 两者都拥有成员函数、公有和私有部分

  • 任何可以使用class完成的工作,同样可以使用struct完成

不同点

  • 两者中如果不对成员不指定公私有,struct默认是公有的,class则默认是私有的

  • class默认是private继承,而struct模式是public继承

  • class可以作为模板类型,struct不行

13、C++中const和static的作用?

const

  • 不考虑类的情况

    • const常量在定义时必须初始化,之后无法更改

    • const形参可以接收const和非const类型的实参

// i 可以是 int 型或者 const int 型
void fun(const int& i){
    
    
    //...
}
  • 考虑类的情况

    • const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化。

    • const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值。

static

  • 不考虑类的情况

    • 隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用。

    • 默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区。

    • 静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用。

  • 考虑类的情况

    • static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。

    • static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问。

14、C++的顶层const和底层const?

顶层const:指的是const修饰的变量本身是一个常量,无法修改,指的是指针,就是 * 号的右边。

底层const:指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是 * 号的左边。

int a = 10;
int* const b1 = &a;        //顶层const,b1本身是一个常量
const int* b2 = &a;        //底层const,b2本身可变,所指的对象是常量
const int b3 = 20;            //顶层const,b3是常量不可变
const int* const b4 = &a;  //前一个const为底层,后一个为顶层,b4不可变
const int& b5 = a;           //用于声明引用变量,都是底层const

执行对象拷贝时有限制,常量的底层const不能赋值给非常量的底层const。

使用命名的强制类型转换函数const_cast时,只能改变运算对象的底层const。

const int a;
int const a;
const int *a;
int *const a;
  • int const a和const int a均表示定义常量类型a。

  • const int *a,其中a为指向int型变量的指针,const在 * 左侧,表示a指向不可变常量。(看成const (*a),对引用加const)

  • int *const a,依旧是指针类型,表示a为指向整型数据的常指针。(看成const(a),对指针const)

15、简单介绍内存池?

内存池是一种内存分配方式。通常我们习惯直接使用new、malloc申请内存。

这样做的缺点在于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。

内存池则是在真正使用内存之前,预先申请分配一定数量、大小相等(一般情况下)的内存块留作备用。

当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。

16、宏定义和函数有何区别?

  • 宏在编译时完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数。

  • 宏定义属于在结构中插入代码,没有返回值;函数调用具有返回值。

  • 宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。

  • 宏定义不要在最后加分号。

17、变量声明和定义区别?

  • 声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间。

  • 相同变量可以在多处声明(外部变量extern),但只能在一处定义。

18、strlen和sizeof区别?

  • sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。

  • sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是’\0’的字符串。

  • 因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。

 int main(int argc, char const *argv[]){
    
       

      const char* str = "name";
      sizeof(str); // 取的是指针str的长度,是8
      strlen(str); // 取的是这个字符串的长度,不包含结尾的 \0。大小是4
      return 0;
  }

19、简单描述内存泄漏?

内存泄漏一般是指堆内存的泄漏,也就是程序在运行过程中动态申请的内存空间不再使用后没有及时释放,导致那块内存不能被再次使用。

20、什么函数不能声明为虚函数?构造函数为什么不能为虚函数?

  • 普通函数(非成员函数)、构造函数、友元函数、静态成员函数、内联成员函数。
  • 当派生类在创建对象的时候会调用基类的构造函数,但是如果基类的构造函数是虚函数的话,派生类的构造函数又会把基类的构造函数覆盖,所以无法进一步执行而出错。
  • 虚函数通过虚函数表来实现,而指向虚函数表的指针也需要在对象实例化后创建,那么就违背了先实例化后调用的准则。

21、常量指针和指针常量区别?

  • 常量指针是一个指针,读成常量的指针,指向一个只读变量。如int const *p或const int *p。

  • 指针常量是一个不能给改变指向的指针。指针是个常亮,不能中途改变指向,如int *const p。

22、a和&a有什么区别?

假设数组int a[10];
int (*p)[10] = &a;
  • a是数组名,是数组首元素地址,+1表示地址值加上一个int类型的大小,如果a的值是0x00000001,加1操作后变为0x00000005。*(a + 1) = a[1]。

  • &a是数组的指针,其类型为int (*)[10](就是前面提到的数组指针),其加1时,系统会认为是数组首地址加上整个数组的偏移(10个int型变量),值为数组a尾元素后一个元素的地址。

  • 若(int *)p ,此时输出 *p时,其值为a[0]的值,因为被转为int *类型,解引用时按照int类型大小来读取。

23、虚函数与纯虚函数的区别?

  • 定义一个函数为虚函数,不代表函数为不被实现的函数。
  • 定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
  • 定义一个函数为纯虚函数,才代表函数没有被实现。
  • 定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

24、数组名和指针(这里为指向数组首元素的指针)区别?

  • 二者均可通过增减偏移量来访问数组中的元素。

数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。

当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。

25、内联函数和宏定义的区别?

宏定义在预处理的时候进行简单的字符串替换,而内联函数在编译时在每个调用内联函数的地方将函数展开,这样不用使内联函数占用栈空间,提高效率。

宏定义没有类型检查,但是内联函数还是具有函数的性质,有参数以及返回值。

26、struct与union的区别?

  • struct可以存储多个成员变量信息;而union每个成员会共用同一个存储空间。
  • 系统分配给union的内存size就是size最大的成员所需空间大小,struct在分配空间时,根据成员类型不同,会存在字节对齐情况,具体对齐标准和机器有关,可能是4字节或8字节等对齐方式。
  • 在任何同一时刻,union值存放了一个被先选中的成员,而结构体struct的所有成员都存在。

27、什么是野指针和悬空指针?

都是是指向无效内存区域(这里的无效指的是"不安全不可控")的指针,访问行为将会导致未定义行为。

野指针,指的是没有被初始化过的指针。

int main(void) {
    
     
  int * p;
  std::cout<<*p<<std::endl;
  return 0;
}

为了防止出错,对于指针初始化时都是赋值为 nullptr,这样在使用时编译器就会直接报错,产生非法内存访问。

悬空指针,指针最初指向的内存已经被释放了的一种指针。

int main(void) {
    
     
  int * p = nullptr;
  int* p2 = new int; 
  p = p2;
  delete p2;
}

此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr。此时再使用,编译器会直接保错。

避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。

产生原因及解决办法

野指针:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。

悬空指针:指针free或delete之后没有及时置空 => 释放操作后立即置空。

28、final和override关键字

final

当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。例子如下:

class Base
{
    
    
    virtual void foo();
};

class A : public Base
{
    
    
    void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
};

class B final : A // 指明B是不可以被继承的
{
    
    
    void foo() override; // Error: 在A中已经被final了
};

class C : B // Error: B is final
{
    
    
};

override

当在父类中使用了虚函数时候,你可能需要在某个子类中对这个虚函数进行重写,以下方法都可以:

class A
{
    
    
    virtual void foo();
}
class B : public A
{
    
    
    void foo(); //OK
    virtual void foo(); // OK
    void foo() override; //OK
}

如果不使用override,当你手一抖,将foo()写成了foo()会怎么样呢?

结果是编译器并不会报错,因为它并不知道你的目的是重写虚函数,而是把它当成了新的函数。

如果这个虚函数很重要的话,那就会对整个程序不利。

所以,override的作用就出来了,它指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的:

class A
{
    
    
    virtual void foo();
};
class B : public A
{
    
    
    virtual void f00(); //OK,这个函数是B新增的,不是继承的
    virtual void f0o() override; //Error, 加了override之后,这个函数一定是继承自A的,A找不到就报错
};

29、static的用法和作用?

  • 隐藏,static函数,static变量均可。当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。

  • 保持变量内容的持久。(static变量中的记忆功能和全局生存期)存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。
    共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。

  • static的第三个作用是默认初始化为0(static变量)。
    其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。

  • static的第四个作用:C++中的类成员声明static

    • 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;

    • 在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;

    • 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;

    • 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;

    • 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

    • static类对象必须要在类外进行初始化,static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化;

    • 由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针的,this指针是指向本对象的指针。正因为没有this指针,所以static类成员函数不能访问非static的类成员,只能访问 static修饰的类成员;

    • static成员函数不能被virtual修饰,static成员不属于任何对象或实例,所以加上virtual没有任何实际意义;静态成员函数没有this指针,虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function。

30、拷贝初始化和直接初始化?

  • 当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。
string str1("I am a string");//语句1 直接初始化
string str2(str1);//语句2 直接初始化,str1是已经存在的对象,直接调用构造函数对str2进行初始化
string str3 = "I am a string";//语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造str3
string str4 = str1;//语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数
  • 为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样就完全等价于直接初始化了(语句1和语句3等价)。但是需要辨别两种情况。

  • 当拷贝构造函数为private时:语句3和语句4在编译时会报错。

  • 使用explicit修饰构造函数时:如果构造函数存在隐式转换,编译时会报错。

31、初始化和赋值的区别?

  • 对于简单类型来说,初始化和赋值没什么区别

  • 对于类和复杂数据类型来说,这两者的区别就大了。

class A{
    
    
public:
    int num1;
    int num2;
public:
    A(int a=0, int b=0):num1(a),num2(b){
    
    };
    A(const A& a){
    
    };
    //重载 = 号操作符函数
    A& operator=(const A& a){
    
    
        num1 = a.num1 + 1;
        num2 = a.num2 + 1;
        return *this;
    };
};
int main(){
    
    

    A a(1,1);
    A a1 = a; //拷贝初始化操作,调用拷贝构造函数
    A b;
    b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2
    return 0;
}

32、free()一个指针两次,会出现什么问题 ?

free一次后,原来指针所指向的堆中的内容已经被清空了,但指针本身的值并没有被置为null,还是指向原来它所指向的内存空间。

再free一次时,由于堆中的内容已经是无效的东西,所以就会出错。

不过,有的编译器在free时并没有清理堆中的内存,有时你对它free两次也不一定出错。不过这是一个很大的隐患,在实际写代码中千万要注意避开这点。

33、浅拷贝是什么?深拷贝是什么?

浅拷贝

  • 在拥有指针成员的类中,一个对象利用拷贝构造函数或者赋值函数拷贝或者赋值给另一个对象的时候,直接将这个对象的指针成员赋值给另一个对象的指针成员,将一个指针赋值给另一个指针,就会使两个指针指向同一个空间,这就产生了浅拷贝。
  • 浅拷贝会造成一些问题例如内存泄漏、同一片内存释放多次、一个指针修改了这块空间的值那么另一个指针也指向这块空间就会出错。

深拷贝

  • 在拷贝构造函数或赋值函数中不是直接的将指针赋给另外一个对象的指针,而是新开辟一块内存空间,将被拷贝或赋值的对象的指针成员指向新开辟的内存空间,然后再将数据拷贝过去。

34、hash表解决冲突的方法?

  • 开放地址法:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。

  • 链地址法:基本思想是将所有哈希地址为 i 的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

  • 建立公共溢出区:这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

  • 再哈希法: 再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。

35、新建一个空类,里面有什么函数?

无参构造函数,析构函数,拷贝构造函数,重载赋值运算符函数。

36、volatile关键字的作用?

volatile 关键字告诉编译器该关键字修饰的变量是随时可能发生变化的。

每次使用它的时候必须从内存中取出它的值,因而编译器生成的汇编代码会重新从它的地址处读取数据放在左值中。

如果该变量是一个寄存器变量或者表示一个端口数据或者是多个线程的共享数据,就容易出错,所以说volatile 可以保证对特殊地址的稳定访问。

37、为什么析构函数一般写成虚函数?

由于类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。

如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。

将析构函数声明为虚函数,在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,要将基类的析构函数声明为虚函数。

#include <iostream>
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

将基类的析构函数声明为虚函数:

#include <iostream>
using namespace std;

class Parent{
    
    
public:
    Parent(){
    
    
        cout << "Parent construct function"  << endl;
    };
    virtual ~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
//Son destructor function
//Parent destructor function

38、C++中的重载、重写(覆盖)和隐藏的区别?

重载(overload)

重载是指在同一范围定义中的同名成员函数才存在重载关系。

主要特点是函数名相同,参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数,重载和函数成员是否是虚函数无关。

class A{
    
    
    ...
    virtual int fun();
    void fun(int);
    void fun(double, double);
    static int fun(char);
    ...
}

重写(覆盖)(override)
重写指的是在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数且:

  • 与基类的虚函数有相同的参数个数

  • 与基类的虚函数有相同的参数类型

  • 与基类的虚函数有相同的返回值类型

//父类
class A{
    
    
public:
    virtual int fun(int a){
    
    }
}
//子类
class B : public A{
    
    
public:
    //重写,一般加override可以确保是重写父类的函数
    virtual int fun(int a) override{
    
    }
}

重载与重写的区别:

  • 重写是父类和子类之间的垂直关系,重载是不同函数之间的水平关系

  • 重写要求参数列表相同,重载则要求参数列表不同,返回值不要求

  • 重写关系中,调用方法根据对象类型决定,重载根据调用时实参表与形参表的对应关系来选择函数体

隐藏(hide)
隐藏指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数,包括以下情况:

  • 两个函数参数相同,但是基类函数不是虚函数。和重写的区别在于基类函数是否是虚函数。
//父类
class A{
    
    
public:
    void fun(int a){
    
    
        cout << "A中的fun函数" << endl;
    }
};
//子类
class B : public A{
    
    
public:
    //隐藏父类的fun函数
    void fun(int a){
    
    
        cout << "B中的fun函数" << endl;
    }
};
int main(){
    
    
    B b;
    b.fun(2); //调用的是B中的fun函数
    b.A::fun(2); //调用A中fun函数
    return 0;
}
  • 两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个类中。
//父类
class A{
    
    
public:
    virtual void fun(int a){
    
    
        cout << "A中的fun函数" << endl;
    }
};
//子类
class B : public A{
    
    
public:
    //隐藏父类的fun函数
   virtual void fun(char* a){
    
    
       cout << "A中的fun函数" << endl;
   }
};
int main(){
    
    
    B b;
    b.fun(2); //报错,调用的是B中的fun函数,参数类型不对
    b.A::fun(2); //调用A中fun函数
    return 0;
}

39、C++有哪几种的构造函数?

C++中的构造函数可以分为4类:

  • 默认构造函数

  • 初始化构造函数(有参数)

  • 拷贝构造函数

  • 移动构造函数(move和右值引用)

  • 委托构造函数

  • 转换构造函数

#include <iostream>
using namespace std;

class Student{
    
    
public:
    Student(){
    
    //默认构造函数,没有参数
        this->age = 20;
        this->num = 1000;
    };  
    Student(int a, int n):age(a), num(n){
    
    }; //初始化构造函数,有参数和参数列表
    Student(const Student& s){
    
    //拷贝构造函数,这里与编译器生成的一致
        this->age = s.age;
        this->num = s.num;
    }; 
    Student(int r){
    
       //转换构造函数,形参是其他类型变量,且只有一个形参
        this->age = r;
        this->num = 1002;
    };
    ~Student(){
    
    }
public:
    int age;
    int num;
};

int main(){
    
    
    Student s1;
    Student s2(18,1001);
    int a = 10;
    Student s3(a);
    Student s4(s3);

    printf("s1 age:%d, num:%d\n", s1.age, s1.num);
    printf("s2 age:%d, num:%d\n", s2.age, s2.num);
    printf("s3 age:%d, num:%d\n", s3.age, s3.num);
    printf("s2 age:%d, num:%d\n", s4.age, s4.num);
    return 0;
}
//运行结果
//s1 age:20, num:1000
//s2 age:18, num:1001
//s3 age:10, num:1002
//s2 age:10, num:1002
  • 默认构造函数和初始化构造函数在定义类的对象,完成对象的初始化工作。

  • 复制构造函数用于复制本类的对象。

  • 转换构造函数用于将其他类型的变量,隐式转换为本类对象。

40、静态变量什么时候初始化?

  • 初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存。

  • 静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但在C和C++中静态局部变量的初始化节点又有点不太一样。在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全部回收。

  • C++中,初始化时在执行相关代码时才会进行初始化,主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以C++标准定为全局或静态对象是有首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的。

好了,今天就到这里,我们就先整理前40道C++基础闯关,大家答对了多少道呢?明天我们继续努力!
在这里插入图片描述

创作不易,白嫖不好,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!

Dragon少年 | 文

如果本篇博客有任何错误,请批评指教,不胜感激 !

猜你喜欢

转载自blog.csdn.net/hhladminhhl/article/details/117922183