51、将字符串“hello world”从开始到打印到屏幕上的全过程?
-
用户告诉操作系统执行HelloWorld程序(通过键盘输入等)
-
操作系统:找到helloworld程序的相关信息,检查其类型是否是可执行文件;并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址。
-
操作系统:创建一个新进程,将HelloWorld可执行文件映射到该进程结构,表示由该进程执行helloworld程序。
-
操作系统:为helloworld程序设置cpu上下文环境,并跳到程序开始处。
-
执行helloworld程序的第一条指令,发生缺页异常
-
操作系统:分配一页物理内存,并将代码从磁盘读入内存,然后继续执行helloworld程序
-
helloword程序执行puts函数(系统调用),在显示器上写一字符串
-
操作系统:找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程
-
操作系统:控制设备的进程告诉设备的窗口系统,它要显示该字符串,窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储映像区
-
视频硬件将像素转换成显示器可接收和一组控制数据信号
-
显示器解释信号,激发液晶屏
-
OK,我们在屏幕上看到了HelloWorld
52、为什么拷贝构造函数必须传引用不能传值?
- 拷贝构造函数的作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。
a 值传递:
- 对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量);
- 对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);
b 引用传递:
- 无论对内置类型还是类类型,传递引用或指针最终都是传递的地址值!
- 而地址总是指针类型(属于简单类型), 显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用
拷贝构造函数用来初始化一个非引用类类型对象,如果用传值的方式进行传参数,那么构造实参需要调用拷贝构造函数,而拷贝构造函数需要传递实参,所以会一直递归。
53、静态函数能定义为虚函数吗?说说你的理解
不可以定义为虚函数!
- static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。
- 静态与非静态成员函数之间有一个主要的区别,那就是静态成员函数没有this指针。
虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable。对于静态成员函数,它没有this指针,所以无法访问vptr。
这就是为何static函数不能为virtual,虚函数的调用关系:this -> vptr -> vtable ->virtual function。
54、虚函数的代价是什么?
- 带有虚函数的类,每一个类会产生一个虚函数表,用来存储指向虚成员函数的指针,增大类的内存!
- 带有虚函数的类的每一个对象,都会有有一个指向虚表的指针,会增加对象的空间大小;
- 不能再是内联的函数,因为内联函数在编译阶段进行替代,而虚函数在运行阶段才能确定到底是采用哪种函数,虚函数不能是内联函数。
55、说一说你了解到的移动构造函数?
背景:
- 有时候我们会遇到这样一种情况,我们用对象a初始化对象b后,对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;
- 拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制;
- C++引入了移动构造函数,专门处理这种,用a初始化b后,就将a析构的情况;
- 与拷贝类似,移动也使用一个对象的值设置另一个对象的值。
- 但是,又与拷贝不同的是,移动实现的是对象值真实的转移(源对象到目的对象),源对象将丢失其内容,其内容将被目的对象占有。
- 移动操作的发生的时候,是当移动值的对象是未命名的对象的时候。这里未命名的对象就是那些临时变量,甚至都不会有名称。
- 典型的未命名对象就是函数的返回值或者类型转换的对象。使用临时对象的值初始化另一个对象值,不会要求对对象的复制:因为临时对象不会有其它使用,因而,它的值可以被移动到目的对象。
- 做到这些,就要使用移动构造函数和移动赋值:当使用一个临时变量对象进行构造初始化的时候,调用移动构造函数。类似的,使用未命名的变量的值赋给一个对象时,调用移动赋值操作;
Example6 (Example6&& x) : ptr(x.ptr)
{
x.ptr = nullptr;
}
Example6& operator= (Example6&& x)
{
delete ptr;
ptr = x.ptr;
x.ptr=nullptr;
return *this;
}
56、什么时候需要合成拷贝构造函数呢?
有三种情况会以一个对象的内容作为另一个对象的初值:
- 对一个对象做显示的初始化操作,X xx = x;
- 当对象被当做参数交给某个函数时,即传参时;
- 当函数返回一个类对象时;
- 如果一个类没有拷贝构造函数,但是含有一个类类型的成员变量,该类型含有拷贝构造函数,此时编译器会为该类合成一个拷贝构造函数;
- 如果一个类没有拷贝构造函数,但是该类继承自含有拷贝构造函数的基类,此时编译器会为该类合成一个拷贝构造函数;
- 如果一个类没有拷贝构造函数,但是该类声明或继承了虚函数,此时编译器会为该类合成一个拷贝构造函数;
- 如果一个类没有拷贝构造函数,但是该类含有虚基类,此时编译器会为该类合成一个拷贝构造函数;
57、构造函数的执行顺序是什么?
- 在派生类构造函数中,先执行所有的虚基类及上一层基类的构造函数调用;
- 对象的vptr被初始化;
- 如果有成员初始化列表,将在构造函数体内扩展开来,这必须在vptr被设定之后才做;
- 执行程序员所提供的代码;
58、哪些函数不能是虚函数?把你知道的都说一说
- 构造函数,虚函数对应一个vtable(虚函数表),类中存储一个vptr指向这个vtable。如果构造函数是虚函数,就需要通过vtable调用,可是对象没有初始化就没有vptr,无法找到vtable,所以构造函数不能是虚函数。
- 内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;
- 静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。
- 友元函数,**友元函数不属于类的成员函数,不能被继承。**对于没有继承特性的函数没有虚函数的说法。
- 普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。
59、什么是纯虚函数,与虚函数的区别?
虚函数和纯虚函数区别?
虚函数:
- 虚函数是为了实现动态编联产生的,目的是通过基类类型的指针指向不同派生类/基类对象时,自动调用相应的、和基类同名的函数(使用同一种调用形式,既能调用派生类又能调用基类的同名函数)。
- 虚函数需要在基类中加上virtual修饰符修饰,因为virtual会被隐式继承,所以子类中相同函数都是虚函数。
- 当一个成员函数被声明为虚函数之后,其派生类中同名函数自动成为虚函数,在派生类中重新定义此函数时要求函数名、返回值类型、参数个数和类型全部与基类函数相同。
纯虚函数:
- 纯虚函数只是相当于一个接口名,但含有纯虚函数的类不能够实例化。
- 纯虚函数首先是虚函数,其次它没有函数体,取而代之的是用“=0”;
- 既然是虚函数,它的函数指针会被存在虚函数表中,由于纯虚函数并没有具体的函数体,因此它在虚函数表中的值就为0,而具有函数体的虚函数则是函数的具体地址。
- 一个类中如果有纯虚函数的话,称其为抽象类。抽象类不能用于实例化对象,否则会报错。
- 抽象类一般用于定义一些公有的方法。
- 子类继承抽象类也必须实现其中的纯虚函数才能实例化对象。
举个例子:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun1()
{
cout << "普通虚函数" << endl;
}
virtual void fun2() = 0;
virtual ~Base() {}
};
class Son : public Base
{
public:
virtual void fun2()
{
cout << "子类实现的纯虚函数" << endl;
}
};
int main()
{
Base* b = new Son;
b->fun1();
b->fun2();
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
60、介绍面向对象的三大特性,并且举例说明
三大特性: 封装、继承和多态。
1、封装
- 数据和代码捆绑在一起,避免外界干扰和不确定性访问。
- 封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰。
2、继承
- 让某种类型对象获得另一个类型对象的属性和方法;
- 它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
常见的继承有三种方式:
- 实现继承:指使用基类的属性和方法而无需额外编码的能力;
- **接口继承:**指仅使用属性和方法的名称、但是子类必须提供实现的能力(抽象类)
3、多态
同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为。
重载实现编译时多态,虚函数实现运行时多态。
允许将子类类型的指针赋值给父类类型的指针。
实现多态有二种方式:覆盖(override),重载(overload)。
覆盖(编译时多态): 是指子类重新定义父类的虚函数的做法。
重载(运行时多态):
- 是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。
- 例如:基类是一个抽象对象——人,那教师、运动员也是人,而使用这个抽象对象既可以表示教师、也可以表示运动员。