C++八股

1. C++和C的区别

(1)基本语句方面,C++有新增的语法和关键字。语法上的区别有头文件和命名空间的不同,C++允许我们定义自己的命名空间,C中不可以。关键字方面的不同,比如动态内存的管理方式不同,C++在malloc和free的基础上增加了new和delete,C++在指针的基础上增加了引用的概念,还增加了auto、explicit体现显示、隐式的类型转换。

(2)函数方面,C++中有函数重载和虚函数的概念,而C中没有。

(3)类方面,C的struct和C++的类也有很大不同,C++的struct不仅有成员变量还可以有成员函数,有访问权限的概念,struct中的成员的访问和继承权限默认为public;C++还可以用class表示类,class和struct的区别在于,class成员的访问和继承权限默认为private。

(4)C++中增加了模板可以复用代码,提供了强大的STL标准库。 

(5)C是⼀种结构化的语⾔,重点在于算法和数据结构。C程序的设计首先考虑的是如何通过代码对输入进行运算处理输出。而C++⾸先考虑的是如何构造⼀个对象模型,让这个模型能够契合相应的问题领域,这样就能通过获取对象的状态信息得到输出。

2. C++中的内存分区

C++内存分区-博客园

(1)代码区:存放函数体的二进制代码,由操作系统进行管理;

(2)全局区:存放全局变量、静态变量(全局、局部)、字符串常量,程序编译时分配,程序结束时释放;

(3)栈(stack):由编译器自动分配释放,存放函数的参数值、局部变量、局部常量等;

(4)堆(heap):由程序员通过new/delete手动开辟和释放,若程序员不释放,程序结束后由操作系统回收。

3. C++中的智能指针

C++智能指针详解_夏之七的博客-CSDN博客

C++ 智能指针 - 全部用法详解 - 知乎

3.1 智能指针的作用和原理

智能指针是帮程序员管理动态分配的内存的,在函数结束时,它会帮助我们自动释放new出来的内存,从而避免内存泄漏。

智能指针是⼀个类,用于管理new出来的内存空间,函数结束后,类会自动调用析构函数,释放自身,同时也会释放new出来的内存空间。

3.2 常用接口

//T是模板参数,也就是传入的类型
T* get(); //获取封装在内部的指针,也就是智能指针托管的指针地址
T& operator*(); //重载*运算符
T* operator->(); //重载->运算符
T& operator=(const T& val); //重载=运算符
T* release(); //取消智能指针对动态内存的托管
 
//重置智能指针托管的内存地址。将参数的指针(不指定则为nullptr),
//与托管的指针比较,如果地址不一致,那么就会析构掉原来托管的指针,
//然后使用参数的指针替代之。然后智能指针就会托管参数的那个指针。
void reset (T* ptr = nullptr);

3.3 四种智能指针

3.3.1 auto_ptr

(1)内部实现:auto_ptr是一个类,包含原始指针成员,当auto_ptr类型的对象被释放掉时,利用析构函数,将拥有的原始指针delete掉。

(2)特点:auto_ptr是独占性的,不允许多个auto_ptr指向同一个资源。将一个auto_ptr对象复制或赋值给另一个auto_ptr对象时,原有对象的指针成员会重置为nullptr。

auto_ptr是C++98定义的智能指针模板,C++11已抛弃。C++11使用更严谨的unique_ptr替代auto_ptr。

3.3.2 unique_ptr(强引用)

强引用是指拥有某个资源的所有权(访问权、生命控制权)。

unique_ptr和auto_ptr的用法和内部实现差不多,都是独占资源的指针。但尝试复制unique_ptr会在编译时报错,而auto_ptr则能通过编译。因此unique_ptr比auto_ptr安全。

如果真的要转移所有权,需要使用std::move()将unique_ptr对象转换成右值后进行复制或赋值,转移所有权后,原unique_ptr对象的指针重置为nullptr。

3.3.3 shared_ptr(强引用)

允许多个shared_ptr对象指向同一处资源,当所有的shared_ptr对象被释放时,该处资源才会被释放。

(1)内部实现:每次复制时,多一个共享同处资源的shared_ptr对象时,计数+1。调用release()时,当前指针会释放资源所有权,计数-1。当计数为0时,释放该资源。可以通过成员函数use_count()来查看资源所有者的个数。

(2)缺陷:模型循环依赖(互相引用或环引用)时,计数会不正常。

