面试知识点总结1:C++

1.介绍一下C++的面向对象;面向对象的优点有哪些, C和C++的区别?

  • 面向过程就是分析出实现需求所需要的步骤,通过函数一步一步实现这些步骤,接着依次调用即可;
  • 面向对象是把整个需求按照特点、功能进行划分,将这些存在共性的部分封装成对象,创建的对象不是为了完成某一个步骤,而是描述某个事物在解决问题的步骤中的行为;举个例子,我们可以将面向过程的步骤中共性的步骤进行封装,做成一个通用的模块,就是一个对象。
  • 面向对象的优点是有封装、继承、多态性的特性,使系统更加灵活、更加易于维护、复用和扩展;缺点是在类的调用的时候需要实例化,开销过大,性能低于面向过程。

2.面向对象的三个特征;封装,继承,多态分别讲一下,封装、继承、多态的存在是为了什么、有什么优点吗?说说多态实现原理?

  • 封装:也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。 简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
  • 继承:是指可以让某个类型的对象获得另一个类型的对象的属性的方法。 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。
  • 多态:就是指一个类实例的相同方法在不同情形下有不同表现形式。具体的,在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。 如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

3.虚函数表

在这里插入图片描述

  • 在基类中声明一个虚函数,会对应产生虚函数表虚函数指针,虚函数表中存放指向每个虚函数的虚指针,继承基类的子类也会产生虚函数表vtable及指向子类vtable的虚指针vptr,当我们定义一个子类的对象时,会生成一个指向子类虚函数表的虚指针vptr。当我们定义一个基类的指针指向子类对象时,子类对象的虚表指针会优先访问子类的虚函数表。

4.纯虚函数:virtual void fun()=0

  • 我们把包含纯虚函数的类称之为抽象类。对于抽象类来说,C++是不允许它去实例化对象的。也就是说,抽象类无法实例化对象。抽象类的子类也可以是抽象类,我们必须在子类中将纯虚函数功能补全,才可以将子类实例化。主要目的是为了实现一种接口的效果。

5.接口与继承

  • (1)纯虚函数只提供接口继承,但可以被实现;
  • (2)虚函数既提供接口继承,也提供了一份默认实现,即也提供实现继承;
  • (3)普通函数既提供接口继承,也提供实现继承。

6.构造函数是否可以为虚函数?析构函数是否可以为虚函数?

  • 构造函数不能是虚函数,析构函数必须是虚函数
  • 构造函数不能是虚函数,因为在我们创建一个对象时,必须要知道对象的准确类型,但是虚函数允许我们调用一个只知道接口而不知道准确类型的函数,在程序运行阶段才知道调用的虚函数和类型,而构造函数需要在编译阶段对对象进行准确类型的构造,因此构造函数没必要也不可以是虚函数。
  • 析构函数必须是虚函数,因为在我们执行子类的析构时,可以将父类占用的内存一起释放掉,如果析构函数不是虚函数的话,可能因为子类调用父类函数而没有及时删除而造成内存泄漏。

7. 是否可以把每个函数都声明为虚函数?

  • 不可以,因为虚函数是有代价的,每构造一个虚函数的对象都必须维护一个虚函数表,因此在使用虚函数的时候都会产生一个系统开销,如果只是很小的类,不需要派生出其他类,就没必要使用虚函数。

