C++面向对象编程基础

面向对象编程:
1.面向对象程序设计
2.类与对象
3.类的继承和派生
4.多态性
5.类的高级特性
6.调试与异常
7.输入输出与文件读写

一、面向对象程序设计:

1.1 面向过程编程

面向过程POP 以功能作为核心,专注于问题如何解决,如何实现,
讲功能实现的细节分解成很多个的步骤,每个步骤被定义成函数,通过函数来实现整个需求
POP 函数作为最小单元,主要考虑怎么做

面向过程编程=数据+算法

总结:

  • 优点:

​ 符合正常人的思考方式

  • 缺点:
    ​ 代码不容易复用,不易于扩展,程序的维护性差,程序的耦合度高

1.2面向对象编程

​ 面向对象编程OOP,考虑的核心不是需求和功能如何实现的问题,而是站在更高
​ 的维度分析需求,关心的是整体的框架
​ 具体怎么做? 拿到一个需求,需要将需求中涉及到的重要事务找出来,在分析每个事物
​ 怎么分析事物? 按照两个维度来分析,分别是属性和方法这两个维度,属性就是整个事物的特征,特征通过数据来表示,方法其实就是函数,函数可以执行,可以做事情

   这些函数做的事情都属于这个事物做的事情,成为这个事物的行为,也就是方法
   分析完事吴,做一个整合,把这个事物的属性和方法整合封装成一个类,最终通过多个类之间的协作,完成整个需求
   面向对象编程以类作为最小单元
   面向对象编程=对象+类+继承+多态+消息(对象调用他的属性或者方法)

小明学习c++去工作

  //第一个事物  人
属性:姓名 ,性别,年龄等;
   方法:学习   
//第二个事物  编程语言
    属性:名称,级别;
第三个事物  工作
    属性: 工作地点 工作职位
public class person
{
   string name;
   char sex;
   int age;
}

public:

1.3 面向对象编程的一些基本概念

  • 类:描述一组一类具有相同的特征(数据)和行为(方法函数)的对象 例如: person类 car类

  • 对象:是类的一个具体实例,是现实世界存在的某个具体事物 例如: 张三

  • 属性: 类中描述事物特征的数据,属性也叫做类的数据成员 例如:车的型号和颜色

  • 方法:类中的函数,用于描述这个类的行为,方法也叫做类的成员函数 例如:车的驱动方法

二、类与对象

2.1类的定义

  • 类的定义一般分为两部分

  • 类的声明部分

  • class 类名
    *
    {
    类的访问控制: private/ protected/public;
    属性
    方法
    }
    类的实现部分:
    在外实现类中声明但没有实现的函数
    类名::类中的某个函数{写函数体实现这个函数};
    

解释:

class是定义类的关键字,后面跟着类名
类名是一个标识符,类名一般使用大驼峰命名法,每给单词字母要大写
一对大括号表示类的作用域,也成为类体
分好作为定义的结束符
private/ protected/public;这三个是访问控制修饰符的关键字,用来对成员做访问控制,控制成员的可见性
类中的方法:
及成员函数,既可以在类中直接实现,也可以在类中

说明:一般来说,当我们定义多个类的时候,会分文件来写,
分为头文件和源文件
会把类的声明写进一个头文件里面 把类的实现放在一个源文件里面

头文件:

#pragma once
class Rectangle
{
private:
	//属性的命名规范:m_类型名缩写+属性名本身含义的单词(小驼峰命名)
	int m_nLenth;//矩形的长 
	int m_nWidth;//矩形的宽
public:
	void setLength(int nLength);//设置长度的方法,方法名使用小驼峰命名法,只声明,没有实现
	void setWidth(int nWidth);
	int getArea();//计算面积
	int getPerimeter();//计算周长
};

源文件:

#include"Rectangle.h"

// 接下来在类外实现类的成员函数
void Rectangle::setLength(int nLength)
{
	m_nLenth = nLength;
}
void Rectangle::setWidth(int nWidth)
{
	m_nWidth = nWidth;

}
int Rectangle::getArea()
{
	return m_nLenth * m_nWidth;
}
int Rectangle::getPerimeter()
{
	return 2 * (m_nLenth * m_nWidth);
}

主函数:

int main() 
{
	//  类定义好之后,最后在main函数中使用他们
	// 使用类的时候要用对象,需要对类进行实例化
	Rectangle rect;
//给reac对象进行初始化赋值;
rect.setLength(20);// 通过调用给长度进行赋值
rect.setWidth(10);
cout << "矩形rect的面积=" << rect.getArea() << "\t矩形rect的周长=" << rect.getPerimeter() << endl;
//使用new的方式在堆内创建一个Rect对象
Rectangle* p_rect = new Rectangle;
p_rect->setLength(200);
p_rect->setWidth(100);
cout << "矩形rect的面积=" << p_rect->getArea() << "\t矩形rect的周长=" <<p_rect->getPerimeter() << endl;
/*delete p_rect;
p_rect=NULL;*/

//类和对象的空间大小相同,不同对象的地址不同
cout << "Rectangle类的大小" << sizeof(Rectangle)<<endl;
cout << "rect对象的大小" << sizeof(rect) << endl;
cout << "rect的首地址" << &rect << endl;
cout << "p_rect的首地址" << p_rect << endl;
return 0;
}

2.2成员的访问控制

在类中。有数据成员和成员函数
在程序可以对这些成员的使用进行限制和控制,通过访问控制修饰符来实现,
具体有三种:

  • private:私有访问权限。只能通过本类中函数访问,对类外是不可见的(一般属性声明成私有的)

  • protected:保护访问权限。在关键字poreced后面声明的成员都具有保护访向权限,即能能被本类函数访问,也可以被这个类的派生类访向

  • public:共有访问权限。在关键字public后面声明的成员都具有共有访问权限,即类内和类外函数都可以进行访问(一般方法声明成公有的)

如果不写,默认的访问权限是private

2.3类的数据成员和成员函数:

类的数据成员:
即类的属性:用于描述类对象的特征,数据成员必须在类中定义,它们定义方式于一般的变量相同,只是收到了访问控制
定义数据成员时有些注意事项:
(1)数据成员的初始化与变量初始化有所不同,不能使用小括号()来初始化,但是可以使用赋值运算符=初始化,也可以使用{}来初始化;
(2)数据成员可以是任意类型,如果是 数组类型,必须指定数组的大小
(3)多个数据成员之间不能重名,一个类作为一个作用域,但是不同类之间的属性可以重名
(4)一般都是先定义好类,在使用类。但是某些情况下,需要在没有定义类的时候先试用类,这个时候需要一个声明,叫做前向声明

类的成员函数:

类的方法:用于描述类对象的行为,目的是完成一项功能,对外提供一些服务
定义成员函数时有一些注意事项:
(1).如果选择在类外实现成员函数,需要使用作用域运算符::来指定该函数属于哪个类
(2)。类中的成员函数时可以重载的,及函数名相同,但是形参的个数或者类型不同
(3)类的成员函数也可以是内联函数有两种方法: 一种是使用关键字inline来声明,;另一种是直接在函数体来实现这个函数
(4)类的成员函数的形参也可以带有默认值

2.4 对象的创建和访问:

如何使用一个类,首先是创建类的对象,然后操作对象调用他的成员,完成具体的功能,
对象的创建:
需要说明该对象所属的类,对象的创建就是类的一次实例化

例如: Rect r1,r2;//创建了Rect类的两个对象:r1和r2;

除了上面这种方式创建,还可以使用new的方式来创建,这个时候对象在堆内存,这对象需要我们自己来释放

例如:Rect * p_r3=new Rect;

对象的访问:
如果是栈内的对象,比如r1,可以通过(.)这个运算符来访问对象的属性和方法;
如果是堆内存的对象,比如p_r3,这个是对象指针,通过(->)来访问对象的属性和方法

类与对象的关系:
类与对象是抽象和具体的关系。类是创建对象的的模板,由这个模板可以创建很多个对象。在程序运行的时候,类是不能被改变的,对象可以。所以说类是静态的。
一个对象是某个类的个实例,创建对象的过程叫做类的实例化,对象被创建后才是真正存在的,对象是动态,它具有一定的生存期
一个类的多个对象分别拥有自己的对象相互独立,也就是说每个对象都在内存中给自己分配空间。但是类的成员函数是由多个对象共享的
类和对象占用的内存空间大小是一样的

​ 通过观察对象的地址可以知道他们给自己开辟了不同的空间

2.5 类的特殊成员

​ 类的成员包括数据成员和成员函数,除此之外,还有一些特殊的成员函数,有一些特殊的作用
​ 比如: 构造函数 、析构函数(拆解)、 拷贝构造函数 赋值函数
​ 这些特殊的成员函数都是用于支持对象的生命周期的。从对象被创建开始,在对象销毁之前,我们都可以使用对象

构造函数:
构造函数用来完成对象的初始化工作,在定义变量时可以给变量提供初始值,这就是初始化,同样在定义对象时也可以给对象的属性提供初始值,这就是对象的初始化

