【C++八股】全网最全的C++语言基础八股准备(持续更新)

编程入门 行业动态 更新时间:2024-10-11 13:30:55

【C++八股】全网<a href=https://www.elefans.com/category/jswz/34/1768819.html style=最全的C++语言基础八股准备(持续更新)"/>

【C++八股】全网最全的C++语言基础八股准备(持续更新)

1. 引用和指针的区别?

  • 指针是一个实体,需要分配内存空间。引用只是变量的别名,不需要分配内存空间。
  • 引用在定义的时候必须进行初始化,并且不能够改变。指针在定义的时候不一定要初始化,并且指向的空间可变。(注:不能有引用的值不能为NULL)
  • 有多级指针,但是没有多级引用,只能有一级引用。
  • 指针和引用的自增运算结果不一样。(指针是指向下一个空间,引用时引用的变量值加1)
  • sizeof引用得到的是所指向的变量(对象)的大小,而sizeof 指针得到的是指针本身的大小。
  • 引用访问一个变量是直接访问,而指针访问一个变量是间接访问。 7) 使用指针前最好做类型检查,防止野指针的出现;
  • 引用底层是通过指针实现的;
  • 作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引用的实质是传地址,传递的是变量的地址。

1. 数组和指针的区别?

  1. 数组在内存中是连续存放的,开辟一块连续的内存空间;数组所占存储空间:sizeof(数组名);数组大小:sizeof(数组名)/sizeof(数组元素数据类型);
  2. 运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。
  3. 编译器为了简化对数组的支持,实际上是利用指针实现了对数组的支持。具体来说,就是将表达式中的数组元素引用转换为指针加偏移量的引用。
  4. 在向函数传递参数的时候,如果实参是一个数组,那用于接受的形参为对应的指针。也就是传递过去是数组的首地址而不是整个数组,能够提高效率
  5. 在使用下标的时候,两者的用法相同,都是原地址加上下标值,不过数组的原地址就是数组首元素的地址是固定的,指针的原地址就不是固定的。

2. C++中的指针参数传递和引用参数传递

  1. 指针参数传递本质上是值传递,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。
  2. 引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。
  3. 引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。
  4. 从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

对于修改函数中数据的函数

  • 如果数据是内置数据类型,则使用指针
  • 如果数据对象是数组,则只能使用指针
  • 如果数据对象是结构,则使用引用或者指针
  • 如果数据是类对象,则使用引用

3. 形参与实参的区别?

  1. 形参变量只有在被调用时才分配内存单元,在调用结束时, 即刻释放所分配的内存单元。因此,形参只有在函数内部有效。 函数调用结束返回主调函数后则不能再使用该形参变量。
  2. 实参可以是常量、变量、表达式、函数等, 无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值, 以便把这些值传送给形参。 因此应预先用赋值,输入等办法使实参获得确定值,会产生一个临时变量。
  3. 实参和形参在数量上,类型上,顺序上应严格一致, 否则会发生“类型不匹配”的错误。
  4. 函数调用中发生的数据传送是单向的。 即只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。 因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化。
  5. 当形参和实参不是指针类型时,在该函数运行时,形参和实参是不同的变量,他们在内存中位于不同的位置,形参将实参的内容复制一份,在该函数运行结束的时候形参被释放,而实参内容不会改变。

4. 值传递、指针传递、引用传递

  1. 值传递:有一个形参向函数所属的栈拷贝数据的过程,如果值传递的对象是类对象 或是大的结构体对象,将耗费一定的时间和空间。(传值)
  2. 指针传递:同样有一个形参向函数所属的栈拷贝数据的过程,但拷贝的数据是一个固定为4字节的地址。(传值,传递的是地址值)
  3. 引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于为该数据所在的地址起了一个别名。(传地址)
  4. 效率上讲,指针传递和引用传递比值传递效率高。一般主张使用引用传递,代码逻辑上更加紧凑、清晰。

5. static的用法和作用?

static 作⽤:控制变量的存储⽅式和可⻅性

  1. 隐藏。(static函数,static变量均可)
    当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
  2. static的第二个作用是保持变量内容的持久。(static变量中的记忆功能和全局生存期)存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。
  3. static的第三个作用是默认初始化为0(static变量)
    其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。
  4. static的第四个作用:C++中的类成员声明static
    1. 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;

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

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

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

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

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

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

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

C 语言的关键字 static 和 C++ 的关键字 static 有什么区别

  • 在C中static用来修饰局部静态变量和外部静态变量、函数。而C++中除了上述功能外,还用来定义类的成员变量和函数。即静态成员和静态成员函数。
  • 注意:编程时static的记忆性,和全局性的特点可以让在不同时期调用的函数进行通信,传递信息,而C++的静态成员则可以在多个对象实例间进行通信,传递信息。

6. 静态变量什么时候初始化

  1. 初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存。
  2. 静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但在C和C++中静态局部变量的初始化节点又有点不太一样。在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全部回收。
  3. 而在C++中,初始化时在执行相关代码时才会进行初始化,主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以C++标准定为全局或静态对象是有首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的。

7. const关键字

  1. 阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
  2. 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
  3. 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
  4. 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量,类的常对象只能访问类的常成员函数;
  5. 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。
  6. const成员函数可以访问非const对象的非const数据成员、const数据成员,也可以访问const对象内的所有数据成员;
  7. 非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员;
  8. 一个没有明确声明为const的成员函数被看作是将要修改对象中数据成员的函数,而且编译器不允许它为一个const对象所调用。因此const对象只能调用const成员函数。
  9. const类型变量可以通过类型转换符const_castconst类型转换为非const类型
  10. const类型变量必须定义的时候进行初始化,因此也导致如果类的成员变量有const类型的变量,那么该变量必须在类的初始化列表中进行初始化;
  11. 对于函数值传递的情况,因为参数传递是通过复制实参创建一个临时变量传递进函数的,函数内只能改变临时变量,但无法改变实参。则这个时候无论加不加const对实参不会产生任何影响。但是在引用或指针传递函数调用中,因为传进去的是一个引用或指针,这样函数内部可以改变引用或指针所指向的变量,这时const 才是实实在在地保护了实参所指向的变量。因为在编译阶段编译器对调用函数的选择是根据实参进行的,所以,只有引用传递和指针传递可以用是否加const来重载。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。

8. 指针和const的用法

  1. 当const修饰指针时,由于const的位置不同,它的修饰对象会有所不同。
  2. int *const p2中const修饰p2的值,所以理解为p2的值不可以改变,即p2只能指向固定的一个变量地址,但可以通过*p2读写这个变量的值。顶层指针表示指针本身是一个常量
  3. int const *p1或者const int *p1两种情况中const修饰*p1,所以理解为*p1的值不可以改变,即不可以给*p1赋值改变p1指向变量的值,但可以通过给p赋值不同的地址改变这个指针指向。底层指针表示指针所指向的变量是一个常量。
  4. 可以带两个const,效果就是叠加,指向的地址和内容都不能改变: int const *const p;

9. extern用法?

  1. extern修饰变量的声明
    如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。
  2. extern修饰函数的声明
    如果文件a.c需要引用b.c中的函数,比如在b.c中原型是int fun(int mu),那么就可以在a.c中声明extern int fun(int mu),然后就能使用fun来做任何事情。就像变量的声明一样,extern int fun(int mu)可以放在a.c中任何地方,而不一定非要放在a.c的文件作用域的范围中。
  3. extern修饰符可用于指示C或者C++函数的调用规范。
    比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。

10. 深拷贝与浅拷贝?

  1. 浅拷贝 —-只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做“(浅复制)浅拷贝”,换句话说,浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。

  2. 深拷贝 —-在计算机中开辟了一块新的内存地址用于存放复制的对象。

  3. 在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

  4. 简单的来说,【浅拷贝】是增加了一个指针,指向原来已经存在的内存。而【深拷贝】是增加了一个指针,并新开辟了一块空间让指针指向这块新开辟的空间。【浅拷贝】在多个对象指向一块空间的时候,释放一个空间会导致其他对象所使用的空间也被释放了,再次释放便会出现错误

11. C++模板是什么,底层怎么实现的?

  1. 编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译
  2. 这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误。

12. C语言struct和C++struct区别

  1. C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT)。
  2. C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数。
  3. C++中,struct的成员可以有权限设置,默认为public(为了与C兼容),且可以和类一样有成员函数,因此能继承,能实现多态,可以实现面对对象编程。
  4. struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名(除:typedef struct class{};);C++中结构体标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C++中被当作类的一种特例。

13. C++虚函数作用及底层实现原理

1. 虚函数表和虚函数表指针的作用

  • C++中虚函数使用虚函数表和 虚函数表指针实现;
  • 虚函数表是一个类的虚函数的地址表,用于索引类本身以及父类的虚函数的地 址,假如子类的虚函数重写了父类的虚函数,则对应在虚函数表中会把对应的虚函数替换为子类的函数的地址;
  • 虚函数表指针存在于每个对象中(通常出于效率考虑,会放在对象的开始地址处), 它指向对象所在类的虚函数表的地址;在多继承环境下,会存在多个虚函数表指针,分别指向对应 不同基类的虚函数表。

2. 虚函数实现原理

  • 编译器在编译的时候,编译器会为每个包含虚函数的类创建一个虚表(即vtable),该表是一个一维数组(而不是一个链表),在这个数组中存放每个虚函数的地址。

  • 编译器另外还为每个带有虚函数的类的对象自动创建一个虚表指针(即vptr),这个指针指向了对象所属类的虚表。在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表。所以在调用虚函数时,就能够找到正确的函数。

3. 虚指针初始化

由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的。换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数

那么虚表指针在什么时候,或者说在什么地方初始化呢

答案是在构造函数中进行虚表的创建和虚表指针的初始化。

构造函数的调用顺序,在构造子类对象时,要先调用父类的构造函数,之后再完成子类的构造。在调用父类的构造函数时,编译器只“看到了”父类,并不知道后面是否后还有继承者,它初始化虚表指针,将该虚表指针指向父类的虚表。当执行子类的构造函数时,虚表指针被重新赋值,指向自身的虚表。

4. C++多态性实现的原理

对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为指向本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以才能实现动态的对象函数调用。

13. 虚函数可以声明为inline吗?声明为static呢?

1. inline

  1. 虚函数用于实现运行时的多态,或者称为晚绑定或动态绑定。而内联函数用于提高效率。内联函数的原理是,在编译期间,对调用内联函数的地方的代码替换成函数代码。内联函数对于程序中需要频繁使用和调用的小函数非常有用。
  2. 虚函数要求在运行时进行类型确定,而内敛函数要求在编译期完成相关的函数替换;
    虚函数可以申明为inline,具体到编译器解释时候,如果编译器在编译的时候就可以确定该虚函数的决议,则编译器以inline方式静态决议该虚函数。如果编译器在编译的时候不能决定,则必须在运行时决议虚函数,此时虚函数不能以inline函数的方式调用

2. static

  1. static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。
  2. 静态与非静态成员函数之间有一个主要的区别。那就是静态成员函数没有this指针。虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable。对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual.
  3. 虚函数的调用关系:this -> vptr -> vtable ->virtual function

14. 编译器处理虚函数表应该如何处理

对于派⽣类来说,编译器建⽴虚函数表的过程其实⼀共是三个步骤:

  • 拷⻉基类的虚函数表,如果是多继承,就拷⻉每个有虚函数基类的虚函数表
  • 当然还有⼀个基类的虚函数表和派⽣类⾃身的虚函数表共⽤了⼀个虚函数表,也称为某个基类为派⽣类的主基类
  • 查看派⽣类中是否有重写基类中的虚函数, 如果有,就替换成已经重写的虚函数地址;查看派⽣类是否有⾃身的虚函数,如果有,就追加⾃身的虚函数到⾃身的虚函数表中。

