Con trỏ (Pointer) là một trong những phần quan trọng nhất để học sinh hiểu C++ ở mức sâu hơn việc chỉ biết viết cú pháp, bởi nó cho thấy chương trình thực sự liên hệ với bộ nhớ như thế nào: một biến nằm ở một vị trí, địa chỉ của biến có thể được lưu lại, và thông qua địa chỉ đó ta có thể đọc hoặc thay đổi chính dữ liệu gốc. Khi nắm chắc &, *, nullptr, quan hệ giữa mảng và con trỏ, cách truyền địa chỉ vào hàm, chuỗi kiểu C, new/delete và vòng đời vùng nhớ, học sinh sẽ hiểu rõ hơn vì sao nhiều đoạn C++ hoạt động đúng hoặc sai thay vì chỉ ghi nhớ mẫu code. Phần con trỏ cũng rèn khả năng trace chương trình rất tốt, vì mỗi bước đều buộc người học phân biệt giá trị, địa chỉ, đối tượng đang sống và pointer đang trỏ tới đâu. Mục tiêu của chuyên đề này không phải dẫn sang linked list, tree hay các cấu trúc dữ liệu phức tạp, mà là làm cho nền tảng Pointer thật chắc; khi học sinh có thể tự giải thích p, *p, &p, p+i, T**, one-past, dangling pointer, memory leak và use-after-free một cách tự nhiên thì các phần C++ và lập trình thi đấu sau này sẽ dễ tiếp cận hơn rất nhiều.

Đăng nhập để tham gia lộ trình luyện tập

Some problems in the training are missing or you do not have permission to view them.

251, 252, 253, 254, 255

CON TRỎ TRONG C++

Mục tiêu là hiểu con trỏ lưu gì, * và & tác động thế nào, cách con trỏ liên hệ với mảng, hàm, chuỗi kiểu C và vùng nhớ cấp phát động. Đây không phải chuyên đề Two Pointers, cũng không mở rộng sang linked list, tree, graph, stack, queue hay các cấu trúc dữ liệu phức tạp.

Con trỏ là một biến dùng để lưu địa chỉ của một đối tượng. Nếu p đang trỏ tới x thì:

  • p là địa chỉ của x;
  • *p là đối tượng nằm tại địa chỉ đó;
  • &x là địa chỉ của x;
  • &p là địa chỉ của chính biến con trỏ p.

Quan hệ cơ bản:

p=&xp = \&x ∗p=x*p = x

Hai biểu thức trên mô tả quan hệ địa chỉ và giải tham chiếu, không có nghĩa p và x cùng kiểu dữ liệu.

Giai đoạn 1 - Địa chỉ và giải tham chiếu

1. Địa chỉ của biến

Toán tử lấy địa chỉ & trả về địa chỉ của một đối tượng.

int x = 10;
int* p = &x;

Ở đây p có kiểu int*, nghĩa là con trỏ có thể trỏ tới một đối tượng kiểu int.

Có thể hình dung:

x  = 10
&p = địa chỉ riêng của biến p
p  = địa chỉ của x
*p = 10

Không nên học thuộc địa chỉ thật như 0x... vì địa chỉ cụ thể có thể thay đổi giữa các lần chạy.

2. Giải tham chiếu

Toán tử * dùng để truy cập đối tượng mà con trỏ đang trỏ tới.

int x = 10;
int* p = &x;

*p = 25;

Sau lệnh trên:

x == 25
*p == 25

Sửa *p chính là sửa đối tượng gốc.

3. Phân biệt p, *p, &p

Đây là ba khái niệm khác nhau:

int x = 7;
int* p = &x;
  • p: địa chỉ của x;
  • *p: giá trị của x;
  • &p: địa chỉ của biến con trỏ p.

Mã trace cơ bản:

tạo biến x

cho p trỏ tới x

đọc p:
    nhận địa chỉ của x

đọc *p:
    nhận giá trị của x

đọc &p:
    nhận địa chỉ của chính biến p

