文章目錄
- 面向程序和面向物件初步認識
- 類的引入
- 類的定義
- 類的兩種定義方法
- 1.宣告和定義全部放在類體中
- 2. 宣告放在.h檔案中,類的定義放在.cpp檔案中
- 類的訪問限定符及封裝
- 訪問限定符
- 封裝
- 類的作用域
- 類的實體化
- 類物件模型
- 如何計算類物件的大小
- 類物件的存盤方式
- this 指標
- this指標的引出
- this指標的特性
- 類的6個默認成員函式
- 建構式
- 建構式的特性
- 解構式
- 特性
- 拷貝建構式
- 特性
- 運算子多載
- ==運算子多載
- = 運算子多載
- const修飾成員函式
- 再談建構式
- 建構式體賦值
- 初始化串列
- 注意事項
- 1.每個成員變數在初始化串列中只能出現一次(初始化只能初始化一次)
- 2.類中包含以下成員,必須放在初始化串列進行初始化:
- 3.盡量使用初始化串列初始化
- 4.成員變數在類中宣告的次序就是其在初始化串列中的初始化順序,與其在初始化串列中的先后順序無關
- explicit關鍵字
- static成員
- 特性
- 1.?宣告為static的類成員稱為類的靜態成員,用static修飾的成員變數,稱之為靜態成員變數;用static修飾的成員函式,稱之為靜態成員函式,靜態成員變數一定要在類外進行初始化,
- 2.靜態成員變數必須在類外定義,定義時不添加static關鍵字
- 3.靜態成員函式沒有隱藏的this指標,不能訪問任何非靜態成員
- 4.訪問靜態成員變數的方法
- 一.當靜態成員變數為公有時,有以下幾種訪問方式:
- 二.當靜態成員變數為私有時,有以下幾種訪問方式:
- 5.靜態成員和類的普通成員一樣,也有public、private和protected這三種訪問級別
- C++11中成員初始化的新玩法
- 友元
- 友元函式
- 友元類
- 內部類
- 再次理解封裝
- 再次理解面向物件
面向程序和面向物件初步認識
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;
};
上面結構體的定義,在C++中更喜歡用class來代替
類的定義
class className
{
// 類體:由成員函式和成員變陣列成
}; // 一定要注意后面的分號
class為定義類的關鍵字,ClassName為類的名字,{}中為類的主體,注意類定義結束時后面分號,
類中的元素稱為類的成員:類中的資料稱為類的屬性或者成員變數; 類中的函式稱為類的方法或者成員函式,
類的兩種定義方法
1.宣告和定義全部放在類體中
宣告和定義全部放在類體中,需要注意:成員函式如果在類中定義,編譯器可能會將其當成行內函式處
理
class 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;
};
2. 宣告放在.h檔案中,類的定義放在.cpp檔案中
//person.h
class Person
{
public:
//顯示資訊
void show();
public:
char* _name;
char* _sex;
int _age;
}
//person.cpp
#include"person.h>
void Person::show()
{
cout<<_name<<" "<<_sex<<" "<<_age<<endl;
}
注意:一般情況下我們采用第二種方式
類的訪問限定符及封裝
訪問限定符
C++實作封裝的方式:用類將物件的屬性與方法結合在一塊,讓物件更加完善,通過訪問權限選擇性的將其
介面提供給外部的用戶使用,

