1

I've got problem with my Matrix program.

There are my errors:

24 bytes in 1 blocks are indirectly lost in loss record 1 of 7
==5334==    at 0x4C2E80F: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5334==    by 0x402B26: Matrix::CountingReference::CountingReference(int, int) (in /home/detek/Pulpit/Matrix/main)
==5334==    by 0x401725: Matrix::Matrix(int, int) (in /home/detek/Pulpit/Matrix/main)
==5334==    by 0x401305: main (in /home/detek/Pulpit/Matrix/main)
==5334== 
==5334== 24 bytes in 1 blocks are indirectly lost in loss record 2 of 7
==5334==    at 0x4C2E80F: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5334==    by 0x402B26: Matrix::CountingReference::CountingReference(int, int) (in /home/detek/Pulpit/Matrix/main)
==5334==    by 0x401725: Matrix::Matrix(int, int) (in /home/detek/Pulpit/Matrix/main)
==5334==    by 0x40187D: Matrix::operator+(Matrix const&) (in /home/detek/Pulpit/Matrix/main)
==5334==    by 0x40144E: main (in /home/detek/Pulpit/Matrix/main)
==5334== 
==5334== 72 bytes in 3 blocks are indirectly lost in loss record 3 of 7
==5334==    at 0x4C2E80F: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5334==    by 0x402B7D: Matrix::CountingReference::CountingReference(int, int) (in /home/detek/Pulpit/Matrix/main)
==5334==    by 0x401725: Matrix::Matrix(int, int) (in /home/detek/Pulpit/Matrix/main)
==5334==    by 0x401305: main (in /home/detek/Pulpit/Matrix/main)
==5334== 
==5334== 72 bytes in 3 blocks are indirectly lost in loss record 4 of 7
==5334==    at 0x4C2E80F: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5334==    by 0x402B7D: Matrix::CountingReference::CountingReference(int, int) (in /home/detek/Pulpit/Matrix/main)
==5334==    by 0x401725: Matrix::Matrix(int, int) (in /home/detek/Pulpit/Matrix/main)
==5334==    by 0x40187D: Matrix::operator+(Matrix const&) (in /home/detek/Pulpit/Matrix/main)
==5334==    by 0x40144E: main (in /home/detek/Pulpit/Matrix/main)
==5334== 
==5334== 120 (24 direct, 96 indirect) bytes in 1 blocks are definitely lost in loss record 5 of 7
==5334==    at 0x4C2E0EF: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5334==    by 0x401712: Matrix::Matrix(int, int) (in /home/detek/Pulpit/Matrix/main)
==5334==    by 0x401305: main (in /home/detek/Pulpit/Matrix/main)
==5334== 
==5334== 120 (24 direct, 96 indirect) bytes in 1 blocks are definitely lost in loss record 6 of 7
==5334==    at 0x4C2E0EF: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5334==    by 0x401712: Matrix::Matrix(int, int) (in /home/detek/Pulpit/Matrix/main)
==5334==    by 0x40187D: Matrix::operator+(Matrix const&) (in /home/detek/Pulpit/Matrix/main)
==5334==    by 0x40144E: main (in /home/detek/Pulpit/Matrix/main)

And my code is here:

It's Matrix.cpp

#include <iostream>
#include <cstdlib>
#include <fstream>
#include <ctime>
#include "Matrix.h"
using namespace std;

ostream& operator<<(ostream& o,const Matrix& Object)
{
    for(int i=0;i<Object.dane->wiersz;i++)
    {
        for(int j=0;j<Object.dane->kolumna;j++)
        {
            o<<Object.dane->wsk[i][j]<<" ";
        }
        o<<endl;
    }
    return o;
}

Matrix::Matrix()
{
    dane=new CountingReference();
}

Matrix::Matrix(int wiersz, int col)
{
    dane=new CountingReference(wiersz,col);
}

Matrix::Matrix(const Matrix& Object)
{
    Object.dane->countingReference++;
    dane=Object.dane;
}

Matrix::~Matrix()
{
    if(dane->countingReference==-1)
    {
        delete dane;
    }
}

int Matrix::readref()
{
    return this->dane->countingReference;
}

