Lập trình

Lập trình

Thứ Tư, 9 tháng 12, 2015

Shallow copy và deep copy

Shadow copy và Deep copy

 Giới thiệu mở đầu

Một function thông qua con trỏ lấy giá trị của một ô nhớ của một chương trình khác . Cả hai function này  đều truy cập được giá trị của ô nhớ nhưng bản thân giá trị của ô nhớ không sao chép ra. Phương thức này được gọi là "shallow" bởi vì thay vì một bản sao giá trị của ô nhớ được tạo ra và gửi cho function nhưng đây ô nhớ được chia sẻ , dùng chung.
VD : int [] p = {1, 2, 3, 4}; // p chưa địa chỉ của {1, 2, 3, 4} 0xff00001
     int *q = NULL; // q có giá trị là 0x00000000
     q = p; // gọi operator = , copy thông thường, gán giá trị của p sang q
Ở trên là một ví dụ về shallow copy, nó có ích trong vài trường hợp ví dụ bạn muốn swap mảng A chưa 10000 phần tử với  mảng B cũng có 1000 phần tử ta chỉ cần swap giá trị địa chỉ 2 cái cho nhau là xong chứ không cần phải swap từng phần tử

int* temp = A;
A = B;
B = temp;

Nhưng có vấn đề khác là ở ví dụ đầu với p q , vùng nhớ của p q là một không tách biệt nhau điều đó dẫn đến khi ta thay đổi p hoặc q thì ô nhớ đều thay đổi.

VD p[1] = 2;
thì mặc dù không muốn nhưng q[1] == 2.

Còn Deep copy thì ta thực hiện copy từng phần tử của p sang q (tất nhiên là q đã có đầy đủ bộ nhớ)


+ Shallow copying (trong class)

 Vì C++ không biết nhiều về class của bạn, nên nó sẽ cung cấp hàm copy mặc định và toán tử gán mặc định như một phương thức sao chép được biết như là  shallow copy. Shallow copy có nghĩa là có nghĩa là C++ copy từng phần tử của class riêng lẻ sử dụng toán tử gán(assignment operator ) . Khi  class đơn giản , không chứa cấp phát động thì điều này làm việc rất tốt.

 Ví dụ class Tuoi{
 private:
  int m_tuoi;
 public:
  Tuoi(int tuoi = 0){ (copy constructor )
  m_tuoi = tuoi;
  }
 }

 Khi C++ làm shallow copy với class này, nó sẽ copy m_tuoi sẽ sử dụng toán sử gán int tiêu chuẩn, đó là tất cả những gì ta muốn làm khi tự viết ra toán tủ gán hay hàm tạo sao chép của riêng mình, vì vậy không có lý do gì để ta viết toán tử hay function mới trong trường hợp này.


 Tuy nhiên, khi thiết kế class nếu nó sử dụng cấp phát bộ nhớ động (dynamically allocated memory) , khi copy từng thuộc tính với shallow sẽ gây ra một vấn đề! Bởi vì phép gán trong con trỏ đơn giản là nó chỉ gán địa chỉ của con trỏ này sang con trỏ kia- Nó không cấp pháp bộ nhớ hay copy nội dung đang được trỏ đến.


 Một ví dụ về class MyString rất hay trên (learncpp.com)


 class MyString
{
private:
    char *m_pchString;
    int m_nLength;

public:
    MyString(char *pchString="")
    {
        // Find the length of the string
        // Plus one character for a terminator
        m_nLength = strlen(pchString) + 1;
       
        // Allocate a buffer equal to this length
        m_pchString= new char[m_nLength];
       
        // Copy the parameter into our internal buffer
        strncpy(m_pchString, pchString, m_nLength);
   
        // Make sure the string is terminated
        m_pchString[m_nLength-1] = '\0';
    }

    ~MyString() // destructor
    {
        // We need to deallocate our buffer
        delete[] m_pchString;

        // Set m_pchString to null just in case
        m_pchString = 0;
    }

