1. 画多重继承虚函数表
注:虚函数表是一种编译时构建的数据结构,它用于在运行时解析对虚函数的调用,是一个存储类成员函数指针的数组。每个拥有虚函数的类都有一个对应的虚函数表。当类对象创建时,对象中会包含一个指向相应虚函数表的指针(称为vptr,虚指针)。这个指针是对象的隐式成员,由编译器自动添加到每个对象中。
每个对象的虚指针指向对应的虚函数表。虚函数表的布局和内容在编译时就已确定,里面存储的是类的虚函数的地址。当一个虚函数被调用时,实际上是通过虚指针来间接调用的。
虚函数表运行时才会被调用,允许通过基类指针或引用调用派生类的方法。特别灵活,子类可以覆盖父类的虚函数实现,而调用者无需知道具体的子类类型。
当然有额外的开销,虚函数调用通常比非虚函数调用慢,因为需要额外的间接寻址。内存也有额外的开销,每个对象需要额外存储一个虚指针,每个类需要一个虚函数表。
2. 循环与递归可以转换吗?各有啥特点
循环和递归是在编程中实现重复操作的两种基本方法。它们在功能上是等效的,理论上任何使用循环的代码都可以通过递归来实现,反之亦然。尽管如此,它们各有其特点和最适用的场景。
从循环到递归:将循环结构的代码转换成递归通常涉及将循环的迭代过程表达为递归调用。每次递归调用相当于循环的一次迭代,并在满足基本条件时结束递归。
从递归到循环:递归函数通常可以重写为使用栈的循环结构,其中栈用来模拟递归调用栈的行为。这种转换可以消除递归的开销,特别是对于深递归调用栈可能导致栈溢出的情况
递归特点:
自然表达:递归提供了一种简洁的方式来处理那些问题本身就是递归的,如树遍历、分治算法等。
简化复杂问题:递归可以将复杂问题分解为更小的子问题,每个子问题与原问题有着相同的形式。
代码简洁:递归代码通常比对应的循环版本更简洁、更易于理解。
性能开销:每次递归调用都需要在调用栈上增加一层,这会增加额外的时间和空间开销。
栈溢出风险:深度递归可能导致调用栈过深,从而引发栈溢出错误。
效率问题:递归可能重复解决相同的子问题,尤其是在没有使用记忆化技术的情况下。
循环特点:
效率高:循环不涉及多次函数调用的开销,因此通常比递归更快。
避免栈溢出:使用循环不会增加调用栈深度,因此不会因深度递归而导致栈溢出。
直观执行流:循环的执行流是顺序的,没有调用栈的额外复杂性,对于追踪程序状态和调试通常更为直接。
代码复杂度:对于本质上递归的算法(如深度优先搜索),用循环实现可能导致代码更加复杂
手动管理状态:在某些复杂的情况下,需要手动模拟栈来保存状态,这可以增加实现的难度。
3. 构造函数与析构函数作用及调用时机
构造函数与析构函数是被编译器隐式调用的。这些函数的调用时间取决于程序执行进入和离开实例化对象的作用域的顺序。通常,析构函数的调用顺序和对应构造函数的调用顺序相反。但是,对象的存储类别可以改变析构函数的调用顺序。
在全局作用域内定义的构造函数在该文件中任何其他函数(包括main函数)开始执行之前执行(尽管文件间的构造函数的执行顺序是不确定的)。在main函数终止时,调用相应的析构函数。exit函数强制程序立即终止并且不执行自动对象的析构函数。该函数常用于在检测到输入错误或打不开要处理的文件时终止程序。abort函数类似于exit函数,但它强制程序立即终止,不允许调用任何对象的析构函数。abort通常用于指示程序的异常中断。
自动局部对象的构造函数在执行到达对应的程序点时调用,对应的析构函数在对象离开该对象所在的作用域时(即定义该对象的执行结束时)调用。自动局部对象的构造函数和析构函数在每次到达和离开该对象的作用域时调用。如果程序使用exit和abort函数终止,则不调用自动对象的析构函数。
static局部对象的构造函数只在执行第一次到达定义对象的程序点时调用一次。对应的析构函数在main函数终止或调用exit函数时调用。全局和static对象的释放顺序和创建顺序相反。如果遇到调用abort函数终止程序,则不调用static对象的析构函数。
4. 进程与线程区别,列出五点
5. 写一个内存拷贝函数
void* copy(void* src,void*dst,int count)
{
assert((src != NULL)&&(dest !=NULL));
char*tmp, *s;
if(dest <=src)
{
tmp = (char*) dest;
s = (char*) src;
while(count--)
*tmp++ = *s++;
}
else
{
tmp = (char*) dest +count;
s = (char*) src +count;
while(count--)
*--tmp = *--s;
}
return dst;
}
6. 写一个快排