8.4 – Структуры

Добавлено 3 июня 2021 в 15:41

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

std::string myName{};
int myBirthYear{};
int myBirthMonth{};
int myBirthDay{};
int myHeightInches{};
int myWeightPounds{};

Однако теперь у вас есть 6 независимых переменных, которые никак не сгруппированы. Если вы хотите передать информацию о себе в функцию, вам придется передавать каждую переменную отдельно. Кроме того, если вы хотите сохранить информацию о ком-то еще, то для каждого дополнительного человека вам нужно будет объявить еще 6 переменных! Как видите, это может быстро выйти из-под контроля.

К счастью, C++ позволяет нам создавать собственные пользовательские агрегированные типы данных. Агрегированный тип данных – это тип данных, который группирует вместе несколько отдельных переменных. Одним из простейших агрегированных типов данных является структура. Структура (сокращенно struct) позволяет нам группировать переменные смешанных типов данных вместе в единое целое.

Объявление и определение структур

Поскольку структуры определяются пользователем, мы сначала должны сообщить компилятору, как выглядит наша структура, прежде чем мы сможем начать ее использовать. Для этого мы объявляем нашу структуру с помощью ключевого слова struct. Вот пример объявления структуры:

struct Employee
{
    int id{};
    int age{};
    double wage{};
};

Это сообщает компилятору, что мы определяем структуру с именем Employee. Структура Employee содержит внутри 3 переменных: int с именем id, int с именем age и double с именем wage. Эти переменные, которые являются частью структуры, называются членами (или полями). Имейте в виду, что Employee – это просто объявление – даже несмотря на то, что мы сообщаем компилятору, что структура будет иметь переменные-члены, в это время память не выделяется. По соглашению имена структур начинаются с заглавной буквы, чтобы отличать их от имен переменных.

Предупреждение


Одна из самых простых ошибок C++ – это забыть о точке с запятой в конце объявления структуры. Это вызовет ошибку компилятора в следующей строке кода. Современные компиляторы, такие как Visual Studio 2010, укажут вам, что вы, возможно, забыли точку с запятой, но более старые или менее сложные компиляторы могут этого не делать, что может затруднить обнаружение реальной ошибки.

Чтобы использовать структуру Employee, мы просто объявляем переменную типа Employee:

Employee joe{}; // структура Employee пишется с заглавной буквы, а переменная joe - нет 

Это определяет переменную типа Employee с именем joe. Как и в случае с обычными переменными, определение переменной типа структуры выделяет память для этой переменной.

Можно определить несколько переменных одного и того же типа структуры:

Employee joe{};   // создаем структуру Employee для Джо
Employee frank{}; // создаем структуру Employee для Фрэнка

Доступ к членам структуры

Когда мы определяем переменную, такую ​​как Employee joe, joe относится ко всей структуре (которая содержит переменные-члены). Чтобы получить доступ к отдельным членам, мы используем оператор выбора члена (который представляет собой точку). Вот пример использования оператора выбора члена для инициализации каждой переменной-члена:

Employee joe{};   // создаем структуру Employee для Джо
joe.id = 14;      // присваиваем значение члену id в структуре joe
joe.age = 32;     // присваиваем значение члену age в структуре joe
joe.wage = 24.15; // присваиваем значение члену wage в структуре joe
 
Employee frank{};   // создаем структуру Employee для Фрэнка
frank.id = 15;      // присваиваем значение члену id в структуреt frank
frank.age = 28;     // присваиваем значение члену age в структуре frank
frank.wage = 18.27; // присваиваем значение члену wage в структуре frank

Как и обычные переменные, переменные-члены структуры не инициализируются и обычно содержат мусор. Мы должны инициализировать их вручную.

В приведенном выше примере очень легко определить, какие переменные-члены принадлежат Джо, а какие – Фрэнку. Это обеспечивает гораздо более высокий уровень организации, чем отдельные переменные. Более того, поскольку переменные-члены Джо и Фрэнка имеют одинаковые имена, это обеспечивает согласованность для нескольких переменных одного и того же типа структуры.

