免费做国际贸易的网站,唐山网站制作方案,巴中网站建设网站推广,兰州手机网站建设目录
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;
}// Aclass A class B class C class D
// Bclass D class B class C class A
// Cclass D class C class B class A
// Dclass 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;
} 运行结果如下 可以看到 虚表指针是在代码段和字符常量区之间的事实上菱形虚拟继承中的虚基表也是在这个范围之间的。 最后再补充一句 对象中只有虚表指针而无虚表虚表指针是在类的构造函数中初始化的而虚表是在编译阶段就生成了的。