c++后台开发面试常见知识点总结(一)c++基础

  • 指针和引用的区别
  • extern,const,static,volatile关键字
  • #define const的区别
  • 关于typedef#define;
  • C++程序中内存使用情况分析(堆和栈的区别)
  • new malloc的异同处,newdelete是如何实现的。
  • CC++的区别
  • C++中的重载,重写,重定义(隐藏)的区别:
  • 析构函数一般写成虚函数的原因。
  • 构造函数为什么一般不定义为虚函数
  • 构造函数或者析构函数中调用虚函数会怎样。
  • 析构函数能抛出异常吗
  • 纯虚函数和抽象类
  • 多态的实现条件,虚指针vptr, 虚函数表vbtl
  • 深拷贝和浅拷贝的区别(举例说明深拷贝的安全性) 
  • 什么情况下会调用拷贝构造函数(三种情况)
  • struct内存对齐方式,为什么要对齐?怎么修改默认对齐方式?struct,union
  • 内存泄露的定义,如何检测与避免?内存检查工具的了解。
  • 成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)
  • 必须在构造函数初始化列表里进行初始化的数据成员有哪些?
  • C++的调用惯例(C++函数调用的压栈过程)
  • C++的四种强制转换 static_castconst_castdynamic_castreinterpret_cast
  • 多重继承,菱形结构,虚基类,虚继承,以及在多继承虚继承下多态的实现,虚继承下对象的内存分布
  • 内联函数有什么优点?内联函数与宏定义的区别?
  • STL容器 迭代器失效总结.
  • 对继承和组合的理解
  • c++ main函数执行之前需要做哪些准备
  • 智能指针,shared_ptr的循环引用导致的内存泄漏怎么解决?
  • 类成员变量用memset()设置会有什么问题?
  • STL alloc实现,alloc的优势和局限,STL中其他的配置器
  • 单例模式;不能被继承的类;定义一个只能在堆上(栈上)生成对象?
  • cmakemakefile的区别   简述cmake到可执行文件的过程
  • 问有没有用过shrink_to_fit,说一下作用,为什么用
  • char (*p) [5] char *p[5]char (*p)()的区别?
  • 如何防止一个类被拷贝
  • c++怎么实现一个函数先于main函数运行,后于main函数执行 
  • 如何删除map中的奇数节点
  • C++的分离式编译  为什么C++类模板不支持分离式编译?
  • 两个文件a,b,文件内部分别定义两个全局变量,用g 编译的时候如何保证两个全局变量初化顺序
  • 哈希表的冲突处理和数据迁移
  • vector的容量扩张为什么是2  最好的策略是什么?reverse()
  • strcpystrncpy的区别
  • malloc涉及的系统调用
  • C++11新特性? lambda表达式, =default;, =deleted 函数
  • C语言程序能不能直接调用C++语言编写的动态链接库。
  • Crestrict关键字:
  • 虚函数表存储在静态存储区
  • 面向对象的三大特性,结合C++语言支持来讲。
  • 红黑树性质
  • malloc的底层实现
  • ++iteriter++那个好
  • c 实现重载 ?
  • 如何突破private的限制?
  • 如何设计一个好的字符串hash函数

 1 指针和引用的区别.

语法上:指针和引用没有关系,引用就是一个已经存在的对象的别名。对引用的任何操作等价于对被引用对象的操作。

1.当引用被创建时,它必须被初始化。而指针则可以在任何时候被初始化。未初始化的引用不合法,未初始化的指针合法但危险。(悬空指针)

2.一旦一个引用被初始化为指向一个对象,它就不能被改变为对另一个对象的引用。而指针则可以在任何时候指向另一个对象。

3.不可能有NULL引用。必须确保引用是和一块合法的存储单元关联。因为不存在指向空值的引用,所以在使用引用之前不需验证它的合法性,而使用指针需要验证合法性。所以使用引用的代码效率要比使用指针的要高。

底层实现上(汇编层):引用是通过指针实现的。在程序一层只要直接涉及对引用变量的操作,操作的总是被引用变量,编译器实现了一些操作,总是在引用前面加上*。实际上如int a=0;int &b=a;中变量b中存放的是a的地址,int*const b=&a;但编译器让对b的操作都自动为*b,

 

指针的大小:32位系统中是4字节,在64位系统中是8字节。因为指针指示的是一个内存地址,所以与操作系统有关。但这个也不是绝对正确的,因为64位系统兼容32位,对应的32程序的指针也是32位的,此时使用sizeof()得到的便是4(32位),例如编写win32程序时,指针就是32位。

 

2 extern,const,static,volatile关键字(定义,用途)

extern关键字的作用:

1extern用在变量或者函数的声明前,用来说明此变量/函数是在别处定义的,要在此处引用extern声明不是定义,即不分配存储空间。也就是说,在一个文件中定义了变量和函数, 在其他文件中要使用它们, 可以有两种方式:1.使用头文件,在头文件中声明它们,然后其他文件去包含头文件;2.在其他文件中直接extern,就可以使用。

2extern C作用:extern “C” 不但具有上述传统的声明外部变量的功能,还具有告知C++链接器使用C函数规范来链接的功能。 还具有告知C++编译器使用C规范来命名的功能。(因为C++支持函数的重载,C++编译器如果以C++规范翻译这个函数名时会把函数名翻译得很乱。)

 

static关键字的作用:

1、函数体内static变量的作用范围为该函数体,该变量的内存只被分配一次,其值在下次调用的时候仍然维持原始值,要是函数体内有对该变量进行更改的行为,再次访问时变量的值是更改后的值。

2 在文件内的static全局变量和static全局函数可以被文件内可见,不能被其他文件可见。其他文件内可以有相同名字的其他的对象和函数,即文件范围的static可以限定变量在在文件范围内部,对其他文件不可见。而非static全局变量和全局函数可以在文件间使用。

3、在类中的static成员变量属于整个类所有,对类的所有对象只有一份拷贝。存储在静态存储区。静态数据成员可以被初始化,初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆;若未对静态数据成员赋初值,则编译器会自动为其初始化为0。全局变量和静态变量存储在静态数据区,在全局静态数据区,内存中所有的字节默认值都是0x00

