>  기사  >  Java  >  Java의 상속, 다형성, 오버로딩 및 재작성 소개

Java의 상속, 다형성, 오버로딩 및 재작성 소개

高洛峰
高洛峰원래의
2017-01-19 13:54:101615검색

다형성이란 무엇인가요? 구현 메커니즘은 무엇입니까? 오버로딩과 다시 쓰기의 차이점은 무엇입니까? 이번에 검토할 매우 중요한 네 가지 개념은 상속, 다형성, 오버로딩 및 덮어쓰기입니다.

상속

간단히 말해서 상속은 기존 유형을 기반으로 하며, 새 메소드를 추가하거나 기존 메소드를 재정의하여(아래에서 설명함, 이 메소드를 덮어쓰기라고 함) 새 유형을 생성합니다. . 상속은 객체지향의 세 가지 기본 특성인 캡슐화, 상속 및 다형성 중 하나입니다. JAVA를 사용할 때 작성하는 모든 클래스는 상속입니다. JAVA 언어에서 java.lang.Object 클래스는 객체지향의 가장 기본적인 기본 클래스이기 때문입니다. 모든 클래스(또는 부모 클래스 또는 슈퍼 클래스). 새로 정의한 클래스가 어떤 기본 클래스에서 상속되는지 명시적으로 지정하지 않으면 JAVA는 기본적으로 Object 클래스에서 상속합니다.

JAVA의 클래스는 다음과 같은 세 가지 유형으로 나눌 수 있습니다.

클래스: 클래스를 사용하여 정의된 클래스로 추상 메소드를 포함하지 않습니다.
추상 클래스: 추상 클래스를 사용하여 정의된 클래스로, 추상 메소드를 포함할 수도 있고 포함하지 않을 수도 있습니다.
인터페이스: 인터페이스를 사용하여 정의된 클래스입니다.

이 세 가지 유형 사이에는 다음과 같은 상속 규칙이 존재합니다.

클래스는 클래스를 상속(확장)하고, 추상 클래스를 상속(확장)하고, 인터페이스를 상속(구현)할 수 있습니다.
추상 클래스는 클래스를 상속(확장)하고, 추상 클래스를 상속(확장)하고, 인터페이스를 상속(구현)할 수 있습니다.
인터페이스는 인터페이스만 확장할 수 있습니다.

위 세 가지 규칙의 각 상속 사례에 사용된 서로 다른 키워드 확장 및 구현은 마음대로 바꿀 수 없습니다. 우리 모두 알고 있듯이 일반 클래스는 인터페이스를 상속한 후 이 인터페이스에 정의된 모든 메서드를 구현해야 합니다. 그렇지 않으면 추상 클래스로만 정의할 수 있습니다. 여기서 Implements 키워드에 "구현"이라는 용어를 사용하지 않는 이유는 개념적으로도 상속 관계를 나타내기 때문이고 추상 클래스 구현 인터페이스의 경우 이 인터페이스 정의를 구현할 필요가 없기 때문입니다. 방법이므로 상속을 사용하는 것이 더 합리적입니다.

위의 세 가지 규칙도 다음 제약 조건을 준수합니다.

클래스와 추상 클래스는 모두 최대 하나의 클래스만 상속하거나 최대 하나의 추상 클래스를 상속할 수 있으며, 이 두 상황은 서로 즉, 클래스나 추상 클래스를 상속받습니다.
클래스, 추상 클래스 및 인터페이스는 상속하는 인터페이스 수에 제한을 받지 않습니다. 이론적으로는 인터페이스 수에 제한이 없습니다. 물론 클래스의 경우 상속하는 모든 인터페이스에 정의된 모든 메서드를 구현해야 합니다.
추상 클래스가 추상 클래스를 상속하거나 인터페이스를 구현할 때 부모 추상 클래스의 추상 메서드나 부모 클래스 인터페이스에 정의된 인터페이스를 부분적으로, 완전히 또는 완전히 구현하지 않을 수 있습니다.
클래스가 추상 클래스를 상속하거나 인터페이스를 구현하는 경우 상위 추상 클래스의 모든 추상 메소드 또는 상위 클래스 인터페이스에 정의된 모든 인터페이스를 구현해야 합니다.

상속이 프로그래밍에 가져오는 이점은 원래 클래스를 재사용(재사용)한다는 것입니다. 모듈 재사용과 마찬가지로 클래스 재사용은 개발 효율성을 향상시킬 수 있습니다. 실제로 모듈 재사용은 수많은 클래스 재사용의 중첩 효과입니다. 상속 외에도 합성을 사용하여 클래스를 재사용할 수도 있습니다. 소위 조합은 원래 클래스를 새 클래스의 속성으로 정의하고 새 클래스에서 원래 클래스의 메서드를 호출하여 재사용을 달성하는 것입니다. 새롭게 정의된 유형과 원형의 유형 사이에 포함된 관계가 없다면, 즉 추상적인 개념에서 새로 정의된 유형으로 표현되는 것들은 황인과 같이 원형으로 표현되는 것들 중 하나가 아니다. 일종의 인간이고, 포함하고 포함되는 관계가 있기 때문에 이때 재사용을 달성하려면 조합이 더 나은 선택입니다. 다음 예는 조합 방법의 간단한 예입니다.