class Rectangle
{
private:
	//属性的命名规范:m_类型名缩写+属性名本身含义的单词(小驼峰命名)
	int m_nLenth;//矩形的长 
	int m_nWidth;//矩形的宽
	//const int m_nHigh;
public:
	//添加构造函数:名称了类名相同,没有返回值,公有访问权限
	Rectangle();//这是一个无参数,函数体为空的构造函数,这个其实就是编译器生成的默认构造函数
	// 添加一个有两个参数构造函数,可以让对象在构造
	Rectangle(int nLength, int nWidth);
	//添加一个有一个参数构造函数
	Rectangle(int nLength);
	
// 初始化表方式  ,即使用了初始化表的方式,有使用了函数体赋值的方式。
Rectangle(int nLength, int nWidth,int nHigh):m_nLenth(nLength),m_nHigh(nHigh)
{
	m_nWidth = nWidth;
	//常量不能不能在函数体进行赋值
	//m_nHigh = m_nHigh;
};
void setLength(int nLength);//设置长度的方法,方法名使用小驼峰命名法,只声明,没有实现
void setWidth(int nWidth);
int getArea();//计算面积
int getPerimeter();//计算周长
};

构造函数的特点:
(1)构造对象的时候,利用特定的值构造函数对象,将对象初始化为一个特定的状态,如果希望将对象初始化为不同状态,往往需要提供多个构造函数
(2)构造函数的函数名必须与类名相同
(3)不能定义构造函数的返回值类型,他没有返回值。
(4)构造函数一般应该声明为公有的函数
(5)构造函数不能被我们显示调用,而是在创建对象时,通过编译器自动调用
(6)每个类都会生成一个默认的构造函数,他的函数体为空,参数也为空。当我们手写的构造函数之后,默认的构造函数就不会生成了。
构造函数使用时的注意事项:
(1)构造函数也支持重载,编译器会根据参数决定调用哪个构造函数

(2)当我们手写了构造函数之后,默认的构造函数就不会生成了。
(3)无论以什么方式定义对象,都会调用类的构造函数,如果编译器根据参数找不到匹配的构造函数,就会产生编译错误。
(4)为了能提供不同的方式来创建对象,往往就需要提供多个构造函数。

构造函数在对对象属性进行初始化的时候,有两种方式:

  1. 在构造函数的函数体中进行初始化赋值。
  1. 使用初始化表的语法对属性进行初始化。如:构造函数名(参数1, 参数2) :属性1(值1),属性2 (值2) {}
    以上两种方式如何选择?对于普通的数据成员,选择哪个都可以。但是对于常量类型的数据成员,必须便用初始化表达方式来赋值,不能在的数体中赋值,
    因为常量是一且被初始化就不能再被修改的变量,所以禁止它在函数体中赋值,其实就是避免它不小心被修改。

析构函数:
析构函数适用于删除一个对象前,做一些清理工作,摊在删除对象之前被调用,比如说释放这个对象申请的一些内存空间,比如(1.关闭文件链接,2.关闭数据库链接等)
对象就想变量一样,离开他们的作用域后,就应该被收回,栈上的变量和对象被编译器自动回收,堆上需要我们自己回收
析构函数的特点:
(1)析构函数的名称是 ~加类名
(2)析构函数不能有参数,不能有返回值
(3)一个类中只有一个析构函数,所以不能重载
(4)当一个对象消失时,编译器会自动调用析构函数,不需要我们显示调用
(5)编译器会生成一个默认的析构函数,它的函数体为空。除非我们手动显式地声明自己的析构函数,就不会生成默认的析构函数了
(6)如果构造了多个对象,我们会发现,对象的构造顺序和析构顺序恰好相反,先构造后析构。举例:构造对象1,再构造对象2,析构的时候,先析构对象2,再析构对象正

class Test2 {
	int m_nLenth;//矩形的长 
	int m_nWidth;//矩形的宽
	char* m_pName=NULL;  // 姓名 ,放在堆内存里面
public:
	Test2(int len, int wid,const char* pName)
	{
		m_nLenth = len;
		m_nWidth = wid;
		// 先申请一块堆内存,然后才能完成赋值
		m_pName = new char[strlen(pName) + 1]; // 根据参数pName字符串来申请一个比字符串多一个字符的空间多一个,因为要存储结束符\0
		strcpy_s(m_pName, strlen(pName)+1, pName);
		cout << "执行了构造函数,构造了" << m_pName << endl;
	}
	~Test2() 
	{
		if (m_pName != NULL)//判断一下,不为空才需要清理
		{
			cout << "执行了析构函数,释放了" << m_pName << endl;
			delete[]m_pName;
			m_pName = NULL;
		}
	}
};

**拷贝构造函数:
**拷贝构造函数可以实现用一个已经存在的对象来初始化构造一个新对象。也是对现有对象的克隆拷贝。
拷贝构造函数的特点:
1)拷贝构造函数的形参是该类对象的引用,并且一般会声明为常引用
如:类名(const 类名& 对象名){函数体;}
2)编译器也会给我们生成一个默认的拷贝构造函数,但是这个默认的拷贝构造函数在某些情况下不能满足我们的要求。比如:我们只对象创建的时候,动态申请了堆内存
这种情况下,不能使用默认的拷贝构造函数,因为默认的拷贝构造函数实现的只是对象属性之间的赋值,简单的赋值,这种简单的赋值我们称之为浅拷贝。
如果在对象创建的时候,动态申请了堆内存空间,也就是说对象的属性中有指针,这个指针指向了对象申请的堆内存空间,这个时候使用默认的拷贝构造函数。
实现的是浅拷贝会造成多个对象共同使用同一块内存空间,会导致很多问题。
怎么避免上面的问题?需要我们手写一个可以实现深拷贝的构造函数,深拷贝就是让每个对象都有自己独立的空间保存自己的属性值。
3)当某个函数的返回值类型为类对象时,这个时候会调用拷贝构造函数生成一个新对象,这会造成资源的浪费。我们往往会返回一个对象的引用或者指针,这样就会调用拷贝

// 演示拷贝构造

class A
{
	int m_a;
public:
	A() {};
	A(int ma) :m_a(ma) {};
	A(const A& a) 
	{
		m_a = a.m_a;
	}
	int getMA() 
	{
		return m_a;
	}
};

class B {
	int m_nLenth;//矩形的长 
	int m_nWidth;//矩形的宽
	char* m_pName = NULL;  // 姓名 ,放在堆内存里面
public:
	B(int len, int wid, const char* pName)
	{
		m_nLenth = len;
		m_nWidth = wid;
		// 先申请一块堆内存,然后才能完成赋值
		m_pName = new char[strlen(pName) + 1]; // 根据参数pName字符串来申请一个比字符串多一个字符的空间多一个,因为要存储结束符\0
		strcpy_s(m_pName, strlen(pName) + 1, pName);
		cout << "执行了构造函数,构造了" << m_pName << endl;
	}
	B(const B& b)//这个拷贝构造代替默认的拷贝数据,实现深拷贝
	{
		m_nLenth = b.m_nLenth;
		m_nWidth = b.m_nWidth;
		if (b.m_pName!=NULL) 
		{
			m_pName = new char[strlen(b.m_pName) + 1]; // 根据参数pName字符串来申请一个比字符串多一个字符的空间多一个,因为要存储结束符\0
			strcpy_s(m_pName, strlen(b.m_pName) + 1, b.m_pName);
		}
	}
	

	~B()
	{
		if (m_pName != NULL)//判断一下,不为空才需要清理
		{
			cout << "执行了析构函数,释放了" << m_pName << endl;
			delete[]m_pName;
			m_pName = NULL;
		}
	}
	void setName(const char* pName)
	{
		strcpy_s(m_pName, strlen(pName) + 1, pName);
	}
	char* show()
	{
		return m_pName;
	}

};

赋值函数:

赋值函数就是利用赋值运算符=把一个对象赋值给另一个对象,完成的就是对象之间属性的赋值。默认的赋值运算符实现的是浅拷贝
当类属性中有指针,需要实现浅拷贝的赋值,如果要实现深拷贝,同样需要我们手写实现自己的赋值函数
赋值函数的使用:
1)类名&operator=(const 类名&对象名){函数体;return *this;}
说明:返回的是这类对象的引用,而不是返回一个对象,这样避免调用拷贝构造函数,避免了创建对象的过程,提高了效率。
赋值函数本是上就是调用类的成员函数,函数名是operator=
赋值函数是不创建新对象的,只是在已存在的对象之间进行赋值。

class C {
	int m_nLenth;//矩形的长 
	int m_nWidth;//矩形的宽
	char* m_pName = NULL;  // 姓名 ,放在堆内存里面
public:
	C(int len, int wid, const char* pName)
	{
		m_nLenth = len;
		m_nWidth = wid;
		// 先申请一块堆内存,然后才能完成赋值
		m_pName = new char[strlen(pName) + 1]; // 根据参数pName字符串来申请一个比字符串多一个字符的空间多一个,因为要存储结束符\0
		strcpy_s(m_pName, strlen(pName) + 1, pName);
		cout << "执行了构造函数,构造了" << m_pName << endl;
	}
	C(const C& b)//这个拷贝构造代替默认的拷贝数据,实现深拷贝
	{
		m_nLenth = b.m_nLenth;
		m_nWidth = b.m_nWidth;
		if (b.m_pName == NULL)
		{
			m_pName = new char[strlen(b.m_pName) + 1]; // 根据参数pName字符串来申请一个比字符串多一个字符的空间多一个,因为要存储结束符\0
			strcpy_s(m_pName, strlen(b.m_pName) + 1, b.m_pName);
		}
	}
	~C()
	{
		if (m_pName != NULL)//判断一下,不为空才需要清理
		{
			cout << "执行了析构函数,释放了" << m_pName << endl;
			delete[]m_pName;
			m_pName = NULL;
		}
	}
	C& operator=(const C& c) 
	{
		if (this != &c)//判断是不是自己给自己赋值,因为这样会导致赋值失败
		{
			//  不是自赋值才可以赋值
			m_nLenth = c.m_nLenth;
			m_nWidth = c.m_nWidth;
			if (c.m_pName != NULL)
			{
				m_pName = new char[strlen(c.m_pName) + 1]; // 根据参数pName字符串来申请一个比字符串多一个字符的空间多一个,因为要存储结束符\0
				strcpy_s(m_pName, strlen(c.m_pName) + 1, c.m_pName);
				

			}
		}
		return *this; // 返回m_pName;
	}
	void setName(const char* pName)
	{
		strcpy_s(m_pName, strlen(pName) + 1, pName);
	}
	char* show()
	{
		return m_pName;
	}

};

