C++ 总结

文章目录

  • C++ 总结
    • 引用和指针的区别?
    • C++中的指针参数传递和引用参数传递
    • 形参与实参的区别?
    • 值传递、引用传递、指针传递。
    • static的用法和作用?
    • 静态变量什么时候初始化
    • const?
    • 指针和const的用法
    • mutable
    • extern用法?
    • 深拷贝与浅拷贝?(注意指针的情况)
    • C++模板是什么,底层怎么实现的?
    • C语言struct和C++struct区别
    • 虚函数可以声明为inline吗?
    • 类成员初始化方式?为什么用成员初始化列表会快一些?
    • 成员列表初始化。
    • 构造函数为什么不能为虚函数?
    • 析构函数为什么要虚函数
    • 析构函数的作用,如何起作用?
    • 构造函数和析构函数可以调用虚函数吗,为什么
    • 构造函数的执行顺序?析构函数的执行顺序?构造函数内部干了啥?拷贝构造干了啥?
    • 构造函数析构函数可否抛出异常
    • 类如何实现只能静态分配和只能动态分配
    • 如果想将某个类用作基类,为什么该类必须定义而非声明?
    • 什么情况会自动生成默认构造函数?
    • 什么是类的继承?
    • 什么是组合?
    • 抽象基类为什么不能创建对象?
    • 纯虚函数?
    • 虚函数
    • 抽象类
    • 类什么时候会析构?
    • 为什么友元函数必须在类内部声明?
    • 介绍一下C++里面的多态?
    • 用C语言实现C++的继承
    • 继承机制中对象之间如何转换?指针和引用之间如何转换?
    • 组合与继承优缺点?
    • 左值右值
    • 移动构造函数
    • C语言的编译链接过程?
    • vector与list的区别与应用?怎么找某vector或者list的倒数第二个元素
    • STL vector的实现,删除其中的元素,迭代器如何变化?为什么是两倍扩容?释放空间?
    • 容器内部删除一个元素
    • STL迭代器如何实现
    • set与hash\_set的区别
    • hashmap与map的区别
    • map、set是怎么实现的,红黑树是怎么能够同时实现这两种容器?为什么使用红黑树?
    • 如何在共享内存上使用stl标准库?
    • map插入方式有几种?
    • STL中unordered\_map(hash\_map)和map的区别,hash\_map如何解决冲突以及扩容
    • vector越界访问下标,map越界访问下标?vector删除元素时会不会释放空间?
    • map\[\]与find的区别?
    • STL中list与queue之间的区别
    • STL中的allocator,deallocator
    • STL中hash\_map扩容发生什么?
    • 各容器的底层数据结构?
    • 函数指针?
    • 说说你对c和c++的看法,c和c++的区别?
    • c/c++的内存分配,详细说一下栈、堆、静态存储区?
    • 堆与栈的区别?
    • 野指针是什么?如何检测内存泄漏?
    • 悬空指针和野指针有什么区别?
    • 内存泄漏
    • new和malloc的区别?
    • delete p与delete\[\]p,allocator
    • new和delete的实现原理,delete是如何知道释放内存的大小的额?
    • malloc申请的存储空间能用delete释放吗
    • malloc与free的实现原理?
    • malloc、realloc、calloc的区别
    • \_\_stdcall和\_\_cdecl的区别?
    • 使用智能指针管理内存资源,RAII
    • 手写实现智能指针类
    • 内存对齐?位域?
    • 结构体变量比较是否相等
    • 为什么内存对齐
    • 函数调用过程栈的变化,返回值和参数变量哪个先入栈?
    • 怎样判断两个浮点数是否相等?
    • 宏定义一个取两个数中较大值的功能
    • define、const、typedef、inline使用方法?
    • \#include的顺序以及尖叫括号和双引号的区别
    • lambda函数
    • **lambda语法分析**
    • hello world程序开始到打印到屏幕上的全过程?
    • 类模板和函数模板的区别是什么?
    • 为什么模板类一般都是放在一个.h文件中
    • C++中类成员的访问权限和继承权限问题。
    • cout和printf有什么区别?
    • 重载运算符
    • 函数重载函数匹配原则
    • 定义和声明的区别
    • C++类型转换有四种
    • 说一下理解ifdef endif
    • 隐式转换,如何消除隐式转换?
    • 虚函数的内存结构,那菱形继承的虚函数内存结构?
    • 多继承的优缺点,作为一个开发者怎么看待多继承
    • 迭代器++it,it++哪个好,为什么
    • C++如何处理多个异常的?
    • 模板和实现可不可以不写在一个文件里面?为什么?
    • 在成员函数中调用delete this会出现什么问题?对象还可以使用吗?
    • 智能指针的作用;
    • class、union、struct的区别
    • 动态联编与静态联编
    • 动态编译与静态编译
    • 动态链接和静态链接区别
    • 在不使用额外空间的情况下,交换两个数?
    • strcpy和memcpy的区别
    • 执行int main(int argc, char \*argv\[\])时的内存结构
    • volatile关键字的作用?
    • 讲讲大端小端,如何检测(三种方法)
    • 空类会默认添加哪些东西?怎么写?
    • 标准库是什么?
    • const char\*与string之间的关系,传递参数问题?
    • new、delete、operator new、operator delete、placement new、placement delete
    • 为什么拷贝构造函数必须传引用不能传值?
    • 值传递和引用传递
    • 空类的大小是多少?为什么?
    • 你什么情况用指针当参数,什么时候用引用,为什么?
    • 大内存申请时候选用哪种?C++变量存在哪?变量的大小存在哪?符号表存在哪?
    • 为什么会有大端小端,htol这一类函数的作用
    • 静态函数能定义为虚函数吗?常函数?
    • this指针调用成员变量时,堆栈会发生什么变化?
    • 设计一个类计算子类的个数
    • 虚函数的代价?
    • 类对象的大小
    • 移动构造函数
    • 何时需要合成构造函数
    • 何时需要合成拷贝构造函数
    • 何时需要成员初始化列表?过程是什么?
    • 程序员定义的析构函数被扩展的过程?
    • 构造函数的执行算法?
    • 构造函数的扩展过程?
    • 哪些函数不能是虚函数
    • sizeof和strlen的区别
    • 简述strcpy、sprintf与memcpy的区别
    • 局部变量全局变量的问题?
    • 数组和指针的区别?
    • C++如何阻止一个类被实例化?一般在什么时候将构造函数声明为private?
    • 如何禁止自动生成拷贝构造函数?
    • assert与NDEBUGE
    • denug和release的区别
    • main函数有没有返回值
    • c++怎么实现一个函数先于main函数运行
    • 虚函数与纯虚函数的区别在于
    • 智能指针怎么用?智能指针出现循环引用怎么解决?
    • strcpy函数和strncpy函数的区别?哪个函数更安全?
    • 为什么要用static\_cast转换而不用c语言中的转换?
    • 成员函数里memset(this,0,sizeof(\*this))会发生什么
    • 方法调用的原理(栈,汇编)
    • 回调函数的作用
    • C++11新特性
    • inline函数
    • 方法调用的原理(栈,汇编)
    • 回调函数的作用
    • C++11新特性
    • inline函数

引用和指针的区别?

(1) 指针是一个实体,需要分配内存空间。引用只是变量的别名,不需要分配内存空间。

(2) 引用在定义的时候必须进行初始化,并且不能够改变。指针在定义的时候不一定要初始化,并且指向的空间可变。(注:不能有引用的值不能为NULL)

(3) 有多级指针,但是没有多级引用,只能有一级引用。

(4) 指针和引用的自增运算结果不一样。(指针是指向下一个地址,引用是引用的变量值加1)。

(5) sizeof引用得到的是所指向的变量(对象)的大小,而sizeof指针得到的是指针本身的大小。

(6) 引用访问一个变量是直接访问,而指针访问一个变量是间接访问。

(7) 使用指针前最好做类型检查,防止野指针的出现;

(野指针:指针变量的值未初始化;指向的地址空间已经被free/delete;超出作用域)

(8) 引用底层是通过指针实现的;

(9) 作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引用的实质是传地址,传递的是变量的地址。

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

(1) **指针参数传递本质上是值传递,它所传递的是一个地址值。**值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。

(2) 引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。**被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。**因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。

(3) 引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。

(4) 从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

形参与实参的区别?

(1) 形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只有在函数内部有效。函数调用结束返回主调函数后则不能再使用该形参变量。

(2) 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。因此应预先用赋值,输入等办法使实参获得确定值,会产生一个临时变量。

(3) 实参和形参在数量上,类型上,顺序上应严格一致,否则会发生"类型不匹配"的错误。

(4) 函数调用中发生的数据传送是单向的。即只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化。

(5) 当形参和实参不是指针类型时,在该函数运行时,形参和实参是不同的变量,他们在内存中位于不同的位置,形参将实参的内容复制一份,在该函数运行结束的时候形参被释放,而实参内容不会改变。

值传递、引用传递、指针传递。

(1) 值传递:有一个形参向函数所属的栈拷贝数据的过程,如果值传递的对象是类对象或是大的结构体对象,将耗费一定的时间和空间。(传值)

(2) 指针传递:同样有一个形参向函数所属的栈拷贝数据的过程,但拷贝的数据是一个固定为4字节的地址。(传值,传递的是地址值)

(3) 引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于为该数据所在的地址起了一个别名。(传地址)

总结:效率上讲,指针传递和引用传递比值传递效率高。一般主张使用引用传递,代码逻辑上更加紧凑、清晰。

static的用法和作用?

C++的static有两种用法:面向过程程序设计中的static和面向对象程序设计中的static。前者应用于普通变量和函数,不涉及类;后者主要说明static在类中的作用。

1. 面向过程设计中的static

(1)静态全局变量

特点:

  • 该变量在全局数据区分配内存
  • 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的值是随机的,除非它被显式初始化);
  • 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;

好处:

  • 静态全局变量不能被其它文件所用;
  • 其它文件中可以定义相同名字的变量,不会发生冲突;

(2)静态局部变量

定义:

​ 在局部变量前,加上关键字static,该变量就被定义成为一个静态局部变量。通常,在函数体内定义了一个变量,每当程序运行到该语句时都会给该局部变量分配栈内存。但随着程序退出函数体,系统就会收回栈内存,局部变量也相应失效。但有时候我们需要在两次调用之间对变量的值进行保存。通常的想法是定义一个全局变量来实现。但这样一来,变量已经不再属于函数本身了,不再仅受函数的控制,给程序的维护带来不便。静态局部变量正好可以解决这个问题。静态局部变量保存在全局数据区,而不是保存在栈中,每次的值保持到下一次调用,直到下次赋新值。

静态局部变量有以下特点:

  • 该变量在全局数据区分配内存;
  • 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
  • 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
  • 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;

(3)静态函数

特点:

在函数的返回类型前加上static关键字,函数即被定义为静态函数。静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。

2. 面向对象的static关键字(类中的static关键字)

(1)静态数据成员

静态数据成员有以下特点:

  • 对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。也就是说,**静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。**所以,静态数据成员的值对每个对象都是一样的,它的值可以更新;

  • 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。

  • 静态数据成员和普通数据成员一样遵从public, protected, private访问规则;

  • 因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它;

  • 静态数据成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式为:

    <数据类型><类名>::<静态数据成员名>=<值>

  • 类的静态数据成员有两种访问形式:

    <类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>

  • 如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员;

  • 静态数据成员主要用在各个对象都有相同的某项属性的时候。

同全局变量相比,使用静态数据成员有两个优势:

  • 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;
  • 以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能;

(2)静态成员函数

与静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务。静态成员函数与静态数据成员一样,都是类的内部实现,属于类定义的一部分。普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。通常情况下,this是缺省的。如函数fun()实际上是this->fun()。与普通函数相比,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针。从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。

