笔试题
静态成员函数的使用规则
1.通过类名调用静态成员函数和非静态成员函数
2.通过类的对象调用静态成员函数和非静态成员函数
3.在类的静态成员函数中使用类的非静态成员
4.在类的非静态成员函数中使用类的静态成员
5.使用类的静态成员变量
虚函数的工作原理
有关虚函数注意事项
最近秋招,学长们都在答题,我也稍微关注了一点动向。先不说编程题,就前面的选择题有时候就能检验一个人的知识或者扩展知识是否达标。大家在学习的时候可能会认真的看完一本书,但企业的笔试题不仅有书上的知识,还有一些延伸或者说是比较偏的知识。这需要我们自己动手检验,才能对某个知识点牢固的掌握,一步一步走向 "精通C++" 的道路(哈哈哈)。本篇先解决两道C++选择题,因为题中涉及到虚函数相关知识。在解答完选择题之后会复习虚函数实现的原理。其实类似的题我在牛客网上也见过,当时不以为然,认为这种题完全没有必要,现在回想起来感觉自己太天真了。如果真的是自己遇到了怎么办?唯一的办法就是打好基础,不要轻视或忽略任何一个知识点。
第一题:关于C++,以下说法正确的是()
A.构造函数可以声明为虚函数
B.纯虚的析构函数可以不用实现
C.静态成员函数的多态也是通过声明为虚函数来实现
D.一个类成员函数无法同时声明为模板函数和虚函数
第二题(选择题):
class Test
{
public:
Test(){}
virtual ~Test(){}
void print()
{
cout << "Test" <
先从第一题开始,A选项明显是错的,构造函数不能是虚函数,具体理由在后面会总结出来。我们可以尝试将构造函数声明为虚函数。会有以下结果:
结论:构造函数不能被声明为虚函数。
以下代码可以验证B选项是否正确:
结论:在不需要实例化Test类对象的情况下,程序不会报错,表明纯虚的析构可以不用实现。
C选项的情况我也敲了代码,有以下这几种情况。情况一,声明一个静态成员函数为虚函数:
#include
#include
using namespace std;
class Test
{
public:
Test(int a){ m = a;}
~Test();
virtual void print(int a);
static void output(){ cout << "Static output" <
结论:函数不能同时声明为静态和虚函数。
情况二,声明一个与静态成员函数同名的虚函数:
#include
#include
using namespace std;
class Test
{
public:
Test(int a){ m = a;}
~Test();
virtual void print(int a);
static void output(){ cout << "Static output" <
结论:可以顺利运行,但此时它能算是静态成员函数的多态吗?
情况三,静态成员函数重载:
#include
#include
using namespace std;
class Test
{
public:
Test(int a){ m = a;}
~Test();
virtual void print(int a);
static void output(){ cout << "Static output" <
结论:静态成员函数的多态可以通过重载来实现。
D选项情况如下:显示函数不能同时声明为静态和虚函数。
#include
#include
using namespace std;
class Test
{
public:
Test(int a){ m = a;}
~Test();
virtual void print();
template
void print(T a)
{
cout << "Template print" <
结论:一个类成员函数可以同时声明为模板函数和虚函数。
(参考:https://www.cnblogs.com/codingmengmeng/p/5906282.html)
//例子一:通过类名调用静态成员函数和非静态成员函数
class Point{
public:
void init(){}
static void output(){}
};
void main()
{
Point::init();
Point::output();
}
编译出错:错误 1 error C2352: “Point::init”: 非静态成员函数的非法调用。结论一:不能通过类名来调用类的非静态成员函数。
//例子二:通过类的对象调用静态成员函数和非静态成员函数
class Point{
public:
void init(){}
static void output(){}
};
void main()
{
Point pt;
pt.init();
pt.output();
}
编译通过。结论二:类的对象可以使用静态成员函数和非静态成员函数。
//例子三:在类的静态成员函数中使用类的非静态成员
#include
using namespace std;
class Point{
public:
void init(){}
static void output(){
cout << "m_x=" << m_x << endl;
}
private:
int m_x;
};
void main()
{
Point pt;
pt.output();
}
编译出错:IntelliSense: 非静态成员引用必须与特定对象相对。因为静态成员函数属于整个类,在类实例化对象之前就已经分配空间了,而类的非静态成员必须在类实例化对象后才有内存空间,所以这个调用就会出错,就好比没有声明一个变量却提前使用它一样。结论三:静态成员函数中不能引用非静态成员。
//例子四:在类的非静态成员函数中使用类的静态成员
#include
using namespace std;
class Point{
public:
void init()
{
output();
}
static void output(){}
private:
int m_x;
};
void main()
{
Point pt;
pt.init();
}
编译通过。结论四:类的非静态成员可以调用静态成员函数,反之不能。
//例子五:使用类的静态成员变量
#include
using namespace std;
class Point{
public:
Point(){
m_nPointCount++;
}
~Point(){
m_nPointCount++;
}
static void output(){
cout << "m_nPointCount=" << m_nPointCount << endl;
}
private:
static int m_nPointCount;
};
//类外初始化静态成员变量时,不用带static关键字
int Point::m_nPointCount = 0;
void main()
{
Point pt;
pt.output();
}
结论五:类的静态成员变量必须先初始化再使用。
另一篇博文《虚函数与纯虚函数》,内容是参考网上的文章和例子。讲述了虚函数和纯虚函数的不同,但并没有涉及到原理。在解决第二个问题前,应先了解以下知识。以下内容源自《C++ Primer Plus》中的13.4。程序调用函数时,将使用哪个可执行代码块?编译器负责回答这个问题。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在C语言中,这非常简单,因为每个函数名都对应一个不同的函数。在C++中,由于函数重载的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而,C/C++编译器可以在编译过程完成这种联编。在编译过程中进行的联编被称为静态联编,又称为早期联编。然而,虚函数使这项工作变得更困难。因为编译器不知道用户将选择什么对象,也不知道使用哪一个函数进行操作。所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,即在运行过程中进行的联编被称为动态联编,又称为晚期联编。
C++规定了虚函数的行为,但将现实方法留给了编译器。不需要知道实现方法就可以使用虚函数,但了解虚函数的工作原理有助于更好地理解概念。通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table, vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到vtbl中。注意,无论类中包含的虚函数是1个还是10个,都只需要在对象中添加1个地址成员,只是表的大小不同而已。
使用虚函数时,程序将查看存储在对象中的vtbl地址,然后转向相应的函数地址表。如果使用类声明中定义的第一个虚函数,则程序将使用数组中的第一个函数地址,并执行具有该地址的函数。如果使用类声明中的第三个虚函数,程序将使用地址为数组中第三个元素的函数。
总之,使用虚函数时,在内存和执行速度方面有一定的成本,包括:
每个对象都将增大,增大量为存储地址的空间;
对于每个类,编译器都将创建一个虚函数地址表(数组);
对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。
代码:
#include
#include
using namespace std;
typedef void(*Fun)(void);
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
};
class Derive : public Base{
public:
virtual void f() { cout << "Deriver::f" << endl; }
};
int main()
{
Base a;
Derive b;
cout << "Base虚函数表地址:" << (int*)(&a) << endl;
cout << "Base虚函数表的第一个函数地址:" << (int*)*(int*)(&a) << endl;
cout << "Base虚函数表的第二个函数地址:" << (int*)*(int*)(&(a)+1) << endl;
cout << "Derive虚函数表地址:" << (int*)(&(b)) << endl;
cout << "Derive虚函数表的第一个函数地址:" << (int*)*(int*)(&(b)) << endl;
cout << "Derive虚函数表的第二个函数地址:" << (int*)*(int*)(&(b)+1) << endl;
return 0;
}
通过这个示例,我们可以看到,我们可以通过强行把&b转成int,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int强制转成了函数指针)。我在不同的平台下运行了上述代码,得到了不太一样的结果,原因是编译器不一样。左边是CodeBlocks所运行的结果,右边是Linux运行的结果。Linux平台下运行的结果和《C++ Primer Plus》中的表述的虚函数机制相同。
以下结论基于Linux平台:
一般继承(无虚函数覆盖)
(1)虚函数按照其声明顺序放于表中。
(2)父类的虚函数在子类的虚函数前面。
一般继承(有虚函数覆盖)
(1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
(2)没有被覆盖的函数依旧。
构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类构造函数声明为虚的没有意义。
析构函数应当是虚函数,除非类不用做基类。试想这么一种情况:类对象中有new分配的内存,如何根据不同的情况释放相应的内存以防止内存泄露?此时,将析构函数声明为虚函数能很好解决这一问题(将相应的delete语句放在析构函数中)。
友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。如果由于这个原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决。
如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本。
假设创建了如下所示代码:
#include
#include
using namespace std;
class A
{
public:
virtual void show(int a) const{cout << "A" <
这将导致以下问题:
新定义将show()定义为一个不接受任何参数的函数。重新定义不会生成函数的两个重载版本,而是隐藏了接受一个int参数的基类版本。总之,重新定义继承的方法并不是重载。如果重新定义派生类中的函数,将不只是使用相同的函数参数列表覆盖基类声明,无论参数列表是否相同,该操作将隐藏所有的同名基类方法。
这引出了两条经验规则:第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变(只适用于返回值,而不适用于参数),因为允许返回类型随类类型的变化而变化:
class A
{
public:
virtual A show(int a) const{cout << "A" <
第二,如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。
class A
{
public:
virtual void show(int a) const;
virtual void show(double b) const;
virtual void show() const;
};
class B : public A
{
public:
virtual void show(int a) const;
virtual void show(double b) const;
virtual void show() const;
};
如果只重新定义一个版本,则另外几个版本将被隐藏,派生类对象将无法使用它们。注意,如果不需要修改,则新定义可只调用基类版本:void B::show() const { A::show(); }
第二题运行结果如下:
其实这道题很简单,经常在牛客网上看到类似的计算类占多少个字节的题。但这种考到虚函数的就我而言还是第一次遇到。要是不知道虚函数的原理,可能就没有办法选出正确的答案。