Cám ơn Mokona đã gợi ý để mình post bài viết này. Có lẽ bạn đang làm 1 project về Object và muốn so sánh ở tính năng Object của C++ với hỗ trợ con trỏ cùng với các ngôn ngữ khác phải không?
Mình cũng đã viết bài viết này lâu rồi (trong 1 đồ án nhỏ về phát triển HDH của môn học HDH) và nhiều lần định post lên giới thiệu với mọi người. Nhưng do vụ “xì căng đan” trong topic “C++ vs tất cả các ngôn ngữ lập trình” cách đây gần 1 tháng vì mọi người cho rằng mình quá “lăng xê” C++ nên lại thôi… Nhưng mình thật sự rất muốn giới thiệu điểm mạnh C++ bởi tính năng con trỏ đến các bạn.
Hy vọng lần này sẽ không tái diễn như trước nữa vì mình chả lấy C++ vs với ngôn ngữ nào nữa trong Topic này, nếu có thì cũng chỉ so đơn giản với một số ngôn ngữ để dễ hình dung mà thôi. Do đó mình mong có sự ủng hộ của các bạn một chút để mình có thê tự tin “spam” kiến thức một chút trong những Topic lần sau. Xin cám ơn các bạn
Quay lại về Topic.
Mình xin nói lại là C++ chuẩn có loại 2 con trỏ.
Nhưng tại thời điểm này (2007), lý ra C++ đã có tới 3 loại con trỏ (con trỏ point *, con trỏ far point, và con trỏ handle^). nhưng far point đã hủy bỏ rồi trên môi trường Windows rồi. Còn con trỏ Handle mới chính thức được các nhà khoa học đưa vào ngôn ngữ C++ vào tháng 11/2005 để C++ tăng khả năng hoạt động trên môi trường .NET và chỉ có thể sử dụng con trỏ này trong VS2005 và ngôn ngữ C++ này gọi là C++/CLI (Common Language Infrastructure).
Con trỏ C++ có thể làm thay đổi cả một hệ thống:
Có ý kiến cho rằng “con trỏ của C/C++ có thể thay đổi cả một hệ thống” Lần trước có 1 bạn bảo có ai thử phân tích câu nói này xem nhưng tôi không có dịp trả lời. Và tại sao 2 hệ điều hành lớn hiện nay là Windows và Linux được phát triển bằng C/C++ mà không phải là một ngôn ngữ khác như PASCAL hay ASM?
Tôi cũng cố gắng trả lời 2 câu hỏi này luôn trong nó luốn trong TOPIC.
+ Vấn đề bắt đầu là con trỏ của C/C++
C++ for DOS có 1 con trỏ có thể làm thay đổi hệ thống chính là far point (32bit). Đó mới đúng là linh hồn của C/C++:
Code:
void far* pMem;
void *pMem;
2 con trỏ này khác nhau chỗ nào? Để giải thích được nó có lẽ mình phải phân tích thêm về cách tổ chức MEMMORY của PC và tổ chức Data trên RAM của hệ điều hành MS DOS cũng như WINDOWS ngày nay.
I. CÁC BỘ NHỚ TRÊN MÁY TÍNH
Trước tiên là quy định về đơn vị bộ nhớ:
Code:
1 byte = 8 bits
1 word = 2 bytes
1 double word = 4 bytes
1 quad word = 8 bytes
1 octal word = 8 bytes
1 paragraph = 16 bytes
1 kilobyte (KB) = 1,024 bytes
1 megabyte (MB) = 1,024KB = 1,048,576 bytes
1 gigabyte (GB) = 1,024MB = 1,073,741,824 bytes
1 terabyte (TB) = 1,024GB = 1,099,511,627,776 bytes
1 petabyte (PB) = 1,024TB = 1,125,899,906,842,624 bytes
Cái này mình chỉ nhắc lại thôi chứ cũng chả có gì khó hiểu.
Bộ nhớ chính mà CPU có thể truy xuất trên PC 5 loại đó là:
1. Registers
2. Cache
3. RAM
4. ROM (ở đây mình không xét tới ROM vì nó chỉ đọc chứ không ghi được)
5. Disk storage
* Registers là các thanh ghi nằm ngay bên trong CPU. Và nó là nơi mà CPU có thể truy xuất nhanh nhất.
CPU32 bit (thế hệ Pentium ngày nay) bao gồm các thanh ghi:
- Nhóm 16 bit.
+ AX, BX, BX, DX: Chức năng chính của nhóm thanh ghi này thực hiện các chức năng là tính toán như dịch trái, dịch phải, cộng trừ nhân chia, nhớ trong quá trình tính toán...
Trong mỗi thanh ghi như vậy lại chia ra 2 thanh ghi con để phục vụ tính toán. Ví dụ AX gồm có AH và AL. AH (8bit cao high trong AX) và AL (8 bit thấp Low trong AX)
+ CS, DS, ES, FS, GS, SS: Nhóm thanh ghi này chính là cha đẻ của con trỏ trong ngôn ngữ C/C++. Tác dụng chính của nó là quản lý bộ nhớ ở vùng thấp LOWMEMORY.
+ CS: Chứa địa chỉ bắt đầu của code trong chương trình.
+ DS: Chứa địa chỉ của các biến khai báo trong chương trình.
+ SS: Chứa địa chỉ của bộ nhớ Stack dùng trong chương trình.
+ ES: Chứa địa chỉ cơ sở bổ sung cho các biến bộ nhớ (Heap).
-> Stack và Heap là gì tôi sẽ phân tích sau.
- Nhóm 32 bit.
+ EAX, EBX, ECX, EDX,...: Giống AX,BX,CX,DX nhưng có độ rộng là 32 bit để phục vụ quá trình tính toán.
- Nhóm 48 bit, 16bit (quản lý Memory).
Những tài liệu tiếng việt rất ít nhắc tới những nhóm thanh ghi trong CPU này trong khi nó rất quan trọng. Có lẽ là vì nó chỉ có ở những CPU 8088 trở về sau cho đến Pentium mà thôi mà thôi.
+ GDTR (48bit), LDTR (48bit): là những thanh ghi chuyên quản lý HighMemory.
+ IDTR (16bit), TR (16bit): Nhóm 2 thanh ghi này chuyên quản lý ngắt (interrupts) và phân luồng xử lý (multitasking).
Và cuối cùng là các thanh ghi cờ (FLAGS) và một số thanh ghi điều khiển (CONTROL)
* Cache:
Bao gồm 2 loại Cache. Level 1 (L1) và Level 2 (L2).
- L1 nằm ngay trong CPU. CPU sử dụng nó để lưu trữ một số thông tin trong quá trình xử lý và L1 càng lớn thì cũng đồng nghĩa là “tốc độ xử lý sẽ nhanh”. Đó là lý do 1 con CPU Pentium IV và 1 con CPU Celeron cùng 1 tốc độ nhưng P4 đắt hơn rất nhiều là đo L1 lớn. CPU 2 Core sẽ có cách L1x2.
- Khi L1 đầy thì CPU sẽ sử dụng tiếp L2. L2 nằm ngay trên Mainboard do đó tốc độ truy xuất L2 sẽ chậm hơn L1.
* RAM
- Là bộ nhớ chính của CPU và tốc độ truy xuất RAM (Level 3) chậm hơn L2. Bởi vì RAM cấu tạo chính là các tụ điện vô cùng nhỏ xếp liên tiếp nhau đại diện cho từng bit dữ liệu. Khi tụ được nạp điện có nghĩa là trạng thái 1 và ngược lại. Vì là tụ điện nên nó sẽ xả mất điện trong khoảng thời gian t giây. Do vậy nó cần 1 khoảng thời gian làm tươi (refesh rate) để nạp điện lại và do đó CPU phải chờ đợi mới mà không thể truy xuất tức ngay lập tức được. Do đó máy tính muốn chạy ổn định trên 2 thanh RAM khác nhau thì buộc 2 thanh RAM này phải cùng 1 tầng số làm tươi.
* Disk Drive
- RAM đầy! một điều luôn xảy ra khi chạy các ứng dụng lớn. Hệ điều hành sẽ giả lập HARDDISK để làm RAM. Và khái niệm này là Virtual RAM. Tốc độ truy xuất trên HDD rất chậm bởi vì khi truy xuất nó có một sự di chuyển của cơ học.
II. TỔ CHỨC DỮ LIỆU TRÊN RAM
Khi xét về con trỏ chúng ta chỉ xét tới RAM. Bởi vì con trỏ của C/C++ chỉ có tác dụng trên RAM chứ không thể nào truy xuất vào L1 và L2 được.
Mỗi BYTE (8bit) trên RAM sẽ được đánh địa chỉ từ thấp lên cao.
Các ngôn ngữ khác chỉ có thể biểu diễn giá trị thực của BYTE trên RAM. Còn riêng với C/C++ thì có thể biễu diễn địa chỉ của của chính BYTE đó và nó chính là biến con trỏ.
Ví dụ:
Code:
typedef struct unsigned char BYTE;
BYTE far* a = 0x2; // Địa chỉ 2 trên hình
cout << (BYTE)*a;
// Nếu RAM hiện tại giống trên hình thì sẽ in ra giá trị 93
// vì 1011101b = 5Dh = 93d
Như vậy con trỏ far chính là con trỏ chính xác trên RAM. Và muốn truy xuất được dữ liệu trên RAM với địa chỉ xác định thì chỉ có một cách là dùng con trỏ far.
Vậy con trỏ bình thường như BYTE *a khác con trỏ BYTE far* a; như thế nào?
Hiện nay, các CPU có 2 kỹ thuật chính để quản lý bộ nhớ là segmentation và paging .
Segmentation: Là cách chia Memory thành nhiều đoạn (segments) liên tục. và Mỗi chương trình khi chạy sẽ được cấp phát 1 hoặc nhiều đoạn Segments. Nhờ vậy mà mỗi chương chình độc lập về bộ nhớ lẫn nhau, nó không thể can thiệp vào vùng nhớ của chương trình khác bằng con trỏ. Lưu ý một điều là không phải lúc nào 1 ứng dụng cũng chỉ là 1 Segment. Một ứng dụng lớn sử dụng nhiều tài nguyên có thể chiếm tới rất nhiều Segments.
- Nếu đứng từ dười lên cao. LowMemory (là 1 MB đầu tiên của RAM) dành riêng để nạp các file Hệ điều hành và CPU truy xuất phần này một cách trực tiếp qua hệ thống BUS không cần các thiết bị điều khiển (nhanh hơn phần khác trên RAM).
- Phần HighMemory chính là phần của ứng dụng. Nó chia ra từng đoạn như vậy. Trong từng Page bao gồm code của chương trình và một khoảng data cho chúng ta khai báo biến.
Ở HDH MSDOS thì phần data này chỉ có 64KB. Do đó nên khai báo một cái mảng double aMang[1000000]; chắc chắn sẽ không bao giờ được. Với Windows thì 4MB nên chúng ta được tự do hơn 1 xíu. Và con trỏ * bình thường mà chúng ta hay dùng chỉ quản lý dữ liệu ở phần Data của chương trình chúng ta mà thôi. Có lẽ tới đây các bạn đã hình dung được sự khác nhau của far point và point.
Paging: Là cách mà CPU giả lập những Segments thành những Page trên HDD, dĩ nhiên nó chỉ sử dụng phương pháp này khi RAM đã đầy. Và tốc độ máy tính lúc này sẽ rất chậm bởi khả năng truy xuất dữ liệu HDD chậm hơn RAM rất nhiều lần.
* Chúng ta sẽ nhìn cây RAM một cách chính xác hơn dưới HDH của MS.
Đây là mô hình tổ chức Memory của MSDOS và WINDOWS (LINUX cũng tương tự nhưng khác đi một chút vì kernel của Linux chắn chắn phải khác Windows rồi).
Tôi xin nói lại là RAM chia ra làm 2 vùng rõ ràng. LowMemory (từ 0 -> 0x9FFFF) và HighMemory (từ 0x9FFFF đến hết RAM -> phần Extended);
Tại sao phải chia làm 2 phần như vậy:
- Phần LowMemory là phần mà CPU có thể truy xuất trực tiếp tới RAM bằng hệ thống BUS của mình. Có lẽ sẽ dể hiểu hơn với cái hình này. Số lượng Address Lines chính là số bit của CPU. Ví dụ CPU16bit (hàm này hiếm rồi) thì sẽ có 16 đường BUS. Nó sẽ truy xuất RAM bằng cách nhảy mỗi step là 16bit (64kb/1 lần truy xuất) và chúng ta có thể tính được số lần truy để CPU có thể truy xuất tới 1 địa chỉ nào đó trên RAM ở phần LowMemory. Tương tự với CPU32.
- Phần HighMemory là phần mà CPU muốn truy xuất được phải thông BUS hệ thống. Do đó phải cài 1 XMS DRIVER. Với MS DOS thì có tên là EMM386.EXE và HIMEM.SYS (2 chương trình quá quen thuộc với những ai hay đi cài đặt máy tính) .
1. Khảo sát LowMemory
+ Trong 1KB đầu tiên của RAM (0x0 – 0x3FF) đó chính là bảng VECTOR ngắt mềm của MSDOS dùng để điều khiển phần cứng. Mỗi ngắt có nhiệm vụ riêng như ngắt màn hình, ngắt phím… các vector ngắt này rất nhiều tôi cũng không rành lắm, nếu bạn nào thích SYSTEM PROGRAMING hoặc lập trình ASM thì quá rành. Bảng Vector Ngắt này viết rất rõ trong cuốn Guild Techhelp của hãng NORTON, mình thấy gần như là đầy đủ.
+ 256 byte tiếp theo (0x400 – 0x4FF) Chứ thông tin bảng VECTOR ngắt cứng và địa chỉ PORT của cổng COM, PRINT…
+ 0x500 – 0xFFFF (64KB) là phần để nạp chương trình HDH MSDOS và để CPU thi hành nó. Khi MS DOS khởi động thì 3 tập tin được nạp lên phần này chính là IO.SYS, MSDOS.SYS, COMMAND.COM, và số dung lượng trống còn rất ít (489K). Do vậy nếu không cài XMS DRIVER thì không thể nào chạy được các ứng dụng lớn.
Thông tin khi mình DEBUG (Trên HDH WINDOWS) phần Conventional Memory (LowMemory). Bạn có thểm xem bằng lệnh mem. Chỉ ưu tiên nạp các ứng dụng HĐH, trong đó MS DOS chiếm gần hết rồi.
Code:
E:\DOCUME~1\EXECUT~1>mem /PROGRAM
Address Name Size Type
------- -------- ------ ------
000000 000400 Interrupt Vector
000400 000100 ROM Communication Area
000500 000200 DOS Communication Area
000700 IO 000370 System Data
000A70 MSDOS 001670 System Data
0020E0 IO 0020C0 System Data
KBD 000CE0 System Program
HIMEM 0004E0 DEVICE=
000490 FILES=
000090 FCBS=
0001B0 LASTDRIVE=
0007D0 STACKS=
0041B0 COMMAND 000A20 Program
004BE0 MSDOS 000070 -- Free --
004C60 COMMAND 0005A0 Environment
005210 MEM 0004E0 Environment
005700 MEM 0174E0 Program
01CBF0 MSDOS 0833F0 -- Free --
09FFF0 SYSTEM 02B000 System Program
0CB000 IO 003100 System Data
MOUSE 0030F0 System Program
0CE110 MSDOS 0004D0 -- Free --
0CE5F0 MSCDEXNT 0001D0 Program
0CE7D0 REDIR 000A70 Program
0CF250 DOSX 0087A0 Program
0D7A00 DOSX 000080 Data
0D7A90 MSDOS 018560 -- Free --
655360 bytes total conventional memory
655360 bytes available to MS-DOS
633056 largest executable program size
1048576 bytes total contiguous extended memory
0 bytes available contiguous extended memory
941056 bytes available XMS memory
MS-DOS resident in High Memory Area
E:\DOCUME~1\EXECUT~1>
Ví dụ như ứng với với 1024KB đầu tiên trên RAM. Chúng ta biết rằng bộ nhớ của màn hình nằm từ vị trí B800:0000 đến B800:0F9F. Như vậy chúng ta hoàn toàn có thể giả lập lệnh clrscr() bằng cách set zeromemory khoảng bộ nhớ này hay printf(). Hoặc lập trình thành một đối tượng chuyên dụng hơn như cout của C++.
1 đoạn CODE lấy ký tự tại tọa độ y, x của của sổ console như sau (Lập trình trên BORLAND C++):
Code:
struct cell{
char chr;
char color;
};
cell far* scr = (cell far* )MK_FP(0xB800, 0);
cell chr = scr [(y-1)*80 + x-1];
-> chr.chr: byte chứa mã ASCII của ký tự tại tọa độ dòng y, cột x
-> chr .color: byte chứa màu của ký tự và màu nền tại tọa độ dòng y, cột x
Bạn nào đã từng lập trình hệ thống thì chắc có thể biết điều này.
Tôi cũng xem một số code của PASCAL. Nó có thể làm được điều này nhưng dưới dạng ngôn ngữ ASM bằng cách gọi ngắt nhưng như vậy thì cũng chẳng còn gì hay.
2. Khảo sát phần HighMemory
Đây là cái nhìn tổng quát hơn về tổ chức RAM. Phần Protected Mode chính là HighMemory. Các chương trình lớn đều được nạp và chạy ở trên phần này.
Và trong vùng Application thì lại được tổ chức bộ nhớ của như sau:
Bao gồm:
- CODE: của chương trình dưới dạng mã máy.
- DATA: Biến toàn cục được khai báo trong chương trình của chúng ta.
- HEAP: lưu trữ bộ nhớ do chương trình cấp phát.
Ví dụ như khi ta khai báo:
int *i = new int[100];
Thì CPU sẽ tìm một khoảng trống trên HEAP có dung lượng 100*sizeof(int) = 200byte. i mang địa chỉ đầu tiên của vùng nhớ này và biến con trỏ i được đưa vào STACK. Do đó vùng nhớ trên HEAP sẽ tồn tại cho đến khi chúng ta gọi delete i.
- STACK: Lưu trữ các biến tạm như trong hàm. Khi kết thúc hàm thì nó sẽ tự động giải phóng.
Bookmarks