14. 类成员初始化方式

  1. 赋值初始化,通过在函数体内进行赋值初始化;
  1. 成员列表初始化,在冒号后使用初始化列表进行初始化。

这两种方式的主要区别在于:

  1. 对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。
  2. 成员列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。

C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率,所以成员初始化列表会快一些,效率更高。

15. 必须使用成员列表初始化的四种情况

  • 当初始化一个引用成员时;
  • 当初始化一个const成员时;
  • 没有默认构造函数的类类型:因为使用初始化列表可以不用调用默认构造函数来初始化,而是直接调用拷贝构造函数进行初始化。
  • 如果类存在继承关系,派生类必须在其初始化列表中调用基类的构造函数。

编译器会一一操作初始化列表,以适当顺序在构造函数之内安插初始化操作,并且在任何显示用户代码前。list中的项目顺序是由类中的成员声明顺序决定的,不是初始化列表中的排列顺序决定的。

16. 构造函数的执行顺序

一个派生类构造函数的执行顺序如下:
① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。
② 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。
③ 类类型的成员对象的构造函数(按照初始化顺序)
④ 派生类自己的构造函数。

析构函数相反。

16. 构造函数的执行算法?

  1. 在派生类构造函数中,所有的虚基类及上一层基类的构造函数调用
  2. 对象的vptr被初始化
  3. 如果有成员初始化列表,将在构造函数体内扩展开来,这必须在vptr被设定之后才做;
  4. 执行程序员所提供的代码;

17. 构造函数为什么不能为虚函数?析构函数为什么要虚函数?

1. 构造函数为什么不能为虚函数?

从存储空间角度,虚函数相应一个指向虚函数表的指针,但是这个指向虚函数表的指针事实上是存储在对象的内存空间的。假设构造函数是虚的,就须要通过 虚函数表来调用,但是对象还没有实例化,也就是内存空间还没有,所以构造函数不能是虚函数。

2. 析构函数为什么要虚函数?

C++中基类采用virtual虚析构函数是为了防止内存泄漏

  • 若析构函数是虚函数,delete时基类和子类都会被释放;
  • 若析构函数不是虚函数,delete时只有基类会被释放,而子类没有释放,存在内存泄露的风险。

18. 析构函数的作用

析构函数与构造函数的作用相反,用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间

特点:析构函数与构造函数同名,但该函数前面加~。 析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数

当撤销对象时,编译器也会自动调用析构函数。 每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员

19. 构造函数和析构函数可以调用虚函数吗

构造函数或者析构函数调用虚函数并不会发挥虚函数动态绑定的特性,跟普通函数没区别

  1. 在C++中,提倡不在构造函数和析构函数中调用虚函数
  2. 构造函数和析构函数调用虚函数时都不使用动态联编,如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本;
  3. 因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数是不安全的,故而C++不会进行动态联编;
  4. 析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经销毁,这个时候再调用子类的虚函数没有任何意义。

20. 哪些函数不能是虚函数(总结)

  1. 构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;
  2. 内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;
  3. 静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。
  4. 友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
  5. 普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。

20. 构造函数、析构函数可否抛出异常

语法上看都可以抛出异常。

  1. C++只会析构已经完成的对象,对象只有在其构造函数执行完毕才算是完全构造妥当。在构造函数中发生异常,控制权转出构造函数之外。因此,对象的构造函数中发生异常,对象的析构函数不会被调用。因此会造成内存泄漏
  2. auto_ptr对象来取代指针类成员,便对构造函数做了强化,免除了抛出异常时发生资源泄漏的危机,不再需要在析构函数中手动释放资源
  3. 如果控制权基于异常的因素离开析构函数,而此时正有另一个异常处于作用状态,C++会调用terminate函数让程序结束;
  4. 如果异常从析构函数抛出,而且没有在当地进行捕捉,那个析构函数便是执行不全的。如果析构函数执行不全,就是没有完成他应该执行的每一件事情。

21. 类如何实现只能静态分配(只能在栈上创建对象)和只能动态分配(只能在堆上创建对象)

1. 只能静态分配

只能静态分配即只能在栈上创建对象

是把new、delete运算符重载为private属性

class A  {  
private:  void* operator new(size_t t){}          //设置为私有void operator delete(void* ptr){}       //重载了new就需要重载delete。对应重载。
public:  A(){}  ~A(){}  
};  

只有使用new运算符,对象才会被建立在堆上,因此只要限制new运算符就可以实现类对象只能建立在栈上。可以将new运算符设为私有。

2. 只能动态分配

只能动态分配即只能在堆上创建对象

只允许动态分配需要禁止直接调用构造函数

把构造、析构函数设为protected属性,再用子类来动态创建

22. 建立类的对象有两种方式

  • 静态建立,静态建立一个类对象,就是由编译器为对象在栈空间中分配内存:A a;
  • 动态建立,A *p = new A();动态建立一个类对象,就是使用new运算符为对象在堆空间中分配内存。这个过程分为两步,第一步执行operator new()函数,在堆中搜索一块内存并进行分配;第二步调用类构造函数构造对象;

23. 如果想将某个类用作基类,为什么该类必须定义而非声明?

派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类必须知道他们是什么。

24. 什么情况会自动生成默认构造函数(合成构造函数)?

.html

惟有默认构造函数”被需要“的时候编译器才会合成默认构造函数

  1. 如果一个类没有任何构造函数,但他含有一个成员对象,该成员对象含有默认构造函数,那么编译器就为该类合成一个默认构造函数,因为不合成一个默认构造函数那么该成员对象的构造函数不能调用;
  2. 没有任何构造函数的类派生自一个带有默认构造函数的基类,那么需要为该派生类合成一个构造函数,只有这样基类的构造函数才能被调用;
  3. 带有虚函数的类,虚函数的引入需要进入虚表,指向虚表的指针,该指针是在构造函数中初始化的,所以没有构造函数的话该指针无法被初始化;
  4. 类使用了虚继承,即带有一个虚基类的类

合成的默认构造函数中,只有基类子对象和成员类对象会被初始化。所有其他的非静态数据成员都不会被初始化。

24. 何时需要合成拷贝构造函数

  1. 如果一个类没有拷贝构造函数,但是含有一个类类型的成员变量,该类型含有拷贝构造函数,此时编译器会为该类合成一个拷贝构造函数;
  2. 如果一个类没有拷贝构造函数,但是该类继承自含有拷贝构造函数的基类,此时编译器会为该类合成一个拷贝构造函数;
  3. 如果一个类没有拷贝构造函数,但是该类声明或继承了虚函数,此时编译器会为该类合成一个拷贝构造函数;
  4. 如果一个类没有拷贝构造函数,但是该类含有虚基类,此时编译器会为该类合成一个拷贝构造函数;

24. 什么情况下会调⽤拷贝构造函数(三种情况)

类的对象需要拷⻉时,拷⻉构造函数将会被调⽤,以下的情况都会调⽤拷⻉构造函数:

  • ⼀个对象以值传递的⽅式传⼊函数体,需要拷⻉构造函数创建⼀个临时对象压⼊到栈空间中。
  • ⼀个对象以值传递的⽅式从函数返回,需要执⾏拷⻉构造函数创建⼀个临时对象作为返回值。
  • ⼀个对象需要通过另外⼀个对象进⾏初始化。

25. 如何禁止自动生成拷贝构造函数?

  • 为了阻止编译器默认生成拷贝构造函数拷贝赋值函数,我们需要手动去重写这两个函数,某些情况下,为了避免调用拷贝构造函数和拷贝赋值函数,我们需要将他们设置成private,防止被调用。
  • 类的成员函数和friend函数还是可以调用private函数,如果这个private函数只声明不定义,则会产生一个连接错误;
  • 针对上述两种情况,我们可以定一个base类,在**base类中将拷贝构造函数和拷贝赋值函数设置成private,**那么派生类中编译器将不会自动生成这两个函数,且由于base类中该函数是私有的,因此,派生类将阻止编译器执行相关的操作。

拷贝构造:B(A)
拷贝赋值:B=A,B已经定义,只是赋值。

25. 什么是类的组合?

  1. 一个类里面的数据成员是另一个类的对象,即内嵌其他类的对象作为自己的成员;

  2. 创建组合类的对象:首先创建各个内嵌对象,难点在于构造函数的设计。创建对象时既要对基本类型的成员进行初始化,又要对内嵌对象进行初始化。

  3. 创建组合类对象,构造函数的执行顺序:先调用内嵌对象的构造函数,然后按照内嵌对象成员在组合类中的定义顺序,与组合类构造函数的初始化列表顺序无关。然后执行组合类构造函数的函数体,析构函数调用顺序相反。

组合与继承优缺点?

一:继承
继承是Is a 的关系,比如说Student继承Person,则说明Student is a Person。

继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。

继承的缺点有以下几点:
①:父类的内部细节对子类是可见的。
②:子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。
③:如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。

二:组合
组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。

组合的优点
①:当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象是不可见的。
②:当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。
③:当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。

组合的缺点:①:容易产生过多的对象。②:为了能组合多个对象,必须仔细对接口进行定义。

26. 抽象基类为什么不能创建对象?

抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。

  • 抽象类的定义:
    称带有纯虚函数的类为抽象类。

  • 抽象类的作用:
    抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。

原因

抽象类尚未实现方法,所以不能创建对象。

创建对象去调用方法是指做明确的事情,而这个抽象方法并不明确,所以只有继承抽象类去实现抽象方法才可以。

27. C++里面的多态性

指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。

  • 编译时多态性:通过重载函数实现
    是在编译的时候,就确定调用函数的类型。
  • 运行时多态性:通过虚函数实现。
    在运行的时候,才确定调用的是哪个函数,动态绑定(运行时绑定)。

28. 什么时候会调用析构函数

有四种方式会调用析构函数:

  1. 生命周期:对象生命周期结束,会调用析构函数。

  2. delete:调用delete时,会删除指针类对象。

  3. 包含关系:对象Dog是对象Person的成员,Person的析构函数被调用时,对象Dog的析构函数也被调用。

  4. 继承关系:当Person是Student的父类,调用Student的析构函数,会调用Person的析构函数。

29. 为什么友元函数必须在类内部声明

1. 友元函数定义

  • 友元函数和普通函数最大的区别在于友元函数可以直接访问类的私有成员和保护成员
  • 友元函数不属于类的成员函数,但是友元函数必须在类内部声明;
  • 友元函数使用friend关键词声明;

2. 类内部声明原因

因为编译器必须能够读取这个结构的声明以理解这个数据类型的大小、行为等方面的所有规则。

有一条规则在任何关系中都很重要,那就是谁可以访问我的私有部分

30. 用C语言实现C++的继承

#include <iostream>
using namespace std;
//C++中的继承与多态
struct A
{virtual void fun() //C++中的多态:通过虚函数实现{cout << "A:fun()" << endl;}int a;
};
struct B :public A //C++中的继承:B类公有继承A类
{virtual void fun() //C++中的多态:通过虚函数实现(子类的关键字virtual可加可不加){cout << "B:fun()" << endl;}int b;
};
//C语言模拟C++的继承与多态
typedef void (*FUN)(); //定义一个函数指针来实现对成员函数的继承
struct _A //父类
{FUN _fun; //由于C语言中结构体不能包含函数,故只能用函数指针在外面实现int _a;
};
struct _B //子类
{_A _a_; //在子类中定义一个基类的对象即可实现对父类的继承int _b;
};
void _fA() //父类的同名函数
{printf("_A:_fun()\n");
}
void _fB() //子类的同名函数
{printf("_B:_fun()\n");
}
void Test()
{//测试C++中的继承与多态A a; //定义一个父类对象aB b; //定义一个子类对象bA* p1 = &a; //定义一个父类指针指向父类的对象p1->fun(); //调用父类的同名函数p1 = &b; //让父类指针指向子类的对象p1->fun(); //调用子类的同名函数//C语言模拟继承与多态的测试_A _a; //定义一个父类对象_a_B _b; //定义一个子类对象_b_a._fun = _fA; //父类的对象调用父类的同名函数_b._a_._fun = _fB; //子类的对象调用子类的同名函数_A* p2 = &_a; //定义一个父类指针指向父类的对象p2->_fun(); //调用父类的同名函数p2 = (_A*)&_b; //让父类指针指向子类的对象,由于类型不匹配所以要进行强转p2->_fun(); //调用子类的同名函数
}

