1. 概述

C++有3种继承方式:公有继承(public)、保护继承(protected)、私有继承(private)。

一个B类继承于A类,或称从类A派生类B。这样的话,类A称为基类(父类),类B称为派生类(子类)。派生类中的成员,包含两部分:一部分是从基类继承过来的,另一类是派生类自己增加的成员。

派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造函数和析构函数),但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限。派生类的访问权限规则如下:

#include<iostream>
using namespace std; class A{
public:
int mA;
protected:
int mB;
private:
int mC;
};

1.1 公有继承

class B:public A{
public:
void printB(){
cout << "printB:\n";
cout << mA << endl; // 可访问基类A的public属性
cout << mB << endl; // 可访问基类A的protected属性
// cout << mC << endl; // 不可访问基类A的private属性
}
};
class SubB:public B{
public:
void printSubB(){
cout << "printSubB:\n";
cout << mA << endl;
cout << mB << endl;
// cout << mC << endl; // 不可访问
}
};

1.2 私有继承

使用私有继承,基类的公有成员和保护成员都将称为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们,而在继承层次结构之外是不可用的。

class D:private A{
public:
void printD(){
cout << "printD:\n";
cout << mA << endl; // 可访问基类A的public属性
cout << mB << endl; // 可访问基类A的protected属性
// cout << mC << endl; // 不可访问基类A的private属性
}
};
class SubD:public D{ // 在继承层次结构之外不可用
public:
void printSubD(){
cout << "printSubD:\n";
// cout << mA << endl; // 不可访问
// cout << mB << endl; // 不可访问
// cout << mC << endl; // 不可访问
}
};

1.3 保护继承

使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。当从派生类派生出第三代类时,私有继承和保护继承的区别便呈现出来了。

使用私有继承时,第三代类将不能使用基类(第一代类)的接口,这是因为基类的公有方法在其派生类(第二代类)中都将变成私有方法;

使用保护继承时,基类的公有方法在第二代类中将变成受保护的,因此第三代类可以使用它们。

class C:protected A{
public:
void printC(){
cout << "printC:\n";
cout << mA << endl; // 可访问基类A的public属性
cout << mB << endl; // 可访问基类A的protected属性
// cout << mC << endl; // 不可访问基类A的private属性
}
};
class SubC:public C{
public:
void printSubC(){
cout << "printSubC:\n";
cout << mA << endl;
cout << mB << endl;
// cout << mC << endl; // 不可访问
}
};

2. 继承中的构造和析构

不是所有的函数都能自动从基类继承到派生类中。构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数不能被继承,必须为每一个特定的派生类分别创建。

  • 子类对象在创建时会首先调用父类的构造函数,父类构造函数执行完毕之后,才会调用子类的构造函数;
  • 当父类构造函数带参时,需要在子类初始化列表中显示调用父类的构造函数;
  • 析构函数调用顺序和构造函数相反。
#include <iostream>
#include <string>
using namespace std; class Animal{
private:
string mName;
public:
Animal(string name) {
cout << "Animal带参构造函数...\n";
mName = name;
}
~Animal(){
cout << "Animal析构函数...\n";
}
}; class Bird:public Animal{
private:
bool can_flight; // 能否飞行
public:
Bird(bool cf, string name):Animal(name){
cout << "Bird带参构造函数...\n";
can_flight = cf;
}
~Bird(){
cout << "Bird析构函数...\n";
} }; int main(){
Bird(true, "海鸥");
return 0;
}

输出:

Animal带参构造函数...
Bird带参构造函数...
Bird析构函数...
Animal析构函数...

注意:operator=也不能被继承,因为它完成类似构造函数的行为。

3 派生类和基类之间的特殊关系

派生类与基类之间有一些特殊关系。

3.1 派生类对象可以使用基类的方法

派生类对象可以使用基类的方法,条件是该方法不是私有的:

#include <iostream>
#include <string>
using namespace std; class Animal{
private:
string mName;
public:
Animal() { mName = "no name";}
Animal(string name) { mName = name;}
void showAnimal(){
cout << "Name: " << mName << endl;
}
};
class Bird:public Animal{
private:
bool can_flight; // 能否飞行
public:
Bird(bool cf, string name):Animal(name){ can_flight = cf; }
void showBird(){
cout << "Can_flight(1-can;0-can't): " << can_flight << endl;
}
}; int main(){
Bird b(true, "海鸥");
b.showAnimal(); // 派生类对象可以使用基类的方法
b.showBird();
return 0;
}

