C++中的类和对象

C++中的类和对象

前面,我们分别讲解了C++和C语言中不同的语言特性,包括使用auto自动推断数据类型,使用new和delete实现动态数据存储等等

至此我们已经能够实现基础的C++程序编写了,下面就将讲解C语言和C++中最大的不同,即C++中的类和对象

C++是一门面向对象的语言,自然界中的万物都可以被视为对象,因此使用面向对象的方法,我们能够实现更加高级,简洁,易于维护的代码


类和对象介绍

假如我们现在要编写一个程序来模拟人.每个人都具有共通的特征:名字,年龄,性别,可以说话,可以坐下等等

总的说,人所具有的所有特征可以分为两类:共通的属性,共通的能力(方法)

所以实际上,我们可以这样来让程序模拟人

    • 数据
      • 性别
      • 年龄
      • 名字
      • 出生日期
    • 能力(方法)
      • 自我介绍
      • 坐下
      • 睡觉

站在这个视角下,我们发现上面的内容是每一个独立的个体(人)之间所具有的共通内容,不同的人只是在具体的数值上具有差别

因此,我们如果想要我们的程序来模拟人,那么势必就需要一种结构,能够定义人的属性(特征),以及人的方法(能力),这种结构就是类.

我们定义出类之后,所有的人都是类的一个实例,即对象,我们可以声明某人属于人这个类,从而创建一个具有上述特征的人

类和C语言中的结构体很类似,我们创建一个结构体变量之后,结构体变量将自动拥有结构体中定义的所有量

类是在结构体的基础上允许定义方法,这样的话我们申明一个类变量(属于为实例),类变量不经具有类中的所有的量,还将会有所有的方法

声明类

下面我们将使用关键字class声明一个类,在它后面紧接着类名,首先不要关注其中的语法

class Human{
    
    //成员拥有的属性
    string name;
    string dateOfBirth;
    string sexual;
    
    //成员拥有的方法
    void IntroduceSelf();
    void Talk();
};

实例化对象

我们创建一个类之后,类仅仅作为一个数据结构,并没有任何一个变量属于这个类.

下面我们将实例化对象

使用类名实例化

Human FirstHuman

我们使用类似于声明int,float等类型变量的方式实例化了一个属于Human的对象FirstHuman

使用指针创建

类似于其他类型,如int型的创建,我们可以使用动态内存分配的方式来实例化一个Human对象

Human * FirstHuman = new Human()

这样我们就创建了一个指向Human对象的指针FirstHuman

就像前面所讲的,我们使用new方法创建的任何指针都可以用delete来释放

delete FirstHuman

访问类的属性和方法

所谓访问指的是对数据进行写入或存取。

我们实例化一个Human类下的对象之后,该对象就具有了类中定义的属性和方法。

例如,上面我们使用两种方法实例化的FirstHuman对象,创建之后都有自己的name,dateOfBirth,sexua等属性,以及IntroduceSelf(),Talk()等方法

但是我们没有为属性存入具体的值,因此实际上内部的值都是未初始化的垃圾值

自然我们就需要对属性进行访问,来实现存取和读入属性,以及对方法进行访问来调用方法

由于我们实例化对象的时候具有两种方法,因此对应的就有两种对属性和方法进行访问的方式

使用.访问由类名实例化的对象

对于用类名实例化的对象,我们可以使用==.==来访问其中的属性与方法

先不要纠结于public关键字,后面我们马上就会讲解

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{

public:
    string name;
    string dateOfBirth;
    string sexual;
    
    void IntroduceSelf(){
        cout << "我的名字是" << name << endl;
        cout << "我的性别是" << sexual << endl;
        cout << "我的出生日期是:" << dateOfBirth<< endl;
    }
};


//主函数
int main(){

    Human FirstHuman;

    FirstHuman.name="Jack";
    FirstHuman.sexual="male";
    FirstHuman.dateOfBirth="20010107";

    cout << FirstHuman.name << '\t' << FirstHuman.sexual << '\t' << FirstHuman.dateOfBirth <<endl;
    FirstHuman.IntroduceSelf();
    

    return 0;
}

编译之后运行得到如下结果

Jack	male	20010107
我的名字是Jack
我的性别是male
我的出生日期是:20010107

使用->访问由指针创建的对象

对于指针创建的对象,我们可以使用==->==来访问其属性与方法

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{

public:
    string name;
    string dateOfBirth;
    string sexual;
    
    void IntroduceSelf(){
        cout << "我的名字是" << name << endl;
        cout << "我的性别是" << sexual << endl;
        cout << "我的出生日期是:" << dateOfBirth<< endl;
    }
};


