1.智能指针:管理指针,避免申请的空间忘记释放,防止内存
1.1 unique_ptr:保证同一时间智能有一个智能指针指向该对象,避免资源泄漏。 1.2 shared_ptr(共享型,强引用):多个智能指针可以指向相同对象,最后一个引用销毁时释放资源,使用use_count查看被引个数。解决了独占性,但是相互引用会导致锁死。 1.3 weak_ptr(弱引用):解决两个强引用导致的死锁问题,将其一替换。
2.内存分配
2.1 栈:编译器管理,存局部变量和函数参数 2.2 堆:由程序员自己开辟和释放,容易出现内存泄漏和空闲碎片的问题 2.3 全局/静态存储区:分为初始化和未初始化区域,存储全局变量和静态变量 2.4 常量区:存储常量,一般不允许修改 2.5 代码区:存放程序的二进制代码
3.指针/引用 参数传递
指针参数传递(开辟空间存放的是指针变量的地址):会开辟一块新的内存空间存放传输而来的值,并指向原指针指向的对象。可以改变指向对象的值,但是不能改变指针的值。 引用参数传递(引用对象的地址值):没有发生拷贝,相当于起别名,都视为间接寻址的操作。
4.从const和static关键字
4.1 static作用:控制变量的存储方式和可见性
4.1.1 修饰局部变量:用static之后局部变量将存放在静态数据区,其生命周期将一直延续到程序执行结束。作用域不变。 4.1.2 修饰全局变量:全局变量既可以在文本文件中被访问,也可以在工程中被访问(extern声明)。static之后会改变其作用域使其智能在本文件可见。 4.1.3 修饰函数:同修饰全局变量。 4.1.4 修饰类:修饰类中函数,则该函数不属于类中特定对象;修饰类中变量,可以通过类和对象进行调用。 4.1.5 类成员\类函数声明static 内存只分配一次,值不变 模块内的static全局变量智能被模块内函数访问 类中的static成员变量属类所有,只有一份拷贝 类中static成员函数,不接收this指针,只能访问类的static变量 static 类对象必须要在类外进⾏初始化,static 修饰的变量先于对象存在,所以 static 修饰的变量要在类外初始化 static 类成员函数不能访问⾮ static 的类成员,只能访问 static修饰的类成员 static 成员函数不能被 virtual 修饰,static不属于任何对象或实例;静态成员函数没有 this 指针,虚函数的实现是为每⼀个对象分配⼀个 vptr 指针,⽽ vptr 是通过 this 指针调⽤的,所以不能为 virtual
4.2 const关键字:
4.2.1 修饰基本数据类型:表示常量不可修改其值 4.2.2 修饰指针变量和引用变量:如果 const 位于⼩星星的左侧,则 const 就是⽤来修饰指针所指向的变量,即指针指向为常量;如果 const 位于⼩星星的右侧,则 const 就是修饰指针本身,即指针本身是常量。
4.2.3 应用到函数:const 所修饰的部分进⾏常量化,保护了原对象的属性。 [注意]:参数 const 通常⽤于参数为指针或引⽤的情况; 4.2.4 类中用法:const 成员变量,只在某个对象⽣命周期内是常量,⽽对于整个类⽽⾔是可以改变的。一个类可以创建多个对象,其const数据成员值可以不同。因此不能在声明中初始化值,只能在构造函数中进行。 4.3 要注意,const 关键字和 static 关键字对于成员函数来说是不能同时使⽤的,因为 static 关键字修饰静态成员函数不含有 this 指针,即不能实例化,const 成员函数⼜必须具体到某⼀个函数。
5.结构体内存对齐问题
结构体成员按照声明顺序存储,第一个成员地址和结构体地址相同,且按照结构体中size最大的成员对齐。
C++11后引入alignas(计算类型对齐方式)与alignof(指定结构体对齐方式)
6.C和C++的区别(函数、类、struct、class)
1.C++有新增的语法和关键字,语法上主要体现在头文件和命名空间的不同。C++允许我们定义自己的空间。关键字上而言他们的动态管理方式不同c++增加了new和delete,且增加了引用的概念,关键字还增加了auto、explict体现显示和隐式转换上的概念要求,还有dynamic_cast增加安全方面的内容。
2.c++增加重载和虚函数的概念。c++支持重载但是c不支持,因为c++函数的名字修饰与c不同。例如int function(int ,double)c++中会修饰成 func_int_double,但是c中会变成 func,因此难以区分,难以进行重载。
3.虚函数的概念,用以实现多态
4.c和c++的struct也有不同,c++中struct不仅仅可以有成员变量还可以有成员函数,且对于struct默认成员访问权限和继承权限都是PIblic
5.c++提供了强大的STL标准库,提高代码重用率。
6.c主要考虑如何通过一个代码,一个过程对输入进行运算处理输出,c++则优先考虑如何构造对象模型。
7.C++和Java区别(语言特性,垃圾回收,应用场景等)
1.指针:Java没有指针的概念,但是有内存的自动管理功能,防止指针操作失误的影响。但是java虚拟机内部还是用了指针的,保证java程序的安全。
2.多重继承:c++支持但是java不支持,但是支持一个类继承多个接口,达到c++中多重继承的功能
3.数据类型和类:所有函数和变量必须是类的一部分,除了基本数据类型其他的都作为类对象,对象将数据和方法结合起来,进行封装,每个对象都有自己的特点和行为。java中取消了C++的struct和union
4.自动内存管理:java自动进行无用内存回收。java中当一个对象不再被用到是,无用内存回收器将给他们加上标签,以线程方式在后台运行。
5.java不支持重载,但是重载被认为是c++的突出特性
6.java不支持预处理(预编译),但是提供了import(功能类似)
7.c++中数据类型隐含转换机制,java中需要限时强制类型转换
8.字符串:c++以NULL为结束,java则以类对象(string和stringBUffer)实现
9.java不支持goto
10.java异常机制用于补货例外时间,增强系统容错能力
8.C++里面怎么定义常量?常量存放在内存哪个位置?
局部常量(栈区);全局常量(符号表);字面值常量(比如字符串,放在常量区)
9.C++中重载和重写,重定义的区别
重载(overload):具有不同参数(参数类型、顺序、个数的不同)列表的同名函数,根据参数列表决定调用哪个函数,不关心返回类型。
重写(override):派生类中重新定义父类中除了函数体外完全相同的虚函数。重写的函数不能是static,一定是虚函数,且其他一定相同
重定义:派生类重新定义父类中相同名字的非virtual函数,参数列表
10.c++的所有构造函数
无参构造、一般构造、拷贝构造、类型转换构造(如果不允许则需将其声明为explict)、赋值运算符的重载、
11.c++的四种强制转换
static_cast:明确之处类型转换,将隐式都替换为显示,因为没有动态类型检查,上行转换(派生类-》基类)安全,下行转换(基类-》派生类)不安全
dynamic_cast:专门用于派生类之间的转换
const_cast:专门用于caost属性转换,去除或增加性质,是四个转换符中唯一一个可以操作常量的。
reinterpret_cast:高位操作,从低层对数据进行重新解释,一来具体平台
12.野指针和悬空指针之间的区别?怎么避免?
野指针:没有被初始化的指针;悬空指针:最初指向的内存以及被释放的指针(注:使用智能指针或者使用前初始化可以有效避免)
13.const修饰指针区分
1.const int * p1;指向整型常量的指针,值不能改
2.int * const p2;整形的常量指针,不能指向别的变量,但是指向变量的值可以修改
3.const int *const p3;指向整形常量的常量指针。
13.函数指针
定义:指向函数的指针变量,函数指针所指地址就是函数的入口地址。
用途:调用函数和做函数的参数,比如回调指针
char * fun(char * p ){}//函数fun
char * (*pf)(char * p); // 函数指针pf
pf = fun; // 函数指针pf指向函数fun pf(p); // 通过函数指针pf调⽤函数fun
14.堆和栈区别
栈:连续的内存空间,有编译器自动分配和回收空间,保存局部变量和函数参数等,函数调用时首先入栈的主函数的下一条地址,然后是函数各个参数,参数由右向左入栈(目的:”函数参数长度可变“特性更方便),局部变量先出栈,然后是参数,最后是栈顶指针最开始存放的地址,程序由该点继续运⾏,不会产⽣碎⽚。栈是⾼地址向低地址扩展,栈低⾼地址,空间较⼩。
堆:程序员手动进行,不连续,容易产生碎片,低地址向高地址,空间大,灵活。
15.函数参数传递的几种方式
值传递:只是拷贝,不影响本体
指针传递:也是一种值传递,形参指向实参地址的指针,会改变本身
引用传递:别名
16.new/delete, malloc/free区别
new和delete是操作符,malloc和free是库函数
执⾏ new 实际上执⾏两个过程:1.分配未初始化的内存空间(malloc);2.使⽤对象的构造函数对空间进⾏初始化;返回空间的⾸地址。
执⾏ delete 实际上也有两个过程:1. 使⽤析构函数对对象进⾏析构;2.回收内存空间(free)。
以上也可以看出 new 和 malloc 的区别,new 得到的是经过初始化的空间,⽽ malloc 得到的是未初始化的空间。所以 new 是 new ⼀个类型,⽽ malloc 则是malloc ⼀个字节⻓度的空间。delete 和 free 同理,delete 不仅释放空间还析构对象,delete ⼀个类型,free ⼀个字节⻓度的空间。
由于 mallo/free 是库函数⽽不是运算符,不在编译器控制权限之内,不能够把执⾏的构造函数和析构函数的任务强加于malloc/free,所以有了 new/delete 操作符。
17.volatile和external关键字
volatile三个特性:
易变性(下一条语句不会使用上一条语句对象的寄存器内容,而是从内存中重新读取)
不可优化性:确保程序员写在代码证的指令一定被执行
顺序性:确保编译器不会进行乱序优化
external:
定义:用来说明此函数在别处定义,但是在此处引用。如果是在 main 函数中进⾏声明的,则只能在 main 函数中调⽤。为啥不直接#include呢,因为extern会加速程序的编译过程。同时也是指示调用规范,告诉链接器用指定语言函数规范进行链接主要原因是 C++ 和 C 程序编译完成后在⽬标代码中命名规则不同,⽤此来解决名字匹配的问题。
18.define和const区别
define:宏定义在预编译阶段进行处理,没有类型,只是遇到时进行展开,简单的展开容易出现边界效应。
const:在编译期间进行处理,有类型也有检查,会分配内存,保留内存地址,不需要多次拷贝,直接加入符号表更加高效。
19.面向对象三大特征
封装、继承、多态
封装:把客观事务封装成抽象的类,并且类可以把自己的数据和方法只让信任的类或者对象操作。
继承:是指让某个类型的对象获得另一个类型的属性的方法。无需编写的情况下对父类的功能进行扩展;
继承有两种实现方式:实现继承(直接用基类的属性和方法);接口继承(仅使用属性和方法的名称,但是子类必须提供实现的能力)
多态(虚函数):就是向不同的对象发送同一个消息,不同对象在接收时会产生不同的行为。一个接口实现多种方法。
多态是晚绑定
地址早绑定(重载--编译期间确定函数的调用地址,静态多态);地址晚绑定(子类重写父类--运行时才能确定地址,动态多态)
20.虚函数相关(虚函数表,虚函数指针),虚函数的实现原理
C++中多态的表象是在基类的函数前加上virtual关键字,在派生类中重写该函数。运行时会根据对象的事迹类型调用相应的函数。当类中包含虚函数时,编译器会为类生成虚函数表,保存虚函数地址。当定义一个派生类对象时,编译器会生成一个虚函数指针(初始化在构造函数中完成),指向该类型的虚函数表。后续如果有⼀个基类类型的指针,指向派⽣类,那么当调⽤虚函数时,就会根据所指真正对象的虚函数表指针去寻找虚函数的地址,也就可以调⽤派⽣类的虚函数表中的虚函数以此实现多态。
补充:如果基类中没有定义成 virtual,那么进⾏ Base B; Derived D; Base *p = D; p->function(); 这种情况下调⽤的则是 Base 中的 function()。因为基类和派⽣类中都没有虚函数的定义,那么编译器就会认为不⽤留给动态多态的机会,就事先进⾏函数地址的绑定(早绑定)
21.编译器处理虚函数表如何处理
派生类而言,编译器建立虚函数表的过程一共三个步骤:
-
拷贝基类虚函数表,多继承就每个都拷贝
-
当基类和派生类共用一个虚函数表的时候,则成为某个基类为派生类的主基类
-
查看派生类中是否有重写基类中的虚函数,有则进行虚函数地址的替换;查看派生类是否有自身的虚函数,有则追加自身的虚函数到虚函数表中
-
D的两个虚函数表来自B,C两个基类的虚函数表重写,那么地位应该一样,为什么D新出现的虚函数会出现在B重写后的虚函数表中而不是C重写后的虚函数表中?
一种可能的解释是,编译器在布局D的内存时,选择了将B的虚函数表放在更靠近D的起始地址的位置。因此,当D定义新的虚函数时,这些函数就被添加到了B的虚函数表的扩展中。但这只是一种可能的实现方式,并不代表所有编译器都会这样做。
另一种可能的解释是,编译器可能会根据虚函数的调用频率或其他优化策略来决定将新的虚函数添加到哪个虚函数表中。但是,这种解释更加依赖于具体的编译器实现和上下文信息。
22.基类中析构函数一般写成虚函数的原因
直观而言:降低内存泄漏的风险。举例:基类的指针指向一个派生类用完准备销毁时,若基类的析构函数不是虚函数则,编译器认为当前对象就是基类,就只调用基类的析构函数,如此派生类的自身内容将无法被析构,造成内存泄漏。若定义为虚函数,则可以根据实际对象,执行派生类的析构函数,再执行基类的析构函数,成功释放内存。
23.构造函数为什么一般不定义为虚函数
-
虚函数调用只需要知道函数接口,不需要知道对象的具体类型。但是,创建对象则需要知道对象的完整信息,因此构造函数不宜为虚函数。
-
虚函数的调用是通过实例化之后对象的虚函数表指针来找到虚函数的地址进行调用,若构造函数是虚的,那么虚函数表指针则不存在,无法找到对应虚函数表来调用虚函数,违反了先实例化后调用的原则。
24.构造函数或析构函数中调用虚函数会发生什么?
-
构造函数中调用虚函数:在构造过程中,派生类的部分可能还没有完全初始化。因此,如果基类的构造函数调用了虚函数,并且这个函数在派生类中被重写了,那么实际上调用的是基类版本的虚函数,而不是派生类版本的。这可能会导致逻辑错误或不符合预期的行为。
-
析构函数中调用虚函数:在析构过程中,派生类的部分可能已经开始被销毁,或者已经处于不可用的状态。如果基类的析构函数调用了虚函数,特别是那些可能依赖于派生类成员状态的虚函数,那么可能会导致未定义行为或程序崩溃。
避免在构造函数或析构函数中调用虚函数是为了防止因对象状态不确定而导致的潜在问题。
25.析构函数的作用,如何起作用?
构造函数:初始化值的作用,一旦进行实例化对象那么系统自动回调一个构造函数;
析构函数:释放对象分配的内存空间。
26.构造函数的执行顺序,析构函数的执行顺序
构造函数顺序:
-
基类构造函数。如果有多个基类,则构造函数的调⽤顺序是某类在类派⽣表中出现的顺序,⽽不是它们在成员 初始化表中的顺序。
-
成员类对象构造函数。如果有多个成员类对象则构造函数的调⽤顺序是对象在类中被声明的顺序,⽽不是它们 出现在成员初始化表中的顺序
-
派⽣类构造函数
析构函数顺序:
-
调⽤派⽣类的析构函数
-
调⽤成员类对象的析构函数
-
调⽤基类的析构函数
27.纯虚函数(接口继承和实现继承)
为了让继承可以出现多种情况:
-
只继承成员函数的接口
-
既继承成员函数的接口又继承成员函数的实现,并且可以在派生类中重写成员函数实现多态
-
其实,声明⼀个纯虚函数的⽬的就是为了让派⽣类只继承函数的接⼝,⽽且派⽣类中必需提供⼀个这个纯虚函数的 实现,否则含有纯虚函数的类将是抽象类,不能进⾏实例化。
-
对于纯虚函数调用这个实现的唯一方法就是在派生类对象中指出其class名称进行调用
28.