C++ 2:短裤

C++ 短裤

  • 1. 引用
    • 1.1 定义
    • 1.2 应用:引用作为函数返回值
    • 1.3 常引用
    • 1.4 const 关键字
  • 2. 动态内存分配
    • 2.1 new运算符
    • 2.2 delete 运算符
  • 3. 内联函数 函数重载 函数缺省参数
    • 3.1 内联函数
    • 3.2 函数重载
    • 3.3 函数的缺省值
    • 使用类的成员变量和成员函数
    • 类的定义与类的成员函数可以分开学
    • 类成员(变量或函数)的可访问范围
    • 成员函数的重载和缺省
    • 构造函数(constructor) & 析构函数(destructor)
  • this 指针
  • 静态成员
    • 1. 普通成员与静态成员区别
    • 2. 访问静态成员
    • 3. 总结:
    • 4. 注意:
  • 成员对象和封闭类
  • 常量对象 常量成员 常引用
  • 友元(friends)
  • 第四周 运算符重载(Operator overloading)
    • 4.1 基本运算符重载
    • 4.2 赋值运算符重载
    • 4.3 运算符重载为友元函数
    • 4.4 可变长数组的实现
    • 4.5 流插入运算符(">>")和流提取运算符的重载("<<")
    • 4.6 类型转换运算符的重载
    • 4.7 自增自减运算符的重载
    • 4.7 TIPS
  • 5. 继承
    • 5.1 继承(派生)
    • 5.2 继承关系和派生关系
    • 5.3 覆盖和保护成员
    • 5.4 派生类的构造函数
    • 5.5 公有继承的赋值兼容规则
  • 6. 多态
    • 6.1 虚函数和多态
    • 5.3 多态的实现原理
    • 5.4 虚析构函数、纯析构函数、抽象类
  • 7. 输入输出模板
    • 7.1 输入输出流相关的类
    • 7.2 流操作算子控制输出格式
    • 7.3 文件读写
    • 7.4 函数模板
    • 7.5 类模板
    • 7.6 类模板与派生、友元和静态成员变量

1. 引用

1.1 定义

  • r引用n,r等价与n(别名)
  • r就是n,而不是n的副本,对r修改,n也跟着改变

类型名 & 引用名 = 某变量名

  • 只能引用变量,不能引用常量和表达式
int n = 4;
int& r = n; 
// r 引用了 n, r 的类型为 int&

1.2 应用:引用作为函数返回值

  • 可以对函数赋值,??????

1.3 常引用

  • 定义引用时,前面加const关键字
  • 单向的,r引用n后,只能通过r访问n,而不能修改n
const int& r = n
// r 的类型为 : const int&
  1. 不能通过常引用修改,引用内容(即不能通过重新给r赋值,来修改n,会编译报错)
    C++ 2:短裤_第1张图片C++ 2:短裤_第2张图片

  2. const int&int&不同的类型

  • const int& r = int& n 或 const int& r = int n
  • int& r = const n 或 int& r = const int& n (必须强制类型转换)

1.4 const 关键字

  • const(=constant): In computer programming, a constant is an identifier with an associated value which cannot be altered by the program during normal execution
  1. 定义量、引用

const int r = 50;
const string r = "O,Fuck"
const int& r = n;

  1. 常量指针

