C++ 類和對象、六個默認成員函數(構造函數、析構函數、拷貝構造函數、賦值運算符重載、取地址操作符重載、const成員)

一、類和對象(類的定義、類的訪問限定符和封裝、this指針)

面向對象(C語言):關注的是過程,分析出求解問題的步驟,通過函數調用逐步解決問題。
面向過程(C++):關注的是對象,將一件事情拆分成不同的對象,靠對象之間的交互完成。

C語言中,結構體中只能定義變量,在C++中,結構體內不僅可以定義變量,也可以定義函數。

struct Student
{
 void SetStudentInfo(const char* name, const char* gender, int age)
 {
 strcpy(_name, name);
 strcpy(_gender, gender);
 _age = age;
 }
 
 void PrintStudentInfo()
 {
 cout<<_name<<" "<<_gender<<" "<<_age<<endl;
 }
 
 char _name[20];
 char _gender[3];
 int _age;
};

int main()
{
 Student s;
 s.SetStudentInfo("Peter", "男", 18);
 return 0; }

類的定義

類中的元素稱爲類的成員:類中的數據稱爲類的屬性或者成員變量; 類中的函數稱爲類的方法或者成員函數。

class className
{
 // 類體:由成員函數和成員變量組成
 
}; // 一定要注意後面的分號

類定義的方式:

  • 聲明和定義全部放在類體中,需要注意:成員函數如果在類中定義,編譯器可能會將其當成內聯函數處理。
  • 聲明放在.h文件中,類的定義放在.cpp文件中。
    在這裏插入圖片描述

類的訪問限定符和封裝

C++實現封裝的方式:用類將對象的屬性與方法結合在一塊,讓對象更加完善,通過訪問權限選擇性的將其接口提供給外部的用戶使用。

訪問限定符的組成:public(公有)、protected(保護)、private(私有)。

訪問限定符說明

訪問限定符只在編譯時有用,當數據映射到內存後,沒有任何訪問限定符上的區別

  • .1.public修飾的成員在類外可以直接被訪問
  • .2. protected和private修飾的成員在類外不能直接被訪問(此處protected和private是類似的)
  • .3. 訪問權限作用域從該訪問限定符出現的位置開始直到下一個訪問限定符出現時爲止
  • .4. class的默認訪問權限爲private,struct爲public(因爲struct要兼容C)

C++中struct和class的區別?

C++需要兼容C語言,所以C++中struct可以當成結構體去使用。另外C++中struct還可以用來定義類。
和class是定義類是一樣的,區別是struct的成員默認訪問方式是public,class是struct的成員默認訪問方式
是private。

封裝

將數據和操作數據的方法進行有機結合,隱藏對象的屬性和實現細節,僅對外公開接口來和對象進行
交互。(封裝的本質是一種管理)

類的作用域

類定義了一個新的作用域,類的所有成員都在類的作用域中。在類體外定義成員,需要使用 :: 作用域解析符指明成員屬於哪個類域。

class Person
{
public:
 void PrintPersonInfo();
private:
 char _name[20];
 char _gender[3];
 int _age;
};
// 這裏需要指定PrintPersonInfo是屬於Person這個類域
void Person::PrintPersonInfo()
{
 cout<<_name<<" "_gender<<" "<<_age<<endl; }

類的實例化

用類類型創建對象的過程,稱爲類的實例化

  • .1. 類只是一個模型一樣的東西,限定了類有哪些成員,定義出一個類並沒有分配實際的內存空間來存儲它。
  • .2. 一個類可以實例化出多個對象,實例化出的對象 佔用實際的物理空間,存儲類成員變量。
    在這裏插入圖片描述

類對象的存儲方式

一個類的大小,實際就是該類中”成員變量”之和,當然也要進行內存對齊,注意空類的大小,空類比
較特殊,編譯器給了空類1個字節來唯一標識這個類。