4、在类中的static成员函数属于整个类所有,static成员函数不接受this指针,没有this指针,因而只能访问类的static成员变量和static成员函数。不能作为虚函数。

    不能将静态成员函数定义为虚函数:虚函数依靠vptrvtable来处理。vptr是一个指针,在类的构造函数中初始化,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable.对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual

   虚函数的调用关系:this -> vptr -> vtable ->virtual function

 

const关键字的作用:const意味着只读 const离谁近,谁就不能被修改;

1、想要阻止一个变量被改变,可以使用const关键字。在定义该const关键字时,通常要对它进行初始化,因为以后再也没有机会去改变它。

2、对于指针来说,可以指定指针本身为const,也可以指定指针所指向的数据为const,或者二者同时指定为const

3、在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值。如果形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用 const 来限制。

4、对于类的成员函数,若指定为const,则表明其实一个常函数,只有类的成员函数有常函数的说法,不能修改类的非静态成员变量。当确定类成员函数不会改变成员变量时,一定将其设为const的;类的const的对象只能调用其const成员函数,因为调用非const函数就有改变变量属性的风险。

5、对于类的成员函数,有时候必须制定其返回值为const,以使得其返回值不能为左值。效率考虑,参数传递,返回值尽量返回const&,除了必须值返回(返回的是一个函数内的临时对象,离开作用域对象清除,此时不能引用返回,必须值返回。)和可变引用&(如对象的操作符重载,需要连续赋值的情况,或cout的情况,必须使用可变引用)

6. const修饰成员变量,必须在构造函数列表中初始化;同时成员数据为引用的也必须在构造函数列表中初始化;static成员数据的初始化,放在类定义外,不加static,若static成员数据没有初始化,则默认为0

 

volatile关键字的作用:

volatile int iNum = 10;

volatile 指出 iNum 是随时可能发生编译器觉察不到的变化的变量,变量可能被某些编译器未知的因素(比如:操作系统、硬件或者其它线程等更改),编译器觉察不到。

程序执行中每次使用它的时候必须从原始内存地址中去读取,因而编译器生成的汇编代码会重新从iNum的原始内存地址中去读取数据。而不是只要编译器发现iNum的值没有发生变化(因为可能是已经发生了变化编译器觉察不到),就只读取一次数据,并放入寄存器中,下次直接从寄存器中去取值(优化做法),而是重新从内存中去读取(不再优化).

 

3 #define const的区别(编译阶段、安全性、内存占用等)

1编译器处理方式不同
  define宏是在预处理阶段展开。
  const常量是编译运行阶段使用。
2 类型和安全检查不同
  define宏没有类型,不做任何类型检查,仅仅是展开。
  const常量有具体的类型,在编译阶段会执行类型检查。
3 存储方式不同
  define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。
  const常量会在内存中分配(可以是堆中也可以是栈中)

 

4)关于typedef#define;

typedef 定义一种类型的别名,不同于宏,它不是简单的字符串替换。如:

typedef void (*pFunParam)(int); pFunx b[10]; 定义了一个函数指针类型的数组,该函数指针指向的函数原型void fun(int)的函数实体

 

typedef #define的区别案例:

  1. typedef char *pStr1;    #define pStr2 char *;    pStr1 s1, s2;     pStr2 s3, s4; 
  2. 在上述的变量定义中,s1s2s3都被定义为char *,而s4则定义成了char,不是我们所预期的指针变量,根本原因就在于#define只是简单的字符串替换typedef则是为一个类型起新名字。

STL中通过将typedef 写在类内部和模板的泛化偏特化,特别针对指针类型实现迭代器的特性萃取。 struct里边写typedef int Aa并不会使得 对象的空间增大。

 

5C++程序中内存使用情况分析(堆和栈的区别)

 

C++中,内存分成5个区,他们分别是栈、堆、自由存储区(可以和堆不区分)、全局/静态存储区,常量存储区。一个由C/C++编译的程序占用的内存分为以下几个部分  

栈(堆栈)由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 

堆: 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配组织方式倒是类似于链表。常用C++new/delete 运算符Cmalloc()/free()realloc等函数;

全局区(静态区)static: 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。  程序结束后有系统释放 。函数和成员函数代码也存储在静态区。

常量区:存放常量,字符串,保证常量不被修改; 程序结束后由系统释放 

程序代码区:存放函数体的二进制代码。静态区。

 

6new malloc的异同处,newdelete是如何实现的。

new的实现过程: 

1.调用相应的 operator new(size_t) 函数,如果 operator new(size_t) 不能成功获得内存,则调用 new_handler() 函数用于处理new失败问题。可以用set_  new_handler()函数设置不同的new_handler() 函数实现不同的内存分配失败时的处理策略。operator new(size_t) 函数可以重载,但是必须包含size_t参数,不同的重载形式,对应到不同形式的new,placement_new operator new的内存分配底层实现调用的也是malloc()函数。

2.在分配到的动态内存块上 调用相应类型的构造函数构造对象并返回其首地址。如果构造函数调用失败。则自动调用operate new对应的operator delete;释放内存。

new包含的分配内存和构造对象两个过程必须都要完成。

delete的实现过程: 

1,先调用对应内存上对象的析构函数、2调用相应的 operator delete(void *) 函数。 operator delete(void *)也是调用free()释放内存。

new malloc的异同处:1new/delete属于运算符,malloc/free属于库函数。2.malloc在申请内存空间失败以后会返回一个null指针,而new在申请内存空间失败以后会返回一个异常。也可以使用nothrownew失败返回空指针,照顾c程序员的编程习惯。3.malloc只负责申请内存,他不能对内存进行初始化,new不仅能申请内存,还可以对内存进行初始化和调用对应对象的构造函数。newC++的运算符,底层的内存分配动作仍然是通过malloc()实现,通过new_handle引入对内存分配失败的处理机制。New没有类似relloc的机制。

malloc分配的内存不够的时候,可以用realloc扩容。realloc是从堆上分配内存的。当扩大一块内存空间时,realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,自然天下太平;如果数据后面的字节不够,那么就使用堆上第一个有足够大的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上。这句话传递的一个重要的信息就是数据可能被移动。使用realloc无需手动把旧的内存空间释放. 因为realloc 函数改变地址后会自动释放旧的内存。

