Главная | Обратная связь | Поможем написать вашу работу!
МегаЛекции

scanf() - чтение данных из потока

Функции

Вызов функции - это оператор. У вызова функции есть приоритет – один из самых высоких. Список аргументов функции считают за один операнд, так что оператор оказывается бинарным (первый операнд - сама функция, второй - список ее аргументов).

Пример записи функции func:

 

double func(double param1, int param2)

{ return param1-0.1*param2; }

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

Формальные параметры разделены запятыми. В нашей функции это param1 типа double и param2 типа int.

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

Определив функцию, мы можем ее неоднократно вызывать, задавая в

качестве фактических параметров нужные нам переменные или

значения. При этом мы можем использовать то значение, которое она

возвращает, а можем его игнорировать (если нам просто надо, чтобы

выполнились операторы в теле функции).

int i;
double x, result;

...

/* два вызова функции в выражении */
result = func(x,i) * func(i+x, 100);

/* Вызываем еще раз, но игнорируем возвращаемое значение */
func(x, i);

38. Функции без возвращаемого значения

/* У этой функции нет возвращаемого значения */
void f() {
...
return;
}

В операторе return нет никакого значения, сразу после ключевого слова стоит точка с запятой.

Также можно написать void вместо списка параметров, если функции параметры не нужны:

int f(void) {
...
return 0;
}

39. Параметры и переменные

int i, j;

/* У первой функции видны i, j файлового уровня. Кроме того, у нее есть формальный параметр k и локальная переменная result В процессе работы эта функция изменяет значение файловой переменной i */


int f1(int k) {
int result;
result = i*j + k;
i += 100;
return result;
}

/* Во второй функции имя формального параметра совпадает с именем переменной i файлового уровня, при работе используется параметр, а не файловая переменная. */


int f2(int i)

{
/* i - параметр, j - файловая */
return i*j;

}

/* С третьей функцией аналогичная ситуация, что и со второй. Только на этот раз маскируется файловая переменная j, и не формальным параметром, а локальной переменной. */


int f3(int k)

{
int j;
j=100;
/* i - файловая, j - локальная */
return i*j + k;

}

Переменная j самого внутреннего блока маскирует не только файловую, но и локальную переменную из внешнего блока. */
int f4 (int k)

{
/* Объявляем переменную и сразу инициализируем */
int j=100;
{
/* Объявляем еще одну локальную с тем же именем, что у файловой и локальной из внешнего блока */
int j=10;
/* i - файловая, j - локальная, причем из внутреннего блока */
return i*j + k;
}

}

40. Необходимость инициализации переменных (автоматические переменные)

/* Файловая переменная без инициализации, будет равна 0 */
int s;

int f() {
/* Локальная без инициализации, содержит "мусор" */
int k;
return k;
}

