目录
4 include头文件的顺序以及双引号""和尖括号<>的区别
14 说说静态局部变量,全局变量,局部变量的特点,以及使用场景
20 说说const int *a, int const *a, const int a, int *const a, const int *const a分别是什么,有什么特点。
4 程序有哪些section,分别的作用?程序启动的过程?怎么判断数据分配在栈上还是堆上?
8 说说 C++ 类对象的初始化顺序,有多重继承情况下的顺序
14 说说类继承时,派生类对不同关键字修饰的基类方法的访问权限
1.part1 C++基础
1 C++特点
1. C++
在
C
语言基础上引入了
面对对象
的机制,同时也
兼容
C
语言
。
2. C++
有三大特性
(1)封装。
(2)继承。
(3)多态;
3. C++
语言编写出的程序结构清晰、易于扩充,程序
可读性好
。
4. C++
生成的代码
质量高
,
效率高
,
5. C++
更加安全,增加了
const
常量、引用、四类
cast
转换(
static_cast
、
dynamic_cast
、
const_cast
、
reinterpret_cast
)、智能指针、
try—catch
等等;
6. C++
可复用性
高,
C++
引入了
模板
的概念,标准模板库
STL
(
Standard Template Library
)。
2 说说C语言和C++的区别
1. C
语言是
C++
的子集,
C++
可以很好兼容
C
语言。但是
C++
又有很多
新特性
,如引用、智能指针、
auto
变量等。
2. C++
是
面对对象
的编程语言;
C
语言是
面对过程
的编程语言。
3. C
语言有一些不安全的语言特性,如指针使用的潜在危险、强制转换的不确定性、内存泄露等。而
C++
对此增加了不少新特性来
改善安全性
,如
const
常量、引用、
cast
转换、智能指针、
try—catch
等等;
4. C++
可复用性
高,
C++
引入了
模板
的概念,后面在此基础上,实现了方便开发的标准模板库
STL
。
C++
的
STL
库相对于
C
语言的函数库
更灵活、更通用
。
3 说说 C++中 struct 和 class 的区别
1. struct
一般用于描述一个
数据结构集合
,而
class
是对一个
对象数据的封装
;
2. struct
中默认的访问控制权限
是
public
的
,而
class
中默认的访问控制权限
是
private
的
。
3.
在继承关系中,
struct
默认是
公有继承
,而
class
是
私有继承
;
4. class
关键字可以用于定义模板参数,就像
typename
,而
struct
不能用于定义模板参数,
4 include头文件的顺序以及双引号""和尖括号<>的区别
1.
区别:
(1)尖括号
<>
的头文件是
系统文件
,双引号
""
的头文件是
自定义文件
。
(2)编译器预处理阶段查找头文件的路径不一样。
2.
查找路径:
(1)使用尖括号
<>
的头文件的查找路径:编译器设置的头文件路径
-->
系统变量。
(2)使用双引号
""
的头文件的查找路径:当前头文件目录
-->
编译器设置的头文件路径
-->
系统变
量。
5 说说C++结构体和C结构体的区别
区别:
(1)
C
的结构体内
不允许有函数存在
,
C++
允许有内部成员函数,且允许该函数是虚函数
。
(2)
C
的结构体对内部成员变量的
访问权限只能是
public
,而
C++
允许
public,protected,private
三种
。
(3)
C
语言的结构体是
不可以继承的
,
C++
的结构体
可继承
。
(4)
C
中
使用结构体需要加上
struct
关键字
,而
C++
中可以省略
struct
关键字直接使用。
![](https://i-blog.csdnimg.cn/blog_migrate/78746e98b3ec9447463ba415c2fdb1bc.png)
6 导入C函数的关键字是什么,C++编译时和C有什么不同?
1.
关键字:
在
C++
中,导入
C
函数的关键字是
extern
,表达形式为
extern “C”
,
extern "C"
的主要作
用就是为了能够正确实现
C++
代码调用其他
C
语言代码。加上
extern "C"
后,会指示编译器这部分代
码按
C
语言
的进行编译,而不是
C++
的。
2.
编译区别:
由于
C++
支持函数重载,因此编译器编译函数的过程中会将函数的
参数类型
也加到编译
后的代码中,而不仅仅是
函数名
;而
C
语言并不支持函数重载,因此编译
C
语言代码的函数时不会带
上函数的参数类型,一般只包括
函数名
7 C++从代码到可执行二进制文件的过程
C++
和
C
语言类似,一个
C++
程序从源码到执行文件,有四个过程,
预编译、编译、汇编、链接
。
预编译:
这个过程主要的处理操作如下:
将所有的
#define
删除,并且展开所有的宏定义处理所有的条件预编译指令,如#if
、
#ifdef 处理#include
预编译指令,将被包含的文件插入到该预编译指令的位置。 过滤所有的注释 添加行号和文件名标识。
编译:
这个过程主要的处理操作如下:
词法分析:将源代码的字符序列分割成一系列的记号。 语法分析:对记号进行语法分析,产生语法树。 语义分析:判断表达式是否有意义。 代码优化。 目标代码生成:生成汇编代码。 目标代码优化。
汇编:
这个过程主要是将汇编代码转变成机器可以执行的指令。
链接:
将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。 链接分为
静态链接和动态链接
。
静态链接
,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你在去 把静态库删除也不会影响可执行程序的执行;生成的静态链接库,Windows
下以
.lib
为后缀,
Linux
下 以.a
为后缀。
动态链接
,是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函 数,生成的可执行文件中没有函数代码,只包含函数的重定位信息,所以当你删除动态库时,可执行程 序就不能运行。生成的动态链接库,Windows下以
.dll
为后缀,
Linux
下以
.so
为后缀。
8 说说 static关键字的作用
1.
定义全局静态变量和局部静态变量
:初始化的静态变量会在数据段分配内存,未初始化的静态变量 会在BSS
段分配内存。直到程序结束,静态变量始终会维持前值。
2.
定义静态函数
:静态函数只能在
本源文件
中使用;如
static void func()
;
3.
定义静态变量
。
静态变量只能在本源文件中使用
;
4.
定义
类中的静态成员变量
:使用静态数据成员,它既可以被当成全局变量那样去存储,但又被隐藏 在类的内部。类中的static
静态数据成员拥有一块单独的存储区,而不管创建了多少个该类的对
象。所有这些对象的静态数据成员都
共享
这一块静态存储空间。
5.
定义类中的静态成员函数
:如静态成员函数也是类的一部分,而不是对象的一部分。所有这些对象 的静态数据成员都共享
这一块静态存储空间。
此外: 当调用一个对象的非静态成员函数时,系统会把该对象的起始地址赋给成员函数的this
指针。而
静态成
员函数不属于任何一个对象,因此
C++
规定静态成员函数没有
this
指针
。既然它没有指向某一对象,也 就无法对一个对象中的非静态成员进行访问。
9 数组和指针的区别
1.
概念:
(1)数组:数组是用于
储存多个相同类型数据的集合
。
数组名是首元素的地址。
(2)指针:指针相当于一个
变量
,它存放的是其它变量在
内存中的地址
。 指针名指向了内存的首
地址。
2.
区别:
(1)
赋值
:同类型指针变量可以相互赋值;数组不行,只能一个一个元素的赋值或拷贝
(2)
存储方式
:
数组:数组在内存中是
连续存放的
,数组的存储空间,不是在静态区就是在栈上。
指针:指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。由
于指针本身就是一个变量,再加上它所存放的也是变量,所以指针的存储空间不能确定。
(3)
求
sizeof
:
数组所占存储空间的内存大小:
sizeof
(数组名)
/sizeof
(数据类型) 在32
位平台下,无论指针的类型是什么,
sizeof
(指针名)都是
4
,在
64
位平台下,无论指针的类型是什么,sizeof
(指针名)都是
8
。
10 说说什么是函数指针,如何定义函数指针,有什么使用场景
1.
概念:
函数指针就是
指向函数
的指针变量。每一个函数都有一个入口地址,该入口地址就是函数指
针所指向的地址。
2.
定义
形式如下:
int func(int a);
int (*f)(int a);
f = &func;
1.
函数指针的
应用场景
:
回调
(
callback
)。我们调用别人提供的
API
函数
(Application
Programming Interface,
应用程序编程接口
)
,称为
Call
;如果别人的库里面调用我们的函数,就叫
Callback
。
11 静态变量什么时候初始化?
对于
C
语言的全局和静态变量,
初始化发生在任何代码执行之前
,属于编译期初始化。
而
C++
标准规定:全局或静态对象当且仅当对象
首次用到时才进行构造
。
12 nullptr调用成员函数可以吗?为什么?
能。
原因:因为在
编译时对象
就绑定了
函数地址
,和指针空不空没关系。
13 说说什么是野指针,怎么产生的,如何避免
1.
概念:
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
2.
产生原因
:释放内存后指针不及时置空(野指针),依然指向了该内存,那么可能出现非法访问的 错误。这些我们都要注意避免。
3.
避免办法:
(1)初始化置
NULL
(2)申请内存后判空
(3)指针释放后置
NULL
(4)使用智能指针
14 说说静态局部变量,全局变量,局部变量的特点,以及使用场景
1.
首先从作用域考虑
:
C++
里作用域可分为
6
种:全局,局部,类,语句,命名空间和文件作用域。
全局变量:全局作用域,可以通过
extern
作用于其他非定义的源文件。
静态全局变量 :全局作用域
+
文件作用域,所以无法在其他文件中使用。
局部变量:局部作用域,比如函数的参数,函数内的局部变量等等。
静态局部变量 :局部作用域,只被初始化一次,直到程序结束。
2.
从所在空间考虑
:除了局部变量在栈上外,其他都在静态存储区。因为静态变量都在静态存储区, 所以下次调用函数的时候还是能取到原来的值。
3.
生命周期
: 局部变量在栈上,出了作用域就回收内存;而全局变量、静态全局变量、静态局部变量 都在静态存储区,直到程序结束才会回收内存。
15 说说内联函数和宏函数的区别
区别:
1.
宏定义不是函数
,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函 数压栈退栈过程,提高了效率;而内联函数本质上是一个函数
,内联函数一般用于函数体的代码比 较简单的函数,不能包含复杂的控制语句,while
、
switch
,并且内联函数本身不能直接调用自 身。
2.
宏函数
是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换 ;
而内联函数
则是 在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这 样可以省去函数的调用的开销,提高效率
3.
宏定义
是
没有类型检查的
,无论对还是错都是直接替换;
而内联函数
在编译的时候会进行类型的检 查,内联函数满足函数的性质,比如有返回值、参数列表等。
内联函数使用的条件:
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果
执行函数体内代码的时间,相比于函数调用的开销较大,那么效率 的收获会很少。另一方面,每一
处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
内联不是什么时候都能展开的,一个好的编译器将会根据函数的定义体,自动地取消不符合要求的
内联。
17 说说new和malloc的区别,各自底层实现原理。
1. new
是操作符,而
malloc
是函数。
2. new
在调用的时候
先分配内存,在调用构造函数,释放的时候调用析构函数
;而
malloc
没有构造函
数和析构函数。
3.
malloc
需要给定申请内存的大小,
返回的指针需要强转;
new
会调用构造函数,不用指定内存的大
小,返回指针不用强转。
4.
new
可以被重载
;
malloc
不行
5.
new
分配内存更直接和安全
。
6.
new
发生错误抛出异常
,
malloc
返回
null
malloc
底层实现:
当开辟的空间小于
128K
时,调用
brk
()函数;当开辟的空间大于
128K
时,调用 mmap()。
malloc
采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将 堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。
new
底层实现:
关键字
new
在调用构造函数的时候实际上进行了如下的几个步骤:
1.
创建一个新的对象
2.
将构造函数的作用域赋值给这个新的对象(因此
this
指向了这个新的对象)
3.
执行构造函数中的代码(为这个新对象添加属性)
4.
返回新对象
18 说说const和define的区别。
const
用于定义
常量
;而
define
用于
定义宏
,而宏也可以用于定义常量。都用于常量定义时,它们的区别有:
1. const
生效于编译的阶段;
define
生效于预处理阶段。
2. const
定义的常量,在
C
语言中
是存储在内存中、需要额外的内存空间的
;
define
定义的常量,运行时
是直接的操作数,并不会存放在内存中
。
3. const
定义的常量
是带类型的
;
define
定义的常量
不带类型
。因此
define
定义的常量不利于类型检
查。
19 说说C++中函数指针和指针函数的区别。
参考回答
1.
定义不同
指针函数本质是一个函数,其返回值为指针。 函数指针本质是一个指针,其指向一个函数。
2.
写法不同
指针函数:int *fun(int x,int y);
函数指针:int (*fun)(int x,int y);
3.
用法不同
指针函数可以在函数体内通过
return
语句返回指向动态分配的内存或全局/静态变量的指针。这样的函数可以方便地返回复杂的数据结构,如数组、字符串等。例如:
int* createArray(int size) {
int* arr = (int*)malloc(size * sizeof(int));
// 对 arr 进行初始化或其他操作
return arr;
}
函数指针可以在程序运行时动态地指向不同的函数。这在使用回调函数(callback)时非常有用,可以根据需要在不同的上下文中调用不同的函数。例如:
nt (*operation)(int, int); // 函数指针的声明
operation = &add; // 将函数指针指向 add 函数
int result = operation(3, 4); // 调用 add 函数进行计算
总之,指针函数和函数指针在定义、写法和用法上存在一些区别。指针函数是一个返回指针的函数,而函数指针是一个指向函数的指针,可以在运行时动态地指向不同的函数。这些概念在C和C++等语言中非常常见,可以提供更大的灵活性和可扩展性。
20 说说const int *a, int const *a, const int a, int *const a, const int *const a分别是什么,有什么特点。
1. const int a; //指的是a是一个常量,不允许修改。
2. const int *a; //a指针所指向的内存里的值不变,即(*a)不变
3. int const *a; //同const int *a;
4. int *const a; //a指针所指向的内存地址不变,即a不变
5. const int *const a; //都不变,即(*a)不变,a也不变
21 说说使用指针需要注意什么?
1.
定义指针时,先初始化为
NULL
。
2.
用
malloc
或
new
申请内存之后,应该
立即检查
指针值是否为
NULL
。防止使用指针值为
NULL
的内
存。
3.
不要忘记为数组和动态内存
赋初值
。防止将未被初始化的内存作为右值使用。
4.
避免数字或指针的下标
越界
,特别要当心发生
“
多
1”
或者
“
少
1”
操作
5.
动态内存的申请与释放必须配对,防止
内存泄漏
6.
用
free
或
delete
释放了内存之后,立即将指针
设置为
NULL
,防止
“
野指针
”
22 说说内联函数和函数的区别,内联函数的作用。
1.
内联函数比普通函数多了关键字
inline
2.
内联函数避免了函数调用的
开销
;普通函数有调用的开销
3.
普通函数在被调用的时候,需要
寻址(函数入口地址)
;内联函数不需要寻址。
4.
内联函数有一定的限制,内联函数体要求
代码简单
,不能包含复杂的结构控制语句;普通函数没有这个要求。
内联函数的作用
:内联函数在调用时,是将调用表达式用内联函数体来替换。避免函数调用的开销。
23 简述C++有几种传值方式,之间的区别是什么?
参考回答
传参方式有这三种:
值传递、引用传递、指针传递
1.
值传递:形参即使在函数体内值发生变化,也不会影响实参的值;
2.
引用传递:形参在函数体内值发生变化,会影响实参的值;
3.
指针传递:在指针指向没有发生改变的前提下,形参在函数体内值发生变化,会影响实参的值;
值传递用于对象时,整个对象会拷贝一个副本,这样效率低;而引用传递用于对象时,不发生拷贝
行为,只是绑定对象,更高效;指针传递同理,但不如引用传递安全。
24 简述const(星号)和(星号)const的区别
//const* 是指针常量, *const 是常量指针int const * a ; //a 指针所指向的内存里的值不变,即( *a )不变,指针可变int * const a ; //a 指针所指向的内存地址不变,即 a 不变,其所指内存值可变
2.C++内存
1 简述堆和栈的区别
区别:
1.
堆栈空间分配不同
。
栈由操作系统自动分配释放
,存放函数的参数值,局部变量的值等;
堆一般由 程序员分配释放。
2.
堆栈缓存方式不同
。栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放;堆则是存放在二级缓存中,速度要慢些。
3.
堆栈数据结构不同
。堆类似数组结构;栈类似栈结构,先进后出。
2 简述C++的内存管理
1.
内存分配方式
:
在
C++
中,内存分成
5
个区,他们分别是
堆、栈、自由存储区、全局
/
静态存储区和常量存储区
。
栈
,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元
自动被释放。
堆
,就是那些由
new
分配的内存块,一般一个
new
就要对应一个
delete
。
自由存储区
,就是那些由
malloc
等分配的内存块,和堆是十分相似的,不过是用
free
来结束自己的
生命。
全局
/
静态存储区
,全局变量和静态变量被分配到同一块内存中
常量存储区
,这是一块比较特殊的存储区,里面
存放的是常量,不允许修改。
2.
常见的内存错误及其对策
:
(1)内存分配未成功,却使用了它。
(2)内存分配虽然成功,但是尚未初始化就引用它。
(3)内存分配成功并且已经初始化,但操作越过了内存的边界。
(4)忘记了释放内存,造成内存泄露。
(5)释放了内存却继续使用它。
对策:
(1)定义指针时,先初始化为
NULL
。
(2)用
malloc
或
new
申请内存之后,应该
立即检查
指针值是否为
NULL
。防止使用指针值为
NULL
的内存。
(3)不要忘记为数组和动态内存
赋初值
。防止将未被初始化的内存作为右值使用。
(4)避免数字或指针的下标
越界
,特别要当心发生
“
多
1”
或者
“
少
1”
操作
(5)
动态内存的申请与释放必须配对,防止
内存泄漏
(6)
用
free
或
delete
释放了内存之后,立即将指针
设置为
NULL
,防止
“
野指针
”
(7)使用智能指针。
3. 内存泄露及解决办法:
什么是内存泄露?
简单地说就是
申请了一块内存空间,使用完毕后没有释放掉
。
(1)
new
和
malloc
申请资源使用后,没有用
delete
和
free
释放;
(2)子类继承父类时,父类析构函数不是虚函数。
(3)
Windows
句柄资源使用后没有释放。
怎么检测?
第一:
良好的编码习惯,使用了内存分配的函数,一旦使用完毕
,
要记得使用其相应的函数释放掉
。
第二:
将分配的内存的指针以链表的形式自行管理
,使用完毕之后从链表中删除,程序结束时可检 查改链表。
第三:
使用智能指针。
第四:一些常见的工具插件,如
ccmalloc
、
Dmalloc
、
Leaky
、
Valgrind
等等。
3 malloc和局部变量分配在堆还是栈?
malloc
是在
堆上分配内存
,需要程序员自己回收内存;局部变量是在
栈中分配内存
,超过作用域就自动回收。
4 程序有哪些section,分别的作用?程序启动的过程?怎么判断数据分配在栈上还是堆上?
![](https://i-blog.csdnimg.cn/blog_migrate/c3d4c80a51393b020f6710deb2354284.png)
一个程序有哪些
section
:
如上图,
从低地址到高地址,一个程序由代码段、数据段、
BSS
段组成。
1.
数据段:
存放程序中
已初始化的
全局变量和静态变量的一块内存区域。
2.
代码段:
存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
3.
BSS
段:存放程序中
未初始化的
全局变量和静态变量的一块内存区域。
4.
可执行程序在运行时又会多出两个区域:堆区和栈区。
堆区:
动态申请内存用。堆从低地址向高地址增长。
栈区:
存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
5.
最后还有一个
文件映射区
,位于堆和栈之间。
堆
heap
:由
new
分配的内存块,其释放由程序员控制(一个
new
对应一个
delete
)
栈
stack
:是那些编译器在需要时分配,在不需要时自动清除的存储区。存放局部变量、函数参数。
常量存储区
:存放常量,不允许修改。
程序启动的过程:
1.
操作系统
首先创建相应的进程并分配私有的进程空间
,然后操作系统的
加载器负责把可执行文件的
数据段和代码段映射到进程的虚拟内存空间中。
2.
加载器读入可执行程序的导入符号表
,根据这些符号表可以查找出该可执行程序的所有依赖的动态
链接库。
3.
加载器针对该程序的每一个动态链接库调用
LoadLibrary
(1)查找对应的动态库文件,加载器为该动态链接库确定一个合适的基地址。
(2)加载器读取该动态链接库的导入符号表和导出符号表,比较应用程序要求的导入符号是否匹
配该库的导出符号。
(3)针对该库的导入符号表,查找对应的依赖的动态链接库,如有跳转,则跳到
3
(4)调用该动态链接库的初始化函数
4.
初始化应用程序的全局变量,对于全局对象自动调用构造函数
。
5.
进入应用程序入口点函数开始执行。
怎么判断数据分配在栈上还是堆上:
首先局部变量分配在栈上;而通过
malloc
和
new
申请的空间是在堆上。
5 初始化为0的全局变量在bss还是data
BSS
段通常是指用来存放程序中
未初始化的或者初始化为
0
的
全局变量和静态变量的一块内存区域。特点是可读写的,
在程序执行之前
BSS
段会自动清
0
。
6 简述C++中内存对齐的使用场景
内存对齐应用于三种数据类型中:
struct/class/union
struct/class/union
内存对齐原则有四个:
1.
数据成员对齐规则
:结构
(struct)
或联合
(union)
的数据成员,
第一个数据成员放在
offset
为
0
的地
方
,以后每个数据成员存储的
起始位置要从该成员大小或者成员的子成员大小的整数倍开始
。
2.
结构体作为成员
:
如果一个结构里有某些结构体成员
,
则结构体成员要从其
内部
"
最宽基本类型成
员
"
的整数倍地址开始存储。
(struct a
里存有
struct b,b
里有
char,int ,double
等元素
,
那
b
应该从
8
的
整数倍开始存储
)
。
3.
收尾工作
:
结构体的总大小,也就是
sizeof
的结果,
必须是其内部最大成员的
"
最宽基本类型成员
"
的
整数倍。不足的要补齐。
(
基本类型不包括
struct/class/uinon)
。
4.
sizeof(union)
,以结构里面
size
最大元素为
union
的
size
,因为在某一时刻,
union
只有一个成员真
正存储于该地址。
什么是内存对齐?
那么什么是字节对齐?在
C
语言中,结构体是一种复合数据类型,其构成元素既可以是基本数据类型 (如int
、
long
、
float
等)的变量,也可以是一些复合数据类型(如数组、结构体、联合体等)的数据单 元。在结构体中,编译器为结构体的每个成员按其自然边界(
alignment
)分配空间。
各个成员按照它 们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的地址相同。
为了使
CPU
能够对变量进行快速的访问,变量的起始地址应该具有某些特性,
即所谓的
“
对齐
”
,比如
4
字
节的
int
型,其起始地址应该位于
4
字节的边界上,即起始地址能够被
4
整除
,也即
“
对齐
”
跟数据在内存中 的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。
为什么要字节对齐?
为了快速准确的访问,若没有字节对齐则会出现多次访问浪费时间。
举例说明(定义一个
char
和
int
型数据不按照字节对齐存储的情况需要多次访问)
3 面向对象
1 简述一下什么是面向对象
1.
面向对象是一种编程思想,把一切东西看成是一个个对象
,把这些对象拥有的属性变量和操作这些 属性变量的函数打包成一个类来表示
2.
面向过程和面向对象的区别
面向过程:根据业务逻辑从上到下写代码
面向对象:
将数据与函数绑定到一起,进行封装
,加快开发程序,减少重复代码的重写过程。
2 简述一下面向对象的三大特征
面向对象的三大特征是
封装、继承、多态
1.
封装:
将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行 交互。
封装
本质上是一种管理
,不想给别人看到的,我们使用
protected/private
把成员
封装起来。开放一些共有的成员函数对成员合理的访问。
2.
继承:
可以使用现有类的所有功能
,并在无需重新编写原来的类的情况下对这些功能进行扩展。
三种继承方式。
![](https://i-blog.csdnimg.cn/blog_migrate/41bc8a8b9ffb1f903ff5da4b7af7b91b.png)
3.
多态:
用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数
。实现多态,有二种方式,
重写,重载
。
3简述一下 C++ 的重载和重写,以及它们的区别
1.
重写
是指
派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重
写的函数一致。只有函数体不同(花括号内)
,派生类对象调用时会调用派生类的重写函数,不会
调用被重写函数。重写的基类中被重写的函数必须有
virtual
修饰。
#include<bits/stdc++.h>
using namespace std;
class A
{
public:
virtual void fun()
{
cout << "A";
}
};
class B :public A
{
public:
virtual void fun()
{
cout << "B";
}
};
int main(void)
{
A* a = new B();
a->fun();//输出B,A类中的fun在B类中重写
}
1.
重载
我们在平时写代码中会用到几个函数但是他们的实现功能相同,但是有些细节却不同。
函数重载是
指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根
据参数列表确定调用哪个函数,重载不关心函数返回类型。
#include<bits/stdc++.h>
using namespace std;
class A
{
void fun() {};
void fun(int i) {};
void fun(int i, int j) {};
void fun1(int i,int j){};
};
4 说说 C++ 的重载和重写是如何实现的
1.
重载:
C++
利用命名倾轧(
name mangling
)技术,来改名函数名,区分参数不同的同名函数。命 名倾轧是在编译阶段完成的。
编译时将参数类型加入以区分不同。
2.
重写:
在
基类的函数前加上
virtual
关键字,在派生类中重写该函数,运行时将会根据对象的实际类 型来调用相应的函数
。如果
对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调 用基类的函数。
虚函数需要注意的地方:
用
virtual
关键字申明
的函数叫做虚函数,虚函数肯定是类的成员函数。
存在虚函数的类都有一个一维的虚函数表叫做虚表
,
类的对象有一个指向虚表开始的虚指针。
虚表是和类对应的,虚表指针是和对象对应的。
多态性是一个接口多种实现
,是面向对象的核心,分为类的多态性和函数的多态性。
重写用虚函数来实现,结合动态绑定
。
纯虚函数
是虚函数再加上
= 0
。
抽象类
是指包括至少一个
纯虚函数的类
。
纯虚函数:
virtual void fun()=0
。即抽象类
必须在子类实现这个函数,即先有名称,没有内容
,在
派生类实现内容。
5 说说构造函数有几种,分别什么作用
C++
中的构造函数可以分为
4
类:默认构造函数、初始化构造函数、拷贝构造函数、移动构造函数。
1.
默认构造函数和初始化构造函数。 在定义类的对象的时候,完成对象的初始化工作。
注意:
有了有参的构造了,编译器就不提供默认的构造函数。
class Student
{
public:
//默认构造函数
Student()
{
num=1001;
age=18;
}
//初始化构造函数
Student(int n,int a):num(n),age(a){}
private:
int num;
int age;
};
int main()
{
//用默认构造函数初始化对象S1
Student s1;
//用初始化构造函数初始化对象S2
Student s2(1002,18);
return 0;
}
2.
拷贝构造函数
#include "stdafx.h"
#include "iostream.h"
class Test
{
int i;
int *p;
public:
Test(int ai,int value)
{
i = ai;
p = new int(value);
}
~Test()
{
delete p;
}
Test(const Test& t)
{
this->i = t.i;
this->p = new int(*t.p);
}
};
//复制构造函数用于复制本类的对象
int main(int argc, char* argv[])
{
Test t1(1,2);
Test t2(t1);//将对象t1复制给t2。注意复制和赋值的概念不同
return 0;
}
赋值构造函数默认实现的是值拷贝(浅拷贝)
。
3.
移动构造函数。用于将其他类型的变量,隐式转换为本类对象。
下面的转换构造函数,将
int
类型的 r转换为
Student
类型的对象,对象的
age
为
r
,
num
为
1004.
Student(int r)
{
int num=1004;
int age= r;
}
6 只定义析构函数,会自动生成哪些构造函数
只定义了析构函数,编译器将自动为我们生成
拷贝构造函数和默认构造函数
。
默认构造函数和初始化构造函数。 在定义类的对象的时候,完成对象的初始化工作。
class Student
{
public:
//默认构造函数
Student()
{
num=1001;
age=18;
}
//初始化构造函数
Student(int n,int a):num(n),age(a){}
private:
int num;
int age;
};
int main()
{
//用默认构造函数初始化对象S1
Student s1;
//用初始化构造函数初始化对象S2
Student s2(1002,18);
return 0;
}
有了有参的构造了,编译器就不提供默认的构造函数。拷贝构造函数
#include "stdafx.h"
#include "iostream.h"
class Test
{
int i;
int *p;
public:
Test(int ai,int value)
{
i = ai;
p = new int(value);
}
~Test()
{
delete p;
}
Test(const Test& t)
{
this->i = t.i;
this->p = new int(*t.p);
}
};
//复制构造函数用于复制本类的对象
int main(int argc, char* argv[])
{
Test t1(1,2);
Test t2(t1);//将对象t1复制给t2。注意复制和赋值的概念不同。
return 0;
}
赋值构造函数默认实现的是值拷贝(浅拷贝)。
答案解析
示例如下:
class HasPtr
{
public:
HasPtr(const string& s = string()) :ps(new string(s)), i(0) {}
~HasPtr() { delete ps; }
private:
string * ps;
int i;
};
如果类外面有这样一个函数:
HasPtr f(HasPtr hp)
{
HasPtr ret = hp;
///... 其他操作
return ret;
}
当函数执行完了之后,将会调用
hp
和
ret
的析构函数,将
hp
和
ret
的成员
ps
给
delete
掉,但是由于
ret
和 hp指向了同一个对象,因此该对象的
ps
成员被
delete
了两次,这样产生一个未定义的错误,所以说,如 果一个类定义了析构函数,那么它要定义自己的拷贝构造函数和默认构造函数。
7 说说一个类,默认会生成哪些函数
定义一个空类
class Empty
{
};
默认会生成以下几个函数:无参的构造函数在定义类的对象的时候,完成对象的初始化工作。
Empty()
{
}
拷贝构造函数
拷贝构造函数用于复制本类的对象
Empty(const Empty& copy)
{
}
赋值运算符
Empty& operator = (const Empty& copy)
{
}
析构函数(非虚)
~Empty()
{
}
8 说说 C++ 类对象的初始化顺序,有多重继承情况下的顺序
1.
创建派生类的对象,基类的构造函数优先被调用
(也优先于派生类里的成员类);
2.
如果类里面有成员类,成员类的构造函数优先被调用
;
(
也优先于该类本身的构造函数)
3.
基类构造函数如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序
而不是它们 在成员初始化表中的顺序;
4.
成员类对象构造函数如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序
而 不是它们出现在成员初始化表中的顺序;
5.
派生类构造函数,作为一般规则派生类构造函数应该不能直接向一个基类数据成员赋值而是把值传递给适当的基类构造函数。
6.
综上可以得出,初始化顺序:
父类构造函数
–>
成员类对象构造函数
–>
自身构造函数
其中成员变量的初始化与声明顺序有关,构造函数的调用顺序是类派生列表中的顺序。
析构顺序和构造顺序相反
。
9 简述下向上转型和向下转型
1.
子类转换为父类:向上转型,使用
dynamic_cast
(expression)
,这种转换相对来说
比较安全
不会有数据的丢失;
2.
父类转换为子类:向下转型
,可以使用强制转换,这种转换时
不安全的,会导致数据的丢失
,原因是
父类的指针或者引用的内存中可能不包含子类的成员的内存
。
10 简述下深拷贝和浅拷贝,如何实现深拷贝
1.
浅拷贝:
又称值拷贝,
将源对象的值拷贝到目标对象中去
,本质上来说
源对象和目标对象共用一份实体
,
只是所引用的变量名不同,地址其实还是相同的
。举个简单的例子,你的小名叫西西,大名 叫冬冬,当别人叫你西西或者冬冬的时候你都会答应,这两个名字虽然不相同,但是都指的是你。
2.
深拷贝
,拷贝的时候先
开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去
,这样两个
指针就指向了不同的内存位置。并且里面的内容是一样的
,
深拷贝情况下,不会出现重复释放同一块内存的错误。
11 简述一下 C++ 中的多态
由于
派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定,这就是多态
。 多态分为
静态多态和动态多态
:
1.
静态多态
:
编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数
,如果有对应的函数,就调用,没有则在编译时报错。比如一个简单的加法函数:
include<iostream>
using namespace std;
int Add(int a,int b)//1
{
return a+b;
}
char Add(char a,char b)//2
{
return a+b;
}
int main()
{
cout<<Add(666,888)<<endl;//1
cout<<Add('1','2');//2
return 0;
}
显然,第一条语句会调用函数
1
,而第二条语句会调用函数
2
,这绝不是因为函数的声明顺序,不信
你可以将顺序调过来试试。
2.
动态多态:
其实要实现动态多态,需要几个条件
——
即动态绑定条件:
1.
虚函数。
基类中必须有虚函数,在派生类中必须重写虚函数。
2.
通过
基类类型的指针或引用来调用虚函数
。
12 说说为什么要虚析构,为什么不能虚构造
1.
虚析构:
将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们
new
一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏
。如果基类的
析构函数不是虚函数,
在特定情况下会
导致派生类无法被析构
。
1.
用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常
析构
2.
用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基
类,不会析构派生类对象,从而造成内存泄漏。为什么会出现这种现象呢,个人认为析构的时
候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑定的对
象来进行,所以只是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构的时候就
要根据指针绑定的对象来调用对应的析构函数了。
C++
默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。
2.
不能虚构造:
1.
从存储空间角度:
虚函数对应一个
vtale,
这个表的地址是存储在对象的内存空间的
。如果将构
造函数设置为虚函数,就需要到
vtable
中调用,
可是对象还没有实例化,没有内存空间分配,如何调用
。(悖论)
2.
从实现上看,
vbtl
在构造函数调用后才建立,因而构造函数不可能成为虚函数。
13
说说模板类是在什么时候实现的
1.
模板实例化:模板的实例化分为显示实例化和隐式实例化,前者是研发人员明确的告诉模板应该使用什么样的类型去生成具体的类或函数,后者是在编译的过程中由编译器来决定使用什么类型来实例化一个模板不管是显示实例化或隐式实例化,最终生成的类或函数完全是按照模板的定义来实现的
2.
模板具体化:当模板使用某种类型类型实例化后生成的类或函数不能满足需要时,可以考虑对模板进行具体化。具体化时可以修改原模板的定义,当使用该类型时,按照具体化后的定义实现,具体化相当于对某种类型进行特殊处理。
3.
代码示例
#include <iostream>
using namespace std;
// #1 模板定义
template<class T>
struct TemplateStruct
{
TemplateStruct()
{
cout << sizeof(T) << endl;
}
};
// #2 模板显示实例化
template struct TemplateStruct<int>;
// #3 模板具体化
template<> struct TemplateStruct<double>
{
TemplateStruct() {
cout << "--8--" << endl;
}
};
int main()
{
TemplateStruct<int> intStruct;
TemplateStruct<double> doubleStruct;
// #4 模板隐式实例化
TemplateStruct<char> llStruct;
}
14 说说类继承时,派生类对不同关键字修饰的基类方法的访问权限
类中的成员可以分为三种类型,分别为
public
成员、
protected
成员、
public
成员。类中可以直接访问自己类的public
、
protected
、
private
成员,但
类对象只能访问自己类的
public
成员
。
1. public
继承:
派生类可以访问基类的
public
、
protected
成员,不可以访问基类的
private
成员
;
派生
类对象可以访问基类的
public
成员
,
不可以访问
基类的
protected
、
private
成员。
2. protected
继承:
派生类可以访问基类的
public
、
protected
成员
,不可以访问基类的
private
成员;
派生类对象不可以访问
基类的
public
、
protected
、
private
成员。
3. private
继承:派生类可以访问基类的
public
、
protected
成员,不可以访问基类的
private
成员;
派生类对象不可以访问
基类的
public
、
protected
、
private
成员。
15 简述一下移动构造函数,什么库用到了这个函数?
移动也使用一个对象的值设置另一个对象的值。
移动实现的是对象值真实的转移
(源对象到目的对
象):
源对象将丢失其内容,其内容将被目的对象占有
。移动操作的发生的时候,是当移动值的对象是未命名的对象的时候。这里未命名的对象就是那些临时变量,甚至都不会有名称。典型的未命名对象就是函数的返回值或者类型转换的对象。
16 构造函数为什么不能被声明为虚函数?
1.
从存储空间角度:虚函数对应一个
vtale,
这个表的地址是存储在对象的内存空间的。如果将构造函数设置为虚函数,就需要到vtable
中调用,可是对象还没有实例化,没有内存空间分配,如何调
用。(悖论)
2.
从使用角度:虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
3.
从实现上看,
vbtl
在构造函数调用后才建立,因而构造函数不可能成为虚函数。从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数。
17 简述一下什么是常函数,有什么作用
类的成员函数后面加
const
,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。
在设计类的时候,一个
原则就是对于不改变数据成员的成员函数都要在后面加
const
,
而对于改变数据成员的成员函数不能加 const
。所以
const
关键字对成员函数的行为作了更明确的限定:有 const
修饰的成员函数(指
const
放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const
修饰的成员函数,对数据成员则是可读可写的。除此之外,在类的成员函数后面加 const
还有什么好处呢?那就是常量(即
const
)对象可以调用
const成员函数,而不能调用非const
修饰的函数。正如非
const
类型的数据可以给
const
类型的变量赋值一样,反之则不成立。
#include<iostream>
using namespace std;
class CStu
{
public:
int a;
CStu()
{
a = 12;
}
void Show() const
{
//a = 13; //常函数不能修改数据成员
cout <<a << "I am show()" << endl;
}
};
int main()
{
CStu st;
st.Show();
system("pause");
return 0;
}
18 说说什么是虚继承,解决什么问题,如何实现?
虚继承是
解决
C++
多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝
。这将存在两个问题:其一,
浪费存储空间
;第二,
存在二义性问题
,通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性。虚继承可以解决多种继承前面 提到的两个问题。
#include<iostream>
using namespace std;
class A{
public:
int _a;
};
class B :virtual public A
{
public:
int _b;
};
class C :virtual public A
{
public:
int _c;
};
class D :public B, public C
{
public:
int _d;
};
//菱形继承和菱形虚继承的对象模型
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
cout << sizeof(D) << endl;
return 0;
}
分别从菱形继承和虚继承来分析:菱形继承中A
在
B,C,D,
中各有一份,虚继承中,
A
共享。上面的虚继承表实际上是一个指针数组。B
、
C
实际上是虚基表指针,指向虚基表。虚基表:存放相对偏移量,用来找虚基类。
19 简述一下虚函数和纯虚函数,以及实现原理
1. C++
中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“
多种形态”
,这是一种泛型技术。如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。非虚函数总是在编译时根据调用该函数的对象,引用或指针的类型而确定。如果调用 虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定或指针所指向的对象所。
属类型定义的版本。虚函数必须是基类的非静态成员函数。虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。
class Person{
public:
//虚函数
virtual void GetName(){
cout<<"PersonName:xiaosi"<<endl;
};
};
class Student:public Person{
public:
void GetName(){
cout<<"StudentName:xiaosi"<<endl;
};
};
int main(){
//指针
Person *person = new Student();
//基类调用子类的函数
person->GetName();//StudentName:xiaosi
}
虚函数(
Virtual Function
)是通过一张虚函数表(
Virtual Table
)来实现的。简称为
V-Table
。在
这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应
实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们
用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指
明了实际所应该调用的函数。
2.
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” virtualvoid GetName() =0
。在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决上述问题,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的函数绝不会调用。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。必须在继承类中重新声明函数(不要后面的=0
)否则该派生类也不能实例化,而且它们在抽象类中往往没有定义。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“
你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它
”
。
//抽象类
class Person{
public:
//纯虚函数
virtual void GetName()=0;
};
class Student:public Person{
public:
Student(){
};
void GetName(){
cout<<"StudentName:xiaosi"<<endl;
};
};
int main(){
Student student;
}
20
说说纯虚函数能实例化吗,为什么?派生类要实现吗,为什么?
参考回答
1.
纯
虚函数不可以实例化,但是可以用其派生类实例化
,示例如下:
class Base
{
public:
virtual void func() = 0;
};
#include<iostream>
using namespace std;
class Base
{
public:
virtual void func() = 0;
};
class Derived :public Base
{
public:
void func() override
{
cout << "哈哈" << endl;
}
};
int main()
{
Base *b = new Derived();
b->func();
return 0;
}
2.
虚函数的原理采用
vtable
。类中含有纯虚函数时,其
vtable
不完全,有个空位。
即
“
纯虚函数在类的
vftable
表中对应的表项被赋值为
0
。也就是指向一个不存在的函数。由于编译器
绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写
此函数,否则也不能生成对象。
”
所以纯虚函数不能实例化。
3.
纯虚函数是在基类中声明的虚函数,它要求任何派生类都要定义自己的实现方法,以实现多态性
。
4.
定义纯虚函数是为了实现一个接口,用来规范派生类的行为,也即规范继承这个类的程序员必须实 现这个函数。派生类仅仅只是继承函数的接口。纯虚函数的意义在于,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但基类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“
你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”
。
21 说说C++中虚函数与纯虚函数的区别
参考回答
1.
虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类。
2.
虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。
3.
虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。
4.
虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。
5.
虚函数的定义形式:
virtual{}
;
纯虚函数的定义形式:
virtual { } = 0
;
在虚函数和纯虚函数
的定义中不能有
static
标识符,原因很简单,被
static
修饰的函数在编译时要求前期绑定
,
然而虚函数
却是动态绑定,而且被两者修饰的函数生命周期也不一样。
答案解析
1.
我们举个虚函数的例子:
class A
{
public:
virtual void foo()
{
cout<<"A::foo() is called"<<endl;
}
};
class B:public A
{
public:
void foo()
{
cout<<"B::foo() is called"<<endl;
}
};
int main(void)
{
A *a = new B();
a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
return 0;
}
这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在
所谓
“
推迟联编
”
或者
“
动态联编
”
上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时
刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以
被成为
“
虚
”
函数。虚函数只能借助于指针或者引用来达到多态的效果。
2.
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
virtual void funtion1()=0
为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀
等子类,但动物本身生成对象明显不合常理。
为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:
virtual ReturnType
Function()= 0;
),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0
,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“
你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”
。
22 说说 C++ 中什么是菱形继承问题,如何解决?
导致问题:
菱形继承会导致数据重复和产生歧义;
解决办法:
使用虚继承,可确保每份数据自继承一次;
23 请问构造函数中的能不能调用虚方法
参考回答
1.
不要在构造函数中调用虚方法,从语法上讲,调用完全没有问题,但是从效果上看,往往不能达到需要的目的。
派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。
同样,进入基类析构函数时,对象也是基类类型。所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。
24 请问拷贝构造函数的参数是什么传递方式,为什么
参考回答
1.
拷贝构造函数的参数必须使用引用传递
2.
如果拷贝构造函数中的参数不是一个引用,即形如
CClass(const CClass c_class)
,那么就相当于采用了传值的方式(pass-by-value)
,而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。需要澄清的是,传指针其实也是传值,如果上面的拷贝构造函数写成CClass(const CClass* c_class),也是不行的。事实上,只有传引用不是传值外,其他所有的传递方式都是传值。
25 如何理解抽象类?
参考回答
1.
抽象类的定义如下:
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现
方法。
在基类中实现纯虚函数的方法是在函数原型后加
“=0”
,有虚函数的类就叫做抽象类。
2.
抽象类有如下几个特点:
1
)
抽象类只能用作其他类的基类,不能建立抽象类对象
。
2
)抽象类不能用作参数类型、函数返回类型或显式转换的类型。
3
)可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。
26 什么是多态?除了虚函数,还有什么方式能实现多态?
参考回答
1.
多态是面向对象的重要特性之一,它是一种行为的封装,就是不同对象对同一行为会有不同的状
态。
(
举例
:
学生和成人都去买票时
,
学生会打折
,
成人不会
)
2.
多态是以封装和继承为基础的。在
C++
中多态分为静态多态(早绑定)和动态多态(晚绑定)两
种,其中动态多态是通过虚函数实现,静态多态通过函数重载实现,代码如下:
class A
{
public:
void do(int a);
void do(int a, int b);
};
27 简述一下虚析构函数,什么作用
参考回答
1.
虚析构函数,是将基类的析构函数声明为
virtual
,举例如下:
class TimeKeeper
{
public:
TimeKeeper() {}
virtual ~TimeKeeper() {}
};
2.
虚析构函数的
主要作用是防止内存泄露
。
定义一个基类的指针
p
,在
delete p
时,如果基类的析构函数是虚函数,这时只会看
p
所赋值的对
象,如果
p
赋值的对象是派生类的对象,就会调用派生类的析构函数(毫无疑问,在这之前也会先
调用基类的构造函数,在调用派生类的构造函数,然后调用派生类的析构函数,基类的析构函数,
所谓先构造的后释放);如果
p
赋值的对象是基类的对象,就会调用基类的析构函数,这样就不会
造成内存泄露。 如果基类的析构函数不是虚函数,在delete p
时,调用析构函数时,只会看指针的数据类型,而不 会去看赋值的对象,这样就会造成内存泄露。
答案解析
我们创建一个
TimeKeeper
基类和一些及其它的派生类作为不同的计时方法。
class TimeKeeper
{
public:
TimeKeeper() {}
~TimeKeeper() {} //非virtual的
};
//都继承与TimeKeeper
class AtomicClock :public TimeKeeper{};
class WaterClock :public TimeKeeper {};
class WristWatch :public TimeKeeper {};
如果客户想要在程序中使用时间,不想操作时间如何计算等细节,这时候我们可以设计
factory
(工
厂)函数,让函数返回指针指向一个计时对象。该函数返回一个基类指针,这个基类指针是指向于
派生类对象的。
TimeKeeper* getTimeKeeper()
{
//返回一个指针,指向一个TimeKeeper派生类的动态分配对象
}
因为函数返回的对象存在于堆中,因此为了
在不使用时我们需要使用释放该对象(
delete
)
TimeKeeper* ptk = getTimeKeeper();
delete ptk;
此处基类的析构函数是非
virtual
的,因此
通过一个基类指针删除派生类对象是错误的
解决办法:
将基类的析构函数改为
virtual
就正确了。
class TimeKeeper
{
public:
TimeKeeper() {}
virtual ~TimeKeeper() {}
};
声明为
virtual
之后,通过
基类指针删除派生类对象就会释放整个对象(基类
+
派生类)
28 说说什么是虚基类,可否被实例化?
1. 在被继承的类前面加上virtual关键字,这时被继承的类称为虚基类,代码如下:
class A
class B1:public virtual A;
class B2:public virtual A;
class D:public B1,public B2;
2. 虚继承的类可以被实例化,举例如下:
class Animal {/* ... */ };
class Tiger : virtual public Animal { /* ... */ };
class Lion : virtual public Animal { /* ... */ }
int main( )
{
Liger lg ;
/*既然我们已经在Tiger和Lion类的定义中声明了"virtual"关键字,于是下面的代码编译OK */
int weight = lg.getWeight();
}
29 简述一下拷贝赋值和移动赋值?
参考回答
1.
拷贝赋值是通过拷贝构造函数来赋值,在创建对象时,使用同一类中之前创建的对象来初始化新创建的对象。
2.
移动赋值是通过移动构造函数来赋值,二者的主要区别在于
1
)拷贝构造函数的形参是一个左值引用,而移动构造函数的形参是一个右值引用;
2
)拷贝构造函数完成的是整个对象或变量的拷贝,而移动构造函数是生成一个指针指向源对象或
变量的地址,接管源对象的内存,相对于大量数据的拷贝节省时间和内存空间。
30 仿函数了解吗?有什么作用
参考回答
1.
仿函数(
functor
)又称为函数对象(
function object
)是一个能行使函数功能的类。仿函数的语
法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载
operator()
运算符,举个例
子:
class Func{
public:
void operator() (const string& str) const {
cout<<str<<endl;
}
};
Func myFunc;
myFunc("helloworld!");
>>>helloworld!
1.
仿函数既能想普通函数一样传入给定数量的参数,还能存储或者处理更多我们需要的有用信息。我们可以举个例子:
假设有一个
vector<string>
,你的任务是统计长度小于
5
的
string
的个数,如果使用
count_if
函
数的话,你的代码可能长成这样:
bool LengthIsLessThanFive(const string& str) {
return str.length()<5;
}
int res=count_if(vec.begin(), vec.end(), LengthIsLessThanFive);
其中
count_if
函数的第三个参数是一个函数指针,返回一个
bool
类型的值。一般的,如果需要将
特定的阈值长度也传入的话,我们可能将函数写成这样:
bool LenthIsLessThan(const string& str, int len) {
return str.length()<len;
}
这个函数看起来比前面一个版本更具有一般性,但是他不能满足
count_if
函数的参数要求:
count_if
要求的是
unary function
(仅带有一个参数)作为它的最后一个参数。如果我们使用仿
函数,是不是就豁然开朗了呢:
class ShorterThan {
public:
explicit ShorterThan(int maxLength) : length(maxLength) {}
bool operator() (const string& str) const {
return str.length() < length;
}
private:
const int length;
};
31 C++ 中哪些函数不能被声明为虚函数?
参考回答
常见的不不能声明为虚函数的有:
普通函数(非成员函数),静态成员函数,内联成员函数,构造函
数,友元函数。
1.
为什么
C++
不支持普通函数为虚函数?
普通函数(非成员函数)只能被
overload
,不能被
override
,声明为虚函数无意义
,因此编译器会
在编译时绑定函数。
2.
为什么
C++
不支持构造函数为虚函数?
这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象
成员才产生的,然而
virtual function
主要是为了再不完全了解细节的情况下也能正确处理对象。另
外,
virtual
函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用
virtual
函数
来完成你想完成的动作
。(这不就是典型的悖论)
构造函数用来创建一个新的对象
,
而虚函数的运行是建立在对象的基础上
,
在构造函数执行时
,
对象尚
未形成
,
所以不能将构造函数定义为虚函数
3.
为什么
C++
不支持内联成员函数为虚函数?
其实很简单,那
内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在
继承后对象能够准确的执行自己的动作
,这是不可能统一的。(再说了,
inline
函数在编译时被展
开
,虚函数在运行时才能动态的绑定函数)
内联函数是在编译时期展开
,
而虚函数的特性是运行时才动态联编
,
所以两者矛盾
,
不能定义内联函数
为虚函数
4.
为什么
C++
不支持静态成员函数为虚函数?
这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没
有要动态绑定的必要性。
静态成员函数属于一个类而非某一对象
,
没有
this
指针
,
它无法进行对象的判别
5.
为什么
C++
不支持友元函数为虚函数?
因为
C++
不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法
。
32 解释下 C++ 中类模板和模板类的区别
参考回答
1.
类模板是模板的定义,不是一个实实在在的类,定义中用到通用类型参数
2.
模板类是实实在在的类定义,是类模板的实例化。类定义中参数被实际类型所代替。
答案解析
1.
类模板的类型参数可以有一个或多个,每个类型前面都必须加
class
,如
template <class T1,class
T2>class someclass{…};
在定义对象时分别代入实际的类型名,如
someclass<int,double> obj;
2.
和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。
3.
模板可以有层次,一个类模板可以作为基类,派生出派生模板类.
32 虚函数表里存放的内容是什么时候写进去的?
参考回答
1.
虚函数表是一个存储虚函数地址的数组
,
以
NULL
结尾。
虚表(
vftable
)在编译阶段生成,对象内存空间开辟以后,写入对象中的 vfptr
,然后调用构造函数。即:虚表在构造函数之前写入
2.
除了在构造函数之前写入之外,我们还需要考虑到
虚表的二次写入机制,通过此机制让每个对象的虚表指针都能准确的指向到自己类的虚表,为实现动多态提供支持。