1. 程式人生 > >【C++】類和物件(4)

【C++】類和物件(4)

一、類的六個預設成員函式

下面是一個Date類,但是它類中什麼成員也沒有,這就是空類。但是它雖然看起來什麼都沒有,實際上不是的,在我們什麼都不寫的情況下,它會自動生成六個預設的成員函式。如圖所示的建構函式、解構函式、拷貝建構函式、賦值過載函式、普通物件取地址函式、const物件取地址函式這六個函式,就算我們自己不寫,也會預設生成。

class Date {};

 

二、建構函式 

建構函式雖然名字是構造,但是其實它並不是構造了物件,而是完成了物件的初始化。我們以Date類為例:

class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
			}
private://封裝起來不允許隨便修改
	int _year;//年   //用_區分成員變數和引數
	int _month;//月
	int _day;//日
};

int main()
{
	Date d1;
	d1.Init(2018, 11, 11);//用Init函式初始化來間接修改值
	d1.Print();

	Date d2;
	d2.Init(2019, 2, 4);
	d2.Print();

	system("pause");
	return 0;
}

這是之前寫的一個簡單的Date類,可以通過 Init 函式 給物件初始化設定內容,但是如果每次建立物件都呼叫該方法設定資訊,未免有點麻煩,那能否在物件建立時,就將資訊設定進去呢?所以說C++就給出了建構函式。

建構函式是一個特殊的成員函式,名字與類名相同,建立類型別物件時由編譯器自動呼叫,保證每個資料成員
都有 一個合適的初始值,並且在物件的生命週期內只調用一次。

 

建構函式的特性

1.函式名與類名相同

2.無返回值

3. 物件例項化時編譯器自動呼叫對應的建構函式。

class Date
{
public:
	Date()
	{
		_year = 2018;
		_month = 11;
		_day = 18;
	}
};

4. 建構函式可以過載。

class Date
{
public:
	Date()
	{
		_year = 2018;
		_month = 11;
		_day = 18;
	}
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
};

int main()
{
	Date d1;//無參呼叫預設建構函式
    //Date d1();錯誤,無參呼叫不能帶括號
	d1.Print();

	Date d2(2019, 1, 1);//帶參呼叫自定義建構函式
	d2.Print();

	system("pause");
	return 0;
}

5. 如果類中沒有顯式定義建構函式,則C++編譯器會自動生成一個無參的預設建構函式,一旦使用者顯式定義編譯器將不再生成。

6. 無參的建構函式和全預設的建構函式都稱為預設建構函式,並且預設建構函式只能有一個。注意:無參建構函式、全預設建構函式、我們沒寫編譯器預設生成的建構函式,都可以認為是預設成員函式。

如下,有兩個預設建構函式,就會引起呼叫歧義。

// 預設建構函式
class Date
{
public:
    Date()
{
    _year = 1900 ;
    _month = 1 ;
    _day = 1;
}
   Date (int year = 1900, int month = 1, int day = 1)
{
    _year = year;
    _month = month;
    _day = day;
}
private :
    int _year ;
    int _month ;
    int _day ;
};

void Test()
{
   Date d1;
}

7.C++把型別分成內建型別(基本型別)和自定義型別。內建型別就是語法已經定義好的型別:如int/char...,自定義型別就是我們使用class/struct/union自己定義的型別。預設的建構函式並不是一點卵用都沒有,針對自定義型別它會進行初始化,呼叫它的預設成員函式,而對於內建型別(基本型別)來說,它並不會初始化,所以我們看到的是隨機值。

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = _minute = _second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	
private:

	//1.基本型別/內建型別
	int _year;
	int _month;
	int _day;

	//2.自定義型別
	Time _t;
};

 

三、解構函式

與建構函式功能相反,解構函式不是完成物件的銷燬區域性物件銷燬工作是由編譯器完成的。而
物件在銷燬時會自動呼叫解構函式完成類的一些資源清理工作

 

解構函式的特性

1. 解構函式名是在類名前加上字元 ~。

2. 無引數無返回值。沒有過載版本。

3. 一個類有且只有一個解構函式。如果不寫,系統會自動生成預設的解構函式。

4. 物件生命週期結束時,C++編譯系統系統自動呼叫解構函式。

class SeqList
{
public:
	SeqList(size_t capacity=10)
	{
		_a = (int*)malloc(10 * sizeof(int));
		_size = 0;
		_capacity = capacity;
	}

	~SeqList()
	{
		free(_a);//釋放堆上的空間
		_a = nullptr;//指標置為空
		_size = _capacity = 0;
	}

private:
	int *_a;
	size_t _size;
	size_t _capacity;
};

int main()
{
	SeqList s1;
	system("pause");
	return 0;
}

5.編譯器生成的預設解構函式,會對自定型別成員呼叫它的解構函式。

class String
{
public:
	String(const char* str = "jack")
	{
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
	~String()
	{
		cout << "~String()" << endl;
		free(_str);
		system("pause");
	}
private:
	char* _str;
};

class Person
{
private:
	String _name;
	int _age;
};

int main()
{
	Person p;
	return 0;
}

 

四、拷貝建構函式

現實生活中,一模一樣的兩個人我們成為雙胞胎,那麼類的物件在構造時候能不能建立一個與一個物件一模一樣的新物件呢?

拷貝建構函式只有單個形參,該形參是對本類型別物件的引用(一般常用const修飾),在用已存在的類型別物件建立新物件時由編譯器自動呼叫。

拷貝建構函式的特性

1. 拷貝建構函式是建構函式的一個過載形式

2. 拷貝建構函式的引數只有一個且必須使用引用傳參使用傳值方式會引發無窮遞迴呼叫

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2018,11,11);
	d1.Print();

	Date d2(d1);
	d2.Print();

	system("pause");
	return 0;
}

 