this指针:
在类的普通成员函数中,都有一个this指针参数,这个参数是隐藏的。this指针里面保存了指向当前对象的地址,谁调用这个函数,this就指向谁。
所以我们可以通过操作this指针来操作当前对象。所以在赋值函数中,最后返回了当前对象: return *this;
除此之外,this指针还有其他的用法,比如当我们的属性变吊名和成员函数的参数变最名同名的时候,可以使用this指针来指定属性名。

class This_Class 
{
	int x;
	int y;
public:
	This_Class(int x = 0, int y = 0)
	{
		this->x = x;//当属性名和局部变量参数名相同时,使用this指针来做区分
		this->y = y;

	}

};

// 除了上面的用法,this指针另外的一种用法就是在赋值函数的用法,用于返回当前对象。

2.6 类的设计(封装)

类的设计也叫类的封装,面向对象有三个特性:封装 继承 多态

以上所有做的所有工作都是类的设计过程。一般来说,我们设计一个类需要以下几个部分:
(1)类的分析过程,抽象出来不同的事物,每个事物封装成一个类,分析他们的属性和方法,根据实际需求,设计他们的访问控制
(2)除了一般的成员函数外,还需要考虑一些特殊的成员函数的使用,如:构造函数需不需要写多个,拷贝构造、赋值时数箭不需要重写实现。
(3)如果希望类外能简介访问到类的私有属性,可以通过类内的公有方法完成。-般来说写两类方法,get和set
(4)其他的方法,根据需求去写。

三.类的继承:

3.1类的继承和派生:

​ 继承是面向对象的三大特性之一,定义类时可以从现有的类继承,被继承的类叫做父类或者基类,新定义的类叫做子类或者派生类。
​ 子类继承父类的属性和方法,实现了代码的复用。子类就不需要再次定义父类中已有的属性和方法,只需要定义父类没有的但是子类需要的属性和方法。
​ 继承可以分为:

  • 单继承:子类只有一个父类
  • 多继承:子类多余一个父类
  • 直接继承:子类对直接父类的继承
  • 间接继承:子类对间接父类的继承(举例,爷爷、父亲、儿子,儿子对爷爷是间接继承,对父亲是直接继承)
派生类的定义语法:以单继承来说明
		class 派生类名:继承方式 基类名
		{
			派生类新增的属性和方法;
		}

​ 继承方式分为:public、protected、private,常用的是public

父类方法的隐藏(重定义):
在子类中定义和父类方法完全同名的方法,叫做方法的隐藏。只要方法名相同,不看参数和返回值是否相同,都会实现隐藏,父类的方法就被隐藏了,
当我们使用子类对象调用该同名方法的时候,会调用子类自己的方法,不再调用父类。

//演示继承
//定义一个动物类,基类
class Animal
{
protected:
	char m_cName[20];
	int m_nAge;
public:
	Animal(const char* name, int age)
	{
		strcpy_s(m_cName, strlen(name) + 1, name);
		m_nAge = age;
		cout << "Animal在构造" << endl;
	}
	~Animal()
	{
		cout << "Animal在析构" << endl;
	}
	char* getName()
	{
		return m_cName;
	}
	void running()
	{
		cout << m_cName << "正在跑" << endl;
	}
	void barking()
	{
		cout << m_cName << "正在叫" << endl;
	}
};
//派生类:Cat类
class Cat :public Animal
{
public:
	Cat(const char* name, int age) :Animal(name, age)//Cat的构造函数需要提供两个参数,用于构造父类Animal,因为Cat继承了Animal的属性和方法,所以构造Cat之前需要先把父类构造出来
	{
		cout << "Cat在构造" << endl;
	}
	~Cat()
	{
		cout << "Cat在析构" << endl;
	}
};
//派生类:Dog类
class Dog :public Animal
{
	//Dog类有一个自己的属性
	char m_cType[20];//狗的品种
public:
	Dog(const char* name, int age, const char* type) :Animal(name, age)
	{
		cout << "Cat在构造" << endl;
		strcpy_s(m_cType, strlen(type) + 1, type);//完成品种属性的赋值
	}
	~Dog()
	{
		cout << "Dog在析构" << endl;
	}
	void barking()//对父类方法的隐藏
	{
		cout << m_cName << "正在汪汪叫" << endl;
	}
	void lookingHouse()//这是Dog类自己的方法,父类没有这个方法
	{
		cout << m_cName << "正在看家" << endl;
	}
};

主函数

int main()
{
	//调试类的继承
	Animal a1("宠物", 5);
	Cat c1("加菲猫", 3);
	Dog d1("大黄", 4, "金毛");
	a1.running();
	a1.barking();
	a1.getName();
	c1.running();
	c1.barking();
	c1.getName();
	d1.running();
	d1.barking();//这个方法被Dog子类重定义了,隐藏了父类的barking方法,所以这里会调用Dog的barking
	d1.getName();
	d1.lookingHouse();//这是Dog自己的方法

}

3.2类的继承方式:

​ 父类中的成员被继承到子类时,其访问权限跟继承方式有关。继承方式改变访问权限叫做派生控制。继承方式确定了基类成员在派生类中的可见性。
​ 比如:B类继承了A类,C又继承了B类,B类对A类的继承方式,会影响A类成员在B类的可见性,也就是会影响到C类对A类成员的访问。
​ 类内成员的访问控制权限和继承方式的派生控制权限,都会影响类成员的可见性。
​ 继承方式分为:public、protected、private三种:

  • 公有继承public:

      		基类中的私有成员不能被派生类的成员函数访问
      		基类中的公有成员在派生类中还是作为公有成员
      		基类中的保护成员在派生类中还是作为保护成员
    
  • 保护继承protected:

      		基类中的私有成员不能被派生类的成员函数访问
      		基类中的公有成员在派生类中还是作为保护成员
      		基类中的保护成员在派生类中还是作为保护成员
    
  • 私有继承private:

      		基类中的私有成员不能被派生类的成员函数访问
      		基类中的公有成员在派生类中还是作为私有成员
      		基类中的保护成员在派生类中还是作为私有成员
      **继承方式总结:**
      	一句话:基类成员在子类中的访问权限,是基类访问权限和继承方式权限相比较,权限较小的那个。
    
    //私有继承,鸟类
    class Bird :private Animal
    {
    public:
    	Bird(const char* name, int age):Animal(name,age){ cout << "Bird在构造" << endl; }
    	void show() { running(); }//这里可以使用running,这是派生类内部,可以使用父类的公有方法
    	~Bird(){ cout << "Bird在析构" << endl; }
    };
    //鸟类的派生类:鹦鹉类
    class YingWu :public Bird
    {
    public:
    	YingWu(const char* name, int age):Bird(name,age){ cout << "YingWu在构造" << endl; }
    	void show()
    	{
    		//running();//由于running被Bird私有继承了,相当于Bird类中的私有成员,自然在派生类中无法访问
    		cout << "我是一只鹦鹉" << endl;
    	}
    	~YingWu(){ cout << "YingWu在析构" << endl; }
    };
    
    //私有继承
    YingWu y1("鹦鹉", 4);
    y1.show();
    //y1.running();//无法在类外使用Animal类中的方法
    

3.3派生类的构造和析构过程:

​ 由于派生类继承了基类的成员,派生类对象既含有自身的成员,又包括从基类继承的成员,因此在构造派生类对象时,也要同时创建从基类继承的部分。
​ 而基类的构造函数是不能继承的,所以在创建派生类对象时,需要调用基类的构造函数为基类的数据成员初始化。
​ 析构的过程和构造过程类似,在删除派生类对象时,能自动调用基类的析构函数删除基类对象。析构和构造顺序是相反的。

派生类构造的过程:
	1)调用基类的构造函数。
	2)按照数据成员的声明顺序依此初始化数据成员,如果数据成员中有对象类型的数据成员,需要调用这个对象类型的构造函数。
	3)执行派生类自己的构造函数。

派生类析构的过程:
	1)执行派生类自己的析构函数。
	2)如果数据成员中有对象类型,需要按照跟声明顺序相反的顺序来析构对象。
	3)调用基类的析构函数。
class Pig :public Animal
{
public:
	Pig(const char* name, int age) :Animal(name, age) { cout << "Pig在构造" << endl; }
	void show() 
	{
		running();
	}
	~Pig() { cout << "pig在析构" << endl; }
};
//pig类的派生类
class YeZhu :public Pig
{
public:
	YeZhu(const char* name, int age) :Pig(name, age)
	{
		cout << "YeZhu在构造" << endl;
	}
	void show()
	{
		running();
	}
	~YeZhu() { cout << "YeZhu在析构" << endl; }
};
YeZhu yeZhu("野猪", 6);
yeZhu.show();

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.4多继承简介:

​ 一个派生类有多余一个的基类,称之为多继承,如果有多继承,上面的构造和析构过程,又多了一些操作。
​ 在构造时需要按照基类的声明顺序逐个构造基类,在析构时按照声明的相反顺序逐个析构基类。
​ 在大多数编程语言中,是不允许多继承的,因为多继承关系繁琐,不便于使用。

