【C++进阶学习】第二弹——继承(下)——挖掘继承深处的奥秘

2024-06-18 04:44

本文主要是介绍【C++进阶学习】第二弹——继承(下)——挖掘继承深处的奥秘,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

继承(上):【C++进阶学习】第一弹——继承(上)——探索代码复用的乐趣-CSDN博客

前言:

在前面我们已经讲了继承的基础知识,让大家了解了一下继承是什么,但那些都不是重点,今天,我们一起来挖掘一下继承底层的一些知识和一些极容易出错的点

目录

一、隐藏

1.1 隐藏的概念

1.2 隐藏的两种类型

二、派生类的默认成员函数

三、继承与友元

四、继承与静态成员

五、总结


一、隐藏

1.1 隐藏的概念

在 C++ 中,继承是一种机制,使得子类可以继承父类的成员变量和成员函数。然而,当子类中出现和父类同名的成员变量或成员函数时,会发生一种特殊的现象,即隐藏

隐藏是指:如果子类中出现了与父类同名的成员变量或成员函数,则子类中的这个成员会“隐藏”父类中的同名成员,使得父类中的同名成员在子类中不可见。

1.2 隐藏的两种类型

具体来说,有以下两种情况:

成员变量隐藏:
如果子类中出现了和父类同名的成员变量,则子类中的这个成员变量会隐藏父类中的同名成员变量。例如:

class Parent {
public:int a;
};class Child : public Parent {
public:int a; // 此处的 a 会隐藏 Parent 中的 a
};int main() {Child c;c.a = 10; // 此处修改的是 Child 中的 a,而不是 Parent 中的 areturn 0;
}

成员函数隐藏:
如果子类中出现了和父类同名的成员函数,则子类中的这个成员函数会隐藏父类中的同名成员函数。例如:

class Parent {
public:void func() {cout << "Parent::func()" << endl;}
};class Child : public Parent {
public:void func() {cout << "Child::func()" << endl;}
};int main() {Child c;c.func(); // 此处调用的是 Child 中的 func(),而不是 Parent 中的 func()return 0;
}

需要注意的是,虽然子类中的成员会隐藏父类中的同名成员,但是父类中的成员仍然存在,只是在子类中不可见。如果想在子类中访问父类中被隐藏的成员,可以使用作用域运算符(::)来显式地指明要访问的成员所在的类。例如:

class Parent {
public:int a;
};class Child : public Parent {
public:int a;
};int main() {Child c;c.a = 10; // 此处修改的是 Child 中的 ac.Parent::a = 20; // 此处修改的是 Parent 中的 areturn 0;
}

总之,在 C++ 中的继承中,隐藏是一种特殊的机制,需要注意避免误用。

二、派生类的默认成员函数

在 C++ 中,当我们定义一个类时,可以省略掉其中的成员函数的实现,而直接在类定义的外部提供其实现。这种情况下,如果我们不提供任何实现,那么 C++ 编译器会自动为我们提供一个默认的构造函数、析构函数和拷贝构造函数和拷贝赋值运算符。

对于派生类来说,情况也是类似的。当我们定义一个派生类时,如果我们不提供任何构造函数,那么 C++ 编译器会自动为我们提供一个默认的构造函数,其构造函数的参数列表和父类的构造函数一致。例如:

class Parent {
public:Parent(int a) : m_a(a) {}int m_a;
};class Child : public Parent {
public:Child() : Parent(0) {} // 此处的构造函数会自动调用 Parent 的构造函数,并传入 0 作为参数
};

同时,如果我们没有提供任何析构函数,那么 C++ 编译器也会自动为我们提供一个默认的析构函数,其析构函数的函数体为空。例如:

class Parent {
public:~Parent() {}
};class Child : public Parent {
public:~Child() {} // 此处的析构函数会自动调用 Parent 的析构函数
};

需要注意的是,如果我们提供了任何一个构造函数或析构函数,那么 C++ 编译器就不会再为我们提供默认的构造函数或析构函数了。这时如果我们需要使用默认的构造函数或析构函数,需要我们自己显式地提供。(析构顺序为先派生类再基类)

另外,对于拷贝构造函数和拷贝赋值运算符来说,如果我们没有提供任何拷贝构造函数和拷贝赋值运算符,那么 C++ 编译器会自动为我们提供一个默认的拷贝构造函数和拷贝赋值运算符,其行为是浅拷贝(Shallow Copy),即直接拷贝成员变量的值。例如:

