-
C++面向对象的三大特性
-
抽象
抽象是指强调实体的本质、内在的属性。在系统开发中,抽象指的是在决定如何实现对象之前的对象的意义和行为。使用抽象可以尽可能避免过早考虑一些细节。
-
继承
继承性是子类自动共享父类数据结构和方法的机制,这是类之间的一种关系。在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容。
-
多态
多态性是指相同的操作或函数、过程可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果,这种现象称为多态性。
-
静态多态:重载/泛型
-
实现方式
重载是在同一作用域内(不管是模块内还是类内,只要是在同一作用域内),具有相同函数名,不同的形参个数或者形参类型。返回值可以相同也可以不同(在函数名、形参个数、形参类型都相同而返回值类型不同的情况下无法构成重载,编译器报错。这个道理很简单,在函数调用的时候是不看返回值类型的)。
泛型在C++中的主要实现为模板函数和模板类。
- 函数模板并不是真正的函数,它只是C++编译生成具体函数的一个模子。
2) 函数模板本身并不生成函数,实际生成的函数是替换函数模板的那个函数,比如add(sum1,sum2),这种替换是编译期就绑定的。
3) 函数模板不是只编译一份满足多重需要,而是为每一种替换它的函数编译一份。
- 函数模板不允许自动类型转换。
- 函数模板不可以设置默认模板实参。比如template <typename T=0>不可以。
-
实现原理
重载是一种静态多态,即在编译的时候确定的。C++实现重载的方式是跟编译器有关,编译过后C++的函数名会发生改变,会带有形参个数、类型以及返回值类型的信息(虽然带有返回值类型但是返回值类型不能区分这个函数),所以编译器能够区分具有不同形参个数或者类型以及相同函数名的函数。插一句,在C语言中编译器编译过后函数名中不会带有形参个数以及类型的信息,因此C语言没有重载的特性。由此带来麻烦的一点是如果想要在C++中调用C语言的库,需要特殊的操作(extern “C”{})。库中的函数经过C编译器编译的话会生成不带有形参信息的函数名,而用C++的编译器编译过后会生成带有形参信息的函数名,因此将会找不到这个函数。extern “C”{}的作用是使在这个作用域中的语句用C编译器编译,这样就不会出错。这也是一种语言兼容性的问题。
-
-
动态多态:虚函数/重写
-
实现方式
重写是在不同作用域内(一个在父类一个在子类),函数名、形参个数、形参类型、返回值类型都相同并且父类中带有virtual关键字(换言之子类中带不带virtual都没有关系)。有一种特殊的情况:函数返回值类型可以不同但是必须是指针或者引用,并且两个虚函数的返回值之间必须要构成父子类关系。这种情况称之为协变,也是一种重写。引入协变的好处是为了避免危险的类型转换。
-
实现原理
重写是一种动态多态,即在运行时确定的。C++实现重写的方式也跟编译器有关,编译器在实例化一个具有虚函数的类时会生成一个vptr指针(这就是为什么静态函数、友元函数不能声明为虚函数,因为它们不实例化也可以调用,而虚函数必须要实例化,这也是为什么构造函数不能声明为虚函数,因为你要调用虚函数必须得要有vptr指针,而构造函数此时还没有被调用,内存中还不存在vptr指针,逻辑上矛盾了)。
vptr指针在类的内存空间中占最低地址的四字节。vptr指针指向的空间称为虚函数表,vptr指针指向其表头,在虚函数表里面按声明顺序存放了虚函数的函数指针,如果在子类中重写了,在子类的内存空间中也会产生一个vptr指针,同时会把父类的虚函数表copy一下当做自己的,然后如果在子类中重新声明了虚函数,会按声明顺序接在父类的虚函数函数指针下。而子类中重写的虚函数则会替换掉虚函数表中原先父类的虚函数函数指针。
在调用虚函数时,不管调用他的是父类的指针、引用还是子类的指针、引用,他都不管,只看他所指向或者引用的对象的类型(这也称为动态联编),如果是父类的对象,那就调用父类里面的vptr指针然后找到相应的虚函数,如果是子类的对象,那就调用子类里面的vptr指针然后找到相应的虚函数。当然这样子的过程相比静态多态而言,时间和空间上的开销都多了(这也是为什么内联函数为什么不能声明为虚函数,因为这和内联函数加快执行速度的初衷相矛盾)。
-
虚函数的调用
当通过指针的形式调用对应类的函数时,程序会经历以下几步:
1)若函数为普通函数,直接调用对应函数,返回。
2)若函数为虚函数,则根据内存首地址存储的虚函数表地址跳转至虚函数表。
3)在虚函数表中查找对应名称的虚函数,找到后调用,返回。
-
不能被定义为虚函数的函数
1)友元函数,它不是类的成员函数
2)全局函数
3)静态成员函数,它没有this指针
4)构造函数,拷贝构造函数,以及赋值运算符重载(可以但是一般不建议作为虚函数)
-
-
-
-
C++中class(类)与struct(结构体)区别
1、class是引用类型,struct是值类型;
2、class可以继承类、接口和被继承,struct只能继承接口,不能被继承;
3、class有默认的无参构造函数,有析构函数,struct没有默认的无参构造函数,且只能声明有参的构造函数,没有析构函数;
4、class可以使用abstract和sealed,有protected修饰符,struct不可以用abstract和sealed,没有protected修饰符;
5、class必须使用new初始化,结构可以不用new初始化;
6、class实例由垃圾回收机制来保证内存的回收处理,而struct变量使用完后立即自动解除内存分配;
7、从职能观点来看,class表现为行为,而struct常用于存储数据;
8、作为参数传递时,class变量以按址方式传递,而struct变量是以按值方式传递的。
另一份解释:
1.struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。
2.struct没有继承,没有封装,要说封装只有初步封装。而class把数据,接口可以以三种类型封装,private,public,protected;还可以继承和派生。
3.它们都可以提供自己的接口函数,构造函数。一个类可以由结构继承而来。struct只能叫做数据的集合,外部可以任意访问,但是类就完成了封装,维护了数据安全,这就是面向对象的理念。
-
C++的堆与栈
-
堆(heap)
堆英文名称 heap,在内存管理的语境下,指的是动态分配的内存空间,这个和数据结构的堆是两回事。这里的内存,被分配之后需要手动释放,否则会引发内存泄漏。
**生长方向:**堆是朝着地址增大的方向生长的
C语言中使用void* malloc(size_t size)来申请一块内存空间,size为申请的字节数。 使用void free(void* ptr) 来手动释放内存。
C++则使用 new 和 delete 来申请释放内存。
-
malloc、free与new、delete的关联与区别
实际上 new, delete 的底层实现是 malloc, free;malloc 只是单纯地申请一块内存空间,但是new不一样,C++中包含面向对象的设计,当我们在new一个对象时,C++不仅要向系统申请一块内存,还需要构造这个对象,调用构造函数,而delete时,则需要调用类的析构函数,然后归还内存空间。
-
-
栈(stack)
在内存管理的语境下,指的是函数调用过程中产生的本地变量和调用数据的区域。 这个栈和数据结构里的栈高度相似,都满足后进先出(last-in-first-out 或 LIFO)。
**生长方向:**栈是朝着地址减小的方向生长的。
某个函数占用的栈空间有个特定的术语,叫做栈帧(stack frame)。
-
-
C++STL
-
STL 通常由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成。
容器:封装完成的数据结构模板类,例如vector、list等
算法:主要是被提供的数据结构算法,往往在std命名空间中定义
迭代器:实现对容器内数据的读写,作为容器与算法之间的润换粘合剂
函数对象:如果一个类将()运算符重载为成员函数,那么这个类就称为函数对象类,这个类的对象就称为函数对象类
适配器:将一个类的接口适配成用户指定形式,使原来不能一起工作的两个类一同工作。容器、迭代器、函数对象均有适配器存在
内存分配器:为模板类提供自定义的内存申请和释放功能
-
STL的实现
1.vector 底层数据结构为数组 ,支持快速随机访问。当已经分配的空间不够装下数据时,分配双倍于当前容量的存储区,把当前的值拷贝到新分配的内存中,并释放原来的内存。
2.list: 底层数据结构为双向链表,支持快速增删。以结点为单位存放数据,结点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间
3.deque: 底层数据结构为一个中央控制器和多个缓冲区,详细见STL源码剖析P146,支持首尾(中间不能)快速增删,也支持随机访问。deque动态地以分段连续空间组合而成,随时可以增加一段新的连续空间并链接起来。不提供空间保留功能。deque采用一块map(不是STL的map容器)作为主控,其为一小块连续空间,其中每个元素都是指针,指向另一段较大的连续空间(缓冲区)。
4.stack : 底层一般用23实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时。
5.queue: 底层一般用23实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时(stack和queue其实是适配器,而不叫容器,因为是对容器的再封装)。
6.priority_queue: 的底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现
7.set: 底层数据结构为红黑树,有序,不重复。
8.multiset: 底层数据结构为红黑树,有序,可重复。
9.map: 底层数据结构为红黑树,有序,不重复。通过map的迭代器不能修改其键值,只能修改其实值。所以map的迭代器既不是const也不是mutable。
10.multimap: 底层数据结构为红黑树,有序,可重复。
11.hash_set: 底层数据结构为hash表,无序,不重复。
12.hash_multiset: 底层数据结构为hash表,无序,可重复 。
13.hash_map : 底层数据结构为hash表,无序,不重复。
14.hash_multimap: 底层数据结构为hash表,无序,可重复。
15.hashtable: 底层数据结构是vector。
-
Map与HashMap之间的选取
hash_map查找速度比map快,而且查找速度基本和数据量大小无关,属于常数级别。而map的查找速度是logn级别。但不一定常数就比log 小,而且hash_map还有hash function耗时。
如果考虑效率,特别当元素达到一定数量级时,用hash_map。
考虑内存,或者元素数量较少时,用map。
-
hashtable如何避免哈希冲突
1)线性探测:先用hash function计算某个元素的插入位置,如果该位置的空间已被占用,则继续往下寻找,知道找到一个可用空间为止。
进行元素搜索的时候,如果hash function计算出来的位置上的元素值与我们搜寻目标不符,就循环往下一一寻找,直到找到吻合者,或直到遇上空格元素。
其删除采用惰性删除:只标记删除记号,实际删除操作等到表格重新整理时再进行。(因为hash table中的每一个元素不仅表述它自己,也关系到其他元素的排列。)
2)二次探测:如果计算出的位置为H且被占用,则依次尝试H+1^2,H+2^2等(解决线性探测中主集团问题)。
3)开链:每一个表格元素中维护一个list,hash function为我们分配一个list,然后在那个list执行插入、删除等操作。
-
不允许有遍历行为的容器有哪些(不提供迭代器)?
1)queque,除了头部外,没有其他方法存取deque的其他元素。
2)stack(底层以deque实现),除了最顶端外,没有任何其他方法可以存取stack的其他元素。
3)heap,所有元素都必须遵循特别的排序规则,不提供遍历功能。
-
STL容器的时间复杂度
-
1.vector
push_back()、)pop_back()、访问:O(1)
insert()、erase():O(n)
-
2.list
push_front、push_back、pop_front、pop_back、insert、erase:O(1)
访问:O(n)
-
3.dequeue
push_front、push_back、pop_front、pop_back、访问:O(1)
insert、erase:O(n)
-
4.map 、set、mulitmap、multiset
插入、查看、删除:O(logN)
-
5.hashmap、hashset、hashmultimap、hashmultiset
插入、查看、删除:O(1)最坏情况:O(N)
-
-