C++程序設計(九)—— 運算符重載及流類庫

一、運算符重載

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章