c++面向对象程序设计教程(第四版)

c++面向对象程序设计教程(第四版)

1.面向对象程序设计

程序=对象+消息

程序一般由类的定义和类的使用两部分组成
程序中的操作都是通过想对象发送消息来实现的,对象接收到消息后,启动有关方法完成操作

对象=属性+行为(数据+函数)

一组具有相同属性和行为的对象的抽象
类和对象之间的关系:抽象和具体的关系
一个对象是类的一个实例
在面向对象程序设计中,先声明类,再由类生成其对象

消息与方法

对象的交互:对象之间的联系
消息传递机制:允许一个对象与另一个对象的交互
消息的性质:

  1. 同一个对象可以接受不同形式的多个消息,做出不同的响应
  2. 相同形式的消息可以传递给不同的对象,做出的响应可以是不同的
  3. 对消息的响应不是必须的,可以不响应
    方法包括:界面,方法体
    方法的界面:给出了 方法名 和 调用协议(成员函数的函数名和参数变)
    方法体:实现某种操作的一系列计算步骤,也就是一段程序(成员函数的函数体)
基本体征

抽象:通过特定的实例抽取共同性质后形成概念的过程
封装:把数据和实现操作的代码集中起来放在对象内部,并尽可能隐蔽对象的内部细节
C++对象中的函数名就是对象的对外接口
好处:降低了人们操作对象的复杂程度,隐藏了复杂性,提供了代码重用性,减轻了开发软件系统的难度
各个对象相互独立、互不干扰
信息隐蔽:对外界隐蔽的做法,有利于数据安全,防止无关人员访问和修改数据
继承:某个类可以继承另一个类的特征和能力
若类之间具有继承关系,则它们具有以下特征:
1.类间具有共享特征
2.类间具有差别或新增部分
3.类间具有层次结构
作用:避免公用代码的重复开发,减少代码和数据冗余,增强一致性来减少模块间的接口和界面
多态:不同的对象收到相同的消息时执行不同的操作
增强了软件的灵活性和重用性,为软件开发与维护提供了极大的便利

优点

可提高程序的重用性
可控制程序的复杂性
可改善程序的可维护性
能更好地支持大型程序设计
增强了计算机处理信息的范围
能很好地适应新的硬件环境

2.C++概述

注释:1. //开始,到行尾结束 2. /**/,可以嵌套//
C++源程序文件扩展名.cpp

头文件: iostream:声明了程序所需的输入和输出操作的有关信息

using namespace std 使用命名空间std,可以保证对C++标准库操作的每一个特性都是唯一的,不至于发生命名冲突
使用、等都需要加using namespace std ,若采用带.h的头文件时,则不需要加
两种头文件不可混用

命名空间

见7.6

输入输出

标准输入流对象cin(c+in)标准输出流对象cout(c+out)更安全更方便
cin:在程序中用于代表标准输入设备,通常指键盘
>>在c++中仍保持右移功能,用于输入时表示将从标准输入流对象cin读取的数值传送给右方指定的变量
当输入字符串(即类型为string的变量)时,提取运算符“>>”的作用是跳过空白字符,读入后面的非空白字符,直到遇到另一个空白字符为止,并在串尾放一个字符串结束标志‘\0’。
cin>>x 必须加>>,x必须是基本数据类型不能是,void类型
允许输入一连串数据cin>>a>>b>>c,两个数据间用空白符(空格、回车、Tab键)分隔
cout:在程序中用于代表标准输出设备,通常指屏幕
<<在c++中仍保持左移功能,用于输入时表示将右方变量的值写到标准输出流对象cout中
cin<<y 必须加<<,y必须是基本数据类型不能是,void类型
允许输入一连串数据cout<<a+b<<c
I/O格式:设置转换基数的操作符:dec(十进制)、hex(十六进制)、oct(八进制)
换行操作符endl,作用与\n一样
cout<<hex<<x<<’ '<<dec<<x<< ’ '<<endl

灵活的局部变量说明

允许变量声明与可执行语句在程序中交替出现
允许在代码块中的任何地方说明局部变量,从说明点到该变量所在的最小分程序末的范围有效,且符合先定义后使用

结构名、联合名、枚举名可直接作为类型名

在定义变量时,不必在结构名、联合名、枚举名前加struct、union、enum

enum Bool {FALSE,TRUE};
struct String
{	char * ptr;
	int length;
};
Bool done; //定义时不加struct、union、enum
String str; //c语言中必须加struct、union、enum	

const修饰符

#define:#define PI 3.14 将程序中的PI替换为3.14,编译后程序中不再出现PI这个标识符,PI不是变量,无类型,不占存储单元,易出错
const:const int i=10 这个常量i是有类型的,占用存储单元,有地址,可以用指针指向它,但是不可修改,消除了#define的不安全性
const和指针:

  1. 指向常量的指针:指向常量的指针变量
    const char * name="chen";
    name[3]='a'; //出错,不允许改变指针所指的常量
    name="zhang" //合法,可以改变指针所指的地址```
    
  2. 常指针:把指针所指的地址声明为对象
	char * const name="chen";
	name[3]='a'; //合法,允许改变指针所指的常量
	name="zhang" //出错,不允许改变指针所指的地址
  1. 指向常量的常指针:这个指针本身不能改变,它所指向的地址中的数据也不能改变
	const char * const name="chen";
   name[3]='a'; //出错,不允许改变指针所指的常量
   name="zhang" //出错,不允许改变指针所指的地址 	

说明:

  1. 用const定义int型常量,可以省略int
  2. 常量一旦建立,在程序的任何地方都不可改
  3. const定义的常量可以有自己的数据类型,c++的编译程序可以进行更严格的类型检查,具有连号的编译式的检查性
  4. 函数的形参也可以用const说明,用于保证形参在该函数内部不被改动

函数原型

在C++中,如果函数调用的位置在函数定义之前,则要求在函数调用之前必须对所调用的函数作函数原型声明,以说明函数的名称、参数类型与个数,以及函数返回值的类型,其主要目的是让c++编译程序进行检查,以确定调用函数的参数以及返回值类型与事先定义的原型是否相符,以保证程序的正确选。

  1. 函数原型的参数表中可以不包含参数的名字,只包含类型
  2. 函数定义由函数说明和函数体两个部分构成。函数说明部分与函数原型基本一样,但函数说明部分中的参数必须给出参数的名字,而且不能包含结尾的分号
  3. 主函数 main 不必进行原型说明,因为它被看成一个自动说明原型的函数。主函数是第1 个被执行的函数,而且不存在被别的函数调用的问题。
  4. 标准C++要求 main 函数必须声明为 int 型,即要求主函数带回一个整型函数值,如果函数执行不正常,则返回数值-1。
  5. 如果一个函数没有返回值,则必须在函数原型中注明返回类型为 void
  6. 如果函数原型中未注明参数,C++假定该函数的参数表为空(void)。
    在c++中,f();与f(void);完全一样,表示该函数不带任何参数
    在C语言中,f(void);表示该函数不带任何参数,f();的参数信息没有给出,可能有参数

void型指针

void通常表示无值,但将void作为指针的类型时,它却表示不确定的类型。这种void型指针是一种通用型指针,也就是说任何类型的指针值都可以赋给void类型的指针变量。

需要指出的是,这里说void型指针是通用指针,是指它可以接受任何类型的指针的赋值,但对已获值的void型指针,对它进行再处理,如输出或者传递指针值时,则必须再进行显式类型转换,否则会出错。

    void* pc;
    int i = 123;
    char c = 'a';
    pc = &i;
	cout << pc << endl;         //输出指针地址006FF730
	cout << *(int*)pc << endl;  //输出值123
    pc = &c;
	cout << *(char*)pc << endl; //输出值a

内联函数

在函数说明前冠以关键字“inline”,该函数就被声明为内联函数,又称内置函数。每当程序中出现对该函数的调用时,C++编译器使用函数体中的代码插入到调用该函数的语句处,同时用实参代替虚参,以便在程序运行时不再进行函数调用。