//主函数
int main(){

    Human * FirstHumanPointer = new Human();

    FirstHumanPointer->name="Jack";
    FirstHumanPointer->sexual="male";
    FirstHumanPointer->dateOfBirth="20010107";

    cout << FirstHumanPointer->name << '\t' << FirstHumanPointer->sexual << '\t' << FirstHumanPointer->dateOfBirth <<endl;
    FirstHumanPointer->IntroduceSelf();
    

    return 0;
}

编译之后得到的结果和上面一样

Jack	male	20010107
我的名字是Jack
我的性别是male
我的出生日期是:20010107

最后,之所以造成使用不同的方法创建得到的不同的对象需要用不同的方法来访问属性与方法的原因其实是因为使用不同的方法实例化的对象其实储存在不同的位置,也正是因为

关键字public和private

我们申明一个类之后,类中包含的所有信息(类的所有属性)可以分为两类,不介意别人知道的数据和需要保密的数据

对于Human这个类来说,我们如果实例化了一个对象Jack,那么Jack的性别和身高都是不介意别人知道的数据,但是Jack的体重和收入可能就是需要保密的隐私数据

因此C++中的类中我们可以使用关键字public和private来将类的属性和方法声明为私有(Private)或者是公有(Public)

就像前面说的Jack的身高和性别一样,这些属性都是不介意别人知道的数据,因此我们在类的定义中声明为public的属性和方法能够在类的外部调用它;而类似于收入这种private的属性,我们只能在类的内部定义的方法去访问它,而不能在外部去访问它

关键字public定义公有属性

下面我们举例来体现public关键字的功能

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{

public:
    //成员拥有的属性
    string name;
    string sexual;
};


//主函数
int main(){

    Human Jack;

    Jack.name="Jack";
    Jack.sexual="male";

    cout << "我的名字是" << Jack.name<< endl;
    cout << "我的性别是" << Jack.sexual << endl;


    return 0;
}

编译成功后,我们运行得到如下结果

我的名字是Jack
我的性别是male

这里我们能够看到,我们在Human类中声明了公有属性name和sexual,我们实例化一个对象Jack后,为name和sexual两个公有属性赋予了具体的值,并且输出了这两个值

这里我们为这两个公有属性赋值和输出值都是在类的外部进行的,即关键字public声明的属性和方法都可以在外部调用

关键字private定义私有属性

下面我们使用关键字private定义私有属性,讲解private关键字的功能

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{
public:
    string name;
    
private:
    string dateOfBirth;
};


//主函数
int main(){

    Human Jack;

    Jack.name="Jack";
    Jack.dateOfBirth="20010107";

    cout << Jack.name<<endl;
    cout << Jack.dateOfBirth<<endl;

    return 0;
}

我们使用gcc编译器在Ubuntu编译器下编译时,会无法通过,并且报错

jackwang@jackwang-ThinkPad-X390-Yoga:~/桌面/TryC++$ g++ TryClass.cpp -o TryClass
>>>
TryClassTryClass.cpp: In function ‘int main()’:

TryClass.cpp:10:12: error: ‘std::__cxx11::string Human::dateOfBirth’ is private
     string dateOfBirth;
            ^
TryClass.cpp:20:10: error: within this context
     Jack.dateOfBirth="20010107";
          ^
TryClass.cpp:22:18: error: ‘class Human’ has no member named ‘name’
     cout << Jack.name<<endl;
                  ^
TryClass.cpp:10:12: error: ‘std::__cxx11::string Human::dateOfBirth’ is private
     string dateOfBirth;
            ^
TryClass.cpp:23:18: error: within this context
     cout << Jack.dateOfBirth<<endl;
                  ^

这是因为我们在类的外部尝试访问(包括赋值和取值 / 引用)私有属性导致无法通过编译

如果我们想要尝试对私有属性进行访问的话,那么就必须要在类的内部进行访问

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{
private:
    string dateOfBirth;
public:
    string name;
    void SetDateOfBirth(){
        cout << "请输入出生日期:";
        cin >>  dateOfBirth;
        cout << "成功设置年龄" <<endl;
    }
    void TalkDateOgBirth(){
        cout << "我的出生日期是:" << dateOfBirth<<endl;
    }
};