new如果分配失败了会抛出bad_alloc的异常,而malloc失败了会返回NULL。因此对于 new,正确的姿势是采用try...catch语法,而malloc则应该判断指针的返回值。为了兼容很多c程序员的习惯,C++也可以采用new(nothrow)的方法禁止抛出异常而返回NULL   new(nothrow)也是通过重载operator new实现的一种placement newNew是使用new hander 处理内存分配失败的情况。

assert 一种预处理宏,使用单个表达式作为断言条件。若条件为false, assert输出信息并终止程序的执行。为true do nothing

 

7 CC++的区别

C++ =C+OOP(面向对象,多态)+GP(泛型编程,模板,STL,模板元编程)+异常处理。


 sizeof()类的大小:

class A {};: sizeof(A) = 1;//空类大小为1g++中每个空类型的实例占1字节空间。
class A { virtual Fun(){} }; sizeof(A) = 4;: //存在虚函数,即存在一个虚指针

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;

 

8C++中的重载,重写,重定义(隐藏)的区别:

重载:全局函数之间或同一个类体里的成员函数之间,函数名相同,参数不同(参数个数,类型)。注意成员函数是否是const的也是不同的重载。函数是否是const只有成员函数。函数返回值不参与重载判定。

重写:子类对父类的重写,要求子类函数与父类函数完全相同,除了修饰符可以不同,比如父类private,子类可以是public。此外,最重要的一点就是,父类的函数必须是虚函数,也就是要有virtual来修饰。父类的虚函数,子类可以重写出自己的版本,可以不重写直接继承父类的版本。对于父类的纯虚函数,子类必须重写自己的版本;有纯虚函数函数的类为抽象类,抽象类不可实例化。抽象基类类似于Java的接口,都不可实例化。抽象基类中的纯虚函数类似于接口中的方法,实现接口的类必须实现接口中的方法。

重定义:子类有父类同名函数,父类的函数就会被隐藏,调用子类对象只能调用子类的函数。这种情况只是简单的作用域限制,不具有面向对象的特性。


9析构函数一般写成虚函数的原因。

 

什么时候类需要定义析构函数:如果类的数据成员中不存在成员(指针)与动态分配的内存相关联,我们一般不用自己定义析构函数,而是采用默认的析构函数析构类对象。一旦与动态分配的内存相关联,为了防止内存泄露,我们需要自己定义析构函数,手动释放动态分配的内存。因为系统默认的析构函数是无法帮助释放动态内存的。因为系统只会释放栈内存,分配的动态内存(堆内存)必须由程序手头释放。

三法则:如果一个类需要析构函数,几乎也需要定义赋值构造函数和重载赋值操作符。因为此时类的成员有指针,此时不能使用默认的复制构造,赋值运算符。

 

析构函数一般写成虚函数的原因:

在类的继承体系中,在分析基类析构函数为什么要定义为虚析构函数之前,我们要先明白虚函数存在的意义就是为了动态绑定,实现面向对象的特性之一 :多态。

我们知道通过基类的指针或者引用可以实现对虚函数的动态绑定,那么当我们通过一个基类指针或者引用来析构一个对象时,我们是无法判断基类现在绑定的对象是基类还是派生类,如果析构函数不是虚函数,那么基类指针只会调用基类的析构函数,如此就发生了一些不该发生的事。只有将析构函数定义为虚函数,才能通过动态绑定,调用对应的析构函数版本,正确的析构类对象。

可以这么说:任何class只要有virtual函数都几乎确定也要有一个virtual析构函数(引用自Effective C++ 条款7)

 

10)构造函数为什么一般不定义为虚函数

构造函数不能为虚函数主要有以下两点:

1、必要性分析:当定义派生类对象时,它会主动依次调用构造函数,顺序为基类的构造函数->一级派生类构造函数->二级派生类构造函数….直到当前派生类的构造函数调用完毕为止,到此派生类对象生成。 而虚函数存在的意义为动态绑定,从上一段话可知,它会从基类开始依次自动调用相应的构造函数,根本就不存在动态绑定的必要。

2、内存角度分析: 
构造函数的作用是生成相应的类对象。虚函数的动态绑定是依据一张虚函数表来确认的最终绑定到哪一个虚函数版本。 而调用构造函数之前,我们对类对象所做的操作仅限于分配内存,还没有对内存进行初始化。此时,内存空间上也不存在虚函数表,因此,按照这样的执行顺序,虚函数的动态绑定是实现不了的。


11构造函数或者析构函数中调用虚函数会怎样。

从语法上讲,调用完全没有问题。但是从效果上看,往往不能达到需要的目的。

1.构造:派生类对象构造期间会首先进入基类的构造函数,在基类构造函数执行时继承类的成员变量尚未初始化,此时调用虚函数,调用的一定是基类的虚函数版本,因为继承类的成员变量尚未初始化,此时对象类型是基类类型,vptr指向的也是基类的vptb,调用不到派生类的虚函数版本。此时虚函数和普通函数没有区别了。起不到多态的效果。

2.析构:假设一个派生类的对象进行析构,首先调用了派生类的析构,然后再调用基类的析构时,遇到了一个虚函数,这个时候有两种选择:Plan A是编译器调用这个虚函数的基类版本,那么虚函数则失去了运行时调用正确版本的意义;Plan B是编译器调用这个虚函数的派生类版本,但是此时对象的派生类部分已经完成析构,数据成员就被视为未定义的值,这个函数调用会导致未知行为。

总结:调用虚函数时,对应的基类或者派生类对象都必须是一个完整正确的对象状态。而在构造或者析构的过程中对象不是一个完整的状态。

 

12析构函数能抛出异常吗

不能。C++标准指明析构函数不能、也不应该抛出异常。

(1) 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。

(2) 通常异常发生时,c++的异常处理机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

 

13纯虚函数和抽象类

virtual ~myClass()=0;有纯虚函数的类是抽象类,不能实例化,抽象类的功能类似于Java的接口。


14多态的实现条件,虚指针vptr, 虚函数表vbtl 

静态绑定和动态绑定:

静态绑定是通过重载和模板技术实现,在编译的时候确定。动态绑定通过虚函数和继承关系来实现,执行动态绑定,在运行的时候确定。

多态实现有几个条件:1.虚函数  2.一个基类的指针或引用指向派生类的对象

