温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

C++入门篇04

发布时间:2020-08-02 22:04:29 来源:网络 阅读:324 作者:loveqqqg 栏目:移动开发

点击链接加入群【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,而是支持很多种类型,类也是可以的。

下小节见!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI