Pull to refresh

Comments 86

Задача 2. Switch и класс:

Не обязательно решать проблему через enum, Можно так:

static const int ONE = 1;
static const int TWO = 2;
Дело в том, что отдельное определение статических членов вне класса:
const int SwitchClass::ONE = 1;
const int SwitchClass::TWO = 2;

может находиться в отдельной единице трансляции (т.е. cpp-файле), а компилятор для case должен подставить интегральную константу, известную на этапе компиляции в текущей единице трансляции.

class SwitchClass
{
public:
    static const int ONE = 1; // являются интегральными константами времени компиляции по стандарту
    static const int TWO = 2;
    ....
};

Кстати, есть совсем старые компиляторы, которые такой синтаксис не поддерживают.
Деление флота на ноль вполне соотвествует IEEE 754. Именно поэтому в HICPP рекомендуется в таком случае вводить какие-нибудь предусловия. Пусть лучше грациозно падает прямо сейчас, чем когда-нибудь еще.
И более того, это действительно зачастую (в математическом коде) удобно, так же как удобен определенный этим же стандартом NAN. Авторы IEEE 754 хорошо знали, что делали. Но к сожалению пользоваться этими свойствами на практике проблематично из-за очень плохой поддержки этого поведения компиляторами да и новым железом тоже.
Много лет занимаюсь тем, что пишу код, реализующий математику (кстати, далеко не самую тривиальную) и ни разу не сталкивался с проблемами из-за «плохой поддержки этого поведения компиляторами да и новым железом тоже». По-моему все замечательно поддержано и компиляторами (по крайней мере gcc, в msvc, говорят, с этим творится полный ад, но кого это волнует?) и железом. Большинство проблем связано с тем, что, сталкиваясь с непонятным поведением вещественных типов, программисты часто не идут изучать Голдберга, а начинают вместо этого выдумывать легенды типа приведенной в первой загадке.
Мне, к сожалению, в основном как раз с MSVC приходится иметь дело на работе :). Тихий ужас во многих отношениях, но деваться некуда. С gcc все более-менее нормально, но и там -ffast-math уже не поставишь.

К сожалению убедить коллег перебираться на Qt пока не удалось :). Ценность NAN-ов тоже никто из тех кто математикой не занимался толком не понимает, увы.
1. Соответствует стандарту IEEE-754
2. Используем
constexpr
3. Соответствует стандарту C++, причем это такой типичный пример, что все, по-моему, о нем знают, даже если ни разу не сталкивались лично.
4. Аналогично
5. Тут вообще у Вас undefined behavior
6. Тоже по стандарту, но этот пример действительно может быть неочевиден (в отличие от 3 или 4)
Стоит отметить, что ответ в 1.2 верный, только если компилятор следует IEEE-754 (он же IEC-559: foldoc.org/IEC+559). В противном случае UB.
См. 18.2.1.1 и 18.3.2.4:
static constexpr bool is_iec559;
True if and only if the type adheres to IEC 559 standard.
Задача 6.Перегрузка виртуальных функций:

Проблема решается просто:

class TestVirtualsChild : public TestVirtuals
{
public:
    using TestVirtuals::fun; // решается этой строчкой

    virtual void fun(int i)
    {
        std::cout<<"int child"<<std::endl;
    }
};
Это происходит из-за того, что имена производного класса по-умолчанию скрывают имена базового, поэтому скрытые имена не участвуют в перегрузке.
using явно вносит имена в область видимости производного класса, что делает их полноценными кандидатами на разрешение перегрузки.
Решает проблему NVI.

class TestVirtuals
{
public:
    void fun(int i)
    {
        doFun(i);
    }

    void fun(float f)
    {
        doFun(f);
    }

    void fun(std::string s)
    {
        std::cout<<"string"<<std::endl;
    }
	
private:
    virtual void doFun(int i)
    {
        std::cout<<"int"<<std::endl;
    }

    virtual void doFun(float f)
    {
        std::cout<<"float"<<std::endl;
    }
};


class TestVirtualsChild : public TestVirtuals
{
private:
    virtual void doFun(int i)
    {
        std::cout<<"int child"<<std::endl;
    }
};
В первом примере поведение операции с плавающей точкой на самом деле зависит от флагов сопроцессора. Можно включить генерирование исключений.
В типе float, к слову, вообще нельзя хранить ноль. Формат такой. Там, если не ошибаюсь, есть специальное «магическое» значение для этого.
Поэтому, вычтя из трех три и добавив потом какое то другое целое число, можно получить число нецелое.
Во float можно хранить ноль. Да, это специальное значение (нулевая мантисса). И x + 0 == x, как и требуется. Почтайте, например начиная отсюда IEEE-754
Моя ошибка, хранить в нем нельзя точно значение 0.1 например. Потому что дробная часть не представима в бинарном виде.

Есть кстати вопрос, может быть подскажете причину. При продолжительном применении операций увеличения или уменьшения на целое число к изначально нулевому float иногда появляется очень маленькая дробная часть. Например, 6428.9999999999998 или 1213.00000000000001.
Думал раньше (основываясь на ошибочных воспоминаниях о нуле), что это следствие невозможности хранения нуля. Видимо, ошибался.
Такое может быть, если при выполнении операций число вышло за тот диапазон, где оно может храниться точно, а потом вернулось обратно.
Есть такая вещь, как машинный эпсилон, обусловленный конечной точностью представления числа. Проще говоря — количество бит для представления числа у вас конечное, а числа в результате вычислений бывают такие, все цифры которых не влезают в эти биты. Нужно немного поработать с сопроцессором, что бы понять его философию. Мне лично помог метод восприятия числа в сопроцессоре не как точного значения X, а как диапазона X±ε. Тогда A + B = C нужно воспринимать как A±ε + B±ε = C±2ε. Обратите внимание, что в результате этой операции погрешность результата C уже 2ε. Если сделать еще цепочку операций, то погрешность результат может достигнуть Nε, величина которого при очень больших N может быть сопоставима или больше результата. Этот факт обязательно надо учитывать при работе с сопроцессором.
Не совсем хорошее представление. Эпсилон в Ваших формулах есть некая константа, тогда как в реальности эпсилон зависит от числа. Чем число больше — тем больше погрешность его представления.

Из этой особенности вытекает ряд важных практических следствий. В частности отсюда следует что очень плохой идеей является сложение float-ов с сильно разными значениями. То есть 1+1 — нормально, а 1+100000000 — плохо, погрешность сопоставима с прибавляемой величиной вплоть до того что получаем X+1==X. Это регулярно вызывает проблемы в такой простой казалось бы задаче как суммирование массива чисел — там надо либо все к более длинному типу приводить (скажем double для суммирования float), либо сортировать массив перед его суммированием, так чтобы мелкие числа суммировались бы первыми.

Зато с другой стороны, перемножение float-ов не вызывает никаких проблем :). Точность там получается как раз соответствующей результату умножения. Для обычных погрешностей это не так, там умножение тоже увеличивает погрешность.
Я не писал, что ε — константа :) Это часть числа. Ну может корректнее было бы написать A±ε₁ + B±ε₂ = C±(ε₁ + ε₂), но смысл от этого не меняется. Идея складывать большие числа не плохая. Главное, что погрешность намного меньше результата операции, а не её операндов. Какая на практике разница, получилось ли 1.123456789 + 987654321 равным 987654321.1 или 987654321.2? А если в прикладной задаче разница есть — значит неправильно построен алгоритм обработки данных. В ситуациях, в которых нужна точность, нужно использовать достаточно точные алгоритмы и средства. Если есть вероятность, что данные содержат больше 18 значащих цифр, и терять их нельзя, то используйте не double, а decimal — 128-битное число с 29-цифровой мантиссой. Если и столько цифр мало, то нужно использовать уже математические библиотеки, хранящие числа в блоках памяти динамического размера.
Если у вас встречается ситуация, что суммирование массива даёт недостаточную точность, для решения задачи, то ошибка в факте существования такого массива, а не в потере точности. Сортировка массива — это маскировка проблемы.
Разница есть, если нужно сложить не два операнда, а, к примеру, тысячу. Если начать суммирование с больших чисел, то каждый раз к сумме будет прибавляться большая же погрешность и за тысячу операций общая погрешность набежит уже вполне себе серьезная по сравнению с самой суммой. Начиная суммирование с малых чисел сложение погрешностей начинается тоже с маленьких чисел и суммарная погрешность набегает уже вполне вменяемой.