3.3.4 weak_ptr(弱引用)

弱引用:只有某个对象的访问权,而没有它的生命控制权。

weak_ptr是为了辅助shared_ptr而引入的,它只提供了对管理对象的一个访问手段,它只可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造或析构不会引起引用计数的增加或减少。

weak_ptr可以解决shared_ptr相互引⽤时的死锁问题,如果两个shared_ptr相互引⽤,那么这两个指针的引⽤计数永远不可能下降为0,也就是资源永远不会释放。解决办法:把其中⼀个改为weak_ptr。

(1)内部实现:

  • 计数区域(SharedPtrControlBlock)结构体引进新的int变量weak_count,来作为弱引用计数;
  • 每个weak_ptr都占指针的两倍空间,一个装着原始指针,一个装着计数区域的指针(和shared_ptr一样的成员);
  • weak_ptr可以由一个shared_ptr或者另一个weak_ptr构造;
  • weak_ptr的构造和析构不会引起shared_count的增加或减少,只会引起weak_count的增加或减少。
  • 被管理资源的释放只取决于shared计数,当shared计数为0,才会释放被管理资源,也就是说weak_ptr不控制资源的生命周期;
  • 但是计数区域的释放却取决于shared计数和weak计数,当两者均为0时,才会释放计数区域。

(2)特点:weak_ptr没有重载*和->,所以不能直接当作指针使用。但可以使用lock()获得一个可用的shared_ptr对象,如果对象已经死了,lock()会失败,返回一个空的shared_ptr。

4. C++中的指针参数传递和引用参数传递的区别

值传递、指针传递、引用传递的区别-博客园

4.1 指针参数传递

指针参数传递是一种值传递,形参和实参是两个独立的对象。指针参数传递拷贝的是指针的值,拷贝后,两个指针是不同的指针,但指针的值是一样的,即指向同一个对象,通过指针可以修改所指对象的值。

4.2 引用参数传递

引用参数传递过程中,引用形参绑定了初始化它的对象,引用形参是实参的别名,对形参的操作就是对实参的操作。

5. C++中的const和static关键字的定义和用途

c++——static总结_c++ static_却道天凉_好个秋的博客-CSDN博客

C++中const,static用法总结-博客园

C++中关键字static和const的使用_static和const怎么用_越线的猫的博客-CSDN博客

5.1 const

(1)const修饰变量时,将该对象转换为一个常量,不可被修改。

(2)const修饰指针时,分为指针常量和常量指针两种情况,指针常量表示指针本身是常量,不可修改,即指向的地址不可修改,但指向的值可以修改;常量指针表示指针指向的是一个常量,即指针的指向可以修该,指向的值不可修改。

(3)const修饰引用时,表示引用的是const常量。

(4)const修饰函数形参时,表示在该参数在函数内不能改变。

(5)const修饰函数返回值时,函数返回值也要赋值给相应的const修饰的对象。

(6)const修饰成员函数时,函数体内不能修改任何成员变量(mutable修饰的变量除外),也不能调用非const成员函数。

(7)const修饰成员变量时,不能在类定义时初始化,只能通过构造函数初始化列表进行。

(8)const修饰类对象时,const对象只能调用const成员函数,而不能调用非const成员函数;而非const对象可以调用类中的const成员函数,也可以调用非const成员函数。

5.2 static

(1)修饰局部变量时,该变量会存放在全局区,其生命周期会延续到程序执行结束(比如子函数中初始化只进行一次,后续调用会跳过初始化,使用上次子函数结束时的值)。但其作用域没有改变,还是限制在语句块中。

(2)修饰全局变量时,限制该变量只能在本文件中访问到。若不加static关键字,则可以在同一个工程中的其他源文件中访问到(添加extern进行声明即可)。

(3)修饰函数时,和修饰全局变量类似,限制了函数的作用域,使得该函数只能在本文件中访问。

(4)修饰成员函数时,表示该成员函数属于一个类而不是属于此类的任何特定对象,静态成员函数只能访问静态成员。static成员函数不能被virtual修饰,static不属于任何实例或对象,所以加上virtual没有任何实际意义。

(5)修饰成员变量时,和修饰成员函数类似,类中的静态成员变量属于该类,该类的所有对象拥有的是同一个静态成员变量,类和对象都可以调用静态成员变量。在类的内部只能声明,定义和初始化要在类的外部。类的静态的数据成员在编译的时候就已经给分配了内存空间,在创建对象的时候,不会再给静态的数据成员分配内存空间。