31. 继承机制中对象之间如何转换?

  • 向上类型转换
    将派生类指针或引用转换为基类的指针或引用被称为向上类型转换,向上类型转换会自动进行,而且向上类型转换是安全的。

  • 向下类型转换
    将基类指针或引用转换为派生类指针或引用被称为向下类型转换,向下类型转换不会自动进行,因为一个基类对应几个派生类,所以向下类型转换时不知道对应哪个派生类,所以在向下类型转换时必须加动态类型识别技术。用dynamic_cast进行向下类型转换。

32. c++左值、右值、左值引用、右值引用

  • 在C++11中可以取地址的、有名字的就是左值

  • 反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。

  • 举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。

  • 左值引用,就是绑定到左值的引用,通过&来获得左值引用。

  • 右值引用,就是绑定到右值的引用,通过&&来获得右值引用。

  • 左值也可以作为右值表达式,变量可以是左值,也可以为右值,但常量只能是右值。

  • C++11之前右值只能被const 类型的引用所指向;而左值可以被const或非const类型引用指向

    int num = 10;
    const int &b = num;
    const int &c = 10;
    

在C++11中右值又分为纯右值将亡值

  1. 纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量和不跟对象关联的字面量值;
  2. 将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。

C++11 标准新引入了另一种引用方式,称为右值引用,用 “&&” 表示。

右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限;其次,常量右值引用的作用就是引用一个不可修改的右值,这项工作完全可以交给常量左值引用完成。

  • 和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化。

    int num = 10;
    //int && a = num;  //右值引用不能初始化为左值
    int && a = 10;
    
  • 右值引用可以修改右值

    int && a = 10;
    a = 11;
    cout << a << endl;   //输出结果为11
    
  • move()函数将左值强制转换到右值

    move(arg)
    //agr:左值对象,该函数返回arg对象的右值形式
    
    int num = 10;
    int&& a = std::move(num);  //编译成功
    cout << a << endl;   //输出结果为10;
    

总结如下:

  • 非常量左值引用可以引用的值的类型只有非常量左值,常量左值引用非常量左值、常量左值及右值

    	int num = 10;int& a = num;	//编译成功,非常量左值引用支持引用非常量左值const int num2 = 100;int& b = num2;	//编译失败,非常量左值引用不支持引用常量左值int& c = 10;	//编译失败,非常量左值引用不支持引用右值const int& d = num;		//编译成功,常量左值引用支持引用非常量左值const int& e = num2;	//编译成功,常量左值引用支持引用常量左值const int& f = 100;		//编译成功,常量左值引用支持引用右值
    
  • 右值引用不支持引用左值;非常量右值引用可以引用的值的类型只有非常量右值,常量右值引用非常量右值、常量右值

    	int num = 10;const int num2 = 100;int&& a = num;	//编译失败,非常量右值引用不支持引用非常量左值int&& b = num2;	//编译失败,非常量右值引用不支持引用常量左值int&& c =10;	//编译成功,非常量右值引用支持引用非常量右值const int&& d = num;	//编译失败,常量右值引用不支持引用非常量左值const int&& e = num2;	//编译失败,常量右值引用不支持引用常量左值const int&& f = 100;	//编译成功,常量右值引用支持引用右值
    

右值引用与左值引用的区别

  • 左值引用绑定到有确定存储空间以及变量名的对象上,表达式结束后对象依然存在;
  • 右值引用绑定到要求转换的表达式、字面常量、返回右值的表达式等临时对象上,赋值表达式结束后就对象就会被销毁。
  • 左值引用后可以利用别名修改左值对象;右值引用绑定的值不能修改。

33. 移动构造函数

移动构造函数是参数类型为右值引用的拷贝构造函数。

移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用

意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只有当用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。

//参数为左值引用的深拷贝构造函数,转移堆内存资源所有权,改变 source.ptr_ 的值
Integer(Integer& source): ptr_(source.ptr_) {source.ptr_ = nullptr;cout << "Call Integer(Integer& source)" << endl;
}//移动构造函数,与参数为左值引用的深拷贝构造函数基本一样
Integer(Integer&& source): ptr_(source.ptr_) {source.ptr_ = nullptr;cout << "Call Integer(Integer&& source)" << endl;
}Integer(int value): ptr_(new int(value)) {cout << "Call Integer(int value)" << endl;
}

34. C语言的编译链接过程?

源代码-->预处理-->编译-->优化-->汇编-->链接-->可执行文件.

  1. 预处理
    读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。.i后缀为预处理后的c文件,.ii后缀为预处理后的C++文件。
  2. 编译阶段
    编译程序所要做的工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码或表示成汇编代码.s后缀为编译后的文件
  3. 汇编过程
    汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。.o后缀为汇编后的目标文件
  4. 链接阶段
    链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。

35. vector与list的区别与应用

  1. vector数据结构
    • vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。因此能高效的进行随机存取,时间复杂度为o(1);但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。
    • 连续存储结构:vector是可以实现动态增长的对象数组,支持对数组高效率的访问和在数组尾端的删除和插入操作,在中间和头部删除和插入相对不易,需要挪动大量的数据。它与数组最大的区别就是vector不需程序员自己去考虑容量问题,库里面本身已经实现了容量的动态增长,而数组需要程序员手动写入扩容函数进形扩容。
  2. list数据结构
    • list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n);但由于链表的特点,能高效地进行插入和删除
    • 非连续存储结构:list是一个双链表结构,支持对链表的双向遍历。每个节点包括三个信息:元素本身,指向前一个元素的节点(prev)和指向下一个元素的节点(next)。因此list可以高效率的对数据元素任意位置进行访问和插入删除等操作。由于涉及对额外指针的维护,所以开销比较大。
  1. 区别:
    vector的随机访问效率高,但在插入和删除时(不包括尾部)需要挪动数据,不易操作。list的访问要遍历整个链表,它的随机访问效率低。但对数据的插入和删除操作等都比较方便,改变指针的指向即可。list是单向的,vector是双向的。vector中的迭代器在使用后就失效了,而list的迭代器在使用之后还可以继续使用。

怎么找某vector或者list的倒数第二个元素

int mySize = vec.size();
vec.at(mySize -2);

list不提供随机访问,所以不能用下标直接访问到某个位置的元素,要访问list里的元素只能遍历,不过你要是只需要访问list的最后N个元素的话,可以用反向迭代器来遍历:

36. 链表和数组有什么区别

36. STL容器 vector容量说明

size()函数返回的是已用空间大小,capacity()返回的是总空间大小,capacity()-size()则是剩余的可用空间大小。

size()capacity()相等,说明vector目前的空间已被用完,如果再添加新元素,则会引起vector空间的动态增长。

由于动态增长会引起重新分配内存空间、拷贝原空间、释放原空间,这些过程会降低程序效率。因此,可以使用reserve(n)预先分配一块较大的指定大小的内存空间,这样当指定大小的内存空间未使用完时,是不会重新分配内存空间的,这样便提升了效率。只有当n>capacity()时,调用reserve(n)才会改变vector容量。

resize()成员函数只改变元素的数目,不改变vector的容量

  1. 空的vector对象,size()capacity()都为0
  2. 当空间大小不足时,新分配的空间大小为原空间大小的2倍。
  3. 使用reserve()预先分配一块内存后,在空间未满的情况下,不会引起重新分配,从而提升了效率。
  4. reserve()分配的空间比原空间小时,是不会引起重新分配的。
  5. resize()函数只改变容器的元素数目,未改变容器大小。
  6. reserve(size_type)只是扩大capacity值,这些内存空间可能还是“野”的,如果此时使用“[ ]”来访问,则可能会越界。而resize(size_type new_size)会真正使容器具有new_size个对象。

37. C++vector的动态扩容,为何是1.5倍或者是2倍

  1. 扩容机制回顾
    当向vector中插入元素时,如果元素有效个数size与空间容量capacity相等时,vector内部会触发扩容机制:

    • 开辟新空间
    • 拷贝元素。
    • 释放旧空间。

    注意:每次扩容新空间不能太大,也不能太小,太大容易造成空间浪费,太小则会导致频繁扩容而影响程序效率。

  2. 如何避免扩容导致效率低
    如果要避免扩容而导致程序效率过低问题,其实非常简单:如果在插入之前,可以预估vector存储元素的个数,提前将底层容量开辟好即可。如果插入之前进行reserve,只要空间给足,则插入时不会扩容,如果没有reserve,则会边插入边扩容,效率极其低下。

  3. 为什么选择以倍数方式扩容

    以倍数的方式扩容比以等长个数的扩容方式效率高。对比可以发现采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度,因此,使用成倍的方式扩容。

  4. 为什么选择1.5倍或者2倍方式扩容,而不是3倍、4倍

    1. 不同的编译器,vector有不同的扩容大小。在vs下是1.5倍,在GCC下是2倍;
    2. 空间和时间的权衡。简单来说, 空间分配的多,平摊时间复杂度低,但浪费空间也多。
    3. 为了防止申请内存的浪费,现在使用较多的有2倍与1.5倍的增长方式,而1.5倍的增长方式可以更好的实现对内存的重复利用。

38. vector如何释放空间

.html

由于vector的内存占用空间只增不减,比如你首先分配了10,000个字节,然后erase掉后面9,999个,留下一个有效元素,但是内存占用仍为10,000个。所有内存空间是在vector析构时候才能被系统回收。

empty()用来检测容器是否为空的,clear()可以清空所有元素。但是即使clear()vector所占用的内存空间依然如故,无法保证内存的回收。

如果需要空间动态缩小,可以考虑使用deque。如果vector,可以用swap()来帮助你释放内存。

vector<Point>().swap(pointVec); //或者pointVec.swap(vector<Point> ())
template < class T >
void ClearVector( vector< T >& vt ) 
{vector< T > vtTemp; veTemp.swap( vt );
}

swap()是交换函数,使vector离开其自身的作用域,从而强制释放vector所占的内存空间,总而言之,释放vector内存最简单的方法是vector<Point>().swap(pointVec)。当时如果pointVec是一个类的成员,不能把vector<Point>().swap(pointVec)写进类的析构函数中,否则会导致double free or corruption (fasttop)的错误,原因可能是重复释放内存

39. 容器内部删除一个元素

  1. 顺序容器:顺序容器有以下三种:可变长动态数组 vector、双端队列 deque、双向链表 list。
    erase迭代器不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效(list除外),所以不能使用erase(it++)的方式,但是erase的返回值是下一个有效迭代器;

    It = c.erase(it);
    
  2. 关联容器:关联容器有以下四种:set、multiset、map、multimap。
    erase迭代器只是被删除元素的迭代器失效,但是返回值是void,所以要采用erase(it++)的方式删除迭代器;

    c.erase(it++);
    

40. set与hash_set的区别

  1. set底层是以红黑树(RB-Tree)实现,hash_set底层是以hash_table实现的;
  2. RB-Tree有自动排序功能,而hash_table不具有自动排序功能;
  3. sethash_set元素的键值就是实值;

41. unordered_map(hash_map)与map的区别

  1. map内部会根据key的大小进行排序,内部使用的是红黑树实现
  2. c++ 标准库的hash_map其实叫作std::unordered_map,底层实现是hash tablehash_map不具有自动排序的功能;
  3. unordered_map存储时是根据key的hash值判断元素是否相同,而map中的元素是按照二叉搜索树存储,进行中序遍历会得到有序遍历。