Что до решения «грубой силой», то оно как правило работает неприемлемо медленно. Все же о вычислительных алгоритмах говорим (где еще нужно тысячи чисел суммировать?). Да и проблему Вы точно так же не решаете, если я её минимизирую, то Вы её просто задвигаете поглубже. И, что интересно, в реальной жизни подобный подход с задвиганием как раз на удивление часто не срабатывает. Даже в примитивной задаче обращения матриц если Вы возьмете «лобовой» подход, то там получается что с float-ами и не очень хорошими исходными данными он «ломается», скажем, на размерности 300, а с double-ами… на размерности 2000. А замена алгоритма на другой, который делает то же самое обращение, но использует другой порядок операций дает метод, который работает до 200.000 на double.
К счастью все далеко не так печально. Поскольку погрешности оказываются фактически случайными, причем разного знака, то они часто взаимно уничтожаются и уж по крайней мере не всегда складываются. Так что катастрофическое увеличение погрешности вещественных чисел — это одна из легенд про float-ы :)
Здесь есть хорошая картинка, где показано, как числа с плавающей точкой представлены на числовой прямой.
«В C++ вычисление логических выражений оптимизируется.» Это не оптимизация, а сокращённый порядок вычисления.
Разница принципиальна. Оптимизация может включиться-выключиться (что может привести к unspecifed и даже undefined behavior).
Встроенные же логические операторы ещё и точку следования вводят.
int show(const char* s, int x) { puts(s); return x; }

int main()
{
  show("one",1) + show("two",2); // one two или two one - как повезёт
  show("one",1) & (show("two",2) + show("three",3)); // one two three в любом порядке
  show("one",1) && (show("two",2) + show("three",3)); // сперва one, затем two three в любом порядке

  int x = 1;
  x++ & ++x; // undefined behavior
  x++ && ++x; // well-defined
  x++ && (++x & x++); // снова undefined
}
Да, в копилку граблей. Сокращённый порядок и строгое следование — стандартная фича именно встроенных операторов &&, || и запятой.
Одноимённые пользовательские операторы этим не обладают, что может привести к неприятностям.
Вот, например,
#include <cstdio>

struct Condition // захотели мы сделать свой логический класс (или, например, указатель)
{
  bool c;
  explicit Condition(bool c) : c(c) { printf("ctor "); }
  operator bool() const { printf("oper "); return c; } // который приводится к штатному булеву типу
};

bool foo() { printf("foo "); return true; }
bool bar() { printf("bar "); return false; }

bool pass(bool v) { printf("\n"); return v; }

bool a = pass( Condition(foo()) && Condition(bar()) ); // foo ctor oper bar ctor oper
bool b = pass( Condition(bar()) && Condition(foo()) ); // bar, ctor, oper

// потом решили, а зачем нам приведение, если мы можем оставаться на уровне Condition
Condition operator&& (Condition const& x, Condition const& y)
{ 
  printf("and ");
  return Condition(x.c && y.c);
}

