C++知识概要文档格式.docx
《C++知识概要文档格式.docx》由会员分享,可在线阅读,更多相关《C++知识概要文档格式.docx(19页珍藏版)》请在冰点文库上搜索。
1.虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)
2.基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)
3.类类型的成员对象的构造函数(按照初始化顺序)
4.派生类自己的构造函数
1.必须使用成员列表初始化的四种情况
∙当初始化一个引用成员时
∙当初始化一个常量成员时
∙当调用一个基类的构造函数,而它拥有一组参数时
∙当调用一个成员类的构造函数,而它拥有一组参数时
1.构造函数为什么不能为虚函数
∙虚函数对应一个指向虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。
问题出来了,假设构造函数是虚的,就须要通过vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?
所以构造函数不能是虚函数。
∙因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtualfunction主要是为了在不完全了解细节的情况下也能正确处理对象。
另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,也就不能使用virtual函数来完成你想完成的动作
1.析构函数为什么要虚函数
C++中基类采用virtual虚析构函数是为了防止内存泄漏。
具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。
假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。
那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。
1.构造函数析构函数可以调用虚函数吗
在构造函数和析构函数中最好不要调用虚函数
构造函数或者析构函数调用虚函数并不会发挥虚函数动态绑定的特性,跟普通函数没区别
即使构造函数或者析构函数如果能成功调用虚函数,程序的运行结果也是不可控的
1.空类的大小是多少?
为什么
∙C++空类的大小不为0,不同编译器设置不一样,vs设置为1
∙C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址
∙带有虚函数的C++类大小不为1,因为每一个对象会有一个vptr指向虚函数表,具体大小根据指针大小确定
∙C++中要求对于类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址
1.移动构造函数
A(A&
&
b){
***
}//a=std:
:
move(b)
1.移动赋值
A&
operator=(A&
return*this;
}
1.类如何实现只能静态分配和只能动态分配
前者是把new、delete运算符重载为private属性。
后者是把构造、析构函数设为protected属性,再用子类来动态创建
建立类的对象有两种方式:
1.静态建立,静态建立一个类对象,就是由编译器为对象在栈空间中分配内存;
2.动态建立,就是使用new运算符为对象在堆空间中分配内存。
这个过程分为两步,第一步执行operatornew()函数,在堆中搜索一块内存并进行分配;
第二步调用类构造函数构造对象
1.什么情况会自动生成默认构造函数
带有默认构造函数的类成员对象
带有默认构造函数的基类
带有一个虚函数的类
带有一个虚基类的类
合成的默认构造函数中,只有基类子对象和成员类对象会被初始化。
所有其他的非静态数据成员都不会被初始化
1.如何消除隐式转换
C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换
如果构造函数只接受一个参数,则它实际上定义了转换为此类类型的隐式转换机制。
可以通过将构造函数声明为explicit加以制止隐式类型转换,关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit。
1.派生类指针转换为基类指针,指针值会不会变
将一个派生类的指针转换成某一个基类指针,编译器会将指针的值偏移到该基类在对象内存中的起始位置
1.C语言的编译链接过程
源代码-->
预处理-->
编译-->
优化-->
汇编-->
链接-->
可执行文件
∙预处理
读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。
包括宏定义替换、条件编译指令、头文件包含指令、特殊符号
∙编译
编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码
∙汇编
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程
∙链接阶段
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
1.容器内部删除一个元素
1.顺序容器
erase迭代器不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效(list除外),所以不能使用erase(it++)的方式,但是erase的返回值是下一个有效迭代器;
it=c.erase(it);
2.关联容器
erase迭代器只是被删除元素的迭代器失效,但是返回值是void,所以要采用erase(it++)的方式删除迭代器;
c.erase(it++)
1.vector越界访问下标,map越界访问下标?
vector删除元素时会不会释放空间
通过下标访问vector中的元素时不会做边界检查,即便下标越界。
也就是说,下标与first迭代器相加的结果超过了finish迭代器的位置,程序也不会报错,而是返回这个地址中存储的值。
如果想在访问vector中的元素时首先进行边界检查,可以使用vector中的at函数。
通过使用at函数不但可以通过下标访问vector中的元素,而且在at函数内部会对下标进行边界检查
map的下标运算符[]的作用是:
将key作为下标去执行查找,并返回相应的值;
如果不存在这个key,就将一个具有该key和value的默认值插入这个map
erase()函数,只能删除内容,不能改变容量大小;
erase成员函数,它删除了itVect迭代器指向的元素,并且返回要被删除的itVect之后的迭代器,迭代器相当于一个智能指针,之后迭代器将失效。
;
clear()函数,只能清空内容,不能改变容量大小;
如果要想在删除内容的同时释放内存,那么你可以选择deque容器
intmain(){
vector<
int>
vec(10,0);
intarr[10]={0,0,0,0,0,0,0,0,0,0};
cout<
<
vec[11]<
endl;
//输出值
*(vec.begin()+11)<
vec.at(11);
//报错,越界
arr[11];
1.vector的增加删除都是怎么做的?
为什么是1.5倍
vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,再插入新增的元素
初始时刻vector的capacity为0,塞入第一个元素后capacity增加为1
不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容
对比可以发现采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度,因此,使用成倍的方式扩容
以2倍的方式扩容,导致下一次申请的内存必然大于之前分配内存的总和,导致之前分配的内存不能再被使用,所以最好倍增长因子设置为(1,2)之间
向量容器vector的成员函数pop_back()可以删除最后一个元素
而函数erase()可以删除由一个iterator指出的元素,也可以删除一个指定范围的元素
还可以采用通用算法remove()来删除vector容器中的元素
采用remove一般情况下不会改变容器的大小,而pop_back()与erase()等成员函数会改变容器的大小,使得之后所有迭代器、引用和指针都失效
1.函数指针
函数指针指向的是特殊的数据类型,函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分
函数指针声明
int(*pf)(constint&
constint&
);
上面的pf就是一个函数指针,指向所有返回类型为int,并带有两个constint&
参数的函数。
应该注意的是*pf两边的括号是必须的否则就是声明了一个返回int*类型的函数
函数指针赋值
指针名=函数名;
指针名=&
函数名;
1.c/c++的内存分配,详细说一下栈、堆、静态存储区
代码段
只读,可共享;
代码段(codesegment/textsegment)通常是指用来存放程序执行代码的一块内存区域。
这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。
在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等
数据段
储存已被初始化了的静态数据。
数据段(datasegment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。
数据段属于静态内存分配。
BSS段
未初始化的数据段。
BSS段(bsssegment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。
BSS是英文BlockStartedbySymbol的简称。
BSS段属于静态内存分配(BSS段和data段的区别是,如果一个全局变量没有被初始化(或被初始化为0),那么他就存放在bss段;
如果一个全局变量被初始化为非0,那么他就被存放在data段)
堆(heap)
堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);
当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
栈(stack)
栈又称堆栈,是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。
除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。
由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。
从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
共享内存映射区域
栈和堆之间,有一个共享内存的映射的区域。
这个就是共享内存存放的地方。
一般共享内存的默认大小是32M
综上:
栈区(stack)
—由编译器自动分配释放,存放函数的参数值,局部变量的值等其操作方式类似于数据结构中的栈
堆区(heap)
—一般由程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统)回收。
注意它与数据结构中的堆是两回事,分配方式倒是类似于链表
全局区(静态区)(static)
—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
程序结束后由系统释放
文字常量区
—常量字符串就是放在这里的。
程序代码区
—存放函数体的二进制代码
1.堆与栈的区别
管理方式:
对于栈来讲,是由编译器自动管理,无需我们手工控制;
对于堆来说,释放工作由程序员控制,容易产生memoryleak
空间大小:
一般来讲在32位系统下,堆内存可以达到4G的空间,但是对于栈来讲,一般都是有一定的空间大小的
碎片问题:
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。
对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出
生长方向:
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;
对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式:
堆都是动态分配的,没有静态分配的堆。
栈有2种分配方式:
静态分配和动态分配。
静态分配是编译器完成的,比如局部变量的分配。
动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现
分配效率:
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:
分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高,堆则是C/C++函数库提供的
1.野指针是什么?
野指针:
指向内存被释放的内存或者没有访问权限的内存的指针。
它的成因有三个:
1.指针变量没有被初始化。
2.指针p被free或者delete之后,没有置为NULL。
3.指针操作超越了变量的作用范围
(觉得存在错误)
1.悬空指针和野指针有什么区别
野指针指,访问一个已删除或访问受限的内存区域的指针,野指针不能判断是否为NULL来避免。
指针没有初始化,释放后没有置空,越界
悬空指针:
一个指针的指向对象已被删除,那么就成了悬空指针。
野指针是那些未初始化的指针
1.内存泄漏
内存泄漏
是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。
内存泄漏并非指内存在物理上消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制(内存泄露的排查诊断与解决)
1.new和delete的实现原理,delete是如何知道释放内存的大小的
1.new表达式调用一个名为operatornew(operatornew[])函数,分配一块足够大的、原始的、未命名的内存空间
2.编译器运行相应的构造函数以构造这些对象,并为其传入初始值
3.对象被分配了空间并构造完成,返回一个指向该对象的指针
new简单类型直接调用operatornew分配内存;
而对于复杂结构,先调用operatornew分配内存,然后在分配的内存上调用构造函数;
对于简单类型,new[]计算好大小后调用operatornew;
对于复杂数据结构,new[]先调用operatornew[]分配内存,然后在p的前四个字节写入数组大小n,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小
delete简单数据类型默认只是调用free函数;
复杂数据类型先调用析构函数再调用operatordelete;
针对简单类型,delete和delete[]等同。
假设指针p指向new[]分配的内存。
因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。
delete[]实际释放的就是p-4指向的内存。
而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃
需要在new[]一个对象数组时,需要保存数组的维度,C++的做法是在分配数组空间时多分配了4个字节的大小,专门保存数组的大小,在delete[]时就可以取出这个保存的数,就知道了需要调用析构函数多少次了
1.使用智能指针管理内存资源,RAII
RAII全称是“ResourceAcquisitionisInitialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源。
因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。
所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定
智能指针(std:
shared_ptr和std:
unique_ptr)即RAII最具代表的实现,使用智能指针,可以实现自动的内存管理,再也不需要担心忘记delete造成的内存泄漏。
毫不夸张的来讲,有了智能指针,代码中几乎不需要再出现delete了
1.内存对齐
1.分配内存的顺序是按照声明的顺序。
2.每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止
3.最后整个结构体的大小必须是里面变量类型最大值的整数倍
classA{
inta;
doubleb;
};
classB{
inta,b;
doublec;
classC{
intc;
classD{
intc,d;
sizeof(int)<
"
<
sizeof(double)<
sizeof(A)<
sizeof(B)<
sizeof(C)<
sizeof(D)<
}//out/*
48
16162424
*/
1.为什么内存对齐
平台原因(移植原因)
∙不是所有的硬件平台都能访问任意地址上的任意数据的;
∙某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
性能原因:
∙数据结构(尤其是栈)应该尽可能地在自然边界上对齐
∙原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;
而对齐的内存访问仅需要一次访问
1.宏定义一个取两个数中较大值的功能
#defineMAX(x,y)(x>
y?
x:
y)
1.define与inline的区别
define是关键字,inline是函数
宏定义在预处理阶段进行文本替换,inline函数在编译阶段进行替换
inline函数有类型检查,相比宏定义比较安全
1.printf实现原理
在C/C++中,对函数参数的扫描是从后向前的。
C/C++的函数参数是通过压入堆栈的方式来给函数传参数的,所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。
printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了。
1.helloworld程序开始到打印到屏幕上的全过程
∙用户告诉操作系统执行HelloWorld程序(通过键盘输入等)
∙操作系统:
找到helloworld程序的相关信息,检查其类型是否是可执行文件;
并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址。
创建一个新进程,将HelloWorld可执行文件映射到该进程结构,表示由该进程执行helloworld程序。
为helloworld程序设置cpu上下文环境,并跳到程序开始处。
∙执行helloworld程序的第一条指令,发生缺页异常
分配一页物理内存,并将代码从磁盘读入内存,然后继续执行helloworld程序
∙helloword程序执行puts函数(系统调用),在显示器上写一字符串
找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程
控制设备的进程告诉设备的窗口系统,它要显示该字符串,窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储映像区
∙视频硬件将像素转换成显示器可接收和一组控制数据信号
∙显示器解释信号,激发液晶屏
∙OK,我们在屏幕上看到了HelloWorld
1.模板类和模板函数的区别是什么
函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。
即函数模板允许隐式调用和显式调用而类模板只能显示调用。
在使用时类模板必须加<
T>
,而函数模板不必
1.C++四种类型转换
∙static_cast能进行基础类型之间的转换,也是最常看到的类型转换。
它主要有如下几种用法:
1.用于类层次结构中父类和子类之间指针或引用的转换,2.进行下行转换(把父类指针或引用转换成子类指针或引用)时,由于没有动态类型检查,所以是不安全的,3.用于基本数据类型之间的转换,如把int转换成char,把int转换成enum,4.把void指针转换成目标类型的指针