关于静态成员函数,可以总结为以下几点:

  • 出现在类体外的函数定义不能指定关键字static;

  • 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;

  • 非静态成员函数可以任意地访问静态成员函数和静态数据成员;

  • 静态成员函数不能访问非静态成员函数和非静态数据成员;

  • 由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长;

  • 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以直接使用如下格式调用类的静态成员函数:

    <类名>::<静态成员函数名>(<参数表>)

静态变量什么时候初始化

(1) 初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存。

(2) 静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但在C和C++中静态局部变量的初始化节点又有点不太一样。在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全部回收。

(3) 而在C++中,初始化时在执行相关代码时才会进行初始化,主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以C++标准定为全局或静态对象是有首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的。

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_cast将const类型转换为非const类型;

(10) const类型变量必须定义的时候进行初始化,因此也导致如果类的成员变量有const类型的变量,那么该变量必须在类的初始化列表中进行初始化;

(11) 对于函数值传递的情况,因为参数传递是通过复制实参创建一个临时变量传递进函数的,函数内只能改变临时变量,但无法改变实参。则这个时候无论加不加const对实参不会产生任何影响。但是在引用或指针传递函数调用中,因为传进去的是一个引用或指针,这样函数内部可以改变引用或指针所指向的变量,这时const才是实实在在地保护了实参所指向的变量。因为在编译阶段编译器对调用函数的选择是根据实参进行的,所以,只有引用传递和指针传递可以用是否加const来重载。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。

指针和const的用法

当const修饰指针时,由于const的位置不同,它的修饰对象会有所不同。

(const出现在*左边,表示被指物是常量;如果出现在右边,表示指针自身是常量;如果出现在两边,表示被指物和指针都是常量)

mutable

(1) 如果需要在const成员方法中修改一个成员变量的值,那么需要将这个成员变量修饰为mutable。即用mutable修饰的成员变量不受const成员方法的限制;

(2) 可以认为mutable的变量是类的辅助状态,但是只是起到类的一些方面表述的功能,修改他的内容我们可以认为对象的状态本身并没有改变的。实际上由于const_cast的存在,这个概念很多时候用处不是很多了。

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程序编译完成后在目标代码中命名规则不同。

深拷贝与浅拷贝?(注意指针的情况)

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NrJNMD5E-1645797543829)(C:\Users\xgc\Desktop\1.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gc3vXHh8-1645797543831)(C:\Users\xgc\Desktop\2.jpg)]

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

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

(1) 编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。

(2) 这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误。

C语言struct和C++struct区别

(1) C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)。

(2) C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数。

(3) C++中,struct的成员默认访问说明符为public(为了与C兼容),class中的默认访问限定符为private,struct增加了访问权限,且可以和类一样有成员函数。

(4) struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名

虚函数可以声明为inline吗?

语法不会出现错误,但是编译器会拒绝inline

(1) 虚函数用于实现运行时的多态,或者称为晚绑定或动态绑定。而内联函数用于提高效率。内联函数的原理是,在编译期间,对调用内联函数的地方的代码替换成函数代码。内联函数对于程序中需要频繁使用和调用的小函数非常有用。

(2) 虚函数要求在运行时进行类型确定,而内联函数要求在编译期完成相关的函数替换;

类成员初始化方式?为什么用成员初始化列表会快一些?

赋值初始化,通过在函数体内进行赋值初始化;

列表初始化,在冒号后使用初始化列表进行初始化。

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

  • 对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。
  • 列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。
  • 赋值初始化是在构造函数当中做赋值的操作,而方法二是做纯粹的初始化操作。C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率。

成员列表初始化。

(1) 必须使用成员初始化的四种情况

  • 当初始化一个引用成员时;

  • 当初始化一个常量成员时;

  • 当调用一个基类的构造函数,而它拥有一组参数时;

  • 当调用一个成员类的构造函数,而它拥有一组参数时;

(2) 成员初始化列表做了什么

  • 编译器会一一操作初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何显示用户代码之前;

  • 列表中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的;

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

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

(2) 使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到相应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数;

析构函数为什么要虚函数

直接的讲,C++中基类采用虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,**当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。**那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用虚函数

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

(1) 构造函数只是起初始化值的作用,但实例化一个对象的时候,可以通过实例去传递参数,从主函数传递到其他的函数里面,这样就使其他的函数里面有值了。一旦实例化对象,系统自动会调用一个构造函数(即使程序员不写,编译器也自动调用一次)。

(2) 析构函数与构造函数的作用相反,用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;特点:析构函数与构造函数同名,但该函数前面加~。析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。当撤销对象时,编译器也会自动调用析构函数。每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员。

构造函数和析构函数可以调用虚函数吗,为什么

(1) 在C++中,提倡不在构造函数和析构函数中调用虚函数

(2) 构造函数和析构函数调用虚函数时都不使用动态绑定,如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本;

(3) 因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数时不安全的,故而C++不会进行动态绑定;

(4) 析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经销毁,这个时候再调用子类的虚函数没有任何意义。

构造函数的执行顺序?析构函数的执行顺序?构造函数内部干了啥?拷贝构造干了啥?

(1) 构造函数顺序(基类-成员类-派生类)

  • 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。

  • 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。

  • 派生类构造函数。

(2) 析构函数顺序(派生类-成员类-基类)

  • 调用派生类的析构函数;

  • 调用成员类对象的析构函数;

  • 调用基类的析构函数。

构造函数析构函数可否抛出异常

(1) C++只会析构已经完成的对象,对象只有在其构造函数执行完毕才算是完全构造妥当。在构造函数中发生异常,控制权转出构造函数之外。因此,在对象的构造函数中发生异常,对象的析构函数不会被调用。因此会造成内存泄漏。

(2) 如果异常从析构函数抛出,而且没有在当地进行捕捉,那个析构函数便是执行不全的。如果析构函数执行不全,就是没有完成他应该执行的每一件事情。

类如何实现只能静态分配和只能动态分配

(1) 前者是把new、delete运算符重载为private属性。后者是把构造、析构函数设为protected属性,再用子类来动态创建

(2) 建立类的对象有两种方式:

  • 静态建立,静态建立一个类对象,就是由编译器为对象在栈空间中分配内存;

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

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

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

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

什么情况会自动生成默认构造函数?

(1) 带有默认构造函数的类成员对象,如果一个类没有任何构造函数,但它含有一个成员对象,而后者有默认构造函数,那么编译器就为该类合成出一个默认构造函数。不过这个合成操作只有在构造函数真正被需要的时候才会发生;如果一个类A含有多个成员类对象,那么类A的每一个构造函数必须调用每一个成员对象的默认构造函数而且必须按照类对象在类A中的声明顺序进行;

(2) 带有默认构造函数的基类,如果一个没有任务构造函数的派生类派生自一个带有默认构造函数基类,那么该派生类会合成一个构造函数调用上一层基类的默认构造函数;

(3) 带有一个虚函数的类

(4) 带有一个虚基类的类

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

什么是类的继承?

(1) 类与类之间的关系

has-A包含关系,用以描述一个类由多个部件类构成,实现has-A关系用类的成员属性表示,即一个类的成员属性是另一个已经定义好的类;

use-A,一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式来实现;

is-A,继承关系,关系具有传递性;

(2) 继承的相关概念

所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类;

(3) 继承的特点

子类拥有父类的所有属性和方法,子类可以拥有父类没有的属性和方法,子类对象可以当做父类对象使用;

(4) 继承中的访问控制

public、protected、private

(5) 继承中的构造和析构函数

(6) 继承中的兼容性原则

什么是组合?

(1) 一个类里面的数据成员是另一个类的对象,即内嵌其他类的对象作为自己的成员;创建组合类的对象:首先创建各个内嵌对象,难点在于构造函数的设计。创建对象时既要对基本类型的成员进行初始化,又要对内嵌对象进行初始化。

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

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

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

(1) 抽象类的定义:

称带有纯虚函数的类为抽象类。

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

(3) 使用抽象类时注意:
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。

抽象类是不能定义对象的。一个纯虚函数不需要(但是可以)被定义。

纯虚函数?

(1) 纯虚函数定义
纯虚函数是一种特殊的虚函数,它的一般格式如下:

class <类名>
{
virtual <类型> <函数名>(<参数表>)=0;
...
};

在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义。凡是含有纯虚函数的类叫做抽象类。这种类不能声明对象,只是作为基类为派生类服务。除非在派生类中完全实现基类中所有的的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象。

(2) 纯虚函数引入原因
1、为了方便使用多态特性,我们常常需要在基类中定义虚函数。
2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔 雀等子类,但动物本身生成对象明显不合常理。

在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义。凡是含有纯虚函数的类叫做抽象类。这种类不能声明对象,只是作为基类为派生类服务。除非在派生类中完全实现基类中所有的的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象。

(2) 纯虚函数引入原因
1、为了方便使用多态特性,我们常常需要在基类中定义虚函数。
2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔、雀等子类,但动物本身生成对象明显不合常理。

为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;)。若要使派生类为非抽象类,则编译器要求在派生类中,必须对纯虚函数予以重载以实现多态性。同时含有纯虚函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

例如,绘画程序中,shape作为一个基类可以派生出圆形、矩形、正方形、梯形等, 如果我要求面积总和的话,那么会可以使用一个 shape * 的数组,只要依次调用派生类的area()函数了。如果不用接口就没法定义成数组,因为既可以是circle ,也可以是square ,而且以后还可能加上rectangle,等等.

虚函数

虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态重载。

抽象类

包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。

类什么时候会析构?

(1) 对象生命周期结束,被销毁时;

(2) delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类析构函数是虚函数时;

(3) 对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。

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

因为编译器必须能够读取这个结构的声明以理解这个数据类型的大小、行为等方面的所有规则。有一条规则在任何关系中都很重要,那就是谁可以访问我的私有部分。

介绍一下C++里面的多态?

(1) 静态多态(重载,模板)

是在编译的时候,就确定调用函数的类型。

当编译器遇到一个模板定义时,它并不生成代码。只有当实例化出模板的一个特定版本时,编译器才会生成代码。

(2) 动态多态(虚函数)

在运行的时候,才确定调用的是哪个函数,动态绑定。运行基类指针指向派生类的对象,并调用派生类的函数。

虚函数实现原理:虚函数表和虚函数指针

函数的运行版本由实参决定,在运行时选择函数的版本,所以动态绑定又称为运行时绑定。

用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(); //调用子类的同名函数}

继承机制中对象之间如何转换?指针和引用之间如何转换?

(1) 向上类型转换

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

(2) 向下类型转换

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

组合与继承优缺点?

(1) 继承

继承是Is a 的关系,比如说Student继承Person,则说明Student is a Person。继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。

继承的缺点有以下几点:

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

(2) 组合

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

组合的优点:

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

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

左值右值

(1) 在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。

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

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

(3) 左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。左值引用通常也不能绑定到右值,但常量左值引用是个"万能"的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的"余生"中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。

(4) 右值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值。

移动构造函数

(1) 我们用对象a初始化对象b后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;

(2) 拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间;

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

C语言的编译链接过程?

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

(1) 预处理

读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号。预编译程序所完成的基本上是对源程序的"替代"工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。(.i预处理后的c文件,.ii预处理后的C++文件。)

(2) 编译阶段

编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。(.s文件)

(3) 汇编过程

汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。(.o目标文件)

(4) 链接阶段

链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够操作系统装入执行的统一整体。

vector与list的区别与应用?怎么找某vector或者list的倒数第二个元素

(1) vector数据结构
vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。因此能高效的进行随机存取,时间复杂度为O(1);但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为O(n)。另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。

连续存储结构:vector是可以实现动态增长的对象数组,支持对数组高效率的访问和在数组尾端的删除和插入操作,在中间和头部删除和插入相对不易,需要挪动大量的数据。它与数组最大的区别就是vector不需程序员自己去考虑容量问题,库里面本身已经实现了容量的动态增长,而数组需要程序员手动写入扩容函数进形扩容。

(2) list数据结构
list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为O(n);但由于链表的特点,能高效地进行插入和删除。

