Шилдт c++_базовый_курс издание 3. Герберт Шилдт С базовый курс
Скачать 9.37 Mb.
|
Глава 10: Структуры и объединения В языке C++ определено несколько составных типов данных, т.е. типов, которые состоят из двух или более элементов. С одним из составных типов — массивом — вы уже знакомы. С двумя другими — структурами и объединениями — вы познакомитесь в этой главе, а знакомство с еще одним типом — классом — мы отложим до главы 11. И хотя структура и объединение предназначены для удовлетворения различных потребностей программистов, оба они представляют собой удобные средства управления группами связанных переменных. При этом важно понимать, что создание структуры (или объединения) означает создание нового определенного программистом типа данных. Сама возможность создания собственных типов данных является признаком могущества языка C++. В C++ структуры и объединения имеют как объектно-ориентированные, так и не объектно-ориентированные атрибуты. В этой главе рассматриваются только последние. Об объектно-ориентированных и их свойствах речь пойдет в следующей главе после введения понятия о классах и объектах. Структуры Структура — это группа связанных переменных. В C++ структура представляет собой коллекцию объединенных общим именем переменных, которая обеспечивает удобное средство хранения родственных данных в одном месте. Структуры — это совокупные типы данных, поскольку они состоят из нескольких различных, но логически связанных переменных. По тем же причинам их иногда называют составными или конгломератными типами данных. Прежде чем будет создан объект структуры, должен быть определен ее формат. Это делается посредством объявления структуры. Объявление структуры позволяет понять, переменные какого типа она содержит. Переменные, составляющие структуру, называются ее членами. Члены структуры также называют элементами или полями. Член структуры — это переменная, которая является частью структуры. В общем случае все члены структуры должны быть логически связаны друг с другом. Например, структуры обычно используются для хранения такой информации, как почтовые адреса, таблицы имен компилятора, элементы карточного каталога и т.п. Безусловно, отношения между членами структуры совершенно субъективны и определяются программистом. Компилятор "ничего о них не знает" (или "не хочет знать"). Начнем рассмотрение структуры с примера. Определим структуру, которая может содержать информацию о товарах, хранимых на складе компании. Одна запись инвентарной ведомости обычно состоит из нескольких данных, например: наименование товара, стоимость и количество, имеющееся в наличии. Поэтому для управления подобной информацией удобно использовать именно структуру. В следующем фрагменте кода объявляется структура, которая определяет следующие элементы: наименование товара, стоимость, розничная цена, имеющееся в наличии количество и время до пополнения запасов. Этих данных часто вполне достаточно для управления складским хозяйством. О начале объявления структуры компилятору сообщает ключевое слово struct. struct inv_type { char item[40]; // наименование товара double cost; // стоимость double retail; // розничная цена int on_hand; // имеющееся в наличии количество int lead_time; // число дней до пополнения запасов }; Имя структуры — это ее спецификатор типа. Обратите внимание на то, что объявление завершается точкой с запятой. Дело в том, что объявление структуры представляет собой инструкцию. Именем типа структуры здесь является inv_type. Другими словами, имя inv_type идентифицирует конкретную структуру данных и является ее спецификатором типа. В предыдущем объявлении в действительности не было создано ни одной переменной. Был определен лишь формат данных. Чтобы с помощью этой структуры объявить реальную переменную (т.е. физический объект), нужно записать инструкцию, подобную следующей. inv_type inv_var; Вот теперь объявляется структурная переменная типа inv_type с именем inv_var. Помните: определяя структуру, вы определяете новый тип данных, но он не будет реализован до тех пор, пока вы не объявите переменную того типа, который уже реально существует. При объявлении структурной переменной C++ автоматически выделит объем памяти, достаточный для хранения всех членов структуры. На рис. 10.1 показано, как переменная inv_var будет размещена в памяти компьютера (в предположении, что double-значение занимает 8 байт, а int-значение — 4). Одновременно с определением структуры можно объявить одну или несколько переменных, как показано в этом примере. struct inv_type { char item[40]; // наименование товара double cost; // стоимость double retail; // розничная цена int on_hand; // имеющееся в наличии количество int lead_time; // число дней до пополнения запасов } inv_varA, inv_varB, inv_varC; Этот фрагмент кода определяет структурный тип inv_type и объявляет переменные inv_varA, inv_varB и inv_varC этого типа. Важно понимать, что каждая структурная переменная содержит собственные копии членов структуры. Например, поле cost структуры inv_varA изолировано от поля cost структуры inv_varB. Следовательно, изменения, вносимые в одно поле, никак не влияют на содержимое другого поля. Если для программы достаточно только одной структурной переменной, в ее определение необязательно включать имя структурного типа. Рассмотрим следующий пример: struct { char item[40]; // наименование товара double cost; // стоимость double retail; // розничная цена int on_hand; // имеющееся в наличии количество int lead_time; // число дней до пополнения запасов } temp; Этот фрагмент кода объявляет одну переменную temp в соответствии с предваряющим ее определением структуры. Ключевое слово struct означает начало объявления структуры. Общий формат объявления структуры выглядит так. struct имя_типа_структуры { тип имя_элемента1; тип имя_элемента2; тип имя_элемента3; тип имя_элементаN; } структурные_переменные; Доступ к членам структуры К отдельным членам структуры доступ осуществляется с помощью оператора "точка". Например, при выполнении следующего кода значение 10.39 будет присвоено полю cost структурной переменной inv_var, которая была объявлена выше. inv_var.cost = 10.39; Чтобы обратиться к члену структуры, нужно перед его именем поставить имя структурной переменной и оператор "точка". Так осуществляется доступ ко всем элементам структуры. Общий формат доступа записывается так. имя_структурной_переменной.имя_члена Оператор "точка" (.) позволяет получить доступ к любому члену любой структуры. Следовательно, чтобы вывести значение поля cost на экран, необходимо выполнить следующую инструкцию. cout << inv_var.cost; Аналогичным способом можно использовать символьный массив inv_var.item в вызове функции gets(). gets(inv_var.item); Здесь функции gets() будет передан символьный указатель на начало области памяти, отведенной элементу item. Если вам нужно получить доступ к отдельным элементам массива inv_var.item, используйте индексацию. Например, с помощью этого кода можно посимвольно вывести на экран содержимое массива inv_var.item. int t; for(t=0; inv_var.itern[t]; t++) cout << inv_var.item[t]; Массивы структур Структуры могут быть элементами массивов. И в самом деле, массивы структур используются довольно часто. Чтобы объявить массив структур, необходимо сначала определить структуру, а затем объявить массив элементов этого структурного типа. Например, чтобы объявить 100-элементный массив структур типа inv_type (который определен выше), достаточно записать следующее. inv_type invtry[100]; Чтобы получить доступ к конкретной структуре в массиве структур, необходимо индексировать имя структуры. Например, чтобы отобразить на экране содержимое члена on_hand третьей структуры, используйте такую инструкцию. cout << invtry[2].on_hand; Подобно всем переменным массивов, у массивов структур индексирование начинается с нуля. Простой пример инвентаризации склада Чтобы продемонстрировать применение структур, разработаем простую программу управления складом, в которой для хранения информации о товарах, размещенных на складе компании, используется массив структур типа inv_type. Различные функции, определенные в этой программе, взаимодействуют со структурой и ее элементами по-разному. Инвентарная ведомость будет храниться в структурах типа inv_type, организованных в массиве invtry. const int SIZE = 100; struct inv_type { char item[40]; // наименование товара double cost; // стоимость double retail; // розничная цена int on_hand; // имеющееся в наличии количество int lead_time; // число дней до пополнения запасов } invtry[SIZE]; Размер массива выбран произвольно. При желании его можно легко изменить. Обратите внимание на то, что размерность массива задана с использованием const-переменной. А поскольку размер массива во всей программе используется несколько раз, применение const-переменной для этой цели весьма оправданно. Чтобы изменить размер массива, достаточно изменить значение константной переменной SIZE, а затем перекомпилировать программу. Использование const-переменной для определения "магического числа", которое часто употребляется в программе, — обычная практика в профессиональном С++-коде. Разрабатываемая программа должна обеспечить выполнение следующих действий: ■ ввод информации о товарах, хранимых на складе; ■ отображение инвентарной ведомости; ■ модификация заданного элемента. Прежде всего напишем функцию main(), которая должна иметь примерно такой вид. int main() { char choice; init_list(); for(;;) { choice = menu(); switch(choice) { case 'e': enter(); break; case 'd': display(); break; case 'u': update(); break; case 'q': return 0; } } } Функция main() начинается с вызова функции init_list(), которая инициализирует массив структур. Затем организован цикл, который отображает меню и обрабатывает команду, выбранную пользователем. Приведем код функции init_list(). // Инициализация массива структур. void init_list() { int t; // Имя нулевой длины означает пустое имя. for(t=0; t Функция init_list() подготавливает массив структур для использования, помещая в первый байт поля item нулевой символ. Предполагается, что если поле item пустое, то структура, в которой оно содержится, попросту не используется. Функция menu_select() отображает команды меню и принимает вариант, выбранный пользователем. // Получение команды меню, выбранной пользователем. int menu() { char ch; cout << '\n'; do { cout << "(E)nter\n"; // Ввести новый элемент. cout << "(D)isplay\n"; // Отобразить всю ведомость. cout << "(U)pdate\n"; // Изменить элемент. cout << "(Q)uit\n\n"; // Выйти из программы. cout << "Выберите команду: "; cin >> ch; }while(!strchr("eduq", tolower(ch))); return tolower(ch); } Пользователь выбирает из предложенного меню команду, вводя нужную букву. Например, чтобы отобразить всю инвентарную ведомость, нажмите букву "D". Функция menu() вызывает библиотечную функцию C++ strchr(), которая имеет такой прототип. char *strchr(const char *str, int ch); Эта функция просматривает строку, адресуемую указателем str, на предмет вхождения в нее символа, который хранится в младшем байте переменной ch. Если такой символ обнаружится, функция возвратит указатель на него. И в этом случае значение, возвращаемое функцией, по определению будет истинным. Но если совпадения символов не произойдет, функция возвратит нулевой указатель, который по определению представляет собой значение ЛОЖЬ. Так здесь организована проверка того, являются ли значения, вводимые пользователем, допустимыми командами меню. Функция enter() предваряет вызов функции input(), которая "подсказывает" пользователю порядок ввода данных и принимает их. Рассмотрим код обеих функций. // Ввод элементов в инвентарную ведомость. void enter() { int i; // Находим первую свободную структуру. for(i=0; i // Если массив полон, значение i будет равно SIZE. if(i==SIZE) { cout << "Список полон.\n"; return; } input (i); } // Ввод информации. void input(int i) { cout << "Товар: "; cin >> invtry[i].item; cout << "Стоимость: "; cin >> invtry[i].cost; cout << "Розничная цена: "; cin >> invtry[i].retail; cout << "В наличии: "; cin >> invtry[i].on_hand; cout << "Время до пополнения запасов (в днях): "; cin >> invtry[i].lead_time; } Функция enter() сначала находит пустую структуру. Для этого проверяется поле item каждого (по очереди) элемента массива invtry, начиная с первого. Если поле item оказывается пустым, то предполагается, что структура, к которой оно относится, еще ничем не занята. Если не отыщется ни одной свободной структуры при проверке всего массива структур, управляющая переменная цикла i станет равной его размеру. Это говорит о том, что массив полон, и в него уже нельзя ничего добавить. Если же в массиве найдется свободный элемент, будет вызвана функция input() для получения информации о товаре, вводимой пользователем. Если вас интересует, почему код ввода данных о новом товаре не является частью функции enter(), то ответ таков: функция input() используется также и функцией update(), о которой речь впереди. Поскольку информация о товарах на складе может меняться, программа ведения инвентарной ведомости должна позволять вносить изменения в ее отдельные элементы. Это реализуется путем вызова функции update(). // Модификация существующего элемента. void update() { int i; char name[80]; cout << "Введите наименование товара: "; cin >> name; for(i=0; i if(i==SIZE) { cout << "Товар не найден.\n"; return; } cout << "Введите новую информацию.\n"; input(i); } Эта функция предлагает пользователю ввести наименование товара, информацию о котором ему нужно изменить. Затем она просматривает весь список существующих элементов, и если указанный товар в нем имеется, то вызывается функция input(), которая обеспечивает прием от пользователя новой информации. Нам осталось рассмотреть функцию display(). Она выводит на экран инвентарную ведомость в полном объеме. Код функции display() выглядит так. // Отображение на экране инвентарной ведомости. void display() { int t; for(t=0; t cout << invtry[t].item << '\n'; cout << "Стоимость: $" << invtry[t].cost; cout << "\nB розницу: $"; cout << invtry[t].retail << '\n'; cout << "В наличии: " << invtry[t].on_hand; cout << "\nДо пополнения осталось: "; cout << invtry[t].lead_time << " дней\n\n"; } } } Ниже приведена законченная программа ведения инвентарной ведомости. Вам следует ввести эту программу в свой компьютер и исследовать ее работу. Внесите некоторые изменения и понаблюдайте, как они отразятся на ее выполнении. Попробуйте также расширить программу, добавив функции поиска в списке заданного товара, удаления уже ненужного элемента или полной очистки инвентарной ведомости. /* Простая программа ведения инвентарной ведомости, в которой используется массив структур. */ #include #include #include #include using namespace std; const int SIZE = 100; struct inv_type { char item[40]; // наименование товара double cost; // стоимость double retail; // розничная цена int on_hand; // имеющееся в наличии количество int lead_time; // число дней до пополнения запасов } invtry[SIZE]; void enter(), init_list(), display(); void update(), input(int i); int menu(); int main() { char choice; init_list(); fоr (;;) { choice = menu(); switch(choice) { case 'e' : enter(); break; case 'd' : display(); break; case 'u': update(); break; case 'q': return 0; } } } // Инициализация массива структур. void init_list() { int t; // Имя нулевой длины означает пустое имя. for(t=0; t //Получение команды меню, выбранной пользователем. int menu() { char ch; cout << '\n'; do { cout << "(E)nter\n"; // Ввести новый элемент. cout << "(D)isplay\n"; // Отобразить всю ведомость. cout << " (U) pdate\n"; // Изменить элемент. cout << " (Q) uit\n\n"; // Выйти из программы. cout << "Выберите команду: "; cin >> ch; }while(!strchr("eduq", tolower(ch))); return tolower(ch); } //Ввод элементов в инвентарную ведомость. void enter() { int i; // Находим первую свободную структуру. for(i=0; i // Если массив полон, значение i будет равно SIZE. if(i==SIZE) { cout << "Список полон.\n"; return; } input(i); } // Ввод информации. void input(int i) { cout << "Товар: "; cin >> invtry[i].item; cout << "Стоимость: "; cin >> invtry[i].cost; cout << "Розничная цена: "; cin >> invtry[i].retail; cout << "В наличии: "; cin >> invtry[i].on_hand; cout << "Время до пополнения запасов (в днях): "; cin >> invtry[i].lead_time; } // Модификация существующего элемента. void update() { int i; char name[80]; cout << "Введите наименование товара: "; cin >> name; for(i=0; i if(i==SIZE) { cout << "Товар не найден.\n"; return; } cout << "Введите новую информацию.\n"; input (i); } // Отображение на экране инвентарной ведомости. void display() { int t; for(t=0; t cout << invtry[t].item << '\n'; cout << "Стоимость: $" << invtry[t].cost; cout << "\nB розницу: $"; cout << invtry[t].retail << '\n'; cout << "В наличии: " << invtry[t].on_hand; cout << "\nДо пополнения осталось: "; cout << invtry[t].lead_time << " дней\n\n"; } } } Передача структур функциям При передаче функции структуры в качестве аргумента используется механизм передачи параметров по значению. Это означает, что любые изменения, внесенные в содержимое структуры в теле функции, которой она передана, не влияют на структуру, используемую в качестве аргумента. Однако следует иметь в виду, что передача больших структур требует значительных затрат системных ресурсов. (Как правило, чем больше данных передается функции, тем больше расходуется системных ресурсов.) Используя структуру в качестве параметра, помните, что тип аргумента должен соответствовать типу параметра. Например, в следующей программе сначала объявляется структура sample, а затем функция f1() принимает параметр типа sample. // Передача функции структуры в качестве аргумента. #include using namespace std; // Определяем тип структуры. struct sample { int a; char ch; }; void f1(sample parm); int main() { struct sample arg; // Объявляем переменную arg типа sample. arg.a = 1000; arg.ch = 'x'; f1(arg); return 0; } void f1(sample parm) { cout << parm.a << " " << parm.ch << "\n"; } Здесь как аргумент arg в функции main(), так и параметр parm в функции f1() имеют одинаковый тип. Поэтому аргумент arg можно передать функции f1(). Если бы типы этих структур были различны, при компиляции программы было бы выдано сообщение об ошибке. Присваивание структур Содержимое одной структуры можно присвоить другой, если обе эти структуры имеют одинаковый тип. Например, следующая программа присваивает значение структурной переменной svar1 переменной svar2. // Демонстрация присваивания значений структур. #include using namespace std; struct stype { int a, b; }; int main() { stype svar1, svar2; svar1.a = svar1.b = 10; svar2.a = svar2.b = 20; cout << "Структуры до присваивания.\n"; cout << "svar1: " << svar1.a << ' ' << svar1.b; cout <<'\n'; cout << "svar2: " << svar2.a << ' ' << svar2.b; cout <<"\n\n"; svar2 = svar1; // присваивание структур cout << "Структуры после присваивания.\n"; cout << "svar1: " << svar1.a << ' ' << svar1.b; cout << '\n'; cout << "svar2: " << svar2.a << ' ' << svar2.b; return 0; } Эта программа генерирует следующие результаты. Структуры до присваивания. svar1: 10 10 svar2: 20 20 Структуры после присваивания, svar1: 10 10 svar2: 10 10 В C++ каждое новое объявление структуры определяет новый тип. Следовательно, даже если две структуры физически одинаковы, но имеют разные имена типов, компилятор будет считать их разными и не позволит присвоить значение одной из них другой. Рассмотрим следующий фрагмент кода. Он некорректен и поэтому не скомпилируется. struct stype1 { int a, b; }; struct stype2 { int a, b; }; stype1 svar1; stype2 svar2; svar2 = svar1; // Ошибка из-за несоответствия типов. Несмотря на то что структуры stype1 и stype2 физически одинаковы, с точки зрения компилятора они являются отдельными типами. Узелок на память. Одну структуру можно присвоить другой только в том случае, если обе они имеют одинаковый тип. Использование указателей на структуры и оператора "стрелка" В C++ указатели на структуры можно использовать таким же способом, как и указатели на переменные любого другого типа. Однако использование указателей на структуры имеет ряд особенностей, которые необходимо учитывать. Указатель на структуру объявляется так же, как указатель на любую другую переменную, т.е. с помощью символа "*", поставленного перед именем структурной переменной. Например, используя определенную выше структуру inv_type, можно записать следующую инструкцию, которая объявляет переменную inv_pointer указателем на данные типа inv_type: inv_type *inv_pointer; Чтобы найти адрес структурной переменной, необходимо перед ее именем разместить оператор "&". Например, предположим, с помощью следующего кода мы определяем структуру, объявляем структурную переменную и указатель на структуру определенного нами типа. struct bal { float balance; char name[80]; } person; bal *p; // Объявляем указатель на структуру. Тогда при выполнении инструкции р = &person; в указатель р будет помещен адрес структурной переменной person. К членам структуры можно получить доступ с помощью указателя на эту структуру. Но в этом случае используется не оператор "точка", а оператор "->". Например, при выполнении этой инструкции мы получаем доступ к полю balance через указатель р: p->balance Оператор "->" называется оператором "стрелка". Он образуется с использованием знаков "минус" и "больше". Оператор "стрелка" (->) позволяет получить доступ к членам структуры с помощью указателя. Указатель на структуру можно использовать в качестве параметра функции. Важно помнить о таком способе передачи параметров, поскольку он работает гораздо быстрее, чем в случае, когда функции "собственной персоной" передается объемная структура. (Передача указателя всегда происходит быстрее, чем передача самой структуры.) Узелок на память. Чтобы получить доступ к членам структуры, используйте оператор "точка". Чтобы получить доступ к членам структуры с помощью указателя, используйте оператор "стрелка". Пример использования указателей на структуры В качестве интересного примера использования указателей на структуры можно рассмотреть С++-функции времени и даты. Эти функции считывают значения текущего системного времени и даты. Для их использования в программу необходимо включить заголовок календарного времени. Второй тип представляет собой структуру tm, которая содержит отдельные элементы даты и времени. Такое представление времени называют поэлементным. Структура tm имеет следующий формат. struct tm { int tm_sec; /* секунды, 0-59 */ int tm_min; /* минуты, 0-59 */ int tm_hour; /* часы, 0-23 */ int tm_mday; /* день месяца, 1-31 */ int tm_mon; /* месяц, начиная с января, 0-11 */ int tm_year; /* год после 1900 */ int tm_wday; /* день, начиная с воскресенья, 0-6 */ int tm_yday; /* день, начиная с 1-го января, 0-365 */ int tm_isdst /* индикатор летнего времени */ } Значение tm_isdst положительно, если действует режим летнего времени (Daylight Saving Time), равно нулю, если не действует, и отрицательно, если информация об этом недоступна. Основным средством определения времени и даты в C++ является функция time(), которая имеет такой прототип: time_t time(time_t *curtime); Функция time() возвращает текущее календарное время системы. Если в системе отсчет времени не производится, возвращается значение -1. Функцию time() можно вызывать либо с нулевым указателем, либо с указателем на переменную curtime типа time_t. В последнем случае этой переменной будет присвоено значение текущего календарного времени. Чтобы преобразовать календарное время в поэлементное, используйте функцию localtime(), которая имеет такой прототип: struct tm *localtime(const time_t *curtime); Функция localtime() возвращает указатель на поэлементную форму параметра curtime, представленного в виде структуры tm. Значение curtime представляет локальное время. Его обычно получают с помощью функции time(). Структура, используемая функцией localtime() для хранения времени в поэлементной форме, размещается в памяти статически и перезаписывается при каждом вызове этой функции. Если нужно сохранить содержимое этой структуры, скопируйте его в какую- нибудь другую область памяти. Следующая программа демонстрирует использование функций time() и localtime(), отображая на экране текущее системное время. // Эта программа отображает текущее системное время. #include #include using namespace std; int main() { struct tm *ptr; time_t lt; lt = time('\0'); ptr = localtime(<); cout << ptr->tm_hour << ':' << ptr->tm_min; cout << ':' << ptr->tm_sec; return 0; } Вот один из возможных результатов выполнения этой программы: 14:52:30 Несмотря на то что ваши программы могут использовать поэлементную форму представления времени и даты (как показано в предыдущем примере), проще всего сгенерировать строку времени и даты с помощью функции asctime(), прототип который выглядит так: char *asctime(const struct tm *ptr); Функция asctime() возвращает указатель на строку, которая содержит результат преобразования информации, хранимой в адресуемой параметром ptr структуре, и имеет следующую форму. день месяц число часы:минуты:секунды год\n\0 Указатель на структуру, передаваемый функции asctime(), часто получают с помощью функции localtime(). Область памяти, используемая функцией asctime() для хранения форматированной строки результата, представляет собой символьный массив (статически выделяемый в памяти), который перезаписывается при каждом вызове этой функции. Если нужно сохранить содержимое данной строки, скопируйте его в какую-нибудь другую область памяти. В следующей программе демонстрируется использование функции asctime() для отображения системного времени и даты. // Эта программа отображает текущее системное время. #include #include using namespace std; int main() { struct tm *ptr; time_t lt; lt = time('\0'); ptr = localtime(<); cout << asctime(ptr); return 0; } Вот один из возможных результатов выполнения этой программы. Wed Jul 28 15:05:51 2004 В языке C++ предусмотрены и другие функции даты и времени, с которыми можно познакомиться, обратившись к документации, прилагаемой к вашему компилятору. Ссылки на структуры Для доступа к структуре можно использовать ссылку. Ссылка на структуру часто используется в качестве параметра функции или значения, возвращаемого функцией. При получении доступа к членам структуры с помощью ссылки используйте оператор "точка". (Оператор "стрелка" зарезервирован для доступа к членам структуры с помощью указателя.) В следующей программе показано, как можно использовать структуру при передаче функции параметров по ссылке. // Демонстрируем использование ссылки на структуру. #include using namespace std; struct mystruct { int a; int b; }; mystruct &f(mystruct &var); int main() { mystruct x, y; x.a = 10; x.b = 20; cout << "Исходные значения полей x.a and x.b: "; cout << x.a << ' ' << x.b << '\n'; y = f (x); cout << "Модифицированные значения полей x.a и x.b: "; cout << x.a << ' ' << x.b << '\n'; cout << "Модифицированные значения полей y.a и y.b: "; cout << y.a << ' ' << y.b << '\n'; return 0; } // Функция, которая получает и возвращает ссылку на структуру. mystruct &f(mystruct &var) { var.a = var.a * var.a; var.b = var.b / var.b; return var; } Вот результаты выполнения этой программы. Исходные значения полей x.a and x.b: 10 20 Модифицированные значения полей х.а и x.b: 100 1 Модифицированные значения полей у.а и y.b: 100 1 Ввиду существенных затрат системных ресурсов на передачу структуры функции (или при возвращении ее функцией) многие С++-программисты для выполнения таких задач используют ссылки на структуры. Использование в качестве членов структур массивов и структур Член структуры может иметь любой допустимый тип данных, в том числе и такие составные типы, как массивы и другие структуры. Поскольку эта тема нередко вызывает у программистов затруднения, имеет смысл остановиться на ней подробнее. Массив, используемый в качестве члена структуры, обрабатывается вполне ожидаемым способом. Рассмотрим такую структуру. struct stype { int nums[10][10]; // Целочисленный массив размерностью 10 х 10. float b; } var; Чтобы обратиться к элементу массива nums с "координатами" 3,7 в структуре var типа stype, следует записать такой код: var.nums[3][7] Как показано в этом примере, если массив является членом структуры, то для доступа к элементам этого массива индексируется имя массива, а не имя структуры. Если некоторая структура является членом другой структуры, она называется вложенной структурой. В следующем примере структура addr вложена в структуру emp. struct addr { char name[40]; char street[40]; char city[40]; char zip[10]; } struct emp { addr address; float wage; } worker; Здесь структура emp имеет два члена. Первым членом является структура типа addr, которая будет содержать адрес служащего. Вторым членом является переменная wage, которая хранит его оклад. При выполнении следующего фрагмента кода полю zip структуры address, которая является членом структуры worker, будет присвоен почтовый индекс 98765: worker.address.zip = 98765; Как видите, члены структур указываются слева направо, от самой крайней внешней до самой дальней внутренней. Структура также может содержать в качестве своего члена указатель на эту же структуру. И в самом деле для структуры вполне допустимо содержать член, который является указателем на нее саму. Например, в следующей структуре переменная sptr объявляется как указатель на структуру типа mystruct, т.е. на объявляемую здесь структуру. struct mystruct { int а; char str[80]; mystruct *sptr; // указатель на объекты типа mystruct }; Структуры, содержащие указатели на самих себя, часто используются при создании таких структур данных, как связные списки. По мере изучения языка C++ вы встретите приложения, в которых применяются подобные вещи. Сравнение С- и С++-структур С++-структуры — потомки С-структур. Следовательно, любая С-структура также является действительной С++-структурой. Между ними, однако, существуют важные различия. Во-первых, как будет показано в следующей главе, С++-структуры имеют некоторые уникальные атрибуты, которые позволяют им поддерживать объектно- ориентированное программирование. Во-вторых, в языке С структура не определяет в действительности новый тип данных. Этим может "похвалиться" лишь С++-структура. Как вы уже знаете, определяя структуру в C++, вы определяете новый тип, который называется по имени этой структуры. Этот новый тип можно использовать для объявления переменных, определения значений, возвращаемых функциями, и т.п. Однако в С имя структуры называется ее тегом (или дескриптором). А тег сам по себе не является именем типа. Чтобы понять это различие, рассмотрим следующий фрагмент С-кода. struct C_struct { int а; int b; } // объявление переменной C_struct struct C_struct svar; Обратите внимание на то, что приведенное выше определение структуры в точности такое же, как в языке C++. Теперь внимательно рассмотрите объявление структурной переменной svar. Оно также начинается с ключевого слова struct. В языке С после определения структуры для полного задания типа данных все равно нужно использовать ключевое слово struct совместно с тегом этой структуры (в данном случае с идентификатором C_struct). Если вам придется преобразовывать старые С-программы в код C++, не беспокойтесь о различиях между С- и С++-структурами, поскольку C++ по-прежнему принимает С- ориентированные объявления. Например, предыдущий фрагмент С-кода корректно скомпилируется как часть любой С++-программы. С точки зрения компилятора C++ в объявлении переменной svar всего лишь избыточно использовано ключевое слово struct, без которого в C++ можно обойтись. Битовые поля структур Битовое поле — это бит-ориентированный член структуры. В отличие от многих других компьютерных языков, в C++ предусмотрен встроенный способ доступа к конкретному разряду байта. Побитовый доступ возможен путем использования битовых полей. Битовые поля могут оказаться полезными в различных ситуациях. Приведем всего три примера. Во-первых, если вы имеете дело с ограниченным объемом памяти, можно хранить несколько булевых (логических) значений в одном байте. Во-вторых, некоторые интерфейсы устройств передают информацию, закодированную именно в битах. И, в-третьих, существуют подпрограммы кодирования, которым нужен доступ к отдельным битам в рамках байта. Реализация всех этих функций возможна с помощью поразрядных операторов, как было показано в предыдущей главе, но битовое поле может сделать вашу программу более прозрачной и читабельной, а также повысить ее переносимость. Метод, который использован в языке C++ для доступа к битам, основан на применении структур. Битовое поле — это в действительности специальный тип члена структуры, который определяет свой размер в битах. Общий формат определения битовых полей таков. struct имя_типа_структуры { тип имя1 : длина; тип имя2 : длина; тип имяN : длина; }; Здесь элемент тип означает тип битового поля, а элемент длина — количество битов в этом поле. Битовое поле должно быть объявлено как значение целочисленного типа или перечисления. Битовые поля длиной 1 бит объявляются как значения типа без знака (unsigned), поскольку единственный бит не может иметь знакового разряда. Битовые поля обычно используются для анализа входных данных, принимаемых от устройств, входящих в состав оборудования системы. Например, порт состояний последовательного адаптера связи может возвращать байт состояния, организованный таким образом. Для представления информации, которая содержится в байте состояний, можно использовать следующие битовые поля. struct status_type { unsigned delta_cts: 1; unsigned delta_dsr: 1; unsigned tr_edge: 1; unsigned delta_rec: 1; unsigned cts: 1; unsigned dsr: 1; unsigned ring: 1; unsigned rec_line: 1; } status; Чтобы определить, когда можно отправить или получить данные, используется код, подобный следующему. status = get_port_status(); if(status.cts) cout << "Установка в исходное состояние"; if(status.dsr) cout << "Данные готовы"; Чтобы присвоить битовому полю значение, достаточно использовать такую же форму, которая обычно применяется для элемента структуры любого другого типа. Например, следующая инструкция очищает битовое поле ring: status.ring = 0; Как видно из этих примеров, доступ к каждому битовому полю можно получить с помощью оператора "точка". Но если общий доступ к структуре осуществляется через указатель, необходимо использовать оператор "->". Следует иметь в виду, что совсем необязательно присваивать имя каждому битовому полю. Это позволяет обращаться только к нужным битам, "обходя" остальные. Например, если вас интересуют только биты cts и dsr, вы могли бы объявить структуру status_type следующим образом. struct status_type { unsigned : 4; unsigned cts: 1; unsigned dsr: 1; } status; Обратите здесь внимание на то, что биты после последнего именованного dsr нет необходимости вообще упоминать. В структуре можно смешивать "обычные" члены с битовыми полями. Вот пример. struct emp { struct addr address; float pay; unsigned lay_off: 1; // работает или нет unsigned hourly: 1: // почасовая оплата или оклад unsigned deductions: 3: // удержание налога }; Эта структура определяет запись по каждому служащему, в которой используется только один байт для хранения трех элементов информации: статус служащего, характер оплаты его труда (почасовая оплата или твердый оклад) и налоговая ставка. Без использования битовых полей для хранения этой информации пришлось бы занять три байта. Использование битовых полей имеет определенные ограничения. Программист не может получить адрес битового поля или ссылку на него. Битовые поля нельзя хранить в массивах. Их нельзя объявлять статическими. При переходе от одного компьютера к другому невозможно знать наверняка порядок следования битовых полей: справа налево или слева направо. Это означает, что любая программа, в которой используются битовые поля, может страдать определенной зависимостью от марки компьютера. Возможны и другие ограничения, связанные с особенностями реализации компилятора C++, поэтому имеет смысл прояснить этот вопрос в соответствующей документации. В следующем разделе представлена программа, в которой используются битовые поля для отображения символьных ASCII-кодов в двоичной системе счисления. Объединения Объединение состоит из нескольких переменных, которые разделяют одну и ту же область памяти. Объединение состоит из нескольких переменных, которые разделяют одну и ту же область памяти. Следовательно, объединение обеспечивает возможность интерпретации одной и той же конфигурации битов двумя (или более) различными способами. Объявление объединения, как нетрудно убедиться на следующем примере, подобно объявлению структуры. union utype { short int i; char ch; }; Объявление объединения начинается с ключевого слова union. Здесь объявляется объединение, в котором значение типа short int и значение типа char разделяют одну и ту же область памяти. Необходимо сразу же прояснить один момент: невозможно сделать так, чтобы это объединение хранило и целочисленное значение, и символ одновременно, поскольку переменные i и ch накладываются (в памяти) друг на друга. Но программа в любой момент может обрабатывать информацию, содержащуюся в этом объединении, как целочисленное значение или как символ. Следовательно, объединение обеспечивает два (или больше) способа представления одной и той же порции данных. Как видно из этого примера, объединение объявляется с помощью ключевого слова union. Как и при использовании структур, при объявлении объединения не определяется ни одна переменная. Переменную можно объявить, разместив ее имя в конце объявления либо воспользовавшись отдельной инструкцией объявления. Чтобы объявить переменную объединения именем u_var типа utype, достаточно записать следующее: utype u_var; В переменной объединения u_var как переменная i типа short int, так и символьная переменная ch разделяют одну и ту же область памяти. (Безусловно, переменная i занимает два байта, а символьная переменная ch использует только один.) Как переменные i и ch разделяют одну область памяти, показано на рис. 10.2. При объявлении объединения компилятор автоматически выделяет область памяти, достаточную для хранения в объединении переменных самого большого по объему типа. Чтобы получить доступ к элементу объединения, используйте тот же синтаксис, который применяется и для структур: операторы "точка" и "стрелка". При непосредственном обращении к объединению (или посредством ссылки) используется оператор "точка". Если же доступ к переменной объединения осуществляется через указатель, используется оператор "стрелка". Например, чтобы присвоить букву 'А' элементу ch объединения u_var, достаточно использовать такую запись. u_var.ch = 'А'; В следующем примере функции передается указатель на объединение u_var. В теле этой функции с помощью указателя переменной i присваивается значение 10. // ... func1(&u_var); // Передаем функции func1() указатель на объединение u_var. // ... } void fund (utype *un) { un->i = 10; /* Присваиваем число 10 члену объединения u_var с помощью указателя. */ } Поскольку объединения позволяют вашей программе интерпретировать одни и те же данные по-разному, они часто используются в случаях, когда требуется необычное преобразование типов. Например, следующая программа использует объединение для перестановки двух байтов, которые составляют короткое целочисленное значение. Здесь для отображения содержимого целочисленных переменных используется функция disp_binary(), разработанная в главе 9. (Эта программа написана в предположении, что короткие целочисленные значения имеют длину два байта.) // Использование объединения для перестановки двух байтов в рамках короткого целочисленного значения. #include using namespace std; void disp_binary(unsigned u); union swap_bytes { short int num; char ch[2]; }; int main() { swap_bytes sb; char temp; sb.num = 15; // двоичный код: 0000 0000 0000 1111 cout << "Исходные байты: "; disp_binary(sb.ch[1]); cout << " "; disp_binary(sb.ch[0]); cout << "\n\n"; // Обмен байтов. temp = sb.ch[0]; sb.ch[0] = sb.ch[1]; sb.ch[1] = temp; cout << "Байты после перестановки: "; disp_binary(sb.ch[1]); cout << " "; disp_binary(sb.ch[0]); cout << "\n\n"; return 0; } // Отображение битов, составляющих байт. void disp_binary(unsigned u) { register int t; for(t=128; t>0; t=t/2) if(u & t) cout << "1 "; else cout << "0 "; } При выполнении программа генерирует такие результаты. Исходные байты: 0000 0000 0000 1111 Байты после перестановки: 0000 1111 0000 0000 В этой программе целочисленной переменной sb.num присваивается число 15. Перестановка двух байтов, составляющих это значение, выполняется путем обмена двух символов, которые образуют массив ch. В результате старший и младший байты целочисленной переменной num меняются местами. Эта операция возможна лишь потому, что как переменная num, так и массив ch разделяют одну и ту же область памяти. В следующей программе демонстрируется еще один пример использования объединения. Здесь объединения связываются с битовыми полями, используемыми для отображения в двоичной системе счисления ASCII-кода, генерируемого при нажатии любой клавиши. Эта программа также демонстрирует альтернативный способ отображения отдельных битов, составляющих байт. Объединение позволяет присвоить значение нажатой клавиши символьной переменной, а битовые поля используются для отображения отдельных битов. // Отображение ASCII-кода символов в двоичной системе счисления. #include #include using namespace std; // Битовые поля, которые будут расшифрованы. struct byte { unsigned a : 1; unsigned b : 1; unsigned с : 1; unsigned d : 1; unsigned e : 1; unsigned f : 1; unsigned g : 1; unsigned h : 1; }; union bits { char ch; struct byte bit; }ascii; void disp_bits(bits b); int main() { do { cin >> ascii.ch; cout << ":"; disp_bits(ascii); }while(ascii.ch!='q'); // Выход при вводе буквы "q". return 0; } // Отображение конфигурации битов для каждого символа. void disp_bits(bits b) { if(b.bit.h) cout << "1"; else cout << "0"; if(b.bit.g) cout << "1"; else cout << "0"; if(b.bit.f) cout << "1"; else cout << "0 "; if(b.bit.e) cout << "1"; else cout << "0"; if(b.bit.d) cout << "1"; else cout << "0"; if(b.bit.c) cout << "1"; else cout << "0"; if(b.bit.b) cout << "1"; else cout << "0"; if(b.bit.a) cout << "1"; else cout << "0"; cout << "\n"; } Вот как выглядит один из возможных вариантов выполнения этой программы. а: 0 1 1 0 0 0 0 1 b: 0 1 1 0 0 0 1 0 с: 0 1 1 0 0 0 1 1 d: 0 1 1 0 0 1 0 0 е: 0 1 1 0 0 1 0 1 f: 0 1 1 0 0 1 1 0 g: 0 1 1 0 0 1 1 1 h: 0 1 1 0 1 0 0 0 i: 0 1 1 0 1 0 0 1 j: 0 1 1 0 1 0 1 0 k: 0 1 1 0 1 0 1 1 1: 0 1 1 0 1 1 0 0 m: 0 1 1 0 1 1 0 1 n: 0 1 1 0 1 1 1 0 o: 0 1 1 0 1 1 1 1 p: 0 1 1 1 0 0 0 0 q: 0 1 1 1 0 0 0 1 Важно! Поскольку объединение предполагает, что несколько переменных разделяют одну и ту же область памяти, это средство предоставляет программисту возможность хранить информацию, которая (в зависимости от ситуации) может содержать различные типы данных, и получать доступ к этой информации. По сути, объединения обеспечивают низкоуровневую поддержку принципов полиморфизма. Другими словами, объединение обеспечивает единый интерфейс для нескольких различных типов данных, воплощая таким образом концепцию "один интерфейс — множество методов" в своей самой простой форме. Анонимные объединения Анонимные объединения позволяют объявлять переменные, которые разделяют одну и ту же область памяти. В C++ предусмотрен специальный тип объединения, который называется анонимным. Анонимное объединение не имеет наименования типа, и поэтому объект такого объединения объявить невозможно. Но анонимное объединение сообщает компилятору о том, что его члены разделяют одну и ту же область памяти. При этом обращение к самим переменным объединения происходит непосредственно, без использования оператора "точка". Рассмотрим такой пример. //Демонстрация использования анонимного объединения. #include using namespace std; int main() { // Это анонимное объединение. union { short int count; char ch[2]; }; // Вот как происходит непосредственное обращение к членам анонимного объединения. ch[0] = 'X'; ch[1] = 'Y'; cout << "Объединение в виде символов: " << ch[0] < cout << "Объединение в виде целого значения: " < return 0; } Эта программа отображает следующий результат. Объединение в виде символов: XY Объединение в виде целого значения: 22872 Число 22872 получено в результате помещения символов X и Y в младший и старший байты переменной count соответственно. Как видите, к обеим переменным, входящим в состав объединения, как count, так и ch, можно получить доступ так же, как к обычным переменным, а не как к составляющим объединения. Несмотря на то что они объявлены как часть анонимного объединения, их имена находятся на том же уровне области видимости, что и другие локальные переменные, объявленные на уровне объединения. Таким образом, член анонимного объединения не может иметь имя, совпадающее с именем любой другой переменной, объявленной в той же области видимости. Анонимное объединение представляет собой средство, с помощью которого программист может сообщить компилятору о своем намерении, чтобы две (или больше) переменные разделяли одну и ту же область памяти. За исключением этого момента, члены анонимного объединения ведут себя подобно любым другим переменным. Использование оператора sizeof для гарантии переносимости программного кода Как было показано, структуры и объединения создают объекты различных размеров, которые зависят от размеров и количества их членов. Более того, размеры таких встроенных типов, как int, могут изменяться при переходе от одного компьютера к другому. Иногда компилятор заполняет структуру или объединение так, чтобы выровнять их по границе четного слова или абзаца. (Абзац содержит 16 байт.) Поэтому, если в программе нужно определить размер (в байтах) структуры или объединения, используйте оператор sizeof. Не пытайтесь вручную выполнять сложение отдельных членов. Из-за заполнения или иных аппаратно-зависимых факторов размер структуры или объединения может оказаться больше суммы размеров отдельных их членов. И еще. Объединение всегда будет занимать область памяти, достаточную для хранения его самого большого члена. Рассмотрим пример. union х { char ch; int i; double f; } u_var; Здесь при выполнении оператора sizeof u_var получим результат 8 (при условии, что double-значение занимает 8 байт). Во время выполнения программы не имеет значения, что реально будет храниться в переменной u_var; здесь важен размер самой большой переменной, входящей в состав объединения, поскольку объединение должно иметь размер самого большого его элемента. Переходим к объектно-ориентированному программированию Эта глава заключает описание не объектно-ориентированных атрибутов C++. Начиная со следующей главы, мы будем рассматривать средства, которые поддерживают объектно- ориентированное программирование (Object Oriented Programming— OOP), или ООП. Чтобы понять объектно-ориентированные средства C++ и научиться их эффективно применять, необходимо глубокое понимание материала этой и предыдущих девяти глав. Поэтому, возможно, вам стоит повторить пройденный материал. Особое внимание при повторении уделите указателям, структурам, функциям и перегрузке функций. |