1. hash_map冲突解决

  • hash_map底层使用的是hash_tablehash_table使用的开链法进行冲突避免,所有hash_map采用开链法进行冲突解决。

2. hash_map扩容

  • 什么时候扩容:当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值—即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。
  • 扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。

42. map插入方式有几种

  1. 用insert函数插入pair数据

    map<int, string> mp;
    mp.insert(pair<int,string>(1,"aaaaa"));
    
  2. insert函数插入value_type数据

    map<int, string> mp;
    mp.insert(map<int, string>::value_type(3,"ccccc"));
    
  3. insert函数中使用make_pair()函数

    map<int, string> mp;
    mp.insert(make_pair<int,string>(2,"bbbbb"));
    
  4. 用数组方式插入数据

    map<int, string> mp;
    mp[4] = "ddddd";
    

43. vector越界访问下标?vector删除元素时会不会释放空间?

  1. 通过下标访问vector中的元素时不会做边界检查,即便下标越界。也就是说,下标与first迭代器相加的结果超过了finish迭代器的位置,程序也不会报错,而是返回这个地址中存储的值。如果想在访问vector中的元素时首先进行边界检查,可以使用vector中的at函数。通过使用at函数不但可以通过下标访问vector中的元素,而且在at函数内部会对下标进行边界检查。
  2. erase()函数,只能删除内容,不能改变容量大小; erase成员函数,它删除了itVect迭代器指向的元素,并且返回要被删除的itVect之后的迭代器,迭代器相当于一个智能指针;clear()函数,只能清空内容,不能改变容量大小;如果要想在删除内容的同时释放内存,那么你可以选择deque容器。

44. map[]与find函数查找的区别?

  1. map的下标运算符[]的作用是:将key作为下标去执行查找,并返回对应的值;如果不存在这个key,就将一个具有该key和值类型的默认值的项插入map中。
  2. map的find函数:用key执行查找,找到了返回该位置的迭代器;如果不存在这个key,就返回尾迭代器。

45. STL中list与deque之间的区别

  1. list不再能够像vector一样以普通指针作为迭代器,因为其节点不保证在存储空间中连续存在;
  2. list插入操作和结合操作都不会造成原有的list迭代器失效;
  3. list不仅是一个双向链表,而且还是一个环状双向链表,所以它只需要一个指针;
  4. list不像vector那样有可能在空间不足时做重新配置、数据移动的操作,所以插入前的所有迭代器在插入操作之后都仍然有效;
  5. deque是一种双向开口连续线性空间,所谓双向开口,意思是可以在头尾两端分别做元素的插入和删除操作;可以在头尾两端分别做元素的插入和删除操作;
  6. dequevector最大的差异,一在于deque允许常数时间内对起头端进行元素的插入或移除操作,二在于deque没有所谓容量概念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来deque没有所谓的空间保留功能。

46. STL容器

  1. vector 底层数据结构为数组 ,支持快速随机访问
  2. list 底层数据结构为双向链表,支持快速增删
  3. deque 底层数据结构为一个中央控制器和多个缓冲区
  4. stack 底层一般用listdeque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时
  5. queue 底层一般用listdeque实现,默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque
  6. priority_queue 的底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现
  7. set 底层数据结构为红黑树,有序,不重复
  8. multiset 底层数据结构为红黑树,有序,可重复
  9. map 底层数据结构为红黑树,有序,不重复
  10. multimap 底层数据结构为红黑树,有序,可重复
  11. hash_set 底层数据结构为hash表,无序,不重复
  12. hash_multiset 底层数据结构为hash表,无序,可重复
  13. hash_map 底层数据结构为hash表,无序,不重复
  14. hash_multimap 底层数据结构为hash表,无序,可重复

47. 函数指针

  1. 定义

    函数指针是指向函数的指针变量。函数指针本身是⼀个指针变量,该指针变量指向⼀个具体的函数。

    如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址,函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。

  2. 函数指针的声明方法

    double (*pf)(int);   // 指针pf指向的函数, 输入参数为int,返回值为double
    
  3. 为什么有函数指针
    调⽤函数和做函数的参数,⽐如回调函数(要将函数作为参数进行传递,必须传递函数名)。

  4. 两种方法赋值:
    指针名 = 函数名; 指针名 = &函数名

  1. 函数指针的使用示例:

    #include <iostream>
    #include <algorithm>
    #include <cmath>using namespace std;double cal_m1(int lines)
    {return 0.05 * lines;
    } double cal_m2(int lines)
    {return 0.5 * lines;
    }void estimate(int line_num, double (*pf)(int lines))
    {cout << "The " << line_num << " need time is: " << (*pf)(line_num) << endl; 
    }int main(int argc, char *argv[])
    {int line_num = 10;// 函数名就是指针,直接传入函数名estimate(line_num, cal_m1);estimate(line_num, cal_m2); return 0;
    }
    

48. c和c++的区别?

  1. 第一点就应该想到C是面向过程的语言,而C++是面向对象的语言。
  2. C语言通过malloc和free来进行堆内存的分配和释放,而C++除此之外还有new/delete关键字;
  3. C++的类是C所没有的,但是C中的struct是可以在C++中正常使用的,并且C++对struct进行了进一步的扩展,使struct在C++中可以和class一样当做类使用,而唯一和class不同的地方在于struct的成员默认访问修饰符是public,而class默认的是private;
  4. C++支持函数重载,而C不支持函数重载,而C++支持重载的依仗就在于C++的名字修饰与C不同,例如在C++中函数int fun(int ,int)经过名字修饰之后变为 _fun_int_int ,而C是 _fun,一般是这样的,所以C++才会支持不同的参数调用不同的函数;
  5. C++中有引用,而C没有;
  6. 当然还有C++全部变量的默认链接属性是外链接,而C是内连接;
  7. C 中用const修饰的变量不可以用在定义数组时的大小,但是C++用const修饰的变量可以(如果不进行&,解引用的操作的话,是存放在符号表的,不开辟内存);
  8. 强制类型转换上也不一样,C的强制类型转换使用()小括号里面加类型进行类型强转的,而C++有四种自己的类型强转方式,分别是const_caststatic_castreinterpret_castdynamic_cast
  9. 由于C++多了一个类,因此和C语言的作用域比起来,就多了一个类作用域,此外,C++还支持namespace名字空间,可以让用户自己定义新的名字空间作用域出来,避免全局的名字冲突问题。

49. c/c++的内存分配,详细说一下栈、堆、静态存储区?

  1. 栈区(stack)— 由编译器自动分配释放,存放函数的参数值局部变量的值等。其操作方式类似于数据结构中的栈。
  2. 堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统)回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。 一般存放newmalloc出来的对象
  3. 全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
  4. 文字常量区常量字符串就是放在这里的。程序结束后由系统释放。
  5. 程序代码区 —存放函数体的二进制代码。

50. 堆与栈的区别?

  1. 管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生内存泄漏。

  2. 空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的。

  3. 碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出。

  4. 生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

  5. 分配方式堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca()函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现。

  6. 分配效率
    栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
    堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多

51. 野指针是什么?成因以及解决方法?

  1. 野指针:不确定其具体指向的指针。“野指针”最常来自于未初始化的指针。

  2. “野指针”的成因主要有3种:
    指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如

    char *p = NULL;
    char *str = new char(100);
    

    指针p被free或者delete之后,没有置为NULL
    指针操作超越了变量的作用范围:不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

  3. 如何避免野指针:

    • 对指针进行初始化
      ①将指针初始化为NULL。

      char *   p  = NULL;
      

      ②用malloc分配内存

      char * p = (char * )malloc(sizeof(char));
      

      ③用已有合法的可访问的内存地址对指针初始化

      char num[ 30] = {0};
      char *p = num;
      
    • 指针用完后释放内存,将指针赋NULL。

      delete(p);
      p = NULL;
      

52. 悬空指针和野指针有什么区别?

  1. 野指针:野指针是不确定其具体指向的指针,一般指那些未初始化的指针。

  2. 悬空指针:一个指针的指向对象已经被删除,那么就成了悬空指针。

53. new和malloc的区别?

  1. new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持;
  2. 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的大小。
  3. new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
  4. 在使用new分配内存空间时,内存空间不够时会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
  5. new会调用operator new函数,申请足够的内存(通常底层使用malloc实现),然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
  6. malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

54. delete p与delete[]p,allocator

  1. delete p会调用一次析构函数,而delete[] p会调用每个成员的析构函数
  2. 如果数组类型是自定义类,那么new[]只能用delete[]来对应,new和delete对应;但是对于普通数据类型而言,他们作用的效果是一样的,例如int* p=new int[10],delete p和delete[] p作用效果是一样的,原因是内部普通数据类型没有析构函数
  3. delete[]时,数组中的元素按逆序的顺序进行销毁;
  4. new在内存分配上面有一些局限性,new的机制是将内存分配和对象构造组合在一起,同样的,delete也是将对象析构和内存释放组合在一起的。
  5. allocator是一个模板类,负责封装堆内存管理的。allocator申请一部分内存,不进行初始化对象,只有当需要的时候才进行初始化操作。
  • delete 释放new分配的单个对象指针指向的内存
  • delete[] 释放new分配的对象数组指针指向的内存

55. new和delete的实现原理, delete是如何知道释放内存的大小的?

1. new

  • 简单类型直接调用operator new分配内存;
  • 而对于复杂结构先调用operator new分配内存,然后在分配的内存上调用构造函数:new[]先调用operator new[]分配内存,然后在p的前四个字节写入数组大小n,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小;

步骤总结如下:

  • new表达式调用一个名为operator new(operator new[])函数,分配一块足够大的、原始的、未命名的内存空间;
  • 编译器运行相应的构造函数以构造这些对象,并为其传入初始值;
  • 对象被分配了空间并构造完成,返回一个指向该对象的指针。

2. delete

  • 简单数据类型默认只是调用free函数;针对简单类型,delete和delete[]等同。
  • 复杂数据类型先调用析构函数再调用operator delete;假设指针p指向new[]分配的内存,因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。
  • 需要在 new [] 一个对象数组时,保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。

56. new、delete和malloc、free混用问题

  • 对于基本类型而言,没有区别。根据需要new和malloc可以混用,new[]和malloc可以混用,delete、delete[]和free可以混用。
  • 对于构造函数没有作用的类,new和malloc可以混用。 对于构造函数有作用的类,如果想混用,需要显式调用构造函数的逻辑实现。
  • 对于没有显式定义析构函数的类,delete、delete[]和free可以混用。
  • 对于显式定义析构函数的类,delete[]和new[]必须配套使用,delete和free如果想混用,free需要显式调用析构函数。

实际使用时,不要混用,因为不知道会发生什么未知的事情!!!

57. malloc、realloc、calloc的区别

相同点

  1. 都是从堆上申请空间;
  2. 都需要对返回值判空;
  3. 都需要用户free释放;
  4. 返回值类型相同(都是void*);
  5. 都需要类型转化;
  6. 底层实现是一样的,都需要开辟多余的空间,用来维护申请的空间。

不同点

  1. 函数参数类型不同;

  2. calloc函数会对申请空间初始化,并且初始化为0;

    void* calloc(size_t n,size_t size);
    int *p = calloc(20, sizeof(int));//申请20个int类型的空间。
    
  3. malloc函数申请的空间的值是随机初始化的,必须使用memset进行初始化;

    void* malloc(unsigned int num_size);
    int *p = malloc(20*sizeof(int));申请20个int类型的空间;
    
  4. realloc函数用于对动态内存进行扩容(及已申请的动态空间不够使用,需要进行空间扩容操作),ptr为指向原来空间基址的指针, new_size为接下来需要扩充容量的大小。

    void realloc(void *ptr, size_t new_Size)
    