基类指针在调用成员函数(虚函数)时,就会通过对象的虚指针vptr去查找该对象的vptl虚函数表。虚函数表的地址vptr在每个对象的首地址。查找该虚函数表中该虚函数的函数指针进行调用。

每个对象中保存的只是一个虚函数表的指针,C++内部为每一个类维持一个虚函数表,该类的对象都指向这同一个虚函数表。

虚函数表中为什么就能准确查找相应的函数指针呢?因为在类设计的时候,派生类的虚函数表是直接从基类继承过来的,如果派生类的虚函数overiide某个基类的虚函数,那么虚函数表的函数指针就会被替换,因此可以根据指针准确找到该调用哪个函数。

虚函数在设计上还具有封装和抽象的作用。比如抽象工厂模式???

 

15深拷贝和浅拷贝的区别(举例说明深拷贝的安全性) 

浅拷贝在针对有指针的类时,会导致一个后果。两个指针指向同一块内存,在释放内存时,该内存会被释放两次,这就会有内存泄露的危险。

深拷贝,指先获取一块内存,然后将要拷贝的内容复制过去。两个指针指向不同的内存,就不会有内存泄露的风险了。

浅拷贝是没有定义拷贝构造函数时系统的默认拷贝构造函数的拷贝方式。

所以,在对含有指针成员的对象(有动态分配内存的对象)进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。


16什么情况下会调用拷贝构造函数(三种情况)

用类的一个对象去初始化另一个对象时;当函数的形参值传递对象时;当函数的返回值是以值传递对象。

17struct内存对齐方式,为什么要对齐?怎么修改默认对齐方式?struct,union

0位置开始存储; 成员变量存储的起始位置是该变量大小的整数倍; 结构体总的大小是其最大元素的整数倍,不足的后面要补齐;。当CPU访问正确对齐的数据时,它的运行效率最高。

Structunion: union

  (1). 共用体和结构体都是由多个不同的数据类型成员组成, 但在任何同一时刻, 共用体只存放了一个被选中的成员, 而结构体的所有成员都存在。

(2). 对于共用体的不同成员赋值,原来成员的值就不存在了,成为了无定义状态。 而对于结构体的不同成员赋值是互不影响的

   修改对齐方式:#pragma pack (2) /*指定按2字节对齐*/

#pragma pack () /*取消指定对齐,恢复缺省对齐*/

 

18)内存泄露的定义,如何检测与避免?内存检查工具的了解 

内存泄漏:内存泄漏指的是在程序里动态申请的内存在使用完后,没有进行释放,导致这部分内存没有被系统回收,久而久之,可能导致程序内存不断增大,系统内存不足。排除内存泄漏对于程序的稳健型特别重要,尤其是程序需要长时间、稳定地运行时。

检查工具:1.Linux下通过工具valgrind检测。2.VS中的

定位内存泄露:

2. windows平台下通过CRT中的库函数进行检测;(只适用于Debug环境下

VS2013中在Debug环境下,通过CRT库本身的内存泄漏检测函数能够分析出内存泄漏,定位内存泄露的位置。 

检查方法:一.main函数最后一行,加上一句_CrtDumpMemoryLeaks()。调试程序,自然关闭程序让其退出(不要定制调试),查看输出如下:

{453} normal block at 0x02432CA8, 868 bytes long. 

{}包围的453就是我们需要的内存泄漏定位值(编译器的内存分配编号)868 bytes long就是说这个地方有868比特内存没有释放。此时只能知道在哪一次的内存分配(编译器的内存分配编号)在程序结束没有释放发生内存泄露,并没有定位到具体的内存泄露的代码行。

接下来,定位代码位置:

main函数第一行加上:_CrtSetBreakAlloc(453); 意思就是让程序执行到申请453这块内存的位置中断。然后调试程序,……程序中断了。查看调用堆栈。双击我们的代码调用的最后一个函数(栈顶),这里是CDbQuery::UpdateDatas(),就定位到了申请内存的代码:

 

在线上运行的时候:

对象计数

方法:在对象构造时计数++,析构时--,每隔一段时间打印对象的数量;若发现对象的个数只增不减的异常,则可以推测该类的对象发生了内存泄露。

优点:没有性能开销,几乎不占用额外内存。定位结果精确。

缺点:侵入式方法,需修改现有代码,而且对于第三方库、STL容器、脚本泄漏等因无法修改代码而无法定位。

Hook Windows系统API

方法:使用windows分配内存的系统ApiHeapAlloc/HeapRealloc/HeapFreenew/malloc的底层调用),记录分配点,定期打印。

优点:非侵入式方法,无需修改现有文件,检查全面,对第三方库、脚本库等等都能统计到。

缺点:记录内存需要占用大量内存,而且多线程环境需要加锁。

 

19成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?

使用成员初始化列表定义构造函数是显式地初始化类的成员,如果不用成员初始化列表,那么类对象对自己的类成员分别进行的是一次隐式的默认构造函数的调用(在进入函数体之前)初始化类的成员,和一次拷贝赋值运算符的调用(进入函数体之后),如果是类对象,这样做效率就得不到保障。

类类型的数据成员对象在进入构造函数体前己经构造完成,也就是说在成员初始化列表处进行对象的构造工作,调用构造函数,在进入函数体之后,进行的是对己构造好的类对象赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器默认的按成员赋值行为))。

 

20必须在构造函数初始化列表里进行初始化的数据成员有哪些

(1) 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
(2) 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
(3) 没有默认构造函数的类类型若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若成员类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。

对象成员:A类的成员是B类的对象,在构造A类时需对B类的对象进行构造,当B类没有默认构造函数时需要在A类的构造函数初始化列表中对B类对象初始化

类的继承:派生类在构造函数中要对自身成员初始化,也要对继承过来的基类成员进行初始化,当基类没有默认构造函数的时候,通过在派生类的构造函数初始化列表中调用基类的构造函数实现。


21 C++的调用惯例(C++函数调用的压栈过程)

在函数调用时,第一个进栈的是主函数中调用点后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。 静态变量在全局静态局。
    当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。


22C++的四种强制转换static_castconst_castdynamic_castreinterpret_cast

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

static_cast

 static_cast用的最多,对于各种隐式转换如intdouble,非constconstvoid*转类型指针等。

