大港网站开发,wordpress 3.4.2,中介做哪些网站,会员管理系统单机免费我发现呀#xff0c;这大家对面试题的需求还是很大的#xff0c;这里总结了上千道知识点#xff0c;能换您一个收藏吗 C 引用和指针的区别#xff1f;
指针是一个实体#xff0c;需要分配内存空间。引用只是变量的别名#xff0c;不需要分配内存空间。
引用在定义的时候… 我发现呀这大家对面试题的需求还是很大的这里总结了上千道知识点能换您一个收藏吗 C 引用和指针的区别
指针是一个实体需要分配内存空间。引用只是变量的别名不需要分配内存空间。
引用在定义的时候必须进行初始化并且不能够改变。指针在定义的时候不一定要初始化并且指向的空间可变。注不能有引用的值不能为NULL
有多级指针但是没有多级引用只能有一级引用。
指针和引用的自增运算结果不一样。指针是指向下一个空间引用时引用的变量值加1
sizeof 引用得到的是所指向的变量对象的大小而sizeof 指针得到的是指针本身的大小。
引用访问一个变量是直接访问而指针访问一个变量是间接访问。
使用指针前最好做类型检查防止野指针的出现
引用底层是通过指针实现的
作为参数时也不同传指针的实质是传值传递的值是指针的地址传引用的实质是传地址传递的是变量的地址。
从汇编层去解释一下引用 x的地址为ebp-4b的地址为ebp-8因为栈内的变量内存是从高往低进行分配的。所以b的地址比x的低。lea eax,[ebp-4] 这条语句将x的地址ebp-4放入eax寄存器mov dword ptr [ebp-8],eax 这条语句将eax的值放入b的地址ebp-8中上面两条汇编的作用即将x的地址存入变量b中这不和将某个变量的地址存入指针变量是一样的吗所以从汇编层次来看的确引用是通过指针来实现的。 C中的指针参数传递和引用参数传递
指针参数传递本质上是值传递它所传递的是一个地址值。值传递过程中被调函数的形式参数作为被调函数的局部变量处理会在栈中开辟内存空间以存放由主调函数传递进来的实参值从而形成了实参的一个副本替身。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行的不会影响主调函数的实参变量的值形参指针变了实参指针不会变。
引用参数传递过程中被调函数的形式参数也作为局部变量在栈中开辟了内存空间但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参本体的任何操作都被处理成间接寻址即通过栈中存放的地址访问主调函数中的实参变量根据别名找到主调函数中的本体。因此被调函数对形参的任何操作都会影响主调函数中的实参变量。
引用传递和指针传递是不同的虽然他们都是在被调函数栈空间上的一个局部变量但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数如果改变被调函数中的指针地址它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量地址那就得使用指向指针的指针或者指针引用。
从编译的角度来讲程序在编译时分别将指针和引用添加到符号表上符号表中记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值而引用在符号表上对应的地址值为引用对象的地址值与实参名字不同地址相同。符号表生成之后就不会再改因此指针可以改变其指向的对象指针变量中的值可以改而引用对象则不能修改。
形参与实参的区别
形参变量只有在被调用时才分配内存单元在调用结束时 即刻释放所分配的内存单元。因此形参只有在函数内部有效。 函数调用结束返回主调函数后则不能再使用该形参变量。
实参可以是常量、变量、表达式、函数等 无论实参是何种类型的量在进行函数调用时它们都必须具有确定的值 以便把这些值传送给形参。 因此应预先用赋值输入等办法使实参获得确定值会产生一个临时变量。
实参和形参在数量上类型上顺序上应严格一致 否则会发生“类型不匹配”的错误。
函数调用中发生的数据传送是单向的。 即只能把实参的值传送给形参而不能把形参的值反向地传送给实参。 因此在函数调用过程中形参的值发生改变而实参中的值不会变化。
当形参和实参不是指针类型时在该函数运行时形参和实参是不同的变量他们在内存中位于不同的位置形参将实参的内容复制一份在该函数运行结束的时候形参被释放而实参内容不会改变。
值传递有一个形参向函数所属的栈拷贝数据的过程如果值传递的对象是类对象 或是大的结构体对象将耗费一定的时间和空间。传值
指针传递同样有一个形参向函数所属的栈拷贝数据的过程但拷贝的数据是一个固定为4字节的地址。传值传递的是地址值
引用传递同样有上述的数据拷贝过程但其是针对地址的相当于为该数据所在的地址起了一个别名。传地址
效率上讲指针传递和引用传递比值传递效率高。一般主张使用引用传递代码逻辑上更加紧凑、清晰。
static的用法和作用
1.先来介绍它的第一条也是最重要的一条隐藏。static函数static变量均可
当同时编译多个文件时所有未加static前缀的全局变量和函数都具有全局可见性。
2.static的第二个作用是保持变量内容的持久。static变量中的记忆功能和全局生存期存储在静态数据区的变量会在程序刚开始运行时就完成初始化也是唯一的一次初始化。共有两种变量存储在静态存储区全局变量和static变量只不过和全局变量比起来static可以控制变量的可见范围说到底static还是用来隐藏的。
3.static的第三个作用是默认初始化为0static变量
其实全局变量也具备这一属性因为全局变量也存储在静态数据区。在静态数据区内存中所有的字节默认值都是0x00某些时候这一特点可以减少程序员的工作量。
4.static的第四个作用C中的类成员声明static
函数体内static变量的作用范围为该函数体不同于auto变量该变量的内存只被分配一次因此其值在下次调用时仍维持上次的值
在模块内的static全局变量可以被模块内所用函数访问但不能被模块外其它函数访问
在模块内的static函数只可被这一模块内的其它函数调用这个函数的使用范围被限制在声明它的模块内
在类中的static成员变量属于整个类所拥有对类的所有对象只有一份拷贝
在类中的static成员函数属于整个类所拥有这个函数不接收this指针因而只能访问类的static成员变量。
类内
static类对象必须要在类外进行初始化static修饰的变量先于对象存在所以static修饰的变量要在类外初始化
由于static修饰的类成员属于类不属于对象因此static类成员函数是没有this指针的this指针是指向本对象的指针。正因为没有this指针所以static类成员函数不能访问非static的类成员只能访问 static修饰的类成员
static成员函数不能被virtual修饰static成员不属于任何对象或实例所以加上virtual没有任何实际意义静态成员函数没有this指针虚函数的实现是为每一个对象分配一个vptr指针而vptr是通过this指针调用的所以不能为virtual虚函数的调用关系this-vptr-ctable-virtual function
静态变量什么时候初始化
初始化只有一次但是可以多次赋值在主程序之前编译器已经为其分配好了内存。
静态局部变量和全局变量一样数据都存放在全局区域所以在主程序之前编译器已经为其分配好了内存但在C和C中静态局部变量的初始化节点又有点不太一样。在C中初始化发生在代码执行之前编译阶段分配好内存之后就会进行初始化所以我们看到在C语言中无法使用变量对静态局部变量进行初始化在程序运行结束变量所处的全局内存会被全部回收。
而在C中初始化时在执行相关代码时才会进行初始化主要是由于C引入对象后要进行初始化必须执行相应构造函数和析构函数在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作并非简单地分配内存。所以C标准定为全局或静态对象是有首次用到时才会进行构造并通过atexit()来管理。在程序结束按照构造顺序反方向进行逐个析构。所以在C中是可以使用变量对静态局部变量进行初始化的。 const?
阻止一个变量被改变可以使用const关键字。在定义该const变量时通常需要对它进行初始化因为以后就没有机会再去改变它了
对指针来说可以指定指针本身为const也可以指定指针所指的数据为const或二者同时指定为const
在一个函数声明中const可以修饰形参表明它是一个输入参数在函数内部不能改变其值
对于类的成员函数若指定其为const类型则表明其是一个常函数不能修改类的成员变量类的常对象只能访问类的常成员函数
对于类的成员函数有时候必须指定其返回值为const类型以使得其返回值不为“左值”。
const成员函数可以访问非const对象的非const数据成员、const数据成员也可以访问const对象内的所有数据成员
非const成员函数可以访问非const对象的非const数据成员、const数据成员但不可以访问const对象的任意数据成员
一个没有明确声明为const的成员函数被看作是将要修改对象中数据成员的函数而且编译器不允许它为一个const对象所调用。因此const对象只能调用const成员函数。
const类型变量可以通过类型转换符const_cast将const类型转换为非const类型
const类型变量必须定义的时候进行初始化因此也导致如果类的成员变量有const类型的变量那么该变量必须在类的初始化列表中进行初始化
对于函数值传递的情况因为参数传递是通过复制实参创建一个临时变量传递进函数的函数内只能改变临时变量但无法改变实参。则这个时候无论加不加const对实参不会产生任何影响。但是在引用或指针传递函数调用中因为传进去的是一个引用或指针这样函数内部可以改变引用或指针所指向的变量这时const 才是实实在在地保护了实参所指向的变量。因为在编译阶段编译器对调用函数的选择是根据实参进行的所以只有引用传递和指针传递可以用是否加const来重载。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
const成员函数的理解和应用
const Stock Stock::topval (②const Stock s) ③const
①处const确保返回的Stock对象在以后的使用中不能被修改
②处const确保此方法不修改传递的参数 S
③处const保证此方法不修改调用它的对象const对象只能调用const成员函数,不能调用非const函数
指针和const的用法
当const修饰指针时由于const的位置不同它的修饰对象会有所不同。
int *const p2中const修饰p2的值,所以理解为p2的值不可以改变即p2只能指向固定的一个变量地址但可以通过*p2读写这个变量的值。顶层指针表示指针本身是一个常量
int const *p1或者const int *p1两种情况中const修饰*p1所以理解为*p1的值不可以改变即不可以给*p1赋值改变p1指向变量的值但可以通过给p赋值不同的地址改变这个指针指向。底层指针表示指针所指向的变量是一个常量。
int const *const p;
mutable
如果需要在const成员方法中修改一个成员变量的值那么需要将这个成员变量修饰为mutable。即用mutable修饰的成员变量不受const成员方法的限制;
可以认为mutable的变量是类的辅助状态但是只是起到类的一些方面表述的功能修改他的内容我们可以认为对象的状态本身并没有改变的。实际上由于const_cast的存在这个概念很多时候用处不是很到了。
extern用法
extern修饰变量的声明
如果文件a.c需要引用b.c中变量int v就可以在a.c中声明extern int v然后就可以引用变量v。
extern修饰函数的声明
如果文件a.c需要引用b.c中的函数比如在b.c中原型是int fun(int mu)那么就可以在a.c中声明extern int funint mu然后就能使用fun来做任何事情。就像变量的声明一样extern int funint mu可以放在a.c中任何地方而不一定非要放在a.c的文件作用域的范围中。
extern修饰符可用于指示C或者C函数的调用规范。
比如在C中调用C库函数就需要在C程序中用extern “C”声明要引用的函数。这是给链接器用的告诉链接器在链接的时候用C函数规范来链接。主要原因是C和C程序编译完成后在目标代码中命名规则不同。
int转字符串字符串转int?strcat,strcpy,strncpy,memset,memcpy的内部实现
c11标准增加了全局函数std::to_string
可以使用std::stoi/stol/stoll等等函数
strcpy拥有返回值有时候函数原本不需要返回值但为了增加灵活性如支持链式表达
深拷贝与浅拷贝
浅复制 —-只是拷贝了基本类型的数据而引用类型数据复制后也是会发生引用我们把这种拷贝叫做“浅复制浅拷贝”换句话说浅复制仅仅是指向被复制的内存地址如果原地址中对象被改变了那么浅复制出来的对象也会相应改变。
深复制 —-在计算机中开辟了一块新的内存地址用于存放复制的对象。 在某些状况下类内成员变量需要动态开辟堆内存如果实行位拷贝也就是把对象里的值完全复制给另一个对象如AB。这时如果B中有一个成员变量指针已经申请了内存那A中的那个成员变量也指向同一块内存。这就出现了问题当B把内存释放了如析构这时A内的指针就是野指针了出现运行错误。
C模板是什么底层怎么实现的
编译器并不是把函数模板处理成能够处理任意类的函数编译器从函数模板通过具体类型产生不同的函数编译器会对函数模板进行两次编译在声明的地方对模板代码本身进行编译在调用的地方对参数替换后的代码进行编译。
这是因为函数模板要被实例化后才能成为真正的函数在使用函数模板的源文件中包含函数模板的头文件如果该头文件中只有声明没有定义那编译器无法实例化该模板最终导致链接错误。
C语言struct和Cstruct区别
C语言中struct是用户自定义数据类型UDTC中struct是抽象数据类型ADT支持成员函数的定义C中的struct能继承能实现多态。
C中struct是没有权限的设置的且struct中只能是一些变量的集合体可以封装数据却不可以隐藏数据而且成员不可以是函数。
C中struct的成员默认访问说明符为public为了与C兼容class中的默认访问限定符为privatestruct增加了访问权限且可以和类一样有成员函数。
struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后在C中必须在结构标记前加上struct才能做结构类型名
虚函数可以声明为inline吗?
虚函数用于实现运行时的多态或者称为晚绑定或动态绑定。而内联函数用于提高效率。内联函数的原理是在编译期间对调用内联函数的地方的代码替换成函数代码。内联函数对于程序中需要频繁使用和调用的小函数非常有用。
虚函数要求在运行时进行类型确定而内敛函数要求在编译期完成相关的函数替换
类成员初始化方式构造函数的执行顺序 为什么用成员初始化列表会快一些
赋值初始化通过在函数体内进行赋值初始化列表初始化在冒号后使用初始化列表进行初始化。
这两种方式的主要区别在于
对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。
列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值就是说初始化这个数据成员此时函数体还未执行。
一个派生类构造函数的执行顺序如下
虚拟基类的构造函数多个虚拟基类则按照继承的顺序执行构造函数。
基类的构造函数多个普通基类也按照继承的顺序执行构造函数。
类类型的成员对象的构造函数按照初始化顺序
派生类自己的构造函数。
方法一是在构造函数当中做赋值的操作而方法二是做纯粹的初始化操作。我们都知道C的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率。
成员列表初始化
必须使用成员初始化的四种情况
当初始化一个引用成员时
当初始化一个常量成员时
当调用一个基类的构造函数而它拥有一组参数时
当调用一个成员类的构造函数而它拥有一组参数时
成员初始化列表做了什么
编译器会一一操作初始化列表以适当的顺序在构造函数之内安插初始化操作并且在任何显示用户代码之前
list中的项目顺序是由类中的成员声明顺序决定的不是由初始化列表的顺序决定的
构造函数为什么不能为虚函数析构函数为什么要虚函数
1. 从存储空间角度虚函数相应一个指向vtable虚函数表的指针这大家都知道但是这个指向vtable的指针事实上是存储在对象的内存空间的。问题出来了假设构造函数是虚的就须要通过 vtable来调用但是对象还没有实例化也就是内存空间还没有怎么找vtable呢所以构造函数不能是虚函数。
2. 从使用角度虚函数主要用于在信息不全的情况下能使重载的函数得到相应的调用。构造函数本身就是要初始化实例那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的不可能通过父类的指针或者引用去调用因此也就规定构造函数不能是虚函数。
3. 构造函数不须要是虚函数也不同意是虚函数由于创建一个对象时我们总是要明白指定对象的类型虽然我们可能通过实验室的基类的指针或引用去訪问它但析构却不一定我们往往通过基类的指针来销毁对象。这时候假设析构函数不是虚函数就不能正确识别对象类型从而不能正确调用析构函数。
4. 从实现上看vbtl在构造函数调用后才建立因而构造函数不可能成为虚函数从实际含义上看在调用构造函数时还不能确定对象的真实类型由于子类会调父类的构造函数并且构造函数的作用是提供初始化在对象生命期仅仅运行一次不是对象的动态行为也没有必要成为虚函数。
5. 当一个构造函数被调用时它做的首要的事情之中的一个是初始化它的VPTR。因此它仅仅能知道它是“当前”类的而全然忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时它是为这个类的构造函数产生代码——既不是为基类也不是为它的派生类由于类不知道谁继承它。所以它使用的VPTR必须是对于这个类的VTABLE。并且仅仅要它是最后的构造函数调用那么在这个对象的生命期内VPTR将保持被初始化为指向这个VTABLE, 但假设接着另一个更晚派生的构造函数被调用这个构造函数又将设置VPTR指向它的 VTABLE等.直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的还有一个理由。可是当这一系列构造函数调用正发生时每一个构造函数都已经设置VPTR指向它自己的VTABLE。假设函数调用使用虚机制它将仅仅产生通过它自己的VTABLE的调用而不是最后的VTABLE全部构造函数被调用后才会有最后的VTABLE。
因为构造函数本来就是为了明确初始化对象成员才产生的然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外virtual函数是在不同类型的对象产生不同的动作现在对象还没有产生如何使用virtual函数来完成你想完成的动作。
直接的讲C中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说如果派生类中申请了内存空间并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数当删除基类指针指向的派生类对象时就不会触发动态绑定因而只会调用基类的析构函数而不会调用派生类的析构函数。那么在这种情况下派生类中申请的空间就得不到释放从而产生内存泄漏。所以为了防止这种情况的发生C中基类的析构函数应采用virtual虚析构函数。
析构函数的作用如何起作用
构造函数只是起初始化值的作用但实例化一个对象的时候可以通过实例去传递参数从主函数传递到其他的函数里面这样就使其他的函数里面有值了。规则只要你一实例化对象系统自动回调用一个构造函数就是你不写编译器也自动调用一次。
析构函数与构造函数的作用相反用于撤销对象的一些特殊任务处理可以是释放对象分配的内存空间特点析构函数与构造函数同名但该函数前面加~。 析构函数没有参数也没有返回值而且不能重载在一个类中只能有一个析构函数。 当撤销对象时编译器也会自动调用析构函数。 每一个类必须有一个析构函数用户可以自定义析构函数也可以是编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员。
构造函数和析构函数可以调用虚函数吗为什么
在C中提倡不在构造函数和析构函数中调用虚函数
构造函数和析构函数调用虚函数时都不使用动态联编如果在构造函数或析构函数中调用虚函数则运行的是为构造函数或析构函数自身类型定义的版本
因为父类对象会在子类之前进行构造此时子类部分的数据成员还未初始化因此调用子类的虚函数时不安全的故而C不会进行动态联编
析构函数是用来销毁一个对象的在销毁一个对象时先调用子类的析构函数然后再调用基类的析构函数。所以在调用基类的析构函数时派生类对象的数据成员已经销毁这个时候再调用子类的虚函数没有任何意义。
构造函数的执行顺序析构函数的执行顺序构造函数内部干了啥拷贝构造干了啥
构造函数顺序
基类构造函数。如果有多个基类则构造函数的调用顺序是某类在类派生表中出现的顺序而不是它们在成员初始化表中的顺序。
成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序而不是它们出现在成员初始化表中的顺序。
派生类构造函数。
析构函数顺序
调用派生类的析构函数
调用成员类对象的析构函数
调用基类的析构函数。
虚析构函数的作用父类的析构函数是否要设置为虚函数
C中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说如果派生类中申请了内存空间并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数当删除基类指针指向的派生类对象时就不会触发动态绑定因而只会调用基类的析构函数而不会调用派生类的析构函数。那么在这种情况下派生类中申请的空间就得不到释放从而产生内存泄漏。所以为了防止这种情况的发生C中基类的析构函数应采用virtual虚析构函数。
纯虚析构函数一定得定义因为每一个派生类析构函数会被编译器加以扩张以静态调用的方式调用其每一个虚基类以及上一层基类的析构函数。因此缺乏任何一个基类析构函数的定义就会导致链接失败。因此最好不要把虚析构函数定义为纯虚析构函数。
构造函数析构函数可以调用虚函数吗
在构造函数和析构函数中最好不要调用虚函数
构造函数或者析构函数调用虚函数并不会发挥虚函数动态绑定的特性跟普通函数没区别
即使构造函数或者析构函数如果能成功调用虚函数 程序的运行结果也是不可控的。
构造函数析构函数可否抛出异常 C只会析构已经完成的对象对象只有在其构造函数执行完毕才算是完全构造妥当。在构造函数中发生异常控制权转出构造函数之外。因此在对象b的构造函数中发生异常对象b的析构函数不会被调用。因此会造成内存泄漏。
用auto_ptr对象来取代指针类成员便对构造函数做了强化免除了抛出异常时发生资源泄漏的危机不再需要在析构函数中手动释放资源
如果控制权基于异常的因素离开析构函数而此时正有另一个异常处于作用状态C会调用terminate函数让程序结束
如果异常从析构函数抛出而且没有在当地进行捕捉那个析构函数便是执行不全的。如果析构函数执行不全就是没有完成他应该执行的每一件事情。
类如何实现只能静态分配和只能动态分配
前者是把new、delete运算符重载为private属性。后者是把构造、析构函数设为protected属性再用子类来动态创建
建立类的对象有两种方式
静态建立静态建立一个类对象就是由编译器为对象在栈空间中分配内存
动态建立A *p new A();动态建立一个类对象就是使用new运算符为对象在堆空间中分配内存。这个过程分为两步第一步执行operator new()函数在堆中搜索一块内存并进行分配第二步调用类构造函数构造对象
只有使用new运算符对象才会被建立在堆上因此只要限制new运算符就可以实现类对象只能建立在栈上。可以将new运算符设为私有。
如果想将某个类用作基类为什么该类必须定义而非声明
派生类中包含并且可以使用它从基类继承而来的成员为了使用这些成员派生类必须知道他们是什么。
什么情况会自动生成默认构造函数
带有默认构造函数的类成员对象如果一个类没有任何构造函数但它含有一个成员对象而后者有默认构造函数那么编译器就为该类合成出一个默认构造函数。不过这个合成操作只有在构造函数真正被需要的时候才会发生如果一个类A含有多个成员类对象的话那么类A的每一个构造函数必须调用每一个成员对象的默认构造函数而且必须按照类对象在类A中的声明顺序进行
带有默认构造函数的基类如果一个没有任务构造函数的派生类派生自一个带有默认构造函数基类那么该派生类会合成一个构造函数调用上一层基类的默认构造函数
带有一个虚函数的类
带有一个虚基类的类
合成的默认构造函数中只有基类子对象和成员类对象会被初始化。所有其他的非静态数据成员都不会被初始化。
什么是类的继承
类与类之间的关系
has-A包含关系用以描述一个类由多个部件类构成实现has-A关系用类的成员属性表示即一个类的成员属性是另一个已经定义好的类
use-A一个类使用另一个类通过类之间的成员函数相互联系定义友元或者通过传递参数的方式来实现
is-A继承关系关系具有传递性
继承的相关概念
所谓的继承就是一个类继承了另一个类的属性和方法这个新的类包含了上一个类的属性和方法被称为子类或者派生类被继承的类称为父类或者基类
继承的特点
子类拥有父类的所有属性和方法子类可以拥有父类没有的属性和方法子类对象可以当做父类对象使用
继承中的访问控制
public、protected、private
继承中的构造和析构函数
继承中的兼容性原则
什么是组合
一个类里面的数据成员是另一个类的对象即内嵌其他类的对象作为自己的成员创建组合类的对象首先创建各个内嵌对象难点在于构造函数的设计。创建对象时既要对基本类型的成员进行初始化又要对内嵌对象进行初始化。
创建组合类对象构造函数的执行顺序先调用内嵌对象的构造函数然后按照内嵌对象成员在组合类中的定义顺序与组合类构造函数的初始化列表顺序无关。然后执行组合类构造函数的函数体析构函数调用顺序相反。 抽象基类为什么不能创建对象
抽象类是一种特殊的类它是为了抽象和设计的目的为建立的它处于继承层次结构的较上层。
1抽象类的定义 称带有纯虚函数的类为抽象类。
2抽象类的作用 抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中由它来为派生类提供一个公共的根派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义这些语义也传给子类子类可以具体实现这些语义也可以再将这些语义传给自己的子类。
3使用抽象类时注意 抽象类只能作为基类来使用其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数而只是继承基类的纯虚函数则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现则该派生类就不再是抽象类了它是一个可以建立对象的具体的类。
抽象类是不能定义对象的。一个纯虚函数不需要但是可以被定义。
纯虚函数定义 纯虚函数是一种特殊的虚函数它的一般格式如下 class 类名 { virtual 类型函数名(参数表)0; … }; 在许多情况下在基类中不能对虚函数给出有意义的实现而把它声明为纯虚函数它的实现留给该基类的派生类去做。这就是纯虚函数的作用。 纯虚函数可以让类先具有一个操作名称而没有操作内容让派生类在继承时再去具体地给出定义。凡是含有纯虚函数的类叫做抽象类。这种类不能声明对象只是作为基类为派生类服务。除非在派生类中完全实现基类中所有的的纯虚函数否则派生类也变成了抽象类不能实例化对象。
纯虚函数引入原因 1、为了方便使用多态特性我们常常需要在基类中定义虚拟函数。 2、在很多情况下基类本身生成对象是不合情理的。例如动物作为一个基类可以派生出老虎、孔 雀等子类但动物本身生成对象明显不合常理。 为了解决上述问题引入了纯虚函数的概念将函数定义为纯虚函数方法virtual ReturnType Function() 0;。若要使派生类为非抽象类则编译器要求在派生类中必须对纯虚函数予以重载以实现多态性。同时含有纯虚函数的类称为抽象类它不能生成对象。这样就很好地解决了上述两个问题。 例如绘画程序中shape作为一个基类可以派生出圆形、矩形、正方形、梯形等 如果我要求面积总和的话那么会可以使用一个 shape * 的数组只要依次调用派生类的area()函数了。如果不用接口就没法定义成数组因为既可以是circle ,也可以是square ,而且以后还可能加上rectangle等等.
相似概念 1、多态性
指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C支持两种多态性编译时多态性运行时多态性。 a.编译时多态性通过重载函数实现 b.运行时多态性通过虚函数实现。 2、虚函数 虚函数是在基类中被声明为virtual并在派生类中重新定义的成员函数可实现成员函数的动态重载。 3、抽象类 包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数所以不能定义抽象类的对象。 类什么时候会析构
对象生命周期结束被销毁时delete指向对象的指针时或delete指向对象的基类类型指针而其基类虚构函数是虚函数时对象i是对象o的成员o的析构函数被调用时对象i的析构函数也被调用。为什么友元函数必须在类内部声明
因为编译器必须能够读取这个结构的声明以理解这个数据类型的大、行为等方面的所有规则。有一条规则在任何关系中都很重要那就是谁可以访问我的私有部分。
介绍一下C里面的多态
1静态多态重载模板
是在编译的时候就确定调用函数的类型。
2动态多态覆盖虚函数实现
在运行的时候才确定调用的是哪个函数动态绑定。运行基类指针指向派生类的对象并调用派生类的函数。
虚函数实现原理虚函数表和虚函数指针。
纯虚函数 virtual int fun() 0;
函数的运行版本由实参决定在运行时选择函数的版本所以动态绑定又称为运行时绑定。
当编译器遇到一个模板定义时它并不生成代码。只有当实例化出模板的一个特定版本时编译器才会生成代码。
用C语言实现C的继承
#include iostream
using namespace std;//C中的继承与多态
struct A
{virtual void fun() //C中的多态:通过虚函数实现{coutA:fun()endl;}int a;
};
struct B:public A //C中的继承:B类公有继承A类
{virtual void fun() //C中的多态:通过虚函数实现子类的关键字virtual可加可不加{coutB:fun()endl;}int b;
};//C语言模拟C的继承与多态typedef void (*FUN)(); //定义一个函数指针来实现对成员函数的继承struct _A //父类
{FUN _fun; //由于C语言中结构体不能包含函数故只能用函数指针在外面实现int _a;
};struct _B //子类
{_A _a_; //在子类中定义一个基类的对象即可实现对父类的继承int _b;
};void _fA() //父类的同名函数
{printf(_A:_fun()\n);
}
void _fB() //子类的同名函数
{printf(_B:_fun()\n);
}void Test()
{//测试C中的继承与多态A a; //定义一个父类对象aB b; //定义一个子类对象bA* p1 a; //定义一个父类指针指向父类的对象p1-fun(); //调用父类的同名函数p1 b; //让父类指针指向子类的对象p1-fun(); //调用子类的同名函数//C语言模拟继承与多态的测试_A _a; //定义一个父类对象_a_B _b; //定义一个子类对象_b_a._fun _fA; //父类的对象调用父类的同名函数_b._a_._fun _fB; //子类的对象调用子类的同名函数_A* p2 _a; //定义一个父类指针指向父类的对象p2-_fun(); //调用父类的同名函数p2 (_A*)_b; //让父类指针指向子类的对象,由于类型不匹配所以要进行强转p2-_fun(); //调用子类的同名函数
}继承机制中对象之间如何转换指针和引用之间如何转换
向上类型转换
将派生类指针或引用转换为基类的指针或引用被称为向上类型转换向上类型转换会自动进行而且向上类型转换是安全的。
向下类型转换
将基类指针或引用转换为派生类指针或引用被称为向下类型转换向下类型转换不会自动进行因为一个基类对应几个派生类所以向下类型转换时不知道对应哪个派生类所以在向下类型转换时必须加动态类型识别技术。RTTI技术用dynamic_cast进行向下类型转换。
组合与继承优缺点
一继承
继承是Is a 的关系比如说Student继承Person,则说明Student is a Person。继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。
继承的缺点有以下几点
①父类的内部细节对子类是可见的。
②子类从父类继承的方法在编译时就确定下来了所以无法在运行期间改变从父类继承的方法的行为。
③如果对父类的方法做了修改的话比如增加了一个参数则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合违背了面向对象思想。
二组合
组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。
组合的优点
①当前对象只能通过所包含的那个对象去调用其方法所以所包含的对象的内部细节对当前对象时不可见的。
②当前对象与包含的对象是一个低耦合关系如果修改包含对象的类中代码不需要修改当前对象类的代码。
③当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。
组合的缺点①容易产生过多的对象。②为了能组合多个对象必须仔细对接口进行定义。
左值右值
在C11中所有的值必属于左值、右值两者之一右值又可以细分为纯右值、将亡值。在C11中可以取地址的、有名字的就是左值反之不能取地址的、没有名字的就是右值将亡值或纯右值。举个例子int a bc, a 就是左值其有变量名为a通过a可以获取该变量的地址表达式bc、函数int func()的返回值是右值在其被赋值给某一变量前我们不能通过变量名找到它(bc)这样的操作则不会通过编译。C11对C98中的右值进行了扩充。在C11中右值又分为纯右值prvaluePure Rvalue和将亡值xvalueeXpiring Value。其中纯右值的概念等同于我们在C98标准中右值的概念指的是临时变量和不跟对象关联的字面量值将亡值则是C11新增的跟右值引用相关的表达式这样表达式通常是将要被移动的对象移为他用比如返回右值引用T的函数返回值、std::move的返回值或者转换为T的类型转换函数的返回值。将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时通过“盗取”的方式可以避免内存空间的释放和分配能够延长变量值的生命期。左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型事实上由于右值通常不具有名字我们也只能通过引用的方式找到它的存在。右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存只是该对象的一个别名。左值引用是具名变量值的别名而右值引用则是不具名匿名变量的别名。左值引用通常也不能绑定到右值但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地非常量左值只能接受非常量左值对其进行初始化。右值值引用通常不能绑定到任何的左值要想绑定一个左值到右值引用通常需要std::move()将左值强制转换为右值。移动构造函数
我们用对象a初始化对象b后对象a我们就不在使用了但是对象a的空间还在呀在析构之前既然拷贝构造函数实际上就是把a对象的内容复制一份到b中那么为什么我们不能直接使用a的空间呢这样就避免了新的空间的分配大大降低了构造的成本。这就是移动构造函数设计的初衷拷贝构造函数中对于指针我们一定要采用深层复制而移动构造函数中对于指针我们采用浅层复制。浅层复制之所以危险是因为两个指针共同指向一片内存空间若第一个指针将其释放另一个指针的指向就不合法了。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针比如a-value置为NULL这样在调用析构函数的时候由于有判断是否为NULL的语句所以析构a的时候并不会回收a-value指向的空间移动构造函数的参数和拷贝构造函数不同拷贝构造函数的参数是一个左值引用但是移动构造函数的初值是一个右值引用。意味着移动构造函数的参数是一个右值或者将亡值的引用。也就是说只用用一个右值或者将亡值初始化另一个对象的时候才会调用移动构造函数。而那个move语句就是将一个左值变成一个将亡值。C语言的编译链接过程
源代码预处理编译优化汇编链接--可执行文件
预处理
读取c源程序对其中的伪指令以#开头的指令和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。.i预处理后的c文件.ii预处理后的C文件。
编译阶段
编译程序所要作得工作就是通过词法分析和语法分析在确认所有的指令都符合语法规则之后将其翻译成等价的中间代码表示或汇编代码。.s文件
汇编过程
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。.o目标文件
链接阶段
链接程序的主要工作就是将有关的目标文件彼此相连接也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。
vector与list的区别与应用怎么找某vector或者list的倒数第二个元素
vector数据结构 vector和数组类似拥有一段连续的内存空间并且起始地址不变。因此能高效的进行随机存取时间复杂度为o(1);但因为内存空间是连续的所以在进行插入和删除操作时会造成内存块的拷贝时间复杂度为o(n)。另外当数组中内存空间不够时会重新申请一块内存空间并进行内存拷贝。连续存储结构vector是可以实现动态增长的对象数组支持对数组高效率的访问和在数组尾端的删除和插入操作在中间和头部删除和插入相对不易需要挪动大量的数据。它与数组最大的区别就是vector不需程序员自己去考虑容量问题库里面本身已经实现了容量的动态增长而数组需要程序员手动写入扩容函数进形扩容。list数据结构 list是由双向链表实现的因此内存空间是不连续的。只能通过指针访问数据所以list的随机存取非常没有效率时间复杂度为o(n);但由于链表的特点能高效地进行插入和删除。非连续存储结构list是一个双链表结构支持对链表的双向遍历。每个节点包括三个信息元素本身指向前一个元素的节点prev和指向下一个元素的节点next。因此list可以高效率的对数据元素任意位置进行访问和插入删除等操作。由于涉及对额外指针的维护所以开销比较大。
区别
vector的随机访问效率高但在插入和删除时不包括尾部需要挪动数据不易操作。list的访问要遍历整个链表它的随机访问效率低。但对数据的插入和删除操作等都比较方便改变指针的指向即可。list是单向的vector是双向的。vector中的迭代器在使用后就失效了而list的迭代器在使用之后还可以继续使用。
int mySize vec.size();vec.at(mySize -2);
list不提供随机访问所以不能用下标直接访问到某个位置的元素要访问list里的元素只能遍历不过你要是只需要访问list的最后N个元素的话可以用反向迭代器来遍历
STL vector的实现删除其中的元素迭代器如何变化为什么是两倍扩容释放空间
size()函数返回的是已用空间大小capacity()返回的是总空间大小capacity()-size()则是剩余的可用空间大小。当size()和capacity()相等说明vector目前的空间已被用完如果再添加新元素则会引起vector空间的动态增长。
由于动态增长会引起重新分配内存空间、拷贝原空间、释放原空间这些过程会降低程序效率。因此可以使用reserve(n)预先分配一块较大的指定大小的内存空间这样当指定大小的内存空间未使用完时是不会重新分配内存空间的这样便提升了效率。只有当ncapacity()时调用reserve(n)才会改变vector容量。 resize()成员函数只改变元素的数目不改变vector的容量。
1. 空的vector对象size()和capacity()都为0
2. 当空间大小不足时新分配的空间大小为原空间大小的2倍。
3. 使用reserve()预先分配一块内存后在空间未满的情况下不会引起重新分配从而提升了效率。
4. 当reserve()分配的空间比原空间小时是不会引起重新分配的。
5. resize()函数只改变容器的元素数目未改变容器大小。
6. 用reserve(size_type)只是扩大capacity值这些内存空间可能还是“野”的如果此时使用“[ ]”来访问则可能会越界。而resize(size_type new_size)会真正使容器具有new_size个对象。 不同的编译器vector有不同的扩容大小。在vs下是1.5倍在GCC下是2倍空间和时间的权衡。简单来说 空间分配的多平摊时间复杂度低但浪费空间也多。使用k2增长因子的问题在于每次扩展的新尺寸必然刚好大于之前分配的总和也就是说之前分配的内存空间不可能被使用。这样对内存不友好。最好把增长因子设为(1,2)对比可以发现采用采用成倍方式扩容可以保证常数的时间复杂度而增加指定大小的容量只能达到O(n)的时间复杂度因此使用成倍的方式扩容。如何释放空间
由于vector的内存占用空间只增不减比如你首先分配了10,000个字节然后erase掉后面9,999个留下一个有效元素但是内存占用仍为10,000个。所有内存空间是在vector析构时候才能被系统回收。empty()用来检测容器是否为空的clear()可以清空所有元素。但是即使clear()vector所占用的内存空间依然如故无法保证内存的回收。
如果需要空间动态缩小可以考虑使用deque。如果vector可以用swap()来帮助你释放内存。
vector(Vec).swap(Vec); 将Vec的内存空洞清除 vector().swap(Vec); 清空Vec的内存 容器内部删除一个元素
顺序容器
erase迭代器不仅使所指向被删除的迭代器失效而且使被删元素之后的所有迭代器失效(list除外)所以不能使用erase(it)的方式但是erase的返回值是下一个有效迭代器
It c.erase(it);
关联容器
erase迭代器只是被删除元素的迭代器失效但是返回值是void所以要采用erase(it)的方式删除迭代器
c.erase(it)
STL迭代器如何实现
迭代器是一种抽象的设计理念通过迭代器可以在不了解容器内部原理的情况下遍历容器除此之外STL中迭代器一个最重要的作用就是作为容器与STL算法的粘合剂。迭代器的作用就是提供一个遍历容器内部所有元素的接口因此迭代器内部必须保存一个与容器相关联的指针然后重载各种运算操作来遍历其中最重要的是*运算符与-运算符以及、--等可能需要重载的运算符重载。这和C中的智能指针很像智能指针也是将一个指针封装然后通过引用计数或是其他方法完成自动释放内存的功能。最常用的迭代器的相应型别有五种value type、difference type、pointer、reference、iterator catagoly;
set与hash_set的区别
set底层是以RB-Tree实现hash_set底层是以hash_table实现的RB-Tree有自动排序功能而hash_table不具有自动排序功能set和hash_set元素的键值就是实值hash_table有一些无法处理的型别
hashmap与map的区别
底层实现不同map具有自动排序的功能hash_map不具有自动排序的功能hashtable有一些无法处理的型别
map、set是怎么实现的红黑树是怎么能够同时实现这两种容器 为什么使用红黑树
他们的底层都是以红黑树的结构实现因此插入删除等操作都在O(logn)时间内完成因此可以完成高效的插入删除在这里我们定义了一个模版参数如果它是key那么它就是set如果它是map那么它就是map底层是红黑树实现map的红黑树的节点数据类型是keyvalue而实现set的节点数据类型是value因为map和set要求是自动排序的红黑树能够实现这一功能而且时间复杂度比较低。
如何在共享内存上使用stl标准库
想像一下把STL容器例如map, vector, list等等放入共享内存中IPC一旦有了这些强大的通用数据结构做辅助无疑进程间通信的能力一下子强大了很多。我们没必要再为共享内存设计其他额外的数据结构另外STL的高度可扩展性将为IPC所驱使。STL容器被良好的封装默认情况下有它们自己的内存管理方案。当一个元素被插入到一个STL列表(list)中时列表容器自动为其分配内存保存数据。考虑到要将STL容器放到共享内存中而容器却自己在堆上分配内存。一个最笨拙的办法是在堆上构造STL容器然后把容器复制到共享内存并且确保所有容器的内部分配的内存指向共享内存中的相应区域这基本是个不可能完成的任务。假设进程A在共享内存中放入了数个容器进程B如何找到这些容器呢一个方法就是进程A把容器放在共享内存中的确定地址上fixed offsets则进程B可以从该已知地址上获取容器。另外一个改进点的办法是进程A先在共享内存某块确定地址上放置一个map容器然后进程A再创建其他容器然后给其取个名字和地址一并保存到这个map容器里。进程B知道如何获取该保存了地址映射的map容器然后同样再根据名字取得其他容器的地址。
map插入方式有几种
用insert函数插入pair数据
mapStudent.insert(pairint, string(1, student_one));
用insert函数插入value_type数据
mapStudent.insert(mapint, string::value_type (1, student_one));
在insert函数中使用make_pair()函数
mapStudent.insert(make_pair(1, student_one));
用数组方式插入数据
mapStudent[1] student_one;
STL中unordered_map(hash_map)和map的区别hash_map如何解决冲突以及扩容
unordered_map和map类似都是存储的key-value的值可以通过key快速索引到value。不同的是unordered_map不会根据key的大小进行排序存储时是根据key的hash值判断元素是否相同即unordered_map内部元素是无序的而map中的元素是按照二叉搜索树存储进行中序遍历会得到有序遍历。所以使用时map的key需要定义operator。而unordered_map需要定义hash_value函数并且重载operator。但是很多系统内置的数据类型都自带这些那么如果是自定义类型那么就需要自己重载operator或者hash_value()了。如果需要内部元素自动排序使用map不需要排序使用unordered_mapunordered_map的底层实现是hash_table;hash_map底层使用的是hash_table而hash_table使用的开链法进行冲突避免所有hash_map采用开链法进行冲突解决。什么时候扩容当向容器添加元素的时候会判断当前容器的元素个数如果大于等于阈值---即当前数组的长度乘以加载因子的值的时候就要自动扩容啦。扩容(resize)就是重新计算容量向HashMap对象里不停的添加元素而HashMap对象内部的数组无法装载更多的元素时对象就需要扩大数组的长度以便能装入更多的元素。
vector越界访问下标map越界访问下标vector删除元素时会不会释放空间
通过下标访问vector中的元素时不会做边界检查即便下标越界。也就是说下标与first迭代器相加的结果超过了finish迭代器的位置程序也不会报错而是返回这个地址中存储的值。如果想在访问vector中的元素时首先进行边界检查可以使用vector中的at函数。通过使用at函数不但可以通过下标访问vector中的元素而且在at函数内部会对下标进行边界检查。map的下标运算符[]的作用是将key作为下标去执行查找并返回相应的值如果不存在这个key就将一个具有该key和value的某人值插入这个map。erase()函数只能删除内容不能改变容量大小; erase成员函数它删除了itVect迭代器指向的元素并且返回要被删除的itVect之后的迭代器迭代器相当于一个智能指针;clear()函数只能清空内容不能改变容量大小;如果要想在删除内容的同时释放内存那么你可以选择deque容器。
map[]与find的区别
map的下标运算符[]的作用是将关键码作为下标去执行查找并返回对应的值如果不存在这个关键码就将一个具有该关键码和值类型的默认值的项插入这个map。map的find函数用关键码执行查找找到了返回该位置的迭代器如果不存在这个关键码就返回尾迭代器。
STL中list与queue之间的区别
list不再能够像vector一样以普通指针作为迭代器因为其节点不保证在存储空间中连续存在list插入操作和结合才做都不会造成原有的list迭代器失效;list不仅是一个双向链表而且还是一个环状双向链表所以它只需要一个指针list不像vector那样有可能在空间不足时做重新配置、数据移动的操作所以插入前的所有迭代器在插入操作之后都仍然有效deque是一种双向开口的连续线性空间所谓双向开口意思是可以在头尾两端分别做元素的插入和删除操作可以在头尾两端分别做元素的插入和删除操作deque和vector最大的差异一在于deque允许常数时间内对起头端进行元素的插入或移除操作二在于deque没有所谓容量概念因为它是动态地以分段连续空间组合而成随时可以增加一段新的空间并链接起来deque没有所谓的空间保留功能。
STL中的allocator,deallocator
第一级配置器直接使用malloc()、free()和relloc()第二级配置器视情况采用不同的策略当配置区块超过128bytes时视之为足够大便调用第一级配置器当配置器区块小于128bytes时为了降低额外负担使用复杂的内存池整理方式而不再用一级配置器第二级配置器主动将任何小额区块的内存需求量上调至8的倍数并维护16个free-list各自管理大小为8~128bytes的小额区块空间配置函数allocate()首先判断区块大小大于128就直接调用第一级配置器小于128时就检查对应的free-list。如果free-list之内有可用区块就直接拿来用如果没有可用区块就将区块大小调整至8的倍数然后调用refill()为free-list重新分配空间空间释放函数deallocate()该函数首先判断区块大小大于128bytes时直接调用一级配置器小于128bytes就找到对应的free-list然后释放内存。
STL中hash_map扩容发生什么
hash table表格内的元素称为桶bucket),而由桶所链接的元素称为节点node),其中存入桶元素的容器为stl本身很重要的一种序列式容器——vector容器。之所以选择vector为存放桶元素的基础容器主要是因为vector容器本身具有动态扩容能力无需人工干预。向前操作首先尝试从目前所指的节点出发前进一个位置节点由于节点被安置于list内所以利用节点的next指针即可轻易完成前进操作如果目前正巧是list的尾端就跳至下一个bucket身上那正是指向下一个list的头部节点。map如何创建
1.vector 底层数据结构为数组 支持快速随机访问
2.list 底层数据结构为双向链表支持快速增删
3.deque 底层数据结构为一个中央控制器和多个缓冲区详细见STL源码剖析P146支持首尾中间不能快速增删也支持随机访问
deque是一个双端队列(double-ended queue)也是在堆中保存内容的.它的保存形式如下:
[堆1] -- [堆2] --[堆3] -- ...
每个堆保存好几个元素,然后堆和堆之间有指针指向,看起来像是list和vector的结合品.
4.stack 底层一般用list或deque实现封闭头部即可不用vector的原因应该是容量大小有限制扩容耗时
5.queue 底层一般用list或deque实现封闭头部即可不用vector的原因应该是容量大小有限制扩容耗时stack和queue其实是适配器,而不叫容器因为是对容器的再封装
6.priority_queue 的底层数据结构一般为vector为底层容器堆heap为处理规则来管理底层容器实现
7.set 底层数据结构为红黑树有序不重复
8.multiset 底层数据结构为红黑树有序可重复
9.map 底层数据结构为红黑树有序不重复
10.multimap 底层数据结构为红黑树有序可重复
11.hash_set 底层数据结构为hash表无序不重复
12.hash_multiset 底层数据结构为hash表无序可重复
13.hash_map 底层数据结构为hash表无序不重复
14.hash_multimap 底层数据结构为hash表无序可重复 vector的增加删除都是怎么做的为什么是1.5倍
新增元素vector通过一个连续的数组存放元素如果集合已满在新增数据的时候就要分配一块更大的内存将原来的数据复制过来释放之前的内存在插入新增的元素对vector的任何操作一旦引起空间重新配置指向原vector的所有迭代器就都失效了 初始时刻vector的capacity为0塞入第一个元素后capacity增加为1不同的编译器实现的扩容方式不一样VS2015中以1.5倍扩容GCC以2倍扩容。对比可以发现采用采用成倍方式扩容可以保证常数的时间复杂度而增加指定大小的容量只能达到O(n)的时间复杂度因此使用成倍的方式扩容。
考虑可能产生的堆空间浪费成倍增长倍数不能太大使用较为广泛的扩容方式有两种以2二倍的方式扩容或者以1.5倍的方式扩容。以2倍的方式扩容导致下一次申请的内存必然大于之前分配内存的总和导致之前分配的内存不能再被使用所以最好倍增长因子设置为(1,2)之间 向量容器vector的成员函数pop_back()可以删除最后一个元素.而函数erase()可以删除由一个iterator指出的元素也可以删除一个指定范围的元素。还可以采用通用算法remove()来删除vector容器中的元素.不同的是采用remove一般情况下不会改变容器的大小而pop_back()与erase()等成员函数会改变容器的大小。
什么是函数指针?
函数指针指向的是特殊的数据类型函数的类型是由其返回的数据类型和其参数列表共同决定的而函数的名称则不是其类型的一部分。
一个具体函数的名字如果后面不跟调用符号(即括号)则该名字就是该函数的指针(注意大部分情况下可以这么认为但这种说法并不很严格)。
函数指针的声明方法
int (*pf)(const int, const int); (1)
上面的pf就是一个函数指针指向所有返回类型为int并带有两个const int参数的函数。注意*pf两边的括号是必须的否则上面的定义就变成了
int *pf(const int, const int); (2)
而这声明了一个函数pf其返回类型为int * 带有两个const int参数。
为什么有函数指针
函数与数据项相似函数也有地址。我们希望在同一个函数中通过使用相同的形参在不同的时间使用产生不同的效果。
一个函数名就是一个指针它指向函数的代码。一个函数地址是该函数的进入点也就是调用函数的地址。函数的调用可以通过函数名也可以通过指向函数的指针来调用。函数指针还允许将函数作为变元传递给其他函数两种方法赋值
指针名 函数名 指针名 函数名
说说你对c和c的看法c和c的区别
第一点就应该想到C是面向过程的语言而C是面向对象的语言一般简历上第一条都是熟悉C/C基本语法了解C面向对象思想那么请问什么是面向对象C和C动态管理内存的方法不一样C是使用malloc/free函数而C除此之外还有new/delete关键字关于malooc/free与new/delete的不同又可以说一大堆最后的扩展_1部分列出十大区别接下来就不得不谈到C中的struct和C的类C的类是C所没有的但是C中的struct是可以在C中正常使用的并且C对struct进行了进一步的扩展使struct在C中可以和class一样当做类使用而唯一和class不同的地方在于struct的成员默认访问修饰符是public,而class默认的是private;C支持函数重载而C不支持函数重载而C支持重载的依仗就在于C的名字修饰与C不同例如在C中函数int fun(int ,int)经过名字修饰之后变为 _fun_int_int ,而C是 _fun一般是这样的所以C才会支持不同的参数调用不同的函数C中有引用而C没有这样就不得不提一下引用和指针的区别文后扩展_2;当然还有C全部变量的默认链接属性是外链接而C是内连接C 中用const修饰的变量不可以用在定义数组时的大小但是C用const修饰的变量可以如果不进行,解引用的操作的话是存放在符号表的不开辟内存当然还有局部变量的声明规则不同多态C特有输入输出流之类的很多下面就不再列出来了 “
c/c的内存分配详细说一下栈、堆、静态存储区
1、栈区stack— 由编译器自动分配释放存放函数的参数值局部变量的值等
其操作方式类似于数据结构中的栈。 2、堆区heap — 一般由程序员分配释放若程序员不释放程序结束时可能由OS操作系统回收。注意它与数据结构中的堆是两回事分配方式倒是类似于链表。 3、全局区静态区static—全局变量和静态变量的存储是放在一块的初始化的全局变量和静态变量在一块区域未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。 4、文字常量区 —常量字符串就是放在这里的。程序结束后由系统释放。 5、程序代码区 —存放函数体的二进制代码。
堆与栈的区别
管理方式对于栈来讲是由编译器自动管理无需我们手工控制对于堆来说释放工作由程序员控制容易产生memory leak。 空间大小一般来讲在32位系统下堆内存可以达到4G的空间从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲一般都是有一定的空间大小的例如在VC6下面默认的栈空间大小是1M好像是记不清楚了。当然我们可以修改 打开工程依次操作菜单如下Project-Setting-Link在Category 中选中Output然后在Reserve中设定堆栈的最大值和commit。 注意reserve最小值为4Bytecommit是保留在虚拟内存的页文件里面它设置的较大会使栈开辟较大的值可能增加内存的开销和启动时间。 碎片问题对于堆来讲频繁的new/delete势必会造成内存空间的不连续从而造成大量的碎片使程序效率降低。对于栈来讲则不会存在这个问题因为栈是先进后出的队列他们是如此的一一对应以至于永远都不可能有一个内存块从栈中间弹出在他弹出之前在他上面的后进的栈内容已经被弹出详细的可以参考数据结构这里我们就不再一一讨论了。 生长方向对于堆来讲生长方向是向上的也就是向着内存地址增加的方向对于栈来讲它的生长方向是向下的是向着内存地址减小的方向增长。 分配方式堆都是动态分配的没有静态分配的堆。栈有2种分配方式静态分配和动态分配。静态分配是编译器完成的比如局部变量的分配。动态分配由alloca函数进行分配但是栈的动态分配和堆是不同的它的动态分配是由编译器进行释放无需我们手工实现。 分配效率栈是机器系统提供的数据结构计算机会在底层对栈提供支持分配专门的寄存器存放栈的地址压栈出栈都有专门的指令执行这就决定了栈的效率比较高。堆则是C/C函数库提供的它的机制是很复杂的例如为了分配一块内存库函数会按照一定的算法具体的算法可以参考数据结构/操作系统在堆内存中搜索可用的足够大小的空间如果没有足够大小的空间可能是由于内存碎片太多就有可能调用系统功能去增加程序数据段的内存空间这样就有机会分到足够大小的内存然后进行返回。显然堆的效率比栈要低得多。
野指针是什么如何检测内存泄漏
野指针指向内存被释放的内存或者没有访问权限的内存的指针。“野指针”的成因主要有3种
指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针它的缺省值是随机的它会乱指一气。所以指针变量在创建的同时应当被初始化要么将指针设置为NULL要么让它指向合法的内存。例如 char *p NULL; char *str new char(100);指针p被free或者delete之后没有置为NULL指针操作超越了变量的作用范围。
如何避免野指针
对指针进行初始化
①将指针初始化为NULL。
char * p NULL;
②用malloc分配内存
char * p (char * )malloc(sizeof(char));
③用已有合法的可访问的内存地址对指针初始化
char num[ 30] {0};
char *p num;
指针用完后释放内存将指针赋NULL。
delete(p);
p NULL;
悬空指针和野指针有什么区别
野指针野指针指访问一个已删除或访问受限的内存区域的指针野指针不能判断是否为NULL来避免。指针没有初始化释放后没有置空越界悬空指针一个指针的指向对象已被删除那么就成了悬空指针。野指针是那些未初始化的指针。 内存泄漏内存泄漏
内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上消失而是应用程序分配某段内存后由于设计错误失去了对该段内存的控制
后果
只发生一次小的内存泄漏可能不被注意但泄漏大量内存的程序将会出现各种证照性能下降到内存逐渐用完导致另一个程序失败
如何排除
使用工具软件BoundsCheckerBoundsChecker是一个运行时错误检测工具它主要定位程序运行时期发生的各种错误
调试运行DEBUG版程序运用以下技术CRT(C run-time libraries)、运行时函数调用堆栈、内存泄漏时提示的内存分配序号(集成开发环境OUTPUT窗口)综合分析内存泄漏的原因排除内存泄漏。
解决方法
智能指针。
检查、定位内存泄漏
检查方法在main函数最后面一行加上一句_CrtDumpMemoryLeaks()。调试程序自然关闭程序让其退出查看输出
输出这样的格式{453}normal block at 0x02432CA8,868 bytes long
被{}包围的453就是我们需要的内存泄漏定位值868 bytes long就是说这个地方有868比特内存没有释放。
定位代码位置
在main函数第一行加上_CrtSetBreakAlloc(453);意思就是在申请453这块内存的位置中断。然后调试程序程序中断了查看调用堆栈。加上头文件#include crtdbg.h
new和malloc的区别
new/delete是C关键字需要编译器支持。malloc/free是库函数需要头文件支持使用new操作符申请内存分配时无须指定内存块的大小编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。new操作符内存分配成功时返回的是对象类型的指针类型严格与对象匹配无须进行类型转换故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * 需要通过强制类型转换将void*指针转换成我们需要的类型。new内存分配失败时会抛出bac_alloc异常。malloc分配内存失败时返回NULL。new会先调用operator new函数申请足够的内存通常底层使用malloc实现。然后调用类型的构造函数初始化成员变量最后返回自定义类型指针。delete先调用析构函数然后调用operator delete函数释放内存通常底层使用free实现。malloc/free是库函数只能动态的申请和释放内存无法强制要求其做自定义类型对象构造和析构工作。
delete p;与delete[]pallocator
动态数组管理new一个数组时[]中必须是一个整数但是不一定是常量整数普通数组必须是一个常量整数new动态数组返回的并不是数组类型而是一个元素类型的指针delete[]时数组中的元素按逆序的顺序进行销毁new在内存分配上面有一些局限性new的机制是将内存分配和对象构造组合在一起同样的delete也是将对象析构和内存释放组合在一起的。allocator将这两部分分开进行allocator申请一部分内存不进行初始化对象只有当需要的时候才进行初始化操作。
new和delete的实现原理 delete是如何知道释放内存的大小的额
new简单类型直接调用operator new分配内存而对于复杂结构先调用operator new分配内存然后在分配的内存上调用构造函数对于简单类型new[]计算好大小后调用operator new对于复杂数据结构new[]先调用operator new[]分配内存然后在p的前四个字节写入数组大小n然后调用n次构造函数针对复杂类型new[]会额外存储数组大小
new表达式调用一个名为operator new(operator new[])函数分配一块足够大的、原始的、未命名的内存空间编译器运行相应的构造函数以构造这些对象并为其传入初始值对象被分配了空间并构造完成返回一个指向该对象的指针。
delete简单数据类型默认只是调用free函数复杂数据类型先调用析构函数再调用operator delete针对简单类型delete和delete[]等同。假设指针p指向new[]分配的内存。因为要4字节存储数组大小实际分配的内存地址为[p-4]系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存这个内存根本没有被系统记录所以会崩溃。需要在 new [] 一个对象数组时需要保存数组的维度C 的做法是在分配数组空间时多分配了 4 个字节的大小专门保存数组的大小在 delete [] 时就可以取出这个保存的数就知道了需要调用析构函数多少次了。
malloc申请的存储空间能用delete释放吗
不能malloc /free主要为了兼容Cnew和delete 完全可以取代malloc /free的。malloc /free的操作对象都是必须明确大小的。而且不能用在动态类上。new 和delete会自动进行类型检查和大小malloc/free不能执行构造函数与析构函数所以动态对象它是不行的。当然从理论上说使用malloc申请的内存是可以通过delete释放的。不过一般不这样写的。而且也不能保证每个C的运行时都能正常。
malloc与free的实现原理
在标准C库中提供了malloc/free函数分配释放内存这两个函数底层是由brk、mmap、munmap这些系统调用实现的;brk是将数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中堆和栈中间称为文件映射区域的地方找一块空闲的虚拟内存。这两种方式分配的都是虚拟内存没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候发生缺页中断操作系统负责分配物理内存然后建立虚拟内存和物理内存之间的映射关系malloc小于128k的内存使用brk分配内存将_edata往高地址推malloc大于128k的内存使用mmap分配内存在堆和栈之间找一块空闲内存分配brk分配的内存需要等到高地址内存释放以后才能释放而mmap分配的内存可以单独释放。当最高地址空间的空闲内存超过128K可由M_TRIM_THRESHOLD选项调节时执行内存紧缩操作trim。在上一个步骤free的时候发现最高地址空闲内存超过128K于是内存紧缩。malloc是从堆里面申请内存也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时就会遍历该链表然后就寻找第一个空间大于所申请空间的堆结点然后就将该结点从空闲结点链表中删除并将该结点的空间分配给程序。
malloc、realloc、calloc的区别
malloc函数
void* malloc(unsigned int num_size);
int *p malloc(20*sizeof(int));申请20个int类型的空间
calloc函数
void* calloc(size_t n,size_t size);
int *p calloc(20, sizeof(int));
省去了人为空间计算malloc申请的空间的值是随机初始化的calloc申请的空间的值是初始化为0的
realloc函数
void realloc(void *p, size_t new_size);
给动态分配的空间分配额外的空间用于扩充容量。
__stdcall和__cdecl的区别
__stdcall
__stdcall是函数恢复堆栈只有在函数代码的结尾出现一次恢复堆栈的代码在编译时就规定了参数个数无法实现不定个数的参数调用
__cdecl
__cdecl是调用者恢复堆栈假设有100个函数调用函数a那么内存中就有100端恢复堆栈的代码可以不定参数个数每一个调用它的函数都包含清空堆栈的代码所以产生的可执行文件大小会比调用__stacall函数大。
使用智能指针管理内存资源RAII
RAII全称是“Resource Acquisition is Initialization”直译过来是“资源获取即初始化”也就是说在构造函数中申请分配资源在析构函数中释放资源。因为C的语言机制保证了当一个对象创建的时候自动调用构造函数当对象超出作用域的时候会自动调用析构函数。所以在RAII的指导下我们应该使用类来管理资源将资源和对象的生命周期绑定。智能指针std::shared_ptr和std::unique_ptr即RAII最具代表的实现使用智能指针可以实现自动的内存管理再也不需要担心忘记delete造成的内存泄漏。毫不夸张的来讲有了智能指针代码中几乎不需要再出现delete了。
手写实现智能指针类
智能指针是一个数据类型一般用模板实现模拟指针行为的同时还提供自动垃圾回收机制。它会自动记录SmartPointerT*对象的引用计数一旦T类型对象的引用计数为0就释放该对象。除了指针对象外我们还需要一个引用计数的指针设定对象的值并将引用计数计为1需要一个构造函数。新增对象还需要一个构造函数析构函数负责引用计数减少和释放内存。通过覆写赋值运算符才能将一个旧的智能指针赋值给另一个指针同时旧的引用计数减1新的引用计数加1一个构造函数、拷贝构造函数、复制构造函数、析构函数、移走函数
内存对齐位域
1、 分配内存的顺序是按照声明的顺序。
2、 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍不是整数倍空出内存直到偏移量是整数倍为止。
3、 最后整个结构体的大小必须是里面变量类型最大值的整数倍。 添加了#pragma pack(n)后规则就变成了下面这样
1、 偏移量要是n和当前变量大小中较小值的整数倍
2、 整体大小要是n和最大变量大小中较小值的整数倍
3、 n值必须为1,2,4,8…为其他值时就按照默认的分配规则
结构体变量比较是否相等
重载了 “” 操作符
struct foo {int a;int b;bool operator(const foo rhs) // 操作运算符重载{return( a rhs.a) (b rhs.b);}};
元素的话一个个比指针直接比较如果保存的是同一个实例地址则(p1p2)为真