8. 重载和重写(覆盖)、重定义

  • 重载:

    • 同一个作用域下,函数名相同,函数的参数不同(参数不同指参数的类型或参数的个数不同或者顺序
    • 重载对返回类型没有要求,可以相同也可以不同,不能根据返回值判断两个函数是否构成重载
    • 当函数构成重载后,调用该函数时,编译器会根据函数的参数选择合适的函数进行调用。
  • 重写:

    • 在不同的作用域下(一个在父类,一个在子类),函数的函数名、参数、返回值完全相同,父类必须含有virtual关键字
    • 被重写的函数不能是static的。必须是virtual的
  • 重定义:

    • 在不同的作用域下(这里不同的作用域指一个在子类,一个在父类 ),函数名相同的两个函数构成重定义。
    • 当两个函数构成重定义时,父类的同名函数会被隐藏,当用子类的对象调用同名的函数时,如果不指定类作用符,就只会调用子类的同名函数。
    • 如果想要调用父类的同名函数,就必须指定父类的域作用符。注意:当父类和子类的成员变量名相同时,也会构成隐藏。

9.引用和指针的区别:

  • 指针是一个变量,有自己的一块空间;而引用只是一个别名;
  • 使用 sizeof() 看一个指针的大小是4字节,而引用则是被引用对象的大小;
  • 指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;
  • 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
  • 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
  • 指针和引用使用**++**运算符的意义不一样,指针指向下一块内存地址的值,引用是当前值加一;
  • 引用一旦绑定不可更改,指针可以更改指向地址。如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露
  • 按值传递和按引用传递:引用是别名,在函数内操作一个通过传引用方式传入的变量,相当于直接操作函数外原本的变量。指针是一个变量,但是不同于一般变量,指针存放的是内存地址。传指针方式传入函数内的指针,只是拷贝外部指针的副本,可以在函数内修改它们共同指向的内存区域,但是却无法在函数内修改函数外指针本身。

10.野指针:

  • 原因
    • 指针定义时未被初始化;
    • 指针被释放时没有置空;
    • 指针超过了变量的作用域。
  • 防范:
    • 初始化指针的时候将其置为nullptr,之后对其操作;
    • 释放指针的时候将其置为nullptr。

11. 数组名和指针的区别:

int a[4];

int *p;

Sizeof(a)sizeof(p)大小不同,a[i]可访问数组元素,都可以通过 *(a+i) *(p+i) 访问

void func(int a[]) {

  cout << sizeof(a); // 把数组传给函数,在函数内,就失去了数组的特定了,就完全成一个指针了

}

将数组传递给函数时退化为指针。

12. C++内存管理:

C++程序在执行时,将内存大方向划分为4个区域

  • 代码区:存放函数体的二进制代码,由操作系统进行管理的
  • 全局区:存放全局变量静态变量以及常量
  • 栈区:由编译器自动分配释放, 存放非静态变量,函数的参数值局部变量返回值等,栈是向下增长的。
  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收,动态内存分配,向上增长的。

堆和栈:

  • 顶地址和栈的最大容量由系统预先规定(通常默认2M或10M);的大小则受限于计算机系统中有效的虚拟内存,32位Linux系统中堆内存可达2.9G空间。
  • 可静态分配或动态分配。静态分配由编译器完成,如局部变量的分配。动态分配由alloca函数在栈上申请空间,用完后自动释放。只能动态分配且手工释放。
  • 不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。而频繁申请释放操作会造成内存空间的不连续,从而造成大量碎片,使程序效率降低。

堆和栈不会相遇,堆和栈都有一个顶指针,分配内存的时候,如果内存空间不够用,系统会抛出异常。如果malloc要求的内存块比虚拟内存还要大,会返回NULL。

13.内存泄漏

  • 内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。比如new了一块空间,但是没有delete,或者在new和delete之间调用了抛出异常的函数导致delete失败。

  • 如何检查内存泄漏:为了判断内存是否泄漏,我们一方面可以使用Linux环境下的内存泄漏检查工具Valgrind,另一方面我们写代码的时候,可以**添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,**以此来判断内存是否有泄漏(shared_ptr)。

  • 内存泄漏如何预防

    • 1)良好的代码规范;
    • 2)事前预防型。如智能指针等;
    • 3)事后查错型。如泄漏检测工具valgrind
  • 智能指针:

    • 1)auto_ptr(指针拷贝构造时,前一个指针沦为空),对于拷贝构造和赋值运算符重载,该智能指针采用管理权转移的方式(当一个指针拷贝构造另一个指针时,当前指针就将对空间的管理权交给拷贝的那个指针,当前指针就指向空);
    • 2)scoped_ptr采用防拷贝的方式(防拷贝就是不允许拷贝,拷贝就会出错;防拷贝的实现:重载拷贝构造和赋值运算符并且声明为私有,使函数外无法通过拷贝构造或赋值运算符对指针进行拷贝);
    • 3)shared_ptr为共享指针,里面采用引用计数,当有shared_ptr指向同一块空间的时候就增加引用计数,当引用计数减为0的时候才释放该智能指针管理的那块空间
  • Linux环境下防止内存泄漏的检测工具Valgrind:Memcheck工具:
    在这里插入图片描述