bool c = pass( Condition(foo()) && Condition(bar()) ); // bar ctor foo ctor and ctor oper
bool d = pass( Condition(bar()) && Condition(foo()) ); // foo ctor bar ctor and ctor oper

int main()
{
	return a+b+c+d;
}


Кстати, на RSDN была статья о том, как с помощью перегруженной запятой уволенный сотрудник устроил экс-коллегам wish you happy debug.
А дайте ссылку на статью, пожалуйста. Интересно было бы почитать
«Дело в том, что begin возвращает константный итератор, который в листинге 4.2 просто копируется в i.»
НЕТ!!!
begin возвращает rvalue — значение итератора. Функция ProcessIterator принимает неконстантное lvalue (iterator&).
Но rvalue может неявно привестись только к константному lvalue (iterator const&), либо к временному (iterator&&).

А словосочетание «константный итератор» вообще неоднозначно. Это или константный экземпляр неконстантного итератора (iterator const), или экземпляр итератора константного доступа (const_iterator).
Но rvalue может неявно привестись только к константному lvalue (iterator const&), либо к временному (iterator&&).

Хочу добавить уточнение: для пользовательских типов сделано послабление. От их временных объектов в выражениях могут браться lvalue-ссылки. Я не помню, где это в стандарте указано, но понимаю для чего это сделано. Дело в том, что у временных объектов пользовательского типа можно вызывать неконстантные методы. А вызов метода с неявным this-аргументом равносильно вызову внешней функции с этим объектом в качестве аргумента.
Я могу и ошибаться. Позже поищу в стандарте, сейчас некогда.

Теперь я понял, почему у меня компилируется, а у автора статьи нет. В студии vector<T>::iterator это класс, а в компиляторе автора скорее всего T*.
Легко убедиться, что дело именно в компиляторе, а не в реализации стандартной библиотеки
struct A {};

A foo() { return A(); } // можно и просто конструктор передавать

void bar(A&) {}
void buz(A&&) {} // только для С++03 и выше

int main()
{
  bar(foo()); // любой VC сожрёт, любой gcc выругается
  buz(foo()); // все современные компиляторы сожрут
}
void buz(A&&) {} // только для С++03 и выше

Вы слишком хорошего мнения о возможностях С++03 :)

&& — это С++11
Лень было лезть в стандарты уточнять, проверил: сделал gcc --std=c++0x, он сожрал. Хотя, возможно, гусь просто немного опережает события, но обычно это опережение идёт как гнутые расширения: --std=gnu++0x
с++0x — это и есть включение фич c++11. Просто тогда еще не был известен точный год, когда стандарт примут :)
>> От их временных объектов в выражениях могут браться lvalue-ссылки.

По стандарту — не могут (и неконстатнтые методы здесь совершенно не при чем… не путайте константность и rvalueness, это две совершенно разные вещи!). В MSVC это поддерживается как расширение языка.
Насчет листинга 4.3 я не понял:
MyVector v;
v.push_back(1);
processIterator(v.begin());

Почему у Вас v.begin() возвращает константный итератор? У меня вызывается неконстантная версия begin(), как и должно было быть, ведь v не константный.
В VS 2008 и 2012 отлично компилируется.
nickolaym все правильно объяснил. В данном случае, возвращается const_iterator — но это «константный итератор» не в том смысле, что он сам является константой, а в том смысле что он, в отличии от просто iterator, не позволяет изменить то, на что он указывает.
Почему это возвращается const_iterator? vector::begin() вовзращает const_iterator только если объект был констатным, в листинге же это не так, вернется обычный iterator
В данном случае, возвращается const_iterator

