Оглавление

 

Тип void

 

Указатель void*

 

Объявление и определение функций

 

Возвращаемое значение

 

Параметры функции

 


Тип void

Тип void (пустой) синтаксически ведет себя как основной тип. Однако использовать его можно только как часть производного типа, объектов типа void не существует. Он используется для того, чтобы указать, что функция не возвращает значения, или как базовый тип для указателей на объекты неизвестного типа.

 
 void f()     // f не возвращает значение
 void* pv;    // указатель на объект неизвестного типа
 
  Переменной типа  void* можно присваивать указатель любого типа. На
первый взгляд  это может показаться не особенно полезным, поскольку
void* нельзя  разыменовать, но  именно это ограничение и делает тип
void*  полезным.  Главным  образом,  он  применяется  для  передачи
указателей функциям,  которые не  позволяют сделать предположение о
типе объекта,  и для возврата из функций нетипизированных объектов.
Чтобы  использовать   такой  объект,   необходимо  применить  явное
преобразование типа.  Подобные функции  обычно находятся  на  самом
нижнем уровне  системы, там,  где осуществляется работа с основными
аппаратными ресурсами. Например:
 
  void* allocate(int size);    // выделить
  void deallocate(void*);      // освободить
 
  f() {
      int* pi = (int*)allocate(10*sizeof(int));
      char* pc = (char*)allocate(10);
  
  deallocate(pi);
  deallocate(pc);
  }

 

 

Указатель void *

В C++ существует специальный тип указателя, который называется указателем на неопределённый тип. Для определения такого указателя вместо имени типа используется ключевое слово void в сочетании с описателем, перед которым располагается символ ptrОперации *.

void *UndefPoint;

С одной стороны, объявленная подобным образом переменная также является объектом определённого типа - типа указатель на объект неопределённого типа. В Borland C++ 4.5 имя UndefPoint действительно ссылается на объект размером в 32 бита со структурой, которая позволяет сохранять адреса.

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

Поэтому переменной UndefPoint невозможно присвоить никаких значений без явного преобразования этих значений к определённому типу указателя.

UndefPoint = 0xb8000000; // Такое присвоение недопустимо.

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

UndefPoint++; // Для типа void * нет такой операции…

Эта операция (как и любая другая для типа указатель на объект неопределённого типа) не определена. И для того, чтобы не разбираться со всеми операциями по отдельности, лучше пресечь подобные недоразумения "в корне", то есть на стадии присвоения значения.

Объектам типа указатель на объект неопределённого типа в качестве значений разрешается присваивать значения лишь в сочетании с операцией явного преобразования типа.

В этом случае указатель на объект неопределённого типа становится обычным указателем на объект какого-либо конкретного типа. Со всеми вытекающими отсюда последствиями.

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

int mmm = 10;
pUndefPointer = (int *)&mmm;
pUndefPointer выступает в роли указателя на объект типа int.
(*(int *)pUndefPointer)++;

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

pUndefPointer++; // Это неверно, инкрементация не определена…
(int *)pUndefPointer++;   // И так тоже ничего не получается…
((int *)pUndefPointer)++; // А так хорошо… Сколько скобок!
++(int *)pUndefPointer;   // И вот так тоже хорошо…

С помощью операции разыменования и с дополнительной операцией явного преобразования типа изменили значение переменной mmm.

pUndefPointer = (int *)pUndefPointer + sizeof(int);
Теперь перенастроили указатель на следующий объект типа int.
pUndefPointer = (int *)pUndefPointer + 1;

И получаем тот же самый результат.

Специфика указателя на объект неопределённого типа позволяет выполнять достаточно нетривиальные преобразования:

(*(char *)pUndefPointer)++;

А как изменится значение переменной mmm в этом случае?

pUndefPointer = (char *)pUndefPointer + 1;

Указатель перенастроился на объект типа char. То есть просто сдвинулся на 1байт.

Работа с указателями на объекты определённого типа не требует такого педантичного напоминания о типе объектов, на которые настроен указатель. Транслятор об этом не забывает.

int * pInt;
int mmm = 10;
pInt = &mmm; // Настроили указатель.
pInt++;      // Перешли к очередному объекту.
*pInt++;     // Изменили значение объекта, идущего следом за
             // переменной mmm.

Напомним, что происходит в ходе выполнения этого оператора.

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

int mmm = 10;
char ccc = 'X';
float fff = 123.45;
pInt = &mmm;
pNullInt = (int *)&ccc;
pNullInt = (int *)&fff; // Здесь будет выдано предупреждение об
                        // опасном преобразовании.

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

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

Объявление и определение функций

 

Любая программа на C++ состоит из функций, одна из которых должна иметь имя main(с нее начинается выполнение программы). Функция начинает выпол­няться в момент вызова. Любая функция должна быть объявлена и определена. Как и для других величин, объявлений может быть несколько, а определение только одно. Объявление функции должно находиться в тексте раньше ее вызова для того, чтобы компилятор мог осуществить проверку правильности вызова.

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

[ класс ] тип имя ([ список параметров ])[throw ( исключения )] { тело функции } Рассмотрим составные части определения.

éС помощью необязательного модификатора класс можно явно задать область видимости функции, используя ключевые слова extern и static:   

    extern — глобальная видимость во всех модулях программы (по умолча­нию);

static — видимость только в пределах модуля, в котором определена функция.

é Тип возвращаемого функцией значения может быть любым, кроме массива и функции (но может быть указателем на массив или функцию). Если функция не должна возвращать значение, указывается тип void.

é Список параметров определяет величины, которые требуется передать в функ­цию при ее вызове. Элементы списка параметров разделяются запятыми. Для каждого параметра, передаваемого в функцию, указывается его тип и имя (в объявлении имена можно опускать).

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

Функцию можно определить как встроенную с помощью модификатора inline, который рекомендует компилятору вместо обращения к функции помещать ее код непосредственно в каждую точку вызова. Модификатор inline ставится пе­ред типом функции. Он применяется для коротких функций, чтобы снизить на­кладные расходы на вызов (сохранение и восстановление регистров, передача управления). Директива inline носит рекомендательный характер и выполняется компилятором по мере возможности. Использование inline-функций может уве­личить объем исполняемой программы. Определение функции должно предше­ствовать ее вызовам, иначе вместо in line-расширения компилятор сгенерирует обычный вызов.

Тип возвращаемого значения и типы параметров совместно определяют тип функции.

Для вызова функции в простейшем случае нужно указать ее имя, за которым в круглых скобках через запятую перечисляются имена передаваемых аргументов. Вызов функции может находиться в любом месте программы, где по синтаксису допустимо выражение того типа, который формирует функция. Если тип возвра­щаемого функцией значения не void, она может входить в состав выражений или, в частном случае, располагаться в правой части оператора присваивания.

Пример функции, возвращающей сумму двух целых величин:

 


                                                                                                                   

 


 

 

 

 


Пример функции, выводящей на экран поля переданной ей структуры:

 


 

 


 

 


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

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

 


 

 


 


Статическая переменная n размещается в сегменте данных и инициализируется один раз при первом выполнении оператора, содержащего ее определение. Авто­матическая переменная m инициализируется при каждом входе в функцию. Авто­матическая переменная p инициализируется при каждом входе в блок цикла.

 

Возвращаемое значение

Механизм возврата из функции в вызвавшую ее функцию реализуется опера­тором

return[ выражение ];

Функция может содержать несколько операторов return (это определяется по­требностями алгоритма). Если функция описана как void, выражение не указыва­ется. Оператор return можно опускать для функции типа void, если возврат из нее происходит перед закрывающей фигурной скобкой, и для функции main. В этой книге для экономии места оператор return в функции main не указан, по­этому при компиляции примеров выдается предупреждение. Выражение, указан­ное после return, неявно преобразуется к типу возвращаемого функцией значе­ния и передается в точку вызова функции.

Примеры:

 

int fl(){return 1;}   // правильно

void f2(){return 1;} // неправильно, f2 не должна возвращать значение

double f3{return 1;} // правильно, 1 преобразуется к типу double

 

Параметры функции

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

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

Существует два способа передачи параметров в функцию: по значению и по адресу.

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

При передаче по адресу в стек заносятся копии адресов параметров, а функция осуществляет доступ к ячейкам памяти по этим адресам и может изменить ис­ходные значения параметров:

 


 


 


Первый параметр (1) передается по значению. Его изменение в функции не влияет на исходное значение. Второй параметр (j) передается по адресу с по­мощью указателя, при этом для передачи в функцию адреса фактического пара­метра используется операция взятия адреса, а для получения его значения в функции требуется операция разыменования. Третий параметр (k) передается по адресу с помощью ссылки.

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

Если требуется запретить изменение параметра внутри функции, используется модификатор const:

int f(const char*);

char* t(char* a, const int* b);