【C++】面向对象编程的三大特性:深入解析继承机制(二)

简介: 【C++】面向对象编程的三大特性:深入解析继承机制

【C++】面向对象编程的三大特性:深入解析继承机制(一)https://developer.aliyun.com/article/1617388


2.7 继承中作用域

2.7.1 继承体系相关知识

  1. 在继承体系中基类和派生类都有独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的访问,这种情况叫隐藏(重定义)-在子类成员函数中,可以使用 基类::基类成员 显式访问
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构造隐藏
  4. 注意在实际中在继承体系里面最好不要定义同名的成员,很容易混洗

2.7.2 继承体系的隐藏

提起作用域,就会想到域起到名字隔离的作用,不同的域可以有同样的名字,但是同一个域不能有同一个名字(函数重载比较特殊,但是变量不可以)

  • 全局/局部变量 - 影响生命周期
  • 类/命名空间域 -不影响生命周期
2.7.2.1 成员变量隐藏
class Person
{
public:
       
private:
    string _name = "xiaoming";
};
class Student : public Person
{
public:
    void Print()
    {
        cout << _name << endl;
    }
private:
    string _name = "zhangsan";
};
int main()
{
    Student st;
    st.Print();//张三
    return 0;
}

具体说明:

  • 关于上述代码,Student继承Person,导致Student中有两个_name。由于继承不是将父类数据拷贝一份给子类,实际上Student只有一个 _name还有一个是父类的
  • 这里父类和子类 _name构成隐藏关系,虽然代码可以跑,但是容易混洗,在继承体系中尽量不要定义同名的成员。
  • 这里会默认优先访问子类的 _name,如果想要访问父类中该成员,可以使用指定类域限定符。如果子类中没有该成员,将会去父类中查找
2.7.2.2 成员函数隐藏
class A
{
    public:
    void fun()
    {
        cout << "func()" << endl;
    }
};
class B : public A
{
    public:
    void fun(int i)
    {
        A::fun();
        cout << "func(int i)->" <<i<<endl;
    }
};
void Test()
{
    B b;
    b.fun(10);
};

这里这段代码说明一件事, B中的fun和A中的fun不是构成重载,因为不是在同一作用域。B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。

2.8 派生类的默认成员函数

2.8.1 默认构造函数

按照继承体系,子类中成员可以分为三类:子类内置类型成员、子类自定义类型成员以及父类成员,父类成员可以看出一个自定义类型的整体,这三类成员都会走初始化列表。

//父类
Person(const char* name = "peter")
    :_name(name)
    {
        cout << "Person()" << endl;
    }
//子类
Student(const char* name, int num)
    :Person(name)
        ,_num(num)
    {
        cout << "Student(int num)" << endl;
    }

具体说明:

在初始化步骤中,内置类型不进行处理,自定义类型会调用构造构造,父类也会调用自身的拷贝构造。

需要注意:

初始化列表中是否显式初始化父类成员,不影响父类成员走初始化列表,这里父类成员将调用复用自身的构造函数,需要考虑自身参数匹配的问题。这边建议父类类中显式写个全缺省构造函数,类的调用父类构造函数初始化,自己初始化自己的变量,也可以避免参数对应不上,父类尽量都走初始化列表。

容易错误的地方:

  • 不能单独对父类成员进行初始化,而是对父类这个整体,参数需要对应上。
  • 在创建 Student 对象时,首先使用 name 参数来构造 Person 部分的对象。这个过程并不会产生匿名对象,而是直接在 Student 对象中嵌入的 Person 部分。

2.8.2 拷贝构造

具体说明:

  • 切割/切片是赋值兼容,编译器进行特殊处理,不产生临时对象将子拷贝给父。内置类型值拷贝,自定义类型调用拷贝构造,父类复用父类的拷贝构造

2.8.3 赋值运算符重载

//父类
Person& operator=(const Person& p)
{
    cout << "Person& operator=(const Person& p)" << endl;
    if (&p != this) _name = p._name;
    return *this;
}
//子类
Student& operator= (const Student& s)
{
    cout << "Student& operator= (const Student& s)" << endl;
    if (this != &s)
    {
        Person::operator=(s);
        _num = s._num;
    }
    return *this;
}

具体说明:

  • 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,隐藏。对此这里按照父类复用父类的赋值运算符重载的话,需要使用指定类域限定符直接到父类中调用

2.8.4 析构函数(有坑)

//父类
~Person()
{
    cout << "~Person()" << endl;
}
//子类
~Student()
{
    Person::~Person();
    cout << _name << endl;
    cout << "~Student()" << endl;
} 

坑点:

  1. 问题一
  • 子类析构里面如果显式写父类的析构就有问题,按照规定构造是先父后子,析构是先子后父,为了析构顺序是先子后父,子类析构函数结束后会自动调用父类析构,如果出现在子类析构中显式调用父类析构,无法保证先子后父的规定。
  1. 问题二:
  • 同时如果显式调用父类析构,那么父类对象的内存空间已经被释放,此时再去访问父类成员将会导致未定义的醒为或者程序崩溃。并且在调用完子类的析构会自动调用再次调用父类的析构,这种行为是未定义的,可能会导致内存泄漏或者程序崩溃。

子类的析构也会隐藏父类,根据多态的需要,析构函数名字会被统一处理成destructor