//多继承 (基类BaseA 和 BaseB)
class BaseA 
{
	int m_a;
	int m_b;
public:
	BaseA(int a, int b) 
	{
		m_a = a;
		m_b = b;
		cout << "BaseA在构造" << endl;
	};
	~BaseA() 
	{
		cout << "BaseA在析构" << endl;
	}

};
class BaseB
{
	int m_a;
	int m_b;
public:
	BaseB(int a, int b)
	{
		m_a = a;
		m_b = b;
		cout << "BaseB在构造" << endl;
	};
	~BaseB()
	{
		cout << "BaseB在析构" << endl;
	}

};

class BaseC:public BaseA,public BaseB
{
	int m_c;
public:
	BaseC(int a, int b,int c,int d,int e) : BaseA(a, b),BaseB(c,d) {
		cout << "BaseC在构造" << endl;

};
	~BaseC()
	{
		cout << "BaseC在析构" << endl;
	}

};

四.类的多态

4.1多态性概念:

​ 多态性是指一个名字有多种解释(一个事物多种形态,一个接口多种方法)。
​ C++的多态分为:编译时的静态多态、运行时的动态多态。前者是通过重载来实现的,后者通过继承与虚函数来实现。

解释静态多态在编译时就确定了对象的类型,通过重载实现不同对象调用各自的同名函数。

//静态多态,通过重载实现
class Person
{
public:
	void buyTicket() { cout << "普通成人需要全价买票" << endl; }
};
class Student :public Person
{
public:
	void buyTicket() { cout << "学生可以半价买票" << endl; }
};
class Child :public Person
{
public:
	void buyTicket() { cout << "6岁以下儿童可以免票" << endl; }
};

动态多态在编译时不确定对象是哪个具体类型,而是统一使用父类的指针或引用来代替所有的派生类对象,只有运行到传入具体派生类对象的时候,
才知道传入的对象是什么类型,才知道应该调用什么对象的同名函数。

动态多态可以概括为:一个接口,多种方法。程序在运行时才决定调用的函数。

class Person
{
public:
	virtual void buyTicket() { cout << "全家买票!!" << endl; };

};
class Student : public Person
{
public:
	void buyTicket() { cout << "学生可以半价买票!!" << endl; };
};
class Child :public Person
{
public:
	void buyTicket() { cout << "儿童可以免票!!" << endl; }
};

主函数:

int main()
{
	//静态多态
	Person p;
	Student s;
	Child c;
	p.buyTicket();
	s.buyTicket();
	c.buyTicket();
	
		//动态多态
	Person_Dyn p1;
	Student_Dyn s1;
	Child_Dyn c1;
	Person_Dyn* Person_Dyn_p[3];
	Person_Dyn_p[0] = &p1;
	Person_Dyn_p[1] = &s1;
	Person_Dyn_p[2] = &c1;
	Person_Dyn_p[0]->buyTicket();
	Person_Dyn_p[1]->buyTicket();
	Person_Dyn_p[2]->buyTicket();
}

4.2虚函数与运行时多态:

​ 动态多态实现的条件:
1)基类中定义虚函数
​ 虚函数语法:virtual 函数返回值 函数名(参数列表){};
2)派生类中重写了基类的虚函数
​ 对于虚函数的重写,要做到函数名和参数都相同,也就是函数原型要相同,这个区别于函数的隐藏,因为函数的隐藏是派生类直接将父类替代了,但这里
​ 由于是统一使用的基类指针,我们也要执行基类的虚函数,所以派生类重写的虚函数要跟基类保持原型的一致。
​ 对于派生类的虚函数重写,可以有virtual,也可以没有,建议加上。
3)用基类指针或者引用来指向派生类对象,并且调用该虚函数

使用动态多态,可以让我们通过基类的指针对所有的派生类(包括直接派生和间接派生)的成员函数进行全方位的访问,在不同的继承关系的对象情况下,
调用统一的函数,产生不同的行为。这就是一个接口,多种方法。这样让我们的代码更简洁,更好维护。

虚函数实现的动态多态内部机制:
编译器为每个包含虚函数的类创建了一个虚函数表,并且为每个对象设置一个虚函数指针指向虚函数表。调用时,通过各自的虚函数指针,
在各自的虚函数表中查找对应虚函数的地址,从而实现调用各自的虚函数。

虚函数用在析构函数上,称为虚析构函数
我们知道,析构函数会在对象回收销毁的时候调用。但是遇到动态多态的情况,由于我们都是用基类指针来操作派生类对象的,所以在回收对象的时候,
都被当作父类对象来看待,都会调用父类的析构函数,这时候如果派生类自己申请了堆内存,自己也写了析构函数来回收这部分内存,但由于都是调用的父类析构函数,
就会导致这部分内存无法被释放,造成内存泄露。
为了让派生类对象能调用自己的析构函数,我们需要将基类的析构函数也定义为虚函数,
这样就可以实现虚函数的多态,就能解决上述问题。这样派生类就可以调用自己的析构函数回收自己的内存空间了。
在做法上,我们只需要将基类的析构函数定义为虚函数即可,派生类不需要处理。

//定义一个基类:员工类,实现了虚析构函数
class Employee_v
{
	char* name = NULL;
	int age;
public:
	Employee_v(const char* n, int a)
	{
		name = new char[strlen(n) + 1];//基类申请了动态的堆内存保存姓名
		strcpy_s(name, strlen(n) + 1, n);
		age = a;
	}
	virtual ~Employee_v()//这里回收上面申请的堆内存
	{
		cout << "执行Employee_v的析构函数" << endl;
		if (name != NULL)
		{
			delete[]name;
		}
		name = NULL;
	}
};
//定义一个教师类,继承员工类
class Teacher_v :public Employee_v
{
	char* m_course = NULL;//课程
public:
	Teacher_v(const char* n, int a, const char* c) :Employee_v(n, a)
	{
		m_course = new char[strlen(c) + 1];//教师类也申请了堆内存
		strcpy_s(m_course, strlen(c) + 1, c);
	}
	~Teacher_v()//教师类回收自己申请的堆内存
	{
		cout << "执行Teacher_v的析构函数" << endl;
		if (m_course != NULL)
		{
			delete[]m_course;
		}
		m_course = NULL;
	}
};
//当基类实现了虚析构后,派生类就可以调用自己的析构函数了
Employee_v* p[2];
p[0] = new Employee_v("某员工", 30);
p[1] = new Teacher_v("某老师", 40, "生成式人工智能大模型");
delete p[0];
delete p[1];//执行可见,这里就执行了Teacher的析构函数,回收了Teacher申请的堆内存

4.3纯虚函数与抽象类:

​ 纯虚函数是不需要定义函数体的特殊虚函数,语法如下:

		virtual 函数返回值 函数名(参数列表)=0;

​ 纯虚函数无法被调用执行,因为没有函数体,它只充当接口的作用。
​ 声明了纯虚函数之后,不写函数体,它的实现是在派生类中完成的。

​ 抽象类就是拥有纯虚函数的类,抽象类是不能创建对象的。
抽象类主要用于定义派生类共有的数据成员和成员函数。抽象类提供的纯虚函数,其实是提供了一种接口的规范。

//纯虚函数
//定义一个含有纯虚函数的类,它称为抽象类,抽象类无法实例化对象
class Abs
{
public:
	virtual void say() = 0;//这是纯虚函数,相当于接口,由派生类实现
};
//定义派生类,实现基类的接口
class Some :public Abs
{
public:
	virtual void say() { cout << "Some One Say" << endl; }
};
//再定义一个派生类,仍然实现同样的接口
class Other :public Abs
{
public:
	virtual void say(){ cout << "Other One Say" << endl; }
};

五、类的高级特性

5.1类的组合:

什么是类的组合?
​ 类的属性不仅可以是基本的数据类型,也可以是类对象。类的组合就是在一个类中内嵌其他类的对象作为成员,因为内嵌对象是该类对象的组成部分,
​ 所以当创建该类对象时,需要先创建内嵌对象。我们在创建含有内嵌对象的类对象时,要使用初始化列表来构造内嵌对象,这个时候需要调用内嵌对象类
​ 的构造函数或者拷贝构造函数。
​ 如果内嵌对象类有无参构造函数,则在组合类构造函数初始化列表中可以不提供对该内嵌对象的初始化,编译器会自动调用内嵌对象的无参构造函数完成对象创建。
​ 如果内嵌对象类有带参的构造函数,则在组合类构造函数初始化列表中必须提供对内嵌对象的初始化参数,完成内嵌对象的构造。
​ 组合类的构造过程:
​ 如果类中有多个内嵌对象,则组合类的构造顺序如下:
​ 1)按照内嵌对象的声明顺序依此构造内嵌对象
​ 2)然后执行组合类自己的构造函数

//类的组合
class Point//内嵌对象类
{
	int m_X;//横坐标
	int m_Y;//纵坐标
public:
	Point(int x,int y);//构造函数
	Point(const Point& p);//拷贝构造函数
	int getX();
	int getY();
};
Point::Point(int x, int y)
{
	m_X = x;
	m_Y = y;
	cout << m_X << "," << m_Y << "在构造" << endl;
}
Point::Point(const Point& p)
{
	m_X = p.m_X;
	m_Y = p.m_Y;
}
int Point::getX() { return m_X; }
int Point::getY() { return m_Y; }
//组合类,包括了内嵌对象类Point
class Circle
{
	Point center;//内嵌对象,代表圆心
	const double PI;
	double m_dRadius;
public:
	Circle(const Point& p, double r) :PI(3.14), center(p)//Circle构造函数中,需要对内嵌对象center完成构造,使用了拷贝构造函数center(p)完成的
	{
		m_dRadius = r;
		cout << "Circle在构造" << endl;
	}
	Circle(const Circle& c) :PI(3.14), center(c.center)//Circle的拷贝构造函数
	{
		m_dRadius = c.m_dRadius;
		cout << "Circle的拷贝构造" << endl;
	}
};


