類和對象
- 類的定義
1、 類的定義包括數據的定義和方法的定義。
2、類中數據的定義不允許直接進行初始化;
3、方法的定義可以通過類型 類名::方法名()放置類體外,但必須要在類內進行聲明。
- 數據成員訪問
除靜態成員外,數據成員的訪問需要通過對象來實現,類的定義並不會爲其分配內存空間,只有實例化後纔有內存空間的分配。
1、public:公有成員,可被類內,子類,類外進行訪問;
2、protected:保護成員,可被類內,子類進行訪問;
3、private:私有成員,僅可被類內進行訪問;
使用方式如下:
class User(){
private:
私有數據定義,私有方法定義;
public:
公有數據定義,公有方法定義;
protected:
保護數據定義,保護方法定義;
- const
類函數後面使用const關鍵字,表示該方法不會修改類的數據成員,但對於指針來說,可以修改指向的數據,但不能修改地址。
格式:類型 函數名(參數)const{};
- 指針對象
將類實例化成一個指針,訪問數據將.改成->。
1、類名 *實例名
2、類名 *實例名 = new 類名 —— delete 實例名
3、類名 *實例名 = new 類名() —— delete 實例名
如果將類實例爲常量指針,則對象只允許調用const方法。
const 類名 *實例名 = new 類名 —— delete (類名 *)實例名
- 構造函數
定義對象使被調用,系統默認提供了構造函數,可以自定義多個構造函數。只有自定義了構造函數,系統就不會提供默認的構造函數。
功能:方便類的數據初始化。
格式要求如下:
1、構造函數名與類名相同;
2、參數個數沒有限制,沒有參數的構造函數爲默認構造函數;
3、沒有返回值
4、方法內可以進行類數據的初始化,通:數據成員(值)實現,放置在函數名和函數體中間。
默認的複製構造函數:
功能:將函數的實例化作爲參數進行按值傳遞,編譯器會調用該類的複製構造函數,來複制實際參數到被調用函數。
場景:
1、複製對象把它作爲參數傳遞給函數;
2、通過使用另一個同類型的對象來初始化新創建的對象;
3、複製對象,並從函數返回這個對象;
格式:函數名與類同名,參數只有一個該類的常量引用類型。
建議:編寫函數,儘量使用按引用方式傳遞參數,這樣可以避免調用複製構造函數,極大提高程序的執行效率。
- 析構函數
程序結束或使用delete釋放對象時被調用。
格式如下:
1、函數名與類名相同,但函數名前需加~;
2、沒有參數
3、沒有返回值,不需要類型,連void也不需要加。
- 內聯成員函數
與內聯函數類型,定義在類體中的成員函數默認是內聯成員函數,定義在類體外需要在類體聲明前加上inline關鍵字或者在類外實現部分加上inline。
一個較爲合理的經驗準則是, 不要內聯超過 10 行的函數.
- 靜態類成員
格式:定義數據前加上static關鍵字
功能:
1、可直接通過類名進行初始化和訪問,也可通過實例訪問。
2、一個類的靜態成員被類的所有實例共享,且只爲它開闢一處內存空間,故只有有一個實例對其進行修改,其他實例的這個值也會隨之改變。
可作爲成員函數的默認參數,其他成員不行。
注意點:
1、靜態成員可以是當前類的類型,其他普通數據成員只能是當前類的指針或引用類型。
2、靜態函數只能訪問靜態成員數據、其他靜態成員函數和類外部的其他函數。
3、靜態函數不能定義爲const函數。
4、在類體外定義靜態函數,不能標識static關鍵字。
5、靜態成員函數有一個類範圍,他們不能訪問類的 this 指針。您可以使用靜態成員函數來判斷類的某些對象是否已被創建。
- 隱藏的this指針
定義:在 C++ 中,每一個對象都能通過 this 指針來訪問自己的地址。this 指針是所有成員函數的隱含參數。因此,在成員函數內部,它可以用來指向調用對象。
功能:使得類的非靜態成員,不同實例之間的操作互不干擾。
機制:編譯器在成員函數中自動添加了this指針用於對數據成員或方法的訪問,並在函數調用時將實例地址隱含作爲實參傳遞給this。
提示:友元函數沒有 this 指針,因爲友元不是類的成員。只有成員函數纔有 this 指針。
- 運算符重載
目的:爲了讓對象之間、對象和數據之間實現運算符的相關操作。
下面實現實例間的四則運算:
class User {
public:
int age;
User() {
;
}
User(const int age_) {
age = age_;
}
User operator+(const User& user) {
User aa;
aa.age = age + user.age ;
return aa;
}
User operator-(const User& user) {
User aa;
aa.age = age - user.age;
return aa;
}
User operator/(const User& user) {
User aa;
aa.age = age / user.age;
return aa;
}
User operator*(const User& user) {
User aa;
aa.age = age * user.age;
return aa;
}
};
User x, y(100), z(50);
x = y + z;
cout<< x.age << endl;
x = y - z;
cout << x.age << endl;
x = y / z;
cout << x.age << endl;
x = y * z;
cout << x.age << endl;
對於自加和自減, 默認情況下,重載符沒有參數,表前置,有參數表後置
class User {
public:
int age;
User() {
;
}
User(const int age_) {
age = age_;
}
void operator++() {
++age;
}
void operator++(int) {
age++;
}
};
下面實現將對象賦值給整型,以及將整型賦值給對象。
class User {
public:
int age;
User() {
;
}
User(const int age_) {
age = age_;
}
void operator=(int a) {
age = a;
}
operator int() {
return age;
}
};
User x, y(100);
int a(66);
x = a;
cout << x.age << endl;
a = y;
cout <<a << endl;
- 友元類和友元方法
目的:使得規定的全局函數或者其他類可以訪問該類的私有數據和方法。定義友元類可以使得指定類可以訪問改類該的私有成員,定義定義友元方法可以指定類中的某一方法可以訪問改類的私有成員。
設定友元類:在類的public內加入下面一句,設定類名1爲自身類的友元類。
friend class 類名1;
設定友元方法:在類的public內加入下面一句,設定類名1的方法1爲自身類的友元方法。
friend 類型 類名1:方法1(參數);
友元方法不僅可以是類的成員函數,也可以是一個全局函數。
friend 類型 函數名(參數);
由於友元方法沒有this指針,要訪問非static成員時,需要對象做參數;
- 類的繼承
定義:當創建一個類時,您不需要重新編寫新的數據成員和成員函數,只需指定新建的類繼承了一個已有的類的成員即可。這個已有的類稱爲基類,新建的類稱爲派生類。
一般格式:
class 子類名: public 基類
一個類從另一個類繼承,有三種派生類型,公有型、保護型、私有型。繼承導致成員的變換如下:
子類的重寫的父類的方法,將會隱藏父類的方法,要想在子類中使用父類的方法,可通過子類.父類::方法名調用即可。
類的繼承不包括繼承父類的構造函數、解析函數、複製構造函數、友元函數、重載運算符函數。
- 虛函數
我們可以定義一個父類的類型指針,但指向子類的構造函數創建的實例。
父類 *實例名 = new 子類();
如果此刻通過實例名調用一個父子類共有的方法,那麼執行的是誰的方法呢?
答:答案是調用父類的方法,因爲編譯器對此方法進行的是靜態綁定,即根據對象定義時的類型來確定調用哪個類的方法。
那有沒有方法讓其調用子類的方法呢?
答:有一種成員函數叫“虛方法”,定義前使用virtual關鍵字,可實現該方法的“動態綁定”,即根據對象運行時的類型來確定調用哪個類的方法,而不是根據對象定義時的類型來確定。
父類的虛函數,即使子類重寫了此函數,但也是虛函數。這是因爲覆蓋的函數放置在了原來父類在虛表的位置。可參考
正常的繼承,一般時先調用父類的構造函數、子類的構造函數、子類的析構函數,最後到父類的析構函數。
但如果定義的是父類類型的指針,但指向子類的實例,則正常情況下是不會調用子類的析構函數,這將導致子類中分配內存的數據成員無法被釋放,從而出現內存泄漏。但如果析構函數是虛函數,則會先調用子類的析構函數再調用父類的析構函數。
所以,在編寫類的析構函數通常是虛函數。
- 純虛方法
格式:在虛方法基礎上,結尾添加=0;,沒有函數體,如下所示:
virtual 類型 函數名(參數) = 0;
作用:包含純虛方法的類稱爲抽象類,不能被實例化,通常充當父類。
抽象類的子類如果實現了父類所有的純虛方法,那麼子類就可以進行實例化。
- 多繼承
一般格式:
class 子類名: public 基類1,public 基類2,public 基類3
如果出現不同基類存在同名的方法,而你恰好需要調用它。如果直接調用會出現編譯錯誤,需要通過::指定調用哪個基類。實例名.基類::方法()
- 虛繼承
出現原因:多繼承中,如果發生瞭如:類B繼承類A,類C繼承類A,類D同時繼承了類B和類C。最終在類D中就有了兩份類A的成員,這在程序中是不能容忍的。
一般格式:
class 類名: virtual 繼承方式 父類名
作用:C++編譯系統在實例化D類時,只會將虛基類的構造函數調用一次,忽略虛基類的其他派生類(class B,class C)對虛繼承的構造函數的調用,從而保證了虛基類的數據成員不會被多次初始化。
- 嵌套類和局部類
類中嵌套一個類,嵌套類只能被外圍類訪問,其他地方是不可見的。
局部類定義在函數體內,這個類只能被函數體內訪問,函數體外是不能訪問的。
類模板
下面以單向鏈表爲例,通過類模板實現對不同數據類型的結點進行相同操作。
#include <iostream>
using namespace std;
class Snode {
/*定義結點,存放數據和指向下一個結點的指針*/
public:
int data;
Snode* next;
Snode() {
next = NULL;
}
};
class Cnode {
/*定義結點,存放數據和指向下一個結點的指針*/
public:
char data;
Cnode* next;
Cnode() {
next = NULL;
}
};
template<class Type>
class SList {
/*定義一個鏈表類,用於創建鏈表,包含添加、遍歷、釋放數據的方法*/
private:
Type* head; //定義頭節點
int node_sum; //結點數量
public:
SList() {
head = NULL;
node_sum = 0; //初始化數據
}
Type* Movetrail() {
/*移動結點到鏈表尾部,方便插入數據*/
Type* temp = head; //定義一個臨時結點指向頭節點
for (int i = 1; i < node_sum; i++) //有n個結點,就移動n-1次
{
temp = temp->next;
}
return temp; //返回尾結點
}
void Add_node(Type* pnode) {
/*移動到最後一個結點,添加數據,一次添加一個,需要判斷鏈表是否爲空,若爲空直接將參數結點賦值給頭節點*/
if (node_sum == 0)head = pnode;
else {
Type* temp = Movetrail();
temp->next = pnode;
}
node_sum++;
}
void Passlist() {
/*遍歷鏈表,依次輸出結點數據*/
Type* temp = head;
if (node_sum == 0)cout << "鏈表爲空" << endl;
else {
for (int i = 0; i < node_sum; i++) {
cout << temp->data << '\t';
temp = temp->next;
}
}
}
~SList() {
/*釋放除頭結點外的所有結點的內存空間,並將頭節點置空。從前往後釋放*/
if (node_sum > 0) {
Type* pdelete = head;
Type* temp;
for (int i = 0; i < node_sum; i++)//n個結點,正常釋放n-1個,算上自定義的pdelete結點,共釋放n個結點
{
temp = pdelete->next;
delete pdelete;
pdelete = temp;
}
node_sum = 0;
pdelete = NULL;
temp = NULL;
}
head = NULL;
}
};
int main() {
SList<Snode> list;
for (int i = 1; i <= 5; i++) {
Snode* node = new Snode();
node->data = i;
list.Add_node(node);
}
list.Passlist();
cout << endl;
SList<Cnode> list1;
for (int i = 97; i <= 101; i++) {
Cnode* node1 = new Cnode();
node1->data =i;
list1.Add_node(node1);
}
list1.Passlist();
return 1;
}
類模板內也可以自定義靜態數據,不同類型的類模板實例的靜態數據是不同的,無法共享。但同類型的模板實例,靜態數據是共享的。