C++程序设计(九)—— 运算符重载及流类库

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Alexshi5/article/details/82826430

一、运算符重载

1、重载对象的赋值运算符

        编译器在默认情况下为每个类生成一个默认的赋值操作,用于同类的两个对象之间赋值。默认的含义是逐个为成员赋值,即将一个对象成员的值赋给另一个对象相应的成员,这种赋值方式对于有些类可能是不正确的。假设类Str的数据成员char *st,则下面的语句经赋值后是有问题的:

Str s1("hello"),s2("world");
s2 = s1;

         经赋值后,s2.st和s1.st是同一块存储地址,当它们的生存周期结束时,存储“hello”的变量被删除了两次,这是个严重的错误。另外,对于s1 = s1的情况,也应不执行赋值操作。因此,程序必须为Str类定义自己的赋值操作符“=”,可使用如下的方法实现:

Str& Str::operator=(Str &s){
    if(this == &s){//防止s=s这样的赋值
        return *this;
    }
    delete st;
    st = new char[strlen(s.st)+1];//申请内存
    strcpy(st,s.st);//将对象s的数据复制一份到申请的内存区
    return *this;//返回this指针指向的对象
}

         上面这个方法必须使用引用参数,关键字operator和=一起表示一个运算符函数,这里可以将operator=整体上视为一个函数名,这样的话就可以把它声明为:

Str& operator= (Str &);

 即函数operator=(Str &)返回Str类对象的引用,它在定义时可以写成如下形式:

Str& Str::operator=(Str &s){
    //函数体
}

调用函数时写成如下形式:

Str s1,s2;
s2.operator=(s1);

 但由于要实现的是=的作用,所以这里可以简写为如下形式:

Str s1,s2;
s2=s1

 下面是一个完整的示例:

class Str{
private:
	char *st;
public:
	Str(char *);//使用字符指针的构造函数
	Str(Str &);//使用对象引用的构造函数
	Str& operator=(char *);//重载使用字符指针的赋值运算符
	Str& operator=(Str &);//重载使用对象引用的赋值运算符
	void print(){
		cout << st << endl;
	}
	~Str(){
		delete st;
	}
};

Str::Str(char *c){
	st = new char[strlen(c)+1];//字符数组以\0结尾,需要多申请一个字符长度的内存空间
	strcpy(st,c);//将字符串复制到内存区st
}

Str::Str(Str &s){
	st = new char[strlen(s.st)+1];//申请内存
	strcpy(st,s.st);//将对象s的字符串复制到申请的内存区
}

Str& Str::operator=(char *c){
	delete st;//先释放内存空间
	st = new char[strlen(c)+1];//重新申请内存
	strcpy(st,c);//将字符串c复制到内存区
	return *this;
}

Str& Str::operator=(Str &s){
	if(this == &s){//防止s=s这样的赋值
		return *this;
	}
	delete st;//先释放内存
	st = new char[strlen(s.st)+1];//重新申请内存
	strcpy(st,s.st);//将对象s的字符串复制到新申请的内存区
	return *this;//返回this指针指向的对象
}
#include <string>
#include <string.h>
#include "Example1.h"
void example1();
int main() {
	example1();
	return 0;
}

void example1() {
	Str s1("hello"), s2("world"), s3(s1);
	s1.print();
	s2.print();
	s3.print();
//	hello
//	world
//	hello
	s2 = s1 = s3;
	s3 = "other";
	s1.print();
	s2.print();
	s3.print();
//	hello
//	hello
//	other
}

 2、运算符重载的实质

        在上面的示例中,当编译器处理s1=s2时,就会去看有没有operator=()为型的函数,如果系统定义了一个这样的函数,那么就调用这个函数,所以运算符重载其实就是函数重载,要重载某个运算符,只需要重载相应的函数就可以了。与其他重载不同的是,它需要使用一个新的关键字operator和一个运算符连用,构成一个运算符函数名。

         一般情况下,为用户定义的类型重载运算符都要求能够访问这个类型的私有成员,所以只能两条路可走:一是将运算符重载为这个类型的成员函数;二是将运算符重载为这个类型的友元。

        C++中的运算符大部分都可以重载,不能重载的只有. :: *和? : ,前面三个是因为在C++中有特定的含义,不准重载以避免不必要的麻烦;后面两个则是因为不值得重载。另外sizeof和#不是运算符,也不能重载,而且= () [] -> 这4个运算符只能用类运算符来重载。

