2018-11-05 C++常考笔试面试题
几种语言的特性
汇编程序:将汇编语言源程序翻译成目标程序
编译程序:将高级语言源程序翻译成目标程序
解释程序:将高级语言源程序翻译成机器指令,边翻译边执行
Java语言:半编译半解释,跨平台
面向对象和面向过程
面向对象:面向主要是以目标对象为研究体,这一思想的实现需要对各种不同属性的类进行封装,进而分析每种类型事物的属性和功能方法,
这种思想将计算机软件系统与外界系统一一对应,进行有针对性的研究。核心在于 (对象 + 消息)
面向过程:C语言是面向过程的编程语言,这种思想主要是为了去实现某种功能或目标去一步步研究算法流程,步步求精,
进而用一种最为简捷的过程来实现最终的目标,核心为 (算法+数据)
C++语言的优势
https://www.cnblogs.com/ckings/p/3632997.html
1.代码可读性好。
2.可重用性好。
3.可移植。
4.C++设计成无需复杂的程序设计环境
5.运行效率高,高效安全
6.语言简洁,编写风格自由。
7.提供了标准库stl
8.面向对象机制
9.很多优秀的程序框架包括Boost、Qt、MFC、OWL、wxWidgets、WTL就是使用的C++。
感觉在面试时说C++面向对象的三大特性,以及STL库,优秀的程序框架即可。
main主函数运行之前先会干什么
- 全局对象的构造函数会在main 函数之前执行,
全局对象的析构函数会在main函数之后执行;
用atexit注册的函数也会在main之后执行。 - 一些全局变量、对象和静态变量、对象的空间分配和赋初值就是在执行main函数之前,而main函数执行完后,还要去执行一些诸如释放空间、释放资源使用权等操作
- 进程启动后,要执行一些初始化代码(如设置环境变量等),然后跳转到main执行。全局对象的构造也在main之前。
https://blog.csdn.net/huang_xw/article/details/8542105
C++源码到可执行程序的过程
https://www.cnblogs.com/smile233/p/8423015.html
这篇博客讲的太详细了,我觉得实际可以精简为:
源代码-->预处理-->编译-->汇编-->链接-->可执行文件
- 预处理
(1)宏定义指令,如#define Name TokenString,#undef等。对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。
(2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif,等等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉
(3)头文件包含指令,如#include "FileName"或者#include <FileName>等。在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/usr/include目录下。在程序中#include它们要使用尖括号。(<>)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号("")。
(4)特殊符号,预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。 - 编译
编译过程就是把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。经过预编译得到的输出文件中,将只有常量。如数字、字符串、变量的定义,以及C语言的关键字,如main,if,else,for,while,{,},+,-,*,\,等等。编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。 - 汇编
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。 - 链接
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
通过调用链接器ld来链接程序运行需要的一大堆目标文件,以及所依赖的其它库文件,最后生成可执行文件。
内存分区
https://www.cnblogs.com/madonion/articles/2269195.html
分为五大块:栈,堆,全局区,常量区和代码区
- 栈区
由系统进行内存的管理,主要存放函数的参数以及局部变量。栈区由系统进行内存管理,在函数完成执行,系统自行释放栈区内存,不需要用户管理。整个程序的栈区的大小可以在编译器中由用户自行设定,默认的栈区大小为3M。 - 全局/静态区
初始化的全局变量和静态变量是在一起的。未初始化的全局变量和静态变量是在相邻的空间中。全局变量和静态全局变量的存储方式是一致的,但是其区别在于,全局变量在整个源代码中都可以使用,而静态全局变量只能在当前文件中有效。比如我们的一个程序有5个文件,那么某个文件中申请了静态全局变量,这个静态全局变量只能在当前文件中使用,其他四个文件均不可以使用。而某个文件中申请了全局变量,那么其他四个文件中都可以使用该全局变量(只需要通过关键字extern申明一下就可以使用了)。事实上static改变了变量的作用范围。 - 字符串常量区
存放字符串常量,程序结束后,由系统进行释放。比如我们定义const char * p = “Hello World”; 这里的“Hello World”就是在字符串常量中,最终系统会自动释放。 - 代码区:存放程序体的二进制代码。比如我们写的函数,都是在代码区的。
- 堆区:由用户手动申请,手动释放。在C中使用malloc,在C++中使用new(当然C++中也可以使用malloc)。
new操作符本质上还是使用了malloc进行内存的申请
1)malloc是C语言中的函数,而new是C++中的操作符。
2)malloc申请之后返回的类型是void*,而new返回的指针带有类型。
3)malloc只负责内存的分配而不会调用类的构造函数,而new不仅会分配内存,而且会自动调用类的构造函数。
堆和栈的区别
- 管理方式:栈是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生内存泄漏。
- 空间大小:堆内存比栈大得多。
- 能否产生碎片:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因 为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出。
- 生长方向:堆生长方向是向上的,也就是向着内存地址增加的方向;栈的生长方向是向下的,是向着内存地址减小的方向增长。
- 分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
- 分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内 存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到 足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
栈溢出
当我们定义的数据所需要占用的内存超过了栈的大小时,就会发生栈溢出。
https://blog.csdn.net/dongtuoc/article/details/79132137
内存泄露和内存碎片
1. 内存泄漏
内存泄漏一般是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。如果没有及时释放,那么这一块内存便不能使用,从而造成内存泄漏。
2. 内存碎片
内存碎片一般是由于空闲的连续空间比要申请的空间小,导致这些小内存块不能被利用。
假设有一块100个单位的连续空闲内存空间,范围是0-99。从中申请一块内存,10个单位,那么申请出来的内存块就为0-9区间。继续申请一块内存,5个单位,第二块得到的内存块就应该为10~14区间。
如果你把第一块内存块释放,然后再申请一块20个单位的内存块。因为刚被释放的内存块不能满足新的请求,所以只能从15开始分配出20个单位的内存块。现在整个内存空间的状态是0-9空闲,10-14被占用,15-24被占用,25-99空闲。其中0-9就是一个内存碎片了。如果10-14一直被占用,而以后申请的空间都大于10个单位,那么0-9就永远用不上了,造成内存浪费。
内存池
https://www.cnblogs.com/pilipalajun/p/5418286.html
内存池是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。
同步和异步
同步:在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
异步。
异步:当一个异步过程调用发出后,调用者不会立刻得到结果。实际处理这个调用的部件是在调用发出后,通过状态、通知来通知调用者,或通过回调函数处理这个调用。异步的实现方式有两种:通过多进程和timmer。
C++常考关键字
https://blog.csdn.net/cc_clear/article/details/76943425
auto
auto申明的变量必须初始化,程序会根据初始化的值的数据类型来自动确定该变量的数据类型。
const
https://www.cnblogs.com/xudong-bupt/p/3509567.html
- const修饰变量
int a1=3; - const修饰指针
int * const p1 = &a1; - const修饰指针指向的变量
const int * p1 = &a1; 等价于 int const * p1 = &a1;
int const*p; 等价于 const int *p; 都表示指针p指向的是一个常量
int *const p; 表示指针p是一个常量,不能更改指向 - const修饰函数
int func () const ;
表示该成员函数不能修改任何成员变量,除非成员变量用mutable修饰。const修饰的成员函数也不能调用非const修饰的成员函数,因为可能引起成员变量的改变。
static
https://www.cnblogs.com/songdanzju/p/7422380.html
- 隐藏:可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。
- 保持变量内容的持久:存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。
- 默认初始化为0:在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置0,然后把不是0的几个元素赋值。如果定义成静态的,就省去了一开始置0的操作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加‘\0’;太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是‘\0’;
- C++中的类成员声明static
主要用于所有对象共享,详见后续的静态成员变量和静态成员函数。
宏定义(#define)
宏定义,#define命令是C语言中的一个宏定义命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。
该命令有两种格式:一种是简单的宏定义,另一种是带参数的宏定义。
(1) 简单的宏定义:#define <宏名> <字符串>
例: #define PI 3.1415926
(2) 带参数的宏定义:#define <宏名> (<参数表>) <宏体>
例如:#define max(x,y) (x)>(y)?(x):(y)
inline
https://blog.csdn.net/qq_33757398/article/details/81390151
inline一般用于内联函数的声明和定义,内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来代替。在函数声明和定义前加上关键字inline,若代码执行时间很短,且代码块比较短小,则可以使用内联函数。
final
https://blog.csdn.net/u012333003/article/details/28696521
C++11中允许将类标记为final,方法时直接在类名称后面使用关键字final,如此,意味着继承该类会导致编译错误。 class Super final {};
C++中还允许将方法标记为final,这意味着无法再子类中重写该方法。这时final关键字至于方法参数列表后面: virtual void SomeMethod() final;
volatile
https://blog.csdn.net/k346k346/article/details/46941497
定义为volatile的变量是说这变量可能会被意想不到地改变,即在你程序运行过程中一直会变,你希望这个值被正确的处理,每次从内存中去读这个值,而不是因编译器优化从缓存的地方读取,比如读取缓存在寄存器中的数值,从而保证volatile变量被正确的读取。
在单任务的环境中,一个函数体内部,如果在两次读取变量的值之间的语句没有对变量的值进行修改,那么编译器就会设法对可执行代码进行优化。由于访问寄存器的速度要快过RAM(从RAM中读取变量的值到寄存器),以后只要变量的值没有改变,就一直从寄存器中读取变量的值,而不对RAM进行访问。
而在多任务环境中,虽然在一个函数体内部,在两次读取变量之间没有对变量的值进行修改,但是该变量仍然有可能被其他的程序(如中断程序、另外的线程等)所修改。如果这时还是从寄存器而不是从RAM中读取,就会出现被修改了的变量值不能得到及时反应的问题。
(1)并行设备的硬件寄存器(如状态寄存器);
(2)一个中断服务子程序中访问到的变量;
(3)多线程应用中被多个任务共享的变量。
mutable
mutalbe的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量(mutable只能由于修饰类的非静态数据成员),将永远处于可变的状态,即使在一个const函数中。
详见 https://www.cnblogs.com/xkfz007/articles/2419540.html
explicit
只能修饰构造函数,防止单参数的构造函数隐式类型转换,把一个常量转换成一个对象。
在没有加explicit之前,可以把一个常量赋给一个对象。
推荐构造函数前最好加explict
size_t
size_t是一些C/C++标准在stddef.h中定义的,这个类型足以用来表示对象的大小,sizeof操作符的结果类型是size_t
size_t的真实类型与操作系统有关,size_t在32位架构上是4字节,在64位架构上是8字节,而int在不同架构下都是4字节,且int为带符号数,size_t为无符号数。
sizeof
sizeof(类型名)或者sizeof 表达式,返回的是类型或者表达式占用字节的大小。
宏和内联函数
先说宏和函数的区别:
- 宏做的是简单的字符串替换(注意是字符串的替换,不是其他类型参数的替换),而函数是参数的传递,参数是有数据类型的,可以是各种各样的类型。
- 宏的参数替换是不经计算而直接处理的,而函数调用是将实参的值传递给形参,既然说是值,自然是计算得来的.
- 宏在编译之前进行,即先用宏体替换宏名,然后再编译的,而函数显然是编译之后,在执行时才调用的。因此,宏占用的是编译的时间,而函数占用的是执行时的时间。
- 宏的参数是不占内存空间的,因为只是做字符串的替换,而函数调用时的参数传递则是具体变量之间的信息传递,形参作为函数的局部变量,显然是占用内存的。
- 函数的调用是需要付出一定的时空开销的,因为系统在调用函数时,要保留现场,然后转入被调用函数去执行,调用完,再返回主调函数,此时再恢复现场,这些操作,显然在宏中是没有的。
https://blog.csdn.net/qq_33757398/article/details/81390151
内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来代替。在函数声明和定义前加上关键字inline,若代码执行时间很短,且代码块比较短小,则可以使用内联函数。
内联函数与宏的区别:
- 内联函数在运行时可调试,而宏定义不可以;
- 编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义则不会;
- 内联函数可以访问类的成员变量,宏定义则不能;
- 在类中声明同时定义的成员函数,自动转化为内联函数。
原码,反码,补码
正整数,三者一样
负整数 -1100110,原码 11100110(用1表示符号位),反码 10011001(符号位不变,原码按位取反),补码 10011010(反码加1)
按位运算符用法
- 按位与(&)
将变量某个位置零:a = a & 0xfe
取出int型a的低字节:c = a & 0xff - 按位或(|)
将int型变量a的低字节置1:a = a | 0xff - 按位异或(^)
使01111010低四位翻转:01111010 ^ 00001111 = 01110101
还可以用于两个变量a和b的交换:a = a ^ b; b = b ^ a; a = a ^ b;
当然,变量的交换还可以用加减法:a = a + b; b = a - b; a = a - b;
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是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。 - 重载
C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。 - 内存区域
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
关于自由存储区与堆的区别:https://www.cnblogs.com/QG-whz/p/5060894.html
指针和引用的区别
引用是一个变量的别名,不能为空,必须在声明的时候初始化,而且之后不能修改为其他的变量的别名;
指针的值是一块内存的地址,可以为空,可以先声明后初始化,后面可以修改其指向的内存地址。
野指针
野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。
- 指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
- 指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。别看free和delete的名字恶狠狠的(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。通常会用语句if (p != NULL)进行防错处理。很遗憾,此时if语句起不到防错作用,因为即便p不是NULL指针,它也不指向合法的内存块。释放内存后必须把指针指向NULL,防止指针在后面不小心又被解引用了。具体参考博文:https://blog.csdn.net/dangercheng/article/details/12618161
- 指针超过了变量的作用范围。即在变量的作用范围之外使用了指向变量地址的指针。这一般发生在将调用函数中的局部变量的地址传出来引起的。局部变量的作用范围虽然已经结束,内存已经被释放,然而地址值仍是可用的,不过随时都可能被内存管理分配给其他变量。
因此指针的使用需要注意:
a. 函数的返回值不能是指向栈内存的指针或引用,因为栈内存在函数结束时会被释放.
b. 在使用指针进行内存操作前记得要先给指针分配一个动态内存。
c. 声明一个指针时最好初始化,指向NULL或者一块内存。
智能指针
https://www.cnblogs.com/wxquare/p/4759020.html
使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等。C++11中引入了智能指针的概念,能更好的管理堆内存。
深拷贝和浅拷贝
https://blog.csdn.net/libin66/article/details/53140284
浅拷贝是只对指针进行拷贝,两个指针指向同一个内存块,深拷贝是对指针和指针指向的内容都进行拷贝,拷贝后的指针是指向不同内的指针。
形参和实参
概念
- 形参
形参出现在函数定义的地方,多个形参之间以逗号分隔,形参规定了一个函数所接受数据的类型和数量。
形参和函数体内部定义的变量统称为局部变量,仅在函数的作用域内可见,同时局部变量还会隐藏在外层作用域中同名的其他所有声明(局部变量和全局变量可以重名) - 实参
出现在函数调用的地方,实参的数量与类型与形参一样,实参用于初始化形参。
实参与形参值传递的方式
- 值传递
在值传递过程中,实参和形参位于内存中两个不同地址中,参数传递的实质是将原函数中变量的值,复制到被调用函数形参所在的存储空间中,这个形参的地址空间在函数执行完毕后,会被回收掉。整个被调用函数对形参的操作,只影响形参对应的地址空间,不影响原来函数中的变量的值,因为这两个不是同一个存储空间。 - 地址传递(指针传递)
这种参数传递方式中,实参是变量的地址,形参是指针类型的变量,在函数中对指针变量的操作,就是对实参(变量地址)所对应的变量的操作,,函数调用结束后,原函数中的变量的值将会发生改变。 - 引用传递
这种参数传递方式中,形参是引用类型变量,其实就是实参的一个别名,在被调用函数中,对引用变量的所有操作等价于对实参的操作,这样,整个函数执行完毕后,原先的实参的值将会发生改变。
如果在实参前加上const关键字修饰,则引用传递可以不改变实参的值,既达到了传值的目的,提高了效率,还保证了原实参不会被修改。
https://blog.csdn.net/u012677715/article/details/73825856
https://www.cnblogs.com/kane0526/p/3913284.html
https://www.cnblogs.com/tanjuntao/p/8678927.html
类与结构体
区别:类的缺省访问权限是private,结构体的缺省访问权限是public。
一般在定义用来保存数据而没有什么操作的类型时使用结构体。
sizeof 类或者结构体
https://blog.csdn.net/EVEcho/article/details/81115683
http://www.cnblogs.com/0201zcr/p/4789332.html
默认情况下:
- 结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)
- 结构体大小必须是所有成员大小的整数倍,也即所有成员大小的公倍数。
如果是使用了#pragma pack (n)指令指定对齐字节数,则取n和sizeof(类型)的较小值作为对齐字节。
#include<iostream>
#pragma pack (2)
using namespace std;
struct TEST
{
char a;
int b;
short c;
};
int main() {
cout << sizeof(TEST);
return 0;
}
#pragma pack (1)时输出7
#pragma pack (2)时输出8
#pragma pack (2)时输出12
面向对象三大特性
- 封装
将抽象出的数据成员、代码成员相结合,将它们视为一个整体。使用者不必了解具体的实现细节,只需要通过外部接口,以特定的访问权限使用类的成员。 - 多态
同一名称,不同的功能实现方式。达到行为标识统一,减少程序中标识符的个数。
编译时的多态:重载
运行时的多态:虚函数 - 继承
基类与派生类,父类与子类的关系
继承的三种方式
- 公有继承(public)
访问控制:基类的public和protected成员访问属性在派生类中保持不变,private成员不可直接访问。
访问权限:派生类中的成员函数可以访问基类中的public和protected成员,不能直接访问基类的private成员。派生类的对象只能访问public成员。 - 私有继承(private)
访问控制:基类的public和protected成员都以private身份出现在派生类中,private成员不可直接访问。
访问权限:派生类的成员函数可以直接访问基类中的public和protected成员,不能直接访问private成员。派生类的对象不能直接访问从基类继承的任何成员。 - 保护继承(protected)
访问控制:基类的public和protected成员都以protected身份出现在派生类中,private成员不可直接访问。
访问权限:派生类的成员函数可以直接访问基类中的public和protected成员,不能直接访问private成员。派生类的对象不能直接访问从基类继承的任何成员。
对于类的对象来说,protected成员与private成员性质相同,都不能通过对象直接访问,对于派生类成员来说,基类的protected成员与public成员性质相同。
基类与派生类转换
https://blog.csdn.net/Jaihk662/article/details/79826056
公有派生类对象可以被当作基类的对象使用,反之则不可
派生类的对象可以隐含转换为基类对象
派生类的对象可以初始化基类的引用
派生类的指针可以隐含转换为基类的指针
通过基类对象名,指针只能使用从基类继承的成员
#include<iostream>
using namespace std;
class BaseA
{
public:
BaseA(int x, int y)
{
this->x = x;
this->y = y;
}
void Disp()
{
printf("x = %d y = %d\n", x, y);
}
private:
int x, y;
};
class B : public BaseA
{
public:
B(int x, int y, int z) : BaseA(x, y), z(z) {}
void Disp()
{
BaseA::Disp();
printf("z = %d\n", z);
}
private:
int z;
};
void Print1(BaseA &Q) { Q.Disp(); }
void Print2(B &Q) { Q.Disp(); }
void Print3(BaseA Q) { Q.Disp(); }
void Print4(B Q) { Q.Disp(); }
int main()
{
BaseA a(3, 4);
a.Disp();
B b(10, 20, 30);
b.Disp(); puts("");
BaseA * pa;
B * pb;
pa = &a;
pa->Disp();
pb = &b;
pb->Disp(); puts("");
pa = &b; //pa为基类指针,指向派生类对象是合法的,因为派生类对象也是基类对象
pa->Disp(); puts(""); //这里调用的是基类的Disp(),如果要实现调用派生类的Disp函数,需要用虚函数实现多态性
// pb = &a; //非法,派生类指针不能指向基类对象
Print1(b); //因为派生类对象也是基类对象,所以可以将派生类对象赋值给基类引用,函数仍然会调用基类的Disp()
Print2(b);
Print3(b); //派生类对象赋值给基类对象时,派生类对象基类部分被复制给形参
Print4(b); puts("");
Print1(a);
Print3(a); puts("");
// Print2(a); Print4(a); //非法,不能用基类对象给派生类引用赋值,也不能用基类对象给派生类对象赋值
//pb = pa; //非法,不能用基类指针给派生类指针赋值
pa = &a;
pb = (B*)pa; //可以强制转换,但是非常不安全
pb->Disp(); //a中没有p成员,而这里会调用派生类的Disp(),导致输出p为垃圾值
return 0;
}
虚基类
当派生类从多个基类派生,而这些基类又有共同基类,则在访问共同基类成员时,会产生冗余。因而需要在继承基类的时候将基类声明为虚基类:
class B1: virtual public B
这样就可以为最远的派生类提供唯一的基类成员,不产生多次复制。在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,其他类对虚基类构造函数的调用被忽略。
重载,重写和重定义
https://www.cnblogs.com/weizhixiang/articles/5760286.html
- 成员函数重载特征:
a 相同的范围(在同一个类中)
b 函数名字相同
c 参数不同
d virtual关键字可有可无
e 不能重载的运算符 ".", ".*", "::", "?:" - 重写(覆盖)是指派生类函数覆盖基类函数,特征是:
a 不同的范围,分别位于基类和派生类中
b 函数的名字相同
c 参数相同
d 基类函数必须有virtual关键字
e 方法被定义为final不能被重写 - 重定义(隐藏)是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
a 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。
b 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏。
函数重载 变量前有无const是否可以重载
https://www.cnblogs.com/qingergege/p/7609533.html
- fun(int i) 和 fun(const int i),不能重载
二者是一样的,是因为函数调用中存在实参和形参的结合。假如我们用的实参是int a,那么这两个函数都不会改变a的值,这两个函数对于a来说是没有任何区别的,所以不能通过编译,提示重定义。 - fun(char *a) 和 fun(const char *a),可以重载
因为char *a 中a指向的是一个字符串变量,而const char *a指向的是一个字符串常量,所以当参数为字符串常量时,调用第二个函数,而当函数是字符串变量时,调用第一个函数。 - char *a和char * const a,不能重载
这两个都是指向字符串变量,不同的是char *a是指针变量 而char *const a是指针常量,这就和int i和const int i的关系一样了,所以也会提示重定义。 - 对于引用,比如int &i 和const int & i 也是可以重载的,原因是第一个 i 引用的是一个变量,而第二个 i 引用的是一个常量,两者是不一样的,类似于上面的指向变量的指针的指向常量的指针。
虚函数
- 虚函数的原理
https://www.cnblogs.com/malecrab/p/5572730.html
http://www.cnblogs.com/malecrab/p/5572119.html
虚函数的动态绑定实现机制:
只有通过基类的引用或者指针调用虚函数时,才能发生动态绑定,如果使用对象来操作虚函数的话,仍然会采用静态绑定的方式。因为引用或者指针既可以指向基类对象,也可以指向派生类对象的基类部分。绝对不要重新定义一个继承而来的virtual函数的缺省参数值,因为缺省参数值都是静态绑定(为了执行效率),而virtual函数却是动态绑定。 - 虚函数的使用
https://blog.csdn.net/LC98123456/article/details/81143102
https://www.jianshu.com/p/f85bd1728bf0
虚函数是声明时前面加了virtual关键字的函数,作用是实现运行时动态绑定。一般有两种函数会声明为虚函数,一种是基类的析构函数,另一种是在派生类重写了基类的普通成员函数,而且使用了一个基类指针调用该成员函数,要想调用派生类对象的同名成员函数,则需要将基类的该成员函数声明为虚函数。
a. 虚析构函数
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "create Base " << endl;
}
virtual ~Base()
{
cout << "delete Base" << endl;
}
};
class Inherit :public Base
{
public:
Inherit()
{
cout << "create Inherit" << endl;
}
~Inherit()
{
cout << "delete Inherit" << endl;
}
};
int main()
{
Base *p;
p = new Inherit;
delete p;
return 0;
}
运行结果:
Base count
Inherit count
Inherit descout
Base descount
上面的代码我将指针的声明和初始化拆开成两行,调试的时候发现声明的时候并没有调用基类的构造函数,在初始化的时候一次性调用了基类和派生类的构造函数,而delete p的时候就调用了派生类和基类的析构函数。有点蒙圈,哪位大佬可以帮我解释一下?
b. 基类虚函数成员:
#include <iostream>
using namespace std;
class A {
public:
A() {
ver = 'A';
}
void print() const {
cout << "The A version is: " << ver << endl;
}
protected:
char ver;
};
class D1 :public A {
public:
D1(int number) {
info = number;
ver = '1';
}
void print() const {
cout << "The D1 info: " << info << " version: " << ver << endl;
}
private:
int info;
};
class D2 :public A {
public:
D2(int number) {
info = number;
}
void print() const {
cout << "The D2 info: " << info << " version: " << ver << endl;
}
private:
int info;
};
int main() {
A a;
D1 d1(4);
D2 d2(100);
A *p = &a;
p->print();
p = &d1;
p->print();
p = &d2;
p->print();
system("pause");
return 0;
}
The A version is: A
The A version is: 1
The A version is: A
我们先看上述代码,派生类中重新定义了基类中的函数,我们在使用一个基类指针指向派生类的时候,本义想要调用派生类中的重定义函数,但是由于基类中此函数不是虚函数,因此指针会静态绑定此函数,结果就不是我们的本意。而如果我们将基类中的方法改成虚函数,如下:
virtual void print() const {
cout << "The A version is: " << ver << endl;
}
The A version is: A
The D1 info: 4 version: 1
The D2 info: 100 version: A
可以看到,将基类方法改成虚函数,那么就会动态绑定,在运行时才决定调用哪个函数。
补充:上面函数后面有个const修饰,表示该成员函数不能修改任何成员变量,除非成员变量用mutable修饰。const修饰的成员函数也不能调用非const修饰的成员函数,因为可能引起成员变量的改变。
mutalbe的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量(mutable只能由于修饰类的非静态数据成员),将永远处于可变的状态,即使在一个const函数中。
详见 https://www.cnblogs.com/xkfz007/articles/2419540.html
纯虚函数和抽象类
https://blog.csdn.net/qq_36221862/article/details/61413619
纯虚函数的声明:
virtual 函数类型 函数名 (参数表列) = 0;
一般用于抽象类中,抽象类不可用于实例化。
复制构造函数(拷贝构造函数)
复制构造函数的作用是用一个已存在的对象去初始化同类型的新对象,形参为本类的对象引用。一般声明形式为:类名 (const 类名 &对象名);
其被调用的三种情况:
- 定义一个对象时,以本类的另一个对象作为初始值;
- 函数的形参是类的对象,调用函数时,将使用实参对象初始化形参对象;
- 如果函数的返回值是类的对象,函数执行完返回主调函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调函数,此时发生复制构造。
拷贝构造函数的参数为引用而不是值传递的原因:如果是值传递,那么在调用拷贝构造函数的时候会用实参对象初始化形参对象,而这个过程又满足第一个条件,则继续调用拷贝构造函数,这里面又会有实参初始化形参,继续下去就会无限循环调用。
静态变量,静态函数
https://www.cnblogs.com/secondtonone1/p/5694436.html
https://www.cnblogs.com/ppgeneve/p/5091794.html
- 静态局部变量和静态全局变量
静态局部变量具有局部作用域。它只被初始化一次,自从第一次初始化直到程序结束都一直存在,即它的生命周期是程序运行就存在,程序结束就结束,
静态全局变量具有全局作用域,他与全局变量的区别在于如果程序包含多个文件的话,他作用于定义它的文件里,不能作用到其他文件里,即被static关键字修饰过的变量具有文件作用域。 - 类的静态成员变量和静态成员函数
静态成员变量和静态成员函数主要是为了解决多个对象数据共享的问题,他们都不属于某个对象,而是属于类的。
静态成员变量定义的时候前面加static关键字,初始化必须在类外进行,前面不加static。其初始化格式如下:
<数据类型><类名>::<静态数据成员名>=<值> //静态变量的初始化
静态成员函数也是属于类的成员,不能直接引用类的非静态成员,如果静态成员函数中要引用非静态成员时,可通过对象来引用。静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);
全局变量,静态全局变量可以被其他文件调用么,为什么
https://blog.csdn.net/candyliuxj/article/details/7853938
https://www.cnblogs.com/invisible2/p/6905892.html
- 全局变量
在a.h中声明全局变量:extern int a; 一般需要加上extern,否则编译器可能默认给一个初始化值。那样会导致多个cpp文件在包含此头文件时发生重复定义。
在a.cpp文件中定义全局变量:int a =10;
在b.cpp中想要使用这个全局变量,有两种方法,第一种是使用extern关键字,例如extern int a; 代表当前变量a 的定义来自于其他文件,当进行编译时,会去其他文件里面找。且在当前文件仅做声明,而不是重新定义一个新的变量。第二种方法是使用include "a.h",这种方法的好处是a里面的方法可以直接拿过来使用。
extern作用
作用一:当它与"C"一起连用时,如extern "C" void fun(int a, int b);,则编译器在编译fun这个函数名时按C的规则去翻译相应的函数名而不是C++的。
作用二:当它不与"C"在一起修饰变量或函数时,如在头文件中,extern int g_nNum;,它的作用就是声明函数或变量的作用范围的关键字,其声明的函数和变量可以在本编译单元或其他编译单元中使用。 - 静态全局变量(static)
注意使用static修饰变量,就不能使用extern来修饰,即static和extern不可同时出现。 static修饰的全局变量的声明与定义同时进行,即当你在头文件中使用static声明了全局变量,同时它也被定义了。
static修饰的全局变量的作用域只能是本身的编译单元。在其他编译单元使用它时,只是简单的把其值复制给了其他编译单元,其他编译单元会另外开个内存保存它,在其他编译单元对它的修改并不影响本身在定义时的值。即在其他编译单元A使用它时,它所在的物理地址,和其他编译单元B使用它时,它所在的物理地址不一样,A和B对它所做的修改都不能传递给对方。
多个地方引用静态全局变量所在的头文件,不会出现重定义错误,因为在每个编译单元都对它开辟了额外的空间进行存储。
友元函数和友元类
友元函数是在类声明中由关键字friend修饰的非成员函数,在它的函数体中能够通过对象名访问private和protected成员。
友元类是将一个类A声明为另一个类B的友元,那么类A的所有成员都可以访问类B中被隐藏的信息。一般是在类A中定义了一个B的对象,通过此对象访问类B的所有成员。
STL中vector和list
https://www.cnblogs.com/shijingjing07/p/5587719.html
vector是一片连续的内存空间,相当于数组。随机访问方便,插入和删除效率低。
list是不连续的内存空间,是双向链表。随机访问效率低,插入和删除方便。