Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ЛекцииЛаб(Часть_1_Книги).doc
Скачиваний:
7
Добавлен:
03.05.2019
Размер:
1.04 Mб
Скачать

§4. Cтруктуры и классы.

В “старом” классическом языке С в структуры можно было включать в качестве элементов только переменные, т. е. поля. В С++ возможности структуры расширены таким образом, что классы и структуры тесно взаимосвязаны. Структура, как и класс, может содержать не только данные, но и код, т. е. функции, манипулирующие этими данными. Структура может иметь конструктор и деструктор.

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

Как видно из предыдущих параграфов, никакой атрибут доступа, ни public, ни private для полей структуры мы не писали. Очевидно, что мы использовали элементы структуры в других функциях, не являющихся членами структуры. Методы в структурах мы не записывали, т. е. этот тип использовали в стиле языка С. Отсюда можно сделать вывод, что по умолчанию члены структуры имеют атрибут доступа public. Благодаря этому мы смогли использовать члены структуры в функциях, не принадлежащих структурам. При этом использовали составное имя, включающее операции “.” (точка) или “->” (стрелка). Если этот умалчиваемый атрибут не изменить, то нарушается принцип инкапсуляции объектно-ориентированного программирования. По этой причине при использовании объектно-ориентированного стиля программировании для полей структуры и частных методов следует явно указать атрибут private. И наоборот, общедоступные методы можно поместить в начало структуры без явного указания ключевого слова public.

Для сравнения напомним, что члены класса в отличие от структуры по умолчанию имеют атрибут доступа private, а явно необходимо записывать ключевое слово public. “Забегая в третий семестр”, заметим, что можно объявить ссылку на объект как синоним другого имени и указатель на объект. В последнем случае для доступа к общедоступным членам класса, как и для структуры, используется операция “->” (стрелка).

П р и м е р.

Для сравнения приведём реализацию класса, описанного в §2 главы 2, с помощью структуры. Там же смотри комментарии, тексты методов и их использование, которые не меняются.

const Nmax=20;

struct DinArr

{

/*Для общедоступных методов по умолчанию атрибут доступа public.*/

DinArr(int size);

void MyDef();

void Show();

bool Simm();

void Rev();

~DinArr();

/* Для полей и метода Change, который используется только в методе этого же класса Rev, явно записываем атрибут доступа private. */

private:

unsigned n;

float *DA;

void Change(float *x, float* y);

} ;

§5. Объединения.

    1. Представление вещественных чисел в памяти компьютера.

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

Вещественное число (число с плавающей запятой) состоит из двух частей: мантиссы и порядка. Например, число в десятичной системе счисления 0,000123 можно записать одним из следующих способов: 0.0000123*10; 0,123*10-3; 1,23*10-4 и т.д. Аналогично 78900=0,789*105=78,9*103 и т.д. Термин “число с плавающей запятой” и связан с тем, что десятичная запятая перемещается (плывёт) по числу. Из такого рода различных записей в десятичной системе счисления нас будет интересовать нормализованное число, соответственно 0,123*10-3 и 0,789*105. Первая его часть называется мантиссой (0,123 и 0,789), а числа -3 и 5 – порядком.

Аналогично различные варианты записи (на бумаге, а не в памяти компьютера) вещественного числа имеют место и в двоичной системе счисления. Например, рассмотрим десятичное число 12,375. Для его перевода в двоичную систему счисления отдельно переводим целую часть (см. гл. 4 файла Lections1Semestr) и отдельно дробную часть. В качестве вспомогательной системы счисления можно использовать шестнадцатеричную. Для перевода дробной части из 10 с.с в 16 с.с выполняем следующее:

дробную часть числа умножаем на 16;

полученную целую часть результата (число от 0 до 15) переводим в 16-ю с.с и берём в качестве первой после запятой 16-й цифры результата;

дробную часть результата, если она не равна нулю, повторно умножаем на 16;

полученную целую часть переводим в 16-ю с.с и берём в качестве следующей 16-й цифры;

дробную часть результата снова умножаем на 16;

это продолжаем, пока не наступит одна из следующих ситуаций:

  1. на некотором шаге, не обязательно в самом начале, получим в дробной части нуль. В этом случае перевод выполнили точно. Это имеет место в нашем примере: 0,375*16=6.0;

  2. получим в дробной части число, которое было раньше. Например, 0,15*16=2,4; 0,4*16=6,4. Если продолжать умножение 0,4*16, будем получать одно и то же, т. е 6,4. В таком случае получаем следующий результат: 0,1510= 0,2666…16=0,2(6)16. Круглые скобки означают, что записанное в них одно или несколько разных чисел будут повторяться бесконечное число раз. Говорят, что это число в периоде, т.е. 6 в периоде;

  3. если не получаем ни нуль, ни повторяющиеся числа, то ограничиваемся заданным предварительно количеством двоичных или шестнадцатеричных цифр. Для числа типа float необходимо получить 24 двоичные цифры, считая от первой значащей, или не менее 7 шестнадцатеричных цифр, не считая первые 16-е нули.

