Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
237
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

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. Определение объекта

В самом простом случае оператор определения объекта состоит из спецификатора типа и

double salary; double wage; int month; int day;

int year;

имени объекта и заканчивается точкой с запятой. Например: unsigned long distance;

В одном операторе можно определить несколько объектов одного типа. В этом случае их имена перечисляются через запятую:

double salary, wage; int month,

day, year; unsigned long distance;

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

Использование подобных переменных очень распространенная ошибка, которую к тому же трудно обнаружить. Рекомендуется явно указывать начальное значение объекта, по крайней мере в тех случаях, когда неизвестно, может ли объект инициализировать сам себя. Механизм классов вводит понятие конструктора по умолчанию, который служит для присвоения значений по умолчанию. (Мы уже сказали об этом в разделе 2.3. Разговор о конструкторах по умолчанию будет продолжен немного позже, в разделах 3.11 и 3.15,

int main() {

где мы будем разбирать классы string и complex из стандартной библиотеки.)

//неинициализированный локальный объект int ival;

//объект типа string инициализирован

//конструктором по умолчанию

string project;

С++ для начинающих

84

// ...

}

Начальное значение может быть задано прямо в операторе определения переменной. В С++ допустимы две формы инициализации переменной явная, с использованием

int ival = 1024;

оператора присваивания:

string project = "Fantasia 2000";

int ival( 1024 );

инеявная, с заданием начального значения в скобках: string project( "Fantasia 2000" );

Оба варианта эквивалентны и задают начальные значения для целой переменной ival

как 1024 и для строки project как "Fantasia 2000".

double salary = 9999.99, wage = salary + 0.01; int month = 08;

Явную инициализацию можно применять и при определении переменных списком: day = 07, year = 1955;

Переменная становится видимой (и допустимой в программе) сразу после ее определения,

поэтому мы могли проинициализировать переменную wage суммой только что определенной переменной salary с некоторой константой. Таким образом, определение:

// корректно, но бессмысленно int bizarre = bizarre;

является синтаксически допустимым, хотя и бессмысленным.

// ival получает значение 0, а dval - 0.0

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

int ival = int(); double dval = double();

//int() применяется к каждому из 10 элементов

Вследующем определении:

vector< int > ivec( 10 );

С++ для начинающих

85

к каждому из десяти элементов вектора применяется инициализация с помощью int(). (Мы уже говорили о классе vector в разделе 2.8. Более подробно об этом см. в разделе

3.10 и главе 6.)

Переменная может быть инициализирована выражением любой сложности, включая

#include <cmath>

вызовы функций. Например:

#include <string>

double price = 109.99, discount = 0.16; double sale_price( price * discount ); string pet( "wrinkles" );

extern int get_value(); int val = get_value();

unsigned abs_val = abs( val );

abs() стандартная функция, возвращающая абсолютное значение параметра. get_value() некоторая пользовательская функция, возвращающая целое значение.

Упражнение 3.3

(a)int car = 1024, auto = 2048;

(b)int ival = ival;

(c)int ival( int() );

(d)double salary = wage = 9999.99;

Какие из приведенных ниже определений переменных содержат синтаксические ошибки?

(e) cin >> int input_value;

Упражнение 3.4

Объясните разницу между l-значением и r-значением. Приведите примеры. Упражнение 3.5

Найдите отличия в использовании переменных name и student в первой и второй

(a)extern string name;

string name( "exercise 3.5a" );

(b)extern vector<string> students;

строчках каждого примера: vector<string> students;

Упражнение 3.6

Какие имена объектов недопустимы в С++? Измените их так, чтобы они стали синтаксически правильными:

С++ для начинающих

86

 

(a) int double = 3.14159;

(b) vector< int > _;

 

 

(c) string namespase;

(d) string catch-22;

 

(e) char 1_or_2 = '1';

(f) float Float = 3.14f;

 

 

 

 

Упражнение 3.7

В чем разница между следующими глобальными и локальными определениями

string global_class; int global_int;

переменных?

int main() {

int local_int; string local_class;

// ...

}

3.3. Указатели

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

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

указатель на int, содержащий значение адреса 1000, направлен на область памяти 1000-1003 (в 32-битной системе);

указатель на double, содержащий значение адреса 1000, направлен на область памяти 1000-1007 (в 32-битной системе).

int

*ip1, *ip2;

complex<double>

*cp;

string

*pstring;

vector<int>

*pvec;

Вот несколько примеров:

5 На самом деле для указателей на функции это не совсем так: они отличаются от указателей на данные (см. раздел 7.9).

С++ для начинающих

87

double *dp;

Указатель обозначается звездочкой перед именем. В определении переменных списком звездочка должна стоять перед каждым указателем (см. выше: ip1 и ip2). В примере ниже lp указатель на объект типа long, а lp2 объект типа long:

long *lp, lp2;

В следующем случае fp интерпретируется как объект типа float, а fp2 указатель на него:

float fp, *fp2;

Оператор разыменования (*) может отделяться пробелами от имени и даже непосредственно примыкать к ключевому слову типа. Поэтому приведенные определения

string *ps;

синтаксически правильны и совершенно эквивалентны: string* ps;

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

//внимание: ps2 не указатель на строку!

string* ps, ps2;

Можно предположить, что и ps, и ps2 являются указателями, хотя указатель только первый из них.

Если значение указателя равно 0, значит, он не содержит никакого адреса объекта.

Пусть задана переменная типа int:

int ival = 1024;

//pi инициализирован нулевым адресом int *pi = 0;

//pi2 инициализирован адресом ival int *pi2 = &ival;

//правильно: pi и pi2 содержат адрес ival pi = pi2;

//pi2 содержит нулевой адрес

Ниже приводятся примеры определения и использования указателей на int pi и pi2: pi2 = 0;

