2-2 C++面向对象特性

前言

本章的内容围绕C++面向对象的三大特性:封装、继承、多态进行展开介绍,每节分别讲解面向对象特性的概念、意义、表现方式和实现原理,。

1. 封装

面向对象程序设计与面向过程程序设计的不同之处在于,面向过程程序设计要分析出解决问题所需要的步骤,将涉及到的动作归纳成函数并按照相应的逻辑顺序调用;而面向对象程序设计从面临的问题中抽象出多个类(class),类中描述出这个类在解决问题的过程中的属性(成员变量)和动作(成员函数)。因此,我们把客观的事物抽象成类这个行为称为封装,封装类的目的不是为了完成一个步骤,而是为了描述某个事物在解决问题中的属性和行为。类可以实例化成对象,并且通过设置访问限定符(未指明时默认为private)把属性和动作向外部进行不同程度的开放和隐藏。

访问限定符 作用
public 公有权限,可以在类的外部访问
private 私有权限,仅能在类的内部访问
protected 受保护权限,能访问private成员处一定可以访问protected成员,在第二节继承处继续讨论受保护权限的意义

举个例子:要实现一个学生管理系统,经需求分析需要封装一个student类、一个学生管理类(先忽略该类的实现),student类具有私有(private,私有的属性/方法只能在类内部访问)的成员变量m_name和公有(public,公有的属性/方法可以在类的外部进行操作)的成员函数getName()、setName()。在main()函数中构建类student的对象实例,并操作对象的属性或方法。在类的外部,例如main函数中无需关注该类内部如何实现。因此封装的意义可总结为:使代码模块化,对外部隐藏实现细节。

class stu_manager;  // 忽略实现
class student
{
private:
    std::string m_name;
public:
    std::string getName()
    {
        return m_name;
    }
    void setName(const std::string name)
    {
        m_name = name;
    }
};

int main()
{
    student s1;
    s1.setName("Evila");
    std::cout<<s1.getName()<<std::endl;  // Evila
    return 0;
}

1.1 类的构造函数

类(class)是某个事物在解决问题中属性和行为的封装产物,类的属性由成员变量表达,类的行为由成员函数表达;构造函数是类中较为特殊的成员函数,构造函数在每次类实例化对象时调用。

  • 构造函数的函数名与类名一致,没有返回值,一般用于赋值类的成员变量。若类没有定义构造函数,则编译器会自动定义默认的构造函数。进入构造函数意味着编译器已经认识了对象的内存结构并分配了存储空间,构造函数的作用是对成员变量进行赋值或申请资源,因此在进入类的构造函数之前会先构造类中的对象成员变量。
class student
{
public:
    student() { m_SchoolName = "牛客大学"; }  // 类的构造函数用于赋值成员变量m_SchoolName的值
private:
    std::string m_SchoolName;
};
  • 默认的构造函数不带有参数,但我们可以实现带参数的构造函数,这使得在实例化对象时可以通过传递参数来更方便的赋值对象的成员变量。
class student
{
public:
    student() { m_SchoolName = "牛客大学"; }  // 默认的构造函数将成员变量m_SchoolName赋值为“牛客大学”
    // 带参数的构造函数将成员变量m_SchoolName赋值为传入的参数
    student(std::string schoolName) { m_SchoolName = schoolName; }  
private:
    std::string m_SchoolName;
};
  • 初始化列表用于初始化成员变量列表,在构造函数后使用冒号':'和成员变量列表进行初始化可以避免1次构造函数调用。相对于构造函数,初始化列表是真正的初始化动作,构造函数则是赋值,初始化列表工作在构造函数之前。类中const成员变量、引用类型、没有默认构造函数的其他类类型必须在初始化列表中进行初始化动作。
class student
{
public:
    student() { m_SchoolName = "牛客大学"; }  // 默认的构造函数将成员变量m_SchoolName赋值为“牛客大学”
    // 使用初始化列表初始化成员变量
    student(std::string schoolName) : m_SchoolName(schoolName), m_ClassName("student") {}
private:
    std::string m_SchoolName;
    const std::string m_ClassName;  // const成员需要在初始化列表中进行初始化
};

