Копирующий конструктор и некоторые особенности инициализации классов. Работа над ошибками с помощью статического анализатора кода. Часть 2.
Сегодня мы рассмотрим инициализацию классов - вызов конструктора и деструктора, особенности их вызова при использовании указателя на экземпляр класса, а также копирующий конструктор – для чего он нужен, как объявляется и где применяется.
Конструктор класса
Каждый класс должен иметь конструктор, он может быть пустым и ничего не делать, но чаще всего конструктор используют для инициализации переменных.
Конструктор может принимать несколько аргументов, давайте рассмотрим создание класса- TestClass1:
Заголовок:
#ifndef TESTCLASS1_H
#define TESTCLASS1_H
#include <QString>
class TestClass1
{
public:
TestClass1();
~TestClass1();
QString getStr() const;
void setStr(const QString &value);
private:
QString str;
};
#endif // TESTCLASS1_H
Реализация:
#include "testclass1.h"
#include <QDebug>
#define TEST_DEEBUG 0
TestClass1::TestClass1()
{
#ifdef TEST_DEEBUG
qDebug() << "Created epmty TestClass1!";
#endif
}
TestClass1::~TestClass1()
{
#ifdef TEST_DEEBUG
qDebug() << "Begin destroying TestClass1 with value: " << this->str << "...";
#endif
#ifdef TEST_DEEBUG
qDebug() << "TestClass1 destroyed!";
#endif
}
QString TestClass1::getStr() const
{
#ifdef TEST_DEEBUG
qDebug() << "TestClass1 string get: " << this->str;
#endif
return this->str;
}
void TestClass1::setStr(const QString &value)
{
#ifdef TEST_DEEBUG
qDebug() << "TestClass1 string set: " << value;
#endif
this->str = value;
}
Здесь используются директивы #ifdef
Если строка #define TEST_DEEBUG 0 раскомментирована то будут выводится дополнительные отладочные сообщения.
Использование:
#include "testclass1.h"
#include <QCoreApplication>
#include <QDebug>
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
qDebug() << "-------------";
qDebug() << "Program start!";
qDebug() << "-------------";
TestClass1 t;
t.setStr("12345");
qDebug() << t.getStr();
qDebug() << "-------------";
qDebug() << "Program end!";
qDebug() << "-------------";
app.quit();
}
Запустим:
-------------
Program start!
-------------
Created epmty TestClass1!
TestClass1 string set: "12345"
TestClass1 string get: "12345"
"12345"
-------------
Program end!
-------------
Begin destroying TestClass1 with value: "12345" ...
TestClass1 destroyed!
Деструктор класса
Деструктор класса вызывается перед уничтожением экземпляра класса, как правило он используется для освобождения памяти, если та была выделена динамически, а также для закрытия всех файлов и соединений.
Неявная инициализация
В c++ существует механизм неявной инициализации, позволяющий компилятору при передаче в качестве аргумента функции экземпляра класса, неявно преобразовать значение, переданное конструктору класса передаваемого в качестве аргумента.
Звучит запутано, давайте приведу пример, на stackoverflow (https://stackoverflow.com/questions/121162/what-does-the-explicit-keyword-mean) я нашел хороший пример.
Создадим тестовый класс Foo:
Заголовок:
#ifndef FOO_H
#define FOO_H
class Foo
{
public:
Foo (int foo);
int getFoo();
private:
int m_foo;
};
#endif // FOO_H
Реализация:
#include "foo.h"
Foo::Foo (int foo) : m_foo (foo)
{
}
int Foo::getFoo()
{
return m_foo;
}
Добавим отдельную функцию, не метод класса, в качестве аргумента принимающая экземпляр класса Foo:
int doBar(Foo foo)
{
return foo.getFoo();
}
И теперь вызываем эту функцию, передавая экземпляр класса в качестве аргумента:
qDebug() << doBar(42);
Программа выведет 42. Таким образом мы, в случае необходимости, можем использовать неявную инициализацию. Но строит избегать её, если это возможно, так как данный подход делает программный код сложнее для понимания и затрудняет отладку. Используйте неявную инициализацию только там, где это архитектурно оправдано!
Использование explicit при объявлении конструктора
Возьмите себе за правило, при объявлении конструкторов с любым количеством аргументов, и даже без них, использовать слово explicit.
Например, добавим новый конструктор класса:
Заголовок:
explicit Foo (QString str);
Реализация:
Foo::Foo (QString str)
{
this->m_foo = str.toInt();
}
Попробуем использовать
qDebug() << doBar(Foo("33"));
Программа выведет 33
А если мы попробуем написать так:
qDebug() << doBar("42");
Получим ошибку:
main.cpp:13:18: error: no matching function for call to 'doBar'
foo.h:18:5: note: candidate function not viable: no known conversion from 'const char [3]' to 'Foo' for 1st argument
Так как мы запретили неявную инициализацию, то получили ошибку.
Добавляем explicit в Testclass1
Вернемся к классу Testclass1 и добавим новый конструктор:
Вернемся к классу Testclass1 и добавим новый конструктор:
TestClass1::TestClass1(const QString &value)
{
#ifdef TEST_DEEBUG
qDebug() << "Created TestClass1 with value:" << value;
#endif
this->str = value;
}
Добавим explicit ко всем конструкторам:
explicit TestClass1();
explicit TestClass1(const QString &value);
Теперь Cppcheck не выдает предупреждений!
Область видимости и локальные переменные
Когда мы объявляем переменные, они существуют только внутри того блока, в котором объявлены. Это касается и экземпляров класса. Для примера добавим код:
for (int i=0; i< 2; i++)
{
TestClass1 t1(QString::number(i));
qDebug() << t1.getStr();
}
Запустим:
-------------
Program start!
-------------
Created epmty TestClass1!
TestClass1 string set: "12345"
TestClass1 string get: "12345"
"12345"
-------------
-------------
Created TestClass1 with value: "0"
TestClass1 string get: "0"
"0"
Begin destroying TestClass1 with value: "0" ...
TestClass1 destroyed!
-------------
Created TestClass1 with value: "1"
TestClass1 string get: "1"
"1"
Begin destroying TestClass1 with value: "1" ...
TestClass1 destroyed!
-------------
Program end!
-------------
Begin destroying TestClass1 with value: "12345" ...
TestClass1 destroyed!
Как видно деструктор класса вызывается каждый раз, кода выполнение кода доходит до закрывающей фигурной скобки.
Область видимости и указатели
Добавим код:
for (int i=10; i< 12; i++)
{
TestClass1 *t2 = new TestClass1(QString::number(i));
qDebug() << t2->getStr();
}
qDebug() << "--------";
qDebug() << "Program end!";
app.quit();
Запустим:
Created TestClass1 with value: "10"
TestClass1 string get: "10"
"10"
Created TestClass1 with value: "11"
TestClass1 string get: "11"
"11"
-------------
Program end!
-------------
Begin destroying TestClass1 with value: "12345" ...
TestClass1 destroyed!
Как видите деструктор для классов не был вызван, а значит не освобождена память, если она выделялась внутри конструктора класса.
При использовании new вы обязаны сами освобождать память!
Изменим код на:
TestClass1 *t2 = new TestClass1(QString::number(i));
qDebug() << t2->getStr();
delete t2;
Запустим:
Created TestClass1 with value: "10"
TestClass1 string get: "10"
"10"
Begin destroying TestClass1 with value: "10" ...
TestClass1 destroyed!
Created TestClass1 with value: "11"
TestClass1 string get: "11"
"11"
Begin destroying TestClass1 with value: "11" ...
TestClass1 destroyed!
-------------
Program end!
-------------
Begin destroying TestClass1 with value: "12345" ...
TestClass1 destroyed!
Теперь деструктор корректно вызывается.
Копирующий конструктор
До сих пор, при создании и удалении класса не возникало особых проблем.
Если ваш класс выделяет память в конструкторе, вы должны эту память освободить в деструкторе. Приведем пример, напишем новый класс - TestClass2:
Заголовок:
#ifndef TESTCLASS2_H
#define TESTCLASS2_H
#include "testclass1.h"
class TestClass2 //: public TestClass1
{
public:
explicit TestClass2();
explicit TestClass2(const QString &value);
~TestClass2();
char* getStrp() const;
void setStrp(const QString);
private:
char *strp;
};
#endif // TESTCLASS2_H
Реализация:
#include "testclass2.h"
#include <QDebug>
#define TEST_DEEBUG 0
TestClass2::TestClass2()
{
#ifdef TEST_DEEBUG
qDebug() << "Created TestClass2!";
#endif
this->strp = NULL;
}
TestClass2::~TestClass2()
{
#ifdef TEST_DEEBUG
qDebug() << "Begin destroying TestClass2 with value: " << QString(this->strp) << "...";
#endif
delete[] this->strp;
#ifdef TEST_DEEBUG
qDebug() << "TestClass2 destroyed!";
#endif
}
TestClass2::TestClass2(const QString &value)
{
#ifdef TEST_DEEBUG
qDebug() << "Created TestClass2 width value " << value;
#endif
this->strp = new char[value.length()+1];
strcpy(this->strp,(char*)value.toStdString().c_str());
}
char* TestClass2::getStrp() const
{
#ifdef TEST_DEEBUG
qDebug() << "TestClass2 pointer get: " << this->strp;
#endif
return strp;
}
void TestClass2::setStrp(const QString value)
{
#ifdef TEST_DEEBUG
qDebug() << "TestClass2 pointer set: " << value << "prev val: " << this->strp;
#endif
delete[] this->strp;
this->strp = new char[value.length()+1];
strcpy(this->strp,(char*)value.toStdString().c_str());
}
Используем созданный класс:
TestClass2 t3("100");
qDebug() << t3.getStrp();
Запустим:
Created TestClass2 width value "100"
TestClass2 pointer get: 100
100
-------------
Program end!
-------------
Begin destroying TestClass2 with value: "100" ...
TestClass2 destroyed!
Пока что всё работает как задумано.
Добавим еще код:
TestClass2 t4("500");
TestClass2 t5 = t4;
qDebug() << t4.getStrp();
qDebug() << t5.getStrp();
Запустим
Created TestClass2 width value "500"
TestClass2 pointer get: 500
500
TestClass2 pointer get: 500
500
-------------
Program end!
-------------
Begin destroying TestClass2 with value: "500" ...
TestClass2 destroyed!
Begin destroying TestClass2 with value: "02??\u000B\u0002" ...
В консоли появилось сообщение:
cppcheck1.exe завершился с кодом -1073740940
Как правило, подобные ошибки, возникающей после окончания работы программы, означают, что была осуществлена попытка два раза освободить память по одному и тому же адресу!
Почему так произошло? Дело в том, что мы создали новый экземпляр класса - t5 и присвоили ему значение экземпляра класса t4. По сути мы получили полную бинарную копию объекта, т.е. все указатели в полях класса t4 и t5 ссылаются на одни и те же области памяти!
Обратите внимание - несмотря на то, что указатели ссылаются на один и тот же участок памяти, сами переменные имеют разные адреса!
Когда пришло время освобождать память, был вызван деструктор для t5, который успешно освободил память, но затем пришло время освободить память в t4, но так как память по этому адресу уже была освобождена, произошло нарушение доступа к памяти, и программа завершилась с ошибкой.
Ранее вы, наверное, уже заметили, что cppcheck выдавал нам предупреждения:
testclass2.cpp:31: Class 'TestClass2' does not have a copy constructor which is recommended since it has dynamic memory/resource allocation(s).
testclass2.cpp:31: Class 'TestClass2' does not have a operator= which is recommended since it has dynamic memory/resource allocation(s).
Рассмотрим первое предупреждение.
Class 'TestClass2' does not have a copy constructor which is recommended since it has dynamic memory/resource allocation(s)
Все классы, которые динамически выделают память должны иметь копирующий конструктор!
Что такое копирующий конструктор? Это обычный конструктор, первый аргумент которого передается по константной ссылке, а все остальные аргументы должны иметь значения по умолчанию.
Добавим копирующий конструктор:
Объявление:
TestClass2(const TestClass2 &old);
Реализация:
TestClass2::TestClass2(const TestClass2 &old)
{
#ifdef TEST_DEEBUG
qDebug() << "Copy constructor called with old value " << old.getStrp();
#endif
int size = sizeof(*old.getStrp()) / sizeof(char);
this->strp = new char[size+1];
strcpy(this->strp,old.getStrp());
}
Обратите внимание - копирующий конструктор всегда объявляется без explicit!
Запустим:
Created TestClass2 width value "500"
Boolval = -1728052840
TestClass2 pointer get: 500
Copy constructor called with old value 500
TestClass2 pointer get: 500
TestClass2 pointer get: 500
500
TestClass2 pointer get: 500
500
-------------
Program end!
-------------
Begin destroying TestClass2 with value: "500" ...
TestClass2 destroyed!
Begin destroying TestClass2 with value: "500" ...
TestClass2 destroyed!
Begin destroying TestClass2 with value: "100" ...
TestClass2 destroyed!
Begin destroying TestClass1 with value: "12345" ...
TestClass1 destroyed!
На этот раз программа отработала и завершилась без ошибок.
Аргументы копирующего конструктора
Копирующий конструктор может иметь аргументы, но они должны иметь значение по умолчанию.
Добавим в класс TestClass2 новое поле:
int boolval;
И методы для доступа к нему:
int TestClass2::getBoolval() const
{
return boolval;
}
void TestClass2::setBoolval(int value)
{
boolval = value;
}
Добавим в обычный конструктор отладочную строку:
TestClass2::TestClass2(const QString &value)
{
#ifdef TEST_DEEBUG
qDebug() << "Created TestClass2 width value " << value;
qDebug() << "Boolval = " << this->boolval;
#endif
this->strp = new char[value.length()+1];
strcpy(this->strp,(char*)value.toStdString().c_str());
}
Запустим:
Created TestClass2 width value "500"
Boolval = -1979711093
TestClass2 pointer get: 500
Copy constructor called with old value 500
TestClass2 pointer get: 500
TestClass2 pointer get: 500
500
TestClass2 pointer get: 500
500
-------------
Program end!
-------------
Begin destroying TestClass2 with value: "500" ...
TestClass2 destroyed!
Begin destroying TestClass2 with value: "500" ...
TestClass2 destroyed!
Begin destroying TestClass2 with value: "100" ...
TestClass2 destroyed!
Begin destroying TestClass1 with value: "12345" ...
TestClass1 destroyed!
Как видите, значение
Boolval = -1979711093
По умолчанию значение int не должно быть равно 0, тут всё зависит от компилятора, в нашем случае выбрано случайное значение -1979711093
Добавим строки
qDebug() << "boolval=" << t4.getBoolval();
qDebug() << "boolval=" << t5.getBoolval();
Запустим:
Created TestClass2 width value "500"
Boolval = -821617984
TestClass2 pointer get: 500
Copy constructor called with old value 500
TestClass2 pointer get: 500
TestClass2 pointer get: 500
500
TestClass2 pointer get: 500
500
boolval= -821617984
boolval= 1152
-------------
Program end!
-------------
Begin destroying TestClass2 with value: "500" ...
TestClass2 destroyed!
Begin destroying TestClass2 with value: "500" ...
TestClass2 destroyed!
Begin destroying TestClass2 with value: "100" ...
TestClass2 destroyed!
Begin destroying TestClass1 with value: "12345" ...
TestClass1 destroyed!
Как видно из строк:
boolval= -821617984
boolval= 1152
Копирующий конструктор, по умолчанию, не инициализирует значение boolval.
Допустим, нам нужно чтобы при копировании значение boolval было равно 1. Это может понадобится для отслеживания копий.
Добавим в конструктор аргумент:
Объявление:
TestClass2(const TestClass2 &old, const int boolval=1);
Реализация:
TestClass2::TestClass2(const TestClass2 &old, const int boolval)
{
#ifdef TEST_DEEBUG
qDebug() << "Copy contructor called with old value " << old.getStrp();
qDebug() << "Copy contructor sets boolval = " << boolval;
#endif
int size = sizeof(*old.getStrp()) / sizeof(char);
this->strp = new char[size+1];
strcpy(this->strp,old.getStrp());
this->boolval = boolval;
}
Запустим - копирующий конструктор инициализировал boolval значением 1:
Copy contructor sets boolval = 1
Таким образом, вы можете инициализировать все необходимые поля класса
Заключение
Сегодня мы рассмотрели конструкторы классов и какие аргументы могут принимать конструкторы.
Рассмотрели неявную инициализацию, её использование.
Было рассмотрено слово explicit и его применение.
Была рассмотрена область видимости и то, как себя ведут локальные переменные и указатели, при выходе за пределы области видимости.
Рассмотрели копирующий конструктор:
- Сообщение: Class 'TestClass2' does not have a copy constructor which is recommended since it has dynamic memory/resource allocation(s)
- Аргументы копирующего конструктора
В следующей статье мы рассмотрим сообщение от cppcheck - Class 'TestClass2' does not have a operator= which is recommended since it has dynamic memory/resource allocation(s).
Исходный код проекта вы можете скачать на GitFlic
Добавить комментарий