You are on page 1of 66

TRƯỜNG ĐẠI HỌC GIAO THÔNG VẬN TẢI

PHÂN HIỆU TẠI TP. HỒ CHÍ MINH


BỘ MÔN CÔNG NGHỆ THÔNG TIN

BÁO CÁO BÀI TẬP LỚN

NỘI DUNG: DỊCH TÀI LIỆU

The C++ Programming Language Fourth Edition

Giảng viên hướng dẫn: TRẦN THỊ DUNG

Sinh viên thực hiện: NGUYỄN ĐÌNH TRINH ĐẠT

Lớp : CÔNG NGHỆ THÔNG TIN K62

Khoá : K62

Tp. Hồ Chí Minh, năm 2022


TRƯỜNG ĐẠI HỌC GIAO THÔNG VẬN TẢI

PHÂN HIỆU TẠI TP. HỒ CHÍ MINH

BỘ MÔN CÔNG NGHỆ THÔNG TIN

BÁO CÁO BÀI TẬP LỚN

NỘI DUNG: DỊCH TÀI LIỆU

The C++ Programming Language Fourth Edition

Giảng viên hướng dẫn: TRẦN THỊ DUNG

Sinh viên thực hiện: NGUYỄN ĐÌNH TRINH ĐẠT

Lớp : CÔNG NGHỆ THÔNG TIN K62

Khoá : K62

Tp. Hồ Chí Minh, năm 2022


NHẬN XÉT CỦA GIÁO VIÊN HƯỚNG DẪN

………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
………………………………………………………………………………………………
…………………………………………………………………………………………...

………………………………………………………………………………………………

………………………………………………………………………………………………

Tp. Hồ Chí Minh, ngày ….… tháng 09 năm 2022


Giáo viên hướng dẫn

Trần Thị Dung

3
Chương 16: CLASSES

Those types are not “abstract”;

they are as real as int and float.

– Doug McIlroy

 Giới thiệu
 Kiến thức cơ bản về Class

+ Menber Funtions (Hàm thành phần); Default Copying(Bản sao mặc định);
Access Control(Kiểm soát truy cập); class and struct (Lớp và cấu trúc);
Constructors( Khởi tạo); explicit Constructors( khởi tạo tường minh); In-Class
Initializers (Bộ khởi tạo trong Class); In-Class Function Definitions(Định nghĩa
hàm trong Class); Mutability(Tính đột biến); Self-Refer-ence(Tự tham khảo);
Member Access(Truy cập thành phần); static Members(Thành phần tĩnh );
Member Types(Các loại thành phần).

+ Concrete Classes (Lớp khởi tạo đối tượng)

- Menber Funtions (Hàm thành phần); Helper Functions (Người trợ giúp);
Overloaded Operators (Nạp chồng toán tử); The Significance of
Concrete (Tầm quan trọng của lớp cụ thể hay lớp không trừu tượng)
 Lời khuyên

16.1 Tổng quan

C++ classes là một công cụ cho việc tạo ra các kiểu mới có thể sử dụng thuận tiện như
những kiểu đã được tích hợp sẵn. Ngoài ra, Lớp dẫn xuất (Derived classes) (§3.2.4,
Chương 20) và temblates (§3.4, Chương 23) cho phép các lập trình viên thể hiện (phân
cấp và tham số) các mối quan hệ giữa các class và tận dụng các mối quan hệ đó.

Mỗi kiểu là đại diện cụ thể cho một khái niệm (ý tưởng, khái niệm, …). Ví dụ, kiểu float
tích hợp trong C++ với các phép toán +, -, *, … của nó, cung cấp một sự gần đúng cụ thể
của khái niệm toán học về một số thực. Một Class là một kiểu do người dùng định nghĩa.
Chúng ta thiết kế ra một kiểu mới để tạo ra một định nghĩa mà chưa được tích hợp sẵn. Ví
dụ: chúng ta có thể cung cấp loại Trunk_line trong chương trình xử lý điện thoại, loại
4
Explosion cho trò chơi điện tử hoặc loại danh sách <Paragraph> cho chương trình xử lý
văn bản. Một chương trình tạo ra với các kiểu tương thích với các khái niệm của chương
trình đó thì sẽ dễ hiểu hơn, dễ lập luận hơn, dễ sửa đổi hơn so với một chương trình
ngược lại. Việc xác định chính xác các kiểu cũng giúp cho chương trình trở nên ngắn gọn
hơn. Ngoài ra, nó làm cho nhiều loại phân tích code có thể thực hiện được. Đặc biệt, nó
cho phép trình biên dịch phát hiện ra việc sử dụng các đối tượng bất hợp pháp mà nếu
không thì chỉ được tìm thấy thông qua kiểm tra toàn diện.

Ý tưởng trong việc xác định một kiểu mới là tách các chi tiết ngẫu nhiên của việc triển
khai (ví dụ: bố cục của dữ liệu được sử dụng để lưu trữ một đối tượng của kiểu) khỏi các
thuộc tính cần thiết nhằm sử dụng nó một cách chính xác (ví dụ: danh sách đầy đủ của các
hàm có thể truy cập dữ liệu). Sự tách biệt như vậy được thể hiện tốt nhất bằng cách phân
luồng tất cả các mục đích sử dụng cấu trúc dữ liệu và các quy trình ‘’ vệ sinh nội bộ`` của
nó thông qua một giao diện cụ thể.

Chương này tập trung vào các kiểu tương đối đơn giản '' cụ thể '' do người dùng xác định
mà về mặt logic không khác nhiều so với các kiểu tích hợp sẵn:

§16.2 Khái niệm cơ bản về lớp, Cơ sở cơ bản để xác định một lớp và các thành phần của
nó.

§16.3 Lớp cụ thể: thảo luận về việc trình bày các lớp cụ thể đẹp và hiệu quả.

Các chương sau đi vào chi tiết hơn và trình bày các lớp trừu tượng và cấu trúc phân cấp
lớp:

Chương 17: Xây dựng, Hủy bỏ, Sao chép và Di chuyển trình bày nhiều cách khác nhau để
kiểm soát việc khởi tạo các đối tượng của một lớp, cách sao chép và di chuyển các đối
tượng cũng như cách cung cấp '' các hành động dọn dẹp '' sẽ được thực hiện khi một đối
tượng bị phá hủy (ví dụ: vượt ra khỏi phạm vi).

Chương 18: Nạp chồng toán tử, giải thích cách xác định các toán tử đơn phân và nhị
phân (VD: +, ∗, ! ) cho các loại do người dùng xác định và cách sử dụng chúng.

Chương 19: Toán tử đặc biệt, cách xác định và sử dụng các toán tử (VD: [], (), ->, new),
“Đặc biệt” ở chỗ chúng được sử dụng bằng những cách khác với toán tử số học và logic.
Đặc biệt, chương này chỉ ra cách định nghĩa một String Class.

5
Chương 20: Lớp dẫn xuất (Derived classes) giới thiệu các tính năng cơ của của ngôn ngữ
hỗ trợ lập trình hướng đối tượng. Các lớp cơ sở và lớp dẫn xuất, các hàm ảo, kiểm soát
truy cập được bảo đảm.

Chương 21: Lớp kế thừa (Class Hierarchies) sử dụng các lớp cơ sở và lớp dẫn xuất để
xây dựng code xung quanh khái niệm lớp kế thừa. Phần lớn chương này nói về các kỹ
thuật lập trình, nhưng vẫn đề cập đến kỹ thuật đa kế thừa (Các lớp có nhiều hơn một lớp
cơ sở).

Chương 22: Run-time type information (RTTI) mô tả các kỹ thuật để điều hướng một
cách rõ ràng các cấu trúc lớp kế thừa. Cụ thể, các loại thay đổi kiểu dynamic_cast và
static_cast được nêu rõ, cũng như xác định kiểu của một đối tượng cho một trong các lớp
cơ sở của nó (typeid).

16.2 Lớp cơ sở

Nội dung cơ bản:

 Một lớp là một kiểu do người dùng định nghĩa.


 Một lớp bao gồm một tập hợp các thành phần. Như là dữ liệu hay các hàm thành
phần của nó.
 Các hàm thành phần có thể được định nghĩa sự khởi tạo (creation), sao chép, di
chuyển, và hủy bỏ.
 Các thành phần được truy cập bằng dấu chấm “.” cho các đối tượng và dấu mũi tên
“->” cho con trỏ.
 Các toán tử, chẳng hạn như +, !, và [], có thể được định nghĩa cho một lớp.
 Một lớp là không gian để chứa các thành phần của nó
 Các thành phần Pulic cung cấp các “class’s interface” hay thường được gọi là các
nguyên mẫu hàm còn các thành phần Private triển khai các chi tiết.
 Struct là một lớp (class) mà các thành phần bên trong nó mặc định được công khai
Public.

Ví dụ:

class X {

private: // thông báo thàn phần sau là private


6
int m;

public: // thông báo thàn phần sau là public

X(int i =0) :m{i} { } // khởi tạo

int mf(int i) // hàm thành phần

int old = m;

m = i; // gán giá trị mới

return old; // trả về giá trị cũ

};

X var {7}; // một biến kiểu X được khởi tạo thành 7

int user(X var, X∗ ptr)

int x = var.mf(7); // truy cập sử dụng dấu chấm “.”

int y = ptr−>mf(9); // truy cập sử dụng dấu mũi tên “->”

int z = var.m; // lỗi: không thể truy cập thành viên private

Các phần sau đây nghiên cứu sâu về phần này và các cơ sở lý luận liên quan.

16.2.1 Hàn thành phần

Triển khai một struct date để xác định cách thể hiện thông tin Data (ngày, tháng, năm) và
các hàm để thực hiện việc này:

struct Date {
7
int d, m, y;

};

void init_date(Date& d, int, int, int); // khởi tạo d

void add_year(Date& d, int n); // nhập năm cho d

void add_month(Date& d, int n); // nhập tháng cho d

void add_day(Date& d, int n); // nhập ngày cho d

Ở đây có thể dễ dàng nhìn thấy là không thể nào kết nối các thành phần một cách rõ ràng.
Để cho rõ ràng thì cần khai báo nguyên mẫu hàm trước như sau:

struct Date {

int d, m, y;

void init(int dd, int mm, int yy); // khởi tạo

void add_year(int n); // nhập năm

void add_month(int n); // nhập tháng

void add_day(int n); // nhập ngày

};

Các hàm được khai báo trong một lớp (struct cũng được coi là một lớp) được gọi là các
hàm thành phần và chỉ có thể dược gọi cho một cho một biến cụ thể của kiểu thích hợp
bằng cách sử dụng các cú pháp để truy cập các thành phần của struct. Ví dụ:

Date my_bir thday;

void f()

Date today;

today.init(16,10,1996);
8
my_bir thday.init(30,12,1950);

Date tomorrow = today;

tomorrow.add_day(1);

// ...

Bởi vì các struct khác nhau có thể có các hàm thành phần trùng tên nhau, nên ta phải chỉ
định tên struct khi định nghĩa một hàm thành phần.

void Date::init(int dd, int mm, int yy)

d = dd;

m = mm;

y = yy;

Trong một hàm thành phần, tên của các thành phần có thể sử dụng mà không cần phải
tham chiếu rõ ràng đến một đối tượng nào. Trong trường hợp đó, tên truy cập đến thành
phần của các đối tượng thứ mà đã được gọi lại. Ví dụ: khi Date::init() được gọi cho today,
m=mm sẽ gán cho today.m. Mặt khác, khi Date::init() được gọi cho my_birthday, m=mm
sẽ gán cho my.birthday.m. Một hàm thành phần trong một lớp “hiểu” đối tượng mà mà nó
đã gọi. Để biết thêm chi tiết về thành phần static ở phần sau.

16.2.2 Hàm mặc định sao chép

Mặc định các đối tượng có thể được sao chép. Đặc biệt, một đối tượng của lớp có thể
được khởi tạo bằng bản sao của một đối tượng thuộc lớp của nó. Ví dụ:

Date d1 = my_birthday;

Date d2 {my_birthday};

9
Mặc định, bản sao của một đối tượng lớp là bản sao của mỗi thành phần và các đối tượng
của lớp có thể được sao chép theo phép gán. Ví dụ:

void f(Date& d)

d = my_birthday;

16.2.3 Kiếm soát truy cập

Ở phần trước nó không chỉ rõ những hàm nào là hàm duy nhất trực tiếp vào việc thể hiện
Date và những hàm duy nhất để truy cập trực tiếp vào các đối tượng của lớp Date. Để
khắc phục hạn chế này thì ta sử dụng thay thế struct bằng một class:

class Date {

int d, m, y;

public:

void init(int dd, int mm, int yy);

void add_year(int n);

void add_month(int n);

void add_day(int n);

};

“public” tách nội dung của class thành 2 phần riêng biệt. Trước public là các thành phần
private và chỉ có thể được sử dụng bởi các hàm thành phần. Còn lại là thành phần public
có các nguyên mẫu hàm. Struct đơn giản là một lớp gồm các thành phần public; các chức
năng bên trong có thể được định nghĩa và sử dụng chính xác như phía trên. Ví dụ:

void Date::add_year(int n)

