1、C++基础语法

static关键字

  首先,无论是全局静态变量还是局部静态变量,只要是初始化了的就在.data区,未初始化的就在.bss区。使用static的场景可以分为以下5类:
(1)全局静态变量:全局变量前面加一个static即全局静态变量,作用域为其所在的文件,即它对其它文件是不可见的。
(2)局部静态变量:在函数体中声明的静态变量,其虽然不会随函数的结束而销毁,但是其作用域和函数体中其它变量一样,在函数外是不可见的。当下一次调用同样的函数时,其值会保持和上次一样。
(3)静态函数:在函数名前面加一个static即为静态函数,作用域为其所在的文件。
(4)类中的静态成员:类中的静态成员需要在类外初始化,它不属于具体的一个该类对象,而是属于整个类,所以使用它也不需要this指针。
(5)类中的静态函数:类中的静态函数也是属于整个类的,调用它也不需要传入this指针,所以函数体中也不能使用类的非静态成员。

const在内存哪里

  被const变量修饰的是常量,不可修改,其约束的是编译器的行为,所以它在内存中的布局和普通变量一样在普通的变量区。

int * const 和const int *

  总的来说的就是const在前面还是后面,当const在前面的时候,const修饰的是解引用之后的内容,即指针指向的对象是const的;当const在*后面的时候,const修饰的是指针本身,即指针地址是不可修改的。

成员函数后面加const

  在成员函数后面加const是防止该成员函数体内对成员对象进行了修改。这与没有加const的函数属于重载,目的是让const对象调用const方法,非const对象调用没加const的方法。

extern

  extern关键字表示其修饰的变量或函数的定义在其它源文件中。
  extern “C”表示接下来的内容按C的规则进行编译。比如C++编译器通过修改所有函数的名字实现函数重载机制,C则是按函数原本的名字进行编译的。

restrict

  restrict,C语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。

C++的四种类型转换

  (1)reinterpret_cast:这个和C中的强转一样,没有任何限制。
  (2)static_cast:用于各种隐式转换,用于void*与其它类型指针之间的相互转化,可用于多态父子类之间的转化但结果未知。
  (3)dynamic_cast:用于父类指针和子类指针,父类引用和子类引用之间的转化,若目标指针并非父类的子类,则会返回空,它是通过运行时类型推断实现的。
  (4)const_cast:用于将const转为非const类型。

讲讲隐式转换

  没有显示类型转换的转换都是隐式转换,如内置类型低精度的变量赋值给高精度的变量会发生隐式转换,如传给一个类的构造函数的变量若不是该类构造函数规定的参数类型的话也可能发生隐式转换。

new/delete和malloc/free的区别

  new/delete是C++ 关键字,需要C++编译器支持。malloc/free是C标准库函数,需要包含相应的头文件。
  new实际分为两步,第一步分配内存,第二步调用类的构造函数;delete同理,第一步调用类的析构函数,第二步释放内存。这里分配和释放内存底层很多时候是用malloc/free实现的。而malloc/free只是分配和释放内存。
  new返回的是对象的指针。而malloc返回的是void型指针。
  new失败后会抛出bad_alloc异常,并且可以使用set_new_handler设置new失败时会调用的回调函数。而malloc失败后会返回空指针。

class和struct区别

  class的默认访问权限和默认继承权限都是private,struct的默认访问权限和默认继承权限都是public。

C++类中可以定义引用数据成员吗

  可以,必须使用初始化列表初始化。

为什么用成员初始化列表会快一些

  对于内置类型而言,效率没有区别;
  对于用户定义的类而言,因为进入构造函数体后,所有对象都需要已经构造好,所以在之前会调用一次默认构造函数,然后在构造函数体内再调用一次特定构造函数,这就会造成多调用了一次构造函数,损失效率。

2、C++思想

