• Không có kết quả nào được tìm thấy

2.2 Lớp và các thành phần của lớp

N/A
N/A
Protected

Academic year: 2022

Chia sẻ "2.2 Lớp và các thành phần của lớp "

Copied!
1
0
0

Loading.... (view fulltext now)

Văn bản

(1)

Chương 2 Lớp

2.1 Mở đầu

Một kiểu dữ liệu là một biểu diễn cụ thể một khái niệm trong thực tế. Ví dụ kiểu int là một biểu diễn cụ thể của khái niệm số nguyên trong toán học.

Trong C++, các kiểu dữ liệu nội tại (built-in data types) :int, long, float, double, char...

cho phép kiểm tra lúc biên dịch và phát sinh mã chương trình tối ưu. Các kiểu dữ liệu này cung cấp một giao diện tự nhiên độc lập với phần cài đặt.

Lớp trong C++ là cài đặt của kiểu dữ liệu trừu tượng do người sử dụng định nghĩa, cho phép kết hợp dữ liệu, các phép toán, các hàm liên quan để tạo ra một đơn vị chương trình duy nhất. Các lớp này có đầy đủ ưu điểm và tiện lợi như các kiểu dữ liệu nội tại. Lớp tách rời phần giao diện (chỉ liên quan với người sử dụng) và phần cài đặt lớp.

2.2 Lớp và các thành phần của lớp

Ví dụ so sánh: Xây dựng kiểu dữ liệu stack.

1. Cách tiếp cận cổ điển:

// Stack1.cpp :

//Dung cau truc va ham toan cuc

#include <iostream.h>

typedef int bool;

typedef int Item;

const bool false = 0, true = 1;

struct Stack {

Item *st, *top;

int size;

};

void StackInit(Stack *ps, int sz) {

ps->st = ps->top = new Item[ps->size=sz];

}

void StackCleanUp(Stack *ps) {

delete [] ps->st;

}

bool StackFull(Stack *ps) {

return (ps->top - ps->st >= ps->size);

}

bool StackEmpty(Stack *ps) {

(2)

return (ps->top <= ps->st);

}

bool StackPush(Stack *ps, Item x) {

if (StackFull(ps)) return false;

*ps->top++ = x;

return true;

}

bool StackPop(Stack *ps, Item *px) {

if (StackEmpty(ps)) return false;

*px = *--ps->top;

return true;

}

void XuatHe16(long n) {

static char hTab[] =

{'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E' ,'F'};

Stack s;

StackInit(&s,8);

int x;

do {

StackPush(&s, n%16);

n /= 16;

} while(n);

while(StackPop(&s,&x)) cout << hTab[x];

StackCleanUp(&s);

} Nhận xét:

Giải quyết được vấn đề.

Khai báo cấu trúc dữ liệu nằm riêng, các hàm xử lý dữ liệu nằm riêng ở một nơi khác.

Do đó khó theo dõi quản lý khi hệ thống lớn. Vì vậy khó bảo trì.

Mọi thao tác đều có tham số đầu tiên là con trỏ đến đối tượng cần thao tác. Tư tưởng thể hiện ở đây là hàm hay thủ tục đóng vai trò trọng tâm. Đối tượng được gởi đến cho hàm xử lý.

Trình tự sử dụng qua các bước: Khởi động, sử dụng thực sự, dọn dẹp.

2. Cách tiếp cận dùng hàm thành phần:

//...

struct Stack {

Item *st, *top;

int size;

(3)

void Init(int sz) {st = top = new Item[size=sz];}

void CleanUp() {if (st) delete [] st;}

bool Full() const {return (top - st >= size);}

bool Empty() const {return (top <= st);}

bool Push(Item x);

bool Pop(Item *px);

};

bool Stack::Push(Item x) {

if (Full()) return false;

*top++ = x;

return true;

}

bool Stack::Pop(Item *px) { if (Empty()) return false;

*px = *--top;

return true;

}

void XuatHe16(long n) {

static char hTab[] =

{'0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F'};

Stack s;

s.Init(8);

int x;

do {

s.Push(n%16);

n /= 16;

} while(n);

while(s.Pop(&x)) cout << hTab[x];

s.CleanUp();

} Nhận xét:

Giải quyết được vấn đề.

Dữ liệu và các hàm xử lý dữ liệu được gom vào một chỗ bên trong cấu trúc. Do đó dễ theo dõi quản lý, dễ bảo trì nâng cấp.

Các thao tác đều bớt đi một tham số so với cách tiếp cận cổ điển. Vì vậy việc lập trình gọn hơn. Tư tưởng thể hiện ở đây là đối tượng đóng vai trò trọng tâm. Đối tượng thực hiện thao tác trên chính nó.

Trình tự sử dụng qua các bước: Khởi động, sử dụng thực sự, dọn dẹp.
(4)

Các hàm thành phần.

Là hàm được khai báo trong lớp.

Hàm thành phần có thể được định nghĩa bên trong hoặc bên ngoài lớp.

Hàm thành phần có nghi thức giao tiếp giống với các hàm bình thường khác: có tên, danh sách tham số, giá trị trả về.

Lớp

Trong cách tiếp cận dùng struct và hàm thành phần, người sử dụng có toàn quyên truy xuất, thay đổi các thành phần dữ liệu của đối tượng thuộc cấu trúc. Ví dụ:

Stack s;

s.Init(10);

s.size = 100; // Nguy hiem for (int i = 0; i < 20; i++)

s.Push(i);

Vì vậy, ta không có sự an toàn dữ liệu. Lớp là một phương tiện để khắc phục nhược điểm trên.

Lớp có được bằng cách thay từ khoá struct bằng từ khoá class.

Trong lớp mọi thành phần mặc nhiên đều là riêng tư (private) nghĩa là thế giới bên ngoài không được phép truy xuất. Do đó có sự an toàn dữ liệu.