//组合类,三角形类,里面包括三个点对象作为属性
class Triangle
{
	Point point1;
	Point point2;
	Point point3;
public:
	Triangle(const Point& p1, const Point& p2, const Point& p3) :point1(p1), point2(p2), point3(p3)//构造函数一,这里使用了拷贝构造完成了点对象的构造
	{
		cout << "Triangle在构造" << endl;
	}
	Triangle(int a, int b, int c, int d, int e, int f) :point3(e, f), point2(c, d), point1(a, b) //构造函数二,使用了点的构造函数完成点对象的构造,点的构造顺序,跟类中内嵌对象的声明顺序有关,跟初始化表的书顺序无关
	{
		cout << "Triangle在构造" << endl;
	}
	void show()
	{
		cout << "第一个点:" << point1.getX() << "," << point1.getY() << endl;
		cout << "第二个点:" << point2.getX() << "," << point2.getY() << endl;
		cout << "第三个点:" << point3.getX() << "," << point3.getY() << endl;
	}
};
//组合类的实现,看一下构造国产
Point p(20, 30);
Circle c(p, 10);

Triangle t(20, 30, 40, 50, 60, 70);
t.show();

5.2静态成员:

​ 静态成员的特性类似于静态变量,静态变量只有一份拷贝,在程序运行期间都有效。所以它只能在全局作用域下面进行初始化。

静态成员指的是类中的静态属性和静态方法:

  • 静态数据成员:

​ 类的普通数据成员(属性)在类的每个对象中都有一个拷贝,存储对象自己的值。但是静态数据成员则不同,每个类只有一个拷贝,由类的所有对象共同来维护,可以实现多个对象之间的数据共享。

 声明语法格式:static 类型标识符 静态数据成员名;

​ 初始化:静态变量只能在全局作用域下面进行初始化,所以静态成员需要在类外初始化(除非是常量)。类外初始化的格式:类型标识符 类名::静态数据成员名=初始值;
​ 访问:对于静态数据成员,除了使用一般的对象访问方式之外,还可以使用类名来访问,语法是:类名::静态数据成员名

静态成员函数:

  • 除了属性,方法也可以定义成静态,只需要在成员函数前加static关键字。

  • 对于静态成员函数来说,既可以通过对象来访问,也可以通过类名::来访问。

    静态成员函数只能访问静态成员(静态属性和静态方法),不能访问非静态成员(非静态的属性和方法)。记住一句话:静态的只能访问静态的,不能访问动态的。
    解释:为了方便理解,举个例子,静态的成员好比说是公共资源,一个类只有一份,多个对象共享这个资源。这个资源可以被普通的成员函数访问,也可以提供一个
    ​ 专门的静态方法来访问,但是不能让这个静态方法访问普通的成员(因为普通的成员属于每个对象的隐私)

    class Child
    {
    public:
    	//定义一个静态数据成员
    	static int count;//count用于统计学生对象的数量,每次初始化一个新对象,就+1
    	int m_nNumber;//学号,根据学生数量自动递增
    	char m_cName[30];//姓名
    	int m_nAge;//年龄
    public:
    	Child(const char* name, int age)
    	{
    		count++;//每次构造一个对象,都对计数+1
    		m_nNumber = 202400 + count;//学号根据计数自动递增
    		m_nAge = age;
    		strcpy_s(m_cName, strlen(name) + 1, name);
    	}
    	int getNumber() { return m_nNumber; }
    	char* getName() { return m_cName; }
    	int getAge() { return m_nAge; }
    	//int getCount() { return count; }
    	static int getCount() //添加一个静态方法
    	{ 
    		return count; 
    		//cout << m_cName << endl;//静态的成员函数不能访问非静态的数据成员
    		//getNumber();//静态的成员函数不能访问非静态的成员函数
    	}
    };
    int Child::count = 0;//静态成员需要在类外初始化
    
    /静态成员
    	Child c1("张三",7);
    	cout << c1.getNumber() << "," << c1.getName() << "," << c1.getAge() << "," << c1.getCount() << endl;
    	Child c2("李四", 8);
    	cout << c2.getNumber() << "," << c2.getName() << "," << c2.getAge() << "," << c2.getCount() << endl;
    	Child c3("王五", 7);
    	cout << c3.getNumber() << "," << c3.getName() << "," << c3.getAge() << "," << c3.getCount() << endl;
    	cout << Child::count << endl;//静态属性,可以通过类名来访问
    	cout << Child::getCount() << endl;//静态方法,可以通过类名来访问
    	//cout << Child::getAge() << endl;//非静态的方法,不能通过类名访问
    

5.3常对象与常成员函数:

​ 常指的是常量,除了普通的数据类型可以定义为常量之外,对象类型也可以定义为常量,这样的对象叫做常对象,常对象具有常性,常对象的属性值不能被修改。

语法格式:const 类名 对象名;

​ 常成员函数的作用是为了访问常对象的属性。因为常对象的属性不能被修改,所以禁止普通成员函数访问。同理,常函数也不能调用普通成员函数。常成员函数可以访问普通数据成员和静态数据成员
​ 常对象可以调用静态成员函数(因为不用担心静态成员函数修改它的属性,因为静态成员函数只能访问静态成员,而普通成员函数属于非静态成员)
​ 普通对象可以调用常成员函数,但会优先调用普通成员函数。

	语法格式:函数返回值类型 函数名(参数表) const{};//注意const出现的位置
void show() const//这是个常成员函数
{
	cout << "执行常成员函数show() const" << endl;
	cout << m_nNumber << "," << m_cName << "," << count << endl;//常函数可以访问普通的属性或者静态属性
	//getAge();//常函数不能调用普通成员函数。
}
//常对象和常成员函数
const Child const_c("赵六", 6);
const_c.show();//常对象可以调用常函数
const_c.getCount();//常对象也可以调用静态函数
//const_c.getAge();//常对象无法调用普通函数
c1.show();//普通对象是可以的调用常函数的,但重载的情况下,会优先调用普通成员函数。

5.4对象数组与对象指针:

​ 对象数组即存放对象的数组
​ 定义:类名 数组名[常量表达式];
​ 访问:数组名[下标].对象成员名

//对象数组
Child c[3] = { Child("数组中的张三",7),Child("数组中的李四",8),Child("数组中的王五",6) };
c[0].show();
c[1].show();
c[2].show();

​ 对象指针即指向对象的指针
​ 定义:类名* 对象指针名;

//对象指针
Child* p[3];
p[0] = &c[0];
p[1] = &c[1];
p[2] = &c[2];
p[0]->show();

六、输入输出流与文件读写

6.1流概念:

​ 流是一种抽象的概念,指的是计算机的数据从一个对象流向另一个对象。流入和流出的对象一般有屏幕、内存、文件等。
​ 数据的流动是由IO流类来实现的。比如说我们常用的cout和cin,就是属于输入输出流类中定义的对象,cout完成数据从内存中流向显示器,cin完成数据从键盘流向内存。
​ 流类库的继承结构:
​ 1)ios_base类:基类,表示流类的一般特征
​ 2)ios类:继承自ios_base类,其中包含一个指针,可以指向流缓存对象(streambuf)
​ 3)ostream类:继承自ios类,提供输出的方法
​ 4)istream类:继承自ios类,提供输入的方法
​ 5)iostream类:继承自istream和ostream类,既可以输入又可以输出
​ 6)ifstream类:继承自istream类,提供文件输入方法(读文件)
​ 7)ofstream类:继承自ostream类,提供文件输出方法(写文件)
​ 8)fstream类:继承自iostream类,提供文件的读写方法,既可以输入又可以输出
​ 对输入输出的理解:
​ 要站在程序的角度去理解,而程序运行在内存中,所以就站在内存角度去看待输入输出,如果拿外面的数据存到内存中就是输入,如果把内存数据拿到外面就是输出。

6.2cout与cin:

cout:
​ 通过插入运算符<<实现输出:
​ cout是iostream头文件中定义的ostream对象,ostream类重载了运算符<<,使之能够识别C++所有的基本数据类型和字符串。

例如:cout<<"hello"; 表示cout将字符串输出到cout对象中,然后cout对象再将它输出到屏幕上。

​ 这里这个<<叫做插入运算符,它的作用就是将数据插入到一个输出流对象中,输出流对象再进一步输出到它关联的设备上。
​ 通过调用成员函数put和write实现输出:
​ 除了使用插入运算符<<输出数据,还可以使用成员函数put和write来输出:
​ put:ostream类的成员函数,用于字符的输出,一次可以输出一个字符。
​ write:ostream类的成员函数,用于字符串的输出。

例如:
cout.put('A').put('B').put('\n');
 cout.write("abcdefg",4).put('\n'); //write的第一个参数是输出的字符串,第二个参数指定输出字符的个数。
char c[] = "Hello C++";
cout.put('A').put('B').put('\n');
cout.write(c, strlen(c)).put('\n');
cout.write("ABCDEFG", 3);

cin:
通过提取运算符>>实现输入:
cin是iostream头文件中定义的istream对象。
例如:int i; cin>>i; 表示从输入流对象cin中提取一个int数据存储到变量i中。