class Parent {
public:Parent(int a) : m_a(a) {}int m_a;
};class Child : public Parent {
public:Child(int a, int b) : Parent(a), m_b(b) {}int m_b;
};int main() {Child c1(1, 2);Child c2(c1); // 此处调用的是 Child 的默认拷贝构造函数,直接拷贝 m_a 和 m_b 的值return 0;
}

但是,如果我们的类中有指针类型的成员变量,那么默认的拷贝构造函数和拷贝赋值运算符就会出现问题,因为它们只会拷贝指针的值,而不会拷贝指针所指向的内存。这时我们需要自己提供一个拷贝构造函数和拷贝赋值运算符,实现深拷贝(Deep Copy)。例如:

class Parent
{
public:Parent(int a):_a(a){}int _a;
};
class Child :public Parent
{
public:int* _b;Child(int a, int b):Parent(a),_b(new int(b))         //深度拷贝{}~Child(){delete _b;_b = nullptr;}Child(const Child& c):Parent(c){_b = new int(*c._b);}Child& operator=(const Child& c){if (this != &c){_a = c._a;delete _b;_b = new int(*(c._b));			}return *this;}void Print(){cout << "_a:" << _a << " " << "_b:" << *_b << endl;}
};
int main()
{Child c1(1, 2);     Child c2(c1);      //此处调用的是c2的深度拷贝c2.Print();Child c3 = c1;c3.Print();return 0;
}

运行结果:

我们通过两张图来总结一下:

三、继承与友元

在C++中,友元关系不能被继承,因为友元关系是独立于类定义的,并不是类的成员。因此,如果在父类中声明了一个友元函数或友元类,子类无法继承这种关系。

下面是一些相关知识点:

1、友元函数不能是成员函数:友元函数不是类的成员函数,因此不能使用this指针,也不能直接访问类的私有成员。需要通过类的对象或引用来访问私有成员。
2、友元关系不具有传递性:如果A类声明了B类为友元,则B类不会自动成为A类的友元。
友元函数可以是模板函数:模板函数可以被声明为类的友元,这样模板函数可以访问类的私有成员。
3、友元类:如果一个类声明另一个类为友元,则该友元类的所有成员函数都可以访问原类的私有成员。
4、友元不能继承:由于友元关系不是类的成员,因此不能被继承。如果在父类中声明了一个友元函数或友元类,子类无法继承这种关系。但子类可以在自己的范围内重新声明该友元关系。
示例:

#include<iostream>
#include<string>
using namespace std;
class Base {
public:	friend void friendFunction(Base&);
protected:int value;
};class Derived : public Base {
public:// friendFunction 在 Derived 中不是友元函数,需要重新声明friend void friendFunction(Derived&);
};void friendFunction(Base& base) {// 可以访问 Base 的私有成员base.value = 10;
}void friendFunction(Derived& derived) {// 可以访问 Derived 的私有成员derived.value = 20;
}int main() {Derived derived;friendFunction(derived);return 0;
}

在上面的示例中,由于友元关系不能被继承,因此在Derived类中需要重新声明friendFunction函数为友元函数,以便在Derived类的实例上调用该函数。

四、继承与静态成员

在 C++ 中,类可以包含静态成员变量和静态成员函数,其中静态成员变量属于类本身,而不是类的某个对象,因此它们可以在不创建类对象的情况下被访问。

当一个类继承另一个类时,子类可以继承其父类的静态成员,并且可以在子类中重新定义这些静态成员。在这种情况下,子类和父类各自拥有自己的静态成员变量,它们之间没有任何关系。

下面是一个简单的例子:

#include<iostream>
using namespace std;class Parent {
public:static int a;
};int Parent::a = 10;      //静态成员的定义只能在类外进行class Child : public Parent {
public:static int a;      //类中只能声明静态成员
};int Child::a = 20;       //静态成员的定义只能在类外进行int main() {cout << Parent::a << endl;    //输出10cout << Child::a << endl;     //输出20return 0;
}

运行结果:

在上面的例子中,Parent 类和 Child 类都有一个静态成员变量 a,它们各自拥有自己的实现。在 main 函数中,我们可以直接通过类名来访问这些静态成员变量。

       需要注意的是,如果子类中没有重新定义父类的静态成员变量,那么子类可以直接访问父类的静态成员变量,例如 Parent::a。如果子类重新定义了父类的静态成员变量,那么子类只能访问自己的静态成员变量,例如 Child::a。

       此外,静态成员函数也可以继承,并且可以在子类中重新定义。在子类中重新定义父类的静态成员函数时,子类的静态成员函数会隐藏父类的静态成员函数,因此如果在子类中需要调用父类的静态成员函数,需要使用作用域运算符 :: 来显式地调用。

       还有一个需要注意的点就是,类中只能声明静态成员,静态成员的定义只能在类外进行。

