Сергей Деревяго. C++ 3rd: комментарии --------------------------------------------------------------- © Copyright Сергей Деревяго, 2000 Версия исправленная и дополненная, 12 Oct 2004 Origin: http://ders.stml.net/cpp/ │ http://ders.stml.net/cpp/ ---------------------------------------------------------------
Введение | |
43 | 1.3.1. Эффективность и структура |
73 | 2.5.5. Виртуальные функции |
79 | 2.7.2. Обобщенные алгоритмы |
128 | 5.1.1. Ноль |
192 | 7.4. Перегруженные имена функций |
199 | 7.6. Неуказанное количество аргументов |
202 | 7.7. Указатель на функцию |
296 | 10.4.6.2. Члены-константы |
297 | 10.4.7. Массивы |
316 | 11.3.1. Операторы-члены и не-члены |
328 | 11.5.1. Поиск друзей |
333 | 11.7.1. Явные конструкторы |
337 | 11.9. Вызов функции |
344 | 11.12. Класс String |
351 | 12.2. Производные классы |
361 | 12.2.6. Виртуальные функции |
382 | 13.2.3. Параметры шаблонов |
399 | 13.6.2. Члены-шаблоны |
419 | 14.4.1. Использование конструкторов и деструкторов |
421 | 14.4.2. auto_ptr |
422 | 14.4.4. Исключения и оператор new |
431 | 14.6.1. Проверка спецификаций исключений |
431 | 14.6.3. Отображение исключений |
460 | 15.3.2. Доступ к базовым классам |
461 | 15.3.2.1. Множественное наследование и управление доступом |
475 | 15.5. Указатели на члены |
477 | 15.6. Свободная память |
478 | 15.6. Свободная память |
479 | 15.6.1. Выделение памяти под массив |
480 | 15.6.2. "Виртуальные конструкторы" |
498 | 16.2.3. STL-контейнеры |
505 | 16.3.4. Конструкторы |
508 | 16.3.5. Операции со стеком |
526 | 17.1.4.1. Сравнения |
541 | 17.4.1.2. Итераторы и пары |
543 | 17.4.1.3. Индексация |
555 | 17.5.3.3. Другие операции |
556 | 17.6. Определение нового контейнера |
583 | 18.4.4.1. Связыватели |
584 | 18.4.4.2. Адаптеры функций-членов |
592 | 18.6. Алгоритмы, модифицирующие последовательность |
592 | 18.6.1. Копирование |
622 | 19.2.5. Обратные итераторы |
634 | 19.4.1. Стандартный распределитель памяти |
637 | 19.4.2. Распределители памяти, определяемые пользователем |
641 | 19.4.4. Неинициализированная память |
647 | 20.2.1. Особенности символов |
652 | 20.3.4. Конструкторы |
655 | 20.3.6. Присваивание |
676 | 21.2.2. Вывод встроенных типов |
687 | 21.3.4. Ввод символов |
701 | 21.4.6.3. Манипуляторы, определяемые пользователем |
711 | 21.6.2. Потоки ввода и буфера |
773 | 23.4.3.1. Этап 1: выявление классов |
879 | А.5. Выражения |
931 | B.13.2. Друзья |
935 | B.13.6. template как квалификатор |
Оптимизация | |
Макросы | |
Исходный код |
В процессе чтения (и многократного) перечитывания C++ 3rd у меня возникало множество вопросов, большая часть которых отпадала после изучения собственно стандарта и продолжительных раздумий, а за некоторыми приходилось обращаться непосредственно к автору. Хочется выразить безусловную благодарность д-ру Страуструпу за его ответы на все мои, заслуживающие внимания, вопросы и разрешение привести данные ответы здесь.
Как читать эту книгу. Прежде всего, нужно прочитать "Язык программирования C++" и только на этапе второго или третьего перечитывания обращаться к моему материалу, т.к. здесь кроме исправления ошибок русского перевода излагаются и весьма нетривиальные вещи, которые вряд ли будут интересны среднему программисту на C++. Моей целью было улучшить перевод C++ 3rd, насколько это возможно и пролить свет на множество интересных особенностей C++. Кроме того, оригинальное (английское) издание пережило довольно много тиражей, и каждый тираж содержал некоторые исправления, я постарался привести все существенные исправления здесь.
Если вы что-то не поняли в русском переводе, то первым делом стоит заглянуть в оригинал: Bjarne Stroustrup "The C++ Programming language", 3rd edition и/или в стандарт C++ (ISO/IEC 14882 Programming languages - C++, First edition, 1998-09-01). К слову сказать, как и любой другой труд сравнимого объема и сложности, стандарт C++ также содержит ошибки. Для того, чтобы быть в курсе последних изменений стандарта, будет полезным просматривать C++ Standard Core Issues List и C++ Standard Library Issues List на его оффициальной странице.
Также не помешает ознакомиться с классической STL, ведущей начало непосредственно от Алекса Степанова. И, главное, не забудьте заглянуть к самому Бьерну Страуструпу.
Кстати, если вы еще не читали "The C programming Language" by Brian W. Kernighan and Dennis M. Ritchie, 2е издание, то я вам советую непременно это сделать -- Классика!
С уважением, Сергей Деревяго.
new
, delete
, type_id
, dynamic_cast
, throw
и блока try
, отдельные выражения и инструкции C++ не требуют поддержки во время выполнения.
Хотелось бы отметить, что есть еще несколько очень важных мест, где мы имеем неожиданную и порой весьма существенную "поддержку времени выполнения". Это конструкторы/деструкторы (сложных) объектов, код создания/уничтожения массивов объектов, пролог/эпилог создающих объекты функций и, отчасти, вызовы виртуальных функций.
Для демонстрации данной печальной особенности рассмотрим следующую программу (замечу, что в исходном коде текст программы, как правило, разнесен по нескольким файлам для предотвращения агрессивного выбрасывания "мертвого кода" качественными оптимизаторами):
#include <stdio.h> #include <stdlib.h> #include <time.h> struct A { A(); ~A(); }; void ACon(); void ADes(); void f1() { A a; } void f2() { ACon(); ADes(); } long Var, Count; A::A() { Var++; } A::~A() { Var++; } void ACon() { Var++; } void ADes() { Var++; } 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); } }В ней функции
f1()
и f2()
делают одно и то же, только первая неявно, с помощью конструктора и деструктора класса A
, а вторая с помощью явного вызова ACon()
и ADes()
.
Для работы программа требует одного параметра -- сколько миллионов раз вызывать тестовые функции. Выберите значение, позволяющее f1()
работать несколько секунд и посмотрите на результат для f2()
.
При использовании качественного оптимизатора никакой разницы быть не должно; тем не менее, на некоторых платформах она определенно есть и порой достигает 10 раз!
А что же 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; f(); } void f2() { ACon(); f(); ADes(); }Как вы думаете, эквивалентны ли данные функции? Правильный ответ -- нет, т.к.
f1()
представляет собой
void f1() // псевдокод { A::A(); try { f(); } catch (...) { A::~A(); throw; } A::~A(); }Т.е. если конструктор успешно завершил свою работу, то языком гарантируется, что обязательно будет вызван деструктор. Т.е. там, где создаются некоторые объекты, компилятор специально вставляет блоки обработки исключений для гарантии вызова соответствующих деструкторов. А накладные расходы в оригинальной
f1()
чаще всего будут вызваны присутствием ненужных в данном случае блоков обработки исключений (фактически, присутствием "утяжеленных" прологов/эпилогов):
void f1() // псевдокод { A::A(); try { // пусто } catch (...) { A::~A(); throw; } A::~A(); }Дело в том, что компилятор обязан корректно обрабатывать все возможные случаи, поэтому для упрощения компилятора его разработчики часто не принимают во внимание "частные случаи", в которых можно не генерировать ненужный код. Увы, подобного рода упрощения компилятора очень плохо сказываются на производительности интенсивно использующего средства абстракции и
inline
функции кода. Хорошим примером подобного рода кода является STL, чье использование, при наличии плохого оптимизатора, вызывает чрезмерные накладные расходы.
Поэкспериментируйте со своим компилятором для определения его abstraction penalty -- гарантированно пригодится при оптимизации "узких мест".
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
и пересчитывая индекс вызываемой виртуальной функции в vtbl
. Однако это спровоцирует существенные расходы времени выполнения, что неприемлемо.
И раз уж так много слов было сказано про эффективность, давайте реально измерим относительную стоимость вызова виртуальной функции.
#include <stdio.h> #include <stdlib.h> #include <time.h> 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(); } long Var, Count; void B::f() { Var++; } void B::vf() { } void D::vf() { Var++; } 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); } }В зависимости от компилятора и платформы, накладные расходы на вызов виртуальной функции составили от 10% до 2.5 раз. Т.о. можно утверждать, что "виртуальность" небольших функций может обойтись сравнительно дорого.
И слово "небольших" здесь не случайно, т.к. уже даже тест с функцией Аккермана (отлично подходящей для выявления относительной стоимости вызова)
#include <stdio.h> #include <stdlib.h> #include <time.h> struct B { int ackf(int x, int y); virtual int vackf(int x, int y); }; struct D : B { int vackf(int x, int y); // замещаем B::vackf }; void f1(B* ptr) { ptr->ackf(3, 5); // 42438 вызовов! } void f2(B* ptr) { ptr->vackf(3, 5); // 42438 вызовов! } int B::ackf(int x, int y) { if (x==0) return y+1; else if (y==0) return ackf(x-1, 1); else return ackf(x-1, ackf(x, y-1)); } int B::vackf(int x, int y) { return 0; } int D::vackf(int x, int y) { if (x==0) return y+1; else if (y==0) return vackf(x-1, 1); else return vackf(x-1, vackf(x, y-1)); } long Count; 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<1000; j++) f1(&d); c2=clock(); printf("f1(): %ld ths 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<1000; j++) f2(&d); c2=clock(); printf("f2(): %ld ths calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } }показывает заметно другие результаты, существенно уменьшая относительную разность времени выполнения.
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 tovc1+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).Суть вопроса в том, разрешено ли в C и C++ взятие адреса элемента, следующего за последним элементом массива. Я мог сделать пример очевидно корректным простой заменой:
copy(vc1,vc1+200,vc2);Однако, я не хотел вводить сложение с указателем в этой части книги. Даже для самых опытных программистов на C и C++ большим сюрпризом является тот факт, что&vc1[200]
не полностью эквивалентноvc1+200
. Фактически, это оказалось неожиданностью и для C комитета, и я ожидаю, что это недоразумение будет устранено в следующих редакциях стандарта.
Так в чем же нарушается эквивалентность? По стандарту C++ мы имеем следующие эквивалентные преобразования:
&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 dereferencevc1+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 и C++, но не в K&R C или С9х. Стандарт С89 говорит, что
&*(vc1+200)
означает разыменованиеvc1+200
(что является ошибкой) и затем взятие адреса результата. И стандарт C++ просто взял эту формулировку из С89. Однако K&R C и С9х устанавливают, что&*
взаимно уничтожаются, т.е.&*(vc1+200) == vc1+200
.
Спешу вас успокоить, что на практике в выражении &*(vc1+200)
некорректное разыменование *(vc1+200)
практически никогда не произойдет, т.к. результатом всего выражения является адрес и ни один серьезный компилятор не станет выбирать значение по некоторому адресу (операция разыменования) чтобы потом получить тот же самый адрес с помощью операции &
.
NULL
, воспользуйтесь
const int NULL=0;
Суть данного совета в том, что согласно определению языка не существует контекста, в котором (определенное в заголовочном файле) значение NULL
было бы корректным, в то время как просто 0
-- нет.
Исходя из того же определения, передача NULL
в функции с переменным количеством параметров вместо корректного выражения вида static_cast<SomeType*>(0)
запрещена.
Безусловно, все это правильно, но на практике NULL
в функции с переменным количеством параметров все же передают. Например, так:
#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); // внимание, ошибка! // вместо NULL нужно использовать // static_cast<const char *>(0) }Именно для поддержки подобного рода практики (некорректной, но широко распространенной) реализациям разрешено определять
NULL
как 0L
(а не просто 0
) на архитектурах, где sizeof(void*)==sizeof(long)>sizeof(int)
.
Приведенный в книге пункт [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() запрещено }Отсутствие данного правила породило бы тонкие ошибки, когда выбор подходящей функции зависел бы от места вызова: в функции-члене или в обычной функции.
va_start()
, необходимо осуществить вызов va_end()
. Причина состоит в том, что va_start()
может модифицировать стек таким образом, что станет невозможен нормальный выход из функции.
Ввиду чего возникают совершенно незаметные подводные камни.
Общеизвестно, что обработка исключения предполагает раскрутку стека. Следовательно, если в момент возбуждения исключения функция изменила стек, то у вас гарантированно будут неприятности.
Таким образом, до вызова va_end()
следует воздерживаться от потенциально вызывающих исключения операций. Специально добавлю, что ввод/вывод C++ может генерировать исключения, т.е. "наивная" техника вывода в std::cout
до вызова va_end()
чревата неприятностями.
cmp3
в качестве аргумента ssort()
нарушило бы гарантию того, что ssort()
вызовется с аргументами mytype*
.
Здесь имеет место досадная опечатка, совершенно искажающая смысл предложения. Следует читать так: Причина в том, что разрешение использования cmp3
в качестве аргумента ssort()
нарушило бы гарантию того, что cmp3()
вызовется с аргументами mytype*
.
Вроде бы все хорошо, но почему только интегрального типа? В чем причина подобной дискриминации? Д-р Страуструп пишет по этому поводу следующее:
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=7.0; };в качестве (непереносимого) расширения языка.
К счастью, это ограничение можно сравнительно легко обойти. Например, посредством введения локального класса:
#include <stdio.h> struct A { // исходный класс int a; A(int a_) : a(a_) { printf("%d\n",a); } }; void f() { static int vals[]={2, 0, 0, 4}; static int curr=0; struct A_local : public A { // вспомогательный локальный A_local() : A(vals[curr++]) { } }; A_local arr[4]; // и далее используем как A arr[4]; } int main() { f(); }Т.к. локальные классы и их использование остались за рамками книги, далее приводится соответствующий раздел стандарта:
9.8 Объявления локальных классов [class.local]
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 в текущем контексте
Y
может быть объявлен внутри локального класса X
и определен внутри определения класса X
или же за его пределами, но в том же контексте (scope), что и класс X
. Вложенный класс локального класса сам является локальным.
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
.
// нет f() в данной области видимости class X { friend void f(); // бесполезно friend void h(const X&); // может быть найдена по аргументам }; void g(const X& x) { f(); // нет f() в данной области видимости h(x); // h() -- друг X }Он взят из списка авторских исправлений к 8-му тиражу и показывает, что если
f
не было в области видимости, то объявление функции-друга f()
внутри класса X
не вносит имя f
в область видимости, так что попытка вызова f()
из g()
является ошибкой.
String s1='a'; // ошибка: нет явного преобразования char в String String s2(10); // правильно: строка для хранения 10 символовможет показаться очень тонкой...
Но она несомненно есть. И дело тут вот в чем.
Запись
X a=b;всегда означает создание объекта
a
класса X
посредством копирования значения некоторого другого объекта класса X
. Здесь может быть два варианта:
b
уже является объектом класса X
. В этом случае мы получим непосредственный вызов конструктора копирования:
X a(b);
b
объектом класса X
не является. В этом случае должен быть создан временный объект класса X
, чье значение будет затем скопировано:
X a(X(b));Именно этот временный объект и не может быть создан в случае explicit-конструктора, что приводит к ошибке компиляции.
12.8 Копирование объектов классов [class.copy]
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()В
f3()
оно происходит, а в f2()
-- нет! Как говорится, все дело в волшебных пузырьках.
Другого объяснения нет, т.к. временная переменная могла была исключена в обоих случаях (ох уж мне эти писатели компиляторов!).
А сейчас рассмотрим более интересный случай -- перегрузку операторов. Внесем в наш класс соответствующие изменения:
#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)