Matrix& Matrix::operator=(const Matrix& Object)
{
    Object.dane->countingReference++;
    if(dane->countingReference==0)
    {
        delete dane;
    }
    dane=Object.dane;
    return *this;
}

Matrix Matrix::operator+(const Matrix& Object) throw(string)
{
    Matrix A(dane->wiersz,dane->kolumna);
    if(dane->wiersz!=Object.dane->wiersz || dane->kolumna!=Object.dane->kolumna)
    {
        string wyjatek = "Nie sa rowne.";
        throw wyjatek;
    }
    else{
        int i,j;
        for(i=0; i<dane->wiersz;i++)
        {
            for(j=0; j<dane->kolumna; j++)
            {  
                A.dane->wsk[i][j]=dane->wsk[i][j]+Object.dane->wsk[i][j];
            }
        }
    }
    return A;
}

Matrix Matrix::operator-(const Matrix& Object) throw(string)
{
    Matrix A(dane->wiersz,dane->kolumna);
    if(dane->wiersz!=Object.dane->wiersz || dane->kolumna!=Object.dane->kolumna)
    {
        string wyjatek = "Nie sa rowne.";
        throw wyjatek;  
    }
    else{
        for(int i=0;i<dane->wiersz;i++)
        {
            for(int j=0;j<dane->kolumna;j++)
            {
                A.dane->wsk[i][j]=dane->wsk[i][j]-Object.dane->wsk[i][j];
            }
        }
    }
    return A;
}

Matrix Matrix::operator*(const Matrix& Object) throw(string)
{
    double temp=0;
    Matrix A(dane->wiersz,dane->kolumna);
    if(dane->kolumna!=Object.dane->wiersz)
    {
        string wyjatek = "Nie sa rowne";
        throw wyjatek;
    }
    else{
        for(int i=0;i<dane->wiersz;i++)
        {
            for(int j=0;j<Object.dane->kolumna;j++)
            {
                for(int k=0;k<dane->kolumna;k++)
                {
                    temp+=dane->wsk[i][j]*Object.dane->wsk[j][k];
                }

                A.dane->wsk[i][j]=temp;
            }
        }
    }

    return A;
}

Matrix Matrix::operator+=(const Matrix& Object) throw(string)
{
    if(dane->wiersz!=Object.dane->wiersz || dane->kolumna!=Object.dane->kolumna)
    {
        string wyjatek = "Nie sa rowne.";
        throw wyjatek;  
    }
    else{
        dane=dane->detach();
        for(int i=0;i<dane->wiersz;i++)
        {
            for(int j=0;j<dane->kolumna;j++)
            {
                dane->wsk[i][j]+=Object.dane->wsk[i][j];
            }
        }
    }

    return *this;
}

Matrix& Matrix::operator-=(const Matrix& Object) throw(string)
{
    if(dane->wiersz!=Object.dane->wiersz || dane->kolumna!=Object.dane->kolumna)
    {
        string wyjatek = "Nie sa rowne.";
        throw wyjatek;  
    }
    else{
        dane=dane->detach();
        for(int i=0;i<dane->wiersz;i++)
        {
            for(int j=0;j<dane->kolumna;j++)
            {
                dane->wsk[i][j]-=Object.dane->wsk[i][j];
            }
        }
    }
    return *this;
}

Matrix& Matrix::operator*=(const Matrix& Object) throw(string)
{
    if(dane->kolumna!=Object.dane->wiersz)
    {
        string wyjatek = "Nie sa rowne.";
        throw wyjatek;
    }
    else
    {
        int m=0,n=0;
        double temp=0;
        m=dane->wiersz;
        n=Object.dane->kolumna;
        Matrix A(m,n);
        for(int i=0;i<dane->wiersz;i++)
        {
            for(int j=0;j<Object.dane->kolumna;j++)
            {
                for(int k=0;k<dane->kolumna;k++)
                {
                    temp+=dane->wsk[i][k]*Object.dane->wsk[k][j];
                }

                A.dane->wsk[i][j] = temp;
                temp=0;
            }
        }

        dane=dane->detach();
        for(int i=0;i<m;i++)
        {
            for(int j=0;j<n;j++)
            {
                dane->wsk[i][j]=A.dane->wsk[i][j];
            }
        }       
    }
    return *this;
}

