C++速成(3)

转载自:
http://www.cnblogs.com/kkdd-2013/p/5370094.html


7. C++模版3.5h

7.1 友元函数和友元类

C++中的模板体现在函数上叫做模板函数,体现在类上就叫做模板类。由于模板用途广泛,经过前辈们不断的归纳总结,最终发展成一套使用规范,功能强大,性能优良的标准模板类。

img

在前面的课程中,我们提到过C++中存在一种朋友关系,这种朋友关系如果体现在函数上,那么我们就称之为友元函数;如果体现在类上,我们就称之为友元类。

img

7.2 友元函数

对于我们定义函数的情形来说,一种情况是将函数定义为全局函数,另一种情况是将函数定义在一个类当中,使其成为类的一个成员函数。如果将全局函数声明为友元,则成为友元全局函数;如果将一个类的成员函数声明为另外一个类的友元函数,那么称该成员函数为友元成员函数。

友元全局函数

我们先来看一个例子

img

我们定义了一个坐标类(Coordinate),那么,如果我们想要定义一个友元,怎么办呢?我们就要使用关键字:friend。要定义一个友元函数,就只需要将关键字friend加在函数声明的前面,最后加上分号即可,同时一定要传入当前这个类的一个对象或者是一个引用或者是指针,总之,能够通过这个函数能够访问到这个对象的私有的成员或者是受保护的成员(如上面的:friend void printXY(Coordinate &c);)。下面我们来看一看,对于Coordinate这个类来说,他的私有的成员有哪些??一个是m_iX代表的是横坐标,一个是m_iY代表的是纵坐标。下面我们来看一看我们提到的使用方法。

img

在第一个方框中,我们写出的第一段程序叫做printXY函数,其是打印横纵坐标。在打印横纵坐标的时候吗,我们需要传入一个Coordinate的对象或者是引用(这儿传入的是引用),需要给大家指出的是传入引用或者指针,其传递效率更高,执行速度更快,所以在这提倡传入引用或者指针,而不提倡直接传入对象的方式。printXY函数在访问的时候,我们只是使用cout来打印一下横坐标和纵坐标。请大家注意我们使用的访问方法式使用这个对象去直接访问它的私有成员。如果我们没有将printXY声明为Coordinate的友元,那么如果我们这样写的话,编译器一定会报错。但是当前情况下,我们已经将printXY这个函数声明为Coordinate这个类的友元了,所以我们通过这样的直接访问形式是可以顺利编译通过的。当我们在mian函数当中去调用printXY函数的时候,我们需要先实例化一个Coordinate的对象,然后将这个对象传递进去,请大家注意,因为我们需要的参数是一个引用,所以我们传递的时候直接传入对象名就可以了,而不需要在对象名前面再加一个取地址符号(&)了。关于全局友元函数的定义和使用方法就先说这么多,后续通过代码实践进一步加深映像。

友元成员函数

我们还是通过一个例子来说明问题

img

在这我们还是以Coordinate这个类为例。定义的时候仍然使用关键字friend。请大家注意后面的写法,我们使用的函数仍然叫做printXY,但是此时的printXY函数并不是一个全局函数,而是一个成员函数,其是Circle这个类中的成员函数。所以,我们要将Circle中的成员函数printXY声明为Coordinate这个类的友元,那么,我们就需要将Circle这个类写出来,然后加上(::)再接printXY,这样就可以将一个类的成员函数声明为另外一个类的友元了。

img

在main函数当中,我们先实例化了一个Coordinate类的对象coor,然后又实例化了一个Circle类的对象circle。在Circle类中的printXY的实现方法与前面所讲到的全局函数的实现方法一样。通过这样的调用,我们可以发现,如果我们将Circle的printXY声明为Coordinate的友元,那么我们在printXY实现的时候就可以直接访问c这个对象下面的m_iX和m_iY,而m_iX和m_iY都是Coordinate下的私有成员,所以通过这样的行为就能够体现出友元给我们带来的方便。当然,友元给我们带来方便的同时,也给我们带来了一定的风险。当我们将Circle中的printXY这个函数声明为Coordinate这个类的友元函数之后,也就破坏了Coordinate这个类的封装性,此时对于数据的直接访问虽然是方便了,但是如果我们不小心改变了这个数据的值也不易擦觉,所以,风险和方便往往是一对相互矛盾。我们除非有特殊的需要,否则一般情况下不建议大家过度使用友元。

7.3 友元函数代码实践

题目描述:

/* ****************************************** */

/* 友元函数

​ 1. 友元全局函数

​ 2. 友元成员函数

/* ****************************************** */

程序框架:

img

先来看第一部分:友元全局函数,所用到的类是Time类