非连续存储结构:list是一个双链表结构,支持对链表的双向遍历。每个节点包括三个信息:元素本身,指向前一个元素的节点(prev)和指向下一个元素的节点(next)。因此list可以高效率的对数据元素任意位置进行访问和插入删除等操作。由于涉及对额外指针的维护,所以开销比较大。

(3) 区别:

  • vector的随机访问效率高,但在插入和删除时(不包括尾部)需要挪动数据,不易操作。list的访问要遍历整个链表,它的随机访问效率低。但对数据的插入和删除操作等都比较方便,改变指针的指向即可。
  • list是单向的,vector是双向的。
  • vector中的迭代器在使用后就失效了,而list的迭代器在使用之后还可以继续使用。
  • list不提供随机访问,所以不能用下标直接访问到某个位置的元素,要访问list里的元素只能遍历,不过你要是只需要访问list的最后N个元素的话,可以用反向迭代器来遍历:

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个对象。

  • 不同的编译器,vector有不同的扩容大小。在vs下是1.5倍,在GCC下是2倍;
  • 空间和时间的权衡。简单来说,空间分配的多,平摊时间复杂度低,但浪费空间也多。
  • 使用k=2增长因子的问题在于,每次扩展的新尺寸必然刚好大于之前分配的总和,也就是说,之前分配的内存空间不可能被使用。这样对内存不友好。最好把增长因子设为(1,2)
  • 对比可以发现采用采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度,因此,使用成倍的方式扩容。

如何释放空间:

由于vector的内存占用空间只增不减,比如你首先分配了10,000个字节,然后erase掉后面9,999个,留下一个有效元素,但是内存占用仍为10,000个。所有内存空间是在vector析构时候才能被系统回收。empty()用来检测容器是否为空的,clear()可以清空所有元素。但是即使clear(),vector所占用的内存空间依然如故,无法保证内存的回收。

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

vector(Vec).swap(Vec);
将Vec的内存空洞清除;
vector().swap(Vec);
清空Vec的内存;

容器内部删除一个元素

(1) 顺序容器(vector, list, deque)

erase迭代器不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效(list除外),所以不能使用erase(it++)的方式,不过erase方法可以返回下一个有效的iterator;

It = c.erase(it);

(2) 关联容器(map, set等)

erase迭代器只是被删除元素的迭代器失效,但是返回值是void,所以要采用erase(it++)的方式删除迭代器;

c.erase(it++)

STL迭代器如何实现

(1) 迭代器是一种抽象的设计理念,通过迭代器可以在不了解容器内部原理的情况下遍历容器,除此之外,STL中迭代器一个最重要的作用就是作为容器与STL算法的粘合剂。

(2) 迭代器的作用就是提供一个遍历容器内部所有元素的接口,因此迭代器内部必须保存一个与容器相关联的指针,然后重载各种运算操作来遍历,其中最重要的是***运算符与->运算符,以及++、–**等可能需要重载的运算符重载。这和C++中的智能指针很像,智能指针也是将一个指针封装,然后通过引用计数或是其他方法完成自动释放内存的功能。

(3) 最常用的迭代器的相应型别有五种:value type、difference type、pointer、reference、iterator catagoly;

set与hash_set的区别

(1) set底层是以RB-Tree实现,hash_set底层是以hash_table实现的;

(2) RB-Tree有自动排序功能,而hash_table不具有自动排序功能;

(3) set和hash_set元素的键值就是实值;

hashmap与map的区别

(1) 底层实现不同;

(2) map具有自动排序的功能,hash_map不具有自动排序的功能;

map、set是怎么实现的,红黑树是怎么能够同时实现这两种容器?为什么使用红黑树?

(1) 他们的底层都是以红黑树的结构实现,因此**插入删除等操作都在O(logn)**时间内完成,因此可以完成高效的插入删除;

(2) 在这里我们定义了一个模版参数,如果它是key那么它就是set,如果它是key+value,那么它就是map;底层是红黑树,实现map的红黑树的节点数据类型是key+value,而实现set的节点数据类型是value

(3) 因为map和set要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度比较低。

如何在共享内存上使用stl标准库?

(1) 想像一下把STL容器,例如map,vector,list等等,放入共享内存中,IPC一旦有了这些强大的通用数据结构做辅助,无疑间通信的能力一下子强大了很多。我们没必要再为共享内存设计其他额外的数据结构,另外,STL的高度可扩展性将为IPC所驱使。STL容器被良好的封装,默认情况下有它们自己的内存管理方案。当一个元素被插入到一个STL列表(list)中时,列表容器自动为其分配内存,保存数据。考虑到要将STL容器放到共享内存中,而容器却自己在堆上分配内存。一个最笨拙的办法是在堆上构造STL容器,然后把容器复制到共享内存,并且确保所有容器的内部分配的内存指向共享内存中的相应区域,这基本是个不可能完成的任务。

(2) 假设进程A在共享内存中放入了数个容器,进程B如何找到这些容器呢?一个方法就是进程A把容器放在共享内存中的确定地址上(fixed offsets),则进程B可以从该已知地址上获取容器。另外一个改进点的办法是,进程A先在共享内存某块确定地址上放置一个map容器,然后进程A再创建其他容器,然后给其取个名字和地址一并保存到这个map容器里。进程B知道如何获取该保存了地址映射的map容器,然后同样再根据名字取得其他容器的地址。

map插入方式有几种?

(1) 用insert函数插入pair数据,

mapStudent.insert(pair<int, string>(1, “student_one”));

(2) 用insert函数插入value_type数据

mapStudent.insert(map<int,string>::value_type (1, “student_one”));

(3) 在insert函数中使用make_pair()函数

mapStudent.insert(make_pair**(1,“student_one”));**

(4) 用数组方式插入数据

mapStudent[1]=“student_one”;

STL中unordered_map(hash_map)和map的区别,hash_map如何解决冲突以及扩容

(1) unordered_map和map类似,都是存储的key-value的值,可以通过key快速索引到value。不同的是unordered_map不会根据key的大小进行排序,

(2) 存储时是根据key的hash值判断元素是否相同,即unordered_map内部元素是无序的,而map中的元素是按照二叉搜索树存储,进行中序遍历会得到有序遍历。

(3) 所以使用时map的key需要定义operator<。而unordered_map需要定义hash_value函数并且重载operator==。但是很多系统内置的数据类型都自带这些,那么如果是自定义类型,那么就需要自己重载operator<或者hash_value()了。

(4) 如果需要内部元素自动排序,使用map,不需要排序使用unordered_map

(5) hash_map底层使用的是hash_table,而hash_table使用的开链法进行冲突避免,所有hash_map采用开链法进行冲突解决。

(6) **什么时候扩容:**当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值—即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。

(7) **扩容(resize)**就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。

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

(1**) 通过下标访问vector中的元素时不会做边界检查,即便下标越界**。也就是说,下标与first迭代器相加的结果超过了finish迭代器的位置,程序也不会报错,而是返回这个地址中存储的值。如果想在访问vector中的元素时首先进行边界检查,可以使用vector中的at函数。通过使用at函数不但可以通过下标访问vector中的元素,而且在at函数内部会对下标进行边界检查。

(2) map的下标运算符[]的作用是:将key作为下标去执行查找,并返回相应的值;如果不存在这个key,就将一个具有该key和value的某人值插入这个map。

(3) erase()函数,只能删除内容,不能改变容量大小;erase成员函数,它删除了itVect迭代器指向的元素,并且返回要被删除的itVect之后的迭代器,迭代器相当于一个智能指针;clear()函数,只能清空内容,不能改变容量大小;如果要想在删除内容的同时释放内存,那么你可以选择deque容器。

map[]与find的区别?

(1) map的下标运算符[]的作用是:将关键码作为下标去执行查找,并返回对应的值;如果不存在这个关键码,就将一个具有该关键码和值类型的默认值的项插入这个map。

(2) map的find函数:用关键码执行查找,找到了返回该位置的迭代器;如果不存在这个关键码,就返回尾迭代器。

STL中list与queue之间的区别

(1) list不再能够像vector一样以普通指针作为迭代器,因为其节点不保证在存储空间中连续存在;

(2) list插入操作和结合才做都不会造成原有的list迭代器失效;

(3) list不仅是一个双向链表,而且还是一个环状双向链表,所以它只需要一个指针;

(4) list不像vector那样有可能在空间不足时做重新配置、数据移动的操作,所以插入前的所有迭代器在插入操作之后都仍然有效;

(5) deque是一种双向开口的连续线性空间,所谓双向开口,意思是可以在头尾两端分别做元素的插入和删除操作;可以在头尾两端分别做元素的插入和删除操作;

(6) deque和vector最大的差异,

  • deque允许常数时间内对起头端进行元素的插入或移除操作,
  • deque没有所谓容量概念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来,deque没有所谓的空间保留功能。

STL中的allocator,deallocator

(1) 第一级配置器直接使用malloc()、free()和relloc(),第二级配置器视情况采用不同的策略:当配置区块超过128bytes时,视之为足够大,便调用第一级配置器;当配置器区块小于128bytes时,为了降低额外负担,使用复杂的内存池整理方式,而不再用一级配置器;

(2) 第二级配置器主动将任何小额区块的内存需求量上调至8的倍数,并维护16个free-list,各自管理大小为8~128bytes的小额区块;

(3) 空间配置函数allocate(),首先判断区块大小,大于128就直接调用第一级配置器,小于128时就检查对应的free-list。如果free-list之内有可用区块,就直接拿来用,如果没有可用区块,就将区块大小调整至8的倍数,然后调用refill(),为free-list重新分配空间;

(4) 空间释放函数deallocate(),该函数首先判断区块大小,大于128bytes时,直接调用一级配置器,小于128bytes就找到对应的free-list然后释放内存。

STL中hash_map扩容发生什么?

(1) hash table表格内的元素称为桶(bucket),而由桶所链接的元素称为节点(node),其中存入桶元素的容器为stl本身很重要的一种序列式容器------vector容器。之所以选择vector为存放桶元素的基础容器,主要是因为vector容器本身具有动态扩容能力,无需人工干预。

(2) 向前操作:首先尝试从目前所指的节点出发,前进一个位置(节点),由于节点被安置于list内,所以利用节点的next指针即可轻易完成前进操作,如果目前正巧是list的尾端,就跳至下一个bucket身上,那正是指向下一个list的头部节点。

各容器的底层数据结构?

(1) vector底层数据结构为数组,支持快速随机访问

(2) list底层数据结构为双向链表,支持快速增删

(3) deque底层数据结构为一个中央控制器和多个缓冲区,详细见STL源码剖析P146,支持首尾(中间不能)快速增删,也支持随机访问

(4) stack底层一般用list或deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时

(5) queue底层一般用list或deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时(stack和queue其实是适配器,而不叫容器,因为是对容器的再封装)