C转换 (type) expression             C++转换:static_cast<type>(expression)  

 

const_cast

只可以用来移除表达式的常量性; 

dynamic_cast:在多态的环境下向下转型。

用在继承体系多态环境中,将指向基类对象的指针或引用转型为指向派生类对象的指针或引用。若指向基类对象的指针或引用在运行时接受的一个派生类的对象,则转型成功。否则转型失败,会以一个null指针(当转型对象是指针)或一个exception(当转型对象是引用)表现出来。

reinterpret_cast:转换函数指针类型:

例:设有一数组,存储的是函数指针,有特定类型;

//FuncPtr是函数指针,指向某个函数,该函数无参数,返回类型为void

typedef void (*FuncPtr)();

FuncPtr funcPtrArray[10];//funcPtrArray是个数组,有10FuncPt

若想将以下函数的一个指针n放入该数组:

dosomething的类型与funcPtrArray接受的类型不同。funcPtrArray内各函数指针所指向的函数返回类型是void

funcPtrArray[0]=&dosomething;//

funcPtrArray[0]=reinterpret_cast<FuncPtr>(&dosomething);//对。

注:函数指针的转型动作不具移植性(C++不保证所有的函数指针都能以此方式重新呈现)。某些情况下这样的转型可能会导致不正确的结果。

23多重继承,菱形结构,虚基类,虚继承,以及在多继承虚继承下多态的实现,虚继承下对象的内存分布

 

 多重继承在菱形结构的情形下,往往导致virtual base classes(虚拟基类)的需求。在non-virtual base的情况下,如果派生类对于基类有多条继承路径,那么派生类会有不止一个基类部分,使用虚继承,让基类为virtual可以消除这样的复制现象。然而虚基类也可能导致另一成本:其实现做法常常利用指针,指向"virtual base class"部分,因此对象内可能出现一个(或多个)这样的指针。

 

24)内联函数有什么优点?内联函数与宏定义的区别?

  1. 宏定义在预编译的时候就会进行宏替换;
  2. 内联函数在编译阶段,在调用内联函数的地方进行替换,减少了函数的调用过程的系统开销,但是使得编译文件变大。因此,内联函数适合简单函数,对于复杂函数,即使定义了内联编译器可能也不会按照内联的方式进行编译。
  3. 内联函数相比宏定义更安全,内联函数可以检查参数,而宏定义只是简单的文本替换。因此推荐使用内联函数,而不是宏定义。

虚函数不可以内联:内联在编译的时候替换,但只有在运行时才能确定调用哪个虚函数)

 

25STL容器迭代器失效总结.

 

vector 迭代器失效情况:

1.push_back()一定使得end()返回的迭代器失效,若发生capacity()增长,导致vector容器的所有迭代器都失效。因为发生了数据移动。

2. erase()使得删除点和删除点后面的迭代器都失效。失效的迭代器不可以进行迭代器操作,如++iter,*iter,指向的是位置内存。但erase(iter)可以返回下一个有效的迭代器。erase的返回值是删除元素下一个元素的迭代器。这个迭代器是vector内存调整过后新的有效的迭代器。

list,set, map 迭代器失效情况:

使用了不连续分配的内存,删除当前的iterator,仅仅会使当前的iterator失效, erase迭代器返回值为void,所以要采用erase(iter++)的方式删除迭代器。如:

解析dataMap.erase(iter++);这句话分三步走,先把iter值传递到erase里面,然后iter自增,然后执行erase,所以iter在失效前已经自增了。

list中,erase(*iter)会返回下一个有效迭代器的值, erase(iter)也会返回void,也需使用erase(iter++)的方式删除迭代器。

deque迭代器失效情况:

 1.deque容器首部或者尾部插入元素不会使得任何迭代器失效。2. 在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效。3.deque容器的任何其他位置的插入和删除操作将使指向该容器元素的所有迭代器失效。

 

26.)对继承和组合的理解

继承是一种is-a关系,组合是一种has-a关系。在功能上来看,它们都是实现功能重用,代码复用的最常用的有效的设计技巧,都是在设计模式中的基础结构。类继承允许我们根据自己的实现来覆盖重写父类的实现细节,父类的实现对于子类是可见的,所以我们一般称之为白盒复用。对象持有通常用来实现配接,如,STLdequestack,整体类对部分类的功能的复用接口的修饰,使其成为另一种特定的面貌。整体类和部分类之间不会去关心各自的实现细节。即它们之间的实现细节是不可见的,故成为黑盒复用。

继承中父类定义了子类的部分实现,而子类中又会重写这些实现,修改父类的实现,设计模式中认为这是一种破坏了父类的封装性的表现。这个结构导致结果是父类实现的任何变化,必然导致子类的改变。然而组合这不会出现这种现象。对象的组合还有一个优点就是有助于保持每个类被封装,并被集中在单个任务上(类设计的单一原则)。这样类的层次结构不会扩大,一般不会出现不可控的庞然大类。而类的继承就可能出来这些问题,所以一般编码规范都要求类的层次结构不要超过3层。

一般优先优先使用对象组合,而不是类继承。

 

27c++ main函数执行之前需要做哪些准备

1.  设置栈指针

2.  non-local static对象构造完成。

non-local static对象包括文件下(全局),命名空间下,类的static对象成员,non-local static对象要在main函数之前构造。函数中的static对象是local static对象,local static对象直到方法被调用的时候,才进行初始化,而且只初始化一次。local static 变量(局部静态变量)同样是在main前就已分配内存,第一次使用时初始化。所有的static对象都分配在全局区,程序结束才释放内存。

3. 将未初始化部分的赋初值:数值型shortintlong等为0boolFALSE,指针为NULL,等等,即.bss段的内容

4..运行全局构造器,估计是C++中构造函数之类的吧

5.main函数的参数,argcargv等传递给main函数,然后才真正运行main函数。,

全局变量、non-local static变量在main执行之前就已分配内存并初始化;local static 变量同样是在main前就已分配内存,第一次使用时初始化。

 

28)手写智能指针shared_ptr什么时候改变引用计数?weak_ptr如何解决引用传递这些是线程安全的吗?线程安全的智能指针是哪一个?

智能指针:使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