引入内联函数主要是为了消除调用函数时的系统开销,以提高运行速度。
说明:

  1. 内联函数在第一次被调用之前必须进行完整的定义,否则编译器将无法知道应该插入什么代码
  2. 在内联函数体内一般不能含有复杂的控制语句,如for语句和switch语句等
  3. 是一种空间换时间的措施,若内联函数较长,较复杂且调用较频繁时会使程序加长很多(一般1~5条语句且使用频繁才定义为内联函数)
  4. 使用内联函数替代宏定义(#define),能消除宏定义的不安全性

带有默认参数的函数

C++允许实参个数与形参个数不同
当进行函数调用时,编译器按从左到右的顺序将实参与形参结合,若未指定足够的实参,则编译器按顺序用函数原型中的默认值来补足所缺少的实参。

  1. 在函数原型中,所有取默认值的参数都必须出现在不取默认值的参数的右边。 int fun(int a, int b, int c = 111);
  2. 在函数调用时,若某个参数省略,则其后的参数皆应省略而采取默认值。不允许某个参数省略后,再给其后的参数指定参数值。
  3. 定义在调用前,应在函数定义中指定默认值,定义在调用后,必须在声明中给出默认值,此时定义中不再给默认值(可能会报错)

函数的重载

在同一作用域内,只要函数参数的类型不同,或者参数的个数不同,或者二者兼而有之,两个或者两个以上的函数可以使用相同的函数名。
说明:

  1. 调用重载函数时,函数返回值类型不在参数匹配检查之列。若函数的参数个数和类型都相同,而只有返回值类型不同,则不允许重载。
  2. 函数的重载与带默认值的函数一起使用时,有可能引起二义性。
    void Drawcircle(int r = 0, int x = 0, int y = 0);
    void Drawcircle(int r);
    Drawcircle(20);
    
  3. 在调用函数时,如果给出的实参和形参类型不相符,C++的编译器会自动地做类型转换工作。如果转换成功,则程序继续执行,在这种情况下,有可能产生不可识别的错误。
void f_a(int x);
void f_a(long x);
int c=f_a(20.83);	//无法确定转换为int还是long		

作用域运算符 ::

通常情况下,如果有两个同名变量,一个是全局的,另一个是局部的,那么局部变量在其作用域内具有较高的优先权,它将屏蔽全局变量。

如果希望在局部变量的作用域内使用同名的全局变量,可以在该变量前加上“::”,此时::value代表全局变量value,“::”称为作用域标识符。

无名联合

C++中的一种特殊联合,在关键字union后没有给出联结名,可使得一组数据成员共享同一内存地址

union
{	int i;
	double d;
}x;

在访问无名变量时,不能访问无名联合变量,而是应该访问联合变量中的成员,如 x.d x.i

强制类型转换

C语言

int i=10;
double x=(double)i;

C++

int i=10;
double x=double(i);			

运算符new和delete

程序运行时,计算机的内存被分为4个区:程序代码区、全局数据区、堆和栈。其中,堆可由用户分配和释放。C语言中使用函数malloc()和free()来进行动态内存管理。C++则提供了运算符new和delete来做同样的工作,而且后者比前者性能更优越,使用更灵活方便。
说明:

  1. 用运算符new分配的空间,使用结束后应该用也只能用delete显式地释放,否则这部分空间将不能回收而变成死空间。
  2. 在使用运算符new动态分配内存时,如果没有足够的内存满足分配要求,new将返回空指针(NULL)。
  3. 使用运算符new可以为数组动态分配内存空间,这时需要在类型后面加上数组大小。
  4. new 可在为简单变量分配空间的同时,进行初始化
指针变量名=new 类型;
int * p;
p=new int;

指针变量名 = new 类型名[下标表达式];
int * p = new int[10];
delete []指针变量名;

指针变量名 = new 类型名(初值);
int *p;
p = new int(99)

delete 指针变量名;  //指针变量保存着new分配的内存的首地址
delete p;

使用malloc函数时必须使用sizeof函数来计算所需要的字节数,new可以根据数据类型自动计算所要分配内存的大小,减少错误发生
new能够自动返回正确的指针类型,mall哦草必须在程序中进行强制数据类型转换

引用

建立引用的作用:为变量另起一个名字,变量的引用被认为变量的别名

  1. 引用与其所代表的变量共享同一内存单元,系统并不为引用另外分配存储空间。编译系统使引用和其代表的变量具有相同的地址。
  2. 引用并不是一种独立的数据类型,它必须与某一种类型的变量相联系。在声明引用时,必须立即对它进行初始化,不能声明完成后再赋值。声明一个引用时,必须同时用另一个变量的名字将它初始化
  3. 为引用提供的初始值,可以是一个变量或者另一个引用。
  4. 引用在初始化后不能再被重新声明为另一个变量的引用
  5. 指针是通过地址间接访问某个变量,而引用则是通过别名直接访问某个变量。
  6. 每次使用引用时,可以不书写 间接运算符*
  7. 不允许建立void类型的引用
  8. 不能建立引用的数组
  9. 不能建立引用的引用。不能建立指向引用的指针。引用本身不是一种数据类型,所以没有引用的引用,也没有引用的指针。
  10. 可以将引用的地址赋值给一个指针,此时指针指向的是原来的变量。
  11. 可以用const对引用加以限定,不允许改变该引用的值,但是它不阻止引用所代表的变量的值。
    引用作为函数参数、使用引用作为函数值
#include <iostream>
using namespace std;

void swap(int &a, int &b)
{
	int t = a;
	a = b;
	b = t;
}

int a[] = {1, 3, 5, 7, 9};

int & index(int i)
{
	return a[i];
}

int main() 
{
	int a = 5, b = 10;
	//交换数字a和b
	swap(a, b);
	cout << "a = " << a << " b = " << b << endl;
	cout << index(2) << endl;   //等价于输出元素a[2]的值
	index(2) = 100;             //等价于将a[2]的值赋为100;
	cout << index(2) << endl;
	
	return 0;
}

3.类和对象

1.基本概念

类的声明

比结构体类型更安全有效的数据类型——类

class 类名{
    public:
        公有数据成员;
        公有成员函数;
    protected:
        保护数据成员;
        保护成员函数;
    private:
        私有数据成员;
        私有成员函数;
};		//最后一定要加分号				//从 { 到 } 是类体
  • 一般情况下,一个类的数据成员应该声明为私有成员,成员函数声明为共有成员。这样,内部的数据整个隐蔽在类中,在类的外部根本就无法看到,使数据得到有效的保护,也不会对该类以外的其余部分造成影响,程序之间的相互作用就被降低到最小。
  • 类声明中的关键字private、protected、public可以任意顺序出现。
  • 若私有部分处于类的第一部分时,关键字private可以省略。这样,如果一个类体中没有一个访问权限关键字,则其中的数据成员和成员函数都默认为私有的。结构体默认公有
  • 不能在类声明中给数据成员赋初值。
  • 数据成员可以是任何数据类型,但是不能用 自动(auto)、寄存器(register)、外部(extern)进行说明

成员访问限定符:(声明各成员的访问属性)

  • private:只能由本类的成员函数访问
  • public:既可以本类的成员函数访问,也可以被类外的对象访问
  • protected:只能由本类及其派生类的成员函数访问

成员函数的定义

可以访问本类中的任何组成员

  • 在类的声明中只给出成员函数的原型,而成员函数的定义写在类的外部。在类外定义的一般形式是:
返回值类型 类名::成员函数名(参数表)
{   
	 函数体
}
  • 隐式定义:将成员函数直接定义在类的内部,没有inline,隐含地定义为内联
  • 显式定义:在类声明中只给出成员函数的原型,而将成员函数的定义放在类的外部。加 inline
    声明和定义时可以都加inline,也可以只在一处加
    使用inline定义内联函数时,必须将类的声明和内联成员函数的定义放在同一个文件或头文件中,否则编译时无法进行代码置换

对象的定义及使用

一个类也就是用户声明的一个数据类型
声明一个类,并不接受和存储具体的值
对象的定义:

  • 在声明类的同时,直接定义对象。在}后直接写出属于该类的对象名表 }op1, op2;
  • 声明了类之后,在使用时再定义对象 ` Score op1, op2;

对象中成员的访问:

  • 通过对象名和对象选择符 . (简称点运算符) 对象名.数据成员名 对象名.成员函数名[ (实参表) ]
  • 通过指向对象的指针(在定义对象时,定义的是指向此对象的指针) ->操作符
class date{
		public:
			int year;
		};
		date d,*p;
		p=&d;
		cout<<p->year;	//        d.year      (* p).year		p->year	三者等价

类的作用域和类成员的访问属性

类的作用域:在类的声明中的一对花括号所形成的作用域。在类的作用域内,任何成员函数可以不受限制地访问该类中的其他成员
一般,公有成员是类的对外接口,私有成员是类的内部数据和内部实现不希望外界访问
将类的成员划分为不同的访问级别好处:信息隐蔽,数据保护

2.构造函数和析构函数

对象的初始化和构造函数

类声明中不能给数据成员赋初值

class Complex{
	double real=0;/**/错误的**
	};

如果一个类中的所有成员都是公有的,可以在定义对象时对数据成员进行初始化

class Complex{
public:
	double real;
	double imag;
	};
	Complex c1={1.1,2.2};

如果类中包含私有成员或保护成员,可以采用类中的公有成员函数来对对象中的数据成员赋初值

  • 构造函数是是属于某一个类的,一种特殊的成员函数,它主要用于为对象分配空间,进行初始化。
  • 构造函数的名字必须与类名相同,而不能由用户任意命名。否则编译程序将把它当做一般的成员函数来处理。
  • 它可以有任意类型的参数,但不能具有返回值。可以由用户提供,也可以由系统生成。
  • 编译系统自动生成的默认构造函数,不带任何参数,函数体是空的,只能为对象开辟数据成员存储空间,不能赋初值
  • 与普通的成员函数一样,构造函数的函数体可以写在类体内,也可写在类体外。
  • 当构造函数直接定义在类内时,系统将构造函数作为内联函数处理
  • 构造函数一般声明为公有成员,但它不需要也不能像其他成员函数那样被显式地调用,它是在定义对象的同时被自动调用,而且只执行一次。
  • 构造函数可以不带参数,此时对对象的初始化是固定的,
    Complex(){				//不带参数的构造函数
    	real=0;
    	imag=0;
    	}
    

在建立对象时采用构造函数给数据成员赋初值,有两种形式:

  • 类名 对象名 [ (实参表) ];

  • 类名 * 指针变量名=new 类名 [ (实参表) ]; //这个对象没有名字,称为 无名对象 // delete 指针变量名

用成员初始化列表对数据成员初始化

在函数首部实现

类名 ::构造函数名 (	  [参数表]	) :数据成员名1(初始值1),数据成员名2(初始值2), ......
{		//构造函数体
}

对于用const修饰的数据成员,或是引用类型的数据成员,不允许用赋值语句直接赋值,可以用成员初始化列表

构造函数的重载

#include <iostream>
using namespace std;
class Date
{	public:
		Date ();						//声明1个无参数的构造函数
		Date (int y,int m,int d);	//声明1个带有3个参数的构造函数
		void showDate();
	private:
		int year;
		int month;
		int day;						//一个类中可以包含多个构造函数,但对每个对象而言,建立对象时只执行其中一个构造函数
};
Date::Date ()						//只要类中定义了一个构造函数,系统将不再提供默认构造函数
{	year=2000;	month=4;	day=28;
}
Date::Date (int y,int m,int d)
{	year=y;	month=m	day=d;
}
inline void showDate()
{	cout<<year<<"."<<month<<"."<<day<<endl;
}
int main()
{	Date date1;	//使用无参构造函数创建对象时,用这个Date date1;	不能用Date date1();表示声明一个函数名为date1返回值类型为Date的普通函数	
	date1.showDate();
	Date date2(2002,11,14);
	date2.showDate();
	return 0;
}

带默认参数的构造函数

Complex (double r=0.0,double i=0.0);	//在声明构造函数时指定默认参数
Complex::Complex(double r,double i)	//在类外定义构造函数时,可以不再指定参数的默认值
{real=r; imag=i}
  • 构造函数在类的声明外定义,默认参数必须在声明时指定
  • 如果构造函数的全部参数都指定了默认值,则在定义对象时可以指定0个或1个或几个实参,这时的构造函数属于默认构造函数
  • (一个类中只能有一个默认构造函数)
  • 如果构造函数的全部参数都指定了默认值不能再定义重载构造函数

析构函数

  • 通常用于撤销对象时的一些清理任务,如释放分配给对象的内存空间等。
  • 析构函数与构造函数名字相同,但它前面必须加一个波浪号(~)。
  • 析构函数没有参数和返回值,
  • 不能被重载,因此只有一个。
  • 当撤销对象时,编译系统会自动调用析构函数。
  • 每个类必须有析构函数,如果没有显式的定义,编译系统会自动生成一个默认析构函数(函数体是空的)
  • 除了主函数结束时,对象被撤销,系统会自动调用析构函数,在以下情况下,析构函数也会被调用
    1. 一个对象被定义在一个函数体内,当该函数被调用结束时,该对象会被释放
    2. 对象是使用new运算符动态创建的,在使用delete释放它时

3.对象数组和对象指针

对象数组

每一个数组元素都是对象的数组
有几个数组元素就调用几次构造函数

类名 数组名[下标表达式]

exam ob1 [4] = {89, 97, 79, 88};	//用只有1个参数的构造函数给对象数组赋值

Exam ob2 [4] = {89, 90};	//用不带参数和带1个参数的构造函数给对象数组赋值,
//先调用带1个参数的构造函数初始化ob2[0]和ob2[1],再调用不带参数的构造函数初始化ob2[2]和ob2[3],

Complex com [3]={
	Complex (1,2),
	Complex (3,4),
	Complex (5,6)
};	//用带有多个参数的构造函数给对象数组赋值

对象指针

每一个对象在初始化后都会在内存中占有一定的空间。因此,既可以通过对象名访问对象,也可以通过对象地址来访问对象。对象指针就是用于存放对象地址的变量。声明对象指针的语法形式为:

类名 * 对象指针名;
用指针访问单个对象成员   exe ob,*p;    p=&ob;		p->show();		(*p).show();
用对象指针访问对象数组	exe ob[2],*p;	p=ob; 

this指针

实际上,给对象赋值就是给对象的数据成员赋值
C++的编译系统只用了一段空间来存放共同的函数代码,
每一个对象的存储空间都只是数据成员所占用的存储空间,函数代码是存储在对象空间之外
称为自引用指针
每当创建一个对象时,系统把this指针初始化为指向该对象,this指针的值是当前调用成员函数的对象的起始地址
this指针是隐式使用的,是作为参数被传递给成员函数的

void disp()	{cout<<"x="<<x<<endl;}
实际使用时,c++编译系统把它处理为:
void disp(*this)	{cout<<"x="<<this->x<<endl;}

4.string类

C++支持两种类型的字符串,第一种是C语言中介绍过的、包括一个结束符’\0’(即以NULL结束)的字符数组,标准库函数提供了一组对其进行操作的函数,可以完成许多常用的字符串操作。如:strcpy、ctrcat、strlen等
C++仍保留了这种格式字符串;
C++标准库中声明了一种更方便的字符串类型,即字符串类string,类string提供了对字符串进行处理所需要的操作。使用string类必须在程序的开始包括头文件string,即要有以下语句:#include <string>

string 对象1,对象2,......;
strnig str1,	str2("china"),	str3="administrator"

在这里插入图片描述
在表达式中,可以string类对象和以‘\0’结束的字符串混在一起使用

5.向函数传递对象

  • 使用对象作为函数参数: void show(类名 对象名)
    对象可以作为参数传递给函数,其方法与传递其他类型的数据相同。在向函数传递对象时,是通过“传值调用”的方法传递给函数的。因此,函数中对对象的任何修改均不影响调用该函数的对象(实参本身)。
  • 使用对象指针作为函数参数:
    对象指针可以作为函数的参数,使用对象指针作为函数参数可以实现传值调用,即在函数调用时使实参对象和形参对象指针变量指向同一内存地址,在函数调用过程中,形参对象指针所指的对象值的改变也同样影响着实参对象的值。
  • 使用对象引用作为函数参数:
    在实际中,使用对象引用作为函数参数非常普遍,大部分程序员喜欢使用对象引用替代对象指针作为函数参数。因为使用对象引用作为函数参数不但具有用对象指针做函数参数的优点,而且用对象引用作函数参数将更简单、更直接。

6.对象的赋值和复制

对象赋值语句

B=A; 将对象A的数据成员逐位复制给B

  1. 两个对象的类型必须相同
  2. 当类中存在指针时,可能会出错

拷贝构造函数

  • 形参是本类对象的引用,作用是:在建立一个对象时使用一个已经存在的对象去初始化这个新对象 Point p2(p1);
  • 因为拷贝构造函数也是一种构造函数,所以其函数名与类名相同,并且该函数也没有返回值。
  • 拷贝构造函数只有一个参数,并且是同类对象的引用。
  • 每个类都必须有一个拷贝构造函数。可以自己定义拷贝构造函数,用于按照需要初始化新对象;如果没有定义类的拷贝构造函数,系统就会自动生成一个默认拷贝构造函数,用于复制出与数据成员值完全相同的新对象。
自定义:
类名::类名(const 类名 &对象名)
{	
		//函数体     通过自定义可以 有选择有变化地复制,如乘2后赋值
}
调用:
类名 对象2(对象1);//代入法
类名 对象2=对象1; //赋值法

当类中有指针类型时,调用默认拷贝构造函数有时会产生错误

调用拷贝构造函数的三种情况:

  1. 当用类的一个对象去初始化该类的另一个对象时;
  2. 当函数的形参是类的对象,调用函数进行形参和实参结合时;
  3. 当函数的返回值是对象,函数调用完毕将返回值(对象)带回函数调用处时。

7.静态成员

为了实现一个类的多个对象之间的数据共享
可以全局变量
将一个数据成员说明为static,静态数据成员。无论建立多少个类的对象,都只有一个静态数据成员的拷贝

static 数据类型 数据成员名;
  1. 静态数据成员的定义与普通数据成员相似,但前面要加上static关键字。
  2. 静态数据成员初始化应在类外单独进行,而且应在定义对象之前进行。一般在main()函数之前、类声明之后的特殊地带为它提供定义和初始化。数据类型 类名::静态数据成员名=初始值;
  3. 静态数据成员属于类(准确地说,是属于类中对象的集合),而不像普通数据成员那样属于某一对象,因此,可以使用“类名::”访问静态的数据成员。格式如下:类名::静态数据成员名
  4. 静态数据成员与静态变量一样,是在编译时创建并初始化。它在该类的任何对象被建立之前就存在。因此,公有的静态数据成员可以在对象定义之前被访问。对象定以后,公有的静态数据成员也可以通过对象进行访问。对象名.静态数据成员名; 对象指针->静态数据成员名;
  5. 私有静态数据成员不能在类外直接访问

在类定义中,前面有static说明的成员函数称为静态成员函数。静态成员函数属于整个类,是该类所有对象共享的成员函数,而不属于类中的某个对象。静态成员函数的作用不是为了对象之间的沟通,而是为了处理静态数据成员。定义静态成员函数的格式如下:

static 返回类型 静态成员函数名(参数表);
类名::静态成员函数名(实参表);
对象.静态成员函数名(实参表);
对象指针->静态成员函数名(实参表);
  1. 静态函数成员主要用来访问静态成员函数。当它与静态数据成员一起使用时,达到了对同一个类中对象之间共享数据的目的。
  2. 私有静态成员函数不能被类外部的函数和对象访问。
  3. 使用静态成员函数的一个原因是,可以用它在建立任何对象之前调用静态成员函数,以处理静态数据成员,这是普通成员函数不能实现的
  4. 编译系统将静态成员函数限定为内部连接,也就是说,与现行文件相连接的其他文件中的同名函数不会与该函数发生冲突,维护了该函数使用的安全性,这是使用静态成员函数的另一个原因。
  5. 静态成员函数是类的一部分,而不是对象的一部分。如果要在类外调用公有的静态成员函数,类名::静态成员函数名()
  6. 非静态成员函数有this指针。静态成员函数没有this指针,可以直接访问本类中的静态数据成员,一般不访问非静态成员,只能通过对象名(或对象指针,对象引用)访问对象的非静态成员

8.友元

类的主要特点之一是数据隐藏和封装,即类的私有成员(或保护成员)只能在类定义的范围内使用,也就是说私有成员只能通过它的成员函数来访问。但是,有时为了访问类的私有成员而需要在程序中多次调用成员函数,这样会因为频繁调用带来较大的时间和空间开销,从而降低程序的运行效率。为此,C++提供了友元来对私有或保护成员进行访问。友元包括友元函数和友元类。

友元函数

友元函数既可以是不属于任何类的非成员函数,也可以是另一个类的成员函数。友元函数不是当前类的成员函数,但它可以访问该类的所有成员,包括私有成员、保护成员和公有成员。

  • 在类中声明友元函数时,需要在其函数名前加上关键字friend。
  • 此声明可以放在公有部分,也可以放在保护部分和私有部分。
  • 友元函数可以定义在类内部,也可以定义在类外部。
将非成员函数声明为友元函数
class Girl{
	public;
	......
	friend void disp(Girl &);
	}
void disp(Girl &x)
{ cout<<x.name<<endl; }
  1. 友元函数虽然可以访问类对象的私有成员,但他毕竟不是成员函数。因此,在类的外部定义友元函数时,不必像成员函数那样,在函数名前加上“类名::”。
  2. 因为友元函数不是类的成员,所以它不能直接访问对象的数据成员,也不能通过this指针访问对象的数据成员,它必须通过作为入口参数传递进来的对象名(或对象指针、对象引用)来访问该对象的数据成员。
  3. 友元是对类的封装机制的补充,友元函数提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。尤其当一个函数需要访问多个类时,友元函数非常有用,普通的成员函数只能访问其所属的类,但是多个类的友元函数能够访问相关的所有类的数据。
class Girl{
	public;
	......
	friend void prdata(const Girl &,const Boy &);
	}	
	class Boy{
	public;
	......
	friend void prdata(const Girl &,const Boy &);
	}
void prdata(const Girl &g,const Boy &b)
{ cout<<g.name<<endl; 
  cout<<b.name<<endl; }
将成员函数声明为友元函数
  • 一个类的成员函数可以作为另一个类的友元,它是友元函数中的一种,称为友元成员函数。
  • 友元成员函数不仅可以访问自己所在类对象中的私有成员和公有成员,
  • 还可以访问friend声明语句所在类对象中的所有成员,这样能使两个类相互合作、协调工作,完成某一任务。
  • 一个类的成员函数作为另一个类的友元函数时,必须先定义这个类。并且在声明友元函数时,需要加上成员函数所在类的类名;
class Girl;	//为Girl类的提前引用声明
	class Boy{
	public;
	......
	void prdata(Girl &);
	}
class Girl{
	public;
	......
	friend void Boy::prdata(Girl &);	//声明类Boy的成员函数prdata为类Girl的友元函数
	}	

void prdata(Girl &x)
{ cout<< name<<endl; 
  cout<<x.name<<endl; }
  int main ()
  {	Boy b("李华",11);
  	Girl g("小红",22);
  	b.prdata(g);
  	return 0; }	//结果是两个对象的内容

友元类

friend 类名;	//此类名为友元类的,这条语句可以放在公有部分,也可在私有部分
class Y{......};
class X{
	....
	friend Y;
	};

当一个类被说明为另一个类的友元类时,它所有的成员函数都成为另一个类的友元函数,这就意味着作为友元类中的所有成员函数都可以访问另一个类中的所有成员。

友元关系单向的,不具有交换性和传递性。

9.类的组合

在一个类中内嵌另一个类的对象作为数据成员,称为类的组合。该内嵌对象称为对象成员,又称为子对象。

class A{
    ···
};
class B{
    A a;
   public:
    ···
};
class X{
	类名1 对象成员1;
	类名2 对象成员2;
	...
	};
类X的构造函数的定义形式:
X::X(形参表0) : 对象成员1(形参表1),对象成员2(形参表2),.......
{	函数体	}				//对象成员1(形参表1),对象成员2(形参表2),.......是 初始化表,作用是对对象成员进行初始化

当调用构造函数X::X()时,首先按各内嵌对象成员在类声明中的顺序依次调用它们的构造函数进行初始化,最后再执行类X的构造函数体,初始化其他成员。析构函数的调用顺序与构造函数相反

10.常类型

常类型的引入就是为了既保护数据共享又防止数据被改动。
常类型是指使用类型修饰符const说明的类型,
常类型的变量或对象成员的值在程序运行期间是不可改变的。

常引用

如果在说明引用时用const修饰,则被说明的引用为常引用。常引用所引用的对象不能被更新。如果用常引用做形参,便不会产生对实参的不希望的更改。

const 类型 & 引用名;

经常用作函数的形参,常参数`