4. Hai con trỏ cùng trỏ một đối tượng

Nhiều con trỏ có thể cùng giữ một địa chỉ. Hiện tượng này thường gọi là bí danh địa chỉ (aliasing).

int x = 5;

int* p = &x;
int* q = &x;

*p = 20;

Khi đó:

x == 20
*q == 20

p và q không phải cùng một biến, nhưng chúng cùng trỏ tới một đối tượng.

5. Đổi mục tiêu của con trỏ

Thay đổi p và thay đổi *p là hai việc hoàn toàn khác nhau.

int x = 1;
int y = 2;

int* p = &x;

p = &y;

Lệnh p = &y làm p trỏ sang y.

Trong khi đó:

*p = 100;

sửa giá trị của đối tượng đang được p trỏ tới.

6. Con trỏ rỗng nullptr

Trong C++ hiện đại, khi một con trỏ chưa trỏ tới đối tượng hợp lệ, nên dùng nullptr.

int* p = nullptr;

Không được giải tham chiếu một con trỏ rỗng:

*p = 5; // sai

Mẫu an toàn:

nếu p khác nullptr:
    mới được dùng *p

Trong C++:

if (p != nullptr) {
    cout << *p;
}

nullptr chỉ biểu diễn trạng thái không trỏ tới đối tượng nào. Nó không tự phát hiện con trỏ treo sau khi một vùng nhớ đã hết vòng đời.

7. Kích thước con trỏ và kích thước đối tượng

sizeof(p) là kích thước của biến con trỏ.

sizeof(*p) là kích thước của đối tượng mà p trỏ tới.

int x = 0;
int* p = &x;

cout << sizeof(p);
cout << sizeof(*p);

Không được suy luận rằng mọi hệ thống luôn có cùng kích thước con trỏ.

Giai đoạn 2 - const, reference và pointer

1. Con trỏ tới dữ liệu chỉ đọc

Dạng:

const int* p;

hoặc:

int const* p;

Hai cách viết trên có cùng ý nghĩa.

p có thể đổi sang địa chỉ khác, nhưng không được sửa đối tượng thông qua p.

int x = 10;
int y = 20;

const int* p = &x;

p = &y;   // hợp lệ
// *p = 5; // không hợp lệ

Có thể đọc từ phải sang trái theo ý nghĩa:

p là pointer tới const int

2. Con trỏ cố định địa chỉ

Dạng:

int* const p = &x;

p bắt buộc được khởi tạo ngay và không thể trỏ sang địa chỉ khác.

Tuy nhiên dữ liệu đích vẫn sửa được.

int x = 10;
int y = 20;

int* const p = &x;

*p = 30;  // hợp lệ
// p = &y; // không hợp lệ

3. Con trỏ cố định tới dữ liệu chỉ đọc

Dạng:

const int* const p = &x;

Cả hai đều bị khóa:

  • không đổi địa chỉ trong p;
  • không sửa *p thông qua p.

4. Cách nhớ ba dạng const

const int* p;

Dữ liệu qua p không sửa được, nhưng p có thể đổi mục tiêu.

int* const p = &x;

p không đổi mục tiêu, nhưng *p sửa được.

const int* const p = &x;

Cả địa chỉ lẫn dữ liệu qua pointer đều không đổi được.

5. Pointer và reference

Reference:

int& r = x;

Pointer:

int* p = &x;

Khác biệt thực chiến quan trọng:

  • pointer có thể là nullptr;
  • pointer có thể đổi sang đối tượng khác;
  • pointer truy cập dữ liệu bằng *p;
  • reference hoạt động như một bí danh của đối tượng sau khi được gắn.

Ví dụ hai hàm cùng sửa một biến:

void tangBangPointer(int* p) {
    if (p != nullptr) {
        ++(*p);
    }
}

void tangBangReference(int& x) {
    ++x;
}

Khi một tham số có thể không tồn tại, pointer biểu diễn trạng thái đó tự nhiên hơn bằng nullptr.