C++和C的区别

  从设计思想上来说,C++ 是面向对象的语言,C是面向过程的语言。从语法上来说,C++具有面向对象的封装,继承,多态特性;增加了许多类型安全机制,如四种类型强制转换;支持泛型编程,如模板类,函数模板。

讲一讲面向对象

  通俗来说,面向过程就是一件事该怎么做,面向对象就是什么事交给谁做,举个例子,面向过程编程就是造一台电脑,面向对象编程就是组装一台电脑。面向对象有三个特性,封装,继承和多态。封装就是将数据和对这些数据的操作集合在一起,并只对外暴露一些接口;继承可以让另一个类获得某一个类的属性和方法;多态则可以让同一个操作在不同的对象上表现出不同的效果。

面向对象三大特征

  封装:封装就是将数据和对这些数据的操作集合在一起,将实现与对外只暴露一些接口。
  继承:可以让某一个类获得另一个类的属性和方法,增加了代码的可重用性。
  多态:同一个操作在不同对象上表现出不同的效果,使用虚函数实现。在C++中就是用虚函数实现的多态。虚函数调用是一种只知道部分信息完成工作的机制,用户只需知道接口而无需知道具体对象,其实现依赖的是虚函数表,有虚函数的类中会有一个指向虚函数表的指针,当一个派生类自己实现了父亲的虚函数后,会将该函数在继承到的虚函数表中的地址改写为其重写的虚函数的地址。

3、指针

指针和引用区别

  指针是对象内存的地址,引用是对象的别名。

指针和数组的区别

  指针是某一对象在内存中的地址,数组是相同类型数据的集合,数组名指向集合的首地址。

野指针

  也叫悬挂指针,指向的是已经被释放的内存空间。

函数指针

  C在编译的时候所有函数都有一个入口地址,函数指针指向的则是某一个函数的入口地址。可用于调用函数或作为函数的参数传递。

虚表指针何时初始化

  在构造函数中初始化。

4、C++11

四种智能指针

  auto_ptr:C++98标准的方案,已经弃用。
  unique_ptr:独占指针,不允许将一个unique_ptr的左值对象赋给另一个unique_ptr对象,但是右值可以。
  shared_ptr:共享指针,多个shared_ptr对象可以指向同一对象,其中一个shared_ptr对象销毁时并不会销毁其指向对象,而是将引用计数-1,当引用计数为0时,才真正销毁所指向的对象。
  weak_ptr:这是一种不控制对象生命周期的智能指针,其指向一个shared_ptr管理的对象,但是它的构造和析构不会使shared_ptr的引用计数增减。设计这种智能指针的目是为了解决两个类中使用shared_ptr交叉引用造成的内存泄漏的问题。

std::weak_ptr::lock存在的意义是什么

  获得一个可用的shared_ptr对象,因为weak_ptr是一个观测者的身份,并没有重载*和->,如果引用计数为0则返回一个空的shared_ptr对象。

右值引用和左值

  右值一般指表达式结束就会消亡的对象,如临时变量。左值一般指表达式结束还会存在的变量。
  右值无法取址而左值可以;右值无法赋值而左值可以。
C++11引入右值引用主要是为了转移语义,消除两个对象交互时的不必要拷贝。

C++11新特性

  常用的:auto关键字,nullptr关键字,智能指针,右值引用,多线程,lambda表达式,可变长模板。

Lambda表达式如何实现的

  编译器通过创建匿名类并重载()操作符实现,捕获列表则通过私有成员实现,将捕获列表参数作为私有成员保存。

原子变量fetch_add接口

  原子变量的fetch_add接口是利用CAS实现的,CAS是一项乐观锁技术。乐观锁是指认为数据访问不会冲突,只有在更新数据的时候才去检查冲突。CAS底层则是汇编代码cmpxchg。

