天天看點

C++之類和對象:this指針、構造函數、拷貝、指派和析構

類和對象

    類的基本思想是資料抽象和封裝, 資料抽象是一種把接口和實作分離的程式設計技術。類的接口包括使用者所能夠執行的操作,類的實作包括類的資料成員、負責接口實作的函數體和各種私有函數。

      封裝實作了類的接口和實作的分離。封裝隐藏了類的實作,封裝過後,使用者隻能通路類的接口,而不能通路類的實作。

      類要想要實作資料抽象和封裝,需要首先定義一個抽象資料類型。在抽象資料類型中,由類的設計者負責考慮類的實作過程;使用該類的程式員則隻需要抽象地思考類型做了什麼,而無需了解細節。

《C++ primer 》這本書上的源碼,類Sales_data.h實作

#include<iostream>
#include<string>
using namespace std;
class Sales_data
{
public:
	Sales_data();//聲明一個無參的構造函數
	Sales_data(string b, int u, double p) :bookno(b), units_sold(u), price(p){}//聲明一個有參數的預設構造函數,用參數的初始化表對資料成員初始化
	friend istream& operator >> (istream &, Sales_data &);//運算符>>重載為友元函數
	friend ostream& operator << (ostream &, Sales_data &);//運算符<<重載為友元函數
	friend Sales_data operator + (Sales_data & lhs, Sales_data & rhs);//聲明友元的運算符重載 + 函數
	Sales_data& operator = (const Sales_data &);//重載指派運算符
	friend bool operator == (Sales_data &, Sales_data &);//聲明有元的重載雙目運算符==函數
	Sales_data &operator += (const Sales_data &);//聲明一個傳回sales_item類型的重載運算符+=函數,形參采用常量引用
	double avg_price();
	string isbn() const;//聲明isbn函數,并傳回書編号
	Sales_data& combine(const Sales_data &);//一個combine成員函數,用于将一個Sales_data對象加到另一個對象上
	//聲明Sales_data類的非成員接口函數
	friend istream &read(istream&, Sales_data&);//聲明一個read函數,将資料從istream讀入到Sales_data對象中,函數傳回類型為istream &
	friend ostream &print(ostream&, const Sales_data&);//聲明一個print函數,函數傳回類型為ostream &
	friend Sales_data add(const Sales_data &, const Sales_data &);//一個add的函數,執行兩個Sales_data對象的加法

private:
	string bookno;//書号
	double units_sold;//銷售出的冊數
	double price;//單本售價
	double revenue;//總銷售額
	double average;
};
Sales_data::Sales_data()//定義無參數的構造函數
{
	bookno = "null";
	units_sold = 0;
	price = 0.0;
}

istream& operator >>(istream &input, Sales_data &s)//對重載運算符>>進行定義
{
	input >> s.bookno >> s.units_sold >> s.price;
	if (input)
	{
		s.revenue = s.units_sold * s.price;
	}
	return input;
}

ostream& operator << (ostream &output, Sales_data &s)//對重載運算符<<進行定義
{
	//output << s.bookno << " "<< s.units_sold << " " << s.revenue << " " << s.price << endl;
	output << s.bookno << " " << s.units_sold << " " << s.revenue << " " << s.avg_price() << endl;
	return output;
}

//将兩個sales_item對象相加時,程式應該檢測其兩個對象的isbn書号是否相同
Sales_data operator + (Sales_data & lhs, Sales_data & rhs)//定義重載運算符+函數,lhs和rhs是sales_item的對象
{
	Sales_data ret;
	ret.bookno = lhs.bookno;
	ret.units_sold = lhs.units_sold + rhs.units_sold;
	ret.revenue = lhs.revenue + rhs.revenue;
	ret.avg_price();
	return ret;
}

bool operator == (Sales_data &lhs, Sales_data &rhs)
{
	return lhs.units_sold == rhs.units_sold && lhs.price == rhs.price && lhs.isbn() == rhs.isbn();
}

Sales_data& Sales_data:: operator = (const Sales_data &lhs)//重載指派運算符=
{
	bookno = lhs.bookno;
	units_sold = lhs.units_sold;
	price = lhs.price;
	return *this;
}

Sales_data& Sales_data ::operator += (const Sales_data &rhs)//
{
	units_sold += rhs.units_sold;
	revenue += rhs.revenue;
	return *this;//将this對象作為左值傳回,*this相當于一個sales_item對象
}