Giai đoạn 3 - Con trỏ với mảng một chiều

1. Tên mảng và phần tử đầu

int a[4] = {10, 20, 30, 40};

Trong nhiều biểu thức, a được chuyển đổi thành con trỏ tới phần tử đầu:

int* p = a;

tương đương:

int* p = &a[0];

Nhưng cần phân biệt:

a
&a[0]
&a

a và &a[0] thường cho cùng địa chỉ phần tử đầu trong ngữ cảnh giá trị, còn &a có kiểu con trỏ tới cả mảng, không phải int*.

2. Công thức chỉ số và con trỏ

Với mảng a:

a[i]=∗(a+i)a[i] = *(a+i)

Ví dụ:

int a[4] = {10, 20, 30, 40};

cout << *(a + 2);

kết quả là 30.

3. Số học con trỏ

Nếu p là int* thì:

p + 1

không có nghĩa tăng địa chỉ thêm đúng 1 byte.

Nó có nghĩa: chuyển sang phần tử int kế tiếp.

Tổng quát:

p+kp+k

là vị trí cách p đúng k phần tử cùng kiểu trong cùng một mảng hợp lệ.

Mã giả:

p trỏ tới phần tử đầu

trong khi p chưa tới cuối:
    xử lý *p
    tăng p sang phần tử kế tiếp

4. Duyệt mảng bằng con trỏ chạy

int a[5] = {2, 4, 6, 8, 10};

int* p = a;
int* end = a + 5;

while (p != end) {
    cout << *p << ' ';
    ++p;
}

Ở đây end = a + 5 là con trỏ one-past, tức vị trí ngay sau phần tử cuối.

Có thể tạo và so sánh end, nhưng không được giải tham chiếu:

*end // sai

5. Khoảng [begin, end)

Đây là cách biểu diễn rất quan trọng trong C++.

Khoảng:

[begin,end)[begin,end)

bao gồm mọi phần tử từ begin đến trước end.

Độ dài:

n=end−beginn=end-begin

Điều kiện quan trọng: phép trừ hai con trỏ chỉ có ý nghĩa khi chúng cùng nằm trong một mảng hợp lệ, hoặc ở vị trí one-past của chính mảng đó.

Trong C++ nên dùng kiểu:

std::ptrdiff_t

cho kết quả hiệu hai con trỏ.

6. Template duyệt pointer range

long long tong(const int* begin, const int* end) {
    long long s = 0;

    while (begin != end) {
        s += *begin;
        ++begin;
    }

    return s;
}

Cách gọi:

int a[] = {3, 5, 7, 9};

cout << tong(a, a + 4);

7. Tìm phần tử và trả địa chỉ

Khi cần xác định chính phần tử nằm trong mảng, pointer có thể đóng vai trò như vị trí.

const int* timMin(const int* begin, const int* end) {
    if (begin == end) {
        return nullptr;
    }

    const int* best = begin;

    for (const int* p = begin + 1; p != end; ++p) {
        if (*p < *best) {
            best = p;
        }
    }

    return best;
}

Kết quả không chỉ cho giá trị nhỏ nhất mà còn cho địa chỉ của phần tử đó trong vùng nhớ của mảng.

Giai đoạn 4 - Con trỏ với hàm

1. Truyền địa chỉ để hàm sửa biến gốc

void gan0(int* p) {
    if (p != nullptr) {
        *p = 0;
    }
}

Gọi:

int x = 15;
gan0(&x);

Hàm nhận bản sao của địa chỉ &x, nhưng *p vẫn truy cập đúng đối tượng x.

2. Hoán đổi hai biến bằng pointer

void swapPtr(int* a, int* b) {
    int t = *a;
    *a = *b;
    *b = t;
}

Điểm quan trọng không nằm ở bài hoán đổi, mà ở cơ chế:

&a và &b của hàm gọi
        ↓
được truyền vào pointer
        ↓
dereference để sửa đối tượng gốc