結構體內存對齊規則

  1. 第一個成員在與結構體偏移量爲0的地址處。
  2. 其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處。
    注意:對齊數 = 編譯器默認的一個對齊數 與 該成員大小的較小值。(VS中默認的對齊數爲8)。
  3. 結構體總大小爲:最大對齊數(所有變量類型最大者與默認對齊參數取最小)的整數倍。
  4. 如果嵌套了結構體的情況,嵌套的結構體對齊到自己的最大對齊數的整數倍處,結構體的整體大小就是所有最大對齊數(含嵌套結構體的對齊數)的整數倍。
// 類中既有成員變量,又有成員函數
class A1 {
public:
 void f1(){}
private:
 int _a;
};
// 類中僅有成員函數
class A2 {
public:
 void f2() {}
};
// 類中什麼都沒有---空類
class A3
{};

sizeof(A1) : 4;
sizeof(A2) : 1;
sizeof(A3) : 1

沒有成員變量的類的大小爲1,這裏開1個字節不是爲了存儲數據,而是爲了佔位

this指針

C++編譯器給每個“非靜態的成員函數“增加了一個隱藏的指針參數,讓該指針指向當前對象(函數運行時調用該函數的對象),在函數體中所有成員變量的操作,都是通過該指針去訪問。只不過所有的操作對用戶是透明的,即用戶不需要來傳遞,編譯器自動完成。

this指針特性

  1. this指針的類型:類類型* const;
  2. 只能在“成員函數”的內部使用;
  3. this指針本質上其實是一個成員函數的形參,是對象調用成員函數時,將對象地址作爲實參傳遞給this形參。所以對象中不存儲this指針;
  4. this指針是成員函數第一個隱含的指針形參,一般情況由編譯器通過ecx寄存器自動傳遞,不需要用戶
    傳遞。
//編譯器處理成員函數隱含的this指針
void Display()           //void Display(Data* this)
{
cout<<_year<<endl;       //cout<<this->_year<<endl;
}

二、類和對象(類的6個默認成員函數)

構造函數

構造函數是一個特殊的成員函數,名字與類名相同,創建類類型對象時由編譯器自動調用,保證每個數據成員都有 一個合適的初始值,並且在對象的生命週期內只調用一次。構造函數的主要任務並不是開空間創建對象,而是初始化對象。

構造函數的特性

  • 1、函數名與類名相同;
  • 2、無返回值;
  • 3、對象實例化時編譯器自動調用對應的構造函數;
  • 4、構造函數可以重載。
class Date
{
public :
 // 1.無參構造函數
 Date ()
 {}
 
 // 2.帶參構造函數
 Date (int year, int month , int day )
 {
 _year = year ;
 _month = month ;
 _day = day ;
 }
private :
 int _year ;
 int _month ;
 int _day ;
};
void TestDate()
{
 Date d1; // 調用無參構造函數
 Date d2 (2015, 1, 1); // 調用帶參的構造函數
 
 // 聲明瞭d3函數,該函數無參,返回一個日期類型的對象
 Date d3(); 
}

注意:如果通過無參構造函數創建對象時,對象後面不用跟括號,否則就成了函數聲明,如上面代碼中聲明瞭 d3 函數。

  • 5、如果類中沒有顯式定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦用戶顯式定義編譯器將不再生成。
class Date
{
public:
 /*
 // 如果用戶顯式定義了構造函數,編譯器將不再生成
 Date (int year, int month, int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 */
private:
 int _year;
 int _month;
 int _day;
};
void Test()
{
 // 沒有定義構造函數,對象也可以創建成功,因此此處調用的是編譯器生成的默認構造函數
  	Date d; 
}

雖然上述構造函數調用之後,對象中已經有了一個初始值,但是不能將其稱作爲類對象成員的初始化,構造
函數體中的語句只能將其稱作爲賦初值,而不能稱作初始化
。因爲初始化只能初始化一次,而構造函數體內
可以多次賦值

  • 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; }

結果:上述代碼不能通過編譯,因爲類Date包含了多個默認構造函數,不知道該調用哪一個;Date::Date 對重載函數的調用不明確。

