补课
静态联编 (使用对象调用) 编译时决定 通过函数重载和模板体现 多态 动态联编 (使用指针或引用调用) 运行时决定 通过继承和虚函数来实现
注意:Derived objD; // 注意:会调用基类构造函数:基类执行Base(0),所以将ObjB.a置为0
虚函数 有了虚函数才能实现动态联编
4.3 虚函数 1. 虚函数的定义及使用 1) 定义: C++通过虚函数实现运行时的多态性。 声明虚函数的方法是在基类中的成员函数原型前加上关键字virtual。其格式如下: class 类名 { …… virtual 类型 成员函数名(参数表); …… };
改造(who()加上virtual) 派生类对象的地址赋值给指向基类的指针
[说明]: 必须首先在基类中声明虚函数。 派生类中与基类虚函数原型完全相同(包括函数名、返回类型、参数个数和参数类型的顺序)的成员函数,即使在说明时前面没有冠以关键字virtual也自动成为虚函数。 只有非静态成员函数可以声明为虚函数。
不允许在派生类中定义与基类虚函数名字及参数特征都相同,仅仅返回类型不同的成员函数。 仅仅函数名相同但参数特征不同的函数,系统视为不同的函数(即函数的重载),但派生类会屏蔽基类的方法。 通过声明虚函数来使用C++提供的多态性机制时,派生类应该从它的基类公有派生。
#include <iostream #include <iostream.h> class A { public: A() { cout<<"A构造 ";foo();} // 在这里,无论如何都是A::foo()被调用! virtual ~A() { cout<<"A析构 ";foo();} // 同上,无论如何都是A::foo()被调用! virtual void foo(){cout<<"A foo \n";}; }; class B: public A void foo(int i){ foo(), cout<<“B foo \n”;}; //因为屏蔽,foo()不存在,err! 不能直接访问,只能A::foo()访问 B() {cout<<“B构造 ”; foo();} //因为屏蔽,foo()不存在,err! virtual ~B() { cout<<"B析构 ";foo();} //因为屏蔽,foo()不存在, err! void main() A * p = new B; delete p; }
2) 虚函数的使用三要素 在基类定义中,必须把成员函数定义为虚函数。 若仅仅是返回类型的不同,则C++认为是错误的;如果是函数原型不同,仅函数名相同,则C++认为是一般的函数重载。 2) 虚函数的使用三要素 在基类定义中,必须把成员函数定义为虚函数。 在public派生类定义中,对虚函数的更新定义只能修改函数体内容,而函数名、返回类型、参数个数、参数类型及参数顺序必须与基类的定义完全相同。 必须用指向基类的指针(或引用)访问虚函数。 尽管可以像调用其他成员函数那样显式地用对象名来调用一个虚函数,但只有在同一个指向基类的指针(或引用)访问虚函数时,运行时的多态性才能实现。
[例]: class A { public: virtual void show() { cout<<“AAA…”<<endl; } //必须有virtual,否则不是虚函数 }; class B : public A void show() {cout<<“BBB…”<<endl;} //派生类中即使在说明时前面没有冠以关键字virtual也自动成为虚函数。
Figure called! Rectangle called! Triangle called!
因为使用普通对象调用虚函数时,系统仍然以静态联编方式完成对虚函数的调用。 void disp( A *p ) { p-> show( ); } main() { A *pa = new A; B *pb = new B; disp( pa ); disp( pb ); B tb; A t = tb; t.show(); delete pa; delete pb; } 或者用“ void disp( A &p )”, 但不可用“ void disp( A a)” 因为使用普通对象调用虚函数时,系统仍然以静态联编方式完成对虚函数的调用。
虚函数的限制 构造函数不能是虚函数,因为构造时对象还是一片未定型的空间。只有在构造完成后,对象才能成为一个类的名副其实的实例。 析构函数可以是虚函数,而且通常说明为虚函数。将析构函数定义为虚函数的目的在于:使用delete运算符删除一个对象时,能确保析构函数被正确的执行。这是因为,设置虚析构函数后,可以利用动态联编方式选择析构函数。
在这个例子中,在执行delete a的时候,实际上只有A::~A()被调用了,而B类的析构函数并没有被调用! 如果将上面A::~A()改为virtual,就可以保证B::~B()也在delete a的时候被调用了。因此基类的析构函数都必须是virtual的。
C++中虚函数的实现 使用虚函数,系统要有一定的空间开销,当一个类带有虚函数时,编译系统就会为该类构造一个虚函数表(virtual function table,简称 V-Table )。它是一个指针数组,存放每个虚函数的入口地址,系统在进行多态关联时的时间开销很少。因此,多态是高效的。
1-private的虚函数能否产生多态? 虚函数使用技巧 class A { public: void foo() { bar();} private: virtual void bar() { cout<<"a\n"; } }; class B: public A virtual void bar() { cout<<"B\n";} void main() A *p=new B; p->foo(); } 在这个例子中,虽然bar()在A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public或者protected的虚函数一样产生多态的效果。并不会因为它是private的,就发生A::foo()不能访问B::bar()的情况,也不会发生B::bar()对A::bar()的override不起作用的情况。 这种写法的语意是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用这个函数。 P指向B 执行B::bar ,动态联编
2-构造函数和析构函数中的调用虚函数能否产生多态? 一个类的虚函数在它自己类的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己类的虚函数自己“多态”。例如:
在new B的时候,基类A的构造函数被调用,但是在A的构造函数中,被调用的是A::foo()而不是B::foo()。 如果你希望delete p的时候,B::foo()被调用,那么你就错了(因为析构不虚)。 将~A() 为virtual ~A(),结果如下:
虚函数的限制 假定基类中有虚函数vf2(),在派生类中声明的成员函数vf2(int)【与基类中的虚函数vf2()名字相同,但参数不同】,vf2(int)是一般的函数重载,屏蔽了基类中的虚函数。
#include <iostream #include <iostream.h> class A { public: A() { cout<<"A构造 ";foo();} // 在这里,无论如何都是A::foo()被调用! virtual ~A() { cout<<"A析构 ";foo();} // 同上,无论如何都是A::foo()被调用! virtual void foo(){cout<<"A foo \n";}; }; class B: public A void foo(int i){ foo(), cout<<“B foo \n”;}; //因为屏蔽,foo()不存在,err! 不能直接访问,只能A::foo()访问 B() {cout<<“B构造 ”; foo();} //因为屏蔽,foo()不存在,err! virtual ~B() { cout<<"B析构 ";foo();} //因为屏蔽,foo()不存在, err! void main() A * p = new B; delete p; }
1. 纯虚函数 为了强制派生类重新定义其基类的虚函数,C++语言引入了纯虚函数(Pure Virtual Function)的概念。 一般格式: virtual 函数原型=0; [例]: virtual void ShowArea()=0;
具有纯虚函数的类称为?
运算符的重载形式 运算符的重载形式有以下两种: ① 运算符重载为类的成员函数 重载为类的成员函数; 重载为类的友元函数。 “operator”是定义运算符重载函数的关键字。 运算符的重载形式有以下两种: 重载为类的成员函数; 重载为类的友元函数。 ① 运算符重载为类的成员函数 运算符函数一般语法形式为: 函数类型 operator 运算符(形参表列) { 函数体; // 对运算符的重载处理 } “函数类型”指定了重载运算符的返回值类型,也就是运算结果类型。 “运算符”给定了要重载的运算符名称,必须是C++中可重载的运算符。 该成员函数必须放在类的public段
运算符重载的规则 [说明]: 重载时,运算符的优先级、结合性以及操作数的个数都不能改变。 由于重载不能改变运算符对象(操作数)的个数。 重载运算符不能有默认参数。因为如果有默认参数,则会改变运算符的参数个数。
加法运算符重载的方法和规则 #include <iostream> using namespace std; class Complex { public: Complex(double r=0.0, double i=0.0) { real=r; imag=i;} Complex Add(Complex &c); // 复数的加法运算 Complex Sub(Complex &c); // 复数的减法运算 void display(); // 复数输出 private: double real; double imag; };
Complex Complex::Add(Complex &c) { return Complex(real+c.real, imag+c.imag); } Complex Complex::Sub(Complex &c) return Complex(real-c.real, imag-c.imag); void Complex::display() cout<<"("<<real<<","<<imag<<")"<<endl;
友元函数重载运算符
1重载输入运算符(2) 几点注意事项: 该重载函数的返回类型是istream类对象的引用【 istream & operator>>】,返回引用的目的在于,把几个输入运算符>>放在同一条输入语句中时,该重载函数仍能正确工作。cin>>x>>y>>z; (cin>>x返回cin,再cin>>y,这两个cin必需是同一个对象,才能确保输入缓冲区的连续读取) (2)该运算符函数有两个参数,第一个参数是对istream类对象的引用,它出现在运算符>>的左边,第二个参数是出现在运算符右边的自定义类型对象。 (3)重载运算符函数operator>>的第二个参数必须是一个引用(因为输入的值要保存在该对象中)。
int main() { Point a(10,10); cin>>a; return 0; } 例exam12-6 输入运算符的重载 #include <iostream.h> class Point { public: Point(int i=0,int j=0) { x=i;y=j;} //<<、>>必须友元重载 friend istream &operator>>(istream &in,Point &a); private: int x,y; }; istream &operator>>(istream &in,Point &a) cout<<"Enter the x and y :"; in>>a.x; in>>a.y; return in; } int main() { Point a(10,10); cin>>a; return 0; }
2重载输出运算符 用友元函数重载输出运算符<<来实现用户自定义类型对象的输出。定义运算符函数的格式如下: ostream &operator<< (ostream &out, user-type &obj) { out<<obj.item1; 。。。。。 out<<obj.itemn; return out; } 与重载输出运算符函数一样,重载输出运算符也不能是成员函数法,但可以是该类的友元函数或独立函数。 user-type &obj 或user-type obj ,因为输出,不会改变obj
#include <iostream.h> class Point { public: Point(int i=0,int j=0) { x=i;y=j;} friend istream &operator>>(istream &in,Point &a); friend ostream &operator<<(ostream &out,Point &a); private: int x,y; }; istream &operator>>(istream &in,Point &a) cout<<"Enter the x and y :"; in>>a.x; in>>a.y; return in; }
ostream &operator<<(ostream &out,Point &a) { out<<a.x<<","<<a.y<<endl; return out; } int main() Point a(10,10); cout<<a; cin>>a; return 0;
5.1 模板的概念 [引例]: 可以看出,这些函数版本的功能都是相同的,只是参数类型和函数返回类型不同。 int max( int x, int y) { return (x>y) ? x: y; } double max( double x, double y) char max( char x, char y) 可以看出,这些函数版本的功能都是相同的,只是参数类型和函数返回类型不同。 那么能否为这些函数只写出一套代码呢? C++解决这个问题的一个方法就是使用模板。
函数模板 函数模板的定义和模板函数的生成 1) 定义函数模板的一般形式: template <class 类型参数名1, class 类型参数名 2, …> 函数返回值类型 函数名(形参表) { //函数体 } 说明函数模板的关键字。 关键字class(或typename)后面的类型参数名是模板形参,它可以代表基本数据类型,也可以代表类。
函数模板
两种解决方法 1、强制类型转换 2、显示给出模板实参
第三种解决方法
字符串比较大小?
函数模板特化
vector push_back()可以将一个元素添加到容器的末尾
类模板的定义 定义类模板,包含两方面内容: 定义类; 在类定义体外定义体内未定义的成员函数。 1) 定义类的一般形式: template<class T1, class T2,……> //模板声明 class Name { … //类的定义 };
向量类模板定义 template<class T> class Vector { T *data; int size; public: Vector(int i) { data=new T[i]; } ~Vector() { delete[] data; } T &operator[](int i) { return data[i]; } };
既可以从类模板派生出类模板,也可以派生出普通类(非模板类)。主要有以下几种形式: 类模板的派生 既可以从类模板派生出类模板,也可以派生出普通类(非模板类)。主要有以下几种形式: 从类模板派生出类模板; 从类模板派生出普通类; 从普通类派生出类模板。
1) 从类模板派生出类模板 从类模板派生出新的类模板的格式如下所示: template <class T> class Base { …… }; template <class T > class Derived: public Base <T> 与定义一般派生类的格式类似,只是在指出它的父类时要加上模板参数。如,Base <T>。
2) 从类模板派生出普通类 从类模板派生出一个普通类的格式如下所示: template <class T> class Base { …… } ; class Derived: public Base<int> 首先,作为新派生出来的普通类的父类,必须是类模板实例化后生成的模板类。例如上面的Base<int>; 其次,在派生类定义之前不需要模板声明语句template <class T>。
template <class T> class Derived:public Base { T data; …… } 3) 从普通类派生出类模板 [例]: class Base { …… }; template <class T> class Derived:public Base { T data; …… }