注意:成员变量在使用初始化列表初始化的顺序,与类中声明成员变量的顺序有关。这是因为类中的成员变量的内存排列顺序在编译期间就确定了,初始化列表的初始化顺序是按照成员变量内存排列顺序进行。若我们编写的初始化列表中的成员变量顺序与类中定义的顺序不一致,则可能会导致顺序错乱的成员变量初始化失败。

class Test
{
public:
    Test() : y(0), x(y + 2) 
    {
        cout<<x<<endl<<y<<endl; // x = 32768 y = 0 x初始化失败
    } 

private:
    int64_T x;
    int y;
};
  • 拷贝构造函数 拷贝构造函数是较为特殊的构造函数,一般用于复制其他对象的成员变量来构造新的对象。如果类中没有定义拷贝构造函数,则编译器会定义默认的拷贝构造函数,默认的拷贝构造函数会复制传入对象的成员变量并赋值给当前实例化的新对象。

注意:若类中定义了指针并动态申请了内存,则该类必须显示定义拷贝构造函数以表明指针如何拷贝。否则,若类未定义拷贝构造函数,编译器会自动实现默认的拷贝构造函数,默认的拷贝构造函数中对指针成员的拷贝是浅拷贝,那么当拷贝构造的对象或被拷贝的对象析构时,二者的成员指针申请的内存资源会一起被释放。

class student
{
public:
    student()
    {
        m_SchoolName = "牛客大学"; // 默认的构造函数将成员变量m_SchoolName赋值为“牛客大学”
        score_list = new int[10]; // 指针动态申请10个int变量所需的内存
    }  
    // 使用初始化列表初始化成员变量
    student(std::string schoolName) : m_SchoolName(schoolName), m_ClassName("student"), score_list(nullptr) {}
    // 拷贝构造函数
    student(const student& stu)
    {
        m_SchoolName = stu.m_SchoolName;    // 将stu的变量值复制给当前构造的实例
        score_list = new int[10];    // 指针成员动态申请10个int变量所需的内存
        // 深拷贝
        for (int i = 0; i < 10; i++)
        {    
            score_list[i] = stu.score_list[i]; // 循环复制score_list中的值
        }
    }
private:
    std::string m_SchoolName;
    const std::string m_ClassName;  // const成员需要在初始化列表中进行初始化
    int* score_list;
};

1.2 类的析构函数

与构造函数类似的,类的析构函数也是类的成员函数,它在类的对象生命周期结束或动态分配的对象被删除时自动调用。析构函数用于释放对象所获取的资源,特别是若对象中有指针类型的成员变量,且动态申请了内存,析构函数中一定要释放动态申请的内存以避免内存泄漏。

  • 析构函数的名字同样与类名相同,但增加了(~)作为前缀。
  • 析构函数没有参数和返回值。
  • 析构函数不能被重载、不能被主动调用。
  • 若类没有定义析构函数,则编译器会定义一个默认的析构函数。在对象的析构过程中,对象的所有非static的对象类型成员变量的析构函数都会被调用。
class student
{
public:
    student()
    {
        m_SchoolName = "牛客大学"; // 默认的构造函数将成员变量m_SchoolName赋值为“牛客大学”
        score_list = new int[10]; // 指针动态申请10个int变量所需的内存
    }  
    ~student() 
    {
        delete[] score_list; // 析构函数 释放动态申请的内存资源
    }
private:
    std::string m_SchoolName;
    const std::string m_ClassName;  // const成员需要在初始化列表中进行初始化
    int* score_list;
};

1.3 this指针与成员函数调用

1.3.1 对象内存布局

类中非static成员变量被顺序置于对象内存中,对象占的内存空间则只计算非static成员变量的内存大小,static成员变量则置于全局静态存储区。static与非static成员函数不置于对象内存空间中,而是与普通函数一样置于代码段,仅有一份内存实例,而不是每个类都有成员函数的拷贝。

1.3.2 this指针