//主函数
int main(){

    Human Jack;

    Jack.name="Jack";
    Jack.SetDateOfBirth();

    cout << "我的名字是"<< Jack.name<<endl;
    Jack.TalkDateOgBirth();

    return 0;
}

这里我们通过在类的内部设置公有方法来访问私有属性,从而实现了对私有属性的写入与读取。

构造函数

前面我们讲解了C++中如何定义一个类、声明公有 / 私有属性与方法以及访问实例化的对象所具有的属性和方法

但是有一个问题就是,类中的方法都是静态的,只有当我们调用的时候才会起效

但是在一些情况下我们却希望每当我们实例化一个对象的时候就需要能够自动的运行的方法,例如我们编写一个数组类,所有的数组都是这个类的一个示例,那么每当我们实例化一个数组的时候,我们都希望能自动调用类中的方法,计算出数组的形状、大小,并将他们作为数组的属性。

这个时候需要用到构造函数了,构造函数是指与类同名的函数,每当我们创建一个类的时候,重构函数就会自动的运行。

构造函数可以放在类的内部声明,也可以放在类的外部声明,但是为了便于代码的阅读,通常将构造函数放在类的内部声明,此外,两种不同的声明方式创建的语法不同,但是都需要放在public中

构造函数本身是一种特殊的函数,不需要和普通函数一样设置函数类型,并且无法调用构造函数

此外,如果我们没有声明构造函数,那么C++编译器默认会创建一个空的构造函数,内部没有任何需要执行的语句

声明构造函数

我们前面的人类的例子中,实例化一个人作为对象并为其赋予name,age等初值是在程序内部进行的,我们更希望在我们实例化的时候让用户来自己决定,这就需要使用构造函数

类的内部声明构造函数

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{
private:
    string name="-1";
    string age="-1";
public:
    Human(){
        if (name=="-1")
        {
            SetName();
        }
        if (age=="-1")
        {
            SetAge();
        }
    }
    void SetName(){
        cout << "请输入姓名:";
        cin >> name;
    }
    void SetAge(){
        cout << "请输入年龄:";
        cin >> age;
    }
    void OutName(){
        cout << name << endl;
    }
    void OutAge(){
        cout << age <<endl;
    }
};


//主函数
int main(){
    Human Jack;
    Jack.OutName();
    Jack.OutAge();
    return 0;
}

编译之后运行得到的结果

jackwang@jackwang-ThinkPad-X390-Yoga:~/桌面/TryC++$ ./TryClass 
>>>
请输入姓名:Jack
请输入年龄:20
Jack
20

类的外部声明构造函数

我们如果要在类的外部声明构造函数,就需要使用域解析运算符::,来限定我们的构造函数是在哪里声明的,即告诉编译器标识符所处的命名空间

关于命名空间将会在后面讲解

例如

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{
private:
    string name="-1";
    string age="-1";
public:
    Human();			#注意这里我们声明了构造函数但是并没有定义他
    void SetName(){
        cout << "请输入姓名:";
        cin >> name;
    }
    void SetAge(){
        cout << "请输入年龄:";
        cin >> age;
    }
    void OutName(){
        cout << name << endl;
    }
    void OutAge(){
        cout << age <<endl;
    }
};

Human::Human()			#这里我们使用域解析运算符来表示Human()这个构造函数在Human这个命名空间中
{
        if (name=="-1")
        {
            SetName();
         }
        if (age=="-1")
        {
            SetAge();
        }
}

//主函数
int main(){
    Human Jack;
    Jack.OutName();
    Jack.OutAge();
    return 0;
}

可以接受参数的构造函数

我们上面是在实例化一个人的时候,由用户根据命令行的输出提示来设置人的名字和年龄

更进一步,通过这种方式来实例化一个人并为其赋予姓名和年龄等属性实际上是非常麻烦的,我们更希望在实例化一个人的时候直接指定其姓名与年龄,这个时候我们就可以使用接受参数的构造函数

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{
private:
    string name="-1";
    string age="-1";
public:
    Human(string humanName, string humanAge){
        name =humanName;
        age=humanAge;
    }
    void IntroduceSelf(){
        cout << "我叫" << name<< endl;
        cout << "今年我" << age << "岁了" <<endl;
    }
};


//主函数
int main(){
    Human Jack("Jack","20");
    Jack.IntroduceSelf();
    return 0;
}

运行之后得到结果

jackwang@jackwang-ThinkPad-X390-Yoga:~/桌面/TryC++$ ./TryClass 
>>>
我叫Jack
今年我20岁了