//定義一個avg_price()常量成員函數
double Sales_data::avg_price()
{
	average = revenue / units_sold;
	return average;
}

//定義一個isbn()常量成員函數
string Sales_data::isbn() const
{
	return bookno;
}

//一個combine成員函數,用于将一個Sales_data對象加到另一個對象上
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
	units_sold += rhs.units_sold;
	revenue += rhs.revenue;
	return *this;
}
//友元函數read()的具體實作,将資料從istream讀入到Sales_data對象中
istream &read(istream &input, Sales_data &rhs)
{
	input >> rhs.bookno >> rhs.units_sold >> rhs.price;
	rhs.revenue = rhs.units_sold * rhs.price;
	return input;
}
//友元函數add()的具體實作,執行兩個Sales_data對象的加法
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
	Sales_data sum = lhs;
	sum.combine(rhs);
	sum.avg_price();
	return sum;
}
           

this指針(https://blog.csdn.net/it_is_me_a/article/details/82181374)

     通常在class定義時要用到類型變量自身時,因為這個時候還不知道變量名,就用this這樣的指針來使用變量名。

#include<iostream>
using namespace::std;

class Date
{
public:
	void Display()
	{
		cout << _year << endl;
	}
	void setDate(int year)
	{
		_year = year;
	}
private:
	int _year = 2000;
};

int main()
{
	Date firstDate, secondDate;
	firstDate.setDate(2018);
	secondDate.setDate(2019);
	firstDate.Display();
	secondDate.Display();

	return 0;
}
           

      當我們對Date這個類執行個體化出很多對象,這些不僅僅隻限于firstDate和secondDate,但是對象内部包含的成員函數和對象是一樣的,當調用函數setDate對_year的數值進行更改,但是并沒有像普通函數一樣傳遞形參或者形參的位址,但是已得到了結果:

C++之類和對象:this指針、構造函數、拷貝、指派和析構

結果表明:我們并沒有傳遞對象或指針,但是達到了我們預期想要的結果。

原因在于this指針,每個對象都擁有一個隐式this指針,通過this指針來通路自己的位址;每個成員函數都有一個隐式this指針形參》(類構造函數沒有隐含的this指針形參),是編譯器自己處理的,我們不能顯式的添加this指針參數定義,也不能在調用時顯式傳遞對象的位址給this指針。

注意:全局變量、靜态變量麼有this指針。this指針在成員函數開始執行前構造,在成員函數執行結束後清除。

構造函數

       每個類都分别定義它的對象被初始化,類通過一個或者幾個特殊的成員函數來控制其對象的初始化過程,這些函數被稱為構造函數。類可以包含多個構造函數 ,不同的構造函數之間必須在參數數量或參數類型上有所差別。構造函數不能被聲明成const對象。

#include<iostream>
#include<string>
using namespace std;
class Sales_data{
public:
    //兩種重載構造函數
    Sales_data(){
        cout<<"調用無參數構造函數!!!"<<endl;
    }
    Sales_data(const string &s,unsigned n,double p):
               bookNo(s),units_sold(n),revenue(p*n){ 
       cout<<"調用有參數構造函數!!!"<<endl;
    }
private:
    string bookNo;
    unsigned units_sold;
    double revenue;
};

int main()
{
    Sales_data p1;
    Sales_data p2("guo",0,0.0);

    return 0;
}        
           

上述代碼的執行結果:

C++之類和對象:this指針、構造函數、拷貝、指派和析構

拷貝構造函數

如果一個構造函數的第一個參數時自身類類型的引用,且任何額外參數都有預設值,則次函數為拷貝構造函數。

class Foo{
public:
    Foo(); //預設構造函數
    Foo(const Foo&);//拷貝構造函數
    ...
}
           

  注意:拷貝函數的第一個參數必須是一個引用類型。

合成拷貝構造函數: 如果我們沒有為一個類定義拷貝構造函數,則編譯器會為我們定義一個。

       對于某些類而言,合成拷貝構造函數用來阻止我們拷貝該類類型的對象,而一般情況,合成的拷貝構造函數會将其參數的成員逐個拷貝到正在建立的對象中,編譯器從給定對象中依次将每個非static成員拷貝到正在建立的對象中。

拷貝初始化

string dots(10, '.');               //直接初始化
string s(dots);                     //直接初始化
string s2 = dots;                   //拷貝初始化
string null_book = "9-999-8999";    //拷貝初始化
string nines = string(100, '9');    //拷貝初始化
           
  • 直接初始化:要求編譯器使用普通的函數比對來選擇與我們提供的參數最比對的構造函數。
  • 拷貝初始化:要求編譯器将右側運算對象拷貝到正在建立的對象中,如果需要的話,還要進行類型轉換。

除了以上使用=初始化變量時拷貝構造函數,在下列情況下也會發生:

  1. 将一個對象作為實參傳遞給一個非引用類型的形參。
  2. 從一個傳回類型為非引用類型的函數傳回一個對象。
  3. 用花括号清單初始化一個數組中的元素或一個聚合類中的成員。(聚合類是指沒有使用者定義的構造函數,沒有私有和保護的非靜态資料成員,沒有基類,沒有虛函數)。

參數和傳回值

      在函數調用的過程中,具有非引用類型的參數要進行拷貝初始化。當一個函數具有非引用的傳回類型時,傳回值會被用來初始化調用方的結果。

      拷貝構造函數第一個參數必須是引用原因:由于拷貝構造函數被用來初始化非引用類類型的參數。如果其自身參數不是引用類型,則調用永遠也不會成功——為了調用拷貝構造函數,我們必須拷貝它的實參,但為了拷貝實參,我們又必須調用拷貝構造函數,如此無限循環。

拷貝指派運算符

與類控制其對象如何初始化一樣,類也可以控制其對象如何指派

Sales_data trans,accum;
trans = accum;
           

重載指派運算符

      重載指派運算符本質上是函數,其名字由operator關鍵字後接表示要定義的運算符的符号組成。是以,指派運算符就是一個名為operator=的函數。類似于任何其他函數,運算符函數也有一個傳回類型和一個參數清單。

class Foo{
    public:
    Foo& operator=(const Foo&);
    //...
};
           

    值得注意的是,标準庫通常要求儲存在容器中的類型要具有指派運算符,通常應該傳回一個指向其左側運算對象的引用。

合成拷貝指派運算符

      與處理拷貝構造函數一樣,如果一個類未定義自己的拷貝指派運算符,編譯器會為他它生成一個合成拷貝指派運算符。類似拷貝構造函數,對于某些類,合成拷貝構造運算符用來禁止該類型對象的指派。

Sales_data& Sales_data::operator=(const Sales_data &rhs)
{
    bookNo = rhs.bookNo;
    units_sold = rhs.units_sold;
    revenue = rhs.revenue;
    return *this;
}
           

       合成拷貝指派運算符會将右側運算對象的每個非static成員賦予左側運算對象的對應成員,這一工作是通過成員類型的拷貝指派運算符來完成的,對于數組的成員,逐個指派數組元素。合成拷貝指派運算符傳回一個指向其左側運算對象的引用。

析構函數

析構函數是類的一個成員函數,它的作用是釋放對象使用資源,并銷毀對象的非static資料成員。

class Foo{
public:
    ~Foo();//析構函數
    //...
};
           

 析構函數不接受參數,是以它不能被重載。對于一個給定類,隻會有唯一一個析構函數。

下面完整的示範析構函數的執行過程。

#include <iostream>
using namespace std;
class Foo{
public:
	Foo(){
		cout << "調用無參構造函數!!!" << endl;
	}
	~Foo(){
		cout << "調用析構函數!!!" << endl;
	}
private:
	int pi;
};

void test(){
	Foo p1; //建立對象
    //在結束時,由系統自動調用析構函數釋放對象
	cout << "調用test()函數!!!" << endl;
}

int main()
{
	test();
	return 0;
}
           

執行結果:

C++之類和對象:this指針、構造函數、拷貝、指派和析構

在什麼情況下調用析構函數,總結如下:

無論何時調用一個對象被銷毀,就會自動調用其析構函數。

  • 變量在離開其作用域時被銷毀。
  • 當一個對象被銷毀時,其成員被銷毀。
  • 容器(無論是标準庫容器還是數組)被銷毀時,其元素被銷毀。
  • 對于動态配置設定的對象,當對指向它的指針應用delete運算符時被銷毀時。
  • 對于臨時對象,當建立它的完整表達式結束時被銷毀。

由于析構函數自動運作,我們的程式可以按需要配置設定資源,而(通常)無須擔心何時釋放這些資源。

注意:當指向一個對象的引用或者指針離開作用域時,析構函數不會執行。