class Stack {

Item *st, *top;

int size;

void Init(int sz) {st = top = new Item[size=sz];}

void CleanUp() {if (st) delete [] st;}

bool Full() const {return (top - st >= size);}

bool Empty() const {return (top <= st);}

bool Push(Item x);

bool Pop(Item *px);

};

Phát biểu:

s.size = 100; // Bao sai

Sẽ bị báo sai lúc biên dịch, nhờ đó tránh được những lỗi run-time rất khó tìm khi thực hiện chương trình.

Thuộc tính truy xuất

Tuy nhiên lớp như trên trở thành vô dụng vì các hàm thành phần cũng trở thành private và không ai dùng được. Điều đó được khắc phục nhờ thuộc tính truy xuất.

Thuộc tính truy xuất của một thành phần của lớp chỉ rõ phần chương trình được phép truy xuất đến nó.

o Thuộc tính private o Thuộc tính public

Các thành phần là nội bộ của lớp, bao gồm dữ liệu và các hàm phục vụ nội bộ được đặt trong phần private. Các hàm nhằm mục đích cho người sử dụng dùng được đặt trong phần public.
(5)

Ví dụ về lớp và thuộc tính truy xuất

// Stack.h

typedef int bool;

typedef int Item;

const bool false = 0, true = 1;

class Stack {

Item *st, *top;

int size;

void Init(int sz) {st = top = new Item[size=sz];}

void CleanUp() {delete [] st;}

public:

Stack(int sz = 20) {Init(sz);}

~Stack() {delete [] st;}

bool Full() const {return (top - st >= size);}

bool Empty() const {return (top <= st);}

bool Push(Item x);

bool Pop(Item *px);

};

// Stack.cpp

#include "stack.h"

bool Stack::Push(Item x) {

if (Full()) return false;

*top++ = x;

return true;

}

bool Stack::Pop(Item *px) {

if (Empty()) return false;

*px = *--top;

return true;

}

Ví dụ về lớp và thuộc tính truy xuất

// he16.cpp

#include "stack.h"

void XuatHe16(long n) {

static char hTab[] = {'0','1','2','3','4','5','6','7', '8','9','A','B','C','D','E','F'};

Stack s(8);

int x;

do {

s.Push(n%16);

n /= 16;

(6)

} while(n);

while(s.Pop(&x)) cout << hTab[x];

}

Tự tham chiếu

Là tham số ngầm định của hàm thành phần trỏ đến đối tượng, có tên là this. Nhờ đó hàm thành phần biết được nó đang thao tác trên đối tượng nào.

Khi một đối tượng gọi một thao tác, địa chỉ của đối tượng được gởi đi một cách ngầm định với tên this, tên các thành phần của đối tượng được hiểu là của đối tượng có địa chỉ this này.

Phương thức thiết lập và hủy bỏ.

Phương thức thiết lập

Phương thức thiết lập được xây dựng nhằm mục đích khắc phục lỗi quên khởi động đối tượng hoặc khởi động dư. Việc quên khởi động đối tượng thường gây ra những lỗi rất khó tìm.

Phương thức thiết lập là hàm thành phần đặc biệt được tự động gọi đến mỗi khi một đối tượng thuộc lớp được tạo ra. Người ta thường lợi dụng đặc tính trên để khởi động đối tượng.

Phương thức thiết lập có tên trùng với tên lớp để phân biệt nó với các hàm thành phần khác.

Có thể có nhiều phiên bản khác nhau của phương thức thiết lập typedef int bool;

typedef int Item;

const bool false = 0, true = 1;

class Stack {

Item *st, *top;

int size;

void Init(int sz) {st = top = new Item[size=sz];}

void CleanUp() {delete [] st;}

public:

Stack(int sz = 20) {Init(sz);}

~Stack() {delete [] st;}

bool Full() const {return (top - st >= size);}

bool Empty() const {return (top <= st);}

bool Push(Item x);

bool Pop(Item *px);

};

Hàm bạn (friends)

Nguyên tắc chung khi thao tác trên lớp là thông qua các hàm thành phần. Tuy nhiên có những trường hợp ngoại lệ, khi hàm phải thao tác trên hai lớp.

Hàm bạn của một lớp là hàm được khai báo ở bên ngoài nhưng được phép truy xuất các thành phần riêng tư của lớp.
(7)

Ta làm một hàm trở thành hàm bạn của lớp bằng cách đưa khai báo của hàm đó vào trong lớp, thêm từ khoá friend ở đầu.

Ta dùng hàm bạn trong trường hợp hàm phải là hàm toàn cục nhưng có liên quan mật thiết với lớp, hoặc là hàm thành phần của một lớp khác.

Ví dụ: Nhân ma trận, không dùng hàm bạn

const int N = 4;

class Vector {

double a[N];

public:

double Get(int i) const {return a[i];}

void Set(int i, double x) {a[i] = x;}

};

class Matrix {

double a[N][N];

public:

double Get(int i, int j) const {return a[i][j];}

void Set(int i, int j, double x) {a[i][j] = x;}

//...

};

Vector Multiply(const Matrix &m, const Vector &v) {

Vector r;

for (int i = 0; i < N; i++) {

r.Set(i, 0);

for (int j = 0; j < N; j++)

r.Set(i, r.Get(i)+ m.Get(i,j)*v.Get(j));

}

return r;

}

Ví dụ minh hoạ: Sử dụng hàm bạn

const int N = 4;

class Matrix;

class Vector {

double a[N];

public:

double Get(int i) const {return a[i];}

void Set(int i, double x) {a[i] = x;}

friend Vector Multiply(const Matrix &m, const Vector &v);

};

class Matrix {

double a[N][N];

public:

(8)

double Get(int i, int j) const {return a[i][j];}

void Set(int i, int j, double x) {a[i][j] = x;}

friend Vector Multiply(const Matrix &m, const Vector &v);

};

Ví dụ minh hoạ: Sử dụng hàm bạn

