Перегрузка и переопределение функций

В контексте разговоров о C++ довольно часто можно встретить слова перегрузка и переопределение. Что же они означают и когда используются?

Перегрузка (англ. overloading) означает, что у вас в одной области видимости определено несколько функций с одинаковым именем. Например:

class A {
  void print();
  void print(int i);
  void print(const A& a);
};

Нужная функция выбирается в момент компиляции исходя из типа переданного аргумента.

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

class Base {
public:
  // реализует некоторый алгоритм
  void doSomething() {
    prepare(); // сперва выполнить подготовку

    // основная часть алгоритма не приведена для краткости
  }
private:
  virtual void prepare() {}
};

class Derived : public Base {
private:
  virtual void prepare() {
    // выполнить подготовку
    std::cout << "hello world :)\n";
  }
};

Derived obj;
obj.doSomething(); 

Поясню, что в последнем примере, при вызове функции doSomething будет работать функция prepare из производного класса Derived.

Зачем нужны потоки в C++

Как вы знаете, C++ поддерживает ввод-вывод посредством потоков (таких как iostream, fstream). Вы не задавались вопросом, зачем они нужны, ведь существуют функции, такие как printf, fgets и т.д.? Потоки искользуют переопределённые операторы << и >> для осуществления ввода и вывода, но ведь то же самое можно реализовать и через функции. Для кого-то реализация:

printf("Hello World!");

выглядит более логично и читабельно чем:

cout << "Hello World";

Попробуем ответить. Самое главное — потоковый ввод-вывод безопасен в отношении типов. Такой код:

int a = 123;
printf("%s", a);

ведёт себе абсолютно не так, как вы ожидаете, а вот потоковый вывод:

int a = 123;
std::cout << a;

не имеет таких проблем.

Другая важная особенность потоков — то что они придерживаются методологии объектной ориентированности.  Допустим, вы пишете программу, которая выводит какие-то данные. На определённом этапе разработки вы решаете, что нужно выводить не только в консоль, но ещё и в файл. Как решить эту проблему на уровне языка C? Видимо, нужно заменить вызовы функции printf на fprintf и передавать дескриптор файла вглубь своей программы. В C++ же вам просто нужно заменить экземпляр потока вывода передавая в функцию не std::cout, а файловый поток, а остальной код остаётся без изменений.

void doSomething(std::ostream& output)
{
   output << "this and that" << a;
}
doSomething(std::cout);
doSomething(std::ofstream("c:\file.txt"));

PS: Нужно сказать, что некоторые современные компиляторы проводят проверку типов для функций типа printf.

С++ больше нет

На днях один мой знакомый побывал на самом крупном столичном книжном рынке. Там очаровательная блондинка потрясла его известием: «С++ больше нет, теперь вместо него С#»

Отсюда

Ближайшие планы

В ближайших планах заметок для публикации на блоге:

  • std::for_each — одноимённый блогу алгоритм стандартной библиотеки
  • CRTP — статическая виртуализация
  • RAII — наверное, половина мощи C++ в этой аббревиатуре.
  • std::auto_ptr — за что любить и ненавидеть умный указатель из стандартной библиотеки шаблонов

В комментариях голосуйте, какие из этих тем наиболее востребованы — они появятся в первую очередь. Также можно предлагать свои, альтернативные, темы интересных заметок.

Спасибо за то что вы читаете это.

Синтаксис указателей в C++

Содержание:

Начнём с того, что указатели бывают на данные либо на функции. Так как C++ обратно совместим с C, то рассмотрим сперва указатели в языке C.

Указатели на данные в стиле C

 

Начинающий изучать язык C знакомится сперва с указателями на данные. Всё просто, для взятия адреса используется operator &, для разыменования — operator *. Например, такой код:

    1 #include <iostream>

    2

    3 using std::cout;

    4

    5 int main(void) {

    6   int a = 0;

    7   cout << a << ‘\n’;

    8   int* pa;

    9   pa = &a;

   10   ++(*pa);

   11   cout << a << ‘\n’;

   12   return 0;

   13 }

Выведет:

0

1

Указатель может указывать на переменную типа структура (или класс), в таком случае, для доступа к членам структуры применяется operator->. Пример:

    1 #include <iostream>

    2

    3 using std::cout;

    4

    5 struct cpp_for_each {

    6   int a;

    7   float b;

    8 };

    9

   10 int main(void) {

   11   cpp_for_each obj;

   12   cpp_for_each* ptr;

   13   ptr = &obj;

   14   ptr->a = 1;

   15   ptr->b = 2.5f — ptr->a;

   16   cout << obj.b << ‘\n’;

   17   return 0;

   18 }