Нет-нет, в этом примере возвращается именно vector<int>::iterator, это можно проверить в отладчике. nickolaym действительно правильно объяснил (за одним исключением, добавлю ему коммент), проблема в том, что begin возвращает временный объект, от которого хотят lvalue reference.
Насчет константности я все понимаю, здесь константность я имел в виду в контексте iterator/const_iterator.
Да, вы правы, посыпаю голову пеплом, там действительно
const_iterator begin() const;
iterator begin();
Все моя чертова невнимательность…
VC компилирует по другой причине. Там rvalue можно приводить к nonconst lvalue.
Это несоответствие (ослабление) стандарта.
Вы либо неудачно сформулировали, либо заблуждаетесь. MSVC такого никогда не сделает.
Про никогда я загнул; MSVC такого не сделает только со встроенными типами. Справедливости ради: MSVC даёт предупреждение, при использование этого расширения.
Это в нём повелось ещё с достандартной эпохи, т.е. до 98 года, — VC5 и моложе.
И, то ли их заломало переписывать этот участок компилятора, то ли оставили для совместимости, чтобы не переписывать MFC и прочее наследие.
Варнинг только добавили.
«На первый взгляд, проблем куча (вызов по нулевому и неинициализированному указателям), и ничего работать не должно.»

Вот именно, что оно никому ничего не должно.
Потому что это — неопределённое поведение, и частный его случай «работать так, как хотел бы автор».
Конкретно в этом месте разложена куча грабель, связанных с разницей между арифметикой указателей и арифметикой ссылок, с виртуальными и статическими вызовами, со статическим и динамическим приведением типов. Причём, заметьте, даже с неявной арифметикой и динамикой — безо всяких (ptr+1) и dynamic_cast…

В целом, статья называется «как я ходил по граблям и кое-где получил в лоб, а кое-где не получил».
Учебным пособием оно ни в коем разе быть не может.
Маленькое пояснение про адресную арифметику и статик-каст.
struct A { int x; };
struct B {
	int y;
	void whoami() { printf("%p\n", (void*)this); }
};
struct C : A, B {};

void test(C* pc)
{
	pc->whoami();
	B* pb = pc;
	pb->whoami();
}

int main()
{
	test(0);     // 00000004  00000000
	test(new C); // 0002F7F4  0002F7F4
}

Потому что нулевой указатель наследника приводится к нулевому указателю предка (компилятор делает ветвление), а вот нулевых ссылок, якобы, не бывает, и компилятор никаких проверок не делает, тупо смещая базу. (Впрочем, получение нулевой ссылки через разыменование нулевого указателя — это неопределённое поведение, и будет там проверка или не будет, или диск отформатируется — никто не знает).
Не тянет это на «загадки с++» никак. Максимум на «шесть задачек для полных новичков в с++». Ответы на «закадки» в стиле «по всей видимости то-то», намекает на то что автор далёк от с++.
Мораль. Функции, выполняющие какие-то побочные действия, в логических выражениях лучше не использовать. Чтобы гарантировать вызов обеих функций с сохранением результата, код листинга 3 нужно переписать следующим образом
bool update1Result =  update1();
bool update2Result =  update2();
bool updatedSuccessfully = update1Result && update2Result ;


Можно еще так:
if (update1() & update2()) { ... }

Будут вызваны обе функции, но if пойдет только если обе вернули true. Конкретно к С++ это имеет весьма малое отношение, т.к. работает в большестве ЯП.
Если update1 или update2 возвращает BOOL, то можем получить внезапное 2 & 1 == 0 и пойти по ветке else.
Тогда можно так:
if (!!update1() & !!update2()) { ... } 
Но лучше, конечно, писать более очевидно…
Разве к bool не происходит форс преобразование 1/0?
Тогда ИМХО это выпендреж, и нужно писать int, если имеется в виду это.
Ибо ничто мне не мешает для стандартного наименования написать typedef bool BOOL;
Собственно так я и подумал.
BOOL — это стандартный тип из windows.h
А в своей программе вы можете переопределить всё что угодно, хоть #define bool int, речь не об этом.
Стандартный тип из прикладной библиотеки. Лихо завернули.
Но как это соотносится с С++? Давайте разберемся в обьективности поднобной несуразицы у Microsoft.
Ответ очевиден — обратная совместимость с историей(Си).

