Cpp_复习整理_2_模板、重载、输入输出流
书接上回
本篇将会总结一下C++中的模板以及重载两大模块,以及输入输出流。
模板
函数模板
为什么要定义模板?
众嗦粥汁,C++
是一种强类型语言,对于变量的类型必须要显式指出。那么就会存在不灵活的问题。
1 |
|
不难发现,上面这三个函数,除了函数类型
和参数类型
,其余部分完全一样,但是却需要三段几乎相同的函数代码。
所以为了节省代码量,更为了提高代码的适用性,C++
提供了模板template
功能。我们可以定义如下的模板:
1 |
|
函数模板的声明通过关键字template
开始,该关键字之后是使用尖括号< >
括起来的类参/形参表
。
- 每个类型参数之前有
typename
或者class
。 - 至少存在一个类参。
- 类参可以代表的是
内部类型
,也可以是用户自定义的结构体
类型或者类
类型。
这样在函数调用中,模板就能根据所给的实参类型(如int
),进行实例化(如得到int Abs(int a)
)。完成对应的操作。
*关键字class
和typename
的区别
上一篇我们总结了C++
中类的使用,我们清楚class
作用是定义一个类。
而在引入了模板之后,class
作为表示类型参数
的标识符,作为模板声明最初的形式:
1 |
|
但是后来人们发现class
在某些情形下会导致二义性错误。引入了typename
这个关键字,所以模板声明也可以是:
1 |
|
至此,在模板定义的语法中,class
和typename
的作用完全一样。
但是,typename
不仅仅在模板定义中起作用,typename
的另外一个作用为:使用嵌套依赖类型。下方举例:
1 |
|
这里的typename
就是在告诉C++编译器
,在其后方是一个类型的名称,而不是T类型
(此例子中假定为obj类
)中的一个静态成员。如果没有typename
,编译器就无法得知T::myType
的确切含义,存在二义性错误,编译就不会通过。
类模板
1 |
|
这是一个简单的类模板,具有两个类参以及一个形参。形参不一定要在类参的后方。
有了模板还要对它进行实例化来使用。诸如:
1 |
|
这样CType
类中的m_t1Data
和m_t2Data[nMax]
就被分别实例化成了int
型和double
型数组。并且nMax
的值为50
。
类模板中还存在一个函数声明fun
,那么有了声明,肯定还要有定义,模板里的函数要怎么定义呢?
1 |
|
++类模板的成员函数起始于
template
,结束于函数体的右花括号}
。CType<T1,T2,nMax>::
是类模板的“变通的类域分辨符”。且变通的类域分辨符的尖括号中,类参需要去掉
class/typename
,而形参需要去掉int
之类的类型。次序和个数要对应不变。当模板被实例化,
T1,T2,nMax
明确之时就是这个“变通的类域分辨符”固定之时。
注意
由于类模板只有在实例化的时候才被确定,所以有些明显的概念错误可能逃过编译器的检查。因此在建立类模板的时候,最好事先进行特定类的具体编程,然后再选择若干类名作为类型参量,进行“特定类”到“通用类”的升级处理,即反向设计通用类。
重载机制
多态性
程序中总有时候,同一个符号或者名字在不同的情况下具有不同的解释,这就是多态性polymorphism
。
而在面向对象的程序设计语言中,由程序员设计的多态性有两种最基本的形式:1)编译时多态性;2)运行时多态性
- 编译时多态性:在程序编译时便可确定下来的多态性,通过重载机制获得,包括
函数重载
和运算符重载
。 - 运行时多态性:必须等到程序动态运行时才可以确定的多态性,主要通过
继承
+动态绑定
获得。
函数重载
C++
语言中,只要在声明函数原型时形参的个数或者对应位置形参的类型不同,那么两个或更多的函数就可以共用一个名字,在同一作用域中允许多个函数共用一个函数名。C
语言中,不支持重载,所以每个函数必须具有唯一的名字。C
语言中有三个求绝对值的函数:abs()
,labs()
,fabs()
,分别用于处理int
,long
和double
的绝对值。
函数重载的错误情形
编译程序根据实际参数个数以及它们对应的类型,选择调用哪个重载函数,因此重载函数必须在形参个数以及类型上区分。
函数类型
无法区分重载函数!1
2int get_value(int index); //get_value(3);
double get_value(int index); //程序无法确定调用哪个函数。给类型用
typedef
取别名,无法区分重载函数。1
2
3typedef double MONEY;
double calculate(double income);
MONEY calculate(MONEY income); //MONEY和double在编译器眼中是完全一样的引用
&
不能作为重载函数的判定依据。1
2void fun(int value);
double fun(int& value); //出现二义性函数的缺省参数,可以理解为函数重载的一种简化形式。如:
1
int fun(int a,int b=2,int c=3);
- 注意:缺省参数的右边必须 全是 或 没有 参数!即所有缺省参数都得在形参列表的右部。
可对应三种不同的调用方式:
1
2
3int fun(int a,int b, int c);//fun(10,11,12);
int fun(int a,int b); //fun(10,11);等价于fun(10,11,3);
int fun(int a); //fun(10);等价于fun(10,2,3);但是缺省参数也可能导致二义性。如:
1
2
3
4
5void fun(int i);
void fun(int i,int j=10);
fun(3,4);//正常调用fun(int,int)
fun(5);//ERROR,无法确定调用哪个对于
fun(5)
,编译器可以认为调用了单参的fun(5)
,也可以认为调用了带缺省参数的fun(5,10)
,因而出现了二义性错误。所以:如果重载函数的参数设置有默认值,则必须保证使用参数默认值后的调用形式与该函数的其它重载形式不相同。
若想为相同的函数原型提供不同的实现方案,则无法通过函数重载完成。
函数重载的注意点
二义性:是致命的,使得编译程序无法生成目标代码。造成二义性的主要原因:
1)
隐式类型转换;2)
缺省参数。编译程序选择重载函数的规则:绑定次序是
- 最优:精确匹配
- 次优:对实参的类型向高类型转换后的匹配(不丢失精度)
- 最次:对实参的类型向低类型及相容类型转换后的匹配(可以运行,但是会报
Warning
)
1
int -> unsigned int -> long -> unsigned long -> float -> double -> long double
函数重载不可滥用,不适当的重载会降低程序的可读性,只有当函数实现的语义非常相近时才会使用重载。
类定义中也可以使用函数重载,如构造函数,赋值运算符的重载等。
运算符重载
运算符,如果一些情况下可以直接代替函数名,并用运算符的书写形式调用函数。那么这种形式将更容易理解。
例如,在基本数据类型中+
表示整数或浮点数的加法。如果用户设定了自定义类型诸如复数,矩阵等,为了使相应的加法运算也能进行,就需要用到运算符重载。
C++
允许运算符的语义由程序员重新定义,实质上运算符重载就是函数重载。分为两种形式:1)
类成员函数;2)
普通函数。
类内重载
1 |
|
参数表中的参数,受到所重载的运算符约束,数量不可随意指定。
类内重载的话,参数列表中的参数个数要比要求个数少一,类内调用的函数因为默认访问的存在,已经隐式具备了一个
*this
的实参;因此对于二元运算符,只需要显式传递一个右操作数。
使用类内重载的运算符
本质上是由左操作数调用类内的运算符函数,因此,左操作数必须是该类的对象,即保证“左操作数”类型正确。
类外重载
1 |
|
注意:
不与对象联合使用,则无默认的
this
指针,因此,必须显式传递与运算符要求的运算数个数相同的参数。对于类的数据封装性,很多时候数据是不对外部函数开放的,因此有两种解决方法:
1)开放数据为
public
。2)(更好的选择)将函数声明为类的友元,如在类定义中加入如下类似的声明:
1
friend obj operator+(obj left,obj right);
运算符重载的规则
绝大部分的运算符可以重载,只有下列运算符不能重载,它们是:
1)
.
成员访问运算符2)
.*
、->*
成员指针访问运算符3)
::
域运算符4)
sizeof
长度运算符5)
?:
条件运算符6)
#
预处理符号在可重载的运算符中
=
(赋值运算符),[]
(下标运算符),()
(函数调用运算符),->
(通过指针访问类成员的运算符)必须定义为类的成员函数,不能定义为友元或普通函数。而
<<
(流输出),>>
(流输入)则必须定义到类外,不能定义为类的成员函数。(因为这两个运算符的主调对象都是io对象)不能创建新的运算符,只能对已有的运算符进行重载。
重载不能改变运算符的操作数个数。
重载不能改变运算符的优先级别与结合性。
重载的运算符函数的参数列表中不能有默认参数,否则就改变了参数的个数,这与
4.
矛盾。重载运算符必须和类类型或者枚举类型一起使用,其参数至少有一个是类对象(或类对象的引用)。
即参数不能全是C++的标准类型,防止用户修改用于标准类型数据的运算符的性质。
1
2
3int operator +(int a,double b){
return a+b;
}
用于类对象的运算符一般必须重载,但
=
(赋值运算符) 和&
(地址运算符) 不必用户重载。习惯上建议将重载运算符的功能与逻辑,类似于该运算符作用于标准类型数据时所实现的功能。
运算符重载函数可以是类的成员函数,也可以是类的友元函数,还可以是既非类的成员函数也不是友元函数的普通函数。
前置++与后置++的重载
1 |
|
为什么会出现这样的情况呢?
我们知道前置++
和后置++
虽然最终结果都是使对象加一,但是实际的作用过程并不相同。
前置++
将对象的本身作为左值返回,而后置++
将对象运算前的副本作为右值返回。
左值与右值
左值lvalue
:指那些求值结果为对象或函数的表达式。一个表示对象的非常量左值可以作为赋值运算符的左侧运算对象。
右值rvalue
:是指一种表达+式,其结果是值而非所在的位置。
那么问题就解决了,上方代码段中,出现ERROR
的代码都是在进行了a++
之后,对结果进行了其它要求左值的运算,而a++
返回的是一个右值,所以报错“需要左值
”。
两者区别
1 |
|
可以看出前置++和后置++的返回值类型不同。
前置++的返回类型是
Age&
,后置++的返回类型为const Age
。这意味着,前置++返回的是左值,后置++返回的是右值。
a++
的类型是const Age
,自然不能进行前置++、后置++、赋值等操作;而++a
的类型是Age&
,可以进行这些操作。- 问题1:
a++
的返回类型为什么要是const
对象呢?- 如果不是
const
对象,那么(a++)++
就可以通过编译,但是运行结果却违背了直觉。虽然看似进行了两次自增,但是a
实际上只增加了1,因为第二次自增作用在第一次自增执行中产生的一个对象。 - 自定义类型的操作符重载,应该与内置类型保持行为一致。
- 如果不是
- 问题2:
++a
的返回类型为什么是引用呢?- 与内置行为保持一致,为了能和其它运算符组合计算。
- 问题1:
还能看出形参不同。
前置++ 没有形参,而后置++ 有一个
int
形参。但是形参并没有被用到,这个形参只是为了绕过语法的限制。因为没有这个
int
形参,前置和后置的重载便无法区分(函数类型不能作为重载的区分)。实现代码不同。
前置只是简单的完成增加操作之后返回对象的引用即可。
后置则需要先设置一个对象的拷贝,再进行自增,最后返回的是拷贝。
效率不同。
后置自增会产生一个临时对象,会造成一次构造函数和析构函数的额外开销,显然后置效率低于前置。
- 因此:除非必须,均建议使用前置自增。
1
for(int i=0;i<10;++i)//使用++i而不是i++
自减类同自增。
类型转换运算符的重载
C++
支持类型的转换,在内部类型中,支持按照类似的写法进行强制类型转换:
1 |
|
有时,我们也需要将类类型转换成内部类型或者转换成另一个类类型。
那么,我们就需要重载类型转换运算符。
转换成内部类型
1 |
|
转换成类类型
在上一篇中便已经提到,单参的构造函数可以实现类型转换的功能。
注意
- 类型转换函数名的前面,不能指定函数类型
- 类的类型转换函数没有参数(其实是隐含了
this
指针) - 类型转换函数的返回类型,由函数名中指定的类型名来确定。
- 类的类型转换函数只能作为成员函数,因为转换的主体是本类的对象。不能作为友元函数或普通函数。
- 类型转换函数与运算符重载函数相似,都是用关键字
operator
开头,区别在于被重载的是类型名。 - “转换构造函数”和“类型转换函数”有一个共同的功能:当需要的时候,编译系统会自动调用这些函数,建立一个无名的临时对象(或临时变量)。
输入输出流
如果一开始学习的是C
语言,那么对于scanf
和printf
一定不陌生吧,它们分别用于数据的输入和输出。
而到了C++
的学习时,输入输出变成了使用cin
和cout
,配合上运算符>>
和<<
,看起来和C
的输入输出方式大相径庭,但是不管这些,使用流输入输出,很方便不是吗?可以根据变量的类型智能读入数据。如果搞过竞赛的话,相信碰到过超大数据导致TLE
的情况,这时候就有可能cin
与cout
太慢了,于是就会使用下面两句代码来提升读取速度:
1 |
|
这两句可以让cin
和cout
的效率比肩scanf
和printf
,但是怎么实现的姑且不提。
我们主要讲讲C++
中的三种输入输出流:
1)标准I/O - iostream
对系统指定的标准设备的输入和输出。即从键盘
输入数据,输出到显示器屏幕
。
C++
的输入输出流,是指由若干字节组成的字节序列,这些字节中的数据按顺序从一个对象传送到另一个对象,就像水流,电流一样,信息从源到目的端的流动。
- 输入操作时:字节流从输入设备(如键盘,磁盘)流向内存。
- 输出操作时:字节流从内存流向输出设备(如屏幕,打印机,磁盘等)。
流中的内容可以是ASCII字符、二进制形式的数据、图形图像、数字音频视频或其它形式的信息。
输入输出流被定义为类
。使用cin/cout
时,需要包含头文件iostream
,分别对应标准输入流和标准输出流。此外还有cerr
和clog
,都对应于标准错误输出流。
标准输入流
在头文件istream
中定义了输入流对象:cin
。
1)>>
从流中提取数据时,会过滤掉输入流中的不可见字符,如空格
、回车
、制表符Tab
等。
2)只有在输入完数据,按回车键后,该行数据才被送入输入缓冲区,形成输入流,提取运算符>>
才能从中提取数据。
缓冲区例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14#include<iostream>
using namespace std;
int main(){
char str[20];
char a,b;
cout << "Give me a string:";
cin >> a; //输入123456789
cout << "a = " << a << endl;
cin.get(b); //从输入流中获取一个字符
cout << "b = " << b << endl;
cin.getline(str,5); //
cout << "str = " << str << endl;
return 0;
}输出结果为:
1
2
3
4Give me a string:123456789
a = 1
b = 2
str = 3456输入流的一些函数
get
函数,有多种重载int get()
从流中读取一个字符,返回该字符的
ASCII码值
。如果读到文件末尾,返回EOF
,表示end of file
,在控制台中输入Ctrl+Z
表示EOF
。istream& get(char& ch)
从输入流中读取一个字符并存放在
ch
中,返回cin本身。上例中流中本来为
123456789
,get之后a
的值为1
,流中还剩下23456789
。istream& get(char* buffer,int n,char ch='\n')
从输入流中读取n-1个字符,或者遇到第三个参数指定的终止符,并由系统自动补上一个
\0
字符,存放到buffer
指针指向的内存空间中。注意:终止符并不会被读入到buffer中,仍会留在输入流缓冲区中。
1
2
3
4
5
6
7
8
9
10
11
12//例子
#include<iostream>
using namespace std;
int main(){
char str[20],c;
cout << "Enter a sentence:" << endl;
cin.get(str,10,'5');
cin.get(c);
cout << "str[] = " << str << endl;
cout << "c = " << c << endl;
return 0;
}输入为
1234567890
,输出为:1
2
3
4Enter a sentence:
1234567890
str[] = 1234
c = 5可见
5
仍在输入流中,直到被c
读取。
getline
函数(istream
类的成员函数)istream& getline(char* buffer,int n,char ch='\n'
,有3个参数第一个参数为由谁获取数据。
第二个参数为读取长度,但是实际读取到
str
中的字符长度会-1,因为要在字符串末尾留出一个位置添加\0
。第三个参数是截止符号,即读取到该字符时,不管是否已经到读取长度,均停止读取。该参数可以缺省,默认值是
\n
。注意:
getline
和get
的区别是getline遇到结束符会将该结束符从缓冲区中丢弃!buffersize
最好与目标数组大小一致!目标容量大于buffersize
会导致空间浪费,而目标容量小于buffersize
则是致命的,会爆数组。
getline
函数(全局函数)getline(cin,string串,'\n')
,用于读取整行数据,并且第二个参数为string
类对象。第三个参数为可缺省的结束符,缺省值为'\n'
。ignore
函数istream& ignore(int n=1,int delim=EOF);
作用:跳过输入流中的
n
个字符,或在遇到指定的终止字符时提前结束(会跳过包括delim
在内的若干字符)。在使用时,因为
ignore
两个参数都具有默认值,所以实际效果,cin.ignore()
和cin.get()
并无二致。read
函数istream& read(char* buffer,int n);
从输入流中读取
n
个字符,注意是n个,并不会在字符串末尾补’\0’。且没有结束符,必须读入n个字符,如果读取字符不足会继续等待输入。
3)需要注意保证从流中读取的数据能正常运行。
Tip:
cin
本质上与其它C++
变量一样,cin
也是一个变量名,它是一个istream
类型的对象。
而变量名通常代表着一段内存区域,cin
也映射到一段内存区域(也就是输入缓冲区)。
简单理解为键盘输入的信息先存放在cin
中,再被流提取运算符>>
提取到内存中。
标准输出流
在头文件ostream
中定义了输出流对象:cout
。
1)使用它不必考虑数据是什么类型,系统会自动判断数据的类型,根据其类型选择调用与之匹配的的运算符重载函数。
2)输出流也开辟了一个缓冲区,用来存放流中的数据,当缓冲区满时输出到屏幕。
endl
和\n
的区别这两个在输出的效果都是换行,但两者本质上是不同的。
endl
实际上是一个函数模板,作为一个IO操作符
,当输出缓冲区中插入一个endl
,则不论缓冲区是否已满,都将立即输出流中的所有数据,然后插入一个换行符,并且此时缓冲区已被清空。
而
\n
只是一个字符,与其它ASCII字符
无异。
输出流的一些函数
put
函数,输出单个字符write
函数,输出指定的字符串tellp
函数,用于获取当前输出流指针的位置seekp
函数,用于设置输出流指针的位置
*流操纵算子
流操纵算子 | 作用 |
---|---|
dec |
以十进制输出整数 |
hex |
以八进制输出整数 |
oct |
以十六进制输出整数 |
fixed |
以普通小数的形式输出浮点数 |
scientific |
以科学计数法形式输出浮点数 |
left |
左对齐,即宽度不足时将填充字符添加到右边 |
right |
右对齐,即宽度不足时将填充字符添加到左边 |
setbase(b) |
设置输出整数时的进制,b=8、10或16 |
setw(w) |
指定输出宽度为w或者读入w个字符(该算子所起的作用是一次性的) |
setfill(c) |
设置填充字符,即宽度不足时用c 填充,默认是空格 |
setprecision(n) |
设置浮点数的精度为n 在与fixed 算子配合使用的情况下,n表示小数位数。 |
setiosflags(flag) |
设置某一个标志的值为1 |
resetiosflags(flag) |
设置某一个标志的值为0 |
以下是setiosflags(flag)
和resetiosflags(flag)
中flag
可以为的值:
标志 | 作用 |
---|---|
ios::left |
输出数据在本域宽范围内向左对齐 |
ios::right |
输出数据在本域宽范围内向右对齐 |
ios::internal |
数值的符号位在域宽内向左对齐,数值向右对齐,中间由填充字符填充 |
ios::dec |
设置整数的基数为10 |
ios::oct |
设置整数的基数为8 |
ios::hex |
设置的整数的基数为16 |
ios::showbase |
强制输出整数的基数(八进制以0开头,十六进制以0x开头) |
ios::showpoint |
强制输出浮点数的小数点和尾数0 |
ios::uppercase |
在以科学计数法格式E和以十六进制输出字母时以大写表示 |
ios::showpos |
对正数显示”+”号 |
ios::scientific |
浮点数以科学计数法格式输出 |
ios::fixed |
浮点数以定点格式(小数形式)输出 |
ios::unitbuf |
每次输出后刷新所有的流 |
ios::stdio |
每次输出后清除stdout 和stderr |
重载>>
和<<
运算符
我们已经学习了C++
中的类,那对类的数据的输入输出是借助类的成员函数实现的,比如我们手上有一个复数类Complex
类:
1 |
|
但是这不符合使用习惯,我们更希望能写成这样的形式,就跟对普通变量的输入输出一样:
1 |
|
那么我们就可以对<<
和>>
运算符进行重载。
用<<
来举个例子:
1 |
|
它有两个含义:
<<
是一个二目运算符,进行了两次调用。第一次左操作数为cout
,右操作数为x
;第二次左操作数为cout
,右操作数为y
。- 这里是对象
cout
调用了ostream
类的成员函数operator<<()
。
据此,当我们对cout
进行重载的时候应该可以得出以下结论:
- 不能将运算符的重载函数定义为Complex类的成员函数,须定义为外部函数。如果访问了私有成员,还必须声明为友元。这是因为
<<
运算符是由ostream
对象cout
来调用的。 - 重载函数的函数类型应当为引用,目的是能够连续调用,实现连续输出。
- 重载函数的两个参数都应当为引用,并且要被输出的对象应当为常引用,防止数据被篡改。
所以,我们就能写出符合需求的重载函数:
1 |
|
而运算符>>
与之类似,只不过是ostream
变成了istream
。
2)文件I/O - fstream
文件I/O不再对系统默认的I/O设备进行输入输出,因此<fstream>
头文件中没有预先定义好的类似cin
和cout
的对象,需要用户自行定义文件流对象(fin
和fout
等),作为文件I/O流的载体。用户可以通过如下的语句,来定义文件对象:
1 |
|
这样创建一个文件输入对象和一个文件输出对象,但是两者都没有绑定到对应的文件,因此无法完成任何输入输出操作。我们可以通过fin.open("in.txt");
,即open函数来绑定一个文件,或者在定义对象时使用构造函数直接绑定一个文件ofstream fout("out.txt");
。
当我们绑定了一个文件之后,还要指定文件的使用方式是诸如只读、只写、既读又写、在文件末尾添加数据、以文本方式使用、以二进制方式使用等等。我们可以使用诸如myFile("out.txt",ios::out | ios::app);
等语句,该语句表示myFile
绑定了out.txt
,并以“输出”和“追加”的方式使用 。以下是模式标记表格:
模式标记 | 适用对象 | 作用 |
---|---|---|
ios::in |
ifstream fstream |
打开文件用于读取数据,如果文件不存在,则打开失败。 是ifstream 和ofstream 对象的默认值 |
ios::out |
ofstream fstream |
打开文件用于读取数据,如果文件不存在,则新建文件。 如果文件已经存在,则打开时会清空文件的内容,除非带有ios::in 或者ios::app 是ifstream 和ofstream 对象的默认值 |
ios::ate |
ifstream |
打开一个文件,并将文件的读指针指向文件末尾。如果文件不存在, 则打开失败。并且,文件打开时,不会清空文件。 ate 是at the end的缩写 |
ios::app |
ofstream fstream |
打开文件,将文件写指针置于文件的EOF 处,用于在文件末尾添加数据,如果 文件不存在,则新建该文件。如果顺利打开,则不清空文件。 |
ios::trunc |
ofstream |
打开文件时会清空内部存储的所有数据(如果文件已存在,则先删除该文件) 单独使用时与ios::out 相同 |
ios::binary |
ifstream ofstream fstream |
以二进制的方式打开文件,缺省时以文本文件打开 |
使用完文件对象之后或者要换一个文件进行绑定,必须要先解除原来绑定的文件,通过close
函数进行:
1 |
|
seekp、seekg和tellp、tellg
seekp
将输出文件中写指针移到指定的位置原型为
ostream& seekp(int offset,int mode);
将指针从
mode
处开始,移动offset
个字节。mode
为文件读写指针的设置模式,有三种选项:mode标志 描述 ios:beg
(默认值)从文件头开始计算偏移量( offset=0
为文件开头,offset
为非负数) 即ostream& seekp(int offset);
ios::end
从文件末尾开始计算偏移量( offset=0
为文件尾,offset
为0或负数)ios::cur
从当前位置开始计算偏移量( offset>0
往文件尾部移动,offset<0
往文件头部移动)seekg
将输入文件中指针移到指定的位置原型为
istream& seekg(int offset,int mode);
作用效果与
seekp
一直,只不过是读指针。tellp
返回输出指针的当前位置tellg
返回输入指针的当前位置
Tips:g->get;p->put。
3)串I/O - stringstream
暂不做讨论。