总之,在 C++ 中,静态成员在继承中的行为与普通成员有所不同,需要注意其使用方法。

五、总结

以上就是C++继承中需要额外注意的点,此外,还有一个很重要的知识点我们还没讲到——多继承、菱形继承、虚拟继承,这几个知识点是有很大关联性的,且我们在平时使用继承时也很容易出错,鉴于篇幅问题,这几个问题会在下一篇单拎出来来讲,今天的内容就到此为止。

感谢各位大佬观看,创作不易,还请各位大佬一键三连!!!

这篇关于【C++进阶学习】第二弹——继承(下)——挖掘继承深处的奥秘的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1071325

相关文章

利用c++判断水仙花数并输出示例代码

《利用c++判断水仙花数并输出示例代码》水仙花数是指一个三位数,其各位数字的立方和恰好等于该数本身,:本文主要介绍利用c++判断水仙花数并输出的相关资料,文中通过代码介绍的非常详细,需要的朋友可以... 以下是使用C++实现的相同逻辑代码:#include <IOStream>#include <vec

基于C++的UDP网络通信系统设计与实现详解

《基于C++的UDP网络通信系统设计与实现详解》在网络编程领域,UDP作为一种无连接的传输层协议,以其高效、低延迟的特性在实时性要求高的应用场景中占据重要地位,下面我们就来看看如何从零开始构建一个完整... 目录前言一、UDP服务器UdpServer.hpp1.1 基本框架设计1.2 初始化函数Init详解

C++ 右值引用(rvalue references)与移动语义(move semantics)深度解析

《C++右值引用(rvaluereferences)与移动语义(movesemantics)深度解析》文章主要介绍了C++右值引用和移动语义的设计动机、基本概念、实现方式以及在实际编程中的应用,... 目录一、右值引用(rvalue references)与移动语义(move semantics)设计动机1

C++ move 的作用详解及陷阱最佳实践

《C++move的作用详解及陷阱最佳实践》文章详细介绍了C++中的`std::move`函数的作用,包括为什么需要它、它的本质、典型使用场景、以及一些常见陷阱和最佳实践,感兴趣的朋友跟随小编一起看... 目录C++ move 的作用详解一、一句话总结二、为什么需要 move?C++98/03 的痛点⚡C++

详解C++ 存储二进制数据容器的几种方法

《详解C++存储二进制数据容器的几种方法》本文主要介绍了详解C++存储二进制数据容器,包括std::vector、std::array、std::string、std::bitset和std::ve... 目录1.std::vector<uint8_t>(最常用)特点:适用场景:示例:2.std::arra

C++构造函数中explicit详解

《C++构造函数中explicit详解》explicit关键字用于修饰单参数构造函数或可以看作单参数的构造函数,阻止编译器进行隐式类型转换或拷贝初始化,本文就来介绍explicit的使用,感兴趣的可以... 目录1. 什么是explicit2. 隐式转换的问题3.explicit的使用示例基本用法多参数构造

C++,C#,Rust,Go,Java,Python,JavaScript的性能对比全面讲解

《C++,C#,Rust,Go,Java,Python,JavaScript的性能对比全面讲解》:本文主要介绍C++,C#,Rust,Go,Java,Python,JavaScript性能对比全面... 目录编程语言性能对比、核心优势与最佳使用场景性能对比表格C++C#RustGoJavapythonjav

C++打印 vector的几种方法小结

《C++打印vector的几种方法小结》本文介绍了C++中遍历vector的几种方法,包括使用迭代器、auto关键字、typedef、计数器以及C++11引入的范围基础循环,具有一定的参考价值,感兴... 目录1. 使用迭代器2. 使用 auto (C++11) / typedef / type alias

C++ scoped_ptr 和 unique_ptr对比分析

《C++scoped_ptr和unique_ptr对比分析》本文介绍了C++中的`scoped_ptr`和`unique_ptr`,详细比较了它们的特性、使用场景以及现代C++推荐的使用`uni... 目录1. scoped_ptr基本特性主要特点2. unique_ptr基本用法3. 主要区别对比4. u

C++11中的包装器实战案例

《C++11中的包装器实战案例》本文给大家介绍C++11中的包装器实战案例,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录引言1.std::function1.1.什么是std::function1.2.核心用法1.2.1.包装普通函数1.2.