本文共 5980 字,大约阅读时间需要 19 分钟。
一个类是另一个类的一种(a kind of)
// 人class People{ public: void eat(string foot) { cout << "eat:" << foot << endl; } void sleep() { cout << "sleep" << endl; }private:};//学生class Student:public People{ public: void learn() { cout << "learn" << endl; }private:};//对于Student来说,People 的每一个特征Student都要拥有,//不仅是Student 如果现在需要一个teacher类,对于People 的特征也要拥有,//这样我们可以将People定义为一个基类,所有拥有这些特征的都可以继承这个类,//这样就减少了不必要的代码重复int main(){ Student st; st.eat("红烧肉"); return 0;}
基类 | 派生类 | 类外 |
---|---|---|
public | public | 可访问 |
protected | protected | 不可访问 |
private | 不可访问 | 不可访问 |
基类 | 派生类 | 类外 |
---|---|---|
public | protected | 不可访问 |
protected | protected | 不可访问 |
private | 不可访问 | 不可访问 |
基类 | 派生类 | 类外 |
---|---|---|
public | private | 不可访问 |
protected | private | 不可访问 |
private | 不可访问 | 不可访问 |
class People{ public: People(int m):ma(m){ cout << "People()" << endl; } ~People(){ cout << " ~People()" << endl; } void eat(string foot) { cout << "eat:" << foot << endl; } void sleep() { cout << "sleep" << endl; }private: int ma;};class Student:public People{ public: //派生类必须通过调用基类的构造函数对基类的成员进行初始化 //如果基类不存在默认的构造函数,则在派生类的初始化列表中进行初始化 //如果基类存在默认的构造函数,如果不在初始化列表中初始化, //则默认调用基类的默认构造函数 Student(int m):People(m) { cout << "Student()" << endl; } ~Student(){ cout << "~Student" << endl; } void learn() { cout << "learn" << endl; }private:};int main(){ Student st(1); st.eat("红烧肉"); return 0;}上图可以看出,先调用基类的构造函数,再调用派生类的构造函数, 析构的时候顺序相反。
基类对象能不能赋给派生类对象? | 不可以 |
---|---|
派生类对象能不能赋给基类对象? | 可以 |
基类指针指向派生类对象? | 可以 |
派生类指针指向基类对象? | 不可以 |
class Base{ public: void Show() { cout << "Base::Show()" << endl; } void Show(int a) { cout << "Base::Show(int)" << endl; }private: int ma; int mb;};class Derived:public Base{ public: void Show() { cout << "Derived::Show()" << endl; }private: int mc;};int main(){ cout << "Base size:" << sizeof(Base) << endl; cout << "Derived size:" << sizeof(Derived) << endl; return 0;}
输出结果
同一个类的所有对象都有自己的成员变量,所有对象共享一套成员方法。 所以sizeof()计算类的大小的时候,只计算成员变量的大小,在上面的例子中,派生类继承了基类的所有的成员变量域成员方法,所以计算派生类的大小的时候,要加上基类的大小。如果两个函数的 函数名相同,参数列表不同,在同一个作用域中,则这两个函数叫重载函数
class Base{ public: //这两个函数为重载函数 void Show() { cout << "Show()" << endl; } void Show(int a) { cout << "Show(int)" << endl; }};
问题: 为什么c++ 中支持重载,c语言中不支持重载?重载实现的原理是什么?
因为在c语言中,编译阶段产生符号表的时候,每个函数产生一个符号,符号的产生是根据函数名来确定的,而c++ 中,符号的产生是根据函数名和参数列表共同确定的,所以,函数名相同,参数列表不同在编译阶段中产生的符号是不同的,在链接的过程中连接器根据符号表查找函数的的定义,不会产生冲突。class Base{ public: void Show() { cout << "Base::Show()" << endl; } void Show(int a) { cout << "Base::Show(int)" << endl; }};class Derived:public Base{ public: void Show() { cout << "Derived::Show()" << endl; }};int main(){ Derived d; d.Show(); //访问派生类的Show() d.Show(10); //编译错误,派生类中没有函数参数的Show()函数 d.Base::Show(); //调用基类的Show() d.Base::Show(10); //调用基类的Show(int) return 0;}
因为派生类中Show() 将基类中的Show函数隐藏了,所以通过对象访问只能访问派生类的Show ,要访问基类的Show只能通过加上作用域。
class Base{ public: void Show(){ cout << "Base::Show()" << endl;} virtual void Display(){ cout << "Base::Display()" << endl;}};class Derived:public Base{ public: void Show(){ cout << "Derived::Show()" << endl;} virtual void Display(){ cout << "Derived::Display()" << endl;}};int main(){ Derived *d = new Derived; Base *b = d; b->Show(); d->Show(); cout << "=====================" << endl; b->Display(); d->Display();}虽然b和d都指向同一个对象。因为函数Show是一个no-virtual函数,它是静态绑定的,也就是编译器会在编译期根据对象的静态类型来选择函数。d的静态类型是Derived*,那么编译器在处理d->Sow()的时候会将它指向Derived::Show()。同理,b的静态类型是Base*,那b->Show()调用的就是Base::Show();
因为Display是一个虚函数,它动态绑定的,也就是说它绑定的是对象的动态类型,b和d虽然静态类型不同,但是他们同时指向一个对象,他们的动态类型是相同的,都是Derived*,所以,他们的调用的是同一个函数:Derived::Display()。
对于虚函数特别要注意的地方
虚函数+缺省参数 虚函数是动态绑定的,但是为了执行效率,缺省参数是静态绑定的。class Base{ public: virtual void Display(int a = 10) { cout << "Base::Display() a = " << a <可以看到因为缺省参数属于静态绑定,所以当b->Display() 默认的参数是10; d->Display() 默认的参数是20 这是我们不愿意看到的,在使用虚函数的时候,一定要考虑号缺省参数的问题。
我们来看一下虚函数是怎样实现动态绑定的?
先来看一下这样一个例子
class Animal // 有纯虚函数的类 =》 抽象类{ public: Animal(string name) :_name(name) { } virtual void bark() = 0;// 纯虚函数protected: string _name;};class Cat : public Animal{ public: Cat(string name):Animal(name){ } void bark() { cout << _name << " bark:喵喵!" << endl; }};class Dog : public Animal{ public: Dog(string name) :Animal(name) { } void bark() { cout << _name << " bark:旺旺!" << endl; }};int main(){ Animal *p1 = new Cat("猫"); Animal *p2 = new Dog("二哈"); int *p11 = (int*)p1; int *p22 = (int*)p2; int tmp = p11[0]; p11[0] = p22[0]; p22[0] = tmp; //Cat 与 Dog 都继承了 Animal 类,而Animal 有一个虚函数bark 两个派生类中都重写了 //bark函数,两个派生类中的bark自动声明为虚函数 //按我们的理解输出的结果为: /* 猫 bark:喵喵 二哈 bark:旺旺 calse Cat */ p1->bark(); p2->bark(); cout << typeid(*p1).name() << endl; return 0;}
实际的输出结果
分析一下这段代码Animal *p1 = new Cat("猫"); Animal *p2 = new Dog("二哈"); //p11 指向 p1 的首地址 int *p11 = (int*)p1; //p22 指向 p2 的首地址 int *p22 = (int*)p2; //交换了p11指向的内容与p22 指向内容的前四个字节 int tmp = p11[0]; p11[0] = p22[0]; p22[0] = tmp;
对象的前四个字节中存储了什么?交换之后对象的方法都交换了,而且对象的类型都变了 ?
每个类的虚函数都有一个对应的虚函数表,该表存储在只读数据段,即.rodata段,在每个含有虚函数类中就多了一个数据成员,vfptr 指向了虚函数表。通过vfptr 可以动态访问类中的虚函数
class Base{ public: virtual void Display(int a = 10) { cout << "Base::Display() a = " << a <可以看到类中不止有int ma,这就验证了上面我们说的。
虚函数表的内容
其中 RTTI (Run-Time Type Identification) ,RTTI 指向了.rodata 段的一个常量字符换,该常量字符串表示类型,例如上面的Base 类的虚函数表中的RTTI指向的就是"Base" 字符串,偏移量表示虚函数在对象中的位置,一般都是0 ,即在对象的起始位置。这样对于这段代码就好理解了Animal *p1 = new Cat("猫"); Animal *p2 = new Dog("二哈"); //p11 指向 p1 的首地址 int *p11 = (int*)p1; //p22 指向 p2 的首地址 int *p22 = (int*)p2; //交换了p11指向的内容与p22 指向内容的前四个字节 //也就是交换了两个对象的虚函数表的地址, int tmp = p11[0]; p11[0] = p22[0]; p22[0] = tmp; //p1此时指向存储的是p2的虚函数表的地址,所以调用p2的bark p1->bark(); //p2指向的p1 的虚函数表的地址,所以调用的p1 的bark p2->bark(); //p1 此时的RTTI是 原来p2的类型。 cout << typeid(*p1).name() << endl;
转载地址:http://txnwi.baihongyu.com/