我们也可以为参数设定默认值,这样我们在实例化的时候可以选择性的给定参数

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{
private:
    string name;
    string age;
public:
    Human(string humanName="-1", string humanAge="-1"){
        name =humanName;
        age=humanAge;
    }
    void IntroduceSelf(){
        cout << "我叫" << name<< endl;
        cout << "今年我" << age << "岁了" <<endl;
    }
};


//主函数
int main(){
    Human Jack("Jack");
    Jack.IntroduceSelf();
    return 0;
}

编译之后得到结果

jackwang@jackwang-ThinkPad-X390-Yoga:~/桌面/TryC++$ ./TryClass
>>>
我叫Jack
今年我-1岁了

初始化列表的构造函数

上面的构造函数中,我们在函数中将接收到的参数的值通过赋值的方式传递给类的属性,实际上这样在C++中我们完全可以通过在构造函数中使用初始化列表的方式做到更高效的赋值,从而避免像上面一样为humanName和humanAge开辟新的空间而造成无谓的开销

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{
private:
    string name;
    string age;
public:
    Human(string humanName="-1", string humanAge="-1")
        :name(humanName),age(humanAge)
    {
        cout << "我叫" << name<< endl;
        cout << "今年我" << age << "岁了" <<endl;
    }
};


//主函数
int main(){
    Human Jack("Jack","20");
    return 0;
}

编译之后运行得到的结果

jackwang@jackwang-ThinkPad-X390-Yoga:~/桌面/TryC++$ ./TryClass 
>>>
我叫Jack
今年我20岁了

通过初始化列表的方式接受参数并赋值给属性是一种十分高效的方式

重载构造函数

前面在讲解C++和C语言的函数的时候,特地讲解了C++中所具有的,而C语言中却不具有的重载函数。

实际上对于构造函数我们同样能够进行重载,来达到根据不同输入来实现不同的功能

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{

private:
    string name="-1";
    string age="-1";

public:
    Human()
    {
        cout << "请输入姓名:";
        cin >> name;
        cout << "请输入年龄:";
        cin >> age;
    }
    Human(string humanAge)
        :age(humanAge)
    {
        cout << "请输入姓名:";
        cin >> name;
    }
    void IntroduceSelf(){
        cout << "我的名字是:" << name <<endl;
        cout << "我的年龄是:" << age <<endl;
    }
};


//主函数
int main(){

    Human Jack;
    Human Sarah("20");
    Jack.IntroduceSelf();
    Sarah.IntroduceSelf();
    return 0;
}

此外,和正常的重载函数一样,C++编译器在分析重载函数的时候是根据接受的参数及其类型来确定的,因此如果我们使用下面的两个构造函数来进行重载,就会导致无法通过编译

Human(string humanName)
        :name(humanName)
    {
        cout << "请输入年龄:";
        cin >> age;
    }
Human(string humanAge)
        :age(humanAge)
    {
        cout << "请输入姓名:";
        cin >> name;
    }

编译时将会有如下报错

jackwang@jackwang-ThinkPad-X390-Yoga:~/桌面/TryC++$ g++ TryClass.cpp -o TryClass
>>>
TryClass.cpp:20:5: error: ‘Human::Human(std::__cxx11::string)’ cannot be overloaded
     Human(string humanAge)
     ^
TryClass.cpp:14:5: error: with ‘Human::Human(std::__cxx11::string)’
     Human(string humanName)
     ^

因为在编译器看来,两个构造函数都接受一个字符串类型的变量,因此实际上两者是完全一样的,因此如果能够进行重载,那么当编译器接收到"20"与”Jack"时候,编译器不知道调用那个构造函数,因此就会报错。

实际上我们只需要把humanAge前面的声明更改为int即可通过编译

析构函数

与构造函数一样,析构函数也是一种特殊的函数,构造函数在实例化的时候被调用,而析构函数在对象销毁时将自动被调用

通常对象的销毁是在主程序main()结束运行之后,因此在主程序结束运行之后就会销毁所有的对象,这个时候就会调用所有实例化的对象的析构函数

这一操作通常用在类中使用new新建了一个指针,由于new和delete必须一一对应,因此我们通常在析构函数中存放一个delete来释放对应的空间,这样当主程序结束运行的时候就会释放空间

析构函数和构造函数类似,都是以类名作为函数名的,只不过在函数名前具有一个~符号,表示析构函数

而且析构函数由于是在主程序结束运行之后才运行的,因此析构函数不接受参数进而也不能进行初始化列表.依赖于参数的数据类型和参数个数的重载析构函数也不支持

