1、set和map的区别
都是关联式容器,底层都是红黑树。
set不允许重复的键值,所有元素自动排序,不能通过迭代器改变set的值,因为set的值就是键。
map不允许重复的键,所有元素都是键值对的方式存在的的,所有元素都是通过键来排序的。map的key不能修改,value能修改。
2、class和struct的区别
C++是向下兼容的,因此C++中保留了很多C的东西,也保留了struct,并做了一些改变。
1、struct定义
struct是一种数据类型,那么就肯定不能定义函数,所以在面向c的过程中,struct不能包含任何函数。
面向过程的编程认为,数据和数据操作是分开的。然而当struct进入面向对象的c++时,其特性也有了新发展,c++中认为数据和数据对象是一个整体,不应该分开。
在C++中struct得到了很大的扩充:
- 1.struct可以包括成员函数
- 2.struct可以实现继承
- 3.struct可以实现多态
2、区别
(1)访问权
默认的继承访问权。class默认的是private,strcut默认的是public。
默认访问权限:struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。
(2)“class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数
(3)大括号上的区别
class和struct如果定义了构造函数的话,都不能用大括号进行初始化
如果没有定义构造函数,struct可以用大括号初始化。
如果没有定义构造函数,且所有成员变量全是public的话,class可以用大括号初始化。
3、const
const 常量:定义时就初始化,以后不能更改。
const 形参:func(const int a){};该形参在函数里不能改变
const修饰类成员函数:该函数对成员变量只能进行只读操作
作用:
(1)阻止一个变量被改变
(2)声明常量指针和指针常量
(3)const修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为”左值”。
4、内存泄漏
产生原因
1、new创建出来的对象没有及时的delete掉;
2、类里面有个void*指针,构造函数里面会给这个指针赋值,析构函数会释放。如果创建了一个类对象的指针变量,那么delete对象的时候,并不会调用类对象的析构函数,那么就会造成内存泄漏;
class Object { private: void* data; const int size; const char id; public: Object(int sz, char c):size(sz), id(c){ data = new char[size]; cout << "Object() " << id << " size = " << size << endl; } ~Object(){ cout << "~Object() " << id << endl; delete []data; } }; int main() { Object* a = new Object(10, ‘A‘);//Object*指针指向一个Object对象; void* b = new Object(20, ‘B‘);//void*指针指向一个Object对象; delete a;//执行delete,编译器自动调用析构函数; delete b;//执行delete,编译器不会调用析构函数,导致data占用内存没有得到回收; cout << "Press any key to continue... ..." << endl; getchar(); return 0; }
3、用new创建对象数组后,没有用delete []
class Object1 { int a; int b; }; int main() { Object1* arry1 = new Object1[100];//创建包含100个Object1的对象数组arry1并返回数组首地址; Object1* arry2 = new Object1[100];//创建包含100个Object1的对象数组arry2并返回数组首地址; delete []arry1;//回收了数组arry1里的所有对象动态创建时占用的内存空间; delete arry2;//回收了数组arry2里的第一个对象动态创建时占用的内存空间,导致其他99个对象的内存空间泄露; cout << "Press any key to continue... ..." << endl; getchar(); return 0; }
5、c和c++的区别
1.面向过程和面向对象的区别
(1)面向过程:面向过程编程就是分析出解决问题的步骤,然后把这些步骤一步一步的实现,使用的时候一个一个的依次调用就可以了。
(2)面向对象:面向对象编程就是把问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。
面向对象就是高度实物抽象化(功能划分)、面向过程就是自顶向下的编程(步骤划分)
2、优缺点
面向过程性能高,因为类调用时需要实例化,开销比较大,好资源。但是不容易维护和拓展;
面向对象性能低,但是容易维护和拓展,更加灵活。
c++是c的超集,c++的编译器能够编译c。
从头到尾开始介绍:
1、使用iostream的输入输出流对象代替了stdio的scanf()和printf()。
2、还增加了许多关键字,using namespace .......
4、c中不带参数的函数原型必须写void,但是c++可以使用空参数列表;
5、c++有无名形参;
5、变量的定义语句可以在任何地方,只要是在使用他之前就行了,但是c必须在函数开头部分。c++可以重复定义变量,c不行。
6、c++使用new和delete对内存进行分配,取代了c的malloc和free;
7、强制类型转换
c:(int)a
c++:(int)a; int(a);
8、c++使用字符串类代替c中的字符数组;
9、引用;
10、重载和多态
6、右值引用
避免无意义的复制,提高性能。
左值:能取地址,或者具名对象,表达式结束后依然存在的持久对象;
右值:不能取地址,匿名对象,表达式结束后就不再存在的临时对象;纯右值、将亡值。
区别:
左值能寻址,右值不能;
左值能赋值,右值不能;
左值可变,右值不能(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变);
c++11
中的右值引用使用的符号是&&
1 Type && a=ReturnRvale();
上述代码声明了一个名为a的右值引用,值等于这个函数返回的临时变量的值。
基于右值引用可以实现转移语义和完美转发新特性。
7、可变模板参数
可变模板参数(variadic templates)是C++ 11新增的最强大的特性之一,它对参数进行高度泛化,它能表示0到任意个数、任意类型的参数。
//template<class... T> template<typename...T> void f(T...args);
省略号作用:
- 声明一个参数包T… args,这个参数包可以包含0到任意个模板参数;
- 在模板定义的右边,可以将参数包展开成一个一个独立的参数。
展开可变模版参数函数的方法一般有两种:一种是通过递归函数来展开参数包,另外一种是通过逗号表达式来展开参数包
8、lambda
9、c++源文件到可执行文件过程
源代码文件中头文件、宏定义等进行分析,生成预编译文件
预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件
10、迭代器和指针
迭代器一般仅用于底层聚合支持类,list、vector、stack、ostream等。
和指针的区别?
迭代器不是指针,是类模板,表现的像指针。
模拟了指针的一些功能,重载了指针的一些操作符。
本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用*取值后的值而不能直接输出其自身。
11、vector和list的区别
1、概念
vector:连续存储的容器,动态数组,在堆上分配空间;
List:动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。
2、底层
vecotr:数组
List:双向链表
3、操作
插入
- vector:如果有剩余空间,就放最后,没有就申请原来元素两倍容量生成新空间,将原来的复制过来,然后析构释放原来的空间;
- LIst:插入一次,申请一次空间;
删除:
- vector:直接删除,后面的往前移动一位;
- LIst:直接删除,只需要更改一下指针
4、性能
vector能随机访问,性能好;list不能;
vector一次分配好内存,不够时候才申请内存;list插入一次就申请一次;
5、应用
需要高效访问,vector
需要高效插入、删除,list
12、STL有什么组成?
容器、迭代器、仿函数(函数对象)、算法、分配器、配接器
他们之间的关系:分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数
13、STL迭代器删除元素
这个主要考察的是迭代器失效的问题。
1.对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器;
2.对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
3.对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
14、STL的分配器allocator
STL的分配器用于封装STL容器在内存管理上的底层细节。
new运算分两个阶段:(1)调用::operator new配置内存;(2)调用对象构造函数构造对象内容
delete运算分两个阶段:(1)调用对象希构函数;(2)掉员工::operator delete释放内存
为了精密分工,STL allocator将两个阶段操作区分开来:内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责;对象构造由::construct()负责,对象析构由::destroy()负责。
同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器,当分配的空间大小超过128B时,会使用第一级空间配置器;当分配的空间大小小于128B时,将使用第二级空间配置器。第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。
15、RTTI
RTTI(Run Time Type Identification)即通过运行时类型识别,程序能够使用基类的指针或引用来检查着这些指针或引用所指的对象的实际派生类型。
C++是一种静态类型语言。其数据类型是在编译期就确定的,不能在运行时更改。然而由于面向对象程序设计中多态性的要求,C++中的指针或引用(Reference)本身的类型,可能与它实际代表(指向或引用)的类型并不一致。有时我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息,这就产生了运行时类型识别的要求。C++要想获得运行时类型信息,只能通过RTTI机制,并且C++最终生成的代码是直接与机器相关的。
RTTI提供了两个非常有用的操作符:typeid和dynamic_cast。
- typeid操作符,返回指针和引用所指的实际类型;
- dynamic_cast操作符,将基类类型的指针或引用安全地转换为其派生类类型的指针或引用。
为了获得一个对象的类型可以使用typeid函数,该函数反回一个对type_info类对象的引用,要使用typeid必须使用头文件<typeinfo>,因为typeid是一个返回类型为typ_info的引用的函数所以这里有必要先介绍一下type_info类。
16、定义常量
c++中定义常量有两种方法:
1.使用#define预处理器
#define SCREEN_HEIGHT 640
2.使用const关键字
const int SCREEN_WIDTH 960;
所谓常量,即在程序执行期间不会改变的变量,常量可以是任意类型的变量,只不过在定义之后值不可修改。
#define PRICE 10 //定义单价常量10 const int PRICE = 10; //定义单价常量10
区别:
#define是定义宏变量,它其实是在编译之前,由预处理指令把代码里面的宏变量用指定的字符串替换,它不做语法检查,而constant 则是定义含有变量类型的常量。
一般说来推荐使用constant定义常量,它在编译时会做语法检查。Effective c++ 的条款1中:“尽量用编译器而不用预处理”,因为#define经常被认为好象不是语言本身的一部分。而且有时候用宏,会出现意想不到的输出结果。
两者比较:
(1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应) 。
(2) 有些集成化的调试工具可以对 const 常量进行调试, 但是不能对宏常量进行调试。
17、析构函数、虚函数
析构函数
与构造函数对应,对象结束生命周期,系统会自动执行析构函数。
不能带任何参数,没有返回值(包括void),只能有一个析构函数,不能重载。
如果没有自定义一个析构函数,系统会自动生成一个析构函数(就算没有定义,也会有一个析构函数),
类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。
虚函数
有了虚函数,基类指针指向派生类对象时候,就可以使用派生类的成员函数(这个函数与基类的成员函数同名)。
只有派生类的虚函数覆盖基类虚函数(函数原型相同)才能构成多态。
在有虚函数的类中,类最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放的是虚函数的入口地址,实际的虚函数在代码段中。当派生类继承基类的时候,也会继承虚函数表,当子类重写父类的虚函数时,继承的这个虚函数表中的虚函数地址也会替换成重写后的地址。
使用虚函数会增加内存开销,降低效率。
静态函数与虚函数
静态函数编译的时候就确定了运行时机,而虚函数在运行的时候动态绑定。
虚函数还用到了虚函数表和虚表指针,增加了一次内存开销。
重载和覆盖
重载:同一个函数名,参数列表不同执行不同或相同的操作,在同一个作用域中;
覆盖:基类中的虚函数,在派生类中重新定一个了,也就是重写;
为什么析构函数必须是虚函数?
将基类的析构函数设置为虚函数,那我们new一个派生类对象的时候,基类指针指向派生类对象,释放基类指针的时候,就能够释放掉子类的空间。
为什么C++默认的析构函数不是虚函数 ?
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。
而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
18、指针和引用的区别
1、内存
指针是一个变量,有自己的内存,sizeof=4,而引用就是原来变量的一个别名,内存大小就是原来变量的大小;
2、初始化
指针可以初始化NULL,而引用必须市一个已经存在的对象
3、指针能改,引用只能是一个对象的引用,不能改;
4、const指针,无const引用;
19、哈希表
构造哈希:取地址法、余数法、平方取中;
处理冲突:链地址法、再哈希、建立一个公共的溢出区、开放定址法
STL使用链地址法,用一个链表保持相同散列值的元素。
虽然链地址法并不要求哈希桶长度必须为质数,但SGI STL仍然以质数来设计哈希桶长度,并且将28个质数(逐渐呈现大约两倍的关系)计算好,以备随时访问,同时提供一个函数,用来查询在这28个质数之中,“最接近某数并大于某数”的质数。
余数法:就是给每个数mod一个合适的值X,得到的余数放在数组中下标与其一致的位置,由于X不会很大,且余数不会超过数X,数组的大小在0..X-1范围内,不会超空间,如果查某个数,将这个数mod X后在直接在数组中找到。如何选择X值呢,通常是选一个较大但不会使数组超空间的质数。
哈希表的桶个数为什么是质数,合数有何不妥?
哈希表的桶个数使用质数,可以最大程度减少冲突概率,使哈希后的数据分布的更加均匀。
如果使用合数,可能会造成很多数据分布会集中在某些点上,从而影响哈希表效率。
20、栈和堆
栈溢出
程序向栈中某个变量写入的字节数超过了这个变量本身所申请的字节数,会导致栈中与其相邻的变量的值被改变。
原因
1、局部数组过大。局部变量是存在栈中,解决的方法:增大占空间,改用动态分配,使用堆heap
2、递归调用层次太多了,递归函数运行时会压栈,压栈太多了,会溢出;
3、指针、数组越界。
栈和堆的区别
都是数据结构,用来存储数据的。
申请方式:
- 栈由系统自动分配和管理;
- 堆由程序员手动分配和管理;
效率:
- 栈由系统分配,速度快,不会有内存碎片;
- 堆中频繁使用malloc和free,会产生内存碎片,效率差;
扩展方向
- 堆是低地址向高地址扩展;
- 栈是高地址向低地址扩展;
局部变量使用栈空间,new/malloc动态申请的内存是堆空间,函数调用时会进行形参和返回值的压栈出栈,也是用的栈空间。
小根堆
堆是一棵完全二叉树(如果一共有h层,那么1~h-1层均满,在h层可能会连续缺失若干个右叶子)。
1)小根堆
若根节点存在左子女则根节点的值小于左子女的值;若根节点存在右子女则根节点的值小于右子女的值。
2)大根堆
若根节点存在左子女则根节点的值大于左子女的值;若根节点存在右子女则根节点的值大于右子女的值。
21、强制类型转换
原因:
c风格太随意,没有关键字和标识符,出了错误不容易排查。
static_cast:代替c中通常的转换,隐式转换。
dynamic_cast:只能派生类之间,且基类有虚函数。
const_cast:只能常量(常量指针->非常量指针、常量引用->非常量引用)
reinterpret_cast:什么都能转,底层,高危操作。
为什么不使用C的强制转换?
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
22、红黑树和AVL
1、平衡性
红黑树是弱平衡的二叉查找树,左右子树高度差不超过1;
AVL是严格的平衡二叉树,每个节点左右子树高度差不超过1;
2、效率
红黑树插入删除的效率低,因为要求低,只要求左右子树高度差不超过一就行了;
而AVL插入删除的效率大,因为要求严格,所以旋转的次数会很多;
3、应用
红黑树适合搜索、插入、删除操作多的情况;
AVL适合查找要求高的;
4、红黑树性质
非黑即红,根叶都黑,红的孩子是黑,节点到任意叶的路径黑节点数目相同;
没有一条路径比其他路径长2倍;
23、怎么用vector存不同类型数据?
用vector<boost::any>,不熟
先创建一个基类,vector元素类型为基类:vector<father*>;
把不同类型都写成同一个基类的派生类,在各自的派生类里面添加自己的数据类型,然后实例化,在push_back()进去
需要使用的时候,再把vec中的元素转化成子类返回;
在需要删除容器里的数据时,需要手动将类delete,不然会内存泄漏。
class Father { public: std::string flag = "I am Father"; }; class ChildOne :public Father { public: std::string c1_flag = "I am ChildOne"; int c1_data = 999; }; class ChildTwo :public Father { public: std::string c2_flag = "I am ChildTwo"; std::string c2_data = "ChildTwo Data"; }; Father * father = new Father(); cout << "father:" << father->flag << endl; ChildOne *c1 = new ChildOne(); cout << "childone:" << c1->flag << endl; ChildTwo *c2 = new ChildTwo(); cout << "childtwo:" << c2->flag << endl; vector<Father*>*f_vec = new vector<Father*>; f_vec->push_back(c1); f_vec->push_back(c2); cout << "c1:" << static_cast<ChildOne*>(f_vec->at(0))->flag <<";"<< static_cast<ChildOne*>(f_vec->at(0))->c1_data << endl; cout << "c2:" << static_cast<ChildTwo*>(f_vec->at(1))->flag <<";"<< static_cast<ChildTwo*>(f_vec->at(1))->c2_data << endl;
24、模板
建立一个通用函数,先不具体指定它用到的数据类型,用虚拟的类型来代替,等调用这个函数时再根据实参来推出真正的类型,这个通用函数就是模板。
还可以类模板。
模板的特化和偏特化?
编译器认为,对特定的类型,如果你对某一个功能有更好的实现,那就听你的。
函数模板特化:这个函数模板的参数是某种特定的类型的话,这个时候,这个模板函数会有针对这个特定参数的特定实现。
类模板特化:模板参数在某种特定类型下的具体实现;
偏特化:显示部分模板参数(假如说原来有Typename T , Typename Y,在使用这个模板时,只用了T,也就是一种类型,那么定义的时候就可以把这个Y省略掉不用)
全特化:对所有的模板参数都进行特化
对主版本模板类、全特化类、偏特化类的调用优先级从高到低进行排序是:全特化类>偏特化类>主版本模板类。