C++学习7-面向对象编程基础(多态性与虚函数、 IO文件流操作)

多态

多态性是指对不同类的对象发出相同的消息将返回不同的行为,消息主要是指类的成员函数的调用,不同的行为是指不同的实现;

函数重载

  • 函数重载是多态性的一种简单形式,它是指允许在相同的作用域内,相同的函数名对应着不同的实现;
  • 函数重载的条件是要求函数参数的类型或个数有所不同。对成员函数的重载有以下的三种表达方式
  1. 在一个类中重载
  2. 在不同类中重载
  3. 基类的成员函数在派生类里面重载;

另外,如果是继承类中,子类与父类的成员函数同名的情况称为重定义;
当我们想要指定哪个类的成员函数需要指定是哪个类,使用格式如下:

	
    对象名.指定类名::成员函数();
	
	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;
}

//运行结果
//我是子类的函数
//我是父类的函数


虚函数

  • 虚函数是一种非静态的成员函数。编译器将其进行动态联编,使调用虚函数的对象在运行时确定,以实现动态联编的多态性;

  • 基类函数具有虚特性的条件是:

  1. 在基类中,将该函数说明为虚函数(virtual);
  2. 定义基类的公有派生类
  3. 在基类的公有派生类中定义该虚函数;
  4. 定义指向基类的指针变量,它指向基类的公有派生类的对象;

例如:在子类继承父类后,调用子类的成员函数时如果父类有一个同名的成员函数,那么运行时会优先调用父类的成员函数。覆盖了子类的成员函数,影响了我们期望程序运行的结果.
加了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 << "正在吃东西!
";
	};

	//使用虚函数,子类调用的是子类自己的成员函数
	//virtual void play() 
	//{
	//	cout << name << "正在玩儿! Pet类的play() 
 ";
	//};

	//运行结果:
	    //加菲正在吃东西!
		//加菲正在玩儿!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 << "正在爬树!
";
	};
	void play() 
	{
		Pet::play();
		cout << name << "玩毛线球!  Cat类的Play() 
";
	};
};

//子类Dog继承自Pet类
class Dog : public Pet
{
public:
	Dog(string theName);
	//Dog类独有的行为;
	void SayHello()
	{
		cout << name << "旺~旺~
";
	};
	void play()
	{
		Pet::play();
		cout << name << "正在追赶那只该死的猫!  Dog类的Play()
";
	};
};

//基类的构造函数
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("求圆形面积
");

		return pi*r*r;
	}
private:

};
//子类【矩形】继承父类【图形】
class 矩形 :public 图形
{
public:
	virtual double 求面积(double L, double m)
	{
		printf("求矩形面积
");
		//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

原文地址:https://www.cnblogs.com/17bdw/p/6096243.html