目錄
一、面向過程和面向對象初步認識
二、類的引入和定義
2.1類的引入
2.2類的定義
三、類的訪問限定符及封裝
3.1訪問限定符?
3.2封裝?
四、類的作用域
五、類的實例化
六、類的對象大小的計算
6.1如何計算對象的大小
6.2類對象的存儲方式?
七、類成員函數的this指針
7.1this指針的引出
7.2this指針的特性
一、面向過程和面向對象初步認識
?面向過程:關注的是過程,分析出求解問題的步驟,通過函數調用逐步解決問題。
例如:洗衣服
拿盆子——>放水——>放衣服——>放洗衣粉——>手搓——>換水——>手搓——>擰干——>晾衣服?
我們知道C語言面向的是過程,例如我們要得到兩數之和的結果,那么得造一個和函數,先進行傳實參,形參相加,得到參數之和,返回之和的值,這就是得到兩數之和的一個過程。
面向對象:關注的是對象,將一件事情拆分成不同的對象,靠對象之間的交互完成。
?還是洗衣服,有四個對象:人、衣服、洗衣粉、洗衣機?
那么洗衣服過程:人將衣服放入洗衣機、倒入洗衣粉,啟動洗衣機,洗衣機就會完成洗衣過程,人不需要關注洗衣機是如何洗衣服的。
那么照樣我們也要得到兩數之和的結果,就不需要去實現函數了,而是直接用函數就行,這個函數是已經被實現的,這就是直接使用對象,不需要關注實現過程。
那么還有疑問的是,對象之間的操作怎么看起來也是一個過程,其實是不同角度產生的看法,那么對于同一件事情,洗衣服,從過程來看,這個衣服需要執行被洗的過程,在乎的是怎么洗的過程,而從對象來看,只需要用洗衣機完成洗衣服,怎么洗的過程不在乎。其實這就相當與面向對象是以更高的角度去看待面向過程。
我再從更高的角度去看待面向對象,那么面向對象又可以看成面向過程。所以對于任何實現從不同角度既可以看成面向過程,也可以看成面向對象
對于c++我們將其看成面向對象的語言,因為對標C語言,c++很多實現要比C語言簡單的多,不用造輪子(自己實現沒必要去浪費時間做的函數),當然你以更高的角度來看c++實現的函數也可以看成過程?,但是你得在更高的角度弄出點東西來,不然也改變不了啥。
二、類的引入和定義
2.1類的引入
回顧C語言實現棧時,需要有一個結構體存放定義的變量,而具體的實現需要額外定義函數完成壓棧、出棧、銷毀棧等。那么在c++中,結構體不僅可以定義變量,也可以定義函數了,同樣我們用棧來舉個例子
#include <iostream>
using namespace std;typedef int DataType;
struct stack
{//棧的初始化void Init(size_t capacity){_array = (DataType*)malloc(sizeof(DataType*) * capacity);if (nullptr == _array){perror("malloc fail");exit(-1);}_capacity = capacity;_size = 0;}//壓棧void Push(const DataType& data){_array[_size] = data;++_size;}//取隊頭元素DataType Top(){return _array[_size - 1];}//棧的銷毀void Destroy(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}//變量的定義DataType* _array;size_t _capacity;size_t _size;
};int main()
{stack s;s.Init(5);s.Push(1);s.Push(2);s.Push(3);cout << s.Top() << endl;s.Destroy();return 0;
}
運行結果:
?
?從上面代碼可以看出,我們將變量的定義與函數的實現都放在結構體中,且這些變量可以在函數中使用,像這種將函數和變量都放在一個結構體中,在這種情況下,在c++中我們稱之為類,也就是說,C語言中的結構體在c++中升級成了類,當然類依然支持C語言中的結構體用法,畢竟c++兼容C語言嘛。那么接下來講解類定義的詳細用法
注意:上述結構體的定義,在c++中更喜歡用class來代替,其實struct和class還是有區別的,在類的訪問限定中會講解。
2.2類的定義
形式規則:
class className
{//類體:由成員函數和成員變量組成};//一定要注意后面的分號
?class為定義類的關鍵字,className為類的名字,{}中為類的主體,注意類定義結束時后面分號不能省略。
類體中內容稱為類的成員:類中的變量稱為稱為類的屬性或成員變量;類中的函數稱為類的方法或者成員函數
以上是類定義的形式規則,但對于類中的成員函數定義和成員變量命名也有一定的規則。
成員函數的定義有兩種方式:
1.成員函數的聲明和定義都放在類體中,需要注意的是,編譯器可能會將其當成內聯函數處理。
?2.類聲明放在.h文件中,成員函數放在.cpp中,注意:成員函數名前需要加類名::
//聲明放在類的頭文件fun.h中
#include <iostream>
using namespace std;class Person
{
public://顯示基本信息void showinfo();
public:char* _name;char* _sex;int _age;
};//定義放在類的實現文件中
#include "fun.h"void Person::showinfo()
{cout << _name << "-" << _sex << "-" << _age << endl;
}int main()
{Person s;s._name = (char*)"張三";//由于張三是字面常量,類型為const char*而_name為char*類型s._sex = (char*)"男";s._age = 18;s.showinfo();return 0;
}
?運行結果:
一般情況下,最好采用第二種方式,但在之后的講解中,為了方便,可能會使用方式一。但在以后的工作中的話,那盡量使用方式二?。
成員變量命名規則的建議:
#include <iostream>
using namespace std;class Date
{
public:void Init(int year){//這里的year到底是成員變量還是函數形參,雖然知道左邊的year是定義的變量,而右邊的year是形參的year//但難免帶來一種令人不友好的感覺year = year;//所以一般建議定義的變量加上前綴或者后綴,對于我而言習慣加上前綴int _year = year;int year_ = year;int myear = year;}
private:int year;int _year;int year_;int myear;
};int main()
{Date d;d.Init(3);return 0;
}
注意:默認直接在類里面定義的函數就是inline函數,不需要加inline關鍵字,當然在編譯時展不展開函數還是取決編譯器 。
三、類的訪問限定符及封裝
3.1訪問限定符?
?在上述代碼中,看到我們加了public、private這類符號,是什么?
像這類符號我們稱之為訪問限定符,其實,這是c++實現封裝方式的必要手段,用類將對象的屬性和方法結合在一塊,讓對象更加完善,再通過訪問訪問限定符選擇性的將其接口提供給外部的用戶使用。
那具體是啥意思,訪問限定符有哪些?look
訪問限定符,用來限定類外是否能訪問類里面的成員,訪問限定符分為三類,public(公有)protected(保護)和private(私有)
訪問限定符說明:
1.public表示類外是可以訪問類里面被public修飾的成員,
2.protected和private表示類外不能直接訪問類里面被這兩個修飾的成員,他們是類似的,但還是有區別的,在繼承階段會講解。
3.訪問權限作用域從該訪問限定符出現的位置開始知道下一個訪問限定符出現時為止,如果后面沒有訪問限定符,作用域就到 } 即類結束
4.上面我們提到了class和struct是有區別,對于class,其成員的默認訪問權限為private,而struct為public(想想,雖然在c++中struct升級成了類,但依然支持C語言struct的用法,而C語言的struct成員都是可以直接被訪問的,不需要限定符。所以在這里struct的默認訪問權限是public)
注意:訪問限定符只在編譯時有用,當數據映射到內存后,沒有任何訪問限定符上的區別?
3.2封裝?
對于對象而言,無非就三大特性:封裝、繼承、多態。繼承和多態后面的章節再說,在類和對象階段,主要說的就是封裝,那么什么是封裝呢?
封裝:將數據和操作數據的方法進行有機結合,隱藏對象的屬性和實現細節,僅對外公開接口來和對象進行交互,封裝本質上是一種管理,讓用戶更方便實用類。說白了,不就是用類將他們包起來,再通過訪問限定符是否對外提供接口來實現交互。
在現實中,這個道理也是一樣的,像電腦,里面的零件都是被封裝的,然后提供一個USB接口給用戶使用。
四、類的作用域
在上面,我們提到并給出了代碼,類中的函數長的聲明和定義要分離,分離定義的函數怎么與類中聲明的函數進行聯系呢?跟命名空間一樣,類也是一個作用域,類的所有成員在類的作用域中,類外定義成員時,需要使用::作用域操作?符指明成員屬于那個類域
?
#include <iostream>
using namespace std;class Person
{
public://打印基本信息void printpersoninfo();
public:char _name[20];char _gender[3];int _age;
};//指明函數屬于哪個類域
void Person::printpersoninfo()
{cout << _name << "-" << _gender << "-" << _age << endl;
}int main()
{Person s;strcpy(s._name , "李四");strcpy(s._gender , "女");s._age = 19;s.printpersoninfo();return 0;
}
運行結果:
?
五、類的實例化
?僅僅有一個類不能對里面的內容進行訪問,而是要創建一個對象,通過對象來對類中的成員進行訪問。用類類型創建對象的過程,稱為類的實例化。
類是對對象進行描述的,是一個模型一樣的東西,限定類有那些成員,定義出一個類并沒有分配實際的內存空間來存儲它。必須實例化出對象才占有實際的物理空間,存儲類成員變量。且一個類可以實例化出多個對象。
比如:房子設計圖,就是一個類,設計圖描述了房子的各種信息,但是并沒有為這些信息建造出一個實際的空間。而只有建造出了真正的房子才是實例化,才分配了空間,且通過設計圖,也可以建造出多個房子,即實例化多個對象。再通過代碼來比較。
#include <iostream>
using namespace std;class Person
{
public://顯示基本信息void printpersoninfo();
public:char _name[20];char _gender[3];int _age;
};//指明函數屬于哪個類域
void Person::printpersoninfo()
{cout << _name << "-" << _gender << "-" << _age << endl;
}int main()
{Person s;//創建對象,且實例化多個對象Person t;Person u;strcpy(s._name , "李四");strcpy(t._name , "王五");strcpy(u._name , "李六");strcpy(s._gender, "女");strcpy(t._gender, "男");strcpy(u._gender, "男");s._age = 18;t._age = 19;u._age = 20;s.printpersoninfo();t.printpersoninfo();u.printpersoninfo();return 0;
}
運行結果:
?
六、類的對象大小的計算
6.1如何計算對象的大小
對于類中既有成員函數和成員變量,那么計算大小時是不是他們都要算呢?look
#include <iostream>
using namespace std;
class A
{
public:void PrintA(){cout << _a << endl;}
private:char _a;
};int main()
{A a1;cout << sizeof(a1);//猜猜多大return 0;
}
??運行結果:
通過結果發現,大小只有1,只計算了成員變量_a 的大小。這是怎么回事呢?且看下面分析
6.2類對象的存儲方式?
以正常的理解,當一個類實例化多個對象時,對象與對象之間的成員變量、成員函數是互不干擾的,但每個對象都會保存一份該成員函數的代碼,相同的代碼保存了多份,實例化了多份,就會形成空間的浪費。
所以給了對象的新的存儲方式,對象中只保存成員變量,而對于成員函數,只保存一份放在公共代碼區,每個對象調用該函數時,其實就是調用公共代碼區的該函數。所以在上述計算大小時,就不會算上成員函數。
再來看幾個例子:
空類:
#include <iostream>
using namespace std;class A3
{};int main()
{cout << sizeof(A3);return 0;
}
?運行結果:
為什么也是1呢,不應該是0嗎?明明是一個空類。
?OK,空類其實也可以實例化對象,這是前提。既然可以實例化對象,那么對象就會在內存分配空間,該對象有地址存在,只不過不存儲任何數據。如果,空類的大小為0表示該類沒有任何成員或對象。這意味著該類不占用內存空間。所以,為了標識該對象存在,編譯器規定該空類的大小為1
類中僅有成員函數:
#include <iostream>
using namespace std;class A2 {
public:void f2() {}
};int main()
{cout << sizeof(A2);return 0;
}
運行結果:
?
大小也是1,根據上面的解釋成員函數是放在公共代碼區,它們在內存中獨立存在,所以在計算大小時,?相當于計算空類的大小。
注意:計算類的大小依然遵守結構體內存對齊規則 !!!
七、類成員函數的this指針
7.1this指針的引出
#include <iostream>
using namespace std;class Date
{
public:void Init(int year = 2023, int month = 11, int day = 23)//給缺省值{_year = year;_month = month;_day = day;}void print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1, d2;d1.Init(2023, 1, 11);d2.Init(2023, 1, 12);d1.print();d2.print();return 0;
}
運行結果:
對于上述類,實例化兩個對象,在表面上兩個對象都調用了Init函數時,但是在深層,函數怎么識別是誰調用?
c++引入了this指針來識別對象的調用:c++編譯器給每個“非靜態的成員函數”增加了一個隱藏的指針參數,讓該指針指向當前對象(函數運行時調用該函數的對象),在函數體中所有“成員變量”的操作,都是通過該指針去訪問。但要注意的是,this指針的所有操作對于程序員而言是透明的,即this指針不需要程序員來傳遞,編譯器會自動完成(可以認為是祖師爺設定好的)。
?7.2this指針的特性
1.this指針的類型:類類型* const,例如Date* const this。由于this被const修飾,所以在成員函數中,不能給this指針賦值。
2.只能在“成員函數”的內容使用
3.this指針本質上是“成員函數”的形參,當對象調用成員函數時,將對象地址作為實參傳遞給this形參。所以對象本身不存儲this指針。
4.this指針是“成員函數”第一個隱含的指針形參,一般情況由編譯器通過ecx寄存器自動傳遞,不需要用戶傳遞。
其實上述代碼的本質就是第二張圖的形式,對應著上述規則。注意的是在調用函數傳實參、以及函數的形參中不能顯示的將this指針寫出來,否則會報錯,但在成員函數中,可以顯示的寫出。
【面試題】?
1.this指針存在哪里?
將上述代碼,轉到反匯編后,可以發現,d1對象和d2對象存放到rcx寄存器。由此this指針在VS下存放在棧區的寄存器中。
2.this指針可以為空嗎 ?
先看代碼1:下面運行結果? A、編譯報錯 B、運行崩潰 C、正常運行
#include <iostream>
using namespace std;class A
{
public:void Print(){cout << "Print()" << endl;}
private:int _a;
};int main()
{A* p = nullptr;p->Print();return 0;
}
運行結果:
答案是正常運行,疑問的是p是空指針,為什么可以訪問Print函數?
其實這是一個誤區,想想前面講解的,首先成員函數不是放在對象中,而是在公共代碼區,其次在這里p并沒有去訪問Print函數,但是,如果沒有訪問該函數,那又是怎么尋得該函數。在C語言中,就知道了,是在編譯、鏈接階段通過符號表來尋找函數名字,所以在這里p的作用就是告訴編譯器它屬于A類,然后在編譯階段,A類中尋找是否存在Print函數。
代碼2:下面運行結果? A、編譯報錯 B、運行崩潰 C、正常運行
#include <iostream>
using namespace std;class A
{
public:void Print(){cout << _a << endl;}
private:int _a;
};int main()
{A* p = nullptr;p->Print();return 0;
}
運行結果:
運行結果是崩潰的,對比第一個代碼,修改了成員函數的輸出,且成員變量并沒有初始化。本質上就是this->_a,這個就是空指針訪問了成員變量,而空指針并不指向任何有效的對象,因此其中的成員變量也是未定義的。?
綜上所述,雖然成員函數的代碼本身放在公共代碼區,但成員函數通常涉及對對象中的成員變量進行操作,這可能導致問題。因此最好不要讓this指針為空,以避免潛在的未定義行為。
end~