Экзамен по дисциплине
«Программирование на языке высокого уровня»

1. Структурные типы данных и переменные этих типов

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

struct point {
    int x;
    int y;
};

При описании самого типа память не выделяется – она резервируется только при объявлении переменной этого типа:

struct point pt;  // здесь для полей x и y выделяется память;

Доступ к полям переменной осуществляется через оператор точки:

pt.x = 5;
pt.y = 7;

Компонентами структуры могут быть и другие структуры:

struct rect {
    struct point pt1;
    struct point pt2;
};
struct rect screen;
screen.pt2.x = 1337;

Интернет

В стандарте C99 и выше возможна инициализация при объявлении:

struct point pt = { .x = 0, .y = 0 };

Кроме того, поддерживаются агрегатные инициализаторы (например, {0} для обнуления всех полей).

2. Передача структурных переменных функциям

В C структуры при передаче в функции передаются по значению – происходит полная копия всех полей:

struct point makepoint(int x, int y);
struct point p = makepoint(3, 4);

Функция может возвращать структуру целиком:

struct point addpoint(struct point p1, struct point p2) {
    p1.x += p2.x;
    p1.y += p2.y;
    return p1;
}

Интернет

В C++ инициализацию копированием может оптимизировать компилятор (RVO, copy elision), но в общем случае копирование больших структур влияет на производительность. Поэтому часто используют передачу по константной ссылке (const &) для избежания лишнего копирования.

3. Указатели на структурные переменные

Для передачи или работы с большими структурами эффективнее использовать указатели. Объявление указателя на структуру:

struct point pt;
struct point *pp = &pt;

Доступ к полям через указатель возможен двумя способами:

// разыменование и обращение к полю
(*pp).x = 10;
// краткая запись через оператор "стрелка"
pp->y = 20;

Интернет

Путём арифметики указателей можно перемещаться по массиву структур:

struct point arr[5];
struct point *p = arr;
p += 2;
// теперь указывает на arr[2]
p->x = 7;

4. Массивы структурных переменных

Массивы структур работают аналогично массивам базовых типов, только каждый элемент – это полноценная структура:

struct key {
    char *word;
    int count;
} keytab[100];
// массив из 100 структур key

Обход массива по индексам:

for (int i = 0; i < 100; i++) {
    keytab[i].count++;
}

С помощью указателя:

struct key *kp;
for (kp = keytab; kp < keytab + 100; kp++) {
    if (kp->count > 0) {
        printf("%4d %s\n", kp->count, kp->word);
    }
}

Интернет

В стандарте C99 введены гибкие массивы (flexible array members), позволяющие описывать структуры с полем-массивом переменной длины:

struct flex {
    size_t n;
	// гибкий массив
    int data[];
};

Такие структуры обычно выделяются динамически с учётом требуемого размера массива.

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

Объединение (union) – это составной тип данных, аналогичный структуре, но память под все его поля выделяется под самый «тяжёлый» (по размеру) компонент, и в каждый момент времени может храниться значение только одного из полей. Это позволяет экономить память, если разные представления данных никогда не используются одновременно. Синтаксис и обращение к полям полностью идентичны структурам:

union room {
	// Целое число
    int a1;
	// Вещественное число
    float a2;
};
union room kreslo;
kreslo.a1 = 5;
// теперь kreslo хранит в себе целое 5

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

struct student {
    char *name;
    int ID;
    union {
        int sovest;
        float eda;
    } ostatok;
} group[25];

group[3].ostatok.sovest = 10;

6. Поля битов. Средство typedef

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

struct {
	// 1 бит
    unsigned int flag1 : 1;
	// 2 бита (значения 0–3)
    unsigned int flag2 : 2;
} komissiya;

Поля битов не могут быть массивами и не имеют адресов.

typedef позволяет создавать псевдонимы для существующих типов:

// теперь "string" – синоним для "char*"
typedef char *string;
string s = "Hello";

Псевдоним используется везде, где можно было бы применить оригинальный тип.

7. Понятие макроподстановки

Интернет

