您好,登錄后才能下訂單哦!
點擊鏈接加入群【C語言】:http://jq.qq.com/?_wv=1027&k=2H9sgjG
上一小節中,我們留下了幾個小問題,不知道大家有沒有做預習哦。
問題一:THIS指針 問題二:運算符重載
話說這個THIS指針是個什么東東呢,我們在寫一個類的時候,為這個類添加了成員變量,添加了成員方法,我們就可以用這個類生成對象來訪問這個對象的成員變量及成員函數啦。
但是問題是它是怎么知道這些成員變量的位置的呢?舉個例子:
0012FF64 CC CC CC CC 燙燙
0012FF68 01 10 38 00 ..8.
0012FF6C 0E 00 00 00 ....
0012FF70 1F 00 00 00 ....
上面是我們的一個str對象,它占 16個字節,前四個字節,其實是一個allocator分配器對象(還記得basic_string的數據成員嗎,保護成員),這個類的大小是1B,但是為了字節對齊,被擴充為4B,第二行數據是指向堆區的數據滴,第三個指的是當前堆區中的數據的長度,第四個指的是當前第二個數據指向的空間的大小,包括沒有數據的空間。如上面的結果是,現在堆區中有 31個字節的空間,已經使用了14個空間,空間首地址是 0X00381001。
好我們簡單的分析了一下string類型。那么我們就以這個類為基礎來研究一下我們今天的知識點吧。
#include <string>
using namespace std; //這個還沒給大家介紹呢,大家先這么用著,它叫做命名空間
int main()
{
string str("abcdefghijklmn");
int index = str.find('b');
return 0;
}
//我先對上面的命名空間做一個簡單的介紹,比如微軟做一個解決方案,這個解決方案可能會涉及30個項目,可能需要幾百個人去做開發,但是誰也不能保證每個人所用的函數名及變量名都是不同的,比如A君負責項目A并寫了一個函數 FUN,B君負責項目B也寫了一個函數FUN,那么這兩個項目最終合體的時候就會沖突啦,所以,我們可以讓每個人都為自己的項目中的內容加一個命名空間,比如A 的命名空間 叫 NMA,B的叫 NMB, 那么我們在調用A君的FUN時 就可以用 NMA::FUN,同樣NMB::FUN 也可以訪問到B的函數,但是如果在自己的項目中,我們就用自己定義的一些函數,那么我們就可以使用全局性的聲明 using namespace NMA; 這樣,我們所使用的函數都是來自NMA中定義的啦,先簡單介紹到這里,大家有個印象就可以了,這些操作最終會讓編譯系統為我們的函數加上某種意義上的范圍。
我們可以很輕松的找到變量 str的首地址,也知道它的大小就是16字節,那么我們來看看
5: string str("abcdefghijklmn");
0040112D lea eax,[ebp-24h]
00401130 push eax
00401131 push offset string "abcdefghijklmn" (0042601c)
00401136 lea ecx,[ebp-1Ch]
00401139 call @ILT+110(std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_str
0040113E mov dword ptr [ebp-4],0
看這里,上面壓入參數,我們不關心都是些什么數據,我們主要關心 lea ecx,[ebp-1Ch],大家多測試幾次,看看這個ecx的值是什么,在這里我就直接告訴大家啦,這個ecx的值記錄的是 str的首地址,怎么樣,奇怪嗎?其實不奇怪滴,在string的函數里面要訪問它的數據成員,肯定要知道數據在什么位置上,而這個位置就是通過ecx這個寄存器傳到函數里面去滴,我們把ecx這個寄存器叫做 this指針寄存器,把這種調用方式叫做 thiscall,這里是一個重點,大家要好好理解,大家也可以再看看它調用其它函數時是不是也通過ecx傳遞了str的首地址呢。
6: int index = str.find('b');
00401145 push 0
00401147 push 62h
00401149 lea ecx,[ebp-1Ch]
0040114C call @ILT+100(std::basic_string<char,std::char_traits<char>,std::allocator<char> >::find) (00
00401151 mov dword ptr [ebp-20h],eax
同樣的,調用find函數,也發現了這句話 00401149 lea ecx,[ebp-1Ch] ,這就是我們傳說中的This指針啦。在函數里面,我們就可以通過this關鍵字來訪問這個對象里面的數據啦。
class CTest
{
public:
int m_age;
public:
void SetAge(int age)
{
this->m_age = age;
}
};
int main()
{
CTest test;
test.SetAge(15);
return 0;
}
14: test.SetAge(15);
00401038 push 0Fh
0040103A lea ecx,[ebp-4] //傳遞THIS指針
0040103D call @ILT+0(CTest::SetAge) (00401005)
現在,我們跟進這個函數,看一下使用this指針的時候,內部的動作
00401070 push ebp //保存環境
00401071 mov ebp,esp
00401073 sub esp,44h
00401076 push ebx
00401077 push esi
00401078 push edi
00401079 push ecx //先將this指針壓棧,為什么呢?因為下面的循環操作需要使用ecx哦,ECX主要用來循環記數哦
0040107A lea edi,[ebp-44h]
0040107D mov ecx,11h //使用了ECX,所以前面要將它壓棧保存起來
00401082 mov eax,0CCCCCCCCh
00401087 rep stos dword ptr [edi]
00401089 pop ecx //還原ECX的值
0040108A mov dword ptr [ebp-4],ecx //將ECX 的值 寫在 這個函數棧楨的最下面,這也就是我們的THIS指針,以后都是通過它來訪問數據哦,看到指針的 //強大了吧
8: this->m_age = age;
0040108D mov eax,dword ptr [ebp-4]
00401090 mov ecx,dword ptr [ebp+8]
00401093 mov dword ptr [eax],ecx
這里是操作,這個地方比較簡單啦,
mov eax,this
mov ecx,參數1也就是age
mov [this],ecx //就是將age賦值給 this指向的位置
這就是傳說中的THIS指針,我們暫時先介紹這么多,哦對了,還有一點,大家想像一下*this會是什么東東呢?對象自己嘛,哈哈,是不是很容易理解,那么我們也就知道了,在函數內部要訪問自己的成員可以加上this-> ,也可以不加,但是如上面的例子,參數名如果也叫 m_age,那么這個時候,m_age = m_age;這兩個m_age都是指的參數,所以,我們要想賦值給對象的m_age,就需要使用this->m_age = m_age(參數);這個現象叫做什么- - ,我給忘了,反正大家應該能理解啦。
好啦,THIS指針已經基本上解決,我們再來看看運算符重載,在學一個東東之前,我們一定要搞清它的意義,這個運算符重載的意義在哪里呢?
我們已經學了類了,我們就可以用面向對象的思想來描述事物 ,比如我們現在描述一下數學中的復數(a+bi) , 應該有實部和虛部,我們就可以將實部和虛部抽取出來做為得復數類的成員,那么我們還知道兩個復數可以進行加減等運算,問題也就是在這里出現滴,怎么運算呢?對吧,跟 兩個整數相加減是不一樣的,它們應該有它們自己的運算法則,這個時候我們就可以使用運算符重載來改寫運算符對它們的行為,這就是它的意義啦。
我們來看一下complex這個復數類,當然啦,STL已經幫我們實現好啦。我們就來分析它吧:
在MSDN中輸入complex然后查詢,已找到的主題會顯示有兩個,那么我們點第一個,關鍵第二個是什么呢?第二個其實就是構造函數啦,哈哈。
我們會看到有許多函數,不過我們今天不研究函數,主要研究運算符重載,運算符重載的方式 返回值類型 operator運算符(參數1,參數2...){} ,應該很容易理解吧。
所以我們來看下,如果我們要實現兩個復數的加法,怎么寫呢? 返回值:應該是個復數,運算符+,參數應該是2個復數,那么我們來寫一下吧
complex<T> operator+(const complex<T>& lhs, const T& rhs); 很簡單吧,減法你也就會了,怎么實現呢?
{
return complex<int>(lhs.real+rhs.real,lhs.img+rhs.img); //感覺 是不是 很好理解,real 是實部,img是虛部
}
還有,乘法就不一樣啦,它有它的運算法則哦,不過前輩們都已經幫我們寫好啦。
比如我們要比較兩個復數是不是相等,怎么辦?肯定要比較實部和虛部是不是都相等,這個操作需要重載 == 運算符
bool operator==(const complex<T>& lhs, const T& rhs); 太簡單啦,實現部分更簡單啦,不介紹啦。
好,我們上面的內容都是可以隨便拿兩個對象來使用的,如
#include <complex>
using namespace std;
int main()
{
complex<int> c1;
complex<int> c2;
complex<int> c3= c1+c2;
return 0;
}
如果,我們想要讓 c1=c1+c2 ,也就是 c1+=c2; 這個時候 ,我們就要重載 +=運算符啦,而且,我們還要記得THIS指針哦,所以,
complex& operator+=(const T& rhs);
上面的代碼能看明白吧,就是將 rhs 加到 調用者自身上啦,要理解兩者的區別哦
complex<int> c3= c1+c2;
c1+=c2; 這個是將c2加到c1上,然后再將結果給c1,我們看到 這里面使用的返回值和參數都是用的引用哦,其實它們內部是怎么實現的呢?
很顯然,rhs搞成const引用是沒有什么問題滴,因為我們只是利用它的數據,并不修改它的數據,所以沒有必要做數據拷貝,只需要用一個引用(指針)能訪問到原數據即可啦。
那么返回的是什么東東呢 ,這個問題很好呢?
返回對象自己,很難理解是吧,就是*this啦,我們可以通過返回*this來返回對象自己,來實現級聯調用,還記得我們之前講過級聯調用嘛。
#include <iostream>
using namespace std;
class CTest
{
public:
CTest& funA()
{
cout<<"funA is called"<<endl;
return *this;
}
CTest& funB()
{
cout<<"funB is called"<<endl;
return *this;
}
CTest& funC()
{
cout<<"funC is called"<<endl;
return *this;
}
};
int main()
{
CTest test;
test.funA().funB().funC().funB().funA().funB().funC().funA(); //級聯調用
return 0;
}
上面的示例演示了級聯調用,那么大家接觸過級聯調用嗎,其實cout<< << << 這個就是級聯調用。
我們來看看cout是個什么東西,
extern _CRTIMP ostream cout; 表示它是一個 ostream的對象,那么我們再去看看ostream
typedef basic_ostream<char, char_traits<char> > ostream; 表示它是一個 模板類(給了類模板特定參數) ,我們再看看basic_ostream吧
在basic_ostream中,我們可以看到他們都返回 *THIS ,在這里不需要研究其它代碼,學習的時候一定要注意不要死扣一個點哦,那叫鉆牛角尖。
_Myt& operator<<(_Myt& (__cdecl *_F)(_Myt&))
{return ((*_F)(*this)); } //返回*this
_Myt& operator<<(_Myios& (__cdecl *_F)(_Myios&))
{(*_F)(*(_Myios *)this);
return (*this); } //返回*this
_Myt& operator<<(ios_base& (__cdecl *_F)(ios_base&))
{(*_F)(*(ios_base *)this);
return (*this); } //返回*this
_Myt& operator<<(_Bool _X)
{iostate _St = goodbit;
const sentry _Ok(*this);
if (_Ok)
{const _Nput& _Fac = _USE(getloc(), _Nput);
_TRY_IO_BEGIN
if (_Fac.put(_Iter(rdbuf()), *this,
fill(), _X).failed())
_St |= badbit;
_CATCH_IO_END }
setstate(_St);
return (*this); } //返回*this
其實還有許多,我們會看到它們都是返回的對象自身,返回值類型都是 類名& : typedef basic_ostream<_E, _Tr> _Myt; 這樣您也就大概明白了級聯調用啦。所以,要記住一點 cout是標準輸出對象,就像我們在C中講過的 stdout對應的結構一樣,同樣的還是 cin,cerr。只不過是cout功能更強大啦,但是我們一般不怎么使用它,我們更多的還是去使用C語言中的函數,所以,這些東西做為了解,知道這么回事就行啦。你能體會到C語言的短小精悍了吧。
運算符重載還有許多,希望大家看一下 C++ PRIMER 第四版,其實大家有了我分析的這些基礎后,再去看書,那就是很容易的事情啦。
總之大家要了解,運算符重載出現的意義,而且我們以后還會看到對指針操作的重載,主要用在迭代器中,這是相當重要的章節,我們在以后會慢慢深入滴。總之大家要記住,我們并不是用C++做開發,而是用其它的一庫進行特定場合的開發,但是這些內容都是基礎,你需要的只是理解好它們的思想,意義,當你忘記某個運算符怎么重載的時候,百度或GOOGLE會告訴你答案,但是這些知識點你要有,如果你都不知道運算符重載,你打開百度也不知道搜索什么呀,所以,擴充自己的知識面是非常重要的,怎么擴充,看書唄,大家切記的一件事,不要看什么視頻,個人也看過一些視頻,各種講師的講的內容著實有點不行,講不到重點,講不到原理,講不到應用,更多的是在講一些語法,跟學生講 i++和++i的區別,甚至i++i++i++的結果,我們C只說了15小節,C++應該也不會太多,但是大多數實用技術都會給大家講到。因為我們面向WINDOWS開發的時候更多的是要對基礎的理解,再有就是對操作系統的理解,而不是語法,你不懂友元,沒有關系,我來告訴你,友元就是一種機制,你在類中的私有數據不希望被外部訪問,但是你又想在某些非成員函數中訪問,問題就出現了, 將那個函數聲明為友元就可以了,我們前面已經看到了很多哦。
template<class T> //函數模板的聲明
complex<T> operator+(const complex<T>& lhs, const complex<T>& rhs); //用于將兩個復數相加,需要訪問私有數據
friend complex<T> operator+(const complex<T>& lhs, const T& rhs); //在復數類中將上面的函數聲明為友元,那么這個函數就可以訪問私有數據啦
友元就是這個思想,用到類上面也是這么個思想,那就是友元類啦。很EASY吧。至于友元的其它細節,我們見到的多了,慢慢研究研究也就會了。
其實學習重要的是什么,是思想,而不是友元怎么用,當有需求的時候,它的作用也就體現出來了。
好吧,這一小節的內容也是很重要的,我們暫時就介紹到這里,下一小節,我們就開始介紹向量,vector,也稱為動態數組,它的思想跟string類似,但是數據不是char,而是支持很多種類型,類也是可以的。
下小節見!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。