БлогNot. С++: пишем пример на структуру, битовые поля, перечисление и объединение

С++: пишем пример на структуру, битовые поля, перечисление и объединение

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

Задача будет такой: описать структурный тип "Персона" с полями "Имя" (память под поле выделяется динамически), "Социальный статус" (со значениями "не определён", "иждивенец", "учащийся", "работающий", "пенсионер"), серия и номер паспорта, дата рождения. Прочитать из текстового файла массив структур указанной размерности, проиллюстрировать добавление новой записи в файл.

Начнём определять типы для нашей будущей структуры. "Социальный статус" здесь прямо-таки напрашивается стать перечислением:

enum status { undefined, dependent, pupil, working, pensioner };

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

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

const char* get_status (const status s) {
 const char* status_strings[] = {
 "undefined", "dependent", "pupil", "working", "pensioner"
 };
 return status_strings[s];
}

Заметим, что при нарушении "естественного" порядка нумерации 0,1,2,... в перечислении придётся изменить и логику этой функции.

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

Номер паспорта состоит из 2 частей - 4-значная серия и 6-значный номер, опишем его отдельной структурой, не забыв, что 6-значное число в тип int может "не влезть", если он 2-байтовый (хотя сегодня такого уже и не бывает):

struct passport {
 unsigned short int series;
 unsigned long int number;
};

Для даты рождения не будем создавать 3 отдельных поля - лучше упакуем её в 2 байта с помощью битовых полей. День месяца может принимать значения от 1 до 31, так что на него понадобится 5 бит, 4 бита займёт месяц со значениями от 1 до 12, оставшиеся 7 бит (при условии, что размер целого - 2 байта) позволят закодировать 27=128 разных лет. Поэтому примем какой-либо год нашей эры за начало отсчёта, скажем, 1970-й - начало "эпохи Unix". Самые старшие биты займёт год, затем месяц, затем день - это гарантирует, что большее число всегда будет соответствовать более поздней дате:

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;
};

Наконец, структурный тип "персона" будет объединять введённые ранее типы данных:

struct person {
 std::string name;
 status status;
 passport passport;
 udate date;
};

Код потребует подключения следующих стандартных библиотек:

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
#include <clocale>
#include <Windows.h>

Напишем функцию-обработчик ошибок, принимающую параметр "сообщение об ошибке" (msg):

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;
}

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;

 //Ищем самого молодого и самого старого студента, сравнивая даты как числа
 udate min = p[0].date, max = min;
 for (unsigned short int i = 1; i < count; i++) {
  udate d = p[i].date;
  if (d.u < min.u) min.u = d.u;
  if (d.u > max.u) max.u = d.u;
 }
 std::cout << std::endl << "Min date=" << min.d.day << "." << 
  min.d.month << "." << (min.d.year + 1970);
 std::cout << std::endl << "Max date=" << max.d.day << "." << 
 max.d.month << "." << (max.d.year + 1970);
 return 0;
}

Файл с данными data.txt находится в той же папке, что и приложение, он может иметь такой вид:

Иванова Маша	1	5004	123456	9744
Петров Вова	2	5016	456789	20000

Разделителем служит символ табуляции '\t' между полями.

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

Таким образом, мы обошлись без манипуляций с отдельными битами объединения udate, но при необходимости можно написать методы для "ручного" извлечения года, месяца и дня из упакованной даты и обратного преобразования даты в упакованный формат. Этим методам придётся использовать операции побитового сдвига и побитовые логические "и", "или":

#include <iostream>
#include <iomanip>

struct date {
 unsigned short int year : 7; //Год 1970=0...до 2098
 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;
}

int main () {
 date d = {2024 - 1970, 10, 4}; //исходная дата
 unsigned short int u = date_to_int(d);
 std::cout << u << std::endl;
 date n = int_to_date (u);
 std::cout << std::setfill ('0') << 
  std::setw (2) << n.day << "." <<
  std::setw (2) << n.month << "." << 
  std::setw (4) << (n.year + 1970);
 return 0;
}

10.11.2013, 16:23 [14305 просмотров]


теги: c++ программирование форматы

К этой статье пока нет комментариев, Ваш будет первым