Купить
 
 
Жанр: Учеба

Язык С++ с комментариями

страница №23

забудьте заглянуть к
самому Бьерну Страуструпу.
С уважением, Сергей Деревяго.

043


1.3.1. Эффективность и структура, стр. 43
За исключением операторов new, delete, type_id, dynamic_cast, throw и блока try,
отдельные выражения и инструкции С++ не требуют поддержки во время выполнения.
Хотелось бы отметить, что есть еще несколько очень важных мест, где мы имеем
неожиданную и порой весьма существенную поддержку времени выполнения. Это
конструкторы/деструкторы (особенно "сложных" объектов), пролог/эпилог создающих
объекты функций, отчасти, вызовы виртуальных функций.
Для демонстрации данной печальной особенности рассмотрим следующую программу:
#include "stdio.h"
#include "stdlib.h"
#include "time.h"

long Var, Count;

struct A {
A(); // не inline
~A();
};

void ACon();
void ADes();

void f1()
{
A a;
}

void f2()
{
ACon();
ADes();
}

int main(int argc,char** argv)
{
if (argc"1) Count=atol(argv[1]);

clock_t c1,c2;
{
c1=clock();

for (long i=0; i"Count; i++)
for (long j=0; j"1000000; j++)
f1();

c2=clock();
printf("f1(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
}
{
c1=clock();

for (long i=0; i"Count; i++)
for (long j=0; j"1000000; j++)
f2();

c2=clock();
printf("f2(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
}
}

A::A() { Var++; }
A::~A() { Var++; }

void ACon() { Var++; }
void ADes() { Var++; }

В ней функции f1() и f2() делают одно и то же, только первая неявно, с помощью
конструктора и деструктора класса A, а вторая с помощью явного вызова ACon() и
ADes().
Для работы программа требует одного параметра — сколько миллионов раз вызывать
тестовые функции. Выберите значение, позволяющее f1() работать несколько секунд
и посмотрите на результат для f2(). В зависимости от компилятора и платформы
разница может достигать 10 раз!

А теперь немного о структуре данного примера. Во-первых, определения вызываемых
A-функций я поместил в конце файла, т.к. некоторые в меру интеллектуальные
оптимизаторы встраивают тело функции непосредственно в место ее вызова (даже без
явной спецификации inline), если определение функции было доступно в точке
вызова и тело функции было тривиальным. Даже более того: после работы
качественного оптимизатора приведенный выше цикл
for (long i=0; i"Count; i++)
for (long j=0; j"1000000; j++)
f2();

может превратиться в
Var+=Count*2*1000000;

И это просто замечательно, но, к сожалению, не в нашем конкретном случае.
Кстати, проверьте качество вашего оптимизатора, перенеся определения A-функций
до их вызова в f1() и f2().
В принципе, встраивание тела функции можно осуществить и в случае присутствия
определения функции после ее вызова, но, как правило, этого не происходит.
Итак, накладные расходы присутствуют. А что же inline? Давайте внесем очевидные
изменения:
struct A {
A() { Var++; }
~A() { Var++; }
};

void f1()
{
A a;
}

void f2()
{
Var++;
Var++;
}

После данных изменений разницы во времени работы f1() и f2() не должно быть. Но,
к несчастью, она будет присутствовать для большинства компиляторов.
Что же происходит? Наблюдаемый нами эффект называется abstraction penalty, т.е.
обратная сторона абстракции или налагаемое на нас некачественными компиляторами
наказание за использование объектно-ориентированных абстракций.
Посмотрим, как abstraction penalty проявляется в нашем случае. Что же из себя
представляет
void f1()
{
A a;
}

эквивалентное
void f1() // псевдокод
{
A::A();
A::~A();
}

И чем оно отличается от простого вызова двух функций:
void f2()
{
ACon();
ADes();
}

В данном случае — ничем! Но, давайте рассмотрим похожий пример:
void f1()
{
A a;
fun();
}

void f2()
{
ACon();
fun();
ADes();
}

Как вы думаете, эквивалентны ли данные функции? Правильный ответ — нет, т.к.
f1() представляет собой
void f1() // псевдокод
{
A::A();

try {
fun();
}
catch (...) {
A::~A();
throw;
}

A::~A();
}

Т.е. если конструктор успешно завершил свою работу, то языком гарантируется, что
обязательно будет вызван деструктор. Т.е. там, где создаются некоторые объекты,
компилятор специально вставляет блоки обработки исключений для гарантии вызова
соответствующих деструкторов. А накладные расходы в оригинальной f1() чаще всего
будут вызваны присутствием ненужных в данном случае блоков обработки исключений:
void f1() // псевдокод
{
A::A();

try {
// пусто
}
catch (...) {
A::~A();
throw;
}

A::~A();
}

Дело в том, что компилятор обязан корректно обрабатывать все возможные случаи,
поэтому для упрощения компилятора его разработчики часто не принимают во
внимание "частные случаи", в которых можно не генерировать ненужный код. Увы,
подобного рода упрощения компилятора очень плохо сказываются на
производительности интенсивно использующего средства абстракции и inline функции
кода. Хорошим примером подобного рода кода является STL, чье использование, при
наличие плохого оптимизатора, вызывает чрезмерные накладные расходы.
Итак, занимаясь оптимизацией "узких мест" обратите внимание на abstraction
penalty.

2.5.5. Виртуальные функции, стр. 73
Требования по памяти составляют один указатель на каждый объект класса с
виртуальными функциями, плюс одна vtbl для каждого такого класса.
На самом деле первое утверждение неверно, т.е. объект полученный в результате
множественного наследования от полиморфных классов будет содержать несколько
"унаследованных" указателей на vtbl.
Рассмотрим следующий пример. Пусть у нас есть полиморфный (т.е. содержащий
виртуальные функции) класс B1:
struct B1 { // я написал struct чтобы не возиться с правами доступа
int a1;
int b1;

virtual ~B1() { }
};

Пусть имеющаяся у нас реализация размещает vptr (указатель на таблицу
виртуальных функций класса) перед объявленными нами членами, тогда данные
объекта класса B1 будут расположены в памяти следующим образом:
vptr_1 // указатель на vtbl класса B1
a1 // объявленные нами члены
b1

Если теперь объявить аналогичный класс B2 и производный класс D
struct D: B1, B2 {
virtual ~D() { }
};

то его данные будут расположены следующим образом:
vptr_d1 // указатель на vtbl класса D, для B1 здесь был vptr_1
a1 // унаследованные от B1 члены
b1
vptr_d2 // указатель на vtbl класса D, для B2 здесь был vptr_2
a2 // унаследованные от B2 члены
b2

Почему здесь два vptr? Потому, что была проведена оптимизация, иначе их было бы
три. Я, конечно, понял, что вы имели ввиду: "Почему не один"? Не один, потому
что мы имеем возможность преобразовывать указатель на производный класс в
указатель на любой из базовых. И полученный указатель должен указывать на
корректный объект базового класса. Т.е. если я напишу:
D d;
B2* ptr=&d;

то в нашем примере ptr укажет в точности на vptr_d2. А собственным vptr класса D
будет являться vptr_d1. Значения этих указателей, вообще говоря, различны.
Почему? Потому что у B1 и B2 в vtbl по одному и тому же индексу могут быть
расположены разные функции, а D должен иметь возможность их правильно заместить.
Т.о. vtbl класса D состоит из нескольких частей: часть для B1, часть для B2
часть для собственных нужд.
Подводя итог, можно сказать, что если мы используем множественное наследование
от большого числа полиморфных классов, то накладные расходы по памяти могут быть
достаточно существенными. Конечно, от этих расходов можно отказаться, реализовав
вызов виртуальной функции специальным образом, а именно: каждый раз вычисляя
положение vptr относительно this. Однако это спровоцирует существенные
относительные расходы времени выполнения, компрометируя тем самым вызов
виртуальных функций.
И раз уж так много слов было сказано про эффективность, давайте реально измерим
относительную стоимость вызова виртуальной функции.
#include "stdio.h"
#include "stdlib.h"
#include "time.h"

long Var, Count;

struct B {
void f();
virtual void vf();
};

struct D : B {
void vf(); // замещаем B::vf
};

void f1(B* ptr)
{
ptr-"f();
}

void f2(B* ptr)
{
ptr-"vf();
}

int main(int argc,char** argv)
{
if (argc"1) Count=atol(argv[1]);

clock_t c1,c2;

D d;
{
c1=clock();

for (long i=0; i"Count; i++)
for (long j=0; j"1000000; j++)
f1(&d);

c2=clock();
printf("f1(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
}
{
c1=clock();

for (long i=0; i"Count; i++)
for (long j=0; j"1000000; j++)
f2(&d);

c2=clock();
printf("f2(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
}
}

void B::f() { Var++; }
void B::vf() { }

void D::vf() { Var++; }

В зависимости от компилятора, накладные расходы на вызов виртуальной функции
составили от 5 до 20 процентов. Т.о. вызов виртуальной функции не несет
существенных накладных расходов — очень хорошая новость.

2.7.2. Обобщенные алгоритмы, стр. 79
Встроенные в С++ типы низкого уровня, такие как указатели и массивы, имеют
соответствующие операции, поэтому мы можем записать:
char vc1[200];
char vc2[500];

void f()
{
copy(&vc1[0],&vc1[200],&vc2[0]);
}

На самом деле, если подойти к делу формально, то писать так мы не должны, хотя
практически всегда ничего неожиданного не произойдет. Вот что говорит об этом
д-р Страуструп:
The issue is whether taking the address of one-past-the-last element of an array
is conforming C and C++. I could make the example clearly conforming by a simple
rewrite:
copy(vc1,vc1+200,vc2);

However, I don't want to introduce addition to pointers at this point of the
book. It is a surprise to most experienced C and C++ programmers that &vc1[200]
isn't completely equivalent to vc1+200. In fact, it was a surprise to the C
committee also and I expect it to be fixed in the upcoming revision of the
standard. (also resolved for C9x - bs 10/13/98).
Суть вопроса в том, означает ли взятие адреса элемента, следующего за последним
одни и те же действия в С и С++. Я мог сделать пример более корректным простой
заменой
copy(vc1,vc1+200,vc2);

Однако, я не хотел вводить сложение с указателем в этой части книги. Даже для
самых опытных программистов на С и С++ большим сюрпризом является тот факт, что
&vc1[200] не полностью эквивалентно vc1+200. Фактически, это оказалось
неожиданностью и для С комитета, и я надеюсь, что это недоразумение будет
устранено в следующих редакциях стандарта.
Так в чем же нарушается эквивалентность? По стандарту С++ мы имеем следующие
эквивалентные преобразования:
&vc1[200] == &(*((vc1)+(200))) == &*(vc1+200)

Действительно ли равенство &*(vc1+200) == vc1+200 неверно?
It is false in C89 and C++, but not in K&R C or C9x. The C89 standard simply
said that &*(vc1+200) means dereference vc1+200 (which is an error) and then
take the address of the result, and the C++ standard copiled the C89 wording.
K&R C and C9x say that &* cancels out so that &*(vc1+200) == vc2+200.
Это неверно в С89 и С++, но не в K&R С или С9х. Стандарт С89 говорит, что
&*(vc1+200) означает разыменование vc1+200 (что является ошибкой) и затем взятие
адреса результата. И стандарт С++ просто взял эту формулировку из С. Однако K&R
С и С9х устанавливают, что &* взаимно уничтожаются, т.е. &*(vc1+200) == vc1+200.
Спешу вас успокоить, что на практике в выражении &*(vc1+200) некорректное
разыменование *(vc1+200) практически никогда не произойдет, т.к. результатом
всего выражения является адрес и ни один серьезный компилятор не станет выбирать
значение по некоторому адресу (операция разыменования) чтобы потом получить тот
же самый адрес с помощью операции &.

5.1.1. Ноль, стр. 128
Если вы чувствуете, что просто обязаны определить NULL, воспользуйтесь
const int NULL=0;

Лично я бы советовал быть более осмотрительным. Существуют платформы, где размер
указателя не равен размеру int. Рассмотрим, например, следующий код:
#include "stdio.h"
#include "stdarg.h"
#include "stdlib.h"

void error(int stat ...)
{
va_list ap;
va_start(ap,stat);

while (const char* sarg=va_arg(ap,const char *))
printf("%s",sarg);

va_end(ap);
exit(stat);
}

int main()
{
error(1,"Случилось ","страшное",NULL);
}

Здесь описана функция error() с переменным числом параметров. Она печатает
переданные ей строки, пока не встретит нулевой указатель и прекращает выполнение
программы с заданным кодом завершения. Если в нашей реализации
sizeof(char*)"sizeof(int), и NULL определен через int, то при запуске
приведенного кода нас ждут большие неприятности.
Как же быть? Прежде всего, не следует использовать подобного рода функции,
провоцирующие ошибки. Ну а если они действительно нужны, то важно помнить, что
функциям с переменным числом параметров нужно передавать в точности ожидаемые
типы. Т.е. приведенный вызов следует переписать так:
error(1,"Случилось ","страшное",static_cast"const char *"(0));

Каюсь, ненужная писанина выводит меня из себя, и в реальной программе я бы
использовал эквивалентное:
error(1,"Случилось ","страшное",(char *)0);

хоть это и не педагогично.

7.4. Перегруженные имена функций, стр. 192
Процесс поиска подходящей функции из множества перегруженных заключается в ...
Приведенный в книге пункт 2 нужно заменить на:
[2] Соответствие, достигаемое "продвижением" ("повышением в чине") интегральных
типов (например, bool в int, char в int, short в int; ¬ B.6.1), float в double.
Также следует отметить, что доступность функций-членов не влияет на процесс
поиска подходящей функции, например:
struct A {
private:
void f(int);
public:
void f(...);
};

void g()
{
A a;
a.f(1); // ошибка: выбирается A::f(int), использование
// которой в g() запрещено
}

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

7.6. Неуказанное количество аргументов, стр. 199
До выхода из функции, где была использована va_start(), необходимо осуществить
вызов va_end(). Причина состоит в том, что va_start() может модифицировать стек
таким образом, что станет невозможен нормальный выход из функции.
Ввиду чего возникают совершенно незаметные подводные камни. Общеизвестно, что
обработка исключения предполагает раскрутку стека. Следовательно, если в момент
возбуждения исключения функция изменила стек, то у вас гарантированно будут
неприятности. Таким образом, до вызова va_end() следует воздерживаться от
потенциально вызывающих исключения операций. Специально добавлю, что ввод/вывод
С++ может генерировать исключения, т.е. "наивная" техника вывода в std::cout до
вызова va_end() чревата неприятностями.
Д-р Страуструп пишет по этому поводу следующее:
It is your job to ensure that va_end() is called. The language and standard
library offers no guarantees. I don't recommend the use of varargs.
Это ваше дело следить за тем, чтобы va_end() была вызвана. Язык и стандартная
библиотека не дают никаких гарантий. Я бы не рекомендовал использовать технику
stdarg.

7.7. Указатель на функцию, стр. 202
Причина в том, что разрешение использования cmp3 в качестве аргумента ssort()
нарушило бы гарантию того, что ssort() вызовется с аргументами mytype*.
Здесь имеет место досадная опечатка, совершенно искажающая смысл предложения.

Следует читать так: Причина в том, что разрешение использования cmp3 в качестве
аргумента ssort() нарушило бы гарантию того, что cmp3() вызовется с аргументами
mytype*.

10.4.6.2. Члены-константы, стр. 296
Можно проинициализировать член, являющийся статической константой интегрального
типа, добавив к объявлению члена константное выражение в качестве
инициализирующего значения.
Вроде бы все хорошо, но почему только интегрального типа? В чем причина подобной
дискриминации? Д-р Страуструп пишет по этому поводу следующее:
The reason for "discriminating against" floating points in constant expressions
is that the precision of floating point traditionally varied radically between
processors. In principle, constant expressions should be evaluated on the target
processor if you are cross compiling.
Причина подобной "дискриминации" плавающей арифметики в константных выражениях в
том, что обычно точность подобных операций на разных процессорах существенно
отличается. В принципе, если вы осуществляете кросс-компиляцию, то такие
константные выражения должны вычисляться на целевом процессоре.
От себя добавлю, что звучит это немного нелогично, т.к. я могу написать так:
class Curious {
static const float c5;
};

const float Curious::c5=7.5f;

а вот такой код:
class Curious { static const float c5=7.5f; };
уже вызовет ошибку. К счастью, далеко не все компиляторы страдают такими
надуманными ограничениями, но, к сожалению, использовав эти послабления, вы
получите непереносимый код, что может превратить в кошмар "простой перенос
тривиального кода" в другую среду.

10.4.7. Массивы, стр. 297
Не существует способа явного указания аргументов конструктора (за исключением
использования списка инициализации) при объявлении массива.
Но вы легко можете его получить, не изменяя при этом исходный класс, что, вообще
говоря, важно. Для этого достаточно воспользоваться локальным классом:
#include "stdio.h"

struct A { // исходный класс
int a;
A(int a_) : a(a_) { printf("%d\n",a); }
};

void fun()
{
static int s;

struct A_fun : public A { // вспомогательный локальный
A_fun() : A(s++) { }
};

A_fun arr[5];
// и далее используем как A arr[5];
}

int main()
{
fun();
}

К сожалению, локальные классы и их использование остались за рамками книги. Я
попытаюсь исправить данное упущение. Вот часть стандарта о локальных классах:
9.8. Объявления локальных классов (Local class declarations [class.local])
- 1 - Класс может быть определен внутри функции; такой класс называется
локальным (local) классом. Имя локального класса является локальным в окружающем
контексте (enclosing scope). Локальный класс находится в окружающем контексте и
имеет тот же доступ к именам вне функции, что и у самой функции. Объявления в
локальном классе могут использовать только имена типов, статические переменные,
extern переменные и функции, перечисления из окружающего контекста. Например:
int x;
void f()
{
static int s;
int x;
extern int g();

struct local {
int g() { return x; } // ошибка, auto x
int h() { return s; } // OK
int k() { return ::x; } // OK
int l() { return g(); } // OK
};
// ...
}

local* p = 0; // ошибка: нет local в текущем контексте

- 2 - Окружающая функция не имеет специального доступа к членам локального
класса, она имеет обычные права доступа (см. class.access). Функции-члены
локального класса, если они есть, должны быть определены внутри определения
класса.
- 3 - Если класс X — локальный класс, то в нем может быть объявлен вложенный
класс Y, который может быть определен позднее, в определении класса X; или он
может быть определен позднее в том же контексте, что и X. Вложенный класс
локального класса сам является локальным.
- 4 - Локальный класс не может иметь статических данных-членов.

11.3.1. Операторы-члены и не-члены, стр. 316
complex r1=x+y+z; // r1=operator+(x,operator+(y,z))

На самом деле данное выражение будет проинтерпретировано так:
complex r1=x+y+z; // r1=operator+(operator+(x,y),z)

Потому что операция сложения левоассоциативна: (x+y)+z.

11.5.1. Поиск друзей, стр. 328
Приведенный в конце данной страницы пример нужно заменить на:
// нет f() в данной области видимости

class X {
friend void f(); // бесполезно
friend void h(const X&); // может быть найдена по аргументам
};

void g(const X& x)
{
f(); // нет f() в данной области видимости
h(x); // h() — друг X
}

Он взят из списка авторских исправлений к 8-му тиражу.

11.7.1. Явные конструкторы, стр. 333
Разница между
String s1='a'; // ошибка: нет явного преобразования char в String
String s2(10); // правильно: строка для хранения 10 символов

может показаться очень тонкой...
На самом деле разница сразу же заметна невооруженным глазом. Если мы пишем
X a=b;

то это подразумевает, что мы хотим создать объект a класса X ,
проинициализировав его значением объекта b некоторого класса. А инициализация
всегда означает копирование, т.е. если b того же типа, что и a, то сразу
вызывается конструктор копирования, что эквивалентно записи
X a(b);

Если же объект b имеет другой тип, то с помощью соответствующего конструктора
(неявно) создается временный объект класса X и далее все идет по накатанной
колее. То есть
X a(X(b));

Надеюсь, что после этих объяснений разница между объявлением s1 и s2 стала
очевидной.
Ввиду всего вышесказанного хотелось бы отметить, что для типов, определяемых
пользователем, не стоит использовать нотацию
X a=b;

т.к. в лучшем случае она эквивалентна более явному
X a(b);

а в худшем приводит к совершенно излишнему созданию временных объектов, как,
например, в объявлении
complex z=5;

Вместе с тем, реализация вправе не создавать ненужные временные объекты. Вот
соответствующая часть стандарта.
12.8. Копирование объектов класса (Copying class objects)
- 15 - Каждый раз, когда временный объект класса копируется при помощи
конструктора копирования, и данный объект и копия имеют идентичные
cv-квалификаторы, реализации разрешено считать оригинал и копию двумя ссылками
на один и тот же объект и не осуществлять копирование, даже если копирующий
конструктор или деструктор имеет побочный эффект. Для возвращающей класс
функции, если выражение в операторе return является именем локального объекта и
cv-квалификаторы локального объекта совпадают с типом возвращаемого значения,
реализации разрешено не создавать временный объект для хранения возвращаемого
значения, даже если копирующий конструктор или деструктор имеет побочный эффект.
В этих случаях объект будет уничтожен позднее (время его жизни определяется
копией).
Давайте не поленимся и напишем маленький класс, позволяющий отследить
возникающие при этом спецэффекты.
#include "stdio.h"
#include "string.h"

struct A {
static const int nsize=10;

char n[nsize];

A(char cn) {
n[0]=cn;
n[1]=0;
printf("%5s.A::A()\n",n);
}

A(const A& a) {
if (strlen(a.n)"=nsize-2) {
n[0]='?';
strcpy(n+1,a.n);
}
else strcpy(n,"беда");
printf("%5s.A::A(const A& %s)\n",n,a.n);
}

~A() {
printf("%5s.A::~A()\n",n);
}

A& operator=(const A& a) {
if (strlen(a.n)"=nsize-2) {
n[0]='=';
strcpy(n+1,a.n);
}
else strcpy(n,"беда");
printf("%5s.A::operator=(const A& %s)\n",n,a.n);
return *this;
}

};

A f1(A a)
{
printf("A f1(A %s)\n",a.n);
return a;
}

A f2()
{
printf("A f2()\n");
A b('b');
return b;
}

A f3()
{
printf("A f3()\n");
return A('c');
}

int main()
{
{
A a('a');
A b='b';
A c(A('c'));
A d=A('d');
}
printf("----------\n");
{
A a('a');
A b=f1(a);
printf("b это %s\n",b.n);
}
printf("----------\n");
{
A a=f2();
printf("a это %s\n",a.n);
}
printf("----------\n");
{
A a=f3();
printf("a это %s\n",a.n);
}
}

Прежде всего, в main() разными способами создаются объекты a, b, c и d. В
нормальной реализации вы получите следующий вывод:
a.A::A()
b.A::A()
c.A::A()
d.A::A()
d.A::~A()
c.A::~A()
b.A::~A()
a.A::~A()

Там же, где разработчики компилятора схалтурили, появятся ненужные временные
объекты, например:
c.A::A()
?c.A::A(const A& c)
c.A::~A()
d.A::A()
d.A::~A()
?c.A::~A()
Т.е. A c(A('c')) превратилось в A tmp('c'), c(tmp). Далее, вызов f1()
демонстрирует неявные вызовы конструкторов копирования во всей красе:
a.A::A()
?a.A::A(const A& a)
A f1(A ?a)
??a.A::A(const A& ?a)
?a.A::~A()
b это ??a
??a.A::~A()
a.A::~A()

На основании a создается временный объект ?a, и передается f1() качестве
аргумента. Далее, внутри f1() на основании ?a создается другой временный объект
— ??a, он нужен для возврата значения. А вот тут-то и происходит исключение
нового временного объекта — b это ??a, т.е. локальная переменная main() b --
это та самая, созданная в f1() переменная ??a, а не ее копия (специально для
сомневающихся: будь это не так, мы бы увидели b это ???a).
Полностью согласен — все это действительно очень запутанно, но разобраться все
же стоит. Для более явной демонстрации исключения временной переменной я написал
f2() и f3():
A f2()
b.A::A()
?b.A::A(const A& b)
b.A::~A()
a это ?b
?b.A::~A()

A f3()
c.A::A()
a это c
c.A::~A()

В f2() оно происходит, а в f3() — нет, как говорится, все дело в волшебных
пузырьках. Другого объяснения нет, т.к. временная переменная должна была
исключиться в обоих случаях (ох уж мне эти писатели компиляторов!).
А сейчас рассмотрим более интересный случай — перегрузку операторов. Внесем в
наш класс соответствующие изменения:
#include "stdio.h"
#include "string.h"

struct A {
static const int nsize=10;
static int tmpcount;

int val;
char n[nsize];

A(int val_) : val(val_) { // для создания временных объектов
sprintf(n,"_%d",++tmpcount);
printf("%5s.A::A(int %d)\n",n,val);
}

A(char cn,int val_) : val(val_) {
n[0]=cn;
n[1]=0;
printf("%5s.A::A(char,int %d)\n",n,val);
}

A(const A& a) : val(a.val) {
if (strlen(a.n)"=nsize-2) {
n[0]='?';
strcpy(n+1,a.n);
}
else strcpy(n,"беда");
printf("%5s.A::A(const A& %s)\n",n,a.n);
}

~A() {
printf("%5s.A::~A()\n",n);
}

A& operator=(const A& a) {
val=a.val;
if (strlen(a.n)"=nsize-2) {
n[0]='=';
strcpy(n+1,a.n);
}
else strcpy(n,"беда");
printf("%5s.A::operator=(const A& %s)\n",n,a.n);
return *this;
}

friend A operator+(const A& a1,const A& a2) {
printf("operator+(const A& %s,const A& %s)\n",a1.n,a2.n);
return A(a1.val+a2.val);
}
};

int A::tmpcount;

int main()
{
A a('a',1), b('b',2), c('c',3);
A d=a+b+c;
printf("d это %s\n",d.n);
printf("d.val=%d\n",d.val);
}

После запуска вы должны получить следующие результаты:
a.A::A(char,int 1)
b.A::A(char,int 2)
c.A::A(char,int 3)
operator+(const A& a,const A& b)
_1.A::A(int 3)
operator+(const A& _1,const A& c)
_2.A::A(int 6)
_1.A::~A()
d это _2
d.val=6
_2.A::~A()
c.A::~A()
b.A::~A()
a.A::~A()

Список страниц

Закладка в соц.сетях

Купить

☏ Заказ рекламы: +3805044-68-872

© Ассоциация электронных библиотек Украины

☝ Все материалы сайта (включая статьи, изображения, рекламные объявления и пр.) предназначены только для предварительного ознакомления. Все права на публикации, представленные на сайте принадлежат их законным владельцам. Просим Вас не сохранять копии информации.