    char* GetString() { return m_pchString; }
    int GetLength() { return m_nLength; }
};

Xét ví dụ dưới đây :

MyString cHello("Hello, world!");

{
    MyString cCopy = cHello; // use default copy constructor
} // cCopy goes out of scope here

std::cout << cHello.GetString() << std::endl; // this will crash

Dòng MyString cHello("Hello, world!"); gọi MyString constructor , nó sẽ cấp phát động bộ nhớ ở heap rồi cho cHello.m_pchString trỏ đến đó và sao đó copy "Hello, world" vào trong vùng nhớ đó

 MyString cCopy = cHello; // use default copy constructor

 dòng này sử dụng hàm tạo copy mặc định bởi vì ta không tự mình cung cấp một hàm copy . Vì vậy nó sẽ copy theo kiểu shallow, tức là giá trị của cHello.m_pchString được copy từ cHello.m_pchString và như kết quả hai con trỏ này trỏ cùng vào một vùng nhớ.

 với dòng : } // cCopy goes out of scope here

 Khi cCopy ra khỏi phạm vi {} thì class  MyString sẽ gọi hàm hủy cho cCopy. Hàm hủy delete hết cấp phát động của cả cCopy.m_pchString và cHello.m_pchString đang trỏ đến, chúng ta cũng vô tình làm ảnh hưởng đến cHello. Chú ý là hàm hủy sẽ đưa con trỏ cCopy.m_pchString trỏ tới 0, nhưng cHello.m_pchString sẽ trỏ vào vùng nhớ đã bị xóa (không hợp lệ).


std::cout << cHello.GetString() << std::endl; // this will crash

Bây giờ bạn có thể thấy tại sao bị crashes. Chùng ta đã delete string cHello đang trỏ tới và bây giờ chúng ta đang cố gắng in giá trị của vùng nhớ không còn được cấp phát.


Deep Copying


Câu trả lời cho vấn đề trên là làm deep copy trên con trỏ không trỏ tới NULL đang được copy (từ nó). Deep copy tạo một bản sao của đối tượng hay biến đang được trỏ tới cho đích đến (chính là đối tượng được gán) tự nó copy . Với cách này đích đến có thể làm bấu cứ cái gì nó muốn và đối tượng được copy từ nó sẽ không bị ảnh hưởng. Làm việc với deep copy yêu caaud chùng ta phải tự mình viết ra copy constructors và nạp chồng toán tử gán.

Chúng ta cùng đến và xem điều đó được làm như thế nào trong class MyString của chúng ta:


// Copy constructor
MyString::MyString(const MyString& cSource)
{
    // because m_nLength is not a pointer, we can shallow copy it
    m_nLength = cSource.m_nLength;

    // m_pchString is a pointer, so we need to deep copy it if it is non-null
    if (cSource.m_pchString)
    {
        // allocate memory for our copy
        m_pchString = new char[m_nLength];

        // Copy the string into our newly allocated memory
        strncpy(m_pchString, cSource.m_pchString, m_nLength);
    }
    else
        m_pchString = 0;
}

Bây giờ chúng ta cùng đến với nạp chồng toán tử toán tử gán:

// Assignment operator
MyString& MyString::operator=(const MyString& cSource)
{
    // check for self-assignment
    if (this == &cSource)
        return *this;

    // first we need to deallocate any value that this string is holding!
    delete[] m_pchString;

    // because m_nLength is not a pointer, we can shallow copy it
    m_nLength = cSource.m_nLength;

    // now we need to deep copy m_pchString
    if (cSource.m_pchString)
    {
        // allocate memory for our copy
        m_pchString = new char[m_nLength];

        // Copy the parameter the newly allocated memory
        strncpy(m_pchString, cSource.m_pchString, m_nLength);
    }
    else
        m_pchString = 0;

    return *this;
}