初始化列表

以一個冒號開始,接着是一個以逗號分隔的數據成員列表,每個"成員變量"後面跟一個放在括號中的初始值或表達式。

class Date
{
public:
 Date(int year, int month, int day)
 : _year(year)
 , _month(month)
 , _day(day)
 {}
 
private:
 int _year;
 int _month;
 int _day;
};

注意

  • 每個成員變量在初始化列表中只能出現一次(初始化只能初始化一次);
  • 類中包含引用成員變量、 const成員變量、 自定義類型成員(該類沒有默認構造函數),必須放在初始化列表位置進行初始化;
class A {
public:
	 A(int a)
	 :_a(a)
	 {}
private:
	 int _a;
};
class B {
public:
	 B(int a, int ref)
	 :_aobj(a)
 	,_ref(ref)
	 ,_n(10)
	 {}
private:
	 A _aobj; // 沒有默認構造函數
	 int& _ref; // 引用
	 const int _n; // const 
};
  • .儘量使用初始化列表初始化,因爲不管你是否使用初始化列表,對於自定義類型成員變量,一定會先使用初始化列表初始化。
class Time
{
public:
	 Time(int hour = 0)
	 :_hour(hour)
	 {
	 cout << "Time()" << endl;
	 }
private:
	 int _hour;
	};
class Date
{
public:
	 Date(int day)
	 {}
private:
	 int _day;
	 Time _t;
	};
int main()
	{
	 Date d(1);
	}
  • 成員變量在類中聲明次序就是其在初始化列表中的初始化順序,與其在初始化列表中的先後次序無關
class A {
public:
 A(int a)
 :_a1(a)
 ,_a2(_a1)
 {}
 
 void Print() {
 cout<<_a1<<" "<<_a2<<endl;
 }
private:
 int _a2;
 int _a1; }
int main() {
 A aa(1);
 aa.Print();
}

輸出結果爲:輸出1 隨機值(因爲聲明時先聲明_a2, 後聲明_a1,因此初始化的時候需要先初始化_a2,此時_a1爲隨機值 )。

explicit關鍵字

構造函數不僅可以構造與初始化對象,對於單個參數的構造函數,還具有類型轉換的作用

class Date
{
public:
	 Date(int year)
	 :_year(year)
	 {}
 
	 explicit Date(int year)
 	:_year(year)
 	{}
 
private:
	 int _year;
	 int _month:
	 int _day; }void TestDate()
{
	 Date d1(2018);
 
	 // 用一個整形變量給日期類型對象賦值
	 // 實際編譯器背後會用2019構造一個無名對象,最後用無名對象給d1對象進行賦值
	 d1 = 2019; 
}

析構函數

與構造函數功能相反,析構函數不是完成對象的銷燬,局部對象銷燬工作是由編譯器完成的。而
對象在銷燬時會自動調用析構函數,完成類的一些資源清理工作。

析構函數特性

析構函數是特殊的成員函數,其特徵如下:

  • 1、析構函數是在類名前加字符~;
  • 2、無參數、無返回值;
  • 3、一個類有且只有一個析構函數。若未顯式定義,系統會自動生成默認的析構函數;
  • 4、 對象生命週期結束時,C++編譯系統系統自動調用析構函數。
typedef int DataType;
class SeqList
{ 
public :
 SeqList (int capacity = 10)
 {
 _pData = (DataType*)malloc(capacity * sizeof(DataType));
 assert(_pData);
 
 _size = 0;
 _capacity = capacity;
  }
 
 //析構函數
 ~SeqList()
 {
 if (_pData)
 {
 free(_pData ); // 釋放堆上的空間
 _pData = NULL; // 將指針置爲空
 _capacity = 0;
 _size = 0;
 }
 }
 
private :
 int* _pData ;
 size_t _size;
 size_t _capacity;
};
  • 5、編譯器生成的默認析構函數,對會自定類型成員調用它的析構函數。