Переменные-члены структуры действуют как обычные переменные, поэтому с ними можно выполнять обычные операции:

int totalAge{ joe.age + frank.age };
 
if (joe.wage > frank.wage)
    std::cout << "Joe makes more than Frank\n";
else if (joe.wage < frank.wage)
    std::cout << "Joe makes less than Frank\n";
else
    std::cout << "Joe and Frank make the same amount\n";
 
// Фрэнк получил повышение
frank.wage += 2.50;
 
// Сегодня день рождения Джо
++joe.age; // использовать префиксный инкремент, чтобы увеличить возраст Джо на 1

Инициализация структур

Инициализация структур путем присвоения значений каждому члену поочередно может быть немного утомительной, поэтому C++ поддерживает более быстрый способ инициализации структур с использованием списка инициализаторов. Это позволяет вам во время объявления инициализировать некоторые или все члены структуры.

struct Employee
{
    int id{};
    int age{};
    double wage{};
};
 
// joe.id = 1, joe.age = 32, joe.wage = 60000.0
Employee joe{ 1, 32, 60000.0 }; 

// frank.id = 2, frank.age = 28, frank.wage = 0.0 (инициализация по умолчанию)
Employee frank{ 2, 28 };

Если список инициализаторов не содержит инициализатора для некоторых элементов, эти элементы инициализируются значением по умолчанию (которое обычно соответствует нулевому состоянию для этого типа). В приведенном выше примере мы видим, что frank.wage по умолчанию инициализируется значением 0.0, поскольку мы не указали для него явное значение инициализации.

Инициализация нестатического члена

Нестатическим (обычным) членам структуры можно дать значение по умолчанию:

struct Rectangle
{
    double length{ 1.0 };
    double width{ 1.0 };
};
 
int main()
{
    Rectangle x{}; // length = 1.0, width = 1.0
 
    x.length = 2.0; // вы можете присваивать другие значения, как обычно
 
    return 0;
}

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

struct Rectangle
{
    double length{ 1.0 };
    double width{ 1.0 };
};
 
int main()
{
    Rectangle x{ 2.0, 2.0 };
 
    return 0;
}

В приведенном выше примере Rectangle x будет инициализирован с длиной и шириной 2,0.

О том, что такое статические члены, мы поговорим в следующей главе. А пока не беспокойтесь о них.

Присваивание структурам

Если мы хотим присвоить значения членам структур, мы можем сделать это по отдельности:

struct Employee
{
    int id{};
    int age{};
    double wage{};
};
 
Employee joe{};
joe.id = 1;
joe.age = 32;
joe.wage = 60000.0;

Если вы хотите присвоить новые значения всем членам, это будет утомительно, особенно для структур с большим количеством членов. Присваивать значения членам структур вы можете так же, используя список инициализаторов:

struct Employee
{
    int id{};
    int age{};
    double wage{};
};
 
Employee joe{};
 
joe = { 1, 32, 60000.0 };
// То же, что и
joe = Employee{ 1, 32, 60000.0 };
 
// Также возможно скопировать все члены из одной переменной в другую
Employee emma{ joe };

Структуры и функции

Большим преимуществом использования структур вместо отдельных переменных является то, что мы можем передать всю структуру в функцию, которая должна работать с ее членами:

#include <iostream>
 
struct Employee
{
    int id{};
    int age{};
    double wage{};
};
 
void printInformation(Employee employee)
{
    std::cout << "ID:   " << employee.id << '\n';
    std::cout << "Age:  " << employee.age << '\n';
    std::cout << "Wage: " << employee.wage << '\n';
}
 
int main()
{
    Employee joe { 14, 32, 24.15 };
    Employee frank { 15, 28, 18.27 };
 
    // Распечатать информацию о Джо
    printInformation(joe);
 
    std::cout << '\n';
 
    // Распечатать информацию о Фрэнке
    printInformation(frank);
 
    return 0;
}

В приведенном выше примере мы передаем всю структуру Employee в printInformation() (по значению, что означает, что аргумент копируется в параметр). Это избавляет нас от необходимости передавать каждую переменную отдельно. Более того, если мы когда-нибудь решим добавить новые члены в нашу структуру Employee, нам не придется изменять объявление или вызов функции!

