C++速成(1)

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

1.C++起航1h

1.1 C++简介

1.2 C++IDE环境搭建

1.3 C++之初体验

1.4 C++语言新特性

1.4.1 C++新特性

1.4.2 C++输入输出

1.4.3 C++namespace

1.5 练习题

#include<stdlib.h>
#include<iostream>
usingnamespace std;

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

/* 知识点:bool类型、命名空间、输入输出                 */

/* 题目要求:                                           */

/*     使用一个函数找出数组中的最大值和最小值           */

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

int getMaxOrMin(int *arr, intcount, boolisMax)
{
    int temp = arr[0];
    for(int i = 1; i <count; i++)
    {
        if(isMax)
        {
            if( temp <arr[i])
            {
                temp = arr[i];
            }
        }
        else
        {
            if(temp >arr[i])
            {
                temp = arr[i];
            }
        }

    }
    return temp;
}

int main()
{
    int arr1[4] = {3,2,5,8};
    bool isMax = false;
    cin >> isMax;   //输入cin和输出cout在iostream头文件中,所以要在程序的开始将这个头文件包含进来,同时还要加上命名空间std
    cout << getMaxOrMin (arr1, 4, isMax) << endl;
    system("pause");//system在stdlib.h头文件中,所以要在程序的开始将这个头文件包含进来
    return 0;
}

下面我们再来思考一个问题,假如这个getMaxOrMin()函数是由某一家公司开发的,比如说这家公司叫CompA。如果公司CompA想要发布这个函数,并且避免与其他公司发布的同名函数,则就需要在自己的函数名字前面加一个明明空间。加好命名空间后,就会将自己的函数放进命名空间中去。在使用的时候就需要加上命名空间,整个程序如下:

#include<stdlib.h>
#include<iostream>
usingnamespace std;

namespace CompA
{
    int getMaxOrMin(int *arr, intcount, boolisMax)
    {
        int temp = arr[0];
        for(int i = 1; i <count; i++)
        {
            if(isMax)
            {
                if( temp <arr[i])
                {
                    temp = arr[i];
                }
            }
            else
            {
                if(temp >arr[i])
                {
                    temp = arr[i];
                }
            }

        }
        return temp;
    }
}


int main()
{
    int arr1[4] = {3,2,5,8};
    bool isMax = false;
    cin >> isMax;   //输入cin和输出cout在iostream头文件中,所以要在程序的开始将这个头文件包含进来,同时还要加上命名空间std
    cout << CompA::getMaxOrMin (arr1, 4, isMax) << endl;
    system("pause");//system在stdlib.h头文件中,所以要在程序的开始将这个头文件包含进来
    return 0;
}

运行结果说明,我们使用这种方式就能够区分各个公司所相同重名的函数,只是在使用的时候,我们需要在函数前面加上相应的命名空间即可。

2. C++离港1.5h

2.1 引用

引例

小时候,院子里有一个小孩,名叫罗某某,外形上来看,头大身子小,所以院子里的其他小朋友就给罗某某起了个外号(别名)叫萝卜头。这样,生活中,罗某某就是这个小朋友的真实姓名,萝卜头就是这个小朋友的别名。

那么,什么叫引用呢????引用就是别名。

计算机中,引用就是某个变量的别名。

思考:能不能只有别名,而没有真实姓名呢?

生活中,如果某个人只有别名的话,是不是就变成了他的真实姓名了呢?可见,只有别名,生活中其实也是不可行的,而在计算机中,肯定也一样的道理,也是无法成立的。


基本数据类型的引用

请看下面一段代码:

#include <iostream>
using namespace std;

int main()
{
    int a = 3;
    int&b = a; //引用必须初始化

    b = 10;
    cout<< a <<endl;
    return 0;
}

代码解读:

我们定义了一个基本数据类型,即定义了一个整形变量a,给它赋了初值3;接着给变量a起了一个别名b(int&b = a;),这里需要注意,如果只起别名b,后面什么都步跟,在编译时就会报错,所以,在起别名时,一定要指明给哪个变量起的别名,即引用一定要初始化。

然后,我们给b赋值10,通过输出的方式来输出变量a的值,我们会看到输出结果是10。从程序运行结果来看,对别名的修改,就是对实际变量的修改。类似于生活中,我们让小萝卜头去干活,实际就是让罗某某去干活是一样的道理。


结构体类型的引用

#include <iostream>
using namespace std;

typedefstruct
{
    int x;
    int y;
}Coor;

int main()
{
    Coor c1;
    Coor&c = c1; //对c1起了个别名叫c
    c.x = 10;
    c.y = 20;
    cout<< c1.x <<endl;
    cout<< c1.y <<endl;
    return 0;
}

代码解读:

首先,定义一个坐标类型的结构体Coor,里面包含横坐标x和纵坐标y;接着在main函数中定义了一个结构体变量c1,然后对结构体c1起了个别名,叫c;再然后对c中的横坐标x和纵坐标y分别进行了赋值操作,即用别名对相应的数据成员做了操作;最后,通过输出函数,将实体c1的横坐标和纵坐标打印出来。从打印结果来看,实体c1的横坐标为10,纵坐标为20。(因为操作别名与操作实体是没有区别的


指针类型的引用

指针类型的引用的定义方式:类型* *&*指针的别名* = *指针

#include <iostream>
using namespace std;

int main()
{
    int a =10;
    int *p = &a; //定义了一个指针指向整形变量a    
    int *&q = p; //给指针p起了一个别名q
    *q = 20;
    cout<< a <<endl;
    return 0;
}

代码解读:

程序中,首先定义了一个整形变量a,并且对其赋了初值10;然后定义了一个指针变量p,让指针p指向了整形变量a(int *p = &a;);接着,我们给指针p起了一个别名叫q,并对q赋值为20(这里相于给*p复制为20,由于p是指向a的,又相当于给a重新赋值为20);最后输出a。从输出结果看,a的值变为了20。


引用作为函数参数

在没有引用之前,要交换两个数,需要如下操作

#include <iostream>
using namespace std;

void fun( int *a, int *b)
{
    int c=0;
    c = *a;
    *a =*b;
    *b = c;
}

int main()
{
    int x=10, y=20;
    fun(&x, &y);
    cout<< x << ',' << y <<endl;
    return 0;
}

从上面的程序来看,比较繁琐,而且需要一定的理解能力。

当我们有了引用之后,程序可以这样来写:

#include <iostream>
using namespace std;

void fun( int&a, int&b)
{
    int c=0;
    c = a;
    a =b;
    b = c;
}

int main()
{
    int x=10, y=20;
    fun(x, y);
    cout<< x << ',' << y <<endl;
    return 0;
}

从上面的程序来看,功能函数就简单多了,传入的两个实参就会分别起别名a和b,对别名进行交换就相当于对实体的实际操作。


2.2 const

const与基本数据类型

首先,没有const关键字时,我们要定义一个整形变量,其在内存中的情况如下:

int a = 3; //变量

变量名 存储地址 存储内容

a &a 3

当我们在变量名前加上关键字const后,它就由一个变量变成了一个常量,如下:

const int a = 3; //常量

这时如果再对a重新赋值5或者其他值,计算机就会报错,因为此时a是一个常量,不得更改。

变量名 存储地址 存储内容

a &a 3


const与指针类型

几种定义方式与区别:

(1) const int *p = NULL;

(2) int const *p = NULL;

(3) int * const p = NULL;

(4) const int * const p = NULL;

(5) int const * const p = NULL;

注意:*(1)*与*(2)*完全等价,*(4)*与*(5)*完全等价

例子:

int x =3;

const int *p = &x;

//此时如果 p = &y; 正确,而如果 p = 5; 错误(因为此时**const*修饰的是 *p)

int x =3;

int *const p = &x;

//此时如果p = &y; 错误(因为此时***const*修饰的是*p*,而*p*只能指向**x

const int x =3;

const int *const p = &x;

//此时如果p = &y; *p = 5; 都是错误的(因为此时不能用****p修改x的值,也不能将p指向其他变量


const与引用

例子:

int x =3;

const int &y = x;

//x = 10; 正确(因为此时x是一个变量,是可变的)

//y = 20; 错误(因为y作为x的别名,其前面加了const修饰符,不可变)


小结

(1)const int x =3; x = 5; X(因为此时x是不可变的)

(2)int x = 3; const int y = x; y = 5; X(因为此时y是常量,不可重新赋值)

(3)int x = 3; const int *y = &x; *y = 5; X(因为此时const修饰的是*y)

(4)int x = 3; z =4; int *const y = &x; y = &z; X(因为此时const修饰的是指针y,其指向不可变)

(5)const int x = 3; const int &y = x; y = 5; X(因为此时x是常量,y是x的引用且y也是常量,就不可以再对y重新赋值)

(6)const int x = 3; int *y = &x; X(因为此时x是常量,而指针y是变量,不能将可变指针指向不可变的变量。如果这样做,计算机是有风险的,风险就是可以用*y来修改x的值)

(7)int x = 3; const int *y = &x;


2.3 函数

函数参数默认值

有如下函数声明:

void fun(int i, int j = 10, int k = 20);

void fun(int i, int j = 10, int k) ;X

注意:有默认参数值的参数必须在参数表的最右端

在函数声明时可以带上参数默认值,而在定义时,不建议带上参数默认值。如果在函数定义的时候也写上函数参数默认值,则实际编译时,有的编译器能够编译通过,而有的编译器编译不会通过。

示例:

#include

#include

函数重载

什么是函数重载???

在相同作用域内,用同一函数名定义的多个函数,但这些多个函数之间的参数个数参数类型不同,则称这些多个函数就叫重载函数。

比如,定义以下两个函数(功能:取最大值)

int getMax(int x, int y, int z)

{

​ //to do;

}

double getMax(double x, double y)

{

​ //to do;

}

从上面两个函数可以看到,函数名都一样,但函数的参数个数和参数类型不同,这样两个函数就是重载函数。

思考:编译器如何识别重载的函数??

实际编译器编译时,经过如下操作:

int getMax(int x, int y, int z)经过编译后得到getMax_int_int_int

double getMax(double x, double y)经过编译后得到getMax_double_double

从上面的编译结果看,编译后会形成一个名称*+*参数的方式形成一个新的函数名,来区分两个重载函数;而调用过程中,计算机是采用的自动识别方式,即根据你传入的实参类型以及实参个数自动识别后,来调用相应的函数。

注意:关于函数重载的相关内容,这篇博客讲的更加深入: C++的函数重载


内联函数

内联函数与普通函数的区别:

img

1、内联函数与普通函数在定义方面区别不大

2、内联函数与普通函数在调用方面区别如下:

如果使用主调函数去调用一个普通函数,将进行如下5个步骤:

去调用fun() ->找到fun()的相关函数入口->执行fun()里的相关代码->返回主调函数->主调函数再运行其他代码直到结束

如果使用主调函数去调用内联函数时,编译时将函数体代码和实参替代掉函数调用语句,这样其实是省掉了上面的过程(2)和(4),这样可以为调用节省很多时间,尤其对循环调用时,将节省更多的时间。

内联函数关键字:****inline

**inline **int max(int a, int b, int c); //声明一个内联函数max

int main()

{

​ int i =10, j = 20, k = 30, m;

​ m = max(i,j,k);

​ cout<< “max = ” << m <

代码实践

使用函数的重载完成返回最大值的方法。

现在有一个数组,定义一个方法getMax(),利用函数的重载,分别实现:

1、随意取出数组中的两个元素,传到方法getMax()中,可以返回较大的一个元素。

2、将整个数组传到方法getMax()中,可以返回数组中最大的一个元素。

完整可执行程序:

#include <iostream>
using namespace std;
/*
  *函数功能:返回a和b的最大值
  *a和b是两个整数
*/
int getMax(int a, int b)
{
return a > b ? a : b;
}

/*
  * 函数功能:返回数组中的最大值
  * arr:整型数组
  * count:数组长度
  * 该函数是对上面函数的重载
*/
int getMax(int *arr,int count)
{
    //定义一个变量并获取数组的第一个元素
int temp=arr[0];
for(int i = 1; i < count; i++)
    {
        //比较变量与下一个元素的大小
        if(temp<arr[i])
        {
            //如果数组中的元素比maxNum大,则获取数组中的值
            temp=arr[i];
        }    
    }
    return temp;
}

int main()
{
    //定义int数组并初始化
    int numArr[3] = {3, 8, 6};

    //自动调用intgetMax(int a, int b)
    cout<<getMax(6,3) <<endl;

    //自动调用返回数组中最大值的函数返回数组中的最大值
    cout<<getMax(numArr,3) <<endl;
    return 0;
}

2.4内存管理

什么是内存管理?

思考:内存的本质是什么?—->资源

思考:谁掌管内存资源? —->操作系统

思考:我们能做什么? —->申请/归还

申请*/*归还内存资源就是内存管理

C++中如何进行内存的申请和释放?

申请 —>使用运算符new

释放 —>使用运算符delete

即:

申请内存:****int *p = new int;

释放内存:* delete p;*

这样就申请和释放一个内存或是某一种类型的内存

思考:如何申请和释放块内存呢?

int *arr = new int[10]; //申请了10个整型的块内存

delete []arr; //释放块内存

思考:申请内存是否一定就能申请成功呢?显然,不一定!!

因此,编码时,就要对可能的申请内存失败进行处理

int *p = new int[1000];

if(NULL == p)

{

//内存分配失败

}

释放内存后,要将相应的指针赋值为空,即

delete p; delete []p;

p = NULL; p = NULL;

如果将释放后的指针不置为空,那么当前的这个指针还是指向相应的那块内存,如果我们不小心又调用了一次delete,那么就会使得同一块内存被重复回收,一旦被重复回收,计算机就会出现异常。

小结:

1) 使用new申请内存,使用delete释放内存

2) 申请内存时需要判断是否申请成功,释放内存后需要将指针置为空

3) new和delete要配套使用

最后看一个实例:

在堆中申请100个char类型的内存,拷贝Hello imooc字符串到分配的堆中的内存中,打印字符串,最后释放内存。

完整可执行程序:

#include <string.h>
#include <iostream>
using namespace std;
int main(void)
{
    char *str = new char[100];//在堆中申请100个char类型的内存
    strcpy(str, "Hello imooc"); //拷贝Hello C++字符串到分配的堆中的内存中
    cout<<str<<endl;//打印字符串
    delete []str;//释放内存
    str=NULL;
    return 0;
}

2.5 练习题


3. C++封装上2h

3.1 类和对象

引例——-狗

假如我们养了一条狗,它有如下信息:

信息*—–*姓名:旺财、年龄:1岁、品种:大型犬

技能*——*叫、跑

当我们具体的指代一个事物的时候,它就是一个对象

可是当我们养了多条狗时,为了便于管理,会建立一张表格

姓名年龄品种
大壮1
二壮2
三壮1
………………
………………

共有技能:叫、跑

如此,就将一群狗狗的信息抽象出来了,这样就可以定义一个类了。

img

想一想:抽象出来的是不是狗的全部信息呢?

为什么不定义这样的信息呢?

狗肉的蛋白质含量是多少?

狗的出栏周期是多少?

这是因为,我们关注的不是吃,所以这些信息对我们没有用!!

结论:目的不同抽象出来的信息也不同。


引例——-电视机

假如有一台电视机,通过电视机上名牌标识,我们可以知道它的名字和型号;通过各种旋钮,我们可以控制它的音量,还可以接通和切断电源。那么我们用类来描述如下:

img

但是仍有很多实现细节在类里面没有描述,这并不意味着它就不存在,而是被隐藏起来了。比如:电路板的工作过程。如果这些通通暴露给看电视的用户,那么看电视的人一定会疯掉。其实呢,这就是选择性暴露。

把实现细节封装起来,只暴露用户所关心的部分,这就叫做封装。

如果我们把电视机的信息都罗列出来,大家一定可以判断哪些信息是需要暴露的,哪些信息是不需要暴露的。

img

可是这些信息都在类中定义,如何才能把想暴露的信息暴露出来,把想隐藏的信息隐藏起来呢?

这对这一问题,C++为我们提供了一个叫做访问限定符的东西。

访问限定符:

public:公共的

private:私有的

protected:受保护的

希望暴露出来的信息就使用public修饰,希望隐藏的信息就使用private修饰。

img


3.2 类对象的定义

对象的实例化

在C++中,类就是一个模板,对象实例化就是计算机根据类的设计,制造出多个对象的过程。

下面定义了一个TV的类,这个电视的类中有两个属性或者叫数据成员,一个是电视的名字,一个是电视的类型;另外还有两个方法或者叫成员函数,一个用来调节音量,一个用来开关电源。

img

实例化对象有两种方式:

(1) 从栈实例化

如果我们要对上面的类进行实例化,并且想从栈中实例化,我们可以这样做:

img

(2) 从堆实例化

如果我们要对上面的类进行实例化,并且想从堆中实例化,我们可以这样做:

img

注意:两种方式实例化的区别

从栈实例化对象,使用完之后,不需要我们自己去理睬,系统自动会将其占用的内存释放掉;而从堆实例化对象,使用完后,我们一定要将这块内存释放掉。

思考:

—-通过栈也好,通过堆也好,我们已经实例化出来了对象,那么有了这些对象就算完了吗?

—-我们实例化出来的这些对象就是个摆设吗?

显然,我们是不会这么无聊的,我们是要通过访问这些对象的各种成员来达到我们的目的的。


对象成员的访问

通过不同实例化对象的方法,生成的对象在访问其数据成员和成员函数的方式也各不相同。

img img img

栈实例化对象访问其成员 堆实例化单一对象访问其成员 堆实例化对象数组访问其成员


代码实践

定义一个坐标类,包含两个数据成员:横坐标x和纵坐标y,两个成员函数,分别用来打印横坐标和纵坐标。

#include<iostream>
#include<stdlib.h>
using namespace std;
class Coordinate
{
public:
    int x;
    int y;
    void printX()
    {
        cout<< x <<endl;        
    }
    void printY()
    {
        cout<< y <<endl;

    }
};

int main()
{
    Coordinate coor; //在栈中实例化一个坐标对象
    coor.x = 10;
    coor.y = 20;
    coor.printX();
    coor.printY();

    Coordinate *p = new Coordinate(); //在堆中实例化一个坐标对象
    //这里需要判断申请的内存是否成功
    if(NULL == p)
    {
        cout<<"申请内存失败"<<endl;
        return 0;
    }
    p->x = 100;
    p->y = 200;
    p->printX();
    p->printY();
    //对象适用完之后,需要释放掉内存
    delete p;
    p = NULL;

    system("pause");
    return 0;
}

3.3 string

思考如下一个问题:我们平时在编码过程中,适用频繁而操作又比较繁琐的数据都有哪些呢?

对于基本数据类型(int、char、float、double、bool),我们虽用的比较频繁,但操作起来还是比较方便的,基本令人满意。只有char数组,也就是通常所说的字符串,我们平时用的比较频繁,但操作上却缺乏一种简单有效的手段,往往只能用一系列的函数来应付,如strlen、strcat、strcmp、strcpy、strncmp、strncpy等,用得多了就觉得特别麻烦,有时会令人抓狂。为了解决这样的麻烦,C++引入了string类型,有了它,至此,程序世界便多了一抹亮色。

string类型

先来看一个例子

img

在这个例子中,我们可以轻松的定义一个字符串类型的名字和兴趣爱好,也可以很容易的输出某个人的兴趣爱好。

但要注意,在使用string类型时,一定要包含string的头文件,而且该头文件也是在std的命名空间下的。


初始化string对象的方式

img


string的常用操作

img


代码实践

题目描述:

1 提示用户输入姓名

2 接收用户的输入

3 然后向用户问好,hello xxxx

4 告诉用户名字的长度

5 告诉用户名字的首字母是什么

6 如果用户直接输入回车,那么告诉用户输入的为空

7 如果用户输入的是imooc,那么告诉用户的角色是一个管理员

#include<iostream>
#include<stdlib.h>
#include<string>
using namespace std;
int main()
{
    string name;
    cout<<"please input your name: ";
    getline(cin, name);
    if(name.empty())
    {
        cout<<"input is null..."<<endl;
        system("pause");
        return 0;
    }
    if(name == "imooc")
    {
        cout<<"You are a  administartor"<<endl;
    }
    cout<<"hello " + name <<endl;
    cout<<"Your name's length is:"<<name.size() <<endl;
    cout<<"Your name's first letter is: "<< name[0] <<endl;

    system("pause");
    return 0;
}

3.4 属性封装

数据的封装

下面看一个例子,例子中定义了一个学生的类,类中含有两个数据成员,一个是姓名,一个是年龄

img

上面的代码给人一种相当亲切,有一种似曾相识的感觉,那是因为之前我们一直都是这么用的,而且用的也很爽。但是,这样用是有问题的,最大的问题是它违背了面向对象的指导思想。

那么面向对象的基本思想是什么呢?

面向对象的核心就是以对象为中心,具体来说,就是要以谁做什么来表达程序的逻辑,体现在代码层面上,就是将所有的数据操作转换为成员函数的调用。换句话说,对象在程序中的所有行为都通过调用自己的函数来完成。

那么如何通过函数来封装数据成员呢?

接着继续看上面这个例子,这次我们把数据成员定义在private的下面,以年龄这个数据成员为例,我们定义两个成员函数,一个用于设置年龄的值,一个用于读取年龄的值

img


封装的好处

以上面Student的类为例,当我们还是用以前的老方法来访问数据成员,如果赋值时出现非法的情况,比如,给年龄数据成员赋值1000,显然是有问题的。但是如果通过数据的封装,就能很好的解决这个问题。

还是以上面Student的类为例,上面的封装只是简单的封装,如果我们对上面的成员函数setAge()做一下改变(如下),就会避免了上面非法输入的问题了。

img

接着以汽车(Car)为例来进一步说明封装的好处,如下

img

我们定义一个Car类,在这个类下面定义了一个private属性的数据成员,表示汽车轮子的个数,针对这个数据成员,我们不希望外界通过某个函数来改变它的值,我们只希望能读取它的值就ok了。我们称这种只能读取而不能设置的属性为只读属性


代码实践

题目描述:

定义一个Student类,含有如下信息:

1 姓名:name

2 性别:gender

3 学分(只读):score

4 学习:study(用于获得学分)

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

/*   **********数据的封装
定义一个Student类,含有如下信息:
姓名:name
性别:gender
学分(只读):score
学习:study(用于获得学分)
/*  ************************************/

class Student
{
public:
    void setName(string _name)
    { 
        m_strName = _name;
    }
    string getName()
    {
        return m_strName;
    }
    void setGender(string _gender)
    { 
        m_strGender = _gender;
    }
    string getGender()
    {
        return m_strGender;
    }
    int getScore()
    {
        return m_iScore;
    }
    void initScore()
    {
        m_iScore = 0;
    }
    void study(int _score)
    {
        m_iScore += _score;
    }
private:
    string m_strName;
    string m_strGender;
    int m_iScore;
};

int main()
{
    Student stu;
    stu.initScore();//如果这里不进行初始化,m_iScore的值就不可控
    stu.setName("Zhangsan");
    stu.setGender("女");
    stu.study(5);//学习一门学分为5分的课程
    stu.study(3);//学习一门学分为3分的课程

    cout<<stu.getName() <<" "<<stu.getGender() <<" "<<stu.getScore() <<endl;

    system("pause");
    return 0;
}

3.5 类外定义

类内定义

问题:什么是类内定义?

将成员函数的函数体写在类的内部的方式称为类内定义。比如下面的Student的类,我们可以看到,在定义成员函数的时候,包括每个成员函数用于实现的函数体,都在类的内部。

img


类内定义与内联函数的关系

类内定义的成员函数,编译器会将其优先编译为内联函数,但是对于复杂的成员函数无法编译成内联函数的,就编译成普通的函数。


类外定义

所谓类外定义是指成员函数的函数体写在类的外面。具体来讲,类外定义又分为以下两种形式:

img

所谓同文件类外定义,是指成员函数虽然定义在类的外面,但是其定义与类的定义在同一个文件当中。如下面的例子:

我们新建一个文件,文件名为Car.cpp,然后在这个文件中定义一个Car的类,定义的时候先写上汽车这个类的定义(class Car),并申明相应的成员函数,但是成员函数的实现或者说成员函数体的定义也写在这个文件中。那么成员函数既然写在了类的外面,我们就要标示出这个成员函数不是普通的函数,而是属于Car这个汽车的函数,我们就需要在这个函数的前面用这个类名再加上::标记出这个函数是属于这个类的。

img

下面介绍分文件类外定义,如果说同文件类外定义算是游击队的话,那分文件类外定义可以算是正规军了。几乎所有的c++项目,但凡是专业一点的c++程序员都会将类的定义分文件来完成,这样做有诸多好处,我们会在后续的课程中来给大家逐步讲解。下面来看一个例子来给大家说明分文件类外定义的写法。

分文件定义的时候,我们需要定义一个.h文件(头文件),类名建议与文件名写成一致。如下是一个Car.h头文件,在头文件中我们申明了类中的所有的数据成员和成员函数,在另外一个文件(Car.cpp)中,我们把所有的成员函数进行定义,定义的方式跟以前一样,最为关键的一点是需要将其头文件(Car.h)包含到Car.cpp中,如果没有这样的包含,Car.cpp将无法找到其相应的申明。

img


代码实践

题目描述:

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

/*  定义一个Teacher类,要求分别采用同文件类外定义和分文件类外定义的方式完成,具体要求如下:
/*   数据成员:
/*       名字
/*       年龄
/*       性别
/*   成员函数:
/*       数据成员的封装函数
/*       授课teach
/*  ************************************************************************/

类内定义方式:

#include<iostream>
#include<stdlib.h>
#include<string>

using namespace std;

class Teacher
{
public:
    void setName(string _name);
    string getName();
    void setGender(string _gender);
    string getGender();
    void setAge(int _age);
    int getAge();
    void teach();

private:
    string m_strName;    
    string m_strGender;
    int m_iAge;
};


void Teacher::setName(string _name)
{
    m_strName = _name;
}
string Teacher::getName()
{
    returnm_strName;
}
void Teacher::setGender(string _gender)
{
    m_strGender = _gender;
}
string Teacher::getGender()
{
    returnm_strGender;
}

void Teacher::setAge(int _age)
{
    m_iAge = _age;
}
int Teacher::getAge()
{
    returnm_iAge;
}
void Teacher :: teach()
{
    cout<<"现在开始上课....."<<endl;
}


int main()
{
    Teacher t;
    t.setName("孔子");
    t.setGender ("男");
    t.setAge(30);
    cout<<t.getName() <<" "<<t.getGender() <<" "<<t.getAge() <<" "<<endl;
    t.teach();
    system("pause");
    return 0;
}

类外定义方式:

程序框架如下

img

头文件(Teacher.h)

#include<string>
using namespacestd;
class Teacher
{
public:
    void setName(string _name);
    string getName();
    void setGender(string _gender);
    string getGender();
    void setAge(int _age);
    int getAge();
    void teach();

private:
    string m_strName;    
    string m_strGender;
    int m_iAge;
};

源文件:

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

usingnamespacestd;

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

/*  定义一个Teacher类,要求分别采用同文件类外定义和分文件类外定义的方式完成,具体要求如下:
/*   数据成员:
/*       名字
/*       年龄
/*       性别
/*   成员函数:
/*       数据成员的封装函数
/*       授课teach
/*  ************************************************************************/

void Teacher::setName(string _name)
{
    m_strName = _name;
}
string Teacher::getName()
{
    returnm_strName;
}
void Teacher::setGender(string _gender)
{
    m_strGender = _gender;
}
string Teacher::getGender()
{
    returnm_strGender;
}

void Teacher::setAge(int _age)
{
    m_iAge = _age;
}
int Teacher::getAge()
{
    returnm_iAge;
}
void Teacher :: teach()
{
    cout<<"现在开始上课....."<<endl;
}


int main()
{
    Teacher t;
    t.setName("孔子");
    t.setGender ("男");
    t.setAge(30);
    cout<<t.getName() <<" "<<t.getGender() <<" "<<t.getAge() <<" "<<endl;
    t.teach();
    system("pause");
    return 0;
}

3.5 对象

思考:实例化的对象是如何在内存中存储的?

思考:类中的代码又是如何存储的?

思考:数据和代码之间又有怎样的关系呢?

带着这些问题,先学习一下对象的结构

对象结构

要想为大家说清对象是如何存储的,就必须先为大家介绍一下内存中按照用途被划分的5个区域。

img

  • 栈区的特点是内存由系统进行控制,无论是分配还是回收都不需要程序员关心;
  • 如果我们使用new来分配一段内存,那么这段内存会分配在堆区,最后我们自己必须用delete回收这段内存;
  • 全局区用来存储全局变量和静态变量;
  • 常量区用来存放一些字符串以及常量;
  • 代码区则是存储编译过后的二进制代码。

下面通过一个例子来说明对象中数据是如何存储的。

img

  首先定义一个Car类,在这个类被实例化之前,是不会占用堆或栈中的内存的,但是当它实例化之后,比如实例化一个car1,实例化一个car2,又实例化一个car3,这个时候每个实例化对象都会在栈中开辟一段内存用来存储各自的数据,但是它们是不同的变量,也就占据着不同的内存,而逻辑代码却只编译出一份,放在代码区,当需要的时候,这个代码区中的代码供所有的对象进行使用。谁需要了就去调用它,找到相应的代码入口,就可以执行相应的程序了。

img

  这时候,我们就注意到一个问题,当我们实例化三个对象之后,每个对象中的数据都是不可控的,都是未知的,因为我们没有对这些数据进行初始化。

​ 如果没有进行初始化,我们就无法对这些数据进行预想的逻辑操作,可见我们必须要对数据进行初始化。


对象初始化

  说到初始化,大家都不会陌生。相信大家都非常熟悉《坦克大战》这款游戏,每关开始的时候,玩家的坦克都会出现在固定的位置(最下面),而敌方的坦克都会在三个地方出现(左上角、右上角,上方中间位置),这就是初始化的结果。下面用程序描述一下初始化的过程。

  如果我们定义一个坦克的类,我们只描述坦克出现的位置,就需要两个变量(一个横坐标,一个纵坐标),另外,还需定义一个初始化函数(init),给横纵坐标赋初值0,那么其后面的位置就清晰可控了。使用的时候,先实例化一个坦克对象t1,通过t1来调用初始化函数(init),这样就将t1中的横坐标和纵坐标都置为0了。如果再实例化一个坦克对象t2,也调用初始化函数(init),这样就将t2的横坐标和纵坐标也置为了0。如下所示。

img

  对于对象的初始化来说,不同的场合,可能有些只需要初始化一次,有些则需要根据条件而初始化多次,所以初始化也分为以下两种类型:

img

下面重点讲解有且仅有一次的初始化操作。

思考:对于有且仅有一次的初始化操作,初始化函数如何避免误操作呢?比如写代码时,一不小心忘记了调用初始化函数,也有可能在写程序时重复调用了初始化函数。那么这些误操作就有可能给程序带来灭顶之灾。为了能够帮助程序员避开这些风险,c++推出了一种新的函数,那就是构造函数,接下来我们就讲讲什么是构造函数。


构造函数

构造函数一个最大的特点就是在对象实例化时被自动调用,通常只需要将初始化的代码写在构造函数内就能够起到初始化数据的作用。这里面要强调的是,构造函数在实例化对象时,被调用且仅被调用一次。定义构造函数时,构造函数的名字必须与类同名。其次,构造函数没有返回值。(写构造函数时,连void这样的返回类型都不用写)。此外,构造函数可以有多个重载形式(重载时要遵循重载函数的规则)。另外,在实例化对象的时候,即便有多个构造函数,也仅用到其中的一个构造函数。最后一条非常重要,当用户没有定义构造函数时,编译器将自动生成一个构造函数(这个构造函数中没有做任何事情)。

总的来说,构造函数具有如下规则和特点:

- 构造函数在对象实例化时被自动调用;

- 构造函数必须与类同名;

  • 构造函数没有返回值;

  • 构造函数可以有多个重载形式;

  • 实例化对象时仅用到一个构造函数;

  • 当用户没有定义构造函数时,编译器将自动生成一个构造函数。

下面我们来看看构造函数是如何定义的。

* *无参构造函数

  所谓无参构造函数就是构造函数没有参数,比如:

img

  在这个Student类中,我们可以看到构造函数Student()与类名相同,构造函数的前面没有任何的返回值,构造函数的内部,我们对数据成员进行了赋值操作(即给了数据成员一个初始值)。

有参构造函数

  所谓有参构造函数就是构造函数含有参数,比如:

img

  这个构造函数的作用,就是用户在实例化一个Student对象时,可以传进来一个name,传进来的name就可以给数据成员一个初始值,从而就初始化了这个数据成员。

  当然,构造函数是可以重载的,只需要遵循重载函数的规则就可以,比如:

img


构造函数代码实践

题目描述:

定义一个Teacher类,自定义无参构造函数,自定义有参构造函数;数据成员包含姓名和年龄;成员函数为数据成员的封装函数。

程序框架如下:

img

头文件(Teacher.h)

#include<iostream>
#include<string>
using namespace std;
class Teacher
{
public:
    Teacher(); //申明无参构造函数
    Teacher(string name, int age);//申明有参构造函数
    void setName(string name);
    string getName();
    void setAge(int age);
    int getAge();
private:
    string m_strName;    
    int m_iAge;
};

源文件:

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

using namespace std;

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

/*  定义一个Teacher类,具体要求如下:
/*   自定义无参构造函数
/*   自定义有参构造函数
/*   数据成员:
/*       名字
/*       年龄
/*   成员函数:
/*       数据成员的封装函数
/*  ************************************************************************/

Teacher::Teacher()
{
    m_strName = "Keiven";
    m_iAge = 20;
    cout<<"Teacher()"<<endl;
}
Teacher::Teacher(string name, int age)
{
    m_strName = name;
    m_iAge = age;
    cout<<"Teacher(string name, int age)"<<endl;
}

void Teacher::setName(string _name)
{
    m_strName = _name;
}
string Teacher::getName()
{
    return m_strName;
}

void Teacher::setAge(int _age)
{
    m_iAge = _age;
}
int Teacher::getAge()
{
    return m_iAge;
}

int main()
{
    Teacher t1;  //调用无参构造函数
    Teacher t2("Mery", 30); //调用有参构造函数

    //下面来看看这两个构造函数是不是完成了数据成员的初始化工作
    cout<< t1.getName() <<" "<< t1.getAge() <<endl;
    cout<< t2.getName() <<" "<< t2.getAge() <<endl;
    system("pause");
    return 0;
}

接下来要说另外一个问题,在我们的构造函数定义的时候,能不能给构造函数赋一个默认值呢?其实是可以的!!

  比如在上面的有参构造函数中,我们给年龄赋值40,如果后面我们不对年龄赋值的话,我们将使用年龄40这个默认值。

Teacher::Teacher(string name, int age = 40)

​ 然后实例化一个t3的对象,

Teacher t3("James"); //调用有参构造函数,只对名字赋值,不对年龄赋值

​ 最后,我们打印t3的相关内容

cout<< t3.getName() <<" "<< t3.getAge() <<endl;

img

从而得到结论:

​ 构造函数除了可以重载,还可以给参数赋默认值,但不能随意的赋默认值,有时会引起编译时不通过,原因是实例化对象时,编译器不知道调用哪一个构造函数了。


默认构造函数

问题:什么是默认构造函数呢?

其实对于大家来说,这仅仅是一个概念,实际内容在前面已经讲过了。为了说明默认构造函数这个概念,我们用一个例子来进行讲解。

img

  在main函数中,我们实例化了两个对象,一个是stu1,另一个是用p指向了内存中的一块空,从堆中实例化一个对象,并用指针p指向了它。无论是从栈中实例化对象还是从堆中实例化对象,都有一个共同的特点,即调用的构造函数都不用传参数。那么对于这样的调用形式,在定义构造函数的时候可以有不同的方式,比如像下面这样去定义,Student(){ }这样定义呢,构造函数本身就没有参数;当然,也可以这样去定义,Student(string name = “Keiven”)。在这两种情况下,实例化Student对象时,都不用给构造函数传递实参。我们把这种在实例化对象时,不需要传递参数的构造函数就称为默认构造函数。


构造函数初始化列表

  首先来看一个例子

img

在这个例子中,我们定义了一个学生Student的类,在这个类中,我们定义了两个数据成员(一个名字,一个年龄),名字和年龄都通过初始化列表(即红色标记部分)进行了初始化。写的时候需要注意,在构造函数后面需要用冒号隔开,对于多个数据成员进行初始化的时候,中间要用逗号隔开,赋值时要用括号进行赋值,而不能用等号进行赋值。

初始化列表特性

初始化列表先于构造函数执行—意味着编译器会献给初始化列表中的数据成员赋值,再执行构造函数中的相关代码。

初始化列表只能用于构造函数

初始化列表可以同时初始化多个数据成员

学习完初始化列表,大家肯定会有这样的疑问:C++大费周章地搞了一个初始化列表,而初始化列表的工作由构造函数完全可以代劳,最多也就稍微慢点,那初始化列表这个功能岂不是意义不大?

下面就通过一个例子来说明初始化列表的必要性。

img

在这个例子中,我们定义了一个圆Circle的类,在这个类中定义了一个pi值,因为pi是不变的,我们用const来修饰,从而pi就变成了一个常量。我们如果用构造函数来初始化这个常量(像上面那样),这样的话编译器就会报错,而且会告诉我们,因为pi是常量,不能再给它进行赋值。也就是说,我们用构造函数对pi进行赋值,就相当于第二次给pi赋值了。那如果我们想给这个pi赋值,并且又不导致语法错误,怎么办呢?唯一的办法就是通过初始化列表来实现(如下)。这个时候,编译器就可以正常工作了。

img


初始化列表代码实践

题目描述:

定义一个Teacher类,自定义有参默认构造函数,适用初始化列表初始化数据;数据成员包含姓名和年龄;成员函数为数据成员的封装函数;拓展:定义可以带最多学生的个数,此为常量。

程序框架如下:

img

头文件(Teacher.h)

#include<iostream>
#include<string>
using namespace std;
class Teacher
{
public:
    Teacher(string name = "Keiven", int age = 20, int m = 100);//申明有参默认构造函数,带有初始值
    void setName(string name);
    string getName();
    void setAge(int age);
    int getAge();
    int getMax();
private:
    string m_strName;    
    int m_iAge;
    const int m_iMax; //由于该数据成员是常量,只能通过初始化列表的方式进行初始化
};

源文件:

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

using namespace std;

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

/*  定义一个Teacher类,具体要求如下:
/*   自定义有参默认构造函数
/*   适用初始化列表初始化数据
/*   数据成员:
/*       名字
/*       年龄
/*   成员函数:
/*       数据成员的封装函数
/*   拓展:
/*       定义可以带最多学生的个数,此为常量
/*  ************************************************************************/


Teacher::Teacher(string name, int age, int m):m_strName(name), m_iAge(age), m_iMax(m) //初始化列表形式定义有参构造函数
{    
    cout<<"Teacher(string name, int age)"<<endl;
}

int Teacher:: getMax()
{
    return m_iMax;
}

void Teacher::setName(string _name)
{
    m_strName = _name;
}
string Teacher::getName()
{
    return m_strName;
}

void Teacher::setAge(int _age)
{
    m_iAge = _age;
}
int Teacher::getAge()
{
    return m_iAge;
}


int main()
{

    Teacher t1("Mery", 30, 150); //调用有参构造函数

    //下面来看看这两个构造函数是不是完成了数据成员的初始化工作
    cout<< t1.getName() <<" "<< t1.getAge() <<" "<< t1.getMax() <<endl;

    system("pause");
    return 0;
}

拷贝构造函数

引例:

img

  在这个例子中,我们定义了一个Student类,在类中又定义了一个默认构造函数,将字符串”Student”打印出来。然后在main函数中,首先实例化一个对象stu1(实例化过程中会调用默认构造函数),然后又实例化了两个对象stu2和stu3,并将stu1的值分别赋给stu2和stu3。但是当我们运行时,屏幕上只打印出一行字符串”Student”的字样,并没有像我们想象中那样应该会打印出三行字符串”Student”的字样,毕竟我们实例化了三个对象,理论上应该调用三次默认构造函数才对。这个时候,我们是不是有这样的疑问:实例化对象的时候不是一定能够调用默认构造函数的吗?现在怎么会出现这种问题呢?

  实际上,后面两次实例化对象确实也调用了构造函数,只不过不是调用的我们在这定义的默认构造函数,而是调用的是另一种特殊的构造函数,叫做拷贝构造函数

拷贝构造函数在定义的时候与普通构造函数基本相同,只是在参数上面有一些严格的要求。

下面通过一个例子来讲解如何定义拷贝构造函数。

img

  还是以Student这个类为例,首先我们已经定义了一个构造函数,与这个构造函数相对比下面红色标记的就是拷贝构造函数。拷贝构造函数在名称上与普通构造函数一样,但是在参数设计上却有所不同。首先要加一个const关键字,其次传入的是一个引用,这个引用还是一个与自己的数据类型完全相同(也就是说,也是一个Student的一个对象)的引用。通过这样的定义方式,我们就定义出了一个拷贝构造函数。如果我们将相应的代码写在拷贝构造函数的实现的部分,那么我们再采用上面两种实例化对象的方式,就会执行拷贝构造函数里面的相应代码。我们发现,在实例化stu2和stu3的时候,我们并没有去定义拷贝构造函数,但是仍然可以将这两个对象实例化出来。可见,拷贝构造函数与普通的构造函数一样。

  • 如果没有自定义拷贝构造函数,则系统会自动生成一个默认的拷贝构造函数。
  • 当采用直接初始化或复制初始化实例化对象时,系统自动调用拷贝构造函数。

构造函数小结:

  构造函数分为无参构造函数有参构造函数两大类。无参构造函数因为没有参数,那么我们可以确定所有的无参构造函数都是默认构造函数;有参构造函数又分为两种:参数带有默认值的有参构造函数,参数不带默认值的有参构造函数。对于参数带有默认值的有参构造函数来说,如果所有的参数都带有默认值,那么它就是一个默认构造函数

  我们在学习构造函数这一块知识的时候,我们会发现系统会自动生成一些函数,这些自动生成的函数又分为普通构造函数拷贝构造函数两大类。如果我们自定义了普通的构造函数,则系统就不会再自动生成普通构造函数,同理,如果我们定义了拷贝构造函数,那么系统也不会再生成拷贝构造函数。

  对于初始化列表,只能连接在普通构造函数或者拷贝构造函数的后面。

*  对于拷贝构造函数,由于参数是确定的,所以不能进行重载。*


析构函数

  如果说构造函数是对象来到世间的第一首我难过呼吸,那么析构函数就是对象离开世间的临终的遗言。

  析构函数在对象销毁时会被自动调用,完成的任务是归还系统的资源,收拾最后的残局。

析构函数的定义

img

思考:析构函数有存在的必要吗?

下面看一个经典的例子来说明析构函数的必要性

img

  还是以Student这个学生的类为例,在它的数据成员中,我们不用string类型来定义姓名,我们改用指针,并且在其构造函数中,让这个指针指向堆中分配的一段内存,那么在这个对象销毁的时候,就必须释放掉这段内存,否则就会造成内存泄漏。要释放内存,最好的时机就是对象被销毁之前,如果销毁早了的话,其他的程序用到这些资源的时候就会报错。可见,设计一个在对象销毁之前被自动调用的函数就非常有必要了,那这个函数就是析构函数。

*  析构函数唯一的功能就是释放资源,其没有参数,所以析构函数就不能重载*

析构函数的特性

  • 如果没有自定义析构函数,那么系统会自动生成一个析构函数。(这一点与构造函数和拷贝构造函数类似)
  • 析构函数在对象销毁时被自动调用(与其相对的构造函数,则是在对象实例化时被自动调用)
  • 析构函数没有返回值,也没有参数,也就不能重载

对象的生命历程

img


析构函数代码实践

题目描述:

  定义一个Teacher类,自定义析构函数;对于普通方式实例化的对象,在销毁对象时是否自动调用析构函数;通过拷贝构造函数实例化的对象,在销毁对象时是否自动调用析构函数;数据成员包含姓名和年龄;成员函数为数据成员的封装函数。

程序框架如下:

img

头文件(Teacher.h)

#include<iostream>
#include<string>
using namespace std;
class Teacher
{
public:
    Teacher(string name = "Keiven", int age = 20);//申明有参默认构造函数,带有初始值
    Teacher(const Teacher &tea); //申明拷贝构造函数
    ~Teacher();
    void setName(string name);
    string getName();
    void setAge(int age);
    int getAge();
    int getMax();
private:
    string m_strName;    
    int m_iAge;
};

源文件:

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

using namespace std;

/*  ************************************************************************/
/*  定义一个Teacher类,具体要求如下:
/*   1、自定义析构函数
/*   2、对于普通方式实例化的对象,在销毁对象时是否自动调用析构函数
/*   3、通过拷贝构造函数实例化的对象,在销毁对象时是否自动调用析构函数
/*   数据成员:
/*       名字
/*       年龄
/*   成员函数:
/*       数据成员的封装函数
/*  ************************************************************************/


Teacher::Teacher(string name, int age):m_strName(name), m_iAge(age) //初始化列表形式定义有参构造函数
{    
    cout<<"Teacher(string name, int age)"<<endl;
}
Teacher::Teacher(const Teacher &tea)
{
    cout<<"Teacher(const Teacher &tea)"<<endl;
}
Teacher::~Teacher()
{
    cout<<"~Teacher()"<<endl;
}


void Teacher::setName(string _name)
{
    m_strName = _name;
}
string Teacher::getName()
{
    return m_strName;
}

void Teacher::setAge(int _age)
{
    m_iAge = _age;
}
int Teacher::getAge()
{
    return m_iAge;
}


int main()
{

    Teacher t1; //在栈上实例化一个对象
    Teacher *p = newTeacher(); //在堆上实例化一个对象
    delete p;

    system("pause");
    return 0;
}

运行结果:

img

我们在按任意键结束后,会有调用的析构函数过程一闪而过(这就是普通构造函数实例化对象销毁时也自动调用了析构函数)

下面通过拷贝构造函数来实例化对象:

int main()
{

    Teacher t1; //在栈上实例化一个对象
    Teacher t2(t1); //通过拷贝构造函数实例化对象
    system("pause");
    return 0;
}

运行结果:

img

我们在按任意键结束后,会有调用的析构函数过程一闪而过(这就是普通构造函数实例化对象和通过拷贝构造函数实例化对象销毁时也自动调用了析构函数)。

3.6 练习题


4. C++封装下3h

4.1 对象成员于数组对象

对象数组

  前面课程我们已经学会了如何实例化一个对象,只有实例化对象后,才能通过这个对象去访问对象的数据成员和成员函数。但是在很多场合下,一个对象是远远不够用的,往往需要一组对象。比如,我们想表示一个班级的学生,并且假设这个班级有50个学生。果我们还是像以前一样,简单的使用对象的实例化的话,就需要定义50个变量来表示这50个学生,显然这样做是很麻烦很愚蠢的。这时,我们就需要通过一个数组来表达这一个班的学生。还有,如果我们去定义一个坐标,那么这一个坐标只能代表一个点,但是,如果我们想去定义一个矩形的话,就需要定义4个点,然后这4个点的连线形成一个矩形,那么这4个点也可以定义成一个数组。说到这里,想必大家应该知道,今天的重点就是对象数组。

  接下来我们看下面这个例子。

img img

  在这里我们定义了一个坐标类(Coordinate),并且定义了其两个数据成员(一个表示横坐标,一个表示纵坐标)。我们在使用的过程中,首先是在栈中实例化了一个对象数组,每个数组元素就是一个坐标的对象,并且均可以访问对象的数据成员(如上,我们给对象数组的第2个元素的横坐标赋值为10);其次我们又在堆上实例化了一个对象数组,同样,每个数组元素均可以访问对象的数据成员(如上,我们给对象数组的第1个元素的纵坐标赋值为20)。记住,在堆上实例化对象数组后,使用完毕,需要将申请的内存释放掉(用delete []p),最后还要赋值为空(NULL)。

  接下来看看在内存中是如何存储的(如下)。

img


对象数组代码实践

题目描述:

定义一个坐标(Coordinate)类,其数据成员包含横坐标和纵坐标,分别从栈和堆中实例化长度为3的对象数组,给数组中的元素分别赋值,最后遍历两个数组。

程序框架如下:

img

头文件(Coordinate.h)

class Coordinate
{
public:
    Coordinate();
    ~Coordinate ();
public:
    int m_iX;
    int m_iY;
};

源程序:

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

#include"Coordinate.h"

using namespace std;

/* 对象数组
/* 要求
      1. 定义Coordiante类
      2. 数据成员:m_iX、m_iY
      3. 分别从栈和堆中实例化长度为3的对象数组
      4. 给数组中的元素分别赋值
      5. 遍历两个数组
/* *****************************************/


Coordinate::Coordinate()
{
    cout <<"Coordinate()"<<endl;
}

Coordinate::~Coordinate ()
{
    cout <<"~Coordinate()"<< endl;
}


int main()
{
    Coordinate coor[3]; //从栈上实例化对象数组
    coor[0].m_iX =3;
    coor[0].m_iY =5;


    Coordinate *p =new Coordinate[3];
    p->m_iX = 7; //直接写p的话,就说明是第一个元素
    p[0].m_iY =9; //等价于 p->m_iY = 9

    p++; //将指针后移一个位置,指向第2个元素
    p->m_iX = 11;
    p[0].m_iY = 13; //这里p指向的是第二个元素,p[0]就是当前元素,等价于p->m_iY = 13

    p[1].m_iX = 15;//第3个元素的横坐标
    p++; 将指针后移一个位置,指向第3个元素
    p[0].m_iY = 17;//这里p指向的是第三个元素,p[0]就是当前元素,等价于p->m_iY = 17


    for(int i = 0; i < 3; i++)
    {
        cout <<"coor_X: "<< coor[i].m_iX <<endl;
        cout <<"coor_Y: "<< coor[i].m_iY <<endl;
    }
    for(int j = 0; j < 3; j++)
    {
        //如果上面p没有经过++操作,就可以按下面来轮询
        //cout <<"p_X: " << p[i].m_iX <<endl;
        //cout <<"p_Y: " << p[i].m_iY <<endl;
        //但是,上面我们对p做了两次++操作,实际p已经指向了第3个元素,应如下操作
        cout <<"p_X: "<< p->m_iX <<endl;
        cout <<"p_Y: "<< p->m_iY <<endl;
        p--;
    }

    //经过了三次循环后,p指向了一个非法内存,不能直接就delete,而应该让p再指向我们申请的一个元素的,如下
    p++; //这样p就指向了我们申请的内存
    delete []p;
    p = NULL;


    system("pause");
    return 0;
}

运行结果:

img

  从运行结果来看,首先看到的是打印出六行“Coordinatre()”,这是因为分别从栈实例化了长度为3的对象数组和从堆实例化了长度为3的对象数组,每实例化一个对象就要调用一次默认构造函数。

​ 最后只打印出三行“~Coordinate()”,那是不是只是从堆上实例化的对象销毁时调用了析构函数,而从栈实例化的对象销毁时,没有调用析构函数呢?

​ 非也,从栈实例化的对象在销毁时,系统自动回收内存,即自动调用析构函数,只是当我们按照提示“请按任意键结束”,按下任何键后,屏幕会闪一下,就在这闪的过程中,会出现三行“~Coordinate()”的字样,只是我们不容易看到而已。


对象成员

  前面我们讲到的类都是比较简单的,它们共同的特点是,其数据成员都是基本的数据类型,但是在现实的生活中,问题要远比这个复杂的多。比如,之前我们编写过汽车的类,但是当时我们只申明了汽车轮子的个数,如果要解决实际的问题,显然这是不够的。起码轮子本身就是一个对象,汽车上还有沙发座椅,还有发动机等等。再比如,我们如果要定义一个房子的类,房子对象当中,应该有各种各样的家俱,还有漂亮的灯饰等等,而这些家俱和灯饰其实也是一个个对象。可见,在对象当中包含着其他对象是一种非常常见的现象,接下来我们就学习一下对象成员。

  为了说明对象成员,我们以坐标系中的一段线段为例,以此来说明对象成员的定义和使用方法。

img

  上面是一个直角坐标系,在这个坐标系中,我们定义了一条线段AB,起点A的坐标为(2, 1),终点B的坐标为(6, 4)。如果我们要定义像这样的一个线段的类,那么每条线段都有两个点连接而成,这意味着我们需要定义一个表示点的类,这个类包含横坐标和纵坐标,而且一个线段中应该包含两个坐标的对象。可见,要描述这个问题,我们至少要定义两个类,一个来定义坐标的点,一个来定义坐标系中的线段。

  先来定义坐标点的类,如下:

img

  在这个类中有两个数据成员,分别表示点的横坐标和纵坐标,另外还包含一个它的构造函数。

  接着看一下线段类的定义,如下:

img

在这个线段的类中,有两个数据成员,这两个数据成员都是点(一个是起点,一个是终点),而且这两个点必须是坐标类型的,另外,我们也定义了它的构造函数。

定义完点的类和线段的类后,我们就可以通过实例化来描述一条线段了,如下:

img

这里大家可能会有这样一个疑问:在这种对象作为数据成员的情况下,当实例化线段(Line)时,到底是先实例化线段还是先实例化作为对象成员的坐标点的对象呢?而当我们去delete p的时候,也就是说,当线段被销毁时,是先销毁点对象还是先销毁线段对象呢?

结论:

  当我们实例化Line对象时,先实例化点A对象,再实例化点B对象,最后实例化Line这个对象。而销毁时,则与创建时相反,先销毁Line这个对象,然后销毁点B这个对象,最后销毁点A这个对象。

上面我们讲的对象作为数据成员时,构造函数都是没有参数的。然而作为一条线段,它的两个点在实例化时,其实是应该可以由调用者来确定的,也就是说,这两个坐标点在Line这个对象实例化的时候,是能够通过给它的构造函数传递参数,从而可以使这两点生成在确定的位置上。也就是说,坐标类的构造函数应该有参数,即如下所示:

img

从而,这就需要线段(Line)的类,它的构造函数也需要有参数,而这些参数未来可以传值给它的数据成员,即如下所示:

img

如果我们在实例化线段时,仅仅如下所示肯定会是出错的。

img

因此,我们需要将代码做进一步改进,即配备初始化列表。在初始化列表中,我们要实例化m_coorA和m_coorB,并且将Line所传入的这四个参数分配到这两个对象成员中去。

img

当做完这些工作之后,我们就可以在主调函数中像之前那样去实例化新的对象了,并且2和1必然会传值给第一个坐标点对象,6和4必然会传值给第二个坐标点对象。


对象成员代码实践

题目描述:

/* 对象成员

/* 具体要求:

定义两个类:

​ 坐标类:Coordinate

​ 数据成员:横坐标m_iX,纵坐标m_iY

​ 成员函数:构造函数、析构函数,数据成员的封装函数

​ 线段类:Line

​ 数据成员:点A m_coorA,点B m_coorB

​ 成员函数:构造函数,析构函数,数据成员的封装函数,信息打印函数

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

程序框架如下:

img

头文件(Coordinate.h)

class Coordinate
{
public:
    Coordinate();
    ~Coordinate();
    void setX(int x);
    int getX();
    void setY(int y);
    int getY();
private:
    int m_iX;
    int m_iY;
};

源程序(Coordinate.cpp)

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

Coordinate::Coordinate ()
{
    cout <<"Coordinate()"<<endl;
}

Coordinate::~Coordinate ()
{
    cout <<"~Coordinate()"<<endl;
}
void Coordinate::setX(int x)
{
    m_iX = x;
}
int Coordinate::getX()
{
    return m_iX;
}
void Coordinate::setY(int y)
{
    m_iY = y;
}
int Coordinate::getY()
{
    return m_iY;
}

头文件(Line.h)

#include "Coordinate.h"

class Line
{
public:
    Line();
    ~Line();
    void setA(int x, int y);
    void setB(int x, int y);
    void printInfo();
private:
    Coordinate m_coorA;
    Coordinate m_coorB;
};

源程序(Line.cpp)

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

using namespace std;

Line::Line()
{
    cout <<"Line()"<< endl;
}
Line::~Line()
{
    cout <<"~Line()"<< endl;
}
void Line::setA(int x, int y)
{
    m_coorA.setX(x);
    m_coorA.setY(y);    

}
void Line::setB(int x, int y)
{
    m_coorB.setX(x);
    m_coorB.setY(y);

}
void Line::printInfo()
{
    cout << "(" << m_coorA.getX() <<","<< m_coorA.getY()<< ")" <<endl;
    cout << "(" << m_coorB.getX() <<","<< m_coorB.getY()<< ")" <<endl;
}

主调程序(demo.cpp)

//我们首先来实例化一个线段类的对象,如下
#include <iostream>
#include Line.h"

using namespace std;

int main()
{
    Line *p = new Line();
    delete p;
    p = NULL;
    system("pause");
    return 0;
}

运行结果如下:

img

   从运行结果来看,先连续调用了两次坐标类的构造函数,再调用了一次线段类的构造函数,这就意味着先创建了两个坐标类的对象,这两个坐标类的对象就是A点和B点,然后才调用线段这个对象,线段这个对象是在A点和B点初始化完成之后才被创建。而在销毁时,先调用的是线段类的西沟函数,然后连续调用两次坐标类的析构函数。可见,对象成员的创建与销毁的过程正好相反,也验证了我们之前给出的结论。

​ 作为一条线段来说,我们非常希望的是,在这条线段创建的时候就已经将线段的起点和终点确定下来。为了达到这个目的,我们往往希望线段这个类的构造函数是带有参数的,并且这个参数将来能够传给这两个点,所以接下来我们将进一步完善这个程序。

完善头文件(Coordinate.h)

class Coordinate
{
public:
    Coordinate(int x,int y);
    ~Coordinate();
    void setX(int x);
    int getX();
    void setY(int y);
    int getY();
private:
    int m_iX;
    int m_iY;
};

完善源程序(Coordinate.cpp)

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

Coordinate::Coordinate(int x, int y)
{
    m_iX = x;
    m_iY = y;
    cout <<"Coordinate()"<< m_iX <<","<< m_iY <<endl;
}

Coordinate::~Coordinate ()
{
    cout <<"~Coordinate()"<< m_iX <<","<< m_iY <<endl;
}
void Coordinate::setX(int x)
{
    m_iX = x;
}
int Coordinate::getX()
{
    return m_iX;
}
void Coordinate::setY(int y)
{
    m_iY = y;
}
int Coordinate::getY()
{
    return m_iY;
}

完善头文件(Line.h)

#include"Coordinate.h"
class Line
{
public:
    Line(int x1, int y1, int x2, int y2);
    ~Line();
    void setA(int x, int y);
    void setB(int x, int y);
    void printInfo();
private:
    Coordinate m_coorA;
    Coordinate m_coorB;
};

完善源程序(Line.cpp)

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

Line::Line(int x1, int y1, int x2, int y2):m_coorA(x1, y1), m_coorB(x2, y2)
{
    cout <<"Line()"<< endl;
}
Line::~Line()
{
    cout <<"~Line()"<< endl;
}
void Line::setA(int x, int y)
{
    m_coorA.setX(x);
    m_coorA.setY(y);    

}
void Line::setB(int x, int y)
{
    m_coorB.setX(x);
    m_coorB.setY(y);

}
void Line::printInfo()
{
    cout <<"("<<m_coorA.getX() <<","<< m_coorA.getY()<<")"<<endl;
    cout <<"("<<m_coorB.getX() <<","<< m_coorB.getY()<<")"<<endl;
}

完善主调程序(demo.cpp)

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

using namespace std;

int main()
{
    Line *p = new Line(1,2,3,4);
    delete p;
    p = NULL;
    system("pause");
    return 0;
}

运行结果:

img

从这个结果来看,我们更能清晰的看到,在实例化对象时,先实例化点A,再实例化点B,最后实例化线段;在销毁对象时,先销毁线段,再销毁点B,最后销毁点A。

最后,我们来看一看,通过这样的值传递,能否正确的打印出来,在主调函数中增加一行代码来调用线段的信息打印函数,如下红色标记:

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

using namespace std;

int main()
{
    Line *p = new Line(1,2,3,4);
    p->printInfo();
    delete p;
    p = NULL;
    system("pause");
    return 0;
}

运行结果:

img

在此结果中,我们看到了A点坐标和B点坐标,即符合信息打印。


4.2 深拷贝与浅拷贝

深拷贝与浅拷贝

  前面我们已经学习了拷贝构造函数,但是我们只是学习了拷贝构造函数的声明方法以及何时被自动调用,但是我们还未学习如何来实现拷贝构造函数。这是因为对象的拷贝并没有想象中那么简单,大致分为两种情况:深拷贝浅拷贝。我们先来看下面这个例子的实现过程:

img

   在这个例子中,我们定义了一个数组的类(Array),在这个类中,定义了一个数据成员(m_iCount),并且定义了构造函数,在其中对数据成员赋了初值5,另外还定义了一个拷贝构造函数。在这个拷贝构造函数是这样实现的,传入的参数是arr,这个参数的数据类型也是Array,所以其肯定也含有数据成员m_iCount,在这个拷贝构造函数中,我们将arr的数据成员m_iCount赋值给本身的m_icount。当我们使用时,先用Array arr1来实例化一个arr1的时候,就会调用到arr1的构造函数,也就是说将arr1中的数据成员m_icount赋了初值5。而我们使用Array arr2 = arr1的时候,也就是用arr1去初始化arr2,这时实例化arr2的时候就会调用到它的拷贝构造函数,拷贝构造函数中的参数arr其实就是arr1,里面代码实现的时候,就相当于将arr1的数据成员m_icount赋值给arr2的数据成员m_icount。

​ 上面这个例子比较简单,下面将这个例子稍微做点修改,如下:

img

   在这个例子中,我们新加了一个数据成员,它是int型的指针m_pArr,其在构造函数中,从堆中申请了一段内存,并且指向了申请的这段内存,内存的大小就是m_icount。而拷贝构造函数中,我们将arr的数据成员m_iCount赋值给本身的m_icount,同时将arr的数据成员m_pArr赋值给本身的m_pArr。当我们使用时,先用Array arr1来实例化一个arr1的时候,就会调用到arr1的构造函数,也就是说将arr1中的数据成员m_icount赋了初值5。而我们使用Array arr2 = arr1的时候,也就是用arr1去初始化arr2,这时实例化arr2的时候就会调用到它的拷贝构造函数,于是就将arr1的数据成员m_icount赋值给arr2的数据成员m_icount,将arr1的数据成员m_pArr赋值给arr2的数据成员m_pArr。

   在这两个例子中,有共同的特点,那就是,只是将数据成员的值作了简单的拷贝,我们就把这种拷贝模式称为浅拷贝。但是对于第一个例子来说,使用浅拷贝的方式来实现拷贝构造函数并没有任何问题,而对于第二个例子来说,肯定是有问题的。我们来思考一下,经过浅拷贝之后,对象arr1中的指针和对象arr2中的指针势必会指向同一块内存(因为我们将arr1的数据成员m_pArr赋值给arr2的数据成员m_pArr),这里假设指向的地址是0x00FF00(如下图所示)。

img

   在这个时候,如果我们先给arr1的m_pArr赋了一些值,也就是说在这段内存中就写了一些值,然后我们再给arr1的m_pArr去赋值的时候,这段内存就会被重写,而覆盖掉了之前给arr1的m_pArr所赋的一些值。这一点还不是最严重的问题,更严重的问题是,当我们去销毁arr1这个对象的时候,我们为了避免内存泄漏,肯定会释放掉m_pArr所指向的这段内存。如果我们已经释放掉了这段内存,我们再去销毁arr2这个对象时,我们肯定也会以同样的方式去释放掉arr2中m_pArr这个指针所指向的这段内存,那么就相当于,同一块内存被释放了两次,那么这种问题肯定是有问题的。面对这种问题,计算机会以崩溃的方式来向你抗议。

   所以我们希望拷贝构造函数所完成的工作是这样的,两个对象的指针所指向的应该是两个不同的内存,拷贝的时候不是将指针的地址简单的拷贝过来,而是将指针所指向的内存当中的每一个元素依次的拷贝过来,这才是我们真正想要的。(如下图所示)

img

如何想要实现这样一个效果呢?我们需要将代码再做适当修改,如下:

img

   这段代码与之前的代码的区别在于其拷贝构造函数,其中的m_pArr不是直接赋值arr中的m_pArr,而是先分配一段内存(这段内存分配成功与否,这里没有判断,因为这个不是这里要将的重点),重点是下面的一段for循环语句。我们应该将arr中的m_pArr的每一个元素都拷贝到当前的m_pArr所指向的相应的内存当中去。这样的拷贝方式与之前所讲到的拷贝方式是有本质区别的。

   我们来总结一下,当进行对象拷贝时,不是简单的做值的拷贝,而是将堆中内存的数据也进行了拷贝,那么就称这种拷贝模式为深拷贝


深浅拷贝代码实践

题目描述:

/* 示例要求

\1. 定义一个Array类。

数据成员:m_iCount

成员函数:

构造函数、拷贝构造函数,析构函数

​ 数据成员的封装函数

​ 要求通过这个例子体会浅拷贝原理

\2. 在1的基础上增加一个数据成员:m_pArr

并增加m_pArr地址查看函数

同时改造构造函数、拷贝构造函数和析构函数

要求通过这个例子体会深拷贝的原理和必要性

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

针对第*1*个要求:

头文件(*Array.h*

class Array
{
public:
    Array();
    Array(const Array &arr);
    ~Array();
    void setCount(int count);
    int getCount();
private:
    int m_iCount;
};

源程序(*Array.cpp*

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

Array::Array()
{
    cout <<"Array()"<< endl;
}
Array::Array(const Array &arr)
{
    m_iCount = arr.m_iCount;
    cout <<"Array(const Array &arr)"<<endl;
}
Array::~Array()
{
    cout <<"~Array()"<< endl;
}
void Array::setCount(int count)
{
    m_iCount = count;
}
int Array::getCount()
{
    return m_iCount;
}

主调函数(*demo.h*

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

using namespace std;

int main()    
{
    Array arr1;
    arr1.setCount(5);

    Array arr2(arr1); //通过Arr1来实例化arr2

    cout <<"arr2.m_iCount"<<" "<< arr2.getCount() << endl;

    system("pause");
    return 0;
}

运行结果:

img

从运行结果看,第一行打印出的是构造函数,也就是说arr1实例化的时候调用的是构造函数;第二行打印出的是拷贝构造函数,也就是说arr2实例化的时候调用的是拷贝构造函数;第三行打印出的是arr2中m_iCount的值为5,这就说明我们用arr1去实例化arr2的时候,也将arr1中的m_iCount的值给了arr2中的m_iCount。这就是浅拷贝,其原理就是将值直接拷贝过去。但是浅拷贝有的时候会带来一些问题,下面我们来看都带来哪些问题,以及如何解决这些问题。

针对第*2*个要求:

修改头文件(*Array.h*

class Array
{
public:
    Array(int count);
    Array(const Array &arr);
    ~Array();
    void setCount(int count);
    int getCount();
    void printAddr(); //新增查看地址函数
private:
    int m_iCount;
    int *m_pArr;    //新增数据成员:m_pArr    
};

修改源程序(*Array.cpp*

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

Array::Array(int count)
{
    m_iCount = count;
    m_pArr = new int[m_iCount];
    cout <<"Array()"<< endl;
}
Array::Array(const Array &arr)
{
    m_iCount = arr.m_iCount;
    m_pArr = arr.m_pArr;//这里先用浅拷贝实现方式来看看会有什么后果?
    cout <<"Array(const Array &arr)"<<endl;
}
Array::~Array()
{
    delete []m_pArr;
    m_pArr = NULL;
    cout <<"~Array()"<< endl;
}
void Array::setCount(int count)
{
    m_iCount = count;
}
int Array::getCount()
{
    return m_iCount;
}
void Array::printAddr()
{
    cout <<"m_pArr的值是:"<< m_pArr << endl;
}

修改主调函数(*demo.h*

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

using namespace std;

int main()    
{
    Array arr1(5);

    Array arr2(arr1); //通过Arr1来实例化arr2

    cout<<"arr1中" ;
    arr1.printAddr(); 
    cout<<"arr2中" ;
    arr2.printAddr();

    system("pause");
    return 0;
}

运行结果:

img

从运行结果来看,我们发现arr1中的m_pArr的值与arr2中的m_pArr的值是一样的,也就是说arr1中的m_pArr与arr2中的m_pArr都指向了同一块内存。此前在析构函数中,我们做了删除工作(delete []m_pArr),这就意味着arr1会删除一次,arr2也会删除一次(因为它们指向的是同一块内存,所以相当于让同一块内存释放了两次),这就一定会造成运行时错误。而这个运行时错误不会再这个时候出现,这是因为我们加了一行(system(“pause”);)代码,在这行代码执行完成后,就会执行相应的析构函数,这个时候就会出现运行时错误,我们来验证一下,即在键盘上敲任意键后,屏幕显示如下:

img

此时,我们发现程序已经死在这了。我们可以看到程序执行了一遍析构函数(因为打印出了析构函数字样“~Array()”),而第二遍析沟函数未执行出来,这就意味着第二次执行析构函数的时候出现了错误。

那么如何来解决这样的问题呢?这时就必须使用深拷贝来解决这个问题了。我们可以看到,此前我们使用的是浅拷贝,直接赋值的方式来进行拷贝的(m_iCount = arr.m_iCount;m_pArr = arr.m_pArr;//浅拷贝实现方式)。深拷贝的方式则需要在拷贝构造函数中给当前的这个指针先分配一段内存,然后将传入的对象的对应位置的内存拷贝到新申请的这段内存中区。那么我们来修改一下构造函数和拷贝构造函数如下:

Array::Array(int count)
{
    m_iCount = count;
    m_pArr = new int[m_iCount];
    for(int i =0; i < m_iCount; i++)
    {
        m_pArr[i] = i;
    }
    cout <<"Array()"<< endl;
}
Array::Array(const Array &arr)
{
    m_iCount = arr.m_iCount;
    m_pArr = new int[m_iCount];
    for(int i = 0; i < m_iCount; i++)
    {
        m_pArr[i] = arr.m_pArr[i];
    }
    cout <<"Array(const Array &arr)"<< endl;
}

接着还是用刚刚的主调函数来查看arr1和arr2中m_pArr所指向的地址是不是还是一样?运行结果如下:

img

从结果我们可以看到,这个时候arr1和arr2中m_pArr的地址已经不相同了,可见它们指向了不同的内存,并且当我们按了任意键后,程序也没有崩溃掉,这是因为arr1和arr2中m_pArr所指向的内存不一样了,所以在调用各自析构函数的时候所释放掉内存位置也不相同,所以能够正常释放掉,也就不会报错或崩溃了。

接着我们再来申明一个函数,通过这个函数将之前我们赋的值都打印出来。整个程序如下:

程序框架:

img

头文件(*Array.h*

class Array
{
public:
    Array(int count);
    Array(const Array &arr);
    ~Array();
    void setCount(int count);
    int getCount();
    void printAddr(); //新增查看地址函数
    void printArr(); //新增打印函数
private:
    int m_iCount;
    int *m_pArr;    //新增数据成员:m_pArr    
};

源程序(*Array.cpp*

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

Array::Array(int count)
{
    m_iCount = count;
    m_pArr = new int[m_iCount];
    for(int i =0; i < m_iCount; i++)
    {
        m_pArr[i] = i;
    }
    cout <<"Array()"<< endl;
}
Array::Array(const Array &arr)
{
    m_iCount = arr.m_iCount;
    m_pArr = new int[m_iCount];
    for(int i = 0; i < m_iCount; i++)
    {
        m_pArr[i] = arr.m_pArr[i];
    }
    cout <<"Array(const Array &arr)"<< endl;
}
Array::~Array()
{
    delete []m_pArr;
    m_pArr = NULL;
    cout <<"~Array()"<< endl;
}
void Array::setCount(int count)
{
    m_iCount = count;
}
int Array::getCount()
{
    return m_iCount;
}
voidArray::printAddr()
{
    cout <<"m_pArr的值是:"<< m_pArr << endl;
}
void Array::printArr()
{
    for(int i = 0; i < m_iCount; i++)
    {
        cout << m_pArr[i] << endl;
    }
}

主调程序(*demo.cpp*

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

using namespace std;

int main()    
{
    Array arr1(5);

    Array arr2(arr1); //通过Arr1来实例化arr2

    cout<<"arr1中" ; 
    arr1.printAddr(); 
    cout<<"arr2中" ;
    arr2.printAddr();

    cout<<"arr1中m_pArr中的值为"<< endl;; 
    arr1.printArr(); 
    cout<<"arr2中m_pArr中的值为"<< endl;;
    arr2.printArr();

    system("pause");
    return 0;
}

运行结果:

img

当我们按下任意键后,仔细看屏幕的话,最后会打印出两行“Array(const Array &arr)”,这是因为最后对象销毁时,arr1和arr2调用了各自的析构函数。


4.3 对象指针

对象指针

所谓对象指针,顾名思义就是有一个指针,其指向一个对象,下面通过一个例子来说明这样一个问题。

img

在这个例子中,我们定义了一个坐标的类(Coordinate),其有两个数据成员(一个表示横坐标,一个表示纵坐标)。当我们定义了这个类之后,我们就可以去实例化它了。如果我们想在堆中去实例化这个对象呢,就要如下所示:

img

通过new运算符实例化一个对象后(这个对象就会执行它的构造函数),而对象指针p就会指向这个对象。我们的重点是要说明p与这个对象在内存中的相关位置以及它们之间的对应关系。

当我们通过这样的方式实例化一个对象后,它的本质就是在内存中分配出一块空间,在这块空间中存储了横坐标(m_iX)和纵坐标(m_iY),此时m_iX的地址与p所保存的地址应该是一致的,也就是说p所指向的就是这个对象的第一个元素(m_iX)。如果想用p去访问这个元素,很简单,就可以这样来访问(p -> m_iX或者p -> m_iY),也可以在p前加上*,使这个指针变成一个对象,然后通过点号(.)来访问相关的数据成员(如(*p).m_iY)。接下来看一下如下的具体范例。

img

注意:这里的new运算符可以自动调用对象的构造函数,而C语言中的malloc则只是单纯的分配内存而不会自动调用构造函数。


对象指针代码实践

题目描述:

/* 示例要求

定义Coordinate类

​ 数据成员:m_iX和m_iY

​ 声明对象指针,并通过指针操控对象

​ 计算两个点,横、纵坐标的和

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

头文件(Coordinate.h)

class Coordinate
{
public:
    Coordinate();
    ~Coordinate();
public:
    int m_iX;
    int m_iY;
};

源程序(*Coordinate.cpp*

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

using namespace std;

Coordinate::Coordinate()
{
    cout <<"Coordinate()"<< endl;
}
Coordinate::~Coordinate()
{
    cout <<"~Coordinate()"<< endl;
}

主调程序(*demo.cpp*

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

#include<stdlib.h>

using namespace std;

int main()
{
    /* 使用两种方法定义对象指针 */
    Coordinate *p1 = NULL;//定义一个对象指针
    p1 = new Coordinate; //让p1指向一段内存,这里也可以写成p1 = new Coordinate(),因为其默认构造函数没有参数
    Coordinate *p2 = new Coordinate();

    /* 使用两种方法让对象指针访问数据成员 */
    p1->m_iX = 10;
    p1->m_iY = 20;
    (*p2).m_iX = 30;
    (*p2).m_iY = 40;
    cout << p1->m_iX +(*p2).m_iX << endl;
    cout << p1->m_iY +(*p2).m_iY << endl;
    delete p1;
    p1 = NULL;
    delete p2;
    p2 = NULL;
    system("pause");
    return 0;
}

运行结果:

img

此外,作为对象指针来说,还可以指向栈中的一块地址,怎么来做呢?我们来修改一下主调程序如下:

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

using namespace std;

int main()
{
    ///* 使用两种方法定义对象指针 */
    //Coordinate *p1 = NULL;//定义一个对象指针
    //p1 = new Coordinate; //让p1指向一段内存,这里也可以写成p1 = new Coordinate(),因为其默认构造函数没有参数
    //Coordinate *p2 = new Coordinate();
    //
    ///* 使用两种方法让对象指针访问数据成员 */
    //p1->m_iX = 10;
    //p1->m_iY = 20;
    //(*p2).m_iX = 30;
    //(*p2).m_iY = 40;
    //cout << p1->m_iX +(*p2).m_iX << endl;
    //cout << p1->m_iY +(*p2).m_iY << endl;
    //delete p1;
    //p1 = NULL;
    //delete p2;
    //p2 = NULL;

    Coordinate p1; //从栈中实例化一个对象p1
    Coordinate *p2 = &p1; //让对象指针p2指向p1
    p2->m_iX = 10;
    p2->m_iY = 20;

    //这里我们来打印p1的横坐标和纵坐标,来说明是对象指针p2操纵了对象p1
    cout <<"对象p1这个点的坐标是:("<< p1.m_iX <<","<< p1.m_iY <<")"<< endl; 

    system("pause");
    return 0;
}

img


对象成员指针

对象成员指针是什么呢?那么我们来想一想,之前我们学习过对象成员。对象成员,就是作为一个对象来说,它成为了另外一个类的数据成员。而对象成员指针呢,则是对象的指针成为了另外一个类的数据成员了。

我们先来回顾一个熟悉的例子,如下:

img

左边呢,我们定义了一个点的坐标类,它的数据成员有点的横坐标和纵坐标;右边呢,我们定义了一个线段类,在这个线段类中,需要有两个点(一个起点和一个终点),我们用点A和点B来表示,我们当时用的是坐标类的对象,分别是m_coorA和m_coorB。现在呢,我们要把它们变成指针,如下:

img

初始化的时候呢,与对象成员初始化的方法可以是一样的,使用初始化列表来初始化,只不过现在是指针了,所以我们赋初值NULL。

img

除了可以使用初始化列表进行初始化以外,还可以使用普通的初始化,比如说,在构造函数中,写成如下方式:

img

当然,更多的是下面的情况,因为我们这是两个指针,一定要指向某一个对象,才能够进行操作,才会有意义。而它指向的就应该是两个点的坐标对象:

img

在这里面,指针m_pCoorA指向了一个坐标对象(1,3),m_pCoorB指向了另外一个坐标对象(5,6)。那么,这就相当于在构造函数当中,我们从堆中分配了内存。既然在构造函数当中从堆中分配了内存,那么我们就需要在析构函数中去把这个内存释放掉,这样才能够保证内存不被泄漏。

此外呢,作为对象成员和对象成员指针还有另外一个很大的不同。作为对象成员来说,如果我们使用sizeof这个对象的话,它就应该是里面所有对象的体积的总和(如下图所示)

img

  而对象成员指针则不同,我们来看一看刚刚对象成员指针我们定义的时候是如何定义的。我们可以看到,我们定义的时候呢,是写了两个指针作为它的对象成员。而我们知道,一个指针在32位的编译器下面,它只占4个基本内存单元,那么两个指针呢,则占8个基本内存单元,而我们前面所讲到的Coordinate类呢,它有两个数据成员,这两个数据成员都是int型的,所以呢,每一个数据成员都应该占4个基本的内存单元。那么这样算下来呢,我们来想一想,如果我们使用sizeof来判断一个line这样的对象,到底有多大呢?如果在line这个对象中定义的是对象成员(即两个Coordinate),那么这两个Coordinate每一个就应该都占8个基本内存单元,那么两个呢,就应该占16个基本内存单元,打印出来就应该是16,但是现在呢,line对象中是两个对象成员指针,那么每一个对象成员指针应该只占4个基本内存单元,所以sizeof(line)计算出来就应该是8,加起来是这两个指针的大小的总和。


内存中的对象成员指针

img

当实例化line这个对象的时候,那么两个指针(m_pCoorA和m_pCoorB)也会被定义出来,由于两个指针都是指针类型,那么都会占4个基本内存单元。如果我们在构造函数当中,通过new这样的运算符从堆中来申请内存,实例化两个Coordinate这样的对象的话呢,这两个Coordinate对象都是在堆中的,而不在line这个对象当中,所以刚才我们使用sizeof的时候呢,也只能得到8,这是因为m_pCoorA占4个基本内存单元,m_pCoorB占4个基本内存单元,而右边的两个Coordinate对象并不在line这个对象的内存当中。当我们销毁line对象的时候呢,我们也应该先释放掉堆中的内存,然后再释放掉line这个对象。


对象成员指针代码实践

/* 对象成员指针

要求:

定义两个类:

​ 坐标类:Coordinate

​ 数据成员:m_iX和m_iY

​ 成员函数:构造函数、西沟函数、数据成员封装函数

​ 线段类:Line

​ 数据成员:点A指针 m_pCoorA,点B指针m_pCoorB

​ 成员函数:构造函数、析构函数、信息打印函数

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

头文件(*Coordinate.h*

class Coordinate
{
public:
    Coordinate(int x, int y);
    ~Coordinate();
    int getX();
    int getY();
public:
    int m_iX;
    int m_iY;
};

源程序(Coordinate.cpp)

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

using namespace std;

Coordinate::Coordinate(int x, int y)
{
    m_iX = x;
    m_iY = y;
    cout <<"Coordinate()  "<< m_iX <<","<< m_iY << endl;
} 
Coordinate::~Coordinate()
{
    cout <<"~Coordinate()  "<< m_iX <<","<< m_iY << endl;
}
int Coordinate::getX()
{
    return m_iX;;
}
int Coordinate::getY()
{
    return m_iY;;
}

头文件(*Line.h*

#include"Coordinate.h"

classLine
{
public:
    Line(int x1, int y1, int x2, int y2);
    ~Line();
    void printInfo();
private:
    Coordinate *m_pCoorA;
    Coordinate *m_pCoorB;
};

源程序(*Line.cpp*

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

using namespace std;

Line::Line(int x1, int y1, int x2, int y2)
{
    //从堆中实例化两个坐标对象,并使指针m_pCoorA和m_pCoorB分别指向这两个对象
    m_pCoorA = new Coordinate(x1, y1);
    m_pCoorB = new Coordinate(x2, y2);
    cout <<"Line()"<< endl;
}
Line::~Line()
{
    delete m_pCoorA;
    m_pCoorA = NULL;
    delete m_pCoorB;
    m_pCoorB = NULL;
    cout <<"~Line()"<< endl;
}
voidLine::printInfo()
{
    cout <<"printInfo()"<< endl;
    cout <<"("<< m_pCoorA->getX() <<","<< m_pCoorA->getY() <<")"<< endl;
    cout <<"("<< m_pCoorB->getX() <<","<< m_pCoorB->getY() <<")"<< endl;
}

主调函数(*demo.cpp*

首先我们只实例化一个线段对象(同时传入四个参数),然后就销毁这个对象,不做其他操作,如下:

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

using namespace std;

int main()
{
    //从堆中实例化一个线段对象,并传入四个参数
    Line *p = new Line(1,2, 3, 4);
    delete p;
    p = NULL;


    system("pause");
    return 0;
}

我们来看一下运行结果:

img

从这个运行结果来看,首先实例化了一个点坐标对象A,然后又实例化了一个点坐标对象B,接着才实例化了一个线段的对象;由于后面调用了delete,A和B就会触发这两个Coordinate对象的析构函数,最后调用Line本身的析构函数。

此外,我们现在在main函数中打印一下信息,通过p来调用printInfo()函数,同时通过sizeof来计算一下其大小,如下代码:

int main()
{
    //从堆中实例化一个线段对象,并传入所个参数
    Line *p = new Line(1,2, 3, 4);
    p->printInfo();

    delete p;
    p = NULL;

    cout <<sizeof(p) << endl;
    cout <<sizeof(Line) << endl;

    system("pause");
    return 0;
}

再来看一下运行结果:

img

从运行结果看,通过p是可以正常调用信息打印printInfo()函数的(屏幕中间已经打印出信息打印函数名,并且也打印出了A点坐标和B点坐标)。最后,打印出4和8,告诉我们,指针p本身大小为4,而Line对象大小为8(说明Line仅仅包含m_pCoorA和m_pCoorB这两个对象成员指针)。


this指针

我们先来看一个下面的例子

img

在这个例子中,我们定义了一个Array数组的类,并且只定义了一个数据成员len,同时定义了三个成员函数:一个是Array类的有參的构造函数,将传入的_len赋值给其数据成员len,还有两个数据成员len的封装函数(getLen和setLen)。通过观察,大家是不是发现参数与数据成员均不重名(比如我们这里的数据成员是len,但是所有的传入参数都是_len)。大家回想一下,在此前的代码中,是不是也都是这种情况?是不是也都是数据成员与它的参数在表达同一个意思的时候用的是不同的名字,这也是当时我们故意这样做的。为什么这样做呢,这是为了顺利完成前面知识的讲解。但是,大家当时是不是都注意到这个问题呢?是否考虑过,如果传入的参数与数据成员重名会怎样呢?下面我们就来一探究竟。还是看一个例子,如下:

img

我们看到在这个例子当中,传入的参数与其数据成员重名了。那么,重名之后,我们发现有两个问题:一个是其构造函数中,一个是setLen()封装函数中,无论是我们还是计算机无法判断究竟是将传入的参数赋值给其数据成员了,还是将其数据成员赋值给传入的参数了。既然计算机无法判断,就会把这样的一种赋值认为是错误的。可见在这个例子中,我们遇到的主要问题呢,就是编译器无法分辨哪个是作为参数的len,哪个又是作为数据成员的len。这就是说,我们迫切需要一种技术,这种技术要么可以标记出参数,要么可以标记出数据成员,那么这种技术就是我们这里所要讲到的this指针。

this指针是什么呢?

this指针就是指向其自身数据的指针。

我们一起来看一下,在刚才的例子中,如果我们实例化一个arr1对象,那么this指针就相当于给arr1取地址,也就是说this就是arr1的地址;如果我们继续实例化一个对象arr2,那么this指针此时就是arr2的地址。我们画一个示意图如下所示:

img

可见,通过this指针就可以访问到它表达的对象的自身的任何数据。比如说,当this是arr1的地址的时候,就可以访问到arr1的数据成员len及其他数据成员;如果this表达的是arr2的地址的时候,也就可以访问到arr2的数据成员len及其他数据成员。这从另一个角度来说,就可以标记处它自身的数据成员,应用到代码当中呢,我们可以写成这样:

img

我们可以看到,如果我们用与数据成员重名的参数,那么我们就可以在数据成员的前面用this加指针符号来表达数据成员的len,然后将参数的len赋值给数据成员的len。这样计算机就不会疑惑究竟是传入的参数赋值给其数据成员了,还是将其数据成员赋值给传入的参数了,从而就可以正常的编译了,进而我们也可以使用与数据成员重名的参数来进行表达了。下面我们回到之前我们没有使用this指针的例子来继续观察。

img

通过观察,我们还发现了什么呢?难道大家没有对成员函数中直接去访问数据成员这种做法产生过怀疑吗?好吧,我们还是先来回顾一下,此前所学的一些知识吧。

img

  这是一个汽车的类,在这个汽车的类中,我们有一个数据成员,这个数据成员就是指这个汽车的轮子的个数,当然,还定义了一个成员函数。当时我们给大家讲的时候,是讲的这些数据成员以及它的成员函数究竟是放在内存中的什么位置的。回顾一下,如上面有图所示,如果我们实例化了一个car1对象,那么car1就拥有了自己的数据成员(轮子的个数 wheelCount),同理我们又实例化了一个car2和car3,那么car2和car3也就拥有了自己的数据成员(轮子的个数wheelCount),但是呢,它的成员函数却只有一份,这份成员函数是写在代码区的,如果car1这个对象,car2这个对象,car3这个对象分别去调用成员函数的时候,那么car1,car2和car3都可以去访问代码区中的这个成员函数,而访问的时候也不会出任何的问题,在成员函数被调用当中呢,也各自调用了各自的数据成员,并且也没有出现混乱。

  那么讲到这里,大家是不是发现了什么呢?既然函数的逻辑代码都是以二进制的方式存储在代码区中,参数中也没有数据成员,那么在调用数据成员的时候怎么可能成功呢?更重要的是,当存在多个对象时,函数又如何确定该调用哪个对象的数据成员呢?要解决这个问题,也归功于this指针,我们继续来看下面这个例子。

img

这个例子非常奇怪,我们仔细的看一下。对比之前的例子,是不是每一个成员函数的参数列表中都多出了一个this指针,那么有了这样一个this指针,刚刚前面所提到的一系列问题也就迎刃而解了。

img

我们可以设想一下,当我们在实例化对象,并使用这些成员函数时,this指针就代表着这个对象本身的地址,也就是说,当我们去实例化arr1的时候,此时在它的构造函数传入参数this,那么当它执行给len赋值10的时候,就相当于是在给this的len赋值10的操作。因为this就指的arr1,所以我们用this去指向len的时候,其实指向的就是arr1这个对象的len,也就不会给其他的对象赋值了。同理如果用arr1去调用另外一个成员函数getLen()的时候呢,我们在这也同时传入了一个this,所以我们在调用return len语句的时候,就相当于调用return this->len,也就是arr1的len,也就不会调用错这个数据成员了。同理当我们去实例化一个arr2对象的时候,这个时候的this就是arr2的地址了,那么调用arr2的成员函数时就跟调用arr1的成员函数一样的意思。从而使arr1和arr2在同时调用成员函数的时候呢,不会产生对象错乱的情况,因为每次调用成员函数呢,都需要this指针。所以C++编译器就干脆把这些事就替我们干了,于是摆在我们面前的成员函数就成了下面的样子(不加this),其实在编译的时候,编译器会自动的为每一个成员函数或者参数列表都加上this指针,因为编译器已经为我们干了这些事情,所以我们在自定义的时候就不需要加this指针这个参数,使用的时候也完全可以当作没有这回事。最后,还有一个问题,那就是系统为每一个成员函数都加了一个this指针,那么这个this指针究竟加在这个参数列表的什么位置呢?是第一个位置,还是最后一个位置呢?为什么要这样设计呢?请看下回分解。

img


this指针代码实践

题目描述:

/* 示例要求

定义一个Array类

数据成员:m_iLen 表示数组长度

成员函数:

构造函数

​ 析构函数

​ m_iLen的封装函数

​ 信息打印函数printInfo

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

程序结构:

img

头文件(*Array.h*

class Array
{
public:
    Array(int len);
    ~Array();
    void setLen(int len);
    int getLen();
    Array printInfo();
private:
    int len;
};

源程序(*Array.cpp*

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

Array::Array(int len)
{
    this->len = len;
}
Array::~Array()
{
    cout <<"~Array()"<< endl;
}
void Array::setLen(int len)
{
    this->len = len;
}
int Array::getLen()
{
    return len;
}
Array Array::printInfo()
{
    cout <<"len = "<< len << endl;
    return *this;   //this本身是一个指针,加上*后就是一个对象
}

主调程序(*demo.cpp*

#include<iostream>
#include<stdlib.h>
#include"Array.h"
using namespace std;
int main()
{
    Array arr1(10);
    arr1.printInfo().setLen(5);  
    cout <<"len = "<< arr1.getLen() << endl;
    system("pause");
    return 0;
}

这里我们先实例化了一个对象arr1,并且对数据成员赋了初值10(实例化的过程就应该调用了构造函数);第二行我们先是调用了printInfo()函数,打印出arr1的长度,并且调用的结果就是返回一个Array对象,然后我们接着又让这个返回的Array对象调用了setLen()函数,并且对这个对象的数据成员赋值5(目的是想看通过这样的操作后,arr1的长度是不是由10变成了5),我们运行一下程序,结果如下:

img

从结果看,出来两遍arr1的长度都是10,可见在这里我们调用setLen()并没有改变arr1的长度,这是为什么呢?这是因为我们调用完printInfo()函数后,返回出去的*this变成的是另外一个对象,它并不是arr1。那么如果想要让它是arr1我们该怎么办呢?我们前面已经学过,如果采用引用的话,就可以实现这一目的了。所以我们在Array.h和Array.cpp中将printInfo()函数修改如下:

Array.h中作如下声明:Array& printInfo();

Array.cpp中作如下实现:

Array &Array::printInfo()
{
    cout <<"len = "<< len << endl;
    return *this;   
}

主调函数不变,我们再来运行程序,结果如下:

img

从运行结果看,arr1的长度已经由10变成了5。更离奇的是,我们使用了连续的点号(arr1.printInfo().setLen(5);),这样就使得多个方法能串起来使用。这一点就能够发挥出this指针的作用。想一下,如果我们这个时候将setLen()也改造一下,是不是它的后面也可以加点号呢?我们来尝试一下,如下:

Array.h中作如下声明:Array& setLen(int len);

Array.cpp中作如下实现:

Array &Array::setLen(intlen)
{
    this->len = len;
    return *this;
}

demo.cpp修改如下:

int main()
{
    Array arr1(10);
    arr1.printInfo().setLen(5).printInfo();  

    system("pause");
    return 0;
}

运行结果如下:

img

从运行结果来看,使得连续的操作都是针对的是arr1的操作。

最后,我们再来通过代码来说明一下this指针的本质。之前我们已经说过,this指针的本质就相当于它所在对象的地址,那么我们来验证一下(通过打印出this指针的值)

我们来修改一下printInfo()函数如下:

Array.h中作如下声明:Array* printInfo(); //让它指直接返回指针

Array.cpp中作如下实现:

Array* Array::printInfo()
{
    cout <<"this指针的地址是: "<<this<< len << endl;
    returnthis;   
}

demo.cpp修改如下:

int main()
{
    Array arr1(10);
    arr1.printInfo();
    cout <<"arr1的地址是: "<<&arr1 << endl;

    system("pause");
    return 0;
}

运行结果如下:

img

从运行结果来看,this指针的值与arr1这个对象的地址是一样的,也就验证了this指针的本质就是其所在对象的地址。


4.4 const进阶

之前我们已经学习过const了,但是还是不够深入,这节课我们继续来学习const。下面先来看一个例子。

img

这里我们定义了一个坐标Coordinate的类,在这个坐标类当中我们定义了两个数据成员,分别表示横坐标和纵坐标(注意:这两个数据成员我们都用了const关键字来修饰),另外我们还定义了一个构造函数,这个构造函数中有两个参数,我们希望将这两个参数传进来后类似初始化两个数据成员。这里如果要想正确的初始化,我们肯定不能像下面这样进行初始化

img

因为这里的m_iX和m_iY都是用const修饰的,也就是说它们是两个常成员,所以上面的初始化方式肯定是错误的。我们必须要通过初始化列表来初始化这两个常成员,如下:

img

从这个例子当中,我们也可以看到,作为一个类的数据成员来说,是可以用const来修饰的,只不过我们此前给大家所讲的一系列例子呢,所修饰的数据成员都是一些基本类型的数据成员。那么,如果要是对象作为数据成员,能不能用const去修饰呢?显然,这也是可以的。我们把这种数据成员就称为常对象成员。为了方便大家爱理解,我们还是以线段这个例子为例。

img

如果有一条线段,当线段的位置一旦确定下来,就不能再更改了。比如上面的这条线段,它的起点是(2, 1)和终点(6, 4)。一旦起点(2, 1)被确定下来后,我们就不能赋其他值了。如果想要达到这个目的,我们必须要将代码写成如下形式:

img

这是一个线段的类,其中有两个对象成员(一个是A点一个是B点),因为我们要实现一旦这两个点被确定后就不能被修改,要实现这样一个功能呢,我们就给这两个点定义成为const类型,也就是常对象。这两个点定义完成后,我们如果想要通过构造函数去初始化它,怎么办呢?我们就必须要写成如下形式(采用初始化列表的形式)

img

而在调用的时候,我们就可以像下面这样去实例化一个线段的对象,然后将线段的参数写全,这些参数就会依次的传递进来,传递进来之后,就可以初始化A点和B点

img

既然const可以修饰数据成员,那么大家把想法可以放的更大胆一些,用const来修饰一个成员函数怎么样呢?这也行!!!???当然行!!!我们把这样的成员函数称为常成员函数。我们来看下面这个例子。

img

还是Coordinate这个类,这个类中,除了有一个Coordinate构造函数外,还定义了两个成员函数(一个普通的changeX函数和一个常成员函数changeX)。然后,我们来定义changeX这个函数,如下

img

思考:常成员函数中为什么不能改变数据成员的值呢?结合我们前面已经学习过的this指针的相关知识,我们一起来分析一下。当我们定义changeX这个成员函数的时候,看上去这个成员函数貌似没有任何的参数,而实际上却隐藏着一个参数,这个参数就是我们前面已经学习过的this指针。比如,我们给m_iX赋值20,实际上在编译的时候,就是给this的m_iX赋值20(如下图所示)

img

当我们的成员函数不是普通的成员函数,而是一个用const修饰过的常成元函数时,又是怎样的情况呢?当我们把它定义成常成员函数的时候,编译器就会编译成如下形式:

img

从编译结果看,它的参数中仍然有一个隐藏的this指针,但是这个this指针是用const来修饰的,显然,此时的this指针已经变成了一个常指针,通过常指针去改变指针所指向的数据肯定是不被允许的。所以,我们如果在常成员函数当中,去修改数据成员的值,这样的做法就异地过是错误的。

此外,我们还发现,在此前我们定义的Coordinate类当中,有两个同名的函数,都叫做changeX(),只不过一个是常成员函数(用const修饰的),一个是普通的成员函数,这两个函数名字相同,参数也相同(就是都没有参数),那么这两个函数可以被称为重载函数吗?结论:它们互为重载函数。怎么样,是不是很神奇。虽然从语法的角度来说,这个的确可以有。但是如果真这样去定义的话,接下来在使用的时候肯定会感到疑惑,比如下面这样,谁能告诉我,你调用的是哪个changeX函数呢?

img

给出答案:这里我们所调用的是那个不带const的普通成员函数。那么,要想调用那个带有const的常常成员函数应当怎么来写呢?此时必须写成如下形式:

img

也就是说,在实例化对象时,必须用****const来修饰这个对象,因而,我们也把这样实例化的对象称为常对象。通过常对象调用的成员函数就是常成员函数。


4.5 常指针与常引用

对象的引用和对象的指针

为了说明对象指针与对象引用的相关知识,我们来看一下下面的例子

img

在这个类中,我们定义了两个数据成员(一个横坐标一个纵坐标),另外,还定义了一个构造函数,还有三个成员函数,其中printInfo()函数是一个常成员函数。那么在实现的时候,也需要在printInfo函数后面加上const关键字来修饰,如下:

img

下面我们来看看对象的引用和对象的指针如何来定以。

img

当我们实例化一个对象coor1的时候,我们就可以给这个对象coor1起一个别名叫做coor2(也就是定义一个引用,引用的名字叫做coor2),当从coor2去调用printInfo()的时候,也会打印出coor1的坐标(3, 5)来。同理,当我们去定义一个对象的指针pCoor,如果让它去指向coor1的话,那么使用pCoor去调用printInfo()的时候,也会打印出coor1的坐标(3, 5)。这里需要提醒大家的是,如果我们定义的是对象的引用,我们就可以直接就用那个对象赋值给这个引用;但是当我们定义的是对象指针的时候,我们在给这个指针赋值的时候,一定特别注意给这个对象前面要加上取地址(&)符号,这样才能正确的赋值。说完了对象引用和对象指针后,如果我们在定义的时候,在前面加上const修饰符,这就变成了对象的常引用和常指针了。


对象的常引用和常指针

img

在这个例子当中,我们定义了一个对象的常引用和对象的常指针,当用coor1去调用printInfo()的时候,肯定不会有问题,会打印出coor1的坐标(3, 5)。关键是当我们用coor2去调用getX()的时候,因为getX这个时候还会传入一个this指针,而这个this指针就是coor2这样的this指针。请注意我们在定义getX和getY的时候,没有在其后面加const,也就是说getX和getY并不是一个常成员函数,这就意味着当用coor2去调用getX()的时候就会出现错误,而出现错误的原因就是因为此时coor2是一个常引用,作为常引用来说,它只有读权限,而getX这里的参数this是一个要求读/写权限的参数,所以其传入的时候就会出现编译错误。所以此时,coor2只能调用其常成员函数。同理使用pCoor来调用getY的时候也是错误的,因为pCoor此时是一个常指针(也只有只读权限)。

下面继续看一个更为复杂的例子。

img

在这个例子中,实例化了两个坐标对象coor1和coor2,然后又定义了一个对象指针,注意,这里定义的对象指针跟刚刚前面定以的有点不一样。之前const的位置是在Coordinate的前面,现在const放在了的后面。如果放在的后面,我们定义的这个pCoor一旦指向了一个对象,那么它就不能再指向另外的对象了。那么我们继续分析下面的三行代码,看看是不是正确。

当pCoor去调用getY,而getY这里要求传入的是可读写权限的对象,而pCoor虽然用const修饰了,但是它的修饰位置是修饰的其本身(意味着这个指针不能指向其他对象),但是这个指针所指向的对象的内容本身是可变的,可见它是一个具有读写权限的指针,只限于它所指向的那个对象可读写,但是它却不能指向其他对象。所以这行代码是正确的。再看下面一行代码,pCoor去指向了coor2,这个就是不允许的(因为pCoor不可以再指向其他对象了),显然这里编译器就会报错。对于第三行代码,pCoor去调用printInfo,显然也是正确的,因为printInfo是一个常成员函数(常成员函数这里传入的this指针要求的是只读权限的),而此时的指针pCoor是具有可读写权限的,所以显然也是正确的。

4.6 练习题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值