С++: пишем пример на структуру, битовые поля, перечисление и объединение
Теорию о составных типах данных в C/C++ можно почитать здесь, а заметка преследует скромную цель разработки законченного приложения-примера, в котором "органично" использовались бы все основные структурные типы - собственно структура с демонстрацией вложения других структурных типов, структура с битовыми полями, перечисление, объединение.
Задача будет такой: описать структурный тип "Персона" с полями "Имя" (память под поле выделяется динамически), "Социальный статус" (со значениями "не определён", "иждивенец", "учащийся", "работающий", "пенсионер"), серия и номер паспорта, дата рождения. Прочитать из текстового файла массив структур указанной размерности, проиллюстрировать добавление новой записи в файл.
Начнём определять типы для нашей будущей структуры. "Социальный статус" здесь прямо-таки напрашивается стать перечислением:
typedef enum status { undefined, dependent, pupil, working, pensioner };
Логично, что "неопределённый" статус будет соответствовать нулевому значению, а остальные статусы упорядочены по возрасту.
К сожалению, enum
всегда интерпретируется как int
и "строковое" значение именованной константы вывести просто так нельзя. Чтобы это всё-таки сделать, нам придётся написать дополнительную функцию, в которой те же самые социальные статусы будут перечислены в строковом массиве:
char *get_status (status s) { char *status_strings[] = { "undefined", "dependent", "pupil", "working", "pensioner" }; return status_strings[s]; }
Заметим, что при нарушении "естественного" порядка нумерации 0,1,2,... в перечислении придётся изменить и логику этой функции.
Показанный подход к получению строковых "имён" констант перечисления очень примитивен, но альтернативой было бы довольно трудоёмкое использование макросов или написание отдельных классов для решения задачи конвертирования перечислений в их строковые аналоги.
Номер паспорта состоит из 2 частей - 4-значная серия и 6-значный номер, опишем его отдельной структурой, не забыв, что 6-значное число в тип int
может "не влезть", если он 2-байтовый (хотя сегодня такого уже и не бывает):
typedef struct passport { unsigned short int series; unsigned long int number; };
Для даты рождения не будем создавать 3 отдельных поля - лучше упакуем её в 2 байта с помощью битовых полей. День месяца может принимать значения от 1 до 31, так что на него понадобится 5 бит, 4 бита займёт месяц со значениями от 1 до 12, оставшиеся 7 бит (при условии, что размер целого - 2 байта) позволят закодировать 27=128 разных лет. Поэтому примем какой-либо год нашей эры за начало отсчёта, скажем, 1970-й - начало "эпохи Unix". Самые старшие биты займёт год, затем месяц, затем день - это гарантирует, что большее число всегда будет соответствовать более поздней дате:
typedef struct date { unsigned short int year :7; //Год 1970=0...до 2098 unsigned short int month :4; unsigned short int day :5; };
Возможно, дата понадобится нам не только как совокупность битовых полей, но и как 2-байтовое целое число, здесь как раз удобным решением окажется объединение, позволяющее интерпретировать одни и те же данные двумя способами:
union udate { date d; unsigned short int u; };
Наконец, структурный тип "персона" будет объединять введённые ранее типы данных:
typedef struct person { char *name; //если сразу name[30] - не пришлось бы выделять память под name status status; passport passport; date date; };
Все наши ключевые слова "typedef
", вообще говоря, на C++ можно и не писать, определение структурного типа происходит автоматически. Но мы всё равно напишем для сохранения совместимости с классическим Си.
Код потребует подключения следующих стандартных библиотек:
#include <stdio.h> #include <stdlib.h> #include <string.h>
Напишем функцию-обработчик ошибок, принимающую параметр "сообщение об ошибке" (msg
):
void error (char *msg) { puts ("\n"); puts (msg); puts ("\nEnter to EXIT"); fflush (stdin); getchar(); exit(0); }
Все остальные действия демо-программы поместим непосредственно в функцию main
, вот она с комментариями:
void main () { const int maxrecords=5; //Максимальное число записей person p[maxrecords]; //Массив структур - наши записи о людях char buf[128],name[40]; //Буфер для чтения строки базы и отдельно - имени, т.к. память будет выделяться FILE *f=fopen ("data.txt","r+t"); //"Базой" будет текстовый файл data.txt из текущей папки if (f==NULL) error("Can't open data.txt"); //Если не удалось открыть - выходим int count=0; //Счётчик записей while (1) { //Так лучше при чтении тектового файла под Windows без Ctrl+Z в конце fgets (buf,128,f); //Читаем сразу строку sscanf (buf,"%s %d %d %ld %u",name,&p[count].status,&p[count].passport.series,&p[count].passport.number,&p[count].date); //И разбираем её по полям структуры p[count].name = new char[strlen(name)+1]; //Выделяем память под поле "имя" if (p[count].name==NULL) { //Если не удалось выделить память - выходим sprintf (buf,"No memory for rec. no. %d",count+1); error (buf); } strcpy(p[count].name,name); //Переписываем в поле "имя" из буфера count++; if (feof(f) || count>maxrecords-1) break; //Если кончились записи или достигнут предел - прервать } //Допишем в файл 1 запись, если нет последней записи с именем Newname if (count && strcmp(p[count-1].name,"Newname")) { udate d = { 2013-1970, 12, 31 }; fseek (f, 0, SEEK_END); fprintf (f, "\n%s %d %d %ld %u","Newname",1,5004,456789,d.u); } fclose (f); //Напечать прочитанный из файла массив записей p for (int i=0; i<count; i++) printf ("\n%d. %s, %s, %04u %06lu, %02d.%02d.%d", i+1, p[i].name, get_status(p[i].status),p[i].passport.series,p[i].passport.number,p[i].date.day, p[i].date.month, p[i].date.year+1970); error (""); }
Файл с данными data.txt
находится в той же папке, что и приложение, он может иметь такой вид:
Ivanov 1 5004 096345 10000 Petrov 2 5001 233245 20000 Sidorov 0 5111 233456 30000
Нашему решению не хватает возможности преобразования "сжатых" дат в числа и обратно, напишем соответствующее решение отдельной демо-программой, функции date_to_int
и int_to_date
дают нужные преобразования формата:
#include <iostream> using namespace std; typedef struct date { unsigned short int year :7; unsigned short int month :4; unsigned short int day :5; }; unsigned short int date_to_int (date d) { return ((d.year<<9)|(d.month<<5)|(d.day&0x001F)); } date int_to_date (unsigned short int n) { date d; d.year=(n&0xFE00)>>9; d.month=(n&0x01E0)>>5; d.day=n&0x001F; return d; } void main () { date d = { 2013-1970, 12, 31 }; cout << "\n" << date_to_int(d); d=int_to_date (22431); cout << "\n" << d.day << "." << d.month << "." << d.year+1970; cin.get(); }
Скачать файлы примера в арихве .zip (2 Кб)
Ниже код обеих программ объединён в одну с использованием только средств C++ (проверено в актуальной сборке Visual Studio 2019).
#include <iostream> #include <fstream> #include <cstdlib> #include <string> #include <clocale> #include <Windows.h> enum status { undefined, dependent, pupil, working, pensioner }; const char* get_status(const status s) { const char* status_strings[] = { "undefined", "dependent", "pupil", "working", "pensioner" }; return status_strings[s]; } struct passport { unsigned short int series; unsigned long int number; }; struct date { unsigned short int year : 7; //Год 1970=0...до 2098 unsigned short int month : 4; unsigned short int day : 5; }; union udate { date d; unsigned short int u; }; struct person { std::string name; status status; passport passport; udate date; }; void error(const char* msg, const int n) { std::cout << std::endl << msg << ", line number = " << n << std::endl << "Press Enter to EXIT"; std::cin.get(); exit(0); } size_t nsubstr(const std::string& str, const std::string& substr) { //Количество вхождений подстроки substr в строку str size_t pos = 0, cnt = 0; while ((pos = str.find(substr, pos)) != std::string::npos) { pos++; cnt++; } return cnt; } bool read_int(std::string& str, const std::string delim, int& data) { //Чтение из строки str очередного целого data с удалением //символов и разделителя delim из прочитанного try { size_t pos = str.find(delim, 0); if (pos == std::string::npos) return false; data = std::stoi(str); str = str.substr(pos + delim.length()); } catch (...) { return false; } return true; } unsigned short int date_to_int(date d) { return ((d.year << 9) | (d.month << 5) | (d.day & 0x001F)); } date int_to_date(unsigned short int n) { date d; d.year = (n & 0xFE00) >> 9; d.month = (n & 0x01E0) >> 5; d.day = n & 0x001F; return d; } int main() { setlocale(LC_ALL, ".1251"); SetConsoleCP(1251); SetConsoleOutputCP(1251); const int maxrecords = 5; //Максимальное число записей person p[maxrecords]; //Массив структур - наши записи о людях std::string buf; //Буфер для чтения строки базы std::fstream f("data.txt"); //"Базой" будет текстовый файл data.txt из текущей папки if (!f) error("Can't open data.txt", 0); //Если не удалось открыть - выходим int count = 0; //Счётчик записей std::string delim("\t"); //Разделитель полей записей, не обязательно односимвольный while (1) { //Так лучше при чтении текстового файла под Windows без Ctrl+Z в конце std::getline(f, buf); //Читаем сразу строку if (nsubstr(buf, delim) != 4) error("Bad data format", count + 1); size_t pos = buf.find(delim, 0); p[count].name = buf.substr(0, pos); buf = buf.substr(pos + delim.length()) + delim; //Оставили в строке часть после имени и добавили разделитель //для последующего разбора int tmp; //Временная переменная для извлечения целого //Чтение остальных полей с контролем данных: if (!read_int(buf, delim, tmp) || tmp < 0 || tmp>4) error("Bad status", count + 1); p[count].status = static_cast<status>(tmp); if (!read_int(buf, delim, tmp) || tmp < 1000) error("Bad passport series", count + 1); p[count].passport.series = (unsigned int)tmp; if (!read_int(buf, delim, tmp) || tmp < 100000) error("Bad passport number", count + 1); p[count].passport.number = (unsigned long int)tmp; if (!read_int(buf, delim, tmp) || tmp < 0 || tmp > 0xffff) error("Bad birthday date", count + 1); p[count].date.u = (unsigned short int)tmp; count++; if (f.eof() || count > maxrecords - 1) break; //Если кончились записи или достигнут предел - прервать } //Допишем в файл 1 запись, если нет последней записи с именем Newname if (count && p[count - 1].name != "Newname") { udate d = { 1973 - 1970, 2, 1 }; f.seekg(0, f.end); f << std::endl << "Newname" << delim << 1 << delim << 5004 << delim << 456789 << delim << d.u; } f.close(); //Напечать прочитанный из файла массив записей p for (size_t i = 0; i < count; i++) std::cout << i + 1 << ". " << p[i].name << ", " << get_status(p[i].status) << ", " << p[i].passport.series << " " << p[i].passport.number << ", " << p[i].date.d.day << "." << p[i].date.d.month << "." << p[i].date.d.year + 1970 << std::endl; //Ищем самого молодого и самого старого студента, пользуясь битовым представлением size_t min = date_to_int(p[0].date.d), max = min; for (size_t i = 1; i < count; i++) { size_t d = date_to_int(p[i].date.d); if (d < min) min = d; if (d > max) max = d; } date d = int_to_date(min); std::cout << std::endl << "Min date=" << d.day << "." << d.month << "." << d.year + 1970; d = int_to_date(max); std::cout << std::endl << "Max date=" << d.day << "." << d.month << "." << d.year + 1970; return 0; }
Файл с данными data.txt
может быть, например, таким (с разделителем-табуляцией между полями):
Иванова Маша 1 5004 123456 9744 Петров Вова 2 5016 456789 20000
10.11.2013, 16:23 [14052 просмотра]