Приведенная выше программа выводит:

ID:   14
Age:  32
Wage: 24.15

ID:   15
Age:  28
Wage: 18.27

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

#include <iostream>
 
struct Point3d
{
    double x{};
    double y{};
    double z{};
};
 
Point3d getZeroPoint()
{
    // Мы можем создать переменную и вернуть ее.
    Point3d temp { 0.0, 0.0, 0.0 };
    return temp;
}
 
Point3d getZeroPoint2()
{
    // Мы можем вернуться напрямую. Мы уже указали тип
    // при объявлении функции (Point3d), поэтому нам не нужно
    // делать это здесь снова.
    return { 0.0, 0.0, 0.0 };
}
 
Point3d getZeroPoint3()
{
    // Мы можем использовать пустые фигурные скобки для
    // инициализации нулями всех членов 'Point3d'.
    return {};
}
 
int main()
{
    Point3d zero{ getZeroPoint() };
 
    if (zero.x == 0.0 && zero.y == 0.0 && zero.z == 0.0)
        std::cout << "The point is zero\n";
    else
        std::cout << "The point is not zero\n";
 
    return 0;
}

Эта программа печатает:

The point is zero

Вложенные структуры

Структуры могут содержать другие структуры. Например:

struct Employee
{
    int id{};
    int age{};
    double wage{};
};
 
struct Company
{
    Employee CEO{}; // Employee (сотрудник) - структура внутри структуры Company (компания)
    int numberOfEmployees{};
};
 
Company myCompany;

В этом случае, если мы хотим узнать, какова зарплата генерального директора (CEO), мы просто дважды используем оператор выбора члена: myCompany.CEO.wage;

Это выбирает член CEO из myCompany, а затем выбирает член wage из CEO.

Для вложенных структур вы можете использовать вложенные списки инициализаторов:

struct Employee
{
    int id;
    int age;
    double wage;
};
 
struct Company
{
    Employee CEO; // Employee - это структура внутри структуры struct
    int numberOfEmployees;
};
 
Company myCompany{{ 1, 42, 60000.0 }, 5 };

Размер структуры и выравнивание структуры данных

Обычно размер структуры – это сумма размеров всех ее членов, но не всегда!

Рассмотрим структуру Employee, но с целочисленными типами фиксированного размера и размером id, равным половине размера age. На многих платформах размер double составляет 8 байтов, поэтому мы ожидаем, что Employee будет 2 + 4 + 8 = 14 байтов. Чтобы узнать точный размер Employee, мы можем использовать оператор sizeof:

#include <cstdint>
#include <iostream>
 
struct Employee
{
    // Целочисленные типы фиксированной ширины мы используем для примера.
    // Избегайте их в реальном коде.
    std::int16_t id{};
    std::int32_t age{};
    double wage{};
};
 
int main()
{
    std::cout << "The size of a double is " << sizeof(double) << '\n';
    std::cout << "The size of Employee is " << sizeof(Employee) << '\n';
 
    return 0;
}

На машине автора эта программа печатает:

The size of a double is 8
The size of Employee is 16

Оказывается, мы можем сказать только то, что размер структуры будет не меньше размера всех содержащихся в ней переменных. Но может быть и больше! По соображениям производительности компилятор иногда добавляет разрывы в структуры (это называется заполнением).

В приведенной выше структуре Employee компилятор невидимо добавляет 2 байта заполнения после члена id, делая размер структуры равным 16 байтов вместо 14. Причина, по которой он это делает, выходит за рамки этого руководства, но читатели, которые хотят узнать подробнее о выравнивании структур, данных могут прочитать об этом в Википедии. Это необязательный материал и не требуется для понимания структур или C++!

Доступ к структурам в нескольких файлах

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

Переменные структур подчиняются тем же правилам, что и обычные переменные. Следовательно, чтобы сделать переменную структуры доступной для нескольких файлов, вы можете использовать ключевое слово extern в объявлении в заголовке и определить переменную в исходном файле.

