1.類的定義
1.1 類定義格式
- class為定義類的關鍵字,Stack為類的名字,{ }中的內容是類的主題為了,注意類定義結束時后面的分號不能省略。類體中的內容稱為類的成員:類中的變量稱為類的屬性或成員變量;類中的函數稱為類的方法或者成員函數。
- 為了區分成員變量,一般習慣上成員變量會加上一個特殊標識,如成員變量前面或者后面加上_或者是m開頭,注意C++中這個并不是強制的,只是一些慣例,具體看公司的要求。
- C++中struct也可以定義類,C++兼容C中的struct的用法,同時struct升級成了類,明顯的變化是struct中可以定義函數,一般情況下我們還是推薦用class定義類。
- 定義在類里面的成員函數默認為inline。
//定義一個類
class Date
{void Init(int year, int month, int day){_year = year;_month = month;_day = day;}//為了好區分到底是成員變量還是參數,一般在成員變量前面加上_或是mint _year;int _month;int _day;};//C++中兼容C語言中的struct的用法
typedef struct ListNodeC
{struct ListNodeC* next;int val;
}ListNode;//在C++中struct升級為類了
//不再需要typedef,ListNodeCPP就可以代表類型struct ListNodeCPP
{void Init(int x){next = nullptr;val = x;}ListNodeCPP* next;int val;
};int main()
{ListNode node1;struct ListNodeC node2;ListNodeCPP node3;return 0;
}//class類如果沒有訪問限定符的修飾,默認是private
//struct類如果沒有訪問限定符的修飾,默認是publicclass Stack
{//默認在類里面的成員函數都是內聯,但是展不展開是編譯器的事情
public:void Init(int n=4){arr = (int*)malloc(sizeof(int) * n);if (arr == nullptr){perror("realloc fail!\n");return;}capacity = n;top = 0;}void Push(int x){// ...擴容arr[top++] = x;}int Top(){assert(top > 0);return arr[top - 1];}void Destroy(){free(arr);arr = nullptr;top = capacity = 0;}private:int* arr;size_t top;size_t capacity;
};int main()
{Date d1;//error C2248 : “Date::Init” : 無法訪問 private 成員(在“Date”類中聲明)d1.Init(2024, 11, 11);//如果想要Init被訪問在Date類加上public,就可以被訪問了//或者是將class類改為struct類Stack st1;st1.Init(10);st1.Push(1);st1.Push(1);st1.Push(1);//int top = st1->arr[st1->top - 1]; C中實現top方法會更加自由,不能進行更好的管理int top=st1.Top();//C++的模式下會更加規范,會更好的進行管理,因為私有成員不能訪問,只能訪問公有函數st1.Destroy();return 0;
}
1.2 訪問限定符
- C++一種實現防撞的方式,用類將對象的屬性和方法結合在一塊,讓對象更加完善,通過訪問權限選擇性的將其接口提供給外部的用戶使用。
- publi修飾的成員在類外可以直接被訪問;protected 和 private 修飾的成員在類外不能直接被訪問,protected 和 private 是一樣的,在之后的繼承章節才能體現出它們的區別。
- 訪問權限作用域從該訪問限定符出現的位置開始直到下一個訪問限定符出現為止,如果后面沒有訪問限定符,作用域就到 } 即類結束。
- class定義成員沒有被訪問限定符修飾時默認為private,struct默認為public。
- 一般成員變量都會被限制為private/protected,需要給別人使用的成員函數會為public。
1.3 類域
- 類定義一個新的作用域,類的所有成員都在類的作用域中,在類體外定義成員時,需要使用 :: 作用域操作符指明成員屬于哪個類域。
- 類域影響的是編譯的查找規則,下面程序中Init如果不指定類域Stack,那么編譯器就把Init當成全局函數,那么編譯時,找不到array等成員的聲明/定義在哪里,就會報錯。指定類域Stack,就是知道Init是成員函數,當前域找不到的array等成員,就會到類域中去查找。
//Stack.h#include<iostream>
using namespace std;
class Stack
{
public://成員函數 唯一不同的:只聲明不定義void Init(int n = 4);//缺省參數:(int n = 4)不能在定義和聲明的地方同時給,只能在聲明的地方給
private:int* arr; //聲明——不開空間size_t top;size_t capacity;
};
//Stack.cpp#define _CRT_SECURE_NO_WARNINGS 1#include"Stack.h"//聲明和定義分離,需要指定類域
//加上 Stack:: 類域,就不會報錯,arr會到Stack這個域里面去找,否則arr不知道去哪里找
//不加上Stack:: 類域,“arr”: 未聲明的標識符//error C2065 : “capacity”: 未聲明的標識符//error C2065 : “top”: 未聲明的標識符
void Stack::Init(int n)
{//這里的Init還是符合只能在類里面使用,不是在類外面,只是類的聲明和定義分開了而已//還是符合私有的在類里面使用不能在類外面使用arr = (int*)malloc(sizeof(int) * n);if (arr == nullptr){perror("realloc fail!\n");return;}capacity = n;top = 0;
}
//Test.cppint main()
{Stack st;st.Init();return 0;
}
類域的作用:
不同的域中可以定義同樣的函數,隔離出了類和類之間的命名沖突
2. 實例化
2.1 實例化概念
- 用類型在物理內存中創建對象的過程,稱為類實例化出的對象。
- 類是對象進行一種抽象描述,是一個模型一樣的東西,限定了類有哪些成員變量,這些成員變量只是聲明,沒有分配空間,用類實例化出對象時,才會分配空間。
- 一個類可以實例化出多個對象,實例化出的對象占用實際的物理空間,存儲類成員變量。例如:
- 類實例化處對象就像是現實中使用建筑設計圖建造出房子,類就像是設計圖,設計圖規劃了多少個房間,房間大小功能,但是沒有實體的建筑存在,也不能住人,用設計圖修建出房子,房子才能住人。同樣類就像是設計圖一樣,不能存儲數據,實例化出的對象分配物理內存存儲數據。
//Stack.h#include<iostream>
#include<assert.h>
using namespace std;
class Stack
{
public://成員函數 唯一不同的:只聲明不定義void Init(int n = 4);//缺省參數:(int n = 4)不能在定義和聲明的地方同時給,只能在聲明的地方給
//private:int* arr; //聲明——不開空間size_t top;size_t capacity;
};
//類實例化對象——對象是實實在在需要內存的,在內存上面存儲數據的#include"Stack.h"
int main()
{//類實例化對象// 對象定義,也就是成員變量的定義,因為成員變量是對象的一部分//定義——開空間,定義不等于初始化Stack st1; //這就是類實例化對象Stack st2; //這就是類實例化對象Stack st3; //這就是類實例化對象//st1、st2、st3才有空間//st1.top = 1; (將成員變量改為public,否則不能訪問)雖然這樣是正確的,因為定義之后有空間了, 但是這樣的寫法不規范,因為top是私有的,//要想改變的話,通過公有的函數來進行改變,這就使C++更加規范//Stack::top = 1;//top沒有空間,不能這樣寫//對象不一定有聲明,但是類是必須要有聲明的,如果類沒有聲明的話,怎么能用類去實例化對象呢return 0;
}
2.2 對象大小
對象的大小 —— 只計算成員變量,不計算成員函數,遵循內存對齊原則
內存對齊規則
- 第一個成員在與結構體偏移量為0的地址處。
- 其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處。
- 注意:對齊數 = 編譯器默認的一個對齊數 與 該成員大小的較小值。
- VS中默認的對齊數是8
- 結構體總大小為:最大對齊數(所有變量類型最大者與默認對齊參數取最小)的整數倍。
- 如果嵌套了結構體的情況,嵌套的結構體對齊到自己的最大對齊數的整數倍處,結構體的整體大小就是在所有最大對齊數(含嵌套結構體的對齊數)的整數倍。
#include"Stack.h"
int main()
{//定義——開空間,定義不等于初始化Stack st1; //類實例化對象Stack st2; Stack st3; cout << sizeof(st1) << endl; //12 x86cout << sizeof(Stack) << endl; //12 x86st1.top = 1; //(將成員變量改為public,否則不能訪問)st1.Init(); // call 函數地址(不需要存在對象里面)st2.top = 2; //(將成員變量改為public,否則不能訪問)st2.Init();return 0;
}
運行結果:
為什么運行出的結果都是 12 ???
調試 ---> 轉到反匯編
通過調試,轉到反匯編,可以看見st1.Init(); 和 st2.Init();調用的都是相同的地CA104Bh)
而 st1 和 st2 各自開空間存儲自己的成員變量,指向的不是同一塊地址
成員函數的地址不在對象里面,那成員函數的地址又是聲明時候確定的呢??
函數的地址是在編譯的時候確定的
所以:
對象的大小 —— 只計算成員變量,不計算成員函數,遵循內存對齊原則
// 計算一下A/B/C實例化的對象是多大?
class A
{
public:void Print(){cout << _ch << endl;}
private:char _ch;int _i;
};class B
{
public:void Print(){//...}
};class C
{
};int main()
{A a;B b;C c;cout << sizeof(a) << endl; //8cout << sizeof(b) << endl; //1cout << sizeof(c) << endl; //1return 0;
}
運行結果:
分析:
- 類A遵循內存對齊原則,所以大小為:8
- B和C一樣:成員函數不占空間,為什么是 1 ???
系統設的機制,B和C都是空類,雖然B有成員函數,成員函數不占空間,1 是為了占位,表示對象存在過,如果1個字節都不給的話,怎么能表示這個對象定義出來了呢?如果一個字節都不開,地址用怎樣給呢?這里開一個字節不存儲有效數據,純粹是為了占位,表示這個對象還在,那為什么不是 2、3呢?肯定是越少越好嘛,節省空間
3. this 指針?
- Date類的d1和d2涉及到的Init和Print函數都是相同的空間,但是打印出來的值為什么是不同的呢??難道是因為 Init 傳了參數,Print 沒有傳參數 ??? —— 答案不是這樣的,引出 this 指針
- 編譯器編譯后,類的成員函數默認都會在形參第?個位置,增加?個當前類類型的指針,叫做 this?指針。實際上:void Init(const Date* this,int year, int month, int day)
- 類的成員函數中訪問成員變量,本質都是通過this指針訪問的,如Init函數中給_year賦值,this- >_year = year;
- C++規定不能在實參和形參的位置顯示的寫this指針(編譯時編譯器會處理),但是不能在實參或形參的位置顯示的加,但是可以在函數里面使用 why???? ?---->之后會提到
class Date
{
public://實際上:void Init(const Date* this,int year, int month, int day)void Init(int year, int month, int day){this->_year = year; //_year訪問的是344行的_year嗎?不是,因為沒空間,編譯語法設計的方面//用一個變量和函數要找到他的出處,出處可以是定義,也可以是聲明,意味著這個變量和和函數是你自己定義的,在編譯的時候找到他的聲明就夠了this->_month = month; //但是在打印和初始化設計的是實實際際的空間this->_day = day;}//實際上:void Print(const Date* this)void Print(){cout << _year << "/" << _month << "/" << _day << endl;}//為了好區分到底是成員變量還是參數,一般在成員變量前面加上_或是m
private:int _year; //344行int _month;int _day;};int main()
{Date d1;Date d2;d1.Init(2024, 7,3);d2.Init(2024, 4,27);// d1.Print(&d1); d1.Print(); // 2024 / 7 / 3// d2.Print(&d2); d2.Print(); // 2024 / 4 / 27}
分析:? ??
?這里的d1和d2涉及到的Init和Print函數都是相同的空間,但是打印出來的值為什么是不同的呢??
難道是 Init 傳了參數,Print 沒有傳參數 ??? —— 引出 this 指針
隱含的this 指針 —— 隱含的指的是:編譯器會在成員函數的實參和形參的位置加一個this指針的參數
void Init(int year, int month, int day)看見的是3個參數,實際是4個參數
void Print()看見的是0個參數,實際上是1個參數,實際上是編譯器加的
void Init(const Date* this,int year, int month, int day)
void Print(const Date* this)
在變量訪問的地方會在成員變量前面自動加上 this->
所以在調用同一個函數,訪問到了不同的變量
在調用d1.Print(); 和 d2.Print();的時候分別將d1和d2的地址傳過去了
所以用this指針在調用同一個函數,訪問到了不同的變量,但是不能在實參或形參的位置顯示的加,但是可以在函數里面使用 why???? ?---->之后會提到
相應題目:
1.下面程序編譯運行結果是()
A.編譯報錯? ? ? ? ? ? ? ? ? ? ? ? ? ? ?B.運行崩潰? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?C.正常運行
class A
{
public:void Print(){cout << "A::Print()" << endl;}
private:int _a;
};int main()
{A* p = nullptr;p->Print(); return 0;
}
運行結果:
可以看見代碼并沒有問題,正常運行,所以選C
分析:
p->Print();
從匯編的角度:call Print(地址),地址是在編譯的時候確定的
雖然這里有一個 -> 但是卻沒有解引用,不是說看見箭頭就是解引用,要去看它實際轉換成的動作是什么,實際的動作是調用函數,在不在對象里面??—不在
p的作用是:
1.編譯器調用成員函數,編譯器要知道Print()成員函數從哪里調的,用對象的指針去調用就知道他是他(類)的成員函數,編譯的時候符合語法找Print的出處,就到類里面去找
2.調用成員函數要傳遞this指針,相當于將p傳給了this,但是this指針是一個空指針,是不會報錯的
所以從始至終都有空指針,但是并沒有進行解引用,所以不會報錯
2.下面程序編譯運行結果是()
A.編譯報錯? ? ? ? ? ? ? ? ? ? ? ? ? ? ?B.運行崩潰? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?C.正常運行
class A
{
public:void Print(){cout << "A::Print()" << endl;cout << _a << endl; // cout << this->_a << endl; 空指針的訪問 }
private:int _a;
};int main()
{A* p = nullptr;p->Print();return 0;
}
運行結果:
代碼和上一道題幾乎一樣,但是可以看見運行有問題,只是因為對空指針進行了解引用操作
分析:
this 指針是一個空指針,用代碼來驗證一下:
class A
{
public:void Print(){cout << this << endl;}
private:int _a;
};int main()
{A* p = nullptr;p->Print();return 0;
}
運行結果:
上面的運行結果驗證了 this 確實是空指針,所以下面的代碼會報錯,因為不能對空指針進行進引用操作
cout << _a << endl;
cout << this->_a << endl; ?空指針的訪問?
考點:
1.對象里面沒有存儲成員函數的指針
2.隱含的傳this指針,誰傳過去?對象——就傳對象的地址,指針——直接就是指針傳過去,成員變量前面要加this:this->_a3.所以這道題選A
上面的兩個例子初不初始化都無所謂,大不了就是隨機值,跟初始化沒有關系
3.this指針存在內存哪個區域中()
A.棧? B.堆? ?C.靜態區? D.常量區? ? E.對象里面
分析:
所以這道題?選 A.棧
拓展:
4. C實現Stack核心代碼? 和? C++實現Satck核心代碼對比:
面向對象顯著的三大特性:封裝、繼承、多態(當然還有其它的特性),下面是對封裝的初步介紹:
- C+中的數據和函數都放到了類里面,通過訪問限定符進行了限制,不能再隨意的通過對象直接修改數據,這就是C++封裝的一種體現,這個是最重要的變化。這里的封裝的本質是一種更嚴格規范的管理,避免出現訪問修改的問題。
- C++中會有一些相對方便的語法,比如Init給的缺省參數會方便很多,成員函數每次不需要傳對象地址,因為this指針隱含的傳遞了,方便了許多。