public class Sub { 
  private Parent p = new Parent(); 
  
  public void doSomething() { 
    // 复用Parent类的方法 
    p.method(); 
    // other code 
  } 
} 
  
class Parent { 
  public void method() { 
    // do something here 
  } 
}

물론 코드를 보다 효율적으로 만들기 위해 필요할 때 원래 유형(예: Parent p)을 초기화할 수도 있습니다. .

원본 클래스를 재사용하기 위해 상속과 조합을 사용하는 것은 점진적인 개발 모델입니다. 이 방법의 장점은 원본 코드를 수정할 필요가 없으므로 원본 코드에 영향을 미치지 않는다는 것입니다. 코드는 새로운 버그를 가져옵니다. 원본 코드를 수정했기 때문에 다시 테스트할 필요가 없습니다. 이는 분명히 우리 개발에 도움이 됩니다. 따라서 원래 시스템이나 모듈을 유지 관리하거나 변형하는 경우, 특히 이에 대한 철저한 이해가 없는 경우 점진적 개발 모델을 선택할 수 있습니다. 이는 개발 효율성을 크게 향상시킬 수 있을 뿐만 아니라 다음으로 인한 위험을 피할 수 있습니다. 원본 코드 수정.

다형성

다형성은 위에서 언급한 것처럼 객체지향의 세 가지 기본 특성 중 하나입니다. 다형성이란 정확히 무엇입니까? 이해를 돕기 위해 먼저 다음 예를 살펴보겠습니다.

//汽车接口 
interface Car { 
  // 汽车名称 
  String getName(); 
  
  // 获得汽车售价 
  int getPrice(); 
} 
  
// 宝马 
class BMW implements Car { 
  public String getName() { 
    return "BMW"; 
  } 
  
  public int getPrice() { 
    return 300000; 
  } 
} 
  
// 奇瑞QQ 
class CheryQQ implements Car { 
  public String getName() { 
    return "CheryQQ"; 
  } 
  
  public int getPrice() { 
    return 20000; 
  } 
} 
  
// 汽车出售店 
public class CarShop { 
  // 售车收入 
  private int money = 0; 
  
  // 卖出一部车 
  public void sellCar(Car car) { 
    System.out.println("车型:" + car.getName() + " 单价:" + car.getPrice()); 
    // 增加卖出车售价的收入 
    money += car.getPrice(); 
  } 
  
  // 售车总收入 
  public int getMoney() { 
    return money; 
  } 
  
  public static void main(String[] args) { 
    CarShop aShop = new CarShop(); 
    // 卖出一辆宝马 
    aShop.sellCar(new BMW()); 
    // 卖出一辆奇瑞QQ 
    aShop.sellCar(new CheryQQ()); 
    System.out.println("总收入:" + aShop.getMoney()); 
  } 
}

실행 결과:

자동차 모델: BMW 단가: 300000
자동차 모델: CheryQQ 단가: 20000
합계 수익: 320000

继承是多态得以实现的基础。从字面上理解,多态就是一种类型(都是Car类型)表现出多种状态(宝马汽车的名称是BMW,售价是300000;奇瑞汽车的名称是CheryQQ,售价是2000)。将一个方法调用同这个方法所属的主体(也就是对象或类)关联起来叫做绑定,分前期绑定和后期绑定两种。下面解释一下它们的定义:

前期绑定:在程序运行之前进行绑定,由编译器和连接程序实现,又叫做静态绑定。比如static方法和final方法,注意,这里也包括private方法,因为它是隐式final的。
后期绑定:在运行时根据对象的类型进行绑定,由方法调用机制实现,因此又叫做动态绑定,或者运行时绑定。除了前期绑定外的所有方法都属于后期绑定。

多态就是在后期绑定这种机制上实现的。多态给我们带来的好处是消除了类之间的耦合关系,使程序更容易扩展。比如在上例中,新增加一种类型汽车的销售,只需要让新定义的类继承Car类并实现它的所有方法,而无需对原有代码做任何修改,CarShop类的sellCar(Car car)方法就可以处理新的车型了。新增代码如下:

// 桑塔纳汽车 
class Santana implements Car { 
  public String getName() { 
    return "Santana"; 
  } 
  
  public int getPrice() { 
    return 80000; 
  } 
}

重载(overloading)和重写(overriding)

重载和重写都是针对方法的概念,在弄清楚这两个概念之前,我们先来了解一下什么叫方法的型构(英文名是signature,有的译作“签名”,虽然它被使用的较为广泛,但是这个翻译不准确的)。型构就是指方法的组成结构,具体包括方法的名称和参数,涵盖参数的数量、类型以及出现的顺序,但是不包括方法的返回值类型,访问权限修饰符,以及abstract、static、final等修饰符。比如下面两个就是具有相同型构的方法:

public void method(int i, String s) { 
  // do something 
} 
  
public String method(int i, String s) { 
  // do something 
}

而这两个就是具有不同型构的方法: 

public void method(int i, String s) { 
  // do something 
} 
  
public void method(String s, int i) { 
  // do something 
}

了解完型构的概念后我们再来看看重载和重写,请看它们的定义:

重写,英文名是overriding,是指在继承情况下,子类中定义了与其基类中方法具有相同型构的新方法,就叫做子类把基类的方法重写了。这是实现多态必须的步骤。
重载,英文名是overloading,是指在同一个类中定义了一个以上具有相同名称,但是型构不同的方法。在同一个类中,是不允许定义多于一个的具有相同型构的方法的。

我们来考虑一个有趣的问题:构造器可以被重载吗?答案当然是可以的,我们在实际的编程中也经常这么做。实际上构造器也是一个方法,构造器名就是方法名,构造器参数就是方法参数,而它的返回值就是新创建的类的实例。但是构造器却不可以被子类重写,因为子类无法定义与基类具有相同型构的构造器。

重载、覆盖、多态与函数隐藏

经常看到C++的一些初学者对于重载、覆盖、多态与函数隐藏的模糊理解。在这里写一点自己的见解,希望能够C++初学者解惑。

要弄清楚重载、覆盖、多态与函数隐藏之间的复杂且微妙关系之前,我们首先要来回顾一下重载覆盖等基本概念。

首先,我们来看一个非常简单的例子,理解一下什么叫函数隐藏hide。

#include <iostream>
using namespace std;
class Base{
public:
  void fun() { cout << "Base::fun()" << endl; }
};
class Derive : public Base{
public:
  void fun(int i) { cout << "Derive::fun()" << endl; }
};
int main()
{
  Derive d;
     //下面一句错误,故屏蔽掉
  //d.fun();error C2660: &#39;fun&#39; : function does not take 0 parameters
  d.fun(1);
     Derive *pd =new Derive();
     //下面一句错误,故屏蔽掉
     //pd->fun();error C2660: &#39;fun&#39; : function does not take 0 parameters
     pd->fun(1);
     delete pd;
  return 0;
}

/*在不同的非命名空间作用域里的函数不构成重载,子类和父类是不同的两个作用域。
在本例中,两个函数在不同作用域中,故不够成重载,除非这个作用域是命名空间作用域。*/
在这个例子中,函数不是重载overload,也不是覆盖override,而是隐藏hide。

接下来的5个例子具体说明一下什么叫隐藏

例1

#include <iostream>
using namespace std;
class Basic{
public:
     void fun(){cout << "Base::fun()" << endl;}//overload
     void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
     void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
     Derive d;
     d.fun();//正确,派生类没有与基类同名函数声明,则基类中的所有同名重载函数都会作为候选函数。
     d.fun(1);//正确,派生类没有与基类同名函数声明,则基类中的所有同名重载函数都会作为候选函数。
     return 0;
}

例2

#include <iostream>
using namespace std;
class Basic{
public:
     void fun(){cout << "Base::fun()" << endl;}//overload
     void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
     //新的函数版本,基类所有的重载版本都被屏蔽,在这里,我们称之为函数隐藏hide
  //派生类中有基类的同名函数的声明,则基类中的同名函数不会作为候选函数,即使基类有不同的参数表的多个版本的重载函数。
     void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;}
     void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
     Derive d;
     d.fun(1,2);
     //下面一句错误,故屏蔽掉
     //d.fun();error C2660: &#39;fun&#39; : function does not take 0 parameters
     return 0;
}

例3

#include <iostream>
using namespace std;
class Basic{
public:
     void fun(){cout << "Base::fun()" << endl;}//overload
     void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
     //覆盖override基类的其中一个函数版本,同样基类所有的重载版本都被隐藏hide
  //派生类中有基类的同名函数的声明,则基类中的同名函数不会作为候选函数,即使基类有不同的参数表的多个版本的重载函数。
     void fun(){cout << "Derive::fun()" << endl;}
     void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
     Derive d;
     d.fun();
     //下面一句错误,故屏蔽掉
     //d.fun(1);error C2660: &#39;fun&#39; : function does not take 1 parameters
     return 0;
}

例4

#include <iostream>
using namespace std;
class Basic{
public:
     void fun(){cout << "Base::fun()" << endl;}//overload
     void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
     using Basic::fun;
     void fun(){cout << "Derive::fun()" << endl;}
     void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
     Derive d;
     d.fun();//正确
     d.fun(1);//正确
     return 0;
}
/*
输出结果
Derive::fun()
Base::fun(int i)
Press any key to continue
*/

例5

#include <iostream>
using namespace std;
class Basic{
public:
     void fun(){cout << "Base::fun()" << endl;}//overload
     void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
     using Basic::fun;
     void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;}
     void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
     Derive d;
     d.fun();//正确
     d.fun(1);//正确
     d.fun(1,2);//正确
     return 0;
}
/*
输出结果
Base::fun()
Base::fun(int i)
Derive::fun(int i,int j)
Press any key to continue
*/