6. C++如何定义常量,常量存放在内存的哪个位置

C++ 全局常量 静态常量 常量存储区 只读_c++ 全局常量 内存_丘上人的博客-CSDN博客

定义常量的2种方式:

(1)宏常量,define在编译的预处理阶段起作用,将MAX替换为20,define定义的只是个常数不带类型,宏常量存放在内存的代码区。

#define MAX = 20

(2)const关键字,在编译、运行阶段起作用,const定义的常数本质是变量,带类型。局部常量,存放在栈区;全局常量,存放在全局区;字面值常量,比如字符串,存放在常量区(全局区)。

7. C++中重载、重写、重定义的区别

C++重载、重写和重定义的区别_c++重载和重写的区别_Loren灬的博客-CSDN博客

  • 重载是指在同一类中,可以有一组函数名相同,参数列表不同(参数个数、顺序、类型三者中至少有一个不同)的函数,如果仅仅是返回值不同,其他都相同,无法构成重载。
  • 重写是指在继承中,子类对父类中的虚函数进行重新实现,函数名、参数列表均不可改变。
  • 重定义是指在继承中,子类对父类中的普通函数进行重新实现,函数名、参数列表均不可改变。
  • 重写和重定义的最大区别是父类的目标函数是否为虚函数。

8. C++中的构造函数

C++ 构造函数详解_c++构造函数_璇焱如柳的博客-CSDN博客

构造函数与类名相同,且没有返回值,作用是初始化对象的数据成员。

8.1 无参构造

即默认构造,如果程序员不手动定义构造函数,编译器会自动定义默认构造函数,函数为空,什么也不做,如果不想使用自动生成的无参构造函数,要自己写出⼀个无参构造函数。

8.2 有参构造

一个类可以重载多个有参构造函数,创建对象时根据传入参数的不同,调用不同的有参构造函数。程序员手动定义有参构造函数后,编译器便不再生成默认的无参构造函数。

8.3 拷贝构造

拷贝构造函数的参数为已存在的同类型的对象,在拷贝构造函数中,会将已存在的对象的数据成员的值复制到新创建的对象中。如果没有显式地写出拷贝构造函数,编译器会默认创建一个拷贝构造函数。但当类中有指针成员时,最好不要使用编译器提供的默认的拷贝构造函数,最好自己定义并且在函数中执行深拷贝。

9. C++的四种强制类型转换

C++的四种强制类型转换包括:static_cast,dynamic_cast,const_cast,reinterpret_cast

9.1 static_cast

C++基础#22:C++中的静态强制static_cast_c++ static_cast_liranke的博客-CSDN博客

(1)static_cast可以用来进行基本类型的转换,例如short和int、int和float、enum与int等。

(2)static_cast也可以用于父类指针和子类指针间的类型转换,上行转换(子类->父类)安全,下行转换(父类->子类)不安全。

(3)也可以用于两个不相关的类之间的类型转换,由A类转换到B类时,必须要在B类中定义以A类对象为参数的构造函数。

(4)static_cast可以把任何表达式都转换成void类型,例如double指针转换成void指针等。

9.2 dynamic_cast

dynamic_cast是为解决虚基类到派生类的转换而设计的。

动态强制(dynamic_cast),是一种运行时类型识别机制,可以从一个虚的父类强制到一个子类。我们常常需要知道,在运行过程中,我们真正在使用的是哪个类的对象,这时,可以明确地使用dynamic_cast来得到运行时类型的信息。

9.3 const_cast

9.4 reinterpret_cast

10. 指针和引用的区别

指针和引用都是一种内存地址的概念,区别有以下几点:

(1)指针是一个实体,而引用只是一个别名。

(2)程序编译时,会将指针和引用都添加到符号表中,对于指针,是将“指针变量名-指针变量的地址”添加到符号表中,指针包含的内容是可以改变的,允许复制和拷贝,sizeof指针得到的是指针类型的大小。

(3)对于引用,是将“引用变量名-引用变量的地址”添加到符号表中,符号表一经完成不可改变,所以引用必须而且只能在定义时绑定到一块内存地址上,后续不能更改,也不能为空。

(4)sizeof引用得到引用对象的大小,而sizeof指针得到的是指针本身的大小。

(5)作为参数传递时,传指针的实质是传值,传递的值是指针本身;传引用的实质是传地址,传递的是引用对象的地址。

