Виртуальные функции. Полиморфизм 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Виртуальные функции. Полиморфизм

Поиск

При наследовании классов существует возможность переопределять функции базовых классов – в производном классе допустимо определить функцию с тем же именем и сигнатурой, что и в базовом, уточняя или изменяя выполняемые действия. Рассмотрим пример.

Пример 8. Проблема статического связывания функций

class Base

{ public:

int func1(int x) {return x*x;}

int func2(int x){return func1(x)/2;}

};

class Child: public Base

{public:

int func1(int x) {return x*x*x;}

};

main()

{ Child c;

cout<<c.func2(5);     //на экран выводится 12

Base *ptb=new Child;

cout<<ptb->func1(2);  //на экран выводится 4

cout<<c.func1(2);     // на экран выводится 8

}

В классе Base определены две функции func1 и func2, причем вторая функция вызывает первую. В классе Child переопределена функция func1, а функция func2 просто наследуется. При вызове функций результаты их работы оказываются для многих неожиданными. Так, вызов c.func2(5) дает результат 12 вместо ожидаемых 62, а вызов ptb->func1(2) дает результат 4, а не 8. Дело в том, что в обоих случаях будет вызвана функция func1 базового класса, а не переопределенная в производном классе. Такое поведение объектов связано со статическим (ранним) связыванием функций при трансляции программы. Когда транслятор в процессе обработки программы встречает вызов какой-либо функции, то на место вызова он подставляет в текст оттранслированной программы адрес вызываемой функции. Таким образом, компилируя тело компонентной функции func2 класса Base, транслятор на место вызова функции func1 подставит адрес компонентной функции func1 из класса Base, так как только эта функция с подобным именем ему известна (содержимое класса Child транслируется позже). В итоге функция Base::func2 всегда будет вызывать функцию Base::func1, как бы ни был оформлен вызов самого метода func2.

Аналогично, компилируя тело функции main и встретив вызов ptb->func1(2), транслятор должен подставить на место вызова адрес функции, которой будет передано управление в данной точке программы. К этому моменту транслятору известны две функции с именем func1: Base::func1 и Child::func1. Так как вызов метода осуществляется для указателя на объект Base, транслятор подставит на место вызова адрес функции именно этого класса (определить, что в указатель ptb записан адрес объекта класса Child, и поэтому вызвать метод Child::func1, транслятор не может).

Таким образом, можно сформулировать проблему: необходимо обеспечить полиморфное поведение некоторой функции, определенной в базовом классе и переопределенной в производных. Под полиморфным поведением понимается реализация функции таким образом, чтобы всякий раз при вызове функции вызывалась именно та ее реализация, которая отражает поведение того объекта, для которого осуществлялся вызов. Полиморфное поведение функций обеспечивается поздним связыванием функций, когда определение того экземпляра одноименных функций, которому будет передано управление, определяется на этапе выполнения программы.

Для того, чтобы компонентная функция обеспечивала полиморфное поведение, ее необходимо объявить виртуальной. Для этого в определении функции в классе необходимо указать ключевое слово virtual.  

virtual тип имя_функции (список_формальных параметров)

{тело функции }

Если мы изменим определение функции func1, объявив ее виртуальной, поведение объектов программы изменится.

Пример 9. Использование виртуальных функций

class Base

{ …

virtual int func1(int x) {return x*x;}

};

class Child: public Base

{…

virtual int func1(int x) {return x*x*x;}

};

main()

{ Child c;

cout<<c.func2(5); //на экран выводится 62

Base *ptb=new Child;

cout<<ptb->func1(2); //на экран выводится 8

}

  В языке С++ позднее связывание реализуется путем поддержки для каждого объекта таблицы виртуальных функций. Таблица виртуальных функций представляет собой массив указателей на реализации виртуальных функций, доступные для данного объекта. Структура объекта при использовании им виртуальных функций, изображена на рисунке 29.6.

 

Указатель на таблицу виртуальных методов (vtbl)
Компонентные данные объекта
Адрес виртуальной функции 1 (&func1)
Адрес виртуальной функции 2 (&func2)
Адрес виртуальной функции n (&funcN)

Вызов виртуальной функции в тексте программы транслятор преобразует в обращение к соответствующей строке таблицы виртуальных функций. Предположим, что pObj – это указатель, в который записан адрес объекта, структура которого отображена на рисунке 6. В таком случае вызов виртуальной функции func2 для этого объекта вида