【訪問限定符說明】
- public修飾的成員在類外可以直接被訪問
- protected和private修飾的成員在類外不能直接被訪問(此處protected和private是類似的)
- 訪問權限作用域從該訪問限定符出現的位置開始直到下一個訪問限定符出現時為止
- class的默認訪問權限為private,struct為public(因為struct要兼容C)
封裝
在類和物件階段,我們只研究類的封裝特性,那什么是封裝呢?
封裝本質上是一種管理:我們使用類將資料和方法都封裝起來,不想對外開放的就用 protected/private 封裝起來,用 public 封裝的成員允許外界對其進行合理的訪問,所以封裝本質上是一種管理,
類的作用域
類定義了一個新的作用域,類的所有成員都在類的作用域中,在類體外定義成員,需要使用 :: 作用域決議符指明成員屬于哪個類域,
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 這里需要指定PrintPersonInfo是屬于Person這個類域
void Person::PrintPersonInfo()
{
cout<<_name<<" "_gender<<" "<<_age<<endl;
}
類的實體化
用型別別創建物件的程序,稱為類的實體化
- 類只是一個模型一樣的東西,限定了類有哪些成員,定義出一個類并沒有分配實際的記憶體空間來存盤它
- 一個類可以實體化出多個物件,實體化出的物件 占用實際的物理空間,存盤類成員變數
- 做個比方,類實體化出物件就像現實中使用建筑設計圖建造出房子,類就像是設計圖,只設計出需要什
么東西,但是并沒有物體的建筑存在,同樣類也只是一個設計,實體化出的物件才能實際存盤資料,占
用物理空間
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
void test()
{
Person man; //類的實體化
man._name="hehe";
man._age="66";
man._sex="男";
man._PrintPersonInfo();
}
類物件模型
如何計算類物件的大小
class A {
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
char _a;
};
那么問題來了?類中既可以有成員變數,又可以有成員函式,那么一個類的物件中包含了什么?如何計算一個類的大
小? 想要知道這個,首先我們要弄明白類在記憶體中的存盤方式,
類物件的存盤方式

那為什么記憶體要這樣存盤類了?
原因:每個物件中成員變數是不同的,但是呼叫同一份函式,如果按照此種方式存盤,當一個類創建多
個物件時,每個物件中都會保存一份代碼,相同代碼保存多次,浪費空間,