int main() {

printf("%d\n", s); /* Всегда печатает 0 */

/* Невозможно предсказать, что увидим */
/* К тому же числа могут быть разными */
printf("%d\n", f());
...;
printf("%d\n", f());
return 0;

41. Статические переменные

int f() {
static int i;
return i;
}

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

Эти переменные создаются один раз за время работы программы, и один раз инициализируются - либо нулем, либо тем значением, которое было задано. Поскольку они «живут» независимо от функции, значит в одном вызове функции в такую переменную можно что-то положить, а в следующем - это что-то использовать.

 

int f() {
static int ncalls=1;

/* Который раз мы эту функцию вызвали? */
printf("number of calls %d\n", ncalls++);

...
}

 

Полезный «трюк», основанный на статических локальных переменных –

возможность выполнять какие-то дорогостоящие «подготовительные» операции

только один раз.

int func() {
/*
Неявная инициализация тоже дала бы 0, но правила хорошего тона требуют... */
static int init_done=0;

if (!init_done) {
/* Здесь мы выполняем какую-то "дорогостоящую«, но разовую работу - например, считываем таблицу значений из файла. А потом указываем, что таблица прочитана и при следующих вызовах этого делать уже не нужно. */
read_table();
init_done = 1;
}

/*
А в этом месте мы пользуемся табличными данными.
*/

...
}

 

42. Передача по значению

Передача параметра по значению" и "передача параметра по

ссылке".

#include <stdio.h>

void f(int k) {
k = -k;
}

int main() {
int i = 1;
f(i);
printf("%d\n", i);
return 0;
}

43. Адреса и указатели

int i;
double d;
/*
Функции передаются адреса переменных i и d. После вызова функции

адреса останутся прежними (pass-by-value), но значения могут измениться */
func(&i, &d);
...

char *s; /* указатель на char */

int *pi; /* указатель на int */
void *pv; /* указатель на void */
char **av /* указатель на указатель на char */

/* Это указатель на функцию, которая возвращает int, а в качестве параметра ожидает char */
int (*pf)(char)

 

Указатели можно использовать не только для параметров, но и в

качестве возвращаемого значения функции. Вот как, например, выглядит

определение и вызов функции, возвращающей указатель на тип char

(такой тип используется для передачи текстовых строк).

char *genstr() {
char *p;
...
return p;
}

char *s;

s = genstr();

 

44. Чем «опасны» указатели?

void f(int *p) {
*p=1;
}

int main() {
int i;
int *ptr;

f(ptr);

...
return 0;
}

 

Что делать, если указатель создан, но пока не известно, какой адрес в него записать? Для этого есть специальное значение указателя - в C это символьная константа NULL, в C++ - 0.

#include <stdlib.h>
main() {
char *p=NULL;
...}

void f(int *p) {
if (p!= NULL) *p=1;
else { printf("Help!!! NULL pointer in f()\n"); abort(); }
}

int main() {
int i; int *ptr=NULL; f(ptr);
...
return 0;

}

45. Ввод-вывод

#include <stdio.h>
int main() {
/* Печатаем целое число и строку */
printf("integer=%d, string=%s \n", 10, "hello");
}

#include <stdio.h>
int main() {
int i;
char c;
/* Вводим целое число и символ */
scanf("%d %c", &i, &c);
}

scanf() - чтение данных из потока

int i, j;
scanf("%d %d", &i, &j);

// или

int i, j;
int *p1,*p2;
p1=&i;
p2=&j;
scanf("%d %d", p1, p2);

46. Функции - часть типа данных

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

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

47. Конструкторы и деструкторы

Важный этап в жизни переменных - их создание и уничтожение. Например, когда создается статическая переменная встроенного типа, можно рассчитывать, что она будет инициализирована нулем. Ее можно также явно инициализировать при создании другим значением. Соответственно, в какой-то момент работы программы приходит время уничтожить переменную и освободить отведенную под нее память.

Чтобы представитель класса вел себя подобным образом, в классе определяют специальные функции - конструкторы, которые и занимаются инициализацией. Когда переменная должна быть уничтожена, вызывается другая функция - деструктор, которая «аккуратно» выполняет все завершающие операции (например, закрывает вспомогательные файлы).

48. Перегрузка операторов и функций

Для того чтобы переменные пользовательского типа можно было использовать в выражениях наравне со встроенными типами, С++ позволяет определять для них функции, которые вызываются при обработке операторов. Например, определив в соответствующем классе подходящую функцию, можно определить оператор умножения для матриц или комплексных чисел. Это действие носит называние перегрузка операторов (operator overloading).

Часто одной функции на оператор оказывается недостаточно. Например, ту же матрицу можно умножить на другую матрицу, а можно на константу. Ясно, что список параметров у этих двух функций будет разным. В С++ позволительно определять несколько функций с одним и тем же именем - лишь бы эти функции различались по типу и количеству параметров. Это и само по себе весьма удобное нововведение, но, главное, такая возможность позволяет задавать в классах наборы функций для перегружаемых операторов.

49.?

50. Пространства имен

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

namespace my_funcs {
long random() {... };
};
и после этого использовать следующим образом: long l;
l = my_funcs::random();

Здесь :: (двойное двоеточие) - оператор разрешения области видимости (scope resolution operator).

Аналогичным способом застраховались от конфликта имен и разработчики стандартной библиотеки - только пространство имен у них называется std. Так что теперь вы можете пользоваться обеими функциями:

l = std::random(); // из stdlib
l = my_funcs::random(); // my own function
Указывать для каждой функции пространство имен довольно утомительно, так что вы с помощью директивы using можете сказать, какой именно функцией (или набором функций) хотите пользоваться: using my_funcs::random;
using std::abs;

l = random(); // my_funcs::random()
l = abs(l); // std::abs();

Можно в директиве using поставить не имя функции, а все пространство имен, при этом все имена без :: будут ссылаться либо на функции в текущем файле (если они не вынесены, как my_funcs::random, в отдельное пространство имен), либо на пространство, указанное в директиве using: using namespace std;
l = random(); // std::random()
l = abs(); // std::abs()

l = my_funcs::random(); // own function

Как пользоваться стандартными файлами заголовков в С++?

До того, как был принят стандарт, довольно долго в С++ не было пространств имен, а стандартные файлы заголовков так же, как и в С, заканчивались суффиксом.h - например <iostream.h>. С принятием стандарта все имена библиотеки вынесли в пространсво std, однако к тому времени на С++ было написано много программ и библиотек. И для того, чтобы сохранилась совместимость с ними, файлы заголовков пришлось исполнить в двух вариантах - в старом, с суффиксом.h, и в новом - вообще без суффикса. При этом к заголовкам, пришедшим из С, прибавили спереди букву c, например <stdio.h> превратился в <cstdio>. А у заголовков, которых в С не было, просто убрали суффикс - <iostream.h> стал называться <iostream>.

 

(Пространство имен — это декларативная область, в рамках которой определяются различные идентификаторы (имена типов, функций, переменных, и т. д.).Пространства имен используются для организации кода в виде логических групп и с целью избежания конфликтов имен, которые могут возникнуть, особенно в таких случаях, когда база кода включает несколько библиотек.Все идентификаторы в пределах пространства имен доступны друг другу без уточнения.Идентификаторы за пределами пространства имен могут получить доступ к членам, используя полное имя идентификатора, например std::vector<std::string> vec;, используя Объявление using для отдельного идентификатора (using std::string) или Директива using (C++) для всех идентификаторов в пространстве имен (using namespace std;).Код в файлах заголовков всегда должен содержать полное имя в пространстве имен.)

51. Исключения (exceptions)

При создании С++ пришлось подумать и об обработке ошибок. Использовать в объектном коде С-стиль, проверяя, какое значение вернула функция не всегда возможно. А о схожести поведения встроенных и пользовательских типов и вовсе пришлось бы забыть. Поэтому в С++ предусмотрен другой принцип работы с ошибками, более гибкий и универсальный. И средства для этого внесены в сам синтаксис языка.

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

52. Наследование и полиморфизм

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

53. Уровни доступа к базовому классу

Разберемся, что означает ключевое слово public (а также и другие уровни защиты - protected, private) применительно к базовому классу. А заодно исправим недочет класса Point, убрав поля x и y из общедоступной секции.

Определение самого первого класса выглядело следующим образом

class Point {
public:
int x,y;
… };

По канонам объектно-ориентированного языка делать общедоступными поля данных нехорошо. Однако, если мы попытаемся разместить x,y в личной секции

class Point {
private:
int x,y;
public:
… };

а потом определить производный класс

class Point1: public Point {
public:
...
int get_x() { return x; }
int get_y() { return y; }
};

то транслятор выдаст сообщение об ошибке - нет доступа к личным полям x и y.

Личные есть личные - никто кроме самого класса Point не имеет права их трогать. Если же мы хотим, чтобы поля были недоступны внешнему миру, но при этом производные классы все-таки могли ими пользоваться, в классе Point надо использовать другое ключевое слово - protected (защищенные).

class Point {
protected:
int x,y;
public:
...
};

Теперь класс-наследник сможет работать с x и y напрямую.

Однако будут ли доступны эти поля следующему классу, производному не от Point, а от Point1? Это как раз и определяется тем, какое ключевое слово поставлено перед указанием базового класса.

Когда мы пишем

class Point1: public Point

то уровни защиты базовых полей и методов в производном классе не меняются - protected-члены класса Point становятся protected-членами Point1, а public-члены так и остаются общедоступными.

Если бы мы написали

class Point1: private Point

то все члены класса Point стали бы личными членами Point1. В частности, мы в программе могли бы пользоваться только функциями get_x, get_y, а функции show, hide, move стали бы недоступными.

Третий вариант

class Point1: protected Point

усиливает защиту на одну ступеньку, превращая защищенные поля в личные, а общедоступные делая защищенными.

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

54. Одноименные поля в производном и базовых классах

Итак, в классе Point у нас были поля x, y, которые мы использовали во всех производных классах. А что бы случилось, если в производном классе мы бы определили свои поля с такими же именами?

class Point {
public:
int x, y;
...
};

class OtherPoint: public Point {
public:
int x, y;
void set_x(int _x) { x=_x; }
};

В такой ситуации методы производного класса стали бы работать со своими полями, а не с полями базового класса. Однако при необходимости мы смогли бы добраться и до базовых полей. Вот как бы это выглядело в производном классе

class OtherPoint: public Point {
public:
int x, y;
void set_x(int _x) { x=_x; }
void set_base_x(int _x) { Point::x=_x; }
};

OtherPoint p;

// меняем поле OtherPoint::x
p.x = 1;

// меняем базовое поле Point::x
p.Point::x = 1;

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

55. Виртуальные функции

Теперь рассмотрим, как ведут себя наследуемые функции.

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

class Point { public: … void show() {... }; … };

class OtherPoint: public Point {
public:
...
void show() {
// Замена для базовой show()
...
// вызов базовой show()
Point::show();
}
};

Но это еще не самое интересное. Гораздо интереснее и полезнее разобраться с вопросом, как функции ссылаются друг на друга.

Вспомним полное определение класса Point (оно нам сейчас понадобится):

class Point {
public:
int x,y;
Point(int _x, int _y): x(_x), y(_y) {};
void show() {
// рисуем точку (x,y)
}
void hide() {
// стираем точку (x,y)
}
void move(int new_x, new_y) {
// перемещаем из (x,y) в (new_x, new_y)
hide();
x=new_x;
y=new_y;
show();
}
};

Теперь представим, что нужно написать класс, который рисует окружность. Что при этом можно унаследовать от класса Point? Координаты точки можем. А вот функции нам придется переписать - окружность и рисовать, и стирать надо по-другому:

class Circle: public Point
protected:
int r;
public: Circle(int _x, int _y, int _r): r(_r), Point(_x,_y) {};

void show() {
// Вариант show для окружности
}
void hide() {
// Вариант hide для окружности
}
void move(int new_x, int new_y) {
hide();
x = new_x;
y = new_y;
show();
}
};