(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表,无序,可重复

函数指针?

(1) 什么是函数指针?

函数指针指向的是特殊的数据类型,函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分。

一个具体函数的名字,如果后面不跟调用符号(即括号),则该名字就是该函数的指针(注意:大部分情况下,可以这么认为,但这种说法并不很严格)。

(2) 函数指针的声明方法

int (*pf)(const int&, const int&);

上面的pf就是一个函数指针,指向返回类型为int,并带有两个const int&参数的函数。注意*pf两边的括号是必须的,否则上面的定义就变成了:

int *pf(const int&, const int&); (2)

而这声明了一个函数pf,其返回类型为int *, 带有两个const int&参数。

(3) 为什么有函数指针

函数与数据项相似,函数也有地址。我们希望在同一个函数中通过使用相同的形参在不同的时间使用产生不同的效果。

(4) 一个函数名就是一个指针,它指向函数的代码。一个函数地址是该函数的进入点,也就是调用函数的地址。函数的调用可以通过函数名,也可以通过指向函数的指针来调用。函数指针还允许将函数作为变元传递给其他函数;

(5) 两种方法赋值:

指针名 = 函数名;指针名 = &函数名

说说你对c和c++的看法,c和c++的区别?

(1) 第一点就应该想到C是面向过程的语言,而C++是面向对象的语言

(2) C和C++动态管理内存的方法不一样,C是使用malloc/free函数,而C++除此之外还有new/delete关键字;(关于malooc/free与new/delete的不同又可以说一大堆,最后的扩展_1部分列出十大区别);

(3) C中的struct和C++的类,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没有;这样就不得不提一下引用和指针的区别(文后扩展_2);

(6) 当然还有C++全部变量的默认链接属性是外链接,而C是内连接;

(7) C中用const修饰的变量不可以用在定义数组时的大小,但是C++用const修饰的变量可以(如果不进行&解引用的操作,是存放在符号表的,不开辟内存);

(8) 当然还有局部变量的声明规则不同,多态,C++特有输入输出流之类的,很多,下面就不再列出来了;

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

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

堆与栈的区别?

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

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

(3) 碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。

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

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

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

野指针是什么?如何检测内存泄漏?

(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;

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

(1) 野指针:野指针指,访问一个已删除或访问受限的内存区域的指针,野指针不能判断是否为NULL来避免。指针没有初始化,释放后没有置空,越界

(2) 悬空指针:一个指针的指向对象已被删除,那么就成了悬空指针。野指针是那些未初始化的指针。

内存泄漏

(1) 内存泄漏

内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制;

(2) 后果

只发生一次小的内存泄漏可能不被注意,但泄漏大量内存的程序将会出现各种证照:性能下降到内存逐渐用完,导致另一个程序失败;

(3) 如何排除

使用工具软件BoundsChecker,BoundsChecker是一个运行时错误检测工具,它主要定位程序运行时期发生的各种错误;

调试运行DEBUG版程序,运用以下技术:CRT(C run-time libraries)、运行时函数调用堆栈、内存泄漏时提示的内存分配序号(集成开发环境OUTPUT窗口),综合分析内存泄漏的原因,排除内存泄漏。

(4) 解决方法

智能指针。

(5) 检查、定位内存泄漏

检查方法:在main函数最后面一行,加上一句_CrtDumpMemoryLeaks()。调试程序,自然关闭程序让其退出,查看输出:

输出这样的格式{453}normal block at 0x02432CA8,868 bytes long

被{}包围的453就是我们需要的内存泄漏定位值,868 bytes long就是说这个地方有868比特内存没有释放。

定位代码位置

在main函数第一行加上_CrtSetBreakAlloc(453);意思就是在申请453这块内存的位置中断。然后调试程序,程序中断了,查看调用堆栈。加上头文件#include <crtdbg.h>

new和malloc的区别?

(1) new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持;

(2) 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。

(3) new操作符内存分配成功时,new返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

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

(5) new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

delete p与delete[]p,allocator

(1) 动态数组管理new一个数组时,[]中必须是一个整数,但是不一定是常量整数,普通数组必须是一个常量整数;

(2) new动态数组返回的并不是数组类型,而是一个元素类型的指针;

(3) delete[]时,数组中的元素按逆序的顺序进行销毁;

(4) new在内存分配上面有一些局限性,new的机制是将内存分配和对象构造组合在一起,同样的,delete也是将对象析构和内存释放组合在一起的。allocator将这两部分分开进行,allocator申请一部分内存,不进行初始化对象,只有当需要的时候才进行初始化操作。

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

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

  • new表达式调用一个名为operator new(operator new[])函数,分配一块足够大的、原始的、未命名的内存空间;

  • 编译器运行相应的构造函数以构造这些对象,并为其传入初始值;

  • 对象被分配了空间并构造完成,返回一个指向该对象的指针。

(2) delete简单数据类型默认只是调用free函数;复杂数据类型先调用析构函数再调用operator delete;针对简单类型,delete和delete[]等同。假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。

(3) 需要在new []一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了4个字节的大小,专门保存数组的大小,在delete []时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。

malloc申请的存储空间能用delete释放吗

不能,malloc/free主要为了兼容C,new和delete 完全可以取代malloc/free的。Malloc/free的操作对象都是必须明确大小的而且不能用在动态类上。new和delete会自动进行类型检查和大小,malloc/free不能执行构造函数与析构函数,所以动态对象它是不行的。当然从理论上说使用malloc申请的内存是可以通过delete释放的。不过一般不这样写的。而且也不能保证每个C++的运行时都能正常。

malloc与free的实现原理?

(1) 在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk、mmap、,munmap这些系统调用实现的;

(2) brk是将数据段(.data)的最高地址指针_edata往高地址推,**mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。**这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系;

(3) malloc小于128k的内存,使用brk分配内存,将_edata往高地址推;malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配;brk分配的内存需要等到高地址内存释放以后才能释放,而mmap分配的内存可以单独释放。当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩。

(4) malloc是从堆里面申请内存,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

malloc、realloc、calloc的区别

(1) malloc函数

void* malloc(unsigned int num_size);int *p = malloc(20*sizeof(int)); //申请20个int类型的空间;

(2) calloc函数

void* calloc(size_t n,size_t size);int *p = calloc(20,sizeof(int));

省去了人为空间计算;malloc申请的空间的值是随机初始化的,calloc申请的空间的值是初始化为0的;

(3) realloc函数

void realloc(void *p, size_t new_size);

给动态分配的空间分配额外的空间,用于扩充容量。

__stdcall和__cdecl的区别?

(1) __stdcall

__stdcall是函数恢复堆栈,只有在函数代码的结尾出现一次恢复堆栈的代码;在编译时就规定了参数个数,无法实现不定个数的参数调用;

(2) __cdecl

__cdecl是调用者恢复堆栈,假设有100个函数调用函数a,那么内存中就有100端恢复堆栈的代码;可以不定参数个数;每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用__stacall函数大。

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

(1) RAII全称是"Resource Acquisition is Initialization",直译过来是"资源获取即初始化",也就是说在构造函数中申请分配资源,在析构函数中释放资源。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。

(2) 智能指针(std::shared_ptr和std::unique_ptr)即RAII最具代表的实现,使用智能指针,可以实现自动的内存管理,再也不需要担心忘记delete造成的内存泄漏。毫不夸张的来讲,有了智能指针,代码中几乎不需要再出现delete了。

手写实现智能指针类

(1) 智能指针是一个数据类型,一般用模板实现,模拟指针行为的同时还提供自动垃圾回收机制。它会自动记录SmartPointer<T*>对象的引用计数,一旦T类型对象的引用计数为0,就释放该对象。除了指针对象外,我们还需要一个引用计数的指针设定对象的值,并将引用计数计为1,需要一个构造函数。新增对象还需要一个构造函数,析构函数负责引用计数减少和释放内存。通过重写赋值运算符,才能将一个旧的智能指针赋值给另一个指针,同时旧的引用计数减1,新的引用计数加1。

(2) 一个构造函数、拷贝构造函数、复制构造函数、析构函数、移走函数;

内存对齐?位域?

(1) 分配内存的顺序是按照声明的顺序

(2) 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。

(3) 最后整个结构体的大小必须是里面变量类型最大值的整数倍

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

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

结构体变量比较是否相等

(1) 重载了"=="操作符

struct foo {int a;int b;bool operator==(const foo& rhs) *// 操作运算符重载*{return( a == rhs.a) && (b == rhs.b);}
};

(2) 元素的话,一个个比;

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

为什么内存对齐

(1) 平台原因(移植原因)

  • 不是所有的硬件平台都能访问任意地址上的任意数据的;

  • 某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常

(2) 性能原因:

  • 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。

  • 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

函数调用过程栈的变化,返回值和参数变量哪个先入栈?

(1) 调用者函数把被调函数所需要的参数按照与被调函数的形参顺序相反的顺序压入栈中,即:从右向左依次把被调函数所需要的参数压入栈;
(2) 调用者函数使用call指令调用被调函数,并把call指令的下一条指令的地址当成返回地址压入栈中(这个压栈操作隐含在call指令中);
(3) 在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后再保存调用者函数的栈顶地址,即当前被调函数的栈底地址(mov ebp,esp);
(4) 在被调函数中,从ebp的位置处开始存放被调函数中的局部变量和临时变量,并且这些变量的地址按照定义时的顺序依次减小,即这些变量的地址是按照栈的延伸方向排列的,先定义的变量先入栈,后定义的变量后入栈;

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

对两个浮点数判断大小和是否相等不能直接用==来判断,会出错!明明相等的两个数比较反而是不相等!对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!浮点数与0的比较也应该注意。与浮点数的表示方式有关。

宏定义一个取两个数中较大值的功能

#define MAX(x,y) ((x\>y?)x:y)

define、const、typedef、inline使用方法?

(1) const与#define的区别:

  • const定义的常量是变量带类型,而#define定义的只是个常数不带类型

  • define只在预处理阶段起作用,简单的文本替换,而const在编译、链接过程中起作用

  • define只是简单的字符串替换没有类型检查。而const是有数据类型的,是要进行判断的,可以避免一些低级错误;

  • define预处理后,占用代码段空间,const占用数据段空间;

  • const不能重定义,而define可以通过#undef取消某个符号的定义,进行重定义;

  • define独特功能,比如可以用来防止文件重复引用

(2) #define和别名typedef的区别

  • 执行时间不同,typedef在编译阶段有效,typedef有类型检查的功能;#define是宏定义,发生在预处理阶段,不进行类型检查;

  • 功能差异,typedef用来定义类型的别名,定义与平台无关的数据类型,与struct的结合使用等。#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。

  • 作用域不同,#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。而typedef有自己的作用域。

(3) define与inline的区别

  1. #define是关键字,inline是函数

  2. 宏定义在预处理阶段进行文本替换,inline函数在编译阶段进行替换

  3. inline函数有类型检查,相比宏定义比较安全;

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

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

lambda函数

(1) 利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象;

(2) 每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。

(3) lambda表达式的语法定义如下:

[capture] (parameters) mutable ->return-type {statement};

(4) lambda必须使用尾置返回来指定返回类型,可以忽略参数列表和返回值,但必须永远包含捕获列表和函数体;

[https://www.cnblogs.com/jimodetiantang/p/9016826.html]{.underline}

lambda语法分析

C++ 11 中的 Lambda 表达式用于定义并创建匿名的函数对象,以简化编程工作。
Lambda 的语法形式如下:

[函数对象参数] (操作符重载函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}

(1) [函数对象参数]

**标识一个 Lambda 表达式的开始,这部分必须存在,不能省略。**函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义 Lambda 为止时 Lambda 所在作用范围内可见的局部变量(包括 Lambda 所在类的 this)。函数对象参数有以下形式:

函数对象参数有以下形式:

  • 空。没有任何函数对象参数。
  • =。函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
  • &。函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是引用传递方式(相当于是编译器自动为我们按引用传递了所有局部变量)。
  • this。函数体内可以使用 Lambda 所在类中的成员变量
  • a。将 a 按值进行传递。按值进行传递时,函数体内不能修改传递进来的 a 的拷贝,因为默认情况下函数是 const 的,要修改传递进来的拷贝,可以添加 mutable 修饰符。
  • &a。将 a 按引用进行传递。
  • a,&b。将 a 按值传递,b 按引用进行传递。
  • =,&a,&b。除 a 和 b 按引用进行传递外,其他参数都按值进行传递。
  • &,a,b。除 a 和 b 按值进行传递外,其他参数都按引用进行传递。

(2) (操作符重载函数参数)

**标识重载的 () 操作符的参数,没有参数时,这部分可以省略。**参数可以通过按值(如: (a, b))和按引用 (如: (&a, &b)) 两种方式进行传递。

(3)mutable 或 exception 声明

**这部分可以省略。**按值传递函数对象参数时,加上 mutable 修饰符后,可以修改传递进来的拷贝(注意是能修改拷贝,而不是值本身)。exception 声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int)。

(4)返回值类型

标识函数返回值的类型,当返回值为 void,或者函数体中只有一处 return 的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。

(5) {函数体}

标识函数的实现,这部分不能省略,但函数体可以为空。

hello world程序开始到打印到屏幕上的全过程?

(1) 用户告诉操作系统执行HelloWorld程序(通过键盘输入等)

(2) 操作系统:找到helloworld程序的相关信息,检查其类型是否是可执行文件;并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址。

(3) 操作系统:创建一个新进程,将HelloWorld可执行文件映射到该进程结构,表示由该进程执行helloworld程序。

(4) 操作系统:为helloworld程序设置cpu上下文环境,并跳到程序开始处。

(5) 执行helloworld程序的第一条指令,发生缺页异常

(6) 操作系统:分配一页物理内存,并将代码从磁盘读入内存,然后继续执行helloworld程序

(7) helloword程序执行puts函数(系统调用),在显示器上写一字符串

(8) 操作系统:找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程

(9) 操作系统:控制设备的进程告诉设备的窗口系统,它要显示该字符串,窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储映像区

(10) 视频硬件将像素转换成显示器可接收和一组控制数据信号

(11) 显示器解释信号,激发液晶屏

(12) OK,我们在屏幕上看到了HelloWorld

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

(1) 类模板没有自动类型推导的使用方式

(2) 类模板在模板参数列表中可以有默认参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PsifX5Kh-1645797543832)(C:\Users\xgc\Desktop\3.jpg)]

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