输出:

Name: 海鸥
Can_flight(1-can;0-can't): 1

3.2 基类指针可以在不进行显式类型转换的情况下指向派生类对象

int main(){
Bird b(true, "海鸥");
Animal *pa = &b;
pa->showAnimal();
return 0;
}

输出:

Name: 海鸥

3.3 基类引用可以在不进行显式类型转换的情况下引用派生类对象

int main(){
Bird b(true, "海鸥");
Animal &ra = b;
ra.showAnimal();
return 0;
}

输出:

Name: 海鸥

然而,基类指针或者引用只能调用基类方法,因此,不能使用pa或ra来调用派生类的showBird方法,只是单向的,不可以将基类对象和地址赋给派生类引用或指针:

int main(){
Animal a("海鸥");
Bird &rb = a; // 非法
Bird *pb = &a; // 非法
return 0;
}

3. 继承中同名成员的处理方法

3.1 同名变量

  • 当子类成员和父类成员同名时,子类依然从父类继承同名成员 。子类访问其成员默认访问子类的成员(本作用域,就近原则);
  • 在子类通过作用域::进行同名成员区分。
#include <iostream>
#include <string>
using namespace std; class Father{
public:
int mParam;
public:
Father():mParam(0){}
void display(){cout << mParam << endl;}
}; class Son:public Father{
public:
int mParam;
public:
Son():mParam(1){}
void display(){
cout << Father::mParam << endl; // 在派生类中使用于基类同名成员,显示使用类名限定符
cout << mParam << endl;
}
}; int main(){
Son son;
cout << son.mParam << endl; // 就近原则,默认访问子类成员
son.display();
return 0;
}

3.2 同名方法

Father.cpp

class Father{
public:
// 重载方法
void func1(){
cout << "Father::void func1()...\n";
}
void func1(int param){
cout << "Father::void func1(int param)...\n";
}
// 非重载方法
void func2(){
cout << "Father::void func2()...\n";
}
};

Son.cpp

class Son:public Father{
public:
void func2(){
Father::func2(); // 基类func2()将隐藏,可通过类作用域运算符指定调用基类func2()方法
cout << "Son::void func2()...\n";
}
}; int main(){
Son son;
son.func2();
return 0;
}

输出:

Father::void func2()...
Son::void func2()...

Daughter.cpp

class Daughter:public Father{
public:
void func1(int param1, int param2){ // 改变参数列表->重新定义继承自基类的方法,同名基类方法将被隐藏
Father::func1(10); // 可通过类作用域运算符指定调用基类方法
cout << "Daughter::void func2(int param1, int param2)...\n";
}
int func1(int param){ // 改变返回值类型->重新定义继承自基类的方法,同名基类方法将被隐藏
Father::func1(10); // 可通过类作用域运算符指定调用基类方法
cout << "Daughter::int func1(int param)...\n";
return param;
}
}; int main(){
Daughter daughter;
cout << daughter.func1(1) << endl;;
return 0;
}

输出:

Father::void func1(int param)...
Daughter::int func1(int param)...
1

总结:

重新定义继承的方法并不是重载。如果重新定义派生类中的继承函数,基类中所有的同名方法都将被隐藏,派生类对象将无法使用它们。(但是在派生类中可以通过类作用域运算符指定调用基类方法)

如果基类方法在基类的类定义中被重载了,则应该在派生类中重新定义所有的基类版本。如果只定义一个版本,则其他版本将被隐藏,派生类对象将无法使用它们。

若不需要修改继承自基类的方法,则只需通过类作用域运算符指定调用基类方法即可。

4. 多继承

4.1 多继承概念

同时继承多个类,即为多继承。

#include <iostream>
#include <string>
using namespace std; class Singer{
public:
void show(){
cout << "Singer::show()..." << endl;
}
};
class Waiter{
public:
void show(){
cout << "Waiter::show()..." << endl;
}
}; class SiningWaiter:public Singer, public Waiter{}; int main(){
SiningWaiter sw;
// sw.show(); // show是从Singer继承而来的,还是从Waiter继承来的呢?
return 0;
}

多继承会带来一些二义性的问题,如果两个基类中有同名的函数或变量,那么通过派生类对象去访问时就不能明确到底是调用从基类1继承的版本还是从基类2继承的版本。

解决方法:显式指定调用哪个基类的版本。

int main(){
SiningWaiter sw;
sw.Singer::show();
sw.Waiter::show();
return 0;
}

输出:

Singer::show()...
Waiter::show()...

4.2 菱形继承和虚继承:菱形继承

两个派生类继承同一基类,而又有某个第三代类同时继承了这两个派生类,这种继承称为菱形继承。

class Animal{
protected:
int age;
public:
Animal(){ age = 0; }
void show(){
cout << "Animal::show()..." << endl;
}
}; class Sheep:public Animal{ // 羊类
public:
void show(){
cout << "Sheep::show()..." << endl;
}
};
class Camel:public Animal{ // 驼类
public:
void show(){
cout << "Camel::show()..." << endl;
}
}; class Alpaca:public Sheep, public Camel{}; // 羊驼类

这种继承主要带来两类问题:

  • 成员访问产生二义性;
  • 第三代类重复继承了第一代类的数据(羊驼类继承自动物类的函数与数据继承了双份)。
int main(){
Alpaca alpaca;
// 问题1:成员访问二义性
// alpaca.show(); // 二义性
alpaca.Sheep::show();
alpaca.Camel::show();
// 问题2:重复继承
cout << "Animal size: " << sizeof(Animal) << endl;
cout << "Sheep size: " << sizeof(Sheep) << endl;
cout << "Camel size: " << sizeof(Camel) << endl;
cout << "Alpaca size: " << sizeof(Alpaca) << endl;
return 0;
}

输出:

Sheep::show()...
Camel::show()...
Animal size: 4
Sheep size: 4
Camel size: 4
Alpaca size: 8

创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。因此,Sheep对象和Camel对象中各含有一个基类对象(即Animal对象),所以才会有Sheep size: 4Camel size: 4。而由于Alpaca同时继承了Sheep和Camel,因此Alpaca对象中将包含两个Animal对象,因此Alpaca size: 8

这种重复继承将带来一些问题。例如,通常可以将派生类都西昂的地址赋予基类指针,但现在将出现二义性:

Alpaca al;
Animal *pa = &al; // 出现二义性

通常,这种赋值将把基类指针设置为派生类对象中的基类对象的地址。但现在al中包含2个Animal对象,有2个地址可供选择,所以应使用类型转换来指定对象:

Animal *pa1 = (Sheep)&al; // the Animal in Sheep
Animal *pa2 = (Camel)&al; // the Animal in Camel

对于这种菱形继承所带来的问题,C++提供了一种方式——采用虚基类。

4.3 菱形继承和虚继承:虚基类

1)虚基类

