8.虚拟继承的主要作用
c++中允许一个类继承自多个父类
- 一个子类可以拥有多个父类
- 子类拥有所有父类的成员变量
- 子类继承所有父类的成员函数
- 子类对象可以当作任意父类对象使用
#include <iostream> #include <string> using namespace std; class BaseA { int ma; public: BaseA(int a) { ma = a; } int getA() { return ma; } }; class BaseB { int mb; public: BaseB(int b) { mb = b; } int getB() { return mb; } }; class Derived : public BaseA, public BaseB//多重继承的语法规则 { int mc; public: Derived(int a, int b, int c) : BaseA(a), BaseB(b) //初始化列表 { mc = c; } int getC() { return mc; } void print() { cout << "ma = " << getA() << ", " << "mb = " << getB() << ", " << "mc = " << mc << endl; } }; int main() { cout << "sizeof(Derived) = " << sizeof(Derived) << endl; // 12 Derived d(1, 2, 3); d.print();//1 2 3 cout << "d.getA() = " << d.getA() << endl;//1 cout << "d.getB() = " << d.getB() << endl;//2 cout << "d.getC() = " << d.getC() << endl;//3 cout << endl; BaseA* pa = &d;//父类指针指向子类对象的地址 BaseB* pb = &d; cout << "pa->getA() = " << pa->getA() << endl;//1 cout << "pb->getB() = " << pb->getB() << endl;//2 cout << endl; void* paa = pa; void* pbb = pb; if( paa == pbb )//比较指针 { cout << "Pointer to the same object!" << endl; } else { cout << "Error" << endl; // Error } cout << "pa = " << pa << endl; //pa = 0xbfe980a4 cout << "pb = " << pb << endl; //pb = 0xbfe980a8 cout << "paa = " << paa << endl; //paa = 0xbfe980a4 cout << "pbb = " << pbb << endl; //pbb = 0xbfe980a8 return 0; }
多重继承的问题一
解决方案:无
通过多重继承得到的对象可能拥有”不同的地址“!!
多重继承的问题二
多重继承可能产生冗余的成员
Teacher、Student都有People类所有成员变量成员函数,
Doctor类同时继承Teacher类、Student类,不现实?一个人可以有两个名字,两个年纪吗?
/*多重继承产生的问题2*/ #include <iostream> using namespace std; class People { string m_name; int m_age; public: People(string name, int age) { m_name = name; m_age = age; } void print() { cout << "Name = " << m_name << ", " << "Age = " << m_age << endl; } }; class Teacher : public People { public: Teacher(string name, int age) : People(name, age) { } }; class Student : public People { public: Student(string name, int age) : People(name, age) { } }; class Doctor : public Teacher, public Student { public: Doctor(string name, int age) : Teacher(name + "1", age + 1), Student(name + "2", age + 2) { } }; int main() { Doctor d("Nyist", 30); //d.print();//error /* test.cpp: In function ‘int main()’: test.cpp:51: error: request for member ‘print’ is ambiguous test.cpp:16: error: candidates are: void People::print() test.cpp:16: error: void People::print() */ d.Teacher::print();//Name = Nyist1, Age = 31 d.Student::print();//Name = Nyist2, Age = 32 return 0; }
当多重继承关系出现闭合是将会产生数据冗余的问题!!!
解决方案:
虚继承
#include <iostream> #include <string> using namespace std; class People { string m_name; int m_age; public: People(string name, int age) { m_name = name; m_age = age; } void print() { cout << "Name = " << m_name << ", " << "Age = " << m_age << endl; } }; class Teacher : virtual public People { public: Teacher(string name, int age) : People(name, age) { } }; class Student : virtual public People { public: Student(string name, int age) : People(name, age) { } }; class Doctor : public Teacher, public Student { public: Doctor(string name, int age) : Teacher(name, age), Student(name, age), People(name, age) { } }; int main() { Doctor d("Delphi", 33); d.print();//Name = Delphi, Age = 33 return 0; }
- 虚继承可以解决数据冗余的问题
- 中间层父类不再关心顶层父类的初始化
- 最终子类必须直接调用顶层父类的构造函数
9.假如让你实现编译器,你怎么去实现多态的语义?
c++多态的实现原理
- 当类中声明虚函数时,编译器会在类中生成一个虚函数表
- 虚函数表是一个存储成员函数地址的数据结构
- 虚函数表是由编译器自动生成和维护的
- virtual成员函数会被编译器放入虚函数表中
- 存在虚函数时,每个对象都有一个指向虚函数表的指针
- 继承的本质就是父子间成员变量的叠加
- c++中的多态是通过虚函数表实现的
- 虚函数表是由编译器自动生成和维护的
- 虚函数的调用效率低于普通成员函数
10.什么是return value optimization?g++中如何关闭这个优化?
返回值优化(Return value optimization,缩写为RVO)是C++的一项编译优化技术。它最大的好处是在于: 可以省略函数返回过程中拷贝构造函数的多余调用,解决 “C++ 中长久以来为人们所诟病的临时对象的效率问题”。
#include <iostream> using namespace std; class RVO { public: RVO() { cout << "I am in constructor" << endl; } RVO(const RVO& c_RVO) { cout << "I am in copy constructor" << endl; } ~RVO() { cout << "I am in destructor" << endl; } int mem_var; }; RVO MyMethod (int i) { RVO rvo; rvo.mem_var = i; return (rvo); } int main() { RVO rvo; rvo=MyMethod(5); }
其中非常关键的地方在于对MyMethod函数的编译处理。
RVO MyMethod (int i) { RVO rvo; rvo.mem_var = i; return (rvo); }
如果没有返回值优化这项技术,那么实际上的代码应该是编译器处理后的代码应该是这样的:
RVO MyMethod (RVO &_hiddenArg, int i) { RVO rvo; rvo.RVO::RVO(); rvo.member = i ; _hiddenArg.RVO::RVO(rvo); return; rvo.RVO::~RVO(); }
- 首先,编译器会偷偷地引入一个参数
RVO& _hiddernArg
,该参数用来引用函数要返回的临时对象,换句话说,该临时对象在进入函数栈之前就已经建立,该对象已经拥有的所属的内存地址和对应的类型;但对应内存上的二进制电位状态尚未改变,即尚未初始化。以上涉及到一点变量的概念。变量本质上是一个映射单位,每个映射单位里都有三个元素:变量名、类型、内存地址。变量名是一个标识符。当要对某块内存写入数据时,程序员使用相应的变量名进行内存的标识,而地址这个元素就记录了这个内存的地址位置。而相应类型则告诉编译器应该如何解释此地址所指向的内存,因为本质上,内存上有的仅仅只是两种不同电位的组合而已。因而变量所对应的地址所标识的内存的内容叫做此变量的值。RVO rvo;
这里我们创建一个变量——RVO类的对象rvo;计算机将圈定一块内存地址为该变量使用,并声明类型,告诉编译器以后要怎么解释这块内存。rvo.RVO::RVO();
但是以上操作尚未改变该内存上的 二进制的电位状态;改变电位状态的工作由rvo对象的构造函数完成。_hiddenArg.RVO::RVO(rvo);
用rvo对象来调用 临时对象 的拷贝构造函数 来对临时对象进行构造。rvo.RVO::~RVO();
函数返回结束; 析构函数内部定义的所有对象总结一下一般的函数调用过程中的变量生成传递:
- 在函数的栈中创建一个名为rvo的对象
- 关键字
return
后的rvo 表示需要用变量rvo来构造需要返回的临时对象- 函数返回结束,析构掉在函数内建立的所有对象
- 继续表达式
rvo=MyMethod(5);
里的操作- 语句结束,析构临时对象
这里,在函数栈里创建的对象rvo在函数MyMethod返回时就被析构,其唯一的操作仅仅是调用函数的返回对象、即所谓的临时对象的拷贝构造函数,然后就被析构了。特别的,如果对象
rvo
是一个带有大量数据的变量,那么这一次变量的创建与销毁的开销就不容小觑。但是,如果开启了返回值优化,那么当编译器识别出了
return
后的返回对象rvo
和函数的返回对象的类型一致,就会对代码进行优化 。编译器转而会将二者的直接关联在一起,意思就是,对rvo
的操作就相当于直接对 临时对象的操作,因而编译器处理后的代码应该是这样的:RVO MyMethod(RVO &_hiddenArg, int i) { _hiddenArg.RVO::RVO(); _hiddenArg.member = i; Return }
可以发现,优化后的函数依然可以处理相同的工作,但是省略掉了一次拷贝构造。
因而,在编译时启用以及不启用 RVO 将产生一下两种不同的行为:若不启用 RVO,预期输出将是:
I am in constructor //main I am in constructor //MyMethod I am in copy constructor //MyMethod I am in destructor //MyMethod I am in destructor //MyMethod I am in destructor //main
若启用 RVO,预期输出将是:
I am in constructor //main I am in constructor //MyMethod I am in destructor //MyMethod I am in destructor //main
g++ -fno-elide-constructors
11.为什么不要在构造函数里调用虚函数?
问题描述
大家都知道在面向对象语言中,多态实际上就是接口的多种不同的实现方式,而虚函数实现多态的机制就是通过指向派生类的基类指针或引用,访问派生类中同名覆盖的成员函数。那么有人知道为什么在父类的构造函数中调用了虚函数却不能实现多态吗?
问题分析
在最初接触到这个问题的时候,也很茫然,好像都知道不能在父类的构造函数里通过调用虚函数来实现多态,但是至于为什么还真的没有想过。但是遇到了这个问题不能不解决呀,所以又要开始发扬打破砂锅问到底的“好品质”了。
首先在查看虚函数实现多态的方法描述里就能知道,虚函数之所以能实现多态完全得益于vptr指针和虚函数表,那么vptr指针到底是什么?虚函数表又是什么?他们之间又有什么关系呢?
解决方案
vptr指针:当类中声明虚函数时,编译器就会在类中自动生成一个虚函数表,同时编译器会在类实例化对象时在对象中加入vptr指针,且指向这个虚函数表。
虚函数表:其实虚函数就是是通过一张虚函数表来实现的,简称为V-Table。在这个表中,主要是一个类的虚函数的地址,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,当用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
因为vptr指针变量是在构造函数中进行初始化的,且初始化过程为:首先对象在创建的时,由编译器对VPTR指针进行初始化 ,只有当对象的构造完全结束后VPTR的指向才最终确定。
所以当构造函数被调用时,编译器生成的VPTR指针只能产生通向它自己的虚函数表的调用,而不是指向最后派生的虚函数表,因为所有构造函数只有被调用后才会有最后派生的虚函数表,且不能知道有哪些子类继承自该父类。
所以这就是为什么在父类构造函数中调用虚函数不能实现多态的原因。
14.两棵树是否相等
递归
/** * Definition for binary tree * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode(int x) : val(x), left(NULL), right(NULL) {} * }; */ class Solution { public: bool isSameTree(TreeNode *p, TreeNode *q) { if (p == NULL && q == NULL) return true; if (p != NULL && q != NULL && p->val == q->val){ return isSameTree(p->left, q->left)&&isSameTree(p->right, q->right); } return false; } };
15.计算斐波那契数列,给定一个n,返回f(n)
int Fib(int n)//递归 { if( (n == 1) || (n == 2) ) { return 1; } if(n > 2) { return Fib(n - 1) + Fib(n - 2); } }
int fib(int n) { if( (n == 1) || (n == 2) ) return 1; int f1=1; int f2=1; int fn=0; for(int i=3;i<=n;i++) { fn=f2+f1; f2=f1; f1=fn; } return fn; }