pObj->func2()

преобразуется транслятором в следующий вызов: 

(*(pObj->vptr[1])) (pObj)

При таком вызове функции нет жесткой привязки вызова к какой-то конкретной реализации компонентной функции в одном из классов, как это происходило при статическом связывании. Теперь будет вызываться та функция, адрес которой записан в элемент с индексом 1 таблицы виртуальных функций объекта. Позднее связывание обеспечивается тем, что заполнение таблицы виртуальных функций объекта происходит уже на этапе выполнения программы. Это делает конструктор, занося в каждую строку таблицы адрес той реализации виртуального метода, который правильно описывает поведение объекта. Указатель на таблицу виртуальных функций обязательно включается в самый "верхний" базовый фрагмент объекта производного класса. В таблицу указателей включаются адреса функций-членов фрагмента самого "нижнего" уровня, содержащего объявления этой функции. Такая дополнительная функциональность конструктора обеспечивается транслятором.

Использование позднего связывания не отрицает возможности вызова из производного класса экземпляра виртуальной функции базового. Просто для подобного использования необходимо указывать при вызове полное квалифицированное имя функции.

Д ля программы из примера 9:

main()

{ Base * ptb=new Child;

cout<<ptb->Base::func1(2); //на экран выводится 4

}

При определении виртуальных функций существует ряд синтаксических особенностей. Если какая-то функция определена в базовом классе как виртуальная, а в производном классе переопределена с тем же прототипом (то есть в производном классе в точности совпадают тип, имя и список параметров компонентной функции), то в производном классе ключевое слово virtual можно опустить – функция останется виртуальной по умолчанию. Если сигнатура (список формальных параметров) функции в производном классе изменится, то полиморфное поведение функций базового и производного классов станет невозможным. Если же в производном классе будет определена компонентная функция с тем же названием и сигнатурой, но с другим типом возвращаемого значения, то транслятор выдаст сообщение об ошибке. Виртуальной функцией может быть только нестатическая компонентная функция класса.

Основной смысл полиморфного определения функций – переопределить в производном классе их поведение в соответствии с его особенностями. Однако, такое определение зачастую приводит к тому, что действие, соответствующее полиморфной функции, в базовом классе неопределенно в предметной области решаемой задачи. Например, создать иерархию классов для геометрических фигур:

Пример 10. Иерархия классов для геометрических фигур

class Point

{ public:

int x,y;

int color;

};

class Shape

{ protected:

// base - точка привязки фигуры

// на плоскости

Point base; 

int color;

public:

virtual void show()

{ }

virtual void hide()

{ }

void move(int xn,int yn)

{ hide()

base.x+=xn; base.y+=yn;

show();

}

};

class Circle:public Shape

{ int radius;

public:

void show()

{/*рисуем окружность c центром в точке base*/}

void hide()

{/*скрываем окружность*/}

};

class Rectangle:public Shape

{//высота и ширина прямоуг-ка

int width,height;

public:

void show()

{/*рисуем прямоугольник с левым верхним углом в base*/}

void hide()

{/*скрываем прямоугольник*/}

};

Классы Окружность (Circle) и Прямоугольник (Rectangle) унаследованы от класса Фигура (Shape). В классе Shape обобщены все общие характеристики и поведение геометрических фигур (цвет, положение базовой точки на плоскости, метод перемещения фигур). В частности, для того, чтобы переместить фигуру, необходимо методом hide скрыть ее в текущей позиции, переместить координаты базовой точки на указанные смещения и отобразить фигуру в новой позиции методом show. Именно это и происходит в методе move класса Shape. Теперь, если вызвать метод move для экземпляра класса Circle, то, учитывая полиморфность объявления методов hide и show, окружность будет корректно переноситься и отображаться:

Circle c(100, 100, 10); //задаем в конструкторе координаты центра (базовой точки) и радиуса

 c.move(-10, 10);

В структуре класса Shape обращает на себя внимание присутствие методов hide и show. Сам объект Shape является программной абстракцией, позволяющей сократить объем описания производных классов, для него не требуется операций отображения и скрытия, тем более, что как объекты этого класса в программе создаваться не будут. Однако, обойтись вообще без методов hide и show в этом классе нельзя – к ним обращается метод move. Здесь возникает противоречие между семантикой и синтаксисом определения класса – методы должны присутствовать в классе, но для них нет полезного наполнения, реально будут вызываться только их полиморфные переопределения в производных классах.