引用计数改变:构造函数中计数初始化为1;拷贝构造函数中计数值加1;析构函数中引用计数减1;赋值运算符中,左边的对象引用计数减/1,右边的对象引用计数加1;在赋值运算符和析构函数中,如果减1后为0,则调用delete销毁对象并释放它占用的内存

unique_ptr唯一拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。

weak_ptr是为了配合shared_ptr而引入的一种智能指针,不具有普通指针的行为,没有重载operator*->,作用在于协助shared_ptr工作,观测资源的使用情况。成员函数use_count()可以观测资源的引用计数,成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象,从而操作资源。

 

shared_ptr的循环引用导致的内存泄漏怎么解决?

https://www.cnblogs.com/itZhy/archive/2012/10/01/2709904.html

使用weak_ptr

http://www.cnblogs.com/TianFang/archive/2008/09/20/1294590.html

 

类成员变量用memset()设置会有什么问题?

不能,因为memset会破坏成员变量对象的内部结构(都赋值为0),当类对象析构时,析构到该成员变量对象时,该成员变量对象不能正常进行析构操作,最终导致crash

注:如果类包含虚函数,则不能用 memset 来初始化类对象。因为包含虚函数的类对象都有一个虚指针指向虚函数表(vtbl),进行memset操作时,虚指针的值也要被overwrite,这样一来,只要一调用虚函数,程序便崩溃。

 

(30)STL alloc实现,alloc的优势和局限,STL中其他的配置器

gnuC中使用了内存池设计,减小了小内存分配的分配次数,提高效率。减少内存的碎片化。但是同时内存池的设计只分配不释放(只拿不还,服务容器),alloc在运行期间不会释放分配的内存。这种占用可能使得其他的进程不能获得足够的内存。在gunc4.9 中有其他的配置器。给8k16k,..., 128k比较小的内存片都维持一个空闲链表。

_pool_alloc, loki_allocator

31)模板的用法与适用场景,模板泛化,偏特化,特化,可变模板参数,举出实例。

 

29)单例模式,C++实现一个线程安全的单例类;用C++设计一个不能被继承的类;如何定义一个只能在堆上(栈上)生成对象? fianl对象 

单例模式:一个类只能被实例化一次,并提供一个访问它的全局访问点。

 

饿汉和懒汉:懒汉式在第一次用到类实例的时候才会去实例化,通常需要用加锁机制实现线程安全。饿汉式在单例类定义的时候就进行实例化。使用no-local static变量存储单例对象,类一加载就实例化。会提前占用系统资源。

特点与选择:由于要进行线程同步,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。这是以空间换时间。在访问量较小时,采用懒汉实现。这是以时间换空间。

 

分析:instance是非局部静态变量,在main执行前就分配内存并初始化,是线程安全的。潜在问题在于no-local static对象(函数外的static对象)在不同编译单元(可理解为cpp文件和其包含的头文件)中的初始化顺序是未定义的。

 

使用场景: 在整个项目中需要一个共享访问点或共享数据,或者类似的实体(有且只有一个,且需要全局访问),那么就可以将其实现为一个单例。

例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;

日志类,一个应用往往只对应一个日志实例;

管理器,比如windows系统的任务管理器就是一个例子,总是只有一个管理器的实例。

单例模式常常与工厂模式结合使用,因为工厂只需要创建产品实例就可以了,在多线程的环境下也不会造成任何的冲突,因此只需要一个工厂实例就可以了。

 

只能建立在堆上:将析构函数设为私有,类对象就无法建立在栈上了。当对象建立在栈上时,是由编译器分配内存空间的,编译器调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。

只能建立在栈上:在类的内部重载operator new(),并设为私有即可。只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。

设计一个fianl: 法一:构造析构放在私有,public中放一个static接口函数,用来创建和释放类的实例。,但是该类只能得到位于堆上的实例,而得不到位于栈上实例。

法二:友元函数;https://www.cnblogs.com/luxiaoxun/archive/2013/06/07/3124948.html

C++11中已经有了final关键字:作用是指定一个类成为一个不能被继承的类(final class),或者指定类的虚函数不能被该类的继承类重写(override),。

使用场景:当一个方法被final修饰后。表示该方法不能被子类重写。比如涉及到某些需要统一处理的需求。

 

  • cmakemakefile的区别   简述cmake到可执行文件的过程

 

make: 一个自动化编译工具,依据makefile文件(编译规则) 批处理编译多个源文件。

cmake:一个读入源文件,自动生成makefile文件的工具,cmakelist文件是cmake工具生成makefile文件的规则,cmakelist通常由程序员编写。

https://blog.csdn.net/weixin_42491857/article/details/80741060

 

  • 问有没有用过shrink_to_fit,说一下作用,为什么用

capicity减少到元素个数,减少容量

,如:vector<int>(ivec).swap(ivec); ivec  shrink_to_fit

 表达式vector<int>(ivec)建立一个临时vector,它是ivec的一份拷贝:vector的拷贝构造函数做了这个工作。但是,vector的拷贝构造函数只分配拷贝的元素需要的内存,所以这个临时vector没有多余的容量。然后我们让临时vectorivec交换数据,这时,ivec只有临时变量的修整过的容量,而这个临时变量则持有了曾经在ivec中的没用到的过剩容量。最后,临时vector被销毁,因此释放了以前ivec使用的内存,收缩到合适。

  •   char (*p) [5] char *p[5]char (*p)()的区别?指向数组的指针,指针数组,函数指针,

char (*p) [5]:定义了个指针,指针指向一个有5个char的数组;

char *p[5]:定义了一个数组,里面有5个指向char的指针;

char (*p)():函数指针,指向 char fun();类型的函数;

 

  • 如何防止一个类被拷贝

是将构造函数和拷贝构造函数声明为private,或者采用c++11delete关键字,

delete关键字可用来禁用某种类型的函数,unique_ptr只能使用移动构造函数,使用delete关键字禁用了拷贝构造函数。

 

  • c++怎么实现一个函数先于main函数运行,后于main函数执行 

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

main函数执行后:全局/静态对象的析构函数会在main函数之后执行;可以用atexit()来注册程序正常终止时要被调用的函数,并且在main函数结束时,调用这些函数,调用顺序与他们被注册时相反

无论程序是否正常退出,都可以用atexit()来调用资源释放的函数;

  • 如何删除map中的奇数节点