虚基类使得从多个第二代类(它们的基类(第一代类)相同)派生出的对象只继承一个基类(第一代类)对象。

通过在类声明中使用关键字virtual,可以使Animal被硬座Sheep和Camel的虚基类(virtual和public的次序无关紧要):

class Sheep:virtual public Animal{...};
class Camel:virtual public Animal{...};
class Alpaca:public Sheep, public Camel{...};

现在,Alpaca对象将只包含一个Animal对象。从本质上说,继承的Sheep和Camel对象共享一个Animal对象,而不是各自引入自己的Animal对象副本。

class Sheep:virtual public Animal{};
class Camel:virtual public Animal{}; class Alpaca:public Sheep, public Camel{}; // 羊驼类
int main(){
Alpaca alpaca;
alpaca.show();
cout << "Alpaca size: " << sizeof(Alpaca) << endl;
return 0;
}

输出:

Animal::show()...
Alpaca size: 12

通过虚继承的方式解决了菱形继承带来的二义性问题。但是为什么Alpaca size: 12呢?

2)虚基类实现原理(难点)

Sheep和Camel通过虚继承的方式派生自Animal,编译器将为Sheep类和Camel类各自增加一个指针vbptr(virtual base pointer),vbptr指向了一张表,这张表保存了当前虚指针(即Sheep和Camel中的vbptr)相对于虚基类首地址的偏移量。

Alpaca派生于Sheep和Camel,将继承二者的vbptr指针,并调整了vbptr与虚基类首地址的偏移量(从‘ 第二代类相对基类的偏移量 ’调整为“ ‘第三代类中的第二代类副本’ 相对基类的偏移量”)。

