Прочие вопросы языка С++.
Приведение типов, ключевые слова const и static.
1. Приведение типов.
1.1. Явное и неявное приведение типов.
В языке Си возможны две следующие записи:
Однако в некоторых случаях неявное приведение может вызвать ошибку. Например, следующий код:
int i = 2;
char * c = i;
в языке С++ вызовет ошибку: неверное приведение из ‘int’ к ‘char*’ (invalid conversion from ‘int’ to ‘char*’), а в языке С – предупреждение: инициализация указателя из целого числа без приведения (initialization makes pointer from integer without a cast).
Аналогичный код, но с явным приведением типов ошибки не вызовет:
int i = 2;
char * c = (char*) i;
То есть, неявное приведение отслеживается компилятором, а при явном приведении вся ответственность ложится на программиста. Таким образом, явно можно привести любой тип к любому другому, что не всегда хорошо.
Note: здесь и далее компиляция кода производилась с помощью компилятора gcc 4.3.
Неявное приведение типов используется в 3-х случаях:
- Инициализация
- Присваивание
- Передача параметра
1.2. Приведения типов, определенные пользователем.
Рассмотрим класс целочисленных комплексных (гауссовых) чисел Complex с конструктором:
Complex( int re = 0; int im = 0 );
Наличие такого конструктора дает возможность выполнить приведение типа int к пользовательскому классу Complex:
Complex N = 2;
Однако нельзя написать строку с обратным приведением типов:
int x = N;
так как у типа int нет конструкторов.
Если необходимо делать приведение типов из класса А в класс В и наоборот, то если классы написаны нами, надо просто добавить конструктор. А что делать если один из классов реализован не нами?
Для того чтобы, например, можно было приводить тип из Complex в int нам необходимо реализовать в классе Complex следующий оператор:
operator int() {
return myRe;
};
Тогда для объекта N класса Complex будет возможно преобразование:
int x = N;
1.3. Ключевое слово explicit.
Рассмотрим еще один пример. Пусть существует класс квадратных матриц Matrix с конструктором
Matrix( size_t size );
создающим нулевую квадратную матрицу размера size. Тогда этот конструктор можно также вызвать строчкой
Matrix M = 3;
Такая запись может быть не очень понятна (можно принять её, например, за создание единичной матрицы, умноженной на три). Чтобы запретить неявное приведение, добавим перед объявлением конструктора explicit:
explicit Matrix( size_t size );
Теперь для вызова конструктора необходимо использовать явное приведение типов:
Matrix M = (Matrix) 3;
В противном случае произойдет ошибка: необходимо приведение 'int' к нескалярному типу 'Matrix' (conversion from ‘int’ to non-scalar type ‘Matrix’ requested).
Note: слово explicit разрешено использовать только c конструкторами. Его использование при конструкторе с одним параметром является хорошим тоном программирования.
1.4. Передача строковой константы в параметр и неявное приведение типов.
Пусть нам необходимо создать такую функцию foo, в качестве параметра которой можно передавать строковую константу. Какой тип параметра подходит для решения этой задачи? Рассмотрим три варианта объявления функции foo:
- void foo( std::string s );
У класса std::string существует следующий конструктор :
std:string( const char * data, size_t len = (size_t) -1 );
При вызове foo("English") будет вызван этот конструктор со значением второго параметра по умолчанию, а затем созданный объект будет передан через стек. Однако это вариант не является хорошим.
Note: значение параметра len по умолчанию означает что данные из data будут скопированы до символа '\0'.
- void foo( std::string &s );
Этот способ запрещен, так как при вызове foo("English") на стеке создастся временный объект и будет передана ссылка на него, но временный объект менять незачем (ведь его изменения не будут видны), а значит, передача ссылки на него бессмыслена.
- void foo( const std::string &s );
А такой способ разрешен и являтеся лучшим из трех. В этом случае, как и в предыдущем, в параметр будет передана ссылка на временный объект. Но так как эта ссылка является константной временный объект изменить будет невозможно.
2. Ключевое слово const.
2.1. Порядок написания слова const.
При объявлении переменных слово const можно писать в любом месте. То есть запись
const int N = 1;
будет равносильна записи
int const N = 1;
Но первый вариант написания является более распространенным.
Однако в зависимости от положения слова const может меняться смысл выражения. Слово const относится к тому, после чего оно написано. Если же const стоит первым, то оно относится к тому, что идет после него.
Рассмотрим три варианта написания const с типом char *:
- const char * s;
- char const * s;
- char * const s;
В первых двух случаях менять нельзя саму строку, а указатель на неё – можно. Во третьем случае константным является указатель, а то, на что он указывает, изменять можно.
Возможна и следующая запись:
const char * const s;
Тогда константными будут и строка, и указатель на неё.
Когда константным является указатель, следующая запись разрешена:
char * const p1 = ...;
char * p2 = p1;
А в случае, когда константной является строка
const char * p1 = ...;
char * p2 = p1;
такая запись вызовет ошибку: недопустимое приведение из 'const char*' в 'char*' (invalid conversion from ‘const char*’ to ‘char*’). Такое присваивание запрещено, как как через указатель p2 мы получим возможность менять то, на что указывает p1, чего быть не должно.
2.2. Константные объекты. Константные методы. Ключевое слово mutable.
Если объект является константным, то
- у него нельзя вызывать не константные методы
- нельзя передавать в параметр по не константной ссылке
Рассмотрим класс квадратных матриц Matrix, имеющих следующие методы:
class Matrix {
...
double get( size_t i, size_t j ) const;
void set( size_t i, size_t j, double value );
double determinant() const;
...
};
Метод determinant() служит для подсчета определителя матрицы. Так как эта задача является достаточно трудоемкой, желательно было бы хранить значение определителя, чтобы не пересчитывать каждый раз заново. Для хранения заведем поле:
double myDeterminant;
Однако методом set() можно изменить матрицу, и значение определителя уже не будет верно для измененной матрицы. Для этого заведем поле, которое отвечает за то, соответствует ли данный определитель матрице, то есть не была ли изменена матрица с момента последнего вычисления определителя:
bool myDeterminantIsUpToDate;
Если значение этого поля true – значение определителя верно, если false – необходимо пересчитывать. Для этого в метод set() добавим строчку:
myDeterminantIsUpToDate = false;
А в метод determinant():
myDeterminantIsUpToDate = true;
Но метод determinant() является константным и не может изменять поля класса. Существуют два решения:
1. Сделать метод determinant() не константным. Но тогда будет невозможно вызывать его у константных матриц, что неправильно.
2. Добавить ключевое слово mutable перед изменяемыми полями:
mutable double myDeterminant;
mutable bool myDeterminantIsUpToDate;
Такие поля можно изменять даже константными методами. Слово mutable часто используется при кэшировании данных, таких как, например, определитель. В прочем, других причин использовать слово mutable нету.
2.3. Строковые константы.
Note: данный подраздел содержит дополнительную информацию о языке Си и является небольшим отступлением от курса лекций.
Указателю на константную строчку можно присвоить строковую константу:
const char * s = "English";
Так как сам указатель s не является константным, то его можно изменить:
s = "Norvegian";
Все строковые константы при запуске собираются в одно место – сегмент константных данных. При этом дубликаты создаются в единственном экземпляре. В рассмотренном случае сегмент константных данных будет выглядеть примерно следующим образом:
До присваивания s указывал на нулевую ячейку (Е), после – на восьмую (N).
На попытку присвоить строковую константу указателю на неконстантную строчку
char * с = "English";
компилятор выдаст предупреждение: исключенное приведение строковой костанты к ‘char*’ (deprecated conversion from string constant to ‘char*’). А попытка изменить эту строку
с[1] = 'a';
*(c+1) = 'a';
вызовет ошибку доступа к памяти во время исполнения.
3. Ключевое слово static.
3.1. Статические переменные и функции.
Если функцию объявить с ключевым словом static:
static void foo() {
...
}
то это будет означать, что функция foo не будет глобальной и может быть вызвана только том файле, котором была определена. То есть символ "foo" будет занят только в пределах данного файла, и его использование других файлах не вызовет ошибку при сборке.
Аналогично можно заводить статические переменные:
static int N;
Такая переменная также будет доступна только в пределах одного файла. С помощью static удобно создавать временные константы, не создавая конфликтов имен в разных файлах. Например:
static const std::string DATA = "DATA";
3.2. Статические переменные внутри функций.
Статическая переменная может быть объявлена внутри функции:
void foo() {
static const int W = getScreenWidth();
...
}
Тогда переменная W будет вычисляться один единственный раз при первом запуске foo(). Далее значение W будет хранится в глобальной памяти даже между запусками foo(), и переменная не будет инициализирована повторно. Это удобно если функция getScreenWidth() трудозатратна, и возвращаемое значение (например, ширина экрана в пикселях) не меняется. Такой способ ускорит выполнение программы, так как теперь getScreenWidth() не будет вызываться при каждом вызове foo().
Рассмотрим еще один пример. Пусть у нас есть класс для работы с документами Document, и метод, возвращающий полное имя файла:
const std::string& Document::getFilePath() const {
if ( Everything is OK ) { //Если всё хорошо и файл существует
return myFilePath; //возращаем путь к файлу
}
return ""; //Если документ был только что создан и не сохранен в файл, то вернуть нечего
}
В случае если документ не был сохранен в файл, метод создаст пустую строку на стеке и вернет ссылку на неё. После чего эта область стека может измениться и программа будет работать неверно. Возможно два варианта решения:
1. Возвращать строку, а не ссылку на неё. Этот вариант не очень хороший.
2. Использовать статическую переменную внутри метода:
const std::string Document::getFilePath() const {
if ( Everything is OK ) {
return myFilePath;
}
static const std::string EMPTY = "";
return EMPTY;
}
Тогда строка EMPTY будет находится в глобальной памяти в единственном экземпляре. Каждый раз когда будет необходимо вернуть пустую строку метод будет возвращать ссылку на EMPTY.
3.3. Статические методы.
В С++ статическими могут также быть методы классов:
class Foo {
static void bar();
void tor();
}
Обычный метод tor() на самом деле имеет один параметр – объект класса:
Foo f;
f.tor();
Функция bar() не имеет параметров вообще, и является не совсем членом класса. Такой метод может быть вызван как обычная глобальная функция, независимо от объекта класса:
Foo::bar();
Зачем же вообще нужны такие методы?
1. Таким способом можно избежать загромождения пространства имен. Для этих целей также можно использовать ключевое слово namespace (однако оно появилось в языке только 1999 году).
2. Статический метод можно сделать приватным, и тогда доступ к нему будет возможен только внутри класса.
3. Статические методы имеют доступ к закрытым полям и методам класса, так как по правам доступа статические методы не отличаются от других методов.
Приведем пример. Пусть у нас есть класс точек (на плоскости):
class Point {
private:
double myX, myY;
public:
double distance( const Point & p ) const;
}
Расстояние между двумя точками будет считаться следующим образом:
Point p1, p2;
p1.distance( p2 );
Можно сказать что параметры "неравноправны", и запись distance( p1, p2 ) будет удобней. Для этого можно создать дружественную для этих классов функцию distance (что бы она имела доступ к закрытым полям). Но останется проблема заполнения пространства имен. Хорошим решением будет реализация статического метода в классе:
class Point {
private:
double myX, myY;
public:
static double distance( const Point & p1, const Point & p2 ) const;
}
И вызов этого метода будет следующим:
Point p1, p2;
Point::distance( p1, p2 );
3.4. Статические поля.
Статическими также могут быть поля класса. Такие поля являются глобальными переменными и существуют в единственном экземпляре независимо от количества объектов класса.
Приведем простейший пример – счетчик количества объектов класса:
class Point {
private:
double myX, myY;
static int ourCounter; //только объявление статической перменной
public:
Point() {
ourCounter++;
}
}
3.5. Инстанциирование статических полей.
После объявления статического поля нехобходимо его определить. Для этого инициализируем счетчик и зададим его начальное значение в одном из срр файлов:
int Point::ourCounter = 0;
В случае, если этого не сделать, произойдет ошибка при сборке: неопределенная ссылка на 'Point::ourCounter' (undefined reference to `Point::ourCounter').