Заключительные примечания по структурам

Структуры в C++ очень важны, поскольку понимание структур – первый важный шаг к объектно-ориентированному программированию! Позже в этой серии обучающих статей вы узнаете о другом агрегированном типе данных, называемом классом, который построен на основе структур. Хорошее понимание структур поможет значительно упростить переход к классам.

Структуры, представленные в этом уроке, иногда называют простыми старыми структурами данных (или структурами POD, plain old data struct), поскольку все члены являются членами данных (переменными). В будущем (когда мы будем обсуждать классы) мы поговорим о других типах членов.

Небольшой тест

Вопрос 1

У вас есть веб-сайт, и вы пытаетесь отслеживать, сколько денег вы зарабатываете в день на рекламе. Объявите структуру advertising (реклама), которая отслеживает, сколько объявлений вы показали читателям, на какой процент объявлений нажимали пользователи, и сколько вы в среднем зарабатывали на каждом клике по объявлению. Значения для каждого из этих полей заполните данными, вводимыми пользователем. Передайте структуру advertising функции, которая печатает каждое из значений, а затем вычисляет, сколько вы заработали за этот день (умножьте все 3 поля вместе).

#include <iostream>
 
// Сначала нам нужно определить нашу структуру Advertising
struct Advertising
{
    int adsShown{};
    double clickThroughRatePercentage{};
    double averageEarningsPerClick{};
};
 
Advertising getAdvertising()
{
    Advertising temp{};
    std::cout << "How many ads were shown today? ";
    std::cin >> temp.adsShown;
    std::cout << "What percentage of ads were clicked on by users? ";
    std::cin >> temp.clickThroughRatePercentage;
    std::cout << "What was the average earnings per click? ";
    std::cin >> temp.averageEarningsPerClick;
    return temp;
}
 
void printAdvertising(Advertising ad)
{
    std::cout << "Number of ads shown: " << ad.adsShown << '\n';
    std::cout << "Click through rate: " << ad.clickThroughRatePercentage << '\n';
    std::cout << "Average earnings per click: $" << ad.averageEarningsPerClick << '\n';
 
    // Следующая строка разделена, чтобы уменьшить длину
    // Нам нужно разделить ad.clickThroughRatePercentage на 100,
    // потому что это процент от 100, а не множитель
    std::cout << "Total Earnings: $" <<
        (ad.adsShown * ad.clickThroughRatePercentage / 100 * ad.averageEarningsPerClick) << '\n';
}
 
int main()
{
    // Объявление переменной структуры Advertising
    Advertising ad{ getAdvertising() };
    printAdvertising(ad);
 
    return 0;
}

Вопрос 2

Создайте структуру для хранения дроби. Структура должна иметь целочисленный числитель и целочисленный знаменатель. Объявите 2 переменные дробей и заполните их данными, введенными пользователем. Напишите функцию под названием multiply, которая берет обе дроби, умножает их вместе и возвращает результат в виде числа с плавающей запятой. Вам не нужно сокращать дробь до наименьшего значения. Выведите результат умножения двух переменных дробей.

#include <iostream>
 
struct Fraction
{
    int numerator{};
    int denominator{};
};
 
Fraction getFraction()
{
    Fraction temp{};
    std::cout << "Enter a value for numerator: ";
    std::cin >> temp.numerator;
    std::cout << "Enter a value for denominator: ";
    std::cin >> temp.denominator;
    std::cout << '\n';
    return temp;
}
 
double multiply(Fraction f1, Fraction f2)
{
    // Не забудьте приведение static_cast, иначе компилятор будет выполнять целочисленное деление!
    return (static_cast<double>(f1.numerator * f2.numerator) / (f1.denominator * f2.denominator));
}
 
int main()
{
    // Создаем нашу первую дробь
    const Fraction f1{ getFraction() };
    const Fraction f2{ getFraction() };
 
    const double result{ multiply(f1, f2) };
    
    std::cout << result << '\n';
 
    return 0;
}

Теги

C++ / CppLearnCppstructДля начинающихОбучениеПрограммированиеСтруктураТипы данных

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.