好了,我们先来一个小小的总结重载与覆盖两者之间的特征

重载overload的特征:

n          相同的范围(在同一个类中);
n          函数名相同参数不同;
n          virtual 关键字可有可无。

覆盖override是指派生类函数覆盖基类函数,覆盖的特征是:

n          不同的范围(分别位于派生类与基类);
n          函数名和参数都相同;
n          基类函数必须有virtual 关键字。(若没有virtual 关键字则称之为隐藏hide)

如果基类有某个函数的多个重载(overload)版本,而你在派生类中重写(override)了基类中的一个或多个函数版本,或是在派生类中重新添加了新的函数版本(函数名相同,参数不同),则所有基类的重载版本都被屏蔽,在这里我们称之为隐藏hide。所以,在一般情况下,你想在派生类中使用新的函数版本又想使用基类的函数版本时,你应该在派生类中重写基类中的所有重载版本。你若是不想重写基类的重载的函数版本,则你应该使用例4或例5方式,显式声明基类名字空间作用域。
事实上,C++编译器认为,相同函数名不同参数的函数之间根本没有什么关系,它们根本就是两个毫不相关的函数。只是C++语言为了模拟现实世界,为了让程序员更直观的思维处理现实世界中的问题,才引入了重载和覆盖的概念。重载是在相同名字空间作用域下,而覆盖则是在不同的名字空间作用域下,比如基类和派生类即为两个不同的名字空间作用域。在继承过程中,若发生派生类与基类函数同名问题时,便会发生基类函数的隐藏。当然,这里讨论的情况是基类函数前面没有virtual 关键字。在有virtual 关键字关键字时的情形我们另做讨论。
继承类重写了基类的某一函数版本,以产生自己功能的接口。此时C++编绎器认为,你现在既然要使用派生类的自己重新改写的接口,那我基类的接口就不提供给你了(当然你可以用显式声明名字空间作用域的方法,见[C++基础]重载、覆盖、多态与函数隐藏(1))。而不会理会你基类的接口是有重载特性的。若是你要在派生类里继续保持重载的特性,那你就自己再给出接口重载的特性吧。所以在派生类里,只要函数名一样,基类的函数版本就会被无情地屏蔽。在编绎器中,屏蔽是通过名字空间作用域实现的。

所以,在派生类中要保持基类的函数重载版本,就应该重写所有基类的重载版本。重载只在当前类中有效,继承会失去函数重载的特性。也就是说,要把基类的重载函数放在继承的派生类里,就必须重写。

这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,具体规则我们也来做一小结:

n          如果派生类的函数与基类的函数同名,但是参数不同。此时,若基类无virtual关键字,基类的函数将被隐藏。(注意别与重载混淆,虽然函数名相同参数不同应称之为重载,但这里不能理解为重载,因为派生类和基类不在同一名字空间作用域内。这里理解为隐藏)
n          如果派生类的函数与基类的函数同名,但是参数不同。此时,若基类有virtual关键字,基类的函数将被隐式继承到派生类的vtable中。此时派生类vtable中的函数指向基类版本的函数地址。同时这个新的函数版本添加到派生类中,作为派生类的重载版本。但在基类指针实现多态调用函数方法时,这个新的派生类函数版本将会被隐藏。
n          如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏。(注意别与覆盖混淆,这里理解为隐藏)。
n          如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数有virtual关键字。此时,基类的函数不会被“隐藏”。(在这里,你要理解为覆盖哦^_^)。

插曲:基类函数前没有virtual关键字时,我们要重写更为顺口些,在有virtual关键字时,我们叫覆盖更为合理些,戒此,我也希望大家能够更好的理解C++中一些微妙的东西。费话少说,我们举例说明吧。

例6

#include <iostream>
using namespace std;
  
class Base{
public:
     virtual void fun() { cout << "Base::fun()" << endl; }//overload
  virtual void fun(int i) { cout << "Base::fun(int i)" << endl; }//overload
};
  
class Derive : public Base{
public:
     void fun() { cout << "Derive::fun()" << endl; }//override
  void fun(int i) { cout << "Derive::fun(int i)" << endl; }//override
     void fun(int i,int j){ cout<< "Derive::fun(int i,int j)" <<endl;}//overload
};
  
int main()
{
 Base *pb = new Derive();
 pb->fun();
 pb->fun(1);
 //下面一句错误,故屏蔽掉
 //pb->fun(1,2);virtual函数不能进行overload,error C2661: &#39;fun&#39; : no overloaded function takes 2 parameters
  
 cout << endl;
 Derive *pd = new Derive();
 pd->fun();
 pd->fun(1);
 pd->fun(1,2);//overload
  
 delete pb;
 delete pd;
 return 0;
}
/*

输出结果

 
Derive::fun()
Derive::fun(int i)
 
Derive::fun()
Derive::fun(int i)
Derive::fun(int i,int j)
Press any key to continue
*/

例7-1

#include <iostream> 
using namespace std;
  
class Base{
public:
     virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
};
  