14.new和malloc:

  • 1)malloc是函数,开辟内存需要传入字节数,如malloc(100);表示在堆上开辟了100个字节的内存,返回void*, 表示分配的堆内存的起始地址,因此malloc的返回值需要强转成指定类型的地址;new是运算符,**开辟内存需要指定类型,返回指定类型的地址,**因此不需要进行强转。
  • 2)malloc开辟的内存永远是通过free 来释放的;而new单个元素内存,用的是delete,如果new[]数组,用的是delete[] 来释放内存的。
  • 3)malloc开辟内存失败返回NULL,new开辟内存失败抛出bad_alloc类型的异常。

15.单例模式:

  • 单例 Singleton 是设计模式的一种,其特点是只有唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;

16.浅拷贝和深拷贝:

  • 浅拷贝只是拷贝数据的地址(共享内存);
  • 深拷贝会拷贝整个数据(内存不共享,额外开辟一块空间), 被拷贝的数据和新生成的数据相互独立。
  • 浅拷贝速度快,但是涉及到动态内存地址的拷贝时容易造成地址的多次删除使程序崩溃,深拷贝没有这个问题但是效率较低。拿cpp设计自己的string类来说 浅拷贝,也就是一个string对象中的char* ptr变量指向某处内存,另外一个string对象的char* ptr也指向了同一块内存,两个指针存储同一个地址值,析构时候就会报错 ;深拷贝,就是在浅拷贝基础上,额外开辟空间,两个ptr指针指向不同的内存,这时候析构,就会正常通过。

17.const 和static区别:

  • 对函数:
    • const(类/函数内不可修改成员变量)定义的常量在超出其作用域之后其空间会被释放。
    • static (相当于当前文件的全局变量,但是其他文件不可访问)定义的静态常量在函数执行后不会释放其存储空间。static声明的全局静态变量不能被其他文件访问,其他文件可以使用同名变量不会产生冲突。非static的变量可以被其他文件访问。
  • 类内
    • 如果是类内声明了一个const变量和static变量。则const成员变量和static成员变量都不可以在声明的时候初始化,const变量和普通成员变量一样,每个对象都有一份,只不过const变量对于每个对象的声明周期来说是常量,且不同对象是可以有不同的值的,所以必须提供构造函数,并在初始化列表中对const成员变量进行初始化。而static变量则是与对象无关的,即使没有对象,通过类也可以访问static变量,类似全局函数,不过作用域在所在文件内。static变量必须在类外进行初始化。
    • 如果类内声明了const成员函数,和static成员函数,static函数的作用是类作用域内的全局函数,不能访问非静态成员及this指针,且不能声明为virtual。const成员函数主要是防止在函数体内修改成员变量。
  • 静态方法(Static Method)与静态成员变量一样,属于类本身,在类装载的时候被装载到内存(Memory),不自动进行销毁,会一直存在于内存中,直到关闭。
  • 非静态方法(Non-Static Method) 又叫实例化方法,属于实例对象,实例化后才会分配内存, 必须通过类的实例来引用。不会常驻内存,当实例对象回收之后,也跟着消失。

18. struct和class区别:

​ 在C++中,class和struct做类型定义时只有两点区别:

  • 默认继承权限。如果不明确指定,来自class的继承按照private继承处理,来自struct的继承按照public继承处理;

  • 成员的默认访问权限。class的成员默认是private权限,struct默认是public权限。

    C里是根本没有“class”,而C的struct从根本上也只是个包装数据的语法机制。如果不是为了和C兼容,C++中就不会有struct关键字。在C中struct不能包含函数,在C++里面:都可以有函数,默认情况下struct中变量是public,而class中是private

