这周结束了程序设计B的理论学习,我想通过这次的总结的机会系统总结这两章一些细节,最后在谈谈这两章的收获。
一、 继承
-
类继承关系的语法形式
class 派生类名 : 基类名表
{
数据成员和成员函数声明
}; -
派生类的生成过程经历了三个步骤:
●吸收基类成员(全部吸收(构造、析构除外),但不一定可见)
在C++的继承机制中,派生类吸收基类中除构造函数和析构函数之外的全部成员。
●改造基类成员
通过在派生类中定义同名成员(包括成员函数和数据成员)来屏蔽(隐藏)在派生类中不起作用的部分基类成员。
●添加派生类新成员
仅仅继承基类的成员是不够的,需要在派生类中添加新成员,以保证派生类自身特殊属性和行为的实现。 -
一个例子:
#include<iostream>
using namespace std ;
class A
{ public :
void get_XY() { cout << "Enter two numbers of x, y : " ; cin >> x >> y ; }
void put_XY() { cout << "x = "<< x << ", y = " << y << '\n' ; }
protected: int x, y ;
};
class B : public A
{ public :
int get_S() { return s ; };
void make_S() { s = x * y ; }; // 使用基类数据成员x,y
protected: int s;
};
class C : public B
{ public :
void get_H() { cout << "Enter a number of h : " ; cin >> h ; }
int get_V() { return v ; }
void make_V() { make_S(); v = get_S() * h ; } // 使用基类成员函数
protected: int h, v;
};
int main()
{ A objA ;
B objB ;
C objC ;
cout << "It is object_A :\n" ;
objA.get_XY() ;
objA.put_XY() ;
cout << "It is object_B :\n" ;
objB.get_XY() ;
objB.make_S() ;
cout << "S = " << objB.get_S() << endl ;
cout << "It is object_C :\n" ;
objC.get_XY() ;
objC.get_H();
objC.make_V() ;
cout << "V = " << objC.get_V() << endl ;
}
- 基类与子类的set、show函数
void set(){ //隐藏了基类中的同名成员
Person::set(); //调用继承于基类的成员函数访问继承于基类的数据成员
cout<<"zhuanye\tt_class\tscore"<<endl;
cin>>zhuanye>>t_class>>score;
}
void show() {
Person::show();
cout<<zhuanye<<" "<<t_class<<" "<<score<<endl;
}
-
●派生类定义了与基类同名的成员,在派生类中访问同名成员时屏蔽(hide)了基类的同名成员
●在派生类中使用基类的同名成员,显式地使用类名限定符:
类名 :: 成员 -
●基类定义的静态成员,将被所有派生类共享(基类和派生类共享基类中的静态成员)
● 派生类中访问静态成员,用以下形式显式说明:
类名 :: 成员 或
通过对象访问 对象名 . 成员 -
基类的初始化
构造函数
● 派生类构造函数声明为:
派生类构造函数 ( 变元表 ) : 基类 ( 变元表 ) , 对象成员1( 变元表 ) … 对象成员n ( 变元表 )
●构造函数执行顺序:基类 -> 对象成员-> 派生类
#include<iostream>
using namespace std ;
class parent_class
{ int data1 , data2 ;
public :
parent_class ( int p1 , int p2 ) { data1 = p1; data2 = p2; }
int inc1 () { return ++ data1; }
int inc2 () { return ++ data2 ; }
void display () {cout << "data1=" << data1 << " , data2=" << data2 << endl ; }
};
class derived_class : private parent_class
{ int data3 ;
parent_class data4 ;
public:
derived_class ( int p1 , int p2 , int p3 , int p4 , int p5 ): parent_class ( p1 , p2 ) , data4 ( p3 , p4 )
{ data3 = p5 ; }
int inc1 ( ) { return parent_class :: inc1 ( ) ; }
int inc3 ( ) { return ++ data3 ; }
void display ( )
{ parent_class :: display ( ) ; data4.display ( ) ;
cout << "data3=" << data3 << endl ;
}
} ;
int main ( )
{ derived_class d1 ( 17 , 18 , 1 , 2 , -5 ) ; d1 . inc1 ( ) ; d1 . display ( ) ; }
注:
1.派生类构造函数和析构函数的使用原则 基类的构造函数和析构函数不能被继承
2. 如果基类没有定义构造函数或有无参的构造函数,
3.派生类也可以不用定义构造函数 如果基类无无参的构造函数,派生类必须定义构造函数
4.如果派生类的基类也是派生类,则每个派生类只负责直接基类的构造
5. 派生类是否定义析构函数与所属的基类无关
析构函数
(1)当派生类中不含对象成员时
●在创建派生类对象时,构造函数的执行顺序是:基类的构造函数→派生类的构造函数;
●在撤消派生类对象时,析构函数的执行顺序是:派生类的析构函数→基类的析构函数。
(2)当派生类中含有对象成员时
●在定义派生类对象时,构造函数的执行顺序:基类的构造函数→对象成员的构造函数→派生类的构造函数;
●在撤消派生类对象时,析构函数的执行顺序:派生类的析构函数→对象成员的析构函数→基类的析构函数。
- 多继承:
构造函数
多个基类的派生类构造函数可以用初始式调用基类构造函数初始化数据成员。多个直接基类构造函数执行顺序取决于定义派生类时指定的各个继承基类的顺序。
派生类名(参数总表):基类名1(参数表1),基类名2(参数表2),…,基类名n(参数表n)
{
// 派生类新增成员的初始化语句
}
多继承方式下构造函数的执行顺序:
●先执行所有基类的构造函数
●再执行对象成员的构造函数
●最后执行派生类的构造函数
析构函数
析构函数的执行顺序与多继承方式下构造函数的执行顺序完全相反,
首先对派生类新增的数据成员进行清理,
再对派生类对象成员进行清理,
最后才对基类继承来的成员进行清理。
- 虚基类:
!!!要使这个公共基类在派生类中只产生一个子对象,必须对这个基类声明为虚继承,使这个基类成为虚基类。
class B { public : int b ;} ;
class B1 : virtual public B { private : int b1 ; } ;
class B2 : virtual public B { private : int b2 ; } ;
class C : public B1 , public B2
{ private : float d ; } ;
//有:
C cc ;
cc . b // ok
10. 例:!!!
#include<iostream>
using namespace std ;
class A
{ public :
A ( ) { cout << "class A" << endl ; }
} ;
class B : public A
{ public :
B ( ) {cout << "class B" << endl ; }
} ;
class C : public A
{ public :
C ( ) {cout << "class C" << endl ; }
} ;
class D : public B , public C
{ public :
D ( ) {cout << "class D" << endl ; }
} ;
int main ( )
{ D dd ; }
两次调用A的构造函数
- 赋值兼容规则:
赋值兼容规则指在程序中需要使用基类对象的任何地方,都可以用公有派生类的对象来替代。
赋值兼容规则中所指的替代包括以下的情况:
a .派生类的对象可以赋给基类对象(强制类型转换)
b .派生类的对象可以初始化基类的引用
c .派生类的对象的地址可以赋给基类类型的指针
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
例:
class Base{
…
};
class Derived:public Base{
…
};
(1) 可以用派生类对象给基类对象赋值。
这样赋值的效果是:对象b中所有数据成员都将具有对象d中对应数据成员的值。例如:
Base b;
Derived d;
b=d;
(2) **可以用派生类对象来初始化基类的引用。**例如:
Derived d;
Base &br=d;
(3) 可以把派生类对象的地址赋值给指向基类的指针
这种形式的转换,是在实际应用程序中最常见到的。例如:
Derived d;
Base *bptr=&d;
(4) 可以把指向派生类对象的指针赋值给指向基类对象的指针。
例如:
Derived *dptr,obj; dptr=&obj;
Base *bptr=dptr;
### 允许将一个声明为指向基类的指针指向其公有派生类对象,但是不能将一个声明为指向派生类对象的指针指向其基类的一个对象。
二、 虚函数与多态
1.定义:
- 编译时的多态是通过静态联编来实现的。静态联编就是在编译阶段完成的联编。编译时多态性主要是通过函数重载和运算符重载实现的。
- 运行时的多态是用动态联编实现的。动态联编是运行阶段完成的联编。运行时多态性主要是通过虚函数来实现的。
1.联编是指一个程序模块、代码之间互相关联的过程。 静态联编,是程序的匹配、连接在编译阶段实现,也称为早期匹配。
重载函数使用静态联编。
2.动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编。 switch 语句和 if 语句是动态联编的例子。
- 基类指针和派生类指针与基类对象和派生类对象4种可能匹配:!!!
直接用基类指针引用基类对象;
直接用派生类指针引用派生类对象;
用基类指针引用一个派生类对象;
用派生类指针引用一个基类对象。
&& 基类指针引用派生类对象
#include<iostream>
#include<cstring>
using namespace std ;
class A_class
{ char name[20] ;
public : void put_name( char * s ) { strcpy_s( name, s ) ; }
void show_name() { cout << name << "\n" ; }
};
class B_class : public A_class
{ char phone_num[ 20 ] ;
public : void put_phone( char * num ) { strcpy_s ( phone_num , num ) ; }
void show_phone() { cout << phone_num << "\n" ; }
};
int main()
{ A_class * A_p ; A_class A_obj ;
B_class B_obj ;
A_p = & A_obj ;
A_p -> put_name( "Wang xiao hua" ) ; A_p -> show_name() ;
A_p = & B_obj ;
A_p -> put_name( "Chen ming" ) ; A_p -> show_name() ;
B_obj.put_phone ( "5555_12345678" );
( ( B_class * ) A_p ) -> show_phone() ;
}
&& 派生类指针引用基类对象
派生类指针只有经过强制类型转换之后,才能引用基类对象
#include<iostream>
using namespace std ;
class Date{
public:
Date( int y, int m, int d ) { SetDate( y, m, d ); }
void SetDate( int y, int m, int d ) { year = y ; month = m ; day = d ; }
void Print() { cout << year << '/' << month << '/' << day << "; " ; }
protected : int year , month , day ;
} ;
class DateTime : public Date
{ public :
DateTime( int y, int m, int d, int h, int mi, int s ) : Date( y, m, d ) { SetTime( h, mi, s ); }
void SetTime( int h, int mi, int s ) { hours = h; minutes = mi; seconds = s; }
void Print()
{ ( ( Date * ) this ) -> Print();
//对 this 指针作类型转换
调用基类成员函数
cout << hours << ':' << minutes << ':' << seconds << '\n' ;
}
private: int hours , minutes , seconds ;
};
int main() {
DateTime dt( 2009, 1, 1, 12, 30, 0 ) ;
dt.Print() ;
}
根据赋值兼容,用基类类型的指针指向派生类,就可以通过这个指针来使用类(基类或派生类)的成员函数。
如果这个函数是普通的成员函数,通过基类类型的指针访问到的只能是基类的同名成员。
而如果将它设置为虚函数,则可以使用基类类型的指针访问到指针正在指向的派生类的同名函数。从而实现运行过程的多态。
- 一个例子:
实现动态联编方式的**前提**:
1. 先要声明虚函数
2. 类之间满足赋值兼容规则
3. 通过指针与引用来调用虚函数。
#include<iostream>
using namespace std ;
class Base
{ public : Base(char xx) { x = xx; }
virtual void who() { cout << "Base class: " << x << "\n" ; }
protected: char x;
} ;
class First_d : public Base
{ public : First_d(char xx, char yy):Base(xx) { y = yy; }
void who() { cout << "First derived class: "<< x << ", " << y << "\n" ; }
protected: char y;
} ;
class Second_d : public First_d
{ public :
Second_d( char xx, char yy, char zz ) : First_d( xx, yy ) { z = zz; }
void who() { cout<<"Second derived class: "<<x<<", "<<y<<", "<<z<<"\n" ; }
protected: char z;
} ;
int main()
{ Base B_obj( 'A' ) ; First_d F_obj( 'T', 'O' ) ; Second_d S_obj( 'E', 'N', 'D' ) ;
Base * p ;
p = & B_obj ; p -> who() ;
p = &F_obj ; p -> who() ;
p = &S_obj ; p -> who() ;
}
注意:
- 一个虚函数,在派生类层界面相同的重载函数都保持虚特性
- 虚函数必须是类的成员函数
- 不能将友元说明为虚函数,但虚函数可以是另一个类的友元
- 析构函数可以是虚函数,但构造函数不能是虚函数
class base
{ public :
virtual void vf1 ( ) ;
virtual void vf2 ( ) ;
virtual void vf3 ( ) ;
void f ( ) ;
} ;
class derived : public base
{ public :
void vf1 ( ) ; // 虚函数
void vf2 ( int ) ; // 重载,参数不同,虚特性丢失
char vf3 ( ) ; // error,仅返回类型不同
void f ( ) ; // 非虚函数重载
} ;
void g ( )
{ derived d ;
base * bp = & d ; // 基类指针指向派生类对象
bp -> vf1 ( ) ; // 调用 deriver :: vf1 ( )
bp -> vf2 ( ) ; // 调用 base :: vf2 ( )
bp -> f ( ) ; // 调用 base :: f ( )
} ;
- 普通析构函数在删除动态派生类对象的调用情况 和
虚析构函数在删除动态派生类对象的调用情况
one:
#include<iostream>
using namespace std ;
class A
{ public:
~A(){ cout << "A::~A() is called.\n" ; }
} ;
class B : public A
{ public:
~B(){ cout << "B::~B() is called.\n" ; }
} ;
int main() {
A *Ap = new B ;
B *Bp2 = new B ;
cout << "delete first object:\n" ;
delete Ap;
cout << "delete second object:\n" ;
delete Bp2 ;
}
two:
#include<iostream>
using namespace std ;
class A
{ public:
virtual ~A(){ cout << "A::~A() is called.\n" ; }
} ;
class B : public A
{ public:
~B(){ cout << "B::~B() is called.\n" ; }
} ;
int main()
{ A *Ap = new B ;
B *Bp2 = new B ;
cout << "delete first object:\n" ;
delete Ap;
cout << "delete second object:\n" ;
delete Bp2 ;
}
说明:
1.派生类应该从它的基类公有派生。
2.必须首先在基类中定义虚函数。
3.派生类对基类中声明虚函数重新定义时,关键字virtual可以不写。
4.一般通过基类指针访问虚函数时才能体现多态性。
5.一个虚函数无论被继承多少次,保持其虚函数特性。
6.虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态函数。
7.构造函数、内联成员函数、静态成员函数不能是虚函数。 (虚函数不能以内联的方式进行处理)
8.析构函数可以是虚函数,通常声明为虚函数。
6.例: 简单图形类
class figure
{ protected : double x,y;
public: void set_dim(double i, double j=0) { x = i ; y = j ; }
virtual void show_area() = 0 ;
};
class triangle : public figure
{ public :
void show_area()
{ cout<<"Triangle with high "<<x<<" and base "<<y <<" has an area of "<<x*0.5*y<<"\n"; }
};
class square : public figure
{ public:
void show_area()
{ cout<<"Square with dimension "<<x<<"*"<<y <<" has an area of "<<x*y<<"\n"; }
};
class circle : public figure
{ public:
void show_area()
{ cout<<"Circle with radius "<<x;
cout<<" has an area of "<<3.14*x*x<<"\n";
}
};
#include<iostream>
using namespace std ;
#include"figure.h"
int main()
{ triangle t ; //派生类对象
square s ; circle c;
t.set_dim(10.0,5.0) ;
t.show_area();
s.set_dim(10.0,5.0) ;
s.show_area() ;
c.set_dim(9.0) ;
c.show_area() ;
}
三、总结
经过了这段时间的学习,我所接受的c++继承、多态知识逐步趋于系统、完整。刚开始接触赋值兼容规则、联编时还有些懵,随着一些例子的演示,我逐渐理解了这些知识点。所谓赋值兼容规则,就是空间大的类赋值给空间小的类,但是只能使用空间小的类的成员。所谓多态,就是个名字,多种语义。谈到知识点,重要的是运用。像赋值兼容规则等一些不知如何运用到系统程序中,稍微有些力不从心啊,看来还是得多看看相关的文章了啊。马上就要进入课程设计了,如果这些知识可以写到程序中(不增大复杂度的话),尽量运用一下,不能让这些自己学过的成为盲区哈!。