Matrix& Matrix::LoadFromFile(const char *string)
{
    int m=0,n=0;
    ifstream inFile;
    inFile.open(string);
    if(inFile.good())
    {
        inFile>>m;
        inFile>>n;
        dane=dane->detach();

        for(int i=0;i<m;i++)
        {
            for(int j=0;j<n;j++)
            {
                inFile>>dane->wsk[i][j];
            }
        }
        inFile.close();
    }
    return *this;
}

double Matrix::read( int i, int j) const
{
    return dane->wsk[i-1][j-1];
}

void Matrix::write(int i, int j, const double x)
{
    dane = dane->detach();
    dane->wsk[i-1][j-1] = x;    
}

bool Matrix::operator == (const Matrix& Object)
{   
    int i,j;
    for(i=0;i<dane->wiersz;i++)
    {
        for(j=0;j<dane->kolumna;j++)
        {
            if(dane->wsk[i][j]!=Object.dane->wsk[i][j])
            {
                return false;
            }   
        }
    }
    return true;
}

Matrix& Random(Matrix& Object)
{
    for(int i=0;i<Object.dane->wiersz;i++)
    {
        for(int j=0;j<Object.dane->kolumna;j++)
        {
            Object.dane->wsk[i][j]=rand()%100+1;
        }
    }

    return Object;
}

Matrix::Mref Matrix::operator()(int i, int j)
{
    return Mref(*this,i,j);
}

Matrix::CountingReference::CountingReference()
{
    wiersz=0;
    kolumna=0;
    wsk=NULL;
    countingReference=0;
}

Matrix::CountingReference::CountingReference(int wier, int kol)
{
    countingReference=0;    
    wiersz=wier;
    kolumna=kol;
    wsk=new double*[wier];
    for(int i=0;i<wier;i++)
    {
        wsk[i]=new double[kol];
    }
}

Matrix::CountingReference::~CountingReference()
{
    for(int i=0;i<wiersz;i++)
    {
        delete [] wsk[i];
    }

    delete [] wsk;
    wsk=NULL;
}

Matrix::CountingReference *Matrix::CountingReference::detach()
{
    CountingReference *pointer;

    if(countingReference==0)
    {
        return this;
    }

    pointer=new CountingReference(wiersz,kolumna);

    for(int i=0;i<wiersz;i++)
    {
        for(int j=0;j<kolumna;j++)
    {
            pointer->wsk[i][j]=wsk[i][j];
        }
    }
    countingReference--;
    return pointer;
}

Matrix::Mref::operator double() const
{
    return s.read(i,k);       
}

Matrix::Mref &Matrix::Mref::operator = (double c)
{
    s.write(i,k,c); 
    return *this;
}

Matrix::Mref &Matrix::Mref::operator = (const Mref& ref)
{
    return operator= ((double)ref);   
}

main.cpp

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <fstream>
#include "Matrix.h"

using namespace std;

int main()
{

    Matrix A(3,3);
    Matrix B(3,3); 
    Matrix C(3,3);
    Matrix D(3,3);
    Matrix F();

    Random(B);
    Random(C);
    Random(D);

    cout<<"B: "<<endl<<B<<endl;
    cout<<"C: "<<endl<<C<<endl;
    cout<<"D: "<<endl<<D<<endl;
    cout<<A.readref()<<endl;
    A=B+C;
    cout<<A.readref()<<endl;
    A=B=C=D;
    cout<<A.readref()<<endl;
return 0;
}

I know, that somewhere in my destructors I don't release memory. But I already checked everything and still didn't find the problem... I don't even know which of my pointers i should release. Could you help me?

EDIT

Here is my Matrix.h

 #include <iostream>
#include <cassert>

using namespace std;

class Matrix{
    private:
        class CountingReference
        {
            friend class Matrix; 
            public:
                double **wsk;
                int wiersz;
                int kolumna;
                int countingReference;
                CountingReference();
                CountingReference(int, int);
                ~CountingReference();
                CountingReference* detach();
        };
        CountingReference *dane;

    public:

        class Mref
        {   
            friend class Matrix;
            Matrix& s;
            int i,k;
            Mref (Matrix& m, int r, int c): s(m), i(r), k(c) {}   

            public:
                operator double() const;
                Mref& operator = (double c);
                Mref& operator = (const Mref& ref);
        };

        friend ostream& operator<<(ostream& o,const Matrix&);
        friend ostream& operator<<(const Matrix&, ostream& o);
        Matrix();
        Matrix(int, int);
        Matrix(const Matrix&);
        ~Matrix();
        Matrix& operator=(const Matrix&);
        Matrix operator+(const Matrix&) throw(string);
        Matrix operator-(const Matrix&) throw(string);
        Matrix operator*(const Matrix&) throw(string);
        Matrix operator+=(const Matrix&) throw(string);
        Matrix& operator-=(const Matrix&) throw(string);
        Matrix& operator*=(const Matrix&) throw(string);
        bool operator == (const Matrix &);
        Matrix& LoadFromFile(const char*);
        friend Matrix& Random(Matrix&);
        double read(int, int) const;
        int readref();  
        void write(int, int, const double);
        Mref operator()(int, int);
        friend class CountingReference;
};
  • 1
    Looks like you bare trying to reinvent `std::shared_ptr`. Why not just use `shared_ptr`? If that's off the table, start with `shared_ptr`, make sure all of your other logic is correct, and then replace the `shared_ptr` with your home brew. Less to have to debug all at once. – user4581301 Jan 10 '18 at 22:55
  • 1
    You can also steal a bit from `shared_ptr`. Build all of the destruction logic into `CountingReference` so that all `Matrix` has is a `CountingReference` that goes out of scope and either `delete`s the contained memory or doesn't depending on the reference count. Right now you are forcing `Matrix` to make decisions about another object based on that other object's data, and that smells bad. – user4581301 Jan 10 '18 at 22:59
  • To complete the question and have build-able code, could you add Matrix.h or a subset of it that provides the compilable definitions of `Matrix` and `CountingReference`? – user4581301 Jan 10 '18 at 23:07
  • I add my Matrix.h file. – Bartek Dettlaff Jan 10 '18 at 23:18
  • Thank you for the header. I see only one `countingReference--;` in the code, in `CountingReference::detach`. I would expect one in the `Matrix` destructor as well. Likely this is the problem. Nothing decrements the reference counter when a containing object is destroyed. – user4581301 Jan 10 '18 at 23:28
  • @BartekDettlaff -- You could eliminate more than half that code. The operators `+`, `-`, `*`, are one liners. Just use the `+=`, `-=`, etc. as helper functions. Example: `Matrix Matrix::operator+(const Matrix& Object) { return Matrix(*this) += Object; }`. Yes -- that's all you need to do -- no need for the code duplication. Of course, your `+=`, `-=`, etc. have to work correctly. – PaulMcKenzie Jan 11 '18 at 00:29
  • @user4581301 I add this line of code in my 'Matrix' at the end of destructor , but there is still the same problem. 22 allocs and 11 frees. – Bartek Dettlaff Jan 11 '18 at 08:41
  • A useful hint when using Valgrind: compile with debugging symbols (e.g. `g++ -g`) so that it can report the line numbers for you. – Toby Speight Jan 11 '18 at 12:43
  • @PaulMcKenzie we can ALMOST do that. Unfortunately because the copy construction will result in the copy having a reference-counted pointer to the same array as the original and change the original. Something more like `return Matrix::clone(*this) += Object;` is required. This is a pretty complicated assignment. Think I might write an answer. – user4581301 Jan 11 '18 at 20:40

2 Answers2

1

based on this page in Valgrind's site : http://valgrind.org/docs/manual/faq.html#faq.deflost usually "indirectly lost " comes as a result of a "definitly lost" error .

seems like there are cases that your constructor does not delete all of the fields of your class what happens when dane->countingReference!=-1 ? how do you delete the inside fields at this case ? valgrind seems to dislike mostly what happens regarding the data allocated after the Counting reference constructor

and at operator+ : you create a new Matrix object and return it by value ,which means you created 2 of them and in some cases (again , because of the term :dane->countingReference==-1 )you do not free them .