С функциями show и hide никуда не деться - рисуем не точку, окружность.

Обратим внимание на move - в ней точно такой же код, как и в функции Point::move. Однако нам пришлось переписать и ее. Если бы мы воспользовались наследуемой функцией, она бы, конечно, вызвала hide и show - но только не новые, а из базового класса. Соответствующие вызовы были вставлены в тело Point::move еще на этапе компиляции - это называется ранним связыванием (early binding).

Вопрос: нельзя ли все-таки сделать так, чтобы мы работали с унаследованной функцией move, но она при этом определяла в момент вызова, с каким именно классом работает, и вызывала правильные варианты функций? Оказывается, можно. Для этого надо всего-навсего сделать функции show и hide виртуальными, поставив в определении базового класса перед ними ключевое слово virtual:

class Point {
public:
int x,y;
Point(int _x, int _y): x(_x), y(_y) {};

virtual void show() {
// рисуем точку (x,y)
}

virtual void hide() {
// стираем точку (x,y)
}

void move(int new_x, new_y) {
// перемещаем из (x,y) в (new_x, new_y)
hide();
x=new_x;
y=new_y;
show();
}
};

Теперь мы можем не повторять код функции move в производном классе, а воспользоваться наследуемой:

class Circle: public Point
protected:
int r;
public:
Circle(int _x, int _y, int _r)
: r(_r), Point(_x,_y)
{};