class Derive : public Base{};
  
int main()
{
     Base *pb = new Derive();
     pb->fun(1);//Base::fun(int i)
     delete pb;
     return 0;
}

例7-2

#include <iostream>
using namespace std;
  
class Base{
public:
     virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
};
  
class Derive : public Base{
public:
  void fun(double d){ cout <<"Derive::fun(double d)"<< endl; }
};
  
int main()
{
     Base *pb = new Derive();
     pb->fun(1);//Base::fun(int i)
     pb->fun((double)0.01);//Base::fun(int i)
     delete pb;
     return 0;
}

  

例8-1

#include <iostream>
using namespace std;
  
class Base{
public:
     virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
};
  
class Derive : public Base{
public:
     void fun(int i){ cout <<"Derive::fun(int i)"<< endl; }
};
  
int main()
{
     Base *pb = new Derive();
     pb->fun(1);//Derive::fun(int i)
     delete pb;
     return 0;
}

例8-2

#include <iostream>
using namespace std;
  
class Base{
public:
     virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
};
  
class Derive : public Base{
public:
     void fun(int i){ cout <<"Derive::fun(int i)"<< endl; }
     void fun(double d){ cout <<"Derive::fun(double d)"<< endl; }    
};
  
int main()
{
     Base *pb = new Derive();
     pb->fun(1);//Derive::fun(int i)
     pb->fun((double)0.01);//Derive::fun(int i)
     delete pb;
     return 0;
}

     

例9

#include <iostream>
using namespace std;
  