const int* p = & n

  • 不能通过常量指针修改其指向的内容(只读)
  • 常量指针的指向可以变化(定义后,可以重新初始化)
  • 可以把非常量指针赋值给常量指针,反过来不行(同1.3 -2:总结起来就是:非const可以赋值给const,但const不能赋值给别人,必须强制类型转换。换言之,const不允许被传递,防止传了几手之后,被间接修改
  • 将函数参数写为常量指针,可以防止函数内部修改参数所指的内容

2. 动态内存分配

2.1 new运算符

  • new运算符的返回值类型为:指针(指向分配空间起始地址)
  1. 分配一个变量的空间
int* p;
p = new int;
  • 动态分配出一个内存大小为sizeof(T)字节的存储空间,并将该内存空间的起始地址赋值给指针p
  1. 分配一个数组的空间

P = new T[N]

  • 分配一个 N*sizeof(T) 字节的存储空间,p指向起始地址

2.2 delete 运算符

  • 只能 deletenew出来的空间(p必须指向new出来的空间)
  • delete只能做一次,如果对一块new出来的空间delete两次,会导致异常(编译没问题,但可能误删数据)
delete对象 用法 备注
释放变量空间 delete p;
释放数组空间 delete [ ] p; p无须指向起始地址,可指数组空间中任意地址

3. 内联函数 函数重载 函数缺省参数

3.1 内联函数

  • 函数语句少时,调用时间显得大
  • 减少函数调用开销
  • 跳过函数调用,直接执行函数体(将函数体直接插到调用函数处)
  1. 定义: inline 关键字
inline int fun(int a, int b){
	...
}

3.2 函数重载

  • 名字相同,参数表(参数个数,参数类型…)不同的函数之间构成,重载关系
  1. 目的
    重名不用怕,编译器通过参数就知道调用哪个函数
  • error:二义性错误
    C++ 2:短裤_第3张图片

3.3 函数的缺省值

  • 缺省值:就是默认值(Default),定义函数时给某些参数提前初始化,如果调用时不写这些参数,就使用缺省值
  1. 使用
  • 缺省参数,统一放到参数表后面
  • 调用函数时,不能在中间缺省参数(前后都有传递参数)
void fun(int a, int b = 2, int c = 3){
	...
}
fun(10); == fun(10, 2, 3);
fun(10, 8); == fun(10, 8, 3);
fun(10, , 8);  != fun(10, 2, 3)
  1. 目的
  • 提高程序的可扩充性,升级更新程序时,如果不传新的参数值,就可以保持原来的默认参数值

  • 数据结构(抽象事物的属性/数据) + 函数(抽象事物的行为/方法)
  • 类的名字就是一种自定义的类型,(class X ,X就是一个自定义的类型,像 int…一样)

C++ 2:短裤_第4张图片

  • 对象所占空间大小等于所有成员变量大小之和,只包含成员变量,不包含成员函数(不占空间)

使用类的成员变量和成员函数

  1. 用法 :

①直接:
对象.成员变量(或函数)
②指针:
先定义“ 类型”的指针指向对象, 再用:指针->成员变量(或函数)

ClassName object;
ClassName * point = & object;
p -> variable = 5;
p -> reference;

③引用:
先定义“类& 类型”的引用,将对象o“重命名”,再通过引用.成员变量(或函数)调用

ClassName object;
ClassName & reference = object;
reference.variable= 5;
reference.function(....);

类的定义与类的成员函数可以分开学

方法:

class ClassName{
	...
};
int ClassName::FunName(){
	...
}
  • ClassName::FunName() : 类的成员函数
  • 不能直接调用,必须通过对象调用的方法(①②③)才能调用

类成员(变量或函数)的可访问范围

  1. 关键字 privatepublicprotected
关键字 作用 备注
private 私有成员,只能在成员函数内被访问
public 公有成员,可在任何地方被访问
protected 保护成员,***空空空空***
  1. 使用:
class ClassName{
	private:
		int a;
	public:
		int b;
		void f1(int c, ...);
	...
};
  • 如果没有这三个关键字,缺省地(= 默认)被认为是私有成员
class ClassName{
	int a;
	...
};
int main(){
	ClassName obj;
	printf("%d", obj.a);			//error
}

在这里插入图片描述

class CEmployee{
	private:
		char szName[30]; 	//名字
		void f();
	public:
		int salary;	//工资
		void setName(char * name);
		void getName(char * name);
		void averageSalary(CEmployee e1,  CEmployee e2);
};
void CEmployee::setName(char * name){
	strcpy(szName, name);		//①
}
void CEmployee::getName(char * name){
	strcpy(name, szName);		//①
}
void CEmployee::averageSalary(CEmployee e1, CEmployee e2){
	cout << e1.szName;		//②
	salary = (e1.salary + e2.salary) / 2;
}
void CEmployee::f(){
	...
}

int main(){
	CEmployee e;
	strcpy(e.szName, "Tom1234567890");		//③
	e.f();		//③
	e.setName("Tom");
	e.salary = 5000;
	return 0;
}
  • ① OK:成员可以访问自己类里的成员
  • ② OK: 成员可以访问同类其他对象的私有成员(类似生物中:同物种间,不产生生殖隔离,可以xx)
  • ③ ERROR: 一个类的私有成员不可以在类的成员函数外被访问
    在这里插入图片描述在这里插入图片描述
    隐藏:设置私有成员,目的是强制对成员变量的访问一定要通过成员函数进行,
    好处:以后成员变量的类型等属性修改以后,只需更改成员函数即可。否则所有直接访问成员变量的语句都要修改。(如果private成员变量的要求变了,可以只将对应public成员函数修改(内部加一些判断语句),来拒绝了不符合“新要求”的private成员变量,这样就可以不用挨个修改不符合新要求的private成员变量了,即通过public成员函数来统一管理privtae成员变量)
    (szName[30]时private成员,在main里不能被修改,但可以(只能)通过public成员setName(char * name),来间接修改)

成员函数的重载和缺省

  1. 同函数的重载、缺省(见3.2-3.3)
  2. 避免二义性
class ClassName{
	void f(int a = 0){ 
		...
	}
	void f(){ 
		...
	}
};
int main(){
	ClassName o;
	o.f();					//error
}

构造函数(constructor) & 析构函数(destructor)

  • 成员函数的一种
  • 名字与类名相同
  • 可以有参数,但不能有返回值(void 也不行)
  • 先生成对象(存储空间),再调用构造函数初始化(先买房再装修
  1. 作用
    给对象初始化(成员变量赋初值…)
  2. 没有构造函数:
    默认生成一个无参数的构造函数(也称:默认构造函数),什么都不干
    如果定义了构造函数,就不会生成默认的构造函数
  3. 只在对象生成时自动调用,对象一旦生成,就再也不能在其上执行构造函数
  4. 一个类可以有多个构造函数(通过参数来判断调用哪个构造函数)
  5. 意义:
    执行初始化工作,不必专门再写初始化函数,也不用担心忘记调用初始化函数
  6. 对象没被初始化就使用,会导致程序出错
  • 构造函数在对象数组中的使用
    pass;

  • 复制构造函数
    只有一个参数,即对同类对象的引用(参数必须是引用)
    没有定义复制构造函数,编译器也会生成默认的复制构造函数(默认构造函数不一定存在,复制构造函数一定会存在??)
    使用:

ClassName(ClassName & o){
	...
}

常引用(一般不会去修改,故一般使用常引用):

ClassName(const ClassName & o){
	this_a = o.a;
	this_b = o.b;
	...
}
class ClassName{
	...
};
ClassName o1;		//调用缺省无参构造函数
ClassName o2(01);		//调用缺省的复制构造函数,将o2初始化和o1一样

复制构造函数起作用的三种情况:

  1. 一个对象o1初始 化另一个对象o2时:
    ClassName o2(o1); 等价于:
    ClassName o2 = o1; //初始化语句,而非赋值语句
  • 赋值语句不会调用赋值构造函数!
ClassName o1, o2;
o2 = o1;		//赋值语句,不调用赋值构造函数
  1. 某函数func()有个参数为 “class ClassName” 的对象o1,调用func()时:
void func(ClassName o1){
	...
}
int main(){
	ClassName o2;
	func(o2);		//调用 "class ClassName" 的复制构造函数
	...
}
  1. 如果一个函数的返回值是 “class ClassName” 的对象时,函数返回时:
ClassName func(){
	ClassName o;
	return o;
}
int main(){
 	cout << func().a <<endl;
 	...
}

常量引用参数的使用:

  1. 如果函数参数是 ClassName o ,调用函数是会使用 ClassName 的复制构造函数给 o 初始化,开销较大
  2. 故采用 引用类型做参数,即ClassName & o (引用不生成副本,就是原变量,换了个名字)
  3. 若希望确保传入函数的实参在函数中不被改变,加const关键字,使传入的参数只读(类似于映像,既不是副本,又不是原变量换名)

Q.为什么要自己写复制构造函数?

!类型转换构造函数:
只有一个参数,而且不是复制构造函数的构造函数,一般就可以看作是转换构造函数
当需要时,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)
目的:实现类型的自动转换

class Comples{
	public:
		double real, imag;
		Complex(int i){		//类型转换构造函数
			real = i; imag = 0;
		}
		Complex(double r, double i){
		real = r; imag = i;
};
int main(){
	Complex o1(7, 8);
	Complex o2 = 12;	//调用转换构造函数
	c1 =  9;	//9被自动转换成一个临时的Complex对象
	...
}

析构函数(destructors)

  • 在对象消亡时起作用(自动调用),对象消亡前作善后工作,比如释放分配的空间…
  • 消亡一个对象,调用一次析构函数
  • 名字与类名相同,在前面加~
  • 没有参数和返回值
  • 一个类最多只能有一个析构函数
  • 定义时没写,会自动生成缺省析构函数,(什么也不做),定义了析构函数就不再生成析构函数
  • 拆房子前,把值钱的东西都搬走
class String{
	private:
		char * p;
	public:
		String(){
			p = new char[10];
		}
		~ String();
};
String::String(){
	delete [] p;
}

delete运算会导致调用析构函数:

ClassName * p;
p = new ClassName;
delete p;		//调用析构函数
p = new ClassName[3];	// 调用构造函数3次
delete [] p;		//调用析构函数3次

tips: new一个对象数组时,要用delete [] p,否则只delete一个对象

tips: return出的临时对象使用完(执行完对应语句)也要消亡→调用析构函数

!!构造函数和析构函数什么时候被调用?

  • 先构造后析构

C++ 2:短裤_第5张图片
C++ 2:短裤_第6张图片!!复制构造函数在不同编译器中的表现:

dev c++中优化:不生成返回值临时对象

this 指针

起因:最初没有C++编译器,要先将C++翻译成C语言
C++ 2:短裤_第7张图片
Ⅰ.

class ClassName{
	int i;
	public:
		void Hello(){
			cout << "Hello" <<endl;
		}
};
int main(){
	ClassName * p = NULL;
	p->Hello();
}
输出:Hello

Ⅱ.而将成员函数Hello()改为:

Hello(){
	cout << i << "Hello" <<endl;
}

就会出错!
原因:Ⅰ中Hello()函数自带一个ClassName * this参数(类似于python里类的每个成员函数第一个参数必须是self)传进去一个指针可以执行Hello()函数,而Ⅱ中传进p以后,输出的i是p->i,p指向NULL,不存在i,所以错误

  • 应用在成员函数中
  • 作用就是指向成员函数所作用的对象
class ClassName{
	int a;
	ClassName func(){
		this->a++;
		return * this;
};
int main(){
	ClassName o1, o2;
	o2 = o1.func();
	...
}
  • 静态成员函数中不能使用this指针
  • 因为静态成员函数并不具体作用于某个对象
  • 因此,静态成员函数的真实参数的个数就是程序中写出的参数个数,而动态成员函数隐藏了一个 ClassName * this

静态成员

在定义时,前面加一个 static 关键字的成员

1. 普通成员与静态成员区别

  1. 普通成员变量与静态成员变量区别

区别Ⅰ:

成员 作用 备注
普通成员变量 每个对象有各自的一份 每个对象自己的属性
静态成员变量 所有对象共享,一共就一份 所有对象共同的属性

区别Ⅱ:
sizeof运算符不会计算静态成员变量

  1. 普通成员函数与静态成员函数区别
    成员|作用|备注
    —|---|–
    普通成员函数|必须具体作用于某个对象|
    静态成员|并不具体作用于某个对象|

2. 访问静态成员

  1. 通过类:

类名::成员名

ClassName::MemberVariable
ClassName::MemberFunction();
  1. 通过对象:

对象名.成员名

ClassName obj;
o.memberVariable;
o.MemberFunction();
//只是一种形式,静态成员函数并不作用在对象上
  1. 通过 “类 类型” 的指针:
ClassName obj;
ClassName * p = & obj;
p->MemberVariable;
p->MemberFunction();
  1. 通过对象的引用(别名):
ClassName obj;
ClassName & reference = obj;
reference.MemberVariable;
reference.MemberFunction();

3. 总结:

  1. 静态成员不需要对象就能访问(是一个类的共性)
  2. 静态成员变量就是全局变量,哪怕一个对象都不存在,类的静态成员变量也存在
    静态成员函数本质上就是全局函数,不需要作用在某个对象上
  3. 设置静态成员这种机制的目的是将和某些类紧密相关的全局变量和函数写到类里,看上去像一个整体,易于维护和理解(若一个类的静态变量写成全局变量,不容易看出全局变量与该类的关系,同时也会误以为全局变量与其他类有关系),静态成员就是一种封装

4. 注意:

  1. 在定义类的文件中对静态成员变量进行一次说明或初始化(在所有成员函数外面)。否则编译能通过,链接不能通过
  2. 在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数
  3. 将静态成员变量的变化写进构造函数和析构函数时,要注意有些是调用复制构造函数(没写就会自动生成),不调用构造函数(如形参中的对象,返回值的对象,临时对象…),而消亡时一定调用析构函数,就会导致的静态成员变量值错误 //解决办法,在类里自己编写对应的复制构造函数

成员对象和封闭类

  • 成员对象(类中有成员是其他类的对象,类间调用)的类叫 封闭(enclosing)类

任何生成封闭类对象的语句,都要让编译器明白,对象中的成员对象,是如何初始化的(即成员对象必须有相应的构造函数)
具体做法:通过封闭类的构造函数的初始化列表
初始化列表中的参数可以是任意复杂的表达式,可以包括函数,变量,只要表达式中的函数或变量有定义就行

!!封闭类构造函数和析构函数的执行顺序

  1. 封闭类对象生成时,先执行所有对象成员的构造函数,再执行封闭类的构造函数(由里向外构造,先把零件造好,在组装成机器)
  2. 对象成员的构造函数调用次序和对象成员在类中的说明次序一直,与它们在成员初始化列表中出现的次序无关
  3. 当封闭类的对象消亡时,先执行封闭类的析构函数,再执行成员对象的析构函数(由外向里,回收机器时,先拆外壳,再拆零件)

实例:
C++ 2:短裤_第8张图片C++ 2:短裤_第9张图片

!!封闭类的复制构造函数:
C++ 2:短裤_第10张图片

常量对象 常量成员 常引用

  1. 常量对象
    如果不希望某个对象的值被改变,定义该对象时可以在前面加const关键字(只读)
  2. 常量成员
  • 常量成员函数:在类的成员函数说明后面加const关键字
class ClassName{
	public:
		void function() const;
	...
};
void ClassName::function() const{
	...
}

作用:执行期间,不应修改其作用的对象
\ 不能修改成员变量的只(静态成员变量除外)
\ 不能调用同类的非常量成员函数(静态成员函数除外)
C++ 2:短裤_第11张图片\常量成员函数的重载:

a:
int f() const{
	...
}
b:
int f(){
	...
}

a和b为重载关系
调用时:
用常量对象const ClassName obj调用 f(),将会调用 int f() const //因为常量对象不能执行非常量成员函数
用对象ClassName obj调用 f(),将会执行 int f()

  1. 常引用:
    见1.3
  • 常引用经常作为函数的参数:
    对象作为函数的参数,生成该参数要调用复制构造函数(生成临时对象),效率低
    指针作为参数,代码不好看
    立即推:用对象的引用作为参数
    但是:对象引用作参数由一定风险→若函数中不小心修改了形参,实参也会被修改
    如果使用常引用,就会避免实参被修改,因为常引用时只读(不能修改原变量)的

友元(friends)

  • 友元分为友元函数和友元类
  • 朋友,可以访问私有
  1. 友元函数:
  • 可以声明有哪些非本类的成员函数,可以访问本类的私有变量(赋予访问权限)

一个类的友元函数可以访问这个类的私有成员
不是这个类的成员函数,可以是一个普通的全局函数,声明为友元后就可以访问这个类的私有成员了

#include 
using namespace std;

class CCar;		
//提前声明CCar类,以便后面的CDriver类使用
//因为CCar类和CDriver类都互相调用了对方,谁放前面都不好,故提前声明一个

class CDriver{
	public:
		void ModifyCar(CCar * pCar);		//改装汽车 
}; 

class CCar{
	private:
		int price;
	friend int MostExpensiveCar(CCar cars[], int total);	//声明友元 
	friend void CDriver::ModifyCar(CCar * pCar);			//声明友元 
};

void CDriver::ModifyCar(CCar * pCar){
	pCar->price += 1000;		//改装后汽车价值增加 
} 

int MostExpensiveCar(CCar cars[], int total){
//求最贵汽车的价格
	int tmpMax = -1;
	for(int i = 0; i < total; ++i)
		if(cars[i].price > tmpMax)
			tmpMax = cars[i].price;
	return tmpMax; 
} 

int main(){
	
	return 0;
}

两种友元函数:
普通的全局函数;
将一个类的成员函数(包括构造、解析函数)说明为另一个类的友元

class A{
	public:
		void function();
};
class B{
	friend void A::function();
};
  1. 友元类
    同友元函数,只是授权的范围变大了,之前授权一个函数可以搞自己的私有成员,现在授权一个类,里面所有成员函数都可以搞自己了
    如果在类A里,把另一个类B声明为友元,B的成员函数就可以访问A的私有成员
class A{
	private:
		int a;
	friend class B;
}
class B{
	public:
		A obj;
		int f(){
			obj.a++;
			...
		}
};
//B生成对象的函数f()就可以调用A的私有成员变量a

总结:
增加私有成员的可访问范围
注意:
友元类之间的关系不能传递下去,也不能继承给子类

第四周 运算符重载(Operator overloading)

4.1 基本运算符重载

缺:对象间的运算符
补:基本运算符重载,扩展新的规则

  1. 把含重载运算符的表达式转换为对运算符函数的调用,把运算符的操作数转换为运算符的参数,可以重载为普通函数,也可以重载为成员函数
  2. 实质就是函数重载,可以多次重载,根据不同情况,自动调用不同的运算符函数
  3. 基本形式:
#include ;
#include 

using namespace std;

class Complex{
	public:
		double real, imag;
		Complex(double r=0.0, double i=0.0) : real(r), imag(i){
			//构造函数:将实部与虚部都初始化为0 
		} 
		Complex operator -(const Complex & c);	//重载为成员函数 
};

Complex operator +(const Complex & a, const Complex & b){	//重载为普通函数(全局函数) 
	return Complex(a.real + b.real, a.imag + b.imag);	//返回一个临时对象:用 实部=a.r+b.r,虚部=a.i+b.i初始化 
}

Complex Complex::operator -(const Complex & c){		//成员函数,只需要一个参数(参数个数 = 运算符个数-1)  
	return Complex(real - c.real, imag - c.imag);	//返回一个临时对象
			//    real - c.real :当前对象(this.real)- 参数对象(c.real) 
}

int main(){
	Complex a(4, 4), b(1, 1), c;
	c = a + b;		//等价于 c = operator+(a, b);
	cout << c.real << "," << c.imag <<endl;
	cout << (a - b).real << "," << (a - b).imag <<endl;	//a-b 等价于 a.operator-(b); 
	return 0;
}
  • 总结:
    1) 重载为全局函数:a + b 等价于 operator +(a, b);
    2) 重载为成员函数:a - b 等价于 a.operator -(b);

4.2 赋值运算符重载

缺:= 两边类型不匹配(int a = double b)
补:将 = 重载(定义新规则)

  1. = 只能重载为成员函数 //因为赋值一定有个载体(对象)承接这个值,所以只能是 obj.operator +(x),而不能是operator +(obj, x)
  2. 实例:将一个字符串赋值给一个String对象
#include 
#include 

using namespace std;

class String{
	private:
		char * str;	//指向动态分配的存储空间 
	public:
		String() : str(new char[1]){	
		//构造函数 new一个只有一个元素的char数组,用该数组的指针(数组名)初始化str 
			str[0] = 0;
		}
		const char * c_str(){
			return str;
		};
		String & operator =(const char *s);	//赋值运算符重载 
		//Q:为什么返回的是 String & 

		~String(){
		//析构函数 
			delete [] str;
		}
}; 

String & String::operator =(const char * s){
	//重载 = 使obj = “hello”能够成立
	delete [] str;
	str = new char[strlen(s)+1];
	strcpy(str, s);
	return * this; 
} 

int main(){
	String s;
	s = "Good Luck, ";		//等价于s.operator = ("Good Luck, ");
	cout << s.c_str() <<endl;
	//String s2 = "Hello!";	//这是初始化语句,没写构造函数,所以会出错 
	
	return 0; 
} 
  • 分析:s.operator =("Good Luck, ");的运行过程:
代码 过程
1.delete [] str; 先将String对象s的str delete掉(重置)
2. str = new char[strlen(s)+1]; 为str重新分配strlen(s)+1(+1放:\0)大小的空间(重定向)
3. strcpy(str, s); 将参数(字符串"Good Luck, ")的内容拷贝过来
4.return * this; 以后讲!!pass
  • Error: [Error] extra qualification 'String::' on member 'String' [-fpermissive]

→ Reason: The qualification student:: is required only if you define the member functions outside the class, usually in .cpp file.

  1. 浅拷贝与深拷贝:
  • case:
String s1, s2;
s1 = "this";
s2 = "that";
s1 = s2

就出问题了!最后一条s1 = s2,如果没有重载=,也能执行,相当于将对象s2赋值给对象s1(完全复制):s1.str对自己指的"this"始乱终弃,去指s2.str指的"that"了

后果:1. 2. 3.
C++ 2:短裤_第12张图片
3.如果再次给s1赋值,调用operator=( ) 就会 delete掉str指向的空间,就把s2的空间删了

  1. 改进:
  • case:
MyString s;
s = "Hello";
s = s;

→重载函数中增加一个判断:

if(this == &s)		//this指向当前对象
	return * this;
  1. 对operator = 返回值类型的讨论:

对运算符进行重载的时候,好的风格是应该保持运算符原本的特性

  • case1:

考虑到a = b = c;
先进行 b = c,如果operator =返回值类型是void,接下来就成了 a = void就出问题了

a = b = c;等价于 a.operator =(b.operator(c))

  • case2:

(a = b) = c;
a=b的返回值是a的引用,
cpp中赋值运算符的返回值是=左边变量的引用(要维持这个性质),
对a的引用赋值成c,变成a 的值与c 的值一样,与b没关系了
会修改a的值:???所以不能是String
(a = b) = c;等价于(a.operator =(b)).operator(c)
所以返回值类型应该是引用:String &

  1. 为String类编写复制构造函数是就会面临和3.一样的问题
    →单独为String类写一个复制构造函数(防止使用缺省的复制构造函数):
String(String & s){
	str = new char[strlen(s.str)+1]'
	strcpy(str, s.str);
}

4.3 运算符重载为友元函数

  1. 一般把运算符重载为类的成员函数
  2. 但有时,重载为成员函数不能满足使用要求,(因为类里已经有一个重载函数了,在放一个就冲突了,所以不能再在类里重载一次,只能用普通函数重载),而重载为普通函数又不能访问类的私有成员,所以需要将运算符重载为友元
  3. 复数运算程序完善:
    case: 复数与浮点数相加(复数实部+浮点数,虚部不变)
class Complex{
	double real, imag;
	public:
		Complex(double r, double i):real(r), imag(i){};
		Complex operator + (double r);
};
Complex Complex::operator +(double r){
	return Complex(real + r, imag);
}

这时:
c + 5就等价于c.operator +(5)
但是:
5 + c就不行了,这时类里已经有一个重载函数了,不能再在类里重载了,所以将+重载为普通函数,来处理这种情况:

Complex operator +(double r, const Complex & c){
	return Complex(c.real + r, c.imag);
}

但是:普通函数又不能调用类中的私有成员(real、imag),所以需要将+重载为友元(就是将普通重载函数,或者说全局函数: operator +(...) 在类里声明为友元):

class Complex{
	public:
		friend Complex operator +(...);
		...
};

4.4 可变长数组的实现

== 标准模板库中的vector

int main(){
//开始数组为空
	CArray a;	
//1. 2.
	for(int i=0; i<5; ++i){
		a.push_back(i);
	}
	CArray a2, a3;
	for(int i=0; i<a.length(); ++i){
		cout << a2[i] << " ";
	}
//3.
	a2 = a3;
	for(int i=0; i<a2.length(); ++i){
//4.
		cout << a2[i] << " ";
	}
	a[3] = 100;
//5.
	CArray a4(a);
	for(int i=0; i<a4.length(); ++i){
		cout << a4[i] << " ";
	}
	return 0;
}

需求:

  1. 动态分配内存来存放数组元素
  2. 需要一个指针成员变量
  3. 重载"=":用于数组间赋值,如a2 = a;
  4. 重载"[]":可以直接通过a[i]输出数组内元素
  5. 复制构造函数:可以通过a4(a)直接使a4复制a构造
  1. class CArray:
class CArray{
	int size;	//数组元素的个数
	int *ptr;	//2.指向动态分配的数组
	public:
		CArray(int s=0);	//s代表数组元素的个数
		CArray(CArray & a);
		~CArray();
		CArray & operator =(const CArray & a);	//3.用于数组对象间的赋值
		CArray & operator [](int i);	//4.重载[]
		void push_back(int v);	//在数组的尾部添加一个元素v
		int length(){
			return size;
		}
			
//构造函数
		CArray::CArray(int s):size(s){
			if(s == 0){
				ptr = NULL;
			}
			else{
				ptr = new int[s];
			}
		}
//5.复制构造函数,深拷贝
		CArray::CArray(CArray & a){
			if(!a.ptr){
				ptr = NULL;
				size = 0;
				return;
			}
			ptr = new int[a.size];
			memcpy(ptr, a.ptr, sizeof(int) * a.size);
			size = a.size;
		}
//析构函数
		CArray::~CArray(){
			if(ptr){
				delete []ptr;
			}
		}
//3.
		CArray & CArray::operator =(const CArray & a){
			if(ptr == a.ptr){
			//防止a = a这样的赋值导致出错
				return * this;
			}
			if(a.ptr == NULL){
				if(ptr){
					delete []ptr;
				}
				ptr = NULL;
				size = 0;
				return * this;
			}
			if(size < a.size){
			//如果原有空间足够大,就不需要重新分配空间
				if(ptr){
					delete []ptr;
				}
				ptr = new int[a.size];
			}
			memcpy(ptr, a.ptr, sizeof(int) * a.size);
			size = a.size;
			return * this;
		}
//4.
		int & CArray::operator [](int i){
		//用以支持根据下标访问数组元素,如a[i] = 4...
			return ptr[i];
		}
		void CArray::push_back(int v){
		//在数组尾部添加一个元素
			if(ptr){
				int * tmpPtr = new int[size+1];	//重新分配空间
				memcpy(tmpPtr, ptr, sizeof(int) * size);	//拷贝原数组内容
				delete []ptr;
				ptr = tmpPtr;
			}
			else{
			//数组本来是空的
				ptr = new int[1];
			}
			ptr[size++] = v;	//加入新的元素
		}
};		

4.5 流插入运算符(">>")和流提取运算符的重载("<<")

在C++中:">>“与”<<“本质上就是左移、右移运算符,但因经常用于cout,cin,所以被称为,流插入运算符(”>>")和流提取运算符("<<")