58. 使用智能指针管理内存资源,RAII

  • 智能指针是C++程序员们一件管理内存的利器,使用智能指针管理内存资源,实际上就是将申请的内存资源交由智能指针来管理,是RAII技术的一种实现。
  • RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中获取资源,在析构函数中释放资源。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用来管理资源,将资源和对象的生命周期绑定。

59. 智能指针

C++11中主要提供三种智能指针:

shared_ptr 、 unique_ptr 、 weak_ptr

auto_ptr是c++98的方案,c++11已经抛弃

为什么要使用智能指针:解决内存泄漏

  • 智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏
  • 使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。
  • 智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间
  • 智能指针包含在头文件<memory>中。

1. shared_ptr

  • shared_ptr实现共享式拥有概念多个智能指针可以指向同一对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用引用计数机制来表明资源被几个指针共享,每一个shared_ptr的拷贝都指向相同的内存。
  • 每使用shared_ptr一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
  • shared_ptr 是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
  • shared_ptr会有相互引用死锁的问题:如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。
  • 对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。
  • 可以通过成员函数use_count()来查看资源的所有者个数。
  • 除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。
  • 当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

引用成环例子

#include <iostream>
#include <memory>class CB;
class CA {public:CA() {std::cout << "CA()" << std::endl;}~CA() {std::cout << "~CA()" << std::endl;}void set_ptr(std::shared_ptr<CB>& ptr) {m_ptr_b = ptr;}private:std::shared_ptr<CB> m_ptr_b;
};class CB {public:CB() {std::cout << "CB()" << std::endl;}~CB() {std::cout << "~CB()" << std::endl;}void set_ptr(std::shared_ptr<CA>& ptr) {m_ptr_a = ptr;}private:std::shared_ptr<CA> m_ptr_a;
};int main()
{std::shared_ptr<CA> ptr_a(new CA());std::shared_ptr<CB> ptr_b(new CB());ptr_a->set_ptr(ptr_b);ptr_b->set_ptr(ptr_a);std::cout << ptr_a.use_count() << " " << ptr_b.use_count() << std::endl;return 0;
}

成员函数

  • use_count 返回引用计数的个数。
  • unique 返回是否是独占所有权( use_count 为 1)。
  • swap 交换两个 shared_ptr 对象(即交换所拥有的对象)。
  • reset放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少。
  • get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr<int> sp(new int(1)); sp 与 sp.get()是等价的。

make_shared 与 new 的区别

  • new会导致内存碎片化,make_shared则不会。

  • new: 先new后赋值的方式,是先在堆上分配一块内存,然后在堆上再建一个智能指针控制块,这两个东西是不连续的,会造成内存碎片化;

  • make_shared: make_shared的方式是直接在堆上新建一块足够大的内存,其中包含两部分,上面是内存(用来使用),下面是控制块(包含引用计数),然后用T的构造函数去初始化分配的内存。

    shared_ptr<int> p1 = make_shared<int>(42);//安全的内存分配返回指向此对象的shared_ptr
    

2. weak_ptr

  • weak_ptr 是一种不影响对象生命周期的智能指针, 它指向一个shared_ptr管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段

  • weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数

  • weak_ptr设计的目的是为配合 shared_ptr而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr或另一个 weak_ptr对象构造, 它的构造和析构不会引起引用记数的增加或减少

  • weak_ptr是用来解决shared_ptr相互引用时的死锁问题(引用成环)。它是对对象的一种弱引用,不会修改对象的引用计数,只能用来检测对象是否已经释放,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

    弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。

引用成环解决方法:以上文的例子来说,解决办法就是将两个类中的一个成员变量改为weak_ptr对象,比如将CB中的成员变量改为weak_ptr对象,即CB类的代码如下:

class CB {public:CB() {std::cout << "CB()" << std::endl;}~CB() {std::cout << "~CB()" << std::endl;}void set_ptr(std::shared_ptr<CA>& ptr) {m_ptr_a = ptr;}private:std::weak_ptr<CA> m_ptr_a;
};

3. auto_ptr

  • 在C++11之前,auto_ptr 的作用与 unique_ptr 类似:独占所指对象采用所有权模式

  • 在C++11之后,auto_ptr已经被弃用。

  • auto_ptr的构造函数是explicit,阻止了一般指针隐式转换为 auto_ptr的构造,所以不能直接将一般类型的指针赋值给auto_ptr类型的对象,必须用auto_ptr的构造函数创建对象;

  • 由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时避免多个auto_ptr对象管理同一个指针;

  • auto_ptr内部实现,析构函数中删除对象用的是delete而不是delete[],所以auto_ptr不能管理数组;

  • auto_ptr与unique_ptr的区别在于:

    如果尝试对 auto_ptr/unique_ptr 进行拷贝或赋值,unique_ptr 会直接在编译阶段报错;而auto_ptr却可以编译通过,只有在运行阶段,且访问到由于被拷贝而导致失去了对象控制权后变成空指针的auto_ptr后,程序才会报错。

    auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
    auto_ptr<string> p2;
    p2 = p1; //auto_ptr不会报错.
    

    此时不会报错,p2剥夺了p1的所有权,所有权转让,p1已经不指向该对象,指向空。当程序运行时访问p1将会报错。
    所以auto_ptr的缺点是:存在潜在的内存崩溃问题

    • 对于特定的对象,只能有一个指针可拥有,这样拥有该对象的智能指针的析构函数会删除该对象,而赋值操作会转让所有权

4. unique_ptr

  • unique_ptr 独占其所指对象,保证同一时间内只有一个智能指针可以指向该对象。

  • 属于跟踪引用和所有权模式

  • 没有一个类似于make_shared的函数用于初始化unique_ptr,unique_ptr只能绑定到一个new返回的指针上。

    unique_ptr<string> p3 (new string ("auto"));   //#4
    unique_ptr<string> p4;                       //#5
    p4 = p3;//此时会报错!!
    

    编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全

  • 另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr将存在一段时间,编译器将禁止这么做

    unique_ptr<string> pu1(new string ("hello world"));
    unique_ptr<string> pu2;
    pu2 = pu1;                                      // #1 not allowed
    unique_ptr<string> pu3;
    pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed
    
  • 如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:

    unique_ptr<string> ps1, ps2;
    ps1 = demo("hello");
    ps2 = move(ps1);
    ps1 = demo("alexia");
    cout << *ps2 << *ps1 << endl;
    
  • unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。

60. 手写实现智能指针类

61. 内存对齐?

1. 什么是内存对齐

  1. 分配内存的顺序是按照声明的顺序。
  2. 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。
  3. 最后整个结构体的大小必须是里面变量类型最大值的整数倍。

添加了#pragma pack(n)后规则就变成了下面这样:

  1. 偏移量要是n和当前变量大小中较小值的整数倍
  2. 整体大小要是n和最大变量大小中较小值的整数倍
  3. n值必须为1,2,4,8…,为其他值时就按照默认的分配规则

2. 为什么要内存对齐

  • 平台原因(移植原因)

    1. 不是所有的硬件平台都能访问任意地址上的任意数据的;
    2. 某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异
  • 性能原因:

    1. 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
    2. 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

62. 结构体变量比较是否相等

  1. 重载 “==” 操作符

    struct foo {int a;int b;bool operator==(const foo& rhs) // 操作运算符重载{return( a == rhs.a) && (b == rhs.b);}
    };
    
  2. 元素的话,一个个比;

  3. 指针直接比较,如果保存的是同一个实例地址,则(p1==p2)为真;

是否可以使用memcmp来比较两个结构体是否相等呢

  • 不可以。memcmp函数是逐个字节进行比较的,而struct存在字节对齐,字节对齐时补的字节内容是随机的,会产生垃圾值,所以无法比较。

63. 位运算

64. 怎样判断两个浮点数是否相等?

  • 对两个浮点数判断大小和是否相等不能直接用==来判断,会出错!
  • 对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!

65. 使用宏实现两个数中求较大值

#define MAX(X,Y) ((X)>(Y)?(X):(Y))

66. const与#define的区别:

  1. const定义的常量是变量带类型,而#define定义的只是个常数不带类型
  2. const在编译、链接过程中起作用,#define只在预处理阶段起作用,简单的文本替换;
  3. define只是简单的字符串替换没有类型检查。而const是有数据类型的,是要进行判断的,可以避免一些低级错误;
  4. define预处理后,占用代码段空间,const占用数据段空间;
  5. const不能重定义,而define可以通过#undef取消某个符号的定义,进行重定义
  6. define独特功能,比如可以用来防止文件重复引用。

67. #define和别名typedef的区别

  1. 执行时间不同,typedef在编译阶段有效,typedef有类型检查的功能;#define是宏定义,发生在预处理阶段,不进行类型检查;
  2. 功能差异,typedef用来定义一种数据类型的别名,定义与平台无关的数据类型,与struct的结合使用等。#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
  3. 作用域不同,#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。而typedef有自己的作用域。

68. define与inline的区别

  1. #define是关键字,inline是函数
  2. 宏定义在预处理阶段进行文本替换,inline函数在编译阶段进行替换;
  3. inline函数有类型检查,相比宏定义比较安全;

69. #include 的顺序以及尖括号和双引号的区别

  • 尖括号表示编译器只在系统默认目录或尖括号内的工作目录下搜索头文件,并不去用户的工作目录下寻找,所以一般尖括号用于包含标准库文件;
  • 双引号表示编译器先在用户的工作目录下搜索头文件,如果搜索不到则到系统默认目录下去寻找,所以双引号一般用于包含用户自己编写的头文件。

70. 模板类和模板函数的区别是什么?

函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。即函数模板允许隐式调用和显式调用而类模板只能显示调用。在使用时类模板必须加<T>,而函数模板不必。

71. 为什么模板类一般都是放在一个h文件中

  • 模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义

72. C++中类成员的访问权限和继承权限问题(public,protected,private)

1. 公有权限

  • public:用该关键字修饰的成员表示公有成员,该成员不仅可以在类内可以被 访问,在类外也是可以被访问的,是类对外提供的可访问接口;
  • private:用该关键字修饰的成员表示私有成员,该成员仅在类内可以被访问,在类体外是隐藏状态;
  • protected:用该关键字修饰的成员表示保护成员,保护成员在类体外同样是隐藏状态,但是对于该类的派生类来说,相当于公有成员,在派生类中可以被访问。

2. 继承权限

  • 若继承方式是public,基类成员在派生类中的访问权限保持不变,也就是说,基类中的成员访问权限,在派生类中仍然保持原来的访问权限;
  • 若继承方式是private,基类所有成员在派生类中的访问权限都会变为私有(private)权限;
  • 若继承方式是protected,基类的公有成员和保护成员在派生类中的访问权限都会变为保护(protected)权限,私有成员在派生类中的访问权限仍然是私有(private)权限。

73. cout和printf有什么区别?

  • cout<<是一个函数,cout<<后可以跟不同的类型是因为cout<<已存在针对各种类型数据的重载,所以会自动识别数据的类型。
  • printf没有类型检查,不安全。cout是通过运算符重载实现的,安全。
  • printf是函数。coutostream对象,和<<配合使用。
  • cout有缓冲输出,输出过程会首先将输出字符放入缓冲区,然后输出到屏幕。 printf无缓冲输出,有输出时立即输出。