Для исключения возможных утечек памяти деструкторы классов, использующих хотя бы одну виртуальную функцию также необходимо объявлять виртуальными.

Чистая виртуальная функция определяется следующим образом:

virtual тип имя_функции(список_формальных_параметров)=0;

Чистая виртуальная функция не имеет реализации, ее нельзя вызвать в программе, она служит лишь как основа для дальнейшего полиморфного переопределения в производном классе. Если в классе определена хотя бы одна чистая виртуальная функция, он становится абстрактным. Главное отличие абстрактных классов – на их основе невозможность создавать объекты, они могут служить только основой для наследования. Класс Shape по замыслу является абстрактным, поэтому его можно переопределить следующим образом:

class Shape

{ protected:

Point base; 

int color;

public:

virtual void show()=0;

virtual void hide()=0;

void move(int xn,int yn)

{ hide()

base.x+=xn; base.y+=yn;

show();

} };

 

 Наследование классов в языке С#.

Принцип наследование как базовый в ООП, реализуется и в языке C#. Синтаксически механизм реализации наследования в С# во многом схож с языком С++. Унаследовать класс от уже существующего можно с помощью объявления вида:

class имя_класса: базовый_класс

{//определение собственных компонент

};

При этом в списке базовых классов может быть указан лишь один класс, множественное наследование реализаций классов в C# запрещено. Этим разработчики языка радикально решили вопрос разрешения имен для одноименных компонент при наследовании.

Компоненты базового класса становятся доступны в производном без дополнительных объявлений и описаний. Область видимости компонент базового класса (protected и public) сохраняется и в производном.

Вызов конструктора базового класса осуществляется с использованием ключевого слова base:

конструктор_производного_класса(список_параметров):

base (список_аргументов)

{

// тело конструктора

}

Если ключевое слово base опустить, то будет вызван конструктор базового класса по умолчанию.

Ключевое слово base используется также для обращения к компоненту (данных или методу) базового класса, если в производном определен компонент с тем же именем:

 class baseClass

{

   public int a;

    public void FuncB()

   {

Console.Write("Вызвана функция базового класса");

}

}

class childClass:baseClass

{

   public int a;

   public void Func()

   {

      a = 1; //обращение к полю а класса baseClass

      base.a = 2; //обращение к полю а класса childClass

Console.Write("Вызвана функция производного класса");

   }

}

Ссылка на производный класс может быть присвоена переменной типа базового класса, для рассмотренных выше классов справедливо:

static void Main(string[] args)

   {

       childClass c = new childClass();

       baseClass b = c;

       b.FuncB();

       //b.Func(); Ошибка! b предоставляет доступ к тем

//компонентам childClass, что унаследованы от baseClass

       Console.ReadKey();

   }

Подобное присваивание, как уже можно было заметить на примере языка С++, позволяет проявлять себя полиморфным функциям

Если базовый класс желает определить метод, который может быть (но не обязательно будет) переопределен в подклассе, он должен пометить его ключевым словом virtual:

public class Person

{ string Name;

int Age;

public virtual void printInfo()

{ Console.Write(“Имя:{0}, Возраст: {1}”, Name, Age);
}

}

 Теперь если производный класс будет переопределять поведение метода printInfo базового класса, он должен будет объявить с использованием ключевого слова override.: При этом существует возможность обратиться и к методу базового класса с использованием ссылки base:

public class Student:Person

{ string Group;

public override void printInfo()

{ Console.Write(“Информация о студенте ”);

base.printInfo();

Console.Write(“Группа:{0}”, Group);
}

}

Все модификаторы, которые могут быть использованы при переопределении методов базового класса в производном, приведены в таблице 29.2.

 

Таблица29. 2- Модификаторы методов при наследовании

Модификатор Целевой элемент Описание
virtual Методы и классы Метод (ы) могут быть переопределены в классах-потомках
override Методы Метод переопределяет виртуальный метод предка
new Методы Метод скрывает одноименный метод базового класса
abstract Классы, методы Абстрактный метод не содержит реализации, только прототип.
sealed Классы, методы Закрытый класс (метод), не допускающий наследования (переопределения)

 

Некоторые строки таблицы 29.2 требуют пояснения. Если в дочернем классе определяется функция с тем, что уже имеется в базовом классе, то происходит скрытие функции базового класса. Для того, чтобы компилятор не генерировал предупреждающее сообщение об этом, скрывающую функцию в дочернем классе можно определить с ключевым словом new:

class baseClass

{

   public void FuncB()

   {

Console.Write("Вызвана функция базового класса");

}

}

class childClass:baseClass

{

   new public void FuncB()

   {

 Console.Write("Вызвана функция производного класса");

   }

}

Ключевое слово sealed сообщает, что метод (класс) будет конечным в цепочке переопределения (наследования). Класс, объявленный с ключевым словом sealed, не может служить базой для наследования, он становится терминальным узлом в дереве иерархии на диаграмме классов. Модификатор sealed можно использовать для метода или свойства, которое переопределяет виртуальный метод или свойство из базового класса. Это запретит производным классам переопределять определенные виртуальные методы или свойства.

Наконец, использование ключевого слова abstract означает, что реализация изменяемого объекта является неполной или отсутствует. Модификатор abstract может использоваться с классами, методами, свойствами, индексаторами и событиями. Модификатор abstract в объявлении класса указывает, что класс предназначен только для использования в качестве базового класса для других классов. Члены, помеченные как абстрактные или включенные в абстрактный класс, должны быть реализованы с помощью классов, производных от абстрактных классов, для них не определена семантика реализации в данном классе. Абстрактные члены аналогичны чистым виртуальным функциям языка С++. Создавать экземпляры абстрактных классов нельзя. Класс, унаследованный от абстрактного, должен переопределять его абстрактные члены (они считаются виртуальными).

Тема реализации механизма наследования в языке C# будет раскрыта не полностью, если не рассмотреть такой тип данных, как интерфейсы. Дело в том, что класс в языке С# может быть унаследован только от одного базового класса, но он может наследовать (реализовывать) сразу нескольким интерфейсам.

Интерфейс – это декларация той функциональности, которую должен обеспечивать реализующий интерфейс класс. Часто говорят, что интерфейс является контрактом, который должен исполнять класс, объявивший себя наследником класса и тем самым вызвавшийся этот контракт исполнять. Определяется интерфейс с помощью ключевого слова interface и содержит лишь набор абстрактных членов:

interface имя{

тип имя_метода1 (список_параметров);

тип имя_свойства{set; get;}

}

Как видно из примера, в определение интерфейса помещаются лишь прототипы методов и свойств. Тела методов и аксессоров свойств не могут быть определены в интерфейсе, они задаются лишь в классах, наследующих интерфейс.

В теле интерфейса не указываются модификаторы доступа к членам, нельзя использовать для методов интерфейса ключевые слова virtual и static.

Например, определим интерфейс, отвечающий за проверку корректности заполнения данных различного типа – телефонных номеров, адресов e-mail, дат и других типов данных, поступающих от при регистрации пользователей, заполнении анкет и т.п.

public interface ICheckExp

{

   bool checkPhone(string phoneNumb);

   bool checkEmail(string emailStr);

bool checkDate(string dateStr);

}

В интерфейс включены три метода, осуществляющие проверку корректности переданных им строк, содержащих, соответственно, номер телефона, адреса электронной почты и даты. Теперь классы, реализующие подобную проверку (например, анкета или web-форма регистрации пользователей), наследуя этот интерфейс возьмут на себя обязательства определить конкретные реализации этих методов. Например, вот так может быть определён класс Anketa

 public class Anketa: ICheckExp

{

string phoneNumb, email, firstName, secondName;

    public bool checkPhone(string phoneNumb){

        string pattern = @"^\(\d{3}\)\d{3}-\d{2}-\d{2}$";

        Regex regex = new Regex(pattern);

        return regex.IsMatch(phoneNumb);

    }

public bool checkEmail(string emailAddr){

        string pattern = @"(.+)@(.+)\.(.+)$”;

        Regex regex = new Regex(pattern);

        return regex.IsMatch(emailAddr);

    }

}

Если в классе не предусмотрена проверка одного из предложенных в интерфейсе типов объектов, он не может его просто проигнорировать - реализация всех методов и свойств интерфейса обязательно должна присутствовать в классе. Если нет алгоритма для одного из методов – можно предложить заглушку вида:

bool checkDate(string dateStr)

{

return true;

}

Фрагмент диаграммы классов для представленной реализации интерфейса будет выглядеть следующим образом:

Теперь для объектов класса Anketa можно вызывать методы проверки из интерфейса ICheckExp:

 static void Main(string[] args)

