Шесть загадок по С++

в 10:20, , рубрики: c++, грабли, задачи для программистов, особенности, Программирование, метки: , , ,

В очередной раз наступив на досадные необязательные грабли, я решил систематизировать свои знания о них. Если вы какое-то время разрабатываете на 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.1 Программа аварийно завершает работу. На ноль делить нельзя. Именно то, чего ожидаешь.
Листинг 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;
}

Ответ

Не компилируется. Статические константные члены класса не являются с точки зрения оператора switch достаточно константными. Проблема решается использованием перечислений (enum) вместо "static const int"
Задача 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. В C++ вычисление логических выражений оптимизируется. Т.е., если на каком-то этапе результат становится очевиден — дальнейшие расчеты не выполняются. В примере используется логическое И. Если один из его операндов ЛОЖЬ, то результат также будет ЛОЖЬ вне зависимости от значения второго операнда. Т.е., если update1 вернет ЛОЖЬ, то update2 даже не будет вызван, и вывод на экран получится

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());

Ответ

Листинг 4.2 Отработает нормально. На экран будет выведено

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"));
Ответ

Листинг 6.2 Перегрузка отработает, как и ожидалось. Вывод программы

int
float
string

Листинг 6.3 Потомок «не видит» перегрузки родителя. Все вызовы метода fun приводятся к целочисленному варианту (который есть в потомке). Вывод программы

int child
int child

Листинг 6.4 Компиляция завершается с ошибкой. Невиртуальная функция как будто исчезает у потомка. Остается возможным только явный ее вызов через явное указание родительского класса.

    tvc.TestVirtuals::fun(std::string("one"));

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

Автор: vadim_ig

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js