11. 野(wild)指针和悬空(dangling)指针的区别,如何避免

野指针是没有被初始化的指针;悬空指针是指向的内存已经被释放了的一种指针。野指针和悬空指针都是指向无效内存区域(无效指的是“不安全不可控”)的指针。如何避免野指针?在定义指针时初始化或者使用智能指针。如何避免悬空指针?合理释放内存:调用delete或free释放内存空间后,需要将指针设置为nullptr;确保作用范围:当指针指向的变量已经离开作用域被释放时,需要将指针设置为nullptr。

12. const修饰指针的三种情况

const int * p1; //常量指针,指向常量的指针,即指向的值不能修改
int * const p2; //指针常量,指针本身是常量,即指针的指向不能修改
const int * const p3; //指向常量的指针常量,即指针的指向和指向的值都不能修改

13. 函数指针

c++函数指针_c++ 函数指针_D@@的博客-CSDN博客

函数指针是一个指针变量,该指针变量指向一个具体的函数,在编译时,每⼀个函数都有⼀个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后, 可用该指针变量调用函数,也可将函数指针当作参数进行传递。

double fun(int a) {...} //fun是一个函数,返回值类型为double
double *pt(int a) {...}; //pt是函数名,此函数的返回值类型为double指针

//正确的声明pf后,便可以将相应函数的地址赋给它:
double (*pf)(int);
pf = fun;
//注意,fun()的特征标(参数类型)和返回类型必须与pf相同。

//两种使用函数指针调用函数的方法
double b = pf(a);
double b = (*pf)(a);

14. 堆和栈的区别

c/c++后台开发必知堆与栈的区别 - 知乎

栈由操作系统自动分配释放,用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。堆由开发人员分配和释放,若开发人员不释放,程序结束时由操作系统回收,分配方式类似于链表。栈和堆的区别如下。

(1)管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏。

(2)空间大小不同。每个进程拥有的栈的大小要远远小于堆的大小。理论上,程序员可申请的堆大小为虚拟内存的大小;栈的大小64bits的Windows默认1MB,64bits的Linux默认10MB。

(3)生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。

4)分配方式不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca函数进行分配,栈的动态分配是由操作系统进行释放,无需我们手工实现。

(5)分配效率不同。栈由操作系统自动分配,会在硬件层级对栈提供支持,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片也会降低分配效率,使得堆的效率比栈要低得多。

15. 函数传递函数的几种方式

(1)值传递,形参是实参的拷贝,函数内部对形参的操作不会影响外部的实参;

(2)指针传递,也是值传递的一种方式,传递的是指向外部实参的指针的拷贝;

(3)引用传递,传递的是引用对象的地址,函数内部对形参的任何操作相当于是对外部实参的操作。

16. new/delete,malloc/free的区别

new / delete ,malloc / free 区别 - 知乎

new/delete、malloc/free 的区别-博客园

相同点:都可以用来在堆上分配和回收内存空间。

不同点:

(1)new能自行计算需要分配的空间,malloc需要手动计算字节数。

(2)new返回具体类型的指针;malloc返回void类型的指针,若想返回具体类型的指针,需要进行强制类型转换。

(3)new内存分配失败时,会抛出bac_alloc异常;malloc分配内存失败时返回NULL。

(4)new/delete是C++操作符,不需要任何库的支持;malloc/free是库函数,需要包含头文件"stdlib.h"。

(5)new操作实际执行两个过程:分配未初始化的内存空间(malloc)、使用对象的构造函数对空间进行初始化并返回空间首地址,若第一步出现问题,则会抛出std::bad_alloc异常,若第二步出现问题,则会自动调用delete释放内存。delete操作实际也有两个过程:使用析构函数对对象进行析构、回收内存空间(free)。所以,new得到的是经过初始化的内存空间,malloc得到的是未初始化的内存空间;delete不仅释放空间还析构对象,而free只内存释放空间。

17. volatile和extern关键字

C/C++知识总结--求职笔试常考的extern、const、volatile三个关键字_extern const volatile_程序员龙一的博客-CSDN博客

17.1 volatile

volatile提醒编译器它后面所定义的变量随时都有可能改变,使编译器对该变量不做优化,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问,保证程序员写在代码中的指令,⼀定会被执行。

volatile变量的几个使用场景:

(1)中断服务程序中修改的供其它程序检测的变量需要加volatile;

