Новости

17.09.2021

Книга «Эффективный C. Профессиональное программирование»

Здесь рассмотрен C17, а также потенциальные возможности C2x. Издание неизбежно станет классикой, с его помощью вы научитесь писать профессиональные и надежные программы на C, которые лягут в основу устойчивых систем и решат реальные задачи.

Объекты, функции и типы


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

Объекты, функции, типы и указатели

Объект — это хранилище, в котором можно представлять значения. Если быть точным, то в стандарте C (ISO/IEC 9899:2018) объектом называется «область хранилища данных в среде выполнения, содержимое которого может представлять значения» с примечанием: «при обращении к объекту можно считать, что он обладает определенным типом». Один из примеров объекта — переменная.

Переменные имеют объявленный тип, который говорит о том, какого рода объект представляет его значение. Например, объект типа int содержит целочисленное значение. Важность типа объясняется тем, что набор битов, представляющий объект одного типа, скорее всего, будет иметь другое значение, если его интерпретировать как объект другого типа. Например, в IEEE 754 (стандарт IEEE для арифметических операций с плавающей запятой) число 1 представлено как 0x3f800000 (IEEE 754–2008). Но если интерпретировать тот же набор битов как целое число, то вместо 1 получится значение 1 065 353 216.

Функции не являются объектами, но тоже имеют тип. Тип функции характеризуется как ее возвращаемым значением, так и числом и типами ее параметров.

Кроме того, в C есть указатели, которые можно считать адресами — областями памяти, в которых хранятся объекты или функции. Тип указателя, основанный на типе функции или объекта, называется ссылочным типом. Указатель, имеющий ссылочный тип T, называют указателем на T.

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

Объявление переменных

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

В листинге 2.1 объявляются два целочисленных объекта с исходными значениями. Эта простая программа также объявляет, но не определяет функцию swap, которая впоследствии будет менять эти значения местами.

Листинг 2.1. Программа, которая должна менять местами два целых числа

Эта демонстрационная программа состоит из функции main с единственным блоком кода между фигурными скобками. Такого рода блоки называют составными операторами. Внутри функции main мы определяем две переменные, a и b. Мы объявляем их как переменные типа int и присваиваем им значения 21 и 17 соответственно. У всякой переменной должно быть объявление. Затем внутри main происходит вызов функции swap, чтобы поменять местами значения этих двух целочисленных переменных. В данной программе функция swap объявлена, но не определена. Позже в этом разделе мы рассмотрим некоторые потенциальные ее реализации.

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

char *src, c;
int x, y[5];
int m[12], n[15][3], o[21];

В первой строчке объявляются две переменные, src и c, которые имеют разные типы. Переменная src имеет тип char *, а c — тип char. Во второй строчке тоже происходит объявление двух переменных разных типов, x и y; первая имеет тип int, а вторая является массивом из пяти элементов типа int. В третьей строчке объявлено три массива, m, n и o, с разной размерностью и количеством элементов.

Эти объявления будет легче понять, если разделить их по отдельным строчкам:

char *src; // src имеет тип char *
char c; // c имеет тип char
int x; // x имеет тип int
int y[5]; // y — это массив из 5 элементов типа int
int m[12]; // m — это массив из 12 элементов типа int
int n[15][3]; // n — это массив из 15 массивов, состоящих из трех
// элементов типа int
int o[21]; // o — это массив из 21 элемента типа int

В удобочитаемом и понятном коде реже встречаются ошибки.

Перестановка значений местами (первая попытка)

У каждого объекта есть срок хранения, который определяет его время жизни (lifetime) — период выполнения программы, на протяжении которого этот объект существует, где-то хранится, имеет постоянный адрес и сохраняет последнее присвоенное ему значение. К объектам нельзя обращаться вне данного периода.

Локальные переменные, такие как a и b из листинга 2.1, имеют автоматический срок хранения (storage duration); то есть они существуют, пока поток выполнения не покинет блок, в котором они определены. Попробуем поменять местами значения, хранящиеся в этих двух переменных.