//输入输出之cin对象
char c1;
cin.get(c1);
cout << c1;

通过调用成员函数

get和getline实现输入:
除了使用提取运算符>>输入数据,还可以使用成员函数get和getline来输入:
get:每次读取一个字符,并且可以读取空格、制表符、换行符等
get(char c):将每次读取到的单个字符,保存到字符变量c中

//get方法读取多个字符:循环读
	char c;
	int n = 0;
	cin.get(c)
//循环,每次读取一个字符然后输出到屏幕,并且记录读取的个数,直到读到换行符才结束
	while (c!='\n')//循环条件:只要不是换行符,就继续读
	{
		n++;
		cout << c;
		cin.get(c);
	}
	cout << endl;
	cout << n << endl;

​ getline:可以读取字符串,一次读取一行
​ getline(char*,int,char);//第一个参数是存放字符串的内存空间首地址,第二个参数是要读取的最大字符数+1(+1用于存放字符串结束标志\0),第三个参数指定分界符(默认是换行符\n)

//getline方法读取字符串,一次读一行
char str1[20], str2[20], str3[20];
cin.getline(str1, 11);
cin.getline(str2, 11);
cin.getline(str3, 11);
cout << "str1:" << str1 << endl;
cout << "str2:" << str2 << endl;
cout << "str3:" << str3 << endl;

cin输入对象的常用函数:
1)good():用于检测输入的数据类型是否匹配,如果匹配返回1,否则返回0
​ 例如:int age; cin>>age; 要使用good函数判断一下用户输入的类型是不是int类型

//good()函数
int age;
cin >> age;
if (cin.good()==1)
{
	cout << "age=" << age << endl;
}
else
{
	cout << "您的输入有误" << endl;
}

2)ignore(count,char):从缓冲区舍弃字符,舍弃count个字符,如果遇到char字符,舍弃char字符前面的所有字符。
​ 作用:主要用来在多次输入的情况下,忽略上一次输入对下一次输入的影响。每次输入都有换行符,如果不加忽略ignore函数,那么上次输入后的回车就会当作下一次的输入数据。

//ignore函数
int a, b;
cin >> a;
cin.ignore(5, '\n');//从缓冲区最多忽略5个字符,如果遇到'\n'就直接忽略换行符前面的所有字符
cin >> b;
cout << "a=" << a << endl;
cout << "b=" << b << endl;
char str[30];
cout << "请输入一个字符串:";
cin >> str;
cout << str << endl;
cout << "请输入任意字符结束程序" << endl;
cin.get();//这里没有阻塞住等待用户输入,说明get已经拿到了字符,拿到的是上面的换行符
cin.get();//这里再增加一行get,才会阻塞住,这时候才能输入任意字符结束

给上面这个段代码加入ignore函数,忽略上次的换行符就可以解决问题
char str[30];
cout << "请输入一个字符串:";
cin >> str;
cout << str << endl;
cout << "请输入任意字符结束程序" << endl;
cin.ignore();//ignore使用默认参数cin.ignore(1,'\n');
cin.get();//此时get不会拿到上次cin输入后的换行符,可以阻塞在这里等待用户输入

​ 3)clear():清除错误状态
​ 作用:输入的时候可能会发生各种错误,比如输入类型不匹配,发生错误就不能继续输入了,如果还想继续输入,可以清除错误状态。
​ 4)sync():清空输入缓存区
​ 遇到错误时,除了clear清除错误状态外,还需要清空已经输入到缓存区的数据,然后才可以继续输入。

//clear函数和sync函数
//下面写一个可以让用户在输入错误以后,不结束程序,连续多次输入的代码,直到用户输入正确
int sales;
while (1)//死循环
{
	cin >> sales;
	if (cin.good()==1)
	{
	break;//输入正确就跳出循环

	}
	else{
		cout << "销售额输入有误";
		cin.clear();//清除错误状态
		cin.sync();//清空缓存区,清除刚才接收到的错误数据
		cin.ignore();//忽略上次输入后的回车,这行不能少,不然进入死循环
		continue;
	}
	
}

6.3格式化输入输出:

​ 首先引入头文件iomanip,才可以实现格式化,常见的如:设置输出域的宽度,填充字符、输出精度等
​ 格式化的两种实现方式:
​ 1)流控制符:设置宽度setw,填充字符setfill,控制精度setprecision,输出进制:十进制dec,八进制oct,十六进制hex
​ 2)成员函数:设置宽度width,填充字符fill,控制精度precision。注意:使用成员函数时,可以返回上次的设置值,这样我们可以保存上次的设置,以便于我们恢复设置。

//格式化输入输出
//设置输出宽度,setw和width函数都可以实现
//作用:设置输出数据宽度,注意:只对下次输出有效
//如果输出的数据宽度比设置的宽度小,会右对齐,并且以默认值空格填充左边的位置
//如果输出的数据比设置的宽度大,则会输出所有数据,忽略宽度的设置
cout << "1234567890" << endl;
cout << setw(6) << 4.5 << endl;
cout << setw(6) << 4.5 <<setw(5)<<6.7<< endl;
//使用width函数,可以保存上次的设置,让我们恢复原来的设置
const char* str[3] = { "abc","abcde","abcdefgh" };
cout << "1234567890" << endl;
for (int i = 0; i < 3; i++)
{
	cout.width(6);
	cout << str[i] << endl;
}
  • 填充字符,可以通过setfill和fill函数实现
  • 作用:设置空白位置的填充字符,永久有效,直到再次被修改
  • 使用setfill流控制符
double value[] = { 1.23,35.22,553.2,2453.25 };
cout << "1234567890" << endl;
cout << setfill('*');
for (int i = 0; i < 4; i++)
{
	cout << setw(10) << value[i] << endl;
}
接下来使用fill函数来实现,可以保存上次的设置,用于恢复设置
double value[] = { 1.23,35.22,553.2,2453.25 };
cout << "1234567890" << endl;
char oldFill = cout.fill('*');//oldFill会接收以前的填充字符,就是默认空格
for (int i = 0; i < 4; i++)
{
	cout << setw(10) << value[i] << endl;
}
//恢复以前的设置
cout.fill(oldFill);
cout << "--------------恢复到以前的设置---------------" << endl;
for (int i = 0; i < 4; i++)
{
	cout << setw(10) << value[i] << endl;
}
  • 控制数字精度,setprecision和precision

  • 作用:控制浮点数的输出精度,永久有效,直到再次被修改

  • 注意: 精度参数代表整个数字的宽度,摆阔了整数和小数部分

    double pi = 3.1415926847;
    double e = 245.29285;
    //通过流控制符setprecision控制
    cout << pi << endl;//double类型默认的精度是6位
    cout << e << endl;
    cout << setprecision(8) <<"pi=" << pi <<" e=" <<e << endl;
    //通过成员函数来控制
    int oldPrecision = cout.precision(4);
    cout << pi << endl;
    cout << e << endl;
    cout << setprecision(oldPrecision) << "pi=" << pi << " e=" << e << endl;
    cout.precision(6);
    cout << "pi=" << pi << " e=" << e << endl;
    

设置显式的数字进制

  • 作用:指定显式的数字进制,永久有效,直到被修改

  • dec:十进制,oct:八进制,hex:十六进制

    通过流控制符来实现

int n = 100;
cout << "默认十进制n=" << n << endl;
cout << oct << "八进制输出,n=" << n << endl;
cout << hex << "十六进制输出,n=" << n << endl;
cout << dec << "十进制输出,n=" << n << endl;

6.4文件读写:

文件读写是通过文件的流对象来完成的,首先引入头文件#include

​ 文件读写可以让我们程序中的数据实现持久化的存储。因为数据是伴随着程序的运行而产生的,数据在内存中,当程序结束后,数据就没有了。所以我们可以通过写入文件将数据存到硬盘中。

文件读写的基本步骤:
(1)创建文件流对象并使用流对象打开文件
(2)读写文件
(3)关闭文件,使用close函数来关闭
文件流对象:
ofstream:文件输出流
ifstream:文件输入流
fstream:文件输入输出流
1)创建文件流对象并使用流对象打开文件:
在对文件读写之前,必须先创建流对象,然后打开文件,有两种方式:
使用默认的构造函数创建文件流对象,然后用对象调用open函数打开文件:
文件流类名 流对象名;
流对象.open(文件位置及文件名,打开方式);
使用有参构造函数在创建文件流对象的同时打开文件:
文件流类名 流对象名(文件位置及文件名,打开方式);
文件的打开方式:
ios::in 打开一个输入文件,读文件,这是ifstream对象的默认打开方式
ios::out 打开一个输出文件,写文件,这是ofstream对象的默认打开方式,如果打开一个已经存在的文件,就覆盖原来文件的内容,直接写入新内容。如果打开的文件不存在,则创建这个文件。
ios::app 打开一个输出文件,写文件,在文件末尾追加数据,不会覆盖原来的内容。
ios::ate 打开一个现有文件(输入输出都可以),并且将读写指针定位到文件结尾。
ios::binary 以二进制打开一个文件,用于存储非字符的数据。默认文件打开方式都是文本模式。当我们读写的内容不是文本的时候,就以二进制的方式来打开进行读写。

文件写入:
1)使用插入运算符<<来写入,支持基本数据类型和字符串

​ 2)使用put函数来写入(一次写入一个字符)

