专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

类与对象

创建对象

假设我们有一个Student类:

class Student
{
public:
    string name;
    int id;
    int age;
    string gender;
    // methods
    void SayHello();
}

下面我们可以通过两种方法创建对象:

1、 类似于声明变量:

    int a = 1;  // 声明变量
    Student student;    // 创建对象
    student.name = "miles"  // 使用'.'访问成员变量

2、 类似于动态分配内存:

    int* ap = new int;  // 动态分配
    Student* student = new Student();   //动态分配Student类型对象
    student->name = "miles";    // 使用'->'访问成员变量


构造函数与析构函数

1. 复制构造函数

关于复制构造函数首先需要清楚两个概念:“浅拷贝”和“深拷贝”。如果某类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

简单来说,当通过b=a进行浅拷贝时,将a对象中的所有成员变量值复制给b,当类中成员变量都是普通数据类型不会有什么问题,但当成员变量中包含原始指针时,浅拷贝仅仅只是将指针指向的地址进行复制,而不是重新开辟新内存空间进行复制,这样a和b中的指针变量都指向同一区域,当其中一个对象执行了释放指针指向的内存操作后,另一对象的指针指向了已经毫无意义的区域,容易引发异常。

如果我们在类中没有显式地声明复制构造函数,那么编译器将为类自动生成默认复制构造函数,但是该默认复制构造函数实现了对象之间的浅拷贝(位拷贝),所以为了防止自动生成的默认复制构造函数中对指针进行浅拷贝,我们需要自己设计复制构造函数重新开辟内存存放原指针指向的数据,实现深拷贝。

复制构造函数的结构为:类名(const 类名& 拷贝对象)

浅拷贝(无复制构造函数):

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

class MyString {
private:
    char* buffer;

public:
    MyString(const char* initString)
    {
        cout << "触发MyString的构造函数" << endl;
        buffer = NULL;
        if (initString != NULL)
        {
            buffer = new char[strlen(initString) + 1];
            strcpy(buffer, initString);
        }
    }

    ~MyString()
    {
        cout << "触发MyString的析构函数"<<endl;
        delete [] buffer;   // 释放内存
    }

    void ToString()
    {
        cout << "buffer:" << buffer << endl;
    }
};

void UseMyString (MyString str)
{
    str.ToString();
}

int main() {
    MyString myString(" hello from miles");
    cout << "before invoke useMyString" << endl;
    UseMyString(myString);
    cout << "after invoke useMyString" << endl;
    return 0;
}

输出结果:

