Сергей Деревяго. C++ 3rd: комментарии --------------------------------------------------------------- © Copyright Сергей Деревяго, 2000 Версия исправленная и дополненная, 12 Oct 2004 Origin: http://ders.stml.net/cpp/ Ў http://ders.stml.net/cpp/ ---------------------------------------------------------------

Сергей Деревяго

C++ 3rd: комментарии


Введение
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++. Что в ней есть? В ней есть все, что нужно для глубокого понимания C++. Дело в том, что практически весь материал стоит на блестящей книге Б.Страуструпа "Язык программирования C++", 3е издание. Я абсолютно уверен, что интересующийся C++ программист обязан прочитать "Язык программирования C++", а после прочтения он вряд ли захочет перечитывать описание C++ у других авторов -- маловероятно, что кто-то напишет собственно о C++ лучше д-ра Страуструпа. Моя книга содержит исправления, комментарии и дополнения, но нигде нет повторения уже изложенного материала.

В процессе чтения (и многократного) перечитывания 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е издание, то я вам советую непременно это сделать -- Классика!

С уважением, Сергей Деревяго.


Стр.43: 1.3.1. Эффективность и структура

За исключением операторов 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 -- гарантированно пригодится при оптимизации "узких мест".


Стр.73: 2.5.5. Виртуальные функции

Требования по памяти составляют один указатель на каждый объект класса с виртуальными функциями, плюс одна 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);
 }
}
показывает заметно другие результаты, существенно уменьшая относительную разность времени выполнения.

Стр.79: 2.7.2. Обобщенные алгоритмы

Встроенные в C++ типы низкого уровня, такие как указатели и массивы, имеют соответствующие операции, поэтому мы можем записать:
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).

Суть вопроса в том, разрешено ли в 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 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 и C++, но не в K&R C или С9х. Стандарт С89 говорит, что &*(vc1+200) означает разыменование vc1+200 (что является ошибкой) и затем взятие адреса результата. И стандарт C++ просто взял эту формулировку из С89. Однако K&R C и С9х устанавливают, что &* взаимно уничтожаются, т.е. &*(vc1+200) == vc1+200.

Спешу вас успокоить, что на практике в выражении &*(vc1+200) некорректное разыменование *(vc1+200) практически никогда не произойдет, т.к. результатом всего выражения является адрес и ни один серьезный компилятор не станет выбирать значение по некоторому адресу (операция разыменования) чтобы потом получить тот же самый адрес с помощью операции &.


Стр.128: 5.1.1. Ноль

Если вы чувствуете, что просто обязаны определить 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).

Стр.192: 7.4. Перегруженные имена функций

Процесс поиска подходящей функции из множества перегруженных заключается в...

Приведенный в книге пункт [2] нужно заменить на:

  1. Соответствие, достигаемое "продвижением" ("повышением в чине") интегральных типов (например, 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() запрещено
}
Отсутствие данного правила породило бы тонкие ошибки, когда выбор подходящей функции зависел бы от места вызова: в функции-члене или в обычной функции.

Стр.199: 7.6. Неуказанное количество аргументов

До выхода из функции, где была использована va_start(), необходимо осуществить вызов va_end(). Причина состоит в том, что va_start() может модифицировать стек таким образом, что станет невозможен нормальный выход из функции.

Ввиду чего возникают совершенно незаметные подводные камни.

Общеизвестно, что обработка исключения предполагает раскрутку стека. Следовательно, если в момент возбуждения исключения функция изменила стек, то у вас гарантированно будут неприятности.

Таким образом, до вызова va_end() следует воздерживаться от потенциально вызывающих исключения операций. Специально добавлю, что ввод/вывод C++ может генерировать исключения, т.е. "наивная" техника вывода в std::cout до вызова va_end() чревата неприятностями.


Стр.202: 7.7. Указатель на функцию

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

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


Стр.296: 10.4.6.2. Члены-константы

Можно проинициализировать член, являющийся статической константой интегрального типа, добавив к объявлению члена константное выражение в качестве инициализирующего значения.

Вроде бы все хорошо, но почему только интегрального типа? В чем причина подобной дискриминации? Д-р Страуструп пишет по этому поводу следующее:

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;
};
в качестве (непереносимого) расширения языка.

Стр.297: 10.4.7. Массивы

Не существует способа явного указания аргументов конструктора (за исключением использования списка инициализации) при объявлении массива.

К счастью, это ограничение можно сравнительно легко обойти. Например, посредством введения локального класса:

#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]

  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. Окружающая функция никаких специальных прав доступа к членам локального класса не имеет, она подчиняется обычным правилам (см. раздел 11 [class.access]). Функции-члены локального класса, если они вообще есть, должны быть определены внутри определения класса.
  3. Вложенный класс Y может быть объявлен внутри локального класса X и определен внутри определения класса X или же за его пределами, но в том же контексте (scope), что и класс X. Вложенный класс локального класса сам является локальным.
  4. Локальный класс не может иметь статических данных-членов.

Стр.316: 11.3.1. Операторы-члены и не-члены

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.

Стр.328: 11.5.1. Поиск друзей

Приведенный в конце данной страницы пример нужно заменить на:
// нет 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() является ошибкой.

Стр.333: 11.7.1. Явные конструкторы

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

Но она несомненно есть. И дело тут вот в чем.

Запись

X a=b;
всегда означает создание объекта a класса X посредством копирования значения некоторого другого объекта класса X. Здесь может быть два варианта:
  1. Объект b уже является объектом класса X. В этом случае мы получим непосредственный вызов конструктора копирования:
    X a(b);
  2. Объект b объектом класса X не является. В этом случае должен быть создан временный объект класса X, чье значение будет затем скопировано:
    X a(X(b));
    Именно этот временный объект и не может быть создан в случае explicit-конструктора, что приводит к ошибке компиляции.
Еще одна тонкость состоит в том, что в определенных условиях реализациям разрешено не создавать временные объекты:

12.8 Копирование объектов классов [class.copy]

  1. Там, где временный объект копируется посредством конструктора копирования, и данный объект и его копия имеют один и тот же тип (игнорируя 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()
В 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)