声明析构函数

与构造函数的声明相同,析构函数的声明具有两种方式,第一种是在类的内部进行声明,第二种是在类的外部进行声明

类的内部声明析构函数

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{

private:
    string name="-1";
    string age="-1";

public:
    Human(string humanName,string humanAge)
        :name(humanName),age(humanAge)
    {
        cout << "我的名字是:" << name <<endl;
        cout << "我的年龄是:" << age <<endl;
    }

    ~Human(){
        cout << "启动析构函数"<<endl;
        cout << "已销毁对象:" << name <<endl;
    }
};


//主函数
int main(){

    Human Jack("Jack","20");
    Human Sarah("Sarah","21");
    cout << "主程序结束运行"<<endl;
    return 0;
}

变异之后得到的结果

jackwang@jwang:~/桌面/TryC++$ ./TryClass 
>>>
我的名字是:Jack
我的年龄是:20
我的名字是:Sarah
我的年龄是:21
主程序结束运行
启动析构函数
已销毁对象:Sarah
启动析构函数
已销毁对象:Jack

类的外部声明析构函数

和在类的外部声明构造函数类似,在类的外部声明析构函数也需要使用域解析运算符来指明析构函数得标识符所处的命名空间

而且也需要在类中的公有区声明

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{

private:
    string name="-1";
    string age="-1";

public:
    Human(string humanName,string humanAge)
        :name(humanName),age(humanAge)
    {
        cout << "我的名字是:" << name <<endl;
        cout << "我的年龄是:" << age <<endl;
    }

    ~Human();
};

Human::~Human(){
    cout << "启动析构函数"<<endl;
    cout << "已销毁对象:" << name <<endl;
}

//主函数
int main(){

    Human Jack("Jack","20");
    Human Sarah("Sarah","21");
    cout << "主程序结束运行"<<endl;
    return 0;
}

编译之后运行得到的结果如下

jackwang@jwang:~/桌面/TryC++$ ./TryClass 
我的名字是:Jack
我的年龄是:20
我的名字是:Sarah
我的年龄是:21
主程序结束运行
启动析构函数
已销毁对象:Sarah
启动析构函数
已销毁对象:Jack

何时使用析构函数

就像前面所讲的,析构函数通常使用在我们在类中使用new新分配一个内存之后,需要使用delete来释放内存,否则会造成系统的内存占用,用户程序占用的系统运行内存多了之后可能会导致系统运行内存不足,因此这个时候就需要重新启动

下面我们创建一个类来封装一个C语言风格的字符串,这样在不使用string的时候就不需要创建字符数组来储存字符串了

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

//导入命名空间
using namespace std;

class MyString{
private:
    char * buffer;
public:
    MyString(const char * initString){
        if(initString!=NULL){
            buffer=new char [strlen(initString)+1];
            strcpy(buffer,initString);
        }
        else
        {
            buffer=NULL;
            cout << "内存分配失败" <<endl;
        }
    }
    ~MyString(){
        cout << "开始销毁对象,释放内存" <<endl;
        delete [] buffer;
        cout << "已成功销毁对象并释放对象"<<endl ;
    }
    const  char * GetString(){
        return buffer;
    }
};
//主函数
int main(){
    MyString newTrial("尝试传入字符串");
    cout << newTrial.GetString() <<endl;
    cout << "程序运行结束" <<endl;

    return 0;
}

由于我们每声明一个字符串的时候,C++编译器都会在后台为其创建一个字符数组并且返回字符数组的首元素地址,所以这里11行我们接受参数的时候需要接受一个指向字符串的指针

而且前面在讲解C++中关于指针没有在C语言中讲解到的const关键字,这里我们不希望改变字符指针指向的字符的内容,因此使用const char *类型的声明

同样的道理在我们申明GetString方法中也有所出现

复制构造函数

常我们会使用像上面一样的在类的构造函数中使用动态内存分配创建一个新的指针,然后在析构函数中释放分配的内存

这样可能会导致一个由于浅复制导致的埋藏的很深的问题,对应的解决方法就是使用复制构造函数

浅复制导致的bug

代码