19.union,struct, class的内存对齐,大小端:

  • 共用体联合体(union)的使用场合,是各数据类型各变量占用空间差不多并且对各变量同时使用要求不高的场合。联合体(union) 中是各变量是“互斥”的——缺点就是不够“包容”;但优点是内存使用更为精细灵活,也节省了内存空间。
  • 结构体(struct) 中所有变量是“共存”的——优点是“有容乃大”,全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配
  • 大小端:
    • 大端:高位存在低地址,低位存在高地址;
    • 小端:高位存在高地址低位存在低地址
1int checkCPUendian()//返回1,为小端;反之,为大端;
{
    union
    {
        unsigned int  a;
        unsigned char b;
    }c;
    c.a = 1;  //0x0000,0x0000,0x0000,0x0001
    return 1 == c.b;  //大端0x0000,0x0000,0x0000,0x0001
}                     //小端0x0001,0x0000,0x0000,0x00002union var{
        char c[5];
        int i;
};
 
int main(){
        union var data;
        data.c[0] = 0x04;
        data.c[1] = 0x03;//写成16进制为了方便直接打印内存中的值对比
        data.c[2] = 0x02; //在数组中存放顺序是固定的,与大小端无关
        data.c[3] = 0x11; //大端小端,应该只是在一个类型内的事,比如int四个字节,应该是这四个字节怎么排列
        data.c[4] = 0x11; 
        printf("%x\n",data.i);//小端 0x11020304
}                            //大端 0x11020304

例2:在数组中存放顺序是固定的,与大小端无关。大端小端,应该只是在一个类型内的事,比如int四个字节,应该是这四个字节怎么排列。

20. STL的容器底层数据结构是什么?

  • STL(Standard Template Library,标准模板库),STL 从广义上分为: 容器(container) 算法(algorithm) 迭代器(iterator)容器算法之间通过迭代器进行无缝连接,STL 几乎所有的代码都采用了模板类或者模板函数。

  • vector: 数组, 支持快速随机访问;

    Map :红黑树, 有序,不重复;

    Set :红黑树; 与map的区别在于map中存储的是 < key-value > ,而set可以理解为关键字即值,即只保存关键字的容器。有序,不重复。

    List :双向链表, 支持快速增删;

    queue: 底层一般用listdeque实现,不用vector的原因应该是容量大小有限制,扩容耗时;

    deque: deque是一个双端队列(double-ended queue),也是在中保存内容的.它的保存形式如下:

    [堆1] --> [堆2] -->[堆3] --> … 每个堆保存好几个元素,然后堆和堆之间有指针指向,看起来像是list和vector的结合品;

    hash_set: 底层数据结构为hash表,无序,不重复。

    • vector是可以快速地在最后添加删除元素,并可以快速地访问任意元素
    • list是可以快速地在所有地方添加删除元素,但是只能快速地访问最开始与最后的元素;
    • deque在开始和最后添加元素都一样快,并提供了随机访问方法,像vector一样使用[]访问任意元素,但是随机访问速度比不上vector快,因为它要内部处理堆跳转;deque也有保留空间.另外,由于deque不要求连续空间,所以可以保存的元素比vector更大,这点也要注意一下.还有就是在前面和后面添加元素时都不需要移动其它块的元素,所以性能也很高。

21.数组越界

  • 发生数组越界在编译的时候不会报错,在运行的时候会发生错误。通常是在输入数组时候加入边界判定条件防止越界,也可以使用vector容器动态调整分配内存大小(解决:STL)

  • 常见排序算法复杂度:
    在这里插入图片描述