常对象

如果在说明对象时用const修饰,则被说明的对象为常对象。常对象的数据成员在对象的整个生存期内不能改变,为常量且必须要有初值。

类名 const 对象名[	参数表	];	
或者
const 类名 对象名[	参数表	];

在定义对象是必须进行初始化

常对象成员

  • 类的数据成员可以是常量或常引用,
  • 使用const说明的数据成员称为常数据成员。
  • 如果在一个类中说明了常数据成员,那么构造函数就只能通过成员初始化列表对该数据成员进行初始化,而任何其他函数都不能对该成员赋值。
类型说明符 函数名(参数表) const;
  • const是函数类型的一个组成部分,因此在声明函数和定义函数时都要有关键字const。在调用时不必加const。
  • 关键字const可以被用于对重载函数进行区分。
  • 常成员函数可以访问常数据成员,也可以访问普通数据成员。
  • 常数据成员可以被常成员函数访问,也可以被普通成员函数访问
  • 常对象只能调用它的常成员对象,而不能调用普通成员函数。常成员函数是常对象唯一的对外接口。
  • 常对象函数不能更新对象的数据成员,也不能调用该类的普通成员函数,这就保证了在常成员函数中绝不会更新数据成员的值。在这里插入图片描述

4.派生类与继承

继承可以在已有类的基础上创建新的类,新类可以从一个或多个已有类中继承成员函数和数据成员,而且可以重新定义或加进新的数据和函数,从而形成类的层次或等级。其中,已有类称为基类或父类,在它基础上建立的新类称为派生类或子类。
从已有类产生新类的过程,类的派生

1.派生类的概念

派生类的声明

class 派生类名: 继承方式 基类名{
	派生类新增的数据成员和成员函数
};		//继承方式:public或private或protected	默认为私有继承

派生类的构成

派生类中的成员包括从基类继承过来的成员和自己增加的成员
1)派生类从基类接受成员
2)调整从基类接受来的成员
一方面是改变基类成员在派生类中的访问属性
另一方面是派生类可以对基类的成员进行重定义:
在派生类中声明一个与基类成员同名的成员,则派生类中的新成员会覆盖基类中的同名成员
3)在派生类中增加新成员
基类中的构造函数和析构函数是不能被继承的

基类成员在派生类中的访问属性

基类中的成员在公有派生类中的访问属性在私有派生类中的访问属性在保护派生类中的访问属性
私有成员不可直接访问不可直接访问不可直接访问
公有成员公有私有保护
保护成员保护私有保护

派生类对基类成员的访问规则

内部访问:有派生类中的新增成员函数对基类继承来的成员的访问
对象访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问

  • 私有继承的访问规则
基类中的成员私有成员公有成员保护成员
内部访问不可访问可访问可访问
外部访问不可访问不可访问不可访问
  • 公有继承的访问规则
基类中的成员私有成员公有成员保护成员
内部访问不可访问可访问可访问
外部访问不可访问可访问不可访问
  • 保护继承的访问规则
基类中的成员私有成员公有成员保护成员
内部访问不可访问可访问可访问
外部访问不可访问不可访问不可访问

2.派生类的构造函数和析构函数

派生类的构造函数和析构函数的执行顺序

当创建派生类对象时,首先执行基类的构造函数,后调用派生类的构造函数的顺序执行。析构函数的调用顺序与构造函数的调用顺序正好相反,先调用派生类的析构函数,后调用基类的析构函数。

派生类的构造函数和析构函数的构造规则

  • 当基类的构造函数没有参数,或没有显示定义构造函数,派生类可以不向基类传递参数,可以不定义构造函数
  • 当基类含有带参数的构造函数时,派生类必须定义构造函数,以提供把参数传递给基类构造函数的途径
  • 若基类使用默认构造函数或不带参数的构造函数,则在派生类中定义构造函数时可略去“:基类构造函数名(参数表)”,此时若派生类也不需要构造函数,则可不定义构造函数。
  • 如果派生类的基类也是一个派生类,每个派生类只需负责其直接基类数据成员的初始化,依次上溯。
派生类名 (参数总表): 基类名(参数表)
{
	派生类新增数据成员的初始化语句
}
派生类名 (参数总表)中包括参数的类型和参数名
基类名(参数表)中只有参数名,没有类型,因为这里是调用基类构造函数,这些参数是实参,可以为派生类构造函数总参数中的参数,也可以是常量或全局部变量
  • 派生类中可以根据需要定义析构函数,用于对派生类中的新增成员清理,基类的清理仍有基类的析构函数负责
  • 析构函数是不带参数的,派生类中是否要自定义析构函数与它所属的基类的析构函数无关
当派生类中含有内嵌的对象成员(子对象)时:
派生类名 (参数总表): 基类名(参数表0),对象成员名1(参数表1),......
{
	派生类新增数据成员的初始化语句
}

定义派生类对象时,,构造函数的执行顺序:

  • (1)调用基类的构造函数,进行初始化
  • (2)调用内嵌的对象成员的构造函数,进行初始化
  • (3)执行派生类的构造函数体,进行初始化
  • 派生类中含有多个内嵌的对象成员时,调用内嵌的对象成员的构造函数顺序由在类中声明的顺序确定
  • 如果一个派生类的基类也是一个派生类,每个派生类只需负责其直接基类数据成员的初始化,依次上溯

3.调整基类成员在派生类中的访问属性的其他方法

同名成员

派生类可以声明与基类成员同名的成员。在没有虚函数的情况下,如果在派生类中定义了与基类成员同名的成员,则称派生类成员覆盖了基类的同名成员,在派生类中使用这个名字意味着访问在派生类中声明的成员。为了在派生类中使用与基类同名的成员,必须在该成员名之前加上基类名和作用域标识符“::”,即

基类名::成员名
class X{
	public:
		int f();
};
class Y:public X{
	public:
		int f();
		void q();
};
void Y::q()
{	f();		//表示访问派生类中的f(),即Y::f()
	X::f();   //表示访问基类中的f()
}
Y obj;
obj,f();		//表示访问派生类中的f(),即Y::f()
obj.X::f();

访问声明

访问声明的方法就是把基类的保护成员或共有成员直接写在私有派生类定义式中的同名段中,同时给成员名前冠以基类名和作用域标识符“::”。利用这种方法,该成员就成为派生类的保护成员或共有成员了。

class B:private A{
	public:		//
		.....
		A::print;		//printA中的函数
		......
};
  • 数据成员也可以使用访问声明
  • 访问声明中只包含不带类型和参数的函数名或变量名
  • 不能改变成员在基类中的访问属性
  • 对于基类中的重载函数名,访问声明将对基类中所有同名函数起作用

4.多重继承

多重继承派生类的声明

class 派生类名:继承方式1  基类名1,.......,继承方式n  基类名n{
	派生类新增的数据成员和成员函数
};
//不写继承方式则默认是private

多重继承派生类的构造函数和析构函数

派生类名 (参数总表): 基类名1(参数表1),基类名2(参数表2),......,基类名n(参数表n)
{
	派生类新增成员的初始化语句
}

  • 多重继承派生类构造函数必须同时负责该派生类所有基类构造函数的调用。
  • 多继承构造函数的调用顺序与单继承构造函数的调用顺序相同,也是遵循先调用基类的构造函数,再调用对象成员的构造函数,最后调用派生类构造函数的原则。析构函数的调用与之相反。
  • 处于同一层次的各个基类构造函数的执行顺序,取决于声明派生类时所指定的各个基类的顺序,与派生类构造函数中所定义的成员初始化列表的各项顺序没有关系

虚基类

  • 虚基类的作用: 如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最低层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。在访问这些同名成员时,必须在派生类对象名后增加直接基类名,使其唯一地标识一个成员,以免产生二义性。
  • 在C++中,如果想使这个公共的基类只产生一个复制,可以将这个基类说明为虚基类
虚基类在派生类中的声明	//用在中间的类,base,base1,base2,derved,在base1,base2需要
class 派生类名:virtual 继承方式 基类名{
...}
#include <iostream>
#include <string>
using namespace std;

class Base{
protected:
	int a;
public:
	Base(){
		a = 5;
		cout << "Base a = " << a << endl;
	}
};

class Base1: public Base{
public:
	Base1() {
		a = a + 10;
		cout << "Base1 a = " << a << endl;
	}
};

class Base2: public Base{
public:
	Base2() {
		a = a + 20;
		cout << "Base2 a = " << a << endl;
	}
};

class Derived: public Base1, public Base2{
public:
	Derived() {
		cout << "Base1::a = " << Base1::a << endl;
		cout << "Base2::a = " << Base2::a << endl;
	}
};

int main() {
	Derived obj;
	return 0;
}
Base a = 5
Base1 a = 15
Base a = 5
Base2 a = 25
Base1::a = 15
Base2::a = 25
class Base1:virtual public Base{
public:
	Base1() {
		a = a + 10;
		cout << "Base1 a = " << a << endl;
	}
};

class Base2:virtual public Base{
public:
	Base2() {
		a = a + 20;
		cout << "Base2 a = " << a << endl;
	}
};
  • 虚基类的初始化与一般的多继承的初始化在语法上是一样的,但构造函数的调用顺序不同。
  • 如果在虚基类中定义有带形参的构造函数,并且没有定义默认形式的构造函数,则整个继承结构中,所有直接或间接的派生 类都必须在构造函数的成员初始化列表中列出对虚基类构造函数的调用,以初始化在虚基类中定义的数据成员。
  • 建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类 的构造函数进行初始化的。该派生类的其他基类对虚基类构造函数的调用都被自动忽略。
  • 若同一层次中同时包含虚基类和非虚基类,应先调用虚基类的构造函数,再调用非虚基类的构造函数,最后调用派生类构造函数。
  • 对于多个虚基类,构造函数的执行顺序仍然是先左后右,自上而下。
  • 若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再调用派生类的构造函数。
  • 关键字virtual与派生方式关键字(public/private/protected)的先后顺序无关
  • 允许一个基类在作为某些派生类虚基类的同时,又作为另一些派生类的非虚基类

5.基类与派生类对象之间的赋值兼容关系

在一定条件下,不同类型的数据之间可以进行类型转换,如可以将整型数据赋值给双精度型变量。在赋值之前,先把整型数据转换成双精度数据,然后再把它赋给双精度变量。这种不同数据类型之间的自动转换和赋值,称为赋值兼容。在基类和派生类对象之间也存有赋值兼容关系,
基类和派生类对象之间的赋值兼容规则是指在需要基类对象的任何地方,都可以用公有派生类的对象代替。

  1. 派生类可以向基类对象赋值
  2. 派生类可以初始化基类对象的引用
  3. 派生类对象的地址可以赋给指向基类对象的指针
  4. 如果函数的形参是基类对象或基类对象的引用,在调用函数时可以用派生类对象作为实参
  5. 声明为指向基类对象的指针可以指向它的公有派生对象,但不允许指向它的私有派生对象
  6. 不允许将一个声明为指向派生类对象的指针指向它的基类的对象

5.多态性

多态性是面向对象程序设计的重要特征之一。
多态性机制不仅增加了面向对象软件系统的灵活性,进一步减少了冗余信息,而且显著提高了软件的可重用性和可扩充性。
多态性就是不同对象收到相同的消息时,产生不同的行为

1.编译时的多态性和运行时的多态性。

在C++中,多态的实现和联编这一概念有关。
一个源程序经过编译、连接,成为可执行文件的过程是把可执行代码联编在一起的过程
接口访问功能不同的函数,从而实现“一个接口,多种方法”。