Указателю не может быть присвоена величина, не являющаяся адресом:

С++ для начинающих

88

// ошибка: pi не может принимать значение int

pi = ival

Точно так же нельзя присвоить указателю одного типа значение, являющееся адресом

double dval;

объекта другого типа. Если определены следующие переменные: double *ps = &dval;

// ошибки компиляции

то оба выражения присваивания, приведенные ниже, вызовут ошибку компиляции:

// недопустимое присваивание типов данных: int* <== double* pi = pd

pi = &dval;

Дело не в том, что переменная pi не может содержать адреса объекта dval адреса объектов разных типов имеют одну и ту же длину. Такие операции смешения адресов запрещены сознательно, потому что интерпретация объектов компилятором зависит от типа указателя на них.

Конечно, бывают случаи, когда нас интересует само значение адреса, а не объект, на который он указывает (допустим, мы хотим сравнить этот адрес с каким-то другим). Для разрешения таких ситуаций введен специальный указатель void, который может

// правильно: void* может содержать

указывать на любой тип данных, и следующие выражения будут правильны:

// адреса любого типа void *pv = pi;

pv = pd;

Тип объекта, на который указывает void*, неизвестен, и мы не можем манипулировать этим объектом. Все, что мы можем сделать с таким указателем, – присвоить его значение другому указателю или сравнить с какой-либо адресной величиной. (Более подробно мы расскажем об указателе типа void в разделе 4.14.)

Для того чтобы обратиться к объекту, имея его адрес, нужно применить операцию

разыменования, или косвенную адресацию, обозначаемую звездочкой (*). Имея

int ival = 1024;, ival2 = 2048;

следующие определения переменных: int *pi = &ival;

мы можем читать и сохранять значение ival, применяя операцию разыменования к указателю pi:

С++ для начинающих

89

//косвенное присваивание переменной ival значения ival2

*pi = ival2;

//косвенное использование переменной ival как rvalue и lvalue *pi = abs(*pi); // ival = abs(ival);

*pi = *pi + 1; // ival = ival + 1;

Когда мы применяем операцию взятия адреса (&) к объекту типа int, то получаем

результат типа int*

int *pi = &ival;

Если ту же операцию применить к объекту типа int* (указатель на int), мы получим указатель на указатель на int, т.е. int**. int** это адрес объекта, который содержит адрес объекта типа int. Разыменовывая ppi, мы получаем объект типа int*, содержащий адрес ival. Чтобы получить сам объект ival, операцию разыменования к

int **ppi = π int *pi2 = *ppi;

cout << "Значение ival\n"

<<"явное значение: " << ival << "\n"

<<"косвенная адресация: " << *pi << "\n"

<<"дважды косвенная адресация: " << **ppi << "\n"

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];

С++ для начинающих

90

int *iter = &ia[0];

int *iter_end = &ia[10];

while (iter != iter_end) { do_something_with_value (*iter); ++iter;

}

Упражнение 3.8

int ival = 1024, ival2 = 2048;

Даны определения переменных:

int *pi1 = &ival, *pi2 = &ival2, **pi3 = 0;

Что происходит при выполнении нижеследующих операций присваивания? Допущены

(a) ival = *pi3;

(e) pi1 = *pi3;

(b) *pi2 = *pi3;

(f) ival = *pi1;

(c) ival = pi2;

(g) pi1 = ival;

ли в данных примерах ошибки?

 

(d) pi2 = *pi1;

(h) pi3 = &pi2;

Упражнение 3.9

 

Работа с указателями один из важнейших аспектов С и С++, однако в ней легко

pi = &ival;

допустить ошибку. Например, код pi = pi + 1024;

почти наверняка приведет к тому, что pi будет указывать на случайную область памяти. Что делает этот оператор присваивания и в каком случае он не приведет к ошибке?

Упражнение 3.10

Данная программа содержит ошибку, связанную с неправильным использованием

int foobar(int *pi) { *pi = 1024;

return *pi;

}

int main() { int *pi2 = 0;

int ival = foobar(pi2); return 0;

указателей:

}

В чем состоит ошибка? Как можно ее исправить?

С++ для начинающих

91

Упражнение 3.11

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

3.4. Строковые типы

В С++ поддерживаются два типа строк встроенный тип, доставшийся от С, и класс string из стандартной библиотеки С++. Класс string предоставляет гораздо больше возможностей и поэтому удобней в применении, однако на практике нередки ситуации, когда необходимо пользоваться встроенным типом либо хорошо понимать, как он устроен. (Одним из примеров может являться разбор параметров командной строки, передаваемых в функцию main(). Мы рассмотрим это в главе 7.)

3.4.1. Встроенный строковый тип

Как уже было сказано, встроенный строковый тип перешел к С++ по наследству от С. Строка символов хранится в памяти как массив, и доступ к ней осуществляется при помощи указателя типа char*. Стандартная библиотека С предоставляет набор функций

//возвращает длину строки int strlen( const char* );

//сравнивает две строки

int strcmp( const char*, const char* );

// копирует одну строку в другую

для манипулирования строками. Например: char* strcpy( char*, const char* );

Стандартная библиотека С является частью библиотеки С++. Для ее использования мы должны включить заголовочный файл:

#include <cstring>

Указатель на char, с помощью которого мы обращаемся к строке, указывает на соответствующий строке массив символов. Даже когда мы пишем строковый литерал,

например

const char *st = "Цена бутылки вина\n";

компилятор помещает все символы строки в массив и затем присваивает st адрес первого элемента массива. Как можно работать со строкой, используя такой указатель?

Обычно для перебора символов строки применяется адресная арифметика. Поскольку строка всегда заканчивается нулевым символом, можно увеличивать указатель на 1, пока очередным символом не станет нуль. Например: