本篇文章连问面试时经常会遇到的类和继承相关25个问题,看看你能回答出几道题呀。
还是先看一下思维导图,如下:
c++的三大特性,说白了其实就是面向对象的三大特性,是指:封装、继承、多态,简单说明如下:
优点:根据第1点中讲的,其实继承优点就是实现了代码的重用和接口的重用;
缺点:子类会继承父类的部分行为,父类的任何改变都可能影响子类的行为,也就是说,如果继承下来的实现不适合子类的问题,那么父类必须重写或者被其他的类替换,这种依赖关系限制了灵活性。
从以上对比看,同一种属性既可以是优点,也可以是缺点,就看个人在编程过程中的灵活运用了。
看代码:
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
class B:public A
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
};
int main()
{
B b;
return 0;
}
输出结果如下:
A()
B()
~B()
~A()
根据结果,可知顺序如下:
个人理解,其实就是实现了接口的重用,同样的接口,派生类与基类不同的实现。
一般来讲多态分为编译时多态和运行时多态,编译时多态就是指的重载哪些,我们通常默认多态是运行时的多态。
运行时多态简单来讲就是:使用基类指针或者引用指向一个派生类对象,在非虚继承的情况下,派生类直接继承基类的虚表指针,然后使用派生类的虚函数去覆盖基类的虚函数,这样派生类对象通过虚表指针访问到的虚函数就是派生类的虚函数了。
更详细的说明请看之前写的这篇文章:c++头脑风暴-多态、虚继承、多重继承内存布局
重载即为函数重载,重载的特征:
覆盖是指派生类函数覆盖基类函数,覆盖的特征:
隐藏是指派生类的函数屏蔽了与其同名的基类函数,特征如下:
总结:函数名相同,参数也相同的情况下,如果基类函数有virtual关键字,则是多态,否则就是隐藏;函数名相同,参数不同的情况下,如果函数位于同一个类中,则是重载,否则就是隐藏。
析构函数可以是虚函数,因为它是对象结束时才调用,不影响虚表构建。
那么析构函数作为虚函数有什么作用呢,看这样一段代码:
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
class B:public A
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
};
int main()
{
A* a = new B;
if ( a != nullptr )
{
delete a;
}
return 0;
}
这段代码执行后输出如下:
A()
B()
~A()
构造的时候是正常的,但是析构的时候只调用了基类的析构函数,此时我们把类A的析构函数修改为virtual,看看结果:
A()
B()
~B()
~A()
一般情况下,只有当一个类被用作基类时才需要使用虚析构函数,这样做的作用是当一个基类的指针删除派生类的对象时,能确保派生类的析构函数会被调用。因为销毁的时候直接销毁的基类指针,此时编译器只知道调用基类析构,并不会主动去调用派生类的析构函数,所以基类析构函数需为虚析构函数,这样运行时程序才会去调用派生类的析构函数,其实这就相当于析构函数的多态,基于多态的作用,这个指向派生类的基类指针会先调用派生类的析构函数,然后再调用基类的析构函数。
所以当类有派生类时,析构函数一定要是虚函数。
初始化列表和赋值的区别如下:
实际上,根据上面第8点,赋值是先声明以后再赋值的,我们初次接触c++的时候就应该知道有些类型是必须要声明的时候就有初值的,这里我想到的有以下类型:
以上三种情况都必须使用初始化列表而不能在构造函数中进行赋值。
多重继承时需要使用虚继承,一般的我们在多重继承时使用虚继承来防止二义性问题。
看下面这段代码:
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout << "A()" << endl;
}
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B: public A
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
};
class C: public A
{
public:
C()
{
cout << "C()" << endl;
}
~C()
{
cout << "~C()" << endl;
}
};
class D:public B, public C
{
};
int main()
{
D d;
return 0;
}
执行后输出结果如下:
A()
B()
A()
C()
~C()
~A()
~B()
~A()
看到没有类A的构造函数和析构函数都执行了两次,这很显然是不正确的,因为执行类B构造函数时要执行一次类A的构造函数,执行类C的时候也要执行一次类A的构造函数,析构函数同理,到这里问题还不大,毕竟可以编译和运行。
把代码改一下,如下:
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout << "A()" << endl;
}
void print()
{
cout << "print()" << endl;
}
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B: public A
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
};
class C: public A
{
public:
C()
{
cout << "C()" << endl;
}
~C()
{
cout << "~C()" << endl;
}
};
class D:public B, public C
{
};
int main()
{
D d;
d.print();
return 0;
}
编译直接就报错了:
test.cpp:54:4: 错误:对成员‘print’的请求有歧义
这就是二义性了,解决办法是使用形如class C: virtual public A
这样的虚继承形式,B虚继承A,C也虚继承A,那样就可以编译通过,且运行也都是没有问题的。
防止类对象被拷贝和赋值,无非是禁止类对象调用拷贝构造函数和赋值函数,在c++11以后有三种方法:
答案是不可以,构造函数是不能声明为virtual的,这与虚函数的机制有关,虚函数是存放在虚表的,而虚表是在构造函数执行过程中才建立的,构造函数声明为virtual就会陷入到是先有鸡还是先有蛋的尴尬境地,所以编译器做了限制。
构造函数可以抛出异常,若有动态分配内存,则要在抛异常之前手动释放。
有关构造函数最全面的说明请看这篇文章:最全面的c++中类的构造函数高级使用方法及禁忌
区别如下:
struct的成员默认是公有的,class的成员默认是私有的。
一个原则:当类中有很少的方法并且有公有数据时,应该使用struct关键字,否则使用class关键字。
可以,但是最好不要抛出,如果一定要抛出,那要在析构函数内部处理,保证析构函数能执行完成。
可以调用,因为虚函数表是在编译期建立的,当调用构造函数时,首先就会初始化虚函数指针,那我们就知道了虚函数的地址,当然可以调用虚函数了。
在函数前面加上friend,这个函数就变成了友元函数,它代表这个函数与某个类成为朋友了,此时访问类的私有成员也是不受限制的。
与友元函数类似,在一个类A中声明另外一个类B为friend类型,那么这个类B就是友元类,它访问类A的私有成员和保护成员都不受限制。
有关友元详细说明,请看这篇文章:c++类访问权限及友元
违反了,友元函数可以不受访问权限的限制而访问类的任何成员,也就是它可以直接接触类的实现,当然违反了封装的原则,只是有时基于我们自身的某些使用场景,不得不使用友元。
非虚继承时,按照继承顺序存储,虚继承时,虚基类的内容放在一块内存的最后面存储。
详细的看之前这篇文章:c++头脑风暴-多态、虚继承、多重继承内存布局
由成员变量和是否有虚函数决定,如果类中有虚函数,那就在所有成员变量的基础上加上一个虚函数指针的大小,在64位机器中,虚函数指针为8个字节,注意计算类大小的时候要考虑字节对齐的问题。
空类大小为1个字节。
new其实就是申请动态内存,而一个类只有虚指针和成员变量才需要内存,所以new一个类就是给虚指针和成员变量申请内存空间。
有呀,编译器编译的时候就给了成员函数地址,且一个类的成员函数是唯一的,所有对象共用。
可以的,看以下代码:
#include <iostream>
using namespace std;
class CPeople
{
public:
double height;
int age;
char sex;
public:
CPeople(){}
~CPeople(){}
void print()
{
cout << "print()" << endl;
}
};
int main()
{
CPeople *people = nullptr;
people->print();
return 0;
}
粗粗一看,代码使用了空指针调用,结合我们知道的,如果使用了空指针,就会发生段错误,那这里肯定也会发生段错误,但实际上编译执行后并没有产生错误,print函数被正确执行了,这就很尴尬了,这是为什么呢?
这是因为类的成员函数的实现机制,上题说了,类的成员函数跟某个对象无关,实际上它被编译后,我们可以把它理解为一个全局性的函数,从汇编的角度看,print函数被编译后真正的函数名是_ZN7CPeople5printEv
这个,并且此时因为print函数没有使用类CPeople的任何成员,它当然可以正常的执行。
但是,假设在print里面调用了某个成员变量呢,如下:
#include <iostream>
using namespace std;
class CPeople
{
public:
double height;
int age;
char sex;
public:
CPeople(){age = 100;}
~CPeople(){}
void print()
{
cout << "age=" << this->age << endl;
}
};
int main()
{
CPeople *people = nullptr;
people->print();
return 0;
}
这次再执行就会报段错误了,为什么呢,因为成员函数是公用的,但是成员变量却是每个对象独有的,没有为people分配空间,就是没给成员变量分配空间,且此时people为空指针,那给成员函数传入的隐形this指针也是空指针,它怎么可能访问到某个成员变量呢。
看一下这段代码:
class CPeople
{
public:
CPeople(){}
~CPeople(){}
virtual void print() = 0;
};
这段代码里面print就是纯虚函数,所谓纯虚函数其实就是虚函数后面加= 0
,此时print函数是不需要实现的,它只是定义了一个抽象接口而已。
同样的,这段代码里面的CPeople就是抽象类了,某个类不论是自己定义了纯虚函数,还是从其他基类继承了纯虚函数但却并没有实现的,都可以称为抽象类,所谓抽象,其实就是具体的反义词,比方说这里只给了一个接口,但是接口到底是怎么实现的,不知道,这就叫做抽象了。
好了,本篇文章就为大家介绍到这里,觉得内容对你有用的话,记得顺手点个在看哦~