1.顺序容器
1.顺序容器:vector,deque,list,forward_list,array,string。其中除list和forward_list外,其它都支持快速随机访问。
deque a = { 1, 2, 3, 4, 5, 6 };
cout << a[4] << endl ;
2.通常使用vector
forward_list和list的额外内存开销很大。
3.若容器的元素类型没有默认构造函数,在使用容器大小参数构造顺序容器时,必须提供一个元素初始化器。(P294-P295)
4. reverse_iterator 按逆序寻址元素的迭代器
5.迭代器范围:标准库的基础
包含两个迭代器,元素范围是[begin,end)
6.只有顺序容器(array除外)的构造函数才能接受大小参数
7.顺序容器是assign操作(array不支持assign)
用参数指定的元素的拷贝替换左边容器的所有元素——相当于左边容器清空后再用右边指定元素赋值。
a.assign(b);
a.assign(iter1,iter2); 某个容器的两个迭代器
a.assign(n,t); //a的size变成n,每个元素都设为t。
assign会使迭代器,指针和引用失效。
8.swap 两个swap的容器要相同类型 (array的类型包括元素类型和size)
除array外:swap不对任何元素拷贝,删除,插入,移动等操作。 仅交换了容器的名字。O(1)时间复杂度。
array:swap会交换元素,O(n)时间复杂度。
array和string的迭代器,指针和引用会失效。其他容器则不会。
2.指针问题
- char*c[]={"ENTER","NEW","POINT","FIRST"};
- char**cp[]={c+3,c+2,c+1,c};
- char***cpp=cp;
- intmain(void)
- {
- printf("%s",**++cpp);
- printf("%s",*--*++cpp+3);
- printf("%s",*cpp[-2]+3);
- printf("%s\n",cpp[-1][-1]+1);
- return0;
- }
3.右值引用
左值和右值:
左值是表达式结束后依然存在的持久对象。
右值是表达式结束时就不再存在的临时对象。
区分左值右值的快捷方法:看能不能对表达式取值,如果能是左值,不能则是右值。
左值引用:
分为非常量左值引用和常量左值引用
非常量左值只能绑定到非常量左值,不能绑定到常量左值,非常量右值和常量右值。
常量左值引用可以绑定到所有类型的值,包括非常量左值,常量左值,非常量右值和常量右值
右值引用:
非常量右值:只能绑定到非常量右值,不能绑定到非常量左值、常量左值和常量右值。
常量右值:可以绑定到非常量右值和常量右值,不能绑定到非常量左值和常量左值。
如果提供了move版本的构造函数,就不会生成默认构造函数。
编译器永远不会自动生成move版本的构造函数和复制函数,需要手动显示添加。
选择构造和赋值的重载函数优先级顺序:
1.常量值只能绑定到常量引用上,不能绑定到非常量引用上。
2.左值优先绑定到左值引用上,右值优先绑定到右值引用上。
2.非常量值优先绑定到非常量引用上。
在move版本的构造函数或赋值函数内部,都直接移动了其内部数据的指针(因为它是非常量右值,是一个临时对象,移动了其内部数据的指针不会导致任何问题,它马上就要被销毁了,我们只是重复利用了其内存 ),这样节省了拷贝数据的大量开销。
右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。
转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。
通过转移语义,临时对象中的资源能够转移其它的对象里。
转移构造函数:
1. 参数(右值)的符号必须是右值引用符号,即“&&”。
2. 参数(右值)不可以是常量,因为我们需要修改右值。
3. 参数(右值)的资源链接和标记必须修改。否则,右值的析构函数就会释放资源。转移到新对象的资源也就无效了。
标准库函数std::move
编译器只对右值引用才能调用转移构造函数和转移赋值函数。而所有命名空间都只能是左值引用。
std::move用来将左值引用转换为右值引用。
精确传递perfect forwarding
将一组参数原封不动的转递给另一个函数
原封不动是指:参数值不变,还有左值/右值,const/not-const属性都不变。
接受一个右值引用的参数,就能够将所有的参数类型原封不动的传递给目标函数。
们在设计类的时候如果有动态申请的资源,也应该设计转移构造函数和转移拷贝函数。在设计类库时,还应该考虑 std::move 的使用场景并积极使用它
4.拷贝构造函数、析构函数
定义一个类是,要显示或者隐式的指定该类型的对象拷贝、移动、赋值和销毁时做什么。
拷贝构造函数的第一个参数必须是一个引用类型:如果不是引用类型,则调用永远不会成功。因为想要调用拷贝构造函数,必须拷贝实参,单位了拷贝实参,有需要调用拷贝构造函数,无限循环。
即使定义了其他构造函数,编译器默认生成合成拷贝构造函数。
explicit:抑制狗杂函数的隐式转换。
重载运算符本质上是函数,起名字由operator关键字后接表示要定义的运算符的符号组成。
赋值运算符通常返回一个指向其左侧运算对象的引用。
析构函数:释放对象使用资源,销毁对象的非static数据成员。
不接受参数,不能被重载,一个给定类只有一个析构函数。
当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
需要析构函数的类也需要拷贝和赋值操作
需要拷贝操作的类也需要赋值操作,需要赋值操作的类也需要拷贝操作
但需要拷贝构造函数或需要拷贝赋值运算符的类不意味着也需要析构函数。
当累的程艳中有指针类型,需要在析构函数中显示delete来释放空间,并且拷贝构造函数和拷贝赋值运算符要求深拷贝而不是浅拷贝。
=default修饰成员声明,显示要求编译器生成合成的版本。
类内用=default表示合成的函数隐式声明为内联
类外用=default表示何晨固定成员不是内联函数
5.内存溢出、内存泄漏
内存泄漏最终会导致内存溢出
内存溢出:申请了一个int却存放了一个long long
内存泄漏:已申请的内存无法释放
3、内存溢出 vs 内存泄漏
【关系】内存泄露是导致内存溢出的原因之一;内存泄露积累起来将导致内存溢出。
【out of memory:】要求分配的内存超出系统能给你的,系统不能满足需求。
例如:申请了一个integer,但存入了一个long,会导致内存溢出。
申请了10个字节的空间,但是写入11个字节的数据。
可能的原因:
(1) 使用非类型安全(non-type-safe)的语言如 C/C++ 等。
(2) 以不可靠的方式存取或者复制内存缓冲区。 (3) 编译器设置的内存缓冲区太靠近关键数据结构。
注意:
1. 内存溢出问题是 C 语言或者 C++ 语言所固有的缺陷,它们既不检查数组边界,又不检查类型可靠性(type-safety)。众所周知,用 C/C++ 语言开发的程序由于目标代码非常接近机器内核,因而能够直接访问内存和寄存器,这种特性大大提升了 C/C++ 语言代码的性能。只要合理编码,C/C++ 应用程序在执行效率上必然优于其它高级语言。然而,C/C++ 语言导致内存溢出问题的可能性也要大许多。其他语言也存在内容溢出问题,但它往往不是程序员的失误,而是应用程序的运行时环境出错所致。
2. 当应用程序读取用户(也可能是恶意攻击者)数据,试图复制到应用程序开辟的内存缓冲区中,却无法保证缓冲区的空间足够时(换言之,假设代码申请了 N 字节大小的内存缓冲区,随后又向其中复制超过 N 字节的数据)。内存缓冲区就可能会溢出。想一想,如果你向 12 盎司的玻璃杯中倒入 16 盎司水,那么多出来的 4 盎司水怎么办?当然会满到玻璃杯外面了! 3. 最重要的是,C/C++ 编译器开辟的内存缓冲区常常邻近重要的数据结构。现在假设某个函数的堆栈紧接在在内存缓冲区后面时,其中保存的函数返回地址就会与内存缓冲区相邻。此时,恶意攻击者就可以向内存缓冲区复制大量数据,从而使得内存缓冲区溢出并覆盖原先保存于堆栈中的函数返回地址。这样,函数的返回地址就被攻击者换成了他指定的数值;一旦函数调用完毕,就会继续执行“函数返回地址”处的代码。非但如此,C++ 的某些其它数据结构,比如 v-table 、例外事件处理程序、函数指针等,也可能受到类似的攻击。
【memoryleak:】系统无法再给你提供内存资源(内存资源耗尽),通常是没能及时清理内存垃圾。
可能的原因:
程序逻辑问题,申请的内存无法释放(如死循环);对象的引用被无意识的保留起来,使得该对象无法被GC;缓存,一旦把对象引用放入缓存中,它很容易被忘掉。(当所需要的缓存项的声明周期由该键的外部引用而不是值决定时,应使用WeakHashMap代表缓存。)监听器和其他回调,如果对于某个接口,客户端在其中注册了回调,却没有显式地取消注册,则它们就会积聚。(应该只保存其弱引用)
预防:
1) 尽早释放无用对象的引用,尤其是大对象。
好的办法是使用临时变量的时候,让引用变量在退出活动域后自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄露。 2) 程序进行字符串处理时,尽量避免使用String,而应使用StringBuffer。 因为每一个String对象都会独立占用内存一块区域 3) 尽量少用静态变量。 因为静态变量是全局的,GC不会回收。 4) 避免集中创建对象尤其是大对象,如果可以的话尽量使用流操作。 JVM会突然需要大量内存,这时会触发GC优化系统内存环境;如使用smartUpload作文件上传,运行过程中经常出现java.outofMemoryError的错误 5) 尽量运用对象池技术以提高系统性能。 生命周期长的对象拥有生命周期短的对象时容易引发内存泄漏,例如大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理,然后解决一块释放一块的策略。 典型的例子就是android中AdapterView对应的Adapter中bindview会重用已有的对象元素。 6) 不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。 可以适当的使用hashtable,vector 创建一组对象容器,然后从容器中去取那些对象,而不用每次new之后又丢弃。 7) 优化配置 在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。
6.sizeof malloc/free new/delete strlen inline 关键字
数据对齐:
指数据所在内存地址必须是该数据长度的整数倍。
sizeof:
float的大小是4字节,double大小是8字节
malloc/free和new/delete的区别
都可以申请动态内存和释放内存。
malloc/free是库函数,只能对内置类型的对象做操作。
new/delete是运算符,在分配内存时会自动执行构造函数,释放内存的的时候执行析构函数。
sizeof和strlen的区别:
sizeof是运算符,strlen是函数。
sizeof可以用类型做参数,strlen只能用char *做参数,而且必须是以“\0"结尾的。
数组传递给sizeof不退化,传递黑strlen退化为指针。
sizeof计算的是某种类型的对象在内存中所占的单元字节,strlen计算的是字符串的长度。
sizeof(string)的值在一个库中是固定不变的。
如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
inline是在编译时展开,宏和模板也是编译时解析
7.面向对象、类
面向对象的三个原则:
封装,继承,多态
里氏代换原则:子类型必须能够替换他们的基本类型。
开闭原则:短剑对扩展应该是开放的,对修改是封闭的。
面向对象设计不外乎遵循五大原则:
第一、单一职责原则,即一个类应该只负责单一的职责,而将其余的职责让其他类来承担,这样每个类之间相互协调来完成一件任务。
第二、开闭原则,即对扩展是开放的,对修改是封闭的,因此需要注重抽象的运用 第三、替换原则 ,子类应该可以替换在父类出现的任何地方 第四、依赖倒置原则,依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。第五、接口分离原则,不要将一大堆方法都糅合在一个接口里面形成一个大而全的接口,要将他们按照职责和功能分离到多个小接口中去
多态:一个接口,多种方法。程序在运行的时候才决定使用的函数。
多态性是允许将子类类型指针赋值给父类型指针。
封装可以隐藏实现细节,使得代码模块化;
继承可以扩展已存在的代码模块 。 封装和继承的目的都是代码重用。
多态是实现了接口重用。
C++Primer:
封装实现了类的接口和实现的分离,封装好的类隐藏了它的实现细节。
继承可以扩展已存在的代码模块。它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
多态:一个接口,多种方法,程序在运行时才知道调用的函数。实现方法:覆盖:子类重新定义父类的虚函数。
常量必须在构造函数的初始化列表里初始化,或者声明为static
析构函数可以是虚函数。(具有多态性的基类,应该声明一个虚析构函数,勒种有虚函数,也建议声明一个虚析构函数)虚析构函数保证了不会出现由于析构函数没调用而导致的内存泄漏。
构造函数不可以虚函数。
常见的不不能声明为虚函数的有:普通函数(非成员函数);静态成员函数;内联成员函数;构造函数;友元函数。
1、为什么C++不支持普通函数为虚函数?
普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时邦定函数。
2、为什么C++不支持构造函数为虚函数?
这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。(这不就是典型的悖论)
1,从存储空间角度
虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
2,从使用角度
虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。
3、为什么C++不支持内联成员函数为虚函数?
其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数)
4、为什么C++不支持静态成员函数为虚函数?
这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态邦定的必要性。
5、为什么C++不支持友元函数为虚函数?
因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
析构函数可以是内联函数。
虚函数的声明必须与基类中定义方式完全匹配。
析构函数不要抛出异常。 不然会无限的递归下去
但可以把异常完全封装在析构函数部分,不让异常抛出函数之外。
8.初始化列表
必须用初始化列表的有:
1.常量成员:因为常量成员只能初始化不能赋值
2.引用类型:必须在定义时初始化,且之后不能再赋值
3.没有默认构造函数的类类型:是直接调用拷贝构造函数初始化的
9.复制构造函数与赋值操作符的区别
复制构造函数又称拷贝构造函数,它与赋值操作符间的区别体现在以下几个方面
1.从概念上区分: 复制构造函数是构造函数,而赋值操作符属于操作符重载范畴,它通常是类的成员函数 2.从原型上来区分: 复制构造函数原型ClassType(const ClassType &);无返回值 赋值操作符原型ClassType& operator=(const ClassType &);返回值为ClassType的引用,便于连续赋值操作 3.从使用的场合来区分: 复制构造函数用于产生对象,它用于以下几个地方:函数参数为类的值类型时、函数返回值为类类型时以及初始化语句,例如(示例了初始化语句,函数参数与函数返回值为类的值类型时较简单,这里没给出示例) ClassType a; // ClassType b(a); //调用复制构造函数 ClassType c = a; //调用复制构造函数 而赋值操作符要求‘=’的左右对象均已存在,它的作用就是把‘=’右边的对象的值赋给左边的对象 ClassType e; Class Type f; f = e; //调用赋值操作符 4.当类中含有指针成员时,两者的意义有很大区别复制构造函数需为指针变量分配内存空间,并将实参的值拷贝到其中;而赋值操作符它实现的功能仅仅是将‘=’号右边的值拷贝至左值,在左边对象内存不足时,先释放然后再申请。当然赋值操作符必须检测是否是自身赋值,若是则直接返回当前对象的引用而不进行赋值操作
10.虚函数
虚函数的入口地址和普通函数的区别:
虚函数是根据虚函数表,找到入口地址再执行的,实现动态联编
普通函数是简单跳转到一个固定的地址。
在每个对象里面都有虚表指针,指向虚表,虚表里面存放了虚函数的地址。
虚函数表是顺序存放虚函数地址的,不需要用到链表。
保证虚函数表的指针存在于对象实例中最前面的位置
一个对象所占的空间大小只取决于该对象中数据成员所占的空间,而与成员函数无关。
一般继承,无虚函数覆盖
特点是:
1.虚函数按照其声明顺序放于表中。
2.父类的虚函数在子类的前面
一般继承,有虚函数覆盖
特点是:
1.覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2.没有被覆盖的函数依旧。
多重继承,无虚函数覆盖
特点:
1.每个父类都有自己的虚表。
2.子类的虚函数地址被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
多重继承,有虚函数覆盖
特点:
三个父类虚函数表中的f()的位置被替换成了子类的函数指针。
11.运算符重载
当运算符函数是一个类成员时,一元运算符能被无参形式重载。
运算符被定义为全局函数:对于一元运算符是一个参数,对于二元运算符是两个参数
运算符函数被定义为成员函数:一元运算符没有参数,二元运算符有一个参数。
12.类型转换问题
dynamic_cast的转换也需要目标类型和源对象有一定的关系:继承关系
更准确的说,dynamic_cast是用来检查两者是否有继承关系。因此该运算符实际上只接受基于类对象的指针和引用的类转换。
dynamic_cast是支持动态的类型转换,即支持运行时识别指针或引用所指向的对象。
const_cast是转换掉表达式的const性质。
reinterpret_cast(expression)是从位模式的角度用type型在比较低的层次重新解释这个expression。
dynamic_cast运行时检查(是4个转换中唯一一个RTTI操作符,提供运行时类型检查)
dynamic——cast用于在继承体系中进行安全的向下转换downcast,即基类指针/引用到派生类指针/引用的转换。若源和目标类没有继承/被继承关系,编译器会报错。根据判断返回值是否为NULL来确认转换是否成功。返回NULL则表示不成功。
dynamic_cast提供了类型安全性。
dynamic——cast不是强制转换
13.小端模式 大端模式
80x86是小端模式,数据的高位保存在内存的高地址上,数据的低字节保存在内存的低地址上。
在计算机系统只,数值用补码存储
对于正数:原码补码反码都是一样的
对于负数:补码求原码是保留符号位然后取反再加1.
负数的反码是符号位不变,其余各个位取反。
int存储:
a[0] | a[1] | a[2] | a[3] |
低位地址 高位地址
int a=0xf1f2f3f4
则a[0]=f4,a[1]=f3,a[2]=f2,a[3]=f1
若short与int在一个联合体中,则这个short的值是a[1]a[0] 即存储的补码是0xf3f4
14.函数调用 压栈
调用函数时,首先进行参数压栈
压栈顺序是先压从右到左的参数,在最后压函数地址。
还要考虑栈的生长方向:在window下栈是从高地址向低地址生长的。
15.static关键字
1.函数体内的static变量作用范围是该函数体,该变量内存只被分配一次,下次使用时维持上次使用后的值
2.在模块内,static全局变量可被模块内所有函数访问,但不能被模块外的函数体访问
3.在模块内static函数只能被模块内的其他函数调用,不能被模块外的其他函数调用。
4.类中的static成员变量属于整个类所有,对类的所有对象只有一个拷贝
5.类中static成员函数属于整个类,没有this指针,只能访问类的static成员。
16.构造函数,析构函数,赋值函数
C++如何阻止一个类被实例化:
抽象类 or 把构造函数设为private
(没有纯虚类,只有抽象类啊啊!!!类中至少有一个纯虚函数)
构造函数声明为private不会阻止编译器生成默认的copy constructor。
若没有定义默认拷贝构造函数,则编译器会生成合成拷贝构造函数。
只要没定义copy constructor,编译器就会生成默认的copy constructor
在类名后面跟一个关键字final防止其他类继承它。
class NoDerived final { };
在函数声明最后面加上final表示不允许覆盖该函数。
在虚函数声明最后面加上override,则派生类一定要覆盖该函数,否则编译器报错。
静态类型:
是变量声明时的类型或表达式生成的类型
动态类型:
是变量或表达式表示的内存中的对象的类型。
不存在从基类向派生类的隐式类型转换
派生类可以向基类的类型转换的原因是:每个派生类对象都包含一个基类部分,而基类的引用或指针可以绑定到该基类部分上。
如果虚函数使用默认参数,则通过积累指针或引用调用该函数,使用的默认参数是基类中定义的。 (默认参数静态绑定)
17.派生类的三种继承方式
1.公有继承
派生类的对象只可以访问基类中的公有成员
派生类可以访问基类的公有成员和保护成员,基类的公有成员和保护成员作为派生类的成员时保持原有状态,派生类的子类对象能访问公有成员,派生类的子类能访问公有成员和保护成员。
2.私有继承方式
派生类对象不可访问基类中所有成员。
派生类可以访问基类的公有成员和保护成员,基类的公有成员和保护成员都作为派生类的私有成员,不能被派生类的子类所访问。
3.保护继承
派生类对象不可访问基类的所有成员
派生类可以访问基类的公有成员和保护成员,基类的公有成员和保护成员都作为派生类的保护成员,不能被派生类的子类的对象访问,但可以被派生类的子类所访问。
在公有继承下:
基类的私有成员 除基类成员函数能访问到,其他都不能访问到(包括基类对象,派生类对象和派生类)
基类的保护成员 只有基类成员,派生类,派生类的子类能访问到。
基类的公有成员 可被基类成员,基类成员,派生类,派生类成员,派生类子类,派生类子类对象访问
友元关系不能传递,不能继承
using声明
派生类只为它可以访问的名字提供using声明。
在类的内部使用using声明语句,可以将该类的直接或间接基类中的任何可访问成员标记出来。
出现在private:该名字只能被类的成员和友元访问
出现在protected:改名字只能被类的成员,友元和派生类访问
出现在public:该类所有用户都可以访问
隐藏:派生类中隐藏基类中的同名函数,就算参数列表不同也会隐藏。
如果基类与派生类的虚函数接收的实参不同,就无法通过基类的引用或指针调用派生类的虚函数。
如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
18.C++空类有哪些成员函数
缺省构造函数。 . 缺省拷贝构造函数。 . 缺省析构函数。 . 缺省赋值运算符。
. 缺省取址运算符。. 缺省取址运算符 const。
空类,声明时编译器不会生成任何成员函数,只会生成1个字节的占位符。
但在定义其对象时,编译器会生成6个成员函数。
19.虚继承,多继承
假如类A和类B各自从类X派生(非虚继承且假设类X包含一些数据成员),且类C同时自类A和B,那么C的对象就会拥有两套X的实例数据(可分别独立访问,一般要用适当的消歧义限定符)。但是如果类A与B各自虚继承了类X,那么C的对象就只包含一套类X的实例数据。
这一特性在应用中非常有用,可以使得虚基类对于由它直接或间接派生的类来说,拥有一个共同的基类对象实例。避免由于带有歧义的组合而产生的问题(如“菱形继承问题”)。其原理是,间接派生类(C)穿透了其父类(上面例子中的A与B),实质上直接继承了虚基类X。
基类可以通过使用virtual来声明虚继承关系
对间接继承了虚基类的类,也必须能直接访问其虚继承来的祖先类,也即应知道其虚继承来的祖先类的地址偏移值
虚继承:继承定义中包含了virtual关键字的继承关系。但是没解决多个基类中存在同名成员时的二义性问题。
二义性问题是指派生类继承自两个基类时,若有成员同名时,在派生类中的调用要指出是出自哪个基类。
虚基类:在虚继承体系中通过virtual继承而来的基类,虚基类只是相对于虚继承的派生类来说,不能说一个类就是派生类。
虚继承主要是解决最底层派生类对象中存在同一个基类的多份拷贝问题。
多重继承
我们知道,在单继承中,派生类的对象中包含了基类部分和派生类自定义部分。同样的,在多重继承(multiple inheritance)关系中,派生类的对象包含了每个基类的子对象和自定义成员的子对象。下面是一个多重继承关系:
|
|
C继承了A,派生类D又继承了B和C,如图所示,一个D对象中含有一个B部分、一个C部分(其中又含有一个A部分)以及在D中声明的非静态数据成员:
构造与析构:
构造一个派生类对象将首先构造它的所有基类子对象,其中基类的构造顺序与派生列表中基类的出现顺序保持一致,即B -> A -> C -> D。 销毁一个派生类对象的顺序正好与其创建的顺序相反,即析构函数的调用顺序正好与构造函数相反,即D -> C -> A -> B。注意派生类的析构函数只负责清除派生类本身分配的资源(析构函数体),派生类的成员及基类都是自动销毁的(隐式析构阶段)。
类型转换:
在多重继承的情况下,可以令某个可访问基类的指针或引用直接指向一个派生类对象。编译器不会在派生类向基类的几种转换中进行比较和选择,在它看来转换到任意一种基类都一样好。
虚继承
尽管在派生列表中不允许同一个基类出现两次,但实际上派生类可以多次继承同一个类。
派生类通常会含有继承链上每个类对应的子部分。在上面的两种情况中,class D都间接地继承了class A两次,那么意味着class D中包含了class A的两份拷贝。所以在一个class D的对象中将含有2组class A的成员,此时若不加前缀限定符直接使用某个成员将引发“二义性”错误:
|
|
当然你可以使用作用域
d.B::str = "songlee";和
d.B::print();来规避“二义性”错误,但这并没有从根本上解决问题。
为了解决上述问题,C++提供了虚继承(virtual inheritance)的机制。虚继承的目的是令某个类作出声明,承诺愿意共享它的基类。其中,共享的基类子对象称为虚基类。在这种机制下,不论虚基类在继承体系中出现多少次,在派生类中都只包含唯一一个共享的虚基类子对象。我们指定虚基类的方式是在派生列表中添加关键字virtual:
|
|
通过在派生列表中添加virtual(关键字public和virtual的顺序随意)指定A为虚基类,B和C将共享A的同一份实例,这样在D的对象中也将只有A的唯一一份实例,所以A的成员可以被直接访问,并且不会产生二义性。
虚继承最典型的应用是iostream继承于istream和ostream,而istream和ostream虚继承于ios:
|
|
注意:
支持向基类的常规类型转换。也就是说即使基类是虚基类,也能通过基类的指针或引用操作派生类的对象。虚继承只是解决了一个派生类对象中存在同一个基类的多份拷贝的问题,并没有解决多个基类存在同名成员的二义性问题。在虚继承中,虚基类是由最低层的派生类负责初始化的。如上例中,当创建一个D对象时,D位于派生的最低层并由它负责初始化共享的A基类部分。含有虚基类的对象的构造顺序与一般的多重继承的构造顺序稍有区别:先初始化虚基类子对象(最低层派生类负责),然后按派生列表中的顺序依次对直接基类(非虚)进行初始化。析构的顺序与构造的顺序正好相反。
20.多重继承
20.多重继承
在多重继承关系中,派生类的对象包含有每个基类的子对象。
对象、指针和引用的静态类型决定了我们能够使用哪些成员
虚继承对象的构造方式:
先初始化虚基类子对象(最低层派生类负责),然后按派生列表中的顺序依次对直接基类(非虚)进行初始化 。
虚基类总是限于非虚基类构造,与它们在继承体系中的测序和位置无关。
编译器按照基类的声明顺序对其一次进行检查,以确定其中是否含有虚基类,如果有,则先构造虚基类,然后按照声明的顺序逐一构造其他非虚基类。
多继承对象的构造方式:
首先构造它的所有基类子对象,其中基类的构造顺序与派生列表中基类的出现顺序保持一致
21.string相关操作
21.string相关操作
1.将string转换成char *[]
s.c_str()
2.将char *[]转换成int类型
atoi(str)
3.int 转换成 string
int i=123;
string s=to_string(i); //i可以是任何算术类型
4.string 转换成 int
stoi(s,p,b) //p默认值为0,b表示转换所用的基数,默认是10进制。
22.debug编译方式和release编译方式
22.debug编译方式和release编译方式
debug:
包含调试信息,并且不作任何优化,源于程序员调试程序。
release:
称为发布版本,往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户能够很好的使用。
23.volatile,mutable
23.volatile,mutable
对象的值可能在程序的控制或者检测之外被改变时,应该将该对象声明为volatile,该关键字告诉编译器不应该对这样的对象做优化,总是重新从它所在内存读取数据,即使它前面的指令刚刚从该处读取过数据。
const和volatile限定符互不影响。对象可以即是const也是volatile的。
和const一样,只能将一个volatile对象的地址赋给一个指向volatile的指针。
mutable:可变的 与const相反。
被mutable修饰的变量(mutable只能用于修饰类的非静态数据成员),将永远处于可变的状态,即使在一个const函数中。
一个const成员函数可以改变一个可变成员的值。
24.全局变量和静态变量
24.全局变量和静态变量
1.全局变量:不显示用static修饰的全局变量,全局变量默认是静态的,作用域是整个模块。在一个文件内定义的全局变量,在另一个文件中,通过extern全局变量名的声明,就可以使用全局变量。
2.静态变量,用static声明的变量
对于全局变态变量,作用域在本模块,在其他模块中不同使用extern声明使用。
25.STL顺序容器
25.STL顺序容器
vector:可变大小数组。支持快速随机访问。在尾部之外的位置插入或者删除元素可能很慢。
deque:双端队列。支持快速随机访问。在头尾位置插入删除速度很快。
list:双向链表。只支持双向顺序访问。在list中任何位置进行插入删除速度都很快。
forward_list:单向链表。只支持单向顺序访问。在链表任何位置进行插入删除操作速度都很快。
array:固定大小数组。支持快速随机访问。不能添加或删除元素。
string:与vector相似,专门用于保存字符。随机访问快。在尾部插入删除速度快。
26.智能指针,new delete
26.智能指针,new delete
#include
shared_ptr:允许多个指针指向同一个对象
unique_ptr:“独占”所指向的对象
weak_ptr:弱引用,指向shared_ptr所管理的对象。
智能指针也是模板
make_shared函数:在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
每个shared_ptr都有一个关联的计数器,通常称为引用计数,无论何时拷贝一个shared_ptr,计数器都会递增。
计数器递增的情况:
用一个shared_ptr初始化另一个shared_ptr
将它作为参数传递给一个函数
作为函数的返回值
计数器递减的情况:
给shared_ptr赋予一个新值
shared_ptr被销毁
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。
new和delete:
new:创建动态对象的同时完成了初始化工作。
delete:销毁给定的指针指向的对象,释放对应的内存。
可用直接初始化方式用new返回的指针来初始化智能指针。
shared_ptrp(new int(42));
如果使用内置指针管理内存,却在new之后在对应的delete之前发生了异常,则内存不会被释放。
unique_ptr
某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指的对象也被销毁。
unique_ptr不支持普通的拷贝或赋值操作。只能采用直接初始化形式。
可通过调用release和reset将指针的所有权从一个unique_ptr转移给另一个unique。
可以从函数返回一个unique_ptr,可以返回一个局部对象的拷贝。
weak_ptr指向由一个shared_ptr管理的对象。
27.new/delete和malloc/free的区别
27.new/delete和malloc/free的区别
都可以用来申请动态内存和释放内存
new/delete:给定数据类型,new/delete会自动计算内存大小,并进行分配或释放。如果是对类进行操作,new/delete还会自动调用相应的构造函数和析构函数。
new返回指定类型的指针
new/delete是运算符
在delete之后,指针就变成了空悬指针,指向一块曾经保存数据对象但现在已经无效的内存的指针。可在delete后将nullptr赋予指针。
malloc/free:没有进行任何数据类型检查,只负责分配和释放给定大小的内存空间。
而malloc返回void*,必须强制类型转化。
malloc/free是标准库函数
对于非内部数据类型的对象,malloc/free无法满足动态对象的要求。
如果p是NULL指针,则无论对该指针free多少次都不会出问题。
如果p不是NULL指针,则free对p连续操作两次就会导致程序运行错误。
28.C++内存分类详情
28.C++内存分类详情
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。
和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。堆可以动态地扩展和收缩。
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的(初始化的全局变量和静态变量在一块区域,未初始化的全局变量与静态变量在相邻的另一块区域,同时未被初始化的对象存储区可以通过void*来访问和操纵,程序结束后由系统自行释放),在C++里面没有这个区分了,他们共同占用同一块内存区。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)
29.堆和栈的区别
29.堆和栈的区别
主要的区别由以下几点:
1、管理方式不同;
2、空间大小不同;
3、能否产生碎片不同;
4、生长方向不同;
5、分配方式不同;
6、分配效率不同;
管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:打开工程,依次操作菜单如下:Project->Setting->Link,在Category中选中Output,然后在Reserve中设定堆栈的最大值和commit。注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。
碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。
生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 malloc函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
30.常成员函数
30.常成员函数
(1)const是函数类型的一部分,在实现部分也要带该关键字。
(2)const关键字可以用于对重载函数的区分。
(3)常成员函数不能更新类的成员变量,也不能调用该类中没有用const修饰的成员函数,只能调用常成员函数。
31.虚函数的用法
31.虚函数的用法
虚函数的用法(构造函数和析构函数)
虚函数的使用方法:
1.在基类中用virtual声明成员函数为虚函数
2.在派生类中重新定义此函数,要求函数名,函数类型,参数个数类型全部与基类一致。并根据派生列的需求重新定义函数体。
3.定义一个基类的指针变量,并使它指向派生类的对象。
4.通过该指针变量调用此虚函数,此时调用的就是指针所指对象类型的虚函数。
有时在基类中定义的非虚函数会在派生类中被重新定义,如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。
32.虚函数的作用
32.虚函数的作用
实现多态性。虚函数的作用就是实现“动态联编”,也就是在程序的运行阶段动态地选择合适的成员函数。
要实现动态联编,即只能用一个基类的指针或者引用来指向派生类对象
33.虚函数实现机制
33.虚函数实现机制
虚函数实现机制:(一个类只有一个虚表,一个对象有一个虚表指针)
每个类用了一个虚表,每个类的对象用了一个虚函数指针
虚函数表的创建:
在基类中,根据声明找到所有的虚函数,按照虚函数的声明顺序,为基类常见一个虚函数表,内容就是指向这些虚函数的函数指针。
在子类中,现将基类的虚函数表复制到该子类的虚函数表中,若子类重写了基类的虚函数,咋把先前复制的基类虚函数表中对应位置改为新写的虚函数。若子类中还定义了新的虚函数,按声明顺序,将这些虚函数地址加载虚函数表的后面。
每个对象都有一个虚表指针。
当执行pBase->show()时,检查show的声明是否是虚函数,若为虚函数,则根据指针所指对象的类型,得到对象所属类的虚函数表的地址,然后获得该虚函数的地址,执行。
如果不是虚函数,则根据指针的类型,来确定调用哪个类的函数。
tips:
1.类中固定静态函数成员不能是虚函数。构造函数不能是虚函数,析构函数可以是虚函数( 当利用delete删除一个指向的时,系统会调用相应的类的析构函数。)。
2.当将基类中的某一声明为虚函数后,派生类中的同名函数(函数名相同、参数列表完全一致、返回值类型相关)自动成为虚函数。
34.C里面的 Struct 和 C++ 里的 Class 的异同
34.C里面的 Struct 和 C++ 里的 Class 的异同
在c中struct不能定义成员函数,而在C++中则是允许的。
主要区别是成员的默认访问控制权限。
这里分两种情况来回答
(1)C的struct与C++的class的区别。
C是一种过程化的语言,struct只是作为一种复杂数据类型定义,struct中只能定义成员变量,不能定义成员函数。 (2)C++中的struct和class的区别。
访问权限上:class中默认的成员访问权限是private的,而struct中则是public的。
继承上:class继承默认是private继承,而struct继承默认是public继承。
其他:“class”这个关键字还用于定义模板参数,就像“typename”,但关键字“struct”不用于定义模板参数。
35.面向对象 面向过程
35.面向对象 面向过程
面向过程是自顶向下逐步,就是分析出解决问题所需的步骤,将代码封装成函数,然后用函数把步骤一步步实现。其最重要的是模块化的思想方法。
面向对象的方法主要是把事物给对象化,包括其属性和行为。抽象出对象的目的并不在于完成某个步骤,而是描述其在整个解决问题的步骤中的行为。
简单点说就是,面向过程就是你把代码封装成函数,然后依次调用去做一件事情;面向对象就是你把要做的事情抽象成对象,告诉对象去做。面向对象三大特性(封装,继承,多态)使得在做复杂的事情的时候效率和正确率得到保证。
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
36.C++多态性
36.C++多态性
多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。多态性是通过虚函数来实现的,只有重写了虚函数的才能算作是体现了C++多态性。多态的目的则是为了接口重用,
不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。
37.封装继承多态
37.封装继承多态
封装:把客观事物抽象成类,实现类的实现与接口分离,让类的实现部分隐藏起来。
继承:派生类除了可以使用现有类的所有功能,还可以在不重写原有类的情况下,对功能进行扩展。
多态:一个接口,多种方法。用虚函数实现的,主要目的是实现接口重用,传递过来的不论究竟是那个类的对象,都可以通过同一个接口调用适合对象的函数