3、<<、>>和++运算符重载

       <<和>>在重载时,操作符左边是流对象的别名而不是操作对象,运算符跟在流对象的后面,它们要直接访问类的私有数据,而且流是标准类库,用户只能继承不能修改,更不能是流库的成员,所以它们必须作为类的友元来进行重载。

       插入符函数的一般形式如下:

ostream& operator<<(ostream& output,类名& 对象名){
    //函数体
    return output;//output是类ostream对象的引用,它是cout的别名,即ostream& output = cout
}

        提取符函数的一般形式如下:

istream& operator >>(istream& input,类名& 类对象){
    //函数体
    return input;//input是类istream对象的引用,它是cin的别名,即istream& input = cin
}

         使用友元函数重载运算符<<

class Test{
private:
	char c;
	int i;
public:
	Test(char c1,int i1):c(c1),i(i1){

	}
	friend ostream& operator<<(ostream& output,Test& t);
};

ostream& operator<<(ostream& output,Test& t){
	output << t.c;
	output << t.i;
	output << endl;
	return output;
}
#include "Example2.h"
void example2();
int main() {
	example2();
	return 0;
}

void example2(){
	Test t1('W',10);
	//使用函数形式调用
	operator<<(cout,t1);
	//使用简写形式调用
	cout << t1;
}

         使用类运算符重载++运算符,示例如下:

class Test1{
private:
	int num;
public:
	Test1(int a):num(a){}
	int operator++();//前缀++
	int operator++(int);//后缀++
	void print(){
		cout << num << endl;
	}
};

int Test1::operator ++(){
	num++;
	return num;
}

int Test1::operator ++(int){//不用给出形参名
	int i = num;
	num++;
	return i;
}
#include "Example3.h"
void example3();
int main() {
	example3();
	return 0;
}

void example3(){
	Test1 t2(10);
	t2.print(); //10
	//使用函数调用形式,前缀++
	int i = t2.operator ++();
	cout << i << endl; //11
	//不使用函数调用形式,前缀++
	int j = ++t2;
	cout << j << endl; //12

	Test1 t3(10);
	t3.print(); //10
	//使用函数调用形式,后缀++
	int k = t3.operator ++(0);
	cout << k << endl; //10
	//不使用函数调用形式,后缀++
	int t = t3++;
	cout << t << endl; //11
}

         有些C++编译器不区分前缀或后缀运算符,这时只能通过对运算符函数进行重载来反映其为前缀或后缀运算符。需要注意的是,不能自己定义新的运算符,只能把C++原有的运算符用到自己设计的类上面去。同时,经过重载,运算符并不改变原有的优先级,也不改变它所需的操作数数目。当不涉及到定义的类对象时,它仍然执行系统预定义的运算,只有用到自己定义的对象上,才执行新定义的操作。

4、类运算符和友元运算符的区别

        如果运算符所需的操作数(尤其是第一个操作数)希望进行隐式的类型转换,则运算符应通过友元来重载;如果一个运算符的操作需要修改类对象的状态,则应当使用类运算符,这样更符合数据封装的要求。但参数是使用引用还是对象,则要根据运算符在使用中的情况来定。

        如果对象作为重载运算符函数的参数,则可以使用构造函数将常量转化为该类型的对象;如果使用引用作为参数,因为这些常量不能作为对象名使用,所以编译系统就会报错。再使用时,一定分清楚场合及使用方法。

        成员运算符比友元运算符少一个参数,这是因为成员运算符具有this指针。

5、下标运算符“[ ]”的重载

class TestArray{
private:
	int _size;
	int* data;
public:
	TestArray(int);
	~TestArray(){
		delete []data;//释放数组所占的内存空间
	}
	const int size(){
		return _size;
	}
	int& operator[](int);//使用类运算符来进行重载
};

