54、形参与实参的区别?
形参变量只有在被调用时才分配内存单元,在调用结束时, 即刻释放所分配的内存单元。因此,形参只有在函数内部有效。函数调用结束返回主调函数后则不能再使用该形参变量。
实参可以是常量、变量、表达式、函数等, 无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值, 以便把这些值传送给形参。因此应预先用赋值,输入等办法使实参获得确定值,会产生一个临时变量。
实参和形参在数量上,类型上,顺序上应严格一致, 否则会发生“类型不匹配”的错误。
函数调用中发生的数据传送是单向的。即只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化。
当形参和实参不是指针类型时,在该函数运行时,形参和实参是不同的变量,他们在内存中位于不同的位置,形参将实参的内容复制一份,在该函数运行结束的时候形参被释放,而实参内容不会改变。
58、从汇编层去解释一下引用
9: int x = 1;
00401048 mov dword ptr [ebp-4],1
10: int &b = x;
0040104F lea eax,[ebp-4]
00401052 mov dword ptr [ebp-8],eax
x的地址为ebp-4,b的地址为ebp-8,因为栈内的变量内存是从高往低进行分配的,所以b的地址比x的低。
lea eax,[ebp-4] 这条语句将x的地址ebp-4放入eax寄存器
mov dword ptr [ebp-8],eax 这条语句将eax的值放入b的地址
ebp-8中上面两条汇编的作用即:将x的地址存入变量b中,这不和将某个变量的地址存入指针变量是一样的吗?所以从汇编层次来看,的确引用是通过指针来实现的。
60、C++模板是什么,你知道底层怎么实现的?
编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误。
68、成员列表初始化?
- 必须使用成员初始化的四种情况
① 当初始化一个引用成员时;
② 当初始化一个常量成员时;
③ 当调用一个基类的构造函数,而它拥有一组参数时;
④ 当调用一个成员类的构造函数,而它拥有一组参数时;
- 成员初始化列表做了什么
① 编译器会一一操作初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何显示用户代码之前;
② list中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的;
70、对象复用的了解,零拷贝的了解
对象复用
对象复用其本质是一种设计模式:Flyweight享元模式。
通过将对象存储到“对象池”中实现对象的重复利用,这样可以避免多次创建重复对象的开销,节约系统资源。
零拷贝
零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。
零拷贝技术可以减少数据拷贝和共享总线操作的次数。
在C++中,vector的一个成员函数emplace_back()很好地体现了零拷贝技术,它跟push_back()函数一样可以将一个元素插入容器尾部,区别在于:使用push_back()函数需要调用拷贝构造函数和转移构造函数,而使用emplace_back()插入的元素原地构造,不需要触发拷贝构造和转移构造,效率更高。举个例子:
#include <vector>
#include <string>
#include <iostream>
using namespace std;
struct Person
{
string name;
int age;
//初始构造函数
Person(string p_name, int p_age): name(std::move(p_name)), age(p_age)
{
cout << "I have been constructed" <<endl;
}
//拷贝构造函数
Person(const Person& other): name(std::move(other.name)), age(other.age)
{
cout << "I have been copy constructed" <<endl;
}
//转移构造函数
Person(Person&& other): name(std::move(other.name)), age(other.age)
{
cout << "I have been moved"<<endl;
}
};
int main()
{
vector<Person> e;
cout << "emplace_back:" <<endl;
e.emplace_back("Jane", 23); //不用构造类对象
vector<Person> p;
cout << "push_back:"<<endl;
// p.push_back("Mike", 36); // 这种写法是无法通过编译
p.push_back(Person("Mike",36));
return 0;
}
//输出结果:
//emplace_back:
//I have been constructed
//push_back:
//I have been constructed
//I am being moved.
而使用如下的方式,则两者间无差别。
int main()
{
Person a("Jane", 23);
vector<Person> e;
cout << "emplace_back:" << endl;
e.emplace_back(a);
cout << "===================" << endl;
Person b("Mike", 36);
vector<Person> p;
cout << "push_back:" << endl;
p.push_back(b);
return 0;
}
72、介绍面向对象的三大特性,并且举例说明
三大特性:继承、封装和多态
(1)继承
让某种类型对象获得另一个类型对象的属性和方法。
它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展
常见的继承有三种方式:
实现继承:指使用基类的属性和方法而无需额外编码的能力
接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力
可视继承:指子窗体(类)使用基窗体(类)的外观和实现代码的能力(C++里好像不怎么用)
例如,将人定义为一个抽象类,拥有姓名、性别、年龄等公共属性,吃饭、睡觉、走路等公共方法,在定义一个具体的人时,就可以继承这个抽象类,既保留了公共属性和方法,也可以在此基础上扩展跳舞、唱歌等特有方法
(2)封装
数据和代码捆绑在一起,避免外界干扰和不确定性访问。
封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰。
(3)多态
同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)。
多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单一句话:允许将子类类型的指针赋值给父类类型的指针
实现多态有二种方式:覆盖(override),重载(overload)。覆盖:是指子类重新定义父类的虚函数的做法。重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。例如:基类是一个抽象对象——人,那教师、运动员也是人,而使用这个抽象对象既可以表示教师、也可以表示运动员。
74、成员初始化列表的概念,为什么用它会快一些?
因为类类型的数据成员对象在进入(构造)函数体时已经构造完成。
用初始化列表会快一些的原因是,对于类型,它少了一次调用构造函数的过程,而在函数体中赋值则会多一次调用。而对于内置数据类型则没有差别。
举个例子:
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout << "默认构造函数A()" << endl;
}
A(int a)
{
value = a;
cout << "A(int "<<value<<")" << endl;
}
A(const A& a)
{
value = a.value;
cout << "拷贝构造函数A(A& a): "<<value << endl;
}
int value;
};
class B
{
public:
B() : a(1)
{
cout << "===========" << endl;
// 1、b在进入函数体时,调用默认构造函数
// 2、b再进行拷贝构造
b = A(2);
}
A a;
A b;
};
int main()
{
B b;
}
从代码运行结果可以看出,在构造函数体内部初始化的对象b多了一次构造函数的调用过程,而对象a则没有。
首先把数据成员按类型分类
1。内置数据类型,复合类型(指针,引用)
2。用户定义类型(类类型)
分情况说明:
对于类型1,在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的
对于类型2,结果上相同,但是性能上存在很大的差别
因为类类型的数据成员对象在进入(构造)函数体时已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,这是调用一个构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)
为什么用成员初始化列表会快一些
75、(超重要)构造函数为什么不能为虚函数?析构函数为什么要虚函数?
1、 从存储空间角度,虚函数相应一个指向vtable虚函数表的指针,这大家都知道,但是这个指向vtable的指针事实上是存储在对象的内存空间的。
问题出来了,假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
2、 从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到相应的调用。
构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。
所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
3、构造函数不须要是虚函数,也不同意是虚函数,由于创建一个对象时我们总是要明白指定对象的类型,虽然我们可能通过实验室的基类的指针或引用去訪问它但析构却不一定,我们往往通过基类的指针来销毁对象。这时候假设析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
4、从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(由于子类会调父类的构造函数);并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,也没有必要成为虚函数。
5、当一个构造函数被调用时,它做的首要的事情之中的一个是初始化它的VPTR。
因此,它仅仅能知道它是“当前”类的,而全然忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(由于类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。
并且,仅仅要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE, 但假设接着另一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的 VTABLE,等.直到最后的构造函数结束。
VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的还有一个理由。可是,当这一系列构造函数调用正发生时,每一个构造函数都已经设置VPTR指向它自己的VTABLE。假设函数调用使用虚机制,它将仅仅产生通过它自己的VTABLE的调用,而不是最后的VTABLE(全部构造函数被调用后才会有最后的VTABLE)。
因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。
直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。
具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。
所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。