В препроцессоре C/C++ макрос – это правило текстовой подстановки, определяемое директивой #define.

Объектные макросы:

#define PI 3.14159

При каждом упоминании PI до компиляции подставляется 3.14159.

Функциональные макросы:

#define SQR(x) ((x) * (x))

При встрече SQR(a+1) препроцессор подставит ((a+1) * (a+1)).

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

8. Включение файлов

Интернет

Подключение заголовочных файлов осуществляется директивой препроцессора #include.

Примеры:

/* системный заголовок */
#include <stdio.h>
/* локальный файл */
#include "myheader.h"

Обычно в заголовочных файлах применяют защитные макрозаголовки (include guards), чтобы предотвратить повторное включение:

#ifndef HEADER_H
#define HEADER_H
// ...
#endif

Это гарантирует, что код внутри будет обработан только один раз, даже если файл включён несколько раз.

9. Условная компиляция

Интернет

В препроцессоре C/C++ условная компиляция позволяет включать или исключать участки кода в зависимости от заданных макросов. Используются директивы:

// если выражение истинно (не ноль)
#if выражение
   /* код */
// иначе, если выражение2 истинно
#elif выражение2
   /* код */
// иначе
#else
   /* код */
#endif

// если MACRO определён
// (аналогично #if defined(MACRO))
#ifdef MACRO
   /* код */
#endif

// если MACRO не определён
// (аналогично #if !defined(MACRO))
#ifndef MACRO
   /* код */
#endif

