Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
80 Теперь получить любую степень нужного числа не составит никакого труда. Вот как реализуется последняя наша задача – напечатать таблицу степеней двойки от 0 до 15: int main() { int val = 2; int exp = 15; cout << " Степени 2\n"; for ( int cnt=0; cnt <= exp; ++cnt ) cout << cnt << ": " << pow( val, cnt ) << endl; return 0; } Конечно, наша функция pow() все еще недостаточно обобщена и недостаточно надежна. Она не может оперировать вещественными числами, неправильно возводит числа в отрицательную степень – всегда возвращает 1. Результат возведения большого числа в большую степень может не поместиться в переменную типа int, и тогда будет возвращено некоторое случайное неправильное значение. Видите, как непросто, оказывается, писать функции, рассчитанные на широкое применение? Гораздо сложнее, чем реализовать конкретный алгоритм, направленный на решение конкретной задачи. 3.2.1. Что такое переменная Переменная, или объект – это именованная область памяти, к которой мы имеем доступ из программы; туда можно помещать значения и затем извлекать их. Каждая переменная С++ имеет определенный тип, который характеризует размер и расположение этой области памяти, диапазон значений, которые она может хранить, и набор операций, применимых к этой переменной. Вот пример определения пяти объектов разных типов: char delimiter; Переменная, как и литерал, имеет определенный тип и хранит свое значение в некоторой области памяти. Адресуемость – вот чего не хватает литералу. С переменной ассоциируются две величины: • собственно значение, или r-значение (от read value – значение для чтения), которое хранится в этой области памяти и присуще как переменной, так и литералу; • значение адреса области памяти, ассоциированной с переменной, или l- значение (от location value – значение местоположения) – место, где хранится r- значение; присуще только объекту. #include С++ для начинающих 81 В выражении ch = ch - '0'; переменная ch находится и слева и справа от символа операции присваивания. Справа расположено значение для чтения (ch и символьный литерал '0'): ассоциированные с переменной данные считываются из соответствующей области памяти. Слева – значение местоположения: в область памяти, соотнесенную с переменной ch, помещается результат вычитания. В общем случае левый операнд операции присваивания должен быть l- значением. Мы не можем написать следующие выражения: salary + salary * 0.10 = new_salary; Оператор определения переменной выделяет для нее память. Поскольку объект имеет только одну ассоциированную с ним область памяти, такой оператор может встретиться в программе только один раз. Если же переменная, определенная в одном исходном файле, должна быть использована в другом, появляются проблемы. Например: // определяет объект fileName string fileName; // ... присвоить fileName значение // файл module1.C // использует объект fileName // увы, не компилируется: // fileName не определен в module1.C ifstream input_file( fileName ); С++ требует, чтобы объект был известен до первого обращения к нему. Это вызвано необходимостью гарантировать правильность использования объекта в соответствии с его типом. В нашем примере модуль module1.C вызовет ошибку компиляции, поскольку переменная fileName не определена в нем. Чтобы избежать этой ошибки, мы должны сообщить компилятору об уже определенной переменной fileName. Это делается с помощью инструкции объявления переменной: // использует объект fileName // fileName объявляется, то есть программа получает // информацию об этом объекте без вторичного его определения extern string fileName; // ошибки компиляции: значения слева не являются l-значениями // ошибка: литерал - не l-значение 0 = 1; // ошибка: арифметическое выражение - не l-значение // файл module0.C // файл module1.C С++ для начинающих 82 ifstream input_file( fileName ) Объявление переменной сообщает компилятору, что объект с данным именем, имеющий данный тип, определен где-то в программе. Память под переменную при ее объявлении не отводится. (Ключевое слово extern рассматривается в разделе 8.2.) Программа может содержать сколько угодно объявлений одной и той же переменной, но определить ее можно только один раз. Такие объявления удобно помещать в заголовочные файлы, включая их в те модули, которые этого требуют. Так мы можем хранить информацию об объектах в одном месте и обеспечить удобство ее модификации в случае надобности. (Более подробно о заголовочных файлах мы поговорим в разделе 8.2.) 3.2.2. Имя переменной Имя переменной, или идентификатор, может состоять из латинских букв, цифр и символа подчеркивания. Прописные и строчные буквы в именах различаются. Язык С++ не ограничивает длину идентификатора, однако пользоваться слишком длинными именами типа gosh_this_is_an_impossibly_name_to_type неудобно. Некоторые слова являются ключевыми в С++ и не могут быть использованы в качестве идентификаторов; в таблице 3.1 приведен их полный список. Таблица 3.1. Ключевые слова C++ asm auto bool break case catch char class const const_cast continue default delete do double dynamic_cast else enum explicit export extern false float for friend goto if inline int long mutable namespace new operator private protected public register reinterpret_cast return short signed sizeof static static_cast struct switch template this throw true try typedef typeid typename union unsigned using virtual void volatile wchar_t while Чтобы текст вашей программы был более понятным, мы рекомендуем придерживаться общепринятых соглашений об именах объектов: • имя переменной обычно пишется строчными буквами, например index (для сравнения: Index – это имя типа, а INDEX – константа, определенная с помощью директивы препроцессора #define); С++ для начинающих 83 • идентификатор должен нести какой-либо смысл, поясняя назначение объекта в программе, например: birth_date или salary; если такое имя состоит из нескольких слов, как, например, birth_date, то принято либо разделять слова символом подчеркивания (birth_date), либо писать каждое следующее слово с большой буквы (birthDate). Замечено, что программисты, привыкшие к ОбъектноОриентированномуПодходу предпочитают выделять слова заглавными буквами, в то время как те_кто_много_писал_на_С используют символ подчеркивания. Какой из двух способов лучше – вопрос вкуса. 3.2.3. Определение объекта В самом простом случае оператор определения объекта состоит из спецификатора типа и имени объекта и заканчивается точкой с запятой. Например: unsigned long distance; В одном операторе можно определить несколько объектов одного типа. В этом случае их имена перечисляются через запятую: double salary, wage; int month, day, year; unsigned long distance; Простое определение переменной не задает ее начального значения. Если объект определен как глобальный, спецификация С++ гарантирует, что он будет инициализирован нулевым значением. Если же переменная локальная либо динамически размещаемая (с помощью оператора new), ее начальное значение не определено, то есть она может содержать некоторое случайное значение. Использование подобных переменных – очень распространенная ошибка, которую к тому же трудно обнаружить. Рекомендуется явно указывать начальное значение объекта, по крайней мере в тех случаях, когда неизвестно, может ли объект инициализировать сам себя. Механизм классов вводит понятие конструктора по умолчанию, который служит для присвоения значений по умолчанию. (Мы уже сказали об этом в разделе 2.3. Разговор о конструкторах по умолчанию будет продолжен немного позже, в разделах 3.11 и 3.15, где мы будем разбирать классы string и complex из стандартной библиотеки.) // неинициализированный локальный объект int ival; // объект типа string инициализирован // конструктором по умолчанию string project; double salary; double wage; int month; int day; int year; int main() { С++ для начинающих 84 // ... } Начальное значение может быть задано прямо в операторе определения переменной. В С++ допустимы две формы инициализации переменной – явная, с использованием оператора присваивания: string project = "Fantasia 2000"; и неявная, с заданием начального значения в скобках: string project( "Fantasia 2000" ); Оба варианта эквивалентны и задают начальные значения для целой переменной ival как 1024 и для строки project как "Fantasia 2000". Явную инициализацию можно применять и при определении переменных списком: day = 07, year = 1955; Переменная становится видимой (и допустимой в программе) сразу после ее определения, поэтому мы могли проинициализировать переменную wage суммой только что определенной переменной salary с некоторой константой. Таким образом, определение: // корректно, но бессмысленно int bizarre = bizarre; является синтаксически допустимым, хотя и бессмысленным. Встроенные типы данных имеют специальный синтаксис для задания нулевого значения: int ival = int(); double dval = double(); В следующем определении: vector< int > ivec( 10 ); int ival = 1024; int ival( 1024 ); double salary = 9999.99, wage = salary + 0.01; int month = 08; // ival получает значение 0, а dval - 0.0 // int() применяется к каждому из 10 элементов С++ для начинающих 85 к каждому из десяти элементов вектора применяется инициализация с помощью int(). (Мы уже говорили о классе vector в разделе 2.8. Более подробно об этом см. в разделе 3.10 и главе 6.) Переменная может быть инициализирована выражением любой сложности, включая вызовы функций. Например: #include – стандартная функция, возвращающая абсолютное значение параметра. get_value() – некоторая пользовательская функция, возвращающая целое значение. Упражнение 3.3 Какие из приведенных ниже определений переменных содержат синтаксические ошибки? (e) cin >> int input_value; Упражнение 3.4 Объясните разницу между l-значением и r-значением. Приведите примеры. Упражнение 3.5 Найдите отличия в использовании переменных name и student в первой и второй строчках каждого примера: vector Упражнение 3.6 Какие имена объектов недопустимы в С++? Измените их так, чтобы они стали синтаксически правильными: #include (a) int car = 1024, auto = 2048; (b) int ival = ival; (c) int ival( int() ); (d) double salary = wage = 9999.99; (a) extern string name; string name( "exercise 3.5a" ); (b) extern vector С++ для начинающих 86 (e) char 1_or_2 = '1'; (f) float Float = 3.14f; Упражнение 3.7 В чем разница между следующими глобальными и локальными определениями переменных? int main() { int local_int; string local_class; // ... } 3.3. Указатели Указатели и динамическое выделение памяти были вкратце представлены в разделе 2.2. Указатель – это объект, содержащий адрес другого объекта и позволяющий косвенно манипулировать этим объектом. Обычно указатели используются для работы с динамически созданными объектами, для построения связанных структур данных, таких, как связанные списки и иерархические деревья, и для передачи в функции больших объектов – массивов и объектов классов – в качестве параметров. Каждый указатель ассоциируется с некоторым типом данных, причем их внутреннее представление не зависит от внутреннего типа: и размер памяти, занимаемый объектом типа указатель, и диапазон значений у них одинаков5. Разница состоит в том, как компилятор воспринимает адресуемый объект. Указатели на разные типы могут иметь одно и то же значение, но область памяти, где размещаются соответствующие типы, может быть различной: • указатель на int, содержащий значение адреса 1000, направлен на область памяти 1000-1003 (в 32-битной системе); • указатель на double, содержащий значение адреса 1000, направлен на область памяти 1000-1007 (в 32-битной системе). Вот несколько примеров: 5 На самом деле для указателей на функции это не совсем так: они отличаются от указателей на данные (см. раздел 7.9). (a) int double = 3.14159; (b) vector< int > _; (c) string namespase; (d) string catch-22; string global_class; int global_int; int *ip1, *ip2; complex С++ для начинающих 87 double *dp; Указатель обозначается звездочкой перед именем. В определении переменных списком звездочка должна стоять перед каждым указателем (см. выше: ip1 и ip2). В примере ниже lp – указатель на объект типа long, а lp2 – объект типа long: long *lp, lp2; В следующем случае fp интерпретируется как объект типа float, а fp2 – указатель на него: float fp, *fp2; Оператор разыменования (*) может отделяться пробелами от имени и даже непосредственно примыкать к ключевому слову типа. Поэтому приведенные определения синтаксически правильны и совершенно эквивалентны: string* ps; Однако рекомендуется использовать первый вариант написания: второй способен ввести в заблуждение, если добавить к нему определение еще одной переменной через запятую: //внимание: ps2 не указатель на строку! string* ps, ps2; Можно предположить, что и ps, и ps2 являются указателями, хотя указатель – только первый из них. Если значение указателя равно 0, значит, он не содержит никакого адреса объекта. Пусть задана переменная типа int: int ival = 1024; Ниже приводятся примеры определения и использования указателей на int pi и pi2: pi2 = 0; Указателю не может быть присвоена величина, не являющаяся адресом: string *ps; //pi инициализирован нулевым адресом int *pi = 0; // pi2 инициализирован адресом ival int *pi2 = &ival; // правильно: pi и pi2 содержат адрес ival pi = pi2; // pi2 содержит нулевой адрес С++ для начинающих 88 pi = ival Точно так же нельзя присвоить указателю одного типа значение, являющееся адресом объекта другого типа. Если определены следующие переменные: double *ps = &dval; то оба выражения присваивания, приведенные ниже, вызовут ошибку компиляции: // недопустимое присваивание типов данных: int* <== double* pi = pd pi = &dval; Дело не в том, что переменная pi не может содержать адреса объекта dval – адреса объектов разных типов имеют одну и ту же длину. Такие операции смешения адресов запрещены сознательно, потому что интерпретация объектов компилятором зависит от типа указателя на них. Конечно, бывают случаи, когда нас интересует само значение адреса, а не объект, на который он указывает (допустим, мы хотим сравнить этот адрес с каким-то другим). Для разрешения таких ситуаций введен специальный указатель void, который может указывать на любой тип данных, и следующие выражения будут правильны: // адреса любого типа void *pv = pi; pv = pd; Тип объекта, на который указывает void*, неизвестен, и мы не можем манипулировать этим объектом. Все, что мы можем сделать с таким указателем, – присвоить его значение другому указателю или сравнить с какой-либо адресной величиной. (Более подробно мы расскажем об указателе типа void в разделе 4.14.) Для того чтобы обратиться к объекту, имея его адрес, нужно применить операцию разыменования, или косвенную адресацию, обозначаемую звездочкой (*). Имея следующие определения переменных: int *pi = &ival; мы можем читать и сохранять значение ival, применяя операцию разыменования к указателю pi: // ошибка: pi не может принимать значение int double dval; // ошибки компиляции // правильно: void* может содержать int ival = 1024;, ival2 = 2048; С++ для начинающих 89 *pi = *pi + 1; // ival = ival + 1; Когда мы применяем операцию взятия адреса (&) к объекту типа int, то получаем результат типа int* int *pi = &ival; Если ту же операцию применить к объекту типа int* (указатель на int), мы получим указатель на указатель на int, т.е. int**. int** – это адрес объекта, который содержит адрес объекта типа int. Разыменовывая ppi, мы получаем объект типа int*, содержащий адрес ival. Чтобы получить сам объект ival, операцию разыменования к ppi необходимо применить дважды. << endl; Указатели могут быть использованы в арифметических выражениях. Обратите внимание на следующий пример, где два выражения производят совершенно различные действия: int i, j, k; int *pi = &i; // i = i + 2 *pi = *pi + 2; // увеличение адреса, содержащегося в pi, на 2 pi = pi + 2; К указателю можно прибавлять целое значение, можно также вычитать из него. Прибавление к указателю 1 увеличивает содержащееся в нем значение на размер области памяти, отводимой объекту соответствующего типа. Если тип char занимает 1 байт, int – 4 и double – 8, то прибавление 2 к указателям на char, int и double увеличит их значение соответственно на 2, 8 и 16. Как это можно интерпретировать? Если объекты одного типа расположены в памяти друг за другом, то увеличение указателя на 1 приведет к тому, что он будет указывать на следующий объект. Поэтому арифметические действия с указателями чаще всего применяются при обработке массивов; в любых других случаях они вряд ли оправданы. Вот как выглядит типичный пример использования адресной арифметики при переборе элементов массива с помощью итератора: int ia[10]; // косвенное присваивание переменной ival значения ival2 *pi = ival2; // косвенное использование переменной ival как rvalue и lvalue *pi = abs(*pi); // ival = abs(ival); int **ppi = π int *pi2 = *ppi; cout << " Значение ival\n" << " явное значение: " << ival << "\n" << " косвенная адресация: " << *pi << "\n" << " дважды косвенная адресация: " << **ppi << "\n" |