因此,最后的Alpaca创建对象后,包含2个vbptr指针(Sheep子对象和Camel子对象各一个)和Animal子对象中的一个整形变量age,总共占12个字节。

就这样,使得菱形继承时,Alpaca对象将只包含一个Animal子对象。从本质上说,继承的Sheep子对象和Camel子对象共享一个Animal子对象,而不是各自引入自己的Animal子对象副本。

即使共享虚基类,但是必须要有一个类来完成基类的初始化(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化,那到底谁应该负责完成初始化呢?C++标准中选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用。

class Animal{
protected:
int age;
public:
Animal(int age){ this->age = age; }
}; class Sheep:virtual public Animal{
public: // 每一次继承子类中都必须书写初始化语句
Sheep(int age):Animal(age){} // 不调用Animal构造
};
class Camel:public Animal{
public: // 每一次继承子类中都必须书写初始化语句
Camel(int age):Animal(age); // 不调用Animal构造
}; class Alpaca:public Sheep, public Camel{
public: // 每一次继承子类中都必须书写初始化语句
Alpaca(int age):Animal(age); // 调用Animal构造
};

5. 静态联编和动态联编

5.1 虚函数

1)虚函数概念

在函数声明时加上关键字virtual。这些函数被称为虚函数(virtual method)或虚方法。

在基类和派生类存在同名函数的情况下。如果函数是通过引用或指针而不是对象本身调用的,虚函数将确定使用基类的方法还是派生类的方法。如果使用了virtual关键字,程序将根据引用或指针指向的对象的类型来选择方法,否则将根据引用或指针的类型来选择。

如果show()不是虚的,则程序的行为如下:

#include <iostream>
#include <string>
using namespace std; class Animal{
protected:
int age;
public:
Animal(){ age = 0; }
void show(){ // 没有添加关键字virtual
cout << "Animal::show()..." << endl;
}
}; class Sheep:public Animal{
public:
void show(){ // 没有添加关键字virtual
cout << "Sheep::show()..." << endl;
}
};
int main(){
Animal al;
Sheep sp;
Animal &ra = al;
Animal *pa = &sp;
ra.show();
pa->show();
return 0;
}

输出:

Animal::show()...
Animal::show()...

如果show()是虚的,则行为如下:

#include <iostream>
#include <string>
using namespace std; class Animal{
protected:
int age;
public:
Animal(){ age = 0; }
void virtual show(){ // 添加关键字virtual
cout << "Animal::show()..." << endl;
}
virtual ~Animal(){} // 虚析构函数
}; class Sheep:public Animal{
public:
void virtual show(){ // 添加关键字virtual
cout << "Sheep::show()..." << endl;
}
};
int main(){
Animal al;
Sheep sp;
Animal &ra = al;
Animal *pa = &sp;
ra.show();
pa->show();
return 0;
}

输出:

Animal::show()...
Sheep::show()...

如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。

2)虚析构函数

virtual ~Animal(){} // 虚析构函数

虚析构函数是为了解决基类指针指向派生类对象,并用基类指针释放派生类对象。确保释放派生类对象时,按正确的顺序调用析构函数

如果基类的析构函数不是虚的,则将只调用对应于指针或引用类型的析构函数:

class Animal{
public:
Animal(){ cout << "Animal构造函数\n"; }
~Animal(){ cout << "Animal析构函数\n";} // 基类的析构函数不是虚的
}; class Sheep:public Animal{
private:
char *mName;
public:
Sheep(){
cout << "Sheep构造函数\n";
mName = new char[64];
memset(mName, 0, 64);
strcpy(mName, "no name");
};
~Sheep(){
cout << "Sheep析构函数\n";
if(mName!=NULL){
delete[] mName;
mName = NULL;
}
}
};
int main(){
Animal *al = new Sheep;
delete al;
return 0;
}

输出:

Animal构造函数
Sheep构造函数
Animal析构函数

这意味着只有Animal的析构函数被调用,即使指针Animal *al指向的是一个Sheep对象。

如果基类的析构函数是虚的,则将调用指针指向的相应对象的析构函数:

class Animal{
public:
Animal(){ cout << "Animal构造函数\n"; }
virtual ~Animal(){ cout << "Animal析构函数\n";} // 基类的析构函数是虚的
}; class Sheep:public Animal{
private:
char *mName;
public:
Sheep(){
cout << "Sheep构造函数\n";
mName = new char[64];
memset(mName, 0, 64);
strcpy(mName, "no name");
};
~Sheep(){
cout << "Sheep析构函数\n";
if(mName!=NULL){
delete[] mName;
mName = NULL;
}
}
};
int main(){
Animal *al = new Sheep;
delete al;
return 0;
}