3. 若未顯示定義,系統生成預設的拷貝建構函式。 預設的拷貝建構函式物件按記憶體儲存按位元組序完成拷
貝,這種拷貝我們叫做淺拷貝,或者值拷貝

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2018,11,11);
	d1.Print();

	Date d2(d1);//預設拷貝構造
	d2.Print();

	system("pause");
	return 0;
}

 

五、賦值運算子過載

在說賦值運算子過載之前我們先了解下運算子過載。

運算子過載

C++為了增強程式碼的可讀性引入了運算子過載,運算子過載是具有特殊函式名的函式,也具有其返回值類
型,函式名字以及引數列表,其返回值型別與引數列表與普通的函式類似。

函式名字為:關鍵字operator後面接需要過載的運算子符號

函式原型:返回值型別 operator操作符(引數列表)。

注意:

1>不能通過連線其他符號來建立新的操作符:比如[email protected]

2>過載操作符必須有一個類型別或者列舉型別的運算元

3>用於內建型別的操作符,其含義不能改變,例如:內建的整型+,不 能改變其含義

4>作為類成員的過載函式時,其形參看起來比運算元數目少1成員函式的操作符有一個預設的形參this,限定為第一個形參

5> .* ::sizeof?: . 注意以上5個運算子不能過載

 

這裡會發現運算子過載成全域性的就需要成員變數是共有的,為了保證封裝性可以過載成成員函式。

// 全域性的operator==
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
   _year = year;
   _month = month;
   _day = day;
}
//private:
   int _year;
   int _month;
   int _day;
};
// 這裡會發現運算子過載成全域性的就需要成員變數是共有的
// 為了保證封裝性可以過載成成員函式。
bool operator==(const Date& d1, const Date& d2)
{
   return d1._year == d2._year;
       && d1._month == d2._month
       && d1._day == d2._day;
}
void Test ()
{
   Date d1(2018, 9, 26);
   Date d2(2018, 9, 27);
   cout<<(d1 == d2)<<endl;
   cout<<operator==(d1,d2)<<endl;
}

bool operator==(Date* this, const Date& d2)
這裡需要注意的是,左運算元是隱含指標this指向的呼叫函式的物件,所以只用寫一個引數。

class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
   _year = year;
   _month = month;
   _day = day;
}
// bool operator==(Date* this, const Date& d2)
// 這裡需要注意的是,左運算元是this指向的呼叫函式的物件
bool operator==(const Date& d2)
{
      return _year == d2._year;
          && _month == d2._month
          && _day == d2._day;
}
private:
   int _year;
   int _month;
   int _day;
};
void Test ()
{
   Date d1(2018, 9, 26);
   Date d2(2018, 9, 27);
   cout<<d1.operator==(d2)<<endl;
   cout<<(d1 == d2)<<endl;
}

賦值運算子過載

class Date
{
public :
Date(int year = 1900, int month = 1, int day = 1)
{
   _year = year;
   _month = month;
   _day = day;
}
Date (const Date& d)
{
   _year = d._year;
   _month = d._month;
   _day = d._day;
}
Date& operator=(const Date& d)
{
   if(this != &d)
     {
       _year = d._year;
       _month = d._month;
       _day = d._day;
     }
}
private:
   int _year ;
   int _month ;
   int _day ;
};

賦值運算子主要有四點:

1. 引數型別

2. 返回值

3. 檢測是否自己給自己賦值

4. 返回*this

5. 一個類如果沒有顯式定義賦值運算子過載,編譯器也會生成一個,完成物件按位元組序的值拷貝。

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(2018,10,1);
	// 這裡d1呼叫的編譯器生成operator=完成拷貝,d2和d1的值也是一樣的。
	d1 = d2;
	system("pause");
	return 0;
}

編譯器生成的預設賦值過載函式已經可以完成位元組序的值拷貝了,但是有的還是要我們自己去實現,就是深拷貝。這個後邊的帖子會總結,到時候會連線過來。

 

六、const成員

 const修飾類的成員函式

const修飾的類成員函式稱之為const成員函式,const修飾類成員函式,實際修飾該成員函式隱含的this
指標
,表明在該成員函式中不能對類的任何成員進行修改

 

注意:

const物件可以呼叫其他的const函式;

非const物件可以呼叫非const成員函式和const成員函式;

const成員函式內可以呼叫其他的const成員函式;

非const成員函式內可以呼叫其他的const成員函式和非const成員函式;

 

七、取地址及const取地址操作符過載

這兩個預設成員函式一般不用重新定義 ,編譯器預設會生成。

class Date
{
public :
Date* operator&()
{
   return this ;
}
const Date* operator&()const
{
   return this ;
}
private :
   int _year ; // 年
   int _month ; // 月
   int _day ; // 日
};

這兩個運算子一般也不需要過載,使用編譯器生成的預設取地址的過載即可,只有特殊情況,才需要過載,比
如想讓別人獲取到指定的內容!