TestArray::TestArray(int a){
	if(a<1){
		cout << "数组初始化长度不能小于1" << endl;
		exit(1);//退出程序
	}
	this->_size = a;
	this->data = new int[a];
}

int& TestArray::operator[](int b){
	if(b<0||b>_size-1){//检查数组是否越界
		cout << "数组越界" << endl;
		delete []data;
		exit(1);
	}
	return data[b];
}
#include "Example4.h"
void example4();
int main() {
	example4();
	return 0;
}

void example4(){
	TestArray t(10);
	cout << "数组的大小是:" << t.size() << endl;
	//给数组赋值
	for(int i=0;i<t.size();i++){
		t[i] = 10*(i+1);
	}
	//循环输出数组内容
	for(int j=0;j<t.size();j++){
		cout << t[j] << " ";
	}
//	数组的大小是:10
//	10 20 30 40 50 60 70 80 90 100
}

 二、流类库

        C++的流类库由几个进行I/O操作的基础类和几个支持特定种类的源和目标的I/O操作的类组成。

1、流类库的基础类

        在C++中,输入输出是通过流来完成的,C++的输出操作将一个对象的状态转换成一个字符序列,输出到某个地方。输入操作也从某个地方接收到一个字符序列,然后将其转换成一个对象的状态所要求的格式。这看起来像数据在流动,于是把接收输出数据的地方叫做目标,把输入数据的地方叫做源,而输入输出操作可以看作字符序列在源、目标以及对象之间的流动。C++把与输入和输出有关的操作定义为一个类体系,把执行输入输出操作的类体系叫做流类,而提供这个流类实现的系统库就叫做流类库。

        C++的流类库预定义了4个流,它们是cin、cout、cerr和clog,可以将cin视作类istream的一个对象,而将cout视为ostream的一个对象。

        流是一个抽象概念,当实际进行I/O操作时,必须将流和一种具体的物理设备联接起来。C++的流类库预定义的4个流所联接的具体设备为:

cin 与标准输入设备相联接
cout 与标准输出设备相联接
cerr 与标准错误输出设备相联接(非缓冲方式)
clog 与标准错误输出设备相联接(缓冲方式)

         操作系统在默认情况下,指定标准输出设备是显示终端,标准输入设备是键盘。

2、默认输入输出格式控制

        关于数值数据,默认方式能够自动识别浮点数并用最短的格式输出,例如将5.0作为5输出,输入3.4e+2就会输出340等。还可以将定点数分成整数和小数部分,示例如下:

void example5(){
	double d = 5.0;
	cout << d << endl; //5

	double d1 = 3.4e+2;
	cout << d1 << endl; //340

	int a;double b;
	cin >> a >> b; //将定点数分为整数和小数部分,比如输入:20.55
	cout << a << " " << b; //输出20 0.55
}

        特别要注意字符的读入规则,对单字符来讲,它将舍去空格,直到读到字符为止。示例如下:

void example6(){
	//读取单字符
	char z;
	cin >> z; //输入字符: 1
	cout << z << endl; //输出:1(舍去空格,直到读取到字符)

	//读取连续的字符
	char a,b,c;
	cin >> a >> b >> c; //输入:123
	cout << a << " " << b << " " << c; //输出1 2 3
}

        对字符串来说,它从读到第一个字符开始,到空格符结束。对于字符数组,使用数组名来整体读入。示例如下:

void example7(){
	//读取字符数组
	char c[8];
	cin >> c; //输入:helloworld (遇到空格就结束)
	c[7] = '\0'; //为了避免赋值的数组越界,应强制在字符数组末尾加上结束符,因为字符数组只有在初始化赋值时才会加上结束符,其他时候由系统决定
	cout << c << endl; //输出hellowo (结束符不会输出)

	//读取字符串
	string s;
	cin >> s; //输入:hello world
	cout << s << endl; //输出:hello (遇到空格就结束)
}

        关于字符数组与数组越界问题可以参考: https://blog.csdn.net/u010355144/article/details/44976495

        对于字符指针,尽管为它动态的分配了地址,但也只能采取逐个赋值的方法,它不仅不以空格结束,反而舍弃空格(读到字符才计数)。因为字符串没有结束位,所以将字符串作为整体输出时,有效字符串的后面将会出现乱码,这时要手工字符串结束符来消除乱码。示例如下:

void example8(){
	const int size = 5;
	char *p = new char[size]; //声明char类型的数组指针
	for(int i=0;i<size-1;i++){ //输入:he llo
		cin >> *(p+i);
	}
	p[size-1] = '\0';
	cout << p; //输出:hell (忽略了空格,一直读取字符,直到遇到结束符)
}

         对于布尔型的值,如果输入为0则是false,否则均为true。输出的时候则只有0和1两个值。示例如下:

void example9(){
	bool b = 0;
	string s;
	s = b?"yes":"no";
	cout << s << endl; //no
	cout << b << endl; //0

	bool b1 = true;
	string s1;
	s1 = b1?"yes":"no";
	cout << s1 << endl; //yes
	cout << b1 << endl; //1
}

三、文件流

        在C++里,文件是通过流来完成的,C++有输入文件流(ifstream)、输出文件流(ofstream)和输入输出文件流(fstream)三种,并已经将它们进行标准化。

1、使用文件流

        示例如下:

#include <fstream>
void example10();
int main() {
	example10();
	return 0;
}

void example10(){
	const int size = 10;
	char ch[size];
	char *p = "hello";
	ofstream out;//建立文件输出流
	out.open("test.txt");//打开一个测试的文件
	out << p;
	out << "world";
	out.close();//关闭输出流

	ifstream in("test.txt");//建立文件流
	for(int i=0;i<size+1;i++){//循环输入字符
		in >> ch[i];
	}
	ch[size] = '\0';//将最后一个字符置为结束符
	in.close();//关闭输入流
	cout << ch;//helloworld
}

2、几个典型流成员的函数

⑴ 输出流的open函数

        它有3个参数,第1个是要打开的文件名,第2个是文件的打开方式,第3个是文件的保护方式,一般都使用默认值。第2个参数可以取如下的值:

ios_base::in 打开文件进行读操作,可避免删除现存文件的内容
ios_base::out 打开文件进行写操作,这是默认模式
ios_base::ate 打开一个已有的输入或输出文件并查找到文件尾
ios_base::app 打开文件以便在文件的尾部添加数据
ios_base::binary 指定文件以二进制方式进行打开,默认为文本方式
ios_base::trunc 如文件存在,将其长度截断为0并清除原有内容

        以上的几个参数,除app外,在其他几种方式下,文件刚打开时,指示当前读写位置的文件指针都定位于文件的开始位置;而app使用文件当前的写指针位于文件末尾。这几种方式也可通过“或”运算符“|”同时使用。

⑵ 输入流的open函数

        可以使用默认构造函数建立对象,然后调用open成员函数打开,示例如下:

ifstream in;
in.open("filename",iosmode);

        或者使用参数构造函数指定文件名和打开模式,示例如下:

ifstream in("filename",iosmode);

         还可以使用指针,示例如下:

ifstream *in;
in->open("filename",isomode);

        其中,输入流的打开模式如下:

ios_base::in 打开文件用于输入,默认模式
ios_base::binary 指定文件以二进制方式打开,默认为文本方式

⑶ close成员函数

        close成员函数用来关闭与一个文件流相关联的磁盘文件,如果没有关闭该文件,因为文件流是对象,所以在文件流对象的生存期结束时,将自动关闭该文件。不过,应养成及时关闭文件的习惯。

⑷ 错误处理函数

        在对一个流对象进行I/O操作时,可能会产生错误,当错误发生时,可以使用文件流的错误处理成员函数进行错误类型判别。成员函数及其功能如下:

bad() 如果进行非法操作,返回true,否则返回false
clear() 设置内部错误状态,如果用缺省参量调用则清除所有错误位
eof() 如果提取操作已经到达文件尾,则返回true,否则返回false
good() 如果没有错误条件和没有设置文件结束标志,返回true,否则返回false
fail() 与good相反,操作失败返回false,否则返回true
is_open() 判定流对象是否成功的与文件关联,如果是返回true,否则返回false

猜你喜欢

转载自blog.csdn.net/Alexshi5/article/details/82826430