在C++中,多态性的实现和联编(也叫绑定)这一概念有关。一个源程序经过编译,连
接,成为可执行文件的过程是把可执行代码联编(或称装配)在一起的过程。其中在运愆多
前就完成的联编称为静态联编,又叫前期联编;而在程序运行时才完成的联编叫动态联编,
也称后期联编。
静态联编是指系统在编译时就决定如何实现某一动作。静态联编要求在程序编译时就
知道调用函数的全部信息。因此,这种联编类型的函数调用速度很快。效率高是静态联编
的主要优点。
动态联编是指系统在运行时动态实现某一动作。采用这种联编方式,一直要到程序运
行时才能确定调用哪个函数。动态联编的主要优点是:提供了更好的灵活性、问题抽象性
和程序易维护性。
静态联编支持的多态性称为编译时多态性,也称静态多态性。在C++中,编译时多。
态性是通过函数重载(包括运算符重载)和模板(在第 6 章企绍)实现的。利用函数重载
机制,在调用同名的函数时,编译系统可根据实参的具体情况确立所要调用的是哪个
函数。
动态联编所支持的多态性称为运行时多态性,也称动态多态性。在C++ 中,运行时多
态性是通过虚函数来实现的。

2.运算符重载

运算符重载:通过创建运算符重载函数来实现
运算符重载函数可以是类外定义的普通函数,也可以是类的成员函数或友元函数
在进行运算符重载时,必须定义一个运算符重载函数,名字为operator运算符 如operator+

类Complex的两个对象的相加
Complex operator+ (Complex om1,Complex om2)
{
	Complex temp;
	temp.real=om1.real+om2.real;
	temp.imag=om1.imag+om2.imag;
	return temp;
}
total=com1+com2;	//或者
total=operator+(com1,com2);
  • C++不能重载的运算符:
 .	成员访问运算符
 . *	成员指针访问运算符
 ::	作用域运算符
 sizeof	长度运算符
 ?:	条件运算符
  • 只能对已有的c++运算符进行重载,不允许自定义新的运算符
  • 一般,重载的功能应该与原有的功能相类似
  • 重载不能改变运算符的操作对象的个数
  • 重载不能改变运算符原有的优先级
  • 重载不能改变运算符原有的结合特性
  • 运算符重载函数的参数至少应有一个是类对象(或类对象的引用),防止用户修改用于标准类型数据的运算符性质
  • 运算符重载函数可以是类外定义的普通函数,也可以是类的成员函数或友元函数
  • 一般用于类对象的运算符必须重载,但赋值运算符“=”不需要。
  • 某些情况下,如数据成员中包含指向动态分配内存的指针成员时,系统提供的对象赋值运算符函数不能满足需求,在赋值时可能出错

友元运算符重载函数

  • 在类外定义的运算符重载函数只能访问类中的公有数据成员
在类的内部,定义
friend 函数类型 operator 运算符 (形参表)
{
	函数体
}
class X{
	...
	friend 函数类型 operator 运算符 (形参表);
	...
};
函数类型 operator 运算符 (形参表)
{
	函数体
}
  • 运算符重载函数可以是返回任何类型,也可以是void类型,通常与它所操作的类的类型相同
  • 有的运算符不能定义为友元运算符重载函数,如赋值运算符=、下标运算符[]、函数调用运算符()
  • 友元运算符重载函数重载的是双目运算符,则参数表中有两个操作数
  • 友元运算符重载函数重载的是单目运算符,则参数表中有一个操作数

成员运算符重载函数

在类的内部,定义
函数类型 operator 运算符 (形参表)
{
	函数体
}
class X{
	...
	函数类型 operator 运算符 (形参表);
	...
};
函数类型 X:: operator 运算符 (形参表)
{
	函数体
}
  • 成员运算符重载函数重载的是双目运算符,则参数表中有一个操作数,作为运算符的右操作数,左操作数是隐含的,为该类的当前对象,通过this指针隐含地传递给函数aa@bb;隐式调用 aa.operator@(bb);显式调用
  • 成员运算符重载函数重载的是单目运算符,则参数表为空@aa;隐式调用 aa.operator@();显式调用

成员运算符重载函数与友元运算符重载函数比较

  • 双目运算符,以下情况必须使用友元函数:
将一个复数与一个整数相加,用成员运算符函数重载+:
Complex operator+ (int a)
{	return (real+a,imag);	}
Complex com1,com;
com1=com+100;	//com1=com.operator(100);
com1=100+com;	//com1=100.operator(com);**错误**
friend Complex operator+ (Complex com,int a)
{	return Complex (com.real+a,com.imag);	}	//右侧是整数
friend Complex operator+ (int a,Complex com)
{	return Complex (a+com.real,com.imag);	}	//右侧是类对象
  • 一般,双目运算符重载为友元运算符重载函数更方便
  • 单目运算符重载为成员运算符重载函数较好
  • 如果运算符所需的操作数(尤其是第一个操作数)希望有隐式类型转换,必须重载为友元函数
  • = () [] -> 只能作为成员函数
  • += -= /= *= &= != ~= %= <<= >>= 建议重载为成员函数

++和–的重载

C++2.1及以后的版本中,C++编辑器可以通过在运算符函数参数表中是否插入关键字int来区分前缀后缀

前缀++ob
ob.operator++ (); //成员函数重载
operator++ (X &ob); //友元函数重载
后缀ob++
ob.operator++ (int); //成员函数重载
operator++ (X &ob,int); //友元函数重载
调用时,参数int一般被传递给值0

友元运算符重载函数没有this指针,不能引用this指针所指的对象,使用友元函数重载自增运算符++或自减运算符–时,采用对象引用参数传递数据

赋值运算符=的重载

对任一类X,如果用户没有自定义的赋值运算符函数,系统将自动生成一个默认的赋值运算符函数
指针悬挂问题:类中有指针类型时,使用默认的赋值运算符函数会产生错误

关于浅层复制的例子:
#include <iostream>
using namespace std;
class STRING{
	public:
		STRING(char *s)
		{	cout<<"Constructor called."<<endl;
			ptr=new char [strlen(s)+1];
			strcpy(ptr,s);
		}
		~STRING(char *s)
		{	cout<<"Destructor called.---"<<ptr<<endl;
			delete ptr;
		}
	private:
		char *ptr;
};
int main()
{
	STRING p1("book");
	STRING p2("jeep");
	p2=p1;	//执行p2=p1后,p2和p1指向同一个book,撤销p2后,p1指向未知
	return 0;
}
关于深层复制的例子:
#include <iostream>
using namespace std;
class STRING{
	public:
		STRING(char *s)
		{	cout<<"Constructor called."<<endl;
			ptr=new char [strlen(s)+1];
			strcpy(ptr,s);
		}
		~STRING(char *s)
		{	cout<<"Destructor called.---"<<ptr<<endl;
			delete ptr;
		}
		STRING &operator= (const STRING &);
	private:
		char *ptr;
};
STRING &STRING::operator= (const STRING &s)
{
	if(this==&s)	return *this;
	delete ptr;
	ptr=new char [strlen(s.ptr)+1];
	strcpy(ptr,s.ptr);
	return *this;
}
int main()
{
	STRING p1("book");
	STRING p2("jeep");
	p2=p1;	//执行p2=p1后,p2和p1分别指向两个book,撤销p2后,p1指向不变
	return 0;
}

下标运算符[]的重载

在C++中,在重载下标运算符时[],认为它是双目运算符,X[Y],X是左操作数,Y是右操作数。X.operator[] (Y);

3.类型转换

1系统预定义类型间的转换

隐式类型转换
int x=5,y;
y=3.5+x;
先将x的值5转换为double型,然后与3.5相加,将8.5转换为整数8,然后赋值给y
  • A=B,B的值转换为A类型
  • 当char或short类型变量与int类型变量进行运算时,char或short类型转换为int类型
  • 当两个操作对象类型不一致时,在算术运算前,级别低的类型自动转换为级别高的类型
显式类型转换

2类类型与系统预定义类型间的转换

转换构造函数

作用:类型转换,将一个其他类型的数据或另一个类的对象转换成他所在类的对象

定义:构造函数,只有一个参数,参数是待转换类型的数据,函数体中是转换的方法
使用:
类名(待转换类型的数据)
类型转换函数

作用:类型转换,将一个类的对象转换成其他类型的数据

函数名:operator 目标类型  前面不能指定函数类型,不能有参数
operator 目标类型()	//目标类型为希望转换成的类型名,既可以是预定义的标准数据类型也可以是其他类的类型
{
	函数体	//通常,最后是return语句,返回值的类型是该函数的目标类型
}
  • 类型转换函数只能定义为一个类的成员函数,不能是类的友元函数
  • 函数名:operator 目标类型 前面不能指定函数类型,不能有参数
  • 必须有return语句
  • 一个类可以有多个类型转换函数
  • 类型转换函数的使用分为显式转换和隐式转换
类Complex是复数,类中有下列两个转换
Complex(int i)	//转换构造函数,int转换为Complex的对象
{
	real=imag=i/2; 
}
operator int()	//类型转换函数
{
	return real+imag;
}

int main()
{
	Complex a1(1,2),a2(3,4);//两次调用带参数的构造函数
	Complex a3;//调用不带参数的构造函数
	a3=a1+a2;
}

类中没有定义两个对象相加的运算符重载函数,a3=a1+a2;是因为C++自动进行隐式转换:

  1. 寻找两个对象相加的运算符重载函数,未找到
  2. 寻找能将Complex类的对象转换为int型数据的类型转换函数operator int,可找到,将a1,a2转换为3和7
  3. 寻找两个证书相加的运算符函数,预定义,,3+7=10
  4. a3=a1+a2 左边是类对象,右边是int型10,隐式调用转换构造函数,将int型10转换为临时对象(5,5),赋给a3

4.虚函数

  • C++规定:基类对象的指针可以指向它的公有派生类,但此时只能访问派生类中从基类继承来的成员,不能访问公有派生类中定义的成员
  • 虚函数的定义是在基类中进行的,它是在基类中需要定义为虚函数的成员函数的声明中冠以关键字virtual,从而提供一种接口界面。定义虚函数的方法如下:
virtual 函数类型 函数名(形参表) {
    函数体
}
  • 虚函数的作用:允许在派生类中重新定义与基类同名的函数,并可以通过基类指针或引用来访问基类和派生类中的同名函数
  • 在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。
  • 虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。
  • 在基类中,只声明虚函数原型(加virtual),在类外定义时不必再加virtual
  • virtual可不写(C++规定,如果在派生类中,没有用virtual显式地给出虚函数声明,这时系统就会遵循以下的规则来判断一个成员函数是不是虚函数:该函数与基类的虚函数是否有相同的名称、参数个数以及对应的参数类型、返回类型或者满足赋值兼容的指针、引用型的返回类型。)
  • 如果派生类中没有对基类的虚函数重新定义,则公有派生类继承其直接基类的虚函数。
  • 一个虚函数无论被公有继承多少次,仍保持其虚函数的特性
  • 虚函数必须是其所在类的成员函数,不能是友元函数,不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。
  • 只有通过基类指针访问虚函数才能获得运行时的多态性,对象名和点运算符的是静态
  • 由于虚函数使用的基础是赋值兼容规则,而赋值兼容规则成立的前提条件是派生类从其基类公有派生。因此,通过定义虚函数来使用多态性机制时,派生类必须从它的基类公有派生。
  • 在派生类对基类中声明的虚函数进行重新定义时,关键字virtual可以写也可以不写。
  • 内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时仍将其看做非内联的。
  • 构造函数不能是虚函数,但是析构函数可以是虚函数,而且通常说明为虚函数。
  • 在一个派生类中重新定义基类的虚函数是函数重载的另一种形式。
  • 多继承可以视为多个单继承的组合,因此,多继承情况下的虚函数调用与单继承下的虚函数调用由相似之处。

虚析构函数

如果在主函数中用new运算符建立一个派生类的无名对象和定义一个基类的对象指针,并将无名对象的地址赋值给这个对象指针,当用delete运算符撤销无名对象时,系统只执行基类的析构函数,而不执行派生类的析构函数。

Base *p;
p = new Derived;
delete p;

输出:调用基类Base的析构函数

原因是当撤销指针p所指的派生类的无名对象,而调用析构函数时,采用了静态连编方式,只调用了基类Base的析构函数。

如果希望程序执行动态连编方式,在用delete运算符撤销派生类的无名对象时,先调用派生类的析构函数,再调用基类的析构函数,可以将基类的析构函数声明为虚析构函数。一般格式为

virtual ~类名(){
    ·····
}

虽然派生类的析构函数与基类的析构函数名字不相同,但是如果将基类的析构函数定义为虚函数,由该类所派生的所有派生类的析构函数也都自动成为虚函数。

虚函数与重载函数的关系

在一个派生类中重新定义基类的虚函数是函数重载的另一种形式,但不同于一般的函数重载
普通的函数重载,函数的参数或参数类型必须有所不同,返回类型也可以不同
虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。

纯虚函数和 抽象类

纯虚函数是一个在基类中说明的虚函数,没有定义,但要求在它的派生类中根据需要对它进行定义,或仍说明为纯虚函数
声明纯虚函数的一般形式如下:

virtual 函数类型 函数名(参数表) = 0;

声明为纯虚函数后,基类中就不再给出程序的实现部分。
纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要重新定义。

如果一个类至少有一个纯虚函数,那么就称该类为抽象类,对于抽象类的使用有以下几点规定:

  • 由于抽象类中至少包含一个没有定义功能的纯虚函数。因此,抽象类只能作为其他类的基类来使用,不能建立抽象类对象。
  • 不允许从具体类派生出抽象类。所谓具体类,就是不包含纯虚函数的普通类。
  • 抽象类不能用作函数的参数类型、函数的返回类型或是显式转换的类型。
  • 可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态性。
  • 如果派生类中没有重新说明纯虚函数,而派生类中只是继承基类的纯虚函数,则这个派生类仍然是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。

模板与异常处理

1.模板的概念

  • 利用模板机制可以显著减少冗余信息,能大幅度地节约程序代码,进一步提高面向对象程序的可重用性和可维护性。
  • 模板是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数,从而实现代码的重用,使得一段程序可以用于处理多种不同类型的对象,大幅度地提高程序设计的效率。
  • 函数模板与类模板
  • 宏定义带来的另一个问题是,可能在不该替换的地方进行了替换,而造成错误。事实上,由于宏定义会造成不少麻烦,所以在C++中不主张使用宏定义。解决以上问题的另一个方法就是使用模板。#define Max(x, y)((x >= y) ? x : y)

2.函数模板与模板函数

所谓函数模板,实际上是建立一个通用函数,其函数返回类型和形参类型不具体指定,用一个虚拟的类型来代表
在调用函数时系统会根据市实参的类型(模板实参)来取代模板中的虚拟类型从而实现了不同函数的功能

template <typename 类型参数>
返回类型 函数名(模板形参表)
{
	函数体
}
template <class 类型参数>
返回类型 函数名(模板形参表)
{
	函数体
}
  • template是一个声明模板的关键字 ,表示声明一个模板
  • 类型参数(通常用C++标识符表示,如T、Type等)实际上是一个虚拟的类型名
  • 类型参数前需加关键字typename或class ,表示其后的参数是一个虚拟的类型名(类型参数)
  • 模板实参:函数模板中的类型参数实例化的参数