Daniel
  • 106
  • 6
  • This term i made, because my lecturer said that i have to do something like that: If there is no reference or reference to itself, the counter is 0, so 0 means "Object exist" so I decide that if this counter is -1 - it means that it has to delete the object. – Bartek Dettlaff Jan 10 '18 at 23:21
  • correct me if im wrong , what you said is that counter!=-1 means that there is an object , and -1 means that there isnt . so you delete it ony if there is no object pointing to it but delete it via the other object that supposed to point to it ? and i saw you only reducing the counter in deattach , youll probably need to do it in the destructor as well – Daniel Jan 11 '18 at 09:25
  • I have already added line in Destructor, but it is still not enough. – Bartek Dettlaff Jan 11 '18 at 10:41
1

The crux of the problem: The Matrix destructor tests dane->countingReference but does not decrement it, so dane->countingReference never reaches the point at which is is destroyed. A dane->countingReference--; and a walk through with a debugger to pick off any off-by-one errors will fix that, but the coupling between Matrix and CountingReference is troubling and making the book keeping much more difficult than it needs to be.

A class should have as few responsibilities as possible and preferably one. This usually makes it easier to write, maintain, and debug. Matrix is responsible for matrix manipulation AND the reference counting AND the memory management, relegating CountingReference to a mere container.

If Matrix, sticks to just doing matrix manipulation, all you need to write and test is matrix manipulation.

If CountingReference is spun off to an equal class responsible for keeping track of the tightly related responsibilities of reference counting and memory management, Matrix can get on with its real job: doing math. Let's take a look at how we can make a smarter CountingReference. First, we take advantage of the fundamental C++ concept Resource Allocation is Initialization or RAII (What is meant by Resource Acquisition is Initialization (RAII)?) to help manage our memory.

class CountingReference
{
private:
    int * countingReference;
public:
    int wiersz;
    int kolumna;
    double **wsk;
    CountingReference();
    CountingReference(int, int);
    CountingReference(const CountingReference &);
    ~CountingReference();
    CountingReference& operator=(CountingReference rhs);
};

Similar to the original, but note that the countingReference member is a pointer and the copy and assignment operators to observe another fundamental C++ concept, The Rule of Three (What is The Rule of Three?). countingReference observing The Rule of Three allows Matrix to take advantage of The Rule of Zero. For now we will ignore the Rule of Five.

There is no need for CountingReference* detach();

Note this is not thread safe. Making this thread safe is really, really nasty if you try to write this without using <atomic> and if you are using <atomic> you might as well use std::shared_ptr and make the whole problem go away.

Implementations:

CountingReference::CountingReference() :
        countingReference(new int(1)), wiersz(0), kolumna(0), wsk(NULL)

{
}

CountingReference::CountingReference(int wier, int kol) :
        countingReference(new int(1)), wiersz(wier), kolumna(kol), wsk(new double*[wier])
{
    for (int i = 0; i < wier; i++)
    {
        wsk[i] = new double[kol];
    }
    cout << countingReference << " was created \n";
}

These constructors are similar to the originals other than using the member initializer list, countingReference being a pointer, and countingReference starting at 1 because it's more intuitive if the reference count matches the number of instances and destroys the shared data on 0.

countingReference is a pointer because all of the instances of CountingReference sharing the data must have the same reference counter. Basically instead of the Matrix instances sharing the pointer to the CountingReference, they will now all have their own CountingReference that share the same countingReference. This is where RAII kicks in and save us a lot of work.

Also note a few debug statements so I'd know if I was handing you bad code. I might still be offering bad code, but if it is, at least the mistake isn't something silly.

CountingReference::CountingReference(const CountingReference & src) :
        countingReference(src.countingReference), wiersz(src.wiersz), kolumna(src.kolumna), wsk(src.wsk)

{
    ++(*countingReference);
    cout << countingReference << " was copied (" << *countingReference << ")\n";
}

Copy constructor. Stores the same array and countingReference as the source and increments countingReference to keep track of the number of references. Part 1 of the Rule of Three.