74. 重载运算符

  1. 我们只能重载已有的运算符,而无权发明新的运算符;对于一个重载的运算符,其优先级和结合律与内置类型一致才可以;
  2. 不能改变运算符操作数个数
  3. 两种重载方式,成员运算符和非成员运算符,成员运算符比非成员运算符少一个参数;下标运算符、箭头运算符必须是成员运算符;
  4. 引入运算符重载,是为了实现类的多态性
  5. 当重载的运算符是成员函数时,this绑定到左侧运算符对象。成员运算符函数的参数数量比运算符对象的数量少一个;至少含有一个类类型的参数;
  6. 下标运算符必须是成员函数,下标运算符通常以所访问元素的引用作为返回值,同时最好定义下标运算符的常量版本和非常量版本;
  7. 箭头运算符必须是类的成员,解引用通常也是类的成员;重载的箭头运算符必须返回类的指针;
  • 不能重载的运算符sizeof 运算符, :: 作用域运算符, ?:条件运算符, . 直接成员运算符, .*,->*:成员指针访问运算符

75. 定义和声明的区别

  1. 如果是指变量的声明和定义

    从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存。而定义就是分配了内存。

  2. 如果是指函数的声明和定义

    声明:一般在头文件里,对编译器说:这里我有一个函数叫function() 让编译器知道这个函数的存在。
    定义:一般在源文件里,具体就是函数的实现过程写明函数体。

76. C++类型转换

  • .html

C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

C++类型转换主要分为两种:隐式类型转换、显式类型转换(强制类型转换)。

所谓隐式类型转换,是指不需要用户干预,编译器默认进行的类型转换行为。C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。

强制类型转换操作符有四种:static_castconst_castreinterpret_castdynamic_cast

1. static_cast 静态类型转换

所谓的静态,即在编译期内即可决定其类型的转换,用的也是最多的一种。它主要有如下几种用法:

  • 用于类层次结构中父类和子类之间指针或引用的转换。进行向上转换(把子类的指针或引用转换成父类表示)是安全的;
  • 进行向下转换(把父类指针或引用转换成子类指针或引用)时,由于没有动态类型检查,所以是不安全的;
  • 用于基本数据类型之间的转换。这种转换的安全性也要开发人员来保证。
    	int val_int;double val_double = 3.1415;val_int = static<int>(val_double);
    
  • 把void指针转换成目标类型的指针不安全
  • 把任何类型的表达式转换成void类型。

2. const_cast

  • 专⻔⽤于 const 属性的转换,是四个转换符中唯⼀⼀个可以操作常量的转换符。

  • const_cast运算符用来去掉类型的const或volatile属性。但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。

  • 对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉const性质(cast away the const)”。一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。

    const char* ptr_char;
    char* p = const_cast<char*>(ptr_char); // 正确:但是通过p写值的行为是未定义的行为
    

3. reinterpret_cast 重新解释类型转换

  • 不到万不得已,不要使⽤这个转换符,⾼危操作。
  • 使⽤特点: 从底层对数据进⾏重新解释,依赖具体的平台,可移植性差; 可以将整形转 换为指针,也可以把指针转换为数组;可以在指针和引⽤之间进⾏肆⽆忌惮的转换。

4. dynamic_cast 子类与父类之间的多态类型准换

  • dynamic_cast 主要用在继承体系中的安全向下转型。它能安全地将指向基类的指针转型为指向子类的指针或引用,并获知转型动作成功是否。转型失败会返回null(转型对象为指针时)或抛出异常bad_cast(转型对象为引用时)。
  • 和static_cast不同,dynamic_cast涉及运行时的类型检查。如果向下转型是安全的(也就是说,如果基类指针或者引用确实指向一个派生类的对象),这个运算符会传回转型过的指针。如果向下转型不安全(即基类指针或者引用没有指向一个派生类的对象),这个运算符会传回空指针。

77. 全局变量和static变量的区别

  1. 全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同
  2. 这两者的区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个原文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。
  3. static全局变量与普通的全局变量的区别是static全局变量只初始化一次,防止在其他文件单元被引用。

78. static函数与普通函数有什么区别?

  • static函数与普通的函数作用域不同。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数应该在一个头文件中说明,使用这些函数的源文件要包含这个头文件。
    • static函数与普通函数最主要区别是:static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝。

79. 静态成员与普通成员的区别

  1. 生命周期
    静态成员变量从类被加载开始到类被卸载,一直存在;
    普通成员变量只有在类创建对象后才开始存在,对象结束,它的生命期结束;
  2. 共享方式
    静态成员变量是全类共享;普通成员变量是每个对象单独享用的;
  3. 定义位置
    普通成员变量存储在栈或堆中,而静态成员变量存储在静态全局区;
  4. 初始化位置
    普通成员变量在类中初始化;静态成员变量在类外初始化,且不添加static关键字
  5. 默认实参
    可以使用静态成员变量作为默认实参.

80. 多继承的优缺点,作为一个开发者怎么看待多继承

  1. C++允许为一个派生类指定多个基类,这样的继承结构被称做多重继承
  2. 多重继承的优点很明显,就是对象可以调用多个基类中的接口
  3. 如果派生类所继承的多个基类有相同的基类,而派生类对象需要调用这个祖先类的接口方法,就会容易出现二义性
    • 加上全局符确定调用哪一份拷贝。比如pa.Author::eat()调用属于Author的拷贝。
    • 使用虚拟继承,使得多重继承类Programmer_Author只拥有Person类的一份拷贝。

81. 迭代器++i,i++哪个好

  1. 前置返回一个引用,后置返回一个对象

    // ++i实现代码为:
    int& operator++()
    {*this += 1;return *this;
    } 
    
  2. 前置不会产生临时对象,后置必须产生临时对象,临时对象会导致效率降低

    //i++实现代码为:                                  
    int operator++(int)                                  
    {int temp = *this;                                     ++*this;                                             return temp;                                    
    } 
    

82. C++如何处理多个异常的?

  1. C++中的异常情况:
    语法错误(编译错误):比如变量未定义、括号不匹配、关键字拼写错误等等编译器在编译时能发现的错误,这类错误可以及时被编译器发现,而且可以及时知道出错的位置及原因,方便改正。
    运行时错误:比如数组下标越界、系统内存不足等等。这类错误不易被程序员发现,它能通过编译且能进入运行,但运行时会出错,导致程序崩溃。为了有效处理程序运行时错误,C++中引入异常处理机制来解决此问题。
  2. C++异常处理机制:
    异常处理基本思想:执行一个函数的过程中发现异常,可以不用在本函数内立即进行处理, 而是抛出该异常,让函数的调用者直接或间接处理这个问题。
    C++异常处理机制由3个模块组成:try(检查)、throw(抛出)、catch(捕获)
    抛出异常的语句格式为:throw 表达式;如果try块中程序段发现了异常则抛出异常。
    try 
    { 
    可能抛出异常的语句;(检查) 
    } 
    catch(类型名[形参名])//捕获特定类型的异常 
    { 
    //处理1; 
    } 
    catch(类型名[形参名])//捕获特定类型的异常 
    { 
    //处理2; 
    } 
    catch(…)//捕获所有类型的异常 
    { 
    } 
    

83. 在成员函数中调用delete this会出现什么问题?对象还可以使用吗?

  1. 在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码段中在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。
  2. 为什么是不可预期的问题
    delete this释放了类对象的内存空间,那么这段内存应该已经还给系统,不再属于这个进程。照这个逻辑来看,应该发生指针错误,无访问权限之类的令系统崩溃的问题才对啊?这个问题牵涉到操作系统的内存管理策略。delete this释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是缓冲或者其他什么原因,导致这段内存空间暂时并没有被系统收回。此时这段内存是可以访问的,你可以加上100,加上200,但是其中的值却是不确定的。当你获取数据成员,可能得到的是一串很长的未初始化的随机数;访问虚函数表,指针无效的可能性非常高,造成系统崩溃。

84. 如果在类的析构函数中调用delete this,会发生什么?

会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃

85. C与C++中union的区别

  • C与C++中的union:一种数据格式,能够存储不同的数据类型,但只能同时存储其中的一种类型。
  • C++ union结构是一种特殊的类。它能够包含访问权限、成员变量、成员函数(可以包含构造函数和析构函数)。它不能包含虚函数和静态数据变量。它也不能被用作其他类的基类,它本身也不能有从某个基类派生而来。Union中的默认访问权限是public。union类型是共享内存的,以size最大的结构作为自己的大小。每个数据成员在内存中的起始地址是相同的。

86. 动态联编与静态联编

  1. 在C++中,联编是指一个计算机程序的不同部分彼此关联的过程。按照联编所进行的阶段不同,可以分为静态联编动态联编
  2. 静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,又称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调用(如函数调用)与执行该操作代码间的关系,确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引用的类型。其优点是效率高,但灵活性差
  3. 动态联编是指联编在程序运行时动态地进行,根据当时的情况来确定调用哪个同名函数,实际上是在运行时虚函数的实现。这种联编又称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果。C++中一般情况下的联编是静态联编,但是当涉及到多态性和虚函数时应该使用动态联编动态联编的优点是灵活性强,但效率低。动态联编规定,只能通过指向基类的指针或基类对象的引用来调用虚函数,其格式为:指向基类的指针变量名->虚函数名(实参表)或基类对象的引用名.虚函数名(实参表)
  4. 实现动态联编三个条件
    • 必须把动态联编的行为定义为类的虚函数;
    • 类之间应满足子类型关系,通常表现为一个类从另一个类公有派生而来;
    • 必须先使用基类指针指向子类型的对象,然后直接或间接使用基类指针调用虚函数;

87. 动态编译与静态编译

  1. 静态编译,编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库;
  2. 动态编译的可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库的命令。所以其优点一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。缺点是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库;二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。

88. 动态链接和静态链接区别

1. 定义

  • 静态连接库就是把(lib)文件中用到的函数代码直接链接进目标程序,程序运行的时候不再需要其它的库文件;
  • 动态链接就是把调用的函数所在文件模块(DLL)和调用函数在文件中的位置等信息链接进目标程序,程序运行的时候再从DLL中寻找相应函数代码,因此需要相应DLL文件的支持。

2. 区别

  • 静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib 中的指令都全部被直接包含在最终生成的 EXE 文件中了。
  • 若使用 DLL,该 DLL 不必被包含在最终 EXE 文件中,EXE 文件执行时可以“动态”地引用和卸载这个与 EXE 独立的 DLL 文件。
  • 静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。
  • 动态库就是在需要调用其中的函数时,根据函数映射表找到该函数然后调入堆栈执行。
  • 如果在当前工程中有多处对dll文件中同一个函数的调用,那么执行时,这个函数只会留下一份拷贝。但是如果有多处对lib文件中同一个函数的调用,那么执行时,该函数将在当前程序的执行空间里留下多份拷贝,而且是一处调用就产生一份拷贝。

88. 静态绑定和动态绑定

  1. 对象的静态类型:对象在声明时采用的类型。是在编译期确定的。
  2. 对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。
  3. 静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期
  4. 动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期

89. 在不使用额外空间的情况下,交换两个数?

  1. 算术

    x = x + y;
    y = x - y;
    x = x - y; 
    
  2. 异或

    x = x^y;// 只能对int,char..
    y = x^y;
    x = x^y;
    x ^= y ^= x;
    

90. strcpy和memcpy的区别

  • 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
  • 复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
  • 用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy

strcpy函数源码(C语言)


char * strcpy(char * dest, const char * src) // 实现src到dest的复制
{if ((src == NULL) || (dest == NULL)) //判断参数src和dest的有效性{return NULL;}char *strdest = dest;        //保存目标字符串的首地址while ((*strDest++ = *strSrc++)!='\0'); //把src字符串的内容复制到dest下return strdest;
}

memcpy函数源码(C语言)

void *memcpy(void *memTo, const void *memFrom, size_t size)
{if((memTo == NULL) || (memFrom == NULL))     //memTo和memFrom必须有效return NULL;char *tempFrom = (char *)memFrom;             //保存memFrom首地址char *tempTo = (char *)memTo;                  //保存memTo首地址      while(size -- > 0)                //循环size次,复制memFrom的值到memTo中*tempTo++ = *tempFrom++ ;  return memTo;
}