由于每个对象的存储空间只包括该对象的成员变量,不包括成员函数代码所占有的空间,即同一个类的所有对象调用的成员函数代码只是单独一份,那么成员函数是如何辨别当前调用自己的是哪一个对象呢?

C++引入了this自引用指针,当使用类实例化对象时,编译器生成this指针指向实例化对象的内存地址,this指针的值为对象的起始地址。 注意:一个对象的this指针并不是对象本身的一部分,不会影响该对象sizeof()的结果。例如,对一个空类的对象执行sizeof(),得到其内存大小是1,而不是4(一个指针的内存大小)。至于为什么空类实例化的对象占用内存大小是1,原因是每个实例对象在内存中都应该有独一无二的内存地址,空类实例化对象时被编译器插进去的一个char,使得这个类的不同对象在内存中都有有独一无二的内存地址。

this作用域是在类内部,当类的非静态成员函数被调用时,编译器会自动将对象的this指针作为隐含参数加入到参数列表中。在非静态成员函数访问非静态成员变量都是通过this指针完成。

1.4 面试考点

1.4.1 面试考点:请介绍面向对象封装的概念、意义和实现机制。

【出现频度】★★★★ 【难度】☆☆ 【参考答案】

  • 概念:类是为了描述某个事物在解决问题中的属性和行为,把客观的事物抽象成类这个行为称为封装。
  • 意义:封装使代码模块化,对外部隐藏实现细节
  • 实现机制:封装将成员变量和成员函数聚集在类中,通过访问限定符限制类成员在类内部和外部的访问权限。类的成员函数有两个特殊形式,即类的构造函数和析构函数。构造函数在类的对象实例化时被调用,一般用于赋值成员变量;析构函数在类的对象生命周期结束时被调用,一般用于释放该对象申请的资源。

2. 继承

继承可以根据一个类去定义另一个类,已有的类被称为基类(父类),继承得到的类称为派生类(子类),派生类在继承后就获得了基类的成员变量和成员函数。继承使得已存在的类能够被扩展,代码得到重用。

2.1 继承的访问控制权限

在第一节封装中,介绍了类成员的public和private访问控制权限,它们限制了类的成员能够被访问的位置。在类的继承过程中,需要指明继承方式是public、protected 或 private,这使得派生类访问基类的成员被限制,派生类在三种继承方式下对基类成员的访问权限总结如下:

基类成员权限 public protected private
公有继承(public) public:派生类可以访问 protected:派生类可以访问 private:派生类禁止访问
保护继承(protected) protected:派生类可以访问 protected:派生类可以访问 private:派生类禁止访问
私有继承(private) private:派生类可禁止访问 private:派生类禁止访问 private:派生类禁止访问

2.2 继承的基本特性

在定义类时,使用继承列表来指明继承自的基类和继承方式,若未指明继承方式则默认为private私有继承方式。基类的构造函数、析构函数、拷贝构造函数与基本重载的运算符不会被继承。

构造/析构函数的调用顺序 派生类在继承自基类后,派生类实例化对象时会执行如下的构造步骤:(1)调用基类的构造函数;(2)派生类中对象成员的构造函数;(3)派生类的构造函数,析构步骤则将构造顺序反转。

class A
{
public:
    A()
    {
        cout<<"A construct"<<endl;
    }
    ~A()
    {
        cout<<"A destructor"<<endl;
    }
};
class person
{
public:
    person()
    {
        cout<<"person construct"<<endl;
    }
    ~person()
    {
        cout<

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++岗面试真题解析 文章被收录于专栏

<p> C++工程师面试真题解析! </p> <p> 邀请头部大厂创作者<a href="https://www.nowcoder.com/profile/73627192" target="_blank">@Evila</a> 及牛客教研共同打磨 </p> <p> 助力程序员的求职! </p>

全部评论
{"pureText":""}
点赞 回复 分享
发布于 2022-07-01 02:30

相关推荐

不愿透露姓名的神秘牛友
11-21 19:05
点赞 评论 收藏
分享
点赞 2 评论
分享
牛客网
牛客企业服务