CountingReference::~CountingReference()
{
    --(*countingReference);
    cout << countingReference << " was decremented (" << *countingReference << ")\n";
    if (!*countingReference)
    {
        cout << countingReference << " was destroyed\n";
        for (int i = 0; i < wiersz; i++)
        {
            delete[] wsk[i];
        }
        delete[] wsk;
        delete countingReference;
    }
}

Destructor. When any CountingReference goes out of scope if decrements the reference count and if the count is now 0, releases the array and countingReference. This automates all of the clean-up so that you cannot leak memory. Part 2 of The Rule of Three.

CountingReference & Matrix::CountingReference::operator=(CountingReference rhs)
{
    cout << countingReference << " was replaced by " << rhs.countingReference << "\n";
    swap(countingReference, rhs.countingReference);
    swap(wsk, rhs.wsk);
    swap(wiersz, rhs.wiersz);
    swap(kolumna, rhs.kolumna);
    return *this;
}

Assignment operator. Using the Copy and Swap Idiom. This isn't the most efficient way to perform the assignment, but it is foolproof. Part 3 of The Rule of Three.

Matrix winds up looking like

class Matrix
{
private:
    class CountingReference
    {
    private:
        int * countingReference;
    public:
        int wiersz;
        int kolumna;
        double **wsk;
        CountingReference();
        CountingReference(int, int);
        CountingReference(const CountingReference &);
        ~CountingReference();
        CountingReference& operator=(CountingReference rhs);
    };
    CountingReference dane;

public:

    friend ostream& operator<<(ostream& o, const Matrix&);
    friend ostream& operator<<(const Matrix&, ostream& o);
    Matrix();
    Matrix(int, int);
    static Matrix clone(const Matrix &);
    Matrix operator+(const Matrix &) const;
    Matrix operator-(const Matrix &) const;
    Matrix operator*(const Matrix &) const;
    Matrix operator+=(const Matrix&);
    Matrix& operator-=(const Matrix&);
    Matrix& operator*=(const Matrix&);
    bool operator ==(const Matrix &);
    friend Matrix& Random(Matrix&);
};

There is no need for a custom destructor, copy constructor or assignment operator because Matrix has no resources that it needs to manage (The Rule of Zero) so they are left to the compiler to generate.

I have discarded all of the exception specifiers (throw(string)) because in my experience they cause much pain for very little benefit. But don't just trust me, read what Herb Sutter has to say on the topic. He generally knows what he's talking about.

The function implementations are greatly simplified as they no longer need to worry about detach and applying the lessons learned from sbi's excellent What are the basic rules and idioms for operator overloading? and another hint described by Paul Mackenzie in the comments above we wind up with functions that look like:

Matrix Matrix::operator+=(const Matrix& Object)
{
    if (dane.wiersz != Object.dane.wiersz
            || dane.kolumna != Object.dane.kolumna)
    {
        string wyjatek = "Nie sa rowne.";
        throw wyjatek;
    }
    else
    {
        for (int i = 0; i < dane.wiersz; i++)
        {
            for (int j = 0; j < dane.kolumna; j++)
            {
                dane.wsk[i][j] += Object.dane.wsk[i][j];
            }
        }
    }

    return *this;
}

Matrix Matrix::operator+(const Matrix &Object) const
{
    return Matrix.clone(*this) +=Object;
}

We deviate slightly from the norm (return Matrix(*this) +=Object;) here because operator+ should not modify the source parameters. Matrix's copy constructor results in two Matrixs with the same reference counted array, and this will allow cause the += to change the original along with the copy. This would be Bad. No one expects x = 10 + 2 to turn 10 into 12.

clone looks something like

Matrix Matrix::clone(const Matrix &Object)
{
    Matrix result(Object.dane.wiersz,
                  Object.dane.kolumna);
    for (int i = 0; i < result.dane.wiersz; i++)
    {
        for (int j = 0; j < result.dane.kolumna; j++)
        {
            result.dane.wsk[i][j] = Object.dane.wsk[i][j];
        }
    }
    return result;
}

Link to complete code and test case.

I did not check any of the program logic that didn't pertain to proper management of the reference counted array. All of the matrix manipulation logic is unchecked and may be bogus.

Don't use this for production code. There is a perfectly good std::shared_ptr in the standard library.

user4581301
  • 33,082
  • 7
  • 33
  • 54