Понедельник, 15 февраля 2021 19:00

Копирующий конструктор и некоторые особенности инициализации классов. Работа над ошибками с помощью статического анализатора кода. Часть 2.

Россия
Оцените материал
(0 голосов)

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

Конструктор класса

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

Конструктор может принимать несколько аргументов, давайте рассмотрим создание класса- 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 выдавал нам предупреждения:

2021-02-15_09-19-06.png

 

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).

Исходный код проекта вы можете скачать на Github.

Прочитано 718 раз Последнее изменение Понедельник, 15 февраля 2021 19:47