Для перевода дробной части из 16-й в 2-ю с.с. записываем каждую 16-ю (но не 10-ю!) цифру в виде тетрады, т.е. четырёх двоичных цифр. Получим 12.37510=С.616=1100,0110. При этом последнюю цифру ‘0’ можем не писать. Как и в 10-й с.с., этот нуль незначащий. Остальные нули рядом с десятичной запятой обязательны!

Это двоичное число, как и в 10-й с.с., записать можно по-разному: 11,00011*22; 1100011*2-3; 1.100011*23. Из приведенных вариантов нас будет интересовать последняя нормализованная запись, в которой в целой части записана одна первая значащая единица. Получим: m= памяти не хранится, но 1.100011; p=310=112, где m —нормализованная мантисса, p — порядок в 2 с.с.

Пусть число объявлено как float. Тогда 4 байта (32 бита) распределяются следующим образом:

один самый “левый” бит отводится под знак мантиссы, или, что то же самое, под знак всего числа. Записывается 0, если мантисса, а, значит и само вещественное число, положительное, и 1 в противном случае. Никакого дополнительного кода для отрицательного вещественного числа, как это было для целых чисел, получать не надо;

следующие 8 разрядов (бит) занимает изменённый порядок записи числа в 2-й с.с., который называется характеристикой числа. Обозначим её x. Знак порядка нигде не хранится. Чтобы он всегда был неотрицательным, порядок увеличивается на 12710, т. е. x=p+12710=p+7F16. (1)

Для нашего примера здесь будет храниться число x=310+12710= 13010=8216=100000102. Это же можно вычислить и так: x=316+7F16=8216 =100000102 ;

последние 23 (32-1-8) разряда занимает мантисса. При этом целая её часть, равная 1, в памяти не хранится, но учитывается при вычислениях. Если дробная часть числа переведена в 16-ю, а, значит и в двоичную с.с не точно, т. е. имели место варианты b) и c) (см. выше перевод), последняя 2-я цифра округляется по обычным правилам. Если первая отбрасываемая 2-я цифра равна 1, то прибавляем двоичную единицу, в противном случае оставляем без изменения.

Таким образом, число 12,375 в формате float будет представлено следующим образом: 01000001010001100000000000000000. Иногда в литературе можно встретить шестнадцатеричную запись этого результата: 4146000016.

Упражнение. Представить число -0.01 как число с плавающей точкой в формате float.

Переводим модуль числа в шестнадцатеричную, а затем в двоичную системы счисления описанным выше способом:

0.01= 0.028F5С28F5C…16=0.0(28F5С)16=0.0000001010001111010111000010100…2.

Так как под мантиссу отводится 23 разряда, то должны получить 25 двоичных цифр, не считая первых после десятичной точки подряд идущих нулей. Почему? По правилу нормализации самая первая значащая единица (в примере в десятичной цифре 2) в память не записывается, а ещё одна дополнительная двоичная цифра нужна для того, чтобы определить, как округлять число. Так как первая отбрасываемая двоичная цифра =0, то получаем

m=0.010001111010111000010102.

Если число “маленькое”, т.е. целая часть =0, а в дробной части после запятой несколько подряд идущих нулей, то получим отрицательный порядок. Так как 0.0000001010001111010111000010102 = 1.01000111101011100001010*2-7,

то p=-710=-716, x= p+7F16=7F16-716=7816=011110002.

В результате получим ответ: 10111100001000111101011100001010.

Рассмотрим обратную задачу. Пусть в ячейке размером 4 байта хранится следующая последовательность нулей и единиц, шестнадцатеричное представление которой такое: С215999A16. Известно, что здесь хранится вещественное число, т.е. в программе записано, например, объявление: float a. Что это за число в 10-й системе счисления?

Для ответа на этот вопрос в обратном порядке выполняем действия, описанные выше.

  1. Запишем двоичное представление числа: 11000010000101011001100110011010.

  2. Единица в старшем бите (самая “левая”) означает, что всё вещественное число отрицательное.

  3. В следующих 8 битах находится характеристика числа, т.е. x=100001002=8416. Из формулы (1) получаем двоичный порядок числа: p=x-7F16=8416-7F16 =516=510.

  4. Из последних 23 разрядов получаем m=0.001010110011001100110102.

  5. Поэтому искомое число

a=1.00101011001100110011010*25=100101.011001100110011010225.(6) 1637.410.

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

0.(6) 16 = 0.6666616 = 6*16-1+6*16-2+6*16-3+6*16-4+6*16-50.410.

Т.к. это отрицательное число, то получаем ответ: - 37.410

5.2. Объявление объединения. (+)

Объявление типа объединения, которые ещё называют смеси, похоже на объявление структурного типа. Только вместо ключевого слова struct используется union. Как и для структурной переменной, возможны три способа объявления переменной типа объединения: раздельное, совместное и анонимное (см. 1.1).

Для изучения объединения сначала рассмотрим следующий код для работы со структурой.

Пример 1.

void main()