Q:cout << 5 << "this";为什么能成立?

A:

  1. cout是在头文件iostream中定义的一个ostream类对象
  2. << 能用在cout上的原因是,<<iostream中进行了重载
  3. cout << 5; 等价于 cout.operator <<(5);
    cout << "this";等价于cout.operator <<("this");
  4. cout << 5 << "this";第一次运算cout << 5的返回值类型仍为cout才能继续进行cout << "this";
ostream & ostream::operator <<(int n){
	...//输出n的代码
	return * this;
}
ostream & ostream::operator <<(const char * s){
	...//输出s的代码
	return * this;
}
  1. 所以cout << 5 << "this";等价于cout.operator <<(5).operator <<("this");

如果要重载cout,定义新的规则,必须重载为全局函数,如:

class CStudent{
	public:
		int nAge;
};
int main(){
	CStudent s;
	s.nAge = 5;
	cout << s << "Hello";
	return 0;
}

想使cout << s直接输出s.nAge,并返回一个cout(即ostream对象),则应:

ostream & operator <<(ostream & o, const CStudent & s){
	o << s.nAge;
	return o;
}

4.6 类型转换运算符的重载

类型名字就是类型转换运算符,如double,可以使用(double)来强制类型转换

  1. 类型转换运算符重载不需要写返回值类型:就是本身
  • 实例:将Complex对象转换为double类型