結論:一個類的大小,實際就是該類中”成員變數”之和,當然也要進行記憶體對齊,注意空類的大小,空類比
較特殊,編譯器給了空類一個位元組來唯一標識這個類, 如果有小伙伴不怎么明白記憶體對齊:可以看看這篇文章:自定義型別的知識點
this 指標
this指標的引出
我們先來定義一個日期類Date
class Date
{
public :
void Display ()
{
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
void SetDate(int year , int month , int day)
{
_year = year;
_month = month;
_day = day;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
int main()
{
Date d1, d2;
d1.SetDate(2018,5,1);
d2.SetDate(2018,7,1);
d1.Display();
d2.Display();
return 0;
}
對于上述類,有這樣的一個問題:
Date類中有SetDate與Display兩個成員函式,函式體中沒有關于不同物件的區分,那當d1呼叫SetDate函式
時,該函式是如何知道應該設定d1物件,而不是設定d2物件呢?
C++中通過引入this指標解決該問題,即:C++編譯器給每個“非靜態的成員函式“增加了一個隱藏的指標參
數,讓該指標指向當前物件(函式運行時呼叫該函式的物件),在函式體中所有成員變數的操作,都是通過該
指標去訪問,只不過所有的操作對用戶是透明的,即用戶不需要來傳遞,編譯器自動完成
this指標的特性
- this指標的型別:型別別* const
- 只能在“成員函式”的內部使用
- this指標本質上其實是一個成員函式的形參,是物件呼叫成員函式時,將物件地址作為實參傳遞給this形參,所以物件中不存盤this指標,
- this指標是成員函式第一個隱含的指標形參,一般情況由編譯器通過ecx暫存器自動傳遞,不需要用戶傳遞

注意:this指標不能為空下面來看一個例子

這里為什么會報錯了?首先這個p是一個空指標,但是并不是物件是空指標就一定報錯,這里其實更重要的一個原因是PrintA里面為this->_a你對p進行了訪問,而空指標是不能訪問的,下面我們再來來p->Show()會不會報錯?

類的6個默認成員函式
如果一個類中什么成員都沒有,我們簡稱其為空類,但是空類中真的什么都沒有嗎?其實不然,任何一個類,即使我們什么都不寫,類中也會自動生成6個默認成員函式,
class Date {}; //空類

建構式
class Date
{
public:
Date(int year = 0, 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;
};
例如,上述日期類中的成員函式Date就是一個建構式,當你用該日期類創建一個物件時,編譯器會自動呼叫該建構式對新創建的變數進行初始化,
注意:建構式的主要任務并不是開空間創建物件,而是初始化物件,(這兒可以先暫時這么理解)
建構式的特性
- 函式名與類名相同,
- 無回傳值,
- 物件實體化時編譯器自動呼叫對應的建構式,
- 建構式可以多載,
class Date
{
public:
// 1.無參建構式
Date ()
{}
Date(int year = 0, 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;
};
void TestDate()
{
Date d1; // 呼叫無參建構式
Date d2 (2015, 1, 1); // 呼叫帶參的建構式
// 注意:如果通過無參建構式創建物件時,物件后面不用跟括號,否則就成了函式宣告
// 以下代碼的函式:宣告了d3函式,該函式無參,回傳一個日期型別的物件
Date d3();
}
- 如果類中沒有顯式定義建構式,則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;
}
- 無參的建構式和全預設的建構式都稱為默認建構式,并且默認建構式只能有一個,注意:無參建構式、全預設建構式、我們沒寫編譯器默認生成的建構式,都可以認為是默認成員函式,
// 默認建構式
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.編譯器對內置型別使用默認建構式時,對其成員賦的是隨機值,但對自定義型別,會呼叫它的默認函式,

這兒并沒有我們自己寫的建構式,所以編譯時會呼叫默認的建構式,又由于類成員都是內置型別,因此賦的都是隨機值,下面我們再來看看自定義型別,

注意:如果你Time類中沒有自己寫建構式,用編譯器默認的建構式,它也是一樣會輸入隨機值的,

解構式
前面通過建構式的學習,我們知道一個物件時怎么來的,那一個物件又是怎么沒呢的?
解構式:與建構式功能相反,解構式不是完成物件的銷毀,區域物件銷毀作業是由編譯器完成的,而
物件在銷毀時會自動呼叫解構式,完成類的一些資源清理作業,
特性
- 解構式名是在類名前加上字符 ~,
- 無引數無回傳值,
- 一個類有且只有一個解構式,若未顯式定義,系統會自動生成默認的解構式,
編譯器自動生成的解構式機制:
?1、編譯器自動生成的解構式對內置型別不做處理,
?2、對于自定義型別,編譯器會再去呼叫它們自己的默認解構式, - 物件生命周期結束時,C++編譯系統系統自動呼叫解構式,
- 先構造的后析構,后構造的先析構

拷貝建構式
建構式:只有單個形參,該形參是對本型別別物件的參考(一般常用const修飾),在用已存在的型別別物件 創建新物件時由編譯器自動呼叫
特性
- 拷貝建構式是建構式的一個多載形式,
- 拷貝建構式的引數只有一個且必須使用參考傳參,使用傳值方式會引發無窮遞回呼叫,
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0, 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(2021, 9, 27);
Date d2(d1); // 用已存在的物件d1創建物件d2
return 0;
}

因此通過形參不寫成參考的形式,會形成無限遞回,
- 若未顯示定義,系統生成默認的拷貝建構式, 默認的拷貝建構式物件按記憶體存盤按位元組序完成拷
貝,這種拷貝我們叫做淺拷貝,或者值拷貝,
一般涉及到堆區的問題,淺拷貝是無法解決問題的,下面我們來舉個例子:
class Stack
{
public:
Stack(int capacity = 4)
{
_ps = (int*)malloc(sizeof(int)* capacity);
_size = 0;
_capacity = capacity;
}
void Print()
{
cout << _ps << endl;// 列印堆疊空間地址
}
private:
int* _ps;
int _size;
int _capacity;
};
int main()
{
Stack s1;
s1.Print();// 列印s1堆疊空間的地址
Stack s2(s1);// 用已存在的物件s1創建物件s2
s2.Print();// 列印s2堆疊空間的地址
return 0;
}
我們可以看到,類中沒有自己定義拷貝建構式,那么當我們用已存在的物件來創建另一個物件時,將呼叫編譯器自動生成的拷貝建構式,這段代碼中,我們的本意是用已存在的物件s1創建物件s2,但編譯器自動生成的拷貝建構式,完成的是淺拷貝,拷貝出來的物件s2將不能滿足我們的要求,

結果列印s1堆疊和s2堆疊空間的地址相同,這就意味著,就算在創建完s2堆疊后,我們對s1堆疊做的任何操作都會直接影響到s2堆疊,
?

這個時候問題就很嚴重了,首先我們對s1的修改都會直接影響s2,而且更重要的一個是:我們對它們共同指向的那塊空間進行了兩次的析構,會造成空間多次釋放的問題,
運算子多載
C++為了增強代碼的可讀性引入了運算子多載,運算子多載是具有特殊函式名的函式,也具有其回傳值類
型,函式名字以及引數串列,其回傳值型別與引數串列與普通的函式類似,
函式名字為:關鍵字operator后面接需要多載的運算子符號,
函式原型:回傳值型別 operator運算子(引數串列)
注意:
1.不能通過連接其他符號來創建新的運算子:比如operator@
2.多載運算子必須有一個型別別或者列舉型別的運算元
3.用于內置型別的運算子,其含義不能改變,例如:內置的整型+,不 能改變其含義
4.作為類成員的多載函式時,其形參看起來比運算元數目少1成員函式的
運算子有一個默認的形參this,限定為第一個形參
5.* 、:: 、sizeof 、?: 、. 注意以上5個運算子不能多載,這個經常在筆試選擇題中出現,
==運算子多載
bool operator==(const Date& d1, const Date& d2) {
return d1._year == d2._year;
&& d1._month == d2._month
&& d1._day == d2._day; }
對于這個多載的函式,你可以定義再類里面,這樣就少一個引數,因為有this指標的存在,你也可以定義在外面,但是定義在外面時,可能你的類成員時private封裝的,無法訪問到,這時有兩個解決辦法:一是把類成員用public封裝,二是用友元函式(之后會講到),
= 運算子多載
Date& operator=(const Date& d)// 賦值運算子多載函式
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
這里為什么要回傳參考了?如果你去測驗發現D1=D2,如果你的回傳值是Date的話,似乎也能過,但是如果你的測驗用例是D1=D2=D3的話,那就一定過不了了,因為你不是回傳的物件本身,無法形成鏈式編程,這也是為什么這兒回傳*this的原因,因為this是指向左運算子的,
其他一些運算子的多載這兒就不多說了,有興趣的小伙伴可以自己去嘗試嘗試,下面來說幾個多載運算子時的注意點,
多載賦值運算子需要注意以下幾點:
一、引數型別設定為參考,并用const進行修飾
賦值運算子多載函式的第一個形參默認是this指標,第二個形參是我們賦值運算子的右運算元,
由于是自定義型別傳參,我們若是使用傳值傳參,會額外呼叫一次拷貝建構式,所以函式的第二個引數最好使用參考傳參(第一個引數是默認的this指標,我們管不了),
其次,第二個引數,即賦值運算子的右運算元,我們在函式體內不會對其進行修改,所以最好加上const進行修飾,
二、回傳值使用參考回傳
原因在=運算子多載中說過了,為了回傳物件自身,形成鏈式編程,(return *this才是回傳自身,不要忘記解參考哦)
三、一個類如果沒有顯示定義賦值運算子多載,編譯器也會自動生成一個,完成物件按位元組序的值拷貝
沒錯,賦值運算子多載編譯器也可以自動生成,并且也是支持連續賦值的,但是編譯器自動生成的賦值運算子多載完成的是物件按位元組序的值拷貝,例如d2 = d1,編譯器會將d1所占記憶體空間的值完完全全地拷貝到d2的記憶體空間中去,類似于memcpy,
但是有些類就不行了,所以有些類還是要我們自己寫賦值運算子多載的,
注意區分拷貝和賦值:
Date d1(2021, 6, 1);
Date d2(d1);
Date d3 = d1;
這里一個三句代碼,我們現在都知道第二句代碼呼叫的是拷貝建構式,那么第三句代碼呢?呼叫的是哪一個函式?是賦值運算子多載函式嗎?
其實第三句代碼呼叫的也是拷貝建構式,注意區分拷貝建構式和賦值運算子多載函式的使用場景:
拷貝建構式:用一個已經存在的物件去構造初始化另一個即將創建的物件,
賦值運算子多載函式:在兩個物件都已經存在的情況下,將一個物件賦值給另一個物件,
const修飾成員函式
我們將const修飾的類成員函式稱之為const成員函式,const修飾類成員函式,實際修飾的是類成員函式隱含的this指標,表明在該成員函式中不能對this指標指向的物件進行修改,
例如,我們可以對類成員函式中的列印函式進行const修飾,避免在函式體內不小心修改了物件:
void Print()const// cosnt修飾的列印函式
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
注意:
在使用const時要注意,權限不能放大,但是可以縮小,
再談建構式
建構式體賦值
在創建物件時,編譯器會通過呼叫建構式,給物件中的各個成員變數一個合適的初始值:
class Date
{
public:
// 建構式
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
雖然上述建構式呼叫之后,物件中已經有了一個初始值,但是不能將其稱作為類物件成員的初始化,建構式體中的陳述句只能將其稱作為賦初值,而不能稱作初始化,因為初始化只能初始化一次,而建構式體內可以多次賦值,
class Date
{
public:
// 建構式
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;// 第一次賦值
_year = 2022;// 第二次賦值
//...
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
初始化串列
初始化串列:以一個冒號開始,接著是一個以逗號分隔的資料成員串列,每個成員變數后面跟一個放在括號中的初始值或運算式,
class Date
{
public:
// 建構式
Date(int year = 0, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
注意事項
1.每個成員變數在初始化串列中只能出現一次(初始化只能初始化一次)
2.類中包含以下成員,必須放在初始化串列進行初始化:
💥.參考成員變數
參考型別的變數在定義時就必須給其一個初始值,所以參考成員變數必須使用初始化串列對其進行初始化,
int a = 10;
int& b = a;// 創建時就初始化
💥.const成員變數
被const修飾的變數也必須在定義時就給其一個初始值,也必須使用初始化串列進行初始化
const int a = 10;//correct 創建時就初始化
const int b;//error 創建時未初始化
💥.自定義型別成員(該類沒有默認建構式)
若一個類沒有默認建構式,那么我們在實體化該類物件時就需要傳參對其進行初始化,所以實體化沒有默認建構式的類物件時必須使用初始化串列對其進行初始化,
在這里再宣告一下,默認建構式是指不用傳參就可以呼叫的建構式:
?1.我們不寫,編譯器自動生成的建構式,
?2.無參的建構式,
?3.全預設的建構式,
class A //該類沒有默認建構式
{
public:
A(int val) //注:這個不叫默認建構式(需要傳參呼叫)
{
_val = val;
}
private:
int _val;
};
class B
{
public:
B()
:_a(2021) //必須使用初始化串列對其進行初始化
{}
private:
A _a; //自定義型別成員(該類沒有默認建構式)
};
總結一下:在定義時就必須進行初始化的變數型別,就必須放在初始化串列進行初始化,
3.盡量使用初始化串列初始化
因為初始化串列實際上就是當你實體化一個物件時,該物件的成員變數定義的地方,所以無論你是否使用初始化串列,都會走這么一個程序(成員變數需要定義出來),
嚴格來說:
1.對于內置型別,使用初始化串列和在建構式體內進行初始化實際上是沒有差別的,其差別就類似于如下代碼:
// 使用初始化串列
int a = 10
// 在建構式體內初始化(不使用初始化串列)
int a;
a = 10;
2.對于自定義型別,使用初始化串列可以提高代碼的效率
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Test
{
public:
// 使用初始化串列
Test(int hour)
:_t(12)// 呼叫一次Time類的建構式
{}
private:
Time _t;
};
對于以上代碼,當我們要實體化一個Test類的物件時,我們使用了初始化串列,在實體化程序中只呼叫了一次Time類的建構式,
?我們若是想在不使用初始化串列的情況下,達到我們想要的效果,就不得不這樣寫了:
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Test
{
public:
// 在建構式體內初始化(不使用初始化串列)
Test(int hour)
{
Time t(hour);// 呼叫一次Time類的建構式
_t = t;// 呼叫一次Time類的賦值運算子多載函式
}
private:
Time _t;
};
這時如果我們要實體化一個Test類的物件,在實體化程序中先呼叫了一次Time類的建構式,又呼叫了一次Time類的賦值運算子多載函式,效率就降下來了,
4.成員變數在類中宣告的次序就是其在初始化串列中的初始化順序,與其在初始化串列中的先后順序無關
還是一樣舉個例子來看看:
#include <iostream>
using namespace std;
int i = 0;
class Test
{
public:
Test()
:_b(i++)
,_a(i++)
{}
void Print()
{
cout << "_a:" << _a << endl;
cout << "_b:" << _b << endl;
}
private:
int _a;
int _b;
};
int main()
{
Test test;
test.Print(); //列印結果test._a為0,test._b為1
return 0;
}
代碼中,Test類建構式的初始化串列中成員變數_b先初始化,成員變數_a后初始化,按道理列印結果test._a為1,test._b為0,但是初始化串列的初始化順序是成員變數在類中宣告次序,所以最終test._a為0,test._b為1,
explicit關鍵字
建構式不僅可以構造和初始化物件,對于單個引數的建構式,還支持隱式型別轉換,
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0) //單個引數的建構式
:_year(year)
{}
void Print()
{
cout << _year << endl;
}
private:
int _year;
};
int main()
{
Date d1 = 2021; //支持該操作
d1.Print();
return 0;
}
在語法上,代碼中Date d1 = 2021等價于以下兩句代碼:
Date tmp(2021); //先構造
Date d1(tmp); //再拷貝構造
在早期的編譯器中,當編譯器遇到Date d1 = 2021這句代碼時,會先構造一個臨時物件,再用臨時物件拷貝構造d1;但是現在的編譯器已經做了優化,當遇到Date d1 = 2021這句代碼時,會按照Date d1(2021)這句代碼處理,這就叫做隱式型別轉換,
實際上,我們早就接觸了隱式型別轉換,只是我們不知道而已,以下代碼也叫隱式型別轉換:
int a = 10;
double b = a; //隱式型別轉換
在這個程序中,編譯器會先構建一個double型別的臨時變數接收a的值,然后再將該臨時變數的值賦值給b,這就是為什么函式可以回傳區域變數的值,因為當函式被銷毀后,雖然作為回傳值的變數也被銷毀了,但是隱式型別轉換程序中所產生的臨時變數并沒有被銷毀,所以該值仍然存在,
但是,對于單引數的自定義型別來說,Date d1 = 2021這種代碼的可讀性不是很好,我們若是想禁止單引數建構式的隱式轉換,可以用關鍵字explicit來修飾建構式,
static成員
宣告為static的類成員稱為類的靜態成員,用static修飾的成員變數,稱之為靜態成員變數;用static修飾的成員函式,稱之為靜態成員函式,靜態成員變數一定要在類外進行初始化,
特性
1.?宣告為static的類成員稱為類的靜態成員,用static修飾的成員變數,稱之為靜態成員變數;用static修飾的成員函式,稱之為靜態成員函式,靜態成員變數一定要在類外進行初始化,
#include <iostream>
using namespace std;
class Test
{
private:
static int _n;
};
int main()
{
cout << sizeof(Test) << endl;
return 0;
}
結果計算Test類的大小為1,因為靜態成員_n是存盤在靜態區的,屬于整個類,也屬于類的所有物件,所以計算類的大小或是類物件的大小時,靜態成員并不計入其總大小之和,
2.靜態成員變數必須在類外定義,定義時不添加static關鍵字
class Test
{
private:
static int _n;
};
// 靜態成員變數的定義初始化
int Test::_n = 0;
注意:這里靜態成員變數_n雖然是私有,但是我們在類外突破類域直接對其進行了訪問,這是一個特例,不受訪問限定符的限制,否則就沒辦法對靜態成員變數進行定義和初始化了
3.靜態成員函式沒有隱藏的this指標,不能訪問任何非靜態成員
class Test
{
public:
static void Fun()
{
cout << _a << endl; //error不能訪問非靜態成員
cout << _n << endl; //correct
}
private:
int _a; //非靜態成員
static int _n; //靜態成員
};
注意:含有靜態成員變數的類,一般含有一個靜態成員函式,用于訪問靜態成員變數
4.訪問靜態成員變數的方法
一.當靜態成員變數為公有時,有以下幾種訪問方式:
#include <iostream>
using namespace std;
class Test
{
public:
static int _n; //公有
};
// 靜態成員變數的定義初始化
int Test::_n = 0;
int main()
{
Test test;
cout << test._n << endl; //1.通過類物件突破類域進行訪問
cout << Test()._n << endl; //3.通過匿名物件突破類域進行訪問
cout << Test::_n << endl; //2.通過類名突破類域進行訪問
return 0;
}
二.當靜態成員變數為私有時,有以下幾種訪問方式:
#include <iostream>
using namespace std;
class Test
{
public:
static int GetN()
{
return _n; //既然你的成員是私有的,那么我就通過公有的函式來回傳你私有的成員
}
private:
static int _n;
};
// 靜態成員變數的定義初始化
int Test::_n = 0;
int main()
{
Test test;
cout << test.GetN() << endl; //1.通過物件呼叫成員函式進行訪問
cout << Test().GetN() << endl; //2.通過匿名物件呼叫成員函式進行訪問
cout << Test::GetN() << endl; //3.通過類名呼叫靜態成員函式進行訪問
return 0;
}
5.靜態成員和類的普通成員一樣,也有public、private和protected這三種訪問級別
所以當靜態成員變數設定為private時,盡管我們突破了類域,也不能對其進行訪問,
注意區分兩個問題:
?1、靜態成員函式可以呼叫非靜態成員函式嗎?
?2、非靜態成員函式可以呼叫靜態成員函式嗎?
問題1:不可以,因為非靜態成員函式的第一個形參默認為this指標,而靜態成員函式中沒有this指標,故靜態成員函式不可呼叫非靜態成員函式,
問題2:可以,因為靜態成員函式和非靜態成員函式都在類中,在類中不受訪問限定符的限制,
C++11中成員初始化的新玩法
C++11支持非靜態成員變數在宣告時進行初始化賦值,但是要注意這里不是初始化,這里是給宣告的成員變數一個預設值,
class A
{
public:
void Print()
{
cout << _a << endl;
cout << _p << endl;
}
private:
// 非靜態成員變數,可以在成員宣告時給預設值,
int _a = 10;
int* _p = (int*)malloc(4);
static int _n; //靜態成員變數不能給預設值
};
初始化串列是成員變數定義初始化的地方,你若是給定了值,就用你所給的值對成員變數進行初始化,你若沒有給定值,則用預設值進行初始化,若是沒有預設值,則內置型別的成員就是隨機值,
友元
友元分為:友元函式和友元類
友元提供了一種突破封裝的方式,有時提供了便利,但是友元會增加耦合度,破壞了封裝,所以友元不宜多
用,
友元函式
這里就以我們之前沒有實作的一個多載來舉例,我們來實作一下<<的多載,
這里要實作首先要明白一個問題:
我們嘗試去多載operator<<,然后發現我們沒辦法將operator<<多載成成員函式,因為cout的
輸出流物件和隱含的this指標在搶占第一個引數的位置,this指標默認是第一個引數也就是左運算元了,但是
實際使用中cout需要是第一個形參物件,才能正常使用,所以我們要將operator<<多載成全域函式,但是這
樣的話,又會導致類外沒辦法訪問成員,那么這里就需要友元來解決,operator>>同理,
class Date
{
// 友元函式的宣告
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// <<運算子多載
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day<< endl;
return out;
}
// >>運算子多載
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
友元函式說明:
?1、友元函式可以訪問類是私有和保護成員,但不是類的成員函式,
?2、友元函式不能用const修飾,
?3、友元函式可以在類定義的任何地方宣告,不受訪問限定符的限制,
?4、一個函式可以是多個類的友元函式,
?5、友元函式的呼叫與普通函式的呼叫原理相同,
友元類
友元類的所有成員函式都可以是另一個類的友元函式,都可以訪問另一個類中非公有成員,
class A
{
// 宣告B是A的友元類 可以理解為B是A的好朋友,A的任何東西B都可以訪問,包括私有型別的,
friend class B;
public:
A(int n = 0)
:_n(n)
{}
private:
int _n;
};
class B
{
public:
void Test(A& a)
{
// B類可以直接訪問A類中的私有成員變數
cout << a._n << endl;
}
};
友元類說明:
1、友元關系是單向的,不具有交換性,
?例如上述代碼中,B是A的友元,所以在B類中可以直接訪問A類的私有成員變數,但是在A類中不能訪問B類中的私有成員變數,
2、友元關系不能傳遞,
?如果A是B的友元,B是C的友元,不能推出A是C的友元,
內部類
概念:如果一個類定義在另一個類的內部,這個內部類就叫做內部類,注意此時這個內部類是一個獨立的
類,它不屬于外部類,更不能通過外部類的物件去呼叫內部類,外部類對內部類沒有任何優越的訪問權限,
注意:內部類就是外部類的友元類,注意友元類的定義,內部類可以通過外部類的物件引數來訪問外部類中的所有成員,但是外部類不是內部類的友元,
特性:
- 內部類可以定義在外部類的public、protected、private都是可以的,
- 注意內部類可以直接訪問外部類中的static、列舉成員,不需要外部類的物件/類名,
- sizeof(外部類)=外部類,和內部類沒有任何關系,
class A {
public:
A(int k,int h)
{
this->_k = k;
this->_h = h;
}
private:
static int _k;
int _h;
public:
class B
{
public:
void foo(const A& a)
{
cout <<_k << endl;//OK a._k也ok
cout << a._h << endl;//OK
}
};
};
int A::_k = 1;
int main()
{
A::B b;
b.foo(A(2,10));
return 0;
}

再次理解封裝
C++是基于面向物件的程式,面向物件有三大特性:封裝、繼承、多型,
C++通過類,將一個物件的屬性與行為結合在一起,使其更符合人們對于一件事物的認知,將屬于該物件的所有東西打包在一起,通過訪問限定符的將其部分功能開放出來與其他物件進行互動,而對于物件內部的一些實作細節,外部用戶不需要知道,知道了有些情況下也沒用,反而增加了使用或者維護的難度,讓整個事情復雜化,
再次理解面向物件
可以看出,面向物件其實是在模擬抽象映射現實世界:

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/303985.html
標籤:其他
上一篇:??學懂C語言檔案操作讀這篇就夠了(萬字總結,附習題)??
下一篇:2021-09-26
