C++初階(類的訪問許可權以及封裝+this指針+構造函數+析構函數+拷貝構造函數+參數列表+友元+內部類)
面向過程與面向對象
C語言是面向過程的,關注的是過程(函數),分析出求解問題的步驟,通過函數調用逐步解決問題。
C++是基於面向對象的,關注的是對象,將一件事情拆分成不同的對象,靠對象之間的交互完成。
類的引入與定義
類的引入
C語言中,結構體內部只能定義變數,C++中的結構體不僅可以定義變數,還可以定義函數。也就是說C語言中的結構體在C++中已經升級為類,一般習慣用class關鍵字定義。
struct student
{
// 成員變數
int age;
int weight;
// 成員函數(方法)
void PrintStudentInfo()
{
cout << "age = " << age << "weight = " << weight << endl;
}
};
類的定義
其中class是定義類的關鍵字,後面跟著的是類名,「{}」內部是類體,最後結尾跟著是『;』。
類體中有類的成員,類中的數據稱為類的屬性或者成員變數; 類中的函數稱為類的方法或者成員函數。
class 類名
{
// 類體:由成員函數和成員變數組成
}; // 分號結尾,和結構體一樣
類的定義的兩種方式:
1.聲明和定義放在一起,全放類體中,此時編譯器可能會把成員函數當中內聯函數處理
2.聲明放在.h的頭文件中,定義放在.cpp的文件中,通常這種方法用的比較多
函數重載
概念:函數重載是函數的一種特殊情況,C++允許在同一作用域中聲明幾個功能類似的同名函數,這些同名函數的形參列表(參數個數 或 類型 或 順序)必須不同,常用來處理實現功能類似數據類型不同的問題。
實現重載的條件:
- 同一個作用域
- 參數個數不同
- 參數類型不同
- 參數順序不同
// 1.參數個數不同
void Func1(int a, int b)
{
cout << "Func(int s, int b)" << endl;
}
void Func1(int a, int b, int c)
{
cout << "Funv2{int a, int b, int c)" << endl;
}
//2. 參數類型不同
void Func2(int a, int b)
{
cout << "Func2(int a, int b)" << endl;
}
void Func2(char a, int b)
{
cout << "Func2(char a. int b)" << endl;
}
// 3.參數順序不同
void Func3(char a, double b)
{
cout << "Func3(char a. double b)" << endl;
}
void Func3(double a, char b)
{
cout << "Func3(double a, char b)" << endl;
}
void test01()
{
int a = 10;
double b = 12.33;
char c = 2;
// 參數個數不同
Func1(a, a);
Func1(a, a, a);
// 參數類型不同
Func2(c, a);
Func2(a, a);
// 參數順序不同
Func3(b, c);
Func3(c, b);
}
調用重載函數的注意:
-
嚴格的類型匹配,如果類型不匹配,那麼嘗試轉換,轉化成功就調用,轉換失敗就報錯
void func(int a) { cout << a << endl; } void test02() { char c = 'c'; func(c);//嘗試將字元類型轉化成int類型 }
-
函數重載和默認參數一起使用的時候,電腦不知道調用哪個函數
void myfunc(int a, int b = 0)
{
cout << myfunc(int a, int b = 0) <<endl;
}
void myfunc(int a)
{
cout << myfunc(int a) <<endl;
}
void test02()
{
myfunc(10);//二義性問題,不知道調用哪個函數
}
- 函數的返回值不作為函數重載的條件:編譯器是通過函數調用時,傳入的參數來判斷用重載的哪個函數,我們調用函數的時候不需要寫返回值,所以返回值不能稱為重載函數的條件
類的訪問許可權以及封裝
訪問許可權
-
在類的內部(作用域範圍內),沒有訪問許可權之分。所有成員可以相互訪問
-
在類的外部(作用範圍外),訪問許可權才有意義:public
、private、protected
-
在類的外部,只有public修飾的成員才能被訪問,在沒有涉及繼承與派生的時候,private和protected時同等級的,外部不允許訪問
訪問屬性 | 屬性 | 對象內部 | 對象外部 |
---|---|---|---|
public | 公有 | 可訪問 | 可訪問 |
protected | 保護 | 可訪問 | 不可訪問 |
private | 私有 | 可訪問 | 不可訪問 |
- public : 修飾的成員在類外可以被訪問
- private和protected :修飾的成員類外不可以被訪問,只有類內才可以被訪問
- 訪問許可權作用域從該訪問限定符出現的位置開始直到下一個訪問限定符出現時為止
- class的默認訪問許可權為private,struct為public(因為struct要兼容C)
- 訪問限定符只在編譯時有用,當數據映射到記憶體後,沒有任何訪問限定符上的區別
struct和class的區別
struct可以定義結構體,也可以和class一樣定義類,定義方法也是一樣的,區別在於struct的默認許可權是public,class的默認許可權是private
封裝
面向對象的三大特性:封裝,繼承和多態。
封裝就是把數據(成員變數)和方法(成員函數)放在了一起,並且進行更嚴格地管理(訪問限定符)。更好地實現類對象之間的交互。
C++對象模型
在C++對象模型中:
- ① 非靜態數據成員被放置到對象內部
- ② static數據成員、靜態和非靜態 函數成員均被放到對象之外
- ③ 對虛函數的支援分為兩步:
- a)每個class會為每個虛函數生成一個指針,這些指針統一放在虛函數表中(vtbl)
- b)每個class的對象都會添加一個指針(vptr),指向相關的虛函數表(vtbl)
- vptr的設定(setting)和重置(resetting)都由每一個class的構造函數,析構函數和拷貝賦值運算符自動完成
- ④ 另外,虛函數表地址的前面設置了一個指向type_info類的指針
- C++提供了一個type_info類來獲取對象類型資訊
也就是說C++的數據和操作是分開存儲的,非靜態的數據會保存在自己的對象內部,而共享成員函數,多個同類型的對象會共用一塊程式碼。
如圖:
提問:那麼既然成員函數程式碼區共享,這塊程式碼是怎麼區分是哪個對象調用自己的呢?
C++提供了特殊的對象指針,this指針,this指針指向被調用的成員函數所屬的對象。
this指針
概念:C++編譯器給每個「非靜態的成員函數「增加了一個隱藏的指針參數,讓該指針指向當前對象(函數運行時調用該函數的對象),在函數體中所有成員變數的操作,都是通過該指針去訪問。只不過所有的操作對用戶是透明的,即用戶不需要來傳遞,編譯器自動完成。
注意:當一個對象被創建之後,它的每個成員函數都含有一個系統自動生成的隱含指針this,用來保存這個對象的地址,this是C++實現封裝的一種機制,它將對象和該對象調用的成員函數鏈接在一起,在外部看來,每個對象都擁有自己的函數成員,但是其實這些函數成員的程式碼時共享,傳入不同的this指針代表這不同的對象調用成員函數。
this指針保存的是本對象的地址,在this指向的空間記憶體儲了成員變數,而類的成員函數是共享的。
class Date
{
public:
// 隱藏了一個指針參數 Date * const this
void Init(int year, int month, int day)
{
_year = year;// 實際操作==> this->_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
this指針的幾個特點:
- this指針的類型是
類名 *const this
,所以this的內容是不允許修改的 - 只能在成員函數內部使用
- this指針本質上其實是一個成員函數的形參,是對象調用成員函數時,將對象地址作為實參傳遞給this形參。所以對象中不存儲this指針(vs存在ecx暫存器中)
- this指針是成員函數第一個隱含的指針形參,一般情況由編譯器通過ecx暫存器自動傳遞,不需要用戶傳遞
- 調用成員函數時,不能顯示地傳實參給this,但可以顯示的用
- 靜態成員函數內部沒有this指針,靜態成員函數不能操作非靜態成員變數(因為靜態成員變數是屬於類的而不是屬於對象的,靜態的變數在編譯的時候就已經被創建出來,而this指向的是對象,所以this指向的空間沒有存儲靜態成員變數)
提問:為什麼靜態成員函數只能使用靜態成員變數?
在類中,靜態成員可以實現多個對象之間的數據共享,並且使用靜態數據成員還不會破壞隱藏的原則,即保證了安全性。因此,靜態成員是類的所有對象中共享的成員,而不是某個對象的成員,所以作為所有對象共享的函數就只能訪問共享的變數,不能造成一個對象對另外一個對象的非靜態成員造成修改的現象。
類的6個默認成員函數
在一個類中,會默認生成6個默認成員函數,即使是空類也是如此。
分類如下:
構造函數
概念: 一個特殊的成員函數,名字與類名相同,創建類類型對象時由編譯器自動調用,一般是用來初始化成員變數,且一個生命周期只調用一次。
構造函數特性:
- 函數名和類名相同
- 無返回值
- 對象實例化是自動調用
- 可以發生重載
class Date
{
public:
// 構造函數
// 1.無參構造函數
Date()
{
}
// 2. 有參構造
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 3.全預設
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
構造函數顯示定義的三種方法:
- 無參構造函數(實例化對象時後面不跟括弧,否則會變成函數的聲明)
- 有參構造函數
- 全預設構造函數
類中無構造函數時,編譯器會默認生成一個無參的默認構造函數。
默認構造函數有三種:全預設、不含參和編譯器給的
在我們不實現構造函數時,編譯器就會調用編譯器默認生成的構造函數,看下面一串程式碼,編譯器的默認構造函數能否實現成員變數的初始化呢?
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
運行結果如下:
構造函數的調用規則
注意事項1:
如果程式設計師提供了有參構造,那麼編譯器就不會提供默認的構造函數,但是編譯器還是會給默認的拷貝構造
注意事項2:
如果程式設計師提供了拷貝構造函數,那麼編譯器就不會提供默認的構造函數和默認的拷貝構造函數
多個對象的構造函數
- 如果類有成員對象,那麼先調用成員對象的構造函數,再調用本身的構造函數
- 析構函數的調用順序反之(壓棧的問題)
- 如果有成員對象,那麼實例化對象的時候,必須保證成員對象的構造和析構函數能被調用
class Buick
{
public:
Buick()
{
cout << "Buick的構造函數" << endl;
}
~Buick()
{
cout << "Buick的構造函數" << endl;
}
};
class BMW
{
public:
BMW()
{
cout << "BMW的構造函數" << endl;
}
~BMW()
{
cout << "BMW的構造函數" << endl;
}
};
class Maker
{
public:
Maker()
{
cout << "Maker的構造函數" << endl;
}
~Maker()
{
cout << "Maker的析構函數" << endl;
}
private:
Buick bui;//成員對象
BMW bmw;//成員對象
};
void test()
{
Maker m1;
}
int main()
{
test();
system("pause");
return EXIT_SUCCESS;
}
運行結果如下:
析構函數
概念: 對象在銷毀時會自動調用析構函數,完成類的一些資源清理工作。
特性:
- 函數名前加一個『~』
- 無參數,無返回值
- 一個類中有且只有一個析構函數,對象的生命周期結束是,編譯器會自動調用
思考:看下面一串程式碼,他們的構造和析構順序是怎麼樣的?
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class B
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
private:
int _b;
};
int main()
{
A a;
B b;
system("pause");
return EXIT_SUCCESS;
}
運行結果如下:
構造的順序和對象的創建順序相同,析構和對象的創建順序相反。(其實就是一個壓棧的問題,先進後出)
拷貝構造函數
概念: 只有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存在的類類型對象創建新對象時由編譯器自動調用。
特性:
- 拷貝構造函數是構造函數的一個重載形式
- 參數有且只有一個,且必須使用引用傳參,使用傳值會引發無窮遞歸
class Date
{
public:
// 拷貝構造
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;
AA _a;
};
下面解釋為什麼傳值會無窮遞歸
拷貝構造函數調用的時機
情景1:對象以值的方式給函數參數
class Maker
{
public:
//構造函數
Maker()
{
cout << "調用構造函數" << endl;
}
//拷貝構造
Maker(const Maker& d)
{
cout << "調用拷貝構造" << endl;
}
~Maker()
{
cout << "調用析構函數" << endl;
}
};
void func(Maker m)//Maker m = m1;
{
}
void test01()
{
Maker m1;
func(m1);
}
首先Maker m1會調用構造函數,其次當m1作為參數傳入的時候,實際上是做了Maker m = m1;會調用拷貝構造,緊接著函數結束,調用兩次析構函數。
運行結果如下:
情景2:用一個已有的對象初始化另外一個對象
void test02()
{
Maker m1;
Maker m2(m1);
}
此時Maker m2(m1);會調用拷貝函數
運行結果如下:
情景3:函數的局部對象以值的方式從函數返回
Maker func2()
{
//局部對象
Maker m;
cout << "局部對象的地址:" << &m << endl;
return m;
}
void test03()
{
Maker m1 = func2();//在這裡實際上發生的是Maker m1 = m;就是我們介紹的第一種情況,會調用拷貝構造
cout << "m1對象的地址:" << &m1 << endl;
}
首先調用func2函數,在fun2函數中創建局部對象m,會調用一次構造函數,輸出局部對象的地址,當執行到return 的時候,調用拷貝構造創建對象m1,並且func2函數執行結束,釋放局部對象,m調用一次析構函數,繼續往下執行,輸出m1對象的地址,函數執行結束調用析構函數。
運行結果如下:
深淺拷貝
如果未定義拷貝構造函數,編譯器會默認生成一個拷貝構造函數,默認的拷貝構造函數會對內置類型按記憶體存儲的位元組序完成拷貝,對於自定義類型編譯器會調用他們的默認構造函數,這種拷貝是淺拷貝。
淺拷貝有很大的潛在危險,看下面一串程式碼:
class Student{
public:
Stduent(const char *name, int Age)
{
this->pName = (char*)malloc(strlen(name)+1);
this->age = Age;
}
~Student()
{
cout << "析構函數" << endl;
if(pName!=NULL)
{
free(pName);
pName = NULL;
}
}
public:
char *pName;
int age;
}
void test()
{
Student s1("小花",18);
Student s2(s1);//調用默認的拷貝構造
}
默認的拷貝構造函數會對內置類型按記憶體存儲的位元組序完成拷貝,所以僅僅是對存儲的內容進行拷貝,在s1和s2中pName指向同一塊空間,在調用析構函數釋放的時候,一塊空間會釋放兩次,導致程式崩潰
深拷貝解決淺拷貝問題:自己申請一塊記憶體空間,然後把值傳遞賦給記憶體空間當中,這樣釋放的堆區就不會重複。
Student(const Student& stu)
{
cout << "自己的拷貝構造函數" << endl;
//1.自己申請空間
pName = (char *)malloc(strlen(stu.pName)+1);
//2.拷貝數據
strcpy(this->pName, stu.pName);
age = stu.age;
}
構造函數中的初始化列表
初始化列表: 以一個冒號開始,接著是一個以逗號分隔的數據成員列表,每個”成員變數”後面跟一個放在括弧中的初始值或表達式。
class
{
public:
Date(int year, int month, int day)// 成員變數定義處 給變數開空間
: _year(year)
, _month(month)
, _day(day)
{}
private:
// 成員變數聲明處
int _year;
int _month;
int _day;
};
注意:
- 每個成員變數只能在初始化列表中出現一次
- 初始化列表是幹什麼的?指定調用成員對象的某個構造函數
- 類中包含定義時必須初始化的成員時,必須使用初始化列表
- 初始化列表只能寫在構造函數
- 如果使用了初始化列表,那麼所有的構造函數都需要寫初始化列表
- 可以使用對象的構造函數傳遞數值給成員對象的變數
必須初始化的變數:
- const成員變數(常量)
- 引用成員變數(引用必須在定義時初始化)
- 無默認構造函數(無參、全預設和編譯器給的默認構造函數)的自定義類型成員變數
class AA
{
public:
AA(int a)// AA類無默認構造函數,必須使用初始化列表
{
_a = a;
}
private:
int _a;
};
class Date
{
Date(int year = 1, int month = 1, int day = 1, int i = 10)
:_a(10)
,_ref(i)
,_aa(10)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 成員變數聲明的地方 還未申請空間
int _year;
int _month;
int _day;
// 以下三個成員變數都是必須在定義時初始化
const int _a;
int& _ref;
AA _aa;
};
我們一般盡量使用初始化列表初始化,因為不管你是否使用初始化列表,對於自定義類型成員變數,一定會先使用初始化列表初始化。
一個小問題:這段程式碼的運行結果是什麼?
class A
{
public:
A(int a)
:_a2(a)
,_a1(_a2)
{
}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
A a(10);
a.Print();
return 0;
}
運行結果:
解釋:
成員變數在初始化列表的初始化順序與聲明順序有關,和在初始化列表的先後順序無關。
程式碼中是 _a1 聲明順序先於 _a2 ,所以 _a2 先給 _a1 賦值,因為此時 _a2 裡面放的是隨機值,所以 _a1 是隨機值,然後10賦值給 _a2 ,所以 _a2 就是10.
explicit關鍵字
在C語言中,有一個隱式類型轉換的概念,相近類型,意義相似的類型可以發生隱式類型轉換。例如int和double兩個類型都是表示大小的,所以可以發生。那麼,在自定義類型中是否可以發生這中轉換嗎?
先看下面一串程式碼:
class Date
{
public:
Date(int year, int month,int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "調用構造函數" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1 = { 2022, 1, 21 };
system("pause");
return EXIT_SUCCESS;
}
其中,Date d1 = { 2022, 1, 21 }; 這裡其實是編譯器先用{ 2022, 1, 21 }構造一個匿名對象,然後把這個對象給d1賦值,因為兩次構造是連續的,所以兩個動作會被編譯器優化為一個構造,兩次都是直接構造,但是意義還是不同的。實際上生成了Date對象的臨時量。
運行結果如下:
如果我們不想這種隱式類型轉換髮生,我們一般在構造函數前面加一個explicit的關鍵字。
explicit Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
static成員
概念: 聲明為static的類成員稱為類的靜態成員,用static修飾的成員變數,稱之為靜態成員變數;用static修飾的成員函數,稱之為靜態成員函數。靜態的成員變數一定要在類外進行初始化。
特性:
- 靜態成員屬於整個類,且不佔類的大小
- 靜態成員變數是在類內聲明,類外定義的
- 靜態成員可以直接通過::指定類域來訪問
- 靜態成員函數中沒有this指針,不能訪問非靜態的成員
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_sCount++;
_year = year;
_month = month;
_day = day;
}
// 靜態成員函數 無this指針,可以直接用::指定類域訪問
// 只能訪問靜態成員變數和函數
static int GetCount()
{
return _sCount;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 成員變數聲明的地方
int _year;
int _month;
int _day;
// 靜態成員變數屬於整個類(所有類)生命周期在整個程式運行期間
// 可以直接用::指定類域訪問
static int _sCount;// 類內聲明,類外初始化定義
};
// 靜態必須要在類外初始化定義
int Date::_sCount = 0;
友元
友元提供了一種突破封裝的方式,有時提供了便利。但是友元會增加耦合度,破壞了封裝,所以友元不宜多用。
類的而主要特點之一就是數據隱藏,即類的私有成員無法在類外使用,但是友元打破了這種規則。
友元語法:
- friend關鍵字只出現在聲明處
- 其他類、類成員函數、全局函數都可聲明為友元
- 友元函數不是類的成員,不帶this指針
- 友元函數可以訪問對象任意成員屬性,包括私有屬性
注意事項:
- 友元關係不能被繼承
- 友元關係是單向的。類A是類B的朋友,但是類B不一樣是類A的朋友
- 友元關係不具有傳遞性。類B是類A朋友,類C是類B的朋友,但是類C不一定是類A的朋友
友元函數
class Building
{
//聲明這個全局函數為Building類的友元函數
friend void Goodgay(Building& bd)
public:
Building()
{
keting = "客廳";
woshi = "卧室";
}
private:
string keting;
string woshi;
};
void Goodgay(Building &bd)
{
cout << bd.keting << endl;
cout << bd.woshi << endl;
}
void test()
{
Building bd;
Goodgay(bd);
}
運行結果如下:
//類的成員函數稱為友元
class Building;//聲明類
class GoodGay
{
public:
void func(Building &bud);
};
class Building
{
//聲明GoodGay類的成員函數func成為Building類的友元函數
friend void GoodGay::func(Building& bd);
public:
Building()
{
keting = "客廳";
woshi = "卧室";
}
private:
string keting;
string woshi;
};
void GoodGay::(Building &bd)
{
cout << bd.keting << endl;
cout << bd.woshi << endl;
}
特性:
- 不能用const修飾
- const修飾的是非靜態的成員函數
- 一個類可以是很多函數的友元
友元類
友元類,就是讓一個類稱為另一個類的友元,這樣這個類就可以訪問另一個類中的所有私有屬性。
class Building
{
//聲明GoodF類為Building類的友元類
friend class GoodF;
friend class GoodF2;
public:
Building()
{
keting = "客廳";
woshi = "卧室";
}
public:
string keting;
private:
string woshi;
};
class GoodF
{
public:
void func(Building& bd)
{
cout << bd.keting << endl;
cout << bd.woshi << endl;
}
};
//2.通過類內指針訪問類的私有成員
class GoodF2
{
public:
GoodF2()
{
cout << "無參構造" << endl;
pbu = new Building;
}
void func()
{
cout << pbu->keting << endl;
cout << pbu->woshi << endl;
}
//拷貝構造
GoodF2(const GoodF2& f2)
{
cout << "拷貝構造" << endl;
//1.申請空間
pbu = new Building;
}
~GoodF2()
{
delete(pbu);
pbu = NULL;
}
public:
Building* pbu;
};
內部類
概念: 如果一個類定義在另一個類的內部,這個內部類就叫做內部類。
特性:
- 外部類天生就是內部類的友元類,內部類具有訪問外部類私有屬性的許可權,但外部類卻沒有訪問內部類的私有屬性的許可權
- 內部類只是受外部類的類域的限制,內部類並不屬於外部類,也就是sizeof(外部類)=外部類
class A
{
private:
static int _a;
int _b;
public:
// 內部類
// B天生就是A的友元 B可以訪問A的私有成員,但是A不可以訪問B的私有
// sizeof(A) = 4 A中成員變數b a屬於整個類,不佔對象的大小 內部類B和在全局定義的基本一致,只是受A類域的限制
class B
{
public:
void f()
{
}
private:
int _a;
};
};
int A::_a = 10;