遍历删除,考虑迭代器失效问题

  1. for(ITER iter=mapTest.begin();iter!=mapTest.end();++iter)

{ ifiter指向的元素是奇数

    mapTest.erase(iter);

} //错误,erase会让迭代器会失效!

 

  1. for(ITER iter=mapTest.begin();iter!=mapTest.end();)

{ ifiter指向的元素是奇数

mapTest.erase(iter++);//正确,iter值传递之后,再++

} 

  1. for(ITER iter=mapTest.begin();iter!=mapTest.end();)

{ ifiter指向的元素是奇数
iter=mapTest.erase(iter);// erase() 成员函数返回下一个元素的迭代器

} 

 

 

  • C++的分离式编译  为什么C++类模板不支持分离式编译?

C++的分离式编译:c++开发中广泛使用声明和实现分开的开发形式,其编译过程是分离式编译,就是说各个cpp文件完全分开编译,然后生成各自的obj目标文件,最后通过连接器link生成一个可执行的exe文件。一个编译单元(translation unit)是指一个.cpp文件以及它所#include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件。.obj文件已经是二进制码,但是不一定能够执行,因为并不保证其中一定有main函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由连接器(linker)进行连接成为一个.exe文件。

C++类模板不支持分离式编译:模板代码的实现体在一个文件里,而实例化模板的测试代码在另一个文件里,编译器编译一个文件时并不知道另一个文件的存在,因此,模板代码就没有进行实例化,编译器自然不会为其生成代码,因此会抛出一个链接错误!

C++类模板不支持分离式编译,即我们必须把类模板的声明和定义写在同一个.h文件中;

 

  • 函数重载

函数重载考虑参数个数,参数类型,不考虑函数返回值类型(函数调用时独立于上下文),

 

两个文件a,b,文件内部分别定义两个全局变量,用g 编译的时候如何保证两个全局变量初化顺序

全局变量  int   a   =   5;   int   b   =   a;  在不同文件中,不能保证b也等于5,也就是说不能保证a先初始化。

解决这种问题的方法是不直接使用全局变量,而改用一个包装函数来访问,例如   

 int   get_a()   
  {   
          static   int   a   =   5;   
          return   a;   
  }   


  int   get_b()   
  {   
          static   int   b   =   get_a();   
          return   b;   
  }   

这样的话,无论get_aget_b是否定义在同一个文件中,get_b总是能够返回正确的结果,原因在于,函数内部的静态变量是在第一次访问的时候来初始化。 

 

哈希表的冲突处理和数据迁移。

处理冲突:hash表实际上由size个的桶组成一个桶数组table[0...size-1] 。当一个对象经过哈希之后。得到一个对应的value , 于是我们把这个对象放到桶table[ value ]中。当一个桶中有多个对象时。我们把桶中的对象组织成为一个链表。这在冲突处理上称之为拉链法。

负载因子: 如果一个hash表中桶的个数为 size , 存储的元素个数为used .则我们称 used / size 为负载因子loadFactor . 一般的情况下,当loadFactor<=1时,hash表查找的期望复杂度为O(1). 因此。每次往hash表中加入元素时。我们必须保证是在loadFactor <1的情况下,才可以加入。

数据迁移:Hash表中每次发现loadFactor==1时,就开辟一个原来桶数组的两倍空间(称为新桶数组),然后把原来的桶数组中元素所有转移过来到新的桶数组中。注意这里转移是须要元素一个个又一次哈希到新桶中的。

缺点:容量扩张是一次完毕的,期间要花很长时间一次转移原hash表中的全部元素。

改进: redis中的dict.c中的设计思路是用两个hash表来进行扩容和转移的工作:当第一个hash表的loadFactor=1时,假设要往字典里插入一个元素。首先为第二个hash表开辟2倍第一个hash表的容量。同一时候将第一个hash表的一个非空桶中所有元素转移到第二个hash表中。然后把待插入元素存储到第二个hash表里。继续往字典里插入第二个元素,又会将第一个hash表的一个非空桶中所有元素转移到第二个hash表中,然后把元素存储到第二个hash表里……直到第一个hash表为空。

      这样的策略就把第一个hash表全部元素的转移分摊为多次转移,并且每次转移的期望时间复杂度为O(1)

 

vector的容量扩张为什么是2  最好的策略是什么?reverse()

vector 在需要的时候会扩容,在 VS 下是 1.5倍,在 GCC 下是 2 倍。

  1. 为什么是成倍增长,而不是每次增长一个固定大小的容量呢?

答:采用成倍方式扩容,可以保证push_back 常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度

  1. 为什么是以 2 倍或者 1.5 倍增长,而不是以 3 倍或者 4 倍等增长呢?

大于2 倍的方式扩容,下一次申请的内存会大于之前分配内存的总和,导致之前分配的内存不能再被使用。所以,最好的增长因子在 1,2)之间。

数学上的证明:当 k =1.5 时,在几次扩展以后,可以重用之前的内存空间了

 

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

 

C语言里面字符串,strcpystrncpy的区别?哪个函数更安全?

strcpy函数:把从src地址开始且含有NULL结束符的字符串赋值到以dest开始的地址空间,返回dest(地址中存储的为复制后的新值)。要求:srcdest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。

 strncpy函数:将字符串src中最多n个字符复制到字符数组dest(它并不像strcpy一样遇到NULL才停止复制,而是等凑够n个字符才开始复制),返回指向dest的指针。要求:如果n > dest串长度,dest栈空间溢出产生崩溃异常。

 

安全性分析:strncpy要比strcpy安全得多,strcpy无法控制拷贝的长度,不小心就会出现dest的大小无法容纳src的情况,就会出现越界的问题,程序就会崩溃。而strncpy就控制了拷贝的字符数避免了这类问题,但是要注意的是dest依然要注意要有足够的空间存放src,而且src dest 所指的内存区域不能重叠,

 

malloc涉及的系统调用(说了brk指针和mmap,没说清楚,非常不满意)。

malloc调用brkmmap系统调用去获取内存。malloc小于128k的内存,使用brk分配内存,malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0)

 

C++11新特性? lambda表达式, =default;, =deleted 函数

Lambda 表达式就是用于创建匿名函数的。

 