3. Pointer được truyền theo giá trị

Xét:

void doiPointer(int* p, int* q) {
    p = q;
}

Nếu gọi:

int x = 1;
int y = 2;

int* p = &x;
int* q = &y;

doiPointer(p, q);

thì pointer p ở hàm gọi không đổi.

Lý do: biến p bên trong hàm chỉ là một bản sao địa chỉ.

Mã giả:

caller có pointer p

gọi hàm(p)

hàm nhận một bản sao của p

đổi bản sao:
    không đổi pointer p ở caller

4. Khi cần thay pointer của bên gọi

Nếu thật sự muốn hàm thay địa chỉ mà pointer ở caller đang giữ, có thể dùng con trỏ cấp hai T**.

void troSang(int** pp, int* dich) {
    *pp = dich;
}

Gọi:

int x = 1;
int y = 2;

int* p = &x;

troSang(&p, &y);

Sau đó p trỏ tới y.

Cơ chế:

pp   = địa chỉ của biến p
*pp  = chính biến p
**pp = đối tượng mà p đang trỏ tới

5. Hàm nhận mảng chỉ đọc

long long tong(const int* a, int n) {
    long long s = 0;

    for (int i = 0; i < n; ++i) {
        s += *(a + i);
    }

    return s;
}

const int* thể hiện rõ rằng hàm chỉ đọc dữ liệu.

6. Hàm trả pointer vào vùng nhớ của bên gọi

Đây là trường hợp hợp lệ:

const int* tim(const int* a, int n, int x) {
    for (int i = 0; i < n; ++i) {
        if (*(a + i) == x) {
            return a + i;
        }
    }

    return nullptr;
}

Pointer trả về vẫn hợp lệ miễn là mảng gốc còn sống.

7. Không trả địa chỉ biến local

Sai:

int* tao() {
    int x = 10;
    return &x;
}

x là biến cục bộ có thời hạn lưu trữ tự động. Khi hàm kết thúc, vòng đời của x kết thúc.

Pointer trả về trở thành con trỏ treo (dangling pointer).

Không được dựa vào việc chương trình "chạy được trên máy mình" để kết luận đoạn code này hợp lệ.

Giai đoạn 5 - Con trỏ với mảng ký tự

1. Chuỗi kiểu C và ký tự kết thúc

Mảng ký tự kiểu C kết thúc bằng ký tự:

'\0'

Ví dụ:

char s[] = "ABC";

Trong bộ nhớ có bốn ký tự:

'A' 'B' 'C' '\0'

2. Duyệt chuỗi bằng pointer

const char* p = s;

while (*p != '\0') {
    cout << *p;
    ++p;
}

Điều kiện kết thúc không dựa vào kích thước pointer mà dựa vào ký tự '\0'.

3. Tính độ dài chuỗi

int doDai(const char* s) {
    const char* p = s;

    while (*p != '\0') {
        ++p;
    }

    return int(p - s);
}

Nếu p dừng đúng tại ký tự '\0' thì:

length=p−slength=p-s

4. Chuỗi chỉ đọc và chuỗi có thể sửa

Chuỗi nằm trong mảng:

char s[] = "abc";

có thể sửa:

s[0] = 'A';

Nhưng không nên dùng pointer không const để sửa string literal.

Dùng:

const char* p = "abc";

và chỉ đọc dữ liệu.

5. Sao chép chuỗi

Khi sao chép C-string phải sao chép cả ký tự kết thúc '\0'.

void copyChuoi(char* dst, const char* src) {
    while (*src != '\0') {
        *dst = *src;
        ++dst;
        ++src;
    }

    *dst = '\0';
}

Điều kiện tiên quyết: vùng đích phải đủ chỗ cho toàn bộ ký tự và thêm một vị trí cho '\0'.

Giai đoạn 6 - Cấp phát động và vòng đời

1. Cấp phát một đối tượng

int* p = new int(10);

new tạo một đối tượng có thời hạn lưu trữ động và trả về địa chỉ của đối tượng đó.

