继承
一个类是另一个类的一种(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只能通过加上作用域。
- 覆盖
基类与派生类中,函数名,函数的返回值与参数列表都相同,基类的函数的是虚函数
(覆盖虚函数表)
先来了解一下c++ 中的静态绑定域动态绑定
- 静态类型:对象在声明时采用的类型,在编译期既已确定;
- 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
- 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
- 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;
我们来看一个例子
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 <<endl;}
};
class Derived:public Base
{
public:
virtual void Display(int a = 20)
{ cout << "Derived::Display() a = " << a << endl;}
};
int main()
{
Derived *d = new Derived;
Base *b = d;
b->Display();
d->Display();
}
可以看到因为缺省参数属于静态绑定,所以当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 <<endl;}
private:
int ma;
};
int main()
{
cout << " Base size " << sizeof(Base) << endl;
return 0;
}
可以看到类中不止有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;