(1) 首先,一个编译单元(translation unit)指一个.cpp文件以及它所#include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.o文件,并且本身包含的就已经是二进制码,但是不一定能够执行,因为并不保证其中一定有main函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由连接器(linker)进行连接成为一个.exe文件。

(2) 对于模板,模板函数的代码其实并不能直接编译成二进制代码,其中要有一个"实例化"的过程。

(3) 在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于链接器)。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在被调用的时候才会实例化出来(重点),所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj中就找不到一行模板实例的二进制代码,于是连接器也黔驴技穷了。

(参考:[https://blog.csdn.net/lijiayu2015/article/details/52650790?utm_source=blogxgwz6]{.underline}

很详细)

C++中类成员的访问权限和继承权限问题。

(1) 三种访问权限

  • public:用该关键字修饰的成员表示公有成员,该成员不仅可以在类内可以被访问,在类外也是可以被访问的,是类对外提供的可访问接口;

  • private:用该关键字修饰的成员表示私有成员,该成员仅在类内可以被访问,在类体外是隐藏状态;

  • protected:用该关键字修饰的成员表示保护成员,保护成员在类体外同样是隐藏状态,但是对于该类的派生类来说,相当于公有成员,在派生类中可以被访问。

(2) 三种继承方式

  • 若继承方式是public,基类成员在派生类中的访问权限保持不变,也就是说,基类中的成员访问权限,在派生类中仍然保持原来的访问权限;

  • 若继承方式是private,基类所有成员在派生类中的访问权限都会变为私有(private)权限;

  • 若继承方式是protected,基类的公有成员和保护成员在派生类中的访问权限都会变为保护(protected)权限私有成员在派生类中的访问权限仍然是私有(private)权限

cout和printf有什么区别?

cout<<是一个函数,cout<<后可以跟不同的类型是因为cout<<已存在针对各种类型数据的重载,所以会自动识别数据的类型。输出过程会首先将输出字符放入缓冲区,然后输出到屏幕。

cout是有缓冲输出:
cout << "abc " <<endl;
或cout < < "abc\n ";cout <<flush; 这两个才是一样的.
endl相当于输出回车后,再强迫缓冲输出。
flush立即强迫缓冲输出。
printf是无缓冲输出。有输出时立即输出

重载运算符

(1) 我们只能重载已有的运算符,而无权发明新的运算符;对于一个重载的运算符,其优先级和结合律与内置类型一致才可以;不能改变运算符操作数个数;

(2) (:: ? sizeof typeid)不能重载;

(3) 两种重载方式,成员运算符和非成员运算符,成员运算符比非成员运算符少一个参数;下标运算符、箭头运算符必须是成员运算符;

(4) 引入运算符重载,是为了实现类的多态性;

(5) 当重载的运算符是成员函数时,this绑定到左侧运算符对象。成员运算符函数的参数数量比运算符对象的数量少一个;至少含有一个类类型的参数;

(6) 从参数的个数推断到底定义的是哪种运算符,当运算符既是一元运算符又是二元运算符(+,-,*,&);

(7) 下标运算符必须是成员函数,下标运算符通常以所访问元素的引用作为返回值,同时最好定义下标运算符的常量版本和非常量版本;

(8) 箭头运算符必须是类的成员,解引用通常也是类的成员;重载的箭头运算符必须返回类的指针;

函数重载函数匹配原则

(1) 名字查找

(2) 确定候选函数

(3) 寻找最佳匹配

定义和声明的区别

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

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

C++类型转换有四种

(1) static_cast能进行基础类型之间的转换,也是最常看到的类型转换。

它主要有如下几种用法:

  • 用于类层次结构中父类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成父类表示)是安全的;进行下行转换(把父类指针或引用转换成子类指针或引用)时,由于没有动态类型检查,所以是不安全的;

  • 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。

  • 把void指针转换成目标类型的指针(不安全!!)

  • 把任何类型的表达式转换成void类型

(2) const_cast运算符用来修改类型的const或volatile属性。除了去掉const或volatile修饰之外,type_id和expression得到的类型是一样的。但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为[指针或引用]。

(3) reinterpret_cast它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。

(4) dynamic_cast主要用在继承体系中的安全向下转型。它能安全地将指向基类的指针转型为指向子类的指针或引用,并获知转型动作成功是否。转型失败会返回NULL(转型对象为指针时)或抛出异常bad_cast(转型对象为引用时)。dynamic_cast会动用运行时信息(RTTI)来进行类型安全检查,因此dynamic_cast存在一定的效率损失。当使用dynamic_cast时,该类型必须含有虚函数,这是因为dynamic_cast使用了存储在VTABLE中的信息来判断实际的类型。

说一下理解ifdef endif

(1) 一般情况下,源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是"条件编译"。有时,希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。

(2) 条件编译命令最常见的形式为:

#ifdef 标识符
程序段1
#else
程序段2
#endif

它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。
其中#else部分也可以没有,即:

#ifdef
程序段1
#endif

(3) 在一个大的软件工程里面,可能会有多个文件同时包含一个头文件,当这些文件编译链接成一个可执行文件上时,就会出现大量"重定义"错误。在头文件中使用#define、#ifndef、#ifdef、#endif能避免头文件重定义。

隐式转换,如何消除隐式转换?

(1) C++的基本类型中并非完全的对立,部分数据类型之间是可以进行隐式转换的。所谓隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为。很多时候用户可能都不知道进行了哪些转换

(2) C++面向对象的多态特性,就是通过父类的类型实现对子类的封装。通过隐式转换,你可以直接将一个子类的对象使用父类的类型进行返回。比如,数值和布尔类型的转换,整数和浮点数的转换等。某些方面来说,隐式转换给C++程序开发者带来了不小的便捷。C++是一门强类型语言,类型的检查是非常严格的。

(3) 基本数据类型:基本数据类型的转换以取值范围的作为转换基础(保证精度不丢失)。隐式转换发生在从小->大的转换中。比如从char转换为int。从int->long。自定义对象、子类对象可以隐式的转换为父类对象。

(4) C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。

(5) 如果构造函数只接受一个参数,则它实际上定义了转换为此类类型的隐式转换机制。可以通过将构造函数声明为explicit加以制止隐式类型转换,关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit。

虚函数的内存结构,那菱形继承的虚函数内存结构?

(1)无继承

​ 在无继承的情况下,对象内存布局中会先放置虚表指针vfptr,然后再按声明顺序存放数据成员。并且可以知道:虚表vftable中按声明先后顺序存放了类中定义的虚函数的地址,此外,类中定义的静态变量并不存在于类中,并且类成员变量的访问权限也不会影响到成员的内存布局。

(2)单继承

​ 在子类的内存布局的最前面存放的是父类的内存布局,一开始依然是存放一个虚表指针,只不过这个虚表指针与父类的虚表指针是不一样的,这个虚表指针所指向的虚函数表中,会包含所有父类中的虚函数(不一定是父类所定义的,也可能是父类从爷爷类继承下来的虚函数)的地址以及子类中新定义的虚函数的地址,如果子类对虚函数进行了重写,那么重写后的虚函数地址将会覆盖相应的父类虚函数地址。这也说明了如果父类和子类中有不同的虚函数,最终在子类中也只有一个虚表指针,对应一个虚函数表。

(3)多重继承(一个子类包含多个父类)

  • 当一个子类继承于多个相互独立的父类的时候,会先按照继承的先后顺序存放每个父类的内存布局,然后才是子类定义的数据成员
  • 而在子类下的每个父类对应的布局中都有一个虚表指针各自对应一个虚函数表(有几个父类就会有虚表指针,自然也就有几个虚表),各个虚函数表的构成只需关注该父类中的虚函数是否在子类中重写
  • 如果子类中新定义了一个虚函数,那么这个虚函数的地址会放在子类布局下先声明继承的那个父类对应的虚表中
  • 如果子类重写了多个父类定义的同名虚函数(如这里的Vfun1),那么在子类布局下每个父类对应的虚表中都会有被重写的虚函数的地址。

(4)菱形继承

(类A,类B分别继承于类D,而类C同时继承于类C、类D)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R51Xha37-1645797543832)(C:\Users\xgc\Desktop\20190621222158725.png)]

简单来说,多重继承下的子类,每个父类在该子类中都对应一个虚函数表,而每个虚函数表的内容只用关注该子类与对应父类之间虚函数关系,与其他父类无关,如果子类重写的虚函数在多个父类中存在定义,那么每个父类与子类对应的虚表中都会存在这个虚函数的地址(实际上只有第一个声明继承的父类与子类对应的虚表中的才是直接存储的该虚函数的地址,其他虚表都是间接寻址第一个虚表相应位置来得到的该虚函数地址)。即使是菱形继承,子类中的虚表也只关心父类中的虚函数与自身虚函数的关系,而并不关心父类的父类。

除此之外,虚表中虚函数地址的顺序,是按照每个虚函数第一次定义和声明的先后顺序。如果子类中含有新定义的虚函数,那么该虚函数的地址只会存在于第一个虚表中的末尾。