Здесь же рассматривается язык С++, в нем и так заложено достаточно обратной совместимости. Зачем в подобных статьях, подобные замечания?
И да, я помню этот тип, я грубо говоря получил сексуальную терапию пытаясь обьявить свой тип одновременно с названной библиотекой. Получалось что все адекватные (bool, BOOL, Bool) переопределены, а использовать BooL и bOoL я не решился.
Я даже боюсь представить, зачем вам понадобился свой булев тип впридачу к уже имеющимся.
Я в затруднении, вы так косвенно сообщаете о своем интересе, или просто констатируете что лучше вам этого не знать? Построение предложения странное.

Здесь то что может вас напугать
Свой класс, с кучей перегрузок, который в некоторых местах можно выводить в GUI, а в некоторых он семантичен встроенному. Плюс используя некоторые макросы можно добиваться дополнительного DEBUG поведения: что то вроде супер-ассертов, с разными зависимостями и воздействиями на другие переменные.
Просто это удобно, в GUI получить список переменных, построить их графики и прочее. Задача стояла специфичная.
«От совмещения виртуальных функций с перегруженными лучше держаться подальше. Если другого выхода нет, осторожность должна просто зашкаливать.»
Это называется «сокрытие имён».

Откройте для себя using — импорт объявлений из класса предка и квалифицированный доступ.

#include <cstdio>

#define WHOAMI() printf("%s\n", __PRETTY_FUNCTION__) // __FUNCSIG__ для MSVC

struct A
{
	void f() { WHOAMI(); }
	virtual void f(int) { WHOAMI(); }
};

struct B : A
{
	void f(const char*) { WHOAMI(); } // перегрузкой сокрыли унаследованные сигнатуры
//	using A::f; // вот так можно открыть их обратно
};

struct C : B
{
	void f(int) { WHOAMI(); } // перегрузкой сокрыли имена, переопределением заменили виртуальную функцию
//	using B::f;
};

int main()
{
	C c;
	c.A::f(); // квалифицированный доступ - не помеха сокрытию
	c.B::f("hello");
	A& a = c;
	a.f(123); // виртуальная функция действительно переопределена
}

Вот, кстати, хорошее объяснение зачем этот механизм вообще нужен.
Смысл в том, что нужно управляемое сокрытие-открытие имён. Одно из решений — делать сокрытие всегда и открывать явно (и, кстати, выборочно) — путь С++. Другое решение — делать открытие всегда и скрывать явно.

С++ контринтуитивен ещё и в управлении доступом: private не делает имя сокрытым, а очень даже открытым, но запрещённым к использованию.
Такова селяви — увы.
Да, увы, это заставляет извращаться с pimpl.
Зато можно переопределять приватные виртуальные функции :)
Ну и стоит отметить, что к виртуальности функций это все не имеет совершенно никакого отношения.
Чуть-чуть имеет. Ибо колдунство и контринтуитивность: в промежуточной базе сигнатура виртуальной функции сокрыта, а в наследнике внезапно переопределена (а не введена новая такая же).
Третий пример показывает свойство логических операторов, которое называется short circuit («короткое замыкание»). У нас в продукте есть несколько операций, которые содержат множество действий и которые должно быть можно отменить в любое время. Поэтому мы используем это свойство, чтобы прекратить выполнение без выброса исключений:

var success = preprocess()
              && checkRequisites()
              && doStuff(a, b)
              && doOtherStuff()
              && finalize();
А я говорю, что операции &&, || и ?: «экономные». Того, что не нужно вычислять, не вычисляют.
Еще зачастую в многословных языках, типа баша, удобно использовать для лаконичной записи различных условий подобные конструкции:

[ -d $file ] && work_with_dir $file || work_with_file $file

Или, например, для записи тернарного оператора, там где его нет.