#include 
using namespace std;
class Complex{
	double real, imag;
	public:
		Complex(double r=0, double i=0): real(r), imag(i){};
//重载强制类型转换运算符
		operator double(){
			return real;
		}
};
int main(){
	Complex c(1.2, 3.4);
	cout << (double)c <<endl;//输出 1.2
	//等价于c.operator double()
	double n = 2 + c;
	//等价于double n = 2 + c.operator double()
	cout << n;//输出 3.2

4.7 自增自减运算符的重载

Q:前置++/–,与后置的区分:
A:

  1. 前置→作为一元运算符重载:
  • 重载为成员函数:
T & operator ++();
T & operator --();
  • 重载为全局函数:
T1 & operator ++(T2);
T1 & operator --(T2);
  1. 后置→重载为二元运算符:(多写一个"没用"的参数)
  • 重载为成员函数:
T & operator ++(int);
T & operator --(int);
  • 重载为全局函数:
T1 & operator ++(T2, int);
T1 & operator --(T2, int);
  • tips:如果只重载了前置,未重载后置,可能有的编译器会出错!

实例:

class CDemo{
	private:
		int n;
	public:
		CDemo(int i=0):n(i){}
		CDemo & operator ++();//前置(++a返回的就是a的引用,因为++a是先对a做++,返回a的引用(小名),就可以直接对++完的a进行操作了)
		CDemo operator ++(int);//后置
		operator int(){
			return n;
		}
		friend CDemo & operator --(CDemo &);
		friend CDemo operator --(CDemo &, int);
};
//······································重载为成员函数··············································
CDemo & CDemo::operator ++(){
//前置++
	++n;
	return * this;
}
//++s即为:s.operator ++();
CDemo CDemo::operator(int k){
//后置++
	CDemo tmp(*this)//记录修改前的对象
	n++;
	return tmp;//返回修改前的对象(后置++返回的不是a,a++前代替a的临时对象tmp,所以返回值类型为对象(tmp))
}
//s++即为:s.operator ++(0);
//······································重载为全局函数··············································
CDemo & operator --(CDemo & d){
//前置
	d.n--;
	return d;
}
//--s即:s.operator ++(0);
CDemo operator --(CDemo & d, int){
//后置--
	CDemo tmp(d);
	d.n--;
	return tmp;
}
//s--即:operator --(s, 0);

tips:前置++/–运行速度快!(并未构造新的临时对象tmp)

4.7 TIPS

  1. C++不允许定义新的运算符
  2. 重载后运算符的含义应该符合日常习惯(+、-的意义,别瞎搞,到时候自己都看不懂):
  3. 运算符重载不改变运算符的优先级
  4. ..*::?:sizeof
  5. 重载运算符()[]->或者=时,运算符重载函数必须声明为类的成员函数

5. 继承

5.1 继承(派生)

  1. 派生类(子类)是通过对基类进行修改(覆盖)和扩充得到的
  2. 在派生类中,拥有基类的全部成员函数和成员变量(private、protected、public:但是!子类的成员函数中不能访问这些private成员)可以扩充新的成员变量和函数
  3. 在派生类对象中,包含着基类对象,而且基类对象存储位置位于派生类对象新增的成员变量之前
  4. 派生类一经定义后,可以单独使用,不依赖于基类

写法:

class 派生类名: public 基类名{
	...
};

调用基类成员:

	基类名::成员函数名();

实例:学籍管理

#include 
#include 
using namespace std;

class CStudent{
	private:
		string name;
		string id;
		char gender;
		int age;
	public:
		void PrintInfo();
		void SetInfo(const string & name_, const string & id_,
			int age_, char gender_);
		string GetName(){return name;}
};

void CStudent::PrintInfo(){
    cout<< "Name:" << name <<endl;
    cout<< "ID:" << id <<endl;
	cout<< "Gender:" << gender <<endl;
	cout<< "Age:" << age <<endl;    
}

void CStudent::SetInfo(const string & name_, const string & id_,
			int age_, char gender_){
    name = name_;
    id = id_;
    age = age_;
    gender = gender_;
}

class CUndergraduateStudent: public CStudent{
	private:
		string department;
	public:
		void QualifiedForBaoyan(){
			cout << "qualified for baoyan" << endl;
		}
		void PrintInfo(){
			CStudent::PrintInfo();	//调用基类的PrintInfo
			cout << "Department:" << department <<endl;
		}
		void SetInfo(const string & name_, const string & id_,
			int age_, char gender_, const string & department_){
				CStudent::SetInfo(name_, id_, age_, gender_);
				department = department_;
			}
};

int main(){
	CUndergraduateStudent s2;
	s2.SetInfo("Fjj", "112233", 19, 'M', "CS");
	cout << s2.GetName() << " ";
	s2.QualifiedForBaoyan();
	s2.PrintInfo();

	return 0;
}

5.2 继承关系和派生关系

  1. 继承关系:"是"关系
    类B继承类A,则B的对象也是A的对象(类:人继承类:动物,则人也是动物)
  2. 复合关系:"有"关系
    类C中“有”成员变量o,o为类D的对象,则D的对象是C对象的固有属性或组成部分(C:人类 D:头类,头类的对象是人对象的一部分)
  3. 复合关系实例:人狗关系(假设一个人最多10条狗,狗只有一个主人)
  • case 1:
class CDog;	//提前声明
class CMaster{
	CDog dogs[10];
};
class CDog{
	CMaster m;
};

Wrong: 循环定义(编译出错),CDog对象和CMaster对象所占内存求不出(矛盾)

  • case 2:
class CDog;	
class CMaster{
	CDog * dogs[10];
};
class CDog{
	CMaster m;
};

Wrong:无法维护不同狗的相同主人信息(一群主人如果更换了,只能挨个狗改)

  • case 3:
class CMaster;	//必须先写CDog类,后写CMaster类	
class CDog{
	CMaster * pm;
};
class CMaster{
	CDog dogs[10];
}

Wrong:还凑合,但狗失去了自由,成为主人的固有属性了,只能通过人对象才能操作狗对象

  • case 4:
class CMaster;	
class CDog{
	CMaster * pm;
};
class CMaster{
	CDog * dogs[10];
}

Right:互为对象指针:“知道”关系(一个类的对象可以通过指针找到另一个对象)

5.3 覆盖和保护成员

覆盖:派生类可以定义一个和基类成员同名的成员

  1. 访问覆盖成员时,缺省的情况时访问派生类中定义的成员,要访问由基类定义的同名成员时,要使用作用域符号::
  2. 不要在派生类里写同名成员变量
  3. 包含成员(protected)
    C++ 2:短裤_第13张图片
  • 注1: 派生类的成员函数可以访问当前对象的基类的保护成员(子类可以使用父类的protected成员,且一个对象不能去访问另一个对象的基类的protected成员,同类也不行见①)
  • 实例:
class Father{
	private: 
		int nPrivate;
	public:
		int nPublic;
	protected:
		int nProtected;
};
class Son: public Father{
	void AccessFather(){
		nPublic = 1;		//T
		nPrivate = 1;		//F
		nProtected = 1;		//Error
		Son f;
		f.nProtected = 1;	//Error:①f不是当前对象
};	
int main(){
	Father f;
	Son s;
		
	f.nPublic = 1;		//T	
	s.nPublic = 1;		//T
	f.nProtected = 1;	//Error
	f.nPrivate = 1;		//Error	
	s.nProtected = 1;	//Error
	s.nPrivate = 1;		//Error
	return 0;
}

5.4 派生类的构造函数

  1. 派生类初始化时:先执行基类的构造函数,再执行派生类的构造函数;派生类消亡时:恰恰相反
  • 实例:
class Bug{
	private:
		int nLegs;
		int nColor;
	public:
		int nType;
		Bug(int Legs, int color);
		void PrintBug(){};
};
class FlyBug: public Bug{
	int nWings;
	public:
		FlyBug(int legs, int color, int wings);
};
//Bug的构造函数
Bug::Bug(int legs, int color){
	nLegs = legs;
	nColor = color;
}
//FlyBug的错误构造函数
FlyBug::FlyBug(int legs, int color, int wings){
	nLegs = legs;		//Error:nLegs是private的
	nColor = color;		//Error
	nType = 1;
	nWings = wings;
}
//正确的FlyBug构造函数:
FlyBug::FlyBug(int legs, int color, int wings): Bug(legs, color){
	nWings = wings;	//也可以直接加在初始化列表中: Bug(legs, color), nWings(wings)
}
  • Q:派生类如何初始化基类的私有成员?
  • A::Bug(legs, color):初始化列表(在派生类里直接初始化所包含的基类对象)
  1. 调用基类构造函数的两种方式:
    - 显示方式:在派生类的构造函数中为基类的构造函数提供参数(初始化列表):derived::derived(arg_derived-list):base(arg_base-list)
    - 隐式方式: 省略基类的构造函数,缺省的调用基类的默认构造函数(无参构造函数),如果基类没有无参构造函数,会出错!
  2. 派生类中包含成员对象时:初始化列表加上成员对象,顺序为:先基类,再成员对象,最后派生类自己;消亡时相反
  • 实例:
class Bug{
	private:
		int nLegs;
		int nColor;
	public:
		int nType;
		Bug(int Legs, int color);
		void PrintBug(){};
};
class Skill{
	public:
		Skill(int n){}
};
class FlyBug: public Bug{
	int nWings;
	Skill sk1, sk2;
	public:
		FlyBug(int legs, int color, int wings);
};

FlyBug::FlyBug(int legs, int color, int wings): Bug(legs, color), sk1(5), sk2(color), nWings(wings){
}

5.5 公有继承的赋值兼容规则

  • 公有派生:
class Base{
	...
};
class Derived: public Base{
	...
};
Base b;
Derived d;

规则1. 派生类对象可以赋值给基类对象(“是”关系):b = d; //缺省情况下:将d中包含的基类对象拷贝给b
规则2. 派生类对象可以初始化基类引用:Base & br = d; //br引用了d中包含的基类对象(别名)
规则3. 派生类对象的地址可以赋值给基类对象的指针:Base * pb = & d; //基类指针指向派生类对象(== 指向派生类对象存储空间最前面)
TIPS:如果派生方式是:private、protected,规则1.2.3.不成立

  • 直接基类和间接基类:(A派出B,B派生C)A是B的直基,是C的间基
  1. 声明派生类时,只需列出它的直接基类:见①
    - 派生类沿着类的层次自动向上继承它的间接基类
    - 派生类的成员包括:
    a)派生类自己定义的成员
    b)直接基类中的所有成员
    c)所有间接基类的全部成员
  • 实例:祖孙三代