(可参考https://blog.csdn.net/qq_28114615/article/details/88077009?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control)

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

(1) C++允许为一个派生类指定多个基类,这样的继承结构被称为多重继承。

(2) 多重继承的优点很明显,就是对象可以调用多个基类中的接口

(3) 如果派生类所继承的多个基类有相同的基类,而派生类对象需要调用这个祖先类的接口方法,就会容易出现二义性

(4) 加上全局符确定调用哪一份拷贝。比如pa.Author::eat()调用属于Author的拷贝。

(5) 使用虚拟继承,使得多重继承类Programmer_Author只拥有Person类的一份拷贝。

迭代器++it,it++哪个好,为什么

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

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

(2) 前置不会产生临时对象,后置必须产生临时对象,临时对象会导致效率降低

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

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

(1) C++中的异常情况:

  • 语法错误(编译错误):比如变量未定义、括号不匹配、关键字拼写错误等等编译器在编译时能发现的错误,这类错误可以及时被编译器发现,而且可以及时知道出错的位置及原因,方便改正。
  • 运行时错误:比如数组下标越界、系统内存不足等等。这类错误不易被程序员发现,它能通过编译且能进入运行,但运行时会出错,导致程序崩溃。为了有效处理程序运行时错误,C++中引入异常处理机制来解决此问题。

(2) C++异常处理机制:

异常处理基本思想:执行一个函数的过程中发现异常,可以不用在本函数内立即进行处理, 而是抛出该异常,让函数的调用者直接或间接处理这个问题。
C++异常处理机制由3个模块组成:try(检查)、throw(抛出)、catch(捕获)
抛出异常的语句格式为:throw表达式;如果try块中程序段发现了异常则抛出异常。

  try{可能抛出异常的语句;(检查)}catch(类型名[形参名])//捕获特定类型的异常{//处理1;}catch(类型名[形参名])//捕获特定类型的异常{//处理2;}catch(...)//捕获所有类型的异常{}

模板和实现可不可以不写在一个文件里面?为什么?

因为在编译时模板并不能生成真正的二进制代码,而是在编译调用模板类或函数的CPP文件时才会去找对应的模板声明和实现,在这种情况下编译器是不知道实现模板类或函数的CPP文件的存在,所以它只能找到模板类或函数的声明而找不到实现,而只好创建一个符号寄希望于链接程序找地址。但模板类或函数的实现并不能被编译成二进制代码,结果链接程序找不到地址只好报错了。
《C++编程思想》第15章(第300页)说明了原因:模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。

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

(1) 在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。

(2) 为什么是不可预期的问题?

delete this之后不是释放了类对象的内存空间了么,那么这段内存应该已经还给系统,不再属于这个进程。照这个逻辑来看,应该发生指针错误,无访问权限之类的令系统崩溃的问题才对啊?这个问题牵涉到操作系统的内存管理策略。delete this释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是缓冲或者其他什么原因,导致这段内存空间暂时并没有被系统收回。此时这段内存是可以访问的,你可以加上100,加上200,但是其中的值却是不确定的。当你获取数据成员,可能得到的是一串很长的未初始化的随机数;访问虚函数表,指针无效的可能性非常高,造成系统崩溃。

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

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

智能指针的作用;

(1) C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

(2) 智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr。shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

(3) 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的

拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象

(4) unique_ptr"唯一"拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。

(5) 智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。

(6) weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象。进行该对象的内存管理的是那个强引用的shared_ptr。weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr设计的目的是为配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它只可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用记数的增加或减少。

class、union、struct的区别

(1) C语言中,struct只是一个聚合数据类型,没有权限设置,无法添加成员函数,无法实现面向对象编程,且如果没有typedef结构名,声明结构变量必须添加关键字struct。

(2) C++中,struct功能大大扩展,可以有权限设置(默认权限为public),可以像class一样有成员函数,继承(默认public继承),可以实现面对对象编程,允许在声明结构变量时省略关键字struct。

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

(4) 在C/C++程序的编写中,当多个基本数据类型或复合数据结构要占用同一片内存时,我们要使用联合体;当多种类型,多个对象,多个事物只取其一时,也可以使用联合体来发挥其长处。在某一时刻,一个union中只能有一个值是有效的。union的一个用法就是可以用来测试CPU是大端模式还是小端模式:

动态联编与静态联编

(1) 在C++中,联编是指一个计算机程序的不同部分彼此关联的过程。按照联编所进行的阶段不同,可以分为静态联编和动态联编;

(2) 静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,又称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调用(如函数调用)与执行该操作代码间的关系,确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引用的类型。其优点是效率高,但灵活性差。

(3) 动态联编是指联编在程序运行时动态地进行,根据当时的情况来确定调用哪个同名函数,实际上是在运行时虚函数的实现。这种联编又称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果。C++中一般情况下的联编是静态联编,但是当涉及到多态性和虚函数时应该使用动态联编。动态联编的优点是灵活性强,但效率低。动态联编规定,只能通过指向基类的指针或基类对象的引用来调用虚函数,其格式为:指向基类的指针变量名->虚函数名(实参表)或基类对象的引用名.虚函数名(实参表)

(4) 实现动态联编三个条件:

  • 必须把动态联编的行为定义为类的虚函数;

  • 类之间应满足子类型关系,通常表现为一个类从另一个类公有派生而来;

  • 必须先使用基类指针指向子类型的对象,然后直接或间接使用基类指针调用虚函数;

动态编译与静态编译

(1) 静态编译,编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库;

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

动态链接和静态链接区别

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

(2) 静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,.a中的指令都全部被直接包含在最终生成的exe文件中了。但是若使用.so,该.so不必被包含在最终exe文件中,exe文件执行时可以"动态"地引用和卸载这个与exe独立的.so文件。静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。

(3) 动态库就是在需要调用其中的函数时,根据函数映射表找到该函数然后调入堆栈执行。如果在当前工程中有多处对.so文件中同一个函数的调用,那么执行时,这个函数只会留下一份拷贝。但是如果有多处对.so文件中同一个函数的调用,那么执行时,该函数将在当前程序的执行空间里留下多份拷贝,而且是一处调用就产生一份拷贝。

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

(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;

strcpy和memcpy的区别

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

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

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

volatile关键字的作用?

volatile关键字是一种限定符用来声明一个对象在程序中可以被语句外的东西修改,比如操作系统、硬件或并发执行线程。

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

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

volatile用在如下的几个地方:

  • 中断服务程序中修改的供其它程序检测的变量需要加volatile;
  • 多任务环境下各任务间共享的标志应该加volatile;
  • 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义。

讲讲大端小端,如何检测(三种方法)

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

(1) 直接读取存放在内存中的十六进制数值,取低位进行值判断

int a = 0x12345678;int *c = &a;c[0] == 0x12 //大端模式c[0] == 0x78 //小段模式

(2) 用共同体来进行判断

union共同体所有数据成员是共享一段内存的,后写入的成员数据将覆盖之前的成员数据,成员数据都有相同的首地址。Union的大小为最大数据成员的大小。

union的成员数据共用内存,并且首地址都是低地址首字节。int i = 1时:大端存储1放在最高位,小端存储1放在最低位。当读取char ch时,是最低地址首字节,大小端会显示不同的值。

union p;
{p.i = 1;int i; if(ch == 1)char ch;
};

空类会默认添加哪些东西?怎么写?

  • Empty(); // 缺省构造函数

  • Empty(const Empty& ); // 拷贝构造函数

  • ~Empty(); // 析构函数

  • Empty& operator=(const Empty&); // 赋值运算符

标准库是什么?

(1) C++标准库可以分为两部分:

标准函数库:这个库是由通用的、独立的、不属于任何类的函数组成的。函数库继承自C语言。

面向对象类库:这个库是类及其相关函数的集合。

(2) 输入/输出I/O、字符串和字符处理、数学、时间、日期和本地化、动态分配、其他、宽字符函数

(3) 标准的C++I/O类、String类、数值类、STL容器类、STL算法、STL函数对象、STL迭代器、STL分配器、本地化库、异常处理类、杂项支持库。

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;

new、delete、operator new、operator delete、placement new、placement delete

(1) new operator 完成了两件事情:用于申请内存和初始化对象。

例如:string* ps = new string(“abc”);

(2) operator new 类似于C语言中的malloc,只是负责申请内存。

例如:void* buffer = operator new(sizeof(string)); 注意这里new前要有个operator。

(3) placement new 用于在给定的内存中初始化对象。

例如:void* buffer = operator new(sizeof(string));buffer = new(buffer) string(“abc”);调用了placement new,在buffer所指向的内存中创建了一个string类型的对象并且初始值为"abc"。

(4) 因此可以看出:new operator 可以分解operator new和placement new两个动作,是 operator new和placement new的结合。与new对应的delete没有placement delete语法,它只有两种,分别是delete operator和operator delete。delete operator和new operator对应,完成析构对象和释放内存的操作。而operator delete只是用于内存的释放,与C语言中的free相似。

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

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

(2) 参数传递过程到底发生了什么?
将地址传递和值传递统一起来,归根结底还是传递的是"值"(地址也是值,只不过通过它可以找到另一个值)。

总结:拷贝构造函数用来初始化一个非引用类类型对象,如果用传值的方式进行传参数,那么构造实参需要调用拷贝构造函数,而拷贝构造函数需要传递实参,所以会一直递归

值传递和引用传递

(1) 值传递:对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量);
对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);如void foo(class_type obj_local){},如果调用foo(obj);首先class_type obj_local(obj),这样就定义了局部变量obj_local供函数内部使用。

(2) 引用传递:无论对内置类型还是类类型,传递引用或指针最终都是传递的地址值,而地址总是指针类型(属于简单类型),显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用(对于类类型)。

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

(1) C++空类的大小不为0,不同编译器设置不一样,vs设置为1;

(2) C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址;

(3) 带有虚函数的C++类大小不为1,因为每一个对象会有一个vptr指向虚函数表,具体大小根据指针大小确定;

(4) C++中要求对于类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址。

你什么情况用指针当参数,什么时候用引用,为什么?

(1) 使用引用参数的主要原因有两个:

  • 程序员能修改调用函数中的数据对象

  • 通过传递引用而不是整个数据–对象,可以提高程序的运行速度

(2) 一般的原则:
对于使用引用的值而不做修改的函数:

  • 如果数据对象很小,如内置数据类型或者小型结构,则按照值传递;

  • 如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针;

  • 如果数据对象是较大的结构,则使用const指针或者引用,提高程序的效率。这样可以节省结构所需的时间和空间;

  • 如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递);

(3) 对于修改函数中数据的函数:

  • 如果数据是内置数据类型,则使用指针

  • 如果数据对象是数组,则只能使用指针

  • 如果数据对象是结构,则使用引用或者指针

  • 如果数据是类对象,则使用引用

大内存申请时候选用哪种?C++变量存在哪?变量的大小存在哪?符号表存在哪?

(1) 大内存申请时,采用堆申请空间,用new申请;

(2) 不同的变量存储在不同的地方,局部变量、全局变量、静态变量;

(3) C++对变量名不作存储,在汇编以后不会出现变量名,变量名作用只是用于方便编译成汇编代码,是给编译器看的,是方便人阅读的

为什么会有大端小端,htol这一类函数的作用

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

静态函数能定义为虚函数吗?常函数?

(1) static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。

(2) 静态与非静态成员函数之间有一个主要的区别。那就是静态成员函数没有this指针。虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable。对于静态成员函数,它没有this指针,所以无法访问vptr。这就是为何static函数不能为virtual。虚函数的调用关系:this -> vptr -> vtable ->virtual function。

this指针调用成员变量时,堆栈会发生什么变化?

当在类的非静态成员函数访问类的非静态成员时,编译器会自动将对象的地址传给作为隐含参数传递给函数,这个隐含参数就是this指针。即使你并没有写this指针,编译器在链接时也会加上this的,对各成员的访问都是通过this的。例如你建立了类的多个对象时,在调用类的成员函数时,你并不知道具体是哪个对象在调用,此时你可以通过查看this指针来查看具体是哪个对象在调用。this指针首先入栈,然后成员函数的参数从右向左进行入栈,最后函数返回地址入栈。

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

(1) 为类设计一个static静态变量count作为计数器;

(2) 类定义结束后初始化count;

(3) 在构造函数中对count进行+1;

(4) 设计拷贝构造函数,在进行拷贝构造函数中进行count +1,操作;

(5) 设计复制构造函数,在进行复制函数中对count+1操作;

(6) 在析构函数中对count进行-1;

虚函数的代价?

(1) 带有虚函数的类,每一个类会产生一个虚函数表,用来存储指向虚成员函数的指针,增大类;

(2) 带有虚函数的类的每一个对象,都会有一个指向虚表的指针,会增加对象的空间大小;

(3) 不能再是内联的函数,因为内联函数在编译阶段进行替代,而虚函数表示等待,在运行阶段才能确定采用哪种函数,虚函数不能是内联函数。

类对象的大小

(1) 类的非静态成员变量大小,静态成员不占据类的空间,成员函数也不占据类的空间大小;

(2) 内存对齐另外分配的空间大小,类内的数据也是需要进行内存对齐操作的;

(3) 虚函数的话,会在类对象插入vptr指针,加上指针大小;

(4) 当该类是某类的派生类,那么派生类继承的基类部分的数据成员也会存在派生类中的空间中,也会对派生类进行扩展。

移动构造函数

