Главная Обратная связь

Дисциплины:

Архитектура (936)
Биология (6393)
География (744)
История (25)
Компьютеры (1497)
Кулинария (2184)
Культура (3938)
Литература (5778)
Математика (5918)
Медицина (9278)
Механика (2776)
Образование (13883)
Политика (26404)
Правоведение (321)
Психология (56518)
Религия (1833)
Социология (23400)
Спорт (2350)
Строительство (17942)
Технология (5741)
Транспорт (14634)
Физика (1043)
Философия (440)
Финансы (17336)
Химия (4931)
Экология (6055)
Экономика (9200)
Электроника (7621)


 

 

 

 



Ввод-вывод в символьные массивы



Абстрактные классы.

Многие классы сходны с классом employee тем, что в них можно дать разумное определение

виртуальным функциям. Однако, есть и другие классы. Некоторые, например, класс shape,

представляют абстрактное понятие (фигура), для которого нельзя создать объекты. Класс shape

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

что невозможно дать осмысленное определение виртуальных функций класса shape:

class shape {

// ...

public:

virtual void rotate(int) { error("shape::rotate"); }

virtual void draw() { error("shape::draw"): }

// нельзя ни вращать, ни рисовать абстрактную фигуру

// ...

};

Создание объекта типа shape (абстрактной фигуры) законная, хотя совершенно бессмысленная

операция:

shape s; // бессмыслица: ``фигура вообще''

Она бессмысленна потому, что любая операция с объектом s приведет к ошибке.

Лучше виртуальные функции класса shape описать как чисто виртуальные. Сделать виртуальную

функцию чисто виртуальной можно, добавив инициализатор = 0:

class shape {

// ...

public:

virtual void rotate(int) = 0; // чисто виртуальная функция

virtual void draw() = 0; // чисто виртуальная функция

};

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

нельзя:

shape s; // ошибка: переменная абстрактного класса shape

Абстрактный класс можно использовать только в качестве базового для другого класса:

class circle : public shape {

int radius;

public:

void rotate(int) { } // нормально:

// переопределение shape::rotate

void draw(); // нормально:

// переопределение shape::draw

circle(point p, int r);

};

Абстрактные классы нужны для задания интерфейса без уточнения каких-либо конкретных деталей

реализации. Например, в операционной системе детали реализации драйвера устройства можно

скрыть таким абстрактным классом:

class character_device {

public:

virtual int open() = 0;

virtual int close(const char*) = 0;

virtual int read(const char*, int) =0;

virtual int write(const char*, int) = 0;

virtual int ioctl(int ...) = 0;

// ...

};

Настоящие драйверы будут определяться как производные от класса character_device.

 

Аргументы функций по умолчанию.

Класс c o m p l e x включает три конструктора, два из которых просто подставляют нулевое значение (по умолчанию ), предоставлены программисту для удобства нотации. Использование перегрузки типично для конструкторов, но также часто обнаруживается и для других функций. Такая перегрузка, однако, весьма трудоемкий обходной путь подстановки аргументов по умолчанию и, в особенности для более сложных конструкторов, крайне избыточнa. Следовательно должна быть предоставлена возможность для непосредственного задания аргументов по умолчанию. Например:

class complex {
. . .
public:
complex(double r=0, double i=0) { re = r; im = i;}
};

Если завершающий аргумент отсутствует, может быть использовано константное выражение по умолчанию. Например :

complex a(1,2);
complex b(1); /* b = complex(1, 0); */
complex c; /* c = complex(0, 0); */

Если составляющая функция такая, как вышеприведенная функция c o m p l e x не только описана, но и определена, (то есть приведено ее тело) в описании класса, то при обращении к ней может быть выполнена константная подстановка, избегая тем самым обычных накладных расходов по вызову функции. Константная подстановка функции не является макроподстановкой, семантика такой функции идентична семантике других функций. Любая функция может быть объявлена включаемой (*1) предшествующим указанием ключевого слова i n l i n e . Включаемые функции могут сделать описания классов крайне раздутым и они в случае разумного использованияа повышают эффективность исполнения, но всегда увеличивают время и память, необходимые для компиляции. Включаемые функции, таким образом, должны использоваться только в случае, если ожидается значительное повышение эффективности исполнения. Они были включены в С++ вследствие опыта использования макросов в С. В некоторых приложениях макросы бывают необходимы (и нет возможности определить макрос в составе класса) но значительно чаще они создают хаос тем, что выглядят как функции, но не подчиняются ни синтаксису функций, ни правилам видимости, ни правилам передачи аргументов.

 

 

