С++ обзор языка


Разрешение неоднозначности



страница5/10
Дата22.06.2019
Размер1.54 Mb.
1   2   3   4   5   6   7   8   9   10

8.1.1. Разрешение неоднозначности
Упомянутая в 6.8 неоднозначность, которая возникает из-за синтаксической схожести явного приведения типа в функциональной записи и объявления, может появляться и в контексте объявления. В этом контексте она возникает как выбор между объявлением функции с лишней парой скобок при имени параметра и объявлением объекта с функциональным стилем записи приведения в инициализации. Так же как и для оператора, разрешение неоднозначности закдючается в том, чтбы считать объявлением любую конструкцию, которая может им быть.
8.2. Смысл описателей
Список описателей появляется после списка (возможно, пустого) decl-спецификаторов (7.1). Каждый описатель содержит ровно одно d-имя; оно определяет объявляемый идентификатор. За исключением некоторах специальных функций (12.3, 12.4) d-имя является просто идентификатором. Спецификаторы auto, static, extern, register, friend, inline, virtual, typedef применяются к каждому d-имени в списке-описателей; тип каждого d-имени зависит как от decl-спецификаторов (7.1), так и от его описателя.
Таким образом, объявление конкретного идентификатора имеет вид:

Т D


где Т - тип, а D - описатель.
В объявлении, где D - просто идентификатор, T является его типом.
В объвлении, где D имеет вид (D1), тип D1 тот же самый, что и у D. Скобки не изменяют тип заключенного в них d-имени, но они могут изменить порядок конструирования типа в сложных описателях.
По всеобщему мнению, синтаксис описателей в С и С++ очень сложен для их записи и чтения. Поэтому рекомендуется использовать описание типа посредством typedef. Например:
typedef char* F(int); //F есть тип функции с параметром типа int

// и результатом типа указатель на char

typedef F* A[10]; // А - тип массива из 10 указателей на F

A* p; // p - указатель на ужасно сложный тип,

// описанный выше
8.2.1. Указатели
В объявлении Т D, где D имеет вид

* список-cv-описателей opt D1,

тип содержащегося в объявлении идентификатора есть “ ... список-cv-описателей указатель на Т”. То есть, cv-описатели относятся к указателю, а не к объекту, на которай он указывает. В переменной типа Т* хранится адрес объекта типа Т. Например, объявления
const ci = 10, *pc = &ci, *const cpc = pc;

int i, *p, *const cp = &i;
объявляют:
1) ci как целую константу,

2) pc как указатель на целую константу,

3) cpc как константный указатель на целую константу,

4) i как целую переменную,

5) p как указатель на целую переменную,

6) cp как константный указатель на целое.


Значения ci, cp, cpc не могут быть изьенены после инициализации. Значение pc может быть изменено, как и значение объекта, указываемого cp. Примеры допустимых действий:
i = ci;

*cp = ci;

pc++;

pc = cpc;

pc = p;
Примеры недопустимых действий:
ci = 1;

ci++;

*pc = 2;

cp = &ci;

cpc++;

p = pc;
Наличие типа константы const позволяет описать четыре варианта типов указателей. Примеры:
char c, c1;

const char d = 'x';
char* pv; // переменный указатель на переменную

pv = &c; // OK

*pv = 'z' // OK
const char* pc; // переменный указатель на константу

pc = &d // OK

*pc = 'z' // ошибка: присваивание константе
char *const cp = &c; // константный указатель на переменную

cp = &c1; // ошибка: присваивание константе

*cp = 'y'; // OK
const char *const cpc = 'b'; // константный указатель на константу

cpc = &d; // ошибка: присваивание константе

*cpc = 'a' // ошибка: присваивание константе
Указателю на константу можно присваивать адрес переменной, потому что вреда от этого быть не может. Например:
pc = &c; // OK
Это, в частности, полезно при описании параметров функции для запрещения модификации аргумента, например:
void strcopy(char* p, const char* q) // не может изменять q
Нельзя, однако, присвоить адрес константы указателю на переменную ибо это позволило бы изменить значение константы, например:
pv = &d // ошибка
Для указателей на массивы и функции приходится пользоваться более сложной записью, например:
int (*vp)[10]; // указатель на массив из 10 целых

int (*fp)(char, char*); // указатель на функцию типа int(char, char*)
8.2.2. Ссылки
В объявлении T D, где D имеет вид

& список-cv-описателей opt D1