Sử dụng:

cout << *p;

Giải phóng:

delete p;
p = nullptr;

2. Cấp phát mảng động

Khi số phần tử chỉ biết lúc chạy:

int n;
cin >> n;

int* a = new int[n];

Giải phóng đúng cặp:

delete[] a;
a = nullptr;

Quy tắc ghép cặp:

new      -> delete
new[]    -> delete[]

Không được trộn hai cặp này.

3. Duyệt mảng động

Sau:

int* a = new int[n];

ta có thể dùng số học con trỏ giống mảng liên tiếp:

for (int* p = a; p != a + n; ++p) {
    cin >> *p;
}

4. new[] không tự thay đổi kích thước

Raw dynamic array không có thao tác resize tự động.

Muốn tăng kích thước, cách cơ bản là:

cấp phát vùng mới lớn hơn

sao chép dữ liệu cần giữ

delete[] vùng cũ

cho pointer chính trỏ sang vùng mới

Mẫu C++:

int* b = new int[moi];

for (int i = 0; i < min(cu, moi); ++i) {
    b[i] = a[i];
}

delete[] a;
a = b;

Sau delete[] a cũ, mọi pointer phụ đang trỏ vào vùng nhớ cũ đều không còn hợp lệ.

5. Rò rỉ bộ nhớ

Rò rỉ bộ nhớ (memory leak) xảy ra khi vùng nhớ động vẫn tồn tại nhưng chương trình không còn giữ được pointer cần thiết để giải phóng nó.

Ví dụ sai:

int* p = new int(5);

p = new int(10);

Địa chỉ vùng đầu tiên đã bị mất trước khi delete.

Mẫu suy nghĩ:

trước khi gán đè một pointer đang sở hữu vùng nhớ:
    hỏi vùng cũ đã được delete chưa?

6. Con trỏ treo sau delete

int* p = new int(7);
int* q = p;

delete p;
p = nullptr;

Sau đó:

  • đối tượng đã hết vòng đời;
  • p là nullptr;
  • q vẫn giữ địa chỉ cũ nhưng địa chỉ đó không còn chỉ tới một đối tượng int đang sống;
  • q là dangling pointer.

Không được:

cout << *q;

Đặt p = nullptr chỉ thay đổi biến p, không tự động cập nhật mọi alias.

7. Thời hạn lưu trữ và "stack/heap"

Trong học con trỏ thường nghe:

  • biến local nằm trên "stack";
  • new lấy vùng nhớ từ "heap".

Đây là mô hình triển khai phổ biến và hữu ích để hình dung, nhưng chuẩn C++ quan trọng hơn ở khái niệm thời hạn lưu trữ (storage duration) và vòng đời đối tượng (object lifetime).

Điều cần nhớ:

biến local:
    thường tự kết thúc vòng đời khi rời scope

đối tượng tạo bởi new:
    tồn tại tới khi được delete phù hợp

Giai đoạn 7 - Con trỏ cấp hai và an toàn bộ nhớ

1. Con trỏ cấp hai T**

int x = 10;
int* p = &x;
int** pp = &p;

Quan hệ:

p    = địa chỉ của x
*p   = x

pp   = địa chỉ của p
*pp  = p
**pp = x

Có thể viết:

∗∗pp=x**pp=x

khi pp trỏ đúng tới p và p trỏ đúng tới x.

2. Sửa dữ liệu qua hai tầng dereference

**pp = 50;

khi đó x trở thành 50.

Trace:

pp
↓
p
↓
x

Mỗi dấu * đi xuống một tầng gián tiếp.

3. Cấp phát mảng trong hàm bằng int**

void taoMang(int** out, int n) {
    *out = new int[n];
}

Gọi:

int* a = nullptr;

taoMang(&a, n);

Sau lời gọi, a giữ địa chỉ vùng nhớ mới.

Bên gọi chịu trách nhiệm:

delete[] a;
a = nullptr;

4. Các lỗi con trỏ thường gặp

