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 тоже является свойством объекта.
присылайте еще задачки, пожалуйста!
Sign up to leave a comment.

Articles

Change theme settings