다형성이란 무엇입니까? 구현 메커니즘은 무엇입니까? 오버로딩과 다시 쓰기의 차이점은 무엇입니까? 이번에 검토할 매우 중요한 네 가지 개념은 상속, 다형성, 오버로딩 및 덮어쓰기입니다.
계승
간단히 말해서 상속은 새 메서드를 추가하거나 기존 메서드를 재정의하여 기존 형식을 기반으로 새 형식을 생성하는 것입니다(아래에서 설명하는 것처럼 이 메서드를 다시 작성이라고 함). 상속은 객체지향의 세 가지 기본 특성인 캡슐화, 상속 및 다형성 중 하나입니다. JAVA를 사용할 때 작성하는 모든 클래스는 상속입니다. JAVA 언어에서 java.lang.Object 클래스는 가장 기본적인 기본 클래스( 또는 모든 클래스의 상위 클래스 또는 슈퍼 클래스). 새로 정의한 클래스가 어떤 기본 클래스에서 상속되는지 명시적으로 지정하지 않으면 JAVA는 기본적으로 Object 클래스에서 상속합니다.
JAVA의 클래스는 다음 세 가지 유형으로 나눌 수 있습니다.
클래스(Class): 클래스를 사용하여 정의된 클래스이며 추상 메소드를 포함하지 않습니다.
추상 클래스(Abstract class): 추상 클래스를 사용하여 정의된 클래스로, 추상 메소드를 포함할 수도 있고 포함하지 않을 수도 있습니다.
인터페이스: 인터페이스를 사용하여 정의된 클래스입니다.
이 세 가지 유형 사이에는 다음과 같은 상속 규칙이 있습니다.
클래스는 클래스, 추상 클래스를 확장하고 인터페이스를 구현할 수 있습니다.
추상 클래스는 클래스를 상속(확장)할 수 있고 추상 클래스를 상속(확장)할 수 있으며 인터페이스를 상속(구현)할 수 있습니다.
인터페이스는 인터페이스만 확장할 수 있습니다.
위 세 가지 규칙의 각 상속 사례에 사용된 서로 다른 키워드 확장 및 구현은 마음대로 바꿀 수 없습니다. 우리 모두 알고 있듯이 일반 클래스는 인터페이스를 상속한 후 이 인터페이스에 정의된 모든 메서드를 구현해야 합니다. 그렇지 않으면 추상 클래스로만 정의할 수 있습니다. 여기서 Implements 키워드에 "구현"이라는 용어를 사용하지 않는 이유는 개념적으로도 상속 관계를 나타내기 때문이고 추상 클래스 구현 인터페이스의 경우 이 인터페이스 정의를 구현할 필요가 없기 때문입니다. 방법이므로 상속을 사용하는 것이 더 합리적입니다.
위의 세 가지 규칙은 다음 제약 조건도 준수합니다.
클래스와 추상 클래스는 모두 최대 하나의 클래스 또는 최대 하나의 추상 클래스만 상속할 수 있으며, 이 두 상황은 상호 배타적입니다. 즉, 클래스 또는 추상 클래스를 상속합니다.
클래스, 추상 클래스 및 인터페이스가 인터페이스를 상속하는 경우 이론적으로 인터페이스 수에 제한이 없습니다. 물론 클래스의 경우 상속하는 모든 인터페이스에 정의된 모든 메서드를 구현해야 합니다.
추상 클래스가 추상 클래스를 상속하거나 인터페이스를 구현할 때 부모 추상 클래스의 추상 메서드나 부모 클래스 인터페이스에 정의된 인터페이스를 부분적으로, 완전히 또는 완전히 구현하지 않을 수 있습니다.
클래스가 추상 클래스를 상속하거나 인터페이스를 구현하는 경우 부모 추상 클래스의 모든 추상 메서드 또는 부모 클래스 인터페이스에 정의된 모든 인터페이스를 구현해야 합니다.
상속이 프로그래밍에 가져오는 이점은 원래 클래스를 재사용할 수 있다는 것입니다. 모듈 재사용과 마찬가지로 클래스 재사용은 개발 효율성을 향상시킬 수 있습니다. 실제로 모듈 재사용은 수많은 클래스 재사용의 중첩 효과입니다. 상속 외에도 합성을 사용하여 클래스를 재사용할 수도 있습니다. 소위 조합은 원래 클래스를 새 클래스의 속성으로 정의하고 새 클래스에서 원래 클래스의 메서드를 호출하여 재사용을 달성하는 것입니다. 새롭게 정의된 유형과 원형의 유형 사이에 포함된 관계가 없다면, 즉 추상적인 개념에서 새로 정의된 유형으로 표현되는 것들은 황인과 같이 원형으로 표현되는 것들 중 하나가 아니다. 인간의 일종이며, 포함하고 포함되는 등의 관계가 있으므로 이때 재사용을 달성하려면 조합이 더 나은 선택입니다. 다음 예는 조합의 간단한 예입니다.
public class Sub { private Parent p = new Parent(); public void doSomething() { // Parent 클래스의 p.method() 메소드 재사용 // 다른 코드 } } class Parent { public void method() { / / 여기서 뭔가를 하세요 } }
물론, 코드를 보다 효율적으로 만들기 위해 원래 유형(예: Parent p)을 사용해야 할 때 초기화할 수도 있습니다.
원본 클래스를 재사용하기 위해 상속과 조합을 사용하는 것은 점진적인 개발 모델입니다. 이 방법의 장점은 원본 코드를 수정할 필요가 없으므로 원본 코드에 새로운 버그가 발생하지 않으며 수정할 필요도 없다는 것입니다. 원본 코드 수정으로 인해 다시 테스트를 하게 되었는데, 이는 분명히 우리 개발에 도움이 되었습니다. 따라서 원래 시스템이나 모듈을 유지 관리하거나 변형하는 경우, 특히 이에 대한 철저한 이해가 없는 경우 점진적 개발 모델을 선택할 수 있습니다. 이는 개발 효율성을 크게 향상시킬 수 있을 뿐만 아니라 다음으로 인한 위험을 피할 수 있습니다. 원본 코드 수정.
다형성
다형성은 위에서 언급한 또 다른 중요한 기본 개념으로 객체지향의 세 가지 기본 특성 중 하나입니다. 다형성이란 정확히 무엇입니까? 이해를 돕기 위해 먼저 다음 예를 살펴보겠습니다.
//자동차 인터페이스 인터페이스 Car { // 자동차 이름 String getName(); // 자동차 가격 가져오기 int getPrice() } // BMW 클래스 BMW 구현 Car { public String getName() } public int; getPrice() { return 300000; } } // CheryQQ 클래스 CheryQQ는 Car { public String getName() { return "CheryQQ"를 구현합니다. { return 20000; } } // 자동차 판매점 public class CarShop { // 자동차 판매 수입 private int Money = 0; // 자동차 판매 public void SellCar(Car car) { System.out.println("자동차 모델: " + car.getName() + " 단가: " + car.getPrice()); // 자동차 판매 수입 증가 += car.getPrice() } // 자동차 판매 수입 합계 public int getMoney() { return Money; } public static void main(String[] args) { CarShop aShop = new CarShop(); // BMW Chery QQ 판매 aShop; .sellCar(new CheryQQ()); System.out.println("총 수익: " + aShop.getMoney());
실행 결과:
모델 : BMW 단가 : 300,000
모델 : CheryQQ 단가 : 20,000
총 수익: 320,000
상속은 다형성을 구현하는 기본이다. 문자 그대로 이해하면 다형성은 여러 상태를 나타내는 유형(두 자동차 유형 모두)입니다(BMW 이름은 BMW이고 가격은 300,000, Chery 이름은 CheryQQ이고 가격은 2,000). 메소드 호출을 메소드가 속한 주체(즉, 객체나 클래스)와 연관시키는 것을 바인딩이라고 하며, 이는 초기 바인딩과 후기 바인딩의 두 가지 유형으로 나뉩니다. 해당 정의는 아래에 설명되어 있습니다.
초기 바인딩: 프로그램이 실행되기 전의 바인딩으로, 컴파일러와 링커에 의해 구현되며 정적 바인딩이라고도 합니다. 예를 들어 정적 메서드와 최종 메서드는 암시적으로 최종 메서드이므로 여기에도 포함됩니다.
후기 바인딩: 런타임 시 개체의 유형에 따른 바인딩으로, 메서드 호출 메커니즘에 의해 구현되므로 동적 바인딩 또는 런타임 바인딩이라고도 합니다. 초기 바인딩을 제외한 모든 방법은 후기 바인딩입니다.
다형성은 후기 바인딩 메커니즘에서 구현됩니다. 다형성이 우리에게 가져오는 이점은 클래스 간의 결합 관계를 제거하고 프로그램을 더 쉽게 확장할 수 있다는 것입니다. 예를 들어, 위의 예에서 새로운 유형의 판매용 자동차를 추가하려면 새로 정의된 클래스가 Car 클래스를 상속하고 원래 코드를 수정하지 않고 모든 메서드를 구현하도록 하면 됩니다. ) CarShop 클래스의 Method는 새로운 자동차 모델을 처리할 수 있습니다. 새로운 코드는 다음과 같습니다:
// Santana 자동차 클래스 Santana는 Car { public String getName() { return "Santana" } public int getPrice() { return 80000 } ;
오버로딩 및 재정의
오버로딩과 재작성은 모두 메서드에 대한 개념입니다. 이 두 개념을 명확히 하기 전에 먼저 메서드의 구조가 무엇인지 이해해 보겠습니다. (영어 이름은 서명이고 일부는 "서명"으로 번역되지만 더 널리 사용되지만 이 번역은 그렇지 않습니다. 정확한). 구조는 메소드의 구성 구조를 말하며, 특히 메소드의 이름과 매개변수를 포함하고 매개변수의 수, 유형 및 출현 순서를 포괄하지만 메소드의 반환 값 유형, 액세스 한정자 및 수정 사항은 포함하지 않습니다. 추상, 정적, 최종 기호 등이 있습니다. 예를 들어, 다음 두 메서드는 동일한 구조를 갖습니다.
public void method(int i, String s) { // 뭔가를 합니다 } public String method(int i, String s) { // 뭔가를 합니다 }
이 두 가지 방법은 구성이 다릅니다.
public void method(int i, String s) { // 뭔가를 합니다 } public void method(String s, int i) { // 뭔가를 합니다 }
게슈탈트의 개념을 이해한 후 오버로딩과 재작성에 대해 살펴보겠습니다. 해당 정의를 살펴보세요.
오버라이딩(Overriding), 영어 이름은 오버라이드(overriding), 상속의 경우 베이스 클래스의 메소드와 동일한 구조를 가진 새로운 메소드가 서브클래스에 정의되는 것을 의미하며, 이를 베이스 클래스의 메소드를 오버라이딩하는 서브클래스(subclass overriding)라고 한다. 이는 다형성을 달성하는 데 필요한 단계입니다.
오버로딩(Overloading)은 영어 이름이 오버로드(overloading)이며, 동일한 클래스에서 이름은 같지만 구조가 다른 메서드를 두 개 이상 정의하는 것을 말합니다. 동일한 클래스 내에서는 동일한 유형의 메소드를 두 개 이상 정의할 수 없습니다.
흥미로운 질문을 생각해 봅시다. 생성자가 오버로드될 수 있습니까? 대답은 물론 그렇습니다. 실제 프로그래밍에서는 종종 이 작업을 수행합니다. 실제로 생성자 이름은 메서드 이름이고 생성자 매개 변수는 메서드 매개 변수이며 반환 값은 새로 생성된 클래스의 인스턴스입니다. 그러나 하위 클래스는 기본 클래스와 동일한 유형의 생성자를 정의할 수 없기 때문에 생성자를 하위 클래스로 재정의할 수 없습니다.
오버로딩, 오버라이딩, 다형성 및 함수 숨기기
일부 C++ 초보자는 오버로딩, 덮어쓰기, 다형성 및 함수 숨기기에 대해 막연하게 이해하고 있는 경우가 많습니다. 나는 C++ 초보자들이 그들의 의심을 해소하는 데 도움이 되기를 바라며 여기에 내 자신의 의견을 쓸 것입니다.
오버로딩, 덮어쓰기, 다형성 및 함수 숨기기 사이의 복잡하고 미묘한 관계를 이해하기 전에 먼저 오버로딩 및 적용 범위와 같은 기본 개념을 검토해야 합니다.
먼저 함수 숨기기가 무엇인지 이해하기 위해 아주 간단한 예를 살펴보겠습니다.
#include <iostream>네임스페이스 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 : 'fun': 함수는 0개의 매개변수를 사용하지 않습니다. d.fun(1); Derive *pd =new Derive(); //다음 문장이 잘못되었으므로 차단됩니다. //pd->fun();error C2660: 'fun': 함수는 0개의 매개변수를 사용하지 않습니다. pd->fun(1); delete pd;
/*네임스페이스가 아닌 다른 범위의 함수는 오버로드를 구성하지 않습니다. 하위 클래스와 상위 클래스는 두 개의 다른 범위입니다.
이 예에서 두 함수는 서로 다른 범위에 있으므로 범위가 네임스페이스 범위가 아닌 한 오버로드되지 않습니다. */
이 예에서 함수는 오버로드되거나 재정의되지 않고 숨겨져 있습니다.
다음 다섯 가지 예에서는 숨김이 무엇인지 구체적으로 설명합니다.
실시예 1
#include <iostream>네임스페이스 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);//정답입니다. 파생 클래스에 기본 클래스와 동일한 이름을 가진 함수 선언이 없으면 기본 클래스에서 동일한 이름을 가진 모든 오버로드된 함수가 후보 함수로 사용됩니다. 0을 반환;}
실시예 2
#include <iostream>네임스페이스 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 함수 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: 'fun' : 함수는 0개의 매개변수를 사용하지 않음 0을 반환;}
실시예 3
#include <iostream>네임스페이스 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 fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ 파생 d; d.fun(); //다음 문장이 잘못되어 차단됩니다. //d.fun(1);error C2660: 'fun': 함수는 1개의 매개변수를 사용하지 않습니다. return 0;}
실시예 4
#include <iostream>네임스페이스 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.fun();//정확한 d.fun(1); //올바른 반환 0;}/*출력 결과 Derive::fun()Base::fun(int i)계속하려면 아무 키나 누르십시오*/
실시예 5
#include <iostream>네임스페이스 std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class 파생 :public Basic{public: 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; .fun();//d.fun(1) 수정;//d.fun(1,2) 수정;//return 0 수정;}/*출력 결과 Base::fun()Base::fun(int i)Derive::fun(int i,int j)계속하려면 아무 키나 누르십시오*/
좋아요, 먼저 오버로딩과 덮어쓰기의 특징을 간단히 요약해 보겠습니다.
과부하의 특성:
n 동일한 범위(동일한 클래스)
n 함수 이름은 동일하지만 매개변수가 다릅니다.
n virtual 키워드는 선택 사항입니다.
재정의는 파생 클래스 함수가 기본 클래스 함수를 다룬다는 의미입니다. 재정의의 특징은 다음과 같습니다.
n 다른 범위(각각 파생 클래스와 기본 클래스에 위치)
n 함수 이름과 매개변수는 동일합니다.
n 기본 클래스 함수에는 virtual 키워드가 있어야 합니다. (가상키워드가 없을 경우 숨김숨기기라고 합니다)
기본 클래스에 함수의 오버로드된 버전이 여러 개 있고 파생 클래스의 기본 클래스에서 하나 이상의 함수 버전을 재정의(재정의)하거나 파생 클래스 함수 버전에 새 버전을 추가하는 경우(동일한 함수 이름, 다른 매개변수) , 그러면 기본 클래스의 오버로드된 버전이 모두 차단됩니다. 여기서는 이를 숨김이라고 부릅니다. 따라서 일반적으로 파생 클래스에서 새 함수 버전을 사용하고 기본 클래스의 함수 버전을 사용하려는 경우 파생 클래스의 기본 클래스에서 오버로드된 모든 버전을 재정의해야 합니다. 기본 클래스의 오버로드된 함수 버전을 재정의하지 않으려면 예제 4 또는 예제 5를 사용하여 기본 클래스 네임스페이스 범위를 명시적으로 선언해야 합니다.
실제로 C++ 컴파일러는 함수 이름이 동일하고 매개변수가 다른 함수 사이에는 관계가 없다고 믿습니다. 이들은 단순히 서로 관련이 없는 두 함수입니다. 단지 C++ 언어에서는 실제 세계를 시뮬레이션하고 프로그래머가 실제 문제를 보다 직관적으로 처리할 수 있도록 오버로딩 및 덮어쓰기 개념을 도입했을 뿐입니다. 오버로드는 동일한 네임스페이스 범위에 속하지만 재정의는 서로 다른 네임스페이스 범위에 있습니다. 예를 들어 기본 클래스와 파생 클래스는 서로 다른 네임스페이스 범위에 있습니다. 상속 프로세스 중에 파생 클래스의 이름이 기본 클래스 함수와 동일한 경우 기본 클래스 함수가 숨겨집니다. 물론 여기서 논의되는 상황은 기본 클래스 함수 앞에 virtual 키워드가 없다는 것입니다. 가상키워드가 있을 경우의 상황에 대해서는 별도로 논의하도록 하겠습니다.
상속된 클래스는 기본 클래스의 함수 버전을 재정의하여 자체 기능 인터페이스를 만듭니다. 이때 C++ 컴파일러는 이제 파생 클래스의 다시 작성된 인터페이스를 사용하려고 하기 때문에 내 기본 클래스의 인터페이스는 제공되지 않을 것이라고 생각합니다(물론 네임스페이스 범위를 명시적으로 선언하는 방법을 사용할 수 있지만, [C++ 기초] 오버로딩, 덮어쓰기, 다형성 및 함수 숨기기(1))를 참조하세요. 기본 클래스의 인터페이스에 오버로드 특성이 있다는 점을 무시합니다. 파생 클래스에서 오버로딩 기능을 계속 유지하려면 인터페이스 오버로딩 기능을 직접 제공하세요. 따라서 파생 클래스에서는 함수 이름이 동일한 한 기본 클래스의 함수 버전이 무자비하게 차단됩니다. 컴파일러에서 마스킹은 네임스페이스 범위를 통해 구현됩니다.
따라서 파생 클래스에서 기본 클래스 함수의 오버로드된 버전을 유지하려면 기본 클래스의 오버로드된 버전을 모두 재정의해야 합니다. 오버로딩은 현재 클래스에서만 유효하며 상속은 함수 오버로딩의 특성을 잃게 됩니다. 즉, 기본 클래스의 오버로드된 함수를 상속받은 파생 클래스에 넣으려면 다시 작성해야 합니다.
여기서 "숨김"은 파생 클래스의 함수가 동일한 이름을 가진 기본 클래스 함수를 차단한다는 의미입니다. 또한 특정 규칙을 간략하게 요약하겠습니다.
n 파생 클래스의 함수가 기본 클래스의 함수와 이름은 동일하지만 매개변수가 다른 경우. 이때, 기본 클래스에 virtual 키워드가 없으면 기본 클래스의 기능은 숨겨집니다. (오버로딩과 혼동하지 않도록 주의하세요. 이름은 같고 매개변수가 다른 함수를 오버로딩이라고 불러야 하지만 여기서는 파생 클래스와 기본 클래스가 동일한 네임스페이스 범위에 있지 않기 때문에 오버로딩으로 이해될 수 없습니다. 숨기는 것으로 이해됨)
n 파생 클래스의 함수가 기본 클래스의 함수와 이름은 동일하지만 매개변수가 다른 경우. 이때 기본 클래스에 virtual 키워드가 있으면 기본 클래스의 함수가 파생 클래스의 vtable에 암시적으로 상속됩니다. 이때 파생 클래스 vtable의 함수는 기본 클래스 버전의 함수 주소를 가리킨다. 동시에 이 새로운 함수 버전은 파생 클래스의 오버로드된 버전으로 파생 클래스에 추가됩니다. 그러나 기본 클래스 포인터가 다형성 호출 함수 메서드를 구현하는 경우 이 새로운 파생 클래스 함수 버전은 숨겨집니다.
n 파생 클래스의 함수가 기본 클래스의 함수와 이름이 같고 매개변수도 동일하지만 기본 클래스 함수에 virtual 키워드가 없는 경우입니다. 이때 기본 클래스의 기능은 숨겨집니다. (여기서는 숨기는 것으로 이해되는 적용 범위와 혼동하지 않도록 주의하십시오.)
n 파생 클래스의 함수가 기본 클래스의 함수와 이름이 같고 매개변수도 동일하지만 기본 클래스 함수에 virtual 키워드가 있는 경우. 이때 기본 클래스의 기능은 "숨겨지지" 않습니다. (여기서는 커버리지로 이해하셔야 합니다^_^).
Interlude: 기본 클래스 함수 앞에 virtual 키워드가 없으면 더 원활하게 다시 작성해야 하며, virtual 키워드가 있으면 override라고 부르는 것이 더 합리적입니다. 누구나 C++를 더 잘 이해할 수 있습니다. 더 이상 고민하지 않고 예를 들어 설명해 보겠습니다.
실시예 6
#include <iostream>사용 네임스페이스 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; }//void fun(int i) { cout << "Derive::fun(int i)" << endl }//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); 가상 함수를 오버로드할 수 없습니다. 오류 C2661: 'fun': 2개의 매개변수를 사용하는 오버로드된 함수가 없습니다. cout << endl; pd = new Derive(); pd->fun(1);//overload pb; delete pd;
출력 결과
파생::재미()
파생::fun(int i)
파생::재미()
파생::fun(int i)
파생::fun(int i,int j)
계속하려면 아무 키나 누르세요.
*/
예제 7-1
#include <iostream> 네임스페이스 std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; int main(){ Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) delete pb;}
예제 7-2
#include <iostream> 네임스페이스 std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; void fun(double d){ cout <<"Derive::fun(double d)"<< endl } }; Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) pb->fun((double)0.01);//Base::fun(int i) delete pb;
예 8-1
#include <iostream> 네임스페이스 std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; void fun(int i){ cout <<"Derive::fun(int i)"<< endl }}{ Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) delete pb return 0;}
예 8-2
#include <iostream> 네임스페이스 std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; 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> 네임스페이스 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(); //파생::fun(int i) pb->fun('a');//파생::fun(int i) pb->fun((double)0.01);//Derive::fun(int i) Derive *pd =new Derive();//Derive::fun(int i) // overload pd->fun('a');//Derive::fun(char c) //overload pd->fun(0.01);//Derive::fun(double d) pb 삭제; 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 섹션
무효 재미(int i)
가상 함수 void fun(int i)의 기본 클래스 버전을 가리킵니다.
정적 부분
공허한 재미(더블디)
다음 세 줄의 코드를 다시 분석해 보겠습니다.
베이스 *pb = new Derive();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
첫 번째 문장이 핵심입니다. 기본 클래스 포인터는 파생 클래스의 개체를 가리킵니다. 우리는 이것이 다형성 호출이라는 것을 알고 있습니다. 런타임 시 기본 클래스 포인터는 다음을 기반으로 하는 파생 클래스 개체입니다. 런타임 객체의 유형이므로 먼저 파생 클래스의 vtable로 이동하여 파생 클래스의 가상 함수 버전을 찾으세요. 파생 클래스가 기본 클래스의 가상 함수를 포함하지 않는 것으로 나타났습니다. 파생 클래스는 기본 클래스의 가상 함수 주소에 대한 포인터만 만들기 때문에 가상 함수의 클래스 버전을 호출하는 것이 당연합니다. 마지막 문장에서 프로그램은 여전히 파생 클래스의 vtable을 찾고 있으며 이 버전의 가상 함수가 전혀 없다는 것을 발견하므로 돌아가서 자체 가상 함수를 호출해야 합니다.
이때 기본 클래스에 여러 가상 함수가 있는 경우 프로그램을 컴파일할 때 프로그램이 "불분명한 호출"이라는 메시지를 표시한다는 점도 여기서 언급할 가치가 있습니다. 예시는 다음과 같습니다
#include <iostream> 네임스페이스 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: 'fun': 오버로드된 함수에 대한 모호한 호출 delete pb; return 0;}
좋습니다. 예제 8-2를 다시 분석해 보겠습니다.
n 예제 8-2에서는 파생 클래스에 대한 함수 버전인 void fun(double d)도 오버로드했으며 기본 클래스의 가상 함수도 다루었습니다. 기본 클래스에는 여러 함수가 있습니다. , 파생 클래스에는 여러 가지 기능이 있습니다. 클래스에는 다음과 같은 여러 가지 기능이 있습니다.
유형 기본 클래스 파생 클래스
Vtable 섹션
무효 재미(int i)
무효 재미(int i)
정적 부분
공허한 재미(더블디)
테이블에서 파생 클래스의 vtable에 있는 함수 포인터가 자신의 재정의된 가상 함수 주소를 가리키는 것을 볼 수 있습니다.
다음 세 줄의 코드를 다시 분석해 보겠습니다.
베이스 *pb = new Derive();
pb->fun(1);//파생::fun(int i)
pb->fun((double)0.01);//Derive::fun(int i)
첫 번째 문장에 대해서는 더 이상 말할 필요가 없습니다. 두 번째 문장은 당연히 파생 클래스의 가상 함수 버전을 호출하는 것입니다. 세 번째 문장은 사실 C++ 프로그램이 매우 이상하게 느껴집니다. 바보. 실행할 때 파생 클래스의 vtable 테이블에 푹 빠져서 방금 보니 원하는 버전이 없다는 것을 깨달았습니다. , 기본 클래스 포인터는 왜 주위를 둘러보고 찾아보지 않습니까? 하하, 시력이 제한되어 있다는 것이 밝혀졌습니다. Vtable 부분(즉, 정적 부분)과 관리하려는 Vtable 부분, 파생 클래스의 공백 fun(double d)은 너무 멀어서 볼 수 없습니다! 게다가 파생 클래스가 모든 것을 처리해야 하는데 파생 클래스가 자체적으로 어떤 힘을 갖고 있지 않습니까? 몸조심하세요^_^
아아, 한숨 쉬실 겁니까? 기본 클래스 포인터는 다형성 호출을 할 수 있지만 파생 클래스에 대한 오버로드 호출은 절대 할 수 없습니다(예제 6 참조).
예제 9를 다시 살펴보겠습니다.
본 실시예의 효과는 실시예 6과 동일하며 목적도 동일하다. 위의 예를 이해하신 후에는 이것도 작은 키스라고 믿습니다.
요약:
오버로딩은 함수의 매개변수 목록을 기반으로 호출할 함수 버전을 선택하는 반면, 다형성은 런타임 개체의 실제 유형을 기반으로 호출할 가상 함수 버전을 선택합니다. 함수는 이를 재정의하여 구현됩니다. 파생 클래스가 기본 클래스의 가상 가상 함수를 재정의하지 않으면 파생 클래스는 자동으로 기본 클래스의 가상 가상 함수를 상속합니다. 함수 버전 이때 기본 클래스 포인터가 가리키는 객체가 기본 유형이든 파생 유형이든 관계없이 파생 클래스가 가상 가상 함수를 재정의하면 기본 클래스 버전의 가상 함수가 호출됩니다. 객체의 실제 유형은 호출할 가상 가상 함수 버전을 선택하는 데 사용됩니다. 예를 들어, 기본 클래스 포인터가 가리키는 객체 유형이 파생 유형인 경우입니다. , 파생 클래스의 가상 가상 함수 버전이 호출되어 다형성을 달성합니다.
다형성을 사용하는 원래 의도는 기본 클래스에서 함수를 virtual로 선언하고 파생 클래스에서 기본 클래스의 가상 가상 함수 버전을 재정의하는 것입니다. 이때 함수 프로토타입은 기본 클래스와 일치합니다. 즉, 동일한 이름과 동일한 이름입니다. 파생 클래스에 새 함수 버전을 추가하면 기본 클래스 포인터를 통해서만 파생 클래스의 새 함수 버전을 호출할 수 없습니다. 파생 클래스의 오버로드된 버전으로. 여전히 동일한 문장이지만 오버로드는 현재 클래스에서만 유효합니다. 기본 클래스에서 오버로드하든 파생 클래스에서 오버로드하든 둘은 서로 관련이 없습니다. 이를 이해하면 예제 6과 9의 출력 결과도 성공적으로 이해할 수 있습니다.
오버로딩은 정적으로 바인딩되고 다형성은 동적으로 바인딩됩니다. 더 자세히 설명하면 오버로딩은 포인터가 실제로 가리키는 객체의 유형과 관련이 없으며 다형성은 포인터가 실제로 가리키는 객체의 유형과 관련이 있습니다. 기본 클래스 포인터가 파생 클래스의 오버로드된 버전을 호출하는 경우 C++ 컴파일러는 이를 불법으로 간주합니다. C++ 컴파일러는 기본 클래스 포인터가 기본 클래스의 오버로드된 버전만 호출할 수 있고 오버로드는 네임스페이스에서만 작동한다고 생각합니다. 물론 이때 기본 클래스 포인터가 가상 함수를 호출하는 경우 상속은 무효화됩니다. 그런 다음 특정 작업을 수행하기 위해 기본 클래스의 가상 가상 함수 버전이나 파생 클래스의 가상 가상 함수 버전을 동적으로 선택합니다. 이는 기본 클래스 포인터가 실제로 가리키는 개체 유형에 따라 결정되므로 오버로딩과 포인터가 필요합니다. 포인터가 실제로 가리키는 객체의 유형은 그것과 아무 관련이 없습니다. 다형성은 포인터가 실제로 가리키는 객체의 유형과 관련이 있습니다.
마지막으로, 가상 가상 함수도 오버로드될 수 있지만 오버로드는 현재 네임스페이스 범위 내에서만 효과적일 수 있습니다. 얼마나 많은 String 객체가 생성되었습니까?
먼저 코드를 살펴보겠습니다.
자바 코드
String str=new String("abc");
이 코드 뒤에는 종종 이 코드 줄에서 얼마나 많은 String 개체가 생성됩니까?라는 질문이 뒤따릅니다. 나는 모든 사람이 이 질문에 대해 잘 알고 있다고 생각하며, 그 대답도 잘 알려져 있습니다. 2. 다음으로, 이 질문에서 시작하여 String 객체 생성과 관련된 일부 JAVA 지식을 검토하겠습니다.
위의 코드 줄은 String str, =, "abc" 및 new String()의 네 부분으로 나눌 수 있습니다. String str은 str이라는 String 유형 변수만 정의하므로 객체를 생성하지 않습니다. = 변수 str을 초기화하고 객체에 대한 참조(또는 핸들)를 할당하며, 이제 객체만 생성하지 않습니다. 문자열("abc")이 남습니다. 그렇다면 왜 new String("abc")을 "abc" 및 new String()으로 간주할 수 있습니까? 우리가 호출한 String 생성자를 살펴보겠습니다.
자바 코드
public String(문자열 원본) {
//다른 코드...
}
우리 모두 알고 있듯이 클래스의 인스턴스(객체)를 생성하는 데 일반적으로 사용되는 두 가지 방법이 있습니다.
객체를 생성하려면 new를 사용하세요.
Class 클래스의 newInstance 메소드를 호출하고 리플렉션 메커니즘을 사용하여 객체를 생성합니다.
new를 사용하여 String 클래스의 위 생성자 메서드를 호출하여 개체를 만들고 해당 참조를 str 변수에 할당했습니다. 동시에 호출된 생성자 메서드에서 허용하는 매개 변수도 String 개체이고 이 개체는 정확히 "abc"라는 것을 확인했습니다. 여기에서 우리는 String 객체를 생성하는 또 다른 방법, 즉 따옴표 안에 포함된 텍스트를 소개해야 합니다.
이 메서드는 String에 고유하며 새 메서드와는 매우 다릅니다.
자바 코드
문자열 str="abc";
이 코드 줄이 String 객체를 생성한다는 것은 의심의 여지가 없습니다.
자바 코드
문자열 a="abc";
문자열 b="abc";
여기는 어때요? 대답은 여전히 하나입니다.
자바 코드
문자열 a="ab"+"cd";
여기는 어때요? 대답은 여전히 하나입니다. 좀 이상해요? 이 시점에서 문자열 풀 관련 지식에 대한 리뷰를 소개해야 합니다.
JVM(JAVA Virtual Machine)에는 많은 String 객체를 저장하고 공유할 수 있는 문자열 풀이 있어 효율성이 향상됩니다. String 클래스는 final이므로 한번 생성되면 그 값을 변경할 수 없으므로 String 객체 공유로 인한 프로그램 혼란을 걱정할 필요가 없습니다. 문자열 풀은 String 클래스에 의해 유지 관리되며 intern() 메서드를 호출하여 문자열 풀에 액세스할 수 있습니다.
String a="abc";를 다시 살펴보겠습니다. 이 코드 줄이 실행되면 JAVA 가상 머신은 먼저 문자열 풀에서 "abc" 값을 가진 개체가 이미 존재하는지 확인합니다. String 클래스의 equals(Object obj) 메소드의 반환 값입니다. 있는 경우 새 개체가 생성되지 않고 기존 개체에 대한 참조가 직접 반환됩니다. 그렇지 않은 경우 개체가 먼저 생성된 다음 문자열 풀에 추가된 다음 해당 참조가 반환됩니다. 그러므로 앞의 세 가지 예 중 처음 두 가지에 왜 이런 대답이 있는지 이해하는 것은 어렵지 않습니다.
세 번째 예의 경우:
자바 코드
문자열 a="ab"+"cd";
상수의 값은 컴파일 타임에 결정되기 때문입니다. 여기서 "ab"와 "cd"는 상수이므로 변수 a의 값은 컴파일 타임에 결정될 수 있습니다. 이 코드 줄의 컴파일된 효과는 다음과 같습니다.
자바 코드
문자열 a="abcd";
따라서 여기서는 "abcd"라는 개체 하나만 생성되어 문자열 풀에 저장됩니다.
이제 다시 질문이 생깁니다. "+" 연결 이후에 얻은 모든 문자열이 문자열 풀에 추가됩니까? 우리 모두는 "=="를 사용하여 두 변수를 비교할 수 있다는 것을 알고 있습니다. 다음과 같은 두 가지 상황이 있습니다.
두 가지 기본 유형(char, byte, short, int, long, float, double, boolean)을 비교하면 그 값이 같은지 판단합니다.
테이블이 두 개체 변수를 비교하면 해당 참조가 동일한 개체를 가리키는지 여부를 판단합니다.
다음으로 "=="를 사용하여 몇 가지 테스트를 수행하겠습니다. 설명의 편의를 위해 문자열 풀에 이미 존재하는 개체를 문자열 풀에 추가되는 개체로 가리키는 것으로 간주합니다.
자바 코드
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"; c는 동일한 개체를 가리킵니다. 이는 e도 문자열 풀에 추가되었음을 의미합니다. if (e == c) { System.out.println(" a +/"cd/" 생성된 개체/"joined/" string 풀 중간"); } // e와 c가 동일한 개체를 가리키지 않으면 e가 문자열 풀에 추가되지 않았음을 의미합니다. else { System.out.println(" a +/"cd/" 생성된 개체/"not added/" to the string pool" ); } String f = "ab" + b; // f와 c가 동일한 객체를 가리키는 경우 f도 문자열 풀에 추가되었음을 의미합니다. if (f == c) { System.out .println("/ "ab/"+ b에 의해 생성된 객체 /"Added/" to the string pool"); } // 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 생성된 개체/"문자열 풀에 추가되지 않음/") } } }
실행 결과는 다음과 같습니다.
문자열 a = "ab";
문자열 b = "cd";
"ab"+"cd"로 생성된 개체는 문자열 풀에 "결합"됩니다.
+ "cd"로 생성된 개체는 문자열 풀에 "추가되지 않습니다".
"ab"+ b로 생성된 개체는 문자열 풀에 "추가되지 않습니다".
a + b로 생성된 객체는 문자열 풀에 "추가되지 않습니다". 위의 결과에서 텍스트를 포함하기 위해 따옴표를 사용하여 생성된 String 객체 사이에 "+" 연결을 사용하여 생성된 새 객체만 추가된다는 것을 쉽게 알 수 있습니다. . 스트링 풀에서. 새 모드에서 생성된 개체(null 포함)를 포함하는 모든 "+" 연결 표현식의 경우 생성된 새 개체는 문자열 풀에 추가되지 않으며 이에 대해 자세히 설명하지 않습니다.
그러나 우리의 관심을 요구하는 상황이 하나 있습니다. 아래 코드를 살펴보십시오.
자바 코드
public class StringStaticTest { // 상수 A public static final String A = "ab"; // 상수 B public static final String B = "cd" public static void main(String[] args) { // 두 개의 상수 사용 +Connect s를 초기화하려면 String s = A + B; if (s == t) { System.out.println("s는 t와 같습니다. 그들은 동일한 객체입니다."); { System.out.println("s는 t와 같지 않습니다. 동일한 객체가 아닙니다.") } } }
이 코드를 실행한 결과는 다음과 같습니다.
s는 t와 같습니다. 왜죠? 그 이유는 상수의 경우 그 값이 고정되어 있어 컴파일 타임에 결정될 수 있지만, 변수의 값은 런타임에만 결정될 수 있습니다. 왜냐하면 이 변수는 다른 메서드로 호출될 수 있기 때문입니다. 변경할 값. 위의 예에서 A와 B는 상수이고 그 값이 고정되어 있으므로 s의 값도 고정되어 있으며 클래스를 컴파일할 때 결정됩니다. 즉, 다음과 같습니다.
자바 코드
문자열 s=A+B;
다음과 동일:
자바 코드
문자열 s="ab"+"cd";
위의 예를 약간 변경하여 어떤 일이 발생하는지 살펴보겠습니다.
자바 코드
public class StringStaticTest { // 상수 A public static final String A; // 상수 B public static final String B; static { A = "ab" = "cd" } public static void main(String[] args) // 두 상수를 + 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 가상 머신은 먼저 해당 객체와 동일한 값을 가진 객체가 문자열 풀에 이미 있는지 확인합니다. 그렇다면 문자열 풀에 있는 객체에 대한 참조를 반환합니다. 같은 값을 가진 String 객체를 반환한 다음 해당 참조를 반환합니다.
이 코드를 살펴보겠습니다.
자바 코드
public class StringInternTest { public static void main(String[] args) { // a가 생성되기 전에 "abcd" 값을 가진 객체가 문자열 풀에 이미 존재하는 것을 방지하기 위해 char 배열을 사용하여 a를 초기화합니다. String a = new String ( new char[] { 'a', 'b', 'c', 'd' }); String b = a.intern() if (b == a) { System.out.println("b는 문자열 풀에 추가되었지만 새 개체는 생성되지 않았습니다."); } else { System.out.println("b는 문자열 풀에 추가되지 않았으며 새 개체는 생성되지 않았습니다."); } } }
실행 결과:
b는 문자열 풀에 추가되지 않았고 새 객체가 생성되었습니다. String 클래스의 intern() 메서드가 동일한 값을 가진 객체를 찾지 못하면 현재 객체를 문자열 풀에 추가한 다음 해당 객체를 반환합니다. 참조이면 b와 a는 동일한 객체를 가리킵니다. 그렇지 않으면 b가 가리키는 객체는 문자열 풀의 JAVA 가상 머신에 의해 새로 생성되지만 해당 값은 a와 동일합니다. 위 코드의 실행 결과는 이 점을 확인시켜줍니다.
마지막으로 JVM(JAVA Virtual Machine)에서 문자열 개체의 저장소와 문자열 풀과 힙 및 스택 간의 관계에 대해 이야기해 보겠습니다. 먼저 힙과 스택의 차이점을 살펴보겠습니다.
스택: 기본 유형(또는 내장 유형)(char, byte, short, int, long, float, double, boolean)과 객체 참조를 주로 저장하며 데이터 공유가 가능하며 속도는 등록보다 빠릅니다. 더미.
힙: 객체를 저장하는 데 사용됩니다.
String 클래스의 소스 코드를 보면 String 개체의 값을 저장하는 value 속성이 있음을 알 수 있습니다. 유형은 char[]이며 문자열이 문자 시퀀스임을 보여줍니다.
String a="abc";를 실행하면 JAVA 가상 머신은 스택에 'a', 'b', 'c' 세 개의 char 값을 생성한 후 힙에 String 객체를 생성하고 해당 값(value ) 는 스택 {'a', 'b', 'c'}에 방금 생성된 세 개의 char 값의 배열입니다. 마지막으로 새로 생성된 String 개체가 문자열 풀에 추가됩니다. 그런 다음 String b=new String("abc"); 코드를 실행하면 "abc"가 문자열 풀에 생성되고 저장되므로 JAVA 가상 머신은 힙에 새 String 개체만 생성하지만 value는 이전 코드 줄이 실행될 때 스택에 생성된 세 가지 char 유형 값 'a', 'b' 및 'c'입니다.
이 시점에서 우리는 이 기사의 시작 부분에서 제기된 String str=new String("abc")이 왜 두 개의 개체를 생성하는지에 대한 질문에 대해 이미 매우 명확했습니다.