#include 
using namespace std;
class Base{
	public: 
		int n;
		Base(int i): n(i){
			cout << "Base " << n << " constructed" <<endl;
		}
		~Base(){
			cout << "Base " << n << " destructed" <<endl;
		}
};
class Derived:public Base{
	public:
		Derived(int i): Base(i){
			cout << "Derived constructed" <<endl;
		}
		~Derived(){
			cout << "Derived destruct" <<endl;
		}
};
class MoreDerived: public Derived{
	public:
		MoreDerived():Derived(4){
//①
			cout << "More Derived constructed" <<endl;
		}
		~MoreDerived(){
			cout << "More Derived destructed" <<endl;
		}
};
int main(){
	MoreDerived Obj;
	return 0;
}

输出结果:

Base 4 constructed
Derived constructed
More Derived constructed
More Derived destructed
Derived destruct
Base 4 destructed

6. 多态

6.1 虚函数和多态

  1. 虚函数:在类的定义中,加入virtual关键字的成员函数(虚函数可以参与多态,普通函数不能)
  • virtual关键字只在类的定义里的函数声明中,写函数体时不用,见①
  • 构造函数和静态成员函数不能是虚函数
class base{
	virtual int get();
};
int base::get(){}	//①
  1. 多态的表现形式(一):
  • 派生类指针可以赋值给基类指针

  • 通过基类指针调用基类和派生类中的同名虚函数时:
    - 1)若该指针指向一个基类的对象,那么被调用的是基类的虚函数
    - 2) 若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数

  • 实例:

class CBase{
	public:
		virtual void SomeVirtualFunc(){}
};
class CDerived: public CBase{
	public:
		virtual void SomeVirtualFunc(){}
};
int main(){
	CDerived ODerived;
	CBase * p = & ODerived;
	//这里p指向的是ODerived对象
	p -> SomeVirtualFunc();
	//调用哪个虚函数,取决于p指向对象的类型,所以这里调用CDerived中的虚函数
	return 0;
}
  1. 多态的表现形式(二):
  • 派生类的对象可以赋值给基类引用
  • 通过基类引用调用基类和派生类中的同名虚函数时:
    - 1)若该引用引用的是一个基类的对象,那么被调用的是基类的虚函数
    - 2) 若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数
  • 实例:
class CBase{
	public:
		virtual void SomeVirtualFunc(){}
};
class CDerived: public CBase{
	public:
		virtual void SomeVirtualFunc(){}
};
int main(){
	CDerived ODerived;
	CBase & r = ODerived;
	//这里r引用的是ODerived对象
	p -> SomeVirtualFunc();
	//调用哪个虚函数,取决于r引用对象的类型,所以这里调用CDerived中的虚函数
	return 0;
}
  1. 多态的作用:提高可扩充性????(如何提高,虚函数有啥用!?),不具体针对哪个对象,根据实际类型自动调用对应的对象,新增加类时,如果该类与原有的类有互动,如果用非多态方法,原有的类都需要添加指向新类的指针参数,如果用多态,则不需要改动原有的类(反正实际运行时是根据类型判断调用哪个类——传哪个类的指针,就去调用哪个类中的虚函数)
  • 多态实例:
#include 
using namespace std;

class A{
	public:
		virtual void Print(){
			cout << "A::Print" <<endl;
		}
};
class B: public A{
	public:
		virtual void Print(){
			cout << "B::Print" <<endl;
		}
};
class D: public A{
	public:
		virtual void Print(){
			cout << "D::Print" <<endl;
		}
};
class E: public B{
	virtual void Print(){
		cout << "E::Print" <<endl;
	}
};
int main(){
	A a;
	B b;
	E e;
	D d;
	A *pa = &a;
	B *pb = &b;
	D *pd = &d;
	E *pe = &e;
	
	pa -> Print();
	pa = pb;
	pa -> Print();
	pa = pd;
	pa -> Print();
	pa = pe;
	pa -> Print();
	return 0;
}

继承关系图:

A
B
D
E

结果:

A::Print
B::Print
D::Print
E::Print
  • 纯虚函数:没有函数体,virtual void func() = 0;,父类只提供个平台,父类本身用不到这些虚函数(见5.4)

  • TIPS:用基类指针数组存放指向各种派生类对象的指针,然后遍历该数组,就能对各个派生类对象做各种操作,是很常用的做法

  • 实例:

问题描述:输入若干几何形体参数,按面积排序输出

Sample Input:

3				//图形数
R 3 5			//矩形	
C 9				//圆	
T 3 4 5			//三角形	
Sample Output:

Triangle:6				
Rectangle:15			
Circle:254.34			
#include 
#include 
#include 
using namespace std;
 
class CShape{
 	public:
 		virtual double Area() = 0;
 		virtual void PrintInfo() = 0;
};
 
class CRectangle: public CShape{
	public:
		int w, h;
		virtual double Area();
		virtual void PrintInfo();
};

class CCircle: public CShape{
	public:
		int r;
		virtual double Area();
		virtual void PrintInfo();
};

class CTriangle: public CShape{
	public:
		int a, b, c;
		virtual double Area();
		virtual void PrintInfo();
};

double CRectangle::Area(){
	return w * h;
}
void CRectangle::PrintInfo(){
	cout << "Rectangle:" << Area() <<endl;
}
double CCircle::Area(){
	return 3.14 * r * r;
}
void CCircle::PrintInfo(){
	cout << "Circle:" << Area() <<endl;
}
double CTriangle::Area(){
	double p = (a + b + c) / 2.0;
	return sqrt(p * (p - a) * (p - b) * (p - c));
}
void CTriangle::PrintInfo(){
	cout << "Triangle:" << Area() <<endl;
}

CShape * pShapes[100];	//①基类指针数组 
int MyCompare(const void * s1, const void * s2);

int main(){
	int i;
	int n;
	CRectangle * pr;
	CCircle * pc;
	CTriangle * pt;
	cin >> n;
	for(i = 0; i < n; i++){
		char c;
		cin >> c;
		switch(c){
			case'R':
				pr = new CRectangle();
				cin >> pr -> w >> pr -> h;
				pShapes[i] = pr;
				break;
			case'C':
				pc = new CCircle();
				cin >> pc -> r;
				pShapes[i] = pc;
				break;
			case'T':
				pt = new CTriangle();
				cin >> pt -> a >> pt -> b >> pt -> c;
				pShapes[i] = pt;
				break;				
		}
	}
	qsort(pShapes, n, sizeof(CShape*), MyCompare);
	for(i=0; i<n; i++){
		pShapes[i] -> PrintInfo(); 
	}
	return 0;
} 

int MyCompare(const void * s1, const void * s2){
	double a1, a2;
	CShape * * p1;		//s1, s2是void *, 不可写 " *s1 " 来取得s1的内容 
	CShape * * p2;
	p1 = (CShape * *) s1;	//s1,s2指向pShapes数组中的元素,素组元素的类型是CShape * 
	p2 = (CShape * *) s2;	//故p1, p2都是指向指针的指针,类型为CShape ** 
	a1 = (*p1) -> Area();	//* p1的类型是Cshape *,是基类指针,故此句为多态 
	a2 = (*p2) -> Area();
	if(a1 < a2)
		return -1;
	else if(a2 < a1)
		return 1;
	else
		return 0;
}
  • 在非构造函数、析构函数的成员函数中调用虚函数,是多态!见②
  • 实例:
#include 
using namespace std;

class Base{
	public:
		void fun1(){
			fun2();		
			//② 相当于this -> fun2(): this为Derived对象d ①
		}
		virtual void fun2(){
			cout << "Base:fun2()" <<endl;
		}
};
class Derived: public Base{
	public:
		virtual void fun2(){
			cout << "Derived:fun2()" <<endl;
		}
};
int main(){
	Derived d;
	Base *pBase = &d;
	//① pBase指针指向的是一个Derived对象d
	pBase -> fun1();
	return 0;
}

结果:Derived:fun2()

  • 在构造函数和析构函数中调用虚函数,不是多态(编译时即可确定),调用的函数是自己的类或基类(如果自己没有,就调用直接基类中定义的虚函数)中定义的函数①,不会等到运行时才决定调用自己的还是派生类的函数
  • 派生类中和基类中虚函数同名同参数表的函数,不加virtual也自动成为虚函数②
  • 实例:
#include 
using namespace std;