#include <iostream>
using namespace std;
template <typename T>
T Max(T *array, int size = 0) {
	T max = array[0];
	for (int i = 1	; i < size; i++) {
		if (array[i] > max) max = array[i];
	}
	return max;
}
int main() {
	int array_int[] = {783, 78, 234, 34, 90, 1};
	double array_double[] = {99.02, 21.9, 23.90, 12.89, 1.09, 34.9};
	int imax = Max(array_int, 6);
	double dmax = Max(array_double, 6);
	cout << "整型数组的最大值是:" << imax << endl;
	cout << "双精度型数组的最大值是:" << dmax << endl;
	return 0;
}
  • 在函数模板中允许使用多个类型参数。但是,应当注意template定义部分的每个类型参数前必须有关键字typename或class。
    template <typename 类型参数1,typename 类型参数2>
  • 在template语句与函数模板定义语句之间不允许插入别的语句。
  • 同一般函数一样,函数模板也可以重载。
  • 函数模板与同名的非模板函数可以重载。在这种情况下,调用的顺序是:首先寻找一个参数完全匹配的非模板函数,如果找到了就调用它;若没有找到,则寻找函数模板,将其实例化,产生一个匹配的模板参数,若找到了,就调用它。

3.类模板与模板类

所谓类模板,实际上就是建立一个通用类,其数据成员、成员函数的返回类型和形参类型不具体指定,用一个虚拟的类型来代表。使用类模板定义对象时,系统会根据实参的类型来取代类模板中虚拟类型,从而实现不同类的功能。

template <typename 类型参数>
class 类名{
	类成员声明
};

template <class 类型参数>
class 类名{
	类成员声明
};

在类声明中,欲采用通用数据类型的数据成员、成员函数的参数或返回类型前面需加上类型参数。

类模板明<实际类型名>对象名( 实参表列 )

类模板中的成员函数(其中含有类型参数)可以定义在类体内的。
若成员函数中有类型参数存在,则C++有一些特殊的规定:

  • 需要在成员函数定义之前进行模板声明
  • 在成员函数名前缀上类名<类型参数>::

在类模板体外定义的成员函数的一般形式如下:

template <typename 类型参数>
函数类型 类名<类型参数>::成员函数名(形参表)
{
    ·····
}

类模板代表一类类,模板类表示某一具体的类

