Некоторые Подробности Разработки

Глава 8

Потоки

Язык C++ не обеспечивает средств для ввода/вывода. Ему это и не нужно; такие средства легко и элегантно можно создать с помощью самого языка. Описанная здесь стандартная библиотека потокового ввода/вывода обеспечивает гибкий и эффективный с гарантией типа метод обработки символьного ввода целых чисел, чисел с плавающей точкой и символьных строк, а также простую модель ее расширения для обработки типов, определяемых пользователем. Ее пользовательский интерфейс находится в . В этой главе описывается сама библиотека, некоторые способы ее применения и методы, которые использовались при ее реализации.

Введение

Разработка и реализация стандартных средств ввода/вывода для языка программирования зарекомендовала себя как заведомо трудная работа. Традиционно средства ввода/вывода разрабатывались исключительно для небольшого числа встроенных типов данных. Однако в C++ программах обычно используется много типов, определенных пользователем, и нужно обрабатывать ввод и вывод также и значений этих типов. Очевидно, средство ввода/вывода должно быть простым, удобным, надежным в употреблении, эффективным и гибким, и ко всему прочему полным. Ничье решение еще не смогло угодить всем, поэтому у пользователя должна быть возможность задавать альтернативные средства ввода/вывода и расширять стандартные средства ввода/вывода применительно к требованиям приложения.
C++ разработан так, чтобы у пользователя была возможность определять новые типы столь же эффективные и удобные, сколь и встроенные типы. Поэтому обоснованным является требование того, что средства ввода/вывода для C++ должны обеспечиваться в C++ с применением только тех средств, которые доступны каждому программисту. Описываемые здесь средства ввода/вывода представляют собой попытку ответить на этот вызов.
Средства ввода/вывода связаны исключительно с обработкой преобразования типизированных объектов в последовательности символов и обратно. Есть и другие схемы ввода/вывода, но эта является основополагающей в системе UNIX, и большая часть видов бинарного ввода/вывода обрабатывается через рассмотрение символа просто как набор бит, при этом его общепринятая связь с алфавитом игнорируется. Тогда для программиста ключевая проблема заключается в задании соответствия между типизированным объектом и принципиально не типизированной строкой.
Обработка и встроенных и определенных пользователем типов однородным образом и с гарантией типа достигается с помощью одного перегруженного имени функции для набора функций вывода. Например:

put(cerr,"x = "); // cerr - поток вывода ошибок

put(cerr,x);

put(cerr,"\n");


Тип параметра определяет то, какая из функций put будет вызываться для каждого параметра. Это решение применялось в нескольких языках. Однако ему недостает лаконичности. Перегрузка операции << значением "поместить в" дает более хорошую запись и позволяет программисту выводить ряд объектов одним оператором. Например:

cerr << "x = " << x << "\n";


где cerr - стандартный поток вывода ошибок. Поэтому, если x является int со значением 123, то этот оператор напечатает в стандартный поток вывода ошибок

x = 123


и символ новой строки. Аналогично, если X принадлежит определенному пользователем типу complex и имеет значение (1,2.4), то приведенный выше оператор напечатает в cerr

x = 1,2.4)


Этот метод можно применять всегда, когда для x определена операция <<, и пользователь может определять операцию << для нового типа.

Вывод

  8.2.1 Вывод Встроенных Типов
  8.2.2 Некоторые Подробности Разработки
  8.2.3 Форматированный Вывод
  8.2.4 Виртуальная Функция Вывода

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

Вывод Встроенных Типов

Класс ostream определяется вместе с операцией << ("поместить в") для обработки вывода встроенных типов:

class ostream {

// ...

public:

ostream& operator<<(char*);

ostream& operator<<(int i) { return *this<

Форматированный Вывод

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

char* oct(long, int =0); // восьмеричное представление

char* dec(long, int =0); // десятичное представление

char* hex(long, int =0); // шестнадцатиричное представление

char* chr(int, int =0); // символ

char* str(char*, int =0); // строка


Если не задано поле нулевой длины, то будет производиться усечение или дополнение; иначе будет использоваться столько символов (ровно), сколько нужно. Например:

cout << "dec(" << x

<< ") = oct(" << oct(x,6)

<< ") = hex(" << hex(x,4)

<< ")";


Если x==15, то в результате получится:

dec(15) = oct( 17) = hex( f);


Можно также использовать строку в общем формате:

char* form(char* format ...);

cout<

Виртуальная Функция Вывода

Иногда функция вывода должна быть virtual. Рассмотрим пример класса shape, который дает понятие фигуры (#1.18):

class shape {

// ...

public:

// ...

virtual void draw(ostream& s); // рисует "this" на "s"

};

class circle : public shape {

int radius;

public:

// ...

void draw(ostream&);

};


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

ostream& operator<<(ostream& s, shape* p)

{

p->draw(s);

return s;

}


Если next - итератор типа определенного в #7.3.3, то список фигур распечатывается например так:

while ( p = next() ) cout << p;

Файлы и Потоки

  8.3.1 Инициализация Потоков Вывода
  8.3.2 Закрытие Потоков Вывода
  8.3.3 Открытие Файлов
  8.3.4 Копирование Потоков

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

Закрытие Потоков Вывода

Деструктор для ostream сбрасывает буфер с помощью открытого члена функции ostream::flush():

ostream::~ostream()

{

flush(); // сброс

}


Сбросить буфер можно также и явно. Например:

cout.flush();

Открытие Файлов

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

#include

void error(char* s, char* s2)

{

cerr << s << " " << s2 << "\n";

exit(1);

}

main(int argc, char* argv[])

{

if (argc != 3) error("неверное число параметров","");

filebuf f1;

if (f1.open(argv[1],input) == 0)

error("не могу открыть входной файл",argv[1]);

istream from(&f1);

filebuf f2;

if (f2.open(argv[2],output) == 0)

error("не могу создать выходной файл",argv[2]);

ostream to(&f2);

char ch;

while (from.get(ch)) to.put(ch);

if (!from.eof() !! to.bad())

error("случилось нечто странное","");

}


Последовательность действий при создании ostream для именованного файла та же, что используется для стандартных потоков: (1) сначала создается буфер (здесь это делается посредством описания filebuf); (2) затем к нему подсоединяется файл (здесь это делается посредством открытия файла с помощью функции filebuf::open()); и, наконец, (3) создается сам ostream с filebuf в качестве параметра. Потоки ввода обрабатываются аналогично.
Файл может открываться в одной из двух мод:

enum open_mode { input, output };


Действие filebuf::open() возвращает 0, если не может открыть файл в соответствие с требованием. Если пользователь пытается открыть файл, которого не существует для output, он будет создан.
Перед завершением программа проверяет, находятся ли потоки в приемлемом состоянии (см. #8.4.2). При завершении программы открытые файлы неявно закрываются.
Файл можно также открыть одновременно для чтения и записи, но в тех случаях, когда это оказывается необходимо, парадигма потоков редко оказывается идеальной. Часто лучше рассматривать такой файл как вектор (гигантских размеров). Можно определить тип, который позволяет программе обрабатывать файл как вектор; см. Упражнения 8- 10.

Копирование Потоков

Есть возможность копировать потоки. Например:

cout = cerr;


В результате этого получаются две переменные, ссылающиеся на один и тот же поток. Главным образом это бывает полезно для того, чтобы сделать стандартное имя вроде cin ссылающимся на что-то другое (пример этого см. в #3.1.6)

Ввод

  8.4.1 Ввод Встроенных Типов
  8.4.2 Состояния Потока
  8.4.3 Ввод Типов, Определяемых Пользователем
  8.4.4 Инициализация Потоков Ввода

Ввод аналогичен выводу. Имеется класс istream, который предоставляет операцию >> ("взять из") для небольшого множества стандартных типов. Функция operator>> может определяться для типа, определяемого пользователем.

Ввод Встроенных Типов

Класс istream определяется так:

class istream {

// ...

public:

istream& operator>>(char*); // строка

istream& operator>>(char&); // символ

istream& operator>>(short&);

istream& operator>>(int&);

istream& operator>>(long&);

istream& operator>>(float&);

istream& operator>>(double&);

// ...

};


Функции ввода определяются в таком духе:

istream& istream::operator>>(char& c);

{

// пропускает пропуски

int a;

// неким образом читает символ в "a"

c = a;

}


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

class istream {

// ...

istream& get(char& c); // char

istream& get(char* p, int n, int ='\n'); // строка

};


Они обрабатывают символы пропуска так же, как остальные символы. Функция istream::get(char) читает один и тот же символ в свой параметр; другая istream::get читает не более n символов в вектор символов, начинающийся в p. Необязательный третий параметр используется для задания символа остановки (иначе, терминатора или ограничителя), то есть этот символ читаться не будет. Если будет встречен символ ограничитель, он останется как первый символ потока. По умолчанию вторая функция get будет читать самое большее n символов, но не больше чем одну строку, '\n' является ограничителем по умолчанию. Необязательный третий параметр задает символ, который читаться не будет. Например:

cin.get(buf,256,'\t');


будет читать в buf не более 256 символов, а если встретится табуляция ('\t'), то это приведет к возврату из get. В этом случае следующим символом, который будет считан из cin, будет '\t'.
Стандартный заголовочный файл определяет несколько функций, которые могут оказаться полезными при осуществлении ввода:

int isalpha(char) // 'a'..'z' 'A'..'Z'

int isupper(char) // 'A'..'Z'

int islower(char) // 'a'..'z'

int isdigit(char) // '0'..'9'

int isxdigit(char) // '0'..'9' 'a'..'f' 'A'..'F'

int isspase(char) // ' ' '\t' возврат новая строка

// перевод формата

int iscntrl(char) // управляющий символ

// (ASCII 0..31 и 127)

int ispunct(char) // пунктуация: ниодин из вышеперечисленных

int isalnum(char) // isalpha() | isdigit()

int isprint(char) // печатаемый: ascii ' '..'-'

int isgraph(char) // isalpha() | isdigit() | ispunct()

int isascii(char c) { return 0<=c &&c<=127; }


Все кроме isascii() реализуются внешне одинаково, с применением символа в качестве индекса в таблице атрибутов символов. Поэтому такие выражения, как

(('a'<=c && c<='z') || ('A'<=c && c<='Z')) // алфавитный


не только утомительно пишутся и чреваты ошибками (на машине с набором символов EBCDIC оно будет принимать неалфавитные символы), они также и менее эффективны, чем применение стандартной функции:

isalpha(c)

Состояния Потока

Каждый поток (istream или ostream) имеет ассоциированное с ним состояние, и обработка ошибок и нестандартных условий осуществляется с помощью соответствующей установки и проверки этого состояния.
Поток может находиться в одном из следующих состояний:

enum stream_state { _good, _eof, _fail, _bad };


Если состояние _good или _eof, значит последняя операция ввода прошла успешно. Если состояние _good, то следующая операция ввода может пройти успешно, в противном случае она закончится неудачей. Другими словами, применение операции ввода к потоку, который не находится в состоянии _good, является пустой операцией. Если делается попытка читать в переменную v, и операция оканчивается неудачей, значение v должно остаться неизменным (оно будет неизменным, если v имеет один из тех типов, которые обрабатываются функциями членами istream или ostream). Отличия между состояниями _fail и _bad очень незначительно и представляет интерес только для разработчиков операций ввода. В состоянии _fail предполагается, что поток не испорчен и никакие символы не потеряны. В состоянии _bad может быть все что угодно.
Состояние потока можно проверять например так:

switch (cin.rdstate()) {

case _good:

// последняя операция над cin прошла успешно

break;

case _eof:

// конец файла

break;

case _fail:

// некоего рода ошибка форматирования

// возможно, не слишком плохая

break;

case _bad:

// возможно, символы cin потеряны

break;

}


Для любой переменной z типа, для которого определены операции << и >>, копирующий цикл можно написать так:

while (cin>>z) cout << z << "\n";


Например, если z - вектор символов, этот цикл будет брать стандартный ввод и помещать его в стандартный вывод по одному слову (то есть, последовательности символов без пробела) на строку.
Когда в качестве условия используется поток, происходит проверка состояния потока и эта проверка проходит успешно (то есть, значение условия не ноль) только если состояние _good. В частности, в предыдущем цикле проверялось состояние istream, которое возвращает cin>>z. Чтобы обнаружить, почему цикл или проверка закончились неудачно, можно исследовать состояние. Такая проверка потока реализуется операцией преобразования (#6.3.2).
Делать проверку на наличие ошибок каждого ввода или вывода действительно не очень удобно, и обычно источником ошибок служит программист, не сделавший этого в том месте, где это существенно. Например, операции вывода обычно не проверяются, но они могут случайно не сработать. Парадигма потока ввода/вывода построена так, чтобы когда в C++ появится (если это произойдет) механизм обработки исключительных ситуаций (как средство языка или как стандартная библиотека) его будет легко применить для упрощения и стандартизации обработки ошибок в потоках ввода/вывода.

Инициализация Потоков Ввода

Естественно, тип istream, так же как и ostream, снабжен конструктором:

class istream {

// ...

istream(streambuf* s, int sk =1, ostream* t =0);

istream(int size, char* p, int sk =1);

istream(int fd, int sk =1, ostream* t =0);

};


Параметр sk задает, должны пропускаться пропуски или нет. Параметр t (необязательный) задает указатель на ostream, к которому прикреплен istream. Например, cin прикреплен к cout; это значит, что перед тем, как попытаться читать символы из своего файла, cin выполняет

cout.flush(); // пишет буфер вывода


С помощью функции istream::tie() можно прикрепить (или открепить, с помощью tie(0)) любой ostream к любому istream. Например:

int y_or_n(ostream& to, istream& from)

/*

"to", получает отклик из "from"

*/

{

ostream* old = from.tie(&to);

for (;;) {

cout << "наберите Y или N: ";

char ch = 0;

if (!cin.get(ch)) return 0;

if (ch != '\n') { // пропускает остаток строки

char ch2 = 0;

while (cin.get(ch2) && ch2 != '\n') ;

}

switch (ch) {

case 'Y':

case 'y':

case '\n':

from.tie(old); // восстанавливает старый tie

return 1;

case 'N':

case 'n':

from.tie(old); // восстанавливает старый tie

return 0;

default:

cout << "извините, попробуйте еще раз: ";

}

}

}


Когда используется буферизованный ввод (как это происходит по умолчанию), пользователь не может набрав только одну букву ожидать отклика. Система ждет появления символа новой строки. y_or_n() смотрит на первый символ строки, а остальные игнорирует.
Символ можно вернуть в поток с помощью функции istream::putback(char). Это позволяет программе "заглядывать вперед" в поток ввода.

Работа со Строками

Можно осуществлять действия, подобные вводу/выводу, над символьным вектором, прикрепляя к нему istream или ostream. Например, если вектор содержит обычную строку, завершающуюся нулем, для печати слов из этого вектора можно использовать приведенный выше копирующий цикл:

void word_per_line(char v[], int sz)

/*

печатет "v" размера "sz" по одному слову на строке

*/

{

istream ist(sz,v); // сделать istream для v

char b2[MAX]; // больше наибольшего слова

while (ist>>b2) cout << b2 << "\n";

}


Завершающий нулевой символ в этом случае интерпретируется как символ конца файла.
В помощью ostream можно отформатировать сообщения, которые не нужно печатать тотчас же:

char* p = new char[message_size];

ostream ost(message_size,p);

do_something(arguments,ost);

display(p);


Такая операция, как do_something, может писать в поток ost, передавать ost своим подоперациям и т.д. с помощью стандартных операций вывода. Нет необходимости делать проверку не переполнение, поскольку ost знает свою длину и когда он будет переполняться, он будет переходить в состояние _fail. И, наконец, display может писать сообщения в "настоящий" поток вывода. Этот метод может оказаться наиболее полезным, чтобы справляться с ситуациями, в которых окончательное отображение данных включает в себя нечто более сложное, чем работу с традиционным построчным устройством вывода. Например, текст из ost мог бы помещаться в располагающуюся где-то на экране область фиксированного размера.

Буферизация

При задании операций ввода/вывода мы никак не касались типов файлов, но ведь не все устройства можно рассматривать одинаково с точки зрения стратегии буферизации. Например, для ostream, подключенного к символьной строке, требуется буферизация другого вида, нежели для ostream, подключенного к файлу. С этими проблемами можно справиться, задавая различные буферные типы для разных потоков в момент инициализации (обратите внимание на три конструктора класса ostream). Есть только один набор операций над этими буферными типами, поэтому в функциях ostream нет кода, их различающего. Однако функции, которые обрабатывают переполнение сверху и снизу, виртуальные. Этого достаточно, чтобы справляться с необходимой в данное время стратегией буферизации. Это также служит хорошим примером применения виртуальных функций для того, чтобы сделать возможной однородную обработку логически эквивалентных средств с различной реализацией. Описание буфера потока в выглядит так:

struct streambuf { // управление буфером потока

char* base; // начало буфера

char* pptr; // следующий свободный char

char* qptr; // следующий заполненный char

char* eptr; // один из концов буфера

char alloc; // буфер, выделенный с помощью new

// Опустошает буфер:

// Возвращает EOF при ошибке и 0 в случае успеха

virtual int overflow(int c =EOF);

// Заполняет буфер

// Возвращет EOF при ошибке или конце ввода,

// иначе следующий char

virtual int underflow();

int snextc() // берет следующий char

{

return (++qptr==pptr) ? underflow() : *qptr&0377;

}

// ...

int allocate() // выделяет некоторое пространство буфера

streambuf() { /* ... */}

streambuf(char* p, int l) { /* ... */}

~streambuf() { /* ... */}

};


Обратите внимание, что здесь определяются указатели, необходимые для работы с буфером, поэтому обычные посимвольные действия можно определить (только один раз) в виде максимально эффективных inline- функций. Для каждой конкретной стратегии буферизации необходимо определять только функции переполнения overflow() и underflow(). Например:

struct filebuf : public streambuf {

int fd; // дескриптор файла

char opened; // файл открыт

int overflow(int c =EOF);

int underflow();

// ...

// Открывает файл:

// если не срабатывает, то возвращает 0,

// в случае успеха возвращает "this"

filebuf* open(char *name, open_mode om);

int close() { /* ... */ }

filebuf() { opened = 0; }

filebuf(int nfd) { /* ... */ }

filebuf(int nfd, char* p, int l) : (p,l) { /* ... */ }

~filebuf() { close(); }

};

int filebuf::underflow() // заполняет буфер из fd

{

if (!opened || allocate()==EOF) return EOF;

int count = read(fd, base, eptr-base);

if (count < 1) return EOF;

qptr = base;

pptr = base + count;

return *qptr & 0377;

}

Эффективность

Можно было бы ожидать, что раз ввод/вывод определен с помощью общедоступных средств языка, он будет менее эффективен, чем встроенное средство. На самом деле это не так. Для действий вроде "поместить символ в поток" используются inline-функции, единственные необходимые на этом уровне вызовы функций возникают из-за переполнения сверху и снизу. Для простых объектов (целое, строка и т.п.) требуется по одному вызову на каждый. Как выясняется, это не отличается от прочих средств ввода/вывода, работающих с объектами на этом уровне.

Упражнения

1. (*1.5) Считайте файл чисел с плавающей точкой, составьте из пар считанных чисел комплексные числа и выведите комплексные числа.

2. (*1.5) Определите тип name_and_address (имя_и_адрес). Определите для него << и >>. Скопируйте поток объектов name_and_address.

3. (*2) Постройте несколько функций для запроса и чтения различного вида информации. Простейший пример - функция y_or_n() в #8.4.4. Идеи: целое, число с плавающей точкой, имя файла, почтовый адрес, дата, личные данные и т.д. Постарайтесь сделать их защищенными от дурака.

4. (*1.5) Напишите программу, которая печатает (1) все буквы в нижнем регистре, (2) все буквы, (3) все буквы и цифры, (4) все символы, которые могут встречаться в идентификаторах C++ на вашей системе, (5) все символы пунктуации, (6) целые значения всех управляющих символов, (7) все символы пропуска, (8) целые значения всех символов пропуска, и (9) все печатаемые символы.

5. (*4) Реализуйте стандартную библиотеку ввода/вывода C () с помощью стандартной библиотеки ввода/вывода C++ ().

6. (*4) Реализуйте стандартную библиотеку ввода/вывода C++ () с помощью стандартной библиотеки ввода/вывода C ().

7. (*4) Реализуйте стандартные библиотеки C и C++ так, чтобы они могли использоваться одновременно.

8. (*2) Реализуйте класс, для которого [] перегружено для реализации случайного чтения символов из файла.

9. (*3) Как Упражнение 8, только сделайте, чтобы [] работало и для чтения, и для записи. Подсказка: сделайте, чтобы [] возвращало объект "дескрипторного типа", для которого присваивание означало бы присвоить файлу через дескриптор, а неявное преобразование в char означало бы чтение из файла через дескриптор.

10. (*2) Как Упражнение 9, только разрешите [] индексировать записи некоторого вида, а не символы.

11. (*3) Сделайте обобщенный вариант класса, определенного в Упражнении 10.

12. (*3.5) Разработайте и реализуйте операцию ввода по сопоставлению с образцом. Для спецификации образца используйте строки формата в духе printf. Должна быть возможность попробовать сопоставить со вводом несколько образцов для нахождения фактического формата. Можно было бы вывести класс ввода по образцу из istream.

13. (*4) Придумайте (и реализуйте) вид образцов, которые намного лучше.

[Назад] [Содержание] [Вперед]

Справочное руководство по С++

1. Введение

2. Договоренности о Лексике

2.1 Комментарии
2.2 Идентификаторы (имена)
2.3 Ключевые слова
2.4 Константы
2.5 Строки
2.6 Характеристики аппаратного обеспечения

3. Запись Синтаксиса

4. Имена и Типы

4.1 Область видимости
4.2 Определения
4.3 Компоновка
4.4 Классы памяти
4.5 Основные типы
4.6 Производные типы

5. Объекты и lvalue (адреса)

6. Преобразования

6.1 Символы и целые
6.2 Float и double
6.3 Плавающие и целые
6.4 Указатели и целые
6.5 Unsigned
6.6 Арифметические преобразования
6.7 Преобразования указателей
6.8 Преобразования ссылок

7. Выражения

7.1 Основные выражения
7.2 Унарные операции
7.3 Мультипликативные операции
7.4 Аддитивные операции
7.5 Операции сдвига
7.6 Операции отношения
7.7 Операции равенства
7.8 Операция побитовое И
7.9 Операция побитовое исключающее ИЛИ
7.10 Операция побитовое включающее ИЛИ
7.11 Операция логическое И
7.12 Операция логическое ИЛИ
7.13 Условная операция
7.14 Операции присваивания
7.15 Операция запятая
7.16 Перегруженные операции

8. Описания

8.1 Спецификаторы класса памяти
8.2 Спецификаторы Типа
8.3 Описатели
8.4 Смысл описателей
8.5 Описания классов
8.6 Инициализация
8.7 Имена типов
8.8 Определение типа typedef
8.9 Перегруженные имена функций
8.10 Описание перечисления
8.11 Описание Asm

9. Операторы

9.1 Оператор выражение
9.2 Составной оператор, или блок
9.3 Условный оператор
9.4 Оператор while
9.5 Оператор do
9.6 Оператор for
9.7 Оператор switch
9.8 Оператор break
9.9 Оператор continue
9.10 Оператор return
9.11 Оператор goto
9.12 Помеченные операторы
9.13 Пустой оператор
9.14 Оператор delete
9.15 Оператор asm

10. Внешние Определения

10.1 Определения функций
10.2 Определения внешних данных

11. Правила Области Видимости

12. Командные Строки Компилятора

12.1 Замена идентификаторов
12.2 Включение файлов
12.3 Условная компиляция
12.4 Управление строкой

13. Неявные Описания

14. Обзор Типов

14.1 Классы
14.2 Функции
14.3 Массивы, указатели и индексирование
14.4 Явные преобразования указателей

15. Константные Выражения

16. Соображения Мобильности

17. Свободная Память

18. Краткое Изложение Синтаксиса

18.1 Выражения
18.2 Описания
18.3 Операторы
18.4 Внешние определения
18.5 Препроцессор

19. Отличия от "старого C"

19.1 Расширения

[Назад] [Содержание] [Вперед]

Введение

Язык программирования C++ - это C*1, расширенный введением классов, inline-функций, перегруженных операций, перегруженных имен функций, константных типов, ссылок, операций управления свободной памятью, проверки параметров функций. Коротко различия между С++ и "старым С" приведены в #15. В этом руководстве описывается язык по состоянию на Июнь 1985.

Договоренности о Лексике

Есть шесть классов лексем: идентификаторы, ключевые слова, константы, строки, операторы и прочие разделители. Символы пробела, табуляции и новой строки, а также комментарии (собирательно - "белые места"), как описано ниже, игнорируются, за исключением тех случаев, когда они служат разделителями лексем. Некое пустое место необходимо для разделения идентификаторов, ключевых слов и констант, которые в противном случае окажутся соприкасающимися.
Если входной поток разобран на лексемы до данного символа, принимается, что следующая лексема содержит наиболее длинную строку символов из тех, что могут составить лексему.

Комментарии

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

2.2 Идентификаторы (имена)

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

Ключевые слова

Следующие идентификаторы зарезервированы для использования в качестве ключевых слов и не могут использоваться иным образом:

asm auto break case char

class const continue default delete

do double else enum extern

float for friend goto if

inline int long new operator

overload public register return short

sizeof static struct switch this

typedef union unsigned virtual void

while


Идентификаторы signed и volatile зарезервированы для применения в будущем.

Константы

  2.4.1 Целые константы
  2.4.2 Явно заданные длинные константы
  2.4.3 Символьные константы
  2.4.4 Константы с плавающей точкой
  2.4.5 Перечислимые константы
  2.4.6 Описанные константы

Как описано ниже, есть несколько видов констант. В #2.6 приводится краткая сводка аппаратных характеристик, которые влияют на их размеры.

Целые константы

Целая константа, состоящая из последовательности цифр, считается восьмиричной, если она начинается с 0 (цифры ноль), и десятичной в противном случае. Цифры 8 и 9 не являются восьмиричными цифрами. Последовательность цифр, которой предшествует 0х или 0Х, воспринимается как шестнадцатеричное целое. В шестнадцатеричные цифры входят буквы от а или А до f или F, имеющие значения от 10 до 15. Десятичная константа, значение которой превышает наибольшее машинное целое со знаком, считается длинной (long); восьмеричная и шестнадцатеричная константа, значение которой превышает наибольшее машинное целое со знаком, считается long; в остальных случаях целые константы считаются int.

Символьные константы

Символьная константа состоит из символа, заключенного в одиночные кавычки (апострофы), как, например, 'х'. Значением символьной константы является численное значение символа в машинном наборе символов (алфавите). Символьные константы считаются данными типа int.
Некоторые неграфические символы, одиночная кавычка ' и обратная косая \, могут быть представлены в соответствие со следующей таблицей escape-последовательностей:

символ новой строки NL(LF) \n
горизонтальная табуляция NT \t
вертикальная табуляция VT \v
возврат на шаг BS \b
возврат каретки CR \r
перевод формата FF \f
обратная косая \ \\
одиночная кавычка (апостроф) ' \'
набор битов 0ddd \ddd
набор битов 0xddd \xddd


Escape-последовательность \ddd состоит из обратной косой, за которой следуют 1, 2 или 3 восьмеричных цифры, задающие значение требуемого символа. Специальным случаем такой конструкции является \0 (не следует ни одной цифры), задающая пустой символ NULL. Escape-последовательность \xddd состоит из обратной косой, за которой следуют 1, 2 или 3 шестнадцатиричных цифры, задающие значение требуемого символа. Если следующий за обратной косой символ не является одним из перечисленных, то обратная косая игнорируется.

Перечислимые константы

Имена, описанные как перечислители, (см. #8.5) являются константами типа int.

Описанные константы

Объект (#5) любого типа может быть определен как имеющий постоянное значение во всей области видимости (#4.1) его имени. В случае указателей для достижения этого используется декларатор *const; для объектов, не являющихся указателями, используется описатель const (#8.2).

Строки

Строка есть последовательность символов, заключенная в двойные кавычки: "...". Строка имеет тип "массив символов" и класс памяти static (см. #4), она инициализируется заданными символами. Все строки, даже если они записаны одинаково, различны. Компилятор располагает в конце каждой строки нулевой (пустой) байт \0 с тем, чтобы сканирующая строку программа могла найти ее конец. В строке перед символом двойной кавычки " обязательно должен стоять \; кроме того, могут использоваться те же escape-последовательности, что были описаны для символьных констант. И, наконец, символ новой строки может появляться только сразу после \; тогда оба, - \ и символ новой строки, - игнорируются.

Запись Синтаксиса

По используемым в данном руководстве синтаксическим правилам записи синтаксические категории выделяются курсивом а литеральные слова и символы шрифтом постоянной ширины*2. Альтернативные категории записываются на разных строках. Необязательный терминальный или нетерминальный символ обозначается нижним индексом "opt", так что

{ выражение opt }


указывает на необязательность выражения в фигурных скобках. Синтаксис кратко изложен в #14.

Имена и Типы

Имя обозначает (денотирует) объект, функцию, тип, значение или метку. Имя вводится в программе описанием (#8). Имя может использоваться только внутри области текста программы, называемой его областью видимости. Имя имеет тип, определяющий его использование. Объект - это область памяти. Объект имеет класс памяти, определяющий его время жизни. Смысл значения, обнаруженного в объекте, определяется типом имени, использованного для доступа к нему.

О

Наши рекомендации