{
10
y += n;

Tuy nhiên, các hàm không phải hàm thành phần thì bị cấm sử dụng các thành phần
private. Ví dụ:

void timewarp(Date& d)

d.y −= 200; // Lỗi : Date::y is private

Hàm init() bây giờ rất cần thiết vì viêc đặt dữ liệu ở chế độ private buộc chúng ta phải
cung cấp cách khởi tạo các thành phần. Ví dụ:

Date dx;

dx.m = 3; // lỗi : m is private

dx.init(25,3,2011); // OK

Một số lợi ích thu được từ việc hạn ché quyền truy cập vào cấu trúc dữ liệu đối với danh
sách các hàm đươc báo rõ ràng. Ví dụ: Bất kỳ lỗi nào khiến Date nhận một giá trị bất hợp
pháp (ví dụ: ngày 36 tháng 12 năm 2016) mà nguyên nhân bởi code của hàm thành phần.
Điều này có thể cho biết rằng đây là giai đoạn đầu tiên của quá trình debug được được
hoàn thành trước khi chương trình có thể chạy được. Đây là một trường hợp đặc biệt của
nhận xét chung rằng bất kỳ thay đổi nào đối với sự thay đổi của Date đều có thể và phải
được thực hiện bằng các sự thay dổi đối với các thành phần của nó. Đặc biệt, nếu ta thay
đổi nguyên mẫu hàm của một lớp, chúng ta chỉ cần thay đổi hàm thành viên để tận dụng
dược sự tích hợp của nguyên mẫu hàm mới.

Việc bảo vệ dữ liệu một cách private dựa trên việc hạn chế sử dụng tên riêng của các
thành viên trong lớp đó. Do đó, nó có thể bị truy cập bằng thao tác địa chỉ và chuyển đổi
kiểu. Nhưng điều này tất nhiên là một sự gian lận. C++ bảo vệ chống lại sự cố thay vì cố
ý pháp vỡ (gian lận). Chỉ phần cứng mới có thể cung cấp khả năng bảo vệ hoàn hảo chống

11
lại việc sử dụng ác ý một ngôn ngữ đa dụng và thậm chí điều đó khó có thể thực hiện
được trong các hệ thống thực tế.

16.2.4 class and struct

Cấu trúc:

class X { ... };

được gọi là định nghĩa lớp; nó định nghĩa một kiểu X.

Theo định nghĩa, struct là một lớp trong đó các thành viên được mặc định là public; đó là,

struct S { /* ... */ };

chỉ đơn giản là viết tắt của

class S { public: /* ... */ };

Hai định nghĩa này của S có thể thay thế cho nhau mặc dù thông thường sẽ chỉ sử dụng
một kiểu. Bạn sử dụng phong cách nào là tùy thuộc vào hoàn cảnh và sở thích.

Theo mặc đinh, các thành phần của một lớp là riêng tư như sau:

class Date1 {

int d, m, y; // private là mặc định

public:

Date1(int dd, int mm, int yy);

void add_year(int n);

};

Tuy nhiên, ta có thể sử dụng các công cụ xác định quyền truy cập private: để nói rằng
phần sau đây là private cũng giống như public:

struct Date2 {

private:
12
int d, m, y;

public:

Date2(int dd, int mm, int yy);

void add_year(int n); // add n years

};

Ở đây Date1 và Date2 là tương đương nhau.

Không bắt buộc phải khai báo dữ liệu trước trong một lớp. Trên thực tế, thường thì hợp lý
nhất khi đặt các dữ liệu private ở cuối cùng để nhấn mạnh các chức năng đưuọc cung cấp
bởi các nguyên mẫu hàm ở trên.Ví dụ:

class Date3 {

public:

Date3(int dd, int mm, int yy);

void add_year(int n); // add n years

private:

int d, m, y;

};

Khi code thực tế, cả public và private thường nhiều hơn các ví dụ hướng dẫn ở đây. Các
chỉ định truy cập có thể được sử dụng nhiều lần trong một class duy nhất. Ví dụ:

class Date4 {

public:

Date4(int dd, int mm, int yy);

private:

int d, m, y;
13
public:

void add_year(int n); // add n years

};

Tuy nhiên, có nhiều hơn một lần public, nhưng trong Date 4 có vẻ hơi lộn xộn và có thể
ảnh hưởng đến bố cục của đối tượng.

16.2.5 Khởi tạo

Việc sử dụng các hàm như init () để cung cấp khởi tạo cho các đối tượng lớp là không phù
hợp và dễ xảy ra lỗi. Bởi vì không có chỗ nào nói rằng một đối tượng phải được khởi tạo,
một lập trình viên có thể quên làm như vậy - hoặc làm như vậy hai lần (thường cho kết
quả thảm hại như nhau). Một cách tiếp cận tốt hơn là cho phép lập trình viên khai báo một
hàm với mục đích rõ ràng là khởi tạo các đối tượng. Bởi vì một hàm như vậy xây dựng
các giá trị của một kiểu nhất định, nó được gọi là một hàm tạo. Một phương thức khởi tạo
được nhận dạng bằng cách có cùng tên với chính lớp đó. Ví dụ:

class Date {

int d, m, y;

public:

Date(int dd, int mm, int yy); // khởi tạo

// ...

};

Khi một lớp có một phương thức khởi tạo, tất cả các đối tượng của lớp đó sẽ được khởi
tạo. Nếu hàm tạo yêu cầu các giá trị, các giá trị này phải được cung cấp:

Date today = Date(23,6,1983);

Date xmas(25,12,1990); // dạng viết tắt

Date my_bir thday; // lỗi: Thiếu giá trị khởi tạo

14
Date release1_0(10,12); // lỗi: Thiếu giá trị thứ 3(yy)

Khi khởi tạo có thế sử dụng kí hiệu {} thay thế:

Date today = Date {23,6,1983};

Date xmas {25,12,1990}; // dạng viết tắt

Date release1_0 {10,12}; // lỗi: Thiếu giá trị thứ 3 (yy)

Có một lời khuyên cho bạn là nên dùng {} thay cho () vì nó sẽ cụ thể những gì đang thực
hiện (khởi tạo), nhằm tránh một số lỗi tiềm ẩn. Có những trường hợp bắt buộc phải dùng
() những rất hiếm. Có nhiều cách khởi tạo các đối tượng của một kiểu. Ví dụ:

class Date {

int d, m, y;

public:

// ...

Date(int, int, int); // ngày, tháng, năm

Date(int, int); // ngày, tháng, năm của hôm nay

Date(int); // ngày, tháng và năm của hôm nay

Date(); // mặc định Date: hôm nay

Date(const char∗); // Date được thể hiện thông qua chuỗi

};

Các hàm tạo tuân theo các quy tắc nạp chồng giống như các hàm thông thường. Miễn là
các hàm tạo có đủ khác biệt về kiểu đối số của chúng, trình biên dịch có thể chọn đúng để
sử dụng:

Date today {4}; // 4, today.m, today.y

Date july4 {"July 4, 1983"};

15
Date guy {5,11}; // 5, 11, today.y

Date now; // mặc định khởi tạo như hôm nay

Date start {}; // mặc định khởi tạo như hôm nay

16.2.6 Khởi tạo tường minh

Theo mặc định, một hàm tạo được gọi bởi một đối số hoạt động như một chuyển đổi
ngầm định từ kiểu đối số sang kiểu của nó. Ví dụ:

complex<double> d {1}; // d== {1,0} (§5.6.2)

Những chuyển đổi ngầm như vậy có thể cực kỳ hữu ích. Số phức là một ví dụ: nếu chúng
ta bỏ đi phần ảo, chúng ta sẽ nhận được một số phức trên trục thực. Đó chính xác là
những gì toán học yêu cầu. Tuy nhiên, trong nhiều trường hợp, những chuyển đổi như vậy
có thể là một nguồn gây nhầm lẫn và sai sót đáng kể. Thử xét với ví dụ Date:

void my_fct(Date d);

void f()

Date d {15}; // thỏa mãn: d nhận giá trị {15,today.m,today.y}

// ...

my_fct(15); // không rõ ràng

d = 15; // không rõ ràng

// ...

Thực tế, điều này không rõ ràng. Không có mỗi liên hệ logic nào giữa số 15 với Date.

May mắn là chúng ta có thể chỉ định một hàm không được sử dụng như một chuyển đổi
ẩn. Một hàm khởi tạo được khai báo với từ khóa tường minh rõ ràng chỉ có thể được sử
dụng để khởi tạo và chuyển đổi rõ ràng:
16
class Date {

int d, m, y;

public:

explicit Date(int dd =0, int mm =0, int yy =0);

// ...

};

Date d1 {15}; // OK: rõ ràng

Date d2 = Date{15}; // OK: rõ ràng

Date d3 = {15}; // lỗi : = Khởi tạo không thể thực hiện ngầm

Date d4 = 15; // lỗi : = Khởi tạo không thể thực hiện ngầm

void f()

my_fct(15); // lỗi : truyền đối số không thể thực hiện ngầm

my_fct({15}); // lỗi:truyền đối số không thể thực hiện ngầm


my_fct(Date{15}); // OK: rõ ràng

// ...

Một khởi tạo với dâu “=” được coi là một sự khởi tạo sao chép.

16.2.7 Khởi tạo trong class

Khi ta sử dụng một số hàm khởi tạo, việc khởi tạo thành phần có thể lặp lại. Ví dụ:

class Date {

int d, m, y;
17
public:

// ...

Date(int, int, int); // ngày, tháng, năm

Date(int, int); // ngày, tháng, năm của hôm nay

Date(int); // ngày, tháng và năm của hôm nay

Date(); // mặc định Date: hôm nay

Date(const char∗); // Date được thể hiện thông qua chuỗi

};

Có thể giải quyết vấn đề đó bằng cách khai báo các đối số mặc định để giảm số lượng
hàm tạo. Ngoài ra, chúng ta có thể thêm trình khởi tạo vào các biến trong class:

class Date {

int d {today.d};

int m {today.m};

int y {today.y};

public:

Date(int, int, int);

Date(int, int);

Date(int);

Date();

Date(const char∗);

// ...

Bây giờ, mỗi hàm tạo có d, m và y được khởi tạo trừ khi chính nó thực hiện nó. Ví dụ:
18
Date::Date(int dd)

:d{dd}

// Kiểm tra Date có hợp lệp hay không

Điều này tương đương với:

Date::Date(int dd)

:d{dd}, m{today.m}, y{today.y}

// Kiểm tra Date có hợp lệp hay không

16.2.8 Định nghĩa hàm trong lớp

Một hàm thành viên được định nghĩa trong định nghĩa lớp - thay vì chỉ được khai báo ở
đó - được coi là một hàm thành viên nội tuyến. Có nghĩa là, định nghĩa trong lớp của các
hàm thành viên dành cho các hàm nhỏ, hiếm khi được sửa đổi, được sử dụng thường
xuyên. Giống như định nghĩa lớp mà nó là một phần, một hàm thành viên được định
nghĩa trong lớp có thể được sao chép trong một số đơn vị dịch bằng cách sử dụng
#include. Giống như bản thân lớp, ý nghĩa của hàm thành viên phải giống nhau ở bất kỳ
đâu #include.

Một thành phần có thể tham chiếu đến một thành viên khác trong lớp của mình một cách
độc lập với nơi thành viên đó được xác định. Xem xét:

class Date {

public:

void add_month(int n) { m+=n; } // cộng thêm tháng của Date

19
// ...

private:

int d, m, y;

};

Nghĩa là, các khai báo thành viên chức năng và dữ liệu là độc lập với thứ tự. Ta có thể
tương đương đã viết:

class Date {

public:

void add_month(int n) { m+=n; } // cộng thêm tháng

// ...

private:

int d, m, y;

};

inline void Date::add_month(int n) // Cộng thêm n tháng vào m

m+=n; // cộng thêm tháng

Kiểu sau này thường được sử dụng để giữ cho các định nghĩa lớp đơn giản và dễ đọc. Nó
cũng cung cấp sự phân tách văn bản về giao diện và triển khai của một lớp.

16.2.9 Khả năng thay đổi

Chúng ta có thể định nghĩa một đối tượng được đặt tên như một hằng số hoặc một biến.
Nói cách khác, tên có thể đề cập đến một đối tượng chứa một giá trị không thể thay đổi

20
hoặc có thể thay đổi. Vì thuật ngữ chính xác có thể hơi vụng về, chúng tôi kết thúc việc đề
cập đến một số biến là hằng số hoặc ngắn gọn vẫn là biến const.

Để trở nên có tác dụng khác ngoài định nghĩa về hằng số đơn giản của các kiểu dựng sẵn,
ta phải có khả năng xác định các hàm hoạt động trên các đối tượng hằng của kiểu do
người dùng định nghĩa. Đối với các hàm tự do có nghĩa là các hàm nhận const T & đối số.
Đối với các lớp, điều đó có nghĩa là chúng ta phải có khả năng xác định các hàm thành
viên hoạt động trên các đối tượng const.

16.2.9.1 Hàm thành viên hằng

Date được định nghĩa cho đến nay cung cấp các hàm thành viên để cung cấp giá trị cho
Date. Nhưng ta chưa có cách kiểm tra giá trị của Date. Có thể dễ dàng sửa lỗi này bằng
cách thêm các chức năng đọc ngày, tháng và năm:

class Date {

int d, m, y;

public:

int day() const { return d; }

int month() const { return m; }

int year() const;

void add_year(int n);

// ...

};

const sau danh sách đối số (trống) trong khai báo hàm chỉ ra rằng các hàm này không sửa
đổi trạng thái của Date.

Đương nhiên, trình biên dịch sẽ phát hiện những vi phạm sự “const” này. Ví dụ:

int Date::year() const

21
{

return ++y; // lỗi : cố gắng thay đổi giá trị thành viên trong hàm const

Khi một hàm thành phần const được định nghĩa bên ngoài lớp của nó, hậu tố const là bắt
buộc:

int Date::year() // lỗi : thiếu const trong loại hàm thành viên

return y;

Nói cách khác, const là một phần của kiểu Date :: day (), Date :: month () và Date :: year
(). Một hàm thành viên const có thể được gọi cho cả các đối tượng const và không phải
const, trong khi một hàm thành viên không phải const chỉ có thể được gọi cho các đối
tượng không phải const. Ví dụ:

void f(Date& d, const Date& cd)

int i = d.year(); // OK

d.add_year(1); // OK

int j = cd.year(); // OK

cd.add_year(1); // lỗi : không thể thay đổi giá trị của một const Date

16.2.9.2 Hằng số vật lý và logic

Đôi khi, một hàm thành viên về mặt logic là const, nhưng nó vẫn cần thay đổi giá trị của
một thành viên. Có nghĩa là, đối với người dùng, hàm dường như không thay đổi trạng
thái của đối tượng của nó, nhưng một số chi tiết mà người dùng không thể quan sát trực
22
tiếp được cập nhật. Điều này thường được gọi là hằng số logic. Ví dụ, lớp Date có thể có
một hàm trả về biểu diễn chuỗi. Việc xây dựng biểu diễn này có thể là một hoạt động
tương đối tốn kém. Do đó, bạn nên giữ một bản sao để các yêu cầu lặp đi lặp lại sẽ chỉ trả
lại bản sao đó, trừ khi giá trị của Date đã được thay đổi. Lưu trữ các giá trị như vậy phổ
biến hơn đối với các cấu trúc dữ liệu phức tạp hơn, nhưng hãy xem cách có thể đạt được
giá trị đó cho một Date:

class Date {

public:

// ...

string string_rep() const; // biểu diễn chuỗi

private:

bool cache_valid;

string cache;

void compute_cache_value();

// ...

};

Theo người dùng, string_rep không thay đổi trạng thái Date của nó, vì vậy rõ ràng nó phải
là một hàm thành viên const. Mặt khác, các thành viên cache và cache_valid thỉnh thoảng
phải thay đổi để thiết kế có ý nghĩa.

Những vấn đề như vậy có thể được giải quyết thông qua bạo lực bằng cách sử dụng ép
kiểu, ví dụ, một const_cast (§11.5.2). Tuy nhiên, cũng có những giải pháp thanh lịch hợp
lý mà không liên quan đến việc gây rối với các quy tắc loại.

16.2.9.3 Có thể thay đổi (Mutable)

Chúng ta có thể định nghĩa một thành viên của một lớp là có thể thay đổi, nghĩa là nó có
thể được sửa đổi ngay cả trong một đối tượng const:

23
class Date {

public:

// ...

string string_rep() const;

private:

mutable bool cache_valid;

mutable string cache;

void compute_cache_value() const; // fill (mutable) cache

// ...

};

Bây giờ ta có thể định nghĩa string_rep () một cách rõ ràng:

string Date::string_rep() const

if (!cache_valid) {

compute_cache_value();

cache_valid = true;

return cache;

Bây giờ ta có thể sử dụng string_rep () cho cả đối tượng const và không phải const. Ví dụ:

void f(Date d, const Date cd)

24
{

string s1 = d.string_rep();

string s2 = cd.string_rep(); // OK!

// ...

16.2.9.4 Tính đột biến thông qua hướng dẫn

Khai báo một thành phần có thể thay đổi là thích hợp nhất khi chỉ một phần nhỏ của biểu
diễn của một đối tượng nhỏ được phép thay đổi. Các trường hợp phức tạp hơn thường
được xử lý tốt hơn bằng cách đặt dữ liệu thay đổi vào một đối tượng riêng biệt và truy cập
nó một cách gián tiếp. Nếu kỹ thuật đó được sử dụng, ví dụ string-with-cache sẽ trở
thành:

struct cache {

bool valid;

string rep;

};

class Date {

public:

// ...

string string_rep() const;

private:

cache∗ c; // khởi tạo

void compute_cache_value() const; // thêm những gì cache đề cập đến

// ...

25
};

string Date::string_rep() const

if (!c−>valid) {

compute_cache_value();

c−>valid = true;

return c−>rep;

16.2.10 Tự quy (Self-Reference)

Các hàm cập nhật trạng thái add_year (), add_month () và add_day ()được định nghĩa
không trả về giá trị. Đối với một tập hợp các hàm cập nhật liên quan như vậy, thường hữu
ích khi trả về một tham chiếu đến đối tượng được cập nhật để các hoạt động có thể được
xâu chuỗi. Ví dụ, ta muốn viết:

void f(Date& d)

// ...

d.add_day(1).add_month(1).add_year(1);

// ...

để thêm một ngày, một tháng và một năm vào d. Để làm điều này, mỗi hàm phải được
khai báo để trả về một tham chiếu đến Date:

class Date {

26
// ...

Date& add_year(int n); // add n years

Date& add_month(int n); // add n months

Date& add_day(int n); // add n days

};

Mỗi hàm thành phần (không phải static) biết đối tượng nào được gọi và có thể tham chiếu
đến nó một cách rõ ràng. Ví dụ:

Date& Date::add_year(int n)

if (d==29 && m==2 && !leapyear(y+n)) {

d = 1;

m = 3;

y += n;

return ∗this;

∗this đề cập đến đối tượng mà hàm thành phần được gọi.

Trong một hàm thành phần (không phải static), từ khóa này là một con trỏ đến đối tượng
mà hàm được gọi. Trong một hàm thành viên không phải hằng số của lớp X, kiểu của
hàm này là X ∗. Tuy nhiên, điều này được coi là không hợp lệ, vì vậy không thể lấy địa
chỉ của điều này hoặc gán cho điều này. Trong một hàm thành viên const của lớp X, kiểu
này là const X ∗ để ngăn việc sửa đổi chính đối tượng.

Hầu hết việc sử dụng điều này là ẩn. Đặc biệt, mọi tham chiếu đến thành phần (không
phải static) từ bên trong một lớp dựa vào việc sử dụng ngầm định điều này để lấy thành
27
viên của đối tượng thích hợp. Ví dụ: hàm add_year có thể tương đương, nhưng thật tệ,
hav e được định nghĩa như thế này:

Date& Date::add_year(int n)

if (this−>d==29 && this−>m==2 && !leapyear(this−>y+n)) {

this−>d = 1;

this−>m = 3;

this−>y += n;

return ∗this;

Một cách sử dụng rõ ràng và phổ biến this là trong thao tác danh sách liên kết. Ví dụ:

struct Link {

Link∗ pre;

Link∗ suc;

int data;

Link∗ insert(int x) // inser t x before this

return pre = new Link{pre ,this,x};

void remove() // remove and destroy this

{
28
if (pre) pre−>suc = suc;

if (suc) suc−>pre = pre;

delete this;

// ...

};

16.2.11 Quyền truy cập thành phần

Một thành viên của lớp X có thể được truy cập bằng cách áp dụng toán tử “.” (dấu chấm)
cho một đối tượng của lớp X hoặc bằng cách áp dụng toán tử “->” (mũi tên) cho một con
trỏ đến một đối tượng của lớp X. Ví dụ:

struct X {

void f();

int m;

};

void user(X x, X∗ px)

m = 1; // error : there is no m in scope

x.m = 1; // OK

x−>m = 1; // error : x is not a pointer

px−>m = 1; // OK

px.m = 1; // error : px is a pointer

29
Rõ ràng, có một chút dư thừa ở đây: trình biên dịch biết liệu một tên liên quan đến X hay
X ∗, vì vậy một toán tử duy nhất sẽ là đủ. Tuy nhiên, một lập trình viên có thể bị nhầm
lẫn, vì vậy từ những ngày đầu tiên của C, quy tắc đã được sử dụng các toán tử riêng biệt.

Từ bên trong một lớp không cần toán tử. Ví dụ:

void X::f()

m = 1; // OK: ‘‘this->m = 1;’’

Đó là, một tên thành viên không đủ điều kiện hoạt động như thể nó đã được đặt trước bởi
this−>. Lưu ý rằng một hàm thành viên có thể tham chiếu đến tên của một thành viên
trước khi nó được khai báo:

struct X {

int f() { return m; } // fine: return this X’s m

int m;

};

Nếu chúng ta muốn tham chiếu đến một thành viên nói chung, thay vì một thành viên của
một đối tượng cụ thể, chúng ta đủ điều kiện bởi tên lớp theo sau là ::. Ví dụ:

struct S {

int m;

int f();

static int sm;

};

int X::f() { return m; } // X’s f

30
int X::sm {7}; // X’s static member sm (§16.2.12)

int (S::∗) pmf() {&S::f}; // X’s member f

16.2.12 Thành phần [Static]

Đây là một cách làm lại bảo toàn ngữ nghĩa của các giá trị phương thức khởi tạo mặc định
cho Date mà không gặp các vấn đề bắt nguồn từ việc phụ thuộc vào toàn cục:

class Date {

int d, m, y;

static Date default_date;

public:

Date(int dd =0, int mm =0, int yy =0);

// ...

static void set_default(int dd, int mm, int yy); // set default_date to
Date(dd,mm,yy)

};

Bây giờ chúng ta có thể xác định hàm tạo Date để sử dụng default_date như sau:

Date::Date(int dd, int mm, int yy)

d = dd ? dd : default_date .d;

m = mm ? mm : default_date .m;

y = yy ? yy : default_date .y;

// ... check that the Date is valid ...

31
Sử dụng set_default (), chúng ta có thể thay đổi ngày mặc định khi thích hợp. Một thành
viên tĩnh có thể được gọi như bất kỳ thành viên nào khác. Ngoài ra, một thành phần static
có thể được tham chiếu mà không cần đề cập đến một đối tượng. Thay vào đó, tên của nó
đủ điều kiện theo tên của lớp của nó. Ví dụ:

void f()

Date::set_default(4,5,1945); // static Date bằng set_default()

Nếu được sử dụng, một thành phần static - một hàm hoặc một thành viên dữ liệu - phải
được định nghĩa ở đâu đó. Từ khóa static không được lặp lại trong định nghĩa của một
thành phần static. Ví dụ:

Date Date::default_date {16,12,1770}; // định nghĩa Date :: default_date

void Date::set_default(int d, int m, int y) // định nghĩa Date::set_default

default_date = {d,m,y}; // gán giá trị mới cho default_date

16.2.13 Loại thành phần

Các kiểu và các loại kiểu khác có thể là thành phần của một lớp. Ví dụ:

template<typename T>

class Tree {

using value_type = T; // member alias

enum Policy { rb, splay, treeps }; // member enum

class Node { // member class

32
Node∗ right;

Node∗ left;

value_type value;

public:

void f(Tree∗);

};

Node∗ top;

public:

void g(const T&);

// ...

};

Một lớp thành phần (thường được gọi là lớp lồng nhau) có thể tham chiếu đến các kiểu và
các thành phần static của lớp bao quanh nó. Nó chỉ có thể tham chiếu đến các thành phần
không phải static khi nó được cung cấp một đối tượng của lớp bao quanh để tham chiếu
đến. Để tránh đi vào sự phức tạp của cây nhị phân, ví dụ ‘‘ f () và g () ’’.

Một lớp lồng nhau có quyền truy cập vào các thành phần của lớp bao quanh nó, thậm chí
đến các thành phần private (giống như một hàm thành viên có), nhưng không có khái
niệm về đối tượng hiện tại của lớp bao quanh. Ví dụ:

template<typename T>

void Tree::Node::f(Tree∗ p)

top = right; // lỗi: không có đối tượng của loại Tree được chỉ định

p−>top = right; // OK

33
value_type v = left−>value; // OK: value_type không được liên kết
với một đối tượng

Một lớp không có bất kỳ quyền truy cập đặc biệt nào đối với các thành viên của lớp lồng
nhau của nó. Ví dụ:

template<typename T>

void Tree::g(Tree::Node∗ p)

value_type val = right−>value; // lỗi: không có đối tượng thuộc


loại Tree :: Node

value_type v = p−>right−>value; // lỗi : Node::right là private

p−>f(this); // OK

Các lớp thành phần là một sự tiện lợi về mặt ký hiệu hơn là một tính năng có tầm quan
trọng cơ bản. Mặt khác, các loại thành phần khác rất quan trọng như là cơ sở của kỹ thuật
lập trình chung dựa trên các kiểu liên kết.

16.3 Lớp cụ thể: Concrete Class 

Các lớp, mà có thể được sử dụng để khởi tạo đối tượng, được gọi là Concrete Class trong
C++. Việc có sẵn các nguyên mẫu hàm cho phép ta:

 Đặc objects trên stack, trong bộ nhớ cấp phát tĩnh và trong các object khác.
 Sao chép và di chuyển các object.
 Tham chiếu trực tiếp đến tên các object (thay vì truy cập thông qua con trỏ và tham
chiếu)

Điều này làm cho các Concrete Class trở nên đơn giản để lập luận và trình biên dịch dễ
dàng tạo ra mã tối ưu. Do đó, ta thích các lớp cụ thể cho các loại nhỏ, được sử dụng

34
thường xuyên và quan trọng về hiệu suất, chẳng hạn như số phức, con trỏ thông minh và
vùng chứa.

Mục đích rõ ràng ban đầu của C ++ là hỗ trợ rất tốt việc định nghĩa và sử dụng hiệu quả
các kiểu do người dùng xác định. Chúng là một nền tảng của lập trình tao nhã. Như
thường lệ, cái đơn giản và trần tục có ý nghĩa thống kê hơn nhiều so với cái phức tạp và
cầu kỳ. Theo cách này, chúng ta hãy xây dựng một lớp Date tốt hơn:

namespace Chrono {

enum class Month { jan=1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov,
dec };

class Date {

public: // public interface:

class Bad_date { }; // exception class

explicit Date(int dd ={}, Month mm ={}, int yy ={}); // {} means


‘‘pick a default’’

// nonmodifying functions for examining the Date:

int day() const;

Month month() const;

int year() const;

string string_rep() const; // string representation

void char_rep(char s[], in max) const; // C-style string representation

// (modifying) functions for changing the Date:

Date& add_year(int n); // add n years

Date& add_month(int n); // add n months

Date& add_day(int n); // add n days


35
private:

bool is_valid(); // check if this Date represents a date

int d, m, y; // representation

};

bool is_date(int d, Month m, int y); // true for valid date

bool is_leapyear(int y); // true if y is a leap year

bool operator==(const Date& a, const Date& b);

bool operator!=(const Date& a, const Date& b);

const Date& default_date(); // the default date

ostream& operator<<(ostream& os, const Date& d); // print d to os

istream& operator>>(istream& is, Date& d); // read Date from is into d

} // Chrono

16.3.1 Hàm thành phần

Đương nhiên, một triển khai cho mỗi chức năng thành viên phải được cung cấp ở đâu đó.
Ví dụ:

Date::Date(int dd, Month mm, int yy)

:d{dd}, m{mm}, y{yy}

if (y == 0) y = default_date().year();

if (m == Month{}) m = default_date().month();

if (d == 0) d = default_date().day();

if (!is_valid()) throw Bad_date();


36
}

Hàm tạo kiểm tra xem dữ liệu được cung cấp có biểu thị Ngày hợp lệ hay không. Nếu
không, chẳng hạn, đối với {30, Month :: feb, 1994}, nó ném ra một ngoại lệ, cho biết rằng
đã xảy ra sự cố. Nếu dữ liệu được cung cấp có thể chấp nhận được, thì quá trình khởi tạo
rõ ràng đã được thực hiện. Khởi tạo là một hoạt động tương đối phức tạp vì nó liên quan
đến việc xác nhận dữ liệu. Điều này là khá điển hình. Mặt khác, khi Ngày đã được tạo, nó
có thể được sử dụng và sao chép mà không cần kiểm tra thêm. Nói cách khác, hàm tạo
thiết lập giá trị bất biến cho lớp (trong trường hợp này, nó biểu thị một ngày hợp lệ). Các
chức năng thành viên khác có thể dựa vào sự bất biến đó và phải duy trì nó. Kỹ thuật thiết
kế này có thể đơn giản hóa mã rất nhiều.

Đã cân nhắc đặt hàm xác thực is_valid () ở chế độ công khai. Tuy nhiên, nhận thấy mã
người dùng tạo ra phức tạp hơn và kém mạnh mẽ hơn mã dựa vào việc bắt ngoại lệ:

void fill(vector<Date>& aa)

while (cin) {

Date d;

try {

cin >> d;

catch (Date::Bad_date) {

// ... my error handling ...

continue;

aa.push_back(d); // see §4.4.2

}
37
}

Tuy nhiên, việc kiểm tra xem tập giá trị {d, m, y} có phải là ngày hợp lệ không phải là
một phép tính phụ thuộc vào biểu diễn của Ngày, vì vậy tôi đã triển khai is_valid () về
mặt hàm trợ giúp:

bool Date::is_valid()

return is_date(d,m,y);

Tại sao lại có cả is_valid () và is_date ()? Trong ví dụ đơn giản này, chúng ta có thể quản
lý chỉ với một, nhưng tôi có thể tưởng tượng các hệ thống trong đó is_date () kiểm tra
xem một bộ (d, m, y) đại diện cho một ngày hợp lệ và is_valid () thực hiện kiểm tra bổ
sung về việc liệu ngày đó có thể được trình bày một cách hợp lý hay không. Ví dụ:
is_valid () có thể từ chối các ngày từ trước khi lịch hiện đại được sử dụng phổ biến.

Như thường thấy đối với các loại cụ thể đơn giản như vậy, các định nghĩa về các hàm
thành viên của Date khác nhau giữa mức nhỏ và không quá phức tạp. Ví dụ:

inline int Date::day() const

return d;

Date& Date::add_month(int n)

if (n==0) return ∗this;

if (n>0) {

int delta_y = n/12; // number of whole years

38
int mm = static_cast<int>(m)+n%12; // number of months ahead

if (12 < mm) { // note: dec is represented by 12

++delta_y;

mm −= 12;

// ... handle the cases where the month mm doesn’t have day d ...

y += delta_y;

m = static_cast<Month>(mm);

return ∗this;

// ... handle negative n ...

return ∗this;

Lưu ý rằng việc gán và khởi tạo sao chép được cung cấp theo mặc định. Ngoài ra, Date
không cần trình hủy vì Date không sở hữu tài nguyên và không yêu cầu dọn dẹp khi nó ra
ngoài khỏi phạm vi cho phép.

16.3.2 Hàm helper

Thông thường, một lớp có một số hàm được liên kết với nó mà không cần được xác định
trong chính lớp đó vì chúng không cần quyền truy cập trực tiếp vào biểu diễn. Ví dụ:

int diff(Date a, Date b); // number of days in the range [a,b) or [b,a)

bool is_leapyear(int y);

bool is_date(int d, Month m, int y);

const Date& default_date();


39
Date next_weekday(Date d);

Date next_saturday(Date d);

Việc xác định các hàm như vậy trong bản thân lớp sẽ làm phức tạp giao diện lớp và tăng
số lượng các hàm có khả năng cần được kiểm tra khi xem xét sự thay đổi đối với biểu
diễn.

Làm thế nào những hàm như vậy '' kết hợp '' với lớp Date? Trong C ++ đầu tiên, cũng như
trong C, các khai báo của chúng đơn giản được đặt trong cùng một tệp như khai báo của
lớp Date. Người dùng cần Ngày sẽ cung cấp tất cả chúng bằng cách bao gồm tệp xác định
giao diện. Ví dụ:

#include "Date.h"

Ngoài ra (hoặc cách khác), chúng ta có thể làm cho liên kết rõ ràng bằng cách bao bọc lớp
và các chức năng trợ giúp của nó trong một không gian tên:

namespace Chrono { // facilities for dealing with time

class Date { /* ... */};

int diff(Date a, Date b);

bool is_leapyear(int y);

bool is_date(int d, Month m, int y);

const Date& default_date();

Date next_weekday(Date d);

Date next_saturday(Date d);

// ...

40
Không gian tên Chrono đương nhiên cũng sẽ chứa các lớp liên quan, chẳng hạn như Time
và Stopwatch, và các chức năng trợ giúp của chúng. Việc sử dụng một không gian tên để
chứa một lớp đơn lẻ thường là một sự phức tạp quá mức dẫn đến sự bất tiện.

Đương nhiên, hàm trợ giúp phải được định nghĩa ở đâu đó:

bool Chrono::is_date(int d, Month m, int y)

int ndays;

switch (m) {

case Month::feb:

ndays = 28+is_leapyear(y);

break;

case Month::apr: case Month::jun: case Month::sep: case Month::nov:

ndays = 30;

break;

case Month::jan: case Month::mar: case Month::may: case Month::jul:

case Month::aug: case Month::oct: case Month::dec:

ndays = 31;

break;

default:

return false;

return 1<=d && d<=ndays;

41
}

Tôi đang cố tình nhầm lẫn ở đây. Một Tháng không nên nằm ngoài phạm vi 1 đến 12,
nhưng nó có thể xảy ra, vì vậy tôi cần phải kiểm tra.

Cuối cùng thì default_date rắc trở thành:

const Date& Chrono::default_date()

static Date d {1,Month::jan,1970};

return d;

16.3.3 Nạp chồng toán tử: Overloaded Operators

Thường hữu ích khi thêm các chức năng để kích hoạt ký hiệu thông thường. Ví dụ: toán
tử == () xác định toán tử bình đẳng, ==, hoạt động cho Date:

inline bool operator==(Date a, Date b) // equality

return a.day()==b.day() && a.month()==b.month() && a.year()==b.year();

Tương tự cho:

bool operator!=(Date, Date); // inequality

bool operator<(Date, Date); // less than

bool operator>(Date, Date); // greater than

// ...

Date& operator++(Date& d) { return d.add_day(1); } // increase Date by one day

42
Date& operator−−(Date& d) { return d.add_day(−1); } // decrease Date by one day

Date& operator+=(Date& d, int n) { return d.add_day(n); } // add n days

Date& operator−=(Date& d, int n) { return d.add_day(−n); } // subtract n days

Date operator+(Date d, int n) { return d+=n; } // add n days

Date operator−(Date d, int n) { return d+=n; } // subtract n days

ostream& operator<<(ostream&, Date d); // output d

istream& operator>>(istream&, Date& d); // read into d

Đối với Date, những toán tử này có thể được coi là những tiện ích đơn thuần. Tuy nhiên,
đối với nhiều loại - chẳng hạn như số phức, vectơ, và các đối tượng giống hàm - việc sử
dụng các toán tử thông thường rất cố gắng trong tâm trí của mọi người rằng định nghĩa
của họ gần như là bắt buộc. Nạp chồng toán tử được đề cập trong Chương 18.

Lưu ý rằng việc gán và khởi tạo sao chép được cung cấp theo mặc định.

16.3.4 Tầm quan trọng của các lớp cụ thể

Tôi gọi các kiểu đơn giản do người dùng định nghĩa, chẳng hạn như Date, các kiểu cụ thể
để phân biệt chúng với các lớp trừu tượng và phân cấp lớp, và cũng để nhấn mạnh sự
giống nhau của chúng với các kiểu dựng sẵn như int và char. Các lớp cụ thể được sử dụng
giống như các kiểu tích hợp sẵn. Các kiểu cụ thể còn được gọi là kiểu giá trị và lập trình
hướng giá trị sử dụng của chúng. Mô hình sử dụng của chúng và '' triết lý '' đằng sau thiết
kế của chúng hoàn toàn khác với những gì thường được gọi là lập trình hướng đối tượng.

Mục đích của loại cụ thể là làm tốt và hiệu quả một việc duy nhất, tương đối đơn giản. Nó
thường không nhằm mục đích cung cấp cho người dùng các phương tiện để sửa đổi hành
vi của một loại cụ thể. Đặc biệt, các loại cụ thể không nhằm mục đích hiển thị hành vi đa
hình thời gian chạy.

Với một trình biên dịch hợp lý tốt, một lớp cụ thể như Date không phải chịu tác nhân ẩn
theo thời gian hoặc không gian. Đặc biệt, không cần chuyển hướng thông qua con trỏ để
truy cập vào các đối tượng của các lớp cụ thể và không có dữ liệu ‘‘housekeeping’’ nào
được lưu trữ trong các đối tượng của các lớp cụ thể. Kích thước của một loại cụ thể được
43
biết trước tại thời điểm biên dịch để các đối tượng có thể được phân bổ trên ngăn xếp thời
gian chạy (nghĩa là không có hoạt động lưu trữ miễn phí). Bố cục của một đối tượng được
biết trước tại thời điểm biên dịch để nội tuyến của các hoạt động đạt được một cách đáng
kể. Tương tự, khả năng tương thích bố cục với các ngôn ngữ khác, chẳng hạn như C và
Fortran, không cần học lại từ đầu.

Thiếu kiểu cụ thể có thể dẫn đến các chương trình rối và lãng phí thời gian khi mỗi lập
trình viên viết mã để thao tác trực tiếp cấu trúc dữ liệu '' đơn giản và thường được sử dụng
'' được biểu diễn dưới dạng tổng hợp đơn giản của các kiểu tích hợp sẵn.

16.4 Lời khuyên

[1]. Biểu diễn các khái niệm dưới dạng các lớp
[2]. Tách nguyên mẫu hàm của một lớp khỏi việc triển khai nó; §16.1.
[3]. Chỉ sử dụng dữ liệu công khai (struct) khi nó thực sự chỉ là dữ liệu và không có bất
biến nào có ý nghĩa đối với các thành viên dữ liệu; §16.2.4.
[4]. Định nghĩa một phương thức khởi tạo để xử lý việc khởi tạo các đối tượng; §16.2.5.
[5]. Theo mặc định, khai báo các hàm tạo một đối số rõ ràng; §16.2.6.
[6]. Khai báo một hàm thành viên giữ nguyên giá trị của đối tượng const; §16.2.9.
[7]. Loại cụ thể là loại đơn giản nhất của lớp. Nếu có thể, hãy thích một kiểu cụ thể hơn
các lớp phức tạp hơn và các cấu trúc dữ liệu đơn giản; §16.3.
[8]. Chỉ tạo một hàm thành thành viên nếu nó cần quyền truy cập trực tiếp vào biểu diễn
của một lớp; §16.3.2.
[9]. Sử dụng một không gian tên để làm cho mối liên kết giữa một lớp và các chức năng
trợ giúp của nó trở nên rõ ràng; §16.3.2.
[10]. Làm cho một hàm thành phần giữ nguyên giá trị của đối tượng của nó thành một hàm
thành phần const; §16.2.9.1.
[11]. Làm cho một hàm cần truy cập vào biểu diễn của một lớp nhưng không cần được gọi
cho một đối tượng cụ thể thành một hàm thành phần static; §16.2.12.

44
CHƯƠNG 18: NẠP CHỒNG TOÁN TỬ
18.1: Giới thiệu
-Mọi lĩnh vực kỹ thuật - và hầu hết các lĩnh vực phi kỹ thuật đều phát triển ký hiệu viết tắt
thông thường để thuận tiện cho việc trình bày và thảo luận liên quan đến các khái niệm
được sử dụng thường xuyên.
-Ví dụ: x+y∗z
-Hầu hết các con-cept mà các toán tử được sử dụng thông thường không phải là các kiểu
tích hợp sẵn trong C ++, vì vậy chúng phải được trình bày dưới dạng các kiểu do người
dùng định nghĩa.
-Lập trình viên định nghĩa phức hợp :: operator + () và complex : operator ∗ () để cung
cấp ý nghĩa cho + và ∗, tương ứng.
class complex { // very simplified complex
double re, im;
public:
complex(double r, double i) :re{r}, im{i} { }
complex operator+(complex);
complex operator∗(complex);
};
- Ví dụ: nếu b và c thuộc loại phức tạp, b + c có nghĩa là b. Điều hành + (c). Chúng ta có
thể biết cách giải thích gần đúng thông thường của các biểu thức phức tạp:
void f()
{
complex a = complex{1,3.1};
complex b {1.2, 2};
complex c {b};
a = b+c;
b = b+c∗a;
c=a∗b+complex(1,2);
}
-Các quy tắc ưu tiên thông thường được giữ nguyên, vì vậy câu lệnh thứ hai có nghĩa là b
= b + (c ∗ a), không phải b = (b + c) ∗ a.Lưu ý rằng ngữ pháp C ++ được viết để ký hiệu
{} chỉ có thể được sử dụng cho các trình khởi tạo và ở phía bên phải của bài tập:
void g(complex a, complex b)
{
45
a = {1,2}; // OK: right hand side of assignment
a += {1,2}; // OK: right hand side of assignment
b = a+{1,2}; // syntax error
b = a+complex{1,2}; // OK
g(a,{1,2});
{a,b} = {b,a}; // syntax error
}
-Nhiều ứng dụng rõ ràng nhất của việc nạp chồng toán tử là đối với các kiểu số. Tuy
nhiên,tính hữu ích của các toán tử do người dùng xác định không bị giới hạn đối với các
kiểu số.
18.2: Các chức năng của nhà điều hành
18.2.1: Toán tử nhị phân và đơn nguyên
-Các hàm xác định ý nghĩa cho các toán tử sau có thể được khaibáo:
+ − ∗ / % ˆ &
| ̃ ! = < > +=
= ∗ = /= %= ˆ= &= |=
<< >> >>= <<= == != <=
>= && || ++ −− −>∗ ,
−> [] () new new[] delete delete[]
-Người dùng không thể xác định các toán tử sau:
:: phân giải phạm vi .
. lựa chọn thành viên .
. ∗ lựa chọn thành viên thông qua con trỏ đến thành viên .
-Không thể nạp chồng ‘‘ toán tử ’’ được đặt tên bởi vì chúng báo cáo các thông tin cơ bản
về thời hạn chọn của chúng:kích thước kích thước của đối tượng,căn chỉnh alignof của đối
tượng,typeid type_info của một đối tượng.
-Toán tử biểu thức điều kiện bậc ba không thể được nạp chồng.
- các ký tự do người dùng định nghĩa được xác định bằng cách sử
dụng ký hiệu toán tử "".
- Không thể xác định mã thông báo toán tử mới, nhưng bạn có thể sử dụng ký hiệu lệnh
gọi hàm khi tập hợp các toán tử này là không đầy đủ. Ví dụ, sử dụng pow (), không phải
∗∗
-Tên của một hàm toán tử là toán tử từ khóa được theo sau bởi chính toán tử đó.
void f(complex a, complex b)

46
{
complex c = a + b; // shor thand
complex d = a.operator+(b); // explicit call
}
18.2.1: Toán tử nhị phân và đơn nguyên
-Một toán tử nhị phân có thể được xác định bởi một hàm thành viên không tĩnh nhận một
đối số hoặc một hàm nonmember nhận hai đối số.
class X {
public:
void operator+(int);
X(int);
};
void operator+(X,X);
void operator+(X,double);
void f(X a)
{
a+1; // a.operator+(1)
1+a; // ::operator+(X(1),a)
a+1.0; // ::operator+(a,1.0)
}
-Ví dụ: người dùng không thể xác định% một bậc hoặc một bậc ba +.
class X {
public: // members (with implicit this pointer):
X∗ operator&(); // prefix unary & (address of)
X operator&(X); // binar y & (and)
X operator++(int); // postfix increment (see §19.2.4)
X operator&(X,X); // error : ter nary
X operator/(); // error : unar y /
};
operator X− (X); // tiền tố một ngôi trừ
operator X− (X, X); // nhị phân y dấu trừ
operator X −− (X &, int); // giảm hậu tố
operator X− (); // lỗi: không có toán hạng
operator X− (X, X, X); // error: ter nary

47
operator X% (X); // lỗi: unar y%
-Ý nghĩa mặc định của &&, ||, và, (dấu phẩy) liên quan đến trình tự: toán hạng đầu tiên
được đánh giá trước toán hạng thứ hai (và đối với && và || toán hạng thứ hai không phải
lúc nào cũng được đánh giá)
18.2.2: Ý nghĩa được xác định trước cho các toán tử
-Ý nghĩa của một số toán tử dựng sẵn được định nghĩa tương đương với một số kết hợp
của các toán tử khác trên cùng các đối số. Ví dụ, nếu a là int, ++ a có nghĩa là a + = 1,
điều này có nghĩa là a = a + 1.
-Các toán tử = (gán), & (address-of) và, (sequencing;) có giá trị trung bình được xác định
trước khi áp dụng cho các đối tượng lớp.
class X {
public:
// ...
void operator=(const X&) = delete;
void operator&() = delete;
void operator,(const X&) = delete;
// ...
};
void f(X a, X b)
{
a = b; // error : no operator=()
&a; // error : no operator&()
a,b; // error : no operator,()
}
18.2.3: Toán tử và các loại do người dùng xác định
-Một hàm toán tử phải là một thành viên hoặc có ít nhất một đối số của kiểu do người
dùng xác định (các hàm xác định lại toán tử mới và xóa không cần).
-Một hàm toán tử nhằm chấp nhận một kiểu dựng sẵn làm toán hạng đầu tiên của nó
không thể là hàm amember.
-Các kiểu liệt kê là các kiểu do người dùng định nghĩa để chúng ta có thể xác định các
toán tử cho chúng.
enum Day { sun, mon, tue, wed, thu, fri, sat };
Day& operator++(Day& d)
{

48
return d = (sat==d) ? sun : static_cast<Day>(d+1);
}
18.2.4: Các đối tượng đi qua
-Đối với các đối số, chúng ta có hai lựa chọn chính :
• Giá trị chuyển tiếp
• Tham khảo qua
-Hiệu suất của việc truyền và sử dụng đối số phụ thuộc vào kiến trúc máy, quy ước giao
diện trình biên và số lần một đối số được truy cập:
void Point::operator+=(Point delta); // pass-by-value
-Các đối tượng lớn hơn, chúng ta chuyển qua tham chiếu.
-Ví dụ, bởi vì một Ma trận rất có thể lớn hơn một vài từ, chúng tôi sử dụng tham chiếu
chuyển qua:
Matrix operator+(const Matrix&, const Matrix&); // pass-by-const-reference
Matrix operator+(const Matrix& a, const Matrix& b) // return-by-value
{
Matrix res {a};
return res+=b;
}
-Lưu ý rằng các toán tử trả về một trong các đối tượng đối số của chúng có thể - và
thường làm - trả về một tham chiếu.
Matrix& Matrix::operator+=(const Matrix& a) // return-by-reference
{
if (dim[0]!=a.dim[0] || dim[1]!=a.dim[1])
throw std::exception("bad Matrix += argument");
double∗ p = elem;
double∗ q = a.elem;
double∗ end = p+dim[0]∗dim[1];
while(p!=end)
∗p++ += ∗q++
return ∗this;
}
18.2.5: Toán tử trong Không gian tên
-Toán tử là một thành viên của một lớp hoặc được định nghĩa trong một số không gian tên
(có thể là không gian tên chung).

49
namespace std { // simplified std
class string {
// ...
};
class ostream {
// ...
ostream& operator<<(const char∗); // output C-style string
};
extern ostream cout;
ostream& operator<<(ostream&, const string&); // output std::string
} // namespace std
int main()
{
const char∗ p = "Hello";
std::string s = "world";
std::cout << p << ", " << s << "!\n";
}
-Tôi đã không làm cho mọi thứ từ có thể truy cập được bằng cách viết:
using namespace std; Thay vào đó, tôi đã sử dụng tiền tố std :: cho chuỗi và cout.
-Toán tử đầu ra cho chuỗi kiểu C là một thành viên của std :: ostream, vì vậy theo định
nghĩa: std::cout << p, có nghĩa: std::cout.operator<<(p). Tuy nhiên, std :: ostream không
có hàm thành viên để xuất ra một chuỗi std ::, vì vậy: std::cout << s, có nghĩa:
operator<<(std::cout,s).
-Đặc biệt, cout nằm trong không gian tên std, vì vậy std được xem xét khi tìm định nghĩa
phù hợp của <<. Theo cách đó, trình biên dịch tìm và sử dụng:
std::operator<<(std::ostream&, const std::string&)
-Hãy xem xét một toán tử nhị phân @. Nếu x thuộc loại X và y thuộc loại Y, x @ y được
giải quyết như sau:
• Nếu X là một lớp, hãy tìm toán tử @ như một thành viên của X hoặc như một thành viên
của một cơ sở của X; và
• tìm kiếm các khai báo của operator @ trong ngữ cảnh xung quanh x @ y; và
• nếu X được định nghĩa trong không gian tên N, hãy tìm các khai báo của operator @
trong N; và

50
• nếu Y được định nghĩa trong không gian tên M, hãy tìm các khai báo của toán tử @
trong M.
-Lưu ý rằng trong tra cứu nhà điều hành không có ưu tiên nào dành cho các thành viên
hơn là những người không phải là thành viên.
X operator!(X);
struct Z {
Z operator!(); // does not hide ::operator!()
X f(X x) { /* ... */ return !x; } // invoke ::operator!(X)
int f(int x) { /* ... */ return !x; } // invoke the built-in ! for ints
};
18.3: Một loại số phức
18.3.1: Các nhà điều hành thành viên và không phải thành viên
-Các toán tử chỉ đơn giản tạo ra một giá trị mới dựa trên các giá trị của các đối số của
chúng, chẳng hạn như +, sau đó được xác định bên ngoài lớp và sử dụng các toán tử thiết
yếu trong việc triển khai chúng:
class complex {
double re, im;
public:
complex& operator+=(complex a); // needs access to representation
// ...
};
complex operator+(complex a, complex b)
{
return a += b; // access representation through +=
}
-Các đối số cho toán tử + () này được truyền theo giá trị, vì vậy a + b không sửa đổi các
toán hạng của nó.
Với những khai báo này, chúng ta có thể viết:
void f(complex x, complex y, complex z)
{
complex r1 {x+y+z}; // r1 = operator+(operator+(x,y),z)
complex r2 {x}; // r2 = x
r2 += y; // r2.operator+=(y)
r2 += z; // r2.operator+=(z)

51
}
-Các toán tử gán tổng hợp như + = và ∗ = có xu hướng dễ xác định hơn so với các toán tử
'' đơn giản '' của chúng + và ∗, hiệu quả thời gian chạy được cải thiện bằng cách loại bỏ
các biến tạm thời cần thiết. Ví dụ:
inline complex& complex::operator+=(complex a)
{
re += a.re;
im += a.im;
return ∗this;
}
18.3.2: Số học chế độ hỗn hợp
-Để đối phó với 2 + z, trong đó z là một phức, chúng ta cần xác định toán tử + để chấp
nhận các toán hạng có kiểu khác nhau. Theo thuật ngữ Fortran, chúng ta cần số học ở chế
độ hỗn hợp. Chúng tôi có thể đạt được điều đó đơn giản bằng cách thêm các phiên bản
thích hợp của các toán tử:
class complex {
double re, im;
public:
complex& operator+=(complex a)
{
re += a.re;
im += a.im;
return ∗this;
}
complex& operator+=(double a)
{
re += a;
return ∗this;
}
// ...
};
-Ba biến thể của toán tử + () có thể được định nghĩa bên ngoài phức hợp:
complex operator+(complex a, complex b)
{

52
return a += b; // calls complex::operator+=(complex)
}
complex operator+(complex a, double b)
{
return {a.real()+b,a.imag()};
}
complex operator+(double a, complex b)
{
return {a+b.real(),b.imag()};
}

-Các hàm truy cập real () và virtual () được định nghĩa trong §18.3.6.
Với các khai báo +, chúng ta có thể viết:
void f(complex x, complex y)
{
auto r1 = x+y; // calls operator+(complex,complex)
auto r2 = x+2; // calls operator+(complex,double)
auto r3 = 2+x; // calls operator+(double,complex)
auto r4 = 2+3; // built-in integer addition
}
18.3.3: Chuyển đổi
-Để đối phó với các phép gán và khởi tạo các biến phức tạp với đại lượng vô hướng,
chúng ta cần chuyển đổi một đại lượng vô hướng (số nguyên hoặc dấu phẩy động) thành
một phức hợp. Ví dụ:
complex b {3}; // should mean b.re=3, b.im=0
void comp(complex x)
{
x = 4; // should mean x.re=4, x.im=0
// ...
}
-Chúng ta có thể đạt được điều đó bằng cách cung cấp một hàm tạo nhận một đối số duy
nhất. Một hàm tạo nhận một đối số chỉ định một chuyển đổi từ kiểu đối số của nó sang
kiểu của hàm tạo.
Ví dụ:

53
class complex {
double re, im;
public:
complex(double r) :re{r}, im{0} { } // build a complex from a double
// ...
};
-Một hàm tạo là một đơn thuốc để tạo một giá trị của một kiểu nhất định. Hàm khởi tạo
được sử dụng khi một giá trị của một kiểu được mong đợi và khi một giá trị đó có thể
được tạo bởi một hàm tạo từ giá trị được cung cấp dưới dạng bộ khởi tạo hoặc giá trị được
gán. Do đó, một hàm tạo yêu cầu một đối số duy nhất không cần được gọi một cách rõ
ràng.
-Ví dụ:
complex b {3};
//có nghĩa complex b {3,0};

-Chuyển đổi do người dùng xác định chỉ được áp dụng hoàn toàn nếu nó là duy nhất
(§12.3). Nếu bạn không muốn một hàm tạo được sử dụng ngầm, hãy khai báo nó một
cách rõ ràng (§16.2.6).
-Đương nhiên, chúng ta vẫn cần hàm tạo có giá trị gấp đôi và một hàm tạo mặc định khởi
tạo một phức hợp thành {0,0} cũng rất hữu ích:
class complex {
double re, im;
public:
complex() : re{0}, im{0} { }
complex(double r) : re{r}, im{0} { }
complex(double r, double i) : re{r}, im{i} { }
// ...
};
-Sử dụng các đối số mặc định, chúng ta có thể viết tắt:
class complex {
double re, im;
public:
complex(double r =0, double i =0) : re{r}, im{i} { }
// ...

54
};
-Theo mặc định, việc sao chép các giá trị phức tạp được định nghĩa là sao chép phần thực
và phần ảo (§16.2.2).
Ví dụ:
void f()
{
complex z;
complex x {1,2};
complex y {x}; // y also has the value {1,2}
z = x; // z also has the value {1,2}
}
18.3.3.1: Chuyển đổi Toán hạng
-Chúng tôi đã xác định ba phiên bản của mỗi trong bốn toán tử số học tiêu chuẩn:
toán tử phức tạp + (phức tạp, phức tạp);
toán tử phức tạp + (phức tạp, kép);
toán tử phức + (kép, phức);
// ...
-Giải pháp thay thế để cung cấp các phiên bản khác nhau của một hàm cho mỗi tổ hợp các
đối số dựa trên các chuyển đổi. Ví dụ, lớp phức hợp của chúng tôi cung cấp một phương
thức khởi tạo có thể chuyển đổi một cách chính xác thành một phức hợp. Do đó, chúng ta
chỉ có thể khai báo một phiên bản của toán tử bình đẳng cho phức hợp:
bool operator==(complex,complex);
void f(complex x, complex y)
{
x==y; // means operator==(x,y)
x==3; // means operator==(x,complex(3))
3==y; // means operator==(complex(3),y)
}
-Không có chuyển đổi ngầm định nào do người dùng xác định được áp dụng cho phía bên
trái của a. (hoặc a ->). Đây là trường hợp ngay cả khi là ẩn ý.
Ví dụ:
void g(complex z)
{
3+z; // OK: complex(3)+z

55
3.operator+=(z); // error : 3 is not a class object
3+=z; // error : 3 is not a class object
}
-Vì vậy, bạn có thể gần đúng khái niệm rằng một toán tử yêu cầu một giá trị làm toán
hạng bên trái của nó bằng cách đặt toán tử đó thành một thành viên. Tuy nhiên, đó chỉ là
ước tính vì có thể truy cập tạm thời với một thao tác sửa đổi, chẳng hạn như toán tử + =
():
complex x {4,5}
complex z {sqr t(x)+={1,2}}; // like tmp=sqr t(x), tmp+={1,2}
18.3.4: Chữ viết
-Chúng tôi có nghĩa đen của các loại tích hợp. Ví dụ: 1,2 và 12e3 là các chữ kiểu kép. Đối
với phức tạp, chúng ta có thể tiến gần đến điều đó bằng cách khai báo các hàm tạo
constexpr (§10.4).
-ví dụ
class complex {
public:
constexpr complex(double r =0, double i =0) : re{r}, im{i} { }
// ...
}
-Cho rằng, một phức hợp có thể được xây dựng từ các bộ phận cấu thành của nó tại thời
điểm biên dịch giống như một nghĩa đen từ một kiểu dựng sẵn.
- Ví dụ:
complex z1 {1.2,12e3};
constexpr complex z2 {1.2,12e3}; // guaranteed compile-time initialization
-Có thể đi xa hơn và giới thiệu một ký tự do người dùng xác định (§19.2.6) để hỗ trợ kiểu
phức tạp của chúng tôi. Đặc biệt, chúng ta có thể định nghĩa i là một hậu tố có nghĩa là ''
tưởng tượng ''.
Ví dụ:
constexpr complex<double> operator "" i(long double d) // imaginar y literal
{
return {0,d}; // complex is a literal type
}
-Điều này sẽ cho phép chúng tôi viết:

56
complex z1 {1.2+12e3i};
complex f(double d)
{
auto x {2.3i};
return x+sqrt(d+12e3i)+12e3i;
}
-Ký tự do người dùng xác định này mang lại cho chúng tôi một lợi thế so với những gì
chúng tôi nhận được từ các hàm tạo constexpr: chúng tôi có thể sử dụng các ký tự do
người dùng xác định ở giữa các biểu thức trong đó ký hiệu {} chỉ có thể được sử dụng khi
đủ điều kiện bởi một tên kiểu. Ví dụ trên gần tương đương với:
complex z1 {1.2,12e3};
complex f(double d)
{
complex x {0,2.3};
return x+sqrt(complex{d,12e3})+complex{0,12e3};
}
18.3.5: Chức năng của Accessor
-Cho đến nay, chúng tôi đã cung cấp phức hợp lớp chỉ với các hàm tạo và toán tử số học.
Điều đó không hoàn toàn đủ để sử dụng thực tế. Đặc biệt, chúng ta thường cần có khả
năng kiểm tra và thay đổi giá trị của phần thực và phần ảo:
class complex {
double re, im;
public:
constexpr double real() const { return re; }
constexpr double imag() const { return im; }
void real(double r) { re = r; }
void imag(double i) { im = i; }
// ...
};
-Tôi không coi việc cung cấp quyền truy cập cá nhân cho tất cả các thành viên trong lớp
là một ý tưởng hay.Đối với nhiều loại, quyền truy cập riêng lẻ (đôi khi được gọi là các
hàm get-and-set) là một lời mời dẫn đến thảm họa.

57
-Ví dụ: đã cho real () và images (), chúng ta có thể đơn giản hóa các phép toán đơn giản,
phổ biến và hữu ích, chẳng hạn như ==, dưới dạng các hàm không phải là bộ nhớ (mà
không ảnh hưởng đến hiệu suất):
inline bool operator==(complex a, complex b)
{
return a.real()==b.real() && a.imag()==b.imag();
}
19.3.6: Sử dụng chuỗi của chúng tôi
-Nếu chúng ta đặt tất cả các bit và các mảnh lại với nhau, lớp phức tạp sẽ trở thành:
class complex {
double re, im;
public:
constexpr complex(double r =0, double i =0) : re(r), im(i) { }
constexpr double real() const { return re; }
constexpr double imag() const { return im; }
void real(double r) { re = r; }
void imag(double i) { im = i; }
complex& operator+=(complex);
complex& operator+=(double);
// -=, *=, and /=
};
-Ngoài ra, chúng tôi phải cung cấp một số chức năng trợ giúp:

complex operator+(complex,complex);
complex operator+(complex,double);
complex operator+(double ,complex);
// binar y -, *, and /
complex operator−(complex); // unar y minus
complex operator+(complex); // unar y plus
bool operator==(complex,complex);
bool operator!=(complex,complex);
istream& operator>>(istream&,complex&); // input
ostream& operator<<(ostream&,complex); // output

58
-Lưu ý rằng các thành viên real () và virtual () là yếu tố cần thiết để xác định các phép so
sánh. Định nghĩa của hầu hết các hàm trợ giúp sau đây cũng dựa trên real () và virtual ().
-Chúng tôi có thể cung cấp các chức năng để cho phép người dùng suy nghĩ về tọa độ
cực:
complex polar(double rho, double theta);
complex conj(complex);
double abs(complex);
double arg(complex);
double norm(complex);
double real(complex); // for notational convenience
double imag(complex); // for notational convenience

-Cuối cùng, chúng ta phải cung cấp một tập hợp các hàm toán học tiêu chuẩn thích hợp:
complex acos(complex);
complex asin(complex);
complex atan(complex);
// ...
-Theo quan điểm của người dùng, kiểu phức được trình bày ở đây gần giống với kiểu
phức <double> được tìm thấy trong <complex> trong thư viện chuẩn (§5.6.2, §40.4).

18.4: Chuyển đổi loại


-Việc chuyển đổi kiểu có thể được thực hiện bằng
• Một hàm tạo nhận một đối số duy nhất (§16.2.5)
• Toán tử chuyển đổi (§18.4.1)
Trong cả hai trường hợp, chuyển đổi có thể
• rõ ràng; nghĩa là, việc chuyển đổi chỉ được thực hiện trong lần khởi tạo trực tiếp
(§16.2.6), tức là
trình khởi tạo không sử dụng dấu =.
• Ngụ ý; nghĩa là, nó sẽ được áp dụng ở bất cứ nơi nào nó có thể được sử dụng một cách
rõ ràng (§18.4.3), ví dụ: như một đối số của hàm.
18.4.1: Nhà điều hành chuyển đổi
-Sử dụng một hàm tạo lấy một đối số duy nhất để chỉ định chuyển đổi kiểu là thuận tiện
nhưng có những tác động có thể không mong muốn. Ngoài ra, một hàm tạo không thể chỉ
định:

59
[1] chuyển đổi ngầm định từ loại do người dùng xác định sang loại tích hợp (vì loại không
phải là lớp), hoặc
[2] chuyển đổi từ một lớp mới sang một lớp đã xác định trước đó (mà không sửa đổi
decla-khẩu phần ăn cho lớp cũ).
-Những vấn đề này có thể được xử lý bằng cách xác định toán tử chuyển đổi cho kiểu
nguồn. Một hàm thành viên X :: operator T (), trong đó T là tên kiểu, xác định một
chuyển đổi từ X sang T. Ví dụ, chúng ta có thể xác định một số nguyên không âm 6 bit,
Tiny, có thể kết hợp tùy ý với các số nguyên trong các phép tính toán học. Tiny ném
Bad_range nếu các hoạt động của nó bị tràn hoặc bị tràn:
class Tiny {
char v;
void assign(int i) { if (i& ̃077) throw Bad_rang e(); v=i; }
public:
class Bad_range { };
Tiny(int i) { assign(i); }
Tiny& operator=(int i) { assign(i); return ∗this; }
operator int() const { return v; } // conversion to int function
};

- kiểu đang được chuyển đổi thành là một phần của tên của công cụ hỗ trợ và không thể
được lặp lại dưới dạng giá trị trả về của hàm chuyển đổi:
Tiny::operator int() const { return v; } // right
int Tiny::operator int() const { return v; } // error
-Bất cứ khi nào một Tiny xuất hiện ở nơi cần sử dụng int, int thích hợp sẽ được sử dụng.
Ví dụ:
int main()
{
Tiny c1 = 2;
Tiny c2 = 62;
Tiny c3 = c2−c1; // c3 = 60
Tiny c4 = c3; // no range check (not necessary)
int i = c1+c2; // i = 64
c1 = c1+c2; // range error: c1 can’t be 64
i = c3−64; // i = -4

60
c2 = c3−64; // range error: c2 can’t be -4
c3 = c4; // no range check (not necessary)
}
-Các loại istream và ostream dựa vào một hàm chuyển đổi để kích hoạt các câu lệnh như:
while (cin>>x)
cout<<x;
-Nếu cả chuyển đổi do người dùng xác định và toán tử do người dùng xác định đều được
xác định, thì có thể có sự không rõ ràng giữa toán tử do người dùng xác định và toán tử
được tích hợp sẵn.
Ví dụ:
int operator+(Tiny,Tiny);
void f(Tiny t, int i)
{
t+i; // error, ambiguous: ‘‘operator+(t,Tiny(i))’’ or ‘‘int(t)+i’’?
}

18.4.2: Nhà điều hành chuyển đổi rõ ràng


-Các toán tử chuyển đổi có xu hướng được xác định để chúng có thể được sử dụng ở mọi
nơi. Ví dụ: thư viện tiêu chuẩn unique_ptr có một chuyển đổi rõ ràng thành bool:
template <typename T, typename D = default_delete<T>>
class unique_ptr {
public:
// ...
explicit operator bool() const noexcept; // does *this hold a pointer (that is
not nullptr)?
// ...
};
-Lý do để khai báo toán tử chuyển đổi này rõ ràng là để tránh việc sử dụng nó trong các
ngữ cảnh đáng ngạc nhiên.
Xem xét:
void use(unique_ptr<Record> p, unique_ptr<int> q)
{
if (!p) // OK: we want this use
throw Inv alid_uninque_ptr{};

61
bool b = p; // error ; suspicious use
int x = p+q; // error ; we definitly don’t want this
}
18.4.3:Sự mơ hồ
-Trong một số trường hợp, giá trị của kiểu mong muốn có thể được xây dựng bằng cách
sử dụng lặp lại các hàm tạo hoặc toán tử chuyển đổi. Ví dụ:
class X { /* ... */ X(int); X(const char∗); };
class Y { /* ... */ Y(int); };
class Z { /* ... */ Z(X); };
X f(X);
Y f(Y);
Z g(Z);
void k1()
{
f(1); // error : ambiguous f(X(1)) or f(Y(1))?
f(X{1}); // OK
f(Y{1}); // OK
g("Mack"); // error : two user-defined conversions needed; g(Z{X{"Mack"}})
not tried
g(X{"Doc"}); // OK: g(Z{X{"Doc"}})
g(Z{"Suzy"}); // OK: g(Z{X{"Suzy"}})
}
-Các chuyển đổi do người dùng xác định chỉ được xem xét nếu không thể giải quyết cuộc
gọi mà không có chúng (tức là chỉ sử dụng các chuyển đổi tích hợp sẵn). Ví dụ:
class XX { /* ... */ XX(int); };
void h(double);
void h(XX);
void k2()
{
h(1); // h(double{1}) or h(XX{1})? h(double{1})!
}
-Sự khăng khăng về phân tích từ dưới lên nghiêm ngặt ngụ ý rằng kiểu trả về không được
sử dụng trong giải quyết nạp chồng toán tử.
Ví dụ:

62
class Quad {
public:
Quad(double);
// ...
};
Quad operator+(Quad,Quad);
void f(double a1, double a2)
{
Quad r1 = a1+a2; // double-precision floating-point add
Quad r2 = Quad{a1}+a2; // force quad arithmetic
}
-Khi đã xác định được kiểu của cả hai phía của một lần khởi tạo hoặc phép gán, thì cả hai
kiểu đều được sử dụng để giải quyết việc khởi tạo hoặc gán.
Ví dụ:
class Real {
public:
operator double();
operator int();
// ...
};
void g(Real a)
{
double d = a; // d = a.double();
int i = a; // i = a.int();
d = a; // d = a.double();
i = a; // i = a.int();
}
18.5: Lời khuyên
[1] Xác định các toán tử chủ yếu để bắt chước cách sử dụng thông thường; §18.1.
[2] Xác định lại hoặc cấm sao chép nếu mặc định không phù hợp với một loại; §18.2.2.
[3] Đối với các toán hạng lớn, hãy sử dụng kiểu đối số tham chiếu const; §18.2.4.
[4] Để có kết quả lớn, hãy sử dụng hàm tạo di chuyển; §18.2.4.
[5] Ưu tiên các chức năng thành viên hơn các thành viên không phải thành viên đối với
các hoạt động cần quyền truy cập vào cơ quan đại diện; §18.3.1.

63
[6] Ưu tiên các chức năng nonmember hơn các thành viên cho các hoạt động không cần
quyền truy cập vào việc gửi lại; §18.3.2.
[7] Sử dụng không gian tên để liên kết các hàm trợ giúp với lớp ‘‘ their ’’; §18.2.5.
[8] Sử dụng các hàm nonmember cho các toán tử đối xứng; §18.3.2.
[9] Sử dụng các hàm thành viên để thể hiện các toán tử yêu cầu giá trị làm toán hạng bên
trái của chúng;
§18.3.3.1.
[10] Sử dụng các ký tự do người dùng xác định để bắt chước ký hiệu thông thường;
§18.3.4.
[11] Cung cấp ‘‘ các hàm set () và get () ’’ cho một thành viên dữ liệu chỉ khi ngữ nghĩa
cơ bản của aclass yêu cầu chúng; §18.3.5.
[12] Thận trọng về việc giới thiệu các chuyển đổi ngầm; §18.4.
[13] Tránh chuyển đổi phá hủy giá trị (‘‘ thu hẹp ’’); §18.4.1.
[14] Không định nghĩa chuyển đổi giống như cả hàm tạo và toán tử chuyển đổi; §18.4.3.

64
65
TÀI LIỆU THAM KHẢO

66

You might also like