22.事务的四个特性:

  • 事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。

  • 事务的四个特性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性

    • 原子性(Atomicity),被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。回滚可以用回滚日志来实现。回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
    • 一致性(Consistency),数据库在事务执行前后都保持一致性状态,在一致性状态下,所有事务对一个数据的读取结果都是相同的。
    • 隔离性(Isolation),一个事务所做的修改在最终提交以前,对其他事务是不可见的。
    • 持久性(Durability),一旦事务提交,则其所做的修改将会永远保存到数据库中,即使系统发生崩溃,事务执行的结果也不能丢失,使用重做日志来保持永久性。

23.并发下事务会产生的问题:

举个例子,事务A和事务B操纵的是同一个资源,事务A有若干个子事务,事务B也有若干个子事务,事务A和事务B在高并发的情况下,会出现各种各样的问题。“各种各样的问题”,总结一下主要就是五种:第一类丢失更新第二类丢失更新脏读不可重复读幻读。五种之中,第一类丢失更新、第二类丢失更新不重要,不讲了,讲一下脏读、不可重复读和幻读。

1、脏读

所谓脏读,就是指事务A读到了事务B还没有提交的数据,比如银行取钱,事务A开启事务,此时切换到事务B,事务B开启事务–>取走100元,此时切换回事务A,事务A读取的肯定是数据库里面的原始数据,因为事务B取走了100块钱,并没有提交,数据库里面的账务余额肯定还是原始余额,这就是脏读。

2、不可重复读

所谓不可重复读,就是指在一个事务里面读取了两次某个数据,读出来的数据不一致。还是以银行取钱为例,事务A开启事务–>查出银行卡余额为1000元,此时切换到事务B事务B开启事务–>事务B取走100元–>提交,数据库里面余额变为900元,此时切换回事务A,事务A再查一次查出账户余额为900元,这样对事务A而言,在同一个事务内两次读取账户余额数据不一致,这就是不可重复读。

3、幻读

所谓幻读,就是指在一个事务里面的操作中发现了未被操作的数据。比如学生信息,事务A开启事务–>修改所有学生当天签到状况为false,此时切换到事务B,事务B开启事务–>事务B插入了一条学生数据,此时切换回事务A,事务A提交的时候发现了一条自己没有修改过的数据,这就是幻读,就好像发生了幻觉一样。幻读出现的前提是并发的事务中有事务发生了插入、删除操作。

24.事务的隔离级别:

事务隔离级别,就是为了解决上面几种问题而诞生的。为什么要有事务隔离级别,因为事务隔离级别越高,在并发下会产生的问题就越少,但同时付出的性能消耗也将越大 ,因此很多时候必须在并发性和性能之间做一个权衡。所以设立了几种事务隔离级别,以便让不同的项目可以根据自己项目的并发情况选择合适的事务隔离级别,对于在事务隔离级别之外会产生的并发问题,在代码中做补偿。

事务隔离级别有4种,但是像Spring会提供给用户5种,来看一下:

1、DEFAULT

默认隔离级别,每种数据库支持的事务隔离级别不一样,如果Spring配置事务时将isolation设置为这个值的话,那么将使用底层数据库的默认事务隔离级别。顺便说一句,如果使用的MySQL,可以使用"select @@tx_isolation"来查看默认的事务隔离级别;

2、READ_UNCOMMITTED

读未提交,即能够读取到没有被提交的数据,所以很明显这个级别的隔离机制无法解决脏读、不可重复读、幻读中的任何一种,因此很少使用;

3、READ_COMMITED

读已提交,即能够读到那些已经提交的数据,自然能够防止脏读,但是无法限制不可重复读和幻读;

4、REPEATABLE_READ

重复读取,即在数据读出来之后加锁,类似"select * from XXX for update",明确数据读取出来就是为了更新用的,所以要加一把锁,防止别人修改它。REPEATABLE_READ的意思也类似,读取了一条数据,这个事务不结束,别的事务就不可以改这条记录,这样就解决了脏读、不可重复读的问题,但是幻读的问题还是无法解决;

5、SERLALIZABLE

串行化,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了。

网上专门有图用表格的形式列出了事务隔离级别解决的并发问题:
在这里插入图片描述

参考:

资料分享整理牛客小姐姐@好好做人111

事务及事务隔离级别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值