(1) 有时候会遇到这样一种情况,我们用对象a初始化对象b后对象a我们就不在使用了,但是对象a的空间还在(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;

(2) 拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制;

(3) C++引入了移动构造函数,专门处理这种,用a初始化b后,就将a析构的情况;

(4) 与拷贝类似,移动也使用一个对象的值设置另一个对象的值。但是,又与拷贝不同的是,移动实现的是对象值真实的转移(源对象到目的对象):源对象将丢失其内容,其内容将被目的对象占有。移动操作发生的时候,是当移动值的对象是未命名的对象的时候。这里未命名的对象就是那些临时变量,甚至都不会有名称。典型的未命名对象就是函数的返回值或者类型转换的对象。使用临时对象的值初始化另一个对象值,不会要求对对象的复制:因为临时对象不会有其它使用,因而,它的值可以被移动到目的对象。做到这些,就要使用移动构造函数和移动赋值:当使用一个临时变量对象进行构造初始化的时候,调用移动构造函数。类似的,使用未命名的变量的值赋给一个对象时,调用移动赋值操作;

(5)

Example6 (Example6&& x) : ptr(x.ptr)
{x.ptr = nullptr;
}// move assignment
Example6& operator= (Example6&& x)
{delete ptr;    ptr = x.ptr;x.ptr=nullptr;return *this;
}

何时需要合成构造函数

(1) 如果一个类没有任何构造函数,但他含有一个成员对象,该成员对象含有默认构造函数,那么编译器就为该类合成一个默认构造函数,因为不合成一个默认构造函数那么该成员对象的构造函数不能调用;

(2) 没有任何构造函数的类派生自一个带有默认构造函数的基类,那么需要为该派生类合成一个构造函数,只有这样基类的构造函数才能被调用;

(3) 带有虚函数的类,虚函数的引入需要进入虚表,指向虚表的指针,该指针是在构造函数中初始化的,所以没有构造函数的话该指针无法被初始化;

(4) 带有一个虚基类的类

  • 并不是任何没有构造函数的类都会合成一个构造函数

  • 编译器合成出来的构造函数并不会显示设定类内的每一个成员变量

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

有三种情况会以一个对象的内容作为另一个对象的初值:

  • 对一个对象做显示的初始化操作,X xx = x;

  • 当对象被当做参数交给某个函数时;

  • 当函数传回一个类对象时;

(1) 如果一个类没有拷贝构造函数,但是含有一个类类型的成员变量,该类型含有拷贝构造函数,此时编译器会为该类合成一个拷贝构造函数;

(2) 如果一个类没有拷贝构造函数,但是该类继承自含有拷贝构造函数的基类,此时编译器会为该类合成一个拷贝构造函数;

(3) 如果一个类没有拷贝构造函数,但是该类声明或继承了虚函数,此时编译器会为该类合成一个拷贝构造函数;

(4) 如果一个类没有拷贝构造函数,但是该类含有虚基类,此时编译器会为该类合成一个拷贝构造函数;

何时需要成员初始化列表?过程是什么?

(1) 当初始化一个引用成员变量时;

(2) 初始化一个const成员变量时;

(3) 当调用一个基类的构造函数,而构造函数拥有一组参数时;

(4) 当调用一个成员类的构造函数,而他拥有一组参数;

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

程序员定义的析构函数被扩展的过程?

(1) 析构函数函数体被执行;

(2) 如果class拥有成员类对象,而后者拥有析构函数,那么它们会以其声明顺序的相反顺序被调用;

(3) 如果对象有一个vptr,现在被重新定义

(4) 如果有任何直接的上一层非虚基类拥有析构函数,则它们会以声明顺序被调用;

(5) 如果任何虚基类拥有析构函数

构造函数的执行算法?

(1) 在派生类构造函数中,所有的虚基类及上一层基类的构造函数调用;

(2) 对象的vptr被初始化;

(3) 如果有成员初始化列表,将在构造函数体内扩展开来,这必须在vptr被设定之后才做;

(4) 执行程序员所提供的代码;

构造函数的扩展过程?

(1) 记录在成员初始化列表中的数据成员初始化操作会被放在构造函数的函数体内,并与成员的声明顺序为顺序;

(2) 如果一个成员并没有出现在成员初始化列表中,但它有一个默认构造函数,那么默认构造函数必须被调用;

(3) 如果class有虚表,那么它必须被设定初值;

(4) 所有上一层的基类构造函数必须被调用;

(5) 所有虚基类的构造函数必须被调用。

哪些函数不能是虚函数

(1) 构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;

(2) 内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;

(3) 静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。

(4) 友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法

(5) 普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。

sizeof和strlen的区别

(1) strlen计算字符串的具体长度(只能是字符串),不包括字符串结束符。返回的是字符个数。

(2) sizeof计算声明后所占的内存数(字节大小),不是实际长度。

(3) sizeof是一个取字节运算符,而strlen是个函数。

(4) sizeof的返回值=字符个数*字符所占的字节数,字符实际长度小于定义的长度,此时字符个数就等于定义的长度。若未给出定义的大小,分类讨论,对于字符串数组,字符大小等于实际的字符个数+1;对于整型数组,字符个数为实际的字符个数。字符串每个字符占1个字节,整型数据每个字符占的字节数需根据系统的位数类确定,32位占4个字节。

(5) sizeof可以用类型做参数,strlen只能用char*做参数,且必须以’\0’结尾,sizeof还可以用函数做参数;

(6) 数组做sizeof的参数不退化,传递给strlen就退化为指针;

简述strcpy、sprintf与memcpy的区别

(1) 操作对象不同

  • strcpy的两个操作对象均为字符串

  • sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串

  • memcpy的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。

(2) 执行效率不同

memcpy最高,strcpy次之,sprintf的效率最低。

(3) 实现功能不同

  • strcpy主要实现字符串变量间的拷贝

  • sprintf主要实现其他数据类型格式到字符串的转化

  • memcpy主要是内存块间的拷贝。

局部变量全局变量的问题?

(1) 局部会屏蔽全局。要用全局变量,需要使用"::",局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那个循环体内。

(2) 如何引用一个已经定义过的全局变量,可以用引用头文件的方式,也可以用extern关键字,如果用引用头文件方式来引用某个在头文件中声明的全局变理,假定你将那个变写错了,那么在编译期间会报错,如果你用extern方式引用时,假定你犯了同样的错误,那么在编译期间不会报错,而在连接期间报错。

(3) 全局变量可不可以定义在可被多个.C文件包含的头文件中,在不同的C文件中以static形式来声明同名全局变量。可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋初值,此时连接不会出错

数组和指针的区别?

(1) 数组在内存中是连续存放的,开辟一块连续的内存空间;数组所占存储空间:sizeof(数组名);数组大小:sizeof(数组名)/sizeof(数组元素数据类型);

(2) 用运算符sizeof可以计算出数组的容量(字节数)。sizeof§,p为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。

(3) 编译器为了简化对数组的支持,实际上是利用指针实现了对数组的支持。具体来说,就是将表达式中的数组元素引用转换为指针加偏移量的引用。

(4) 在向函数传递参数的时候,如果实参是一个数组,那用于接受的形参为对应的指针。也就是传递过去是数组的首地址而不是整个数组,能够提高效率;

(5) 在使用下标的时候,两者的用法相同,都是原地址加上下标值,不过数组的原地址就是数组首元素的地址是固定的,指针的原地址就不是固定的。

C++如何阻止一个类被实例化?一般在什么时候将构造函数声明为private?

(1) 将类定义为抽象基类或者将构造函数声明为private;

(2) 不允许类外部创建类对象,只能在类内部创建对象

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

(1) 为了阻止编译器默认生成拷贝构造函数和拷贝赋值函数,我们需要手动去重写这两个函数,某些情况下,为了避免调用拷贝构造函数和拷贝赋值函数,我们需要将他们设置成private,防止被调用。

(2) 类的成员函数和friend函数还是可以调用private函数,如果这个private函数只声明不定义,则会产生一个连接错误;

(3) 针对上述两种情况,我们可以定一个base类,在base类中将拷贝构造函数和拷贝赋值函数设置成private,那么派生类中编译器将不会自动生成这两个函数,且由于base类中该函数是私有的,因此,派生类将阻止编译器执行相关的操作。

assert与NDEBUGE

(1) assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行,原型定义:

#include <assert.h>void assert( int expression );

assert的作用是现计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用abort来终止程序运行。如果表达式为真,assert什么也不做。

(2) NDEBUG宏是Standard C中定义的宏,专门用来控制assert()的行为。如果定义了这个宏,则assert不会起作用。定义NDEBUG能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。

(3) C Standard中规定了assert以宏来实现。<assert.h>被设计来可以被多次包含,其中一上来就undef assert,然后由NDEBUG宏来决定其行为。

denug和release的区别

(1) 调试版本,包含调试信息,所以容量比Release大很多,并且不进行任何优化(优化会使调试复杂化,因为源代码和生成的指令间关系会更复杂),便于程序员调试。Debug模式下生成两个文件,除了.exe或.dll文件外,还有一个.pdb文件,该文件记录了代码中断点等调试信息;

(2) 发布版本,不对源代码进行调试,编译时对应用程序的速度进行优化,使得程序在代码大小和运行速度上都是最优的。(调试信息可在单独的PDB文件中生成)。Release模式下生成一个文件.exe或.dll文件。

(3) 实际上,Debug和Release并没有本质的界限,他们只是一组编译选项的集合,编译器只是按照预定的选项行动。事实上,我们甚至可以修改这些选项,从而得到优化过的调试版本或是带跟踪语句的发布版本。

main函数有没有返回值

程序运行过程入口点main函数,main()函数返回值类型必须是int,这样返回值才能传递给程序激活者(如操作系统)表示程序正常退出。main(int args, char **argv) 参数的传递。参数的处理,一般会调用getopt()函数处理,但实践中,这仅仅是一部分,不会经常用到的技能点。

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

(1) 如果在main函数之前声明一个类的全局的对象。那么其执行顺序,根据全局对象的生存期和作用域,肯定先于main函数。

class simpleClass{public:simpleClass() {cout << "simpleClass constructor.." << endl; }};simpleClass g_objectSimple; //step1全局对象 *int _main(int argc, _TCHAR* argv[]) //step3 *{return 0;
}

(2) 定义在main()函数之前的全局对象、静态对象的构造函数在main()函数之前执行。

(3) Main函数执行之前,主要就是初始化系统相关资源;

  • 设置栈指针

  • 初始化static静态和global全局变量,即data段的内容

  • 将未初始化部分的全局变量赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL,等等,即.bss段的内容

  • 全局对象初始化,在main之前调用构造函数

  • 将main函数的参数,argc,argv等传递给main函数,然后才真正运行main函数

(4) main函数执行之后

  • 全局对象的析构函数会在main函数之后执行;

  • 可以用_onexit注册一个函数,它会在main之后执行;

虚函数与纯虚函数的区别在于

(1) 纯虚函数只有定义没有实现,虚函数既有定义又有实现;

(2) 含有纯虚函数的类不能定义对象,含有虚函数的类能定义对象;

智能指针怎么用?智能指针出现循环引用怎么解决?

(1) shared_ptr

调用一个名为make_shared的标准库函数

shared_ptr<int> p = make_shared<int>(42);

通常用auto更方便,

auto p = shared_ptr<int> p2(new int(2));

每个shared_ptr都有一个关联的计数器,通常称为引用计数,一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象;shared_ptr的析构函数就会递减它所指的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。

(2) unique_ptr

一个unique_ptr拥有它所指向的对象。某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。

(3) weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变引用计数,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。

(4) 弱指针用于专门解决shared_ptr循环引用的问题,weak_ptr不会修改引用计数,即其存在与否并不影响对象的引用计数器。

循环引用就是:两个对象互相使用一个shared_ptr成员变量指向对方。弱引用并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。

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

(1) 函数原型

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个字符。