{

Anketa a=new Anketa();

string strEmail, strPhone;

Console.WriteLine("Введите адрес электронной почты");

strEmail=Console.ReadLine();

if (c.checkEmail(strEmail))

   Console.WriteLine("Корректный адрес электронной почты");

 else

Console.WriteLine("Некорректный адрес электронной почты");

Console.WriteLine("Введите номер телефона");

strPhone = Console.ReadLine();

if (c.checkPhone(strPhone))

   Console.WriteLine("Корректный телефонный номер");

else

   Console.WriteLine("Некорректный телефонный номер");

     …           

}

Допускается наследовать класс от нескольких интерфейсов, тогда он должен реализовывать методы всех своих интерфейсов.

class MyClass: IInterface1, IInterface2, …, InterfaceN

{ //реализация методов и свойств всех интерфейсов из списка

//наследования

}

Интерфейсы сами могут вступать в отношения наследования – можно определить один интерфейс как наследник уже определенного(-ых). Всегда можно проверить, реализует ли класс тот или иной интерфейс с использованием ключевого is:

MyClass obj=new MyClass();

if(obj is ISomeInterface))

obj.someInterfaceMethod();

Хотя по понятным причинам нельзя создавать экземпляры интерфейсов, в программе можно определять ссылки на интерфейс и связывать их с экземплярами объектов. При этом совместим со ссылкой на интерфейс будет лишь тот объект, который реализует интерфейс. Через эту ссылку можно вызвать любой метод интерфейса в реализации для конкретного объекта класса:

 Anketa a=new Anketa();

iCheckExp iCheck=a;

if(iCheck.checkEmail(strEmail))

{…}

Для чего нужны интерфейсы? С их помощью можно формулировать стереотипы поведения объектов классов. Поведение объектов классов проще документировать – если он реализует некоторый интерфейс, значит умеет делать то, что предписано интерфейсом. Объекты с помощью интерфейсов могут однообразно обрабатываться: например, если класс наследует стандартный интерфейс System.Collection.IEnumerable, то тогда (и только тогда) его объекты могут перебираться в стандартном операторе цикла foreach. Или другой пример: если посмотреть справку по классу языка C# TList<>, инкапсулирующего поведение списка объектов некоторого типа, то можно увидеть, что унаследован от нескольких интерфейсов:

public class List<T>: IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable

Каждый из интерфейсов в списке наследования привносит свою долю функциональности в целевой класс и их реализация в классе List<T>  определены размер, перечислители и методы синхронизации, возможен индивидуальный доступ к элементам, осуществляемый при помощи индекса с обобщением для произвольного типа T. За счет того, что эта функциональность декларирована в интерфейсах, можно сделать вывод, что во многом такие же действия доступны и для класса очередь (Queue<T>), список наследования которого:

public class Queue<T>: IEnumerable<T>, ICollection, IEnumerable

Таким образом, интерфейс являются эффективным средством обобщения описания поведения объектов в программе.

 

Контрольные вопросы.

1. В чем заключается механизм наследования? Какие преимущества он предоставляет в ООП?

2. Как реализуется механизм наследования в программах на объектно-ориентированном языке программирования?

3. От чего зависит доступность унаследованного члена класса в производном классе в языке С++?

4. Что такое виртуальная функция? Приведите свой пример эффективного полиморфного поведения функций.

5. Почему для классов с объявленными виртуальными методами в языке C++ необходимо объявлять виртуальным и деструктор?

6. Что такое абстрактный класс С++? Какие ограничения существуют при его использовании? Для чего подобные классы определяются в программе?

7. Чем реализация механизма в языке С# отличается от реализации С++?

8. Что такое интерфейс С#? Как он определяется и используется в программе?


Лекция 30 Классы и шаблоны

Цели лекции: Рассмотрение зависимости работы методов классов от различных типов данных.

Шаблоны, которые иногда называют родовыми или параметризо­ванными типами, позволяют создавать (конструировать) семейства родственных функций и классов.

Шаблон семейства функций (function template) определяет потенциально неограниченное множество род­ственных функций. Он имеет следующий вид:

template <слисок_ параметров_ша6лона> определекие_функции

Здесь угловые скобки являются неотъемлемым элементом определения. Список параметров шаблона должен быть заключен именно в угловые скобки.

Аналогично определяется шаблон семейства классов:

template <список_параметров_шаблона> определение_класса