触发MyString的构造函数
before invoke useMyString
buffer: hello from miles
触发MyString的析构函数
after invoke useMyString
触发MyString的析构函数
*** Error in `./test': double free or corruption (fasttop): 0x00000000020dc030

在main函数中,调用useMyString时将MyString对象作为参数传入,由于类中没有自定义复制构造函数,这时调用了编译器生成的默认复制构造函数,实现了浅拷贝,由于类中只有一个原始指针,所以useMyString中拷贝后的对象“str”的指针与原对象“myString”的指针指向同意内存地址。当函数useMyString返回时,局部变量“str”需要销毁,调用“str”的析构函数,对指针指向的内存进行释放。此时原对象“myString”的指针指向的区域也被释放,在main函数结束时调用“myString”的析构函数时,由于指针已经被释放,则会出现“重复释放”的异常。

为了避免浅拷贝引起原始指针重复释放问题,我们必须自定义复制构造函数:

MyString (const MyString& copySource)
{
    cout << "调用复制构造函数" << endl;
    buffer = NULL;
    if(copuSource.buffer != NULL)
    {
        buffer = new char[strlen(copySource)+1];
        strcpy(buffer, copySource.buffer);
    }
}

定义了复制构造函数后,useMyString函数传入MyString参数时就会调用我们自定义的复制构造函数,为原始指针开辟新内存,不会影响原对象,避免出错。

输出结果:

触发MyString的构造函数
before invoke useMyString
调用复制构造函数
buffer: hello from miles
触发MyString的析构函数
after invoke useMyString
触发MyString的析构函数

在复制构造函数中使用了“const”关键字和“引用”,使用const关键字可以禁止复制构造函数修改原对象,而使用引用是防止将对象作为参数传入而递归调用自己,直到用完内存为止。

另外,通过赋值运算符“=”编译器提供的默认赋值运算符将导致浅拷贝,所以当类中包含原始指针时,必须编写复制构造函数和复制赋值运算符。

为了演示错误,虽然我们在MyString中使用char* buffer作为字符串类型成员变量,但是在实际中不这样使用,而是使用std::string,因为string不是原始指针,我们不需要编写复制构造函数,因为编译器为我们自动生成的默认复制构造函数中调用了string的复制构造函数。

2. 不允许复制的类与单例模式

在许多场景都要求类对象禁止被复制,例如系统管理员、网络连接等,这时我们需要禁止程序复制类对象,可以生命私有的复制构造函数和私有的赋值运算符:

class Manager{
private:
    Manager(const Manager& manager)
    {
        ...
    }
    Manager& operator= (const Manager&);

}

但是有时我们要求某些类最多只能存在一个实例,即单例模式,上面的不允许复制的类只是禁止了对对象的复制,而无法保证程序中该类最多只包含一个实例。

为了保证实例只构造一次,我们需要使用static关键字,static的作用分为:

1、 static修饰类的成员变量时,该成员变量在所有实例中共享;
2、 static修饰函数中声明的局部变量时,该变量只初始化一次,下一次依据上一次的值。
3、 static修饰函数方法时,该方法在所有实例间共享,static函数在内存只能只有一份,普通函数在每个被调用中维持一份拷贝。

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

class Manager
{
private:
    Manager(){};    // 默认构造函数
    Manager(const Manager&);    // 复制构造函数
    const Manager& operator=(const Manager&);   // 重载赋值运算符

    string name;

public:
    static Manager& GetInstance()
    {
        static Manager manager;     // 只会构造一次
        return manager;
    }
}


继承

1. 构造顺序和析构顺序

构造子类时,基类对象在子类对象实例化之前被实例化,实例化基类对象时,首先实例化成员属性,再调用构造函数。

当实例准备退出作用域时,析构顺序与构造顺序正相反。

2. 继承方式

私有继承:意味着对于子类实例来说,基类的所有公有成员和方法都是私有的,不能从外部访问,即便是基类中的公有成员和方法都只能在子类中访问,无法通过子类实例访问。

从继承结构来看,公有继承具有is-a关系,私有继承更像has-a关系。

保护继承:保护继承也表示has-a关系,子类可以访问基类所有公有和保护成员,但是仍无法通过子类实例访问基类公有成员。

但是需要注意的是,仅当必要时才使用私有或保护继承,如果要突出has-a的关系,更好的选择是将基类实例作为子类实例的一个成员属性。这样做的好处是兼容性好,党关系发生变化时不需要去改变继承结构。

3. 切除(转换)问题

将子类实例赋值给基类实例时(显示赋值或参数传递时),编译器只复制子类实例的基类部分,将会丢失子类特有的信息。如果要避免切除问题,不要按值传递参数,赢以指向基类的指针或const引用的方式传递。

#include <iostream>
#include <string>
using namespace std;

/**
 * 基类
 */
class HuMan
{
public:
    int age;
    string name;

    HuMan(int mAge, const string &mName):age(mAge),name(mName)
    {
        cout << "HuMan构造函数" << endl;
    }

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

};

/**
 * 子类
 */
class Student: public HuMan
{
public:
    int grade;  // 子类特有的属性

    Student(int mAge, const string &mName, int mGrade) : HuMan(mAge, mName),grade(mGrade) {
        cout << "Student构造函数" << endl;
    }

    void SayHello() override {
        cout << "我是学生:" << name << endl;
    }
};

int main() {
    Student student(10, "miles", 3);    // 子类实例
    student.SayHello();     // 我是学生:miles
    HuMan huMan = student;      // 子类实例赋值为基类实例,切除问题
    huMan.SayHello();       // 我的名字是:miles,年龄是10
    return 0;
}

由上诉代码可见,将子类实例student赋值为基类实例huMan时,发生了“切除问题”,只复制了基类部分,子类的“grade”和重写方法“SayHello()”丢失,无法调用。


多态

多态是面向对象编程的一种特征,在程序中实现以一种方式(一个代码或一个函数)处理不同的类型相似(有公共父类)的实例。例如使用父类的指针指向子类实例,调用子类的方法。

但是在c++中,如果将子类实例传入形参为父类指针或引用的函数,在函数中参数只会被看作是父类,无法使用父类指针或引用调用原子类实例的方法,无法实现我们想要的多态,为了实现多态,我们需要使用虚函数

1. 虚函数

虚函数是应该在子类中重写的成员函数,当使用基类指针或引用来引用子类实例时,可以通过指针或引用调用相应子类实例成员。

#include <iostream>
#include <string>
using namespace std;

/**
 * 基类
 */
class HuMan
{
public:
    int age;
    string name;

    HuMan(int mAge, const string &mName):age(mAge),name(mName)
    {
        cout << "HuMan构造函数" << endl;
    }

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

};

/**
 * 子类
 */
class Student: public HuMan
{
public:
    int grade;  // 子类特有的属性

    Student(int mAge, const string &mName, int mGrade) : HuMan(mAge, mName),grade(mGrade) {
        cout << "Student构造函数" << endl;
    }

    void SayHello() {
        cout << "我是学生:" << name << endl;
    }
};

void Introduce(HuMan& huMan){
    huMan.SayHello();
}

int main() {
    Student student(10, "miles", 3);    // 子类实例
    Introduce(student);     // 我的名字是:miles,年龄是10
    return 0;
}

如上所示,在父类方法中没有使用虚函数,虽然在main()函数中传入了子类实例,但是在函数Introduce(HuMan&)中调用的是父类的SayHello()方法。

为了实现多台行为,我们可以在父类方法上加上virtual关键字,可以确保编译器调用方法的覆盖版本。

需要注意的是,基类的析构函数也应该设置为虚函数。考虑如下情景:一个函数接受的参数是父类指针或引用,如果传入的是通过new声明的子类实例,那么在函数中对该指针或引用调用delete则只能调用基类的析构函数,不会调用子类的析构函数。这样可能会导致资源未释放、内存泄露等问题。所以为了避免这种问题应该将析构函数声明为virtual。

2. 虚函数的工作原理-虚函数表

如何在向上转型后仍然可以在运行时调用对应子类的成员?上面提到的虚函数方式可以解决这个问题,而虚函数则是通过虚函数表来实现的。

例如有一个基类Base和一个子类Son,Son中覆盖了Base的部分虚函数,那么在实例化对象时,将会创建一个隐藏的指针VFT*(virtual function table),可将其视为一个包含函数指针的静态数组,其中每个指针都指向一个虚函数,而子类中没有覆盖的函数指针则会指向父类对应的虚函数。

所以,在进行向上转型后实例仍然保留着VFT指针,可以找到覆盖的方法。

3. 菱形问题

假设现在有三层继承结构:基类Base,子类Son1、Son2、Son3,子子类GrandSon。其中GrandSon继承于三个子类Son1~3,子类Son1~3都继承于Base。如果在程序中创建一个GrandSon实例,会隐式创建三个父类实例:son1、son2、son3 。这不仅使程序运行效率大大降低,也降低了内存使用效率:

#include <iostream>
using namespace std;

/**
 * 基类
 */
class Base
{
public:
    int age;

    Base(): age(10) {
        cout << "base 构造器" << endl;
    }
    Base(int age) : age(age) {
        cout << "base 构造器" << endl;
    }
};

/**
 * 子类
 */
class Son1: public Base
{
public:
    int grade;

    Son1(int age, int grade) : Base(age), grade(grade)
    {
        cout << "son1 构造器" << endl;
    }

};

class Son2: public Base
{
public:
    int grade;

    Son2(int age, int grade) : Base(age), grade(grade)
    {
        cout << "son2 构造器" << endl;
    }

};

class Son3: public Base
{
public:
    int grade;

    Son3(int age, int grade) : Base(age), grade(grade)
    {
        cout << "son3 构造器" << endl;
    }

};

/**
 * 子子类
 */
 class GrandSon: public Son1, public Son2, public Son3
 {
 public:
     GrandSon(int age, int grade, int age1, int grade1, int age2, int grade2) : Son1(age, grade), Son2(age1, grade1),
                                                                                Son3(age2, grade2)
     {
         cout << "grandSon 构造器" << endl;
     }

 };

int main() {
    Son1 son1(10,10);
    cout << "sizeof son: "<< sizeof(son1) << endl;
    GrandSon grandSon(10,10,10,10,10,10);
    cout << "sizeof grandson:" << sizeof(grandSon) << endl;
    return 0;
}

执行输出:

base 构造器
son1 构造器
sizeof son: 8
base 构造器
son1 构造器
base 构造器
son2 构造器
base 构造器
son3 构造器
grandSon 构造器
sizeof grandson:24      // 24 = 8 * 3

由上诉代码和输出结果可以看出,GrandSon实例的创建会引发三个父类的实例的创建。解决方式是在Son类中使用虚继承:

class Son1: public virtual Base
{
public:
    int grade;

    Son1(int age, int grade) : Base(age), grade(grade)
    {
        cout << "son1 构造器" << endl;
    }

};

输出为:

base 构造器
son1 构造器
sizeof son: 12
base 构造器
son1 构造器
son2 构造器
son3 构造器
grandSon 构造器
sizeof grandson:28

由输出可知,实例化GrandSon类虽然实例化三个父类Son1~3,但是只实例化一次Base(Son1~3的公共父类)。此外,发现son1的sizeof为12,比不使用虚继承时增加4字节,这里多出来的4字节推测与虚继承有关,可以参考上面的虚函数表指针来理解。

文章永久链接:https://tech.souyunku.com/42503

未经允许不得转载:搜云库技术团队 » 类与对象

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们