目录
1. 第一题
2. 第二题
3. inline 函数可以是虚函数吗
4. 静态成员函数可以是虚函数吗
5. 构造函数可以是虚函数吗
6. 析构函数可以是虚函数吗
7. 拷贝构造和赋值运算符重载可以是虚函数吗
8. 对象访问普通函数快还是访问虚函数快
9. 虚函数表是什么阶段生成的?存在哪里的?
1. 第一题
class A
{
public:
virtual void Func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void Test() { Func(); }
};
class B : public A
{
public:
void Func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main()
{
B* ptr = new B;
ptr->Test();
return 0;
}
// A: A->0
// B: B->1
// C: A->1
// D: B->0
// E: 编译出错
// F: 以上都不正确
答案是什么呢?
分析过程:
- 首先,派生类 B 继承 A类,会将B类的方法继承下来,但注意,继承是派生类有访问基类方法的权限,而并不是说基类的方法在派生类中也有一份,继承后的基类方法依旧属于基类;
- 其次,多态的条件以及注意多态是接口继承;
- 最后,根据多态判别调用什么方法,即指向的什么对象,就调用谁的方法。
如下:
2. 第二题
class A{
public:
A(char *s) { std::cout << s << std::endl; }
~A(){}
};
class B :virtual public A
{
public:
B(char *s1, char*s2) :A(s1) { std::cout << s2 << std::endl; }
};
class C :virtual public A
{
public:
C(char *s1, char*s2) :A(s1) { std::cout << s2 << std::endl; }
};
class D :public B, public C
{
public:
D(char *s1, char *s2, char *s3, char *s4) :B(s1, s2), C(s1, s3), A(s1)
{
std::cout << s4 << std::endl;
}
};
int main() {
D *p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
// A:class A class B class C class D
// B:class D class B class C class A
// C:class D class C class B class A
// D:class A class C class B class D
分析过程如下:
第一个问题:为什么D类的实例化对象要显示调用A的构造呢?并且此时我们发现如果不调用A的构造还会报错,如下:
class D :public B, public C
{
public:
D(char *s1, char *s2, char *s3, char *s4) :B(s1, s2), C(s1, s3) /* A(s1) */
{
std::cout << s4 << std::endl;
}
};
现象如下:
原因是因为这是一个菱形虚拟继承 ,如图所示:
而菱形虚拟继承带来的结果就是A只有一份,既然只有一份(B和C类共享),在B和C类进行初始化是不合适的。因此需要在D类调用A的构造函数。
但是我们发现,B和C类也在初始化列表中显式调用了A的构造函数。那这是为什么呢?因为有些情况下,我们可能会单独实例化B和C的对象,此时就需要在B和C中初始化A类了。因此B和C类也需要显示调用A的构造函数。
但是对于D实例化的对象来说,只会在D中对A类的资源进行初始化。
清楚了这个问题 ,接下来就简单了,之前说过,初始化列表的初始化顺序是由继承的先后顺序决定的。谁先继承,就先初始化谁。而在这里,继承的先后顺序:A、B、C;
因此,初始化列表的初始化顺序:A、B、C
即最后的答案就是 class A class B class C class D
3. inline 函数可以是虚函数吗
我们之前学习过 inline 函数,内联函数的特点就是:
会在调用的地方展开,潜台词就是没有函数地址,而虚函数的地址会进入虚函数表,那么既然内联函数都没有地址了,也就不可能是虚函数了。
因此我们的结论就是:inline不可以是虚函数。
但是,结果不是这样:
class A
{
public:
inline virtual void Func()
{
std::cout << "haha" << std::endl;
}
};
void Test22()
{
A a;
a.Func();
}
现象如下:
当我们用A实例化的对象a去调用 Func() 时,发现不仅没有编译报错,还能正常调用 Func()。
那是不是我们分析错了呢?
- 首先,内联函数我们当初学的时候说过,inline 只是一个建议,具体这个函数最后会不会是一个内联函数是由编译器决定的;
- 具体就是,如果编译器认为这个函数是符合需求的 (如没有递归,且代码量很少) 那么编译器就会将这个函数声明为 inline 函数,会在调用的地方展开该函数。
因此,最后的结论就是,inline函数可以是虚函数,但是这个 inline 是否会有效,即 inline 函数是否会在调用的地方展开,就不一定了,测试 demo 如下:
class A
{
public:
inline virtual void Func()
{
std::cout << "haha" << std::endl;
}
};
class B
{
public:
virtual void Func()
{
std::cout << "hehe" << std::endl;
}
};
void Test23(void)
{
A* ptr = new B;
// 多态调用
ptr->Func();
A a;
// 普通调用
a.Func();
}
现象如下:
多态调用,但此时函数未被展开,即 inline 无效。
普通调用,此时函数就被展开了,inline 有效。
可以看到,如果一个虚函数被声明为 inline 时:
- 如果这个函数是多态调用,inline 就会失效;
- 如果这个函数是普通调用,inline 就会有效,但最后该函数会不会被展开 (inline是否有效) 是由编译器决定的。
总而言之,inline 函数可以是虚函数。
4. 静态成员函数可以是虚函数吗
测试 demo 如下:
class A
{
public:
static virtual void Func()
{
std::cout << "haha" << std::endl;
}
};
void Test24(void)
{
A a;
a.Func();
}
现象如下:
可以看到,故静态成员函数不可以是虚函数,为什么呢?
- 首先,静态成员函数是没有 this 指针的 (因为它属于整个类,而不属于某个对象),没有 this 指针就无法访问对象中的虚表指针,也就无法找到虚表;
- 而虚函数存在的价值就是为了构成多态,而静态成员函数都无法访问虚表,怎么能构成多态呢? 因此,将虚函数声明为静态函数是无意义的,编译器进行了强制检查,如果一个虚函数是静态的,那么会编译报错。
总而言之,静态成员函数不可以是虚函数。
5. 构造函数可以是虚函数吗
测试 demo 如下:
class A
{
public:
virtual A() { std::cout << "A()" << std::endl; }
};
void Test25(void)
{
A a;
}
现象如下:
可以看到,发生了编译报错,构造函数不可以是虚函数,为什么呢?
首先我们需要搞明白一个问题:对象中的虚表指针是在什么创建好的呢? 测试 demo 如下:
class A
{
public:
A() { std::cout << "A()" << std::endl; }
virtual void Func() { std::cout << "haha" << std::endl; }
};
void Test25(void)
{
A a;
}
启动进程,调出监视窗口,如下:
可以看到,当 A 实例化的对象 a 还没有进入构造函数之前,具体是初始化列表的时候,虚表指针是没有被初始化的 (虚表是在编译阶段就已经构造好了)。
可以看到对象中的虚表的指针是在初始化列表阶段中才进行初始化的。
那么也就是说先在初始化列表中初始化虚表指针,但如果此时将构造函数声明为虚函数,而虚函数的多态调用,需要到虚表去找,但是此时虚表指针都没有被初始化,怎么找到虚表你呢?此时就出问题了。
因此如果将构造函数定义为虚函数,那么此时构造函数无法进入虚表 (找不到虚表),换言之,构造函数不可以是虚函数。
6. 析构函数可以是虚函数吗
可以,并且最好是将析构函数定义为虚函数。
因为这样就可以做到,如果我指向的是一个基类,调用的就是基类的析构;如果我指向的是一个派生类,调用的是派生类的析构,可以做到合理释放资源。
7. 拷贝构造和赋值运算符重载可以是虚函数吗
拷贝构造不可以是虚函数,因为拷贝构造函数也是一个构造函数,原因与构造函数类似;
赋值运算符重载可以是虚函数,因为调用赋值的两个对象是已经存在的对象,既然已经存在的对象,如果有虚函数,那么虚表的指针是被初始化过了的,也就是说赋值运算符重载可以进入虚表,虽然赋值运算重载可以是虚函数,但是赋值运算符重载实现多态是没有实际价值的。
8. 对象访问普通函数快还是访问虚函数快
- 如果符合多态调用,访问普通函数快,因为此时调用虚函数是一个运行时决议,需要去虚表中找虚函数的地址;
- 如果符合普通调用,且此时调用虚函数是一个编译时决议,那么一样快。
9. 虚函数表是什么阶段生成的?存在哪里的?
构造函数中的初始化列表阶段初始化的是虚函数表的指针(虚表指针是存于对象中的),不是虚函数表,虚函数表是编译阶段时生成的。
那虚函数表存在哪里呢?
首先看看虚拟进程地址空间,具体如下:
我们用下面的 demo 验证下虚表的大概位置:
class A
{
public:
A() {}
virtual void Func() { std::cout << "Func()" << std::endl; }
};
int global_val = 10;
int main()
{
A a;
// 代码段的地址
printf("code address: %p\n", main);
// 字符常量区的地址
const char* str = "haha\n";
printf("string address: %p\n", str);
// 静态区的地址
static int i = 0;
printf("static address: %p\n", &i);
// 全局变量的地址
printf("global address: %p\n", &global_val);
// 虚表的地址
printf("vft_ptr: %p\n", *(int*)(&a));
return 0;
}
运行结果如下:
可以看到, 虚表指针是在代码段和字符常量区之间的,事实上,菱形虚拟继承中的虚基表也是在这个范围之间的。
最后,再补充一句:
- 对象中只有虚表指针,而无虚表;
- 虚表指针是在类的构造函数中初始化的,而虚表是在编译阶段就生成了的。