C++学习笔记
----继承
文章目录
一.继承的概念
- 概念
继承代表了 is a kind of关系(抽象和具体,一般和特例,父和子,基本和扩展等),此时被继承的类称为基类(父类),而继承的类称为派生类(子类)。 - 好处
(1)软件重用:
a. 派生类可以直接利用基类已有的功能,简化类的设计
b. 具有从属关系的类可以通过继承机制联系起来,体现派生类对象和基类对象之间的“is a kind of”的关系
c. 设计并测试好了通用类可以组成类库重复使用
(2)接口重用:
a. 基类中定义的函数可以在派生类中重新定义
b. 实现函数功能的动态绑定
c. 体现了接口与实现相分离的思想
二.派生类的定义
class 派生类名: 继承方式 基类名
{
派生类新加的数据成员
派生类新加的成员函数
}
继承方式:public, protected, private
class element
{
protected:
int width;
int height;
public:
void setWidth(int w){ width = w; }
void setHeight(int h){ height = h; }
void print(){ cout << "width=" << width << ", height=" << height << endl; }
};
class Rectangle: public element
{
private:
int id;
public:
int getArea(){ return (width * height); }
void print(){ cout << "id=" << id << endl; }
};
三.基类和派生类的关系
- 基类中的成员函数不可访问派生类的任何成员。基类的对象不可访问派生类的任何成员和函数。
- 派生类的成员函数可以访问基类的受保护成员和公有成员。派生类的对象访问基类受继承方式的影响:
四.派生类对象调用成员函数
- 成员函数只存在于派生类:直接调用
- 成员函数只存在于基类:直接调用
- 成员函数既存在于派生类,又存在于基类:只调用派生类中与其参数匹配的同名成员函数,若派生类中无与参数匹配的成员函数,则编译错误,编译程序不会再在基类中寻找参数匹配的成员函数
#include<iostream>
using namespace std;
class element
{
protected:
int width;
int height;
public:
void setWidth(int w){ width = w; }
void setHeight(int h){ height = h; }
void print(){ cout << "width=" << width << ", height=" << height << endl; }
};
class Rectangle: public element
{
private:
int id;
public:
Rectangle(int i){ id=i; }
int getArea(){ return (width * height); }
void print(){ cout << "id=" << id << endl; }
};
int main()
{
Rectangle rec(1);
rec.setHeight(5);
rec.setWidth(10);
rec.print();
return 0;
}
(公有继承)若想访问基类中的print()成员函数:C++动态联编,实现接口重用
五.派生类的构造函数和析构函数
派生类不能继承基类的构造函数和析构函数
- 构造函数
(1)对派生类对象继承的基类数据进行初始化的初始值必须以参数的形式添加到派生类构造函数的参数列表中,然后在派生类构造函数体内直接调用基类公有的或受保护访问属性的成员函数对派生类目标对象继承的基类数据成员进行初始化。
class element
{
private:
int width;
int height;
public:
void setWidth(int w){ width = w; }
void setHeight(int h){ height = h; }
};
class Rectangle: public element
{
private:
int id;
public:
Rectangle(int i, int w, int h)
{
id=i;
setWidth(w);
setHeight(h);
}
};
(2)适用于基类没有私有访问属性的数据成员:直接在派生类构造函数的函数体内用赋值语句对派生类对象继承的基类的公有和受保护数据成员设置初始值。
class element
{
protected:
int width;
int height;
public:
void setWidth(int w){ width = w; }
void setHeight(int h){ height = h; }
};
class Rectangle: public element
{
private:
int id;
public:
Rectangle(int i, int w, int h)
{
id=i;
width=w;
height=h;
}
};
(3)适用于基类有构造函数的情况:即是在派生类构造函数的函数体内用赋值语句对派生类增加的数据成员赋初始值,在派生类构造函数的函数头用包含基类类名的初始化列表将派生类对象对象继承的基类数据成员的初始化交给基类的构造函数来完成。
派生类类名::派生类类名(基类所需的形参, 派生类所需的形参, 子对象形参): 基类类名(基类所需的形参), 子对象名(子对象形参)
{
用派生类的形参通过赋值语句初始化派生类增加的数据成员
}
class element
{
protected:
int width;
int height;
public:
element(int w, int h): width(w), height(h){ }
};
class Rectangle: public element
{
private:
int id;
public:
Rectangle(int i, int w, int h): element(w, h){ id=i; }
};
class element
{
protected:
int width;
int height;
public:
element(int w, int h): width(w), height(h){ }
};
class Rectangle: public element
{
private:
int id;
element shape;
public:
Rectangle(int i, int w, int h): element(w, h), shape(w, h){ id=i; }
};
- 拷贝构造函数
派生类类名::派生类类名(const 派生类类名 &r): 基类类名(r), 子对象名(r.子对象名)
{
派生类数据成员1 = r.派生类数据成员1;
...
}
class element
{
protected:
int width;
int height;
public:
element(int w, int h): width(w), height(h){ }
element(const element &e){ width=e.width; height=e.height; }
};
class Rectangle: public element
{
private:
int id;
public:
Rectangle(int i, int w, int h): element(w, h){ id=i; }
Rectangle(const Rectangle &r): element(r){ id = r.id; }
};
派生类构造函数的调用顺序:
> 根据派生类定义时基类的顺序依次调用基类构造函数对派生类对象继承的基类数据成员初始化;
> 根据派生类子对象数据成员定义的顺序调用其所属类的构造函数(如果派生类有子对象数据成员的话);
> 派生类构造函数体内的代码。
- 析构函数
当派生类对象撤消调用派生类的析构函数时,基类的析构函数也会同时自动隐式地被调用以释放基类数据成员所占用的资源。
派生类析构函数的执行顺序与派生类构造函数的调用顺序正好相反:
> 派生类析构函数体内的代码;
> 根据派生类子对象数据成员定义顺序的相反次序调用其所属类的析构函数(如果派生类有子对象数据成员的话);
> 根据派生类定义时基类顺序的相反次序依次调用其基类的析构函数。
五.基类对象和派生类对象的关系
- 两个不同类的类对象一般是不能互相赋值的,但同一个公有继承的派生类的两个对象则可互相赋值。
- 公有派生的派生类对象可以赋值给其基类对象,反之则不然。
(1)被赋值的基类对象只能访问基类的公有成员,而不能访问派生类中新增的成员。
(2)经过显式类型转换成基类对象的派生类对象可以被基类对象赋值,也可以用该对象来访问派生类的公有成员。
element e; //基类
Rectangle rec; //派生类
e = rec; //派生类对象可赋值给基类对象
rec = e; //基类对象不可赋值给派生类对象
(element)rec = e; //派生类对象可经过显式转换为基类对象
rec = (Rectangle) e; //基类对象不可经过显式转换为派生类对象
- 私有继承和保护继承的派生类,其对象之间不能相互赋值。
六.基类对象指针和派生类对象指针的关系
- 指向两个不同类类对象的指针是不能互相赋值的,但若指针指向的两个类对象具有公共继承关系,则两个指针可互相赋值。
- 派生类对象指针(或引用)可以赋值给基类对象指针(或引用),反之则不然。
(1)被赋值的基类对象指针只能访问基类的公有成员,而不能访问派生类中新增的成员。
(2)经过类型转换运算符将基类指针显式转换为指向派生类的指针可访问派生类的公有成员。
element e,*p_e = &e; //基类
Rectangle rec, *p_rec = &rec; //派生类
p_e = p_rec; //派生类对象指针可赋值给基类对象指针
p_rec = p_e; //基类对象指针不可赋值给派生类对象指针
(element *)p_rec = p_e; //派生类对象指针不可转换为基类对象指针
p_e -> print(); //基类对象指针不能调用派生类的成员函数
((Rectangle *)p_e) -> print(); //基类对象指针转换后可调用派生类的成员函数
- 私有继承和保护继承的派生类的对象指针与其基类对象指针之间不能互相赋值。
七.静态联编和动态联编
- 静态联编是指函数名与其在内存中的可执行代码之间的对应关系在编译时就已经确定了。函数调用时会根据其代码的地址自动转移去执行该函数在内存的代码。静态联编又称早期(运行前)绑定。
- 动态联编是指类的成员函数的调用语句在编译时并不知道要执行的是哪个内存地址的代码。成员函数的调用会根据目标对象的动态类型(而不是静态类型)在程序运行时(而不是在编译阶段)将函数名绑定到具体的函数实现上。动态联编又称晚期(编译后)绑定。
- 动态联编:
(1)条件:
a. 基类及其公有继承的派生类中有同名的具有public或protected访问属性的成员函数,且这两个函数的原型完全相同但实现不同;
b. 必须在基类将希望动态联编的成员函数用virtual关键字声明为虚函数;
c. 必须用基类对象指针或基类对象的引用来调用函数
(2)虚函数:
虚函数是在基类中以关键字virtual说明,并在派生类中重新定义的一个非静态成员函数,格式为:
virtual 函数类型 成员函数名(参数列表);
> 虚函数在类外部定义时可以不带virtual,在公有继承的派生类中与虚函数原型相同的函数也是虚函数(可不带virtual)
> 只有类的成员函数才能声明为虚函数,静态成员函数、内联函数、友元函数和构造函数都不能声明为虚函数。
> 析构函数可以为虚函数,且通常必须为虚函数。此时派生类的析构函数也是虚函数。
析构函数为虚函数:
#include<iostream>
using namespace std;
class element
{
protected:
int width;
int height;
public:
element(int w=0, int h=0){
width = w;
height = h;
}
void print(){ cout << "width=" << width << ", height=" << height << endl; }
~element(){
cout << "element delete" << endl;
}
};
class Rectangle: public element
{
private:
int id;
public:
Rectangle(int i=0, int w=0, int h=0):element(w, h){ id=i; }
void print(){ cout << "id=" << id << endl; }
~Rectangle(){
cout << "Rectangle delete" << endl;
}
};
int main()
{
Rectangle *pr = new Rectangle;
element *p;
p = pr;
p->print();
delete p;
return 0;
}
将两个构造函数改为虚函数:
...
virtual ~element(){ cout << "element delete" << endl; }
...
virtual ~Rectangle(){ cout << "Rectangle delete" << endl; }
...
(3)用基类对象指针或引用调用虚函数实现动态联编的过程为:
- 根据基类对象指针(或引用)指向的对象的虚函数表指针,找到对象所属类的虚函数表;
- 根据虚函数名查找虚函数表,获得该虚函数的入口地址;
- 如果指针指向的是派生类对象,则查找的是派生类的虚函数表,执行的是派生类虚函数的代码;
- 如果指针指向的是基类对象,则查找的是基类的虚函数表,执行的是基类虚函数的代码。
指针p指向基类对象,指针pr_指向派生类对象:
#include<iostream>
using namespace std;
class element
{
protected:
int width;
int height;
public:
element(int w=0, int h=0){
width = w;
height = h;
}
void print(){ cout << "width=" << width << ", height=" << height << endl; }
};
class Rectangle: public element
{
private:
int id;
public:
Rectangle(int i=0, int w=0, int h=0):element(w, h){ id=i; }
void print(){ cout << "id=" << id << endl; }
};
int main()
{
Rectangle *pr = new Rectangle;
element *p = new element;
p->print();
element *pr_;
pr_ = pr;
pr_->print();
return 0;
}
将基类中的print改为虚函数:
...
virtual void print(){ cout << "width=" << width << ", height=" << height << endl; }
...
(4)纯虚函数
动态联编要求在基类定义虚函数,但基类的虚函数有时不知道如何实现。对于这样一些物理上无法实现而逻辑上又不得不存在的抽象的虚函数,可以将其在基类中用不包括任何代码的纯虚函数来定义。而其具体的实现则可在派生类中完成。
virtual 函数返回类型 纯虚函数名(参数表) = 0;
> 在虚函数表中,纯虚函数的地址为NULL。
> 包含纯虚函数的类称为抽象类。由于无法实例化一个含纯虚函数的抽象类,因而不能创建抽象类的对象。抽象类不能用作参数类型、函数返回和显式转换的类型,但可定义指向抽象类的指针或引用。
#include <iostream>
using namespace std;
class CMusic
{
public:
CMusic() { cout << "Music default constructor" << endl; }
virtual void listen() = 0;
void sing() { cout << "Singing music" << endl; }
virtual ~CMusic() { cout << "Music default destructor" << endl; }
};
class CRockMusic : public CMusic
{
public:
CRockMusic() {cout << "Rock music default constructor" << endl; }
void listen() { cout << "Listening rock music" << endl; }
void sing() {cout << "Singing rock music" << endl; }
virtual ~CRockMusic() { cout << "Rock music default destructor" << endl; }
};
int main()
{
CMusic *p_music;
CRockMusic *p = new CRockMusic;
p_music = p;
p_music->listen();
p_music->sing();
delete p_music;
return 0;
}
八.基类和派生类间同名函数的调用
class element
{
protected:
int width;
int height;
public:
void setWidth(int w){ width = w; }
void setHeight(int h){ height = h; }
void print(){ cout << "width=" << width << ", height=" << height << endl; }
};
class Rectangle: public element
{
private:
int id;
public:
Rectangle(int i){ id=i; }
int getArea(){ return (width * height); }
void print(){ cout << "id=" << id << endl; }
};
- 若在派生类的print函数中需要调用基类的print函数:
在派生类的print函数中改为:
void print(){
element::print();
cout << "id=" << id << endl;
}
- 若在主函数中需要调用基类的print函数:
在主函数中改为:
Rectangle rec(1);
rec.setHeight(5);
rec.setWidth(10);
rec.element::print();
或者在主函数中改为:
Rectangle rec(1);
rec.setHeight(5);
rec.setWidth(10);
Rectangle *p_r = &rec;
element *p_e;
p_e = p_r;
p_e->print();
- 虚函数(见七.(3))
九.多重继承
多重继承即从两个或两个以上的基类继承而产生的派生类。
- 格式:
class 派生类名: 继承方式1 基类名1, 继承方式2 基类2, ...
{
派生类新增的数据成员和成员函数
};
- 多重继承派生类构造函数执行的顺序是先根据派生类定义基类出现的次序依次调用基类的构造函数,然后在执行派生类构造函数体内的语句,定义格式:
派生类类名::派生类类名(基类1形参, 基类2形参, ..., 派生类形参):基类名1(参数1), 基类名2(参数2)...
{
派生类成员初始化赋值语句;
}
#include <iostream>
using namespace std;
class CA
{
public:
void setA(int x) { a = x; }
void printA() { cout << a << endl;}
private:
int a;
};
class CB{
public:
void setB(int x) { b = x; }
void printB() { cout << b << endl; }
private:
int b;
};
class CC : public CA, private CB{
public:
void setC(int x, int y, int z)
{
setA(x);
setB(y);
c = z;
}
void printC() { cout << c << endl; }
private:
int c;
};
int main()
{
CC obj;
obj.setA(1);
obj.printA();
obj.setC(2,3,4);
obj.printC();
obj.setB(5); //不可访问,private
obj.printB(); //不可访问,private
return 0;
}
- 特点:
- 如果多重继承派生类的基类中有两个或两个以上基类含有同名的数据成员和成员函数,则在派生类中需要用“类作用域::成员名”的形式指出成员所属的类来访问该成员,以避免二义性。
- 派生类不能作为自己的基类。(class A :public B, public A)
- 基类不能在一个派生类的继承列表中出现两次以上。(class A : public B, public B)
- 基类不能既是直接基类又是间接基类。(class A:public B, public C; class C: public B)
- 重复继承二义性
多重继承的派生类的两个以上的基类同时又是从另外同一个基类继承的派生类时访问公共基类成员有可能会出现二义性。
#include <iostream>
using namespace std;
class CA
{
public:
void setA(int x) { i = x; }
void printA() { cout << i << endl;}
protected:
int i;
};
class CB : public CA{
public:
void setB(int x1, int y)
{
setA(x1);
b = y;
}
void printB() { cout << b << endl; }
protected:
int b;
};
class CC : public CA{
public:
void setC(int x2, int z)
{
setA(x2);
c = z;
}
void printC() { cout << c << endl; } //[Error] reference to 'i' is ambiguous
private:
int c;
};
class CD : public CB, public CC{
public:
void setD(int x1, int x2, int y, int z, int u)
{
setB(x1, y);
setC(x2, z);
d = u;
}
void printD() { cout << d << endl; }
private:
int d;
};
int main()
{
CD obj;
obj.setD(0, 1, 2, 3, 4);
obj.printA(); \\[Error] request for member 'printA' is ambiguous
obj.printA(); \\[Error] request for member 'printA' is ambiguous
return 0;
}
(1)在派生类中使用作用域运算符::标明该成员的作用域及继承路径来避免二义性。
obj.CB::printA(); // 0
obj.CC::printA(); // 2
(2)虚基类:在定义派生类时将公共基类用virtual声明为虚基类,使公共基类的成员在重复继承的派生类中只产生一个拷贝。一个基类可以在生成一个派生类作为虚基类,而在生成另一个派生类时不作为虚基类。
class CB : virtual public CA{...};
class CC : virtual public CA{...};
...
obj.printA(); // 1
- 虚基类解决二义性的原理
(1)每个虚基类的直接派生类都有一个虚基表为该类的所有对象共享,虚基类的派生类对象所分配的内存中会增加一个指向虚基表的指针,虚基表中记录了本类对象内存开始位置与虚基类成员内存之间的偏移量。
(2)CA类对象分配的内存有i数据。CB和CC分别有一个虚基表,派生类CB(CC)的对象分
配的内存只有b©和各自的虚基表地址数据,而没有虚基类的i数据。
(3)派生类对象obj分配的内存中有5个数据:
①从CB继承的虚基表指针;
②从CB继承的b数据;
③从CC继承的虚基表指针;
④从CC继承的c数据;
⑤派生类的d数据。
x1和x2分别是两个虚基表的数据项,表示对象d的内存地址到公共基类成员base的的内存地址的偏移量。通过虚基表指针vbp1或vbp2找到虚基表,获得偏移量X1或x2,进而找到公共基类的数据i。
(4)CD类对象的内存中都没有i数据的拷贝,i只有一个备份。
- 包含虚基类的派生类的构造函数
(1)如果构造函数成员初始化列表中,同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。
(2)如果构造函数成员初始化列表中,有多个对虚基类构造函数的调用时,则按照派生类定义时,虚基类出现次序从左至右执行,非虚基类的构造函数若有多个也是如此。