6.8 类模板 Stack 的使用举例。
在此例子中建立了字符型和整型两个堆栈。
#include<iostream>
using namespace std;
const int size=10;
template<typename Type>//模板声明,其中 Type 为类型参数
class Stack		//类模板名为 stack
{
	publicvoid init()
		{ tos=0;}
		void push(Type ch);//声明成员函数 push 的原型,函数参数为 Type 类型
		Type pop();//声明成员函数 pop 的原型,返回类型为 Type 类型
	private:
	Type stck[size];//数组类型为 Type,即数组可取任意类型
	int tos;
};
template<typename Type>//模板声明
void Stack<Type>::push(Type ob)//在类模板体外定义成员函数 push
{ 
	if (tos==size)
	{ 	cout<<"Stack is full";
		return;
	}
	stck [tos]=ob;
	tos++;
}
template <typename Type >//模板声明
Type stack <Type>::pop()//在类模板体外定义成员函数pop
{
	if (tos==0{ 	cout<<"stack is empty";
		return 0;
	}
	tos--;
	return stck[tos];
}
int main()
{ 
	//定义字符堆栈
	Stack <char>s;//用类模板定义对象 s,此时 Type 被 char 替代
	int i;
	s. init();
	s.push('a');
	s.push('b');
	s.push("e');
	for(i=0;i<3;i++) 
		cout<<"pop s:"<<s.pop()<<endl;
	//定义整型堆栈
	Stack <int>is;//用类模板定义对象 is,此时 Type 被 int 替代
	is.init();
	is.push(1);
	is.push(3);
	is.push(5);
	for (i=0;i<3;i++)
	cout<<"pop is:"<<is.pop()<<endl;
	 return 0;
}
#include <iostream>
#include <string>
using namespace std;

const int size = 10;
template <class T>
class Stack{
private:
	T stack[size];
	int top;
public:
	void init() {
		top = 0;
	}
	void push(T t);
	T pop();
};

template <class T>
void Stack<T>::push(T t) {
	if (top == size) {
		cout << "Stack is full!" << endl;
		return;
	}
	stack[top++] = t;
}

template <class T>
T Stack<T>::pop() {
	if (top == 0) {
		cout << "Stack is empty!" <<endl;
		return 0;
	}
	return stack[--top];
}

int main() {
	Stack<string> st;
	st.init();
	st.push("aaa");
	st.push("bbb");
	cout << st.pop() << endl;
	cout << st.pop() << endl;

	return 0;
}
  • 在每个类模板定义之前,都需要在前面加上模板声明 template <class 类型参数>或template <typename 类型参数>
  • 类模板在使用时,必须在类模板名字后面缀上<类型参数>
  • 模板类可以有多个类型参数

4.异常处理

我们在编写程序时,不仅要保证程序的正确性,而且还要求程序安全可靠,具有一定的容错能力。也就是说,一个程序不仅要在正常的环境下运行正确,而且在环境出现意外或用户操作不当的情况下,也应该有正确合适的处理和防范。C++提供了专门的异常处理
机制。

6.4.1异常处理概述

程序中常见的错误分为两大类:编译时的错误和运行时的错误。
编译时的错误主要是语法错误,如关键字拼写错误,语句末尾缺分号,括号不匹配等。这类错误相对比较容易修正,因为编译系统会指出在第几行,是什么样的错误。
运行时的错误则不然,其中有些甚至是不可预料的,如算法出错;有些虽然可以预料但却无法避免,如内存空间不够,无法实现指
定的操作等:还有在函数调用时存在的一些错误,如无法打开输入文件、数组下标越界等。
程序在运行过程中出现的错误统称为异常,对异常的处理称为异常处理。
我们在设计程序时,应当事先分析程序运行时可能出现的各种意外情况,并且分别制定出相应的处理方法,使程序能够继续执行,或者至少给出适当的提示信息。传统的异常处理方法基本上是采取判断或分支语句来实现,

传统的异常处理方法可以满足小型的应用程序需要。但是在一个大型软件系统中,包含许多模块,每个模块又包含许多函数,函数之间又互相调用,比较复杂。如果在每一个函数中都设置处理异常的程序段,会使程序过于复杂和庞大。传统的异常处理机制无法保证程序的可靠运行,而且采用判断或分支语句处理异常的方法不适合大量异常的处理,更不能处理不可预知的异常。C++提供的异常处理机制逻辑结构非常清晰,而且在一定程度上可以保证程序的健壮性。

6.4.2 异常处理的方法

C++处理异常的办法是:如果在执行一个函数过程中出现异常,可以不在本函数中立即处理,而是发出一个信息,传给它的上一级(即调用函数)来解决,如果上一级函数也不能处理,就再传给其上一级,由其上一级处理。如此逐级上传,如果到最高一级还无法处理,运行系统一般会自动调用系统函数 terminate,由它调用 abort 终止程序。
这样的异常处理方法使得异常的引发和处理机制分离,而不是由同一个函数完成。这样做法的好处是使底层函数(被调用函数)着重用干解决实际任务,而不必过多地考虑对异常的处理,以减轻底层函数的负担,而把处理异常的任务上移到上层去处理。例如在主函数中调用十几个函数,只需在主函数中设计针对不类型的异常处理,而不必在每个函数中都设置异常处理,这样可以大大提高效率。

  • C ++处理异常的机制是由检查,抛出和捕获 3 个部分组成的,分别由 3 种语句来完成:
  • try(检查).throw(抛出)和 catch(捕获)。
1.异常的抛出

抛出异常使用 throw 语句,其格式如下:
throw 表达式;
如果在某段程序中发现了异常,就可以使用 throw 语句抛出这个异常给调用者,该异常由与之匹配的 catch 语句来捕获。
throw 语句中的“表达式”是表示抛出的异常类型,异常类型由表达式的类型来表示。

int Div(int x,int y)
{ if (y==0)
	throw y;	//抛出异常,当除数 y为 0 时,语句 throw 将抛出 int 型异常
return x/y;
}
2.异常的检查和捕获
try
{
	被检查的复合语句
}
catch (异常类型声明1)
{
	进行异常处理的复合语句1
}
catch (异常类型声明2)
{
	进行异常处理的复合语句2
}
.
.
.
catch (异常类型声明n)
{
	进行异常处理的复合语句n
}
  • try后面的复合语句是被检查语句,容易引起异常的语句,称为代码的保护段
  • catch用来捕获throw抛出的异常,catch子句后的复合语句是异常处理语句
  • 异常类型声明部分指明了catch子句处理的异常的类型
  • 被检测的语句或程序段必须放在try块中,否则不起作用
  • try和catch块必须有花括号括起来的复合语句,即使花括号内只有一个语句也不能省略花括号
  • 一个try_catch结构中只能有一个try块,可以有多个catch块
  • catch后面的括号中一般只写异常信息的类型名
  • 如果在catch子句中没有指定异常信息的类型,而用了三点删节符...,则表示可以捕获任何类型的异常信息
  • 在某种情况下,在throw语句中可以不包括表达式throw; 此时它将正在处理的异常信息再次抛出,给其上一层的catch处理
  • C++中,一旦抛出一个异常,而程序有不捕获的话,系统会调用系统函数 terminate,由它调用 abort 终止程序。
断言

C:#include<assert.h>
C++:#include

7.C++的流类库与输入输出

1.C++为何建立自己的输入/输出系统

C++除了完全支持C语言的输入输出系统外,还定义了一套面向对象的输入/输出系统。
C++的输入输出系统比C语言更安全、可靠。
C++的编译系统对数据类型进行严格的检查,凡是类型不正确的数据都不可能通过编译
C++需要定义众多的用户自定义类型,C语言的scanf和printf无法对这些数据进行I/O操作
C++的输入/输出系统 是类型安全的、可以防止格式控制符与输入输出数据的类型不一致的错误。另外,C++可以通过重载运算符“>>”和"<<",使之能用于用户自定义类型的输入和输出,并且向预定义类型一样有效方便。C++的输入/输出的书写形式也很简单、清晰,这使程序代码具有更好的可读性。

2 C++流的概述

C++的输入/输出是以字节流的形式实现的。
在输入操作中,字节流从输入设备(如键盘、磁盘、网络连接等)流向内存;在输出操作中,字节流从内存流向输出设备(如显示器、打印机、网络连接等)。
字节流可以是ASCII码、二进制形式的数据、图形/图像、音频/视频等信息。
文件和字符串也可以看成有序的字节流,分别称为文件流和字符串流。
“流”指的是数据从一个源流到一个目的的抽象,它负责在数据的生产者(源)和数据的消费者(目的)之间建立联系,并管理数据的流动。
凡是数据从一个地方传输到另一个地方的操作都是流的操作,从流中提取数据称为输入操作(通常又称提取操作),向流中添加数据称为输出操作(通常又称插入操作)。
I/O流类库:包含许多用于输入输出的类,称为流类
用流类定义的对象称为流对象,输入流对象,输出流对象

用于输入/输出的头文件

常用的头文件有:

  • iostream 包含了对输入/输出流进行操作所需的基本信息。使用cin、cout等流对象进行针对标准设备的I/O操作时,须包含此头文件。
  • fstream 用于用户管理文件的I/O操作。使用文件流对象进行针对磁盘文件的操作,须包含此头文件。
  • strstream 用于字符串流的I/O操作。使用字符串流对象进行针对内存字符串空间的I/O操作,须包含此头文件。
  • iomanip 用于输入/输出的格式控制。在使用setw、fixed等大多数操作符进行格式控制时,须包含此头文件。

用于输入/输出的流类

iostream类库中包含了许多用于输入/输出操作的类。其中,类istream支持流输入操作,类ostream支持流输出操作,类iostream同时支持流输入和输出操作。

在这里插入图片描述

预定义的流对象

C++中包含几个预定义的流对象,它们是
标准输入流对象cin: 是istream的派生类istream_withassign的对象,与标准输入设备相联系
标准输出流对象cout: 是ostream的派生类istream_withassign的对象,与标准输出设备相联系
非缓冲型的标准出错流对象cerr: 是ostream的派生类istream_withassign的对象,与标准错误输出设备相联系
缓冲型的标准出错流对象clog: 是ostream的派生类istream_withassign的对象,与标准错误输出设备相联系

  • cerr和clog的区别:
  • cerr是不经过缓冲区,直接向显示器输出有关信息,发给它的任何内容都立即输出
  • clog中的信息存放在缓冲区中,缓冲区满后或遇上endl时向显示器输出

输入输出流的成员函数

put函数

put函数:输出一个字符

cout.put(char c);

参数不但可以是字符,还可以是字符的ASCII码(也可以是一个整型表达式)

cout<<'A'
cout.put('A');
cout.put(65);
cout.put(20+45);
都是相同的
可以在一个语句中连续调用put函数
cout.put(65),cout.put(66),cout.put(67),cout.put('\n');
get函数

作用是从输入流中读取一个字符(包括空白字符),赋给字符变量ch,如果读取成功则函数返回非0值,失败(遇文件结束符EOF)函数返回0值
功能与提取运算符<<类似,区别在于,get函数在读入数据是可以包括空白字符,提取运算符<<在默认情况下拒绝接受空白字符

cin.get(ch);
getline函数

功能是从输入流中读取n-1个字符,赋给字符数组,然后插入一个字符串结束标志\n
如果在读取n-1个字符之前遇到指定的终止字符,则提前结束读取,然后插入一个字符串结束标志\n

cin.getline(字符数组,字符个数n,终止标志字符);
cin.getline(字符指针,字符个数n,终止标志字符);
  • 可以读取空格,无第三个参数时不包括\n
  • 只能用于输入字符型数据,而cin<<可以读取C++标准类型的各类数据(重载后还可以用于输入自定义类型的数据)
ignore函数

功能是跳过输入流中n个字符(默认n=1),活在遇到指定的终止字符(默认是EOF)时提前结束

cin.ignore(n,终止字符)

可以不带参数或只带一个参数

cin.ignore()相当于cin.ignore(1,EOF)

3.预定义类型的输入输出

插入运算符与提取运算符

cin>>变量;
cout<<常量或变量;

  • 在 C++中“<”本来是被定义为左位移运算符,由于在 iostream 头文件中对它进行了重载,使它能用作标准类型数据的输出运算符。

  • 插入运算符(也称输出运算符)“<<”是一个双目运算符,有两个操作数,左操作数为输出流类 ostream 的一个流对象(如 cout),右操作
    数为一个系统预定义的标准类型的常量或变量。

在头文件 iostream 中有一组运算符函数对运算符“<<”进行重载,以便能用它输出各种标准类型的数据,其原型具有:ostream &operator<<( ostream & 类型名);的形式。其中,类型名是指 int、float,double,char*,char 等 C++标准类型。这表明,只要输出的数据属于其中的一种,就可以直接使用插入运算符“<<”完成标准类型数据的输出任务。
可以把不同类型的数据组合在一条语句中
编译程序根据出现在“<<”操作符右边的变量或常量的类型来决定调用形参为哪种标准类型的运算符重载函数。

  • 在C++中“>>”本来是被定义为右位移运算符,由于在iostream头文件中对它进行了重载,使它能用作标准类型数据的输入运算符。
  • 提取运算符(也称输入运算符)“>>”也是一个双目运算符,有两个操作数,左面的操作数是输入流类istream的一个对象(如cin),右面
    的操作数是系统预定义的任何标准数据类型的变量。
    在头文件iostream中也有一组提取运算符函数对运算符“>>”进行重载,以便能用它输人各种标准类型的数据,其原型具有istream& operator>>(istream&类型名&);的形式,其中,类型名是指 int、float,double,char*,char 等 C++标准类型。这表明,只要输人的数据属于其中的一种,就可以直接使用提取运算符“>>”完成标准类型数据的输入任务。
  • 在默认情况下,运算符>>将跳过空白符,然后读入后面的与变量类型相对应的值
  • 当输入字符串(即类型为 char的变量)时,提取运算符“>>”的作用是跳过空白,读入后面的非空白字符,直到遇到另一个空白字符为止,并在串尾放一个字符串结束标态’\0’。因此,输入字符串遇到空格时,就当作本数据输人结束。例如:
    char
    str;
    cin>>stri
    当键入的字符串为:Object_Oriented Programming!则输入后,str中的字符串是“Object_Oriented”,而后面的字符串“Programming!”被略去
  • 数据输入时,系统除检查是否有空白外,还检查输入数据与变量的匹配情况。例
    如,对于语句
    cin>>i>>x;
    //i为 int 型,x为 float 型
    若从键盘输人:
    56.79 32.5
    得到的结果就不是预想的
    i=56,x=32.5,
    而是
    i=56, x=0.79,
    这是因为,系统是根据变量的类型来分隔输人的数据的。在这种情况下,系统把 56.79 中小
    数点前面的整数部分赋给了整型变量 i,而把剩下的 0.79 赋给了浮点型变量 x。

输入输出的格式控制

使用 ios 类中有关格式控制的流成员函数进行格式控制

在这里插入图片描述

  1. 设置状态标志
    即是将某一状态标志位置“1”,可使用setf函数
一般格式:
long ios::setf(long flags)
一般的调用格式
流对象.setf(ios::状态标志);
例:
istream isobj;
ostream osobj;
isobj.setf(ios::skipws);
osobj.setf(ios::left);
  • 状态标志ios中被定义成枚举值,所以在引用这些状态标志时要在前面加 ios::
  • 在使用setf函数设置多项标志时中间应用 或运算符| 分隔 cout.setf(ios::showpos|ios::dec|ios::scientific)
  • setf函数设置格式后,如果想改变设置为同组的另一种状态,应调用unsetf函数,先终止原来设置的状态,再设置其他状态
  1. 清除状态标志
    即是将某一状态标志位置“0”,可使用unsetf函数
一般格式:
long ios::unsetf(long flags)
一般的调用格式
流对象.unsetf(ios::状态标志);

  1. 设置宽域
int ios::width(int n);
  • 所设置的宽域仅对下一个流输出操作有效,之后回复默认宽域0
  1. 设置实数的精度
    设置实数的精度为n位
int ios::precision(int n);
  • 调用precision函数的设置在程序中一直有效,除非被重新设置
    在以一般十进制小数形式输出时n代表有效数字
    在以fixed(固定小数位)形式和scientific(指数)形式输出时,n为小数位数
  1. 填充字符
    fill函数
    作用是当输出值不满宽域时,默认情况下填充字符为空格。使用时必须与width函数配合
char ios::fill(char ch);
  • 调用fill函数的设置在程序中一直有效,除非被重新设置
  • 填充字符的填充位置由ios::left(字符填充在数据右边,输出数据左对齐)和ios::right(默认是这个设置)
#include<iostream>
int main()
{
	cout<<"-------1--------\n";
	cout.width(10);//设置域宽为 10 位
	cout<<123<<endl;//输出整数 123,占 10 位,默认为右对齐
	cout<<"-------2--------\n";
	cout<<123<<endl;	//输出整数 123,上面的 width(10)已不起作用
	//此时按系统默认的域宽输出(按数据实际长度输出)
	cout<<"-------3--------\n";
	cout.fill('&');	//设置填充字符为'&'
	cout.width(10);//设置域宽为 10 位
	cout<<123<<endl;//输出整数 123,占 10 位,默认为右对齐
	cout<<"-------4--------\n";
	cout.setf(ios::left);//设置左对齐
	cout<<123<<endl;//输出整数 123,上面的 width(10)已不起作用
	//按数据实际长度输出,左对齐
	cout<<"-------5--------\n";
	cout.precision(4);//设置实数的精度为 4 位
	cout<<123.45678<<endl;//以一般十进制小数形式输出时,有效数字为 4
	cout<<"-------6--------\n";
	cout setf(ios::fixed)//用定点格式(固定小数位数)显示浮点数
	cout<<123.45678<<endl;//以 fixed形式输出时,小数位数占 4 位
	cout<<"-------7--------\n;
	cout,width(15);//设置域宽为 15 位
	cout unsetf(ios::fixed)//清除用定点格式(小数形式)显示浮点数
	cout setf(ios::scientific)://用科学表示法格式(指数)显示浮点数
	cout<<123.45678<<endl;//用科学表示法格式(指数)输出此浮点数,小数占4位
	cout<<"-------8--------\n";
	int a=21;
	cout.setf(ios::showbase);//输出整数时显示基数符号
	cout.unsetf(ios::dec);//终止十进制数的格式设置
	cout.setf(ios::hex);//设置以十六进制数输出格式
	cout<<a<<endl;//以十六进制数输出a
	return 0;
}
使用称为操纵符的特殊类型的函数进行格式控制

所有不带形参的操纵符都定义在头文件iostream.h中
带形参的操纵符都定义在头文件iomainip.h中,
在这里插入图片描述

使用用户自定义的操纵符进行输入输出格式控制
istream &操纵符名(istream &stream)
{
	自定义代码
	return stream;
}

ostream &操纵符名(ostream &stream)
{
	自定义代码
	return stream;
}

操作符函数中返回流对象stream(也可以用其他标识符,但必须与形参表中的流对象相同)

4.用户自定义类型的输入输出

  • 重载提取运算符,重载插入运算符 不能是所操作的类的成员函数,可以使该类的友元函数或普通函数
  • 函数的第一个参数out是ostream类对象的引用,out必须是输出流对象,可以是任何的标识符,必须与return后的相同
    • 函数的第一个参数in是istream类对象的引用,in必须是输入流对象,可以是任何的标识符,必须与return后的相同
istream &operator>>(istream &in,用户自定义的类型名 &obj)
{
	in>>obj.item1;
	in>>obj.item2;
	...
	return in;
}
ostream &operator<<(ostream &out,用户自定义的类型名 &obj)
{
	out<<obj.item1;
	out<<obj.item2;
	...
	return out;
}

5.文件的输入输出

所谓“文件”,一般指存放在外部介质上的数据的集合。
一批数据是以文件的形式存放在外部介质 (如磁盘、光盘、U 盘)上的。
操作系统以文件为单位对数据进行管理,也就是说,如果想查找存放在外部介质上的数据,必须先按文件名找到所指定的文件,然后再从该文件中读取数据,而要把数据存储在外部介质上,必须先建立一个文件(以文件名标识),才能向它输出数据。
在前面各章节中,数据的输入输出均使用 cin 和 cout 通过标准输入设备(通常指键盘) 和输出设备(通常指显示器)来进行的。
在实际应用中,常用磁盘作为数据存放的中介,应用程序的输入模块通过键盘或其他输人设备将数据读入磁盘,处理模块对存放在磁盘中的数 据进行加工,加工后的数据或者仍然存放在磁盘上,以备以后再处理,或者由输出模块通过 显示器或打印机等设备输出。从操作系统的角度来说,每一个与主机相连的输人输出设备 都可以看出是一个文件。
例如,键盘是输人文件,显示器和打印机是输出文件。还有磁盘文 件、光盘文件和U盘文件等外存文件,其中磁盘文件是使用最广泛的外存文件。
C++把文件看做字符序列,即文件是由一个一个字符数据顺序组成的。
根据数据的组织形式,文件可分为文本文件和二进制文件。
文本文件又称 ASCII 文件,它的每个字节存 放一个 ASCII 代码,代表一个字符。
二进制文件则是把内存中的数据,接其在内存中的存 储形式原样写到磁盘上存放。
用文本形式输出时,一个字节对应一个字符,因而便于对字符进行逐个处理,也便于输出字 符,缺点是占存储空间较多。
用二进制数形式输出数据,可以节省存储空间和转换时间,但 一个字节不能对应一个字符,不能直接以字符形式输出。
对于需要暂时保存在外存上,以后又需要输入到内存的中间结果数据,通常用二进制数形式保存。
操作系统命令一般将文件作为一个整体来处理的,例如删除文件、复制文件等。
由于文 件的内容可能千变万化,文件的大小各不相同,为了以统一的方式处理文件,在 C++ 中引人 了流式(stream)文件的概念,即无论文件的内容是什么,一律看成是由字符(或字节)构成 的序列,即字符流。
流式文件中的基本单位是字节,磁盘文件和内存变量之间的数据交流以字节为基础。为了叙述方便,不章中凡用到外存文件的地方均以磁盘文件来代表。

文件的打开与关闭

文件的打开

C++中进行文件输入输出的基本过程:首先创建一个流对象,然后将这个流对象与文件向关联,即打开文件,此时才能进行读写操作,然后关闭文件,关闭一个文件,就 是取消这种关联。

类名说明功能
ifstream输入文件流类用于文件的输入
ofstream输出文件流类用于文件的输出
fstream输入输出文件流类用于文件的输入/输出
  • 这三个文件流类都定义在fstream.h
  • 1.在程序中包含头文件fstream, 由于文件的输人输出要用到以上的 3 个文件流类, 而这 3 个文件流类都定义在 fstream 头文件中,所以首先在程序中要包含头文件 fstream
    (2)建立流。要以磁盘文件为对象进行输人输出,必须建立一个文件流类的对象,通过 文件流对象将数据从内存输出到磁盘文件,或者通过文件流对象从磁盘文件将数据输入到内存
  • 2.建立流,即定义流类的对象,如ifstream in; ofstream out; fstream both;分别定义了输人流对象 in,输出流对象 out,输人输出流对象 both。其实,在用标准设 备为对象的输人输出中,也是要定义对象的,如 cin 和 cout 就是流对象,C++就是通过流对 象进行输人输出的,由于 cin,cout 已在头文件 iostream 中事先定义,所以用户不需要自己 定义。
  • 3.使用成员函数open打开文件,使某一指定的磁盘文件与某一已定义的文件流对象建立关联,open 函数是上述 3 个流类的成员函数,其原型是在 fstream 中定义的. 调用成员函数open 的一般形式为:文件流对象.open(文件名,打开方式);文件名可以包括路径(如“D:\C ++ \filel.dat”),如默认路径.则默认为当前目录下的文件。“打开方式”决定文件将如何被打开.
方式功能
ios::app打开一个输出文件,用于将数据添加到文件尾部
ios::ate打开一个现存文件,把文件指针移到文件末尾
ios::in打开一个文件,以便进行输入操作
los::nocreate打开一个文件,若文件不存在,则打开失败,创建问题
ios::noreplace打开一个文件,若文件存在,则打开失败,替代问题
ios::out打开一个文件,以便进行输出操作
ios:: trunc打开一个文件,若文件已存在,删除其中全部数据;若文件不存在,则建立新文件
ios::binary以二进制方式打开一个文件,默认为文本文件

下面对这些值作进一步的说明:

  • 如果希望向文件尾部添加数据,则应当用“ios::app”方式打开文件,但此时文件必须 存在。打开时,文件位置指针移到文件尾部。用这种方式打开的文件只能用于输出。
  • 用“ios::ate”方式打开一个已存在的文件时,文件位置指针自动移到文件的尾部,数据可以写人文件中的任何地方。
  • 用“ios::in”方式打开的文件只能用于输人数据,而且该文件必须已经存在。如果用 类ifstream来定义一个流对象,则隐含为输入流对象,不必再说明使用方式。
  • 用“ios::out方式打开文件,表示可以向该文件输出数据。如果用类ofstream 来定义一个流对象,则隐含为输出流对象,不必再说明使用方式。
  • 通常,当用 open 函数打开文件时,如果文件存在,则打开该文件,否则建立该文件。
  • 但当用“ios::nocreate”方式打开文件时,表示不建立新文件,在这种情况下,如果要打开的文件不存在,则函数 open调用失败。
  • 如果使用“ios::noreplace”方式打开文件,则表示不修改原来文件,而是要建立新文件。因此,如果文件已经存在,则 open函数调用失败
  • (注意: 新版本的 C++ I/O 类库中不提供 os::nocreate 和 ios::noreplace)。
  • 当使用“ios::trunc”方式打开文件时,如果文件已存在,则清除该文件的内容,文件长度被压缩为零。 实际上,如果指定“ios::out”方式,且未指定“ios:'ate”方式或“ios:: app” 方式,则隐含为“ios::trunc”方式。
  • 如果使用“ios::binary”方式,则以二进制数方式打开文件,默认所有的文件以文本方式打开。
  • 在用文本文件向计算机输入时,把回车和换行两个字符转换为一个换行符,而在输出时把换行符转换为回车和换行两个字符。
  • 对于二进制文件则不进行这种转换,在内存 中的数据形式与输出到外部文件中的数据形式完全一致,一对应。
  • 了解了文件的使用方式后,可以通过以下步骤打开文件: (1)定义一个流类的对象,例如: ofstream out; 定义了类 ofstream 的对象 out,它是一个输出流对象。 (2)使用 open 函数打开文件,也就是使某一文件与上面定义的流对象建立关联。 例如: out.open(“test.dat”,ios::out); 表示调用成员函数 open,使文件流对象 out 与文件 test. dat 建立关联,即打开磁盘文件 test. dat,并指定它为输出文件,文件流对象 out 将向磁盘文件 test. dat 输出数据。ios: out 表示以输出方式打开一个文件。 以上是打开文件的一般操作步骤。
  • 实际上,由于文件的输入输出方式参数有默认值,对于类 ifstream,文件打开方式的默认值为 ios::in;而对于类 ofstream,文件打开方式的默认值为ios::out。 因此,上述语句通常可写成: out.open(“test-dat”);
  • 当一个文件需要用两种或多种方式打开时,可以用“位或”操作符(即“I”)把几种方式组合在一起。例如,为了打开一个能用于输人和输出的二进制文件,则可以采用以下方法打开 文件: fstream mystream; mystream.open("test.dat",ios::in | ios:iout | ios::binary);
  • 在实际编程时,还有一种打开文件的方法,即在定义文件流对象时指定参数,通过调用文件流类的构造函数来实现打开文件的功能,例如: ofstream out(“test.dat”); 因为定义文件流类 ifstream.ofstream 与 fstream 的对象时,都能自动打开文件流类的构造函数,这些构造函数的参数及默认值与 open 函数的完全相同。这是打开一个文件的最 常见的形式,使用起来比较方便。以上打开文件的语句相当于: ofstream out; out.open(“test.dat”);
  • 文件打开操作失败,与文件相联系的流对象的值将是0
if(!out)
{
	cout<<"cannot open file!\n"
	//错误处理代码
}
文件的关闭

close函数
是流类中的成员函数,不带参数
out.close();

文件的读写

文本文件的读写
#include <iostream>
#include <fstream>
using namespace std;

int main() {
	ofstream fout("f1.dat", ios::out);	//ios::out可以省略,默认是ios::out
	if (!fout) 
	{	cout << "Cannot open output file\n" ;
		return 1;
	}
	fout <<10<<" "<<123.456<<" "<<"this is a text file.\n";
	fout.close();
	return 0;
}
  • 在VC++6.0环境下运行时,如果在本例中 采用带后缀.h的头文件,必须包括头文件fstream.h,iostream.h自动包含在fstream.h
二进制文件的读写

前面已经介绍,文件可分为文本文件和二进制文件。文本文件又称 ASCII 文件,它的每个字节存放一个 ASCII 代码,代表一个字符。二进制文件则是把内存中的数据,按其在内存中的存储形式原样写到磁盘上存放。
最初设计流的目的是用于文本,因此在默认情况下,文件用文本方式打开。这就是说,在输入时,回车和换行两个字符(十进制数13 和 10)要自动转换为换行符“\n”(十进制数 10);在输出时,回车符“\n”(十进制数 10)自动转换为回车和换行两个字符(十进制数 13 和10)。这些转换在二进制数方式下是不进行的。
对于二进制文件的操作也需要先打开文件,操作结束后要关闭文件。在打开文件时要用“ios::binary”指定为以二进制数形式传送和存储。
对二进制文件进行读写有两种方式,其中一种使用的是函数 get 和 put,另一种使用的是函数 read 和 write。这 4 种函数也可以用于文本文件的读写。在此主要介绍对二进制文件的读写。除字符转换方面略有差别外,文本文件的处理过程与二进制文件的处理过程基本相同。

get函数:输入流类istream中定义的成员函数,可以从与流对象连接的文件中读取数据,每次读取一个字节
put函数:输出流类ostream中定义的成员函数,可以向与流对象连接的文件中写入数据,每次写入一个字节

有时需要读写一组数据(如一个结构变量的值),为此 C++提供了两个函数 read 和write,用来读写一个数据块,这两个函数最常用的调用格式如下:

inf.read(char *buf,int len)
teutf write(const char *buf,int len)

read 是流类 istream 中的成员函数,有两个参数:第 1 个参数 buf 是一个指针,它指向读入数据所存放的内存空间的起始地址;第 2 个参数len 是一个整数值,它是要读入的数据的字节数。read 函数的功能是。从与输入文件流对象 inf 相关联的磁盘文件中,读取len 个字节(或遇 EOF 结束),并把它们存放在字符指针 buf 所指的一段内存空间内。如果在 len个字帝(字符)被读击之前就达到了文作尾,则 read 函数停止执行。
write 是流类 ostream 的成员函数,参数的含义及调用注意事项与 read 函数类似。write 函数的功能是:将字符指针 buf 所给出的地址开始的 len 个字节的内容不加转换地写到与输出文件流对象 outf相关联的磁盘文件中。
注意:第1个参数的数据类型为char*如果是其他类型的数据,必须进行类型转换,

int array[]={50,60,70};
read((char*)& array,sizeof (array));
检测文件结束

在文件结束的地方有一个标志位,记为 EOF(end of file)。采用文件流方式读取文件时,使用成员函数 eof(),可以检测到这个结束符。如果该函数的返回值非零,表示到达文件尾:返回值为零,表示末到达文件尾。

该函数的原型是:
int eof();
用法示例
ifstream ifs;
...
if(!ifs.eof())
...
还有一个检测方法就是检查该流对象是否为零,为零表示文件结束
ifstream ifs;
...
if(!ifs)
...

为真。

二进制数据文件的随机读写

前面介绍的文件操作都是按一定顺序进行读写的,因此称为顺序文件。对于顺序文件而言,只能按实际排列的顺序,一个一个地访问文件中的各个元素。为了增加对文件访问的灵活性,C++系统总是用读或写文件指针记录文件的当前位置,在类 istream 及类 ostream中定义了几个与读或写文件指针相关的成员函数,可以在输入输出流内随机移动文件指针,
从而对文件的数据进行随机读写。
类 istream 提供了 3 个成员函数来对读指针进行操作,它们是:

函数功能
tellg()返回输入文件读指针的当前位置
seekg(文件中的位置)将输入文件中读指针移到指定的位置
seekg(位移量,参照位置)以参照位置为基准移动若干字节

函数参数中的“文件中的位置”和“位移量”都是 long 型整数,以字节为单位。位移量为负数则向前(开头)移动,为正数向后
“参照位置”可以是下面的三者之一:

函数功能
ios::beg从文件开头计算要移动的字节数
ios::cur从文件指针的当前位置计算要移动的字节数
ios::end从文件末尾计算要移动的字节数

类ostream 提供了 3 个成员函数来对写指针进行操作,它们是:

函数功能
tellp()返回输出文件写指针的当前位置
seekp(文件中的位置)将输出文件中写指针移到指定的位置
seekp(位移量,参照位置)以参照位置为基准移动若干字节

函数seekg和函数seekp的第2个参数可以省略,此时默认为ios::beg
如果是既可以输入又可以输出的文件,可以任意用seekp和seekg

6 命名空间和头文件命名规则

1 命名空间

一个大型软件通常是由多个模块组成的,这些模块往往是由多人合作完成的,不同的人分别完成不同的模块,最后组合成一个完整的程序。假如不同的人分别定义了函数和类,放在不同的头文件中,在主文件需要用这些函数和类时,就用#include 命令行将这些头文件包括进来。由于各头文件是由不同的人设计的,有可能在不同的头文件中用了相同名字定义的函数或类。这样在程序中就会出现命名冲突,引起程序出错。另外,如果在程序中用到第三方的库,也容易产生同样的问题。为了解决这一问题,ANSI C++引人了命名空间,用来处理程序中常见的同名冲突问题。所谓命名空间,实际上就是一个由程序设计者命名的内存区域。程序设计者可以根据需要指定一些有名字的命名空间,将各命名空间中声明的标识符与该命名空间标识符建立关联,保证不同命名空间的同名标识符不发生冲突。声明命名空间的方法很简单,下面的代码就是在命名空间 NS 中定义了两个简单变量 i和j:

namespace NS
{
	int i=5;
	int j=10;
}

其中,namespace 是定义命名空间所必须写的关键字,
NS 是用户自己指定的命名空间的名字,
花括号内是命名空间的作用域。
声明了命名空间后,就可以解决名字冲突的问题。
C++中命名空间的作用类似于操作系统中的目录和文件的关系,由于文件很多,不便管理,而且容易重名,于是人们设立若干子目录,把文件分别放到不同的子目录中,不同子目录中的文件可以同名。调用文件时应指出文件路径。
除了用户可以声明自己的命名空间外,C++还定义了一个标准命名空间 std。在本书的各章节程序中,我们经常使用语句:
using namespace std;其含义就是使用标准命名空间 std。
std(standard 的缩写)是标准 C++指定的一个命名空间,标准 C++ 库中的所有标识符都是在这个名为 std 的命名空间中定义的,或者说标准头文件(如 iostream)中的函数、类、对象和类模板是在命名空间 std 中定义的。如果要使用输入输出流对象(如 cin,cout),就要告诉编译器该标识符可在命名空间 std 找到。

//其方法有两种,一种是像本书前面章节中所写的程序一样,在源文件中使用“using namespace std;"语句。例如:
#include<iostream>
using namespace std;
int main()
{
}
另一种是在该标识符前加命名空间和作用域标识符::
#include<iostream>
int main()
{	std::cout<<"hello"<<endl;
	return 0;
}

说明:
当前使用的 C++库大多是几年前开发的,由于 C++的早期版本中没有命名空间的概念,库中的有关内容也没有放在 std 命名空间中,因而在程序中不必对 std 进行声明。这也是目前有的程序中没有使用“using namespace std,”语句的原因。但是,用标准的 C++ 编程是应该对命名空间 std 的成员进行声明或限定的(可以采用前面介绍过的任一种方法)。

2 头文件命名规则

由于 C++是从 C 语言发展而来的,为了与 C兼容,C++保留了 C语言中的一些规定。例如,在C语言中头文件用“.h”作为后缀,如 stdio, h,math. h 等。为了与C语言兼容,许多C++早期版本的编译系统头文件都是采用“*.h”形式,如 iostream. h 等。但后来 ANSI C++建议头文件不带后缀“:h”。近年推出的 C++编译系统新版本则采用了 C++的新方法,头文件名不再有后缀“.h”,如 iostream.cmath 等。但为了使原来编写的 C++程序能够运行,在 C++程序中使用头文件时,既可以采用 C++中不带后缀的头文件,也可以采用C语言中带后缀的头文件。

1.带后缀的头文件的使用
在 C语言程序中头文件包括后缀“.h”,如 stdio.h、string. h 等。由于 C 语言没有命名空间,头文件不存放在命名空间中,因此在 C++程序中,如果使用带后缀“.h”的头文件,不必用命名空间。只需在文件中包含所用的头文件即可。如:
#include<stdio.h>
2.不带后缀的头文件的使用
C++标准要求系统提供的头文件不包括后缀“.h”,例如 string.iostream。为了表示C++与 C 语言的头文件既有联系又有区别,C++所用的头文件不带后缀字符“.h”,而是在C语言的相应的头文件名之前加上前缀字符 c。例如,C语言中的头文件 stdio. h,在 C++中相应的头文件为cstdio,C语言中的头文件string. h,在 C++中相应的头文件为cstring
使用C++中不带后缀“.h”的头文件,需要在程序中声明命名空间std

使用头文件的两种方法是等价的,可以任意选用。

8.STL标准模板库

1.容器、算法和迭代器的基本概念

泛型程序库,所有组件由模板构成
所有C++编译器和操作系统都适用
容器、迭代器和算法是STL的三个基本组成部分
STL容器是对象的集合,包括vector、list、stack、queue、deque、set、map等
STL算法是对容器进行处理的函数,包括copy、sort、merge、search等
迭代器就像指向容器中对象的指针,STL算法通过迭代器在容器上进行操作

2.容器

1.vector容器

vector容器与数组类似,包含一组地址连续的存储单元

#include <iostream>
#include <vector>
using namespace std;
int main() {
	vector<int> number;	//容器类型<类型>容器名
	number.insert(number.begin(), 99);
	number.insert(number.begin(), 98);
	number.insert(number.end(), 97);
	number.push_back(669);
	cout << "删除前: " << endl;
	for (int i = 0; i < number.size(); i++)
		cout << number[i] << endl ;
	number.erase(number.begin());
	cout << "删除后: " << endl;
	for (int i = 0; i < number.size(); i++)
		cout << number[i] << endl ;
	return 0;
insert()函数

insert()函数有三种用法:

  1. iterator insert(iterator loc,const TYPE &val);在指定位置loc前插入值为val的元素,返回指向这个元素的迭代器
  2. void insert(iterator loc,size_type num,const TYPE &val);在指定位置loc前插入num个值为val的元素
  3. void insert(iterator loc,input_iterator start,input_iterator end);在指定位置loc前插入区间[start,end)的所有元素
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
	vector<int> intVector;
	for(int i=0;i<10;i++)
		intVector.push_back(i+10);
	vector<int>::iterator thrIterator=intVector.begin();//定义迭代器
	cout<<"vector的内容1:"<<endl;
	for(thrIterator=intVector.begin();thrIterator!=intVector.end();thrIterator++)
		cout<<* theVector<<" ";//使用迭代器显示vector的内容
	intVector.insert(theIterator,4,5);//插入4个5到vector中
	cout<<"vector的内容2:"<<endl;
	for(thrIterator=intVector.begin();thrIterator!=intVector.end();thrIterator++)
		cout<<* theVector<<" ";
	return 0;
}

本程序演示如何使用 vector 容器中的函数 push_back 等的用法,“intVector. push_back(i+10);"将 i+10 添加到 vector 末尾;“intVector. insert( thelterator,4,5);”将5 添加到 vector 末尾。

  • push_back 函数的功能是添加一个元素到 vector 末尾。
  • push_back 函数的语法是 void push_back(const TYPE &val);
  • pop_back 函数的功能是删除当前 vector 最后的一个元素。
  • pop_back 函数的语法是: void pop back();
  • vector 中的提供的 at()函数负责返回指定位置的元素。与数组运算符[]相比,at()函数更加安全,不会访问 vector 内越界的元素。
  • vector 中提供的 erase()函数用于删除元素,erase ()函数有两种用法:
    第一种的语法是:
  • iterator erase(iterator loc); 其功能是删除指定位置 loc的元素。例如:
    number.erase(number.begin());
    第二种的语法是:
  • iterator erase(iterator start, iterator end); 其功能是删除区间[start,end)的所有元素,而返回值是指向删除的最后一个元素的下一位
    置的迭代器。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
	unsigned int i;
	vector<int>number;//定义存放整型数据的 vector 容器
	number.insert(number.begin(),99);//头插元素 99
	number.insert(number.begin(),98);//头插元素 98
	number.insert(number.end(),97);//尾插元素 97
	cout<<"删除前:"<<endl;
	for (i=0;i<number.size();i++//显示容器中的元素
		cout<<number.at(i)<<endl;
	number.erase(number.begin());//删除第一个元素
	number.erase(number.begin());//再删除第一个元素
	cout<<"删除后:"<<endl;
	for (i=0;i<number.size();i++//显示容器中的元素
	cout<<number.at(i)<<endl;
	return 0;
}
vector 的构造函数

vector 构造函数的使用方式有四种:

  • vector(); 无参数,构造一个空的 vector;
  • vector(size_type num, const TYPE &val); 两个参数,构造一个由参数 num 表示个数,参数 val 表示值的 vector;
  • vector(const vector &from); 一个参数,构造一个与参数 from 相同的 vector;
  • vector(input_iterator start, input_iterator end); 两个参数,构造一个值取自迭代器的 vector,开始位置和终止位置由参数指定。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
	unsigned int i;
	vector<int>number(5,99);//构造 number,5 个元素,值均为 99
	for (i=0;i<number.size();i++)//显示容器中的元素
		cout<<number.at (i)<<" ";
	cout<<endl:
	vector<int>numberl(number);//构造 numberl,拷贝 number 容器中的 5 个元素,值均为 99
	for (i-O;i<number1.size();i++)//显示 number1 容器中的元素
		cout<<numberl.at(1)<<" ";
	cout<<endl;
	vector<int>number2(number.begin(),number.end());//构造 number2,拷贝 number 容器中的 5 个元素,值均为 99
	for (i-O;i<number2.size();i++)//显示 number2 容器中的元素
		cout<<number2.at(i)<*”";
	cout<<endl;
	return o;
}
访问 vector 信息

访问 vector 信息的函数有 max_size、size()、capacity()和 empty()。
max_size返回vector 可以最多容纳元素的数量
size()返回 vector 当前元素的数量
capacity()返回vector 所能容纳的元素数量(在不重新分配内存的情况下);
empty()判断 vector 是否为空,为空时返回 TRUE,否则返回 FALSE。

存取 vector 信息

存取 vector 信息可以使用 构造函数、push_back()、insert()、数组运算符、赋值运算符、pop_back()、erase()、begin()、end()、rbegin、rend()、size、maxsize 等。

关于运算符

针对 vector,可以使用的运算符包括:

  • 标准运算符: == != <= >= < >
  • vectors 之间大小的比较是按照词典规则,要访问 vector 中的某特定位置的元素可以使用[]操作。
  • 如果两个 vector 具有相同的容量,所有相同位置的元素相等,则这两个 vector被认为是相等的。
  • 注意:如果 vector 用来存储用户自定义类的对象,必须重载“==”和“<”运算符。
sort函数

sort(number.begin,number.end()); 用于排序,从小到大

2.list容器

逆转函数reverse
#include <iostream>
#include <list>
using namespace std;
int main() 
{
	list<int> number;//定义存放整型数据的list容器
	list<int>::iterator numberIterator;//定义迭代器
	number.insert(number.begin(),99);//头插元素 99
	number.insert(number.begin(),98);//头插元素 98
	number.insert(number.end(),97);//尾插元素 97
	cout << "链表内容:" << endl;
	for (numberIterator= number.begin(); numberIterator!= number.end(); ++numberIterator)//使用迭代器显示链表内容
		cout << *numberIterator<< endl;
	number.reverse();//逆转链表
	cout << "逆转后的链表内容:" << endl;
	for (numberIterator= number.begin(); numberIterator!= number.end(); ++numberIterator)//使用迭代器显示链表内容
		cout << *numberIterator<< endl;
	

	return 0;
}
STL通用算法for_each()遍历
#include <iostream>
#include <list>
#include <string>
#include <algorithm>
using namespace std;
void PrintLine(string& StringLine)
{
	cout<<StringLine<<endl;
}
int main(void) 
{
	list<string> Food;//定义存放整型数据的list容器
	Food.push_back("---食物清单---");	
	Food.push_back("  牛奶");	
	Food.push_back("  草莓");	
	Food.push_back("  香蕉");	
	Food.push_back("-------------");
	for_each(Food.begin(),Food.end(),PrintLine);
	return 0;
}

Food.begin()表示起始位置,Food.end()表示末尾位置,PrintLine是自编的函数,负责输出链表的内容

STL通用算法count()统计
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;
int main(void) 
{
	list<int> Scores;
	Scores.insert(Scores.begin(),100);	
	Scores.insert(Scores.begin(),80);	
	Scores.insert(Scores.begin(),45);	
	Scores.insert(Scores.begin(),100);	
	Scores.insert(Scores.begin(),75);	
	Scores.insert(Scores.begin(),99);	
	Scores.insert(Scores.begin(),100);	
	int number(0);
	number=count(Scores.begin(),Scores.end(),100);
	cout<<number<<"个"<<endl;
	return 0;
}

count()算法负责统计与给定值相等的对象的个数

STL通用算法count_if()统计
#include <iostream>
#include <list>
#include <string>
#include <algorithm>
using namespace std;
const string FlashDriveCode("0003");//代码0003表示16GBU盘
class IsFlashDrive
{
	public:
	bool operator() (string& SalesRecord)
	{
		return SalesRecord.substr(0,4)==FlashDriveCode;//比较两个字符串的前四位
	}
};
int main(void) 
{
	list<int> SalesRecord;
	SalesRecord.insert(Scores.begin(),"0001 4GB");	
	SalesRecord.insert(Scores.begin(),"0003 16GB");	
	SalesRecord.insert(Scores.begin(),"0002 8GB");	
	SalesRecord.insert(Scores.begin(),"0003 16GB");	
	SalesRecord.insert(Scores.begin(),"0004 64GB");	
	SalesRecord.insert(Scores.begin(),"0003 16GB");	
	int number(0);
	number=count_if(SalesRecord.begin(),SalesRecord.end(),IsFlashDrive());
	cout<<number<<"个"<<endl;
	return 0;
}

IsFlashDrive()返回为true时增加number的值

STL通用算法search算法进行定位
#include <iostream>
#include <list>
#include <string>
#include <algorithm>
using namespace std;
int main(void) 
{
	list<char> Target;
	list<char> List;
	Target.push_back('l');	
	Target.push_back('l');	
	List.push_back('i');	
	List.push_back('n');	
	List.push_back('l');
	List.push_back('l');
	list<char>::iterator PositionOfNull=search(List.begin(),List.end(),Target.begin(),Target.end())//使用迭代器作查询
	if(PositionOfNull!=list.end())
		cout<<"找到"<<endl;
	else
		cout<<"未找到"<<endl;
	return 0;
}

search算法是一种定位算法,负责在一个序列中找另一个序列第一次出现的位置,位置为指针所指,查找成功会返回一个指向List中序列匹配的第一个字符的iterator,否则返回List.end()的值

list的构造函数

list构造函数的使用方式有四种:

  • list(); 无参数,构造一个空的 list;
  • list(size_type num, const TYPE &val); 两个参数,构造一个由参数 num 表示个数,参数 val 表示值的 list;
  • list(const vector &from); 一个参数,构造一个与参数 from 相同的 list;
  • list(input_iterator start, input_iterator end); 两个参数,构造一个值取自迭代器的 list,开始位置和终止位置由参数指定。
访问 list信息

访问 vector 信息的函数有 max_size、size()、empty()。
max_size返回list可以最多容纳元素的数量
size()返回 list当前元素的数量

empty()判断 list是否为空,为空时返回 TRUE,否则返回 FALSE。

存取 list信息

插入元素:insert函数,用法参照vector的方法
删除元素:remove函数,函数原型是void remove(const T& x);

3.容器适配器

容器适配器是C++提供的三种模板类,与容器相结合,提供 栈、队列、优先队列的功能

  • 栈是一种访问受限的容器,只允许在存储器的一端进行插入和删除,并且符合后进先出的规则。
  • 做插入和删除操作的这一端称为栈顶,另一端则称为栈底。
  • 插人操作称为进栈或入栈。删除操作则称为退栈或出栈。
  • 在 STL 中,栈是以其他容器作为内部结构的,STL 提供了接口。
  • 栈的基本操作包括:
  • 判栈空 empty()、返回栈中元素的个数 size()、退栈(不返回值)pop()、取栈顶元素(不删除栈顶元素)top()、进栈 push()。

使用栈可以解决很多实际问题,例如我们要解决将一个十进制数转换为八进制数输出的问题。通过手工计算,不难发现,每次除以 8 以后的余数是需要输出的结果,只不过,输出的顺序与得到的余数的次序刚好相反,所以可以考虑将每次得到的余数压入栈中。输出的时候依次退栈并输出栈顶元素。

#include <iostream>
#include <stack>
using namespace std;
int main ()
{
	stack<int>mystack;//定义整型栈
	int num=100,temp;
	cout<<num<<"的八进制是:";
	while (num)//num 不为零
	{
		mystack.push(num% 8);//将 num% 8 进栈
		num=num/8;//num 整除 8
	}
	while (!mystack.empty())//栈不空时
	{ 	temp=mystack.top();//取栈顶元素
		mystack.pop();//退栈
		cout<<temp;
	}
	cout <kendl;
	return 0;
}
程序运行结果:
100 的人进制数是:144
队列
  • 队列也是一种访问受限的容器,只允许在存储器的两端进行插入和删除,并且符合先进先出的规则。
  • 做插入操作的这一端称为队尾,另一端则称为队头。
  • 插入操作称为进队列,删除操作则称为出队列。
  • 在 STL 中,队列是以其他容器作为内部结构的,STL 提供了接口。
  • 队列的基本操作包括:
  • 判队列空 empty()、返回队列中元素的个数 size()、出队列(不返回值)pop()、取队头元素(不删除队头元素)front()、进队列(在队尾插入新元素)push()、返回队尾元素的值(不删除该元素)back()。
#include <iostream>
#include <queue>
using namespace std;
int main ()
{
	queue<int>myqueue;//定义整型队列
	for (int i=3;i<=21;i=i+3)//进队列
	myqueue.push(i);
	while (!myqueue.empty())//队列不为空
	{
		cout<<myqueue.front()<<" ";
		myqueue.pop();//出队列
	}
	cout <<endl;
	return 0;
}
程序运行结果:
3 6 9 12 15 18 21
优先队列
  • 优先队列是一种特殊的队列。
  • 优先队列容器也是一种从一端进队、从另一端出队的队列。
  • 与普通队列不同,队列中最大的元素总是位于队头位置,因此,优先队列并不符合先进先出的要求,出队时,是将队列中的最大元素出队。
#include <iostream>
#include <functional>
#include <queue>
#include <cstdlib>
#include <ctime>
using namespace std;
int main ()
{
	const int Size=6;
	int i;
	priority_queue<int>nums;//定义整型优先队列
	srand((unsigned)time(0));
	for (i=O;i<Size;i++)	//产生 Size 个随机数进优先队列
	{
		int temp=rand();
		cout<<temp<<endl;
		nums.push(temp);	//压入优先队列
	}
	cout<<"优先队列的值:"<<endl;
	for (i=O;i<Size;i++)//size个数出优先队列
	{
		cout<<nums.top()<<endl;
		nums.pop();
	}
	cout<<endl;
	return 0;
}
  • 优先队列的基本操作包括:
  • 判队列空 empty()、返回队列中元素的个数 sitze()、出队列(不返回值)pop()、进队列(在队尾插入新元素)push()、返回优先队列队头元素的值(不删除该元素)top()
以优先队列为例,总结容器适配器的用法如下。
1.构造函数

priority_queue()是默认的构造函数,创建一个空的 priority_queue 对象。

2.拷贝构造函数

priority_queue(const priority_queue&)
用一个优先队列对象创建新的优先队列对象。

3.进队

void push (const value_type&)
进队的元素x移至队列中的正确位置,保证队列优先级高的元素始终位于队首。
push 函数无返回值。

4.出队

void pop()
将优先级最高的元素删除。

5.取队头元素

const value_type& top() const
优先队列容器提供的是取队头元素的函数,而并不提供获取队尾元素的函数。

6.判队列空

bool empty()
判断优先队列是否为空。

4.deque 容器

  • deque 是双端队列,是一种放松了访问限制的队列。
  • 对于普通队列,只能从队尾插入元素,从队头删除元素;而在双端队列中,队尾和队头都可以插入元素,也都可以删除元素。
  • 其实,deque 与 vector 是类似的,只不过,deque 内部的数据机制和执行性能与 vector 不同,如果考虑到容器元素的内存分配策略和操作的性能,deque 相对 vector 较为有优势。
deque 的构造函数

deque 构造函数的使用方式有四种:

  • deque ();
  • deque(size_type num, const TYPE &val);
  • deque (const vector &from);
  • deque(input_iterator start, input_iterator end);
访问 deque 信息
  • 访问 deque 信息的函数有 max_size、size()和 empty()
  • max_size 返回 deque 可以最多容纳元素的数量;
  • size()返回 deque 当前元素的数量;
  • empty()判断 deque 是否为空,为空时返回 TRUE,否则返回 FALSE。
存取 deque 信息

存取 deque 信息可以使用 构造函数、push_back()、push_front()、insert()、数组运算符、赋值运算符、pop_back()、pop_front()、erase()、begin()、end()、rbegin、rend()、size、maxsize 等。

#include <iostream>
#include <functional>
#include <queue>
#include <cstdlib>
#include <ctime>
using namespace std;
int main ()
{
	const int Size=6;
	unsigned int i;
	deque<int>nums;//定义整型deque容器
	srand((unsigned)time(0));
	for (i=O;i<Size;i++)	//产生 Size 个随机数进双端队列
	{
		int temp=rand();
		cout<<temp<<endl;
		nums.push(temp);	
	}
	cout<<双端队列的值:"<<endl;
	for (i=O;i<nums.size();i++)//数组方式访问
	{
		cout<<"nums["<<i<<"]="<<nums [i]<<endl;
	}
	cout<<endl;
	return 0;
}

5 set,multiset,map 和 multimap 容器

C++ STL不仅提供 vector string 和 list 等方便的容器,还提供了 set、multiset、map和multimap 容器。这些都是关联式容器。

set 关联式容器
  • set 作为一个容器可以用来存取相同数据类型的数据,最主要的是 set中每个元素的值必须唯一,而且系统能根据元素的值自动进行排序。
#include<iostream>
#include<set>
#include<string>
using namespace std;
int main()
{
	set<string>s;//定义字符串型 set 容器
	s.insert("linxiaocha");//插人字符串到set容器中
	s.insert("chenweixing");
	s.insert("gaoying");
	s.insert("chenweixing");
	set<string>::iterator myit;//使用迭代器访问每个元素
	for(myit=s.begin(); myit!=s.end();++myit)
		cout<<*myit<<endl;
	cout<<endl;
	return 0}
map关联式容器
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main({
	map<string,int>s;//定义 map 容器
	s["Monday"]=1;//设置适当的值	
	s["Tuesday"]=2;
	s["wednesday"]=3;
	s["Thurday"]=4;
	s["Friday"]=5;
	s["Saturday"]=6;
	s["Sunday"]=7;
	cout<<s["Wednesday"]<<endl;
	cout<<s["Sunday"]<<endl;
	cout<<endl;
	return 0;
}
程序运行结果:
3
7
  • 程序将英文的星期一到星期天与数字 1~7对应。这就是 map 的特性。
  • map<string,int>表示一个字符串与一个数字对应。这里,字符串是关键字,每个关键字只能在 map中出现一次,整数是对应关键字的值。
  • 这个特性为编程处理一对一数据,提供了方便。
    限于篇幅,在此只简单介绍 set 和 map 的概念。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值