Правда не думаю, что делать так везде — хорошая практика, но если для себя то можно улучшить читаемость.
А это не только в баше — во многих языках операции and и or определены таким способом
def __and__ (x, y) :
  if x :
    return y
  else :
    return x

def __or__ (x, y) :
  if x :
    return x
  else :
    return y
</code>
В perl и php это стандартная практика: doSomething() or die "Something went wrong";
Еще это свойство применяется, если результат вычислений может быть не определен в определенных условиях. Например:

// count - число элементов, sum - их сумма
bool average_more_than_one = (count > 0) && (float(sum)/count > 1.f);
Про вторую загадку:
Я был готов разлюбить С++ сиюминутно, но вогнал ваш код. И в обоих случая вызвался метод ребенка, предугадуемое поведение.
Про подстановку константного значения вы придумали, это никак не влияет на таблицу функций.

разлюбил
оно не влияет на таблицу функций, и всегда вызывается метод ребенка — все верно именно так там и написано, просто при кодогенереции, подставляется иммено то значение, которое объявленно для конкретного класса, без вывода реального класса.
я понимаю почему так происходит. я говорю что это неочевидно и не правильно.
даже если unexpected behavior хорошо задокументирован и логичен, это не значит что он перестает быть таким.
согласитесь, это не то поведение, что может ожидать хотя бы один программист в мире
Автор верно подметил, если вы давно знакомы с с++ то вам это известно.
И для каждого такого случая есть правило, которое люди игнорируют, пока сами не наткнутся на такой гемор:

1. При делении всегда проверять на 0
2. Не юзать switch, юзать if if else (оптимизатор, если будет уверен, сам все сделает)
3. Не использвать функции в условных операторах (и внутри помнить что не все операнды вычисляются)
4. Ну тут комилятор подсказывает
5. Тонкость, которую нуна знать, потому что указатель может быть и мусорный вообще
6. Всегда в наследуемом классе определать все вирт функции, и все, которые учавствуют в перегрузке. Для первого случая можно юзать патерн — невертуальный интерфейс.
Я не решил задачу 4, сказав, что будет предупреждение. Наверно, насиделся слишком много на Embarcadero (который в такой ситуёвине всё-таки сделает временный объект). Но совершенно верно сказал, что предупреждение будет убрано, если указать const.

Задачу 6 можно разглючить, написав using TestVirtuals::fun;
P.S. И да, проверил 4 на MinGW, временного объекта он не делает.
По всей видимости, считается, что float не является точным типом, потому и нуля как такового в нем представлено быть не может.

Дело не в ошибках округления (которых тут, кстати говоря, нет — настоятельно рекомендую ознакомиться с тем, как хранятся числа с плавающей точкой и почему 0.5 — точное число, а 0.3 — всего лишь приближенное значение), а в том, что (-)INFINITY — это результат, который получается при делении на ноль числа с плавающей точкой. Конечно, можно заставить приложение вываливаться с «Division by zero» в таких случаях.
4.1 не скомпилируется: не указано пространство имен для endl.
Это я опечатался. Сейчас поправлю
Без int i = 5; i = i+++i+++i пост не полон :)
Задача 5. Вызов метода объекта по указателю
Конечно, остается вопрос «Зачем делать метод, не использующий свойства объекта, нестатическим».
Метод может использовать свойства объекта, но не при каждом вызове, в зависимости от аргументов, параметров шаблона, значений глобальных переменных или внешних функций (rand(), к примеру). Будут ли использованы свойства объекта, может зависеть и от класса-наследника, к которому принадлежит объект, если данный метод виртуальный или вызывает виртуальные методы, но в данном случае произойдет сбой уже при попытке вызвать виртуальный метод, так как указатель vptr тоже является свойством объекта.
присылайте еще задачки, пожалуйста!
Only those users with full accounts are able to leave comments. Log in, please.