class Base{
	public:
		virtual void hello(){
			cout << "hello from Base-1" <<endl;
		}
		virtual void bye(){
			cout << "bye from Base-1" <<endl;
		}
}; 
class Derived: public Base{
	public:
		void hello(){
		//②
			cout << "hello from Derived-2" <<endl;
		}
		Derived(){
			hello();	//①构造函数中调用虚函数不是多态,所以这里直接调用Derived自己的hello
		}
		~Derived(){
			bye();
		}
};
class MoreDerived: public Derived{
	public:
		void hello(){
			cout << "hello from MoreDerived-3" <<endl;
		}
		void bye(){
			cout << "bye from MoreDerived-3" <<endl;
		}
		MoreDerived(){
			cout << "constructing MoreDerived-3" <<endl;
		}
		~MoreDerived(){
			cout << "destructing MoreDerived-3" <<endl;
		}
};
int main(){
	MoreDerived md3;	//③
	Derived *pd2 = & md3;
	pd -> hello();	//④ 是多态
	return 0;
}

结果:

//③:
hello from Derived-2
constructing MoreDerived-3
//④:
hello from MoreDerived-3
//消亡:
destructing MoreDerived-3
bye from Base-1

Why?
比如说,new一个对象时,会先调用基类的构造函数(这时这个对象所属的派生类还没有初始化),如果构造函数中的虚函数是多态,调用基类的构造函数中的虚函数时,实际调用的是该派生类中对应的虚函数,而派生类这时还没有初始化好,没法被调用

5.3 多态的实现原理

  1. “多态”的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定 —— 这叫“动态联编”
  2. 虚函数表:多态就是通过查虚函数表来知道具体调用哪个虚函数(如果将b的虚函数表替换为a的虚函数表,调用b时,会跳转去执行a)
  3. 多态:空间开销(虚函数表)、时间开销(查虚函数表)

5.4 虚析构函数、纯析构函数、抽象类

  1. 虚析构函数:
  • Q: 通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数,但是删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数(析构不够完整,没调用派生类的析构函数,见:实例①
  • 解决方法:将基类的析构函数声明为virtual,这样通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数:见:实例②
  • TIPS1:一般来说,一个类如果定义了虚函数,则应该将析构函数也定义为虚函数 / 一个类打算作为基类使用,也应该将析构函数定义为虚函数
  • TIPS2:构造函数不可以是虚函数
  • 实例①:
#include 
using namespace std;

class Base{
	public:
		~Base(){
			cout << "bye from Base-1" <<endl;
		}
};
class Derived: public Base{
	public:
		~Derived(){
			cout << "bye from Derived-2" <<endl;
		}
};
int main(){
	Base *pb;
	pb = new Derived();
	delete pb;
	return 0;
}

输出:

bye from Base-1
//只执行了基类(Base)的虚构函数,没有执行派生类(Derived)的虚构函数
  • 实例②:
virtual ~son(){...};

输出结果:

bye from Derived-2
bye from Base-1
//析构自底向上
  1. 纯虚函数:没有函数体的虚函数
class A{
	 private:
	 	int a;
	 public:
	 	virtual void Print() = 0;
	 	void fun(){ cout << "fun";}
};
  1. 抽象类:包含纯虚函数的类
    - 抽象类只能作为基类的派生新类使用,不能创建抽象类的对象(抽象类一定是被包含在一个独立的对象里),见:① ③
    - 抽象类的指针和引用可以指向由抽象类派生出来的类的对象(基类指针/引用都可以指向/引用派生类对象)
    - 在抽象类的成员函数内可以调用纯虚函数(是多态),但是在构造函数或析构函数内部不能调用纯虚函数(不是多态),见 ②
    - 如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类,见 ③
#include 
using namespace std;

class A{
	public:
		virtual void f() = 0;
		void g(){
			this -> f();	//① 
		}
		A(){
			//f();			//②
		}
};
class B: public A{
	public:
		void f(){			// ③ 
			cout << "B:f()" <<endl;
		}
};
int main(){
	B b;
	b.g();
	return 0;
}

输出结果:B:f()

①:多态,取决于作用的对象(如果作用对象为A,但A的f()是纯虚函数,就出错,但这种情况不会发生,因为A为抽象类吧,不能用抽象类定义对象),这里this 指向的是b,调用B的f()
②:编译出错,构造函数中调用虚函数,不是多态,只能调用自己的虚函数,而A的f()为纯虚函数,连函数体都没有,没法调用
③:B实现了A全部的纯虚函数,只有f(),所以B不是抽象类,可以创建对象b,如果没有实现f(),则B b;会出错: [Error] cannot declare variable 'b' to be of abstract type 'B'

7. 输入输出模板

7.1 输入输出流相关的类

  1. 相关的类
对文件读操作
对文件写操作
ios
istream
ostream
ifstream
iostream
ofstream
fstream
作用 对象
istream 输入的流类 cin
osream 输出的流类 cout
ifstream 从文件读取数据的类
ofstream 向文件写入数据的类
iostream 既用于输入,又用于输出
fstream 既能从文件读取数据,又能向文件写入数据
  1. 标准流对象:
    - 输入流对象:cin(用于从键盘读取数据,也可以重定向为从文件中读取数据)
    - 输出流对象:cout (用于向屏幕输出数据,也可以重定向为向文件写入数据)、cerrclog(对应于标准错误输出流,用于向屏幕输出错误信息,cerr不用缓冲区,而clog先存缓冲区,刷新或区满才输出到屏幕)

  2. 输出重定向

#include 
using namespace std;

int main(){
	int x, y;
	cin >> x >> y;
	freopen("test.txt", "w", stdout);	//将标准输出(stdcout)重定向到test.txt文件 
	if(y == 0)
		cerr << "error." <<endl;	//cerr没有被重定向 
	else
		cout << x / y;	//输出结果到test.txt 

	return 0;
} 
  1. 输入重定向
#include 
using namespace std;

int main(){
	double f;
	int n;
	freopen("t.txt", "r", stdin);	//cin被改为从t.txt中读取数据 
	cin >> f >> n;
	cout << f << "," << n <<endl;

	return 0;
} 
  1. 判断输入流结束
int x;
while(cin >> x){
	...
}
return 0;
  • 如果是文件输入:freopen("some.txt", "r", stdin);,读到文件尾部,输入流就算结束
  • 如果从键盘输入,则在单独一行输入Ctrl + z代表输入流结束
  1. istream类的成员函数:
    1) istrem & getline(char * buf, int buffSize);从输入流中读取bufSize-1个字符到缓冲区buf,或读到碰到'\n'为止(两个条件哪个先满足,按哪个来)
    2)istream & getline(char * buf, int bufSize, char delim);从输入流中读取bufSize-1个字符到缓冲区,或碰到delim字符(分隔符)为止(满足其一即可)

    • 两个函数都会自动在buf中读入数据的结尾添加\0
    • \ndelim都不会被读入buf,但会被从输入流中取走。如果输入流中\ndelim之前的字符个数达到或超过了bufSize个,就导致读入出错,其结果就是:虽然本次读入已经完成,但是之后的读入就会失败了
    • 可以用if(!cin.getline(...))判断输入是否结束

    3)bool eof();判断输入流是否结束
    4)int peek();返回下一个字符,但不从流中去掉
    5)istream & putback(char c);将字符ch放回输入流(头部,最前面)
    6)istream & ignore(int nCount = 1, int delim = EOF); 从流中删掉最多nCount个字符,遇到EOF时结束

7.2 流操作算子控制输出格式

流操作纵算子:
#include

  1. 整数流的基数:流操作算子:dec(十进制)、oct(八进制)、hex(十六进制)、setbase(任意进制)见:实例1
  • 实例1:
#include 
#include 
using namespace std;

int main(){
	int n = 10;
	
	cout << n <<endl;
	cout << hex << n << "\n"
		 << dec << n << "\n"
		 << oct << n << endl; 

	return 0;
} 
  • 输出结果:
10	//原
a	//十六进制
10	//十进制
12	//八进制
  1. 浮点数精度:precisionsetprecision 见:实例2
  • 实例2:
    precision是成员函数,调用方式为:
cout.precision(5);

setprecision是流操作算子,调用方式为:

cout << setprecision(5);	//可以连续输出(设置后,后面的浮点数都按这个精度来)

功能相同:指定输出浮点数的有效位数(非定点方式输出时)
指定输出浮点数的小数点后的有效位数(定点方式输出时)
定点方式:小数点必须出现在个位后面
TIPS:浮点数输出最多6位有效数字

  • 1非定点:
#include 
#include 
using namespace std;

int main(){
	double x = 1234567.89, y = 12.34567;
	int n = 12234567;
	int m = 12;
	
	cout << setprecision(6) << x << endl
		 << y << endl << n << endl << m; 

	return 0;
} 
  • 输出结果:
1.23457e+006
12.3457
12234567
12
  • 2定点:
#include 
#include 
using namespace std;

int main(){
	double x = 1234567.89, y = 12.34567;
	int n = 12234567;
	int m = 12;
	//setiosflags以小数点位置固定的方式输出
	cout << setiosflags(ios::fixed) <<
			setprecision(6) << x << endl
		 << y << endl << n << endl << m; 

	return 0;
} 
  • 输出结果:
1234567.890000
12.345670
12234567
12

TIPS:用resetiosflags(ios::fixed)来取消定点模式

cout << setiosflags(ios::fixed) << 			
	setprecision(6) << x << endl << 
	resetiosflags(ios::fixed) << x;
  1. 设置域宽:setwwidth(按一定宽度输出,不足补空格/0) 见:实例3
  • 实例3:
    setwwidth功能一样,一个是成员函数,一个是流操作算子,调用方式不同:
cin >> setw(4); 
cin.width(5);
cout << setw(4);
cout.width(5);

TIPS:设置宽度是一次性的,每次读入和输出之前都要设置宽度

#include 
#include 
using namespace std;

int main(){
	int w = 4;
	char string[10];
	cin.width(5);
	while(cin >> string){
		cout.width(w++);
		cout << string <<endl;
		cin.width(5);
	}

	return 0;
} 
  • 输出结果:
1234567890
1234
 5678
    90
  • 总结实例1.2.3:
#include 
#include 
using namespace std;

int main(){
	int n = 141;
	//1)分别以十六进制,十进制,八进制先后输出n
	cout << "1) " << hex << n << " " << dec << n << " " << oct << n <<endl;
	double x = 1234567.89, y = 12.34567;
	//2)保留5位有效数字
	cout << "2) " << setprecision(5) << x << " " << y << " " <<endl;	
	//3)保留小数点后面5位
	cout << "3) " << fixed << setprecision(5) << x << " " << y <<endl;
	//4)科学计数法输出,且保留小数点后面5位
	cout << "4) " << scientific << setprecision(5) << x << " " << y << endl;
	//5)非负数要显示正号,输出宽度为12字符,宽度不足则用'*'填补
	cout << "5) " << showpos << fixed << setw(12) << setfill('*') << 12.1 <<endl;
	//6)非负数要显示正号,输出宽度为12字符,宽度不足则右边用填充字符填充
	cout << "6) " << noshowpos << setw(12) << left << 12.1 <<endl;
	//7)输出宽度为12字符,宽度不足则左边用填充字符填充
	cout << "7) " << setw(12) << right << 12.1 <<endl;
	//8)宽度不足时,负号和数值分列左右,中间用填充字符填充
	cout << "8) " << setw(12) << internal << -12.1 <<endl;
	cout << "9) " << 12.1 <<endl;
	
	return 0; 
} 
  • 输出结果:
1) 8d 141 215
2) 1.2346e+006 12.346
3) 1234567.89000 12.34567
4) 1.23457e+006 1.23457e+001
5) ***+12.10000
6) 12.10000****
7) ****12.10000
8) -***12.10000
9) 12.10000
  1. 用户自定义的操作纵算子
  • 实例4:
ostream &tab(ostream &output){
	return output << '\t';
}
cout << "aa" << tab << "bb" <<endl;
  • 输出结果:
aa    bb	//中间空了个tab

!:iostream里对<<进行了重载(成员函数)

ostream & operator(ostream & (*p)(ostream &));

该函数内部会调用p所指向的函数,且以*this作为参数,如hex、dec、oct…都是函数

7.3 文件读写

  1. 创建文件:
#include 

ofstream outFile("clients.dat", ios::out|ios::binary);
//创建文件
  • clients.dat 要创建的文件名字
  • ios::out 文件打开方式
    - ios::out 输出到文件,删除原有的内容
    - ios::app 输出到文件,保留原有内容,总是在尾部添加
  • ios::binary 以二进制文件格式打开文件

→ 也可以先创建ofstream对象,再用open函数打开(文件名可以给出绝对路径,也可以给出相对路径。没有交代路径信息,就是在当前文件夹下找文件)

ofstream fout;
fout.open("test.out", ios::out|ios::binary);

→判断打开是否成功:

if(!fout){
	cout << "File open error!" <<endl;
}
  1. 文件的读写指针
    对输入文件,有一个读指针
    对输出文件,有一个写指针
    对输入输出文件,有一个读写指针
    标识文件操作的当前位置,该指针在哪里,读写操作就在哪里进行
  • 实例1:文件的写指针
ofstream fout("a1.out", ios::app);	//以添加方式打开
long location = fout.tellp();	//取得写指针的位置
location = 10;
fout.seekp(location);	//将写指针移动到第10个字节处(距文件开头)
fout.seekp(location, ios::beg);	//从头数location
fout.seekp(location, ios::cur);	//从当前位置数loaction
fout.seekp(location, ios::end);	//从尾部数location

- location 可以为负值

  • 实例2:读指针
ifstream fin("a1.in", ios::ate);	//打开文件,定位文件指针到文件尾(位置就是文件大小)
long location = fin.tellg();	//取得读指针的位置(位置(相对)就是距文件头多少字节)
location = 10L;
fout.seekg(location);	//将读指针移动到第10个字节处(距文件开头)
fout.seekg(location, ios::beg);	//从头数location
fout.seekg(location, ios::cur);	//从当前位置数loaction
fout.seekg(location, ios::end);	//从尾部数location
  1. 字符文件读写
    →文件流也是流,所以流的成员函数和流操作算子也同样适用于文件流
  • 实例:写一个程序,将文件in.txt里面的整数排序后,输出到out.txt
#include 
#include 
#include 
#include 
using namespace std;

int main(){
	vector<int> v;
	ifstream srcFile("in.txt", ios::in);
	ofstream destFile("out.txt", ios::out);
	int x;
	while(srcFile >> x)
	//只有文件没读完,就不断将文件中内容读到x中
		v.push_back(x);
	sort(v.begin(), v.end());
	for(int i=0; i<v.size(); i++)
		destFile << v[i] << " ";
	destFile.close();
	srcFile.close();
	
	return 0;
}
  1. 二进制文件读写
  • 二进制读文件:
    ifstreamfstream的成员函数:
    istream & read(char * s, long n);
    :将文件读指针指向的地方的n个字节内容,读入到内存地址s,然后将文件读指针向后移动n字节(以ios::in方式打开文件时,文件读指针开始指向文件开头)
    
  • 二进制写文件:
    ofstreamfstream的成员函数:
    istream & write(const char * s, long n);
    :将内存地址s处的n个字节内容,写人到文件中写指针指向的位置,然后将文件写指针向后移动n字节(以ios::out方式打开文件时,文件写指针开始指向文件开头,以ios::app方式打开文件时,文件写指针开始指向文件尾部)
    
  • 实例1:在文件中写入和读取一个整数(以二进制形式)
#include 
#include 
using namespace std;

int main(){
	ofstream fout("some.dat", ios::out|ios::binary);
	int x = 20;
	fout.write((const char *)(&x), sizeof(int));
	fout.close();
	ifstream fin("some.dat", ios::in|ios::binary);
	int y;
	fin.read((char *) & y, sizeof(int));
	fin.close();
	cout << y <<endl;
	
	return 0;
}
  • 实例2:
    →从键盘输入几个学生的姓名和成绩,并以二进制文件形式保存
#include 
#include 
using namespace std;

struct Student{
	char name[20];
	int score;
};

int main(){
	Student s;
	ofstream OutFile("C:\\Users\\Administrator\\Desktop\\students.dat", ios::out|ios::binary);
	while(cin >> s.name >> s.score)
		OutFile.write((char *) & s, sizeof(s));
	OutFile.close();
	
	return 0;
}

→读出该student文件:

#include 
#include 
using namespace std;

struct Student{
	char name[20];
	int score;
};

int main(){
	Student s;
	ifstream inFile("students.dat", ios::in|ios::binary);
	if(!inFile){
		cout << "error" <<endl;
		return 0;
	}
	while(inFile.read((char *) & s, sizeof(s))){
		int readedBytes = inFile.gcount();	//看刚才读了多少个字节
		cout << s.name << " " << s.score <<endl; 
	}
	inFile.close();
		
	return 0;
}

→二进制文件的读写:更改二进制文件中的内容

#include 
#include 
#include//**是不包括strlen的,要使用cstring**
using namespace std;

struct Student{
	char name[20];
	int score;
};

int main(){
	Student s;
	fstream iofile("C:\\Users\\Administrator\\Desktop\\students.dat", 
					ios::in|ios::out|ios::binary);
	if(!iofile){
		cout << "error" <<endl;
		return 0;
	}
	iofile.seekp(2 * sizeof(s), ios::beg);	//s*sizeof(s):将写指针定位到第三个记录 
	iofile.write("xxx", strlen("xxx")+1);
	iofile.seekg(0, ios::beg);	//定位读指针到开头 
	while(iofile.read((char *) & s, sizeof(s))){
		cout << s.name << " " << s.score <<endl; 
	}
	iofile.close();
		
	return 0;
}
  • 二进制存储的优势:
    1.节省空间(文本存储中会有回车/空格…的浪费;另外,如果存1000000,文本方式中为字符串,7位,占7个字节,而用二进制方式,只cp4字节的int型)
    2.二进制格式,每个数据所占空间固定(等长、对齐),便于查找(二分查找…)

  • 实例:文件拷贝程序 mycopy.cpp

/*用法示例:
mycopy src.dat dest.dat
即将 src.dat 拷贝到dest.dat 如果 dest.dat 原来就有,则原来的文件会被覆盖 
*/
#include 
#include 
using namespace std;

int main(int argc, char * argv[]){
	if(argc != 3){
		cout << "File name missing!" <<endl;
		return 0;
	}
	ifstream inFile(argv[1], ios::binary|ios::in);	//打开文件用于读
	if(!inFile){
		cout << "Source file open error." <<endl;
		return 0;
	} 
	ofstream outFile(argv[2], ios::binary|ios::out);	//打开文件用于写
	if(!outFile){
		cout << "New file open error." <<endl;
		inFile.close();	//打开的文件一定要关闭
		return 0; 
	} 
	char c;
	while(inFile.get(c))	//每次读取一个字符,整个文件都读完退出while 
		outFile.put(c);		//每次写入一个字符 
	outFile.close();
	inFile.close();
	return 0;
}
  1. 二进制文件和文本文件的区别
    Unix与Linux换行符:\n(ASCII码:0x0a)
    Windows换行符:\r\n(ASCII码:0x0d0a) endl就是\n
    Mac OS换行符:\r(ASCII码:0x0d)

Unix/Linux下打开文件,用不用ios::binary没区别
而在Windows下,如果不用ios::binary则:
- 读取文件时,所有\r\n会被当成一个字符\n处理,即少读一个字符\r
- 写入文件时,写入单独的\r时,系统自动在前面加一个\r,即多写一个\r

7.4 函数模板

  1. 函数模板:

函数模板就是让编译器可以适应各种参数类型

template<class 类型参数1class 类型参数2, ...>
返回值类型 模板名(形参表){
	...
};

TIPS: class可以替换为typename

  • 实例:Swap模板
#include 
using namespace std;

template <class T>
void Swap(T & x, T & y){
	T tmp = x;
	x = y;
	y = tmp;
}

int main(){
	int n = 1, m = 2;
	Swap(n, m);	//编译器自动生成void Swap(int &, int &)函数
	cout << n << " " << m <<endl;
	double f = 1.2, g = 2.3;
	Swap(f, g);	//编译器自动生成void Swap(double &, double &)函数
	cout << f << " " << g <<endl;
	return 0;
}
  1. 函数模板中可以有不止一个类型参数:
template<class T1, class T2>
T2 print(T1 arg1, T2 arg2){
	cout << arg1 << " " << arg2 <<endl;
	return arg2;
}
  • 实例:求数组最大元素的函数模板
#include 
using namespace std;

template <class T>
T MaxElement(T a[], int size){
	T tmpMax = a[0];
	for(int i=1; i<size; ++i)
		if(tmpMax < a[i])
			tmpMax = a[i];
	return tmpMax;
}

int main(){
	int s1[3] = {2, 9, 8};
	cout << MaxElement(s1, 3) <<endl;
	double s2[4] = {1.2, 2.5, 3.5, 0.9};
	cout << MaxElement(s2, 4)<<endl;
	return 0;
}
  1. 模板实例化:
    1)调用实参时,自动生成实参类型对应的函数
    2)不通过参数,用手动(强制)实例化函数模板,见:实例1
  • 实例1:
#include 
using namespace std;

template <class T>
T Inc(T n){
	return 1 + n;
}

int main(){
	cout << Inc<double>(4) / 2;	//输出2.5
	return 0;
}
  1. 函数模板可以重载,只要它们的形参表或类型参数表不同即可
template<class T1, class T2>
void print(T1 arg1, T2 arg2){
	cout << arg1 << " " << arg2 <<endl;
}template<class T>
void print(T arg1, T arg2){
	cout << arg1 << " " << arg2 <<endl;
}template<class T, class T2>
void print(T arg1, T arg2){
	cout << arg1 << " " << arg2 <<endl;
}
  1. 函数模板和函数的次序:
  • 在有多个函数和函数模板名字相同的情况下,对一条函数调用语句,编译器:
    1)先找参数完全匹配的普通函数(非由模板实例化得来的函数)
    2)再找参数完全匹配的模板函数
    3)再找实参经过自动类型转换后能匹配的普通函数
    4)以上都没有,报错!

  • 实例:

#include 
using namespace std;

double F(double a, double b){
	cout << "1)参数匹配的普通函数" <<endl;
	return 0;
}
template <class T>
T F(T a, T b){
	cout << "2)参数完全匹配的模板函数" <<endl;
	return 0;
}
template <class T, class T2>
T F(T a, T2 b){
	cout << "3)实参匹配实例化后匹配的模板函数" <<endl;
	return 0;
}

int main(){
	int i = 4, j = 5;
	
	F(1.2, 3.4);	//输出1)
	F(i, j);		//输出2)
	F(1.2, 3);		//输出3)
//	F(1, 2) 	//报错! 
	return 0;
}
  1. 匹配模板函数时,不会进行类型自动转换:
template<class T>
T myFun(T arg1, T arg2){
	cout << arg1 << " " << arg2 << "\n";
	return arg1;
}
int main(){
	myFun(5, 7);	//ok
	myFun(5.8, 8.4);	//ok
	myFun(5, 8.4);	//[error]:no matching function for call to 'myFun(int, double)'
  • 实例:Map

将区间[s, e)内的元素,通过变化op()后,放到以x为起始位置的一段空间

#include 
using namespace std;

template<class T, class Pred>
void Map(T s, T e, T x, Pred op){
//一般s,e,x都是指针,而op为函数指针,指向变换函数 
	for(; s!=e; ++s, ++x)
	//到e为止,e处元素未参与变化 
		*x = op(*s);
}

double Square(double x){return x * x;}
int Cube(int x){return x * x * x;}

int a[5] = {1, 2, 3, 4, 5}, b[5];
double c[5] = {1.1, 2.1, 3.1, 4.1, 5.1}, d[5];

int main(){
	Map(a, a+5, b, Square);
	for(int i=0; i<5; ++i)	cout << b[i] << ",";
	cout <<endl;
	
	Map(a, a+5, b, Cube);
	for(int i=0; i<5; ++i)	cout << b[i] << ",";
	cout <<endl;
	
	Map(c, c+5, d, Square);
	for(int i=0; i<5; ++i)	cout << d[i] << ",";
	cout <<endl;
	return 0;
}

其中,Map(a, a+5, b, Square);被实例化为:

void Map(int * s, int * e, int * x, double(*op)(double)){
	for(; s!=e; ++s, ++x)
		*x = op(*s);
}

7.5 类模板

把一些相似的类定义成类模板,再通过实际情况进行实例化

template<class 类型参数1class 类型参数2, ...>	//类型参数表
class 类模板名{
	...	//成员变量和函数
};
  1. 类模板里的成员函数的写法(拿到外面写时):
template<class 类型参数1, class 类型参数2, ...>
返回值类型 类模板名<类型参数名列表>::成员函数名(参数表){
	...
}
  1. 用类模板定义对象:
类模板名<真实类型参数表> 对象名(构造函数实参表);
  • 实例:Pair类模板
#include 
using namespace std;

template<class T1, class T2>
class Pair{
	public:
		T1 key;	//关键字
		T2 value;	//值
		Pair(T1 k, T2 v):key(k), value(v){};
		bool operator < (const Pair<T1, T2> & p) const;
};

template<class T1, class T2>
bool Pair<T1, T2>::operator < (const Pair<T1, T2> & p) const{
//Pair 的成员函数operator <
	return key < p.key; 
}

int main(){
	Pair<string, int> student("Tom", 19);
	//实例化出一个类Pair
	cout << student.key << " " << student.value; 
	return 0;
}
  1. 模板类:由类模板实例化得到的类,叫模板类
    →同一个类模板的两个模板类是不兼容的
Pair<string, int> * p;
Pair<string, double> a;
p = & a;	//wrong
  1. 函数模板可以作为类模板的成员
#include 
using namespace std;

template<class T>
class A{
	public:
		template<class T2>
		void Fun(T2 t){cout << t;}	//成员函数模板! 
};

int main(){
	A<int> a;
	a.Fun('K');	//Func被实例化, T2 -> char
	a.Fun("hello");	//Func再次被实例化,T2 -> const char * 
	return 0;
}
  1. 类模板与非类型参数:类模板的<类型参数表>中可以出现非类型参数(实例化时,非类型参数不是由具体“类型”替代,而是由具体参数替代)
template<class T, int size>
class CArray{
	T array[size];
	public:
		void Print(){
			for(int i=0; i<size; ++i)
				cout << array[i] <<endl;
};
CArray<double, 40> a2;
CArray<int, 50> a3;	//a2 a3属于不同的类

7.6 类模板与派生、友元和静态成员变量

  1. 类模板与继承:
    - 类模板从类模板派生, 见:实例1
    - 类模板从模板类派生, 见:实例2
    - 类模板从普通类派生, 见:实例3
    - 普通类从模板类派生, 见:实例4
  • 实例1:类模板从类模板派生
#include 
using namespace std;

template<class T1, class T2>
class A{
	T1 v1;
	T2 v2; 
};

template<class T1, class T2>
class B: public A<T2, T1>{
	T1 v3;
	T2 v4;
};

template<class T>
class C: public B<T, T>{
	T v5;
};

int main(){
	B<int, double> obj1;	//实例化出:B(int v3; double v4;)、A(double v1; double v2;) 
	C<int> obj2;	//实例化出:C(int v5)、B(int v3; int v4)、A(int v1; int v2;) 
	
	return 0;
}
  • 实例2:类模板从模板类派生
#include 
using namespace std;

template<class T1, class T2>
class A{
	T1 v1;
	T2 v2; 
};

template<class T>
class B: public A<int, double>{
	T v;
};

int main(){
	B<char> obj1;	//自动生成两个模板类:A、B 

	return 0;
}
  • 实例3:类模板从普通类派生
#include 
using namespace std;

class A{
	int v1; 
};

template<class T>
class B: public A{
//所有从B实例化得到的类,都以A为基类 
	T v;
};

int main(){
	B<char> obj1;	

	return 0;
}
  • 实例4:普通类从模板类派生
#include 
using namespace std;

template<class T>
class A{
	T v1;
	int n; 
};

class B: public A<int>{
	double v;
};

int main(){
	B obj1;	

	return 0;
}
  1. 类模板与友元
    - 函数、类、类的成员函数作为类模板的友元, 见:实例1
    - 函数模板作为类模板的友元, 见:实例2
    - 函数模板作为类的友元, 见:实例3
    - 类模板作用类模板的友元, 见:实例4
  • 实例1:函数、类、类的成员函数作为类模板的友元
void Fun1(){}
class A{};
class B{
	public:
		void Fun(){}
};
template<class T>
class Tmp1{
	friend void Fun1();
	friend class A;
	friend void B::Fun();
};
//任何从Tmp1实例化来的类,都有以上三个友元 
  • 实例2:函数模板作为类模板的友元
#include 
#include 
using namespace std;
template<class T1, class T2>
class Pair{
	private:
		T1 key;
		T2 value;
	public:
		Pair(T1 k, T2 v):key(k), value(v){};
		bool operator < (const Pair<T1, T2> & p) const;
		template<class T3, class T4>
		friend ostream & operator << (ostream & o, const Pair<T3, T4> & p);
}; 
template<class T1, class T2>
bool Pair<T1, T2>::operator < (const Pair<T1, T2> & p) const{
//"小"的意思就是关键字小
	return key < p.key; 
}
template<class T1, class T2>
ostream & operator << (ostream & o, const Pair<T1, T2> & p){
	o << "(" << p.key << "," << p.value << ")";
	return o;
}

int main(){
	Pair<string, int> student("Tom", 29);
	Pair<int, double> obj(12, 3.14);
	cout << student << " " << obj;
	return 0;
}

TIPS:任意从:

template<class T1, class T2>
ostream & operator << (ostream & o, const Pair<T1, T2> & p)

生成的函数,都是任意Pair模板类的友元

  • 实例3:函数模板作为类的友元
#include 
using namespace std;

class A{
	int v;
	public:
		A(int n):v(n){}
		template<class T>
		friend void Print(const T & p);
};
template<class T>
void Print(const T & p){
	cout << p.v;
}
int main(){
	A a(4);
	Print(a);
	return 0;
}

TIPS:所有从

template<class T>
void Print(const T & p)

生成的函数,都成为A的友元,但是自己写的函数:void Print(int a){ }不会成为A的友元

  • 实例4:类模板作为类模板的友元
#include 
using namespace std;

template<class T>
class B{
	T v;
	public:
		B(T n):v(n){}
		template<class T2>
		friend class A;
};
template<class T>
class A{
	public:
		void Func(){
			B<int> o(10);
			cout << o.v <<endl;
		}
};

int main(){
	A<double> a;
	a.Func();
	return 0;
}

TIPS:A类,成了B类的友元。任何从A模板实例化出来的类,都是任何B实例化出来的类的友元

  1. 类模板与静态成员变量(static):
    类模板中可以定义静态成员,那么从该类模板实例化得到的所有类,都包含同样的静态成员
#include 
using namespace std;

template<class T>
class A{
	private:
		static int count;
	public:
		A(){count++;}
		~A(){count--;};
		A(A &){count++;}
		static void PrintCount(){cout << count <<endl;}
};
template<> int A<int>::count = 0;	//声明静态成员,顺便初始化,也可以不初始化
template<> int A<double>::count = 0;
int main(){
	A<int> ia;
	A<double> da;
	ia.PrintCount();
	da.PrintCount();
	return 0;
}

你可能感兴趣的:(c++)