lambda表达式的本质就是重载了()运算符的类,这种类通常被称为functor,即行为像函数的类。因此lambda表达式对象其实就是一个匿名的functor。编译器自动将lambda表达式转换成函数对象执行

=default; 指示编译器生成该函数的默认实现。这有两个好处:一是让程序员轻松了,少敲键盘,二是有更好的性能。
defaulted 函数相对的就是 =deleted 函数, 实现 non copy-able 防止对象拷贝,要想禁止拷贝,用 =deleted 声明一下两个关键的成员函数就可以了:

 

  • C语言程序能不能直接调用C++语言编写的动态链接库。

不能,因为C++支持重载,在编译函数的声明时,会改写函数名(可以通过链接指示进行解决);另外,C语言不支持类,无法直接调用类的成员函数(可以通过加入中间层进行解决);C语言也不能调用返回类型或形参类型是类类型的函数。

 

  • Crestrict关键字:

restrictc99标准引入的,它只可以用于限定和约束指针,并表明指针是访问一个数据对象的唯一且初始的方式. 即它告诉编译器,所有修改该指针所指向内存中内容的操作都必须通过该指针来修改, 而不能通过其它途径(其它变量或指针)来修改;这样做的好处是,能帮助编译器进行更好的优化代码,生成更有效率的汇编代码。

现在程序员用restrict修饰一个指针,意思就是只要这个指针活着,我保证这个指针独享这片内存,没有别人可以修改这个指针指向的这片内存,所有修改都得通过这个指针来。由于这个指针的生命周期是已知的,编译器可以放心大胆地把这片内存中前若干字节用寄存器cache起来。

 

  • 通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符。
  • 虚函数表存储在静态存储区 

https://www.cnblogs.com/chenhuan001/p/6485233.html

 

  • 重载、重写和重定义的区别
  1. 1.重载:函数名相同,参数列表不同
  2. 2.重写:也称为覆盖,派生类覆盖基类的虚函数。函数名,参数列表必须相同,返回类型一般也必须相同,存在一个例外(当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。也就是说,如果DB派生得到,则基类的虚函数可以返回B*而派生类的对应函数可以返回D*,只不过这样的返回类型要求从DB的类型装换是可访问的)。

注:(1)静态函数不能被定义为虚函数,也不能被重载

2)重写函数的访问修饰符可以不同

  1. 3.重定义:也称为隐藏,派生类重新定义基类中有相同名称的非虚函数(有相同名称就可)

 

面向对象的三大特性,结合C++语言支持来讲。

 

  1. 1.封装:
  2. 1.封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。
  3. 2.封装有两个重要的优点:确保用户代码不会无意间破坏封装对象的状态;被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。
  1. 2.继承
  1. 1.继承使我们可以更容易地定义与其他类相似但不完全相同的新类。
  2. 2.继承可以实现代码重用,提高软件开发的效率。
  1. 3.多态:一个接口,多种实现,同样的消息被不同的对象接受时导致不同的行为。多态分为静态多态和动态多态。
  1. 1.静态多态通过函数重载、模板实现;
  2. 2.动态多态通过虚函数实现,当使用基类的指针(或引用)调用虚函数时将发生动态绑定,使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。

多态的好处:可以忽略派生类和基类的区别,而以统一的方式使用派生类和基类的对象,提高了代码的复用性和可拓展性。

 

红黑树性质:红黑树是许多平衡搜索树中的一种,可以保证在最坏情况下基本动态操作的时间复杂度为O(lgn)。通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而使近似于平衡。

红黑树须满足条件:

  1. 每个结点或是红色的,或是黑色的。
  2. 根节点是黑色的。
  3. 每个叶结点(NIL)是黑色的。
  4. 如果一个结点是红色的,则它的两个子结点都是黑色的。
  5. 对每个结点,从该结点到其所有后代结点的简单路径上,均包含相同数目的黑色结点。

AVL树和红黑树的区别:

  1. 1.红黑树是近似平衡的二叉树,每次插入和删除操作最多只需要三次旋转就能达到平衡,实现起来也更为简单。
  2. 2.平衡二叉树严格平衡的二叉树,每次插入和删除新节点需要旋转的次数不可预知,实现起来比较复杂。

 

malloc的底层实现:

malloc函数将可用的内存块连接为一个空闲链表。调用malloc函数时,它沿着空闲链表寻找一个大到足以满足用户所需要的内存块。然后,将该内存块一分为二。一块分配给用户使用,另一个块重新连接到空闲链表。当用户申请一个大的内存片段,而内存块被切分为小的内存片段,无法满足用户的请求时,malloc函数请求延时,将相邻的小的空闲块合并成大的内存块。如果找不到合适的内存块,就通过系统调用brk,将break指针向高地址移动,获取新的内存块,连接到空闲链表中。另外,如果所申请的内存大于128k,调用mmap在文件映射区域找一块空闲的虚拟内存。如果分配内存失败,会返回NULL指针。

 

++iteriter++那个好?

前置版本的递增运算符避免了不必要的工作,它把值加1后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个位修改的内容,如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。对于相对复杂的迭代器类型,这种后置版本的操作就是一种浪费。

 

c 实现重载 ? 函数指针

如何突破private的限制?友元函数
C 模拟虚函数?

 

如何设计一个好的字符串hash函数

对于一个Hash函数,评价其优劣的标准应为随机性或离散性,即对任意一组标本,进入Hash表每一个单元(cell)之概率的平均程度,因为这个概率越平均,两个字符串计算出的Hash值相等hash collision的可能越小,数据在表中的分布就越平均,表的空间利用率就越高。

C++ 11 定义了一个新增的哈希结构模板定义于头文件 <functional>std::hash<T>,模板类,(重载了operator()),实现了散列函数: unordered_mapunordered_multimap 默认使用std::hash; std::hash;实现太简单

同时,C++ STL 里面实现了一个万用的hash function 针对任何类型的

 

boost::hash 的实现也是简单取值。

DJBHash是一种非常流行的算法,俗称"Times33"算法。Times33的算法很简单,就是不断的乘33,原型如下:

hash(i) = hash(i-1) * 33 + str[i]Time33在效率和随机性两方面上俱佳

https://blog.csdn.net/g1036583997/article/details/51910598

posted @ 2019-12-23 23:42  谁在写西加加  阅读(1412)  评论(0编辑  收藏  举报