Арифметика указателей

 

С указателями на данные возможны арифметические операции сложения и вычитания. Пример:

    1 #include <iostream>

    2

    3 using std::cout;

    4

    5 int main(void) {

    6   int a = 0;

    7   int b = 0;

    8   ptrdiff_t inInts = &a — &b;

    9   ptrdiff_t inChars =

   10     reinterpret_cast<char*>(&a)

   11     — reinterpret_cast<char*>(&b);

   12   cout

   13     << inChars << ‘ ‘

   14     << inInts << ‘\n’;

   15   return 0;

   16 }

Единица измерения разницы между указателями — это количество элементов указываемого типа. В приведённом примере в переменной inInts будет разница в int-ах, а в inChars — в char-ах. Надо заметить, что данный код может выводить на экран разные числа в зависимости от:

  • компилятора
  • размера int
  • вида сборки (debug/release)

Кроме вычитания (вычисления разницы указателей), к указателям можно применять операции сложения и вычитания с целым числом. Если в приведённом выше примере сложить &b и inInts, то мы получим &a:

    1 #include <iostream>

    2

    3 using std::cout;

    4

    5 int main(void) {

    6   int a = 0;

    7   int b = 0;

    8   ptrdiff_t inInts = &a — &b;

    9   int* c = &b + inInts;

   10   cout <<

   11     ((&a == c)

   12       ? «Equal\n»

   13       : «Not equal\n»);

   14   return 0;

   15 }

Таким образом с двумя указателями можно производить следующие арифметические операции:

—  ==  !=  <  >  <=  >=

а с одним указателем и числом такие:

+  —  ++  —  +=  -=

С арифметикой указателей связана операция доступа к элементу массива по индексу. В приведённом ниже примере, все переменные получат значение элемента массива с индексом 3, т.е. это всё эквивалентные записи одной и той же идеи:

    1 #include <iostream>

    2

    3 using std::cout;

    4

    5 int main(void) {

    6   int a[5] = {1,2,3,4,5};

    7   int v1 = *(a + 3);

    8   int v2 = *(3 + a);

    9   int v3 = a[3];

   10   int v4 = 3[a];

   11   int v5 = *(&a[0] + 3);

   12   cout << v1 << ‘\n’;

   13   cout << v2 << ‘\n’;

   14   cout << v3 << ‘\n’;

   15   cout << v4 << ‘\n’;

   16   cout << v5 << ‘\n’;

   17   return 0;

   18 }

Константность

 

Надо заметить, что если мы перепишем самый первый пример и заменим там строку 8 таким образом:

    8   const int* pa;

то компилятор откажется компилировать строку 10, аргументируя тем, что мы пытаемся изменить данные используя указатель на константные, то есть неизменяемые, данные.

d:\vc\cppforeach\cppforeach\cppforeach.cpp(10) : error C3892: ‘pa’ : you cannot assign to a variable that is const

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

    8   const int* const pa;

А если мы напишем так:

    8   int* const pa;

То двигать указатель мы по-прежнему не сможем, а менять содержимое памяти по указателю сможем.

Есть простое правило, как определить, какой указатель перед нами: нужно прочитать его тип справа налево и послушать, что получается:

  • int * — указатель на целое
  • const int * — указатель на целую константу
  • int * const — константный указатель на целое
  • const int * const — константный указатель на целую константу

Указатели на функции в стиле С

Кроме данных и указателей на них в языке существуют функции и указатели на функции. Принцип работы с указателями на функции довольно понятный, а синтаксис приведён в следующем примере:

    1 #include <iostream>

    2

    3 using std::cout;

    4

    5 void f(void) {

    6   cout << «Do something\n»;

    7 }

    8

    9 int main(void) {

   10   // обычный вызов

   11   f();

   12

   13   // вызов через взятие указателя

   14   // и его разыменование

   15   (*&f)();

   16

   17   // сохранить указатель

   18   // в переменную

   19   void (*pf)(void) = &f;

   20   // вызов функции по указателю

   21   (*pf)();

   22

   23   // тип для данных указателей

   24   typedef void (*funcType)(void);

   25   // переменная этого типа

   26   funcType pf2 = &f;

   27   (*pf2)();

   28   return 0;

   29 }