class String
{
public:
 String(const char* str = "jack")
 {
 _str = (char*)malloc(strlen(str) + 1);
 strcpy(_str, str);
 }
 ~String()
 {
 cout << "~String()" << endl;
 free(_str);
 }
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;
 }
private:
 int _year;
 int _month;
 int _day;
};
int main()
{
 Date d1;
 Date d2(d1);
 return 0; }
  • 3、若未顯示定義,系統生成默認的拷貝構造函數。 默認的拷貝構造函數對象按內存存儲按字節序完成拷貝,這種拷貝我們叫做淺拷貝(值拷貝)。
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;
  // 這裏d2調用的默認拷貝構造完成拷貝,d2和d1的值也是一樣的。
 Date d2(d1);
 return 0; 
 }
  • 4、編譯器生成的默認拷貝構造函數已經可以完成字節序的值拷貝,但是下列這種情況程序會崩潰,需要考慮深拷貝去解決

由於s1存在代碼段,也就是常量區,不能被修改,因此不能對s1進行操作。

// 這裏會發現下面的程序會崩潰掉!!!這裏就需要深拷貝去解決。
class String
{
public:
 String(const char* str = "jack")
 {
 _str = (char*)malloc(strlen(str) + 1);
 strcpy(_str, str);
 }
 ~String()
 {
 cout << "~String()" << endl;
 free(_str);
 }
private:
 char* _str;
};
int main()
{
 String s1("hello");
 String s2(s1);
}

賦值運算符重載

運算符重載

C++爲了增強代碼的可讀性引入了運算符重載,運算符重載是具有特殊函數名的函數,也具有其返回值類型,函數名字以及參數列表,其返回值類型與參數列表與普通的函數類似。

函數名字: 關鍵字operator後面接需要重載的運算符符號

函數原型: 返回值類型 operator 操作符(參數列表)

注意:

  • 不能通過連接其他符號來創建新的操作符:比如operator@
  • 重載操作符必須有一個類類型或者枚舉類型的操作數;
  • 用於內置類型的操作符,其含義不能改變,例如:內置的整型+,不 能改變其含義;
  • 作爲類成員的重載函數時,其形參看起來比操作數數目少1個成員函數,操作符有一個默認的形參this,限定爲第一個形參;
  • .* 、:: 、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; }
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 == 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

一個類如果沒有顯式定義賦值運算符重載,編譯器也會生成一個,完成對象按字節序的值拷貝。
這裏d1調用的編譯器生成operator=完成拷貝,d2和d1的值也是一樣的。(淺拷貝)

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(2018101);
 
 // 這裏d1調用的編譯器生成operator=完成拷貝,d2和d1的值也是一樣的。
 d1 = d2;
 return 0; }

取地址操作符重載

取地址及const取地址操作符重載,這兩個運算符一般不需要重載,使用編譯器生成的默認取地址的重載即可,只有特殊情況,才需要重載,比如想讓別人獲取到指定的內容!

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

const成員

const修飾類的成員函數

將const修飾的類成員函數稱之爲const成員函數,const修飾類成員函數,實際修飾該成員函數隱含的this
指針,表明在該成員函數中不能對類的任何成員進行修改。

在這裏插入圖片描述

class Date
{ 
public :
 void Display ()
 {
 cout<<"Display ()" <<endl;
 cout<<"year:" <<_year<< endl;
 cout<<"month:" <<_month<< endl;
 cout<<"day:" <<_day<< endl<<endl ;
 }
 void Display () const
 {
 cout<<"Display () const" <<endl;
 cout<<"year:" <<_year<< endl;
 cout<<"month:" <<_month<< endl;
 cout<<"day:" <<_day<< endl<<endl;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};
void Test ()
{
 Date d1 ;
 d1.Display ();
 
 const Date d2;
 d2.Display ();
}

結論:const成員函數內不可以調用其它的非const成員函數, 非const成員函數內可以調用其它的const成員函數。 const對象不可以調用非const成員函數,非const對象可以調用const成員函數。

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