面向对象的三大特性 - 封装、继承

C++ 中的面向对象三大特性是:封装(Encapsulation)继承(Inheritance)多态(Polymorphism)。这三者构成了面向对象编程(OOP)的核心理念。

封装

封装是将数据和操作数据的函数绑定在一起,并对外部隐藏实现细节,只暴露必要的接口。这种机制有助于提高代码的安全性和可维护性。实际上之前的学习,基本上都属于封装的体现。

通过 privateprotectedpublic 控制访问权限,以限制外部直接访问内部状态,并提供 getter/setter 函数访问或修改私有数据。

代码如下:

class Student {
private:
    string _name;
    int _age;

public:
    Student(const string& name, int age);
    void set_name(const string& name);
    void set_age(int age);
    string get_name();
    int get_age();
};

Student::Student(const std::string &name, int age) {
    this->_name = name;
    this->_age = age;
}

void Student::set_name(const std::string &name) {
    this->_name = name;
}

string Student::get_name() {
    return this->_name;
}

void Student::set_age(int age) {
    this->_age = age;
}

int Student::get_age() {
    return this->_age;
}

封装的核心是 隐藏细节,对外提供接口,有助于防止非法访问,提高模块独立性。

继承

C++ 中的继承是类与类之间的关系,是一个很简单很直观的概念,与现实世界中的继承类似,例如儿子继承父亲的财产。

继承是指在已存在类的基础上创建新类,这个新类就被称为 派生类。被继承的类称为 父类基类,继承的类称为 子类派生类子类父类 通常放在一起称呼,基类派生类 通常放在一起称呼。

在 C++ 中,派生(Derive)和继承是一个概念,只是站的角度不同。继承是儿子接收父亲的产业,派生是父亲把产业传承给儿子。

例如,人和狗都属于动物,因此,我们可以定义一个 Animal 类作为父类,HumanDog 作为子类;同理,学生和老师都属于人的职业,因此,我们可以定义 Human 类作为父类,StudentTeacher 作为子类,如下图所示:

继承的基本语法

子类(派生类)的声明方法如下:

class 子类名: 继承方式 父类名 {
    派生类新增的成员
};

继承方式包括:

  1. public:公有继承;
  2. private:私有继承(默认);
  3. protected:保护继承;

C++ 中继承关系非常复杂,3 种继承方式下又有 3 种访问限定符,那一共有 9 种继承方式,这也是 C++ 设计的一大缺陷。其实我们在使用中 99% 都使用的公有继承,几乎很少使用 protected/private 继承,也不提倡使用 protected/private 继承,因为 protected/private 继承下来的成员都只能在派生类的类中使用,实际中继承维护性不强。因此,为了降低学习难度,我们只讲 public 公有继承的方式。

继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如,HumanDog 类继承于 Animal 类,也就是 Animal 类的子类,那么 Human 类与 Dog 类就拥有 Animal 的成员变量和成员函数,示例如下:

#include <iostream>
using namespace std;

// 父类
class Animal {
public:
    int eyes;
    int legs;
    void eat() {
        cout << "吃饭" << endl;
    }

    void sleep() {
        cout << "睡觉" << endl;
    }
};

// Animal 子类 1
class Human: public Animal {};
// Animal 子类 2
class Dog: public Animal {};

int main() {
    Animal obj_1;
    obj_1.eat();

    Human obj_2;
    obj_2.sleep();

    Dog obj_3;
    obj_3.legs = 4;
    cout << "狗有 " << obj_3.legs << " 条腿" << endl;
    return 0;
}

由上述代码可知,子类可以继承父类的全部成员变量和方法,除了构造函数与析构函数

派生类除了拥有基类的成员,还可以定义自己的新成员,以增强类的功能。例如:狗会汪汪叫,人会工作,代码如下:

// Animal 子类 1
class Human: public Animal {
public:
    // 在子类中定义新成员
    void work() {
        cout << "工作" << endl;
    }
};
// Animal 子类 2
class Dog: public Animal {
public:
    // 在子类中定义新成员
    void bark() {
        cout << "汪汪汪" << endl;
    }
};