std::move和std::forward

  首先是引用折叠,对于左值T(A&),不管T后面加多少个&都会被折叠成左值,对于右值T(A&&),T后面加奇数个&会被折叠成左值,加偶数个&&会被折叠成右值。
  因此,move作用是将左值转成右值,它首先使用remove_reference利用偏特化获得去除&后的原始类型,然后用static_cast转换成右值。
  而forward用作完美转发,直接static_cast<T&&>即可,左值T(A&)折叠成左值,右值T(A&&)折叠成右值。

5、函数

谈谈多态和虚函数

  多态就是同一个操作作用于不同的对象可以产生不同的效果,在C++中就是用虚函数实现的多态。虚函数调用是一种只知道部分信息完成工作的机制,用户只需知道接口而无需知道具体对象,其实现依赖的是虚函数表,有虚函数的类中会有一个指向虚函数表的指针,当一个派生类自己实现了父亲的虚函数后,会将该函数在继承到的虚函数表中的地址改写为其重写的虚函数的地址。

怎么用C实现虚函数

  仿照linux中虚拟文件系统的做法,结构体中有一个op对象,里面都是各种函数指针,不同的文件系统实现不同的函数,然后赋值给该函数指针。

析构函数为何必须是虚函数?C++默认的为何又不是虚函数呢?

  delete父类指针时必须要调用子类的析构函数。
  若默认是虚函数则每个类都需要引入虚函数表,没有必要。

静态函数与虚函数的区别

  静态成员函数属于一个类而不属于任何一个实例。虚函数是用于实现多态的,指向派生类的父类指针可以调用派生类的虚函数。

重载和重写和隐藏

  重载:函数名和返回值相同,参数列表不同。
  重写:不同层级的类中实现的不同的虚函数。
  隐藏:不同层级的类中实现的同名函数。

讲讲析构函数

  当一个对象生命期结束的时候会调用它的析构函数,析构函数的调用顺序为:派生类的析构函数->成员对象的析构函数->父类的析构函数。

析构函数可以是纯虚函数吗

  可以。

在main函数之前执行的函数

  静态成员的构造函数。

C语言如何进行函数调用

  将函数参数压栈;
  将函数返回值压栈;
  将栈帧指针(ebp寄存器值)压栈;
  移动栈顶和栈帧指针,为函数创建一个新的栈帧。

C参数压栈顺序

  从右往左

为什么从右往左压栈

  对于可变长参数,从左往右压栈编译器难以确定明确参数的地址,而从右往左,则最左的参数地址就是栈帧指针加上一个返回值的长度。

C++如何处理返回值

  生成一个临时对象返回。不过现代编译器大多有RVO机制,直接在函数返回处构造对象;

拷贝构造函数什么时候被调用

  三种情况:
  用一个类对象初始化另一个类对象时;
  函数参数值传递时;
  返回值为类对象时;

C++中拷贝构造函数的形参能否进行值传递

  不能,因为使用形参会调用拷贝构造函数,造成循环调用。

获得某一成员偏移量的方法

#define  offsetof(TYPE,MEMBER) ((size_t)&((TYPE*)0) ->MEMBER)

dowhile(0)的目的

  让代码不受分号,大括号的影响。

6、C++模型

  菱形继承
1.png
  虚继承:
2.png

虚函数表如何实现多态

  子类继承父类之后,如果重新实现了虚函数,会替换掉虚函数表中的相应的函数指针,从而实现多态。

memory_order

{
memory_order_relaxed, //不对执行顺序做保证
memory_order_acquire, //本线程中,所有后续的读操作必须在本条原子操作完成后执行
memory_order_release, // 本线程中,所有之前的写操作完成后才能执行本条原子操作
memory_order_acq_rel, //同时包含 memory_order_acquire 和 memory_order_release
memory_order_consume,// 本线程中,所有后续的有关本原子类型的操作,必须在本条原子操作完成之后执行
memory_order_seq_cst // 全部存取都按顺序执行
} memory_order;

7、STL

STL由什么组成

  容器,迭代器,分配器,算法,仿函数,配接器六个部分。

