Tải bản đầy đủ (.doc) (152 trang)

Tìm hiểu về ngôn ngữ lập trình đa mô thức c

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (2.19 MB, 152 trang )

1

Chương 1

Tổng quan về các vấn đề liên quan
1.1

Lập trình hướng đối tượng với C++

1.1.1

C++

C++ là một ngôn ngữ lập trình đa mô thức (multi-paradigm), biên dịch, định kiểu tĩnh. C++ hỗ trợ lập
trình cấu trúc, lập trình hướng đối tượng, lập trình khái lược, lập trình hàm, siêu lập trình dựa trên
khuôn hình (template metaprogramming).
Tác giả đầu tiên của C++ là Bjarne Stroustrup, cái tên C++ có nghĩa là ngôn ngữ C++ được phát triển từ
C. Tuy vậy C++ không phải là một mở rộng của C, và thực tế thì C vẫn có nhiều điểm không tương thích
với C++ (chương trình viết bằng C nếu biên dịch bằng trình dịch C++ thì có thể sẽ có lỗi).

1.1.2

C++11

Vào tháng 11/2011, ISO đã thông qua chuẩn C++11, thay thế cho chuẩn cũ là C++03. C++11 là sự thay
đổi lớn của C++, bao gồm các thay đổi tron bản thân ngôn ngữ đến thay đổi trong thư viện chuẩn.
Mục đích của C++11 là [1]:
• Đưa C++ trở thành ngôn ngữ tốt hơn cho lập trình hệ thống và xây dựng thư viện (thông qua một
số tính năng mới như thread, new memory model, pseudo-random number generation,
. . . ).
• Làm cho C++ dễ dạy và học hơn (nullptr, range-for, auto, override, . . . ).


.
Có thể tìm hiểu về những cái mới trong C++11 tại [2], [3].
Bản nháp của chuẩn C++11 (miễn phí) tại n-

std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf .
Dưới đây, ta sẽ tìm hiểu một số tính năng mới của C++ mà sẽ được sử dụng nhiều trong BKPaint.


2
1.1.2.1 nullptr - con trỏ null
Trong ngôn ngữ C, macro NULL được định nghĩa là (void*)0. Tuy nhiên, C++ lại không thể định nghĩa
theo cách này, bởi vì C++ yêu cầu chuyển đổi kiểu tường minh từ void* sang kiểu con trỏ. Ví dụ:
1

int *a = NULL;

Sẽ sinh lỗi biên dịch.
Do vậy, C++ định nghĩa NULL là 0, nhờ đó câu lệnh trên là hợp lệ.
Tuy nhiên, cũng vì thể mà NULL được coi là một hằng nguyên thay vì hằng con trỏ, do đó:
1

void foo (char *);

// 1

2

void foo (int);

// 2


3

...

4

foo (NULL);

Câu lệnh foo (NULL) sẽ gọi hàm foo() thứ 2, rõ ràng không phải điều mà lập trình viên mong muốn. Nếu
muốn gọi hàm foo thứ nhất thì phải sử dụng phép chuyển kiểu tường minh.
C++11 cung cấp từ khoá nullptr, nó đại diện cho hằng con trỏ nguyên. nullptr có kiểu là nullptr_t, có
thể đượng chuyển kiểu không tường minh sang kiểu con trỏ bất kỳ và kiểu bool. Nếu muốn chuyển
sang kiểu nguyên phải ép kiểu.
1

int *a = nullptr; bool b = nullptr; // =

2

false

3
4

foo (nullptr);

// call foo (char *)

5

6
7
8

c = nullptr; if (c) {
}

9
10

int a = nullptr;

// illegal

Tham khảo: [4, section. 4.10, pg. 80].
1.1.2.2 auto - tự động suy diễn kiểu
Trong C++, kiểu của một đối tượng có thể rất dài. Ví dụ:
1

std::vector<int>::iterator vi = v.begin(); for (; vi != v.end(); ++vi) {

2

...
}


Chương 1. Tổng quan về các vấn đề liên quan

3


3
4

Với từ khoá auto, kiểu của biến khi khởi tạo sẽ tự động được suy diễn bởi trình dịch trong thời gian
biên dịch (compile-time) dựa vào giá trị trả về của biểu thức khởi tạo cho biến [4, section. 7.1.6.4, pg.
148]. Nhờ vậy mà mã nguồn ngắn gọn, dễ hiểu hơn: Ví dụ:
1
2
3
4

// auto v; // Compile error
std::vector<int> v; for (auto vi = v.begin(); vi != v.end(); ++vi) {
...

5
6

}

7
8

auto i = 145;

// int auto d = 14.5;// double

9


auto f = 14.5f;

// float

10
11

auto p = f * i;

// float

12
13
14

auto const v2 = v; // const std::vector<int> auto vi2 = v2.begin()
std::vector<int>::const_iterator

//

15

Chú ý: ý nghĩa cũ của từ khoá auto (auto local storage) ở C++11 không còn hợp lệ.

1.1.2.3 Range-for
Nhiều ngôn ngữ cung cấp câu lệnh for cho phép lặp qua một tập hợp, mảng, . . . dễ dàng. Ví dụ Python:
1

l = [1, 2, 3, 4] for i in l:


2

print (i)

3

Hay Java:
1
2

int[] l = new int[]{1, 2, 3, 4}; for (int i : l) {

3
4

}

...


4
Với C++03 trở về trước, phép lặp qua một tập hợp, mảng, . . . sử dụng iterator hoặc lặp theo chỉ số
mảng với cú pháp khá phức tạp. C++11 cung cấp Range-for với chức năng tương tự như các ngôn ngữ
khác:
1
2
3
4

int l[] = {1, 2, 3, 4}; for (int i : l)

{ std::cout << i << ’\n’;
}

Với Range-for có thể thay thế l là một mảng, hoặc đối tượng của một lớp bất kỳ miễn là có hai
phương thức begin() và end() trả về hai iterator trỏ đến phần tử đầu và cuối tương ứng. Như vậy tất
cả các Container của STL đều có thể sử dụng được với Range-for.

Range-for của C++11 cho phép tùy chọn kiểu của phần tử:
1
2
3

for (int &i : l) { i *= 2;
}

Ở đây, i là một tham chiếu của một phần tử thuộc l, thay vì là một phiên bản copy. Nhờ vậy mà phép
gán bên trong vòng lặp có thể thay đổi được nội dung của l. So với vòng lặp trong Java, không thể thay
đổi được nội dung của mảng.
Tham khảo [4, section. 6.5.4, pg. 128].

1.1.2.4

Chỉ thị override

Giả sử ta có lớp A và lớp B là lớp dẫn xuất của A.
1
2
3

struct A { virtual void f(); virtual void

g() const; void h();

4
5

};

6
7

struct B : public A { void f();
void g(); void h();

8
9

};

10
11

Lập trình viên có ý định định nghĩa lại các phương thức ảo f(), g(), h() của A tại B. Nhưng với đoạn mã
trên chỉ có f() là được định nghĩa lại, trong khi A::g() const và B::g() là hai phương thức không liên


Chương 1. Tổng quan về các vấn đề liên quan

5

quan nhau, A::h() không phải là phương thức ảo. Đây có thể là do lỗi của lập trình viên. Lỗi này là lỗi

ngữ nghĩa nên khi trình biên dịch không báo, rất khó để phát hiện ra.
C++11 cung cấp chỉ thị override cho phép khai báo tường minh một phương thức của lớp dẫn xuất sẽ
định nghĩa lại phương thức ảo của lớp cơ sở.
1

struct B : public A { void f() override;

2

void g() override; void h()

3
4
5

override;
};

Bây giờ, khi biên dịch thì trình dịch sẽ báo lỗi ở khai báo phương thức B::g() và B::h() bởi vì trong lớp
cơ sở của B là A không có phương thức ảo nào có chữ ký giống với những phương thức này. Nhờ vậy
mà có thể dễ dàng sửa lỗi.
Chú ý: override không phải là một từ khoá mới, do đó vẫn có thể sử dụng override làm định danh, mặc
dù không được khuyến khích.
Tham khảo: [4, section. 10.3, pg. 225].

1.2

Qt Framework

1.2.1


Giới thiệu

Qt Framework là một bộ công cụ lập trình đa nền tảng dùng để phát triển các ứng dụng GUI cũng như
các ứng dụng dòng lệnh. Chương trình Qt chỉ cần viết một lần sau đó có thể biên dịch trên nhiều nền
tảng, nhiều hệ điều hành khác nhau mà hầu như không cần sửa lại mã nguồn. Hiện nay Qt hỗ trợ các
hệ điều hành Linux, MacOS, Windows, Symbian, Meamo, Meego, . . . Qt hiện đang thuộc bản quyền
của Digia ( được phân phối dưới 2 giấy phép khác nhau là giấy phép phần mềm
tự do mã nguồn mở (GPL v2) và giấy phép phần mềm độc quyền (Qt Commericial License). [5]
Ngôn ngữ chủ yếu được sử dụng bởi Qt là C++ và QML (một ngôn ngữ script tương tự Javascript).
Ngoài ra còn có thể lập trình sử dụng Qt bằng nhiều ngôn ngữ khác như D, Python, . . .
Bản chính thức mới nhất của Qt là Qt 4.8.3.


6

HÌNH 1.1: Biên dịch một Project Qt

1.2.2

Lập trình C++ với Qt

Chương trình C++ sử dụng Qt thực chất không phải là C++ chuẩn mà là một mở rộng của C++. Các file
mã nguồn sử dụng các cú pháp của Qt sẽ được dịch ra file mã nguồn C++ trước khi biên dịch bằng
trình dịch C++ như bình thường. Chương trình làm nhiệm vụ này là moc (metaobject compiler).
Nhờ vậy mà chương trình Qt có nhiều tính năng mới không có trong
C++ như Meta Object, Property, Signal-slot, . . .
Hình 1.1 là sơ đồ quá trình biên dịch một Project Qt. Ta thấy:
• Các file tiêu đề khai báo lớp (*.h) được vừa được bao gồm trong file cài đặt tương ứng, vừa được


moc dịch thành file cài đặt lớp mới (moc_*.cpp). Những file moc_*.cpp này thực chất là file cài
đặt các phương thức riêng của lớp cần thiết cho các tính năng mở rộng của Qt,
• Các file mô tả giao diện chương trình (*.ui) được biên dịch thành file tiêu đề khai báo lớp (dạng
ui_*.h). File tiêu đề này khai báo một lớp Ui_* mới chứa các thành phần giao diện
cần thiết.
• Các file mô tả tài nguyên (*.qrc) được biên dịch thành các file qrc_.cpp. Nhưng file này chứa nội
dung của các tài nguyên được mô tả trong các file .qrc. Như vậy, các tài nguyên này trở thành một
phần của chương trình thay vì tách rời file thực thi riêng, file tài nguyên
riêng.
• Các file cài đặt lớp (*.cpp) được biên dịch thằnh object như bình thường,


Chương 1. Tổng quan về các vấn đề liên quan

7

• Các file object sau khi biên dịch sẽ được liên kết với nhau và với các thư viện ngoài cần thiết (trong
đó có các thư viện của Qt) để tạo thành file thực thi.
Chỉ những lớp mà trong mô tả có macro Q_OBJECT mới được dịch bằng moc.
Qt Framework bao gồm nhiều module như: QtCore, QtGui, QtNetwork, QtXml, . . . . Mỗi module bao
gồm nhiều lớp có nhiệm vụ liên quan đến nhau. Các lớp có sẵn của Qt đều bắt đầu bằng chữ ’Q’, đa số
các lớp đều có tổ tiên là lớp QObject. Chi tiết về các module cũng như các lớp của Qt xem tại:

/>
1.2.3

Một số kỹ thuật hướng đối tượng trong Qt

1.2.3.1


Cơ chế cha-con

Các đối tượng thuộc lớp QObject và các lớp dẫn xuất được tổ chức thành các cây các đối tượng. Mỗi
đối tượng có thể có một đối tượng cha (parent) và nhiều đối tượng con (children). Trong destructor
của lớp QObject có lời gọi hủy các đối tượng con. Nhờ vậy mà người lập trình có thể tự do tạo đối
tượng trên bộ nhớ heap mà không phải quan tâm đến vấn đề hủy đối tượng.
Điều này rất có ích đối với các ứng dụng GUI. VD: đối tượng mainWindow (cửa sổ chính) là gốc của
cây, các đối tượng menuBar (thanh menu), toolBar (thanh công cụ) . . . là con của mainWindow, các
button (nút bấm) là con của toolBar. Khi mainWindow bị huỷ thì nó sẽ kéo theo các menuBar, toolBar
bị hủy. Đến lượt mình, toolBar cũng hủy các button con của nó.
Ngoài ra, trong khai báo lớp QObject thì destructor được khai báo là virtual nên cho dù con trỏ
mainWindow có được cast về QObject* thì lời gọi delete vẫn gọi đúng hàm hủy của lớp

QMainWindow.

1.2.3.2

Cơ chế signal - slot (tín hiệu - khe cắm)

Đây là một cơ chế giúp cho việc truyền thông điệp giữa các đối tượng trong Qt trở nên đơn giản hơn so
với nhiều Framework khác.

Signal (tín hiệu) được một đối tượng "emit (phát ra)" trong quá trình tồn tại của nó. Có thể là khi
nhận được một sự kiện từ bên ngoài, hay cũng có thể là bất cứ lúc nào mà người lập trình mong
muốn.

Slot (khe cắm) thực chất là một phương thức của một đối tượng nào đó. Slot giống như một phương
thức C++ bình thường ngoại trừ việc nó có thể được tự động gọi khi nhận được một signal nào đó với
điều kiện là trước đó đã được connect (kết nối).



8
Chỉ có những lớp dẫn xuất của QObject mới có thể sử dụng signal và slot.
Ví dụ: Đối tượng button thuộc lớp QPushButton (nút bấm) có signal clicked() được emit khi người
dùng bấm vào nút đó. Ở đối tượng mainWindow thuộc lớp MainWindow (cửa sổ chính), nếu ta
connect slot xử lý việc bấm nút buttonClicked() với signal QPushButton::clicked() thì khi người
dùng bấm nút, phương thức buttonClicked() sẽ được thực hiện. 1


Chương 1. Tổng quan về các vấn đề liên quan
1

Xem

thêm

4.8/signalsandslots.html

về signal, slot, connect, emit tại />
9


Chương 2. Phân tích thiết kế mức kiến trúc

10

Chương 2

Phân tích thiết kế mức kiến trúc
2.1


Biểu đồ Use-case

2.1.1

Biểu đồ Use-case tổng quát
BKPaint
BKPaint
Undo / Redo
Zoom
Replay painting
user

Painting
Save file
Open file

HÌNH 2.1: Biểu đồ Use-case tổng quát

Hình 2.1.
• Tác nhân của hệ thống: Người sử dụng (User).
• Các Use-case chính:
– Painting: nhóm các Use-case vẽ hình,
– Replay painting: nhóm các Use-case thực hiện lại các thao tác vẽ,
– Save: Lưu file,
– Open: Mở file,
– Undo/Redo: Undo / Redo


Chương 2. Phân tích thiết kế mức kiến trúc


11

– Zoom: Phóng to, thu nhỏ

2.1.2

Phân



Use-case

Painting
BKPaint
BKPaint
Draw Shape

Erase

Fill

SetPainter

Painting

HÌNH 2.2: Phân rã Use-case Painting

Hình 2.2. Các Use-case:
• Draw shape: Vẽ các hình đồ hoạ,

• Erase: Tẩy một phần của bức vẽ,
• Fill Area: Đổ màu
• Set painter: Thay đổi tùy chọn của đối tượng vẽ (nét vẽ, chổi lông, ...)

2.1.3

Phân ra Use-case Replay painting
BKPaint
BKPaint
Play

Pause

Resume

Stop

Replay painting

HÌNH 2.3: Phân rã Use-case Replay painting

Hình 2.3. Các Use-case:
• Play: Bắt đầu quá trình replay cho đến kết thúc,


Chương 2. Phân tích thiết kế mức kiến trúc
• Pause: Tạm dừng quá trình replay, • Resume: Tiếp
tục quá trình replay,
• Stop: Kết thúc quá trình replay.


12


Chương 2. Phân tích thiết kế mức kiến trúc

2.2

13

Biểu đồ trình tự

Ở các biểu đồ trình tự dưói đây, đều xuất hiện một đối tượng đặc biệt gọi là GUI Thread. Đây là thread
chính của chương trình, quản lý các điều khiển trên nó, cũng như giao tiếp với người dùng. Tất cả các
hàm xử lý sự kiện đều hoạt động trên thread này.

2.2.1

Biểu đồ trình tự cho Use-case Open

gui:

user:

mw1:MainWindow

open
actionOpen()
request filename
provide filename
create


mw2:MainWindow
create

player:Player
create

alt
[is graphic]

[else]

canvas:Canvas

load (filename)
loadImage()

load(filename)

reloadPlayer()

HÌNH 2.4: Biểu đồ trình tự cho Use-caseOpen


Chương 2. Phân tích thiết kế mức kiến trúc

14

Khi người dùng yêu cầu mở một file. một cửa sổ chính ( MainWindow) sẽ được tạo ra (thực chất là 1
tiến trình mới) độc lập với cửa sổ chính hiện tại và file được mở trên đó.


2.2.2

Biểu đồ trình tự cho Use-case Save
:User

sd

:GuiThread

:MainWindow

:Player

[ Save ]

open
on_actionSave_triggered()
Request filename
Input filename
save(filename)

HÌNH 2.5: Biểu đồ trình tự cho Use-caseSave

2.2.3
2.2.3.1

Biểu đồ trình tự cho Use-case Painting
Biểu đồ trình tự cho Use-case Draw shape


BKPaint hỗ trợ vẽ nhiều loại shape khác nhau (hình chữ nhật, hình elip, đa giác, . . . ) với cách vẽ khác
nhau. Nhưng nhìn chung thì quá trình vẽ hình có trình tự kiểu như hình 2.6.


Chương 2. Phân tích thiết kế mức kiến trúc

15

HÌNH 2.6: Biểu đồ trình tự cho Use-case Draw shape

Trong đó:
• Khi nhấn chuột tại một điểm thì sinh ra một thao tác vẽ kiểu MousePress,
• Khi di chuyển chuột thì tùy theo chế độ vẽ hiện tại mà sinh ra một thao tác vẽ có kiểu là lớp dẫn
xuất của PaintOperation,
• Khi nhả chuột thì yêu cầu thêm một đối tượng kết thúc chuỗi các thao tác vẽ,
(Player::addFinishChangeShape()).


Chương 2. Phân tích thiết kế mức kiến trúc
2.2.3.2

16

Biểu đồ trình tự cho Use-case Erase
:User

sd

:GuiThread


:Player

:Canvas

er:Erase

[ Erase ]
press mouse
mousePressEvent()
create()

addOperation(er)

operate()

move mouse
mouseMoveEvent()
opt

[ mousepressed ]
create()

addOperation(er)

operate()

HÌNH 2.7: Biểu đồ trình tự cho Use-caseErase

Khi nhấn chuột hoặc nhấn chuột và kèm theo di chuyển chuột, canvas sẽ tạo ra một thao tác Erase
đại diện cho việc xoá màn hình. Và thực hiện thao tác đó.


2.2.4

Biểu đồ trình tự cho Use-case Replay Painting


Chương 2. Phân tích thiết kế mức kiến trúc
:User

sd

:GuiThread

17

:MainWindow

:Player

:Canvas

:Timer

[ Replay ]
replay
on_actionReplay_triggered()
enablePainting(false)

play()
startPlayer()

setMode(RePlay)

start

loop

[ not end ]
timeout
timerTimeout()
playNextOp()

start

timeout
timerTimeout()
ref

[ Player.Stop ]
stop()

HÌNH 2.8: Biểu đồ trình tự cho Use-caseReplay


Chương 2. Phân tích thiết kế mức kiến trúc
:User

sd

:GuiThread


18

:MainWindow

:Player

:Canvas

[ Pause ]
pause
on_actionPause_triggered()
pause()
stop

HÌNH 2.9: Biểu đồ trình tự cho Use-case Pause
:User

sd

:GuiThread

:MainWindow

:Player

:Timer

[ Resume ]
resume
on_actionResume_triggered()

resume()
start

HÌNH 2.10: Biểu đồ trình tự cho Use-case Resume

:Timer


Chương 2. Phân tích thiết kế mức kiến trúc
:User

sd

:GuiThread

:MainWindow

19
:Player

:Canvas

:Timer

[ Stop ]
stop
on_actionStop_triggered()
stop()
sd


[ Player.Stop ]
stop
stopPlayer()
setMode(old_mode)

stopPlayer()
enablePainting(true)

HÌNH 2.11: Biểu đồ trình tự cho Use-caseStop

2.2.5

Biểu đồ trình tự cho Use-case Undo / Redo


Chương 2. Phân tích thiết kế mức kiến trúc
:User

sd

:GuiThread

20

:MainWindow

:Canvas

:Player


[ Undo ]
undo
on_actionUndo_triggered()
undo()
undo()
pushUndoStack()

repaint()

HÌNH 2.12: Biểu đồ trình tự cho Use-caseUndo

Khi người dùng yêu cầu undo, player sẽ chuyển thao tác vẽ gần nhất vào stack (ở đây là undoStack),
sau đó canvas sẽ yêu cầu player vẽ lại từ đầu (trừ thao tác vừa được đưa và
undoStack).


Chương 2. Phân tích thiết kế mức kiến trúc
:User

sd

:GuiThread

21

:MainWindow

:Canvas

:Player


po:PaintOperation

[ Redo ]
undo
on_actionRedo_triggered()
redo()
redo()
popUndoStack(po)

operate()

HÌNH 2.13: Biểu đồ trình tự cho Use-case
Redo

Ngược lại, khi có yêu cầu redo, player sẽ chuyển thao tác trên đỉnh của undoStack vào danh sách các
thao tác, sau đó thực hiện thao tác này.

2.3

Biểu đồ lớp

2.3.1

Biểu đồ lớp tổng quan

Hình 2.14 là biểu đồ tổng quan tất cả các lớp của BKPaint. Trong đó:
• MainWindow là lớp cửa sổ chính, là chứa tất cả các thành phần giao diện khác,
• ToolBar, ColorPicker, . . . là các thành phần giao diện,
• Canvas là lớp đại diện cho bức vẽ, nơi người dùng thao tác lên để vẽ hình,

• Player là lớp có nhiệm vụ lưu trữ, quản lý các thao tác vẽ,
• PaintOperation là lớp cơ sở cho tất cả các loại thao tác vẽ.


Chương 2. Phân tích thiết kế mức kiến trúc

2.3.2

Biểu đồ lớp chi tiết cho MainWindow và các thành phần giao diện

22


Chương 2. Phân tích thiết kế mức kiến trúc

23


Chương 2. Phân tích thiết kế mức kiến trúc

2.3.3

Biểu đồ lớp chi tiết cho Canvas và Player

24


Chương 2. Phân tích thiết kế mức kiến trúc

2.3.4


Biểu đồ lớp chi tiết cho PaintOperation và các lớp liên quan

HÌNH 2.18: Biểu đồ lớp cho PaintOperation và các lớp liên quan

25


×