{

struct TS

{float f; int K;

} S={25.6, 22};

/* Совместное объявление структуры и инициализация её полей. */

printf("%d\n", sizeof(TS));

/* Выведем число 8 — объём занимаемой памяти для размещения всех полей структуры: 4 байта для вещественного числа и столько же для целого */

cout<<endl<<S.f; // Выведем число 25.6

cout<<endl<<hex<<S.K; // Выведем число 16

/* Манипулятор hex используется для ввода и вывода данных (здесь для вывода целого числа) в шестнадцатеичной системе счисления (2210=1616 )*/

/* Рассмотрим аналогичный код для работы с объединением. Объявим теперь тип объединения (TU) и переменную этого типа (U).: */

union TU1

{float f; int K;

} U1= {25.6};

/* Объявление объединения аналогично объявлению структуры, только вместо struct записывается union. Но инициализацию в объединении можно выполнить только для одного поля ! */

cout<< endl<<sizeof(U1)<<endl; // Выводится число 4

cout<<endl<<U1.f; // Число 25.6

cout<<endl<<hex<<U1.K; // Выводится 41cccccd

getch();

}

5.3. Сравнение объединения и структуры. (+)

Основное отличие объединения от структуры в том, что объединение позволяет нескольким переменным различных типов занимать один участок памяти. Его объём равен количеству байт, необходимых для размещения самого “длинного ” поля. В нашем примере оба поля имеют одинаковый размер и занимают четыре байта оперативной памяти. Переменная f и K размещаются в одной и той же области памяти. Содержимое этих четырёх байт рассматривается как вещественное число, если используем U.f или как целое число с идентификатором U.K. Число 25.6 в формате float в четырёх байтах будет представлено так: 01000001110011001100110011001101. А то, что получили при выводе U.K , т.е. 41cccccd —это тот же “набор нулей и единиц” в шестнадцатеричной системе счисления, так как при выводе использовали манипулятор вывода hex.

Рассмотрим пример 2.

union TU2

{int N; short n; char ch[2];

} U2= {0x12345678};

cout<< sizeof(U2)<<endl;

cout<<hex<<U2.N<<endl<<” “<<U2.n<<endl;

cout<<” “<<U2.ch[1]<<U2.ch[0]<<endl;

В результате получим:

4

12345678

5678

Vx

Анализ этого и других результатов показывает, что в современных системах С++ поля объединения выравниваются по правому краю. Выполнение оператора printf("%x %x", 'x', 'V'); показывает, что символ ‘x’ имеет код 7816, а 'V' — код 5616.

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

|----------------------------- U2.N-------------------------|

|------------- U2.n----------|

В последнем “правом” байте находится символ U2.ch[0], а в предпоследнем — символ U2.ch[1].

Элементом (полем) объединения может быть структура. Рассмотрим следующий пример 3.

union TU3 // Тип объединения

{ // Анонимное объявление структуры в объединении

struct

{ unsigned short lo;

unsigned short hi;

} w;

long N;

};

// Раздельное объявление переменной типа объединение.

TU3 U3; U3.N=0x12345678L; // или U3.N=0x12345678;

cout<< hex<<endl<<U3.N<<endl;

cout<<hex<<U3.w.lo<<endl;

cout<<hex<<U3.w.hi<<endl;

В результате получим:

12345678

5678

1234

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

|----------------------- U3.N-------------------------------|

|-----------U3.w.hi----------|---------- U3.w.lo----------|

C помощью объединения можно, например, “посмотреть” каждый байт целого четырёхбайтного числа.

Пример 4.

union TU4

{ char ch[4];

long a; // b) short a; // c) unsigned a ;

} U4;

U4.a=0x30334142; // или U4.a=0x30334142L;

cout<<endl<<"Size of union "<<sizeof(U4)<<endl;

for(int i=3; i>=0; i--)

cout<<U4.ch[i]<<" ";

cout<< endl<< U4.a<<endl;

cout<<hex<< U4.a<<endl;

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

Рассмотрим следующее, например, анонимное объединение:

union

{int k; float f;};

f=25.6;

cout<<k;

Будет выведено 41cccccd (см. объединение U1). Этот пример показывает, что объединение чем-то похоже на ссылочный тип. Но в отличие от последнего одна и та же ячейка не просто по-разному называется. Её содержимое можно интерпретировать с разными типами данных.

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

Например, после объявления TU4 &SU=U4 к одной и той же области памяти из четырёх байт можно обращаться как с помощью идентификатора SU (SU.ch[i]), так и с помощью U4 (U4.ch[i]).

Объявим указатель на объединение:

TU3 *pU3=new TU3;

Тогда доступ к полям осуществляется, как и для указателя на структуру, с помощью операции ->. Например

pU3->N=0x12345678L;

cout<< hex<<pU3->N<<endl;

cout<<hex<<pU3->w.lo<<endl;

cout<<hex<<pU3->w.hi<<endl;

Заметим, что внутри объединения используется по-прежнему просто структура, а не указатель на неё. Поэтому остаётся и операция точка для доступа к полям вложенной структуры w.