Con trỏ chưa khởi tạo

Sai:

int* p;
*p = 5;

p đang chứa giá trị không xác định.

Nên khởi tạo:

int* p = nullptr;

hoặc gán ngay một địa chỉ hợp lệ.

Giải tham chiếu nullptr

Sai:

int* p = nullptr;
cout << *p;

Vượt biên mảng

Sai:

int a[3];
int* p = a + 3;

cout << *p;

a + 3 là one-past nên có thể dùng làm mốc kết thúc, nhưng không được dereference.

Use-after-free

Sai:

int* p = new int(10);

delete p;

cout << *p;

delete sai loại

Sai:

int* a = new int[10];

delete a;

Đúng:

delete[] a;

Double delete

Sai:

int* p = new int(5);

delete p;
delete p;

Sau delete, không được giải phóng lại cùng đối tượng qua một pointer cũ.

5. AddressSanitizer trong GNU C++14

Trong lúc học pointer, AddressSanitizer rất hữu ích để phát hiện các lỗi như:

  • truy cập vượt biên;
  • use-after-free;
  • một số lỗi cấp phát/giải phóng;
  • truy cập bộ nhớ không hợp lệ.

Có thể biên dịch khi luyện tập:

g++ -std=c++14 -fsanitize=address,undefined -g main.cpp -o main

Sanitizer là công cụ hỗ trợ phát hiện lỗi, không thay thế việc hiểu vòng đời và tính hợp lệ của pointer.

6. Template tổng hợp mảng động bằng raw pointer

đọc n

cấp phát:
    a = new int[n]

nhập dữ liệu bằng pointer chạy

xử lý dữ liệu:
    dùng begin = a
    dùng end = a + n

nếu cần đổi kích thước:
    cấp phát vùng mới
    sao chép phần cần giữ
    delete[] vùng cũ
    cập nhật a sang vùng mới

khi không còn dùng:
    delete[] a
    a = nullptr

Đây là điểm kết thúc phù hợp của chuyên đề raw pointer cơ bản. Không cần đưa thêm linked list, cây, đồ thị hay các cấu trúc dữ liệu phức tạp để hiểu chắc con trỏ.

Ghi nhớ theo bản chất

&x lấy địa chỉ của x.

p = &x cho p trỏ tới x.

*p truy cập đối tượng mà p đang trỏ tới.

p + i di chuyển theo đơn vị phần tử cùng kiểu trong cùng một mảng hợp lệ.

a[i] tương đương *(a + i).

Khoảng [begin, end) dùng end làm mốc kết thúc và không dereference end.

Pointer truyền vào hàm vẫn được truyền theo giá trị; muốn thay pointer ở caller có thể dùng T**.

new ghép với delete.

new[] ghép với delete[].

nullptr là trạng thái không trỏ tới đối tượng nào; nó không tự phát hiện dangling pointer.

Sau khi object hết vòng đời, mọi pointer còn giữ địa chỉ cũ của object đó đều không được dereference.

Phần 1. Cơ Bản

Mở

