Khi phát triển những hệ thống phức tạp, việc cho phép truy cập thành phần bên trong của đối tượng sẽ xảy ra nhiều vấn đề gây ảnh hưởng hệ thống. Một lập trình viên có thể nhầm lẫn thay đổi trạng thái của đối tượng trong hệ thống. Ngồi ra có những lập trình viên cố ý tinh chỉnh thơng tin của đối tượng để phá hoại hệ thống. Trong cả hai trường hợp nếu hệ thống đó điều khiển thiết bị y tế hoặc hệ thống tên lửa quân sự thì kết quả có thể là chết người.
C++ cung cấp vài cách để bảo vệ dữ liệu bên trong của một đối tượng, tránh truy cập từ bên ngoài. Cách đơn giản nhất là sử sụng phạm vi truy xuất private và public để kiểm soát phạm vi của tất cả thành phần trong một lớp.
Trình biên dịch cấm truy cập tới những thành phần riêng tư (private). Ví dụ sau, tại nơi sử dụng không thể thay đổi thuộc tính denominator của đối tượng SimpleRational bằng 0 được. Mà khi đó người sử dụng (client) chỉ có thể truy cập thơng qua những phương thức được cung cấp bởi người thiết kế lớp.
Sau đây là một số nguyên tắc chung, nếu tuân thủ chương trình sẽ dễ dàng xây dựng mà mở rộng hơn:
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 51
• Các thuộc tính nên để là riêng tư (private). Người sử dụng không thể thay đổi trạng thái của đối tượng. Vì nếu cho phép thay đổi rất dễ đặt đối tượng vào trạng thái khơng xác định (ví dụ thay đổi mẫu số của đối tượng phân số bằng 0). Trạng thái của một đối tượng chỉ nên thay đổi thông qua một phương thức của đối tượng mà lập trình viên đã thiết kế sẵn.
• Các phương thức cung cấp dịch vụ cho người dùng, vì thế trong lớp nên để phạm vi truy cập là public. Lập trình viên phải đảm bảo rằng các phương thức này không đưa đối tượng vào trạng thái không hợp lệ. • Có những phương thức khơng được sử dụng bên ngồi, mục đích là phục
vụ cho những phương thức khác trong cùng lớp thì nên để phạm vi là private. Người ta thường gọi những phương thức này là phương thức hỗ trợ (helper methods).
Lập trình viên cố tình lựa chọn giới hạn phạm vi truy xuất tới thành phần của lớp là vì một vài ưu điểm sau:
• Linh hoạt trong cài đặt: Khái niệm một lớp gồm hai phần:
✓ Giao diện (class interface – visible part): Đây là phần người sử dụng có thể thấy và sử dụng. Giao diện của một lớp chỉ ra những gì mà đối tượng có thể làm.
✓ Phần thực thi (class implementation – hidden part): Người sử dụng không thể thấy bất kỳ phương thức hay thuộc tính có phạm vi private của đối tượng. Vì thế thơng tin riêng tư sẽ bị ẩn với người dùng. Phần thực thi của một lớp chỉ ra cách hồn thành những chức năng của đối tượng.
Có thể xem đối tượng như là một cái hộp đen: người dùng không cần biết cách đối tượng làm việc mà chỉ quan tâm đối tượng có thể làm gì.
Một nguyên tắc nhỏ trong thiết kế lớp là: Dữ liệu nên để private và phương thức mà cung cấp dịch vụ cho khách hàng nên để là public.
• Giảm lỗi.
Tại nơi sử dụng không thể lạm dụng các thành phần private của lớp vì nó đã bị ẩn với người dùng, từ đó giảm thiểu lỗi phát sinh.
• Che dấu sự phức tạp: chi tiết cài đặt của các chức năng đối tượng đã được ẩn. Tại nơi sử dụng chỉ cần quan tâm chức năng đó có thể làm gì.
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 52
4.4| CÀI ĐẶT TÍNH ĐĨNG GĨI
Việc chia dấu thơng tin được gọi là tính đóng gói (encapsulation). Chi tiết cài đặt chỉ được thể hiện tùy vào đối tượng thích hợp (như ngưới thiết kế lớp, người phát triển ứng dụng sử dụng một lớp).
Tóm lại sử dụng đóng gói sẽ làm cho:
• Phần mềm linh hoạt hơn và dễ thay đổi • Phần mềm mạnh mẽ và đáng tin cậy hơn
• Lập trình viên có thể dễ dàng hiểu và quản lý trong quá trình phát triển phần mềm.
4.5| PHƯƠNG THỨC KHỞI TẠO (CONSTRUCTORS)
Làm thế nào để đảm bảo các thuộc tính của một đối tượng có giá trị ban đầu hợp lý trước khi người dùng bắt đầu sử dụng đối tượng. Một lớp có thể định nghĩa một phương thức khởi tạo (contructor) để đảm bảo đối tượng được khởi tạo giá trị ban đầu. Định nghĩa phương thức khởi tạo tương tự như định nghĩa một phương thức thông thường. Với một vài lớp, người dùng có thể cung cấp thơng tin cho phương thức khởi tạo để sử dụng khi khởi tạo đối tượng. Trong một lớp có thể có nhiều phương thức khởi tạo (overload). Sau đây là ví dụ về lớp Account được cung cấp phương thức deposit và withdraw, cũng như phương thức khởi tạo. //bankaccountmethods.cpp #include <iostream> #include <iomanip> #include <string> class Account {
// String representing the name of the account's owner
std::string name; // The account number int id;
// The current account balance double balance;
public:
// Initializes a bank account object
Account(const std::string& customer_name, int account_number, double amount):
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 53
balance(amount) {
if (amount < 0) {
std::cout << "Warning: negative account balance\n";
balance = 0.0; }
}
// Adds amount amt to the account's balance. void deposit(double amt)
{
balance += amt; }
// Deducts amount amt from the account's balance,
// if possible.
// Returns true if successful; otherwise, it returns false.
// A call can fail if the withdraw would // cause the balance to fall below zero bool withdraw(double amt)
{
bool result = false; // Unsuccessful by default
if (balance - amt >= 0) {
balance -= amt;
result = true; // Success }
return result; }
// Displays information about the account object void display()
{
std::cout << "Name: " << name << ", ID: " << id << ", Balance: " << balance << '\n'; } }; int main() { Account acct1("Joe", 2312, 1000.00); Account acct2("Moe", 2313, 500.29); acct1.display(); acct2.display(); std::cout << "---------------------" << '\n'; acct1.withdraw(800.00);
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 54 acct2.deposit(22.00); acct1.display(); acct2.display(); } Kết quả:
Name: Joe, ID: 2312, Balance: 1000 Name: Moe, ID: 2313, Balance: 500.29 ---------------------
Name: Joe, ID: 2312, Balance: 200 Name: Moe, ID: 2313, Balance: 522.29
Những đặc điểm của phương thức khởi tạo khác với các phương thức khác: • Phương thức khởi tạo có tên trùng tên lớp
• Phương thức khơng có kiểu trả về thậm chí cả void.
• Khơng cần có câu lệnh gọi mà được tự động thực thi khi đối tượng vừa được tạo ra.
Phương thức khởi tạo sẽ khởi tạo tất cả các thuộc tính bằng các giá trị do người dùng cấp. Lưu ý rằng phải cung cấp giá trị khi khai báo tạo đối tượng:
Vì lớp Account chỉ chứa một phương thức khởi tạo có tham số, nên nếu người dùng khai báo tạo đối tượng như
Account acct3; // báo lỗi, phải cung cấp đủ đối số cho contructor.
Không giống những phương thức thông thường khác, chúng ta có thể sử dụng cặp ngoặc { để chứa các tham số như sau:
Một phương thức khởi tạo có thể khơng u cầu tham số truyền vào từ người dùng được gọi là phương thức khởi tạo mặc định (default constructor). Nếu lập
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 55
trình viên khơng xây dựng bất kỳ phương thức khởi tạo nào cho lớp thì trình biên dịch sẽ cung cấp một phương thức khởi tạo mặc định khơng làm gì cả. Cịn nếu lập trình viên có định nghĩa ít nhất một phương thức khởi tạo thì trình biên dịch sẽ không tạo ra phương thức khởi tạo mặc định này.
Ngoài cách khởi tạo bằng phương thức khởi tạo mặc định và phương thức khởi tạo có tham số như trên chúng ta cịn có thể khởi tạo một đối tượng thông qua một đối tượng đã có.
Trong ngơn ngữ C++, phương thức khởi tạo sao chép được trình biên dịch cung cấp mặc định cho các lớp đối tượng do người dùng định nghĩa, được gọi là
phương thức khởi tạo sao chép mặc đinh (default copy contructor). Phương thức
này để sao chép tồn từng thành phần thuộc tính của đối tượng nguồn sang đối tượng đích.
Phương thức khởi tạo sao chép của một lớp còn được gọi khi chúng ta truyền hoặc nhận giá trị trả về đối tượng của lớp đó theo kiểu tham trị, hoặc khi chúng ta thực hiện câu lệnh gán để gán thông tin của đối tượng này cho đối tượng khác. Tuy nhiên trong quá trình sao chép là sao chép từng thành phần theo dạng từng bit, nếu các thành phần dữ liệu là kiểu con trỏ thì quá trình sao chép sẽ chỉ sao chép giá trị của con trỏ chứ không thật sự sao chép vùng nhớ mà chúng quản lý. Trong trường hợp này nếu chúng ta chỉ sử dụng phương thức khởi tạo sao chép mặc định mà trình biên dịch cấp thì sẽ xảy ra những hiệu ứng lề khơng mong muốn.
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 56
Từ ví dụ trên ta thấy hai đối tượng c, d có thuộc tính tu cùng trỏ vào một ơ nhớ, điều này dẫn đến những hiệu ứng lề không mong muốn.
Như vậy trong trường hợp này, tức trong trường hợp thành phần dữ liệu của lớp đối tượng có các biến con trỏ và chúng đang quản lý các vùng nhớ được cấp phát động, nếu có nhu cầu sao chép các đối tượng dạng này, người lập trình phải viết lại phương thức khởi tạo sao chép để việc sao chép được tiến hành chính xác và khơng bị hiệu ứng lề như ví dụ trên nữa.
Cú pháp của phương thức khởi tạo sao chép:
<Tênlớp>(const <Tênlớp> &<ĐốiTượngNguồn>) {
//cấp phát vùng nhớ cho thuộc tính đích sau đó sao chép giá trị. }
Ở ví dụ trên, phương thức khởi tạo sao chép phải được viết lại như sau:
Tóm lại, một cách đơn giản, bình thường nếu kiểu dữ liệu của các thành phần dữ liệu (thuộc tính) bên trong một lớp đối tượng khơng phải là kiểu dữ liệu con trỏ thì chúng ta khơng cần phải cài đặt phương thức tạo lập sao chép cho lớp, bởi khi đó sử dụng phương thức khởi tạo sao chép mặc định của trình biên dịch là đủ.
4.6| PHƯƠNG THỨC HỦY ĐỐI TƯỢNG (DESTRUCTORS)
Tương tự như vấn đề khởi tạo cấu trúc bên trong đối tượng, vấn đề dọn dẹp thông tin bên trong đối tượng trước khi hủy cũng là một vấn đề rất quan trọng mà người
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 57
lập trình cần phải nắm rõ. Một khi đối tượng khơng cịn được sử dụng nữa, các tài nguyên đã được cấp phát cho đối tượng có thể trở thành rác, bởi vì những tài nguyên được cấp phát động chỉ bị hủy khi có câu lệnh hủy chứ khơng được tự động hủy bởi trình biên dịch khi hết phạm vi hoạt động. Trong lập trình hướng đối tượng, phương thức hủy (destructor) sẽ đảm nhận nhiệm vụ này. Phương thức hủy được gọi mặc định ngay khi đối tượng kết thúc phạm vi sử dụng để hủy vùng nhớ dành cho đối tượng, thu hồi toàn bộ tài nguyên đã cấp phát cho đối tượng trong quá trình sống của đối tượng.
Việc dọn dẹp thơng tin bên trong đối tượng trước khi hủy giúp tránh xảy ra những tình huống khơng mong muốn, chẳng hạn như trường hợp rị rỉ bộ nhớ:
• Chương trình chạy một lát thì khơng chạy nữa do hết bộ nhớ
• Khi chương trình đang chạy, các phần mềm khác không chạy được nữa do đã bị chương trình của chúng ta giành hết bộ nhớ, chỉ khi đóng chương trình thì các phần mềm khác mới chạy được.
• Sau khi chương trình của chúng ta chạy xong thì máy bị treo, phải khởi động lại máy.
Phương thức hủy trong C++ có những đặc điểm quan trọng sau: • Có tên trùng tên lớp, có dấu ~ đặt ngay phía trước
• Khơng có giá trị trả về • Khơng có tham số đầu vào
• Mỗi lớp chỉ có một phương thức hủy
• Phương thức hủy sẽ được tự động gọi khi đối tượng của lớp hết phạm vi sử dụng
Do vậy, trong trường hợp lớp đối tượng không sử dụng các vùng nhớ cấp phát động thì người sử dụng khơng cần phải xây dựng phương thức hủy, khi đo chỉ cần sử dụng phương thức hủy mặc định của trình biên dịch là đủ.
Cú pháp cài đặt phương thức hủy:
~Tênlớp() {
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 58
}
4.7| PHƯƠNG THỨC SET, GET (SET, GET METHODS)
Trong một lớp, do dữ liệu đã được đóng gói và ẩn thơng tin nên bên ngồi khơng thể nào truy cập đến những thành phần private. Vì vậy để có thể truy cập tới thành phần private thì người lập trình phải thiêt kế thêm chức năng cho phép người dung nhận lại giá trị của thuộc tính private (getter) và cho phép người dùng thay đổi giá trị thuộc tính private (setter).
Tương tự phương thức getter, chúng ta sẽ cần xây dựng những phương thức setter khi cần cung cấp chức năng thay đổi dữ liệu (thành phần private) của đối tượng cho người dùng:
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 59
// Allows a client to reassign the numerator void set_numerator(int n)
{
numerator = n; }
// Allows a client to reassign the denominator. // Disallows an illegal fraction (zero
denominator). void set_denominator(int d) { if (d != 0) denominator = d; else {
// Display error message
std::cout << "Zero denominator error\n"; exit(1); // Exit the program
} }
4.8| CON TRỎ THIS
Xét ví dụ:
Câu lệnh ctr1.clear() sẽ thực hiện truyền ngầm định địa chỉ của đối tượng ctr1 tới lời gọi của phương thức clear. Đây là cách để xác định trường count là của đối tượng nào. Nếu ctr1.clear() thì count là của đối tượng ctr1 cịn nếu ctr2.clear() thì count là của ctr2.
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 60
Trong một định nghĩa phương thức, lập trình viên có thể truy cập tới đối số ngầm định thơng qua từ khóa this. Trong thân phương thức thì this là một con trỏ dùng trỏ tới đối tượng đang thực hiện lời gọi đến phương thức đó. Lớp Counter ở trên được viết lại như sau:
Một vài lập trình viên ln sử dụng con trỏ this để dễ đọc hơn và để chỉ đây là thuộc tính của lớp chứ khơng phải là một biến tồn cục hay cục bộ, giúp tránh nhầm lẫn trong trường hợp được nêu ở ví dụ sau:
Một trường hợp sử dụng khác của con trỏ this là dùng để truyền đối tượng hiện hành tới một hàm hay phương thức khác. Giả sử có một hàm tồn cục tên log, hàm này yêu cầu truyền tham số là một đối tượng lớp Counter.
Nếu trong phương thức clear muốn gọi tới hàm log và truyền chính đối tượng hiện tại thì sẽ được viết như sau:
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 61
4.9| PHƯƠNG THỨC HẰNG (CONST METHOD)
Xét lại ví dụ sau:
//simplerational.cpp #include <iostream> #include <cstdlib>
// Models a mathematical rational number class SimpleRational
{
int numerator; int denominator; public:
// Initializes the components of a Rational object SimpleRational(int n, int d): numerator(n),
denominator(d) {
if (d == 0) {
// Display error message
std::cout << "Zero denominator error\n"; exit(1); // Exit the program
} }
// The default constructor makes a zero rational number
// 0/1
SimpleRational(): numerator(0), denominator(1) {} // Allows a client to reassign the numerator
void set_numerator(int n) {
numerator = n; }
// Allows a client to reassign the denominator. // Disallows an illegal fraction (zero
denominator).
void set_denominator(int d) {
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 62
if (d != 0)
denominator = d; else
{
// Display error message
std::cout << "Zero denominator error\n"; exit(1); // Exit the program
} }
// Allows a client to see the numerator's value. int get_numerator()
{
return numerator; }
// Allows a client to see the denominator's value. int get_denominator()
{
return denominator; }
};
// Returns the product of two rational numbers SimpleRational multiply(SimpleRational f1, SimpleRational f2)