Думаю, вы без труда догадаетесь сами, как вызвать функции с параметрами и как сохранить возвращаемое значение. Арифметика указателей не действует для указателей на функции. Единственные операции, которые с ними можно проводить — это проверять на равенство.

Указатели на члены данных (C++)

 

Если класс или структура содержит несколько членов данных одного типа, мы можем создать указатель на такие члены данных и обращаться к ним.

    1 #include <iostream>

    2

    3 using std::cout;

    4 using std::ostream;

    5

    6 // наш тестовый класс

    7 struct test {

    8   int first;

    9   int second;

   10   void out(ostream& o) const ;

   11 };

   12

   13 ostream& operator<<(

   14   ostream& out, const test& obj) {

   15   out << obj.first << ‘ ‘

   16       << obj.second;

   17   return out;

   18 };

   19

   20 void test::out(ostream& o) const {

   21   o << *this << ‘\n’;

   22 }

   23

   24 void zero(

   25   test* p, int test::* v) {

   26   p->*v = 0;

   27 }

   28

   29 int main(void) {

   30   // создаём и инициализируем

   31   // объект

   32   test obj;

   33   obj.first = 1;

   34   obj.second = 1;

   35   obj.out(cout);

   36

   37   // обнуляем первый член

   38   zero(&obj, &test::first);

   39   obj.out(cout);

   40

   41   // обнуляем второй член

   42   zero(&obj, &test::second);

   43   obj.out(cout);

   44

   45   // создаём ссылку на объект

   46   test& ref = obj;

   47   // тип — указатель на int

   48   // — член данных test

   49   typedef int test::* int_in_test;

   50

   51   // манипулируем первым членом

   52   // через ссылку

   53   int_in_test v = &test::first;

   54   ref.*v = 2;

   55   obj.out(cout);

   56

   57   // аналогично со вторым

   58   v = &test::second;

   59   ref.*v = 3;

   60   obj.out(cout);

   61

   62   return 0;

   63 }

Указатели на функции

 

С ними всё по аналогии:

    1 #include <iostream>

    2

    3 using std::cout;

    4

    5 struct test {

    6   void do1(void) {cout <<«1\n»;}

    7   void do2(void) {cout <<«2\n»;}

    8 };

    9

   10 typedef void (test::*vtv)(void);

   11

   12 void zero(

   13   test* p, vtv v) {

   14   (p->*v)();

   15 }

   16

   17

   18 int main(void) {

   19   test obj;

   20   zero(&obj, &test::do1);

   21   zero(&obj, &test::do2);

   22   return 0;

   23 }

Надо заметить, что метод класса может возвращать указатель на свои закрытые члены-данных или методы. Аналогия может быть такая: у класса есть ключи к закрытым членам, он открывает их и отдаёт  вовне. Пользователям же такого класса ключи не нужны. Таким образом, компилятор проверяет права на доступ при взятии адреса на член, после этого, если права подтверждены, код может передать этот указатель в другие места программы, где прав на взятие указателя может не быть, но тем не менее уже взятый указатель будет доступен.

    1 #include <iostream>

    2

    3 using std::cout;

    4

    5 struct test;

    6

    7 typedef void (test::*vtv)(void);

    8

    9 struct test {

   10  static vtv getFunc(char c) {

   11   if(c == ‘1’) return &test::do1;

   12   if(c == ‘2’) return &test::do2;

   13   return 0;

   14  }

   15 private:

   16   void do1(void) {cout <<«1\n»;}

   17   void do2(void) {cout <<«2\n»;}

   18 };

   19

   20 int main(void) {

   21   test obj;

   22   vtv vtv1 = test::getFunc(‘1’);

   23   vtv vtv2 = test::getFunc(‘2’);

   24   (obj.*vtv1)();

   25   (obj.*vtv2)();

   26   return 0;

   27 }

Вот и всё на сегодня.

Must Read: Герб Саттер "Новые сложные задачи на C++"