2.8.5 小结

  • 派生类的构造函数必须调用基类的构造函数(复用)初始化基类的那一部分成员,如果基类没有默认的构造函数,那么必须在派生类构造函数的初始化列表显式调用
  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
  • 派生类的operator=必须调用基类的operator=完成基类的复制
  • ↑↑↑以上都是说子类继承父类的那一部分,需要父类调用复用自己默认函数
  • 派生类的析构函数会在调用完成后完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
  • 派生类对象初始化先调用基类构造再调用派生类构造
  • 派生类对象析构清理先调用派生类再调用基类的析构

2.9 继承与友元

友元关系不能继承,也就是说基类友元一般不能访问子类私有和保护成员

class Student;
class Person
{
    public:
    friend void Display(const Person& p, const Student& s);
    protected:
    string _name; // 姓名
};
class Student : public Person
{
    protected:
    int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;
    cout << s._stuNum << endl;
}
void main()
{
    Person p;
    Student s;
    Display(p, s);
}

父类的友元不一定是子类的友元(父亲的朋友不一定是你的朋友),意味着友元关系不能继承。如果想要访问子类,可以添加友元。这里编译器是从上到下扫描的,所以需要先声明Student类

2.8 继承与静态成员

**基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,无论派生多少个子类,都只有一个static成员实例。**对此静态成员属于当前类,也属于基类。

那么利用该特性,子类的构造都需要调用父类的构造,可以统计父类有多少个派生类。

class Person
{
    public :
    Person () {++ _count ;}
    protected :
    string _name ; // 姓名
    public :
    static int _count; // 统计人的个数。
};
int Person :: _count = 0;
class Student : public Person
{
    protected :
    int _stuNum ; // 学号
};
class Graduate : public Student
{
    protected :
    string _seminarCourse ; // 研究科目
};
void TestPerson()
{
    Student s1 ;
    Student s2 ;
    Student s3 ;
    Graduate s4 ;
    cout <<" 人数 :"<< Person ::_count << endl;
    Student ::_count = 0;
    cout <<" 人数 :"<< Person ::_count << endl;
}


【C++】面向对象编程的三大特性:深入解析继承机制(三)https://developer.aliyun.com/article/1617390

相关文章
|
7月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
5月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
168 12
|
5月前
|
存储 监控 算法
基于 C++ 哈希表算法的局域网如何监控电脑技术解析
当代数字化办公与生活环境中,局域网的广泛应用极大地提升了信息交互的效率与便捷性。然而,出于网络安全管理、资源合理分配以及合规性要求等多方面的考量,对局域网内计算机进行有效监控成为一项至关重要的任务。实现局域网内计算机监控,涉及多种数据结构与算法的运用。本文聚焦于 C++ 编程语言中的哈希表算法,深入探讨其在局域网计算机监控场景中的应用,并通过详尽的代码示例进行阐释。
113 4
|
7月前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
7月前
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
9月前
|
存储 算法 安全
基于红黑树的局域网上网行为控制C++ 算法解析
在当今网络环境中,局域网上网行为控制对企业和学校至关重要。本文探讨了一种基于红黑树数据结构的高效算法,用于管理用户的上网行为,如IP地址、上网时长、访问网站类别和流量使用情况。通过红黑树的自平衡特性,确保了高效的查找、插入和删除操作。文中提供了C++代码示例,展示了如何实现该算法,并强调其在网络管理中的应用价值。
|
9月前
|
安全 编译器 C++
C++ `noexcept` 关键字的深入解析
`noexcept` 关键字在 C++ 中用于指示函数不会抛出异常,有助于编译器优化和提高程序的可靠性。它可以减少代码大小、提高执行效率,并增强程序的稳定性和可预测性。`noexcept` 还可以影响函数重载和模板特化的决策。使用时需谨慎,确保函数确实不会抛出异常,否则可能导致程序崩溃。通过合理使用 `noexcept`,开发者可以编写出更高效、更可靠的 C++ 代码。
272 1
|
9月前
|
存储 程序员 C++
深入解析C++中的函数指针与`typedef`的妙用
本文深入解析了C++中的函数指针及其与`typedef`的结合使用。通过图示和代码示例,详细介绍了函数指针的基本概念、声明和使用方法,并展示了如何利用`typedef`简化复杂的函数指针声明,提升代码的可读性和可维护性。
252 1
|
10月前
|
编译器 PHP 开发者
PHP 8新特性解析与实战应用####
随着PHP 8的发布,这一经典编程语言迎来了诸多令人瞩目的新特性和性能优化。本文将深入探讨PHP 8中的几个关键新功能,包括命名参数、JIT编译器、新的字符串处理函数以及错误处理改进等。通过实际代码示例,展示如何在现有项目中有效利用这些新特性来提升代码的可读性、维护性和执行效率。无论你是PHP新手还是经验丰富的开发者,本文都将为你提供实用的技术洞察和最佳实践指导。 ####
177 1
|
10月前
|
数据安全/隐私保护 iOS开发 开发者
iOS 14隐私保护新特性深度解析####
随着数字时代的到来,隐私保护已成为全球用户最为关注的问题之一。苹果在最新的iOS 14系统中引入了一系列创新功能,旨在增强用户的隐私和数据安全。本文将深入探讨iOS 14中的几大隐私保护新特性,包括App跟踪透明度、剪贴板访问通知和智能防追踪功能,分析这些功能如何提升用户隐私保护,并评估它们对开发者和用户体验的影响。 ####

推荐镜像

更多
  • DNS