赞
踩
关键字static C++中类的(static)静态成员变量与(static)静态成员函数、c++中static的作用
1)修饰类型
a、修饰全局变量:表明一个全局变量只对定义在同一文件中的函数可见。
b、修饰局部变量:表明该变量的值不会因为函数终止而丢失。
c、修饰函数:表明该函数只在同一文件中调用。
d、修饰类的数据成员:表明对该类所有对象这个数据成员都只有一个实例。即该实例归所有对象共有。
e、修饰类的成员函数:这意味着一个静态成员函数只能访问它的参数、类的静态数据成员和全局变量。
2)静态成员函数与非静态成员函数的区别
静态成员函数只属于类本身,随着类的加载而存在,不属于任何对象,是独立存在的。静态成员函数不存在this指针,不能访问非静态成员变量。静态变量和函数必须在类内声明,在类外初始化。
3)静态变量和函数的优点
a、实现共享:因为静态成员函数和静态成员变量属于类,不属于类的实体,这样可以被多个对象所共享
b、静态成员函数主要为了调用方便,不需要生成对象就能调用。
4)为什么静态成员函数不能申明为const
const修饰符用于表示函数不能修改成员变量的值,该函数必须是含有this指针的类成员函数
关键字const 关于C++ const 的全面总结、C++define和const的区别、C++总结:C++中的const和constexpr
1)修饰类型
a、修饰变量
const int a和int const a含义相同
b、修饰指针 指针常量和常量指针
常量指针表示常量(指针内容)不可变,指针常量表示指针(指针地址)不可变
①、指针常量:指针类型的常量(int *const p)
指针类型的常量,必须初始化,一旦初始化后指针的地址不能改变
可以改值,不能改地址
int a = 97;int b = 98;int* const p = &a;*p = 98;//正确p = &b;//编译出错
②、常量指针:指向常量的指针(int const *p)
所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,但是对象的值可以通过其它途径进行改变。
可以改地址,不能改值
int a = 97;int b = 98; int const *p = &a;*p = 98; //编译出错 p = &b;//正确
c、修饰函数
写在参数中,修饰参数中的形参、指针和引用,防止修改
写在开头,修饰返回值变量或指针
d、修饰类成员变量
const修饰类的成员常量,不能被修改,同时它只能在初始化列表中赋值。
e、修饰类成员函数
写在成员函数后,不改变对象的成员变量,只能调用常量成员函数,不能调用类中任何非const成员函数,能够访问对象的const成员,而其他成员函数不可以。非常量成员函数既可以调用常量成员函数也可以调用非常量成员函数
f、修饰类对象、类指针、类引用
const类对象/指针/引用,表示该对象为常量对象,只能调用类的const成员函数,类其中的任何成员都不能被修改。非常量类对象既可以调用常量成员函数,也可以调用非常量成员函数
2)常量函数成员为什么无法调用非常量数据成员 关于类的常量成员函数和非常量成员函数之间的可调用关系
底层 const的 this指针,无法传参拷贝给非底层 const的指针对象
3)define和const的区别
a、定义类型
define定义的只是个常数不带类型,const定义的常数是变量也带类型。
b、作用阶段
define是在编译的预处理阶段起作用,而const是在编译运行的时候起作用。
c、作用方式
define只是简单的字符串替换,没有类型检查。而const有对应的数据类型,是要进行判断的。
d、占用空间
define占用代码段空间,const占用数据段空间
e、代码调试
define是不能进行调试的,因为在预编译阶段就已经替换掉了,const常量可以进行调试的。
f、重定义
define可以通过#undef取消某个符号的定义,再重新定义,const不能重定义。
g、重复引用
define可以防止头文件重复引用,const不行
关键字volatile C++中volatile关键字介绍
1)易变的
假设有读、写两条语句,依次对同一个 volatile 变量进行操作,那么后一条的读操作不会直接使用前一条的写操作对应的 volatile 变量的寄存器内容,而是重新从内存中读取该 volatile 变量的值。
2)不可优化的
在 C/C++ 语言中,volatile 的第二个特性是“不可优化性”。volatile 会告诉编译器,不要对 volatile 声明的变量进行各种激进的优化(甚至将变量直接消除),从而保证程序员写在代码中的指令一定会被执行。
3)顺序执行的
能够保证 volatile 变量间的顺序性,不会被编译器进行乱序优化。
关键字mutable C++中的mutable关键字
mutalbe的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。
在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。
关键字final:C++如何防止一个类被继承 如何防止一个类被继承、 C++ final关键字
1)final关键字
2)继承友元函数
私有构造函数类的友元函数,该友元函数类虚拟继承自私有构造函数类,该类可以实例化,但无法被继承,继承该类的类无法调用构造函数而实例化失败
关键字struct:类和结构体的区别 C++中Struct与Class的区别与比较
1)自身访问权限
默认的防控属性(访问控制)不同,struct是public的,而class是private
2)继承访问权限
继承关系中默认防控属性的区别,struct默认是public的,而class是private
3)模板使用
class这个关键字还可用于定义模板参数,就像typename。但是strcut不用与定义模板参数
关键字inline 内联函数 —— C 中关键字 inline 用法解析、面试—内联函数和宏定义的区别、关于c内联函数不能有循环递归
1)内联函数
在系统下栈空间是有限的,假如频繁大量的使用就会造成因栈空间不足所造成的程式出错的问题。为了解决这个问题,特别的引入了inline修饰符,表示为内联函数。将递归部分替换为程序代码。
2)内联函数与宏的区别
a、可调试
内联函数在运行时可调试,而宏定义不可以;
b、类型检查
编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义则不会;
c、访问权限
内联函数可以访问类的成员变量,宏定义则不能;
d、其他场景
在类中声明同时定义的成员函数,自动转化为内联函数。
3)内联函数可以递归吗?
不能递归。不是内联函数中不能有循环和递归语句,而是当内联函数中出现了复杂的逻辑控制语句后,编译器会不再认为它是一个内联函数。
指针 C++指针详解
1)指针对象
用来保存指针的对象,就是指针对象。定义指针变量时,在变量名前写一个 * 星号,这个变量就变成了对应变量类型的指针变量。指针变量可以指向变量、结构体、类、数组、函数
2)解析地址对象
对于结构体和类,两者的差别很小,所以几乎可以等同,则使用->符号访问内部成员
3)野指针 C++中的空指针和野指针、C++空指针调用成员函数
a、未经初始化的指针是个野指针,所以在定义指针变量的时候一定要进行初始化。如果实在是不知道指针的指向,则使用nullptr或NULL进行赋值。
b、free和delete后指针没有设置为NULL,让人误以为这是合法的指针
c、指针访问越界,访问了一个不该访问的内存
4)指针赋值(浅拷贝)
指针赋值和int变量赋值一样,就是将地址的值拷贝给另外一个。指针之间的赋值是一种浅拷贝,是在多个编程单元之间共享内存数据的高效的方法。
例:int* p2 = p1;
5)浅拷贝和深拷贝
6)指针的算数运算
将递增递减操作符用于指针时,将把指针的值增加其指向的数据类型占用的字节数
例:char array1[20] = “abcdefghijklmnopqrs”;
char* ptr1 = array1;ptr1++;等于ptr1的地址加上sizeof(char)此时指向b
7)void* 指针
一种特殊的指针类型,可用于存放任意对象的地址,但是丢失了类型信息。如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。因为,编译器不允许直接对void*类型的指针做解指针操作(提示非法的间接寻址)。常用于输入输出。
8)值传递和指针传递 C/C++语言中值传递、指针传递和引用传递、为什么传递类类型参数时尽量用引用传递
a、无法回传:实参传递给形参,是按值传递的,被调函数无法修改传递的参数达到回传的效果。传递变量的指针可以轻松解决上述问题。
b、传递类:有时我们会传递类或者结构体对象,而类或者结构体占用的内存有时会比较大,通过值传递的方式会拷贝完整的数据,降低程序的效率。而指针只是固定大小的空间,效率比较高。当然如果你用C++,使用引用效率比指针更高。所以C++中,传递类或结构时,引用>指针>值传递
9)函数指针和指针函数 函数指针和指针函数用法和区别
a、函数指针
①、原理:函数指针本质是一个指针,其指向一个函数。
②、写法:int (*fun)(int x,int y);
每一个函数本身也是一种程序数据,一个函数包含了多条执行语句,它被编译后,实质上是多条机器指令的合集。在程序载入到内存后,函数的机器指令存放在一个特定的逻辑区域:代码区。既然是存放在内存中,那么函数也是有自己的指针的。函数名单独进行使用时就是这个函数的指针。
例:int(*p_func)(int,int) 指向返回类型为int,有2个int形参的函数的指针
b、指针函数
①、原理:指针函数本质是一个函数,其返回值为指针。
②、写法:int* fun(int x,int y);
10)函数返回值
这里唯一需要注意的是不要把非静态局部变量的地址返回。我们知道局部变量是在栈中的,由系统创建和销毁,返回之后的地址有可能有效也有可能无效,这样会造成bug。可以返回全局变量、静态的局部变量、动态内存等的地址返回。
指针和引用的区别 C++中引用和指针的区别、引用与指针有什么区别?
1)内存分配
程序为指针变量分配内存区域,而不为引用分配内存区域。
2)修改内容
指针使用时要在前加 * ,引用可以直接使用。
3)是否可变
引用在定义时就被初始化,之后无法改变;指针可以发生改变。 即引用的对象不能改变,指针的对象可以改变。
4)空引用和空指针
没有空引用,但有空指针。这使得使用引用的代码效率比使用指针的更高。因为在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。
5)求大小
对引用使用“sizeof”得到的是变量的大小,对指针使用“sizeof”得到的是变量的地址的大小。
6)多级引用和多级指针
理论上指针的级数没有限制,但引用只有一级。即不存在引用的引用,但可以有指针的指针。
int **p //合法、int &&p //非法
7)递增操作
++引用与++指针的效果不一样。
例如就++操作而言,对引用的操作直接反应到所指向的对象,而不是改变指向;而对指针的操作,会使指针指向下一个对象,而不是改变所指对象的内容。
类型转换 C++四种强制类型转换
1)静态类型转换static_cast
这应该四种中是最常见的,但没有运行时类型检查来保证转换的安全性。
进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的
2)动态类型转换dynamic_cast
a、其他三种都是编译时完成的,dynamic_cast 是运行时处理的,运行时要进行类型检查。
b、不能用于内置的基本数据类型的强制转换
c、dynamic_cast 要求 <> 内所描述的目标类型必须为指针或引用。dynamic_cast 转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回 nullptr
d、在类的转换时,在类层次间进行上行转换(子类指针指向父类指针)时,dynamic_cast 和 static_cast 的效果是一样的。在进行下行转换(父类指针转化为子类指针)时,dynamic_cast 具有类型检查的功能,比 static_cast 更安全。
e、使用 dynamic_cast 进行转换的,基类中一定要有虚函数,否则编译不通过(类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义)。这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表中,只有定义了虚函数的类才有虚函数表
3)常量类型转换const_cast
用于去掉const属性,目标类型只能是指针或者引用
例:const A a = { 200 };A* a1 = const_cast<A*>(&a);
4)重新解释reinterpret_cast
reinterpret_cast是简单的二进制拷贝,用于任何指针向任何指针的转换,可以用于将指针类型和整型类型相互转换,但是不可用于浮点和整型之间的转换,也不可以用于枚举和整型的转换,他不进行类型检查,是四种转换中最危险的,需要谨慎使用。
a、改变指针或引用的类型
b、将指针或引用转换为一个足够长度的整形
c、将整型转换为指针或引用类型。
RTTI运行时类型识别 RTTI
内存分区 C/C++程序内存的分配、C++内存管理(面试版)
从下往上,从低地址往高地址依次为:
1)代码区(.text):主要是用来存储代码的区域,也就是计算机所要执行的指令,如上述例子中的所有代码。
2)静态数据区和已初始化全局区(.data):存储静态变量和已经初始化的全局变量,如int y = 15;。
3)未初始化全局区(.bss):用来存储为未初始化的全局变量,如int x;。
4)堆(.heap):使用malloc()函数分配的内存,如value = (int*)malloc(size0f(int)*5);。
5)进程间的文件共享区域:使用mmap映射的一块内存用于进程间的文件共享。
6)栈(.stack):用来存储局部变量,比如函数参数或者等等,在执行完后会释放这部分内存。
堆和栈的区别 堆与栈的区别、C++为什么栈比堆快
1)管理方式不同
栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;
2)空间大小不同
每个进程拥有的栈的大小要远远小于堆的大小。理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小 64bits 的 Windows 默认 1MB,64bits 的 Linux 默认 10MB;
3)生长方向不同
堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
4)分配方式不同
堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由操作系统进行释放。
5)分配效率不同
栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。所以栈比堆快。
6)存放内容不同
栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,一般情况下是按照从右向左的顺序入栈,注意静态变量是存放在数据段或者BSS段,是不入栈的。出栈的顺序正好相反。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。
内存泄漏 C++中内存泄漏的几种情况
1)没有delete
没有匹配的调用new和delete函数
2)没有delete[]
释放对象数组时在delete中没有使用方括号
3)没有将基类的析构函数定义为虚函数
当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露
4)缺少拷贝构造函数
两次释放相同的内存是一种错误的做法,同时可能会造成堆的奔溃。按值传递会调用(拷贝)构造函数,引用传递不会调用。
内存对齐
1)规则
内存对齐的长度取决于三个值:系统默认值(64位下位8字节,32位下位4字节),最大数据成员长度,pragma pack指定长度。
大于系统默认值时=min(最大数据成员长度,指定长度),小于系统默认值时=min(系统默认值,指定长度)
2)目的
a、平台原因
不是所有的硬件平台(特别是嵌入式系统中使用的低端处理器)都能访问任意地址上的任意数据,某些硬件平台只能访问对齐的地址,否则会出现硬件异常。
b、性能原因
如果数据存放在未对齐的内存空间中,则处理器访问变量时需要进行两次内存访问才能完整读取该变量的值,而对齐的内存访问仅需一次访问。
3)如何压缩 struct,不会产生内存对齐?
设置#pragma pack为1
new和malloc的区别 C++new和malloc的区别
1)属性
new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
2)参数
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
3)返回类型
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void *** ,需要通过强制类型转换将void*指针转换成我们需要的类型。
4)分配失败
new内存分配失败时,会抛出bac_alloc异常**。malloc分配内存失败时返回NULL。
5)调用对象
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
6)重载
C++允许重载new/delete操作符,而malloc不允许重载。
7)内存区域
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。
面向对象 c++面向对象的三个特点
封装、继承、多态。其中,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态则是为了实现另一个目的——接口重用
raii机制 C++Rall机制详解
RAII 是 resource acquisition is initialization 的缩写,意为“资源获取即初始化”。它是 C++ 之父 Bjarne Stroustrup 提出的设计理念,其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。
类的访问权限 C++三种继承方式下的访问权限控制
类的内存模型、成员函数、数据成员和类大小 类的成员函数和数据成员分别存在哪里,类大小的计算、【C++】类中成员函数的存储方式以及计算类的大小
1)内存位置
类的成员函数和非成员函数代码存放在代码区,数据成员分为静态变量和非静态变量,静态变量在类定义的时候,就分配好了,存放在数据区,然后非静态变量时在构造对象的时候,存放在堆栈中。
2)类大小
每个对象所占用的存储空间只是该对象的数据部分(虚函数指针和虚基类指针也属于数据部分)所占用的存储空间,而不包括函数代码所占用的存储空间。
3)内存模型
所有对象有着各自的数据成员,但调用公共的函数代码段
4)this指针 c++中this指针的用法详解、C++ this指针
成员函数存在一个隐藏变量是什么?this指针
类为什么只能直接调用静态类成员函数,而非静态类成员函数只有类对象才能调用呢?不同的对象使用的是同一个函数代码段,它怎么能够分别对不同对象中的数据进行操作呢?
原因是类的非静态类成员函数其实都内含了一个指向类对象的指针型参数(即this指针),因而只有类对象才能调用(此时this指针有实值),调用非静态成员函数和数据成员需要通过this指针。
5)成员函数
类中包括成员变量和成员函数。new出来的只是成员变量,成员函数始终存在,所以如果成员函数未使用任何成员变量的话,不管是不是static的,都能正常工作。 即空的对象指针能直接调用该成员函数而不崩溃:A a=NULL,a->func();
6)如何禁止类实例化时候的动态分配方式?如何建立一个只在栈或者堆上生成的类? 面试题:实现一个只能在堆上(栈上)生成对象的类
a、实现一个只能在堆上生成对象的类
编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。因此,将析构函数设为私有,类对象就无法建立在栈上了。
b、实现一个只能在栈上生成对象的类
将重载new和delete的函数设置为私有即可
构造函数和析构函数
1)构造函数可以抛异常吗?构造函数中抛异常
可以
2)析构函数可以抛异常吗?不能在析构函数里面抛出异常
不可以,因为函数发生了异常而导致函数的局部变量的析构函数被调用,析构函数又抛出异常,本来局部对象抛出的异常应该是由它所在的函数负责捕获的,现在函数既然已经发生了异常,必定不能捕获
3)能否在有参构造函数中调用无参构造函数?无参构造函数修改类成员是否对正在构造的类产生影响?优缺点?面试总结—2、C++中构造函数调用构造函数
不能在构造函数体中调用自己,会出现无限递归调用。默认构造函数还未执行完,却调用了另一个构造函数,这样相当于产生了一个匿名的临时对象,将这个匿名临时对象初始化自己的数据成员,但是原对象的数据成员并没有得到初始化,因此其值也是不确定的
4)实例派生类构造和析构顺序 基类、派生类、派生类成员变量的构造和析构顺序
a、执行基类的构造函数;
b、初始化派生类的成员变量,由于B是一个类类型,所以会调用B的默认构造函数,此时如果B没有默认构造函数会报错;
c、执行派生类的构造函数。
d、析构与构造顺序相反
5)空类包含哪些函数 C++中一个空类含有哪些默认的成员函数
默认构造函数、析构函数、拷贝构造函数、赋值运算符(等号:operator=)、取址运算符(operator&)
拷贝构造函数,深拷贝和浅拷贝 C++拷贝构造函数详解 、C++浅拷贝和深拷贝的区别
1)拷贝构造函数
拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量。
例:CExample(const CExample& C)
2)调用时机
a、对象以值传递的方式传入函数参数
b、对象以值传递的方式从函数返回
c、对象需要通过另外一个对象进行初始化;
3)类的浅拷贝和深拷贝
浅拷贝没有处理静态数据成员和堆上的指针,只对对象中的数据成员进行简单的赋值
深拷贝对于对象中动态成员,就不是仅仅简单地赋值了,而是重新动态分配空间
4)禁止默认拷贝
对象的复制大多在进行值传递时发生,声明一个私有拷贝构造函数可以防止按值传递。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。
5)拷贝构造的参数能不能是值传递 拷贝构造函数为什么不能用值传递
不能。因为对象值传递的时候调用拷贝构造函数,所以拷贝构造的的参数为值传递会循环调用拷贝构造,导致死循环。
模板 C++重要知识清单:泛型编程(函数模板和类模板机制)包含模板机制的底层实现原理、C++模板编译、006模板的编译机制_对模板进行两次编译
1)模板机制
C++提供两种模板机制:函数模板、类模板
template ,template 告诉C++编译器我们要开始泛型编程了,看到T不要报错,它指得是一种数据类型
函数模板可以像普通函数一样被重载,C++编译器优先考虑普通函数,如果函数模板可以产生更好的匹配,则选择函数模板,可以通过空模板实参列表的语法限定编译器只通过模板匹配。
2)底层原理
a、编译器并不是把函数模板处理成能够处理任何类型的函数,编译器从函数模板通过具体类型产生不同的函数
b、编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译;在调用的地方对参数进行替换后的代码进行编译,其实就是C++编译器帮程序员根据函数模板生成了需要参数类型的函数原型
3)模板函数声明和定义为什么要放在一起
关于C++模板函数声明与定义的问题、为什么C++编译器不能支持对模板的分离式编译
多态、静态绑定和动态绑定 C++多态、C++的动态绑定和静态绑定
1)静态多态(静态绑定)
静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数
a、函数重载
在同一个作用域下,函数名相同,函数的参数不同。
b、函数模板
使用泛型来定义函数,其中泛型可用具体的类型(int 、double等)替换。通过将类型作为参数,传递给模板
2)动态多态(动态绑定)
即运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
通过基类类型的引用或者指针调用虚函数 ,如CDerived1 *p=new CDerived2,CDerived1是静态类型,CDerived2是动态类型。
3)不用指针和引用赋值会不会引发动态绑定 为什么要使用父类指针和引用实现多态,而不能使用对象?
不会。参数如果不是指针或者引用,会进行拷贝传参。子类对象赋值给父类对象,把子类中包含的父类成员变量的值拷贝过去,但是子类的虚表不会给拷贝过去(虚表是指针),则函数中这个父类对象的虚表是父类的,所以无法实现多态。而指针或者引用是直接指向子类对象,不会进行拷贝赋值,这样虚函数表是子类的虚函数表,故能实现多态。
重载、重定义和重写C++重载、重定义、重写的区别、C++重载操作符(operator)介绍
1)重载(Overload)
在同一个作用域下,函数名相同,函数的参数不同,函数返回值可能不同
2)重定义(隐藏)(Overdefine)
a、在不同的作用域下(这里不同的作用域指一个在子类,一个在父类 ),函数名相同的两个函数构成重定义。
b、当两个函数构成重定义时,父类的同名函数会被隐藏,当用子类的对象调用同名的函数时,如果不指定类作用符,就只会调用子类的同名函数。
3)重写(覆盖)(Override)
在不同的作用域下(一个在父类,一个在子类),函数的函数名、参数、返回值完全相同,父类必须含有virtual关键字
带virtual关键字的就是虚函数,只有重写了虚函数才算实现了多态。
虚函数和纯虚函数
1)虚函数怎么实现的 C++虚函数表剖析
虚函数表和虚函数表指针
2)纯虚函数 C++纯虚函数和抽象类
将函数定义为纯虚函数,则编译器要求在派生类中必须予以重载以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。
3)构造函数中能否调用虚函数 构造函数中是否可以调用虚函数
a、从语法上讲,调用完全没有问题。
b、派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。 同样,进入基类析构函数时,对象也是基类类型。 所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。
4)基类的析构函数为什么是虚函数 C++基类析构函数为什么要定义为虚函数
当基类指针指向派生类的时候,若基类析构函数不声明为虚函数,在析构时,只会调用基类而不会调用派生类的析构函数,从而导致内存泄露。
C++新特性
1)C++11新特性
C++11常用新特性快速一览
2)C++ Primer中的C++11新特性
C++Primer第五版中的c++11特性归纳(一)、C++Primer第五版中的c++11特性归纳(二)
C++Primer第五版中的c++11特性归纳(三)、C++Primer第五版中的c++11特性归纳(四)
C++Primer第五版中的c++11特性归纳(五)、C++Primer第五版中的c++11特性归纳(六)
3)C++14新特性
关于C++14:你需要知道的新特性
返回类型推导auto、泛型lambda(参数可用auto)、lambda捕获值可初始化、二进制常量和单引号用作数字分位符
4)C++17新特性
C++17新属性详解
泛型模板(参数可用auto)、std:variant代替多元组tuple、结构化绑定(使用auto推导多类型返回值tuple)、if和switch中可以像for语句中声明
5)C++20新特性
C++20新特性个人总结
concept和requires对泛型类型约束、支持协程、lambda支持模板、lambda支持捕获this、
右值引用 C++11 右值引用&&
1)含义
临时对象被使用完之后会被立即析构,在析构函数中free掉申请的内存资源。 如果能够直接使用临时对象已经申请的资源,并在其析构函数中取消对资源的释放,这样既能节省资源,有能节省资源申请和释放的时间。
2)实现
通过加入定义转移构造函数(&&)和转移赋值操作符重载来实现右值引用
3)移动语义
准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。
4)完美转发
在泛型函数中,必须为每一个参数必须重载两种类型,T& 和 const T&以满足所有类型。而右值引用能够将所有的参数类型原封不动的传递给目标函数。
lamda表达式 C++中的Lambda表达式详解
lamda表达式可以递归调用吗 C++lambda递归的三种写法
auto关键字的优缺点 C++之auto的使用
智能指针 C++ STL 四种智能指针
智能指针主要解决忘记delete、拷贝构造或复制带来的问题,所有智能指针都未采用深拷贝,由于开销太大。
1)auto_ptr
auto_ptr ,由 C++98 引入,定义在头文件,其功能和用法类似于 unique_ptr。
缺点:auto_ptr 有拷贝语义,拷贝后原对象无效,再次访问原对象会导致程序崩溃,并且过期时会删除两次。
使用场景:任何情况下都不应该使用 auto_ptr。
2)unique_ptr
unique_ptr 由 C++11 引入,旨在替代不安全的 auto_ptr。
a、unique_ptr 比 auto_ptr 更加安全,因为 unique_ptr 则禁止了拷贝构造函数和赋值运算符,但提供了移动语义,即可以使用 std::move() 进行控制权限的转移。使用auto_ptr赋值编译成功,运行报错;unique_ptr编译失败。
b、如果 unique_ptr 是个临时右值,编译器允许拷贝语义。如函数返回值为unique_ptr,还没机会访问就析构
c、unique_ptr可以放在容器中,auto_ptr不行
d、可以管理数组成员,使用delete[]析构
e、可以自定义析构函数
使用场景:如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。
3)shared_ptr
a、原理
shared_ptr使用引用计数机制,允许多个指针指向同一个对象
b、计数变化
拷贝构造时增加现有计数,赋值操作符时减少原有计数,增加现有计数
使用场景:多个指向同一对象的指针,或将指针作为参数或者函数的返回值进行传递,应选择 shared_ptr
4)weak_ptr
weak_ptr 是为了配合 shared_ptr 而引入的一种智能指针,weak_ptr 只对 shared_ptr 进行引用,而不改变其引用计数,当被观察的 shared_ptr 失效后,相应的 weak_ptr 也相应失效。
使用场景:解决 shared_ptr 的两个对象互相引用导致的循环引用问题,将对方声明为weak_ptr即可自动析构
5)手撕shared_pt C++面试题:手撕智能指针shared_ptr
a、当创建智能指针类的新对象时,初始化指针,并将引用计数设置为1;
b、当能智能指针类对象作为另一个对象的副本时,拷贝构造函数复制副本的指向辅助类对象的指针,并增加辅助类对象对基础类对象的引用计数(加1);
c、使用赋值操作符对一个智能指针类对象进行赋值时,处理复杂一点:先使左操作数的引用计数减 1(为何减 1:因为指针已经指向别的地方),如果减1后引用计数为 0,则释放指针所指对象内存。然后增加右操作数所指对象的引用计数(为何增加:因为此时做操作数指向对象即右操作数指向对象);
d、完成析构函数:调用析构函数时,析构函数先使引用计数减 1,如果减至 0 则 delete 对象。
shared_ptr线程安全 shared_ptr的线程安全性、C++ shared_ptr的线程安全问题
1)引用计数
shared_ptr的引用计数是线程安全的,在手段上使用了atomic原子操作
2)对象资源
a、安全问题
shared_ptr对象所管理的资源存放在堆上,它可以由多个shared_ptr所访问,所以这也是一个临界资源。因此当多个线程访问它时,会出现线程安全的问题。
b、原因
shared_ptr对象有两个变量,一个是指向的对象的指针,还有一个就是我们上面看到的引用计数, 当shared_ptr发生拷贝的时候,是先拷贝智能指针,然后再拷贝引用计数,也就是说,shared_ptr的拷贝并不是一个原子操作。
c、解决方法
①、加锁:当我们多个线程访问同一个shared_ptr时,应该要进行加锁操作
②、使用侵入式智能指针:因为不涉及引用计数的拷贝,对引用计数的增减是原子的,只用拷贝对象指针,所以是线程安全的。
侵入式智能指针模板类 C++侵入式智能指针的实现
1)优点
a、唯一计数
一个资源对象无论被多少个侵入式智能指针包含,从始至终只有一个引用计数变量,不需要在每一个使用智能指针对象的地方都new一个计数对象,这样子效率比较高,使用内存也比较少,也比较安全
b、传递方便
因为引用计数存储在对象本身,所以在函数调用的时候可以直接传递资源对象地址,而不用传递智能指针
c、防止丢失
非侵入式智能指针对象的拷贝,必须带着智能指针模板,否则就会出现对象引用计数丢失
2)步骤
a、计数基类
将引用计数变量从资源类中抽离出来,封装成一个基类,该基类包含了引用计数变量。如果一个类想使用智能指针,则只需要继承自该基类即可;
b、模板类
引用计数的基类,设计成模板类,接受引用计数类型作为参数,比如使用int类型或者原子计数类型作为引用计数变量。默认情况下应该使用原子计数类型作为引用计数变量。
c、只能继承
引用计数基类的构造函数、拷贝构造函数、析构函数应为protected,即该类只能通过继承关系被使用。
d、新对象计数置0
拷贝构造函数并不拷贝引用计数基类的数据成员,而是重新将原子计数_atomic置为0——因为每个对象都有一个自己的引用计数,当发生对象拷贝构造时,新的对象的计数应该置为0,而不应该拷贝旧对象的计数。
e、赋值计数不变
赋值操作operator=,比如A=B,同上面一样,只对资源类的成员进行拷贝,而不拷贝其引用计数基类的数据成员。也就是说,将B的值赋给A,A的引用计数应该保持不变,而不能将B的引用计数拷贝过来——这是对象的拷贝,而不是智能指针的拷贝。
enable_shared_from_this C++11新特性之十:enable_shared_from_this
1)原理:
正常的shared_ptr只能传递智能指针模板类,该机制提供了一个传递对象的能力,并且两个智能指针
2)使用
对象需继承std::enable_shared_from_this该基类,并且使用shared_from_this()返回一个新的共用对象,两个智能指针共享该对象的所有权,并且引用计数都相同
3)区别
并不是侵入式智能指针类,引用计数并非保存在对象中,仍然保存在智能指针类中
C++11中的同步与互斥 线程的互斥和同步(8)- C++11中的互斥锁和条件变量
1)互斥锁
a、mutex 最基本的互斥锁,不可重入
b、timed_mutex 具有超时机制的互斥锁
c、recursive_mutex 可重入的互斥锁
d、recursive_timed_mutex 结合 timed_mutex 和 recursive_mutex 特点的互斥量
2)互斥锁管理类
lock_guard 基于作用域的互斥量管理
unique_lock 更加灵活的互斥量管理
3)条件变量
std::condition_variable
unique_lock和lock_guard c++11中的lock_guard和unique_lock使用浅析、C++11中lock_guard和unique_lock的特点和区别,condition_variable为何搭配unique_lock使用
1)lock_guard
a、创建即加锁,作用域结束自动析构并解锁,无需手工解锁
b、不能中途解锁,必须等作用域结束才解锁
c、不能复制,需要使用锁的时候,首先考虑使用 lock_guard,简单、明了、易读
const std::lock_guardstd::mutex lock(g_i_mutex);
2)unique_lock
a、创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定
b、可以随时加锁解锁
c、作用域规则同 lock_grard,析构时自动释放锁
d、不可复制,可移动
e、条件变量需要该类型的锁作为参数(此时必须使用unique_lock)
atomic类 【C++11并发】何为std::atomic,其原理如何,以及使用注意事项
1)定义
std::atomic<>的每个实例化和完全特化表示一种类型,不同的线程可以同时操作(该类型实例),而不会引发未定义的行为,该类重载了常用的运算符,并且包括特殊的运算函数
2)示例
std::atomic value(0),value++
a=a+12相当于a.fetch_add(12,std::memory_order_seq_cst)
3)优点 C++11之atomic原子操作
使用互斥锁大约2000ms,使用原子操作大约10ms,大大提升程序运行效率
4)底层原理 atomic的底层实现
原子操作是由底层硬件支持的一种特性,在汇编语言中加入一个lock前缀,方法包括锁bus和锁cache
空指针nullptr C++中NULL和nullptr的区别
NULL在C++中就是0,这是因为在C++中void* 类型是不允许隐式转换成其他类型的,所以之前C++中用0来代表空指针,但是在重载整形的情况下,NULL会被当成int型的0而不是void*。所以,C++11加入了nullptr,可以保证在任何情况下都代表空指针,而不会出现上述的情况
STL六大组件 C++_STL六大组件详解、仿函数 ( 函数对象 ) 详解、STL之仿函数实现详解、C++迭代器 iterator详解
1)容器Container
各种基本数据结构
a、序列式容器:向量(vector)、双端队列(deque)、列表(list)
b、关联式容器:集合(set)、多重集合(multiset)、映射(map)和多重映射(multimap)
2)迭代器Iterator
用来在容器的元素上遍历
a、为所有容器提供了一组很小的公共接口。
b、迭代器是一种智能指针,智能指针定义为存储指向动态分配对象指针的类,迭代器封装了指针的同时,还对指针的一些基本操作如*、->、++、==、!=、=进行了重载,使其具有了遍历复杂数据结构的能力
c、迭代器失效的情况? 迭代器失效的几种情况总结
①、序列式容器
该数据结构的元素是分配在连续的内存中,insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除点之后的迭代器全部失效,也就是说insert(*iter)(或erase(*iter)),然后在iter++,是没有意义的。解决方法:erase(*iter)的返回值是下一个有效迭代器的值。 iter =cont.erase(iter);
②、关联式容器
对于关联容器(如map, set,multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前iterator即可。这是因为map之类的容器,使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响。
3)算法Algorithm
用来处理群集内的元素。它们可以出于不同的目的而搜寻、排序、修改、使用那些元素。通过迭代器的协助,我们可以只需编写一次算法,就可以将它应用于任意容器,这是因为所有的容器迭代器都提供一致的接口。
4)适配器Adapter C++ STL中的容器适配器详解
a、定义
适配器是一种类,为已有的类提供新的接口,目的是简化、约束、使之安全、隐藏或者改变被修改类提供的服务集合
b、C++中定义了3种容器适配器,它们让容器提供的接口变成了我们常用的的3种数据结构:栈stack,队列queue和优先队列priority_queue。默认情况下,栈和队列都是基于deque实现的,而优先级队列则是基于vector实现的。
5)分配器Allocator
a、负责空间配置与管理。从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template。
b、默认的allocator是一个由两级分配器构成的内存管理器,当申请的内存大小大于128byte时,就启动第一级分配器通过malloc/free直接向系统的堆空间分配,如果申请的内存大小小于128byte时,就启动第二级分配器,从一个预先分配好的内存池中取一块内存交付给用户,这个内存池由16个不同大小(8的倍数,8~128byte)的空闲列表组成,allocator会根据申请内存的大小(将这个大小round up成8的倍数)从对应的空闲块列表取表头块给用户。
c、优点
①、小对象的快速分配
②、避免了内存碎片的生成
6)仿函数Function object
一个行为类似函数的类对象,它可以没有参数,也可以带有若干参数,任何重载了调用运算符operator()的类的对象都满足函数对象的特征,函数对象可以把它称之为smart function。
数组和链表、vector、deque和list C++三种容器:list、vector和deque的区别
1)数组和链表的区别
a、vector为存储的对象分配一块连续的地址空间 ,随机访问效率很高。但是插入和删除需要移动大量的数据,效率较低。尤其当vector中存储的对象较大,或者构造函数复杂,则在对现有的元素进行拷贝的时候会执行拷贝构造函数。
b、list中的对象是离散的,随机访问需要遍历整个链表, 访问效率比vector低。但是在list中插入元素,尤其在首尾插入,效率很高,只需要改变元素的指针。
2)C中数组越界怎么解决?给数组扩容的几种方式
a、创建一个扩容的临时数组,然后赋值给原数组,使用循环遍历方式
b、创建一个扩容的临时数组,然后赋值给原数组,使用Array的静态方法
c、使用Array的静态方法Array.Resize()扩容(优先考虑)
3)在插入多查询少时使用哪种数据结构?
链表
4)有序链表如何查找某个元素?
链表:顺序查找O(N)、建立跳跃表查找O(logN)
5)数组vector底层工作原理?vector扩容机制? C++vector操作、C++vector实现原理
关于vector简单的讲就是一个动态增长的数组,里面有一个指针指向一片连续的内存空间,当空间装不下的时候会自动申请一片更大的空间(空间配置器)将原来的数据拷贝到新的空间,然后就会释放旧的空间。当删除的时候空间并不会被释放只是清空了里面的数据。
6)数组vector的resize和reverse有什么不同?vector resize和reverse区别
a、reserve是容器预留空间,但并不真正创建元素对象,在创建对象之前,不能引用容器内的元素,因此当加入新的元素时,需要用push_back()/insert()函数。
b、resize是改变容器的大小,并且创建对象,因此,调用这个函数之后,就可以引用容器内的对象了,因此当加入新的元素时,用operator[]操作符,或者用迭代器来引用元素对象。
c、参数不同:reserve函数之后一个参数,即需要预留的容器的空间;resize函数可以有两个参数,第一个参数是容器新的大小,第二个参数是要加入容器中的新元素
7)双端数组deque的底层实现?STL源码剖析——deque的实现原理和使用方法详解
deque采用一块map作为主控,map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的储存空间主体。为了维持整体连续的假象,deque的设计及迭代器前进后退等操作都颇为繁琐。
8)数组vector和双端数组deque插入删除效率?关于C++中vector和deque的使用效率测试记录
vector的优势是对中间的操作速度快,deque优势是对两端的操作速度快
9)STL遍历vector有几种方法? vector的N种遍历方法
数组下标、迭代器、范围for、foreach方法、at方法
哈夫曼树和哈夫曼编码 最全哈夫曼树哈夫曼编码讲解,兄弟你值得拥有
1)哈夫曼树
哈夫曼树(Huffman Tree)是在叶子结点和权重确定的情况下,带权路径长度最小的二叉树,也被称为最优二叉树。
2)哈夫曼编码
用于压缩编码,把左分支权值改为0,右分支权值改为1,按照路径重新给每个子节点分配编码
3)什么树要么没有节点,要么只有俩节点?
哈夫曼树、满二叉树
字典树或trie树 从Trie树(字典树)谈到后缀树(10.28修订)、剑指Offer——Trie树(字典树)、搜索引擎关键字智能提示的一种实现
1)定义
Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。
2)性质
a、根节点不包含字符,除根节点外每一个节点都只包含一个字符。
b、从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
c、每个节点的所有子节点包含的字符都不相同。
3)优点
最大限度地减少无谓的字符串比较,查询效率比哈希表高。 Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
4)应用
是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
5)时间复杂度
Trie树的平均高度h为len,所以Trie树的查询复杂度为O(h)=O(len),总的复杂度为O(n*len),
红黑树和map 红黑树动画在线演示、教你初步了解红黑树、浅析红黑树(RBTree)原理及实现、浅谈AVL树,红黑树,B树,B+树原理及应用
STL的map的底层实现? c++ map与unordered_map区别及使用
STL的map中所有操作的复杂度? 二叉查找树与红黑树概念性质及操作时间复杂度
等于红黑树的所有操作复杂度,都是O(lgn)
STL的set的底层实现?STL–set介绍及set的使用
在插入和查询差不多的情况下选择哪种数据结构?红黑树
红黑树和哈希表比较 哈希表和红黑树的对比
HashMap用红黑树而非B+树原因? 为什么HashMap使用红黑树而不是AVL树或者B+树
哈希表和unorderedmap 哈希表(散列表)原理详解
1)哈希冲突 哈希表冲突及处理冲突的方法
a、开放定址法(再散列法)
①、线性探测再散列:顺序往下查找
②、二次探测再散列:在表的左右查找
③、伪随机探测再散列:设置一个固定的随机数列,每次冲突都按该数列查找
b、再哈希法同时构造多个不同的哈希函数,
优点:这种方法不易产生聚集
缺点:增加了计算时间
c、拉链法
将所有哈希地址为i的元素构成一个称为同义词链的单链表,大于8转为红黑树存储
d、建立公共溢出区
凡是和基本表发生冲突的元素,一律填入溢出表
2)如何改善Hash表性能?哈希扩容 【数据结构之哈希表(二)】 哈希表的扩容实现机制
a、原理:当表的实际装载因子达到默认的负载因子值(负载极限)时,就会触发哈希表的扩容。
b、Java中的哈希扩容:
每次扩容时,哈希表的容量增加为原先的两倍。于是在扩容被触发时(实际装载因子达到默认装载因子时),增加一个元素的性能是比较差的,因为要等待原先的表rehash之后才能增加该元素。
c、Redis中的哈希扩容:渐进式哈希
①、步骤
在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
②、增删改查
增加只增加在新哈希表,新插入的键值对会放在链表的头部,而不是在尾部继续插入,头插法的时间复杂度为O(1),并且最新插入的数据往往更可能频繁地被获取
查找一个key的话,会先在ht[0]里面进行查找,如果没有找到的话,就会继续到ht[1]里面进行查找。
删除和修改都在两个表进行
八大排序 八大排序算法、快速排序算法——C/C++、堆排序算法(图解详细流程)
1)直接插入排序
将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。
2)希尔排序
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
3)简单选择排序
在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
4)堆排序
初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。
5)冒泡排序
在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。比较相邻两数发现它们的排序与排序要求相反时,就将它们互换。
6)快速排序
选择一个基准元素,通过一趟排序讲待排序的记录分割成独立的两部分,此时基准元素在其排好序后的正确位置,然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序
7)归并排序
归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
8)快排和归并的时间复杂度为什么是nlogn? 归并排序的时间复杂度为什么为nlogn
快排和归并的递归次数都约为nlogn,每一次排序比较约为n,所以时间复杂度是nlogn
9)对于一个基本有序的数组进行排序用什么最快? 对于一个基本有序的数组进行排序、如何对一个几乎有序的数列进行排序最好
插入排序、增强冒泡排序(每一轮不从下一个开始,而是从无序的开始)、k+1的最小堆(保证前k+1个有最小值且调整次数少)
排序算法稳定性 排序算法稳定性
1)稳定的排序算法:冒泡排序、插入排序、归并排序、基数排序
a、冒泡排序:比较的是相邻元素,将较小或较大的向后移动,可以人为控制相等不交换
b、插入排序:比较是从有序序列的末尾开 始,可以人为控制把数插在相等的数后面
c、归并排序:可以人为控制两个数相等时合并不改变顺序
d、基数排序:按照数字的位数进行排序,可以人为控制相等数的顺序
2)不稳定的排序算法:堆排序、快速排序、希尔排序、直接选择排序
a、选择排序:序列5 8 5 2 9, 第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了
b、快速排序:不稳定发生在中枢元素和a[j] 交换的时刻
c、堆排序:父子节点调整顺序可能跳过相等的节点
d、希尔排序:在各自不同步长的序列里交换,可能导致相等的数位置被打乱
快排的优化方法 快速排序的4种优化
1)随机选取基准值或使用头尾中三数取中选取基准值,提升平均性能
2)当范围在5-20小范围间选择插入排序,提升算法效率
3)尾递归优化,将纵向递归调用改为横向迭代循环,减少栈深度避免崩溃
4)聚集元素,将一次划分后与基准值相同的元素移到基准值附近,提升重复数组排序效率
5)使用多线程分组快排,再使用归并排序合并
sort()底层实现 C++一道深坑面试题:STL里sort算法用的是什么排序算法?
STL的sort算法,数据量大时采用QuickSort快排算法,分段归并排序。一旦分段后的数据量小于某个门槛(16),为避免QuickSort快排的递归调用带来过大的额外负荷,就改用Insertion Sort插入排序。如果递归层次过深,还会改用HeapSort堆排序。
海量数据处理 教你如何迅速秒杀掉:99%的海量数据处理面试题、海量数据处理技巧、面试必须掌握的十个海量数据问题及解决方案、海量数据处理 - 10亿个数中找出最大的10000个数(top K问题)
1)top-k问题:最大的100个数、出现次数最多的100个数、AB大文件找出重复数据、
模板:Hash映射 + Hash_map统计/trie树统计 + 桶内堆排top-k+归并排序
先借助哈希算法,计算每一条数据的hash值,按照hash值将海量数据分布存储到多个桶中,所谓桶,一般可以用小文件实现,每个小文件的大小比内存限制要小。根据hash函数的唯一性,相同的数据一定在同一个桶中。如此,我们再依次处理这些小文件,用最小堆求每个小文件内的top-k,最后用归并排序做多个文件的top-k合并
若内存能放下所有数据,直接hash_map+堆排
2)trie树:
字符串去重
适用范围:数据量大,重复多,但是数据种类小可以放入内存
3)位图和布隆过滤器 布隆过滤器究竟是什么,这一篇给讲的明明白白的
如何找到海量数据出现次数大于2的数?2-Bitmap 位图(bitmap)的理解及应用实例 布隆过滤
位图适合处理出现大于两次、出现一次=是否重复、出现个数的问题
集合求交、判断数是否存在大数据集中、数据字典,进行数据的判重,或者集合求交集
4)倒排索引
5)多层桶结构
6)外部排序
7)MapReduce
8)蓄水池算法 蓄水池采样算法(Reservoir Sampling)原理,证明和代码、大数据工程师必备之蓄水池抽样算法
a、题目:给出一个数据流,这个数据流的长度很大或者未知。并且对该数据流中数据只能访问一次。请写出一个随机选择算法,使得数据流中所有数据被选中的概率相等
b、算法:首先构建一个可容纳 k个元素的数组,将序列的前 k个元素放入数组中。然后对于第 j(j>k)个元素开始,以k/j的概率来决定该元素是否被替换到数组中(数组中的 k个元素被替换的概率是相同的)。当遍历完所有元素之后,数组中剩下的元素即为所需采取的样本。
动态规划 告别动态规划,连刷40道动规算法题,我总结了动规的套路
第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 dp[] 吧。这个时候有一个非常非常重要的点,就是规定你这个数组元素的含义,例如你的 dp[i] 是代表什么意思?
第二步骤:找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2]……dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步,后面我会讲几种类型的题来说。
第三步骤:找出初始值。学过数学归纳法的都知道,虽然我们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],我们可以通过 dp[n-1] 和 dp[n-2] 来计算 dp[n],但是,我们得知道初始值啊,例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我们必须要能够直接获得 dp[2] 和 dp[1] 的值,而这,就是所谓的初始值。
手撕字符串 经典面试题之手撕字符串函数
如何设计一个长链接压缩成短连接的服务?如何避免冲突,最后能有加密性质,防止短连接被盗用? 如何实现 "长"链接变 “短” 链接?、短链接、短网址使用的是什么算法?
编译流程 一个C++源文件从文本到可执行文件经历的过程
1)预处理(产生.i文件,-E)
a、将所有**#define删除**,并将宏定义展开。
b、处理一些条件预编译指令如#ifndef,#ifdef,#elif,#else,#endif等。将不必要的代码过滤掉。
c、处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。这个过程是递归进行的,因为被包含的文件可能也包含其他文件。
d、预处理过程还会过滤掉所有注释/**/和//里面的内容,另外还会添加行号和文件名标识,最后会保留#pragma编译器指令,因为编译器需要使用它们。
2)编译(产生.s文件,-s)
编译就是将预处理的文件进行一系列的词法分析,语法分析,语义分析,以及优化后产生相应的汇编代码文件
词法分析,语法分析,语义分析,源代码优化,代码生成和目标代码优化
3)汇编(产生.o或.obj文件,-c)
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程,即生成目标文件
4)链接(产生.out或.exe文件,-o)
链接就是把每个源代码独立的编译,然后按照它们的要求将它们组装起来,链接主要解决的是源代码之间的相互依赖问题
a、静态链接
将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中,因此对应的链接方式称为静态链接
b、动态链接
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。
动态库和静态库 windows中静态库lib和动态dll的区别及使用方法
1)静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,lib 中的指令都全部被直接包含在最终生成的 EXE 文件中,最终的可执行文件exe会比较大。但是若使用 DLL,该 DLL 不必被包含在最终 EXE 文件中,EXE 文件执行时可以“动态”地引用和卸载这个与 EXE 独立的 DLL 文件。
2)静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。
C++函数参数入栈顺序和调用约定 C语言中函数参数入栈的顺序
C语言函数参数采用自右向左的入栈顺序,主要原因是为了支持可变长参数形式
设计模式的七大原则 设计模式的六大原则
1)总原则:开闭原则(Open Close Principle)
开闭原则就是说对扩展开放,对修改关闭。
2)单一职责原则
每个类应该实现单一的职责。
3)里氏替换原则(Liskov Substitution Principle)
任何基类可以出现的地方,子类一定可以出现。
4)依赖倒转原则(Dependence Inversion Principle)
写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。
5)接口隔离原则(Interface Segregation Principle)
使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。
6)迪米特法则(最少知道原则)(Demeter Principle)
一个类对自己依赖的类知道的越少越好。也就是说无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部
7)合成复用原则(Composite Reuse Principle)
原则是尽量首先使用组合的方式,其次考虑继承,继承要依赖里氏替换原则。
常见设计模式 C++各类设计模式及实现详解
1)工厂模式
a、简单工厂模式
优点:代码量较少,逻辑简单。
缺点:违背了开闭原则。客户把需要生产的产品名字传给工厂,工厂内部用if语句进行判断并生产对应产品。
当新增产品类型时,需要修改原来的工厂代码。
b、工厂方法模式
优点:符合开闭原则。一个工厂对应生产一种产品,用户只需要找到对应工厂,不需要传入产品名字。
缺点:每增加一种产品,需要新增一类工厂和产品,相比于简答工厂需要更多类定义。工厂之间没有关系。
c、抽象工厂模式
在工厂方法上,每个工厂继承自抽象工厂类,每个工厂可以生产多种产品。
2)单例模式及应用场景 C++中的单例模式、C++的三种单例模式-----深度解析、C++实现单例的5种方法总结
编码技巧 《Effective C++》 总结笔记
智力题
智力题之【老鼠吃毒药问题】
12个球,其中有1个坏球和其他11个重量不一样,给你一个天平,称3次,找出不一样的那个
(规定时间过桥问题)A、B、C、D 四个人,要在夜里过一座桥。他们通过这座桥分别需要耗时 1、2、5、10 分钟
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。