(2)多线程应用中被几个任务共享的变量;

(3)并行设备的硬件寄存器。

17.2 extern

(1)extern置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义,即所谓全局声明。

(2)另一种用法是 extern "C",主要作用是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。C和C++编译函数的区别:由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

18. define和const的区别

c++语言基础#define与const的区别_c++ define和const区别_迷失的犬的博客-CSDN博客

(1)define是在代码编译阶段中的预处理过程进行的,宏定义在预处理过程将代码中所有的标识符替换成后面的常量了,只是作简单的替换,不会去检查验证后面的常量是否正常(有可能会导致边界效应);其次,在程序运行的整个声明周期里,不能对该标识符进行调试。最重要的是,宏定义不占据数据内存空间,它是占用代码段空间。

(2)const是一个关键字,const常量是在编译和运行期间起作用,而且用const修饰的常量是需要有类型说明的(像int,string等类型)。并且常量定义后在运行期间是不能再进行修改的,const可以修饰变量、函数参数、返回值、指针、引用、成员变量、成员函数等。

19. 面向对象的三大特性,并举例说明

C++⾯向对象的三大特征是:封装、继承、多态。

19.1 封装

封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

19.2 继承

继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或“派生类”;被继承的类称为“父类”或“基类”,继承就是从一般到特殊的过程。

继承概念的实现方式有三类:实现继承、接口继承。

(1)实现继承是指使用基类的属性和方法而无需额外编码的能力;

(2)接口继承是指仅使用基类的属性和方法的名称,子类必须提供实现的能力。

19.3 多态

20. 多态的实现

多态是指为不同的数据类型提供统一的接口,分为静态多态和动态多态。

(1)静态多态包括函数重载和模板函数,静态多态在编译时期就决定了调用哪个函数。

(2)动态多态是:指针或引用类型可以根据运行中实际指向的派生类型的不同,来执行不同派生类的方法。动态多态通过子类重写父类的虚函数来实现,因为是在运行期间决定调用的函数,所以称为动态多态。动态多态的实现与虚函数表、虚函数指针相关。

⼀般情况下我们不区分这两个时所说的多态就是指动态多态。

21. 虚函数相关(虚函数表,虚函数指针),虚函数的实现原理

21.1 虚函数的定义

在某基类中用virtual关键字修饰,并在一个或多个派生类中可以重写的成员函数,就是虚函数。

21.2 虚函数的作用

C++中,基类的成员函数有两种,一种是基类希望其派生类进行覆盖的函数,另一种是基类希望派生类直接继承不要改变的函数。C++中虚函数的作用主要是实现了多态。关于多态,简而言之就是用父类型的指针指向子类的实例,然后通过父类的指针调用实际子类的成员函数,这种技术可以让父类的指针有多种形态,用不变的代码可以实现不同的功能和属性。

21.3 虚函数的实现原理

如果不存在虚函数的话,对象执行方法的过程:根据对象声明的类型以及方法的名称,去代码区找到对应的方法,然后调用。

引入了虚函数表指针vptr、虚函数表V-Table后,每个对象有一个虚函数表指针,每个类有一个虚函数表。虚函数表指针大小为4字节,存储在对象的起始地址。虚函数表中存储的是对应类的所有虚函数的地址(函数指针)。

有了虚函数之后的对象执行方法过程:
1、先根据对象本身的虚函数表指针找到虚函数表;
2、在虚函数表中查找有没有该方法,有的话就执行,没有找到的话跳转到3;
3、根据声明的类型和方法名称去代码区找该方法(原本的过程)。

虚函数表如何储存函数:
一、单重继承
(1)子类继承了父类,那么虚函数表一开始是和父类一样的。
(2)如果子类重写了父类的某个虚函数,那么在虚函数表中,子类重写的虚函数地址会覆盖父类相应的虚函数地址。
(3)如果子类新增加了某个虚函数,这个虚函数的地址会添加到虚函数表的尾部。

二、多重继承
(1)
(2)
(3)


 

22. 编译器如何处理虚函数表

23. 析构函数一般写成虚函数的原因

24. 构造函数为什么一般不定义为虚函数

25. 构造函数或析构函数中调用虚函数会怎么样

26. 析构函数的作用,如何起作用?

27. 析构函数的执行顺序?

28. 纯虚函数

29. 静态绑定和动态绑定

30. 深拷贝和浅拷贝

31. 什么情况下会调用拷贝构造函数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值