我们使用如下代码作为示例

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{
    private:
        string name;
        const int * age;

    public:
        Human(string humanName="-1",const int * humanAge=nullptr)
            :name(humanName)
        {
            age = NULL;
            if (humanAge!=nullptr)
            {
                age = new int;
                *age = *humanAge;
            }
            else
            {
                cout << "内存分配失败,无法载入年龄" <<endl;
            }
        }

        ~Human(){
            cout << "开始销毁对象,释放分配的内存" <<endl;
                delete age;
            cout << "已释放分配的内存" << endl;
        }

        void IntroduceSelf(){
            cout << "我的名字是:" << name << endl;
            cout << "我的年龄是:" << *age << endl;
        }
};

void LetHimIntroduce(Human aPersion){
    aPersion.IntroduceSelf();
}

int main(){
    string jackName="Jack";
    int jackAge=20;

    Human Jack(jackName,&jackAge);
    LetHimIntroduce(Jack);
    
    return 0;
}

这里对上面的程序进行一下分析,

首先我们声明了一个Human类,Human类中我们声明了两个私有属性name和age,但是和前面的程序不同的是,我们这里age是一个指向整数的指针,所以我们需要在构造函数中去初始化这两个值

其次,由于name是一个已经初始化(分配地址)的变量,因此可以直接使用初始化列表的方式来接受参数,但是由于我们需要让代码更加鲁棒,因此还需要检测传入的指针是否成功分配了地址,所以我们不在初始化列表中将指针初始化,而是先取消age指针变量的内存(将其置为NULL),然后检测到传入的指针成功分配之后,将会使用动态内存分配为age分配新的内存,对应的我们在析构函数中释放了为age分配的内存

接下来我们在类中定义了一个IntroduceSelf()方法来进行自我介绍

和前面的代码最大的不同在于我们这里定义了以Human类的对象作为参数的函数LetHimIntroduce(),在这里面我们去调用Human类的IntroduceSelf()方法来进行自我介绍

我们编译成功后运行得到如下结果

jackwang@jwang:~/桌面/TryC++$ ./TryClass 
>>>
我的名字是:Jack
我的年龄是:20
开始销毁对象,释放分配的内存
munmap_chunk(): invalid pointer
已放弃 (核心已转储)

发现我们的程序在最后主程序运行结束之后销毁对象的时候崩了,这就意味有一个隐藏的很深的bug,这个bug能够通过编译器的检查,但是却会造成致命的错误

分析

这个bug实际上是由浅复制所导致的,我们对其中的每一步进行分析,来检查下bug发生在哪里

  1. 由于我们能够成功地调用IntroduceSelf方法,因此我们成功地实例化了对象Jack
  2. 接下来我们调用LetHimIntroduce函数.这里我们使用的是值传递,因此会首先实例化另一个对象aPerson,然后将对象Jack的属性复制给aPerson

所以实际上整个程序中,我们实例化了两个Human类的对象

由于aPerson对象的属性是从Jack对象的属性复制来的,因此aPerson的age指针变量的内容和Jack对象的age指针变量的内容是一样的

即两者指向的是同一个由new分配的地址

因此我们LetHimIntroduce函数运行结束之后,会将aPerson对象销毁,这个时候aPerson对象和Jack对象共享的age指针已经被释放了,接下来主程序运行结束,Jack对象的age指针释放

而由于两个对象的age指向同一个new分配的地址,所以实际上在销毁第二个对象的时候,我们对已经释放掉的由new创建的指针再一次进行了释放

这就是Bug的原因

所以问题关键在于我们定义的函数是一个值传递,这将会造成创建一个新的Human对象,但是新的对象却并非原对象的完全复制,即仅将原对象的属性进行了复制,但是对于指针类的属性并没有复制指针指向的内容

所以正式这种浅复制导致了Bug

解决思路

对于这个问题,我们可能两种思路,第一种是在函数中使用引用,这样能够避免开辟第二个Human对象,但实际上这样是没有用的

因为当函数结束运行之后,即便是引用的对象也会被释放,因此实际上这种思路是不行的

第二种思路就是进行深复制,不仅将对象的属性进行复制,对指针类属性指向的内容也进行复制,这样就能够避免两个对象销毁时候释放同一个指针

使用复制构造函数解决浅复制导致的问题

我们为了解决这个问题,就需要在LetHimIntroduce函数中创建Human类的时候确保是深复制,此外还需要能够进行正常的构造对象

因此我们需要重载一个复制构造函数

复制构造函数按照以下方式来创建

类名(const 类名 & 对象名){
    /**Code**/
}