91. 执行int main(int argc, char *argv[])时的内存结构

  • 参数的含义是程序在命令行下运行的时候,需要输入argc 个参数,每个参数是以char 类型输入的,依次存在数组里面,数组是 argv[],所有的参数在指针char * 指向的内存中,数组的中元素的个数为 argc 个,第一个参数为程序的名称。

92. volatile关键字

volatile关键字定义

  • volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

  • 声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。

volatile关键字的作用

  1. 保证变量写操作的可见性;
  2. 保证变量前后代码的执行顺序;

底层原理

  • 被volatile修饰的变量被修改时,会将修改后的变量直接写入主存中,并且将其他线程中该变量的缓存置为无效,从而让其它线程对该变量的引用直接从主存中获取数据,这样就保证了变量的可见性。
  • 但是volatile修饰的变量在自增时由于该操作分为读写两个步骤,所以当一个线程的读操作被阻塞时,另一个线程同时也进行了自增操作,此时由于第一个线程的写操作没有进行所以主存中仍旧是之前的原数据,所以当两个线程自增完成后,该变量可能只加了1。因而volatile是无法保证对变量的任何操作都是原子性的

一个参数可以既是const又是volatile吗

  • 可以,用const和volatile同时修饰变量,表示这个变量在程序内部是只读的,不能改变的,只在程序外部条件变化下改变,并且编译器不会优化这个变量。每次使用这个变量时,都要小心地去内存读取这个变量的值,而不是去寄存器读取它的备份。

  • 注意:在此一定要注意const的意思,const只是不允许程序中的代码改变某一变量,其在编译期发挥作用,它并没有实际地禁止某段内存的读写特性。

适用场景

  • 变量的修改不依赖于变量本身
    像是i++i+=这类的操作在多线程下都是不能保证变量的原子性的。
  • 该变量没有包含在具有其他变量的不变式中。

93. const char* 与string之间的关系?

  1. string 是c++标准库里面其中一个,封装了对字符串的操作,实际操作过程我们可以用const char*给string类初始化
  2. 三者的转化关系如下所示:
  • string转const char*

    string s = “abc”; 
    const char* c_s = s.c_str(); 
    
  • const char* 转string,直接赋值即可

    const char* c_s = “abc”; 
    string s(c_s); 
    
  • string 转char*

    string s = “abc”; 
    char* c; 
    const int len = s.length(); 
    c = new char[len+1]; 
    strcpy(c,s.c_str()); 
    
  • char* 转string

    char* c = “abc”; 
    string s(c); 
    
  • const char* 转char*

    const char* cpc = “abc”; 
    char* pc = new char[strlen(cpc)+1]; 
    strcpy(pc,cpc);
    
  • char* 转const char*,直接赋值即可

    char* pc = “abc”; 
    const char* cpc = pc;
    

94. 为什么拷贝构造函数必须传引用不能传值?

  • 拷贝构造函数的作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。

    class Base
    {
    public:Base(){}Base(const Base &b){..}//
    }
    
  • 拷贝构造函数用来初始化一个非引用类类型对象,如果用传值的方式进行传参数,那么构造实参需要调用拷贝构造函数,而拷贝构造函数需要传递实参,所以会一直递归,导致内存溢出。。

95. 空类的大小是多少?为什么?

  1. C++空类的大小不为0,不同编译器设置不一样,vs设置为1;
  2. C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址;
  3. 带有虚函数的C++类大小不为1,因为每一个对象会有一个vptr指向虚函数表,具体大小根据指针大小确定;
  4. C++中要求对于类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址。

96. 类对象/类的大小

  1. 类的非静态成员变量大小,静态成员不占据类的空间,成员函数也不占据类的空间大小;
  2. 内存对齐另外分配的空间大小,类内的数据也是需要进行内存对齐操作的;
  3. 虚函数的话,会在类对象插入vptr指针,加上指针大小;
  4. 当该类是某类的派生类,那么派生类继承的基类部分的数据成员也会存在在派生类中的空间中,也会对派生类进行扩展。
class A{}; sizeof(A) = 1; //空类在实例化时得到⼀个独⼀⽆⼆的地址,所以为 1.
class A{virtual Fun(){} }; sizeof(A) = 4(32bit)/8(64bit) //当 C++ 类中有虚函数的时候,会有⼀个指向虚函数表的指针(vptr)
class A{static int a; }; sizeof(A) = 1;
class A{int a; }; sizeof(A) = 4;
class A{static int a; int b; }; sizeof(A) = 4;

97. 设计一个类计算子类的个数

  1. 为类设计一个static静态变量count作为计数器;
  2. 类定义结束后初始化count;
  3. 在构造函数中对count进行+1;
  4. 设计拷贝构造函数,在进行拷贝构造函数中进行count +1,操作;
  5. 设计复制构造函数,在进行复制函数中对count+1操作;
  6. 在析构函数中对count进行-1;
class Test
{
public:Test() :_a(0){t_count++;}Test(Test& a){t_count++;}~Test(){t_count--;}static int GetCount()//静态成员函数{return t_count;}
private:int _a;static int t_count;//静态成员变量   声明
};int Test::t_count = 0;//静态成员变量在类外初始化,且不添加static关键字void testCount()
{Test t1, t2;cout << Test::GetCount() << endl;//cout << t_count << endl;
}int main()
{Test::GetCount();Test t1, t2;cout << Test::GetCount() << endl;//2//cout << t1.GetCount() << endl;//也可以通过对象访问testCount();//4cout << Test::GetCount() << endl;//2system("pause");return 0;
}

98. 虚函数的代价?

  1. 带有虚函数的类,每一个类会产生一个虚函数表,用来存储指向虚成员函数的指针,增大类;
  2. 带有虚函数的类的每一个对象,都会有一个指向虚表的指针,会增加对象的空间大小;

99. sizeof 和strlen 的区别

  1. strlen计算字符串的具体长度(只能是字符串),不包括字符串结束符。返回的是字符个数。
  2. sizeof计算声明后所占的内存数(字节大小),不是实际长度。
  3. sizeof是一个取字节运算符,而strlen是个函数
  4. sizeof的返回值=字符个数*字符所占的字节数,字符实际长度小于定义的长度,此时字符个数就等于定义的长度。若未给出定义的大小,分类讨论,对于字符串数组,字符大 小等于实际的字符个数+1;对于整型数组,字符个数为实际的字符个数。字符串每个字符占1个字节,整型数据每个字符占的字节数需根据系统的位数类确定,32位占4个字节。
  5. sizeof可以用类型做参数,strlen只能用char*做参数,且必须以‘\0’结尾,sizeof还可以用函数做参数;
  6. 数组做sizeof的参数不退化,传递给strlen就退化为指针;

100. 写一个比较大小的模板函数

#include<iostream>
using namespace std;
template<typename type1,typename type2>//函数模板
type1 Max(type1 a,type2 b)
{return a > b ? a : b;
}
void main()
{cout<<"Max = "<<Max(5.5,'a')<<endl;
}

101. c++怎么实现一个函数先于main函数运行

#include <iostream>//第⼀种:gcc扩展,标记这个函数应当在main函数之前执⾏。同样有⼀个__attribute((destructor)),标记函数应当在程序结束之前(main结束之后,或者调⽤了exit后)执⾏;
__attribute((constructor))
void before() {printf("before main 1\n");
}//第⼆种:全局 static 变量的初始化在程序初始阶段,先于 main 函数的执⾏
int test1(){printf("before main 2\n");return 1;
}
static int i = test1();// 第三种:利⽤ lambda 表达式
int a = []() {printf("before main 3\n");return 0;
}();int main()
{printf("main function\n");return 0;
}

输出:

before main 1
before main 2
before main 3
main function

102. 虚函数与纯虚函数的区别

  1. 纯虚函数只有定义没有实现,虚函数既有定义又有实现;
  2. 含有纯虚函数的类不能定义对象,含有虚函数的类能定义对象;

103. strcpy函数和strncpy函数的区别?哪个函数更安全?

  • 函数原型

    char* strcpy(char* strDest, const char* strSrc)
    char* strncpy(char* strDest, const char* strSrc, int pos)
    
  • strcpy函数: 如果参数 dest 所指的内存空间不够大,可能会造成缓冲溢出(buffer Overflow)的错误情况,在编写程序时请特别留意,或者用strncpy()来取代。

  • strncpy函数:用来复制源字符串的前n个字符,src 和 dest 所指的内存区域不能重叠,且 dest 必须有足够的空间放置n个字符。

104. 成员函数里memset(this,0,sizeof(*this))会发生什么

  • 有时候类里面定义了很多int,char,struct等c语言里的那些类型的变量,一个个在构造函数中将它们初始化为0太麻烦,所以直接就memset(this, 0, sizeof *this);,将整个对象的内存全部置为0。对于这种情形可以很好的工作,但是下面几种情形是不可以这么使用的:
    • 类含有虚函数表:这么做会破坏虚函数表,后续对虚函数的调用都将出现异常;
    • 类中含有C++类型的对象:例如,类中定义了一个list的对象,由于在构造函数体的代码执行之前就对list对象完成了初始化,假设list在它的构造函数里分配了内存,那么我们这么一做就破坏了list对象的内存。

105. 随机数的生成

#include<time.h>  
srand((unsigned)time(NULL)); 
cout<<(rand()%(b-a))+a;
  • 由于rand()的内部实现是用线性同余法做的,所以生成的并不是真正的随机数,而是在一定范围内可看为随机的伪随机数。
  • 种子写为srand(time(0))代表着获取系统时间,电脑右下角的时间,每一秒后系统时间的改变,数字序列的改变得到的数字不同,这才得带不同的数字,形成了真随机数,即使是真随机数,也是有规律可循。

106. C++中push_back和emplace_back的区别

  • 在 C++11 之后,vector 容器中添加了新的方法:emplace_back() ,和 push_back() 一样都是在容器末尾添加一个新的元素进去。

  • emplace_back() 函数在原理上比 push_back() 有了一定的改进,包括在内存优化方面和运行效率方面。内存优化主要体现在使用了就地构造(直接在容器内构造对象,不用拷贝一个复制品再使用)+强制类型转换的方法来实现,在运行效率方面,由于省去了拷贝构造过程,因此也有一定的提升。

  • emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。

107. 变量的声明和定义有什么区别

  • 变量的定义为变量分配地址和存储空间,变量的声明不分配地址。一个变量可以在多个地方声明,但是只在一个地方定义。加入extern修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。
  • 说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。

108. vector的底层原理

109. vector中的reserve和resize的区别

110. vector中的size和capacity的区别

  • size表示当前vector中有多少个元素(finish - start);
  • capacity函数则表示它已经分配的内存中可以容纳多少元素(end_of_storage - start);

111. vector中erase方法与algorithn中的remove方法区别

  • vector中erase方法真正删除了元素,迭代器不能访问
  • remove只是简单地将元素移到了容器的最后面,迭代器还是可以访问到。因为algorithm通过迭代器进行操作,不知道容器的内部结构,所以无法进行真正的删除

112. vector迭代器失效的情况

  • 当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。
  • 当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it);

113. 正确释放vector的内存(clear(), swap(), shrink_to_fit())

  • vec.clear():清空内容,但是不释放内存。
  • vector().swap(vec):清空内容,且释放内存,想得到一个全新的vector。
  • vec.shrink_to_fit():请求容器降低其capacity和size匹配。
  • vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。

114. list的底层原理

  • list的底层是一个双向链表,使用链表存储数据,并不会将它们存储到一整块连续的内存空间中。恰恰相反,各元素占用的存储空间(又称为节点)是独立的、分散的,它们之间的线性关系通过指针来维持,每次插入或删除一个元素,就配置或释放一个元素空间。
  • list不支持随机存取,如果需要大量的插入和删除,而不关心随即存取