protected 受保护权限

以上两段代码中,所有的成员都是写在 public 中的,如果 Animal 父类的成员属性是写在 private 权限下,虽然可以调用公共成员,但是父类的私有属性在子类内部是无法直接访问的,代码如下:

// 父类
class Animal {
private:
    int _legs = 0;

public:
    // 私有属性 _legs 的 getter 函数
    int get_legs() {
        return this->_legs;
    }
};

// 子类
class Dog: public Animal {
public:
    void set_legs(int legs) {
        _legs = legs;
    }
};

此时可以发现,尽管我们可以在子类 Dog 中调用父类的公共成员 getter/setter 函数来获取或设置父类中的私有属性 legs,但是无法在子类 Dog 的内部调用父类 Animal 的私有属性。

这就涉及到了之前学习的访问控制权限,如下表所示:

访问控制关键字 权限 类与对象内部 类与对象外部 子类
public 公共 可以访问 可以访问 可以访问
private 私有 可以访问 不可以访问 不可以访问
protected 保护 可以访问 不可以访问 可以访问

这里,我们可以看到,私有成员在子类中是无法访问的,因此,可以将父类的访问权限,从私有权限 private 改成保护权限 protected

代码如下:

#include <iostream>
using namespace std;

// 父类
class Animal {
protected:
    int _legs = 0;

public:
    // 私有属性 _legs 的 getter 函数
    int get_legs() {
        return this->_legs;
    }
};

// 子类
class Dog: public Animal {
public:
    void set_legs(int legs) {
        _legs = legs;
    }
};

int main() {
    Animal obj_1;
    Dog obj_2;

    cout << "obj_1: " << obj_1.get_legs() << endl;

    obj_2.set_legs(2);
    cout << "狗有 " << obj_2.get_legs() << " 条腿" << endl;
    return 0;
}

继承机制下的构造函数与析构函数执行顺序

子类继承父类后,当创建子类对象时,也会调用父类的构造函数与析构函数。那么,如果子类也创建了自己的构造函数,此时父类与子类的构造和析构函数的执行顺序是谁先谁后呢?

示例如下:

// 父类
class Father {
public:
    Father() {
        cout << "调用 父类 构造函数" << endl;
    }

    ~Father() {
        cout << "调用 子类 析构函数" << endl;
    }
};

// 子类
class Son: public Father {
public:
    Son() {
        cout << "调用 父类 构造函数" << endl;
    }

    ~Son() {
        cout << "调用 子类 析构函数" << endl;
    }
};


int main() {
    // 创建对象
    Son obj_1;
    return 0;
}

由以上代码运行结果可以得出:继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反

同名成员

如果子类中的成员(包括成员变量与成员函数)和父类中的成员出现同名的情况,程序是如何处理的呢?

先说结论:

  1. 子类中的同名成员可以直接访问;
  2. 父类中的同名成员需要加上作用域前缀,才可以访问。

示例如下:

#include <iostream>
using namespace std;


// 父类
class Father {
public:
    int a = 1;

    void func() {
        cout << "调用 父类 func 函数" << endl;
    }
};

// 子类
class Son: public Father {
public:
    int a = 2;

    void func() {
        cout << "调用 子类 func 函数" << endl;
    }
};

int main() {
    // 创建子类对象
    Son obj_1;

    // 调用同名属性
    cout << "子类中 a: " << obj_1.a << endl;
    cout << "父类中 a: " << obj_1.Father::a << endl;

    // 修改 a 的值
    obj_1.a = 5;
    cout << "子类中 a: " << obj_1.a << endl;
    cout << "父类中 a: " << obj_1.Father::a << endl;

    // 调用同名方法
    obj_1.func();
    obj_1.Father::func();

    return 0;
}

还有一点需要注意的是,子类中定义的函数是不能重载父类中的同名函数的。这是因为父类与子类属于两个不同的作用域,而函数重载要求必须发生在同一作用域中。