输出:

Animal构造函数
Sheep构造函数
Sheep析构函数
Animal析构函数

如果指针指向的是Sheep对象,将调用Sheep的析构函数,然后再自动调用基类的析构函数。

因此,使用虚析构函数能够确保正确的析构函数序列被调用。对于1)虚函数概念小节中将Animal析构函数声明为virtual并不是很重要,因为Sheep析构函数没有执行任何操作。然而,如果Sheep包含一个执行某些操作的析构函数,则基类Animal必须有一个虚析构函数,即使该虚析构函数不执行任何操作,见本小节Sheep的析构函数

因此,为基类声明一个虚析构函数成为一种惯例。

3)虚函数实现原理

》from 传智播客

》 from C++ Primer Plus

通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table,vtbl)。(每个类编译器都为其创建了一个虚函数地址表(数组))虚函数表中存储了为类对象进行声明的虚函数的地址。

例如,基类对象包含一个指针vptr,该指针指向基类中所有虚函数的地址表。派生类对象也包含一个指针vptr,该指针指向派生类中所有虚函数的地址表。

如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该虚函数表将保存继承自基类的函数原始版本的地址。如果派生类定义了新的虚函数,该函数的地址也将被添加到虚函数表中。

注意,无论类中包含的虚函数是1个还是10个,都只需要在对象中添加1个地址成员,只是表的大小不同而已。

调用虚函数时,程序将根据调用对象隐藏的指针vptr转向相应的虚函数地址表(函数地址数组)。如果使用类声明中定义的第1个虚函数,则程序将使用数组中的第1个函数地址,并执行具有该地址的函数代码块。如果使用类声明中的第3个虚函数,程序将使用数组中的第3个函数。

5.2 静态联编

程序调用函数时,将使用哪个可执行代码块呢?编译器将负责回答这个问题。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。

在C语言中,函数名联编非常简单,因为每个函数名都对应一个不同的函数。在C++中,由于函数重载的原因,这项任务更加复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而,C/C++编译器可以在编译阶段就完成这种联编。在编译阶段中进行的联编被称为静态联编,又称为早期联编。

编译器根据函数调用者的对象类型,在编译阶段就确定函数的调用地址,这就是静态联编。

5.3 动态联编

然而,虚函数导致具体使用哪一个函数时不能在编译时确定的,因为编译器不知道用户将选择哪种类型的对象。所以,编译器必须生成能够在程序运行时选择正确的虚函数的代码,这被称为动态联编,又被称为晚期联编。

在运行阶段才能确定调用哪个函数。

编译器对非虚方法使用静态联编,对虚方法使用动态联编。例如:

Scientist *st = new Physicist;
st->show_all();

根据st指向的对象类型将show_all()调用Physicist:show_all()而不是Scientist:show_all()。但只有在运行程序时才能确定st指向的对象类型。所以编译器生成的代码将在程序执行时,根据对象类型将show_all()关联到Physicist:show_all()。