Шаблон семейства классов определяет способ построения отдельных классов подобно тому, как класс определяет правила построения и формат отдельных объектов. В определении класса, входящего в шаблон, особую роль играет имя класса. Оно является не именем отдельного класса, а параметризованным именем семейства классов.

Как уже отмечалось в связи с шаблонами функций, определение шаблона может быть только глобальным.

Рассмотрим векторный класс (в число данных входит одномерный массив). Какой бы тип ни имели элементы массива (целый, вещественный, с двойной точностью и т.д.), в этом классе должны быть определены одни и те же базовые операции, например доступ к элементу по индексу и т.д. Если тип элементов вектора задавать как параметр шаблона класса, то система будет формировать вектор нужного типа (и соответствующий класс) при каждом определении конкретного объекта.

Следующий шаблон позволяет автоматически формировать классы векторов с указанными свойствами:

// файл “template.vec” - шаблон векторов

template <class Т> //Т - параметр шаблона

class Vector

{

Т *data; // Начало одномерного массива

int size; // Количество элементов в массиве
public:

Vector(int);      // Конструктор класса vector

~Vector(){ delete[] data; } // Деструктор
// Расширение действия (перегрузка) операции "[]":
T& operator[] (int i) { return data[i];}

// Внешнее определение конструктора класса:

template <class T>

Vector <T>:: Vector (int n) {data = new T[n]; size = n;}

Когда шаблон введен, у программиста появляется возможность определять конкретные объекты конкретных классов, каждый из которых параметрически порожден из шаблона. Формат определения объекта одного из классов, порождаемых шаблоном классов:

 

имя_параматризированного_класса <фактические_параметры_шаблона>

имя_объекта (параметры_конструктора);

В нашем случае определить вектор, имеющий восемь вещественных координат типа double, можно следующим образом:

Vector <double> Z(8); Проиллюстрируем сказанное следующей программой:

//формирование классов с помоцыо шаблона

#include "template.vec" // Шаблон классов "вектор"

#include <iostream.h>

main ()

{

// Создаем объект класса "целочисленный вектор":

Vector <int> X(5);

// Создаем объект класса "символьный вектор":

Vector <char> С (5);

// Определяем компоненты векторов:

for (int i = 0; i < 5; i++)

{ X[i] = i; C[i] = 'A' + i;}

for (i = 0; i < 5; i++)

cout << " " << X[i] << ' ' << C[i];

Результат выполнения программы:

OA1B2C3D4E

В программе шаблон семейства классов с общим именем Vector используется для формирования двух классов с массивами целого и символьного типов. В соответствии с требованием синтаксиса имя параметризованного класса, определенное в шаблоне (в примере Vector), используется в программе только с последующим конкрет­ным фактическим параметром (аргументом), заключенным в угловые скобки. Параметром может быть имя стандартного или определенно­го пользователем типа. В данном примере использованы стандартные типы int и char. Использовать имя vector без указания фактического параметра шаблона нельзя - никакое умалчиваемое значение при этом не предусматривается. В списке параметров шаблона могут присутствовать формальные параметры, не определяющие тип, точнее - это параметры, для кото­рых тип фиксирован:

 

//Р10-12.СРР

#include <iostream.h>

template <class T, int size = 64>

class row { Т *data; int length;

public:  

row() { length = size; data = new T[size]; }

~row() { delete[] data; }

T& operator [] (int i) { return data[i]; }

};

void main()

{ row <float,8> rf; 

row <int,8> ri;

for (int i = 0; i < 8; i++) { rf[i] = i;   ri[i] = i * i; }

for (i = 0; i < 8; i++) cout << "   " << rf[i] << ' ' << ri[i]; }

 

Результат выполнения программы:

00  11  24   39  4 16  5 25   6 36  7 49

В качестве аргумента, заменяющего при обращении к шаблону параметр size, взята константа. В общем случае может быть исполь­зовано константное выражение, однако выражения, содержащие пе­ременные, использовать в качестве фактических параметров шаблонов нельзя.

Контрольные вопросы:

1. Как определять и использовать шаблоны?

2. Как можно задавать в шаблонах разные виды членов?

3. Как определять специализации и частичные специализации для шаблона класса и для его члена?

4. Как разрешаются имена в определениях шаблона класса?

5. Как можно определять шаблоны в пространствах имен?

 

 




Поделиться:


Последнее изменение этой страницы: 2021-02-07; просмотров: 486; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.148.104.138 (0.016 с.)