Copy Constructor được dùng rất nhiều trong lập trình và đối với lập trình hướng đối tượng khi dữ liệu của đối tượng là một con trỏ thì nó lại rất cực kì quan trọng mà bạn phải chú tâm đến.
Ở bài viết trước [Học OOP] Bài 3: Lớp trong lập trình hướng đối tượng mình đã giới thiệu sơ về Constructor và Destructor. Trong bài này mình tiếp tục giới thiệu về Copy Constructor và đây là một chủ đề khá quan trọng nó sẽ giúp bạn hiểu hơn về các cơ chế hoạt động của ngôn ngữ lập trình.
1. Copy Constructor là gì?
a. Giới thiệu Copy Constructor
Copy Constructor là một Constructor đặc biệt, nó được dùng để tạo một bản sao của một đối tượng đã có trước đó. Copy Constructor có tham số là địa chỉ vùng nhớ tham chiếu đến đối tượng cần sao chép.
Trong hình minh họa trên, Ban đầu chúng ta có đối tượng A có địa chỉ vùng nhớ là 0x0100, nó là một hình tròn màu vàng viền đen. Do nhu cầu nên chúng ta muốn tạo ra một bản sao giống hệt Đối tượng A và đặt tên là Đối tượng B có địa chỉ vùng nhớ 0x0300. Vai trò copy constructor trong ví dụ trên chỉ có vậy.
b. Vì sao phải dùng Copy Constructor?
Sau khi đọc xong phần giới thiệu có lẽ bạn đặt khá nhiều câu hỏi, vai trò chỉ là sao chép thì tại sao lại nói là nó quan trọng, trong khi phép gán đã giải quyết được vấn đề sao chép?
Như bạn cũng biết, Pointer (Con trỏ) trong C++ lưu địa chỉ vùng nhớ, chính về thế, nếu bạn muốn sao chép nội dung 2 pointer trỏ đến bằng phép gán b = a thì lúc này nó chỉ gán địa chỉ vùng nhớ của a cho b mà thôi, và nội dung của 2 là một, nên khi bạn tác động lên dữ liệu mà a hoặc b trỏ đến thì cả nội dung của a và b đều sẽ thay đổi. Nên phương án gán này không hợp lý.
Trong C++ khi bạn truyền vào hàm, thủ tục một đối tượng, thì nó sẽ gọi copy constructor của kiểu dữ liệu đó để thực hiện 1 số sao chép. Nếu kiểu dữ liệu đó không có copy constructor thì nó sẽ tự gán giá trị của biến đó cho một biến khác. Ví dụ
int m = 1, n = 2; int max(int a, int b) { if (a>b) return a; return b; }
Trong ví dụ trên khi bạn gọi max(m, n) ở đây bạn truyền vào tham trị nó sẽ gọi copy constructor của class số int và bạn hiểu là phương thức đó thực hiện sao chép dữ liệu để nếu bạn có thay đổi giá trị a b ở trong hàm thì 2 biến m, n ở ngoài truyền vào sẽ không ảnh hưởng.
Sau khi xử lí xong, và thoát khỏi hàm, nó sẽ gọi hàm destructor để hủy các biến mà nó tạo ra, trong trường hợp này bao gồm những biến được tạo bằng constructor và Copy constructor. Phần về Destructor mình sẽ nói bên dưới.
Như vậy nếu đối tượng của mình có dữ liệu là một pointer cấp phát động, chẳng hạn mình sẽ tạo ra một class chứa mảng số nguyên như sau:
class MangInt { private: int* A; int n; public: void xuat() { for (int i=0; i<n; i++) cout << a[i] << " "; } MangInt() { n = 10; A = new int [n]; // cấp phát động mảng A 10 phần tử for (int i=0; i<=9; i++) A[i]=i; // gán giá trị 0-9 cho các phần tử } ~MangInt() { delete [] A; } }; void vidu(MangInt array) { array.xuat(); } int main() { MangInt Array1; vidu(Array1); }
Ở ví dụ trên khi khởi tạo đối tượng mình sẽ cho nó cấp phát động mảng A, và theo nguyên tắc thông thường, có cấp phát động phải có thu hồi bộ nhớ, nên mình sẽ thu hồi bộ nhớ ở destructor, và bây giờ mình khai báo MangInt Array1; sau đó truyền Array1 vào hàm vidu
Như mô tả bên trên nó sẽ gọi copy constructor nhưng mình chưa khai báo cho nó nên mặc định nó sẽ làm là
– Gán Array1.n cho array.n
– Gán Array1.A cho array.A (lưu ý đây là pointer nên nó chép địa chỉ)
Sau khi xử lí xong hàm, lúc thoát khỏi hàm nó sẽ gọi Destructor cho những gì nó tạo khi chạy hàm đó. Như vậy bao gồm nó sẽ gọi destructor của array (trong đây nó thu hồi bộ nhớ của array)
Như vậy sau khi chạy xong hàm vidu trong main() thì nó sẽ kết thúc hàm main và vẫn theo nguyên tắc cũ là gọi Destructor cho những gì nó đã tạo trong hàm đó. Lúc này Array1 được gọi hủy. Theo như trong code hàm hủy, mình delete pointer A, mà địa chỉ pointer A đó đã bị hủy trước đó trong hàm vidu, nên lúc này xảy ra hiện tượng lỗi thu hồi 2 lần trên cùng một vùng nhớ.
Chính vì thế mà vai trò Copy Constructor là khi nó được chạy thì nó sẽ tạo ra vùng nhớ mới bằng cách cấp phát động và gán giá trị qua chứ không phải gán địa chỉ của 2 pointer theo cách mặc định của c++
2. Cài đặt Copy Constructor như thế nào?
“Bài viết này mình nói khá dài dòng, nhưng bạn chỉ cần nhớ một câu duy nhất Khi nào dữ liệu của class có cấp phát động, hãy cài đặt Copy Constructor”
a. Khai báo copy constructor
Tên_Class(const Tên_Class & tenbien) { // cấp phát động cho dữ liệu có con trỏ // gán các dữ liệu của tên biến cho đối tượng hiện tại }
b. Ví dụ về khai báo copy constructor
Mình sẽ lấy ví dụ về MangInt bên trên để viết copy constructor cho nó
MangInt(const MangInt& mang) { n = mang.n; // gán n của mang qua n của đối tượng cần sao chép A = new int [n]; // cấp phát động lại vùng nhớ mới for (int i=0; i<n; i++) A[i] = mang.A[i]; // sao chép hết giá trị từng phần tử qua vung nhớ mới }
3. Bài tâp ứng dụng Copy Constructor
a. Đề bài
Viết định nghĩa lớp String để biểu diễn khái niệm chuỗi ký tự với các phương thức thiết lập và huỷ bỏ, các hàm thành phần tính chiều dài chuỗi, so sánh hai chuỗi, nối hai chuỗi, đảo chuỗi, xuất chuỗi.
Lưu ý: không được sử dụng kiểu string của C++ (của thư viện chuẩn <string>)
Ở bài này theo yêu cầu bạn nên tạo một class có 2 dữ liệu là poiner thể hiện mảng kí tự char và một biến số nguyên lưu độ dài xâu.
b. Code lời giải
Bài này mình đã làm quá lâu rồi, nên một số chỗ chưa hợp lý, tuy nhiên các bạn có thể tham khảo để hiểu về cách ứng dụng Copy Constructor và viết lại theo cách riêng của mình.
String.h
#pragma once class String { private: char *pString; int DoDai; public: String(); String(const char* str); String(const String & a); // copy constructor ~String(); int GetLength(); void xuat(); int SoSanh(const String & a); void NoiChuoi(const String & a); void nhap(); void DaoChuoi(); };
String.cpp
#include <iostream> #include "String.h" #include <cstring> using namespace std; String::String() { DoDai = 1; pString = NULL; } String::String(const char * str) { DoDai = strlen(str)+1; pString = new char[DoDai]; strcpy_s(pString, DoDai, str); pString[DoDai-1] = '\0'; } String::String(const String & a) { DoDai = a.DoDai; pString = new char[DoDai]; strcpy_s(pString, DoDai, a.pString); } String::~String() { delete [] pString; } int String::GetLength() { return DoDai - 1; } void String::xuat() { if (pString != NULL) cout << pString; } int String::SoSanh(const String & a) { if (a.pString == NULL && pString == NULL) return 0; if (a.pString == NULL) return 1; if (pString == NULL) return -1; return strcmp(pString, a.pString); } void String::NoiChuoi(const String & a) { if (a.pString == NULL && pString == NULL) return; if (a.pString == NULL) return; char *p = new char[DoDai + a.DoDai - 1]; if (pString == NULL) { strcpy_s(p, a.DoDai, a.pString); delete [] pString; pString = p; return; } for (int i = 0; i <= DoDai-2; i++) p[i] = pString[i]; for (int i = 0; i < a.DoDai; i++) p[i + DoDai - 1] = a.pString[i]; delete [] pString; pString = p; DoDai = DoDai + a.DoDai - 1; } void String::nhap() { cin >> pString; DoDai = strlen(pString); } void String::DaoChuoi() { _strrev(pString); }
Main.cpp
#include <iostream> #include "String.h" using namespace std; int main() { String a("con vit"), b(" con ga"); a.NoiChuoi(b); cout << "length = "<< a.GetLength() << endl; a.xuat(); a.DaoChuoi(); cout << endl; a.xuat(); system("pause"); return 0; }
Sau khi giải xong bài này bạn sẽ đặt hỏi vì sao khi khai báo kiểu String a=”con vit”, b=” con ga”; bị sai kết quả hay vì sao không thể gọi ghép string bằng a+b chẳng hạn, thì nó cần đến overloading operator để tạo lại toán tử, cả phép gán = cũng có thể overloading lại, nên khi bạn không định nghĩa toán tử này mà sử dụng nó sẽ cho kết quả không mong muốn. Ở những bài sau mình sẽ nói về overloading operator =
Bạn có thể làm thêm các bài tập dưới đây:
Bài tập 3 – Bài này mình giải có sai sót, bạn vui lòng làm lại và ứng dụng copy constructor.