115. 什么情况下用vector,什么情况下用list,什么情况下用deque

  • vector可以随机存储元素(即可以通过公式直接计算出元素地址,而不需要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。除非必要,我们尽可能选择使用vector而非deque,因为deque的迭代器比vector迭代器复杂很多。
  • list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。
  • 需要从首尾两端进行插入或删除操作的时候需要选择deque。

116. priority_queue的底层原理

  • priority_queue:优先队列,其底层是用堆来实现的。在优先队列中,队首元素一定是当前队列中优先级最高的那一个

117. map 、set、multiset、multimap的底层原理

map 、set、multiset、multimap的底层实现都是红 黑 树

红 黑 树 的 特 性

  • 每个结点或是红色或是黑色;
  • 根结点是黑色;
  • 每个叶结点是黑的;
  • 如果一个结点是红的,则它的两个儿子均是黑色;
  • 每个结点到其子孙结点的所有路径上包含相同数目的黑色结点

118. 为何map和set的插入删除效率比其他序列容器高

对于关联式容器来说,不需要做内存的拷贝和内存的移动,他们的存储方式是以节点的方式存储,只需要保留对父子节点的指针,所以实现插入和删除的时候只需要移动节点的指针。

119. 为何map和set每次Insert之后,以前保存的iterator不会失效?

不会失效,map和set的节点是malloc申请出来的,尽管insert了新的节点,内存并没有变,变得只是指针的指向。所以insert的时候迭代器并不会失效。但是对于序列式容器来说,当插入一个新的元素,有可能需要扩容,因为需要保证内存的连续存放,所以会引起迭代器的失效。

120. 当数据元素增多时(从10000到20000),map的set的查找速度会怎样变化?

RB-TREE用二分查找法,时间复杂度为logn,所以从10000增到20000时,查找次数从log10000=14次到log20000=15次,多了1次而已。

121. map 、set、multiset、multimap的特点

  • set和multiset会根据特定的排序准则自动将元素排序,set中元素不允许重复,multiset可以重复。
  • map和multimap将key和value组成的pair作为元素,根据key的排序准则自动将元素排序(因为红黑树也是二叉搜索树,所以map默认是按key排序的),map中元素的key不允许重复,multimap可以重复。
  • map和set的增删改查速度为都是logn,是比较高效的

122. 迭代器失效的问题

插入操作

  • 对于vector和string,如果容器内存被重新分配,iterators,pointers,references失效;如果没有重新分配,那么插入点之前的iterator有效,插入点之后的iterator失效;
  • 对于deque,如果插入点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,deque的迭代器失效,但reference和pointers有效;
  • 对于list和forward_list,所有的iterator,pointer和refercnce有效。

删除操作

  • 对于vector和string,删除点之前的iterators,pointers,references有效;off-the-end迭代器总是失效的;
  • 对于deque,如果删除点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,off-the-end失效,其他的iterators,pointers,references有效;
  • 对于list和forward_list,所有的iterator,pointer和refercnce有效。
  • 对于关联容器map来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为

123. STL线程不安全的情况

  • 在对同一个容器进行多线程的读写、写操作时;
  • 在每次调用容器的成员函数期间都要锁定该容器;
  • 在每个容器返回的迭代器(例如通过调用begin或end)的生存期之内都要锁定该容器;
  • 在每个在容器上调用的算法执行期间锁定该容器。

124. 介绍 C++ 所有的构造函数

类的对象被创建时,编译系统为对象分配内存空间,并⾃动调⽤构造函数,由构造函数完成成员的初始化⼯作。即构造函数的作⽤:初始化对象的数据成员。

  • ⽆参数构造函数: 即默认构造函数,如果没有明确写出⽆参数构造函数,编译器会⾃动⽣成默认的⽆参数构造函数,函数为空,什么也不做,如果不想使⽤⾃动⽣成的⽆参构造函数,必需要⾃⼰显示写出⼀个⽆参构造函数。
  • ⼀般构造函数: 也称重载构造函数,⼀般构造函数可以有各种参数形式,⼀个类可以有多个⼀般构造函数,前提是参数的个数或者类型不同,创建对象时根据传⼊参数不同调⽤不同的构造函数。
  • 拷⻉构造函数: 拷⻉构造函数的函数参数为对象本身的引⽤,⽤于根据⼀个已存在的对象复制出⼀个新的该类的对象,⼀般在函数中会将已存在的对象的数据成员的值⼀⼀复制到新创建的对象中。如果没有显示的写拷⻉构造函数,则系统会默认创建⼀个拷⻉构造函数,但当类中有指针成员时,最好不要使⽤编译器提供的默认的拷⻉构造函数,最好⾃⼰定义并且在函数中执⾏深拷⻉。
  • 类型转换构造函数: 根据⼀个指定类型的对象创建⼀个本类的对象,也可以算是⼀般构造函数的⼀种,这⾥提出来,是想说有的时候不允许默认转换的话,要记得将其声明为 explict 的,来阻⽌⼀些隐式转换的发⽣。
  • 赋值运算符的重载:注意,这个类似拷⻉构造函数,将=右边的本类对象的值复制给=左边的对象,它不属于构造函数,=左右两边的对象必需已经被创建。如果没有显示的写赋值运算符的重载,系统也会⽣成默认的赋值运算符,做⼀些基本的拷⻉⼯作。

125. 内存泄漏(泄露)的定义,如何检测与避免?

  • 定义:内存泄漏简单的说就是申请了⼀块内存空间,使⽤完毕后没有释放掉。
  • 它的⼀般表现⽅式是程序运⾏时间越⻓,占⽤内存越多,最终⽤尽全部内存,整个系统崩溃。由程序申请的⼀块内存,且没有任何⼀个指针指向它,那么这块内存就泄漏了。

126. c++学习之new int()和new int[]的区别

  • new int[] 是创建一个int型数组,数组大小是在[]中指定,例如:

    int * p = new int[3]; //申请一个动态整型数组,数组的长度为[]中的值
    

    没有赋初值,并定义一个整型指针p指向该地址空间开始处

  • new int()是创建一个int型数,并且用()括号中的数据进行初始化,例如:

    int *p = new int(10); // p指向一个值为10的int数。
    

    申请一个整型变量空间,赋初值为10,并定义一个整型指针p指向该地址空间

127. 手写字符串函数 strcat, strcpy, strncpy, memset, memcpy实现

//把 src 所指向的字符串复制到 dest,注意: dest定义的空间应该⽐src⼤。
char* strcpy(char *dest,const char *src) {char *ret = dest;assert(dest!=NULL);//优化点1:检查输⼊参数assert(src!=NULL);while(*src!='\0')*(dest++)=*(src++);*dest='\0';//优化点2:⼿动地将最后的'\0'补上return ret;
}
//考虑内存重叠的字符串拷⻉函数 优化的点
char* strcpy(char *dest,char *src) {char *ret = dest;assert(dest!=NULL);assert(src!=NULL);memmove(dest,src,strlen(src)+1);return ret;
}
//把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。
char* strcat(char *dest,const char *src) {//1. 将⽬的字符串的起始位置先保存,最后要返回它的头指针//2. 先找到dest的结束位置,再把src拷⻉到dest中,记得在最后要加上'\0'char *ret = dest;assert(dest!=NULL);assert(src!=NULL);while(*dest!='\0')dest++;while(*src!='\0')*(dest++)=*(src++);*dest='\0';return ret;
}//把 str1 所指向的字符串和 str2 所指向的字符串进⾏⽐较。
//该函数返回值如下:
//如果返回值 < 0,则表示 str1 ⼩于 str2。
//如果返回值 > 0,则表示 str1 ⼤于 str2。
//如果返回值 = 0,则表示 str1 等于 str2。
int strcmp(const char *s1,const char *s2) {assert(s1!=NULL);assert(s2!=NULL);while(*s1!='\0' && *s2!='\0') {if(*s1>*s2)return 1;else if(*s1<*s2)return -1;else {s1++,s2++;}}//当有⼀个字符串已经⾛到结尾if(*s1>*s2)return 1;else if(*s1<*s2)return -1;elsereturn 0;
}//在字符串 str1 中查找第⼀次出现字符串 str2 的位置,不包含终⽌符 '\0'。
char* strstr(char *str1,char *str2) {char* s = str1;assert(str1!='\0');assert(str2!='\0');if(*str2=='\0')return NULL;//若str2为空,则直接返回空while(*s!='\0') {//若不为空,则进⾏查询char* s1 = s;char* s2 = str2;while(*s1!='\0'&&*s2!='\0' && *s1==*s2)s1++,s2++;if(*s2=='\0')return str2;//若s2先结束if(*s2!='\0' && *s1=='\0')return NULL;//若s1先结束⽽s2还没结束,则返回空s++;}return NULL;
}
//模拟实现memcpy函数 从存储区 str2 复制 n 个字符到存储区 dst。
void* memcpy(void* dest, void* src, size_t num) {void* ret = dest ;size_t i = 0 ;assert(dest != NULL ) ;assert(src != NULL) ;for(i = 0; i<num; i++) {//因为void* 不能直接解引⽤,所以需要强转成char*再解引⽤//此处的void*实现了泛型编程*(char*) dest = *(char*) src ;dest = (char*)dest + 1 ;src = (char*) src + 1 ;}return ret ;
}//考虑内存重叠的memcpy函数 优化的点
void* memmove(void* dest, void* src, size_t num) {char* p1 = (char*)dest;char* p2 = (char*)src;if(p1<p2) {//p1低地址p2⾼地址for(size_t i=0; i!=num; ++i)*(p1++) = *(p2++);}else {//从后往前赋值p1+=num-1;p2+=num-1;for(size_t i=0; i!=num; ++i)*(p1--) = *(p2--);}return dest;
}

128. 讲讲大端小端

  • 大端模式:是指数据的高字节保存在内存的低地址中,⽽数据的低字节保存在内存的高地址端。
  • 小端模式,是指数据的高字节保存在内存的高地址中,低位字节保存在在内存的低地址端。

直接读取存放在内存中的⼗六进制数值,取低位进⾏值判断

int a = 0x12345678;
int *c = &a;
c[0] == 0x12 ⼤端模式
c[0] == 0x78 ⼩段模式

⽤共同体来进⾏判断

  • union 共同体所有数据成员是共享⼀段内存的,后写⼊的成员数据将覆盖之前的成员数据,成员数据都有相同的⾸地址。 Union 的⼤⼩为最⼤数据成员的⼤⼩。
  • union 的成员数据共⽤内存,并且⾸地址都是低地址⾸字节。 Int i= 1时:⼤端存储1放在最高位,⼩端存储1放在最低位。当读取char ch时,是最低地址⾸字节,⼤⼩端会显示不同的值。
#include <stdio.h>
int main() {union {int a; //4 byteschar b; //1 byte
} data;data.a = 1; //占4 bytes,⼗六进制可表示为 0x 00 00 00 01
//b因为是char型只占1Byte, a因为是int型占4Byte
//所以,在联合体data所占内存中, b所占内存等于a所占内存的低地址部分
if(1 == data.b) {//⾛到这⾥意味着说明a的低字节,被取给到了b//即a的低字节存在了联合体所占内存的(起始)低地址,符合⼩端模式特征printf("Little_Endian\n");
} else {printf("Big_Endian\n");
}
return 0;
}

129. 回调函数

  • 就是⼀个通过函数指针调⽤的函数。如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤为调⽤它所指向的函数时,我们就说这是回调函数;
  • 因为可以把调⽤者与被调⽤者分开。调⽤者不关⼼谁是被调⽤者,它需知道的只是存在⼀个具有某种特定原型、某些限制条件(如返回值为int)的被调⽤函数。

更多推荐

【C++八股】全网最全的C++语言基础八股准备(持续更新)

本文发布于:2024-03-06 15:28:09,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1715678.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:最全   语言   基础

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!