头文件(*Time.h*

复制代码

#ifndef TIME_H
#define TIME_H

#include<iostream>
using namespace std;

class Time
{
public:
    Time(int hour, int min, int sec);
private:
    int m_iHour;
    int m_iMinute;
    int m_iSecond;
};

#endif

复制代码

在Time类中,我们声明了它的构造函数Time并传入三个参数:时,分,秒,还有三个私有数据成员,也分别是时,分,秒。接下来我们来看一看它的构造函数是如何实现的。

源程序(*Time.cpp*

复制代码

#include"Time.h"

Time::Time(int hour, int min, int sec)
{
    m_iHour = hour;
    m_iMinute = min;
    m_iSecond = sec;
}

复制代码

我们看到,它的构造函数的实现非常简单,就是将传入的三个参数分别赋值给它的三个数据成员。

那么,接下来我们在主调程序中定义一个全局函数printTime(),在printTime()函数当中有一个参数Time(这里写成引用形式),如下:

void printTime(Time &t)  //pringtTime函数定义
{
    cout <<t.m_iHour <<":"<<t.m_iMinute <<":"<<t.m_iSecond << endl;
}

当我们写完之后,我们发现如果不声明友元,就相当于通过一个对象去直接访问它的私有数据成员,所以是不能够成功的,不妨我们按一下F7看一下编译是否通过??

img

我们看到编译过程失败,失败提示Time类中的私有数据成员。如果我们想要访问,及必须要将printTime函数声明为Time类的友元函数,如何声明呢??只需要在Time.h文件中加入一行代码:friend void printTime(Time &c);如下所示:

复制代码

#ifndef TIME_H
#define TIME_H

#include<iostream>
using namespace std;

class Time
{
    friend void printTime(Time &c); //将全局函数printTime声明为Time类的友元函数
public:
    Time(int hour, int min, int sec);
private:
    int m_iHour;
    int m_iMinute;
    int m_iSecond;
};

#endif

复制代码

这样之后,我们再来按一下F7看一下编译是否能够通过??

img

我们看到,当我们将printTime函数声明为Time类的友元之后,编译就能够顺利通过了。

接下来我们去到main函数下去使用一下,品尝一下我们做出来的成果。

首先,我们需要定义一个Time类的对象t,并且我们需要给这个对象t传入三个参数:时、分、秒。然后,将t作为参数传入到printTime函数中,看一下t能否打印出我们传入的时、分、秒来???

main**函数**

复制代码

int main()
{
    Time t(6,34,25);
    printTime(t);
    system("pause");
    return 0;
}

复制代码

我们按一下F5,来看一下运行结果:

img

通过运行结果,我们可以看到,打印出了我们传入的时分秒(6:34:25)。可见,通过声明友元,就能够使得传入进来的对象去访问它的私有的数据成员和成员函数(这里只展示了访问它的私有数据成员的方法)。

下面再来实现另外一个例子:友元成员函数。友元成员函数要求至少有两个类才可以实现,所以这里又定义了另外一个类Match(比赛)。在Match这个类当中,我们声明了一个printTime函数,这里的printTime函数和之前定义的名字是相同的,只不过现在定义在了类的内部,变成了一个成员函数,所以为了不进行互相干扰,我们把之前的printTime函数注释掉。

头文件(*Match.h*

复制代码

#ifndef MATCH_H
#define MATCH_H

class Time; //由于printTime函数中要用到Time类,所以这里需要声明一下
class Match
{

public:
    void printTime(Time &t);
}

#endif

复制代码

我们再来改造一下Time.h文件,如下:

复制代码

#include<iostream>
#include"Match.h"
using namespace std;

class Time
{
    friend void Match::printTime(Time &c); //将Match的成员函数printTime声明为Time类的友元函数
public:
    Time(int hour, int min, int sec);
private:
    int m_iHour;
    int m_iMinute;
    int m_iSecond;
};

复制代码

当我们这样写完之后,在Match的printTime中就可以传入一个对象,并且通过这个对象来调用Time中的私有数据成员:时、分、秒了,如下:

源程序(*Match.cpp*

复制代码

#include"Match.h"
#include"Time.h"
#include<iostream>
using namespace std;

void Match::printTime(Time &t)
{
    cout <<t.m_iHour <<":"<<t.m_iMinute <<":"<<t.m_iSecond << endl;
}

复制代码

然后,我们调到demo.cpp中,首先我们需要定义一个Match的对象m,然后通过对象m来调用printTime函数,如下:

复制代码

#include<iostream>
#include"stdlib.h"
#include"Time.h"
#include"Match.h"

using namespace std;

int main()
{
    Time t(6,34,25);
    Match m;
    m.printTime(t);
    system("pause");
    return 0;
}

复制代码

然后,我们按一下F7,看一下能否编译通过,如下:

img

我们看到,编译顺利通过,然后我们再按F5,看一看运行结果:

img

我们可以看到,打印出的是6点34分25秒(6:34:25),结果也如我们所料。此外,作为printTime这样的友元声明来说,它与访问限定符public、private、protected并不形成交叉关系,也就是说,它们并不构成约束,所以友元函数声明既可以写在访问限定符外面,也可以写在访问限定符里面。通过尝试也验证了这一点。可见,作为友元函数的声明的位置没有约束,可是我们仍然建议大家将其写在类的最前面,也就是说访问限定符的外面,这是因为,作为一个类来说,它对外如何暴露是非常重要的,我们把重要的放在前面有助于编程过程中减小犯错的概率。

7.4 友元类

友元类的定义与友元函数的定义非常相似,也是使用关键字friend,后面跟一个类的类名即可。需要大家特别注意的是,如果我们要声明一个友元类的时候,需要在当前这个类的前面先声明这个类,如下所示:

img

上面我们声明了Circle类为Coordinate类的友元类,而且在Coordinate类前面也声明了一下Circle类。

当我们将Circle这个类声明为Coordinate类的友元类之后,我们就可以在Circle这个类当中去定义一个Coordinate的对象了,并且可以通过这个对象任意访问Coordinate这个类当中的私有的数据成员和成员函数(在上面的例子中,我们只定义了数据成员,而没有定义成员函数)。

我们来看一下实际定义Circle类的时候是如何做的?

img

我们看到,实际定义Circle的时候,我们就是在访问限定符private的下面定义了一个Coordinate的对象m_coor,在任何Circle当中的成员函数中,都可以通过这个对象来访问Coordinate中私有的数据成员或者成员函数。

关于友元的注意事项

  • 友元关系不可以传递(比如:B是A的朋友,C是B的朋友,但C未必就是A的朋友)
  • 友元关系的单向性(比如:A是B的朋友,B不一定就是A的朋友,所以在声明有缘的时候,一定要搞清楚到底A是B的友元,还是B是A的友元)
  • 友元声明的形式及数量不受限制(可以既有友元函数也有友元类,而且声明数量也不受限制)

注意:

友元只是封装的一种补充,其并不是一个很好的语法。也就是说,是不得已而为之所用的语法,如果在前期设计巧妙的话,实际上是可以避开友元的。这就是说,友元的使用破坏了封装性,使得类的封装性看上去更差,从而也体现了一种定向暴露的思想(我把谁当做朋友,也就相当于我把数据定向的暴露给谁了)。

7.5 友元类代码实践

题目描述:

/* ****************************************** */

/* 友元类

/* ****************************************** */

程序框架:

img

从程序框架上来看,跟之前所讲的友元函数是一样的,也定义了两个类:一个是Time类,一个是Match类。但是这里所讲的内容跟之前有些不同,我们一起来看一看。

首先在Time类(Time.h文件)中,我们在private下面又定义了一个成员函数printTime(),在这个成员函数下面是三个数据成员:时、分、秒。

头文件(*Time.h*

复制代码

#ifndef TIME_H
#define TIME_H

class Time
{    
public:
    Time(int hour, int min, int sec);
private:
    void printTime(); 
    int m_iHour;
    int m_iMinute;
    int m_iSecond;
};

#endif

复制代码

我们再来看一看printTime函数是如何定义的,跳到Time.cpp中

源程序(*Time.cpp*

复制代码

#include"Time.h"
#include<iostream>
using namespace std;

Time::Time(inthour, intmin, intsec)
{
    m_iHour = hour;
    m_iMinute = min;
    m_iSecond = sec;
}
void Time::printTime()
{
    cout << m_iHour <<"时"<< m_iMinute <<"分"<< m_iSecond <<"秒"<< endl;
}

复制代码

此外,我们在Match.h中又加了一个成员函数testTime(),并且在Match.cpp中做了实现,这个实现是通过数据成员m_tTimer来调用printTime以及时、分。秒数据成员。我们可以看到,无论是成员函数还是数据成员都是写在了Time类中的private下面的。我们再来看一下Match.h中的数据成员,其是一个Time的数据成员m_tTimer(计时)。如下所示:

头文件(*Match.h*

复制代码

#ifndef MATCH_H
#define MATCH_H

#include"Time.h"

class Match
{
public:
    Match(int hour, int min, int sec);
    void testTime();
private:
    Time m_tTimer;
};

#endif

复制代码

源程序(*Match.cpp*

复制代码

#include"Match.h"
#include<iostream>
using namespace std;

Match::Match(int hour, int min, int sec):m_tTimer(hour, min, sec)//通过初始化列表来实例化m_tTimer的三个参数
{
   // to do
}

void Match::testTime()
{
    m_tTimer.printTime();
    cout << m_tTimer.m_iHour <<":"<< m_tTimer.m_iMinute <<":"<< m_tTimer.m_iSecond << endl;
}

复制代码

主调程序(*demo.cpp*

复制代码

#include<iostream>
#include"stdlib.h"
#include"Time.h"
#include"Match.h"

using namespace std;

int main()
{
    Match m(6,30,56);
    m.testTime();

    system("pause");
    return 0;
}

复制代码

在这里我们实例化了一个Match对象m,并传入了三个参数:时分秒,接着通过对象m来调用testTime()函数。

程序到这我们并没有在Time.h中声明Match为其友元类,所以当我们按F7的时候,可以看到:

img

也就是说,此时我们所调用的printTime函数或者数据成员,都因为其是private的访问形式而无法被调用。

所以此时,我们需要到Time.h文件中,将Match类声明为Time类的友元类,如下:

复制代码

#ifndef TIME_H
#define TIME_H

class Match;//因为要声明友元,必须在此前要先声明一下
class Time
{
    friend Match; //声明Match类为Time类的友元类
public:
    Time(int hour, int min, int sec);
private:
    void printTime(); 
    int m_iHour;
    int m_iMinute;
    int m_iSecond;
};

#endif

复制代码

现在我们再来看一看运行结果:

img

从打印结果可以看到,一个是“6时30分56秒”,一个是“6:30:56”,之所以能打印出中而两行结果,,这与Match中的testTime函数的实现有关系。我们看到第一行“m_tTimer.printTime()”这一行打印出来的就是“6时30分56秒”,第二行通过cout的方式就是打印出的“6:30:56”。

7.6 static

前面的课程我们介绍了:普通的数据成员和普通的成员函数的定义方法,又介绍了const关键字,并且讲解了用const关键字来修饰数据成员和成员函数,我们把这样的数据成员和成员函数称之为常数据成员和常成员函数。

这节课,我们再为大家介绍一个关键字:static(静态的)。在C++中提到静态,就不得不提到两个基本概念:静态数据成员和静态成员函数。我们以一个例子来看一下静态数据成员和静态成员函数的定义方法。

img

这里,我们定义了一个Tank类(坦克),在这个Tank类中,我们定义了一个普通的数据成员(string m_strCode;),那么如何去定义一个静态数据成员呢?那就是,就在数据成员的前面加上关键字static即可(如:static int s_iCount;),此时我们就称这个数据成员m_iCount为静态数据成员。又如何去定义一个静态成员函数呢?我们就在成员函数的前面加上关键字static即可(如:static int getCount() { return s_iCount; })。

问题:那么什么情况下我们要用到静态数据成员和静态成员函数呢?我们来描述一个场景。

大家都玩过坦克大战的游戏吧,你会发现当自己方的坦克非常多的时候,每辆坦克作战就会很英勇;当自己方坦克比敌方少很多的时候,就会变得很胆怯。那么,这样的话,你就希望每辆坦克作为对象来说,都能够知道自己方还有多少辆坦克的存在,那么此时我们就需要一个静态变量来记录这个值,这个值在我们的这个例子中就是m_iCount。

作为静态变量来说,它并不依赖于对象,而是依赖于类。这如何来理解呢?如果我们不实例化对象,那么作为静态的数据成员s_iCount仍然在内存中是存在的,这个也是静态数据成员与普通数据成员最大的区别。如果是普通的数据成员,则必须要实例化之后,这个数据成员在内存中才能够存在。

那么对于静态数据成员来说,因为它并不依赖于对象的实例化,所以静态的数据成员并不在构造函数中去实例化,它的实例化过程往往是单独进行的。如(int Tank::m_iCount = 0;)请大家注意:为静态数据成员初始化的时候,不要再加static关键字了,而直接写成:类型+类名+数据成员的名称+初值。此外,我们对于这个类来说,大家也可以看一看所书写的算法:我们定义了一个s_iCount(表示坦克的数量),刚初始化的时候坦克数量为0,如果我们将“s_iCount++”写在构造函数中,将“s_iCount–”写在析构函数中,那么,每当我们实例化一个坦克对象的时候,坦克数量就会增加一个;每当销毁一辆坦克的时候,坦克数量就会减少一个;而作为每个对象来说,都可以通过直接访问s_iCount来获取到自己同伴的数量。那么,访问的时候,作为访问方法来说又有两种:一种是不通过对象,而是直接通过类的访问方法;两一种是,如果实例化了一个对象,也可以通过这个对象来访问的方法。如下所示:

img

下面我们再来从内存当中给大家强调一下普通数据成员和静态数据成员究竟有什么区别。

我们还是以Tank这个类为例,当我们通过Tank这个类实例化t1、t2、t3和t4这4个对象之后,作为普通数据成员code就分别随着t1、t2、t3和t4的产生而诞生了,并且诞生了4个code,每个code都有自己的编号。可是,在这4个对象诞生之前s_iCount就已经诞生了,而且只诞生一次。即:t1、t2、t3和t4这4个对象产生的过程当中,s_iCount**的值会变化,但是*s_iCount*这个静态数据成员的个数不会发生变化**。

img

在前面的例子当中,我们是使用的普通的数据成员调用静态的数据成员或成员函数,那么,反之,用静态成员函数去调用普通的数据成员或者是普通的成员函数是不是一样成立呢??答案是:不可行的!!如果这样来写,编译器就会报错!!从逻辑上来讲,静态的数据成员和静态的成员函数都是随类的产生而产生,也就是说,其是依赖于类的;而普通的数据成员是依赖于对象的。如果,一个对象如果产生的话,那么我们在静态的成员函数当中去调用非静态的数据成员,显然是会失败的。因为人家一个对象还没有呢,这就是一个时机的问题。而从原理上来说,也是不成立的。

*this*指针来谈静态成员函数

我们修改一下Tank这个类,如下:

img

我们在这里定义了一个fire()的成员函数,还定义了一个静态的成员函数getCount(),另外,还定义了一个普通的数据成员m_strCode和一个静态的数据成员s_iCount。当我们通过fire()函数去调用普通的数据成员m_strCode和静态的数据成员s_iCount的时候,这里fire()函数虽然看上去没有传入参数,实际上它却传了一个隐形的this指针,通过这个隐形的this指针,我们就知道当前所要调用的是哪一个对象对应的数据成员或者是成员函数了。而在调用静态数据成员或静态成员函数的时候,因为它并不与对象相关,而只是与类相关,换句话说,它是一个全局的变量,那么在调用时前面不加this也无所谓,用不着区分,直接修改它的值或者去调用相应的静态成员函数。反之,如果我们使用的是静态的成员函数,那么作为静态的成员函数来说,它并不会传入一个隐形的this指针,那么这个时候你又怎么知道你所要调用的数据成员究竟是哪一个对象的数据成员呢。所以,在静态的成员函数当中,无法调用非静态的数据成员或者非静态的成员函数,可是,我们却可以在静态的成员函数当中去调用静态的数据成员,因为我们可以将静态的成员函数看作是一个全局函数,而静态的数据成员也是一个全局的数据,所以,如果通过静态的成员函数去调用一个静态的数据成员是没有问题的,而当调用一个非静态的数据成员时,就会因为this指针找不到,无法确定其是哪个对象的数据成员而造成编译时报错。

下面介绍一下静态数据成员和静态成员函数的注意事项。

  • 静态数据成员必须单独初始化(因为其并不随着对象的产生而产生,它是随着类的产生就已经产生了,所以说,类产生之后,对象还没有进行实例化时,它就应该已经具有一个初值了,所以它不能够写到构造函数当中去,而只能写到类的外边,直接进行初始化)。
  • 静态成员函数不能调用非静态数据成员和非静态成员函数(反之,非静态的成员函数则可以调用静态的数据成员和静态的成员函数)。
  • 静态数据成员只有一份,并且不依赖于对象而存在(这就是说,如果我们通过sizeof去求一个对象的大小时,那么,请大家记住,它一定不包含静态数据成员的)。

7.7 静态代码实践

题目描述:

/* ************************************* */

/* 静态数据成员与静态成员函数

要求:定义Tank类

​ 数据成员:坦克编号(m_cCode),坦克数量:s_iCount(这是一个静态的数据成员)

​ 成员函数:构造函数,析构函数,fire,getCount(这是一个静态成员函数)

/* ************************************* */

程序框架:

img

头文件(*Tank.h*

复制代码

#ifndef TANK_H
#define TANK_H
class Tank
{
public:
    Tank(char code);
    ~Tank();
    void fire();
    static int getCount();//定义getCount函数为静态成员函数
private:
    static int s_iCount;//定义s_iCount为静态数据成员
    char m_cCode;
};

#endif

复制代码

源程序(*tank.cpp*

复制代码

#include<iostream>
#include"Tank.h"

using namespace std;

intTank::s_iCount = 0; //首先对s_iCount进行了初始化,注意,这是在构造函数的前面进行了初始化

Tank::Tank(charcode)
{
    m_cCode = code;
    s_iCount++; 
    cout <<"Tank()"<< endl;
}

Tank::~Tank()
{
    s_iCount--;
    cout <<"~Tank()"<< endl;
}
void Tank::fire()
{
    cout <<"Tank --> fire()"<< endl;
}
intTank::getCount() //getCount函数虽然是静态成员函数,但是在这里定义的时候不需要再加上static关键字了
{
    return s_iCount;
}

复制代码

当我们做完这些之后,就可以在demo.cpp文件中去使用它们了。

如何来使用呢??我们先来尝试一下直接访问静态成员函数如下:

复制代码

#include<iostream>
#include"stdlib.h"
#include"Tank.h"

using namespace std;

int main()
{
    cout <<Tank::getCount() << endl;

    system("pause");
    return 0;
}

复制代码

这里直接访问getCount的结果就是将坦克数量s_iCount的值直接返回回来,而s_iCount在初始化的时候已经初始化为0了,所以在这返回的应该就是0。运行结果如下:

img

我们看到,打印出来的结果的确是0。也许你会说,这个0会不会是某一个初值或者说是一种巧合呢?那么,我们将s_iCount的初始值设为10,再来看一看结果如何?

img

我们看到,打印出来的结果也是10。可见,这个初始化的动作是非常靠前的,它会在类的编译之初就已经将静态的数据成员全部初始化好了。当我们直接去访问的时候,我们所访问到的结果就是我们初始化的数据。

下面我们来实例化一个坦克的对象t1,并且传入一个坦克编号(’A’),这个还不是重点,重点是当我们实例化了一个对象之后,我们再去调用静态成员函数getCount的时候,我们来看一看这个结果:

复制代码

int main()
{
    Tank t1('A');
    cout <<Tank::getCount() << endl;

    system("pause");
    return 0;
}

复制代码

结果如下:

img

我们看到,打印出了一行“Tank()”,说明构造函数得以执行,并且执行了那一行(s_iCount++;)的操作,因为刚刚我们将s_iCount的初始值改成了10,所以当s_iCount++之后,它的值就变成了11。

不仅如此,我们还可以通过对象t1来直接访问getCount函数,访问的时候用(.)来访问,如下:

复制代码

int main()
{
    Tank t1('A');
    cout << t1.getCount() << endl;

    system("pause");
    return 0;
}

复制代码

我们按F5来看一看运行结果:

img

我们看到,打印出来的结果也是11,跟之前是一样的。可见,无论是通过类名+冒号(tank::)的方式,还是通过对象+点号(t1.)的方式,两者都可以访问静态的数据成员以及静态的成员函数(这里仅以静态的成员函数为例)。

接下来,我们来看一看,当执行完析构函数之后,它的数量有没有减少。为了能够体现出来,我们需要在堆上去实例化一个坦克的对象,如下:

复制代码

int main()
{
    Tank *p = new Tank('A');
    cout <<Tank::getCount() << endl;
    Tank *q = new Tank('B');
    cout << q->getCount() << endl;
    delete p;
    delete q;
    cout <<Tank::getCount() << endl;
    system("pause");
    return 0;
}

复制代码

我们按F5看一看运行结果如下:

img

我们看到,一开始大衣橱“Tank()”,说明Tank的构造函数执行了一次,此时我们去调用getCount函数的时候,我们发现打印出的是11,然后我们又实例化了一个对象,然后打印出的是12,接下来由于分别销毁了对象p和对象q,所以执行了两次析构函数,打印出了两行“~Tank()”,再去调用getCount函数的时候,此时坦克的数量就又回到了初始状态10。

好了,关于静态数据成员以及静态成员函数的定义及其使用方法就讲到这里,还有一些内容是需要给大家提醒的。

首先,我们来看一看,对于静态的成员函数来说,能不能加*const*这个关键字呢??只要大家稍微想一想前面讲到的this指针的相关知识,就可以知道,这是不可以的。这是为什么呢?因为,这是因为,加上*const*的本质就是给其隐形的*this*指针加了*const*,而作为静态的成员函数来说,其根本就没有*this*指针,所以*const*嫁给谁呢,肯定是不妥的。为了能够让大家看清楚这一点,我们就不见棺材不掉泪。

修改头文件(Tank.h)中的getCount函数如下:

staticint getCount() const;

修改源程序(Tank.cpp)中getCount函数如下:

int Tank::getCount() const
{
    return s_iCount;
}

然后,在main函数中,我们不需要写什么内容,我们只需要按F7看一看编译能不能通过即可。

img

我们看到,编译失败。失败的原因就是“getCount”静态成员函数不允许修饰符,也就是说,不允许const关键字来修饰。可见,在静态的成员函数的后面加关键字const是不允许的。我们将程序再改回来,然后再来尝试一下,在普通成员函数fire函数中去调用getCount函数,即在普通的成员函数中去调用静态成员函数可不可以呢?我们说,这是可行的。

修改源程序(Tank.cpp)中的fire函数如下:

void Tank::fire()
{
    getCount();
    cout <<"Tank --> fire()"<< endl;
}

我们按F7看一看编译是否通过

img

我们看到,编译时成功的。

如果我们把这种;逻辑反过来看可不可以。也就是说,在静态成员函数中去调用普通的成员函数可不可以呢??

修改源程序(Tank.cpp)中的getCount函数如下:

int Tank::getCount()
{
    fire();
    return s_iCount;
}

我们按F7看一看编译是否通过?

img

我们看到编译失败。失败原因是:Tank中的fire是非静态的成员函数进行了非法调用。这就是我们上节课所讲到的,静态的成员函数只能调用静态的数据成员,而不能调用非静态的数据成员或者是非静态的成员函数

7.8 运算符重载

什么是运算符重载呢?所谓运算符重载,就是给原有运算符赋予新的功能。比如说,加好(+)是用来做两个数字相加操作的(如:1+1=2),但是,我们往往用加号来让两个字符串作拼接(如:慕+课=慕课),那么,这个时候就是给加号做了运算符的重载。我们来看一个常见的例子:

img

在这个例子当中,我们就使用了加号去连接多个字符串,使其拼接成一个字符串,而且,我们在打印的时候,也可以将拼接好的字符串直接打印出来,那么这个字符串就进行了多个运算符的重载(加号运算符做了重载,输出运算符也做了重载,等号运算符也做了重载)。那么,问题是:这些重载究竟是怎么做的呢?后续课程为大家一一讲解。

我们再来看另外一个例子:

img

在这个例子中,有两个坐标,这两个坐标其实也是可以相加的,相加之后你二舅会形成一个新的坐标。可是,这两个坐标相加对于加号本身来说,并不具备这样的功能。为了能让其具备这样的功能,我们就可以通过运算符重载来实现。同时,如果我们想直接输出一个坐标,我们也可以通过重载输出运算符来实现。

下面我们来看一看运算符重载的本质!

其实,运算符重载的本质就是:函数重载。它并没有什么神秘的地方。

下面介绍一下,定义运算符重载的关键字:operator。

那么,运算符重载其实有很多种,包括:一元运算符重载、二元运算符的重载等。

一元运算符重载

对于一元运算符的重载,在这里,我们举两个例子:

-*(负号)的重载*

++*符号的重载*

所谓负号,就是将减号(-)写在某个对象的前面,把它当作一元运算符(所谓一元运算符,就是这个符号只与一个操作数进行运算,比如说:加号其实就是一个二元运算符,因为加号需要由两个操作数来进行运算,而正号,即就是将加号放在一个操作数的前面,那么它就是一个一元运算符),使这个对象进行取反运算。

++符号也是一个一元运算符,可是++符号的运算可以放在操作数的前面,也可以放在操作数的后面。

负号的重载方式:一种是友元函数重载,一种是成员函数重载。友元函数重载,换句话说,就是在类当中去定义一个友元函数,这个友元函数是一个全局函数,用这个友元函数来做运算符的重载,从而实现一个符号多了一项功能。成员函数的重载,其实就是在定义一个类的成员函数。

负号(-)成员函数重载

img

我们来看这个例子,在这个例子当中,我们定义了一个Coordinate坐标类。如果我们想要对负号(-)做成员函数的重载,我们就应该写成Coordinate& operator-(),因为是一元运算符,而且它是作为类的成员函数存在,所以这里不需要传入任何的参数。实现的时候怎么来做呢?如下

img

请大家注意,其实我们不传入参数作为一个普通的成员函数来说,它还是有一个隐性的this指针的,而这个隐性的this指针其实就是它的操作数,那么我们就可以将每一个数据成员取反后,再重新复制给它,使得它的所有的数据成都由正变成了负,或者是都由负变成了正,然后将(*this)作为返回值返回出去即可。我们再来看一看如何使用,如下:

img

需要在对象的前面写一个负号(-),就会使得当前的这个Coordinate对象所有的值都会取反,就相当于使用Coordinate这个对象coor1去调用operator-()这个函数,也就是说这两者是等价的。当计算机遇到这样的时候,就会将其解释成一个函数的调用。

负号(-)友元函数重载

下面我们再来看一下友元函数重载是怎样实现负号运算符重载的。

img

我们来看这个例子,还是Coordinate这个坐标类,但是这个时候,我们使用友元申明:通过friend关键字来申明一个全局函数operator-,那么这个时候我们需要传入一个参数,这个参数就是Coordinate的一个引用,其返回值也是一个Coordiante的引用。实现的时候我们需要通过这个引用变量分别对它的每一个数据成员取反,然后再赋值给其本身,最后将(*this)返回回去,如下:

img

调用的时候,当我们给对象coor1取反的时候,计算机就会将其解释成operator-(),并且传入对象coor1,即:operator-(coor1),如下:

img

请大家比对成员函数重载和友元函数重载的区别。对于成员函数重载,调用的时候是coor1.operator-(),而对于友元函数重载,这里写的是operator-(coor1),这是有本质区别的。

接下来我们说一说++符号的重载,其分为前置++符号重载和后置++符号重载。所谓前置++符号重载,就是将++写在操作数的前面,这一点在C语言当中都已经学习过

++运算符前置重载

我们通过例子来说明这个问题。如果想要做++运算符的前置重载,并且把它当作成员函数来操作的话,我们可以这样来写:

img

这里注意:因为++是一个一元运算符,所以operator++()中也不用传入任何参数。

那么,定义的时候,我们作为一个成员函数来进行定义,使它的每一个数据成员都做++操作,最后将(*this)返回出去,如下:

img

这里可以想见一下,外面接收到的值其实就是++之后的值了。如果我们++coor1,就相当于coor1.operator++()的操作,即:

img

++运算符后置重载

那该如何实现后置++呢?计算机要对前置++和后置++进行区分。所以后置++是需要特别注意的。定义的时候,返回值不再是引用,而是Coordinate的一个对象,并且operator++()中必须要传入一个int,即一定要是operator++(int),这里的int是一个标识,它标识当前的++符号做的是后置重载,如下:

img

在使用的时候,这里的int的地方并不传入任何值,即便是将来使用传入任何值也没有意义,,因为我们根本就不去使用它,只是一个标识而已。

那么,如何去定义呢??我们要保证有一个旧的值,所以我们要先来定义一个临时对象Coordinate old,然后将当前这个值先保存在old对象当中,最后通过return将这个old返回出去,如下:

img

如果接收到后置++的值,那么,后置++的值呢,也是没有++之前的值,但是它的下一行代码如果再去使用当前这个对象的时候,其里面的值就已经发生变化了。因为,我们再给old赋完值后呢,当前的这个对象就已经将其当中的数据成员都做了++操作。使用的时候,请大家注意,这个时候是后置++,如下:

img

当后置++的时候,系统会默认为我们传入一个值,这个值呢,一般来时就是0,它没有什么意义,但是它能够表达出,这是一个在调用后置++的运算。

一元运算符重载代码实践

题目描述:

/* *********************************** **/

/* 运算符重载……一元运算符重载

要求:定义一个Coordinate坐标类

​ 成员函数:构造函数,getX,getY

​ 数据成员:m_iX,m_iY

​ 1、负号运算符重载(成员函数、友元函数)

​ 2、++运算符重载(前置、后置)

*/

/* *********************************** **/

程序框架:

img

头文件(*Coordinate.h*

复制代码

#ifndef COORDINATE_H
#define COORDINATE_H

#include<iostream>
using namespace std;

class Coordinate
{
public:
    Coordinate(int x, int y);//构造函数
    int getX();
    int getY();
private:
    int m_iX;
    int m_iY;
};

#endif

复制代码

源程序(*Coordinate.cpp*

复制代码

#include"Coordinate.h"

Coordinate::Coordinate(int x, int y) //将传入的参数x和y分别赋值给数据成员m_iX和m_iY
{
    m_iX = x;
    m_iY = y;
}
int Coordinate::getX()
{
    return  m_iX;
}
int Coordinate::getY()
{
    return  m_iY;
}

复制代码

下面根据题目描述,首先给大家要演示的是负号的成员函数的重载。

我们再回到Coordinate.h文件,在Coordinate.h当中,我们需要先将它的声明写出来。那么,对于负号运算符来说,它返回出来的应该是它本身,这样的话呢,它还可以再进行负号运算符的运算。怎样让它返回出来是对象本身呢??我们把它写成一个引用,即:Coordinate &operator-(),括号里不需要加任何的参数,如下:

复制代码

#ifndef COORDINATE_H
#define COORDINATE_H

#include<iostream>
using namespace std;

class Coordinate
{
public:
    Coordinate(int x, int y);//构造函数
    Coordinate& operator-();
    int getX();
    int getY();
private:
    int m_iX;
    int m_iY;
};

#endif

复制代码

我们再转到Coordinate.cpp中去实现它。那么,作为负号运算符重载呢,其实就是将它里面的m_iX和m_iY这两个坐标分别取负值,怎么来写呢?如下:

复制代码

Coordinate& Coordinate::operator -()
{
    m_iX = -m_iX;  //this ->m_iX = -this->m_iX ;
    this ->m_iY = -this->m_iY; // m_iY = -m_iY;
    return  *this;
}

复制代码

接下来我们调到demo.cpp文件下面,先实例化一个对象coor1,并且传入两个初值1和3,也就是说,它的横坐标是1,它的纵坐标是3。接下来我们通过cout来打印一下它的横纵坐标。通过调用getX来取得它的横坐标,通过调用getY来取得它的纵坐标。接下来就是我们的重点了,首先对coor1取负号(其实这里就相当于调用了coor1.operator-()),这样的效果就相当于将(*this)取反,同时将当前的这个对象的横纵坐标都做了取负号的操作。那么,接下来我们就可以通过cout去打印coor1的横纵坐标了,如下:

复制代码

#include<iostream>
#include"Coordinate.h"

using namespace std;

int main()
{
    Coordinate coor1(1,3);
    cout << coor1.getX() <<","<< coor1.getY() << endl;
    -coor1; //coor1.operator-();
    cout << coor1.getX() <<","<< coor1.getY() << endl;
    system("pause");
    return 0;
}

复制代码

然后我们按F5,看一看运行结果:

img

从运行结果我们可以看到,在没有取负号之前,coor1的坐标是(1, 3),取负号之后变成了(-1, -3)。

我们看到,当前我们去定义的时候采用的是引用这种方式,接下来,我们要给大家体验一下这种引用方式的好处。

什么好处呢?如果我们在这取了一次负号,那么我们再给它取一次负号会怎样呢?学过数学的我们肯定知道,负负得正,如下:

复制代码

int main()
{
    Coordinate coor1(1,3);
    cout << coor1.getX() <<","<< coor1.getY() << endl;
    -(-coor1);//coor1.operator-();
    cout << coor1.getX() <<","<< coor1.getY() << endl;
    system("pause");
    return 0;
}

复制代码

我们来看一看是不是负负得正的效果呢?如下:

img

我们看到,取两次负号的结果就是两次打印结果是一样的,也就是如我们所想的负负得正的效果。

接下来我们给大家演示一下友元函数的负号运算符的重载。我们需要将成员函数运算符重载先注释掉。趁现在注释掉成员函数运算符重载后,我们也要给大家演示一个现象。我们看到刚才对coor1取负号是行得通的,现在当运算符重载去掉之后,对coor1取负号后,再来看一看是否行得通?我们按F7后,显示如下:

img

我们可以看到,编译出错了,告诉我们一元”-”这个负号没有定义运算符或者是预定义运算符可接收的类型转换。这里说这么多,其实就一个意思:当前的对象coor1是无法通过负号直接进行操作的,如果想要可以操作,必须是它可以认识的,想要认识,就要加上运算符重载。由于运算符重载的代码我们已经注释掉了,所以计算机编译时就会报错了。

接下来,我们采用友元函数运算符进行重载。

我们调到Coordinate.h文件当中,修改如下:

复制代码

#ifndef COORDINATE_H
#define COORDINATE_H

#include<iostream>
using namespace std;

class Coordinate
{
    friend Coordinate& operator-(Coordinate &c);//这里需要插入一个参数,其实就是在成员函数当中传入的this,此时体现上就是一个Coordinate对象:Coordinate c即可;如果为了传递效率,这里也可以写成一个引用形式
public:
    Coordinate(int x, int y);//构造函数
    //Coordinate &operator-();
    int getX();
    int getY();
private:
    int m_iX;
    int m_iY;
};

#endif

复制代码

然后我们写出它的定义,调到Coordinate.cpp文件当中,修改如下:

复制代码

#include"Coordinate.h"

Coordinate::Coordinate(int x, int y) //将传入的参数x和y分别赋值给数据成员m_iX和m_iY
{
    m_iX = x;
    m_iY = y;
}
int Coordinate::getX()
{
    Return  m_iX;
}
int Coordinate::getY()
{
    return  m_iY;
}
//Coordinate &Coordinate::operator -()
//{
//    m_iX = -m_iX;  //this ->m_iX = -this->m_iX ;
//    this ->m_iY = -this->m_iY; // m_iY = -m_iY;
//    return *this;
//} 

Coordinate& operator-(Coordinate &c)
{
    c.m_iX = -c.m_iX;
    c.m_iY = -c.m_iY;
    return c;
}

复制代码

接下来再调到demo.cpp文件当中,先不作任何修改,看看编译是否通过

img

我们看到,编译是没有问题的

接下来再按F5,看一看运行的结果:

img

我们看到,与成员函数的重载运行结果是一样的。

接下来再来看一下++符号的前置运算符重载和后置运算符重载。

我们跳到Coordinate.h文件当中,前置++我们需要返回来的仍然是Coordinate这个当前对象指针:Coordinate& operator++();因为是一元运算符,所以在这个成员函数重载的时候,这里不需要传入参数。

然后,我们需要去实现它。如何来实现呢?

复制代码

Coordinate& Coordinate::operator++()
{
    m_iX++; //++m_iX;
    m_iY++; //++m_iY;
    return *this;
}

复制代码

接着转到demo.cpp中,如下:

复制代码

#include<iostream>
#include"Coordinate.h"

using namespace std;

int main()
{
    Coordinate coor1(1,3);
    cout << coor1.getX() <<","<< coor1.getY() << endl;
    ++coor1;
    cout << coor1.getX() <<","<< coor1.getY() << endl;

    //-coor1; //coor1.operator-();
    //cout << coor1.getX() << "," << coor1.getY() << endl;
    system("pause");
    return 0;
}

复制代码

我们来看一下运行结果:

img

我们看到,经过++之后,坐标由原来的(1,3)变成了(2,4),显然++是起到了作用。

接下来,我们再看一下后置++。

后置++与前置++有什么区别呢?首先,我们来看一下后置++重载的声明:

Coordinate operator++(int);这里有一个参数,其实这个参数只是一个标识而已,只起到告诉编译器这是一个后置++重载的作用;另外,这里的返回值不再跟之前一样返回一个引用了,而是返回的是一个Coordinate对象。这是为什么呢?我们来想一想前置++和后置++的区别:前置++后,此时这个表达式的值就是这个而对象++之后的值了;后置++后,此时这个表达式的值是这个对象++之前的值,当我们下一行代码再去访问这个对象的时候,就是++之后的值。所以,我们在实现的时候要进行区分开来。

复制代码

Coordinate Coordinate::operator++(int) //后置++的实现
{
    Coordinate old(*this); //这里使用了默认的拷贝构造函数
    this->m_iX++;
    this->m_iY++;
    return old; //注意:这里return出去的是old,不是this
}

复制代码

我们再回到demo.cpp中,

复制代码

#include<iostream>
#include"Coordinate.h"

using namespace std;

int main()
{
    Coordinate coor1(1,3);
    cout << coor1.getX() <<","<< coor1.getY() << endl;
    cout << (coor1++).getX() <<","  ;
    cout << (coor1++).getY() << endl;

    system("pause");
    return 0;
}

复制代码

我们按F5看一下运行结果:

img

我们看到打印出的是由原来的(1,3)变成了后来的(1,4),这是为什么呢?

(1,3)输出是没有问题的,因为这是原有的输入;(1,4)的输出是为什么呢?这是因为,当我们cout << (coor1++).getX() <<”,” ;之后,返回的是当前的数值,而之后再次调用这个对象时,就是++之后的值了,所以当后面一句cout << (coor1++).getY() << endl;后,就得到的是3加1后的值,即4。

二元运算符重载

加号(+)成员函数重载

我们来看下面的这个例子

img

在这个类当中,我们除了声明了Coordinate的构造函数之外,还声明了Coordinate operator+();从声明形式来看,它是一个成员函数。这里需要注意的是:在这个成员函数中需要传入一个参数(const Coordinate &coor),我们如何来定义operator+这个函数呢?

img

我们看到,定义的时候首先需要定义一个临时的Coordinate对象temp,然后传入的这个coor对象的横坐标要与当前对象的横坐标相加,然后将相加之后的值赋值给这个临时对象temp的横坐标(纵坐标处理与横坐标一样),最后将这个临时对象temp作为返回值返回出去。那么,在使用的时候,我们就可以这样来写:

img

首先,我们定义一个coor1坐标对象,它传入的是3和5,即coor1的横坐标是3,纵坐标是5,同理,coor2的横坐标是4,纵坐标是7,然后我们再定义一个结果,这个结果是coor3对象,用coor3来接收coor1与coor2相加的和。请大家注意:这个对方的加号(*+*)就已经用到了运算符重载,其就相当于***coo1.operator+(coor2)这里需要注意的是:其实作为函数重载来说,加号(*+*)作为二元运算符在它传入的另外一个参数的前面其实是有一个隐形参数****this*的,那这个*this*就相当于传入的第一个参数**coor1

加号(+)友元函数重载

友元函数重载相对于成员函数重载来说,其更能说明问题。如下:

img

友元函数重载,大家可以看到,我们需要通过friend关键字来声明一个友元函数,并且这个友元函数是在Coordinate这个类当中声明的友元函数,它的两个参数分别是:const Coordinate &c1和const Coordinate &c2。这里我们可以写上const,也可以不写const。如果我们写上const的话,大家可以想象一下,在当我们去定义函数的时候,我们就无法在函数当中去修改c1和c2的值了。大家想一想加法,在加法当中,我们让一个加数和另外一个加数去相加的时候,我们与而不希望在加的过程中去修改加数本身的值,所以加上const其实是一种设计上的规范。然后,我们再来看一看它的实现:

img

我们看到,在实现加号友元函数重载的时候,我们也需要先定义一个临时的Coordinate对象temp,然后我们传入两个参数c1和c2,将c1的横坐标与c2的横坐标相加后赋值给临时对象temp的横坐标,c1的纵坐标与c2的纵坐标相加后赋值给临时对象temp的纵坐标,最后将这个临时对象temp作为返回值返回出去。那么,我们在mian函数中去使用的时候,其实与加号的成员函数重载是一样的,即

img

输出符号(<<)友元函数重载

我们在定义输出符号运算符的时候,可以写成这样:

img

在定义输出符号(<<)的友元函数重载的时候,其友元函数的返回值必须是ostream&,其传入的第一个参数也必须是ostream的一个引用,第二个参数是要进行输出的对象。当我们去实现这个函数的时候,我们怎么来写呢?

img

我们看到,是用out替代了原来的cout的位置,替代之后,其他部分的写法不变,最后,也是必须的,要将out返回出去。接下来,我们看一看如何来使用:

img

如果我们定义一个Coordinate的对象coor,其横坐标是3,纵坐标是5,那么我们通过cout就可以直接输出coor了。如果我们不进行<<运算符的重载,这样写肯定是错误的。如果我们进行了<<元素安抚的重载,这样写就相当于operator<<(cout, coor)。

通过这个例子,其实我们从侧面也能够理解cout**是什么呢?它就是一个对象,是一个什么对象呢?就是一个*ostream*类型的对象**。

接下来我们来考虑一个问题:输出运算符可以采用成员函数重载吗?

针对这个问题,我们从成员函数重载的特点来给大家说一说。如果我们使用成员函数重载,比如说之前提到的加号(+)运算符的成员函数重载,我们一起来回顾一下:在加号运算符当中,我们使用成员函数重载,这个时候需要传入的只有一个参数,这个参数其实是第二个加数,而第一个加数默认就是当前这个对象。可是,对于输出运算符来说,我们可以看到,第一个参数必须是ostream,这就意味着不能是这个this指针,也就不能是当前这个对象,所以,当我们去重载输出运算符的时候,是绝对不可以通过成员函数去进行重载的,而必须采用友元函数进行重载

索引符号([ ])成员函数重载

索引运算符更多的是运用在数组当中,这里我们先把它运用到Coordinate这个类当中。我们来看一看如何来定义:

img

声明的时候,我们需要将索引运算符作为一个成员函数放到Coordinate这个类当中,定义方法同样是用关键字operator,然后再加索引运算符([ ])。因为是索引,所以我们需要传入一个int类型的索引index,并且返回一个整型值。接下来,我们来看一看其实现方法:

img

函数是这样定义的:传入的参数index,先判断一下它等不等于0,如果它等于0,那么我们就将横坐标的值返回出去;如果它等于1,那么我们就将纵坐标的值返回出去;如果它是其他值,我们这里暂时没有进行处理。实际上,我们应该抛出一个异常。

接下来,我们来看一看它的使用:

img

如果我们实例化了一个Coordinate的对象coor,纯如两个值:3和5。3是coor的横坐标,5是coor的纵坐标。如果我们通过cout来输出coor的0号元素,即:cout << coor[0],那么实际上就是输出横坐标值;如果我们输出coor的1号元素,即:cout << coor[1],那么实际上就是输出纵坐标值。大家可以看一看,当我们去调用coor,然后接索引的时候,其实就相当于调用coor.operator或者coor.operator

下面我们来思考一个问题:索引运算符可以采用友元函数重载吗?答案也是否定的。原因是:友元函数重载,它的第一个参数可以是成员函数中的那个this指针,也可以是其他的值,可是,作为索引运算符来说,它的第一个参数必须是this指针,因为,只有第一个参数是this指针,才能够传入索引,才能够使得这个索引所表达的是当前这个对象的当中的成员。比如说,在我们这个例子当中,第一个参数一定时this指针,它表达的就是Coordinate的一个对象,接下来传入的0或1,所表达的就是传入的this指针所指向的这个对象当中的0号元素或者是1号元素,所谓0号元素就是当前这个对象的横坐标值,1号元素就是当前这个对象的纵坐标值。所以,对于索引运算符必须采用成员函数进行重载,而无法采用友元函数进行重载。

二元运算符重载代码实践

题目描述:

/* ******************************* */

/* 运算符重载—-二元运算符重载

要求:定义Coordinate坐标类

​ 成员函数:构造函数、getX、getY

​ 数据成员:m_iX、m_iY

​ 1、加号(+)运算符重载(成员函数、友元函数)

​ 2、输出运算符(<<)重载

​ 3、索引运算符([])重载

*/

/* ******************************* */

头文件*(Coordinate.h)*

复制代码

#ifndef COORDINATE_H
#define COORDINATE_H

class Coordinate
{
public:
    Coordinate(int x, int y);
    Coordinate operator+(Coordinate &c); //申明加号(+)运算符成员函数重载
    int getX();
    int getY();
private:
    int m_iX;
    int m_iY;
};

#endif

复制代码

源程序*(Coordinate.cpp)*

复制代码

#include"Coordinate.h"

Coordinate::Coordinate(int x, int y)
{
    m_iX = x;
    m_iY = y;
}

int Coordinate::getX()
{
    return  m_iX;
}

int Coordinate::getY()
{
    return  m_iY;
}

Coordinate Coordinate::operator+(Coordinate &c) //加号(+)运算符成员函数重载实现方法
{
    Coordinate temp(0,0);
    temp.m_iX = this->m_iX + c.m_iX;
    temp.m_iY = this->m_iY + c.m_iY;
    return temp;
}

复制代码

主调程序*(demo.cpp)*

复制代码

#include<iostream>
#include"Coordinate.h"

using namespace std;

int main()
{
    Coordinate coor1(1,3);
    Coordinate coor2(2,4);
    Coordinate coor3(0,0);
    coor3 = coor1 + coor2;
    cout << coor3.getX() <<","<< coor3.getY() << endl;

    system("pause");
    return 0;
}

复制代码

我们按F5,看一看运行结果:

img

从运行结果可以看到,我们这里的加号(+)运算符的成员函数重载是ok的。那么,我们再来看一看加号(+)运算符友元函数的重载是如何实现的。

我们需要在以上代码中,将成员函数重载的代码注释掉。然后,在Coordinate.h文件中,通过friend关键字来声明一个友元函数:friend Coordinate operator+(Coordinate &c1, Coordinate &c2);接着在Coordinate.cpp文件中去实现这个友元函数,如下:

复制代码

Coordinate operator+(Coordinate &c1, Coordinate &c2) //加号(+)运算符友元函数重载实现方法
{
    Coordinate temp(0,0);
    temp.m_iX = c1.m_iX + c2.m_iX;
    temp.m_iY = c1.m_iY + c2.m_iY;
    return temp;
}

复制代码

demo.cpp文件不变,我们来按一下F5,看一看运行结果与之前的成员函数重载是否一样:

img

我们看到的结果也是(3,7),与之前是一样的。

接下来,我们来看一看输出运算符(<<)的重载。根据我们之前的客商所讲,输出运算符的重载必须是友元函数的重载。另外,还要注意,它的返回值是一个ostream类型,即声明为:friend ostream& operator<<(ostream &output, Coordinate &coor);接着在Coordinate.cpp文件中去实现这个友元函数,如下:

复制代码

ostream& operator<<(ostream &output, Coordinate &coor)//输出运算符友元函数重载实现方法
{
    //注意:这里不需要写成coor.getX()或coor.getY()了,
    //因为coor是一个友元,直接访问太的数据成员就可以了
    output<<coor.m_iX <<","<<coor.m_iY;
    return output;
}

复制代码

然后,转到demo.cpp中,修改如下:

复制代码

int main()
{
    Coordinate coor1(1,3);
    Coordinate coor2(2,4);
    Coordinate coor3(0,0);
    coor3 = coor1 + coor2;
    //cout << coor3.getX() << "," << coor3.getY() << endl;
    cout << coor3 << endl;

    system("pause");
    return 0;
}

复制代码

我们按F5看一看运行结果:

img

我们看到是ok的,直接输出了3和7,那么它的输出效果与之前用cout分别输出(cout << coor3.getX() << “,” << coor3.getY() << endl;)的效果是一样的。另外,需要提醒大家的是,在Coordinate.h文件中,我们用到了ostream,就需要在一开始的地方就应该包含iostream头文件(#include),包含完之后,还需要声明一下它的命名空间(using namespace std;)。

最后,我们来看一下索引运算符的重载。

我们之前已经学过,对于索引运算符,其必须要使用成员函数重载。在这里,其返回值是int类型,因为我们传入索引0的时候,想要返回的是横坐标值,传入索引1的时候,想要返回的是纵坐标的值。声明如下:int operator[](int index);接着在Coordinate.cpp文件中去实现这个成员函数,如下:

复制代码

int Coordinate::operator[](int index) //索引运算符成员函数重载实现方法
{
    if(0 == index)
    {
        return m_iX;
    }
    if(1 == index)
    {
        return m_iY;
    }
    //注意:这里如果既不是0,也不是1,就需要我们抛出异常处理
    //这里抛出异常不是我们的重点,就忽略异常情况
}

复制代码

然后,转到demo.cpp中,修改如下:

复制代码

int main()
{
    Coordinate coor1(1,3);
    Coordinate coor2(2,4);
    Coordinate coor3(0,0);
    coor3 = coor1 + coor2;
    //cout << coor3.getX() << "," << coor3.getY() << endl;
    //cout << coor3 << endl;
    cout << coor3[0] << endl;
    cout << coor3[1] << endl;

    system("pause");
    return 0;
}

复制代码

我们按F5看一看运行结果:

img

我们看到,输出结果分别是3和7,可见,我们的索引运算符重载也是可以实现的。

7.9 函数模板

思考:为什么要引入模板呢?

对于这个问题,我们通过一个例子让大家切实体会一下,模板给我们带来的好处。

当我们要写一个比较大小的函数时,如果我们要比较的两个数是整数,那么,我们往往会这样来定义:

img

首先,传入两个int类型的参数a和b,然后去比较a和b的大小,将较大的数通过return返回出来,使它成为max这个函数的返回值。

可是,如果我们又想比较两个float类型的数,那么这两个数也要取其中的最大值,那么,我们可以写成这样:

img

同样的,如果我们要比较两个char类型的字符呢,那么,我们就可以写成这样:

img

通过上面的三个函数,大家可以看到,除了数据类型有所不同之外,它们的运算逻辑是完全相同的。那么,在这种情况下,大家会发现,如果我们要写上这三个函数,对于我们程序员来说,简直太痛苦,而且所做的工作重复性较高,没有多大意义。所以,这种情况下,最好有一种方案:将类型作为参数传递进去,通过计算机帮我们把这三个函数做出来,做出来之后就可以分别通过这三个函数来处理int类型,float类型、char类型的数据,并且能够根据传入的数据类型做相应的处理,从而得到相应的返回值,于是,作为程序员的我们就省事多了。

那么,如果想要这么做,我们需要学习三个关键字:template(模板)、typename、class(注意:这里的class不是表示类的,而是表示数据类型的)。我们来看一看具体的使用方法。

img

我们看到,当我们要定义一个函数模板的时候,我们需要通过关键字template来声明一个函数模板,通过class或者typename这个关键字来声明一个参数T,这个参数T能够表明一种数据类型。如果我们再去写max这个函数的时候,它的返回值就写上T,它的参数类型也用T来作参数类型,并且函数内部逻辑不变。于是,当我们未来要传入的是int类型的时候,计算机就会通过这个函数模板将它实例化一个模板函数,这个时候模板函数中的数据类型T就变成了int,就会去处理int类型的数据了。如果遇到的是float类型,T就变成了float,然后也就会去处理float类型的数据了。我们来看一看具体的使用。

img

当我们用max去比较两个数的大小的时候,如果我们不指定它的数据类型T,那么,计算机会根据自己的判断去选择一种模板函数,选择之后就会用自己的计算逻辑,比如说这里的第一行代码,传入的100和99都是int类型的,它就会自动实例化一个int类型的模板函数,然后对100和99进行相应处理,并将处理互殴的结果100返回出来作为返回值赋值给ival。如果我们制定了数据类型,比如说这里的第二行代码,同样是调用max函数,但是这里用了一对尖括号指明了数据类型为char类型,那么,这就指定了所传入的参数一定要是char类型的参数才可以,然后将返回值返回出去。

那么,我们上面所说的模板函数和函数模板,我们给大家指出来,如下所示:

img

函数模板是函数的模具,通过模子就可以生产出一个一个的函数,那么,通过函数模板生产出来的函数就称之为模板函数。在计算机当中,如果我们仅仅写出了函数模板而没有去使用它,那么,计算机是不会产生任何代码数据的,因为它也不知道要产生什么样的代码数据。只有当我们去使用函数模板的时候,计算机才会知道具体要实例化出一个怎样的模板函数来,这个时候才会产生真正的代码数据,从而才会参与逻辑运算。

下面我们来看一看通过关键字typename如何来定义一个函数模板。

img

以上是一个数据交换的函数,在使用上没有什么不同,我们可以看到:当我们调用swap函数的时候,我们所传入的参数是int类型,这就意味着前面的数据类型T就替换成了int。

7.10 变量作为模板参数

变量作为模板参数如何来使用呢??我们直接来看使用的方法:

img

这里通过关键字template声明了一个函数模板,注意,这个时候传入的不再是类型,而是一个变量,而这个变量在我们真正去使用的时候才会将这个函数模板实例化成一个模板函数,它才是一个确定的值,如果不使用它,仍然是没有任何代码数据产生。使用的时候,我们在这传入的不是类型,而是一个确定的数值,此时这个值就是一个常数,只不过在这体现出来的看上去像是一个变量,但真正编译出来就是一个常数。

7.11 多参数函数模板

模板有的时候变得很复杂,因为我们不能确定,在我们的日常应用当中只有一个类型作为模板的参数,如果有多个参数我们该如何去处理呢?我们来看一看:

img

我们看到,当有多个参数时,需要用逗号(,)隔开,并且,尖括号中的两个typename关键字是不能省略的。当我们写成这样之后,T和C就变成了函数模板的参数,那么,在display函数中,其中一个参数a是T类型的参数,另一个参数b是C类型的参数。使用的时候,我们需要将T类型和C类型都指定出来,如下:

img

在这我们调用display的时候,就爱那个int类型作为第一个参数,将string类型作为第二个参数

注意:*typename**class*这两个关键字可以混用,它们所起的作用是一样的。如下所示:

img

我们还可以这样来混用,如下所示:

img

在这里,用typename哎定义了一个数据类型T,另外一个则是int类型的变量size。我们在使用的时候,就可以制定出size的值以及T的数据类型,如下:

img

这里调用display的时候,第一个传入的参数是int类型,即用int代替了上面的T,第二参数是5,即用5代替了上面的size,打印的效果就是打印出5个a。

7.12 函数模板与重载

其实函数的模板看上去就已经具有重载的意思了,因为通过函数的模板可以拓展出无数个模板函数来,我们可以尽情的去想象它能够拓展出来的数据类型,那么,这些拓展出来的模板函数之间就能形成了一种重载关系。此外,不同的函数模板所拓展出来的模板函数也能够形成重载。我们来看一看:

img

上面这三个函数的模板骑士都有所不同。第一个函数模板只有一个参数a,第二个函数模板有两个参数a和b,那么第一个与第二个就形成了参数个数不同的函数重载关系。第三个函数模板也只有一个参数,看上去与第一个函数模板一样,但是,第三个函数模板,它的模板参数本身就有两个:一个是T类型,一个是int类型的size变量,那么这个时候当我们去使用的时候:

img

你会发现,通过三个不同的函数模板可以实例化出三个不同的模板函数,这三个不同的模板函数之间就形成了重载关系。大家请注意:我们在定义出函数模板的时候,函数模板本身并不是互相重载的关系,因为当我们仅仅定义出函数模板,在内存当中并不会产生任何的数据代码,只有当我们去使用它的时候,编译器才会为我们产生出相应的函数代码出来,这些函数代码之间才可以称得上具有重载关系。

7.13 函数模板的代码实践

题目描述:

/* **************** */

/* 函数模板

​ 要求:定义函数模板display

*/

/* **************** */

程序代码:

复制代码

#include <iostream>
#include <stdlib.h>
using namespace std;

template <typename T>
void display(T a)
{
    cout << a << endl;
}

template <typename T, class S>
void display(T t, S s)  //C++是区分大小写的
{
    cout << t << "和" << s << endl;
}

template <typename T, int KSize>
void display(T a)
{
    for(int i = 0; i < KSize; i++)
    {
        cout << a << endl;
    }
}


int main()
{
    //调用第一个display函数模板
    cout << "第一个函数模板调用示例:" << endl;
    display<int>(10);
    display<double>(10.89); 
    display<char>('A');

    //调用第二个display函数模板
    cout << "第二个函数模板调用示例:" << endl;
    display<int, double>(20,8.8);

    //调用第三个display函数模板
    cout << "第三个函数模板调用示例:" << endl;
    display<int,5>(100);//这里一旦传入5之后,KSize就成为了一个常量,并且作为循环次数

    system("pause");
    return 0;
}

复制代码

运行结果:

img

练习:定义一个函数模板,功能是交换两个数的位置

复制代码

#include <iostream>
using namespace std;
/**
 * 定义模板函数swapNum
 * 实现功能:交换两个数的位置
 */
template<typename T>
void swapNum(T &a, T &b)
{
    T temp = a;
    a = b;
    b = temp;
}

int main(void)
{
    int x = 10;
    int y = 20;
    // 调用模板函数
    swapNum<int>(x,y);
    cout << "x = " << x << endl;
    cout << "y = " << y << endl;
    system("pause");
    return 0;
}

复制代码

运行结果:

img

7.14 类模板

为什么会有类模板呢?这和函数模板的道理是一样的,是因为在很多使用场合下,一个类会用到很多次,而在用的时候发现很多重复的地方,只有它的数据类型不同,所以这个时候我们就要用到类模板。我们看下面一个例子:

img

在这里,我们定义了一个类:MyArray,其中,我们用T这种数据类型来定义了它的数据成员的指针,并且还定义了一个成员函数display,请大家注意,这个成员函数的定义时写在类内的。那么在类模板的情况下,在类内定义成员函数的时候并没有什么不同。但是,在类模板的情况下,在类外定义成员函数的时候,则大不相同,如下:

img

我们看到,在类模板的情况下,当在类外定义成员函数的时候,需要在这个成员函数的上方,先把template 这一行代码写出来(注意,它的写法与在类的上方的写法是一样的),我们每定义一个成员函数,都要在这个定义的成员函数上方加上这一行代码;同时,我们需要在这个成员函数的类名后面用尖括号括上相应的参数T(如果有两个参数,需要用逗号隔开)。在使用的时候,我们如果实例化一个对象,我们就需要在类名的后面用尖括号括上当前这个对象是什么数据类型。

img

与函数模板一样,类模板并不产生实质性代码,只有当我们去实例化一个对象时,将类的后面写上一个固定的参数,这个时候才会产生数据代码,而这套数据代码,我们就称之为模板类,那么,这样的关系与前面所讲的函数模板与模板函数的道理一样。

下面我们来看一看,类模板当中使用多个参数的情况。当我们有多个参数时,我们举了一种比较复杂的情况(既有类型作为参数,也有变量作为参数)如下:

img

使用的时候,也分为类内定义和类外定义的成员函数。对于类内定义,我们不必多说了,而对于类外定义,如下:

img

在使用的时候,我们同样也要给定两个参数,如下:

img

特别提醒:*VS*中模板代码不能分离编译

7.15 类模板代码实践

题目描述:

/* ************************************** */

/* 类模板

​ 定义类模板MyArray:

​ 成员函数:构造函数、析构函数、display函数

​ 数据成员:m_pArr

*/

/* ************************************** */

程序框架:

img

需要大家特别注意的是:如果我们要定义一个类模板,我们必须将类的声明以及类的定义部分写在同一个.h文件当中,未来在使用的时候把它包含进来,所以在MyArray.cpp文件当中没有写任何代码,而是将所有代码都写在了MyArray.h文件当中了。

另外,定义类模板的时候,还需要注意以下内容

  • 在类的上面一行,要写上template关键字,然后加上模板参数列表;
  • 如果我们在类的内部定义函数时(类内定义),那么,我们不需要有什么特别需要注意的地方;
  • 如果我们在类的外部定义函数时(类外定义),则需要在每一个函数的上面加上template关键字,然后再加上模板参数列表。另外,在函数定义时,还需要用尖括号括上相应参数。

头文件(*MyArray.h*

复制代码

#ifndef MYARRAY_H
#define MYARRAY_H

#include <iostream>
using namespace std;

template <typename T, int KSize, int KVal>
class MyArray
{
public:
    MyArray();
    ~MyArray()            //这里我们对析构函数进行的是类内定义
    {
        delete []m_pArr;
        m_pArr = NULL;
    }
    void display();
private:
    T *m_pArr; //用到了模板参数T
};

template <typename T, int KSize, int KVal>
MyArray<T, KSize, KVal>::MyArray()    //类外定义构造函数,要加上上面这一句
{
    m_pArr = new T[KSize]; //等价于   T *m_pArr = new T[KSize];
    for(int i = 0; i < KSize; i++)
    {
        m_pArr[i] = KVal;
    }
}

//template <typename T, int KSize, int KVal>
//MyArray<T, KSize, KVal>::~MyArray()    //类外定义析构函数,要加上上面这一句
//{
//    delete []m_pArr;
//  m_pArr = NULL;
//}

template <typename T, int KSize, int KVal>
void MyArray<T, KSize, KVal>::display()  //类外定义display函数,要加上上面的这一句
{
    for(int i = 0; i < KSize; i++)
    {
        cout << m_pArr[i] << endl;
    }
}

#endif

复制代码

此外,在定义数据成员的时候,往往也要用到模板中的参数,比如我们在这用到了模板参数中的T,未来我们把T定义成什么类型,就可以定义出一个什么样类型的数组。我们看到,当前我们定义的数据成员是一个指针,在它的构造函数当中,我们将其定义为一个数组,并且在构造函数当中,我们已经将这个数组中的每一个值都做了赋值操作,这样这个数组的每一个元素的值都相同,都是KVal。

接下来我们跳到demo.cpp文件中,来使用一下MyArray这个类。那么,我们必须先将这个类的模板实例化成一个模板类,如何来写呢?我们这样来写:

复制代码

#include <iostream>
#include "MyArray.h"

using namespace std;

int main()
{
    //实例化一个模板类arr
    MyArray<int,5,6> arr; //实例化了一个整型数组arr,含有5个元素,每个元素的值都是6
    arr.display();
    system("pause");
    return 0;
}

复制代码

我们按F5,看一看运行结果:

img

我们看到结果打印出了5个6,这正是我们所实例化的数组arr,其具有5个元素,并且每个元素都赋值为6

练习:定义一个矩形类模板,该模板中含有计算矩形面积和周长的成员函数,数据成员为矩形的长和宽。

复制代码

#include <iostream>
using namespace std;

/**
 * 定义一个矩形类模板Rect
 * 成员函数:calcArea()、calePerimeter()
 * 数据成员:m_length、m_height
 */
template<typename T>
class Rect
{
public:
    Rect(T length, T height);
    T calcArea();
    T calcPerimeter();
public:
    T m_length;
    T m_height;
};

/**
 * 类属性赋值
 */
template<typename T>
Rect<T>::Rect(T length, T height)
{
    m_length = length;
    m_height = height;
}

/**
 * 面积方法实现
 */
template<typename T>
T Rect<T>::calcArea()
{
    return m_length * m_height;
}

/**
 * 周长方法实现
 */
template<typename T>
T Rect<T>::calcPerimeter()
{
    return ( m_length + m_height) * 2;
}

int main(void)
{
    Rect<int> rect(3, 6);
    cout << rect.calcArea() << endl;
    cout << rect.calcPerimeter() << endl;
    system("pause");
    return 0;
}

复制代码

运行结果:

img

7.16 标准模板类

C++标准模板库,简称为STL:Standard Template Lib。关于STL涉及的部分比较多,我们这里只选具有代表性和最常用的部分给大家进行分享。

vector向量

vector**的本质就是:对数组的封装**。大家可以将其看作是一个数组,只不过对于vector这个数组来说,相对于我们之前所学的传统数组的功能要强大得多。它可以根据所存储的元素个数,自动变长或者缩短,同时,它还具有一个很优秀的特点,即能够在常数时间内完成数据的随机读取。也就是说,无论这个向量数组中有10个元素还是10000个元素,它都能够很快的找出我们想要的数据来。

img

具体使用方法如下:

img

img

向量初始化之后必须要有一些配套的使用方法:

img

接下来我们来看一看实际使用的例子:

img

我们去定义一个向量的时候,我们可以写上vector,然后就像我们使用类模板的时候通过类模板去实例化一个模板类,需要传入一个参数int,后面再加上一个变量名vec。当变量vec去调用push_back函数的时候,就会在当前这个向量的最尾部插入一个传入的元素(这里传入的是10),而当调用pop_back函数的时候,就会将这个向量的最尾部的那个元素删掉(这里删掉的就是10,因为上面一行在最尾部插入了10,那么10成了这个向量的最尾部元素了),当我们再去调用size函数的时候,打印出来的就是这个向量元素的个数(这里打印出来的数据元素个数应该是0,因为我们初始化的是一个空向量)。

除此之外,对于一个数组来说,遍历这个数组是非常常见的一种操作。我们来看一看如何来遍历向量,如下:

img

除了这种遍历方法之外,还有一种常用的遍历方法,那就是用迭代器来进行遍历。我们来看一看什么是迭代器。

迭代器:iterator

通过迭代器(iterator),我们就可以访问标准模板库中的每一个元素了。比如,对于向量来说,我们即可以通过迭代器去遍历向量中的每一个元素,如下:

img

在这里,我们定义了一个向量vec,并且通过push_back插入了一个字符串”hello”。那么,我们如何来定义一个向量的迭代器呢?其定义方法如下:

vector<数据类型>::iterator 迭代器名,然后通过这个迭代器指向这个向量的第一个元素(vec.begin()),接着就可以用for循环来遍历这个向量的元素了。

链表:list

链表模板的本质是什么呢?我们用一张示意图来说明一下list的数据结构。

img

作为一个链表,它会有一个头结点,也就是第一个结点,而且每一个链表是由若干结点组成(如果一个结点也没有,我们就称之为空链表)。对于每一个结点来说,又由两部分组成(数据域和指针域),上图中的A、B、C、D、E不分就是各自结点的数据域,指针域用来将各个结点串联起来,A结点的指针指向B结点的指针,B结点的指针又指向C结点的指针,以此类推。如果当前的链表是双向链表,也就是说,它不仅可以从头找到尾,还可以从尾找到头,这也是链表的一种。对于链表来说,如果我们想要插入一个数据,比如说,我们在这想要在D和E结点之间插入一个数据,我们就要让D的指针指向要插入进来的数据指针,再让插入进来的指针指向E结点的指针就可以了。相对于向量来说,我们要想在向量的中间部分插入一个数据,那么其后面的每一个数据就要向后移动一个位置,这相对于链表来说,这工作量有点大,相对困难一些。所以,对于链表来说,其特点就是:插入数据的速度快。在使用方法上,链表与向量的使用方法基本相同,也可以通过迭代器进行遍历访问。

映射:map

关于映射,先给大家讲解一下它的数据结构。对于映射来说,存储的数据都是成对出现的,我们把它标记为(key, value),如下所示:

img

由于映射是成对出现的,所以我们就可以通过它的键(key)来找到对应的值(value)。具体的使用方法,我们通过下面一个例子来说明。

img

第一行我们定义了一个映射对象m,我们需要向这个对象中放若干对key和value,那么,我们就需要通过pair来定义若干对key和value。在这里我们通过pair来定义了两对p1和p2,对于p1来说,它的key就是10,它的value就是shanghai,对于p2来说,它的key就是20,它的value就是beijing,然后我们通过映射对象m来调用insert函数,分别将p1和p2这两对放到映射m当中去。如果我们想要访问shanghai,那么我们就可以通过打印m[10]来获取,同理,要想访问beijing,就可以通过打印m[20]来获取。我们看到这种访问方式跟数组的访问方式很相似。

标准模板库代码实践

题目描述:

/* ************************************** */

/* 通过使用标准模板库,学习其用法 */

/* ************************************** */

程序代码(*demo.cpp*

复制代码

#include <iostream>
#include <vector>
#include <list>
#include <map>
#include <string>
using namespace std;

int main()
{
    cout << "---------------------vector的相关用法------------------------" << endl;
    vector<int> vec;  //定义了一个向量vec,其存储的是int类型的数据元素
    vec.push_back(3); //从向量尾部插入一个数据3,由于vec是一个空向量,这里插入的就是其第一个数据
    vec.push_back(4); //再向其尾部插入一个数据4
    vec.push_back(6); //再向其尾部插入一个数据6
    vec.push_back(8);
    cout << "---------获取当前向量元素个数----------------" << endl;
    cout << vec.size() << endl; //打印当前向量的大小,即数据元素的个数
    cout << "------清除尾部数据后,当前向量元素个数-------" << endl;
    vec.pop_back(); //清除尾部数据
    cout << vec.size() << endl; //打印当前向量的大小,即数据元素的个数
    cout << "---------方法1:遍历向量元素-----------------" << endl;
    for(int i = 0; i < vec.size(); i++) //类似与数组遍历向量元素
    {
        cout << vec[i] << endl;
    }
    cout << "---------方法2:遍历向量元素-----------------" << endl;
    for(vector<int>::iterator itor = vec.begin(); itor != vec.end(); itor++) //迭代器遍历向量元素
    {
        cout << *itor << endl; //迭代器相当于一个指针
    }
    cout << "---------获取当前向量的第一个元素------------" << endl;
    cout << vec.front() << endl; //获取第一个元素
    cout << "---------获取当前向量的最后一个元素---------------------" << endl;
    cout << vec.back() << endl; //获取最后一个元素

    cout << "---------------------list的相关用法-------------------------" << endl;
    list<int> list1;//定义了一个列表list1,其存储的是int类型的数据元素
    list1.push_back(5);
    list1.push_back(9);
    list1.push_back(12);
    //cout << "---------方法1:遍历列表元素-----------------" << endl;
    //for(int j = 0; j < list1.size(); j++)//这里报警告,j是int类型,而list的size返回类型不是size
    //{
    //    cout << list1[j] << endl; //这里报错,list没有[]运算
    //}
    cout << "---------迭代器遍历列表元素-----------------" << endl;
    for(list<int>::iterator itor1 = list1.begin(); itor1 != list1.end(); itor1++)  //迭代器遍历列表元素
    {
        cout << *itor1 << endl; //迭代器相当于一个指针
    }
    cout << "---------------------map的相关用法-------------------------" << endl;
    map<int, string> m;
    pair<int, string> p1(20, "hello");
    pair<int, string> p2(40, "world");
    m.insert(p1);//将p1和p2放到map当中
    m.insert(p2);
    cout << "---------通过索引遍历map-----------------" << endl;
    cout << m[20] << endl;
    cout << m[40] << endl;
    cout << "---------通过迭代器遍历map---------------" << endl;
    for(map<int, string>::iterator itor2 = m.begin(); itor2 != m.end(); itor2++)
    {
        //cout << *itor2 << endl; //这样写肯定会报错,因为map是成对出现的,这样写计算机不知道该如何输出
        //正确输出方法如下:
        cout << itor2->first << " " << itor2->second << endl; //输出键值key和value
    }
    system("pause");
    return 0;
}

复制代码

运行结果:

img

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值