Арифметические операции с указателями и с указателями на массивы.

Операции с указателями

С указателями можно выполнять следующие операции: разадресация (*), присваивание, сложение с константой, вычитание, инкремент (++), декремент (– –), сравнение, приведение типов. При работе с указателями часто используется операция получения адреса (&). Операция разадресации, или разыменования, предназначена для доступа к величине, адрес которой хранится в указателе. Эту операцию можно использовать как для получения, так и для изменения значения величины (если она не объявлена как константа):

char a;//переменная типа char

char * p = new char;/*выделение памяти под указатель и под динамическую переменную типа char */

*p = 'Ю'; a = *p;//присваивание значения обеим переменным

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

#include <stdio.h>

int main()

{unsigned long int A=0Xсс77ffaa;

unsigned int* pint =(unsigned int *) &A;

unsigned char* pchar =(unsigned char *) &A;

printf(" | %x | %x |", *pint, *pchar);}

на IBM PC выведет на экран строку:

| ffaa | aa |

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

При смешивании в выражении указателей разных типов явное преобразование типов требуется для всех указателей, кроме void*. Указатель может неявно преобразовываться в значение типа bool.

Присваивание без явного приведения типов допускается только указателям типа void* или если тип указателей справа и слева от операции присваивания один и тот же.

Присваивание указателей данных указателям функций (и наоборот) недопустимо.

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

Указатели и массивы в языке Си++ тесно связаны. Имя массива можно использовать как указатель на его первый элемент, поэтому пример с массивом alpha можно записать так:

int main()
{
char alpha[] = "abcdefghijklmnopqrstuvwxyz";
char* p = alpha;
char ch;

while (ch = *p++)
cout << ch << " = " << int (ch)
<< " = 0" << oct(ch) << '\n';
}

Можно также задать описание p следующим образом:

char* p = &alpha[0];

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

void f()
{
extern "C" int strlen(const char*); // из <string.h>
char v[] = "Annemarie";
char* p = v;
strlen(p);
strlen(v);
}

Но в том и загвоэдка, что обойти это нельзя: не существует способа так описать функцию, чтобы при ее вызове массив v копировался ($$4.6.3).

Результат применения к указателям арифметических операций +, -, ++ или -- зависит от типа указуемых объектов. Если такая операция применяется к указателю p типа T*, то считается, что p указывает на массив объектов типа T. Тогда p+1 обозначает следующий элемент этого массива, а p-1 - предыдущий элемент. Отсюда следует, что значение (адрес) p+1 будет на sizeof(T) байтов больше, чем значение p. Поэтому в следующей программе

main()
{
char cv[10];
int iv[10];

char* pc = cv;
int* pi = iv;

cout << "char* " << long(pc+1)-long(pc) << '\n';
cout << "int* " << long(pi+1)-long(pi) << '\n';
}

с учетом того, что на машине автора (Maccintosh) символ занимает один байт, а целое - четыре байта, получим:

char* 1
int* 4

Перед вычитанием указатели были явной операцией преобразованы к типу long ($$3.2.5). Он использовался для преобразования вместо "очевидного" типа int, поскольку в некоторых реализациях языка С++
указатель может не поместиться в тип int (т.е. sizeof(int)<sizeof(char*)).

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

void f()
{
int v1[10];
int v2[10];

int i = &v1[5]-&v1[3]; // 2
i = &v1[5]-&v2[3]; // неопределенный результат

int* p = v2+2; // p == &v2[2]
p = v2-2; // *p неопределено
}

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

 

 

Ввод-вывод в символьные массивы.

Строка являются массивом символов. Значением строки является указатель на первый ее символ:

char *string = "строка\n";

Здесь указатель на символы string будет содержать адрес первого символа 'c' строки "строка\n", которая размещается в некоторой области памяти, начиная с этого адреса:

Здесь string[3] = = 'о'.

Рассмотрим фрагмент программы:

char buffer[ ] =" "; // Инициализация
// строки из 10 пробелов.
char *string = buffer; // string указывает на начало буфера.
string="проба\n"; // Присваивание!

При инициализации создается строка buffer и в нее помещаются символы (здесь 10 пробелов). Инициализация char *string=buffer устанавливает указатель string на начало этой строки.

