目录
* 面向对象的程序设计中提供了类的继承机制,允许在保持原有类的基础上,进行更加具体详细的类的定义,以原有类为基础产生新的类,也可以说,从原有类排在生出新的类
* 派生新类的过程包括,吸收已有类的成员,调整已有类的成员和添加新成员三个步骤
一、类的继承与派生
(一)继承关系
* 类的继承就是新的类从已有类哪里得到已有的特性,从已有类的产生新类的过程就是派生
* 原有类称为基类或者父类,产生的新类称为子类或者派生类
(二)派生类的定义
* 定义语法:
* class 派生类名:继承方式 基类名1,继承方式 基类名2……
* { 派生类成员说明 }
* class paisheng:public jicheng1,private jicheng2
* { public:
* paisheng();
* ~paisheng();
}
* 一个派生类可以有多个基类,这种情况称为多继承,一个派生只有一个基类的情况称为单继承
* 一个班基类可以同时派生出多个派生类,一个类从父类继承来的特征可以被新的类继承,这就形成一个类的家族称为类族
* 直接参与派生出某类的就称为直接基类,基类的基类甚至更高层的基类称为间接基类
* 继承方式规定了如何访问从基类继承的成员:public private protected 分别表示公有继承 保护继承和私有继承
* 派生类成员是指除了从基类继承来的所有成员之外,新增的数据和函数成员
(三)派生类生成的过程
* 派生新类的过程分为三个步骤:吸收基类成员、改造基类成员、添加新的成员
* 主要目的是实现代码的重用和扩充
* 1、吸收基类成员
* 派生类包括它全部基类中除了构造函数和析构函数之外的所有成员,再继承过程中,析构函数和构造函数不被继承
* 2、改造基类成员
* 对于基类成员的改造包括两个方面,一个是基类成员的访问控制,主要依靠派生类定义时的继承方式来控制,另一个是对基类数据或函数成员的覆盖或隐藏
* 隐藏就是简单的在派生类中声明一个和基类数据或者函数同名的成员
* 如果派生类声明了一个和某基类成员同名的新成员(如果是成员函数则参数列也要相同,参数不同的情况下属于重载)
* 派生的新成员就隐藏了外层的同名成员,这称为同名隐藏
* 3、添加新成员
* 派生新成员的加入是继承和派生机制的核心,是保证派生类在功能上有所发展的关键
* 在派生过程中基类的构造函数和析构函数是不能被继承的,因此需要实现一些特别的初始化和扫尾清理工作,需要加入新的构造和析构函数
* 构造函数的作用:1、给对象建立标识符2、为对象数据成员开辟内存空间 3、完成数据成员初始化
二、访问控制
* 派生类继承的数据成员和函数成员的访问属性在派生过程中是可以进行调整的,基类的成员可以有三种访问属性
* 基类的自身成员可以对基类中的任何一个其他成员进行访问,但是通过该类的对象就只能访问该类的公有成员
* 继承方式不同导致的原来具有的不同属性的基类成员在派生类中的访问属性也有所不同,这里的访问来自两个方面:
* 1、派生类中的新增成员访问从基类中继承的成员
* 2、在派生类外部(非类族的成员),通过派生类的对象访问从基类继承的成员
(一)公有继承
* 当类的继承方式是公有继承时,基类的公有成员和保护成员的访问属性在派生类中不变,而基类的私有成员不可直接访问
* 在类族之外只能通过派生类的对象访问从基类继承的公有成员,而无论是派生类的成员还是派生类的对象都无法直接访问基类的私有成员
//公有继承
class point1
{
public:
void intpoint1(int a, int b)
{
x = a;
y = b;
}
void show()
{
cout << x << y;
}
private:
int x;
int y;
};
class point2 :public point1//派生类point2继承基类point1,继承方式是公有
{
public:
point2(int c, int d,int a,int b) :w(c),q(d)
{
intpoint1(a, b);//派生类的成员函数调用基类的成员函数
}
private:
int w;
int q;
};
int main()
{
point1 A;
point2 B(1,2,3,5);
B.show();//派生类对象调用基类的成员函数
return 0;
}
(二)私有继承
* 当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员的身份出现在派生类中,而基类的私有成员在派生类中不可直接访问
* 派生类的其他成员可以直接访问他们,但是派生类的对象无法直接访问他们,无论是派生类的成员,还是派生类的对象都无法直接访问从基类继承的私有成员
* 私有继承进一步派生的话,所有的成员都成为私有属性,无法在新的派生中直接进行访问,相当于中止基类功能的继续派生,所以私有继承的使用比较少
* 派生类的对象只能访问派生类新定义的公有成员,不能访问基类的成员
//私有继承
class point1
{
public:
void intpoint1(int a, int b)
{
x = a;
y = b;
}
void show()
{
cout << x << y;
}
private:
int x;
int y;
};
class point2 :private point1
{
public:
void intpoint2(int c, int d,int a,int b)
{
intpoint1(a, b);//对于派生类,基类的数据成员和成员函数都是私有属性,派生类的其他成员都可以对它进行访问
w = c;
q = d;
cout << c << d << a << b;
}
private:
int w;
int q;
};
class point3 :public point2
{
void intpoint3(int c, int d,int a, int b)
{
// intpoint1(a, b);//对于子类point3来说,intpoint1()是基类point2的私有成员,无法用子类的函数进行访问
s = c;
f = d;
}
private:
int s;
int f;
};
int main()
{
point1 A;
point2 B;
B.intpoint2(1, 2, 3, 4);
// B.show();//不可访问,对于派生类,基类的数据成员和成员函数都是私有属性,通过派生类的对象无法进行访问
return 0;
}
(三)保护继承
* 保护继承中,基类的公有成员和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可以直接进行访问
//保护继承
class point1
{
public:
void intpoint1(int a, int b)
{
x = a;
y = b;
}
void show()
{
cout << x << y;
}
private:
int x;
int y;
};
class point2 :protected point1
{
public:
void intpoint2(int c, int d, int a, int b)
{
intpoint1(a, b);//对于派生类,基类的数据成员和成员函数都是私有属性,派生类的其他成员都可以对它进行访问
w = c;
q = d;
cout << c << d << a << b;
}
private:
int w;
int q;
};
class point3 :public point2
{
void intpoint3(int c, int d, int a, int b)
{
intpoint1(a, b);//对于子类point3来说,intpoint1()是基类point2的保护成员成员,可以用子类的函数进行访问
s = c;
f = d;
}
private:
int s;
int f;
};
int main()
{
point1 A;
point2 B;
B.intpoint2(1, 2, 3, 4);
// B.show();//不可访问,对于派生类,基类的数据成员和成员函数都是保护属性,通过派生类的对象无法进行访问
return 0;
}
(四)对比
* 公有成员:类的成员、友元函数、类的对象、子类函数
* 保护成员:类的成员、友元函数、子类函数
* 私有成员:类的成员函数,友元函数
三、类型兼容规则
* 类型兼容规则是指在任何需要基类对象的地方,都可以使用公有派生类的对象来替代
* 通过公有继承,派生类得到了基类中除了构造函数、析构函数之外的所有成员,这样公有派生类实际上具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决
* 类型兼容规则中所指的替代包含以下情况:
* 1、派生类的对象可以隐含的转换为基类的对象
* 2、派生类的对象可以初始化基类的引用
* 3、派生类的指针可以隐含转换为基类的指针
* 在替代后,派生类的对象就可以作为基类的对象使用,但是只能使用从基类继承的成员
* 如果B是基类,D是B类的公有派生类,则D类中包含了除了构造函数和析构函数之外的全部成员,这时根据类型兼容规则,在基类B对象可以出现的地方,都可以使用派生类D代替
* class B{};
* classD:public B()
* B b1;
* B*pb1;
* D b2;
* 派生类对象可以隐含的转换为基类对象,可以用派生类对象中从基类继承来的成员,诸葛赋值给基类成员
* b2=b1;
* 派生类对象可以初始化基类对象的引用
* B&pb3=b2;
* 派生类对象的地址可以隐含的转换为指向基类的指针//基类的指针只能访问基类的成员
* 由于类型兼容规则的引入,对于基类以及公有的派生类的对象,可以使用完全相同的函数同一进行处理,因为形参是基类的对象时,实参可以是派生类的对象(或者指针)
* 类型兼容规则是多态性的重要基础之一
//类型兼容规则
class base1
{
public:
void display()const
{
cout << "base1::display()" << endl;
}
};
class base2 :public base1
{
public:
void display()const
{
cout << "base2::display()" << endl;
}
};
class der :public base2
{
public:
void display()const
{
cout << "der::display()" << endl;
}
};
void fun(base1* p)//可以将派生类对象的地址赋值给基类的指针,但通过这个基类的指针,只能访问到从这个基类继承的成员
{
p->display();
}
int main()
{
base1 b1;
base2 b2;
der d3;//类的对象
fun(&b1);//用base1的指针调用函数
fun(&b2);//用base2的指针调用函数
fun(&d3); //用der的指针调用函数
return 0;
}
四、派生类的构造和析构函数
* 继承的目的是为了发展,派生类继承了基类的成员,实现了原来代码的重用,这只是一部分,而代码的扩充才是最重要的,只有添加新的成员,才能加入新的功能
* 派生才有实际意义
* 由于基类的构造函数和析构函数不能被继承,在派生类中,如果对派生类新增的成员初始化,就必须为派生类添加新的构造函数
* 但是派生类的构造函数只负责对新增成员的初始化,对于从基类继承下来的成员,其初始化工作还是由基类的构造函数完成
* 同样,对于派生类对象的扫尾工作也加入了新的析构函数
(一)构造函数
* 要使用派生类就要声明该类的对象,在使用对象前必须对基类的成员对象和新增成员对象进行初始化
* 派生类对于基类的很多成员对象是不能直接及逆行访问的,因此要完成对基类的对象成员的初始化工作,需要通过调用基类的构造函数
* 需要合适的初值作为参数,其中一些参数要传递给基类的构造函数,另外一些要用于派生类新增的成员对象的初始化
* 构造派生类对象时,首先调用基类的构造函数,来初始化他们的数据成员,再按照构造函数初始化列表中指定的方式初始化新增的成员对象,最后才执行派生类构造函数的函数体
* 派生类构造函数的一般语法形式;
* 派生类名::派生类名(参数表):基类名1(基类初始化列表),基类名2(初始化列表)……,成员对象名1(成员对象1初始化列表),……
* { 派生类构造函数其他初始化操作 }
* 如果对基类初始化时,需要调用基类带有形参的构造函数时,派生类就必须声明构造函数
* 派生类构造函数执行的次序:
* 1、调用基类构造函数,调用顺序按照他们被继承时的声明顺序(从左到右)
* 2、对派生类新增的成员对象进行初始化,调用顺序按照他们在类中声明的顺序
* 3、执行函数体中的构造函数的内容
* 构造函数初始化列表中的基类名、对象名之间的次序无关紧要,可以时任意的,无论怎样,派生类构造函数的执行顺序是确定的
//派生类构造函数
class A
{
public:
A(int i)
{
cout << i << endl;
}
};
class B
{
public:
B(int j)
{
cout << j << endl;
}
};
class C
{
public:
C()
{
cout << "C" << endl;
}
};
class D:public A, public B, public C
{
public:
D(int x, int y, int z, int h):A(x), B(h),C(),b(y),a(z),c()//基类名(初始化列表),基类对象名(初始化列表)//无参也初始化,按照声明顺序
{
cout << "完成啦"<<endl;
}
private:
A a;
B b;
C c;
};
int main()
{
D d(1, 2, 3, 4);//先调用基类的构造函数,再调用内嵌对象的构造函数,最后是函数体
return 0;
}
*
(二)复制构造函数
* 派生类要写复制构造函数,一般需要为基类相应的复制构造函数传递参数
* 假如A是B的派生类
* A的复制构造函数:
* A::B(const A&p):B(p){函数体}
//派生类的复制构造函数
class A
{
public:
A(int i)
{
cout << i << endl;
}
};
class B
{
public:
B(int j)
{
cout << j << endl;
}
};
class C
{
public:
C()
{
cout << "C" << endl;
}
};
class D :public A, public B, public C
{
public:
D(int x, int y, int z, int h) :A(x), B(h), C(), b(y), a(z), c()//构造函数
{
cout << "完成啦" << endl;
}
private:
A a;
B b;
C c;
};
int main()
{
D d(1, 2, 3, 4);
return 0;
}
(三)析构函数
先构造的后析构,后构造的先析构
//派生类的析构函数
class A
{
public:
A(int i)
{
cout << i << endl;
}
~A() { cout << "a1" << endl; }
};
class B
{
public:
B(int j)
{
cout << j << endl;
}
~B() { cout << "b1" << endl; }
};
class C
{
public:
C()
{
cout << "C" << endl;
}
~C() { cout << "c1" << endl; }
};
class D :public A, public B, public C
{
public:
D(int x, int y, int z, int h) :A(x), B(h), C(), b(y), a(z), c()//构造函数
{
cout << "完成啦" << endl;
}
private:
A a;
B b;
C c;
};
int main()
{
D d(1, 2, 3, 4);
return 0;
}
五、派生类成员的标识和访问
在派生类中成员一共分为以下四种:不可访问成员、私有成员、公有成员、保护成员
(一)作用域标识符
::用来限定要访问成员所在的类
* 一般使用形式: 类名::成员名
* 类名::成员名(参数表)
* 在不同作用域声明的标识符,可见性原则是:
* 外层声明一个标识符,内层没有声明同名的标识符,则外层标识符在内层可见
* 如果在内层声明了同名标识符,则在内层外层标识符不可见,成为内层标识符隐藏了外层标识符,这种现象称为隐藏规则
//多继承同名隐藏
class A
{
public:
A(int i)
{
cout << i << endl;
}
void show()
{
cout << "是A" << endl;
}
~A() { cout << "a1" << endl; }
};
class B
{
public:
B(int j)
{
cout << j << endl;
}
void show()
{
cout << "是B" << endl;
}
~B() { cout << "b1" << endl; }
};
class C
{
public:
C()
{
cout << "C" << endl;
}
void show()
{
cout << "是C" << endl;
}
~C() { cout << "c1" << endl; }
};
class D :public A, public B, public C
{
public:
D(int x, int y, int z, int h) :A(x), B(h), C(), b(y), a(z), c()//构造函数
{
cout << "完成啦" << endl;
}
void show()
{
cout << "是D" << endl;
}
private:
A a;
B b;
C c;
};
int main()
{
D d(1, 2, 3, 4);
A* p = &d;//定义一个对象指针
d.show();//隐藏外部同名函数show()
d.A::show();//用基类名和作用域分辨符进行访问
d.B::show();//用基类名和作用域分辨符进行访问
d.C::show();
p->show();//用对象指针来调用对象的show()函数
//析构函数的调用顺序和构造函数的调用顺序刚好相反,先构造的后析构,后构造的先析构
return 0;
}
*
* 在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域,二者作用范围不同,是相互包含的两个层
* 派生类在内层,如果派生类声明了一个成员与基类同名,则隐藏了外部成员,使用成员名只能访问派生类成员
* 如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数列表不同,从基类继承的同名函数的所有重载形式也都会被隐藏
* 如果要访问被隐藏的成员,就要使用作用域分辨符和基类名来限定
* 1、如果派生类的多个基类拥有同名的成员,同时,派生类又新增这样的同名成员,在这种情况下,派生类成员将隐藏所有基类的同名成员
* 用对象名.成员名或者对象指针->成员名,可以唯一标识和访问的派生类新增成员,基类的同名成员可以用基类名和作用域分辨符来访问
* 2、如果派生类没有增加新的同名函数,但是基类中有同名函数,那么必须用作用域分辨符,否则会引起二义性
//同名函数二义性
class A
{
public:
A(int i)
{
cout << i << endl;
}
void show()
{
cout << "是A" << endl;
}
~A() { cout << "a1" << endl; }
};
class B
{
public:
B(int j)
{
cout << j << endl;
}
void show(int b)
{
cout << "是B="<<b << endl;
}
~B() { cout << "b1" << endl; }
};
class C
{
public:
C()
{
cout << "C" << endl;
}
void show(int a,int b)
{
cout << "是C" <<a<<b<< endl;
}
~C() { cout << "c1" << endl; }
};
class D :public A, public B, public C
{
public:
D(int x, int y, int z, int h) :A(x), B(h), C(), b(y), a(z), c()//构造函数
{
cout << "完成啦" << endl;
}
void show() { cout << "是D"<<endl; }
using B::show;//using 用于基类的函数名,如果是同名但是参数不同的函数不会隐藏,会称为重载函数
using C::show;//如果两个using的基类函数,并且函数名和参数名相同,依旧会造成二义性
using A::show;
private:
A a;
B b;
C c;
};
int main()
{
D d(1, 2, 3, 4);
A* p = &d;//定义一个对象指针
//d.show();//因为D中没有show函数,show函数的调用具有二义性
d.A::show();//用基类名和作用域分辨符进行访问
d.B::show(2);//用基类名和作用域分辨符进行访问
d.show(2);
d.show(1, 1);
d.C::show(2,1);
p->show();//用对象指针来调用对象的show()函数
//析构函数的调用顺序和构造函数的调用顺序刚好相反,先构造的后析构,后构造的先析构
d.show();//内层优先,在基类函数使用using的情况下与派生类的函数名和参数名都相同,调用的是派生类的函数
return 0;
}
* 也可以使用对象指针进行访问
* 3、using的功能
* 一个功能是将一个作用域中的名字引用到另外一个作用域中
* using namespace std;
* 将using用于基类中的两个函数名,这样派生类中如果定义重名,但是参数不同的函数,基类的函数不会被隐藏,两个重载函数会并存在作用域中
*
(二)虚基类
当某类的部分或者全部直接基类是从另一个共同基类派生来的,这些直接基类中从上一级共同基类继承来的成员就拥有相同的名称
* 这些同名数据成员在内存中同时拥有多个副本,同一个函数名会有多个映射,可以使用作用域分辨符来唯一标识并访问他们
* 也可以将这些共同基类设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只有一个副本,同一个函数名也只有一个映射
* 虚基类的声明是在派生类的定义过程中进行的,其语法形式是:
* class 派生类名:virtual 继承方式 基类名
* 声明基类为派生类的虚基类,在多继承情况下,虚基类的关键字的作用范围和继承方式的关键字相同,只对紧随其后的基类起作用
* 声明虚基类之后,虚基类的成员在进一步派生的过程中和派生类一起维护同一个内存的数据副本
* 在定义了虚基类后,就等于告诉了系统,这里的a是base1和base2所共有的,对于调用base1和base2构造函数的修改都是针对同一个a而言(也就是基类和两个派生类所共有的)。
*
//虚基类
class A
{
public:
void A1()
{
cout << "A" << endl;
}
int x;
//~A() { cout << "a1" << endl; }
};
class B:virtual public A
{
public:
int y;
//~B() { cout << "b1" << endl; }
};
class C :virtual public A
{
public:
int z;
//~C() { cout << "c1" << endl; }
};
class D : public B, public C
{
public:
int q=x;
void A1()
{
cout << "D" << endl;
}
private:
};
int main()
{
D d;
d.x = 1;
cout << d.x;
d.A1();
return 0;
}
(三)虚基类及其派生类的构造函数
* 构造一个类的对象的一般顺序是:
* 1、如果该类有直接或者间接的虚基类,则先执行虚基类的构造函数
* 2、如果该类有其他基类,按照他们在继承中声明的顺序,分别执行他们的构造函数
* 3、按照在类的定义中出现的顺序,对派生类中的新增成员及逆行初始化,对于类类型的成员对象,如果出现在构造函数初始化列表中,,执行其构造函数,没出现执行默认构造函数
* 对于基本类型的成员对象,如果出现在构造函数的初始化列表,则使用其中的值为其赋初值,否则什么都不做
* 4、执行构造函数的函数体
//虚基类的构造函数
class A
{
public:
A(int a) : x(a) {}
void A1()
{
cout << "A" << endl;
}
int x;
//~A() { cout << "a1" << endl; }
};
class B :virtual public A
{
public:
B(int a) :A(a) {}
int y;
//~B() { cout << "b1" << endl; }
};
class C :virtual public A
{
public:
C(int a) :A(a) {}
int z;
//~C() { cout << "c1" << endl; }
};
class D : public B, public C
{
D(int a) :A(a),B(a), C(a){}//构造函数
public:
int a;
void A1()
{
cout << "D" << endl;
}
private:
};
int main()
{
D d(2);
d.x = 1;
cout << d.x;
d.A1();
return 0;
}
六、深度探讨
(一)组合与继承
(二)派生类对象的内存布局
* 派生类对象的内存布局满足的要求是,一个基类指针,无论指向基类对象,还是派生类对象,通过它来访问一个基类中定义的数据成员,都可以使用相同的步骤
* 1、单继承的情况
* 2、多继承的情况
* 3、虚拟继承的情况
(三)基类向派生类转换以及安全问题
派生类指针可以隐含的转换为基类指针,但是基类指针想转换为派生类指针,必须显性进行
* A*p=new B()//隐形转换
* B*p2=static_cast<B*>(p2);//显性转换;
* 1、基类对象无法显性转化成派生类对象
* B b;
* A a=static_csat<A>(b)//不可以,是错误的,
* 除非A类中有接收B类型参数的构造函数
* 派生类可以转换成基类,是因为,基类对象的复制构造函数接受一个基类引用的参数,二派生类的对象快要给基类初始化,因此基类的复制构造函数可以被调用,转换就可以发生
* 2、执行基类向派生类的转换时,一定要确保被转换的指针和引用所指向的对象符合转换的目标
* 否则会发生不安全
* 3、在多重继承情况下,执行基类指针到派生类指针的转换时,有时需要将指针所存储的地址进行调整后,才能得到新指针的值
* 如果A是B的虚拟基类,B类型的指针可以隐含的转换为A类型的指针,但是A类型的指针却没有办法通过static_cast转换成B类型的指针
* 因为位置不同,难以计算出转换后的地址
* 4、如果指针的转换涉及void类型的转换,即使最初的指针类型和最后的指针类型是兼容的,但是只有最初和最后的类型不完全相同,那么转换结果就是不正确的