前言
感谢以上作者的整理,以下内容都是结合了我自己的一些理解。
一、基础篇
1.1 面向对象基本特征
封装,继承,多态。
封装
定义:就是隐藏对象的属性和实现细节,仅对外公开接口(method),控制在程序中属性的读和修改的访问级别(public/protected/private)。
目的:封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是要通过外部接口,以特定的访问权限来使用类的成员。
继承
是面向对象的基本特征之一,继承机制允许创建分等级层次的类。
定义:继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
注意:C++支持多重继承(java只支持 单继承),可能会导致菱形继承。
多态
定义:多态同一个行为具有多个不同表现形式或形态的能力。是指一个类实例(对象)的相同方法在不同情形有不同表现形式。
(主要体现在重写和重载)
几种具体的表现
重写
子类继承父类后对父类方法进行重新定义。
重写和覆盖的区别
覆盖:基类没有virtual 派生类有基类同名且同参数返回值的函数
重载
对已有方法的参数类型和数量的改变。
覆盖
基类没有virtual,派生类有基类同名且同参数返回值的函数
上转型(子类转父类)
父类引用指向子类对象。
正确用法:
Parent* pParent = new Child;
Child child; Parent* pParent = (Parent*)child;
错误用法
Child* pChild= new Parent;
二、基础面试问题
2.1 delete和free的异同
相同点
都是释放内存
区别点
1.delete会先调用析构再释放内存,free只会释放内存。
2.2 new和malloc的异同
相同点
区别点
- 申请的内存所在位置
new:自由存储区(free store)上为对象动态分配内存空间,
malloc:从堆上动态分配内存。
自由存储区:C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。
堆:操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。
自由存储区是否是堆:需要看new的实现,自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。
- 返回类型安全性
new返回标准类型指针。
malloc返回void*
- 内存分配失败时的返回值
new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
- 是否需要指定内存大小
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
- 是否调用构造函数/析构函数
- 对数组的处理
C++提供了new[]与delete[]来专门处理数组类型:
A * ptr = new A[10];//分配10个A对象
使用new[]分配的内存必须使用delete[]进行释放:
delete [] ptr;
new对数组的支持体现在它会分别调用构造函数函数初始化每一个数组元素,释放对象时为每个对象调用析构函数。注意delete[]要与new[]配套使用,不然会找出数组对象部分释放的现象,造成内存泄漏。
至于malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小:
int * ptr = (int *) malloc( sizeof(int) );//分配一个10个int元素的数组
- new与malloc是否可以相互调用
- 是否可以被重载
delete操作符可以被重载,但同时要重载new
free是函数,不可以操作符重载,但可以写同名函数,重载函数。 - 能够直观地重新分配内存
- 客户处理内存分配不足
注意:delete一般是不可以释放malloc的资源,free和new一样。
但对于简单类型单单释放是不会报错的,对于对象类型的话,可能会存在问题。
2.3 野指针和内存泄漏
野指针
定义:指针指向未知内存,导致访问越界/非法访问等问题。
几种情况:
- 指针没初始化
- 指针指向的内存释放后,指针没置空
- 指针操作超越了变量的作用范围
内存泄漏
定义:堆区内存(new/malloc)没有释放,导致运行时内存增加,直到一定程度导致程序崩溃。
内存泄漏场景
- malloc和free未成对出现;new/new []和delete/delete []未成对出现;
1>在堆中创建对象分配内存,但未显式释放内存;比如,通过局部分配的内存,未在调用者函数体内释放:
2>在构造函数中动态分配内存,但未在析构函数中正确释放内存;\ - 未定义拷贝构造函数或未重载赋值运算符,从而造成两次释放相同内存的做法;比如,类中包含指针成员变量,在未定义拷贝构造函数或未重载赋值运算符的情况下,编译器会调用默认的拷贝构造函数或赋值运算符,以逐个成员拷贝的方式来复制指针成员变量,使得两个对象包含指向同一内存空间的指针,那么在释放第一个对象时,析构函数释放该指针指向的内存空间,在释放第二个对象时,析构函数就会释放同一内存空间,这样的行为是错误的;
- 没有将基类的析构函数定义为虚函数。
2.4 构造函数
2.4.1 构造函数是否可以是虚函数——否
2.4.2 构造/析构函数是否占据内存空间
对象的大小是指在类实例化出的对象当中,他的数据成员所占据的内存大小,而不包括成员函数,所以不占用。
2.5 静态链接和动态链接有什么区别?
- 静态链接:是在编译链接时直接将需要的执行代码拷贝到调用处;
优点:在于程序在发布时不需要依赖库,可以独立执行,
缺点:在于程序的 体积会相对较大,而且如果静态库更新之后,所有可执行文件需要重新链接;- 动态链接:是在编译时不直接拷贝执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定代码时,在共享执行内存中寻找已经加载的动态库可执行代码,实现运行时链接;
优点:在于多个程序可以共享同一个动态库,节省资源;
缺点:在于由于运行时加载,可能影响程序的前期执行性能。
2.6 常见的拷贝
深拷贝和浅拷贝
- 浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享一块内存;
- 深拷贝会创造一个相同的对象,新对象与原对象不共享内存,修改新对象不会影响原对象。
拷贝构造函数
拷贝赋值操作符重载
2.7 字节对齐
字节对齐基本原则
结构体内最大长度的变量为一次访问长度
2.8 sizeof std::string
sizeof(std::string)的结果 可能是 4、12,32\28。
string的实现在各库中可能有所不同,但是在同一库中相同一点是,无论你的string里放多长的字符串,它的sizeof()都是固定的,字符串所占的空间是从堆中动态分配的,与sizeof()无关。
sizeof(string) == 4 可能是最为典型的实现之一,
不过也有sizeof()为 12,32 字节的库,
同时也与编译器有关,在windows 32位操作系统下, 使用vs2013/vs2015编译器测试,sizeof(string) == 28。
2.9 元编程和泛型编程区别
元编程目的:代码生成代码
泛型编程目的:减小代码对特定数据类型的依赖
c++的模板元编程恰巧能同时做到。
2.10 类型转换相关
为什么不使用C的强制转换?
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
static_cast
用于基本数据类型之间的转换、子类向父类的安全转换、void*和其他类型指针之间的转换。
- 比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
const_cast
用于去除const或volatile属性;
dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。
因为编译器默认向上转换总是安全的,而向下转换时,dynamic_cast具有类型检查的功能;
dynamic_cast转换失败时,对于指针会返回目标类型的nullptr,对于引用会返回bad_cast异常;
- 向上转换:指的是子类向基类的转换
- 向下转换:指的是基类向子类的转换
reinterpret_cast
用于不同类型指针之间、不同类型引用之间、指针和能容纳指针的整数类型之间的转换。
- 几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
2.11 memcpy()、 memmove()和memccpy()
memcpy
void *memcpy( void *dst, const void *src, size_t count );
功能:拷贝src地址 count字节 到dst地址
memmove
void *memmove( void *dst, const void *src, size_t count );
功能:memmove用于拷贝字节,如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中,但复制后源内容会被更改。但是当目标区域与源区域没有重叠则和memcpy函数功能相同。
memccpy
void *memccpy( void *dest, void *src, unsigned char c, unsigned int count );
功能:由src所指内存区域复制不多于count个字节到dest所指内存区域,如果遇到字符c则停止复制。
返回值:如果c没有被复制,则返回NULL,否则,返回一个指向紧接着dest区域后的字符的指针。
区别与相同
这三个函数的功能均是将某个内存块复制到另一个内存块,唯一的区别是,当内存发生局部重叠的时候,memmove保证拷贝的结果是正确的,memcpy不保证拷贝的结果的正确。
内存块重叠问题
拷贝的目的地址在源地址的范围内,有重叠。
例如:
char s[32] = "abcdefg"; char* p = s; p++: strcpy(p, s);
2.12 类模板和模板类
类模板 :通过模板实现的通用类
template <class 类型参数> class 类名{ 类成员声明 };
模板类:模板类是类模板实例化后的一个产物,就是传入泛型之后最终生成的类。
2.13 字符串问题
sizeof和strlen 对于常量字符串
char []会自动在末尾添加’\0’,
sizeof是加上’\0’的大小。
strlen则是判断字符串第一个’\0’的位置 之前都是他的长度。
int main() { const char str1[] = "123"; const char str2[] = { "123" }; printf("str1 sizeof %d,strlen %d \n", sizeof(str1),strlen(str1)); printf("str2 sizeof %d,strlen %d \n", sizeof(str1), strlen(str1)); system("pause"); return 0; }
2.14 数组问题
数组越界不报错
int main2() { int i; int a[5] = {0}; for (i = 0; i <= 30; i++) { a[i] = 0; printf("a[%d]:%d\n",i, a[i]); } return 0; }
这段代码结果就是:无限循环输出。
原因:
- 数组越界不报错
只会警告:C6201:索引"30"超出了“O"至"4"的有效范围(对于可能在堆栈中分配的缓冲区"a")。 - 越界访问会访问 修改了i值
a内存空间 从0x00CFF7E0到 0x00CFF7F2 20个字节 5个Int长度
i为0x00CFF7FC即位置
越界访问会访问到i值,同时a[i] = 0;会把i置空为0,导致i=0;所以会一直循环。
2.15 共用体
- 当说明一个共用体变量时,系统分配给它的内存的大小是:
- 当最大的变量的大小 <= 最大类型的大小, 是最大类型的大小。
union TestUnion { int a;// 4 byte float b;//4 byte char c;//1 byte }; 输出结果是:4
- 当最大的变量的大小 >= 最大类型的大小,是最大类型的整数倍。(要考虑内存对齐)
union TestUnion { int a;// 4 byte float b;//4 byte char c[12];//12 byte double d;//8 byte }; 输出结果是:16