Листинг 2.2 отображает нашу первую попытку реализовать функцию swap.

Листинг 2.2. Функция swap

Функция swap объявляет два параметра, a и b, с помощью которых вы передаете ей аргументы. В C есть разница между параметрами и аргументами. Первые — это объекты, которые объявляются вместе с функцией и получают значения при входе в нее, а вторые — это выражения, разделяемые запятыми, которые указываются в выражении вызова функции. Мы также объявляем в функции swap временную переменную t типа int и инициализируем ее с помощью значения a. Данная переменная используется для временного хранения значения a, чтобы оно не было утеряно во время перестановки.

Теперь мы можем скомпилировать и проверить нашу полноценную программу, запустив сгенерированный исполняемый файл:

Результат может вас удивить. Изначально переменные a и b равны 21 и 17 соответственно. Первый вызов printf внутри функции swap показывает, что эти два значения поменялись местами, однако, если верить второму вызову printf в main, исходные значения остались неизменными. Посмотрим, что же произошло.

В языке C передача аргументов при вызове происходит по значению; то есть когда вы предоставляете функции аргумент, его значение копируется в отдельную переменную, доступную для использования внутри этой функции. Функция swap присваивает значения объектов, переданных в виде аргументов, соответствующим параметрам. Изменение значений параметров в функции не влияет на значения в вызывающем коде, поскольку это разные объекты. Следовательно, переменные a и b сохраняют исходные значения в main во время второго вызова printf. Программа должна была поменять местами значения этих двух объектов. Протестировав ее, мы обнаружили в ней ошибку (или дефект).

Перестановка значений местами (вторая попытка)

Чтобы исправить эту ошибку, мы можем переписать функцию swap с помощью указателей. Применим операцию косвенного обращения (или разыменовывания) *, чтобы объявить и разыменовать указатели, как показано в листинге 2.3.

Листинг 2.3. Переработанная функция swap с использованием указателей

В объявлении или определении функции операция * выступает частью объявления указателя, сигнализируя о том, что параметр является указателем на объект или функцию заданного типа. В переписанной функции swap указано два параметра, pa и pb, объявленных как указатели типа int.

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

pa = pb;

Здесь значение указателя pa заменяется значением указателя pb. Теперь посмотрите, как происходит присваивание в функции swap:

*pa = *pb;

Это выражение разыменовывает указатель pb, считывает значение, на которое тот ссылается, разыменовывает указатель pa и затем вместо значения по адресу, на который ссылается pa, записывает значение, на которое ссылается pb.

При вызове функции swap в main необходимо также указать амперсанд (&) перед именем каждой переменной:

swap(&a, &b);

Унарная операция & используется для взятия адреса. Она генерирует указатель на свой операнд. Это изменение необходимо, поскольку функция swap теперь принимает в качестве параметров указатели на объекты типа int, а не просто значения данного типа.

В листинге 2.4 показана вся программа swap целиком, с описанием объектов, создаваемых во время ее работы, и их значений.

Листинг 2.4. Имитация передачи аргументов по ссылке

Во время входа в блок main переменным a и b присваиваются значения 21 и 17 соответственно. Затем код берет адреса этих объектов и передает их функции swap в качестве аргументов.

Параметры pa и pb внутри swap объявлены в виде указателей типа int и содержат копии аргументов, переданных этой функции из вызывающего кода (в данном случае main). Эти копии адресов по-прежнему ссылаются на те же объекты, поэтому, когда функция swap меняет эти объекты местами, содержимое исходных объектов, объявленных в main, тоже меняется. Данный подход имитирует передачу аргументов по ссылке: сначала генерируются адреса объектов, которые передаются по значению, а затем они разыменовываются для доступа к исходным объектам.

С полным содержанием статьи можно ознакомиться на сайте "Хабрахабр":

https://habr.com/ru/company/piter/blog/577854/


Комментарии: 0

Пока нет комментариев


Оставить комментарий






CAPTCHAОбновить изображение

Наберите текст, изображённый на картинке

Все поля обязательны к заполнению.

Перед публикацией комментарии проходят модерацию.