void show() {
// Вариант show для окружности
}
void hide() {
// Вариант hide для окружности
}
};

Теперь, если где-нибудь в программе мы напишем

Circle c(10,10);
Point p(20,20);
c.move(50,50);
p.move(70,70);

то и в третьей, и в четвертой строке сработает функция Point::move. Однако благодаря ключевому слову virtual она в третьей строке вызовет Circle::show и Circle::hide, а в четвертой - Point::show и Point::hide. Это замечательное свойство - во время выполнения программы определять, функцию из какого именно класса надо использовать - называется поздним связыванием (late binding).

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

Point *a[2];
Circle c;
Point p;
a[0] = &c;
a[1] = &p;

for (int i=0; i<2; i++)
a[i]->show();

и не задумываться о том, на какой именно тип объекта указывает конкретный элемент массива - благодаря тому, что функция show объявлена виртуальной, для точки будет вызвана Point::show, а для окружности - Circle::show.

Это и есть полиморфизм - способность объекта вести себя по-разному в зависимости от того, как им пользуются. Если с ним работают через ссылку или указатель на базовый класс, то он и ведет себя как базовый (разумеется, объект Circle рисовать будет окружность, а не точку, но по интерфейсу, то есть, по набору доступных полей и методов, это будет именно объект базового класса.

56. Абстрактные классы. Чистые виртуальные функции.

Сейчас наша сеть наследования выглядит так - базовый класс Point, его производный класс - Circle. Можно расширить эту сеть - например, написать еще производных от Point классов - Rectangle, Polygon, и так далее. Однако такая схема наследования не совсем логична. Point - это уже некий реальный объект, который помимо свойств, характерных для всех фигур (в нашем утрированном примере это координаты x,y и метод move) содержит еще и специфические только для своего типа детали - методы show и hide. Гораздо логичнее выделить все общее в отдельный класс, например, в класс Figure, а специфичные для конкретной фигуры детали определять непосредственно в производных классах.

С координатами при таком подходе проблем нет. Но нам надо внести в базовый класс метод move, который вызывает show и hide. А show и hide уже относятся к тем самым специфическим особенностям, которые мы хотим удалить из базового класса. Решить эту проблему в С++ помогают чистые виртуальные функции (pure virtual functions). Вот как могло бы выглядеть соответствующее определение базового класса:

class Figure {
protected:
int x, y;
public:
Figure(int _x, int _y): x(_x), y(_y) {};

void move(int new_x, int new_y) {
hide();
x = new_x;
y = new_y;
show();
};

virtual void show() = 0;
virtual void hide() = 0;
};

Обратим внимание, как записаны в определении класса show и hide:

virtual void show() = 0;
virtual void hide() = 0;

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

Теперь мы можем более логично построить нашу сеть наследования, сделав класс Point наравне с другими производным от Figure:

class Point: public Figure {
public:
Point(int _x, int _y): Figure(_x,_y) {};
void show() { /* show для точки */ };
void hide() { /* hide для точки */ };
};

class Circle: public Figure {
protected:
int r;
public:
Circle(int _x, int _y, int _r)
: Figure(_x,_y), r(_r)
{};
void show() { /* show для окружности */ };
void hide() { /* hide для окружности */ };
};

class Section: public Figure {
protected:
int len;
public:
Section(int _x, int _y, int _len)
: Figure(_x,_y), len(_len)
{};
void show() { /* show для отрезка */ };
void hide() { /* hide для отрезка */ };
};

Соответственно, если нас не интересуют специфические детали конкретных объектов, мы можем работать с ними по интерфейсу Figure:

Figure *ptr[3];

Point p(0,0);
Circle c(10,10, 20);
Section s(20,20,2);

ptr[0] = &p;
ptr[1] = &c;
ptr[2] = &s;

// Прячем все фигуры в массиве,
// независимо от типа
for (int i=0; i<3; i++)
ptr[i]->hide();

Следует обратить внимание на цикл в последнем примере. Это снова иллюстрация полиморфизма - очень полезного свойства имеющих общую базу объектов. Все они могут работать по интерфейсу любого своего базового класса.

Подобный стиль работы широко применяется в объектно-ориентированном программировании. Например, в С++ стиле ввода-вывода можно считывать данные с терминала, из файла, даже из массива - и при этом будут использоваться одними и теми же функциями.

В С++ у производного класса может быть несколько базовых классов. Если бы мы написали, например

class Circle
: public Point, public ListItem
{...; }

то смогли бы работать с указателями/ссылками на Circle и как с точками, и как с элементами списка.

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

57. Виртуальные конструкторы

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

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

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

Подобного рода задачи возникают при необходимости продублировать большой набор объектов, хранящийся, например, в виде дерева. Одно из применений - в каком-нибудь графическом редакторе мы создавали чертеж из отрезков, фигур, строк и так далее. Все это - объекты разного типа, но они вполне могут быть наследниками одного базового класса, скажем, TElement, и все изображение хранится в виде связного списка указателей на этот базовый тип. Затем в какой-то момент мы хотим скопировать чертеж (например, для вывода на печать). Но про каждый из объектов мы знаем только то, что он является наследником TElement.

Таким образом, у нас возникает необходимость скопировать весь список с сохранением типов объектов, но самих типов мы не знаем.

Функции, которые помогают решить эту задачу, и называют виртуальными конструкторами.

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

Делается это следующим образом: - в базовом классе определяются две виртуальные (в примере ниже это makeobject и copyobject), которые создают объект (copyobject еще и копирует в новый объект содержимое образца) и возвращают указатель на него:

#include <string>
#include <iostream>
using namespace std;

class A {
protected:
int val;
public:
A(int v=0): val(v) {};
A(const A& src): val(src.val) {};

// makeobject() is a virtual default ctor
virtual A* makeobject() {
A* ptr=new A;
return ptr;
}

// copyobject() is a virtual copy ctor
virtual A* copyobject() {
A* ptr=new A(*this);
}

virtual void show() {
cout << "Object of type A, val=" << val << endl;
}

}; // End of class A definition

Затем, при разработке производного класса вы замещаете эти две функции другими - создающими и копирующими объект вашего нового типа, но по прежнему возвращающими указатель на базовый класс:

class B: public A {
string s;
public:
B(const char *str): s(str) {};

// Copy ctor
B(const B& src): A(src), s(src.s) {};

// Default ctor
B(): s("") {};

// virtual default ctor for class B
A* makeobject() {
B* ptr=new B;
return ptr;
}

// virtual copy ctor for class B.
A* copyobject() {
B* ptr=new B(*this);
return ptr;
}

virtual void show() {
cout << "Object of type B, s=" << s << endl;
}

}; // end of class B definition

Проверить, как это работает, поможет небольшая программа

main() {

A a(1);
B b("Hello");

A* psrc[2] = {&a, &b};
A* pmake[2];
A* pcopy[2];

for (int i=0; i<2; i++) {
pmake[i] = psrc[i]->makeobject();
pcopy[i] = psrc[i]->copyobject();

psrc[i] ->show();
pmake[i]->show();
pcopy[i]->show();
}

return 0;
}

Object of type A, val=1
Object of type A, val=0
Object of type A, val=1
Object of type B, s=Hello
Object of type B, s=
Object of type B, s=Hello

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

 

Поделиться:





Воспользуйтесь поиском по сайту:



©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...