解锁C++多态的魔力:灵活与高效的编码艺术(上)

在这里插入图片描述

文章目录


前言

多态性是面向对象编程的重要特性之一,而C++通过虚函数、继承等机制实现了这一强大的功能。多态性使得代码更加灵活和可扩展,允许不同类型的对象以统一的方式进行操作。在本篇文章中,我们将深入探讨C++中多态的实现原理、使用场景及其优劣势,并通过具体代码示例展示如何利用多态来提升代码的可维护性和复用性。


🌸一、多态的定义与概念

**多态(Polymorphism)**是面向对象编程中的一个重要概念,字面意思是“多种形态”。在编程中,多态指的是使用相同的接口或方法名来操作不同类型的对象,从而实现不同的行为。它允许一个接口在不同的上下文中表现出不同的行为,增加了程序的灵活性和可扩展性。

🌻1.1 多态的核心思想:

多态性使得一个基类可以定义统一的接口,而不同的子类则提供具体的实现。在程序运行时,可以根据对象的实际类型选择调用适当的函数实现。这样做可以通过相同的代码处理不同类型的对象,而不必显式地指定它们的类型。

🌻1.2 多态的两种主要形式:

  1. 编译时多态(静态多态):通过函数重载和运算符重载来实现,编译器在编译时决定调用哪个函数。这种多态是在编译阶段确定的,因此被称为静态多态。
    • 例如:函数重载、运算符重载。
  2. 运行时多态(动态多态):通过虚函数和继承来实现,程序在运行时根据对象的实际类型决定调用哪个函数。这种多态是在运行时确定的,因此被称为动态多态。
    • 例如:虚函数、接口实现。

🌸二、多态的使用条件

🌻2.1 基类指针或引用

在C++的多态性中,基类指针或引用是实现多态调用的关键。通过基类指针或引用指向派生类对象,可以在运行时调用派生类的重写方法,而不依赖于对象的静态类型。这种方式称为运行时多态动态多态

2.1.1 为什么需要基类指针或引用

在C++中,如果直接使用派生类对象,即使它重写了基类的虚函数,编译器仍然会使用静态绑定,即在编译时确定调用的函数版本。而使用基类指针或引用时,C++会使用动态绑定(通过虚函数表)来决定在运行时调用派生类的版本。这是多态的核心机制。

【示例代码】

以下是一个使用基类指针或引用实现多态的简单示例:

#include <iostream>class Animal {
public:virtual void sound() const {   // 基类中的虚函数std::cout << "Some generic animal sound" << std::endl;}virtual ~Animal() = default;   // 虚析构函数
};class Dog : public Animal {
public:void sound() const override {  // 派生类中重写 sound 方法std::cout << "Woof" << std::endl;}
};class Cat : public Animal {
public:void sound() const override {  // 另一个派生类中重写 sound 方法std::cout << "Meow" << std::endl;}
};void makeSound(const Animal& animal) {  // 基类引用,支持多态animal.sound();  // 动态绑定,根据实际对象类型调用派生类的方法
}int main() {Dog dog;Cat cat;// 使用基类引用,触发多态makeSound(dog);  // 输出:WoofmakeSound(cat);  // 输出:Meow// 使用基类指针,也可以实现多态Animal *animalPtr = new Dog();animalPtr->sound();  // 输出:Woofdelete animalPtr;return 0;
}

【代码分析】

  1. 基类中的虚函数Animal类中的sound方法是虚函数,允许在派生类中重写。
  2. 基类指针或引用makeSound函数接受一个Animal的引用,而不是具体的DogCat对象,使其能够调用不同的sound实现。
  3. 动态绑定:在main函数中,通过基类引用和指针来调用派生类的sound方法,输出的是实际派生类的结果。

🌻2.2 虚函数(virtual function)

在C++中,虚函数(virtual function) 是一种特殊的成员函数,通过它可以实现运行时多态。虚函数允许基类的指针或引用在运行时根据对象的实际类型调用派生类的重写方法,而不仅仅局限于基类的实现。这种机制在面向对象设计中非常重要,尤其在抽象接口、工厂模式等设计模式中广泛应用。

2.2.1 虚函数的定义和基本特性
  • 虚函数是在基类中用关键字 virtual 声明的成员函数。
  • 虚函数可以在派生类中被重写(override),并在运行时决定调用派生类的重写方法。
  • 虚函数必须通过基类指针或引用来调用,才能触发多态行为。
2.2.2 如何定义虚函数

虚函数在基类中声明时加上 virtual 关键字即可。推荐使用override关键字在派生类中重写虚函数,便于编译器检查是否正确地进行了重写。

[示例代码]

以下是一个虚函数的简单示例:

#include <iostream>class Animal {
public:virtual void sound() const {   // 基类中的虚函数std::cout << "Some generic animal sound" << std::endl;}
};class Dog : public Animal {
public:void sound() const override {  // 派生类中重写 sound 方法std::cout << "Woof" << std::endl;}
};class Cat : public Animal {
public:void sound() const override {  // 另一个派生类中重写 sound 方法std::cout << "Meow" << std::endl;}
};void makeSound(const Animal &animal) {  // 基类引用,支持多态animal.sound();  // 动态绑定,根据实际对象类型调用派生类的方法
}int main() {Dog dog;Cat cat;makeSound(dog);  // 输出:WoofmakeSound(cat);  // 输出:Meowreturn 0;
}

【代码解析】

  1. 基类声明虚函数Animal类中的sound方法声明为虚函数,因此派生类可以重写该方法。
  2. 派生类重写虚函数DogCat类分别重写了sound方法,提供了各自的实现。
  3. 多态调用makeSound函数接受Animal类型的引用作为参数,在运行时会根据传入对象的实际类型调用相应的sound实现,输出WoofMeow

【注意事项】

  1. 构造函数不能是虚函数:构造函数不支持virtual关键字,因为对象在构造时还未完成初始化。
  2. 静态成员函数不能是虚函数:静态成员函数不依赖于对象,无法实现多态。
  3. 基类指针或引用:虚函数的多态性只能通过基类的指针或引用来调用,如果直接使用派生类对象,则编译时会使用静态绑定。
总结
  • 虚函数实现了C++的多态机制,允许基类指针或引用在运行时动态选择合适的派生类实现。
  • 虚函数表支持动态绑定,通过表中指针定位到实际调用的函数。

虚函数使得代码在结构上更加灵活,提升了程序设计的可扩展性。

🌻2.3 重写虚函数(Override virtual function)

2.3.1 虚函数重写的三大条件

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

class Person
{
public:virtual void BuyTicket() const//虚函数{cout << "买全价票" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket() const//虚函数{cout << "买半价票" << endl;}
};void Func(const Person& people)
{people.BuyTicket();
}int main()
{Func(Person()); //普通人Func(Student()); //学生return 0;
}

在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。

2.3.2 虚函数重写的两个例外

在C++中,虚函数重写存在两个例外情况,即使满足了通常的虚函数重写条件,也不会被认为是对基类虚函数的重写。这两个例外是:

  1. 参数默认值不参与重写

在C++中,虚函数的重写不会受到参数默认值的影响,即使在基类的虚函数中定义了默认参数值,派生类重写时也可以选择不同的默认值。但是,当调用虚函数时,默认参数值总是根据指针或引用的静态类型确定,而不是动态类型。这意味着默认参数值在多态调用中不会变化。

示例:

#include <iostream>class Base {
public:virtual void printMessage(int times = 1) const {  // 基类虚函数,默认值为1for (int i = 0; i < times; ++i)std::cout << "Base message" << std::endl;}
};class Derived : public Base {
public:void printMessage(int times = 3) const override { // 重写时设置默认值为3for (int i = 0; i < times; ++i)std::cout << "Derived message" << std::endl;}
};int main() {Base *ptr = new Derived();ptr->printMessage();  // 输出1次,因为默认值取自Base类delete ptr;return 0;
}

解释:虽然Derived类为printMessage方法设置了默认值3,但在多态调用时,默认值取决于基类Base的定义(即1),因为编译器在静态类型为Base时就已确定默认值。

  1. 返回类型的协变限制

虽然C++支持协变返回类型(即派生类的重写函数可以返回一个更具体的类型),但协变限制仅限于指针引用类型。如果基类的虚函数返回非指针或非引用类型,派生类不能重写该虚函数并更改返回类型。

示例:

#include <iostream>class Base {
public:virtual int getValue() const {  // 基类虚函数返回int类型return 42;}
};class Derived : public Base {
public:// 错误:无法重写并更改返回类型// double getValue() const override {//     return 3.14;// }
};

解释Base类的getValue函数返回int类型。即使Derived类想返回double,这种重写是不允许的,因为返回类型不是指针或引用,违反了协变的限制。

🌻2.4 虚析构函数(Virtual Destructor)的重写

在C++中,虚析构函数(Virtual Destructor)是一种特殊的析构函数,通过在基类中将析构函数声明为虚函数,可以确保在通过基类指针删除派生类对象时,派生类的析构函数被正确调用。这在涉及多态和动态内存管理时尤为重要,可以有效避免内存泄漏资源未正确释放的问题。

2.4.1 为什么需要虚析构函数?

当基类指针指向派生类对象时,如果删除对象时基类的析构函数不是虚函数,那么调用的仅仅是基类的析构函数,而不会调用派生类的析构函数。这样,派生类中分配的资源就无法释放,导致内存泄漏或其他资源管理问题。

示例

以下是一个不使用虚析构函数的例子,演示潜在的内存泄漏问题:

#include <iostream>class Base {
public:~Base() {  // 非虚析构函数std::cout << "Base destructor called" << std::endl;}
};class Derived : public Base {
public:~Derived() {  // 派生类析构函数std::cout << "Derived destructor called" << std::endl;}
};int main() {Base* obj = new Derived();delete obj;  // 仅调用 Base 的析构函数,不调用 Derived 的析构函数return 0;
}

输出

 Base destructor called

解释:在删除obj时,由于基类的析构函数不是虚函数,因此只调用了Base的析构函数,没有调用Derived的析构函数。派生类中可能分配的资源未被释放,导致潜在的内存泄漏。

2.4.2 使用虚析构函数

通过将基类的析构函数声明为虚函数,可以确保正确调用派生类的析构函数,避免内存泄漏问题:

#include <iostream>class Base {
public:virtual ~Base() {  // 虚析构函数std::cout << "Base destructor called" << std::endl;}
};class Derived : public Base {
public:~Derived() override {  // 重写析构函数std::cout << "Derived destructor called" << std::endl;}
};int main() {Base* obj = new Derived();delete obj;  // 正确调用 Derived 和 Base 的析构函数return 0;
}

输出

Derived destructor called
Base destructor called

解释:在delete obj时,虚析构函数确保先调用Derived的析构函数,然后调用Base的析构函数,资源得到正确释放。

2.4.3 虚析构函数的注意事项
  1. 虚析构函数的必要性:任何含有虚函数的基类都应定义虚析构函数,以确保派生类对象通过基类指针删除时能够正确析构。
  2. 性能影响:虚析构函数会引入一定的性能开销(如虚函数表查找)。但对于具有多态需求的类,这是一个合理的取舍。
  3. 纯虚析构函数:基类析构函数也可以定义为纯虚函数,用于将类设计为抽象基类,但必须提供函数体,因为析构函数始终需要可执行代码。
class AbstractBase {
public:virtual ~AbstractBase() = 0;  // 纯虚析构函数
};AbstractBase::~AbstractBase() {}  // 提供析构函数体
总结
  • 虚析构函数确保通过基类指针删除派生类对象时正确调用派生类的析构函数,避免内存泄漏。
  • 基类析构函数声明为虚函数是实现多态的良好实践,尤其当基类有其他虚函数时。
  • 纯虚析构函数可用来定义抽象基类,但仍需提供函数体。

🌻2.5 C++11的override和final

在C++中,overridefinal是C++11引入的两个关键字,主要用于类的继承和虚函数的管理。它们在面向对象编程中用于提高代码的安全性和可读性,确保虚函数的正确性和防止意外的重写。

2.5.1 override 关键字

override 关键字用于显式声明一个函数是从基类中**重写(override)**的虚函数。它能够帮助编译器检查函数是否确实重写了基类中的虚函数。如果函数签名不匹配(比如返回类型不同或参数不同),编译器会报错。

使用override的主要好处是:

  • 增加代码的可读性,表明该函数是重写基类中的函数。
  • 提供编译期检查,避免因为函数签名不匹配导致的隐藏错误。

示例

class Base {
public:virtual void display() const {std::cout << "Base display" << std::endl;}
};class Derived : public Base {
public:void display() const override {  // 正确重写了基类的 display 函数std::cout << "Derived display" << std::endl;}
};

如果你误写了函数签名,比如忘记了 const 修饰符:

class Derived : public Base {
public:void display() override {  // 错误,没有 const 修饰符std::cout << "Derived display" << std::endl;}
};

编译器会报错,因为你没有正确重写基类的函数。

2.5.2 final 关键字

final 关键字用于两种情况:

  1. 防止类被继承:当你不希望某个类再被继承时,可以将这个类标记为final
  2. 防止虚函数被重写:当你不希望派生类重写某个虚函数时,可以将该虚函数标记为final

示例1:防止类被继承

class FinalClass final {// 该类不能再被继承
};// 下面的代码会导致编译错误
class DerivedClass : public FinalClass {// 错误:FinalClass 被标记为 final,不能被继承
};

示例2:防止虚函数被重写

class Base {
public:virtual void display() const {std::cout << "Base display" << std::endl;}
};class Derived : public Base {
public:void display() const final {  // 这个函数不能再被派生类重写std::cout << "Derived display" << std::endl;}
};// 下面的代码会导致编译错误
class MoreDerived : public Derived {
public:void display() const override {  // 错误:Derived::display 被标记为 final,不能被重写std::cout << "MoreDerived display" << std::endl;}
};

总结:

  • override:用于确保你正在重写基类中的虚函数,提供编译期检查。
  • final:用于防止类被继承或者虚函数被重写。

这两个关键字提高了代码的安全性,避免继承或虚函数重写中的常见错误。

🌻2.6 重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述

🌸三、抽象类

在C++中,抽象类是一种不能直接实例化的类,通常作为其他类的基类,目的是为子类提供接口定义。抽象类至少包含一个纯虚函数(pure virtual function),这是抽象类的核心特征。

🌻3.1 抽象类的定义

抽象类的定义中包含纯虚函数,纯虚函数的声明形式为:

virtual 返回类型 函数名(参数列表) = 0;

这个 = 0 表示该函数是纯虚函数,必须在派生类(子类)中实现。

以下是一个抽象类的简单例子:

#include <iostream>
using namespace std;// 定义抽象类 Shape
class Shape {
public:// 纯虚函数virtual void draw() = 0;virtual double area() = 0;
};// 定义派生类 Circle
class Circle : public Shape {
private:double radius;
public:Circle(double r) : radius(r) {}// 实现抽象类中的纯虚函数void draw() override {cout << "Drawing a Circle" << endl;}double area() override {return 3.14159 * radius * radius;}
};// 定义派生类 Rectangle
class Rectangle : public Shape {
private:double width, height;
public:Rectangle(double w, double h) : width(w), height(h) {}// 实现抽象类中的纯虚函数void draw() override {cout << "Drawing a Rectangle" << endl;}double area() override {return width * height;}
};int main() {Shape* shape1 = new Circle(5.0);   // 创建Circle对象Shape* shape2 = new Rectangle(4.0, 6.0); // 创建Rectangle对象shape1->draw();    // 调用Circle的draw方法cout << "Area: " << shape1->area() << endl; // 调用Circle的area方法shape2->draw();    // 调用Rectangle的draw方法cout << "Area: " << shape2->area() << endl; // 调用Rectangle的area方法delete shape1;delete shape2;return 0;
}
  1. Shape 是一个抽象类,它包含两个纯虚函数 draw()area()
  2. CircleRectangle 是从 Shape 派生的类,它们实现了抽象类中的纯虚函数。
  3. main() 函数中,定义了两个指向抽象类的指针 shape1shape2,分别指向 CircleRectangle 对象,并调用了它们的具体实现。

🌻3.2 抽象类的特点:

  • 不能直接实例化抽象类对象。例如 Shape 不能直接创建对象。
  • 抽象类中的纯虚函数必须在派生类中实现,否则派生类也会变成抽象类。
  • 抽象类可以有数据成员和普通成员函数,但如果有纯虚函数,则它仍然是抽象类。

结语

通过对C++多态性的深入了解,我们可以更好地编写具有高扩展性和灵活性的代码。多态不仅让代码变得更具适应性,还能够减少代码重复,提高维护效率。在未来的开发中,合理运用多态将为我们的项目带来显著的提升。希望本文的讲解能够帮助读者在实践中更好地掌握这一重要概念。
在这里插入图片描述

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,17的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是17前进的动力!
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/450006.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UE5 猎户座漂浮小岛 04 声音 材质

UE5 猎户座漂浮小岛 04 声音 材质 1.声音 1.1 导入 wav格式 1.2 循环播放 1.3 mp3转wav 1.4 新手包素材&#xff08;火焰 &#xff09; particle&#xff1a;颗粒 2.材质 2.1 基本颜色 M_Yellow 2.2 混合模式与双面材质 2.3 金属感、高光、粗糙度 M_AluminumAlloy 2.4 自…

视频网站开发:Spring Boot框架的高效实现

5 系统实现 5.1用户信息管理 管理员管理用户信息&#xff0c;可以添加&#xff0c;修改&#xff0c;删除用户信息信息。下图就是用户信息管理页面。 图5.1 用户信息管理页面 5.2 视频分享管理 管理员管理视频分享&#xff0c;可以添加&#xff0c;修改&#xff0c;删除视频分…

Codeforces Round 770 (Div. 2)

比赛链接&#xff1a;Dashboard - Codeforces Round 770 (Div. 2) - Codeforces A. Reverse and Concatenate 题意&#xff1a; 思路&#xff1a; 假设 s "abba" 经过1次操作后 -> "abbaabba" s "abcd" 经过一次操作后 -> "abcd…

JavaWeb合集12-Redis

十二、Redis 1、Redis 入门 Redis是一个基于内存的key-valule 结构数据库。 特点&#xff1a;基于内存存储&#xff0c;读写性能高 场景&#xff1a;适合存储热点数据(热点商品、资讯、新闻) Redis安装包分为Windows版和Linux版&#xff1a; Windows版 下载地址: https://gith…

unity 屏幕波动反馈打击效果(附资源下载)

unity 屏幕波动反馈打击效果 一枪打出去整个屏幕都回波动的效果反馈。 知识点&#xff1a; 1、动画事件 2、屏幕后处理 效果如图&#xff1a;&#xff08;波动速度浮动都可调整&#xff09; 附件下载

Java 枚举类

枚举类型 在Java编程语言中&#xff0c;枚举类&#xff08;Enum Class&#xff09;是一种特殊的类&#xff0c;它用于表示一组固定的常量。这些常量通常用于定义变量的合法取值&#xff0c;比如一周的天数、交通信号灯的颜色等。枚举类提供了一种类型安全的方式来使用这些常量&…

CasADi库C++用法整理学习---以NMPC代码为例

参考几个使用方法博客 1 官方文档写的很清楚 对SM&#xff0c;DM&#xff0c;XM数据类型疑惑。什么时候使用什么样的类型&#xff0c;还是都可以&#xff1f; x MX.sym(“x”) 这将创建一个 11 矩阵&#xff0c;即一个包含名为 x 的符号基元的标量。这只是显示名称&#xff…

基于Java的免税商品优选购物商城设计与实现代码(论文+源码)_kaic

目 录 摘 要 Abstract 第一章 绪论 1.1 课题开发的背景 1.2 课题研究的意义 1.3 研究内容 第二章 系统开发关键技术 2.1 JAVA技术 2.2 MyEclipse开发环境 2.3 Tomcat服务器 2.4 Spring Boot框架 2.5 MySQL数据库 第三章 系统分析 3.1 系统可行性研究…

ClickHouse的原理及使用,

1、前言 一款MPP查询分析型数据库——ClickHouse。它是一个开源的&#xff0c;面向列的分析数据库&#xff0c;由Yandex为OLAP和大数据用例创建。ClickHouse对实时查询处理的支持使其适用于需要亚秒级分析结果的应用程序。ClickHouse的查询语言是SQL的一种方言&#xff0c;它支…

01数组算法/代码随想录

一、数组 好久没写算法题&#xff0c;之前喜欢按着习惯选择刷题&#xff0c;很早以前就听说代码随想录&#xff0c;今天跟着代码随想录再过一遍算法 1.1二分查找 常见疑问 middle一定是在[left, right]这个范围内标准代码不会越界&#xff0c;因为在else if中出现越界后&…

【windows个性化】在 Windows 10/11 上使 Windows 任务栏半透明/透明

实现目的&#xff1a;在 Windows 10/11 上使 Windows 任务栏半透明/透明. TranslucentTB plugin 描述 只需几 MB 内存&#xff0c;几乎不占用 CPU&#xff0c;便可以在 Windows 10/11 上使 Windows 任务栏半透明/透明的轻量小工具 功能 高级颜色选择器(color picker)&…

X(twitter)推特广告类型有哪些?如何选择?

X&#xff08;twitter&#xff09;推特是全球最热门的几大社交媒体平台之一&#xff0c;也是很多电商卖家进行宣传推广工作的阵地之一。在营销过程中不可避免地需要借助平台广告&#xff0c;因此了解其广告类型和适配场景也十分重要。 一、广告类型及选择 1.轮播广告 可滑动的…

JavaScript的100个概念

JavaScript对初学者来说既是福音&#xff0c;也是挑战。一方面&#xff0c;掌握它后可以构建各种项目&#xff0c;比如网站、应用程序和服务器&#xff0c;还能在很多行业找到工作。但另一方面&#xff0c;它又充满怪异之处&#xff0c;并被各种复杂的库和框架包围。在这段课程…

FLUX LoRA模型揭秘:COMFYUI中的AI绘画新动力!

嗨&#xff0c;艺术爱好者们&#xff01;今天我们要揭开一个神秘的面纱——FLUX LoRA模型&#xff0c;这个模型就像是一个神奇的魔法师&#xff0c;将COMFYUI中的AI绘画提升到了一个新的高度&#xff01; 想象一下&#xff0c;你有一个AI助手&#xff0c;它能够理解你的绘画梦…

【Redis】Zset类型常用命令

文章目录 一. Zset有序集合简介.二. 添加元素相关命令.2.1 向有序集合中添加元素(zadd) 三. 查询元素相关操作.3.1 查询有序集合中的元素个数( zcard zcount)3.2 查询指定区间内的元素(zrange zrevrange zrangebyscore)3.3 查询有序集合中指定成员的排名(zrank zrevrank )3.4 查…

钴粉Co纳米微球100nm|CoNP/CoN2-C催化剂

钴粉Co纳米微球100nm|CoNP/CoN2-C催化剂 钴粉Co纳米微球100nm是一种具有独特物理和化学性质的纳米材料&#xff0c;因其高比表面积、优良的磁性和化学稳定性&#xff0c;在多个领域展现出广阔的应用前景。以下是关于钴粉Co纳米微球100nm的相关信息&#xff1a; 性质 高比表面…

ubuntu20.04安装gerrit

1、update 2、updategrale 一&#xff1a;安装前准备 配置管理gerrit的专属账号&#xff1a;(本次测试安装我用的ROOT) sudo adduser gerrit sudo usermod -a -G sudo gerrit //分配sudo 权限 sudo su gerrit java、git环境&#xff1a; sudo apt-get update sudo apt-g…

群晖前面加了雷池WAF,安装失败,然后无法识别出用户真实访问IP

有nas的相信对公网都不模式&#xff0c;在现在基础上传带宽能有100兆的时代&#xff0c;有公网代表着家里有一个小服务器&#xff0c;像百度网盘&#xff0c;优酷这种在线服务都能部署为私有化服务。但现在运营商几乎不可能提供公网ip&#xff0c;要么自己买个云服务器做内网穿…

多jdk版本环境下,jenkins系统设置需指定JAVA_HOME环境变量

一、背景 由于不同项目对jdk版本的要求不同&#xff0c;有些是要求jdk11&#xff0c;有些只需要jdk8即可。 而linux机器上安装jdk的方式又多种多样&#xff0c;最后导致jenkins打包到底使用的是哪个jdk&#xff0c;比较混乱。 1、java在哪 > whereis java java: /usr/bin/…

测试人生 | 双非院校,2年工作经验年薪近20万

本人本科毕业于双非院校&#xff0c;大学毕业之后就开始从事软件测试工作&#xff0c;有两年多的工作经验&#xff0c;工作当中学习的测试技能较少&#xff0c;比较多重复的工作内容&#xff0c;主要对软件进行功能测试、简单的接口测试及专项测试&#xff0c;自学的测试知识零…