第一章 类
1.1 为什么构造函数不可以是虚函数
①从存储空间角度
虚函数对应一个vtable(虚函数表),这大家都知道,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。
②从使用角度
虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。
虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
③构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它。但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
④从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数
从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数
⑤当一个构造函数被调用时,它做的首要的事情之一是初始化它的V P T R。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码- -既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。
所以它使用的V P T R必须是对于这个类的V TA B L E。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内, V P T R将保持被初始化为指向这个V TA B L E, 但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置V P T R指向它的 V TA B L E,等.直到最后的构造函数结束。V P T R的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生 类顺序的另一个理由。
但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置V P T R指向它自己的 V TA B L E。如果函数调用使用虚机制,它将只产生通过它自己的V TA B L E的调用,而不是最后的V TA B L E(所有构造函数被调用后才会有最后的V TA B L E)。
1.2、为什么析构函数可以是虚函数
编译器总是根据类型来调用类成员函数。但是一个派生类的指针可以安全地转化为一个基类的指针。这样删除一个基类的指针的时候,C++不管这个指针指向一个基类对象还是一个派生类的对象,调用的都是基类的析构函数而不是派生类的。如果你依赖于派生类的析构函数的代码来释放资源,而没有重载析构函数,那么会有资源泄漏。
所以建议的方式是将析构函数声明为虚函数。如果你使用MFC,并且以CObject或其派生类为基类,那么MFC已经为你做了这件事情;CObject的析构函数是虚函数。一个函数一旦声明为虚函数,那么不管你是否加上virtual 修饰符,它在所有派生类中都成为虚函数。但是由于理解明确起见,建议的方式还是加上virtual 修饰符。
C++不把虚析构函数直接作为默认值的原因是虚函数表的开销以及和C语言的类型的兼容性。有虚函数的对象总是在开始的位置包含一个隐含的虚函数表指针成员。如果是对于MFC类CPoint和CSize这样的小型类,增加一个指针就增加了很多内存占用,而且使得其内存表示和基类POINT和SIZE不一致。
2.虚函数表
请移步:https://www.cnblogs.com/LUO77/p/5771237.html
3.动态绑定与静态绑定
- 静态绑定发生在编译期,动态绑定发生在运行期;
- 对象的动态类型可以更改,但是静态类型无法更改;
- 要想实现动态,必须使用动态绑定;
- 在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定;
- 静态多态是指通过模板技术或者函数重载技术实现的多态,其在编译器确定行为。动态多态是指通过虚函数技术实现在运行期动态绑定的技术
动态绑定:有一个基类,两个派生类,基类有一个virtual函数,两个派生类都覆盖了这个虚函数。现在有一个基类的指针或者引用,当该基类指针或者引用指向不同的派生类对象时,调用该虚函数,那么最终调用的是该被指向对象对应的派生类自己实现的虚函数。
4.虚函数表是针对类的还是针对对象的?同一个类的两个对象的虚函数表是怎么维护的?
编译器为每一个类维护一个虚函数表(本质是一个函数指针数组,数组里面存放了一系列函数地址 ),每个对象的首地址保存着该虚函数表的指针,同一个类的不同对象实际上指向同一张虚函数表。调用形式:*(this指针+调整量)[虚函数在vftable内的偏移]()
在类内部添加一个虚拟函数表指针,该指针指向一个虚拟函数表,该虚拟函数表包含了所有的虚拟函数的入口地址,每个类的虚拟函数表都不一样,在运行阶段可以循此脉络找到自己的函数入口。纯虚函数相当于占位符, 先在虚函数表中占一个位置由派生类实现后再把真正的函数指针填进去。除此之外和普通的虚函数没什么区别。
在单继承形式下,子类的完全获得父类的虚函数表和数据。子类如果重写了父类的虚函数(如fun),就会把虚函数表原本fun对应的记录(内容MyClass::fun)覆盖为新的函数地址(内容MyClassA::fun),否则继续保持原本的函数地址记录。
使用这种方式,就可以实现多态的特性。假设我们使用如下语句:
-
MyClass*pc= new MyClassA;
-
pc->fun();
因为虚函数表内的函数地址已经被子类重写的fun函数地址覆盖了,因此该处调用的函数正是MyClassA::fun,而不是基类的MyClass::fun。
如果使用MyClassA对象直接访问fun,则不会出发多态机制,因为这个函数调用在编译时期是可以确定的,编译器只需要直接调用MyClassA::fun即可。
注:对象不包含虚函数表,只有虚指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表
详情可以参考:http://www.cnblogs.com/fanzhidongyzby/archive/2013/01/14/2859064.html
第二章 其他
2. extern关键字的作用
2.1 概述
extern置于变量或函数前,用于标示变量或函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。它只要有两个作用:
- 当它与“C”一起连用的时候,如:extern "C" void fun(int a,int b);则告诉编译器在编译fun这个函数时候按着C的规矩去翻译,而不是C++的(这与C++的重载有关,C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同)
#ifdef __cplusplus extern "C" { #endif /*...*/ #ifdef __cplusplus } #endif
- 当extern不与“C”在一起修饰变量或函数时,如:extern int g_Int;它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块或其他模块中使用。记住它是一个声明不是定义!也就是说B模块(编译单元)要是引用模块(编译单元)A中定义的全局变量或函数时,它只要包含A模块的头文件即可,在编译阶段,模块B虽然找不到该函数或变量,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。
2.2 .为什么不将全局变量的定义放在头文件中
首先要说明什么是全局变量,c语言中全局变量一般是指定义在函数体外的变量。全局变量按可访问性可分为外部变量和内部变量。
内部变量是指 使用了static关键字修饰的全局变量,它的可访问范围(作用域)被限定在本源文件所在的链接文件模块中,不能被其它文件模块引用。反之没有被static关键字修饰的全局变量则是外部变量,其它文件模块可以通过extern关键字引用该全局变量并访问。
要说明的是全局变量 无论是内部变量还是外部变量,的存储类别都是静态的,也就是放到静态内存区域中,它编译链接阶段就已经分配好了固定的内存。
搞清楚上面的内容,就很容易得出若把全局变量放在头文件会有哪些问题;
一 对内部变量来说,每个include该头文件的文件模块中都会单独为这个内部变量分配静态内存空间,这个空间是相对独立的,是一种空间浪费,同时还失去了全局变量访问一致性的特点,实在没有什么意义。如果这个头文件只被一个模块使用,对于这个文件模块来说应该没啥问题。
二 对外部变量来讲,这个头文件被多个文件模块include的情况下,链接过程会报错,因为符号冲突,所有include这个头文件的模块都会有这个全局符号。在这个头文件仅仅只被一个模块include的时候可以正常使用。
经上分析得出要避免全局变量定义在头文件中,因为当这个头文件被多方include的时候会产生一些不必要的麻烦,就这么多。
全局变量作用域范围较广,被错误修改后排查定位问题比较困难,若非必要尽少使用。
下面说一下比较好的方式就是全局变量只定义在实现文件(.c,.m)中,对内部变量没啥说的它只在文件模块内部使用,对外部变量可以在该模块头文件中使用extern关键字修饰一下,这样其它文件模块只要直接include该头文件就可以使用模块中的外部变量了。
2.2.static关键字的作用
- 修饰局部变量
static修饰局部变量时,使得被修饰的变量成为静态变量,存储在静态区。存储在静态区的数据生命周期与程序相同,在main函数之前初始化,在程序退出时销毁。(无论是局部静态还是全局静态)
- 修饰全局变量
全局变量本来就存储在静态区,因此static并不能改变其存储位置。但是,static限制了其链接属性。被static修饰的全局变量只能被该包含该定义的文件访问(即改变了作用域)。
- 修饰函数
static修饰函数使得函数只能在包含该函数定义的文件中被调用。对于静态函数,声明和定义需要放在同一个文件夹中。
- 修饰成员变量
用static修饰类的数据成员使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象,所有的对象都只维持同一个实例。 因此,static成员必须在类外进行初始化(初始化格式:int base::var=10;),而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化。
- 修饰成员函数
用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针,因而只能访问类的static成员变量。静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。例如可以封装某些算法,比如数学函数,如ln,sin,tan等等,这些函数本就没必要属于任何一个对象,所以从类上调用感觉更好,比如定义一个数学函数类Math,调用Math::sin(3.14);还可以实现某些特殊的设计模式:如Singleton;
- 最重要的特性:隐藏
当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏。
不可以同时用const和static修饰成员函数。
C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。我们也可以这样理解:两者的语意是矛盾的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。
2.3.const的作用
- 定义变量为只读变量,不可修改
- 修饰函数的参数和返回值(后者应用比较少,一般为值传递)
- const成员函数(只需要在成员函数参数列表后加上关键字const,如char get() const;)可以访问const成员变量和非const成员变量,但不能修改任何变量。在声明一个成员函数时,若该成员函数并不对数据成员进行修改操作,应尽可能将该成员函数声明为const成员函数。
-
const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.即对于class A,有const A a;那么a只能访问A的const成员函数。而对于:A b;b可以访问任何成员函数。
使用const关键字修饰的变量,一定要对变量进行初始化
2.4.指针与引用的区别
- 指针只是一个变量,只不过这个变量存储的是一个地址;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已,不占用内存空间。
- 引用必须在定义的时候初始化,而且初始化后就不能再改变;而指针不必在定义的时候初始化,初始化后可以改变。
- 指针可以为空,但引用不能为空(这就意味着我们拿到一个引用的时候,是不需要判断引用是否为空的,而拿到一个指针的时候,我们则需要判断它是否为空。这点经常在判断函数参数是否有效的时候使用。)
- “sizeof 引用" = 指向变量的大小 , "sizeof 指针"= 指针本身的大小
- 指针可以有多级,而引用只能是一级
2.5.new与malloc的区别
- malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
- 对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
- new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void指针。
2.6.智能指针怎么实现?什么时候改变引用计数?
- 构造函数中计数初始化为1;
- 拷贝构造函数中计数值加1;
- 赋值运算符中,左边的对象引用计数减一,右边的对象引用计数加一;
- 析构函数中引用计数减一;
- 在赋值运算符和析构函数中,如果减一后为0,则调用delete释放对象。
2.7.内联函数,宏定义和普通函数的区别
- 内联函数要做参数类型检查,这是内联函数跟宏相比的优势
- 宏定义是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换, 内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的压栈出栈的开销,提高效率。
- 内联函数是指嵌入代码,就是在调用函数的地方不是跳转,而是把代码直接写到那里去。对于短小简单的代码来说,内联函数可以带来一定的效率提升,而且和C时代的宏函数相比,内联函数 更安全可靠。可是这个是以增加空间消耗为代价的
- const与#define的区别:宏在预处理阶段替换,const在编译阶段替换;宏没有类型,不做安全检查,const有类型,在编译阶段进行安全检查
2.8. C++内存管理
栈: 存放函数参数以及局部变量 , 在出作用域时 , 将自动被释放 . 栈内存分配运算内置于处理器的指令集中 , 效率 很 高 , 但分配的内存容量有限 .
堆 :new 分配的内存块 ( 包括数组 , 类实例等 ), 需 delete 手动释放 . 如果未释放 , 在整个程序结束后 ,OS 会帮你回收掉 .
自由存储区: malloc 分配的内存块 , 需 free 手动释放 . 它和堆有些相似 .
全局/静态区: 保存自动全局变量和static变量(包括static全局和局部变量)。静态区的内容在整个程序的生命周期内都存在,有编译器在编译的时候分配(数据段(存储全局数据和静态数据)和代码段(可执行的代码/只读常量))。
常量存储区: 常量 (const) 存于此处 , 此存储区不可修改 .
栈与堆的区别:
管理方式不同: 栈是编译器自动管理的,堆需手动释放
空间大小不同: 在32位OS下,堆内存可达到4GB的的空间,而栈就小得可怜.(VC6中,栈默认大小是1M,当然,你可以修改它)
能否产生碎片不同:对于栈来说,进栈/出栈都有着严格的顺序(先进后出),不会产生碎片;而堆频繁的new/delete,会造成内存空间的不连续,容易产生碎片.
生长方向不同:栈向下生长,以降序分配内存地址;堆向上生长,以升序分配内在地址.
分配方式不同:堆动态分配,无静态分配;栈分为静态分配和动态分配,比如局部变量的分配,就是动态分配(alloca函数),函数参数的分配就是动态分配(我想的…).
分配效率不同:栈是系统提供的数据结构,计算机会在底层对栈提供支持,进栈/出栈都有专门的指令,这就决定了栈的效率比较高.堆则不然,它由C/C++函数库提供,机制复杂,堆的效率要比栈低得多.
可以看出,栈的效率要比堆高很多,所以,推荐大家尽量用栈.不过,虽然栈有如此多的好处,但远没有堆使用灵活.
第四章 实战
4.1 .手写strcpy,memcpy,strcat,strcmp等函数
https://blog.csdn.net/gao1440156051/article/details/51496782
https://blog.csdn.net/wilsonboliu/article/details/7919773
16.i++是否为原子操作?
不是。操作系统原子操作是不可分割的,在执行完毕不会被任何其它任务或事件中断,分为两种情况(两种都应该满足)
(1) 在单线程中, 能够在单条指令中完成的操作都可以认为是" 原子操作",因为中断只能发生于指令之间。
(2) 在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。
i++分为三个阶段:
内存到寄存器
寄存器自增
写回内存
这三个阶段中间都可以被中断分离开.
17.有关数组,指针,函数的三者结合问题
数组指针和指针数组的区别:https://blog.csdn.net/men_wen/article/details/52694069
右左法则的说明:http://www.cnblogs.com/zhangjing0502/archive/2012/06/08/2542059.html
指针常量和常量指针:https://www.zhihu.com/question/19829354
https://blog.csdn.net/xingjiarong/article/details/47282563
注意:所谓指向常量的指针或引用(即常量引用、常量指针),不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值,但这些对象却可以通过其他途径改变。
const int *a; 等价于int const *a; const在前面所以内容不可以改变,但是指针指向可以改变。也就是常量指针
int *const a; 表示的是指针指向不可改变,但是指针所存放的内容可以改变,也即是指针常量
补充:
18.C++中类与结构体的区别?
- 最本质的一个区别就是默认的访问控制: struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。
- “class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数。
19.析构函数的作用?
析构函数是用来释放所定义的对象中使用的指针,默认的析构函数不用显示调用,自建的析构函数要在程序末尾调用。
如果你的类里面只用到的基本类型,如int char double等,系统的默认析构函数其实什么都没有做
但如果你使用了其他的类如vector,string等,系统的默认析构函数就会调用这些类对象的析构函数
如果是自己写析构函数的话,如果你的类里面分配了系统资源,如new了内存空间,打开了文件等,那么在你的析构函数中就必须释放相应的内存空间和关闭相关的文件;这样系统就会自动调用你的析构函数释放资源,避免内存泄漏
例如:
-
class A
-
{
-
private:
-
char *data;
-
public:
-
A()
-
{
-
data = new char[ 10];
-
}
-
~A()
-
{
-
delete[] data;
-
}
-
};
- 1
A a;
a 中将 new 10个 char
当 a 这个变量消亡的时候,将自动执行 ~A(),释放空间
对象消亡时,自动被调用,用来释放对象占用的空间,避免内存泄漏
20.虚函数的作用?
虚函数可以让成员函数操作一般化,用基类的指针指向不同的派生类的对象时,基类指针调用其虚成员函数,则会调用其真正指向对象的成员函数,而不是基类中定义的成员函数(只要派生类改写了该成员函数)。若不是虚函数,则不管基类指针指向的哪个派生类对象,调用时都会调用基类中定义的那个函数。虚函数是C++多态的一种表现,可以进行灵活的动态绑定。
重点可参考:https://www.cnblogs.com/wangxiaobao/p/5850949.html
http://www.cnblogs.com/fanzhidongyzby/archive/2013/01/14/2859064.html
21.操作系统和编译器如何区分全局变量和局部变量?
操作系统只管调度进程,编译器通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运行的时候被加载。局部变量则分配在栈里面 。
22. Makefile文件的作用?
makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。
23.结构体和联合体的区别?
结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。
对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。
24.列表初始化问题?
使用初始化列表主要是基于性能问题,对于内置类型,如int, float等,使用初始化列表和在构造函数体内初始化差别不是很大;但是对于类类型来说,最好使用初始化列表。这样就可以直接调用拷贝构造函数初始化,省去了一次调用默认构造函数的过程。
-
struct Test1
-
{
-
Test1() // 无参构造函数
-
{
-
cout << "Construct Test1" << endl ;
-
}
-
Test1( const Test1& t1) // 拷贝构造函数
-
{
-
cout << "Copy constructor for Test1" << endl ;
-
this->a = t1.a ;
-
}
-
Test1& operator = ( const Test1& t1) // 赋值运算符
-
{
-
cout << "assignment for Test1" << endl ;
-
this->a = t1.a ;
-
return * this;
-
}
-
int a ;
-
};
-
struct Test2 //普通初始化
-
{
-
Test1 test1 ;
-
Test2(Test1 &t1)
-
{
-
test1 = t1 ;
-
}
-
};
- 1
-
struct Test2 //2.列表初始化
-
{
-
Test1 test1 ;
-
Test2(Test1 &t1):test1(t1){}
-
}
- 1
-
Test1 t1 ; //调用
-
Test2 t2(t1) ;
- 1
普通初始化:
列表初始化:
下列情况一定要使用初始化成员列表
- 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
- 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
- 需要初始化的数据成员是对象的情况
参考地址:https://www.cnblogs.com/weizhixiang/p/6374430.html
25. 重载与重写的区别?
从定义上来说:重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。重写:是指子类重新定义父类虚函数的方法。
从实现原理上来说:重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数。重写:当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。
补充:“隐藏”是指派生类的函数屏蔽了与其同名的基类函数。规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
26.类型安全以及C++中的类型转换?
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。C只在局部上下文中表现出类型安全,比如试图从一种结构体的指针转换成另一种结构体的指针时,编译器将会报告错误,除非使用显式类型转换。然而,C中相当多的操作是不安全的。
详情可以移步:https://blog.csdn.net/chengonghao/article/details/50974022
四种类型转换:
- static_cast <T*> (content) 静态转换.在编译期间处理,可以实现C++中内置基本数据类型之间的相互转换。如果涉及到类的话,static_cast只能在有相互联系的类型中进行相互转换,不一定包含虚函数。
- dynamic_cast<T*>(content) 动态类型转换;也是向下安全转型;是在运行的时候执行;基类中一定要有虚函数,否则编译不通过。在类层次间进行上行转换时(如派生类指针转为基类指针),dynamic_cast和static_cast的效果是一样的。在进行下行转换时(如基类指针转为派生类指针),dynamic_cast具有类型检查的功能,比static_cast更安全。
- const_cast<T*>(content) 去常转换;编译时执行;
- reinterpret_cast<T*>(content) 重解释类型转换;
详情可以移步:https://blog.csdn.net/u010025211/article/details/48626687
https://blog.csdn.net/xtzmm1215/article/details/46475565
https://blog.csdn.net/xingkongfenqi/article/details/49148885
27.内存对齐的原则以及作用?
- 结构体内的成员按自身长度自对齐(32位机器上,如char=1,short=2,int=4,double=8),所谓自对齐是指该成员的起始地址必须是它自身长度的整数倍。如int只能以0,4,8这类地址开始。
- 结构体的总大小为结构体的有效对齐值的整数倍(默认以结构体中最长的成员长度为有效值的整数倍,当用#pragrma pack(n)指定时,以n和结构体中最长的成员的长度中较小者为其值)。即sizeof的值,必须是其内部最大成员的整数倍,不足的要补齐。
例如:
-
class A
-
{
-
char c;
-
int a;
-
char d;
-
};
-
cout << sizeof(A) << endl;
-
class B
-
{
-
char c;
-
char d;
-
int a;
-
};
-
cout << sizeof(B) << endl;
- 1
sizeof(A)=12,sizeof(B)=8;
因为左边是1+(3)+4+1+(3)=12,而右边是1+1+(2)+4=8。括号中为补齐的字节。
内存对齐的作用:
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:经过内存对齐后,CPU的内存访问速度大大提升。
详情可以移步:https://blog.csdn.net/chy19911123/article/details/48894579
28.关键字registr,typdef的作用?
register关键字的作用:
请求CPU尽可能让变量的值保存在CPU内部的寄存器中,减去CPU从内存中抓取数据的时间,提高程序运行效率。
使用register关键字应注意什么?
1.只有局部变量才可以被声明用register修饰
(register不能修饰全局变量和函数的原因:全局变量可能被多个进程访问,而用register修饰的变量,只能被当前进程访问)
2.不能用取地址获取用register修饰的变量的地址(原因:变量保存在寄存器中,而取地址获取的地址的是内存的地址)
3. 用register修饰的变量一定要是CPU所接受的数据类型
typedef关键字的作用:
给数据类型定义一个新名字,
1. 提高了移植性
2. 简化复杂的类型声明,提高编码效率
3. 解释数据类型的作用
29.什么情况下需要将析构函数定义为虚函数?
当基类指针指向派生类的对象(多态性)时。如果定义为虚函数,则就会先调用该指针指向的派生类析构函数,然后派生类的析构函数再又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。所以,将析构函数声明为虚函数是十分必要的。
详情可以移步:https://blog.csdn.net/jiadebin890724/article/details/7951461
30.有关纯虚函数的理解?
纯虚函数是为你的程序制定一种标准,纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
-
class A{
-
protected:
-
void foo(); //普通类函数
-
virtual void foo1(); //虚函数
-
virtual void foo2() = 0; //纯虚函数
-
}
- 1
带纯虚函数的类抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。
虚函数是为了继承接口和默认行为
纯虚函数只是继承接口,行为必须重新定义
(在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常 理。所以引入了纯虚函数的概念)
详情可以参考:https://blog.csdn.net/ybhjx/article/details/51788396
30.基类指针指向派生类,派生类指针指向基类?
基类指针可以指向派生类对象,从而实现多态,例如:
-
#include <iostream>
-
using namespace std;
-
class Shape {
-
public:
-
virtual double area() const = 0; //纯虚函数
-
};
-
class Square : public Shape {
-
double size;
-
public:
-
Square( double s) {
-
size = s;
-
}
-
virtual double area() const {
-
return size * size;
-
}
-
};
-
class Circle : public Shape {
-
double radius;
-
public:
-
Circle( double r) {
-
radius = r;
-
}
-
virtual double area() const {
-
return 3.14159 * radius * radius;
-
}
-
};
-
int main()
-
{
-
Shape* array[ 2]; //定义基类指针数组
-
Square Sq(2.0);
-
Circle Cir(1.0);
-
array[ 0] = &Sq;
-
array[ 1] =&Cir;
-
for ( int i = 0; i < 2; i++) /
-
{
-
cout << array[i]->area() << endl;
-
}
-
return 0;
-
}
- 1
上面的不同对象Sq,Cir(来自继承同一基类的不同派生类)接受同一消息(求面积,来自基类的成员函数area()),但是却根据自身情况调用不同的面积公式(执行了不同的行为,它是通过虚函数实现的)。我们可以理解为,继承同一基类的不同派生对象,对来自基类的同一消息执行了不同的行为,这就是多态,它是通过继承和虚函数实现的。而接受同一消息的实现就是基于基类指针。
但是要注意的是,这个指针只能用来调用基类的成员函数。
如果试图通过基类指针调用派生类才有的成员函数,则编译器会报错。
为了避免这种错误,必须将基类指针强制转化为派生类指针。然后派生类指针可以用来调用派生类的功能。这称为向下强制类型转换,这是一种潜在的危险操作。
派生类指针不可以指向基类对象,例如:
有个people类是基类,成员有姓名和身份证号,有个派生类学生student,添加了成员学号,现在如果你说的这个情况成立student的指针----pt让他指向people成员t,则t只有两个成员变量,而*pt有3个,现在pt->学号这个变量在pt下是可以使用的,但它指向的实体却没有这个变量,所以出错,于是C++直接就避免了这样的隐式转换。
所以根据上述信息我们可以知道:
进行上行转换(把派生类的指针或引用转换成基类表示)是安全的; 进行下行转换(把基类指针或引用转换成派生类表示)是不安全的。
参考链接:https://blog.csdn.net/flyingbird_sxf/article/details/41358737
https://www.cnblogs.com/rednodel/p/5800142.html
31. 继承机制中引用和指针之间如何转换?
基类——>派生类:用dynamic_cast转换(显示转换),首先检查基类指针(引用)是否真正指向一个派生类对象,然后再做相应处理,对指针进行dynamic_cast,成功返回派生类对象,失败返回空指针,对引用进行dynamic_cast,成功返回派生类对象,失败抛出一个异常。 不允许隐式转换。
派生类——>基类:可以用dynamic_cast或者直接进行类型转换(直接赋值)。
32.c语言和c++有什么区别?
C语言是结构化的编程语言,它是面向过程的,而C++是面向对象的。
封装:将数据和函数等集合在一个单元中(即类)。被封装的类通常称为抽象数据类型。封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
继承:继承主要实现重用代码,节省开发时间。它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,可以通过指向派生类的基类指针,来调用实现派生类中的方法。有编译时多态和运行时多态。
33.C++中的公有,私有,保护的问题?
类 | 类对象 | 公有继承派生类 | 私有继承派生类 | 保护继承派生类 | 公有继承派生类对象 | 私有继承派生类对象 | 保护继承派生类对象 | |
公有成员 | √ | √ | √ | √ | √ | √ | X | X |
私有成员 | √ | X | X | X | X | X | X | X |
保护成员 | √ | X | √ | √ | √ | X | X | X |
√:代表可以访问,X代表不能访问。
参考链接:https://zhidao.baidu.com/question/551075894.html
34.如何实现类对象只能静态分配或动态分配?
C++中建立类的对象有两种方式:
(1)静态建立,例如 A a;
静态建立一个类对象,就是由编译器为对象在栈空间中分配内存。使用这种方法,是直接调用类的构造函数。
(2)动态建立,例如 A* p = new A();
动态建立一个类对象,就是使用new运算符为对象在堆空间中分配内存。这个过程分为两步:第一步执行operator new( )函数,在堆空间中搜索一块内存并进行分配;第二步调用类的构造函数构造对象。这种方法是间接调用类的构造函数。
只能动态分配:
其实,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性(其实不光是析构函数,只要是非静态的函数,编译器都会进行检查)。如果类的析构函数在类外部无法访问,则编译器拒绝在栈空间上为类对象分配内存。 因此,可以将析构函数设为private,这样就无法在栈上建立类对象了。但是为了子类可以继承,最好设置成protected。
- class A
- {
- protected:
- A(){}
- ~A(){}
- public:
- static A* create(){return new A();}
- void destory(){delete this;}
- };
只能静态分配:
只有使用new运算符,对象才会被建立在堆上。因此只要限制new运算符就可以实现类对象只能建立在栈上。可以将new运算符设为私有。
- class A
- {
- private:
- void* operator new(size_t t){} //注意函数的第一个参数和返回值都是固定的
- void operator delete(void* ptr)() //重载了new就需要重载delete
- public:
- A(){}
- ~A(){}
- };
35.explicit关键字的作用?
C++中, 一个参数的 构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数), 承担了两个角色。 1 是个 构造器 ,2 是个默认且隐含的类型转换操作符。
所以, 有时候在我们写下如 AAA = XXX, 这样的代码, 且恰好XXX的类型正好是AAA单参数构造器的参数类型, 这时候 编译器就自动调用这个构造器, 创建一个AAA的对象。
这样看起来好象很酷, 很方便。 但在某些情况下(见下面权威的例子), 却违背了我们(程序员)的本意。 这时候就要在这个构造器前面加上explicit修饰, 指定这个构造器只能被明确的调用/使用, 不能作为类型转换操作符被隐含的使用。
-
class Test1
-
{
-
public:
-
Test1( int n)
-
{
-
num=n;
-
} //普通构造函数
-
private:
-
int num;
-
};
-
class Test2
-
{
-
public:
-
explicit Test2(int n)
-
{
-
num=n;
-
} //explicit(显式)构造函数
-
private:
-
int num;
-
};
-
int main()
-
{
-
Test1 t1= 12; //隐式调用其构造函数,成功
-
Test2 t2= 12; //编译错误,不能隐式调用其构造函数
-
Test2 t2(12); //显式调用成功
-
return 0;
-
}
- 1
Test1的 构造函数带一个int型的参数,代码23行会隐式转换成调用Test1的这个构造函数。而Test2的构造函数被声明为explicit(显式),这表示不能通过隐式转换来调用这个构造函数,因此代码24行会出现编译错误。
普通构造函数能够被 隐式调用。而explicit构造函数只能被显式调用。
36.内存溢出,内存泄漏的原因?
内存溢出是指程序在申请内存时,没有足够的内存空间供其使用。原因可能如下:
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据
- 代码中存在死循环或循环产生过多重复的对象实体
- 递归调用太深,导致堆栈溢出等
- 内存泄漏最终导致内存溢出
内存泄漏是指向系统申请分配内存进行使用(new),但是用完后不归还(delete),导致占用有效内存。常见的几种情况:
(1) 在类的构造函数和析构函数中没有匹配的调用new和delete函数
两种情况下会出现这种内存泄露:一是在堆里创建了对象占用了内存,但是没有显示地释放对象占用的内存;二是在类的构造函数中动态的分配了内存,但是在析构 函数中没有释放内存或者没有正确的释放内存
(2) 在释放对象数组时在delete中没有使用方括号
方括号是告诉编译器这个指针指向的是一个对象数组,同时也告诉编译器正确的对象地址值病调用对象的析构函数,如果没有方括号,那么这个指针就被默认为只指向一个对象,对象数组中的其他对象的析构函数就不会被调用,结果造成了内存泄露。
(3)没有将基类的析构函数定义为虚函数
当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露
参考链接: https://blog.csdn.net/hyqwmxsh/article/details/52813307
缓冲区溢出(栈溢出)
程序为了临时存取数据的需要,一般会分配一些内存空间称为缓冲区。如果向缓冲区中写入缓冲区无法容纳的数据,机会造成缓冲区以外的存储单元被改写,称为缓冲区溢出。而栈溢出是缓冲区溢出的一种,原理也是相同的。分为上溢出和下溢出。其中,上溢出是指栈满而又向其增加新的数据,导致数据溢出;下溢出是指空栈而又进行删除操作等,导致空间溢出。
37.auto_ptr类与shared_ptr类?
从c++11开始, auto_ptr已经被标记为弃用, 常见的替代品为shared_ptr。shared_ptr的不同之处在于引用计数, 在复制(或赋值)时不会像auto_ptr那样直接转移所有权。 两者都是模板类,却可以像指针一样去使用。只是在指针上面的一层封装。
auto_ptr实际也是一种类, 拥有自己的析构函数, 生命周期结束时能自动释放资源,正因为能自动释放资源, 特别适合在单个函数内代替new/delete的调用, 不用自己调用delete,也不用担心意外退出造成内存的泄漏。
atuo_ptr的缺陷:
- auto_ptr不能共享所有权,即不要让两个auto_ptr指向同一个对象(因为它采用的是转移语义的拷贝,原指针会变为NULL)。
- auto_ptr不能管理对象数组(因为它内部的析构函数调用的是delete而不是delete[])。
- auto_ptr不能作为容器对象,STL容器中的元素经常要支持拷贝,赋值等操作,在这过程中auto_ptr会传递所有权。
详情原因可以参考:https://blog.csdn.net/uestclr/article/details/51316001
https://blog.csdn.net/kezunhai/article/details/38514823
shared_ptr 使用引用计数的方式来实现对指针资源的管理。同一个指针资源,可以被多个 shared_ptr 对象所拥有,直到最后一个 shared_ptr 对象析构时才释放所管理的对象资源。
可以说,shared_ptr 是最智能的智能指针,因为其特点最接近原始的指针。不仅能够自由的赋值和拷贝,而且可以安全的用在标准容器中。
38. 有4种情况,编译器必须为未声明的constructor的classes合成一个default constructor:
l “带有默认构造函数”的成员对象
l “带有默认构造函数”的基类
l “带有虚函数”的类
l “带有虚拟基类”的类
被合成的构造函数只能满足编译器(而非程序员)的需要。在合成默认的构造函数中,只有基类的子对象和成员对象会被初始化,其他非静态的数据成员(如整数,指针等)都不会被初始化。
所以并不是任何的类如果没有定义默认的构造函数,都会被合成一个出来。
39. 虚基类
在C++中,如果在多条继承路径上有一个公共的基类,那么在这些路径中的某几条路径的汇合处,这个公共的基类就会产生多个实例(从而造成二义性).如果想使这个公共的基类只产生一个实例,则可将这个基类说明为虚基类. 这要求在从base类派生新类时,使用关键字virtual将base类说明为虚基类.
用例子说明吧。
class base{protected:int b};
clase base1:public base{..};
clase base2:public base{..};
clase derived:public base1,public base2 {..};
derived d;
d.b //错误.
d.base::b //错误. 因为不知是用d.base1::b还是d.base2::b
=================================================
class base{protected:int b..};
clase base1:virtual public base{..}; //说明base为base1虚基类
clase base2:virtual public base{..}; //说明base为base2虚基类
clase derived:public base1,public base2 {..};
derived d;
d.b //对.
d.base::b //对. 因为d.base::b和d.base1::b还是d.base2::b都是引用同一虚基类成员b,具有相同的值.
40. 模板的特例化
引入原因:编写单一的模板,它能适应大众化,使每种类型都具有相同的功能,但对于某种特定类型,如果要实现其特有的功能,单一模板就无法做到,这时就需要模板特例化。
定义:是对单一模板提供的一个特殊实例,它将一个或多个模板参数绑定到特定的类型或值上。
函数模板特例化:必须为原函数模板的每个模板参数都提供实参,且使用关键字template后跟一个空尖括号对<>,表明将原模板的所有模板参数提供实参。
1. template <typename T>
2. void fun(T a)
3. {
4. cout << "The main template fun(): " << a << endl;
5. }
6.
7. template <> // 对int型特例化
8. void fun(int a)
9. {
10. cout << "Specialized template for int type: " << a << endl;
11. }
12.
13. int main()
14. {
15. fun<char>('a');
16. fun<int>(10);
17. fun<float>(9.15);
18. return 0;
19. }
对于除int型外的其他数据类型,都会调用通用版本的函数模板fun(T a);对于int型,则会调用特例化版本的fun(int a)。注意,一个特例化版本的本质是一个实例,而非函数的重载。因此,特例化不影响函数匹配。
类模板的特例化:
1. template <typename T>
2. class Test{
3. public:
4. void print(){
5. cout << "General template object" << endl;
6. }
7. };
8.
9. template<> // 对int型特例化
10. class Test<int>{
11. public:
12. void print(){
13. cout << "Specialized template object" << endl;
14. }
15. };
另外,与函数模板不同,类模板的特例化不必为所有模板参数提供实参。我们可以只指定一部分而非所有模板参数,这种叫做类模板的偏特化 或部分特例化(partial specialization)。例如,C++标准库中的类vector的定义:
[cpp] view plain copy
1. template <typename T, typename Allocator>
2. class vector
3. {
4. /*......*/
5. };
6.
7. // 部分特例化
8. template <typename Allocator>
9. class vector<bool, Allocator>
10. {
11. /*......*/
12. };
在vector这个例子中,一个参数被绑定到bool类型,而另一个参数仍未绑定需要由用户指定。注意,一个类模板的部分特例化版本仍然是一个模板,因为使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。