Что такое полиморфизм? Каков механизм его реализации? В чем разница между перегрузкой и перезаписью? Это четыре очень важные концепции, которые мы собираемся рассмотреть на этот раз: наследование, полиморфизм, перегрузка и перезапись.
наследование
Проще говоря, наследование заключается в создании нового типа на основе существующего типа путем добавления новых методов или переопределения существующих методов (как обсуждается ниже, этот метод называется переписыванием). Наследование — это одна из трех основных характеристик объектно-ориентированного подхода — инкапсуляция, наследование и полиморфизм. Каждый класс, который мы пишем при использовании JAVA, является наследованием, поскольку в языке JAVA класс java.lang.Object является наиболее фундаментальным базовым классом ( или родительский класс или суперкласс) всех классов. Если определяемый нами новый класс явно не указывает, от какого базового класса он наследуется, то JAVA по умолчанию будет наследовать его от класса Object.
Мы можем разделить классы в JAVA на следующие три типа:
Класс: класс, определенный с помощью class и не содержащий абстрактных методов.
Абстрактный класс: класс, определенный с использованием абстрактного класса, который может содержать или не содержать абстрактные методы.
Интерфейс: класс, определенный с использованием интерфейса.
Между этими тремя типами существуют следующие правила наследования:
Классы могут расширять классы, абстрактные классы и реализовывать интерфейсы.
Абстрактные классы могут наследовать (расширять) классы, они могут наследовать (расширять) абстрактные классы и могут наследовать (реализовать) интерфейсы.
Интерфейсы могут только расширять интерфейсы.
Обратите внимание, что различные ключевые слова расширения и реализации, используемые в каждом случае наследования в трех приведенных выше правилах, не могут быть заменены по желанию. Как мы все знаем, после того как обычный класс унаследовал интерфейс, он должен реализовать все методы, определенные в этом интерфейсе, иначе его можно определить только как абстрактный класс. Причина, по которой я не использую здесь термин «реализация» для ключевого слова «реализует», заключается в том, что концептуально оно также представляет отношение наследования, и в случае интерфейса реализации абстрактного класса ему не обязательно реализовывать это определение интерфейса. Любой. метод, поэтому разумнее использовать наследование.
Вышеупомянутые три правила также соответствуют следующим ограничениям:
И классы, и абстрактные классы могут наследовать не более одного класса или не более одного абстрактного класса, и эти две ситуации являются взаимоисключающими, то есть они наследуют либо класс, либо абстрактный класс.
Когда классы, абстрактные классы и интерфейсы наследуют интерфейсы, их количество не ограничено. Теоретически они могут наследовать неограниченное количество интерфейсов. Конечно, класс должен реализовывать все методы, определенные во всех наследуемых им интерфейсах.
Когда абстрактный класс наследует абстрактный класс или реализует интерфейс, он может частично, полностью или полностью не реализовывать абстрактные методы родительского абстрактного класса или интерфейсы, определенные в интерфейсе родительского класса.
Когда класс наследует абстрактный класс или реализует интерфейс, он должен реализовать все абстрактные методы родительского абстрактного класса или все интерфейсы, определенные в интерфейсе родительского класса.
Преимущество, которое наследование приносит в наше программирование, — это повторное использование (повторное использование) исходных классов. Как и повторное использование модулей, повторное использование классов может повысить эффективность нашей разработки. Фактически повторное использование модулей — это наложенный эффект повторного использования большого количества классов. Помимо наследования, мы также можем использовать композицию для повторного использования классов. Так называемая комбинация заключается в том, чтобы определить исходный класс как атрибут нового класса и добиться повторного использования путем вызова методов исходного класса в новом классе. Если между вновь определенным типом и исходным типом нет включенной связи, то есть из абстрактной концепции, вещи, представленные вновь определенным типом, не являются одними из вещей, представленных исходным типом, например, желтые люди. Это тип человеческого существа, и между ними существуют отношения, включающие и включенные, поэтому в настоящее время комбинация является лучшим выбором для достижения повторного использования. Следующий пример представляет собой простой пример комбинации:
public class Sub { Private Parent p = new Parent(); // Повторно использовать метод p.method() родительского класса } } class Parent { public void Method() { / / сделай что-нибудь здесь } }
Конечно, чтобы сделать код более эффективным, мы также можем его инициализировать, когда нам нужно использовать исходный тип (например, Parent p).
Использование наследования и комбинирования для повторного использования исходных классов — это модель поэтапной разработки. Преимущество этого метода в том, что нет необходимости изменять исходный код, поэтому в исходном коде нет необходимости. повторное тестирование из-за изменений в исходном коде, что очевидно полезно для нашей разработки. Следовательно, если мы поддерживаем или трансформируем исходную систему или модуль, особенно если у нас нет их полного понимания, мы можем выбрать модель поэтапной разработки, которая может не только значительно повысить эффективность нашей разработки, но и избежать рисков, вызванных модификации исходного кода.
Полиморфизм
Полиморфизм — еще одна важная базовая концепция. Как упоминалось выше, это одна из трех основных характеристик объектно-ориентированного подхода. Что такое полиморфизм? Давайте сначала посмотрим на следующий пример, чтобы лучше понять:
// Интерфейс интерфейса автомобиля Car { // Имя автомобиля String getName(); // Получаем цену автомобиля int getPrice() } // Класс BMW BMW реализует Car { public String getName() { return "BMW" } public int; getPrice() { return 300000; } } // Класс CheryQQ CheryQQ реализует 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()); // Увеличиваем доход от продажи автомобилей += car.getPrice() } // Общий доход от продаж автомобилей public int; getMoney() { return Money; } public static void main(String[] args) { CarShop aShop = new CarShop(); // Продаем BMW aShop.sellCar(new BMW()); // Продаем 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 и реализовать все его методы, не внося никаких изменений в исходный код. SellCar(Car car. ) класса CarShop. Метод может обрабатывать новые модели автомобилей. Новый код выглядит следующим образом:
// Класс Santana Car Santana реализует Car { public String getName() { return "Santana" } public int getPrice() { return 80000 } };
Перегрузка и переопределение
Перегрузка и переписывание — это оба понятия для методов. Прежде чем разъяснить эти два понятия, давайте сначала разберемся, какова структура метода (английское название — подпись, некоторые переводятся как «подпись», хотя оно используется более широко, но такого перевода нет). точный). Структура относится к составной структуре метода, в частности, включая имя и параметры метода, включая количество, тип и порядок появления параметров, но не включает тип возвращаемого значения метода, модификаторы доступа и модификации. такие как абстрактный, статический и конечный символ. Например, следующие два метода имеют одинаковую структуру:
public void метод(int i, String s) { // делаем что-то } public String метод(int i, String s) { // делаем что-то }
Это два метода с разными конфигурациями:
public void метод(int i, String s) { // делаем что-то } public void метод(String s, int i) { // делаем что-то }
Поняв концепцию гештальта, давайте взглянем на перегрузку и переписывание. Пожалуйста, взгляните на их определения:
Overriding, английское название overrideing, означает, что в случае наследования в подклассе определяется новый метод с той же структурой, что и метод базового класса, который называется подклассом, переопределяющим метод базового класса. Это необходимый шаг для достижения полиморфизма.
Перегрузка, английское название — перегрузка, относится к определению более одного метода с одинаковым именем, но разными структурами в одном классе. В одном классе не разрешается определять более одного метода одного и того же типа.
Давайте рассмотрим интересный вопрос: Можно ли перегрузить конструкторы? Ответ, конечно, да, мы часто делаем это в реальном программировании. Фактически, конструктор также является методом. Имя конструктора — это имя метода, параметры конструктора — это параметры метода, а его возвращаемое значение — это экземпляр вновь созданного класса. Однако конструктор не может быть переопределен подклассом, поскольку подкласс не может определить конструктор того же типа, что и базовый класс.
Перегрузка, переопределение, полиморфизм и сокрытие функций
Часто можно заметить, что некоторые новички в C++ имеют смутное представление о перегрузке, перезаписи, полиморфизме и сокрытии функций. Я напишу здесь некоторые из своих мнений, надеясь помочь новичкам в C++ развеять свои сомнения.
Прежде чем мы поймем сложную и тонкую взаимосвязь между перегрузкой, перезаписью, полиморфизмом и сокрытием функций, мы должны сначала рассмотреть основные понятия, такие как перегрузка и покрытие.
Для начала давайте рассмотрим очень простой пример, чтобы понять, что такое сокрытие функции.
#include <iostream>using пространство имен 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();ошибка C2660 : 'fun': функция не принимает 0 параметров d.fun(1); Derive *pd =new Derive(); //Следующее предложение неверно, поэтому оно заблокировано //pd->fun(); ошибка C2660: 'fun': функция не принимает 0 параметров pd->fun(1); delete pd;}
/*Функции в разных областях, не относящихся к пространству имен, не представляют собой перегрузку. Подклассы и родительские классы — это две разные области.
В этом примере две функции находятся в разных областях, поэтому они не перегружаются, если только эта область не является областью пространства имен. */
В этом примере функция не перегружена и не переопределена, а скрыта.
Следующие пять примеров конкретно объясняют, что такое сокрытие.
Пример 1
#include <iostream>using пространство имен std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//перегрузка 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>using пространство имен std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//перегрузка void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: //Новая версия функции, все перегруженные версии базового класса заблокированы, здесь мы называем это скрыть функцию скрыть //Если в производном классе есть объявление функции с тем же именем, что и у базового класса, то функция с таким же именем в базовом классе не будет использоваться в качестве функции-кандидата, даже если базовый класс имеет несколько версий перегруженных функций с разными списками параметров. 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();ошибка C2660: 'fun': функция работает не принимать 0 параметров вернуть 0;}
Пример 3
#include <iostream>using пространство имен std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//перегрузка 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(){ Derive d; d.fun(); //Следующее предложение неверно, поэтому оно заблокировано //d.fun(1);ошибка C2660: 'fun': функция не принимает 1 параметр, возвращает 0;}
Пример 4
#include <iostream>using пространство имен std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//перегрузка 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); //Правильный возврат 0;}/*Вывод результата Derive::fun()Base::fun(int i)Нажмите любую клавишу, чтобы продолжить*/
Пример 5
#include <iostream>using пространство имен std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//перегрузка 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; .fun();//Исправить d.fun(1);//Исправить d.fun(1,2);//Правильный возврат 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++] Перегрузка, перезапись, полиморфизм и сокрытие функций (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; }//переопределить 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(); pd->fun(1); pd->fun(1,2);//перегрузка delete pb; delete return 0;}/*
Вывод результатов
Произвести::fun()
Derive::fun(int i)
Произвести::fun()
Derive::fun(int i)
Derive::fun(int i,int j)
Нажмите любую клавишу, чтобы продолжить
*/
Пример 7-1
#include <iostream> using namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< class Derive : public Base{}; int main() { Base *pb = new Derive(); pb->fun(1); //Base::fun(int i) delete pb;}
Пример 7-2
#include <iostream> using namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< 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 return 0;}
Пример 8-1
#include <iostream> using namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< class Derive : public Base{public: 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> using namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< 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> с использованием пространства имен 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(); //Derive::fun(int i) pb->fun('a');//Derive::fun(int i) pb->fun((double)0.01);//Derive::fun(int i) Derive *pd =new Derive(); pd->fun(1);//Derive::fun(int i) // перегрузка pd->fun('a');//Derive::fun(char c) //overload pd->fun(0.01);//Derive::fun(double d) удалить ПБ; удалить ПД; вернуть 0;}
Примеры 7-1 и 8-1 легко понять. Я привел эти два примера здесь, чтобы каждый мог их сравнить и помочь каждому лучше понять:
n В примере 7-1 производный класс не охватывает виртуальную функцию базового класса. В этот момент адрес, указанный указателем функции в виртуальной таблице производного класса, является адресом виртуальной функции базового класса.
В примере 8-1 производный класс переопределяет виртуальную функцию базового класса. В этот момент адрес, на который указывает указатель функции в виртуальной таблице производного класса, является адресом собственной переопределенной виртуальной функции производного класса.
Примеры 7-2 и 8-2 выглядят немного странно. На самом деле, если сравнить их по вышеизложенным принципам, ответ будет ясен:
n В примере 7-2 мы перегрузили версию функции для производного класса: void fun(double d) На самом деле это всего лишь прикрытие. Давайте проанализируем это конкретно. В базовом классе есть несколько функций, а в производном классе — несколько функций:
тип производного класса базового класса
Раздел виртуальной таблицы
пустота веселья (int i)
Указывает на версию виртуальной функции базового класса void fun(int i)
статическая часть
пустота веселья (двойное д)
Давайте еще раз проанализируем следующие три строки кода:
База *pb = новый Derive();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
Первое предложение является ключевым. Указатель базового класса указывает на объект производного класса. Мы знаем, что это полиморфный вызов. Второе предложение следует за ним. Указатель базового класса во время выполнения оказывается объектом производного класса, основанным на. тип объекта среды выполнения, поэтому сначала перейдите к виртуальной таблице производного класса, чтобы найти версию виртуальной функции производного класса. Обнаружено, что производный класс не охватывает виртуальную функцию базового класса. производный класс создает указатель только на адрес виртуальной функции базового класса, поэтому естественно вызывать версию виртуальной функции базового класса. В последнем предложении программа все еще ищет виртуальную таблицу производного класса и обнаруживает, что виртуальной функции этой версии вообще нет, поэтому ей приходится вернуться назад и вызвать собственную виртуальную функцию.
Здесь также стоит упомянуть, что если базовый класс в данный момент имеет несколько виртуальных функций, при компиляции программы программа выдаст сообщение «Непонятный вызов». Примеры следующие:
#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; }}; public Base{public: void fun(double d){ cout <<"Derive::fun(double) d)"<< endl; } }; int main(){ Base *pb = new Derive(); pb->fun(0.01);//ошибка C2668: 'fun': неоднозначный вызов перегруженной функции delete pb; return 0;}
Хорошо, давайте еще раз проанализируем пример 8-2.
В примере 8-2 мы также перегрузили версию функции для производного класса: void fun(double d), а также рассмотрели виртуальную функцию базового класса. В базовом классе есть несколько функций. , а производный класс имеет несколько функций. Класс имеет несколько функций:
тип производного класса базового класса
Раздел виртуальной таблицы
пустота веселья (int i)
пустота веселья (int i)
статическая часть
пустота веселья (двойное д)
Из таблицы мы видим, что указатель функции в vtable производного класса указывает на собственный адрес переопределенной виртуальной функции.
Давайте еще раз проанализируем следующие три строки кода:
База *pb = новый Derive();
pb->fun(1);//Derive::fun(int i)
pb->fun((double)0.01);//Derive::fun(int i)
Нет необходимости говорить больше о первом предложении. Второе предложение — это само собой разумеющийся вызов виртуальной функции производного класса. Третье предложение, эй, это снова кажется странным. На самом деле, программы на C++ очень странные. глупо. При запуске они погружаются в таблицу vtable производного класса, я просто посмотрел на нее и понял, что нет той версии, которая мне нужна, я действительно не могу разобраться. , почему указатели базового класса не оглядываются и не ищут его? Ха-ха, оказывается, зрение базового класса настолько старо, что у него, должно быть, пресбиопия. Все, что видят его глаза, - это его собственная не-. Часть Vtable (то есть статическая часть) и часть Vtable, которой вы хотите управлять, пустота производного класса. fun(double d) так далеко, что его не видно! Кроме того, производный класс должен обо всем позаботиться, разве у производного класса нет собственных возможностей Эй, хватит спорить, каждый может? берегите себя^_^
Увы! Вы собираетесь вздохнуть? Указатель базового класса может выполнять полиморфные вызовы, но никогда не может выполнять перегруженные вызовы производных классов (см. пример 6)~~~
Давайте еще раз посмотрим на пример 9.
Эффект этого примера такой же, как и эффекта примера 6, с той же целью. Я считаю, что после того, как вы поймете приведенные выше примеры, это тоже маленький Kiss.
краткое содержание:
Перегрузка выбирает версию функции для вызова на основе списка параметров функции, тогда как полиморфизм выбирает версию виртуальной функции для вызова на основе фактического типа объекта среды выполнения. Полиморфизм реализуется через производные классы к базовым классам. Функция реализуется путем ее переопределения. Если производный класс не переопределяет виртуальную виртуальную функцию базового класса, производный класс автоматически унаследует виртуальную виртуальную функцию базового класса. Версия функции. В этот момент независимо от того, является ли объект, на который указывает указатель базового класса, базовым типом или производным типом, виртуальная виртуальная функция версии базового класса будет вызвана, если производный класс переопределяет виртуальную виртуальную функцию. базового класса, он будет вызываться во время выполнения в соответствии с фактическим типом объекта, который используется для выбора вызываемой версии виртуальной виртуальной функции. Например, если тип объекта, на который указывает указатель базового класса, является производным типом. , будет вызвана версия виртуальной виртуальной функции производного класса, что обеспечит полиморфизм.
Первоначальное намерение использования полиморфизма состоит в том, чтобы объявить функцию виртуальной в базовом классе и переопределить версию виртуальной виртуальной функции базового класса в производном классе. Обратите внимание, что прототип функции на данный момент соответствует базовому классу. то есть то же имя и тот же тип параметра; если вы добавляете новую версию функции в производный класс, вы не можете динамически вызывать новую версию функции производного класса через указатель базового класса. как перегруженная версия производного класса. Все то же самое предложение: перегрузка действительна только в текущем классе. Независимо от того, перегружаете ли вы ее в базовом классе или в производном классе, они не связаны друг с другом. Если мы это поймем, мы также сможем успешно понять выходные результаты в примерах 6 и 9.
Перегрузка связана статически, полиморфизм связан динамически. Для дальнейшего пояснения: перегрузка не имеет ничего общего с типом объекта, на который фактически указывает указатель, а полиморфизм связан с типом объекта, на который фактически указывает указатель. Если указатель базового класса вызывает перегруженную версию производного класса, компилятор C++ считает это незаконным. Компилятор C++ думает только, что указатель базового класса может вызывать только перегруженную версию базового класса, и перегрузка работает только в пространстве имен. текущего класса. Действительно в домене, наследование потеряет возможность перегрузки. Конечно, если указатель базового класса в это время вызывает виртуальную функцию, Затем он также будет динамически выбирать версию виртуальной виртуальной функции базового класса или версию виртуальной виртуальной функции производного класса для выполнения определенных операций. Это определяется типом объекта, на который фактически указывает указатель базового класса, поэтому перегрузка и указатели. Тип объекта, на который фактически указывает указатель, не имеет к этому никакого отношения; полиморфизм связан с типом объекта, на который фактически указывает указатель.
Наконец, чтобы уточнить, виртуальные виртуальные функции также могут быть перегружены, но перегрузка может быть эффективна только в пределах текущего пространства имен. Сколько объектов String было создано?
Давайте сначала посмотрим на фрагмент кода:
Java-код
Строка str = новая строка («abc»);
За этим кодом часто следует вопрос, то есть сколько объектов String создается этой строкой кода? Думаю, этот вопрос знаком всем, и ответ известен: 2. Далее мы начнем с этого вопроса и рассмотрим некоторые знания JAVA, связанные с созданием объектов String.
Мы можем разделить приведенную выше строку кода на четыре части: String str, =, «abc» и new String(). String str определяет только переменную типа String с именем str, поэтому она не создает объект; = инициализирует переменную str и присваивает ей ссылку (или дескриптор) на объект и, очевидно, не создает объект; Теперь только new. Осталась строка("abc"). Итак, почему new String("abc") можно рассматривать как "abc" и new String()? Давайте посмотрим на конструктор String, который мы вызвали:
Java-код
общественная строка (оригинальная строка) {
//другой код...
}
Как мы все знаем, существует два часто используемых метода создания экземпляров (объектов) класса:
Используйте new для создания объектов.
Вызовите метод newInstance класса Class и используйте механизм отражения для создания объекта.
Мы использовали new для вызова вышеуказанного метода конструктора класса String, чтобы создать объект и присвоить его ссылку переменной str. При этом мы заметили, что параметр, принимаемый вызываемым методом-конструктором, также является объектом String, и этот объект — именно «abc». Исходя из этого, мы должны представить другой способ создания объекта String — текст, заключенный в кавычки.
Этот метод уникален для String и сильно отличается от нового метода.
Java-код
Строка str="abc";
Нет сомнений в том, что эта строка кода создает объект String.
Java-код
Строка а="abc";
Строка b="abc";
А что здесь? Ответ по-прежнему один.
Java-код
Строка a="ab"+"cd";
А что здесь? Ответ по-прежнему один. Немного странно? На этом этапе нам необходимо представить обзор знаний, связанных с пулом строк.
В виртуальной машине JAVA (JVM) имеется пул строк, в котором хранится множество объектов String, и его можно использовать совместно, что повышает эффективность. Поскольку класс String является финальным, его значение нельзя изменить после создания, поэтому нам не нужно беспокоиться о путанице в программе, вызванной совместным использованием объектов String. Пул строк поддерживается классом String, и мы можем вызвать метод intern() для доступа к пулу строк.
Давайте вернемся к String a="abc";. Когда эта строка кода выполняется, виртуальная машина JAVA сначала выполняет поиск в пуле строк, чтобы узнать, существует ли уже такой объект со значением «abc». Ее решение основано на этом. Возвращаемое значение метода равно(Object obj) класса String. Если да, то новый объект не будет создан, и будет возвращена прямая ссылка на существующий объект; в противном случае сначала будет создан объект, затем добавлен в пул строк, а затем будет возвращена его ссылка. Поэтому нам нетрудно понять, почему первые два из трех предыдущих примеров имеют такой ответ.
Для третьего примера:
Java-код
Строка a="ab"+"cd";
Потому что значение константы определяется во время компиляции. Здесь «ab» и «cd» — константы, поэтому значение переменной a можно определить во время компиляции. Скомпилированный эффект этой строки кода эквивалентен:
Java-код
Строка а="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"; // If e и c Указывает на тот же объект, это означает, что e также было добавлено в пул строк if (e == c) { System.out.println(" a +/"cd/" Созданный объект/"joined/" string середина бассейна"); } // Если e и c не указывают на один и тот же объект, это означает, что e не был добавлен в пул строк else { System.out.println(" a +/"cd/" созданный объект/"не добавлен/" в stringpool" ); } 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 создал объект/"не добавлен/" в пул строк");
Результаты бега следующие:
Строка а = «аб»;
Строка b = «cd»;
Объект, созданный с помощью «ab» + «cd», «присоединяется» к пулу строк.
Объект, созданный с помощью + «cd», «не добавляется» в пул строк.
Объект, созданный с помощью «ab» + b, «не добавляется» в пул строк.
Объект, созданный с помощью a + b, «не добавляется» в пул строк. Из приведенных выше результатов мы легко видим, что будут добавлены только новые объекты, созданные с использованием связей «+» между объектами String, созданными с использованием кавычек для включения текста. . в пуле строк. Для всех выражений подключения «+», которые содержат объекты, созданные в новом режиме (включая 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) { // Использовать две константы +Connect для инициализации 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-код
Строка s=A+B;
Эквивалентно:
Java-код
Строка 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-код
публичный собственный String intern();
Это родной метод. При вызове этого метода виртуальная машина JAVA сначала проверяет, существует ли объект со значением, равным объекту, в пуле строк. Если да, она возвращает ссылку на объект в пуле строк; если нет, она сначала создает; объект в пуле строк с тем же значением, а затем возвращает его ссылку.
Давайте посмотрим на этот код:
Java-код
public class StringInternTest { public static void main(String[] args) { // Используйте массив символов для инициализации a, чтобы избежать того, что объект со значением "abcd" уже существует в пуле строк до создания a String a = новая строка (new char[] { 'a', 'b', 'c', 'd' }); String b = a.intern(); if (b == a) { System.out.println("b был добавлен в пул строк, новый объект не был создан"); } else { System.out.println("b не был добавлен в пул строк, новый объект не был создан"); } } }
Результаты запуска:
b не был добавлен в пул строк и был создан новый объект. Если метод intern() класса String не находит объект с тем же значением, он добавляет текущий объект в пул строк, а затем возвращает его. ссылка, затем b и a Указывают на один и тот же объект; в противном случае объект, на который указывает b, заново создается виртуальной машиной JAVA в пуле строк, но его значение такое же, как и a. Результат выполнения приведенного выше кода только подтверждает это.
Наконец, давайте поговорим о хранении объектов String в виртуальной машине JAVA (JVM) и взаимосвязи между пулом строк, кучей и стеком. Давайте сначала рассмотрим разницу между кучей и стеком:
Стек: в основном сохраняет базовые типы (или встроенные типы) (char, byte, short, int, long, float, double, boolean) и ссылки на объекты. Данные могут быть разделены, и его скорость уступает только скорости регистрации. куча.
Куча: используется для хранения объектов.
Когда мы посмотрим на исходный код класса String, мы обнаружим, что у него есть атрибут value, в котором хранится значение объекта String. Тип — char[], что также показывает, что строка представляет собой последовательность символов.
При выполнении String a="abc"; виртуальная машина JAVA создает в стеке три символьных значения '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"), поднятый в начале этой статьи, создает два объекта.