vector和list区别

  都是线性容器,但是vector是顺序表,list是链表。vector支持随机访问,list只支持顺序访问。vector插入数据的时间复杂度为O(n),list插入数据的时间复杂度为O(1)。

map和set区别?分别如何实现?

  区别:map是键值对容器;set是数据集合。map的迭代器key是不可修改的,value可修改;set的迭代器是不可修改的。
  实现:底层都是使用红黑树实现的,set的key就是value。因为都是使用红黑树实现的,所以数据都是有序的,也所以set的迭代器是const型的,map的迭代器的key是const的,因为修改key值会影响红黑树的有序性。

map的key如果是结构体需要注意什么问题

  重载<

有上千万条数据的map如何释放内存

  因为小于128Byte的数据内存是用二级配置器申请的,使用完后会放回内存池,并没有真正释放,所以需要和临时空map对象swap后销毁临时map对象。

介绍一下STL的allocator

  是STL中广泛使用的对象配置器,提高了对象创建和销毁的效率。
  在C++中,new实际分为两步,第一步分配内存,第二步调用构造函数,delete也分为两步,第一步调用析构函数,第二步释放内存。STL为了分工精细,使用配置器分配内存,使用construct函数和destroy函数处理对象的构造和析构。配置器分为两层,当所需内存大于128Byte时使用第一层配置器,使用标准库中的malloc,realloc,free等函数;小于等于128Byte时使用第二级配置器,其使用了内存池技术,以8Byte为单位分为了16种大小的槽位,每个槽位管理一个空闲链表,当空闲链表中没有空闲内存块时则从内存池中取一定数量的内存块放到空闲链表中。这里的空闲链表并没有多占用一点内存,其将next指针和内存块的头数据放在一个union中,避免了内存浪费。

说一下STL迭代器删除元素

  STL使用erase删除迭代器内容。对于序列容器来说,erase返回所删除的迭代器的下一迭代器,但删除迭代器元素会导致后面迭代器均失效。对于关联容器来说,erase会导致被删除的迭代器失效,但是其它迭代器内容不失效。对于list来说,erase会返回迭代器的下一迭代器,且其它迭代器均不是失效。

迭代器作用

  迭代器的主要目的是将容器和算法分离,迭代器让容器的数据访问和遍历操作一致,这样算法使用迭代器对数据进行操作和遍历,就不需要关心具体的容器类型,适用性也就更强。

resize和reserve

  resize改变的是size值,变小则后面的元素舍弃掉,变大则后面的元素默认初始化;
  reserve改变的是容量capacity,不增加元素。

8、编译

C++源文件到可执行文件的过程

C++源文件—>(预处理阶段)—>宏展开后的源文件—>(编译阶段)—>汇编文件—>(汇编阶段)—>可重定位的二进制文件—>(链接阶段)—>可执行文件

include头文件使用双引用号””和尖括号<>有啥区别

  预处理阶段寻找头文件的目录不同。
  使用双引号会先从当前目录寻找头文件,然后从编译器指定目录寻找头文件,最后从标准库目录下寻找头文件。
  使用尖括号则只会从编译器指定目录和标准库目录下寻找头文件。
标准库目录一般是usr/include。

g++和gcc区别

  (1).c文件被gcc当做C文件编译,g++ 当做C++ 文件编译;.cpp文件都会被当做C++ 文件编译。
  (2)g++ 在编译阶段调用的是gcc,链接则由自己完成。
  (3)如果后缀是.c,使用gcc编译器,才不会定义__cplusplus宏。
  (4)extern “C”与gcc和g++ 无关。

#9 5、标准库

malloc原理

  大于128k时候从mmap区申请内存,free直接归还内存。
  申请小于128k的时候首先会从空闲链表中寻找合适的内存块,glibc的malloc管理三组链表,fast bins,unsorted bins和bins,fast bins存放的是小内存块,一般是小于64Byte的,如果fast bins中没有的话就去unsorted bins中找,还没有则在bins中找,再没有就使用brk系统调用从堆区中申请内存。