(2) 如果目标长>指定长>源长,则将源长全部拷贝到目标长,自动加上’\0’
如果指定长<源长,则将源长中按指定长度拷贝到目标字符串,不包括’\0’
如果指定长>目标长,运行时错误;

为什么要用static_cast转换而不用c语言中的转换?

(1) 更加安全;

(2) 更直接明显,能够一眼看出是什么类型转换为什么类型,容易找出程序中的错误;可清楚地辨别代码中每个显式的强制转;可读性更好,能体现程序员的意图;

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

有时候类里面定义了很多int,char,struct等c语言里的那些类型的变量,我习惯在构造函数中将它们初始化为0,但是一句句的写太麻烦,所以直接就memset(this, 0, sizeof *this);将整个对象的内存全部置为0。对于这种情形可以很好的工作,但是下面几种情形是不可以这么使用的;

  • 类含有虚函数表:这么做会破坏虚函数表,后续对虚函数的调用都将出现异常;

  • 类中含有C++类型的对象:例如,类中定义了一个list的对象,由于在构造函数体的代码执行之前就对list对象完成了初始化,假设list在它的构造函数里分配了内存,那么我们这么一做就破坏了list对象的内存。

方法调用的原理(栈,汇编)

(1) 机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。而为单个过程分配的那部分栈称为帧栈;帧栈可以认为是程序栈的一段,它有两个端点,一个标识起始地址,一个标识着结束地址,两个指针结束地址指针esp,开始地址指针ebp;

(2) 由一系列栈帧构成,这些栈帧对应一个过程,而且每一个栈指针+4的位置存储函数返回地址;每一个栈帧都建立在调用者的下方,当被调用者执行完毕时,这一段栈帧会被释放。由于栈帧是向地址递减的方向延伸,因此如果我们将栈指针减去一定的值,就相当于给栈帧分配了一定空间的内存。如果将栈指针加上一定的值,也就是向上移动,那么就相当于压缩了栈帧的长度,也就是说内存被释放了。

(3) 过程实现

  1. 备份原来的帧指针,调整当前的栈帧指针到栈指针位置;

  2. 建立起来的栈帧就是为被调用者准备的,当被调用者使用栈帧时,需要给临时变量分配预留内存;

  3. 使用建立好的栈帧,比如读取和写入,一般使用mov,push以及pop指令等等。

  4. 恢复被调用者寄存器当中的值,这一过程其实是从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了

  5. 恢复被调用者寄存器当中的值,这一过程其实是从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了。

  6. 释放被调用者的栈帧,释放就意味着将栈指针加大,而具体的做法一般是直接将栈指针指向帧指针,因此会采用类似下面的汇编代码处理。

  7. 恢复调用者的栈帧,恢复其实就是调整栈帧两端,使得当前栈帧的区域又回到了原始的位置。

  8. 弹出返回地址,跳出当前过程,继续执行调用者的代码。

(4) 过程调用和返回指令

  1. call指令

  2. leave指令

  3. ret指令

回调函数的作用

(1) 当发生某种事件时,系统或其他函数将会自动调用你定义的一段函数;

(2) 回调函数就相当于一个中断处理函数,由系统在符合你设定的条件时自动调用。为此,你需要做三件事:1,声明;2,定义;3,设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用;

(3) 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数;

(4) 因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。

C++11新特性

[https://www.jianshu.com/p/78c700c8d72d]{.underline}

inline函数

(1) 引用inline函数的原因:

在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。

(2) inline使用限制

inline的使用时有所限制的,inline只适合函数体内部代码简单的函数使用,不能包含复杂的结构控制语句例如while、switch,并且不能内联函数本身不能是直接递归函数(即,自己内部还调用自己的函数)。

(3) 关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。

(4) 慎用inline

  • 虽然说内联函数可以提高执行效率,但是不可以将所有的函数都定义为内联函数。

  • 内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。

  • 如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

以下的情况不适合用内联函数:

  • 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

  • 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如"偷偷地"执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了inline不应该出现在函数的声明中)。

那些类型的变量,我习惯在构造函数中将它们初始化为0,但是一句句的写太麻烦,所以直接就memset(this, 0, sizeof *this);将整个对象的内存全部置为0。对于这种情形可以很好的工作,但是下面几种情形是不可以这么使用的;

  • 类含有虚函数表:这么做会破坏虚函数表,后续对虚函数的调用都将出现异常;

  • 类中含有C++类型的对象:例如,类中定义了一个list的对象,由于在构造函数体的代码执行之前就对list对象完成了初始化,假设list在它的构造函数里分配了内存,那么我们这么一做就破坏了list对象的内存。

方法调用的原理(栈,汇编)

(1) 机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。而为单个过程分配的那部分栈称为帧栈;帧栈可以认为是程序栈的一段,它有两个端点,一个标识起始地址,一个标识着结束地址,两个指针结束地址指针esp,开始地址指针ebp;

(2) 由一系列栈帧构成,这些栈帧对应一个过程,而且每一个栈指针+4的位置存储函数返回地址;每一个栈帧都建立在调用者的下方,当被调用者执行完毕时,这一段栈帧会被释放。由于栈帧是向地址递减的方向延伸,因此如果我们将栈指针减去一定的值,就相当于给栈帧分配了一定空间的内存。如果将栈指针加上一定的值,也就是向上移动,那么就相当于压缩了栈帧的长度,也就是说内存被释放了。

(3) 过程实现

  1. 备份原来的帧指针,调整当前的栈帧指针到栈指针位置;

  2. 建立起来的栈帧就是为被调用者准备的,当被调用者使用栈帧时,需要给临时变量分配预留内存;

  3. 使用建立好的栈帧,比如读取和写入,一般使用mov,push以及pop指令等等。

  4. 恢复被调用者寄存器当中的值,这一过程其实是从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了

  5. 恢复被调用者寄存器当中的值,这一过程其实是从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了。

  6. 释放被调用者的栈帧,释放就意味着将栈指针加大,而具体的做法一般是直接将栈指针指向帧指针,因此会采用类似下面的汇编代码处理。

  7. 恢复调用者的栈帧,恢复其实就是调整栈帧两端,使得当前栈帧的区域又回到了原始的位置。

  8. 弹出返回地址,跳出当前过程,继续执行调用者的代码。

(4) 过程调用和返回指令

  1. call指令

  2. leave指令

  3. ret指令

回调函数的作用

(1) 当发生某种事件时,系统或其他函数将会自动调用你定义的一段函数;

(2) 回调函数就相当于一个中断处理函数,由系统在符合你设定的条件时自动调用。为此,你需要做三件事:1,声明;2,定义;3,设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用;

(3) 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数;

(4) 因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。

C++11新特性

[https://www.jianshu.com/p/78c700c8d72d]{.underline}

inline函数

(1) 引用inline函数的原因:

在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。

(2) inline使用限制

inline的使用时有所限制的,inline只适合函数体内部代码简单的函数使用,不能包含复杂的结构控制语句例如while、switch,并且不能内联函数本身不能是直接递归函数(即,自己内部还调用自己的函数)。

(3) 关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。

(4) 慎用inline

  • 虽然说内联函数可以提高执行效率,但是不可以将所有的函数都定义为内联函数。

  • 内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。

  • 如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

以下的情况不适合用内联函数:

  • 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

  • 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如"偷偷地"执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了inline不应该出现在函数的声明中)。

C++杂记(一)——常见知识总结相关推荐

  1. 计算机C语言常用语句,计算机二级C语言考试常见知识积累

    形式化方法是一种特别的基于数学的技术,用于软件和硬件系统的形式规范.开发以及形式验证.下面是小编整理的关于计算机二级C语言考试常见知识,欢迎大家参考! 选择结构 [考点1]关系运算 关系运算符有6个, ...

  2. 软件开发中常见知识总结

    最近在准备软件开发的笔试面试,复(yu)习(xi)了一些在软件开发中的常见知识.为了给自己攒点RP,故与大家分享一二. 软件开发需要准备的比较多,主要分为编程语言,数据结构和算法,计算机网络,计算机操 ...

  3. 关于计算机软件系统的知识,一些关于计算机的常见知识

    [导语]电子计算机(electroniccomputer),通称电脑,简称计算机(computer),是现代的一种利用电子技术和相关原理根据一系列指令来对数据进行处理的机器.计算机的种类按照其用途分为 ...

  4. lq_C/C++常见知识补充002

    C/C++常见知识补充002 memset()函数与fill()函数 string.h头文件下的常用函数 struct结构体变量 浮点数比较 黑盒测试 memset()函数与fill()函数 作用:给 ...

  5. python常见知识

    python常见面试题 1.python中可变对象和不可变对象: 可变对象:list dict set 不可变对象:int float bool string tuple 文章链接: https:// ...

  6. 升级笔记本CPU的常见知识汇总

    现在升级笔记本变成了常见的事:内存.硬盘都很容易升级.当然,只要你想挑战更高性能,把笔记本CPU升级一番也未尝不可. 什么本子才有必要升级CPU? 只要你的本子采用的是T9xxx系列以下的CPU,都有 ...

  7. http简介以及常见知识和http请求头,响应头,状态码,内容类型对照表分享(超实用)

    1.http简介 HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的 ...

  8. 【软考网络管理员】2023年软考网管初级常见知识考点(1)- 计算机硬件基础

    [写在前面]也是趁着五一假期前再写几篇分享类的文章给大家,希望看到我文章能给备考的您带来一些帮助,5月27号也是全国计算机软件考试统一时间,也就不用去各个地方找资料和代码了.紧接着我就把我整理的一些资 ...

  9. 【软考网络管理员】2023年软考网管初级常见知识考点(2)- 数据通信技术

    [写在前面]也是趁着五一假期前再写几篇分享类的文章给大家,希望看到我文章能给软考网络管理员备考的您带来一些帮助,5月27号也是全国计算机软件考试统一时间,也就不用去各个地方找资料和代码了.紧接着我就把 ...

最新文章

  1. 两款PHP开发的开源的在线办公软件简介
  2. 【数据挖掘】关联规则挖掘 Apriori 算法 ( 关联规则简介 | 数据集 与 事物 Transaction 概念 | 项 Item 概念 | 项集 Item Set | 频繁项集 | 示例解析 )
  3. 基于FPGA的FFT变换实现
  4. 微信小程序背景图虚化
  5. jQuery入口函数的四种写法【1分钟掌握】
  6. Jenkins自定义主题教程
  7. 机器视觉开源代码集合(转载)
  8. 属猴的人2021年运势预测
  9. MikroTik RouterOS安装chr授权到阿里云虚拟机(转)
  10. DateUtils(一个日期工具类)
  11. MacOS配置Sql Server环境
  12. android 烟花动画效果图,android利用图片实现烟花效果
  13. 小程序中如何使用vantUi库
  14. python调用def函数的格式_Python 函数def 模块调用函数方法及全局变量
  15. 计算机硬盘会计分录,电脑加装固态硬盘如何做分录
  16. 树莓派机器人小车(创乐博)修炼指南(一)
  17. python有向图遍历_用Networkx进行有向图遍历
  18. 配置和使用Nexus私有仓库
  19. 支持tcam的服务器,使用tcam实现数据流的选择性路由
  20. MSBUILD : error MSB4132: The tools version “2.0“ is unrecognized. Available tools versions are “4.0“

热门文章

  1. mac mysql my.cnf 路径_my.cnf文件在macOS上的位置
  2. 函数式接口和Stream流式思想
  3. bug修复日记---------玩家信息被重复写入数据库
  4. 底层网络知识详解:从二层到三层-第8讲-世界这么大,我想出网关:欧洲十国游与玄奘西行
  5. linux下如何清空一个文件?
  6. 【分享】一位平凡测试人的故事
  7. 立创eda学习笔记五:如何自己画器件的符号和封装并上传
  8. rails应用无法读取kafka数据报错Kafka::Error: Failed to find group coordinator
  9. 2022-02-14 influxdb集群coordinator启动及请求处理流程解析
  10. SQL查询语句大全(转)