class Base{
public:
     virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
 
};
class Derive : public Base{
public:
    void fun(int i){ cout <<"Derive::fun(int i)"<< endl; }
    void fun(char c){ cout <<"Derive::fun(char c)"<< endl; }
    void fun(double d){ cout <<"Derive::fun(double d)"<< endl; }    
};
int main()
{
     Base *pb = new Derive();
     pb->fun(1);//Derive::fun(int i)
     pb->fun(&#39;a&#39;);//Derive::fun(int i)
     pb->fun((double)0.01);//Derive::fun(int i)
  
     Derive *pd =new Derive();
     pd->fun(1);//Derive::fun(int i)
     //overload
     pd->fun(&#39;a&#39;);//Derive::fun(char c)    
     //overload
     pd->fun(0.01);//Derive::fun(double d)    
  
     delete pb;
     delete pd;
     return 0;
}

 

例7-1和例8-1很好理解,我把这两个例子放在这里,是让大家作一个比较摆了,也是为了帮助大家更好的理解:
n          例7-1中,派生类没有覆盖基类的虚函数,此时派生类的vtable中的函数指针指向的地址就是基类的虚函数地址。
n          例8-1中,派生类覆盖了基类的虚函数,此时派生类的vtable中的函数指针指向的地址就是派生类自己的重写的虚函数地址。
在例7-2和8-2看起来有点怪怪,其实,你按照上面的原则对比一下,答案也是明朗的:
n          例7-2中,我们为派生类重载了一个函数版本:void fun(double d)  其实,这只是一个障眼法。我们具体来分析一下,基类共有几个函数,派生类共有几个函数:
类型
 基类
 派生类
 
Vtable部分
 void fun(int i)
 指向基类版的虚函数void fun(int i)
 
静态部分
  
 void fun(double d)
 

 
我们再来分析一下以下三句代码
Base *pb = new Derive();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
 
这第一句是关键,基类指针指向派生类的对象,我们知道这是多态调用;接下来第二句,运行时基类指针根据运行时对象的类型,发现是派生类对象,所以首先到派生类的vtable中去查找派生类的虚函数版本,发现派生类没有覆盖基类的虚函数,派生类的vtable只是作了一个指向基类虚函数地址的一个指向,所以理所当然地去调用基类版本的虚函数。最后一句,程序运行仍然埋头去找派生类的vtable,发现根本没有这个版本的虚函数,只好回头调用自己的仅有一个虚函数。
 
这里还值得一提的是:如果此时基类有多个虚函数,此时程序编绎时会提示”调用不明确”。示例如下

#include <iostream>
using namespace std;
  
class Base{
public:
     virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
          virtual void fun(char c){ cout <<"Base::fun(char c)"<< endl; }
};
  
class Derive : public Base{
public:
  void fun(double d){ cout <<"Derive::fun(double d)"<< endl; }
};
  
int main()
{
     Base *pb = new Derive();
          pb->fun(0.01);//error C2668: &#39;fun&#39; : ambiguous call to overloaded function
     delete pb;
     return 0;
}

好了,我们再来分析一下例8-2。

n          例8-2中,我们也为派生类重载了一个函数版本:void fun(double d)  ,同时覆盖了基类的虚函数,我们再来具体来分析一下,基类共有几个函数,派生类共有几个函数:

유형
기본 클래스
파생 클래스

Vtable 부분
void fun(int i)
void fun(int i)

정적 부분

void fun(double d)
테이블에서 파생 클래스의 vtable에 있는 함수 포인터가 자체적으로 다시 작성된 가상 함수 주소를 가리키는 것을 볼 수 있습니다.

다음 세 줄의 코드를 분석해 보겠습니다.

Base *pb = new Derive();
pb->fun(1);//Derive::fun(int i )
pb->fun((double)0.01);//Derive::fun(int i)

첫 번째 문장에 대해서는 더 말할 필요가 없습니다. 파생 클래스의 가상 함수를 호출하는 게 당연하다.. 세 번째 문장, 야, 또 이상한 느낌이 든다. 사실 C++ 프로그램은 실행할 때 파생 클래스의 vtable 테이블에 빠져들기도 했다. 젠장, 생각도 못했는데 내가 원하는 버전이 뭔지 정말 모르겠는데 그냥 돌아다니면서 찾아보면 되지 않나? 기본 클래스가 너무 낡아서 눈으로만 볼 수 있는 것 같아요. Vtable이 아닌 부분(즉, 정적 부분)과 관리하고 싶은 Vtable 부분(이중)입니다. d) 파생 클래스는 너무 멀어서 볼 수 없습니다! 게다가 파생 클래스가 모든 것을 처리해야 하지 않습니까? 당신은 자신의 권리가 있습니까? , 각자 자기 일을 하세요^_^

아아! 기본 클래스 포인터는 다형성 호출을 할 수 있지만 파생 클래스는 절대 호출할 수 없습니다(예제 6 참조).

예제 9를 다시 살펴보겠습니다.
이 예제의 효과는 예제 6과 동일하며 접근 방식은 동일하지만 목표는 동일합니다. 위의 예를 이해하신 후에는 이것도 작은 키스라고 믿습니다.
요약:

오버로딩은 함수의 매개변수 목록을 기반으로 호출할 함수 버전을 선택하는 반면, 다형성은 런타임 개체의 실제 유형을 기반으로 호출할 가상 함수 버전을 선택하는 것을 구현합니다. 상태는 파생 클래스가 기본 클래스의 가상 가상 함수를 재정의하여 달성됩니다. 파생 클래스가 기본 클래스의 가상 가상 함수를 재정의하지 않으면 파생 클래스는 자동으로 기본 클래스의 가상 가상 함수 버전을 상속합니다. . 이때 기본 클래스 포인터가 가리키는 객체가 기본 유형이든 파생 유형이든 관계없이 파생 클래스가 가상 가상 함수를 재정의하는 경우 기본 클래스 버전의 가상 함수가 호출됩니다. 실제 유형은 호출할 가상 가상 함수 버전을 선택하는 데 사용됩니다. 예를 들어 기본 클래스 포인터가 가리키는 객체 유형이 파생 유형인 경우 파생 클래스의 가상 가상 함수 버전이 호출되어 다형성을 달성합니다.

다형성을 사용하는 원래 의도는 기본 클래스에서 함수를 virtual로 선언하고 파생 클래스에서 재정의 기본 클래스의 가상 가상 함수 버전을 재정의하는 것입니다. 이때 일관성을 유지하십시오. 즉, 파생 클래스에 새 함수 버전을 추가하면 기본 클래스 포인터를 통해 파생 클래스의 새 함수 버전을 동적으로 호출할 수 없습니다. version은 파생 클래스의 오버로드된 버전으로만 사용됩니다. 여전히 동일한 문장이지만 오버로드는 현재 클래스에서만 유효합니다. 기본 클래스에서 오버로드하든 파생 클래스에서든 둘은 서로 관련이 없습니다. 이를 이해하면 예제 6과 9의 출력 결과도 성공적으로 이해할 수 있습니다.

오버로딩은 정적으로 연결되고, 다형성은 동적으로 연결됩니다. 더 자세히 설명하면 오버로딩은 포인터가 실제로 가리키는 객체의 유형과 관련이 없으며 다형성은 포인터가 실제로 가리키는 객체의 유형과 관련이 있습니다. 기본 클래스 포인터가 파생 클래스의 오버로드된 버전을 호출하는 경우 C++ 컴파일러는 이를 불법으로 간주합니다. C++ 컴파일러는 기본 클래스 포인터가 기본 클래스의 오버로드된 버전만 호출할 수 있고 오버로드는 네임스페이스에서만 작동한다고 생각합니다. 현재 클래스의 도메인 내에서 유효하면 상속은 오버로딩 기능을 잃게 됩니다. 물론 이때 기본 클래스 포인터가 가상 함수를 호출하면 기본 클래스 또는 가상의 가상 함수 버전도 동적으로 선택됩니다. 파생 클래스의 가상 함수 버전. 특정 작업을 수행하려면 기본 클래스 포인터가 실제로 가리키는 객체 유형에 따라 결정되므로 오버로드는 포인터가 실제로 가리키는 객체 유형과 관련이 없으며 다형성 포인터가 실제로 가리키는 개체의 유형과 관련이 있습니다.

마지막으로 명확히 하자면, 가상 가상 함수도 오버로드될 수 있지만 오버로드는 현재 네임스페이스 범위 내에서만 유효할 수 있습니다.

얼마나 많은 String 객체가 생성되었나요?
먼저 코드를 살펴보겠습니다.
Java 코드
String str=new String("abc");
이 코드 뒤에 오는 내용은 종종 이것이 정확히 무엇인지에 대한 질문입니다. 코드 줄은 몇 개나 생성되었습니까? 나는 모든 사람이 이 질문에 대해 잘 알고 있다고 생각하며, 그 대답도 잘 알려져 있습니다. 2. 다음으로, 이 질문에서 시작하여 String 객체 생성과 관련된 일부 JAVA 지식을 검토하겠습니다.
我们可以把上面这行代码分成String str、=、"abc"和new String()四部分来看待。String str只是定义了一个名为str的String类型的变量,因此它并没有创建对象;=是对变量str进行初始化,将某个对象的引用(或者叫句柄)赋值给它,显然也没有创建对象;现在只剩下new String("abc")了。那么,new String("abc")为什么又能被看成"abc"和new String()呢?我们来看一下被我们调用了的String的构造器: 
Java代码 
public String(String original) {  
    //other code ...  
}  
大家都知道,我们常用的创建一个类的实例(对象)的方法有以下两种: 
使用new创建对象。
调用Class类的newInstance方法,利用反射机制创建对象。
我们正是使用new调用了String类的上面那个构造器方法创建了一个对象,并将它的引用赋值给了str变量。同时我们注意到,被调用的构造器方法接受的参数也是一个String对象,这个对象正是"abc"。由此我们又要引入另外一种创建String对象的方式的讨论——引号内包含文本。 
这种方式是String特有的,并且它与new的方式存在很大区别。 
Java代码 
String str="abc";  
毫无疑问,这行代码创建了一个String对象。 
Java代码 
String a="abc";  
String b="abc";  
那这里呢?答案还是一个。 
Java代码 
String a="ab"+"cd";  
再看看这里呢?答案仍是一个。有点奇怪吗?说到这里,我们就需要引入对字符串池相关知识的回顾了。 
在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。 
我们再回头看看String a="abc";,这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,它的判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。因此,我们不难理解前面三个例子中头两个例子为什么是这个答案了。 
对于第三个例子: 
Java代码 
String a="ab"+"cd";  
由于常量的值在编译的时候就被确定了。在这里,"ab"和"cd"都是常量,因此变量a的值在编译时就可以确定。这行代码编译后的效果等同于: 
Java代码 
String a="abcd";  
因此这里只创建了一个对象"abcd",并且它被保存在字符串池里了。 
现在问题又来了,是不是所有经过“+”连接后得到的字符串都会被添加到字符串池中呢?我们都知道“==”可以用来比较两个变量,它有以下两种情况: 
如果比较的是两个基本类型(char,byte,short,int,long,float,double,boolean),则是判断它们的值是否相等。
如果表较的是两个对象变量,则是判断它们的引用是否指向同一个对象。
下面我们就用“==”来做几个测试。为了便于说明,我们把指向字符串池中已经存在的对象也视为该对象被加入了字符串池: 
Java代码 

public class StringTest {
  public static void main(String[] args) {
    String a = "ab";// 创建了一个对象,并加入字符串池中
    System.out.println("String a = \"ab\";");
    String b = "cd";// 创建了一个对象,并加入字符串池中
    System.out.println("String b = \"cd\";");
    String c = "abcd";// 创建了一个对象,并加入字符串池中
  
    String d = "ab" + "cd";
    // 如果d和c指向了同一个对象,则说明d也被加入了字符串池
    if (d == c) {
      System.out.println("\"ab\"+\"cd\" 创建的对象 \"加入了\" 字符串池中");
    }
    // 如果d和c没有指向了同一个对象,则说明d没有被加入字符串池
    else {
      System.out.println("\"ab\"+\"cd\" 创建的对象 \"没加入\" 字符串池中");
    }
  
    String e = a + "cd";
    // 如果e和c指向了同一个对象,则说明e也被加入了字符串池
    if (e == c) {
      System.out.println(" a +\"cd\" 创建的对象 \"加入了\" 字符串池中");
    }
    // 如果e和c没有指向了同一个对象,则说明e没有被加入字符串池
    else {
      System.out.println(" a +\"cd\" 创建的对象 \"没加入\" 字符串池中");
    }
  
    String f = "ab" + b;
    // 如果f和c指向了同一个对象,则说明f也被加入了字符串池
    if (f == c) {
      System.out.println("\"ab\"+ b  创建的对象 \"加入了\" 字符串池中");
    }
    // 如果f和c没有指向了同一个对象,则说明f没有被加入字符串池
    else {
      System.out.println("\"ab\"+ b  创建的对象 \"没加入\" 字符串池中");
    }
  
    String g = a + b;
    // 如果g和c指向了同一个对象,则说明g也被加入了字符串池
    if (g == c) {
      System.out.println(" a + b  创建的对象 \"加入了\" 字符串池中");
    }
    // 如果g和c没有指向了同一个对象,则说明g没有被加入字符串池
    else {
      System.out.println(" a + b  创建的对象 \"没加入\" 字符串池中");
    }
  }
}

运行结果如下: 
String a = "ab";
String b = "cd";
"ab"+"cd" 创建的对象 "加入了" 字符串池中
a  +"cd" 创建的对象 "没加入" 字符串池中
"ab"+ b   创建的对象 "没加入" 字符串池中
a  + b   创建的对象 "没加入" 字符串池中
从上面的结果中我们不难看出,只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中,对此我们不再赘述。 
但是有一种情况需要引起我们的注意。请看下面的代码: 
Java代码 

public class StringStaticTest {
  // 常量A
  public static final String A = "ab";
  
  // 常量B
  public static final String B = "cd";
  
  public static void main(String[] args) {
    // 将两个常量用+连接对s进行初始化
    String s = A + B;
    String t = "abcd";
    if (s == t) {
      System.out.println("s等于t,它们是同一个对象");
    } else {
      System.out.println("s不等于t,它们不是同一个对象");
    }
  }
}

这段代码的运行结果如下: 
s等于t,它们是同一个对象
这又是为什么呢?原因是这样的,对于常量来讲,它的值是固定的,因此在编译期就能被确定了,而变量的值只有到运行时才能被确定,因为这个变量可以被不同的方法调用,从而可能引起值的改变。在上面的例子中,A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说: 
Java代码 
String s=A+B;  
等同于: 
Java代码 
String s="ab"+"cd";  
我对上面的例子稍加改变看看会出现什么情况: 
Java代码 

public class StringStaticTest {
  // 常量A
  public static final String A;
  
  // 常量B
  public static final String B;
  
  static {
    A = "ab";
    B = "cd";
  }
  
  public static void main(String[] args) {
    // 将两个常量用+连接对s进行初始化
    String s = A + B;
    String t = "abcd";
    if (s == t) {
      System.out.println("s等于t,它们是同一个对象");
    } else {
      System.out.println("s不等于t,它们不是同一个对象");
    }
  }
}

 

它的运行结果是这样: 
s不等于t,它们不是同一个对象
只是做了一点改动,结果就和刚刚的例子恰好相反。我们再来分析一下。A和B虽然被定义为常量(只能被赋值一次),但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。 
由于字符串池中对象的共享能够带来效率的提高,因此我们提倡大家用引号包含文本的方式来创建String对象,实际上这也是我们在编程中常采用的。 
接下来我们再来看看intern()方法,它的定义如下: 
Java代码 
public native String intern();  
这是一个本地方法。在调用这个方法时,JAVA虚拟机首先检查字符串池中是否已经存在与该对象值相等对象存在,如果有则返回字符串池中对象的引用;如果没有,则先在字符串池中创建一个相同值的String对象,然后再将它的引用返回。 
我们来看这段代码: 
Java代码 

public class StringInternTest {
  public static void main(String[] args) {
    // 使用char数组来初始化a,避免在a被创建之前字符串池中已经存在了值为"abcd"的对象
    String a = new String(new char[] { &#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;d&#39; });
    String b = a.intern();
    if (b == a) {
      System.out.println("b被加入了字符串池中,没有新建对象");
    } else {
      System.out.println("b没被加入字符串池中,新建了对象");
    }
  }
}

运行结果: 
b没被加入字符串池中,新建了对象
如果String类的intern()方法在没有找到相同值的对象时,是把当前对象加入字符串池中,然后返回它的引用的话,那么b和a指向的就是同一个对象;否则b指向的对象就是JAVA虚拟机在字符串池中新建的,只是它的值与a相同罢了。上面这段代码的运行结果恰恰印证了这一点。 
最后我们再来说说String对象在JAVA虚拟机(JVM)中的存储,以及字符串池与堆(heap)和栈(stack)的关系。我们首先回顾一下堆和栈的区别: 
栈(stack):主要保存基本类型(或者叫内置类型)(char、byte、short、int、long、float、double、boolean)和对象的引用,数据可以共享,速度仅次于寄存器(register),快于堆。
堆(heap):用于存储对象。
我们查看String类的源码就会发现,它有一个value属性,保存着String对象的值,类型是char[],这也正说明了字符串就是字符的序列。 
当执行String a="abc";时,JAVA虚拟机会在栈中创建三个char型的值'a'、'b'和'c',然后在堆中创建一个String对象,它的值(value)是刚才在栈中创建的三个char型值组成的数组{'a','b','c'},最后这个新创建的String对象会被添加到字符串池中。如果我们接着执行String b=new String("abc");代码,由于"abc"已经被创建并保存于字符串池中,因此JAVA虚拟机只会在堆中新创建一个String对象,但是它的值(value)是共享前一行代码执行时在栈中创建的三个char型值值'a'、'b'和'c'。 
说到这里,我们对于篇首提出的String str=new String("abc")为什么是创建了两个对象这个问题就已经相当明了了。

更多Java中继承、多态、重载和重写介绍相关文章请关注PHP中文网!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.