Обычно применяется для портируемости (различные ОС, платформы), отладки (#ifdef DEBUG), и защиты заголовков от многократного включения.

#ifndef HEADER_H
#define HEADER_H
// ...
#endif

10. Стандартные библиотеки math.h и string.h. Основные возможности

Интернет

math.h предоставляет функции для математических вычислений с плавающей точкой:

• Тригонометрические: sin, cos, tan, asin, acos, atan.

• Экспоненциальные и логарифмические: exp, log, log10, pow, sqrt.

• Округление и абсолютная величина: ceil, floor, fabs.

• Специальные: modf, frexp, ldexp.

Каждый прототип начинается с double, есть варианты для float и long double (sinf, sinl и т.д.).


string.h содержит функции для работы со строками и блоками памяти:

• Копирование и конкатенация строк: strcpy, strncpy, strcat, strncat.

• Сравнение: strcmp, strncmp, strcoll.

• Поиск: strchr, strrchr, strstr.

• Длина: strlen.

• Работа с памятью: memcpy, memmove, memset, memcmp.

11. Стандартные библиотеки ctype.h и stdlib.h. Основные возможности

Интернет

ctype.h предоставляет проверки и преобразования символов:

• Классификация: isalpha, isdigit, isalnum, isspace, ispunct, isupper, islower.

• Преобразование регистра: toupper, tolower.


stdlib.h охватывает утилиты общего назначения:

• Конвертация строк в числа: atoi, atof, strtol, strtod.

• Динамическое выделение памяти: malloc, calloc, realloc, free.

• Управление процессом: exit, abort.

• Поиск элементов: bsearch, qsort.

• Генерация псевдослучайных чисел: rand, srand.

12. Обработка двоичных файлов. Последовательный и произвольный доступ

Интернет

Открытие двоичного файла:

// чтение
FILE *f = fopen("data.bin", "rb");
// запись (с обнулением)
FILE *g = fopen("data.bin", "wb");
// чтение / запись без обнуления
FILE *h = fopen("data.bin", "r+b");

Последовательный доступ – чтение / запись блоками через fread / fwrite, указатель смещается автоматически:

struct record rec;
while (fread(&rec, sizeof(rec), 1, f) == 1) {
    // обработать rec
}

Произвольный доступ – использование fseek / ftell / rewind, для перемещения по файлу:

// перейти к 10-му элементу:
fseek(f, 9 * sizeof(struct record), SEEK_SET);
fread(&rec, sizeof(rec), 1, f);
// получить текущую позицию
long pos = ftell(f);
// вернуться в начало
rewind(f);

Это позволяет эффективно обновлять отдельные записи без чтения всего файла.

13. Аргументы, используемые по умолчанию

В прототипе функции C++ можно указывать значения параметров по умолчанию. Если при вызове функции некоторые аргументы пропущены, компилятор подставит указанные в прототипе значения.

• Параметры со значениями по умолчанию должны идти после всех обязательных параметров.

• Если пропустить один аргумент, необходимо пропустить и все последующие параметры.

// Прототип с аргументами по умолчанию
void ShowMes(char *msg, int x = 0, int y = 0);

// Корректные вызовы
ShowMes("Привет");
// x=0, y=0
ShowMes("Координаты", 5);
// x=5, y=0
ShowMes("Координаты", 5, 7);
// x=5, y=7

// Неверный вызов (пропущен msg, но указан x):
ShowMes(10, 20); // Ошибка

14. Использование ссылок

Ссылка (reference) в C++ – это псевдоним (алиас) для уже существующей переменной. Ссылка не занимает дополнительной памяти и после инициализации неизменна (нельзя «перепривязать» её к другой переменной).

• Объявление ссылки: int &r = original;

• При передаче параметров в функцию ссылка позволяет модифицировать оригинал без явного использования указателя, причём синтаксис вызова не отличается от передачи по значению.

int a = 5;
int &r = a;
// r – псевдоним для a
r += 10;
// теперь a == 15

void inc(int &x) { x++; }
inc(a);
// a изменится, как если
// бы мы передавали указатель

Интернет

В качестве возвращаемого типа функции ссылка позволяет реализовать «левыхзначный» вызов, например:

int arr[10];
int& elem(int i) { return arr[i]; }
elem(3) = 42;
// присваивает arr[3] значение 42

15. Встроенные функции

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

• Преимущества: уменьшение времени вызова, потенциальное ускорение.

• Недостатки: рост размера скомпилированного кода, ухудшение локальности инструкций при чрезмерном использовании.

• Компилятор может игнорировать inline, если функция слишком сложна (содержит циклы, switch, goto, множественные return и т.п.).

inline int Sqr(int x) {
    return x * x;
}

16. Операция разрешения видимости

Оператор :: в C++ позволяет явно указать область видимости имени, например, обратиться к глобальной переменной, скрытой локальной, либо к статическому члену класса.

• Доступ к глобальной переменной внутри функции, где есть локальная с тем же именем ::name

• Доступ к статическому полю или методу класса: ClassName::staticMember

// глобальная
int value = 100;

void func() {
    int value = 50;
	// выводит 50 (локальная)
    std::cout << value << "\n";
	// выводит 100 (глобальная)
    std::cout << ::value << "\n";
}

// Статический член класса
class Counter {
public:
    static int count;
};
int Counter::count = 0;
void inc() {
    Counter::count++;
}

17. Перегрузка функций

Перегрузка функций (function overloading) в C++ означает возможность объявить несколько функций с одинаковым именем, но различающимся списком параметров (количеством и/или типами аргументов). Компилятор при вызове выбирает ту версию функции, чья сигнатура наилучшим образом соответствует переданным аргументам. При этом отличаться может только набор параметров – изменение только возвращаемого типа или только наличия ссылочного параметра не считается перегрузкой.

// принимает целое
void Showmes(int);
// принимает строку
void Showmes(char*);

При вызове Showmes(5) используется первая версия, при Showmes("Hello") – вторая.

Интернет

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

18. Особенности описания переменных в программах на C++

В отличие от C, где локальные переменные должны определяться в начале блока или функции, в C++ переменную можно объявить и инициализировать в любом месте кода, где это необходимо, что повышает гибкость и читаемость. Кроме того, модификатор const в C++ делает переменную истинно константной, позволяя использовать её в местах, требующих compile-time константу (например, при определении размеров массивов статической длины). При описании переменных пользовательских типов (структур или классов) ключевое слово struct опускать не нужно – достаточно написать имя класса или структуры напрямую:

// вместо struct Point p;
Point p;
const int N = 10;
// допустимо в C++
int arr[N];

19. Определение классов

Класс в C++ – это расширение структурного типа, в котором помимо полей (данных) могут определяться методы (функции-члены), а также спецификаторы доступа (public, protected, private). Определение класса начинается с ключевого слова class (или struct, с отличием доступа по умолчанию) и включает в себя список членов:

class Point {
private:
    int x, y;
public:
    // конструктор
    Point(int xx, int yy);
	// деструктор
    ~Point();
	// метод для чтения поля
    int getX() const;
	// метод для записи поля
    void setX(int);
};

Если использовать struct вместо class, все члены по умолчанию будут public.

20. Элементы класса

Элементами класса в C++ являются:

• Данные-члены (поля) – могут быть простыми типами, объектами других классов, указателями или ссылками, но не самим классом.

• Функции-члены (методы) – либо определённые прямо в теле класса (в этом случае они автоматически считаются inline), либо объявленные в классе и реализованные вне него с помощью операции разрешения видимости ClassName::method.

• Специальные методы:

• Конструкторы (создают и инициализируют объект),

• Деструкторы (освобождают ресурсы при уничтожении объекта),

• Операции копирования / перемещения,

• Оператор присваивания.

• Статические члены – данные и методы, общие для всех объектов класса (существуют независимо от наличия экземпляров).

В зависимости от спецификатора доступа члены могут быть доступны из любой части программы (public), только внутри класса и его наследников (protected) или исключительно внутри самого класса (private).

21. Правила обращения к элементам класса

Обратиться к элементам данных и методам класса в C++ можно четырьмя способами:

• Через объект класса и оператор .:

obj.method();
int v = obj.field;

• Через указатель на объект и оператор ->:

ptr->method();
ptr->field = 5;

• Через имя класса и оператор разрешения видимости :: (для доступа к статическим членам):

ClassName::staticField = 10;
ClassName::staticMethod();

• Без каких-либо приставок – только в теле метода самого класса (неявный доступ к своим членам):

void ClassName::foo() {
	// эквивалент this->field
    field = 3;
	// вызов метода helper()
    helper();
}

22. Конструктор и деструктор

Конструктор – специальный метод с именем класса и без возвращаемого типа, автоматически вызывается при создании объекта. Если конструктор не определён, компилятор генерирует конструктор по умолчанию.

class MyClass {
public:
    // конструктор по умолчанию
    MyClass();
	// перегружённый конструктор
    MyClass(int a, int b);
};

MyClass::MyClass() {
    /* инициализация */
}
MyClass::MyClass(int a, int b) : x(a), y(b) { }

Деструктор – метод с тем же именем, что и класс, но с префиксом ~, вызывается при уничтожении объекта; не имеет параметров и возвращаемого значения:

class MyClass {
public:
    // деструктор
    ~MyClass();
};
MyClass::~MyClass() {
    /* очистка ресурсов */
}

23. Перегрузка операций

Язык C++ позволяет задать для классов собственные реализации операторов. Сигнатура функции-перегрузчика имеет вид

ReturnType operatorX(Params);

где X – знак операции (+, -, << и т.п.).

• Унарные операторы определяются с нулём параметров или одним параметром при внешней реализации.

• Бинарные – с одним параметром (если метод класса) или двумя (если свободная функция).
Пример перегрузки + для класса Proverka:

class Proverka {
	int a, b;
public:
	Proverka(int aa, int bb): a(aa), b(bb) {}
	Proverka operator+(const Proverka &other) const {
		return Proverka(a + other.a, b + other.b);
	}
};

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

24. Обработка исключительных ситуаций

Интернет

В C++ для обработки ошибок и исключительных ситуаций используется механизм исключений:

• Генерация исключения через throw:

if (x == 0)
    throw std::runtime_error("Деление на ноль");

• Захват через try / catch:

try {
    dangerousOperation();
}
catch (const std::exception &e) {
    std::cerr << "Ошибка: " << e.what() << std::endl;
}

• Можно объявлять несколько блоков catch для разных типов исключений.

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

• Для собственных типов исключений обычно наследуют от std::exception или его потомков