用覆盖的方式写入
ofstream out_file("D:\\data.txt", ios::out);//用有参构造方法,创建文件输出流对象的同时就打开文件,文件不存在则会自动创建,注意windows的路径分隔符需要两个反斜杠
out_file << "hello" << " " << 234 << endl;//对文件进行写入,即输出操作
out_file.put('A');
out_file.close();//最后记得关闭文件

用追加的方式写入
ofstream out_file("D:\\data.txt", ios::app);
out_file.put('B');
out_file.close();//最后记得关闭文件

​ 3)使用write函数写入文件,可以将内存中的一段二进制数据写入文件中,比如说可以将自定义类型的数据写入文件
​ write函数的参数:
​ 第一个参数:指定输出数据的内存起始地址,参数类型为(char *),如果不是这个类型,需要强转
​ 第二个参数:指定输出的字节数,从起始地址开始输出多少个字节的数据,类型为int

//用write函数以二进制的形式写入非文本类型的二进制数据,比如写入数组
ofstream outfile;
outfile.open("D:\\data.txt", ios::out | ios::binary);
int arr[] = { 24,26,32,56 };
outfile.write((char*)arr, sizeof(arr));//注意,第一个参数需要强转
outfile.close();
//用write写入一个对象
ofstream outfile;
outfile.open("D:\\test.txt", ios::out | ios::binary);
MyClass c(10, 5);
outfile.write((char*)&c, sizeof(c));//注意,第一个参数需要强转
outfile.close();
class MyClass
{
	int m_nLen;
	int m_nWid;
public:
	MyClass(){}
	MyClass(int x, int y)
	{
		m_nLen = x;
		m_nWid = y;
	}
	void setLen(int len) { m_nLen = len; }
	void setWid(int wid) { m_nWid = wid; }
	void show() { cout << m_nLen << "," << m_nWid << endl; }
};

文件读取:
(1)使用提取运算>>读取文件,支持基本数据类型和字符串,文件中有空白字符(空格、Tab、Enter)作为数据之间的分隔符,空白符不会作为数据读取。

//读取文件 
//准备一个字符数组,在准备一个整型变量没在用来接受从文件中读取的内容
//data.txt 中存贮的内容是: hello 234

char s[10];
int i;
ifstream in_file("D:/data.txt", ios::in);//读文件保证文件已经存在

使用提取运算符来读 >>
in_file >> s >> i;// 读到的是hello 234;s读到的是hello,i读到的是234
cout << "s="<<s << "\ti=" << i << endl;	

(2)使用get函数来读,get函数有几个重载:
第一种:int get(); //返回读取到的字符的ASCII码

//使用get函数来读
//data.txt中存储的内容是:hello 234
//第一种:int get(); 
char c;
ifstream in_file("D:\\data.txt");
c=in_file.get();//这个get()返回int类型,赋值给c,完成隐式转换
cout << c << endl;//读到了h
in_file.close();

​ 第二种:istream& get(char& c);//读取到的字符给了c,返回一个输入流对象的引用

//第二种:istream& get(char& c);
char c;
ifstream in_file("D:\\data.txt");
in_file.get(c);
cout << c << endl;//读到了h
in_file.close();

​ 第三种:istream& get(char* buf,int num, char c);//读到的字符存入buf开始的地址,直到读到了num个字符或者遇到了c字符就结束,c默认是换行符,返回一个输入流对象的引用

//第三种:istream& get(char* buf,int num, char c);
ifstream in_file("D:\\data.txt");
char str[100];
in_file.get(str,100);
cout << str << endl;
in_file.close();

​ 3)getline()方法,一次读一行
​ 它的参数跟get的第三个重载是一样的。

//使用getline方式来读,一次读一行
char a[100];
ifstream in_file("D:\\data.txt");
for (int i = 0; i < 3; i++)// 读了三行
{
	in_file.getline(a, 100);
	cout << a << endl;
}
in_file.close();

​ 4)read()函数用来读取整块数据到内存中,可以读取自定义类型或者二进制类型。跟write函数作用相反。
​ read函数的参数:
​ 第一个参数:保存读出数据的内存地址,参数类型为(char *)
​ 第二个参数:指定读取的字节数,类型是int

//使用read读取一整块数据到内存中,可以是任何二进制数据
//刚才在test.txt文件中写入了一个对象,现在把它读出来
ifstream infile("D:\\test.txt", ios::in | ios::binary);
MyClass m;//准备一个对象,用它来接受读到的对象
infile.read((char*)&m, sizeof(m));
m.show();
infile.close();

文件读写的位置指针:
在文件读取数据时,通常按照数据在文件中的顺序依此读取,不会重复读取同一个位置的数据。写入文件也是类似,依此向后写入。
文件的读写位置是由位置指针来决定的,在流对象中,有一个数据成员就是位置指针,专门用来保存文件读写的位置。这个指针分为写指针和读指针:
读指针,是跟ifstream对应的,指定下一次读数据的位置:
我们可以使用以下函数来操作读指针:
seekg函数:用来移动读指针到指定的位置,g就是get代表读。
seekg(int n):n>0时,移动到文件的第n个字节后,n=0时,代表移动到文件起始位置。
seekg(int n,ios::beg):从文件的起始位置,beg就是begin,向后移动n个字节,n>=0
seekg(int n,ios::end):从文件的结尾位置向前移动n个字节,n<=0
seekg(int n,ios::cur):从文件当前位置向前或向后移动n个字节,cur就是current,n可以是正数或负数
tellg函数:用来返回读指针当前所在的位置
例如:streampos n=流对象.tellg(); n是streampos类型,可以当作整型数据看待。
写指针,是跟ofstream对应的,指定下一次写数据的位置:
我们可以使用以下函数来操作写指针:
seekp函数:移动写指针的位置,它的重载参数跟上面的读一样,这里省略
tellp函数:跟上面的读一样,省略

//使用读写指针来移动读写的位置
//给文件中写两个对象,然后通过移动读指针,只读取第二个对象
MyClass c1(200, 100);
MyClass c2(400, 200);
ofstream outfile("D:\\test.txt", ios::out | ios::binary);
outfile.write((char*)&c1, sizeof(c1));//c1写入了文件
outfile.write((char*)&c2, sizeof(c2));//c2写入了文件
outfile.close();
//接下来读取第二个对象
ifstream infile("D:\\test.txt", ios::in | ios::binary);
MyClass c;//准备一个对象来存储读到的内容
//移动读指针到第二个对象的起始位置
infile.seekg(sizeof(c1), ios::beg);//从开始位置移动一个对象的字节数,跳过第一个对象
infile.read((char*)&c, sizeof(c2));//将第二个对象读出来存到c中
c.show();
infile.close();

错误处理函数:
错误处理函数用于获得当前流对象的状态,来判断是否可以正常进行读写,以便在出现错误的时候,及时处理。
eof():判断文件是否到结尾,到结尾处,返回true
bad():出现严重的错误,返回true,这种情况下,不可以继续读写。
fail():操作失败,返回true,比如打开文件失败,或者读出的数据类型不匹配。
good():表示流正常,返回true。
clear():清除错误状态。

//错误状态函数
//eof()函数用于判断文件读写是否到结尾,返回int类型,如果到结尾会返回非0值,否则返回0
//给data.txt中写入abc三个字符
char a;
ifstream ifs("D:\\data.txt");
if (ifs.good())
{
	//循环读取,一次只读一个字符
	while (!ifs.eof())//eof作为条件判断,没到结尾就继续读
	{
		ifs.get(a);
        if (ifs.fail())//加一个判断,如果读取失败了,就不输出
	//		{
	//			break;
			}
		cout << a;//我们会发现,组后一个字符被输出了两次,这是由于eof函数的特性决定的,这是可以避免的。
	}
}
ifs.close();
//使用getline方法一次读一行,结合eof使用
char a[100];
ifstream ifs("D:\\data.txt");
if (!ifs)
{
	cout << "打开文件失败" << endl;
}
else
{
	while (!ifs.eof())
	{
		ifs.getline(a, 100);
		cout << a << endl;
	}
}
ifs.close();
  • 输入输出文件流fstream
    它兼具读写功能,它的构造:
    fstream iofile(“D:\d.txt”,ios::in|ios::out);
    注意:使用读写流操作文件,比较麻烦,需要时刻关注读指针和写指针的位置,建议少用。

    //输入输出文件流对象fstream实现文件的读写
    //读写d.txt文件,文件中存储了一些数据
    fstream iofile("D:\\d.txt", ios::in | ios::out);
    //文件中内容的大小不知道,那就先获取文件的大小
    iofile.seekg(0, ios::end);//读指针移动到最后,此时读指针值就等于文件的字节数
    streampos len_of_file = iofile.tellg();
    //拿到了文件的字节长度之后,我们就可以读文件,我们可以准备一个对应长度的空间来存放
    //准备一块堆内存,来存放读到的文件数据
    char* data = new char[len_of_file];
    //接下来将文件的内容读出来,需要将读指针再移动回开头处
    iofile.seekg(0, ios::beg);
    iofile.read(data, len_of_file);//文件数据已经保存到data中
    //输出data的内容进行展示
    cout << "从文件中读到的内容:" << endl;
    for (int i = 0; i < len_of_file; i++)
    {
    	cout << data[i];
    }
    cout << endl;
    //接下来,再把读到的内容写入文件中
    iofile.seekp(0, ios::end);//移动写指针到文件末尾,再开始写
    iofile.write(data, len_of_file);//将data中的数据写入文件
    delete[]data;
    data = NULL;
    iofile.close();
    

七、调试与异常

7.1C++开发中常见的问题:

(1)数字0和字母o容易混淆,尤其是某些输入法,区别很小。中英文切换问题,符号写成了中文。
(2)数组下标从0开始,注意数组越界的问题。
(3)括号匹配问题,当代码层次多的时候,容易出现括号匹配多或者少的问题。
(4)分号的问题,类结束要有分号,函数结束不能有分号,除非函数在一行内完成。
(5)变量作用域的问题,要时刻关注你使用的变量的值,以及它的作用域。尤其是一个变量被多次使用的情况下,以及存在不同作用域下的同名变量情况下。
(6)在做文件读写操作时,忘记关闭文件连接,导致文件始终被占用,后面的程序无法再次操作这个文件。
(7)运算符优先级的问题,不确定的时候去查一下,或者用()改变优先级。
(8)语法问题,C++语法复杂而且灵活,如果忘记了某些语法,及时去查找,确定以后再使用。查找也就是搜索问题答案的能力,也是很重要的,这个可以叫做搜商。
要想方设法提高自己的搜商,学会分辨知识的出处和源头,以及真伪。还有一个推荐的方式:问GPT人工智能。
(9)内存问题,比如说内存越界,比如操作字符串时,忘记了加结束符,导致读写错误。比如说内存泄露问题,忘记了回收堆内存,比如说多次释放同一块内存。
(10)使用指针的时候容易造成内存问题,需要注意:
定义指针的时候,如果还不能确定指向,那就先指向NULL
使用指针的时候,最好先判断一下是不是NULL
指针操作字符串时,由于字符串是常量,不要修改字符串的值
避免内存泄露,我们在堆内存申请的动态空间,要记得不用的时候释放掉。释放掉之后,再把指向它的指针置空。

7.2VS调试技巧:

  • 调试是软件开发中很重要的部分,必不可少的部分。

  • 调试也叫除错,Debug,是发现和减少计算机程序或电子设备中错误的一个过程。

    调试的基本步骤:

    1)发现程序错误的存在
    2)以隔离、消除等方式对错误进行定位,再确定错误产生的原因
    3)提出纠正错误的解决办法
    4)纠正后,重新对程序进行测试,以确定bug被消除

    调试快捷键:

	F5:启动调试,遇到断点会停下来
	Ctrl+F5:直接执行,不调试,有断点也不停
	F9:创建断点、取消断点
	F11:逐语句,每次执行一条语句,遇到函数可以进入函数内部执行,观察函数内部的每一行代码执行情况
	F10:逐过程,每次执行一个过程,一个过程可以是一条语句,或者是一次函数的调用。遇到函数不会进入函数内部,直接执行完毕返回当前位置。

7.3异常概念:

​ 异常就是在程序中发生难以预料的、不正常的事件,导致程序偏离正常流程的现象。
​ 异常跟bug不太一样,异常一般不会报语法错误,IDE往往检测不出来,编译也能通过,程序可以正常运行,但是在某些情况下会发生异常,导致程序错误。

例如:
​			访问数组的下标越界了,在越界时又写入了数据。
​			用new动态申请内存失败,返回空指针。
​			算术运算溢出。
​			整数除法中除数为0
​			通过野指针来访问对象等。

​ 异常发生后,需要对异常进行处理,否则程序会中断。所以需要在程序中添加一些异常处理的语句,异常处理就是在程序运行时,对异常进行检测、捕获、提示、传递、处理等过程。
​ 有异常处理后,遇到对应的异常,程序就不会中断,而是按照异常处理的方案执行。
​ 建议:当我们的程序自己无法处理的错误发生时,我们应该加异常处理。但是异常处理也不是越多越好,异常加太多会影响程序执行的效率,导致程序难以维护。

7.4异常处理语句:

​ C++提供了异常抛出语句throw和捕获处理异常的语句try-catch,通常一个try搭配多个catch。它们构成了一个流程控制逻辑。
​ 当一个函数发现自己无法处理的错误时,就用throw语句抛出异常,让函数的直接调用者或者间接调用者去处理这个错误。
​ 如果直接调用者或者间接调用者也不能处理,最终交给main函数来处理,如果main函数也处理不了,程序只能终止。
​ **throw object:**当程序出现问题抛出一个异常时,由throw完成,他会抛出一个对象,一旦抛出异常,就开时寻找try-catch模块去处理,寻找和抛出的对象类型相匹配的catch处理单元,才能处理。
​ **try:包含了可能会出现异常的代码,后面通常会跟多个catch模块。
**​ **catch(异常类型 异常变量):用于捕获对应类型的异常。catch的最后可以加一个catch(…),代表可以捕获任意类型的异常。**类似于条件判断语句中的else。
​ 异常类型:异常类型可以分为基本类型和聚合类型
​ 基本类型:如:int、char、float等
​ 聚合类型:如:指针、数组、字符串、结构体、类
​ C++标准库定义的一些函数,抛出的异常类型,都是exception类或者它的派生类的异常。
异常层次:
​ 一个try对应多个catch模块,每个catch模块捕获处理不同类型的异常,catch(…)可以捕获任意类型的异常。
​ try模块可以抛出任何类型的异常,任何异常都只能被捕获一次。
​ 未被处理的异常会继续顺着函数调用栈向上传递,直到被处理为止,否则程序只能终止执行。

int div_excep()//加了异常处理
{
	int a = 0;
	int b = 0;
	cout << "输入两个整数,空格分开" << endl;
	cin >> a >> b;
	//在除法之前,加一些判断,如果发送除零错误,就抛出异常,让异常处理代码去处理这个异常
	if (b == 0)//如果输入了0,就抛出异常
	{
		//throw "发生除零错误";
		throw 0;
	}

	return a / b;

}
//异常的向上传递
int res()
{
	int r;
	r = div_excep();
	return r;
}
//异常向上传递
try
{
	int try_res = res();
	cout << "结果是:" << try_res << endl;
}
catch (const char* msc)//捕获一个字符串类型的异常对象
{
	cout << msc << endl;
}
catch (int i)//捕获一个int类型的异常对象
{
	cout << i << endl;
}
catch (...)//捕获任何类型的异常对象
{
	cout << "发生了未知类型的异常" << endl;
}
return 0;

​ 异常就是在程序中发生难以预料的、不正常的事件,导致程序偏离正常流程的现象。
​ 异常跟bug不太一样,异常一般不会报语法错误,IDE往往检测不出来,编译也能通过,程序可以正常运行,但是在某些情况下会发生异常,导致程序错误。

例如:
​			访问数组的下标越界了,在越界时又写入了数据。
​			用new动态申请内存失败,返回空指针。
​			算术运算溢出。
​			整数除法中除数为0
​			通过野指针来访问对象等。

​ 异常发生后,需要对异常进行处理,否则程序会中断。所以需要在程序中添加一些异常处理的语句,异常处理就是在程序运行时,对异常进行检测、捕获、提示、传递、处理等过程。
​ 有异常处理后,遇到对应的异常,程序就不会中断,而是按照异常处理的方案执行。
​ 建议:当我们的程序自己无法处理的错误发生时,我们应该加异常处理。但是异常处理也不是越多越好,异常加太多会影响程序执行的效率,导致程序难以维护。

7.4异常处理语句:

​ C++提供了异常抛出语句throw和捕获处理异常的语句try-catch,通常一个try搭配多个catch。它们构成了一个流程控制逻辑。
​ 当一个函数发现自己无法处理的错误时,就用throw语句抛出异常,让函数的直接调用者或者间接调用者去处理这个错误。
​ 如果直接调用者或者间接调用者也不能处理,最终交给main函数来处理,如果main函数也处理不了,程序只能终止。
​ **throw object:**当程序出现问题抛出一个异常时,由throw完成,他会抛出一个对象,一旦抛出异常,就开时寻找try-catch模块去处理,寻找和抛出的对象类型相匹配的catch处理单元,才能处理。
​ **try:包含了可能会出现异常的代码,后面通常会跟多个catch模块。
**​ **catch(异常类型 异常变量):用于捕获对应类型的异常。catch的最后可以加一个catch(…),代表可以捕获任意类型的异常。**类似于条件判断语句中的else。
​ 异常类型:异常类型可以分为基本类型和聚合类型
​ 基本类型:如:int、char、float等
​ 聚合类型:如:指针、数组、字符串、结构体、类
​ C++标准库定义的一些函数,抛出的异常类型,都是exception类或者它的派生类的异常。
异常层次:
​ 一个try对应多个catch模块,每个catch模块捕获处理不同类型的异常,catch(…)可以捕获任意类型的异常。
​ try模块可以抛出任何类型的异常,任何异常都只能被捕获一次。
​ 未被处理的异常会继续顺着函数调用栈向上传递,直到被处理为止,否则程序只能终止执行。

int div_excep()//加了异常处理
{
	int a = 0;
	int b = 0;
	cout << "输入两个整数,空格分开" << endl;
	cin >> a >> b;
	//在除法之前,加一些判断,如果发送除零错误,就抛出异常,让异常处理代码去处理这个异常
	if (b == 0)//如果输入了0,就抛出异常
	{
		//throw "发生除零错误";
		throw 0;
	}

	return a / b;

}
//异常的向上传递
int res()
{
	int r;
	r = div_excep();
	return r;
}
//异常向上传递
try
{
	int try_res = res();
	cout << "结果是:" << try_res << endl;
}
catch (const char* msc)//捕获一个字符串类型的异常对象
{
	cout << msc << endl;
}
catch (int i)//捕获一个int类型的异常对象
{
	cout << i << endl;
}
catch (...)//捕获任何类型的异常对象
{
	cout << "发生了未知类型的异常" << endl;
}
return 0;
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值