Эта книга из серии Must Read. Вот ссылка на неё на сайте самого автора. Книга вышла в серии под названием «C++ In-Depth» под редакцией создателя языка — Бьярна Страуструпа. Книга выдержана в традиционной уже для этой серии книг манере задач для самостоятельного обдумывания с последующим подробным объяснением. Причём вопросы делятся на вопросы для новичков в C++ и вопросы для профессионалов. Не удивляйтесь, если прочитав её вы увидите, что-то знакомое — некоторые материалы на блоге используют информацию подчерпнутую и в ней, равно как и в любых других источниках. Темы задач, предложенных в книге довольно обширны — это: обобщённое программирование и стандартная библиотека, безопасность исключений, дизайн классов, наследование и полиморфизм, оптимизация и эффективность, головоломки и отдельно рассмотрены ошибки при проектировании класса std::string.

OZON.ru

Вокруг public, private и protected

Тема довольно избита, однако, я всё-таки решил начать с неё. Думаю, новичкам будет полезно.

Итак, public, private и protected — это модификаторы доступа, а не видимости, как ошибочно думают некоторые. Private члены видны снаружи класса, но не доступны.

Теперь кратко, кому какой доступ они предоставляют.

  • Public — доступ открыт всем, кто видит определение данного класса.
  • Private — доступ открыт самому классу (т.е. функциям-членам данного класса) и друзьям (friend) данного класса, как функциям, так и классам.
  • Protected — доступ открыт классам, производным от данного.

Далее приведены примеры доступа с указанием какие поля в каких местах программы доступны.

class some {
  friend void f(some&);
public:
  int a_;
protected:
  int b_;
private:
  int c_;
};

void f(some& obj) {
  obj.a_ = 0; // OK
  obj.b_ = 0; // OK
  obj.c_ = 0; // OK
}

void g(some& obj) {
  obj.a_ = 0; // OK
  obj.b_ = 0; // compile time error
  obj.c_ = 0; // compile time error
}

class derived : public some {
  derived() {
    a_ = 0; // OK
    b_ = 0; // OK
    c_ = 0; // compile time error
  }
};

В C++ существует public-наследование, private-наследование и protected-наследование. В зависимости от того, какой тип используется, изменяется доступ к членам базового класса для клиентов производного. В таблице сведена информация об этом изменении.

Исходный модификатор доступа
public private protected
public-наследование public private protected
private-наследование private private private
protected-наследование protected private protected

Следует добавить, что производный класс может изменить модификатор доступа с protected на public, разместив using объявление в соответствующей секции класса:

class some {
public:
  int a;
protected:
  int b;
private:
  int c;
};

class derived : public some {
public:
  using some::b;
};

void f(derived& obj) {
  obj.b = 0; // OK
}

Напоследок приведу несколько приёмов, с помощью которых можно «достучаться» до закрытых функций или данных. Допустим, у нас есть класс some и нам нужно обнулить закрытую переменную c:

  • Модифицировать определение класса, добавив друга (функцию или класс)
    class some {
      friend class some_friend;
    public:
      int a;
    protected:
      int b;
    private:
      int c;
    };
    
    class some_friend {
    public:
      static void hack(some& obj) {
        obj.c = 0;
      }
    };
    
  • Воспользоваться препроцессором:
    #define private public
    
    class some {
    public:
      int a;
    protected:
      int b;
    private:
      int c;
    };
    
    void hack(some& obj) {
      obj.c = 0;
    }
    
  • Создать класс с таким же расположением в памяти и воспользоваться reinterpret_cast для преобразования указателей:

    class some {
    public:
      int a;
    protected:
      int b;
    private:
      int c;
    };
    
    class hack_some {
    public:
      int a;
      int b;
      int c;
    };
    
    void h(some& obj) {
      reinterpret_cast<hack_some*>(&obj)->c = 0;
    }
    
  • Если у «взламываемого» класса есть шаблонная функция, можно её специализировать своим типом:

    class some {
    public:
      int a;
      template<class T> void func(void) {
        a = b + c;
      }
    protected:
      int b;
    private:
      int c;
    };
    
    class hack_template_param{};
    
    template<>
    void some::func<hack_template_param>(void) {
      c = 0;
    }
    
    void hack(void) {
      some o;
      o.func<hack_template_param>();
    };
    

Очевидно, что способ с reinterpret_cast работает только для доступа к закрытым членам данных или вызова виртуальных функций. Остальные же способы позволяют как модифицировать закрытые данные, так и вызывать закрытые невиртуальные методы.

На сегодня всё.

UPD (31.08.2013): Для компиляторов GCC и CLANG существует опция -fno-access-control, которая позволяет отключить контроль доступа к членам класса со стороны компилятора.

UPD (31.08.2013): Переформатировал исходный код под использование встроенного плагина вордпресса.