C++ | 类继承的更多相关文章

  1. javascript类继承的一些实验

    其实一开始编js没怎么用过对象,一般都用func,func,func···但是用多了,感觉代码一点都不美观,还要这里包一个函数,那里包一个函数,或者一直都是函数调用,不好看,而且一些重用的都要重写的话 ...

  2. C++ 类继承的对象布局

    C++多重继承下,对象布局与编译器,是否为虚拟继承都有很大关系,下面将逐一分析其中的差别,相同点为都按照类继承的先后顺序布局(类内按照虚表.成员声明先后顺序排列).该类情况为子类按照继承顺序排列,如c ...

  3. C++中public,protected,private派生类继承问题和访问权限问题

    C++中public,protected,private派生类继承问题和访问权限问题 当一个子类从父类继承时,父类的所有成员成为子类的成员,此时对父类成员的访问状态由继承时使用的继承限定符决定. 1. ...

  4. Javascript类继承-机制-代码Demo【原创】

    最近看到<Javascript设计模式>,对js模拟的”继承方式“有了更深一步的了解,虽然之前也总是用到prototype.new ,但只是知其然不知所以然,现在将类继承的方法整理如下,暂 ...

  5. smartjs 0.2 OOP讲解 - Klass 类继承

    SmartJS2.0加入OOP的功能.OOP包括klass与factory两个对象. Klass 类继承 与其他的类继承相比,smartjs使用了执行指针的概念(后面例子中会介绍),另外提供base基 ...

  6. C++——类继承

    类库:类库由类声明和实现构成.类组合了数据表示和类方法,因此提供了比函数库更加完整的程序包. 类继承:从已有的类派生出新的类,派生类继承了原有类(称为基类)的特征,包括方法. 通过类继承可以完成的工作 ...

  7. cocos2dx中常见的类及类继承关系

    场景:CCScene,继承自CCNode,几乎完全等于CCNode类 CCNode继承自CCObject,CCObject是真正意义上的父类,CCObject又继承自CCCopying类,CCCopy ...

  8. C++学习笔记(十二):类继承、虚函数、纯虚函数、抽象类和嵌套类

    类继承 在C++类继承中,一个派生类可以从一个基类派生,也可以从多个基类派生. 从一个基类派生的继承称为单继承:从多个基类派生的继承称为多继承. //单继承的定义 class B:public A { ...

  9. Android(java)学习笔记118:类继承的注意事项

    /* 继承的注意事项: A:子类只能继承父类所有非私有的成员(成员方法和成员变量) B:子类不能继承父类的构造方法,但是可以通过super(马上讲)关键字去访问父类构造方法. C:不要为了部分功能而去 ...

  10. Lua类和类继承实现

    Lua本身是不能像C++那样直接实现继承,但我们可以用万能的table表来实现. 以下我总结了三种方式的类以及继承的实现 第一.官方的做法,使用元表实现 原理参照<Programming in ...

随机推荐

  1. 实验2:Open vSwitch虚拟交换机实践(补实验一作业链接)

    实验1:SDN拓扑实践 实验2:Open vSwitch虚拟交换机实践 一.实验目的 能够对Open vSwitch进行基本操作: 能够通过命令行终端使用OVS命令操作Open vSwitch交换机, ...

  2. idea 切换database数据库方言

    在适配各种国产数据库时常常遇到存在数据库方言的情况,例如openGauss支持oracle兼容模式.. 在这种情况下,就需要在idea里对方言进行切换,否则无法执行oracle语法的sql. 1.连接 ...

  3. 记一个快捷在线接口YAPI

    在线地址:http://192.168.252.152:3000 1.idea中file下setting中plugins搜索并加载插件YAPI 2.idea中的.idea中.misc.xml文件配置. ...

  4. java 环境变量配置详细教程(2023 年全网最详细)

    前言: 在上一篇文章中,壹哥给大家重点讲解了 Java 实现跨平台的原理,不知道你现在有没有弄清楚呢?如果你还有疑问,可以在评论区留言- 之前的三篇文章,主要是理论性的内容,其实你暂时跳过不看也是可以 ...

  5. java多线程--6 死锁问题 锁Lock

    java多线程--6 死锁问题 锁Lock 死锁问题 多个线程互相抱着对方需要的资源,然后形成僵持 死锁状态 package com.ssl.demo05; public class DeadLock ...

  6. Spring 注解整理

    更多内容,前往IT-BLOG 一.核心容器 [1]@Configuration:告诉 Spring 这是一个配置类(配置类=配置文件)[2]@Bean:给容器中注册一个 Bean :类型为返回值类型, ...

  7. idea application.properties图标显示异常(无小树叶)

    项目中(多级模块)如果没有主启动类时,新建的application.properties文件显示图标是文本格式图标,就如第一张图所示 添加该项目该模块下主启动类,就可以解决图标显示问题,也就可以看到可 ...

  8. 导致sql注入的根本原因

    导致sql注入的根本原因 1.sql注入的定义 SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在 ...

  9. Docker容器中使用GPU

    背景 容器封装了应用程序的依赖项,以提供可重复和可靠的应用程序和服务执行,而无需整个虚拟机的开销.如果您曾经花了一天的时间为一个科学或 深度学习 应用程序提供一个包含大量软件包的服务器,或者已经花费数 ...

  10. window安装openslide库

    下载openslide二进制文件: 链接:https://openslide.org/download/  将下载好的ZIP文件解压到Anaconda的Library目录下(你也可以选择自己喜欢的目录 ...