所以上面的例子修改之后的结果就是

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{
    private:
        string name;
         int * age;
        string state;

    public:
        Human(string humanName="-1",int * humanAge=nullptr)
            :name(humanName)
        {
            state="浅";
            cout << "开始进行浅复制" << endl;
            if (humanAge!=nullptr)
            {
                age = new int;
                *age = *humanAge;
                cout << "浅复制指针指向的地址是:" << age << endl; 
            }
            else
            {
                cout << "内存分配失败,无法载入年龄" <<endl;
            }
        }

        Human(const Human & aPerson)
            :name(aPerson.name)
        {
            state="深";
            cout << "开始进行深复制" << endl;
            age = NULL;
            if ( aPerson.age!=nullptr)
            {
                age = new int;
                *age = *(aPerson.age); 
                cout << "深复制的指针指向的地址是:" << age <<endl;
            }
            else
            {
                cout << "深度复制内存分配失败,无法载入年龄" << endl;
            }
            
        }

        ~Human(){
            cout << "开始销毁对象,释放分配的内存" <<endl;
            cout << "释放对象状态:"<<state<<'\t' << "释放对象地址:" << age<<endl;
            delete age;
            cout << "已释放分配的内存" << endl;
        }

        void IntroduceSelf(){
            cout << "我的名字是:" << name << endl;
            cout << "我的年龄是:" << *age << endl;
        }
};

void LetHimIntroduce(Human aPersion){
    aPersion.IntroduceSelf();
}

int main(){

    string jackName="Jack";
    int jackAge=20;

    Human Jack(jackName,&jackAge);

    LetHimIntroduce(Jack);
    return 0;
}

此外,需要注意的是,我们再22和40行都是通过*(age)=*(humanAge)的方式进行的,如果直接使用age=humanAge的将会导致age指向的地址和humanAge指向的地址相同,这样将会在主程序运行结束之后释放humanAge的地址之后再销毁对象的时候重复释放

这点可以使用复制赋值运算符operate=来解决,这点将在后面讲解

移动构造函数

我们在有些情况下可能会造成对象的多次自动复制,例如在上面的代码中我们再添加一个复制函数

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{
    private:

    public:
        string name;
         int * age;
        string state;
        Human(string humanName="-1",int * humanAge=nullptr)
            :name(humanName)
        {
            state="浅";
            if (humanAge!=nullptr)
            {
                age = new int;
                *age = *humanAge;
            }
            else
            {
                cout << "内存分配失败,无法载入年龄" <<endl;
            }
        }

        Human(const Human & aPerson)
            :name(aPerson.name)
        {
            state="深";
            age = NULL;
            if ( aPerson.age!=nullptr)
            {
                age = new int;
                *age = *(aPerson.age); 
            }
            else
            {
                cout << "深度复制内存分配失败,无法载入年龄" << endl;
            }
            
        }

        ~Human(){
            delete age;
        }

        void IntroduceSelf(){
            cout << "我的名字是:" << name << endl;
            cout << "我的年龄是:" << *age << endl;
        }
};

void LetHimIntroduce(Human aPersion){
    aPersion.IntroduceSelf();
}

Human Copy(Human & aPerson){
    Human aPersonCopy(aPerson.name+"的复制",aPerson.age);
    return aPersonCopy;
}

int main(){

    string jackName="Jack";
    int jackAge=20;

    Human Jack(jackName,&jackAge);
	Human JackCopy(Copy(Jack));
    JackCopy.IntroduceSelf();
    return 0;
}

编译成功之后运行得到的结果如下

jackwang@jackwang-ThinkPad-X390-Yoga:~/桌面/TryC++$ ./TryClassNew 
>>>>
我的名字是:Jack的复制
我的年龄是:20

上面的代码中,我们在Copy函数中对Jack进行了复制,这个过程中由于传入的是引用,因此不会新创建一个Human对象

但是在Copy函数结束运行之后,由于返回值是Human类,因此会进行值传递,新创建一个Human类作为返回值

接下来我们又新建了一个JackCopy对象,然后对JackCopy对象进行的操作

在这个过程中,我们为Copy函数的返回值新建了一个Human对象,这个Human对象只在这里调用一次,在后面不会再被调用

所以如果我们的对象具有较大的数组作为属性的话,这种复制会带来性能瓶颈,因此C++中提供了移动构造函数,来避免类似的仅会使用某个对象的副本一次所造成的开销。

我们通过移动构造函数来解决这个问题,移动构造函数会将临时资源暂时的移动,从而避免复制带来的开销

我们只需要重载一个移动构造函数即可

类名(类名 && 对象名){
    /**Code**/
}

