В очередной раз наступив на досадные необязательные грабли, я решил систематизировать свои знания о них. Если вы какое-то время разрабатываете на C++, то можете и не найти здесь ничего нового, но кому-то приведенный в статье материал точно поможет. Если бы я знал это лет пять назад, то однозначно сэкономил бы несколько безвозвратно потерянных дней жизни и нервных клеток.
Чтобы было интереснее, материал представлю в виде простых задачек. Сразу подчеркну, что я не считаю приведенные примеры просчетами языка. Во многом появляется смысл и логика, если вопрос обдумать. Это скорее случаи, когда может отказать интуиция, особенно если голова забита чем-нибудь еще. Есть и пара примеров вида «Ну чего этому компилятору надо, только что то же самое работало!»
И последнее замечание. Это не будут задачи на внимательность типа «Тут я поставил точку с запятой сразу после for — а никто и не заметил». Проблемы не в опечатках. Все необходимые библиотеки можно считать подключенными — не относящийся к описываемой ситуации код я опускал, чтобы не загромождать статью.
Условие всех задач.
Дан некоторый код. Иногда он разбит на отдельные листинги. Нужно ответить, скомпилируется ли программа, а если скомпилируется, что будет выведено на экран, и как себя поведет приложение, если компиляция завершится с ошибкой, то по какой причине.
Задача 1. Делим на 0
Листинг 1.1:
int g = 3;
g -= 3;
int f = 1 / g;
std::cout << "f is " << f << std::endl;
Листинг 1.2:
float g = 3;
g -= 3;
float f = 1 / g;
std::cout << "f is " << f << std::endl;
Листинг 1.2 Все работает. Вывод «f is inf». По всей видимости, считается, что float не является точным типом, потому и нуля как такового в нем представлено быть не может. Только бесконечно малое. А на бесконечно малое делить можно. Об этом нужно помнить и не надеяться, что в случае деления на ноль программа упадет, и вы сразу узнаете об ошибке.
Задача 2. Switch и класс
Листинг 2
class SwitchClass
{
public:
static const int ONE;
static const int TWO;
void switchFun(int number)
{
switch(number)
{
case ONE:
std::cout<<"1"<<std::endl;
break;
case TWO:
std::cout<<"2"<<std::endl;
break;
}
}
};
const int SwitchClass::ONE = 1;
const int SwitchClass::TWO = 2;
int main()
{
SwitchClass object;
object.switchFun(SwitchClass::ONE);
return 0;
}
Задача 3. Логические выражения
Листинг 3
bool update1()
{
std::cout<<"update1"<<std::endl;
...//Выполнение обновления и возврат результата
}
bool update2()
{
std::cout<<"update2"<<std::endl;
... //Выполнение обновления и возврат результата
}
int main()
{
bool updatedSuccessfully = update1() && update2();
return 0;
}
update1
Если же результатом выполнения update1 будет ИСТИНА, то будет вызвана update2 и программа выведет
update1
update2
Мораль. Функции, выполняющие какие-то побочные действия, в логических выражениях лучше не использовать. Чтобы гарантировать вызов обеих функций с сохранением результата, код листинга 3 нужно переписать следующим образом
bool update1Result = update1();
bool update2Result = update2();
bool updatedSuccessfully = update1Result && update2Result ;
Задача 4. Итератор как параметр
Листинг 4.1 Объявление функции
typedef std::vector<int> MyVector;
void processIterator(MyVector::iterator& i)
{
std::cout<<*i<<endl;
}
Листинг 4.2
MyVector v;
v.push_back(1);
for (MyVector::iterator i = v.begin(); i != v.end(); ++i) {
processIterator(i);
}
Листинг 4.3
MyVector v;
v.push_back(1);
processIterator(v.begin());
1
Листинг 4.3 Приведет к ошибке компиляции. Дело в том, что begin возвращает константный итератор, который в листинге 4.2 просто копируется в i. Переменная цикла i уже не является константой и может быть свободно передана в нашу функцию по обычной ссылке. В 4.3 же мы пытаемся «отобрать» у значения спецификатор (qualifier) константности. Чтобы заставить все варианты вызова работать, функцию можно переписать следующим образом
void processIterator(const MyVector::iterator& i)
{
std::cout<<*i<<endl;
}
Задача 5. Вызов метода объекта по указателю
Листинг 5
class Test
{
public:
void testFun()
{
std::cout<<"testFun"<<std::endl;
}
private:
int mValue;
};
int main()
{
Test *t, *t1 = NULL;
t->testFun();
t1->testFun();
return 0;
}
testFun
testFun
А причина проста: хоть метод testFun не является статическим, обращения к свойствам объекта в нем не происходит. Если бы метод выглядел следующим образом
void testFun()
{
std::cout<<"testFun"<<std::endl;
mValue = 0;
}
то без проблем бы, конечно, не обошлось.
Вывод. Не рассчитывайте, что вызов нестатического метода по неинициализированному или даже нулевому указателю приведет к аварийному завершению программы. Может сложиться так, что все отработает. Конечно, остается вопрос «Зачем делать метод, не использующий свойства объекта, нестатическим». Ответом на него будет: «Мало ли. В жизни и не такое случается».
Задача 6.Перегрузка виртуальных функций
Листинг 6.1 Определение классов
class TestVirtuals
{
public:
virtual void fun(int i)
{
std::cout<<"int"<<std::endl;
}
virtual void fun(float f)
{
std::cout<<"float"<<std::endl;
}
void fun(std::string s)
{
std::cout<<"string"<<std::endl;
}
};
class TestVirtualsChild : public TestVirtuals
{
public:
virtual void fun(int i)
{
std::cout<<"int child"<<std::endl;
}
};
Листинг 6.2
TestVirtuals tv;
tv.fun(1);
tv.fun(1.f);
tv.fun(std::string("one"));
Листинг 6.3
TestVirtualsChild tvc;
tvc.fun(1);
tvc.fun(1.f);
Листинг 6.4
TestVirtualsChild tvc;
tvc.fun(std::string("one"));
int
float
string
Листинг 6.3 Потомок «не видит» перегрузки родителя. Все вызовы метода fun приводятся к целочисленному варианту (который есть в потомке). Вывод программы
int child
int child
Листинг 6.4 Компиляция завершается с ошибкой. Невиртуальная функция как будто исчезает у потомка. Остается возможным только явный ее вызов через явное указание родительского класса.
tvc.TestVirtuals::fun(std::string("one"));
Вывод. От совмещения виртуальных функций с перегруженными лучше держаться подальше. Если другого выхода нет, осторожность должна просто зашкаливать.
Автор: vadim_ig