тип идентификатора в объявлении есть “... список-cv-описателей ссылка на Т”. Тип void запрещен.


Ссылка является другим именем объекта. Главное применение ссылок состоит в описании параметров и возврате значений для функций вообще и для одинаково названных операций в частности. Примеры:
int i = 1;

int& r = i; // r и i ссылются теперь на один int

int x = r; // x = 1

r = 2; // i =2

void f(double& a) {a += 3.14;} // параметр - ссылка

// ...

double d = 0;

f(d); // прибавление 3.14 к d
Ссылка обязательно должна быть инициализирована (должно быть что-то, для чего она является именем - 8.4.3). Ни одна операция ссылку не меняет. Пример:
int ii = 0;

int& rr = ii;

rr++; // ii увеличивается на 1
Чтобы получить указатель на объект помеченный ссылкой rr, можно написать rr&, что эквивалентно ii&.
Не существует ссылки на ссылки, ссылок на битовые поля (9.6), массивов ссылок и указателей на ссылки.
Ссылки на константы играют важную роль в качестве параметров функций (5.2.2).
8.2.3. Указатели на компоненты класса
В объявлении T D, где D имеет вид

имя класса :: * список-cv-описателей opt D1

тип идентификатора в объявлении есть “... список-cv-описателей указатель на компонент класса имя-класса типа Т”. Например,


class X {

public:

void f(int);

int a;

};

int X ::* pmi = &X::a;

void (X::* pmf)(int) = &X::f;
объявляет pmi и pmf указателями на компоненты класса Х типа int и void(int), соответственно. Их можно использовать следующим образом:
X obj;

// ...

obj.*pmi = 7; // присваивает 7 компоненту класса типа int

(obj.*pmf)(7); // вызов компонентной функции с параметром 7
Указатель на компонент класса не может указывать на статический компонент класса (9.4).
Указатели на компоненты класса могут быть полезны в качестве компонентов массивов.
8.2.4. Массивы
В объявлении T D, где D имеет вид

D1 [константное-выражение]

тип идентификатора в объявлении есть “...массив элементов типа Т”. Если константное-выражение присутствует, оно должно быть целочисленным больше нуля. Оно задает число элементов массива. Если оно равно N, массив имеет N элементов, пронумерованных от 0 до N-1.


Массив может состоять из элементов любого основного типа кроме void, из указателей некоторого типа, из указателей на компоненты класса, из объектов класса, из элементов типа перечисления или из массивов какого-либо типа.
Когда несколько спецификаций вида “массив из” являются смежными, создается многомерный массив; константные выражения, задающие границы массива, могут быть опущены только для первого элемента последователтности.
Возможность опускать первую размерность в объявлении массива полезна для параметра функции, имеющей тип массива, а также в случае, когда массив является внешним, а описание, распределяющее память, дается где-либо еще. Первое константное-выражение может быть также опущено, если за описателем следует список-инициализаторов (8.4). В этом случае размер массива равен количеству элементов в инициализации (8.4.1). Примеры:
float fa[17], *afp[17];
объявляют массив чисел типа float и массив указателей на числа типа float.
static int x3d[3][5][7];
объявляет статический трехмерный массив целых чисел размером 3*5*7.
Когда идентификатор массива используется в выражении, он обычно преобразуется в указатель на первый элемент массива. Операция индексации обычно интерпретируется таким образом, что Е1[Е2] равносильно *((E1)+(E2)).
В С++ невозможно описание размерностей многомерных массивов через запятую, как это делается в большинстве языков программирования. Поэтому ошибочно объявление

int bad[5,2];
Указатели и массивы в С++ связаны очень тесно. Имя массива можно использовать как указатель на его первый элемент. Например, программа печати целых значений букв нижнего регистра может выглядеть следующим образом:
int main()

{

char alfa[] = "abcdefghijklmnopqrstuvwxyz";

char p* = alfa;

char ch;

while (ch = *p++)

cout << ch << " = " << int(ch) << " = 0" << oct(ch) << '\n';

}
Когда к указателю р типа *Т применяется арифметическая операция, предполагается, что он указывает на элемент массива из Т; р+1 означает следующий элемент массива, а р-1 - предыдущий элемент. Отсюда следует, что значение р+1 будет на sizeof(T) больше значения р.
Имя массива является константой, потому присваивание массиву невозможно.
8.2.5. Функции
В объявлении T D, где D имеет вид

D1 (список-объявлений-параметров) список-cv-описателей opt D1

тип идентификатора в объявлении есть “... список-cv-описателей функция с параметрами типа список-объявлений-параметров и результатом типа Т”.


список-объявлений-параметров:

список-объявлений-пар opt ... opt

список-объявлений-пар , ...
список-объявлений-пар:

объявление-параметра

список-объявлений-пар, объявление-параметра
объявление-параметра:

decl-спецификаторы описатель

decl-спецификаторы описатель = выражение

decl-спецификаторы абстрактный-описатель opt

decl-спецификаторы абстрактный-описатель opt = выражение
Если список-объявлений-параметров заканчивается многоточием, это означает, что число аргументов будет равно или больше указанного числа параметров; если список пуст, функция не имеет параметров.
Одно и то же имя может быть использовано для нескольких различных функций в одной и той же области действия; это называется совмещением имен (overloading).
Типы параметров и тип результата являются частями типа функции. Список-cv-описателей может быть частью объявления или описания нестатической компонентной функции или указателя на компонентную функцию класса (9.3.1). Список-cv-описателей является в таком случае частью типа функции.
Функции не могут возвращать функции или массивы, но могут возвращать ссылки на них. Не разрешены массивы функций, но возможны массивы указателей на функции.
Список-объявлений-параметров используется для проверки и приведения фактических параметров в вызовах.
В объявлении функции можно давать имена ее параметрам (компилятор их игнорирует); идентификатор, именующий параметр в объявлении функции не может, однако, использоваться, так как немедленно заканчивается его область действия. Имена параметров в разных объявлении и описании функции не обязательно должны быть одинаковы. Пример. Объявление
f(),

*fpi(int),

(*pif)(const char*, const char*);
описывает функцию f без параметра с целым результатом, функцию fpi с целым параметром и указателем на целое как результатом и указатель pif на функцию, которая имеет два параметра - указатели на константные литеры - и целый результат.
Семантика подстановки параметров идентична семантике инициализации. Сначала проверяются типы параметров, а затем, если необходимо, производится неявное преобразование типа. Например, для функции, объявленной как
double sqrt(double);
законен вызов
double sr2 = sqrt(2);
Имя функции является константой, потому присваивание функции невозможно.
8.2.6. Параметры по умолчанием
Если в объявлении параметра задано выражение, оно воспринимается как значение параметра по умолчанию. При этом все следующие параметры также должны иметь значения по умолчанию, заданные в этом или предыдущих объявлениях функции. Значения по умолчанию подставляютя в вызов функции при отсутствии в нем соответствующих фактических параметров. Пример. Функция, объявленная как

point(int = 3, int = 4);

может быть вызвана следующими способами:



point(1, 2); point(1); point();
8.3. Описания функций
Каждая функция, используемая в программе, должна быть где-то описана (только один раз). Описание функции имеет вид:
описание-функции:

decl-спецификаторы opt описатель ctor-инициализатор opt тело-функции


тело-функции:

составной оператор


Описатель в описании-функции согласно 8.2.5 должен иметь вид

D1 (список-объявлений параметров) список-cv-описателей opt

Пример:
int max(int a, int b, int c)



{

int m = (a > b) ? a : b;

return (m > c) ? m : c;

}
Здесь int - decl-спецификаторы, max(int a, int b, int c) - описатель, остальное - тело-функции.
Нетерминал ctor-инициализатор употребляется только в конструкторе (12.1, 12.6).
Ключевые слова из список-cv-описателей могут появляться только в объявлении компонентной функции, в описании нестатической компонентной функции и в объявлении указателя на компонентную функцию класса (8.3.1). Этот список является частью типа функции.
Неиспользуемые формальные параметры не обязаны иметь имена. Пример:

void print (int a, int)

{ printf(“a = %d\n”, a) }

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


8.3.1. Подстановка параметров
Когда вызывается функция, под ее формальные параметры выделяется память и каждый из них инициализируется значением соответствующего фактического параметра. Семантика подстановки параметров идентична семантике инициализации. Есть особые правила для подстановки массивов (8.3.3), средства передавать параметр без проверки типа, и средства для задания параметров по умолчанию (8.2.6). Рассмотрим
void f(int val, int& ref)

{

val++;

ref++;

}
Когда вызывается f(), выражение val++ увеличивает локальную копию первого фактического параметра, тогда как ref++ увеличивает второй фактический параметр. Использование функций, которые изменяют фактические параметры, может сделать программу трудно понимаемой и потому таких параметров лучше избегать. Однако передача большого объекта по ссылке может быть эффективнее, чем его передача по значению. В этом случае можно объявить такой параметр как const, чтобы не позволить функции изменить значение подставляемого объекта. Пример:
void f(const large& arg)

{

// значение "arg" не может быть изменено

}
Аналогично, если параметр, являющийся указателем, описывается как const, подразумевается, что функция не меняет значение указываемого объекта. Примеры:
extern int strlen(const char*);

extern char* strcopy(char* to, const char* from);
Заметим, что семантика подстановки параметров отлична от семантики присваивания. Это важно для параметров, специфицированных как const, параметров-ссылок и параметров некоторых типов, определяемых пользователем.
8.3.2. Возврат значения
Функция, объявленная не как void, должна возвращать значение. Например:
int f() {} // ошибка

void g() {} // все в порядке
Возвращаемое значение указывается в операторе return. Например:
int fac(int n) { return (n >1) ? n*fac(n-1) : 1 }
В функции может быть больше одного оператора return, например:
int fac(int n)

{

if (n >1) return n*fac(n-1)

else return 1

}
Как и семантика подстановки параметров, семантика возврата значения идентична семантике инициализации: тип возвращаемого значения проверяется на согласованность с объявленным типом результата и выполняютя необходимые преобразования типа. Пример:
double f()

{

// . . .

return 1; // неявно преобразуется к double(1)

}
Возвращать указатель на локальную переменную блока функции нельзя, так как эта переменная после выхода из функции уничтожается. Пример:
int* f() {

int local = 1;

// . . .

return &local; // ошибка

}
Аналогичная ситуация при использовании ссылок:
int& f() {

int local = 1;

// . . .

return local; // ошибка

}
8.3.3. Массивы в качестве параметров
Если в качестве параметра функции используется массив, подставляется указатель на его первый элемент. Пример:
int strlen(const char*);
void f()

{

char v[] = "an array";

strlen(v);

strlen("Nicolas");

};
Иначе говоря, Т[] преобразуется к Т*, когда он передается функции. Следовательно, присваивание элементу параметра-массива меняет значение фактического параметра-массива. Таким образом, массив не подставляется (и не может подставляться) значением.
Размер массива недоступен функции. Это неудобство можно обойти несколькими способами: строки заканчиваются нулем, который можно проверить, а для других массивов можно задать второй параметр, содержащий размер массива, например:
void compute(int* vec_prt, int vec_size);
С многомерными массивами все хитрее. Рассмотрим описание функции, которая работает с двумерными матрицами. Если размерность известна на стадии компиляции, проблем нет:
void print_m34(int m[3][4])

{

for (int i =0; i<3; i++) {

for (int j = 0; j<4; j++)

cout << " " << m[i][j];

cout << '\n';

}

}
Хотя матрица все равно передается как указатель, объявленные размерности дают данные для циклов.
Первую размерность можно заменить передачей параметра:
void print_mi4(int m[][4], int dim1)

{

for (int i =0; i

for (int j = 0; j<4; j++)

cout << " " << m[i][j];

cout << '\n';

}

}
Сложный случай возникает, когда нужно передать обе размерности, так как описание м[][] недопустимо. Вот правильное решение:
void print_mij(int** m, int dim1, int dim2)

{

for (int i =0; i

for (int j = 0; j

cout << " " << ((int*)m)[i*dim2+j];

cout << '\n';

}

}
Здесь выражение, используемое для доступа к элементам, эквивалентно тому, которое генерирует компилятор, когда он знает последнюю размерность.
8.3.4. Совмещение имен функций
Когда несколько функций выполняют сходную работу над объектами разных типов, может быть удобно дать им одно и то же имя. Этот механизм называется совмещением имен (overloading). Пример:
void print(int)

void print(const char*)
Когда вызывается функция с совмещенным именем f, компилятор должен понять, к какой из функций с именем f следует обратиться. Для этого типа всех фактических параметров сравниваются с типами формальных параметров всех функций с именем f. Задача заключается в том, чтобы вызвать функцию, которая лучше всех (по типам параметров) соответствует вызову или выдать сообщение об ошибке, если подходящей функции нет. Пример:
void print(double);

void print(long);
void f()

{

print(1L); // print(long)

print(1.0); // print(double)

print(1); // ошибка: неясно, print(long) или print(double)

Подробные правила согласования объясняются в 13.2. В упрощенном виде они применяются в следующем порядке:


1) Точные совпадения, т.е. согласования, которые совсем не используют или используют преобразования, без которых нельзя обойтись (например, преобразование имени массива к указателю, имени функции к указателю на функцию и Т к const T).

2) Согласования, использующие приведение к арифметическому типу (4.1), например, char в int, short в int и их беззнаковых аналогов unsigned, а также float в double, int во float и т.д..

3) Согласования, которые используют преобразования, определенные пользователем (12.3).

4) Согласования, использующие многоточия ... в объявлении функции.


Правила отождествления имеют такую сложнею форму в основном из-за того, что необходимо использовать уже имеющиеся в С правила преобразования числовых типов. Примеры:
void print(int)

void print(const char*)

void print(double);

void print(long);

void print(char);
void h(char c, int i, short s, float f)

{

print(c); // точное согласование, вызывается print(char)

print(i) // точное согласование, вызывается print(int)

print(s) // приведение к числовому типу, вызывается print(int)

print(f) // приведение к числовому типу, вызывается print(double)

print('a') // точное согласование, вызывается print(char)

print(49) // точное согласование, вызывается print(int)

print("a") // точное согласование, вызывается print(const char*)
При указанных правилах можно гарантировать, что, когда эффективность или точность вычислений для используемых типов различаются, будет использоваться простейший алгоритм.
8.3.5. Произвольное число параметров
Для некоторых функций невозможно заранее знать число и типы всех параметров, которые могут оказаться в вызове. Пример:
Объявление

printf(const char* ...);

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



printf(“hello world\n”);

printf("Мое имя %s %s\n", first_name, second_name);

printf(“%d+%d=%d\n”, 2, 3, 5);
Такая функция полагается на информацию, не доступную компилятору при интерпретации ее списка параметров. В случае printf() первым параметром является строка формата, содержащая специальные последовательности символов, позволяющие printf() правильно обрабатывать остальные параметры. %s означает "жди параметра типа *char, а %d означает "жди параметра типа int".
Очевидно, что если параметр не был описан, у компилятора нет информации для проверки и преобразования типа фактического параметра. При необходимости проверки типов, следует использовать функции с совмещенными именами или функции с параметрами по умолчанию. Многоточие обычно применяют для того, стобы установить интерфейс с библиотечными С-функциями, которые были определены, когда альтернативы не было:
extern "C" int fprintf(FILE*, const char* ...);

extern "C" int execl(const char* ...);
Стандартный набор макросов для доступа к неспецифицированным параметрам таких функций можно найти в . Ниже следует описание функции ошибок, которая получает один целый параметр, указывющий серьезность ошибки, после которого идет произвольное число строк. Идея состоит в том, чтобы генгерировать сообщение об ошибке с помощью передачи каждого слова как отдельного строкового параметра.
extern void error(int ...);

extern char* itoa(int);
main(int argc, char* argv[])

{

switch(argc) {

case1: error(0, argv[0], (char*)0); break;

case2: error(0, argv[0], argv[1], (char*)0); break;

default: error(1, argv[0], 'c', itoa(argc-1), "параметрами", (char*)0);

}

// . . .

}
Функцию ошибок можно определить так:
#include

void error(int severity ...)

// "severity" с последующем списком char*, оканчивающимся нулем

{

va_list ap;

va_start(ap, severity); // раскрутка аргумунтов

for (;;) {

char* p = va_arg(ap, char*);

if (p == 0) break;

cerr << p << ' ';

}

va_end(ap) // очистка стека

cerr << '\n';

if ceverity exit severity;

}
Сначала описывается и нициализируется вызовом va_start() объект типа va_list. Макрос va_start получает в качестве параметров имя этого объекта и имя первого формального параметра. Макрос va_arg() используется для выбора неименованных параметров по порядку. При каждом обращении к нему программист должен задать тип. Перед возвратом из функции, в которой был использован va_start(), должен быть вызван va_end(). Причина в том, что va_start() может изменить стек так, что нельзя будет успешно осуществить возврат; va_end() аннулирует эти изменения. Преобразовывать 0 к (char*)0 необходимо потому, что sizeof(int) не обязательно совпадает с sizeof(char*). Это иллюстрирует те сложности, скоторыми программисту приходится сталкиваться из-за того, что при использовании многоточия теряется контроль типов.


Поделитесь с Вашими друзьями:
1   2   3   4   5   6   7   8   9   10


База данных защищена авторским правом ©vossta.ru 2019
обратиться к администрации

    Главная страница