所以最后得到的函数如下

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{
    private:

    public:
        string name;
         int * age;
        string state;
        Human(string humanName="-1",int * humanAge=nullptr)
            :name(humanName)
        {
            state="浅";
            if (humanAge!=nullptr)
            {
                age = new int;
                *age = *humanAge;
            }
            else
            {
                cout << "内存分配失败,无法载入年龄" <<endl;
            }
        }

        Human(const Human & aPerson)
            :name(aPerson.name)
        {
            state="深";
            age = NULL;
            if ( aPerson.age!=nullptr)
            {
                age = new int;
                *age = *(aPerson.age); 
            }
            else
            {
                cout << "深度复制内存分配失败,无法载入年龄" << endl;
            }
            
        }

        Human(const Human && aPerson)
            :name(aPerson.name)
        {
            state="浅";
            age = NULL;
            if ( aPerson.age!=nullptr)
            {
                age = new int;
                *age = *(aPerson.age); 
            }
            else
            {
                cout << "深度复制内存分配失败,无法载入年龄" << endl;
            }
            
        }

        ~Human(){
            delete age;
        }

        void IntroduceSelf(){
            cout << "我的名字是:" << name << endl;
            cout << "我的年龄是:" << *age << endl;
        }
};

void LetHimIntroduce(Human aPersion){
    aPersion.IntroduceSelf();
}

Human Copy(Human & aPerson){
    Human aPersonCopy(aPerson.name+"的复制",aPerson.age);
    return aPersonCopy;
}

int main(){

    string jackName="Jack";
    int jackAge=20;

    Human Jack(jackName,&jackAge);
	Human JackCopy(Copy(Jack));
    JackCopy.IntroduceSelf();
    return 0;
}

这样就能够解决性能瓶颈

this指针

在C++中,有一个重要的保留的关键字就是this,this实际上是一个指针,将会指向当前对象的地址

我们在类的方法中会调用类的各种属性,而编译器在编译的时候实际上会默认添加一个this指针

下面的程序就详细的体现了这一点

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{

private:
    string name_1;
    string name_2;

public:
    Human(string humanName)
    {
        name_1=humanName;
        //等价于下面的语句
        this->name_2=humanName;
    }

    void IntroduceSelf(){
        cout << "我的名字是:" << name_1 <<endl;
        //等价于下面的语句
        cout << "我的名字是:" << this->name_2 << endl;
    }
};

int main(){
    Human Jack("Jack");
    Jack.IntroduceSelf();
    return 0;
}

编译成功之后运行得到如下的结果

jackwang@jackwang-ThinkPad-X390-Yoga:~/桌面/TryC++$ ./TryClassNew 
>>>
我的名字是:Jack
我的名字是:Jack

关键字friend声明友元

前面我们讲过,在类中声明为private的属性和方法无法在类的外部进行访问,但我们可以用过声明友元的方式来进行访问类中声明为private的属性和方法

声明友元函数

我们可以通过如下的方式来声明一个友元函数来访问类的private信息

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human{

private:
    string name;
    //声明友元
    friend void IntroduceByOther(Human & aPerson);

public:
    Human(string humanName)
    {
        name=humanName;
    }

    void IntroduceSelf(){
        cout << "我的名字是:" << name <<endl;
    }

};

void IntroduceByOther(Human & aPerson){
    cout << "他的名字是" << aPerson.name << endl;
}
int main(){
    Human Jack("Jack");
    IntroduceByOther(Jack);
    return 0;
}

编译之后运行的得到的结果

jackwang@jackwang-ThinkPad-X390-Yoga:~/桌面/TryC++$ ./TryClassNew
>>>
他的名字是Jack

声明友元类

我们使用下面的方式来声明一个友元类,这样能够友元类中访问类的private属性或方法

#include <iostream>
#include <string>

//导入命名空间
using namespace std;

class Human
{
private:
    string name;
    //声明友元
    friend class Animal;
public:
    Human(string humanName)
    {
        name=humanName;
    }

    void IntroduceSelf(){
        cout << "我的名字是:" << name <<endl;
    }

};

class Animal
{
private:
    string species="dog";
public:
    void Display(Human & aPerson){
        cout << aPerson.name << "是" << species << "的主人" << endl;
    }
};

int main(){
    Human Jack("Jack");
    Animal Dog;
    Dog.Display(Jack);
    return 0;
}

编译之后运行得到的结果

jackwang@jackwang-ThinkPad-X390-Yoga:~/桌面/TryC++$ ./TryClassNew 
>>>
Jack是dog的主人
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值