多态
多态性是指对不同类的对象发出相同的消息将返回不同的行为,消息主要是指类的成员函数的调用,不同的行为是指不同的实现;
函数重载
- 函数重载是多态性的一种简单形式,它是指允许在相同的作用域内,相同的函数名对应着不同的实现;
- 函数重载的条件是要求函数参数的类型或个数有所不同。对成员函数的重载有以下的三种表达方式
- 在一个类中重载
- 在不同类中重载
- 基类的成员函数在派生类里面重载;
另外,如果是继承类中,子类与父类的成员函数同名的情况称为重定义;
当我们想要指定哪个类的成员函数需要指定是哪个类,使用格式如下:
对象名.指定类名::成员函数();
CTest obj;
obj.fun(); //调用子类的成员函数
obj.Base::fun(); //指定调用哪个类的成员函数
实例代码
#include "stdafx.h"
//1 函数重载,必须是在相同的作用域内
//2 下面的情况叫做重定义
//3 当处于类的继承关系的时候,子类有和父类同名的函数,或者变量
// 子类会覆盖掉父类的函数或者变量,这个情况叫做重定义
class Base
{
public:
void fun()
{
printf("我是父类的函数");
}
};
//继承Base类,继承类内同名函数的情况称为重定义
class CTest :public Base
{
public:
void fun()
{
printf("我是子类的函数");
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CTest obj;
obj.fun(); //调用子类的成员函数
obj.Base::fun(); //指定调用哪个类的成员函数
return 0;
}
//运行结果
//我是子类的函数
//我是父类的函数
虚函数
虚函数是一种非静态的成员函数。编译器将其进行动态联编,使调用虚函数的对象在运行时确定,以实现动态联编的多态性;
基类函数具有虚特性的条件是:
- 在基类中,将该函数说明为虚函数(virtual);
- 定义基类的公有派生类
- 在基类的公有派生类中定义该虚函数;
- 定义指向基类的指针变量,它指向基类的公有派生类的对象;
例如:在子类继承父类后,调用子类的成员函数时如果父类有一个同名的成员函数,那么运行时会优先调用父类的成员函数。覆盖了子类的成员函数,影响了我们期望程序运行的结果.
加了virtual关键字后,那么在定义父类对象指向子类型对象的指针时,就可以调用子类的成员函数;
实例代码
以下程序之所以会有这样奇怪的行为,是因为C++的创始者希望用C++生成的代码至少和它的老前辈C一样快。
所以程序在编译的时候,编译器将检查所有的代码,在如何对某个数据进行处理和可以对该类型的数据进行何种处理之间寻找一个最佳点。
正是这一项编译时的检查影响了刚才的程序结果:cat 和 dog 在编译时都是 Pet 类型指针,编译器就认为两个指针调用的 play() 方法是 Pet::play() 方法,因为这是执行起来最快的解决方案。
而引发问题的源头就是我们使用了 new 在程序运行的时候才为 dog 和 cat 分配 Dog 类型和 Cat 类型的指针。
这些是它们在运行时才分配的类型,和它们在编译时的类型是不一样的!
为了让编译器知道它应该根据这两个指针在运行时的类型而有选择地调用正确的方法(Dog::play() 和 Cat::play()),我们必须把这些方法声明为虚方法。
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;
class Pet
{
public:
Pet(string theName);
//吃东西
void eat()
{
cout << name << "正在吃东西!\n";
};
//使用虚函数,子类调用的是子类自己的成员函数
//virtual void play()
//{
// cout << name << "正在玩儿! Pet类的play() \n ";
//};
//运行结果:
//加菲正在吃东西!
//加菲正在玩儿!Pet类的play()
//加菲玩毛线球!Cat类的Play()
//欧迪正在吃东西!
//欧迪正在玩儿!Pet类的play()
//欧迪正在追赶那只该死的猫!Dog类的Play()
//不使用虚函数,子类调用的是Pet类的成员函数
void play()
{
cout << name << "正在玩儿! ";
cout << "Pet类的play()" << endl;
};
//运行结果
//加菲正在吃东西!
//加菲正在玩儿!Pet类的play()
//欧迪正在吃东西!
//欧迪正在玩儿!Pet类的play()
protected:
string name;
};
//子类Cat继承自Pet类
class Cat : public Pet
{
public:
Cat(string theName);
void climb()
{
cout << name << "正在爬树!\n";
};
void play()
{
Pet::play();
cout << name << "玩毛线球! Cat类的Play() \n";
};
};
//子类Dog继承自Pet类
class Dog : public Pet
{
public:
Dog(string theName);
//Dog类独有的行为;
void SayHello()
{
cout << name << "旺~旺~\n";
};
void play()
{
Pet::play();
cout << name << "正在追赶那只该死的猫! Dog类的Play()\n";
};
};
//基类的构造函数
Pet::Pet(string theName)
{
name = theName;
}
//子类调用基类的参数
Cat::Cat(string theName) : Pet(theName)
{
}
//子类调用基类的参数
Dog::Dog(string theName) : Pet(theName)
{
}
int main()
{
Pet *cat = new Cat("加菲"); //声明指向cat类对象的指针
Pet *dog = new Dog("欧迪"); //声明指向Dog类对象的指针
cat->eat();
cat->play(); //子类Cat调用Pet类的成员函数
dog->eat();
dog->play(); //子类Dog调用Pet类的成员函数
delete cat;
delete dog;
return 0;
}
静态联编与动态联编
联编是指程序自身彼此关联的过程。按照联编所进行的阶段不同,可分为静态联编和动态联编;
- 静态联编:是指在程序编译链接阶段进行联编,也称为早期联编。这种联编工作由于在程序运行之前完成,所调用的函数与执行该函数的代码之间的关系已确定;
实例代码
下面的代码是未使用虚函数时,CBase是父类,CMyClass是子类;
#include "stdafx.h"
#include <iostream>
//using namespace std;
using std::cout;
using std::endl;
class CBase {
public:
void fun() { cout << "CBase:fun" << endl; }
};
//子类CMyClass继承CBase父类(基类)
class CMyClass : public CBase {
public:
void fun() { cout << "CMyClass:fun" << endl; }
};
int _tmain(int argc, _TCHAR* argv[]) {
printf("hahahaha");
CBase *p;
CBase objA;
CMyClass objB;
p = &objA;
p->fun(); //这里调用objA对象的fun函数
p = &objB;
p->fun(); //这里调用objB对象的fun函数
return 0;
}
在反汇编代码里可以更清晰的看到静态联编是直接调用了已经固定的值;
int _tmain(int argc, _TCHAR* argv[]) {
012C2680 push ebp
012C2681 mov ebp,esp
012C2683 sub esp,0E8h
012C2689 push ebx
012C268A push esi
012C268B push edi
012C268C lea edi,[ebp-0E8h]
012C2692 mov ecx,3Ah
012C2697 mov eax,0CCCCCCCCh
012C269C rep stos dword ptr es:[edi]
012C269E mov eax,dword ptr [__security_cookie (012CB004h)]
012C26A3 xor eax,ebp
012C26A5 mov dword ptr [ebp-4],eax
printf("hahahaha");
012C26A8 push offset string "hahahaha" (012C8B3Ch)
012C26AD call _printf (012C13B1h)
012C26B2 add esp,4
CBase *p;
CBase objA;
CMyClass objB;
p = &objA;
012C26B5 lea eax,[objA] //引用objA对象
012C26B8 mov dword ptr [p],eax //解引用
p->fun();
012C26BB mov ecx,dword ptr [p]
012C26BE call CBase::fun (012C11C2h) //这里调用了objA对象的成员函数fun(),静态联编是固定的地址;
p = &objB;
012C26C3 lea eax,[objB]
012C26C6 mov dword ptr [p],eax
p->fun();
012C26C9 mov ecx,dword ptr [p]
012C26CC call CBase::fun (012C11C2h) //这里调用了objB对象的成员函数fun(),静态联编是固定的地址;
return 0;
012C26D1 xor eax,eax
}
-
动态联编:是指在程序运行时进行的联编,也称晚期联编。动态联编要求在运行时解决程序中的函数调用与执行该函数代码间的关系。
继承是动态联编的基础,虚函数是动态联编的关键。
实例代码
#include "stdafx.h"
#include <iostream>
//using namespace std;
using std::cout;
using std::endl;
class CBase {
public:
//继承是动态联编的基础,虚函数是动态联编的关键。
//这里使用了virtual关键字声明 fun()是一个虚函数
virtual void fun() { cout << "CBase:fun" << endl; }
};
//继承是动态联编的基础,虚函数是动态联编的关键。
//子类CMyClass继承CBase父类(基类)
class CMyClass : public CBase {
public:
//由于对虚函数进行重载,因此,在派生类中虚函数前的virtual关键字可以省略;
void fun() { cout << "CMyClass:fun" << endl; }
};
int _tmain(int argc, _TCHAR* argv[]) {
printf("hahahah");
CBase *p;
CBase objA;
CMyClass objB;
p = &objA;
p->fun(); //这里调用objA对象的fun函数
p = &objB;
p->fun(); //这里调用objB对象的fun函数
return 0;
}
在反汇编代码里可以更清晰的看到动态联编是直接调用的寄存器,而不是像静态联编代码时一样写入了固定的地址值;
20: int _tmain(int argc, _TCHAR* argv[]) {
00D22800 push ebp
00D22801 mov ebp,esp
00D22803 sub esp,0E8h
00D22809 push ebx
00D2280A push esi
00D2280B push edi
00D2280C lea edi,[ebp-0E8h]
00D22812 mov ecx,3Ah
00D22817 mov eax,0CCCCCCCCh
00D2281C rep stos dword ptr es:[edi]
00D2281E mov eax,dword ptr [__security_cookie (0D2B004h)]
00D22823 xor eax,ebp
00D22825 mov dword ptr [ebp-4],eax
21: printf("hahahah");
00D22828 push offset string "hahahah" (0D28B64h)
00D2282D call _printf (0D213DEh)
00D22832 add esp,4
22: CBase *p;
23: CBase objA;
00D22835 lea ecx,[objA]
00D22838 call CBase::CBase (0D213C5h)
24: CMyClass objB;
00D2283D lea ecx,[objB]
00D22840 call CMyClass::CMyClass (0D212ADh)
25: p = &objA;
00D22845 lea eax,[objA]
00D22848 mov dword ptr [p],eax
26: p->fun();
00D2284B mov eax,dword ptr [p]
00D2284E mov edx,dword ptr [eax]
00D22850 mov esi,esp
00D22852 mov ecx,dword ptr [p]
00D22855 mov eax,dword ptr [edx]
00D22857 call eax //这里调用了objA对象的成员函数fun(),动态联编是不确定的地址,运行时才能确定;
00D22859 cmp esi,esp
00D2285B call __RTC_CheckEsp (0D2116Dh)
27: p = &objB;
00D22860 lea eax,[objB]
00D22863 mov dword ptr [p],eax
28: p->fun();
00D22866 mov eax,dword ptr [p]
00D22869 mov edx,dword ptr [eax]
00D2286B mov esi,esp
00D2286D mov ecx,dword ptr [p]
00D22870 mov eax,dword ptr [edx]
00D22872 call eax //这里调用了objB对象的成员函数fun(),动态联编是不确定的地址,运行时才能确定;
//由于对虚函数进行重载,因此,在派生类中虚函数前的virtual关键字可以省略,所以objB也是不确定的地址;
00D22874 cmp esi,esp
00D22876 call __RTC_CheckEsp (0D2116Dh)
29: return 0;
00D2287B xor eax,eax
30: }
重载与重定义与重写
- 重载函数
C++编译器能够根据函数参数的类型、数量和排序顺序的差异,来区分同名函数,其技术称为重载函数;
只要参数个数不同,参数类型不同,参数顺序不同,参数顺序不同,函数就可以重载。然后返回类型不同则不允许函数重载。
例如:一个void型和char类型、int类型的同名函数就无法产生重载;
成员函数被重载的特征:
(1)相同的范围(在同一个类中)
(2)函数名字相同
(3)参数不同
(4)virtual关键字可有可无
- 重定义
子类覆盖基类的同名函数,函数类型可以不同;(继承)
- 重写
子类覆盖基类的同名函数,函数类型相同;
纯虚函数与抽象类
- 纯虚函数是一种特殊的虚函数,是一种没有具体实现的虚函数,它在父类无实现,但是功能交给不同的子类去实现;
实例代码
#include "stdafx.h"
class 图形 //包含有纯虚函数的类,叫做抽象类
//不能定义抽象类的对象
{
public:
virtual double 求面积(double r, double pi) = 0;//纯虚函数
void fun()
{
}
};
//子类【圆形】继承父类【图形】
class 圆形 :public 图形
{
public:
virtual double 求面积(double r,double pi)
{
printf("求圆形面积\n");
return pi*r*r;
}
private:
};
//子类【矩形】继承父类【图形】
class 矩形 :public 图形
{
public:
virtual double 求面积(double L, double m)
{
printf("求矩形面积\n");
//L*m
return L*m;
}
private:
};
int _tmain(int argc, _TCHAR* argv[])
{
//图形 obj;//不能定义抽象类的对象
//父类的纯虚函数功能由子类负责实现;
圆形 obj;
图形 *p = &obj;
double rs = p->求面积(20.5,3.14);
//父类的纯虚函数功能由子类负责实现;
矩形 obj1;
图形 *p1 = &obj1;
double rs1 = p1->求面积(20.5, 3.14);
return 0;
}
虚析构
虚析构函数一般用在基类,用于防止对衍生类对象delete基类指针造成的内存泄露。
如果基类的析构函数为虚函数,且衍生类有自定义析构函数实现时,delete基类指针时会同时调用衍生类的析构函数。如果基类析构函数不是虚函数,那么就只调用基类的析构函数,而基类析构函数不可能释放衍生类(子类)的其他资源。这是非常危险的!
实例代码
#include "stdafx.h"
#include <iostream>
using std::cout;
using std::endl;
class CClassA
{
public:
CClassA() { cout << "CClassA" << endl; }
//将析构函数定义为虚析构函数
virtual ~CClassA() {
cout << "~CClassA"<<endl;
}
//调用普通析构函数时的运行
// ~CClassA() {
// cout << "~CClassA" << endl;
//}
};
class CClassB : public CClassA
{
public:
CClassB() { cout << "CClassB" << endl; }
//将析构函数定义为虚析构函数
virtual ~CClassB() {
cout << "~CClassB" << endl;
}
//调用普通析构函数时的运行
//~CClassB() {
// cout << "~CClassB" << endl;
//}
};
int _tmain(int argc, _TCHAR* argv[])
{
CClassA *pobjA = new CClassB;
delete pobjA;
return 0;
}
//=======虚析构函数运行结果
// CClassA
// CClassB
// ~CClassB
// ~CClassA
//=======普通析构函数运行结果
//CClassA
// CClassB
// ~CClassA
C++ IO操作
IO流是指输入\输出的一系列操作,输出使用<<符号向cout输出字符,输入使用>>符号向cin写入字符,格式如下
//输出操作
cout >> "hello world!"
//输入操作,赋值变量a
int a;
cin << a ;
IO 常用关键字
- istream(输入流)类型,提供输入操作;
- ostream(输出流)类型,提供输出操作;
- cin,一个istream对象,从标准输入读取数据;
- cout,一个ostream对象,从标准输出写入数据;
- ofstream,向文件写入数据;
- ifstream,从文件读取数据;
- getline函数,从一个给定的istream读取一行数据,存入一个给定的string对象中;
>> 运算符,用来从一个istream对象读取输入数据
<< 运算符,用来从一个ostream对象写入输出数据
条件状态
- s.eof() ,若流s的eofbit置位,则返回true;
- s.close(), 关闭文件流
实例代码
#include "stdafx.h"
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
//使用ofstream 声明fout对象然后写进D:\\test1.txt
ofstream fOut("D:\\test1.txt");
//写入的内容
fOut << "hello world 1" <<endl;
fOut << "hello world 2" << endl;;
//关闭文件流
fOut.close();
//向文件读取数据
ifstream fIn("D:\\test1.txt");
//当文件流结束就跳出循环,到末尾返回0用非运算符成假,跳出循环
while (!fIn.eof())
{
char str[20];
//从一个给定的istream读取一行数据,存入一个给定的string对象中;
fIn.getline(str, 20);
cout << str << endl;
}
//关闭文件流
fIn.close();
return 0;
}
文件模式-格式控制
每个流都有一个关联的文件模式,用来指出如何使用文件。
- out ,以写方式打开
只可以对ofstream或fstream对象设定out模式
- in ,以读方式打开
只可以对ifsteam或fsteam对象设定in模式
- trunc,截断文件
只有当out也被设定时才可设定trunc模式
实例代码
#include "stdafx.h"
#include <fstream>
#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
//文件模式为读写方式打开,如果文件已存在则先删除该文件
fstream fInOut("D:\\test2.txt", ios::in | ios::out | ios::trunc);
//1.成员函数
double dNum = 9.46878546;
//在浮点数指定数字个数显示
fInOut.precision(4);
fInOut << dNum << endl;
//2.格式控制符
//设置浮点值的精度。
fInOut << setprecision(6) << dNum;
fInOut << setprecision(5) << dNum;
fInOut << setprecision(3) << dNum;
fInOut << setprecision(2) << dNum;
fInOut.close();
return 0;
}
建议完成《C++ primer》的练习
第八章 IO库
建议完成本章的练习:8.1, 8.2, 8.6, 8.7, 8.10