Chú ý là toán tử gán của chúng ta rất giống với hàm tạo copy, nhưng có ba khác biệt lớn :
    + Chúng ta kiểm ta xem có nó tự gán với chính mình hay không
    + Chúng ta return *this bởi vì chúng ta có thể xâu chuỗi toán tử gán
    + Chúng ta cần deallocate một cách rõ ràng giá trị cái mà string đã tổ chức

Chúng ta phải delete vùng nhớ được cấp phát trước khi copy vì nếu không làm (mặc dù code không bị crash) nhưng chúng ta sẽ có một memory leak, nó làm lãng phí bộ nớ mỗi lần chúng ta thực hiện phép gán.

Tại sao cần phải kiểm tra xem nó có tự gán với mình trước không?

Thứ nhất là nếu chúng ta không cần copy thì tại sao phải làm một cái.
Lý do thứ 2 là nó sẽ dẫn đến lỗi khi trong class sử dụng cấp phát động



// Problematic assignment operator
MyString& MyString::operator=(const MyString& cSource)
{
    // Note: No check for self-assignment!

    // first we need to deallocate any value that this string is holding!
    delete[] m_pchString;

    // because m_nLength is not a pointer, we can shallow copy it
    m_nLength = cSource.m_nLength;

    // now we need to deep copy m_pchString
    if (cSource.m_pchString)
    {
        // allocate memory for our copy
        m_pchString = new char[m_nLength];

        // Copy the parameter the newly allocated memory
        strncpy(m_pchString, cSource.m_pchString, m_nLength);
    }
    else
        m_pchString = 0;

    return *this;
}


Điều gì xảy ra với cHello = cHello ;

khi đó con trỏ this trỏ cHello và cSource cũng tham chiếu đến cHello vì vậy điều này dẫn đến m_pchString và cSource.m_pchString là một.

Sau đó khi thực hiện lệnh delete[] m_pchString; tức là cSource.m_pchString cũng bị delete theo dẫn đến lệnh copy tiếp theo sẽ thực hiện mà đối tượng được copy đến đã bị delete


Ngăn chặn việc copy:

Đôi khi chúng ta chỉ đơn giản không muốn class của chúng ta bị copy. Cách tốt nhất là thêm khai báo cho toán tử gán và copy constructor vào trong mục private của class.

class MyString
{
private:
    char *m_pchString;
    int m_nLength;

    MyString(const MyString& cSource);
    MyString& operator=(const MyString& cSource);
public:
    // Rest of code here
};


Trong trường hợp nayfC++ không tự động tạo một hàm tạo mặc định hay toán tử gán mặc định bởi vì chúng ta đã nói với compiler là chúng ta đã tự định nghĩa nó. Nhưng không thể truy cập vào nó vì nó private.

Ví dụ với code :

#include <iostream>
#include <conio.h>
#include <string.h>

using namespace std;

class MyString
{
private:
    char *m_pchString;
    int m_nLength;

    MyString(const MyString& cSource);
    MyString& operator=(const MyString& cSource);
public:
    // Rest of code here
};

MyString::MyString(const MyString& cSource){
    m_nLength = strlen(cSource.m_pchString);
    strncpy(m_pchString, cSource.m_pchString, m_nLength);
}

int main(){
    MyString s; // error
    MyString t = s;
    return 0;
}

Khi chạy ta nhận được lỗi : [Error] no matching function for call to 'MyString::MyString()


Tóm lại:


- Copy constructor mặc định và toán tử gán mặc định làm shallow copy , điều này tốt khi class gồm nhưng biến không cấp phát động.
- Nhưng class sử dụng cấp vát động cần một hàm tạo sao chép và toán tử gán riêng để deep copy
- Toán tử gán thường được thực hiện giống như hàm tạo sao chép nhưng đầu tiên phải kiểm tra xem nó có tự gán cho nó không và return *this, deallocates những bộ nhớ đã được cấp phát trước đó để deep copying
-Nếu bạn không muốn class sao chép sử dụng hàm tạo copy và toán tử gán prototype trong file header class.

Không có nhận xét nào:

Đăng nhận xét