Vector Multiply(const Matrix &m, const Vector &v) {

Vector r;

for (int i = 0; i < N; i++) {

r.a[i] = 0;

for (int j = 0; j < N; j++)

r.a[i] += m.a[i][j]*v.a[j];

}

return r;

}

Hàm thành phần hằng (const member functions).

Hàm thành phần hằng là hàm thành phần có thể áp dụng được cho các đối tượng hằng.

Ta qui định một hàm thành phần là hằng bằng cách thêm từ khoá const vào cuối khai báo của nó.

Ta khai báo hàm thành phần là hằng khi nó không thay đổi các thành phần dữ liệu của đối tượng.

extern char *strdup(const char *s);

class string {

char *p;

public:

string(char *s = "") {p = strdup(s);}

~string() {delete [] p;}

string(const string &s2) {p = strdup(s2.p);}

void Output() const {cout << p;}

void ToLower() {strlwr(p);}

};

inline char *strdup(const char *s) {

return strcpy(new char[strlen(s) + 1], s);

}

void main() {

const string Truong("Dai Hoc Ban Cong Ton Duc Thang");

string s("ABCdef");

s.Output();

s.ToLower();

s.Output();

(9)

Truong.Output();

Truong.ToLower(); // Bao loi }

Thành phần tĩnh (static members)

Thành phần dữ liệu tĩnh là thành phần dữ liệu dùng chung cho mọi đối tượng thuộc lớp.

Hàm thành phần tĩnh là hàm thành phần có thể hoạt động không cần dữ liệu của đối tượng, nói cách khác, nó không cần đối tượng.

Ta dùng hàm thành phần tĩnh thay vì hàm toàn cục vì nó có liên quan mật thiết với lớp.

Ta còn dùng hàm thành phần tĩnh để tạo đối tượng có giá trị trả về cho biết việc tạo đối tượng có thành công theo nghĩa luận lý hay không.

Ví dụ về thành phần tĩnh: CDate

typedef int bool;

const bool false = 0, true = 1;

class CDate {

static int dayTab[][13];

int day, month, year;

public:

static bool LeapYear(int y) {return y%400 == 0 || y%4==0 &&

y%100 != 0;}

static int DayOfMonth(int m, int y);

static bool ValidDate(int d, int m, int y);

void Input();

};

int CDate::dayTab[][13] = {

{0,31,28,31,30,31,30,31,31,30,31,30,31}, {0,31,29,31,30,31,30,31,31,30,31,30,31}

};

int CDate::DayOfMonth(int m, int y) {

return dayTab[LeapYear(y)][m];

}

bool betw(int x, int a, int b) {

return x >= a && x <= b;

}

bool CDate::ValidDate(int d, int m, int y) {

return betw(m,1,12) && betw(d,1,DayOfMonth(m,y));

(10)

}

void CDate::Input() {

int d,m,y;

cin >> d >> m >> y;

while (!ValidDate(d,m,y)) {

cout << "Please enter a valid date: ";

cin >> d >> m >> y;

}

day = d; month = m; year = y;

}

Ví dụ về thành phần tĩnh: Stack

// stack.h

typedef int Item;

class Stack {

Item *st, *top;

int size;

void Init(int sz) {st = top = new Item[size=sz];}

void CleanUp() {if (st) delete [] st;}

public:

Stack(int sz = 20) {Init(sz);}

~Stack() {CleanUp();}

static Stack *Create(int sz);

bool Full() const {return (top - st >= size);}

bool Empty() const {return (top <= st);}

bool Push(Item x);

bool Pop(Item *px);

};

// Stack.cpp

#include <iostream.h>

#include "stack.h"

bool Stack::Push(Item x) {

if (Full()) return false;

*top++ = x;

return true;

}

bool Stack::Pop(Item *px) {

if (Empty()) return false;

*px = *--top;

return true;

}

Stack *Stack::Create(int sz)

(11)

{

Stack *ps = new Stack(sz);

if (!ps->st) {

delete ps;

return NULL;

}

return ps;

}

void main() {

Stack *ps = new Stack(50000); // khong biet tao duoc stack khong

Stack *pr = Stack::Create(50000);

if (!pr)

{ cout << "Khong the tao stack"; return; } else

pr->Push(5); // ...

}

2.3 Thiết lập và huỷ bỏ đối tượng

Ta cần kiểm soát khi nào phương thức thiết lập được gọi, khi nào phương thức huỷ bỏ được gọi.

Khi nào đối tượng thiết lập được gọi? Khi đối tượng được tạo ra.

Khi nào phương thức huỷ bỏ được gọi? Khi đối tượng bị huỷ đi.

Thời gian từ khi đối tượng được tạo ra đến khi nó bị huỷ đi được gọi là thời gian sống.

Vậy vấn đề xác định khi nào phương thức thiết lập và huỷ bỏ được gọi trở thành:

Khi nào đối tượng được tạo ra ? Khi nào đối tượng bị huỷ đi ?

Thời gian sống của đối tượng khác nhau tuỳ thuộc đối tượng đó thuộc lớp lưu trữ (storage class) nào. Trong C++ có các lớp lưu trữ sau:

auto global static

free stored

Ta sẽ lần lượt xét các loại sau:

Đối tượng tự động Đối tượng toàn cục Đối tượng tĩnh

Đối tượng được cấp phát động Đối tượng thành phần

Đối tượng tự động

Đối tượng tự động (automatic objects) là đối tượng được tự động sinh ra và tự động bị huỷ đi.

Đối tượng địa phương

Là các biến được khai báo, định nghĩa bên trong một lớp.

(12)

Nó được tự động sinh ra khi chương trình thực hiện ngang dòng lệnh chứa định nghĩa và bị huỷ đi sau khi chương trình hoàn tất khối chứa định nghĩa đó.

Khi khởi động một đối tượng bằng một đối tượng cùng kiểu, cơ chế tạo đối tượng mặc nhiên là sao chép từng bit, do đó đối tượng được khởi động sẽ chia sẻ tài nguyên với đối tượng nguồn.

Đối tượng là biến địa phương

#include <iostream.h>

#include <string.h>

extern char *strdup(const char *s);

class string{

char *p;

public:

string(char *s = "") {p = strdup(s);}

~string() {cout << "delete "<< (void *)p << "\n"; delete []

p;}

void Output() const {cout << p;}

};

char *strdup(const char *s) {

return strcpy(new char[strlen(s) + 1], s);

}

void main() {

string a("Nguyen Van A");

string b = a; // String b(a) a.Output(); cout << "\n";

b.Output(); cout << "\n";

}

Xuất liệu khi thực hiện đoạn chương trình trên:

Nguyen Van A Nguyen Van A delete 0x0f06 delete 0x0f06

Đối tượng b có nội dung vật lý giống với a, nghĩa là dùng chung phần tài nguyên (con trỏ đến chuỗi “Nguyen Van A”). Khi kết thúc hàm main() hai đối tượng này bị huỷ, phần tài nguyên chung bị huỷ 2 lần (SAI).

Đối tượng là tham số truyền bằng giá trị

Đối tượng là tham số hàm, truyền bằng giá trị thì tham số hình thức là bản sao của tham số thực sự, nên có nội dung vật lý giống tham số thực sự do cơ chế sao chép từng bit.

extern char *strdup(const char *s);

class String {

char *p;

(13)

public:

String(char *s = "") {p = strdup(s);}

~String() {cout << "delete "<< (void *)p << "\n"; delete []

p;}

void Output() const {cout << p;}

bool Compare(String s) const;

};

bool String::Compare(String s) const {

return strcmp(p,s.p);

}

void main() {

String a("Nguyen Van A");

String b("Le Van Beo");

int c = a.Compare(b);

cout << (c > 0 ? "a > b" : c == 0 ? "a = b" : "a < b") << "\

n";

}

Khi thực hiện đoạn chương trình trên, ta được xuất liệu (có thể thay đổi ở những lần thực hiện khác nhau hoặc ở máy khác):

delete 0x0f34 a > b

delete 0x0f34 delete 0x0f22

Đối tượng là giá trị trả về

extern char *strdup(const char *s);

class String {

char *p;

public:

String(char *s = "") {p = strdup(s);}

~String() {cout << "delete "<< (void *)p << "\n"; delete []

p;}

void Output() const {cout << p;}

bool Compare(String s) const;

String UpCase() const;

};

String String::UpCase() const {

String r = *this;

strupr(r.p);

return r;

}

(14)

void main() {

clrscr();

String a("Nguyen Van A");

cout << "a = "; a.Output(); cout << "\n";

String A;

A = a.UpCase();

cout << "a = "; a.Output(); cout << "\n";

cout << "A = "; a.Output(); cout << "\n";

}

Đối tượng là giá trị trả về

Khi thực hiện đoạn chương trình trên, ta được xuât liệu

a = Nguyen Van A delete 0x0f36 delete 0x0f36 a = 2¤2¤EN VAN A A = 2¤2¤EN VAN A delete 0x0f36 delete 0x0f36

Null pointer assignment

Đối tượng giá trị trả về là bản sao của biểu thức trả về. Do đó có sự chia sẻ tài nguyên (SAI).

Các lỗi sai gây ra ở đoạn chương trình trên do sao chép đối tượng (phát biểu String r =

*this), đối tượng giá trị trả về và phép gán (A = a.Upcase).

Ta có thể khắc phục lỗi gây ra do phép gán bằng cách thay hai phát biểu khai báo A và gán bằng phát biểu khởi động:

String A = a.UpCase();

void main() {

clrscr();

String a("Nguyen Van A");

cout << "a = "; a.Output(); cout << "\n";

String A = a.UpCase();

cout << "a = "; a.Output(); cout << "\n";

cout << "A = "; a.Output(); cout << "\n";

}

Xuất liệu trong trường hợp này là a = Nguyen Van A

delete 0x0d32 a = NGUYEN VAN A A = NGUYEN VAN A delete 0x0d32 delete 0x0d32

Null pointer assignment

Phương thức thiết lập sao chép

(15)

Các lỗi sai nêu trên gây ra do sao chép đối tượng, ta có thể khắc phục bằng cách

“dạy” trình biên dịch sao chép đối tượng một cách luận lý thay vì sao chép từng bit theo nghĩa vật lý. Điều đó được thực hiện nhờ phương thức thiết lập sao chép.

Phương thức thiết lập sao chép là phương thức thiết lập có tham số là đối tượng cùng kiểu, dùng để sao chép đối tượng.

Phương thức thiết lập sao chép thực hiện sao chép theo nghĩa logic, thông thường là tạo nên tài nguyên mới (sao chép sâu).

Phương thức thiết lập sao chép cũng có thể chia sẻ tài nguyên cho các đối tượng (sao chép nông). Trong trường hợp này, cần có cơ chế để kiểm soát sử huỷ bỏ đối tượng.

extern char *strdup(const char *s);

class String {

char *p;

public:

String(char *s = "") {p = strdup(s);}

String(const String &s) {p = strdup(s.p);}

~String() {cout << "delete "<< (void *)p << "\n"; delete []

p;}

void Output() const {cout << p;}

bool Compare(String s) const;

String UpCase() const;

};

char *strdup(const char *s) {

return strcpy(new char[strlen(s)+1], s);

}

Phương thức thiết lập sao chép

bool String::Compare(String s) const {

return strcmp(p,s.p);

}

String String::UpCase() const {

String r = *this;

strupr(r.p);

return r;

}

void main() {

clrscr();

String a("Nguyen Van A");

String b("Le Van Beo");

String aa = a;

int c = a.Compare(b);

cout << (c > 0 ? "a > b" : c == 0 ? "a = b" : "a < b") << "\

n";

(16)

cout << "a = "; a.Output(); cout << "\n";

String A = a.UpCase();

cout << "a = "; a.Output(); cout << "\n";

cout << "A = "; A.Output(); cout << "\n";

}

Xuất liệu trong trường hợp trên như sau:

delete 0x0d84 a > b

a = Nguyen Van A delete 0x0d84 a = Nguyen Van A A = NGUYEN VAN A delete 0x0d96 delete 0x0d72 delete 0x0d62 delete 0x0d50

Mỗi đối tượng đều có tài nguyên riêng nên không xảy trường hợp một vùng tài nguyên bị giải phóng nhiều lần.

Phương thức thiết lập sao chép

Tham số của phương thức thiết lập sao chép bắt buộc là tham chiếu.

Phương thức thiết lập sao chép có thể được dùng để sao chép nông, tài nguyên vẫn được chia sẻ nhưng có một biến đếm để kiểm soát.

Lưu ý:

Nếu đối tượng không có tài nguyên riêng thì không cần phương thức thiết lập sao chép.

Khi truyền tham số là đối tượng thuộc lớp có phương thức thiết lập sao chép, ta nên truyền bằng tham chiếu và có thêm từ khoá const.

Sao chép nông và sao chép sâu

Dùng phương thức thiết lập sao chép như trên, trong đó đối tượng mới có tài nguyên riêng là sao chép sâu.

Ta có thể sao chép nông bằng cách chia sẻ tài nguyên và dùng một biến đếm để kiểm soát số thể hiện các đối tượng có chia sẻ cùng tài nguyên.

Khi một đối tượng thay đổi nội dung, nó phải được tách ra khỏi các đối tượng dùng chung tài nguyên, nói cách khác, nó phải có tài nguyên riêng (như sao chép sâu).

Ví dụ về sao chép nông

extern char *strdup(const char *s);

class StringRep {

friend class String;

char *p;

int n;

StringRep(const char *s) {p = strdup(s); n = 1;}

~StringRep() {cout << "delete ” << (void *)p << "\n";}

};

(17)

char *strdup(const char *s) {

return strcpy(new char[strlen(s)+1], s);

}

class String {

StringRep *rep;

public:

String(const char *s = "") {rep = new StringRep(s);}

String(const String &s) {rep = s.rep; rep->n++;}

~String();

void Output() const {cout << rep->p;}

bool Compare(String s) const;

String UpCase() const;

void ToUpper();

};

String::~String() {

if (--rep->n <= 0) delete rep;

}

bool String::Compare(String s) const {

return strcmp(rep->p,s.rep->p);

}

String String::UpCase() const {

String r(rep->p);

strupr(r.rep->p);

return r;

}

void main() {

clrscr();

String a("Nguyen Van A");

String b("Le Van Beo");

String aa = a;

int c = a.Compare(b);

cout << (c > 0 ? "a > b" : c == 0 ? "a = b" : "a < b") << "\

n";

cout << "a = "; a.Output(); cout << "\n";

String A = a.UpCase();

cout << "a = "; a.Output(); cout << "\n";

cout << "A = "; A.Output(); cout << "\n";

}

(18)

Xuất liệu khi thực hiện đoạn chương trình trên như sau:

a > b

a = Nguyen Van A a = Nguyen Van A A = NGUYEN VAN A delete 0x0d8a delete 0x0d72 delete 0x0d58 delete 0x0d84 a > b

a = Nguyen Van A delete 0x0d84 a = Nguyen Van A A = NGUYEN VAN A delete 0x0d96 delete 0x0d72 delete 0x0d62 delete 0x0d50 a > b

a = Nguyen Van A a = Nguyen Van A A = NGUYEN VAN A delete 0x0d8a delete 0x0d72 delete 0x0d58

Đối tượng tĩnh

Đối tượng tĩnh có thể là đối tượng được định nghĩa toàn cục, được định nghĩa có thêm từ khoá static (toàn cục hoặc địa phương).

Đối tượng toàn cục hoặc tĩnh toàn cục được tạo ra khi bắt đầu chương trình và bị huỷ đi khi kết thúc chương trình.

Đối tượng tĩnh địa phương được tạo ra khi chương trình thực hiện đoạn mã chứa định nghĩa đối tượng lần đầu tiên và bị huỷ đi khi kết thúc chương trình.

Trình tự thực hiện chương trình gồm:

Gọi phương thức thiết lập cho các đối tượng toàn cục Thực hiện hàm main()

Gọi phương thức huỷ bỏ cho các đối tượng toàn cục

Ví dụ về đối tượng toàn cục

Xét đoạn chương trình sau:

#include <iostream.h>

void main() {

cout << "Hello, world.\n";

}

Hãy sửa lại đoạn chương trình trên để có xuất liệu:

Entering a C++ program saying...

(19)

Hello, world.

And then exitting…

Yêu cầu không thay đổi hàm main() dưới bất kỳ hình thức nào.

Đoạn chương trình được sửa lại như sau:

#include <iostream.h>

void main() {

cout << "Hello, world.\n";

}

class Dummy {

public:

Dummy() {cout << "Entering a C++ program saying...\n";}

~Dummy() {cout << "And then exitting...";}

};

Đối tượng là thành phần của lớp

Đối tượng có thể là thành phần của đối tượng khác, khi một đối tượng thuộc lớp “lớn”

được tạo ra, các thành phần của nó cũng được tạo ra. Phương thức thiết lập (nếu có) sẽ được tự động gọi cho các đối tượng thành phần.

Nếu đối tượng thành phần phải được cung cấp tham số khi thiết lập thì đối tượng kết hợp (đối tượng lớn) phải có phương thức thiết lập để cung cấp tham số thiết lập cho các đối tượng thành phần.

Cú pháp để khởi động đối tượng thành phần là dùng dấu hai chấm (:) theo sau bởi tên thành phần và tham số khởi động.

Khi đối tượng kết hợp bị huỷ đi thì các đối tượng thành phần của nó cũng bị huỷ đi, nghĩa là phương thức huỷ bỏ sẽ được gọi cho các đối tượng thành phần, sau khi phương thức huỷ bỏ của đối tượng kết hợp được gọi.

class Diem {

double x,y;

public:

Diem(double xx, double yy {x = xx; y = yy;}

// ...

};

class String {

char *p;

public:

String(char *s) {p = strdup(s);}

String(const String &s) {p = strdup(s.p);}

~String() {cout << "delete "<< (void *)p << "\n"; delete []

p;

//...

};

(20)

Đối tượng là thành phần của lớp

class TamGiac {

Diem A,B,C;

public:

void Ve() const;

// ...

};

class SinhVien {

String MaSo;

String HoTen;

int NamSinh;

public:

};

TamGiac t; // Bao sai SinhVien s; // Bao sai

Đối tượng là thành phần của lớp - thiết lập

class Diem {

double x,y;

public:

Diem(double xx, double yy {x = xx; y = yy;}

// ...

};

class String {

char *p;

public:

String(char *s) {p = strdup(s);}

String(const String &s) {p = strdup(s.p);}

~String() {cout << "delete "<< (void *)p << "\n"; delete []

p;

//...

};

Khởi động đối tượng thành phần: Dùng dấu hai chấm (:)

class TamGiac {

Diem A,B,C;

public:

TamGiac(double xA, double yA, double xB, double yB, double xC, double yC):A(xA,yA), B(xB,yB),C(xC,yC){}

void Ve() const;

// ...

};

TamGiac t(100,100,200,400,300,300);

Khởi động đối tượng thành phần: Dùng dấu hai chấm (:)

class SinhVien {

(21)

String MaSo;

String HoTen;

int NamSinh;

public:

SinhVien(char *ht, char *ms, int ns):HoTen(ht), MaSo(ms) {NamSinh = ns;}

//...

};

SinhVien s(“Nguyen Van Beo”, “19920005”, 1985); //Ok

Khởi động đối tượng thành phần: Dùng dấu hai chấm (:)

Cú pháp dùng dấu hai chấm cũng được dùng cho đối tượng thành phần thuộc kiểu cơ bản.

class Diem {

double x,y;

public:

Diem(double xx, double yy):x(xx), y(yy){}

// ...

};

class SinhVien {

String MaSo, HoTen;

int NamSinh;

public:

SinhVien(char *ht, char *ms, int ns):HoTen(ht), MaSo(ms), NamSinh(ns){}

//...

};

Đối tượng thành phần - Huỷ bỏ

Khi đối tượng kết hợp bị huỷ bỏ, các đối tượng thành phần của nó cũng bị huỷ bỏ.

class SinhVien {

String MaSo, HoTen;

int NamSinh;

int SoMon;

double *Diem;

public:

SinhVien(char *ht, char *ms, int ns, int sm, double *d);

~SinhVien() {delete [] Diem;}

//...

};

SinhVien::SinhVien(char *ht, char *ms, int ns, int sm, double

*d):HoTen(ht), MaSo(ms), NamSinh(ns), SoMon(sm) {

memcpy(Diem = new double[SoMon], d, SoMon*sizeof(double));

}

Đối tượng là thành phần của mảng

Khi một mảng được tạo ra, các phần tử của nó cũng được tạo ra, do đó phương thức thiết lập sẽ được gọi cho từng phần tử một.
(22)

Vì không thể cung cấp tham số khởi động cho tất cả các phần tử của mảng nên khi khai báo mảng, mỗi đối tượng trong mảng phải có khả năng tự khởi động, nghĩa là có thể được thiết lập không cần tham số.

Đối tượng có khả năng tự khởi động trong các trường hợp sau:

– Lớp không có phương thức thiết lập.

– Lớp có phương thức thiết lập không tham số.

– Lớp có phương thức thiết lập mà mọi tham số đều có giá trị mặc nhiên.

class Diem {

double x,y;

public:

Diem(double xx, double yy):x(xx), y(yy) {}

void Set(double xx, double yy) {x = xx, y = yy;}

// ...

};

class String {

char *p;

public:

String(char *s) {p = strdup(s);}

String(const String &s) {p = strdup(s.p);}

~String() {cout << "delete "<< (void *)p << "\n"; delete []

p;}

// ...

class SinhVien {

String MaSo;

String HoTen;

int NamSinh;

public:

SinhVien(char *ht, char *ms, int ns):HoTen(ht), MaSo(ms), NamSinh(ns){}

//...

};

String as[3]; // Bao sai Diem ad[5]; // Bao sai

SinhVien asv[7]; // Bao sai

Dùng phương thức thiết lập với tham số có giá trị mặc nhiên

class Diem {

double x,y;

public:

Diem(double xx = 0, double yy = 0):x(xx), y(yy){}

void Set(double xx, double yy) {x = xx, y = yy;}

// ...

};

(23)

class String {

char *p;

public:

String(char *s = “”) {p = strdup(s);}

String(const String &s) {p = strdup(s.p);}

~String() {cout << "delete "<< (void *)p << "\n"; delete []

p;}

// ...

class SinhVien {

String MaSo;

String HoTen;

int NamSinh;

public:

SinhVien(char *ht = “Nguyen Van A”, char *ms = “19920014”, int ns = 1982):HoTen(ht), MaSo(ms), NamSinh(ns){}

//...

};

String as[3]; // Ok: Ca ba phan tu deu la chuoi rong Diem ad[5]; // Ok: ca 5 diem deu la (0,0)

SinhVien asv[7]; // Ok: Het sai ca 7 sinh vien deu co cung hoten, maso, namsinh

Dùng phương thức thiết lập không tham số

class Diem {

double x,y;

public:

Diem(double xx, double yy):x(xx), y(yy){}

Diem():x(0), y(0){}

// ...

};

class String {

char *p;

public:

String(char *s) {p = strdup(s);}

String() {p = strdup(“”);}

~String() {cout << "delete "<< (void *)p << "\n"; delete []

p;}

// ...

};

class SinhVien {

String MaSo;

String HoTen;

int NamSinh;

public:

(24)

SinhVien(char *ht, char *ms, int ns):HoTen(ht), MaSo(ms), NamSinh(ns){}

SinhVien():HoTen(“Nguyen Van A”), MaSo(“19920014”), NamSinh(1982){}

//...

};

String as[3]; // Ca ba phan tu deu la chuoi rong Diem ad[5]; // ca 5 diem deu la (0,0)

SinhVien asv[7];// Ca 7 sinh vien deu co cung hoten, maso, namsinh

Đối tượng được cấp phát động

Đối tượng được cấp phát động là các đối tượng được tạo ra bằng phép toán new và bị huỷ đi bằng phép toán delete

Phép toán new cấp đối tượng trong vùng heap (hay vùng free store) và gọi phương thức thiết lập cho đối tượng được cấp.

Dùng new có thể cấp một đối tượng và dùng delete để huỷ một đối tượng.

Dùng new và delete cũng có thể cấp nhiều đối tượng và huỷ nhiều đối tượng.

class String {

char *p;

public:

String(char *s) {p = strdup(s);}

String(const String &s) {p = strdup(s.p);}

~String() {delete [] p;}

//...

};

class Diem {

double x,y;

public:

Diem(double xx, double yy):x(xx),y(yy){};

//...

};

//...

Cấp và huỷ một đối tượng

int *pi = new int;

int *pj = new int(15);

Diem *pd = new Diem(20,40);

String *pa = new String("Nguyen Van A");

//...

delete pa;

delete pd;

delete pj;

delete pi;

Cấp và huỷ nhiều đối tượng

(25)

Trong trường hợp cấp nhiều đối tượng, ta không thể cung cấp tham số cho từng phần tử được cấp:

int *pai = new int[10];

Diem *pad = new Diem[5]; // Bao sai String *pas = new String[5]; // Bao sai //...

Thông báo lỗi cho đoạn chương trình trên như sau:

Cannot find default constructor to initialize array element of type 'Diem' Cannot find default constructor to initialize array element of type String’

Lỗi trên được khắc phục bằng cách cung cấp phương thức thiết lập để đối tượng có khả năng tự khởi động.

class String {

char *p;

public:

String(char *s = "Alibaba") {p = strdup(s);}

String(const String &s) {p = strdup(s.p);}

~String() {delete [] p;}

//...

};

class Diem {

double x,y;

public:

Diem(double xx, double yy):x(xx),y(yy){};

Diem():x(0),y(0){};

//...

};

Khi đó mọi phần tử được cấp đều được khởi động với cùng giá trị int *pai = new int[10];

Diem *pad = new Diem[5];

// ca 5 diem co cung toa do (0,0) String *pas = new String[5];

// Ca 5 chuoi cung duoc khoi dong bang “Alibaba”

Việc huỷ nhiều đối tượng được thực hiện bằng cách dùng delete và có thêm dấu [] ở trước.

delete [] pas;

delete [] pad;

delete [] pai;

 (?)

Có thể thay ba phát biểu trên bằng một phát biểu duy nhất sau được không ?

delete pas,pad,pai; // ??

2.4 Giao diện và chi tiết cài đặt

(26)

Lớp có hai phần tách rời, một là phần giao diện khai báo trong phần public để người sử dụng “thấy” và sử dụng, và hai là chi tiết cài đặt bao gồm dữ liệu khai báo trong phần private của lớp và chi tiết mã hoá các hàm thành phần, vô hình đối với người dùng.

Ta có thể thay đổi uyển chuyển chi tiết cài đặt, nghĩa là có thể thay đổi tổ chức dữ liệu của lớp, cũng như có thể thay đổi chi tiết thực hiện các hàm thành phần (do sự thay đổi tổ chức dữ liệu hoặc để cải tiến giải thuật). Nhưng nếu bảo đảm không thay đổi phần giao diện thì không ảnh hưởng đến người sử dụng, và do đó không làm đổ vỡ kiến trúc của hệ thống.

Lớp ThoiDiem có thể được cài đặt với các thành phần dữ liệu là giờ, phút, giây hoặc tổng số giây tính từ 0 giờ.

Lớp ThoiDiem – Cách 1

class ThoiDiem {

int gio, phut, giay;

static bool HopLe(int g, int p, int gy);

public:

ThoiDiem(int g = 0, int p = 0, int gy = 0) {Set(g,p,gy);}

void Set(int g, int p, int gy);

int LayGio() const {return gio;}

int LayPhut() const {return phut;}

int LayGiay() const {return giay;}

void Nhap();

void Xuat() const;

void Tang();

void Giam();

};

Lớp ThoiDiem – Cách 2

class ThoiDiem {

long tsgiay;

static bool HopLe(int g, int p, int gy);

public:

ThoiDiem(int g = 0, int p = 0, int gy = 0) {Set(g,p,gy);}

void Set(int g, int p, int gy);

int LayGio() const {return tsgiay / 3600;}

int LayPhut() const {return (tsgiay%3600)/60;}

int LayGiay() const {return tsgiay % 60;}

void Nhap();

void Xuat() const;

void Tang();

void Giam();

};

Có thể xem chi tiết đầy đủ lớp thời điểm cài đặt bằng giờ, phút, giây và cài đặt bằng tổng số giây trong tập tin nguồn thgian.cpp và thgian2.cpp,

Tương tự lớp Stack có thể được cài đặt dưới dạng mảng hoặc dưới dạng danh sách liên kết.

2.5 Các nguyên tắc xây dựng lớp

(27)

Khi ta có thể nghĩ đến “nó” như một khái niệm riêng rẽ, xây dựng lớp biểu diễn khái niệm đó. Ví dụ lớp SinhVien.

Khi ta nghĩ đến “nó” như một thực thể riêng rẽ, tạo đối tượng thuộc lớp. Ví dụ đối tượng Sinh viên “Nguyen Van A” (và các thuộc tính khác như mã số, năm sinh…).

Lớp là biểu diễn cụ thể của một khái niệm, vì vậy lớp luôn luôn là DANH TỪ.

Các thuộc tính của lớp là các thành phần dữ liệu, nên chúng luôn luôn là DANH TỪ.

Các hàm thành phần là các thao tác chỉ rõ hoạt động của lớp nên các hàm này là ĐỘNG TỪ.

Các thuộc tính dữ liệu phải vừa đủ để mô tả khái niệm, không dư, không thiếu.

// SAI

class TamGiac {

Diem A,B,C;

double ChuVi, DienTich;

public:

//...

};

// DUNG

class TamGiac {

Diem A,B,C;

public:

//...

double ChuVi()const; double DienTich() const;

};

Cá biệt có thể có một số thuộc tính suy diễn đòi hỏi nhiều tài nguyên hoặc thời gian để thực hiện tính toán, ta có thể khai báo là dữ liệu thành phần. Ví dụ tuổi trung bình của dân Việt Nam.

class QuocGia {

long DanSo;

double DienTich;

double TuoiTrungBinh;

//...

public:

double TinhTuoiTB() const;

//...

};

Chi tiết cài đặt, bao gồm dữ liệu và phần mã hoá các hàm thành phần có thể thay đổi uyển chuyển nhưng phần giao diện, nghĩa là phần khai báo các hàm thành phần cần phải cố định để không ảnh hưởng đến người sử dụng (xem phần 2.4). Tuy nhiên nên cố gắng cài đặt dữ liệu một cách tự nhiên theo đúng khái niệm.
(28)

Dữ liệu thành phần nên được kết hợp thay vì phân rã

Chi tiết cài đặt, bao gồm dữ liệu và phần mã hoá các hàm thành phần có thể thay đổi uyển chuyển nhưng phần giao diện, nghĩa là phần khai báo các hàm thành phần cần phải cố định để không ảnh hưởng đến người sử dụng (xem phần 2.4).

Dữ liệu thành phần nên được kết hợp thay vì phân rã

Trong mọi trường hợp, nên có phương thức thiết lập để khởi động đối tượng.

Nên có phương thức thiết lập có khả năng tự khởi động không cần tham số.

Nếu đối tượng có nhu cầu cấp phát tài nguyên thì phải có phương thức thiết lập, phương thức thiết lập sao chép để khởi động đối tượng bằng đối tượng cùng kiểu và có phương thức huỷ bỏ để dọn dẹp. Ngoài ra còn phải có phép gán (chương tiếp theo).

Ngược lại, đối tượng đơn giản không cần tài nguyên riêng thì không cần phương thức thiết lập sao chép và cũng không cần phương thức huỷ bỏ.

2.6 Một số ví dụ về lớp

Lớp Diem biểu diễn khái niệm điểm trong mặt phẳng với hai thành phần toạ độ x và y. Khai báo lớp Diem được đặt trong tập tin diem.h và chi tiết cài đặt các hàm thành phần được đặt trong tập tin diem.cpp. Có thể xây dựng một số ứng dụng của lớp điểm như trong tập tin tdiem.cpp hoặc dongho.cpp với các ứng dụng tương ứng là tdiem.exe và dongho.exe (hoặc dongho2.exe, dongho3.exe).

Lớp DaGiac biểu diễn khái niệm đa giác trong mặt phẳng có các thao tác tịnh tiến, vị tự, quay, vẽ… Phần khai báo được đặt trong tập tin dagiac.h, phần cài đặt trong tập tin

dagiac.cpp và ứng dụng tdg.cpp cho phép tạo, vẽ, tịnh tiến và quay đa giác. Tập tin thực thi là tdg.exe

Tài liệu tham khảo

Tài liệu liên quan

Môû roäng voán töø: töø ngöõ veà thôøi tieát Ñaët vaø traû lôøi caâu hoûi Khi naøo?. Daáu chaám, daáu

Ñaây laø heä phöông trình cô baûn ñeå giaûi chuùng ta coù theå thöïc hieän pheùp theá, söû duïng maùy tính boû tuùi hoaëc söû duïng ñònh thöùc Crame(hay

Em haõy tìm trong saùch Tieáng Vieät caâu vaên coù söû duïng daáu hai chaám coù taùc duïng baùo hieäu boä phaän caâu ñöùng sau noù laø lôøi noùi

™ Baàu khí quyeån cuûa chuùng ta hieän nay ñöôïc hình thaønh vaø tieán hoùa chuû yeáu laø keát quaû cuûa caùc quaù trình sinh hoïc... Söï tieán hoùa, thaønh phaàn

Maëc duø ñaây laø phaùc ñoà ngaén ngaøy hôn söû duïng caùc thuoác môùi hôn vaø chöa ñöôïc ñöa vaøo thöïc hieän nhöng lyù do cuûa keát quaû ñaùnh giaù naøy coù theå xuaát phaùt töø thöïc

Lieäu caùc cöïu hoïc vieân CHV - “saûn phaåm” cuûa caùc chöông trình ñaøo taïo CTÑT Thaïc syõ coù söû duïng caùc naêng löïc cuûa chuyeân ngaønh ñöôïc ñaøo taïo trong coâng vieäc vaø

Baøi baùo söû duïng nhöõng döõ lieäu saün coù treân caùc trang web ñeå toång hôïp, phaân tích thoâng tin veà BÑG döôùi caùc taùc ñoäng tröïc tieáp cuûa BÑKH vaø vaán ñeà loàng gheùp

Naêng löôïng sinh hoïc • Vaät lieäu sinh hoïc luoân ñöôïc xem laø moät nguoàn naêng löôïng • Vieäc söû duïng vaät lieäu sinh hoïc môùi giuùp laøm giaûm vieäc ñoát nhieân lieäu hoùa