Операция же присваивания в последней строке не копирует приведенную строку "проба\n" в массив buffer, а изменяет значение указателя stringтак, что он начинает указывать на строку "проба\n":

Чтобы скопировать строку "проба\n" в buffer, можно поступить так:

char buffer[ ] = " ";
char *p ="проба\n";
int i =0;
while ( ( buffer[i] = p[i] ) != '\0' ) i++;

Или так:
char buffer[ ] = " "
char * p = "проба\n";
char * buf = buffer;
while (*buf ++ = *p ++ );

Здесь сначала *p копируется в *buf, т.е. символ 'п' копируется по адресу buf, который совпадает с адресом buffer, т.е. buffer[0] становится равен 'п'. Затем происходит увеличение указателей p и buf, что приводит к продвижению по строкам "проба\n" и buffer соответственно. Последний скопированный символ будет '\0', его значение - 0 и оператор while прекратит цикл.

Еще проще воспользоваться библиотечной функцией, прототип которой находится в файле string.h:

strcpy( buffer, "проба\n");

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

 

 

5. Виртуальные базовые классы.

Иногда применение множественного наследования предполагает достаточно тесную связь между

классами, которые рассматриваются как "братские" базовые классы. Такие классы-братья обычно

должны проектироваться совместно. В большинстве случаев для этого не требуется особый стиль

программирования, существенно отличающийся от того, который мы только что рассматривали. Просто

на производный класс возлагается некоторая дополнительная работа. Обычно она сводится к

переопределению одной или нескольких виртуальных функций (см. $$13.2 и $$8.7). В некоторых

случаях классы-братья должны иметь общую информацию. Поскольку С++ - язык со строгим контролем

типов, общность информации возможна только при явном указании того, что является общим в этих

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

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

конкретизироваться разными способами:

class window {

// головная информация

virtual void draw();

};

Для простоты рассмотрим только один вид общей информации из класса window - функцию draw().

Можно определять разные более развитые классы, представляющие окна (window). В каждом

определяется своя (более развитая) функция рисования (draw):

class window_w_border : public virtual window {

// класс "окно с рамкой"

// определения, связанные с рамкой

void draw();

};

class window_w_menu : public virtual window {

// класс "окно с меню"

// определения, связанные с меню

void draw();

};

Теперь хотелось бы определить окно с рамкой и меню:

class window_w_border_and_menu

: public virtual window,

public window_w_border,

public window_w_menu {

// класс "окно с рамкой и меню"

void draw();

};

Каждый производный класс добавляет новые свойства окна. Чтобы воспользоваться комбинацией всех

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

представления вхождений базового класса window в эти производные классы. Именно это обеспечивает

описание window во всех производных классах как виртуального базового класса.

Можно следующим образом изобразить состав объекта класса window_w_border_and_menu:

Чтобы увидеть разницу между обычным и виртуальным наследованием, сравните этот рисунок с

рисунком из $$6.5, показывающим состав объекта класса satellite. В графе наследования каждый

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

единственным объектом этого класса. Напротив, каждый базовый класс, который при описании

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

 

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

Виртуальный деструктор

Практически всегда деструктор делается виртуальным. Делается это для того, чтобы корректно (без утечек памяти) уничтожались объекты не только заданного класса, а и любого производного от него. Например: в игре уровни, звуки и спрайты могут создаваться загрузчиком, а уничтожаться — менеджером памяти, для которого нет разницы между уровнем и спрайтом.

Пусть (на C++) есть тип Father и порождённый от него тип Son:

class Father

{

public:

Father() {}

~Father() {}

};

 

class Son : public Father

{

public:

int* buffer;

Son() : Father() { buffer = new int[1024]; }

~Son() { delete[] buffer; }

};

Нижеприведённый код является некорректным и приводит к утечке памяти.

Father* object = new Son(); // вызывается Son()

delete object; // вызывается ~Father()!!

Однако, если сделать деструктор Father виртуальным:

class Father

{

public:

Father() {}

virtual ~Father() {}

};

 

class Son : public Father

{

private:

int* buffer;

public:

Son() : Father() { buffer = new int[1024]; }

~Son() { delete[] buffer; }

};

вызов delete object; приведет в последовательному вызову деструкторов ~Son и ~Father.

 

 



Просмотров 915

Эта страница нарушает авторские права




allrefrs.su - 2024 год. Все права принадлежат их авторам!