Bài toán Tried AC Độ khó
251   *(Ẩn) 0 0 (Không có)
252   *(Ẩn) 0 0 (Không có)
253   *(Ẩn) 0 0 (Không có)
254   *(Ẩn) 0 0 (Không có)
255   *(Ẩn) 0 0 (Không có)
PCBNAM0000001   Dòng thông báo (Message Line) 2 1 1
PCBNAM0000002   Mã hồ sơ (Record Code) 0 0 1
PCBNAM0000003   Số nguyên có dấu (Signed Integer) 0 0 1
PCBNAM0000004   Số nguyên có số 0 đầu (Integer with Leading Zeroes) 0 0 1
PCBNAM0000005   Họ và tên nguyên dòng (Full Name as a Line) 0 0 1
PCBNAM0000006   Hai số nguyên trên một dòng (Two Integers on One Line) 0 0 1
PCBNAM0000007   Đảo thứ tự ba số (Reverse Three Integers) 0 0 1
PCBNAM0000008   Nhiều khoảng trắng giữa các số (Multiple Spaces Between Integers) 0 0 1
PCBNAM0000009   Tên và tuổi trên hai dòng (Full Name and Age on Two Lines) 1 1 1
PCBNAM0000010   Kích thước và dãy số nguyên (Array Size and Integers) 0 0 1
PCBNAM0000011   Số lượng và hai bản sao dãy (Two Copies of an Integer Array) 0 0 1
PCBNAM0000012   Đối chiếu N bằng len (Compare Declared and Actual Counts) 0 0 1
PCBNAM0000013   Token rỗng với dấu phân cách tường minh (Empty Field with Explicit Delimiter) 0 0 1
PCBNAM0000014   Dấu phẩy kèm khoảng trắng (Comma-Separated Integers with Spaces) 0 0 1
PCBNAM0000015   Số nguyên có dấu cộng (Signed Integers with Plus Signs) 0 0 1
PCBNAM0000016   Số thực ký pháp khoa học (Scientific Notation Inputs) 0 0 1
PCBNAM0000017   Kho hàng (Warehouse Record) 0 0 1
PCBNAM0000018   Bản ghi phân cách hỗn hợp (Mixed-Delimiter Record) 0 0 1
PCBNAM0000019   Hồ sơ tuyển sinh (Admissions Record) 1 1 1
PCBSAU0000001   Bốn phép toán cơ bản (Four Basic Operations) 0 0 1
PCBSAU0000002   Thứ tự ưu tiên của biểu thức (Operator Precedence) 0 0 1
PCBSAU0000003   Thương và số dư (Quotient and Remainder) 0 0 1
PCBSAU0000004   Chia nguyên với số âm (Negative Floor Division) 0 0 2
PCBSAU0000005   Lũy thừa (Exponentiation) 0 0 2
PCBSAU0000006   Khoảng cách trên trục số (Distance on a Number Line) 0 0 2
PCBSAU0000007   Chuỗi phép gán kết hợp (Augmented Assignment Chain) 0 0 2
PCBSAU0000008   Lớn nhất và nhỏ nhất (Minimum and Maximum) 0 0 2
PCBSAU0000009   Tổng và tích ba số (Sum and Product of Three Integers) 0 0 2
PCBSAU0000010   Tổng từ 1 đến n (Sum of the First N Integers) 0 0 2
PCBSAU0000011   Số cái bắt tay (Handshake Count) 0 0 2
PCBSAU0000012   Chuyển đổi nhiệt độ (Temperature Conversion) 0 0 2
PCBSAU0000013   Đổi centimet thành mét và centimet (Centimetres to Metres and Centimetres) 0 0 2
PCBSAU0000014   Giờ, phút và giây (Hours Minutes and Seconds) 0 0 2
PCBSAU0000015   Tiền đặt cọc vỏ chai (Bottle Deposits) 0 0 3
PCBSAU0000016   Khối lượng kiện hàng (Parcel Weight) 0 0 3
PCBSAU0000017   Tách các chữ số (Extract Four Digits) 0 0 3
PCBSAU0000018   Diện tích và thể tích (Area and Volume) 0 0 3
PCBSAU0000019   Điểm trung bình có trọng số (Weighted Average Score) 0 0 3
PCBSAU0000020   Lãi kép (Compound Interest) 0 0 4
PCBSAU0000021   Quy đổi tiền tệ (Currency Conversion) 0 0 4
PCBSAU0000022   Hóa đơn mua hàng (Purchase Receipt) 0 0 4
PCBSAU0000023   Chia pizza (Pizza Sharing) 0 0 4
PCBSAU0000024   Tính số thùng sơn (Paint Can Calculator) 0 0 4
PCBSAU0000025   Xuất hai chữ số thập phân (Print Two Decimal Places) 0 0 2
PCBSAU0000026   Xuất k chữ số thập phân (Print K Decimal Places) 0 0 2