You are on page 1of 22

ÔN TẬP CUỐI KỲ MÔN KỸ THUẬT LẬP TRÌNH

Mục lục
Bài 1: Tổng quan về ngôn ngữ lập trình.........................................................................................................2
1. Có 3 thành phần căn bản của bất cứ 1 NNLT nào:..................................................................................2
2. Phân biệt giữa Compiler (biên dịch) và Interpreter (thông dịch).............................................................2
3. Các mô thức lập trình...............................................................................................................................2
4. C và C++..................................................................................................................................................3
Bài 2: Quản lý bộ nhớ.......................................................................................................................................3
1. Con trỏ (pointer).......................................................................................................................................3
2. Cấp phát bộ nhớ động...............................................................................................................................4
Bài 3: Hàm (Function)......................................................................................................................................6
1. Hàm và truyền tham số.............................................................................................................................6
2. Đa năng hóa hàm (Overloading)..............................................................................................................7
3. Đa năng hóa toán tử..................................................................................................................................7
4. Con trỏ hàm..............................................................................................................................................8
5. Khái quát hóa hàm (Function templates).................................................................................................9
6. Hàm nặc danh – cú pháp lambda..............................................................................................................9
Bài 4 + 5: Kỹ thuật viết mã nguồn hiệu quả và Phong cách lập trình........................................................11
1. Các kỹ thuật viết mã nguồn hiệu quả.....................................................................................................11
2. Static, Stack, Heap..................................................................................................................................12
3. Tính Sigmoid..........................................................................................................................................12
4. Một số quy tắc cơ bản trong lập trình.....................................................................................................13
Bài 6: Đệ quy và khử đệ quy..........................................................................................................................14
1. Phân loại đệ quy.....................................................................................................................................14
2. Đệ quy có nhớ và đệ quy quay lui..........................................................................................................15
3. Khử đệ quy.............................................................................................................................................16
Bài 7: Các cấu trúc dữ liệu.............................................................................................................................18
Bài 8: Bẫy lỗi và lập trình phòng ngừa..........................................................................................................19
1. Assertion.................................................................................................................................................19
2. Xử lý ngoại lệ.........................................................................................................................................20
Bài 9: Gỡ lỗi, kiểm thử và tinh chỉnh mã nguồn..........................................................................................21
1. Gỡ lỗi......................................................................................................................................................21
2. Kiểm thử.................................................................................................................................................21
3. Tinh chỉnh mã nguồn..............................................................................................................................22
Bài 1: Tổng quan về ngôn ngữ lập trình
1. Có 3 thành phần căn bản của bất cứ 1 NNLT nào:
 Mô thức lập trình là những nguyên tắc chung cơ bản, dùng bởi LTV để xây
dựng chương trình.
 Cú pháp của ngôn ngữ là cách để xác định cái gì là hợp lệ trong cấu trúc các
câu của ngôn ngữ (cách viết một chương trình hợp lệ)
 Ngữ nghĩa là xác định ý nghĩa thao tác cần phải thực hiên, ứng với tổ hợp kí tự
dựa vào ngữ cảnh của nó.
 Lỗi cú pháp được chương trình dịch phát hiện và thông báo cho người lập chương trình biết ,
chỉ có các chương trình không còn lỗi cú pháp mới có thể được dịch sang ngôn ngữ máy.
 Lỗi ngữ nghĩa chỉ được phát hiện khi thực hiện chương trình trên dữ liệu cụ thể .
2. Phân biệt giữa Compiler (biên dịch) và Interpreter (thông dịch)
 Compiler
 Quá trình biên dịch bao gồm việc dịch toàn bộ mã nguồn của chương trình thành
mã máy tương ứng. Kết quả là một file thực thi độc lập, có thể chạy trên hệ điều
hành mà không cần có compiler.
 Mã nguồn được biên dịch chỉ cần thực hiện một lần và có thể được chia sẻ và thực
thi trên nhiều hệ thống khác nhau mà không cần biên dịch lại.
 Ví dụ về chương trình được biên dịch là ngôn ngữ C/C++, Fortran, Pascal.
 Interpreter
 Interpreter đọc và thực thi mã nguồn lúc chương trình chạy, dịch từng câu lệnh
thành mã máy và thực thi ngay lập tức
 Mã nguồn được thông dịch mỗi lần chạy, không tạo ra file thực thi độc lập
 Ví dụ về chương trình được thông dịch là Python, Ruby.
Tuy nhiên, đôi khi các ngôn ngữ lập trình kết hợp cả compiler và interpreter. Ví dụ, ngôn ngữ
Java được biên dịch thành bytecode bởi compiler và sau đó được thực thi thông qua một
interpreter gọi là Java Virtual Machine (JVM). Tương tự đối với C#
3. Các mô thức lập trình
 Hướng mệnh lệnh (Imperative paradigm). Ví dụ: C
 Thành phần: các lệnh khai báo, lệnh gán, lệnh điều khiển chương trình, chia
chương trình thành các chương trình con (function)
 Hướng mệnh lệnh tập trung vào việc thay đổi trạng thái của các biến thông qua các
câu lệnh. Các câu lệnh được sắp xếp tuần tự và thực hiện theo từng bước.
 Hướng chức năng (Functional paradigm) Ví dụ: Racket, Haskell
 Thành phần: các CTDL và các hàm liên quan, các hàm cơ sở, các toán tử
 Đặc trưng cơ bản: modul hóa chương trình
 Hướng chức năng tập trung vào việc xác định các hàm và biểu thức toán học. Các
hàm không thay đổi trạng thái và sử dụng giá trị đầu vào để tính toán kết quả.
 Hướng logic (Logic paradigm) Ví dụ: Prolog
 Dựa trên các tiên đề, các quy luật suy diễn và các truy vấn. Ctr thực hiện từ việc
t.kiếm có hệ thống trong 1 tập các sự kiện, sử dụng 1 tập các luật để đưa ra kết luận
 Hướng đối tượng (Object-oriented programming) Ví dụ: Java, C++
 Tập trung vào đối tượng, gồm các thuộc tính (properties) và phương thức
(methods), để mô phỏng thế giới thực.
 Đối tượng được xem như một thực thể độc lập có khả năng tự thực hiện các hành
động và tương tác với nhau
4. C và C++
 Ngôn ngữ C
 Ra đời: 1970, gắn liền với sự phát triển của HĐH Unix. TG: Dennis Ritchie
 Mục tiêu: Đề cao tính hiệu quả, Có khả năng truy xuất phần cứng ở cấp thấp, Ngôn
ngữ có cấu trúc (thay cho lập trình bằng hợp ngữ)
 Ngôn ngữ C++
 Ra đời năm 1979 bằng việc mở rộng ngôn ngữ C. Tác giả: Bjarne Stroustrup
 Mục tiêu: Thêm các tính năng mới, Khắc phục một số nhược điểm của C
 Bổ sung những tính năng mới so với C:
 Lập trình hướng đối tượng (OOP)
 Lập trình tổng quát (template)
 Nhiều tính năng nhỏ giúp lập trình linh hoạt hơn nữa (thêm kiểu bool,
định nghĩa chồng hàm, namespace, xử lý ngoại lệ,...)

Bài 2: Quản lý bộ nhớ


1. Con trỏ (pointer)
 Con trỏ là một biến chứa địa chỉ vùng nhớ của một biến khác.
 Các phép toán trên con trỏ
 Cộng hoặc trừ với 1 số nguyên n trả về 1 con trỏ cùng kiểu, là địa chỉ mới trỏ tới 1
đối tượng khác nằm cách đối tượng đang bị trỏ n phần tử
 Trừ 2 con trỏ cho ta khoảng cách (số phần tử) giữa 2 con trỏ
 KHÔNG có phép cộng, nhân, chia 2 con trỏ
 Có thể dùng các phép gán, so sánh các con trỏ
 Ví dụ: int *pint; và pint đang chứa địa chỉ là 300 thì phép toán pint +=5 sẽ chứa địa
chỉ là 300 + 4*5 = 320
 ++ và -- có độ ưu tiên cao hơn * nên *p++ tương đương với *(p++) tức là tăng địa chỉ mà
nó trỏ tới chứ không phải tăng giá trị mà nó chứa.
 Con trỏ *void
 Là con trỏ không định kiểu. Nó có thể trỏ tới bất kì một loại biến nào.
 Để truy cập được đối tượng thì trước hết phải ép kiểu biến trỏ void thành biến trỏ
có định kiểu của kiểu đối tượng
 Ví dụ:
float x;
void *p; // khai báo con trỏ void
p = &x; // p chứa địa chỉ số thực x
*p = 2.5; // báo lỗi vì p là con trỏ void
*( (float*) p) = 2.5; // ép kiểu con trỏ *void x = 2.5
p = &y; // p chứa địa chỉ số nguyên y
*((int*)p) = 2; // y = 2
 Con trỏ và mảng
 Ví dụ với mảng int a[10] thì &a[0] là địa chỉ phần tử đầu tiên của mảng cũng là địa
chỉ của mảng. Và ta cũng có tên mảng a = &a[0];
 Tuy nhiên “a” là một hằng số nên không thể gán a++ để đi đến phần tử kế tiếp
được, khi đó ta dùng con trỏ int *pa; pa = &a[0];
pa +1 sẽ trỏ vào phần tử thứ 2 của mảng
*(pa+i) sẽ là nội dung của a[i]
 Tương tự đối với con trỏ và xâu

 Mảng các con trỏ


 Con trỏ trỏ tới con trỏ
 Ví dụ: int x = 12;
int *p1 = &x;
int **p2 = &p1; // con trỏ p2 trỏ tới con trỏ p1
 Đối với mảng hai chiều: M[i][k] = *(*(M+i)+k)
2. Cấp phát bộ nhớ động
 Cấp phát và thu hồi bộ nhớ động trong C và C++
 Trong C sử dụng các hàm malloc, calloc, realloc và free từ thư viện stdlib.h
Dùng malloc và calloc để cấp phát (malloc trả về *void còn calloc cho all ptử = 0 )
// cấp phát mảng số nguyên gồm 5 phần tử
int *array = (int*) malloc(5 * sizeof(int));
if (array == NULL) {
printf("Cấp phát không thành công do không đủ bộ nhớ");
}
// Giải phóng bộ nhớ
free(array);
Với mảng trên 5 phần tử, ta hoàn toàn có thể thay đổi thành 10 ptử, sử dụng realloc
 Trong C++ sử dụng toán tử new và delete
// Cấp phát một mảng số thực động có 5 phần tử
double* array = new double[5];
// Giải phóng bộ nhớ
delete[] array;
 Cấp phát bộ nhớ động cho mảng hai chiều
 Trong ngôn ngữ C++
Ví dụ khác để cấp phát động cho mảng hai chiều chứa các số thực float
// Khởi tạo ma trận với R hàng và C cột
float ** M = new float *[R];
for (i=0; i < R; i++)
M[i] = new float[C];
// Dùng M[i][j] cho các phần tử của ma trận
// Giải phóng
for(i=0; i<R; i++) delete [] M[i]; // Giải phóng các hàng
delete [] M;
 Trong ngôn ngữ C
float** mt;
// cap phat mang cac con tro cap 1
*mt = (float **) malloc(R* sizeof(float *));
int i;
for (i = 0; i < R; i++)
{
// cap phat cho tung con tro cap 1
(*mt)[i] = (float *) malloc(C* sizeof( float ));
}
// giai phong bo nho cho cac mang con tro cap 1 mt[i]
for (i = 0; i<m; i++) free(mt[i]);
// giai phong bo nho cho mang con tro mt
free(mt);
Link code ví dụ tham khảo: https://onlinegdb.com/dEbyMLK9d
Bài 3: Hàm (Function)
1. Hàm và truyền tham số
 Truyền tham trị (cả trong C và C++): Khi truyền tham trị, một bản sao của giá trị gốc được
tạo và truyền vào hàm. Bất kỳ thay đổi nào được thực hiện trên tham số trong hàm không
ảnh hưởng đến giá trị gốc.

 Truyền tham số bằng địa chỉ (thực hiện được cả trong C và C++) : Khi truyền tham số
bằng địa chỉ, địa chỉ của biến gốc được truyền vào hàm. Thay đổi được thực hiện trực tiếp
trên địa chỉ và ảnh hưởng đến giá trị gốc.

 Truyền tham chiếu (chỉ thực hiện được trong C++): Sử dụng toán tử & (tham chiếu), Khi
một hàm trả về một tham chiếu, chúng ta có thể gọi hàm ở phía bên trái của một phép
gán.
 Tham số ngầm định: Khi gọi hàm có nhiều tham số có giá trị mặc định, chúng ta chỉ có thể
bỏ bớt các tham số theo thứ tự từ phải sang trái và phải bỏ liên tiếp nhau
int MyFunc(int a = 1, int b, int c = 3, int d = 4); // ✖
int MyFunc(int a, int b = 2, int c = 3, int d = 4); // ✔
Ví dụ:
int get_value (int x, int a = 2, int b = 1, int c = 0) {
return a * x * x + b * x + c;
}
int x = 5 , a = 3, b = 4, c = 2 ;
printf("a=2, b=1, c=0: %d\n", get_value(x));
printf("a=%d, b=1, c=0: %d\n", a, get_value(x, a));
printf("a=%d, b=%d, c=0: %d\n", a, b, get_value(x, a, b));
printf("a=%d, b=%d, c=%d: %d\n", a, b, c, get_value(x, a, b, c));

2. Đa năng hóa hàm (Overloading)


Trong C, tên hàm là duy nhất. Trong C++ còn có cơ chế đa năng hóa hàm, vì vậy tên hàm
không phải duy nhất.
Ví dụ: int abs( int i ); double abs(double i); long abs (long i);
3. Đa năng hóa toán tử
Định nghĩa lại chức năng của các toán tử đã có sẵn. Đa năng hóa các toán tử không thể có các
tham số có giá trị mặc định và không đa năng hóa đối với :: . ? sizeof
data_type operator operator_symbol ( parameters ){
....................................
}
Ví dụ:
4. Con trỏ hàm
 Khi trong hàm main chạy đến dòng lệnh gọi hàm function, HDH sẽ tìm đến địa chỉ của
hàm function trên bộ nhớ ảo và chuyển mã lệnh của hàm function cho CPU tiếp tục xử lý.
 Cú pháp khai báo con trỏ hàm
<return_type> (*<name_of_ptr>) (<data_type_of_parameters>);
5. Khái quát hóa hàm (Function templates)
 Cú pháp Khai báo khuôn mẫu hàm: template < parameter-list > function-declaration
Ví dụ:
template <typename T>
T maxval (T x, T y) {
return (x > y) ? x : y;
}
int i = maxval (3, 7); // returns 7
double d = maxval (6.34, 18.523); // returns 18.523

 Từ khóa auto tự động xác định kiểu dữ liệu của tham số và hàm
Ví dụ:
auto maxval (auto x, auto y) {
return (x > y) ? x : y;
}

6. Hàm nặc danh – cú pháp lambda


 Lợi ích của lambda là không nhất thiết phải khai báo tên hàm ở một nơi khác, mà có thể tạo
ngay một hàm (dùng một lần hay hiểu chính xác hơn là chỉ có một chỗ gọi một số tác vụ
nhỏ). Như vậy, ta sẽ giảm được thời gian khai báo một hàm, thường được sử dụng để viết
mã nguồn ngắn gọn và đơn giản hơn.
 Cú pháp của hàm lambda
[capture list] (parameters) mutable throw() → return_type {
// body of lambda function
}
 [capture list]: Danh sách các biến ngoài (có thể là biến toàn cục hoặc biến cục bộ
trong phạm vi bên ngoài hàm lambda) mà hàm lambda cần truy cập. Ký hiệu (&) là
biến được truy cập bằng tham chiếu, bỏ ký hiệu (&) hoặc sử dụng cách khai báo [=]
sẽ được hiểu là truy cập giá trị. Nếu không có biến nào cần truy cập, bạn có thể bỏ
qua phần này hoặc sử dụng [] rỗng.
 (parameters): Danh sách các tham số khác của hàm
 mutable: Khi sử dụng từ khóa mutable trong hàm lambda, bạn có thể thay đổi giá trị
của các biến capture trong phạm vi của hàm lambda mà không làm thay đổi trạng
thái const của hàm lambda chính nó.
 throw(): Ngoại lệ có thể xảy ra trong lambda
 return_type: kiểu dữ liệu trả về
 Các ví dụ minh họa:
 Ví dụ 1:
int m = 0; int n = 0;
auto func = [&, n] (int a) mutable {
m = ++n + a;
cout << m << “ “ << n;
// kết quả: m = 5 và n = 1
};
func(4);
cout << m << “ “ << n;
// kết quả: m = 5 và n = 0
Giải thích: [&, n] tức là m được truyền tham chiếu còn n được truyền tham trị
Do có mutable nên trong phạm vi hàm lambda thì n vẫn có thể thay đổi giá trị, nếu
không có mutable thì sẽ báo lỗi: biến n chỉ read-only không được phép thựuc hiện +
+n

 Ví dụ 2:
vector< vector<int> > a = {
{1, 3, 7}, {2, 3, 4, 5}, {9, 8, 15}, {10, 12},
};
// su dung ham sort san co trong thu vien algorithm tren mang vecto a
// sap xep cac vector theo thứ tự giảm dần tổng các phần tử
sort (a.begin(), a.end(), [] (vector<int> x, vector<int> y) {
// xay dung ham so sanh cho viec sap xep
int sum1 = 0;
for (unsigned int i = 0; i < x.size(); i++)
sum1 += x[i];

int sum2 = 0;
for (unsigned int j = 0; j < y.size(); j++)
sum2 += y[j];
return sum1 > sum2;
});

Bài 4 + 5: Kỹ thuật viết mã nguồn hiệu quả và Phong cách lập trình
1. Các kỹ thuật viết mã nguồn hiệu quả
 Khởi tạo một lần, dùng nhiều lần
Ví dụ như giá trị sin(0.31) dùng nhiều lần thì nên khởi tạo một biến mới float s = sin(0.31);
 Hàm nội tuyến (Inline Functions)
 Inline functions (hàm nội tuyến) là một loại hàm trong ngôn ngữ lập trình C++. Từ
khoá inline được sử dụng để đề nghị (không phải là bắt buộc) compiler (trình biên dịch)
thực hiện inline expansion (khai triển nội tuyến) với hàm đó hay nói cách khác là chèn
code của hàm đó tại địa chỉ mà nó được gọi.
 Chỉ cần thêm từ khoá “inline” phía trước của hàm
Ví dụ: inline max (int a, int b) { return a > b ? a : b; }
 Trình biên dịch có thể không thực hiện nội tuyến trong các trường hợp như: Hàm chứa
vòng lặp; Hàm chứa các biến tĩnh; Hàm đệ quy; Hàm chứa câu lệnh switch hoặc goto.
 Tiết kiệm chi phí gọi hàm, Tiết kiệm chi phí sao chép các biến trên ngăn xếp khi hàm
được gọi, Tiết kiệm chi phí cuộc gọi trả về từ một hàm.
 Tăng kích thước file thực thi do sự trùng lặp của cùng một mã. Hàm nội tuyến có thể
không hữu ích cho nhiều hệ thống nhúng. Vì trong các hệ thống nhúng, kích thước mã
quan trọng hơn tốc độ.
 Hàm macros
Ví dụ 1: #define for(i,a,b) for(int i = a; i <= b; i++)
Khi đó trong hàm main:
for(i,0,n) sum += i;  for(int i = 0; i <= n; i++) sum += i;
Ví dụ 2: #define expr 2 + 5
Khi đó trong hàm main:
cout << 3 * expr;  cout << 3 * 2 + 5; Kết quả là 11
 Biến tĩnh (static variable)
 Biến static là một kiểu biến đặc biệt trong C++ mà giá trị của nó được lưu trữ và duy
trì trong suốt quá trình chạy chương trình, bất kể phạm vi và thời gian sống của biến.
 Có hai loại biến static trong C++: biến static cục bộ (local static)
và biến static toàn cục (global static).
 Biến static cục bộ: Biến static cục bộ được khai báo bên trong một hàm hoặc một
khối mã (block). Điều đặc biệt về biến static cục bộ là nó chỉ được khởi tạo một lần
và giá trị của nó được duy trì qua các lần gọi hàm. Biến static cục bộ được sử dụng
để lưu trữ thông tin tạm thời hoặc tính toán trên nhiều lần gọi hàm.
Ví dụ:
 Biến static toàn cục: được khai báo bên ngoài tất cả các hàm và khối mã. Biến static
toàn cục có thể truy cập từ bất kỳ hàm nào trong cùng file và giá trị của nó cũng
được duy trì trong suốt quá trình chạy chương trình.
2. Static, Stack, Heap
Khi thực hiện, vùng dữ liệu data segment của một chương trình được chia làm 3 phần:
static, stack, và heap data.
 Static: global hay static variables
 Stack data: các biến cục bộ của chương trình con
 Heap data:
 Dữ liệu được cấp phát động (ví dụ, pchar trong ví dụ trên).
 Dữ liệu này sẽ còn cho đến khi ta giải phóng hoặc khi kết thúc chương trình.
3. Tính Sigmoid
 Chọn số các điểm (N = 1000, 10000, ...) tùy theo độ chính xác mà bạn muốn Tốn kém
thêm không gian bộ nhớ cho mỗi điểm là 2 giá trị float hay double tức là 8 – 16 bytes
 Khởi tạo giá trị cho mảng khi bắt đầu thực hiện
 Tính sigmoid
 Bạn đã biết X0
 Tính Delta = X1 - X0
 Tính Xmax = X0 + N * Delta;
 Với X đã cho, nếu X > Xmax thì sigmoid(X) = 1; nếu X<X0 thì sigmoid(X) = 0
 Ngược lại X0 < X < Xmax thì Tính i = (X – X0) / Delta;
 1 phép trừ số thực và 1 phép chia số thực
 Tính sigmoid(X)
 1 phép nhân float và 1 phép cộng float
Ví dụ: Giả sử giá trị N = 10000, người ta muốn sử dụng phương pháp nội suy tuyến tính để tìm
hàm sigmoid(178.89) khi đã biết giá trị của sigmoid(0.21). X0 = 0.21 và X1 = 0.22.
Vậy theo em số lượng phép toán với số thực dấu phẩy động là bao nhiêu phép để ra được
sigmoid(178.89). Giả sử phép toán y = a*x + b được coi là 2 phép toán dấu phẩy động khác nhau.
Trả lời: 1. Tính Delta = X1 – X0 = 0.01; 2 và 3. Tính Xmax = X0 + N*Delta = 100.21
Ta cần tính sigmoid(178.89) thì X = 178.89 > 100.21 nên sigmoid(178.89) = 1. Vậy cần 3 phép toán
Bổ sung: Nếu yêu cầu tính sigmoid(1.78) thì ta cần làm như sau:
Khi chưa xây dựng được mảng thì cần tính toán X2, X3, X4…
Tính X 2= X 1+ σ ' ( X 1 )∗Delta mà σ ' ( X 1 ) =X 1 ( 1− X 1 ) cần 2 phép toán(1 cộng , 1 nhân)
 Tính X2 cần 4 phép toán (2 phép toán tính σ ' ( X 1 ) và 2 phép toán tính X 1+σ ' ( X 1 )∗Delta ¿
Tính đến sigmoid(1.78) tức là X157 ta cần thiết 4.(157-2+1) + 3 = 627 phép toán
Vậy ta có công thức tổng quát:
 Nếu X > Xmax thì cần 3 phép toán số thực dấu phẩy động
 Nếu X < X0 thì cần 0 phép toán vì kết quả trả về là 0
 Trường hợp còn lại thì cần
4∗i−1 với i=( X− X 0)/ Delta

4. Một số quy tắc cơ bản trong lập trình


 Định dạng (format)
 Dùng dòng trống để chia các phần của code
 Dùng dấu cách để thụt đầu dòng, phân cách các phần tử, các biểu thức phức tạp
 Dùng ( ) để nhóm biểu thức điều kiện, biểu thức phức tạp
 Cách đặt tên (naming conventions)
 Các biến i, j, k thường dùng làm chỉ số
 Các biến n thường dùng đặt cho biến số tự nhiên
 Các biến x, y, z thường đặt cho số thực, tọa độ
 Đặt tên có ý nghĩa, dễ hiểu. Ví dụ biến đo khoảng cách thì nên đặt là distance

 Viết đặc tả hàm (specification)


 Chú thích (comments)
Bài 6: Đệ quy và khử đệ quy
1. Phân loại đệ quy
 Đệ quy trực tiếp
 Đệ quy tuyến tính
 Là đệ quy có dạng
P( ) {
if (base_case) thực hiện S;
else {
thực hiện S* ;
gọi P( );
}
}
với S , S* là các thao tác không đệ quy.
Ví dụ như đệ quy tính giai thừa
 Đệ quy nhị phân
 Là đệ quy có dạng
P(){
if (base_case) thực hiện S;
else {
thực hiện S*;
gọi P( ); gọi P( );
}
}
với S, S* là các thao tác không đệ quy.
Ví dụ như đệ quy tính số fibonaxi hay bài toán tháp Hà Nội
 Đệ quy phi tuyến
 Là đệ quy mà lời gọi đệ quy được thực hiện bên trong vòng lặp
P(){
for (start to end ){
thực hiện S ;
if (điều kiện dừng) then thực hiện S*;
else gọi P( );
}
}
với S, S* là các thao tác không đệ quy.
 Đệ quy gián tiếp – đệ quy tương hỗ
 Trong đệ quy tương hỗ có 2 hàm, và trong thân của hàm này có lời gọi của hàm kia,
điều kiện dừng và giá trị trả về của cả hai hàm có thể giống nhau hoặc khác nhau
 Ví dụ:
long X(int n) {
if(n == 0) return 1;
else return X(n-1) + Y(n-1);
}
long Y(int n) {
if(n == 0) return 1;
else return X(n-1) * Y(n-1);
}
2. Đệ quy có nhớ và đệ quy quay lui
 Đệ quy có nhớ
Mỗi khi giải được một vấn đề con ta nên lưu lại lời giải và tái sử dụng kết quả khi vấn đề
con đó được gọi tới trong các lần tiếp theo. Phương pháp này gọi là đệ quy có nhớ.

Ví dụ khác (dãy con tăng dài nhất) https://onlinegdb.com/x0es3ZdJB


 Đệ quy quay lui
Quay lui là kỹ thuật giải quyết vấn đề bắt đầu từ lời giải rỗng và xây dựng dần lời giải bộ
phận (partial solution) để ngày càng tiến gần tới lời giải bài toán. Nếu một lời giải bộ phận
không thể tiếp tục phát triển, ta sẽ bỏ nó và quay sang xét tiếp các ứng cử viên khác
void Try(int i) {
foreach (ứng viên được chấp nhận C) {
<update các biến trạng thái>
<ghi nhận x[i] mới theo C>
if (i == n) <ghi nhận một lời giải>
else Try(i + 1);
<trả các biến về trạng thái cũ>
}
Code bài toán n con hậu: https://onlinegdb.com/TPgxr3-Gg
3. Khử đệ quy
 Khử đệ quy bằng vòng lặp: ví dụ đệ quy tính giai thừa có thể khử bằng while hoặc for
 Khử đệ quy đuôi
Xét thủ tục P dạng  Giải thuật vòng lặp
P(X) ≡ if B(X) then D(X) while ( !B(X) ) {
else { A(X) ;
A(X) ; X = f(X) ;
P(f(X)) ; }
} D(X) ;

 Khử đệ quy tuyến tính bằng stack



Xét đệ quy tuyến tính dạng sau: Khử đệ quy thực hiện P(X) bằng stack:
P(X) ≡ {
P(X) ≡ if C(X) then D(X) create_stack(S); // tạo stack S
else { while(not(C(X)){
A(X) ; A(X);
P(f(X)) ; push(S,X); // cất giá trị X vào stack S
B(X) ; X := f(X);
} }
D(X);
while( not(empty(S)) ){
pop(S,X); // lấy dữ liệu X từ S
B(X);
}
}
Ví dụ về bài toán chuyển số thập phân sang hệ nhị phân
Tương tự với bài toán khử đệ quy tính giai thừa: https://onlinegdb.com/jwE_utuaK
 Khử đệ quy nhị phân bằng stack
Xét đệ quy nhị phân dạng sau: Khử đệ quy thực hiện P(X) bằng stack:
P(X) ≡ if C(X) then D(X) P(X) ≡ {
else { create_stack (S);
A(X); P(f(X)); push(S, (X, 1));
B(X); P(g(X)); while (k != 1) {
} while (not C(X)){
A(X);
push (S, (X, 2));
X := f(X);
}
D(X) ;
pop(S, (X, k));
if (k != 1) {
B(X);
X := g(X);
}
}
}

Bài 7: Các cấu trúc dữ liệu


Kiến thức được ghi chép trong sổ gotIT, ngoài ra có một số kiến thức lưu ý khác sau đây:
 Hàm vector.capacity() trong vector của ngôn ngữ C++ được sử dụng để trả về số lượng phần
tử tối đa mà vector có thể chứa mà không cần thay đổi kích thước bộ nhớ.
 Cơ chế nhân đôi vùng nhớ (Doubling Policy Memory Allocation)
Giả sử vecto đang có capacity là 4 và đã sử dụng hết, giờ cần push_back thêm một
phần tử mới khi đó một vùng nhớ gấp đôi sẽ được cấp phát, các giá trị cũ được copy
sang vùng nhớ mới
 STL Containers C++ 98/03 định nghĩa 3 loại container:
 Sequence Containers: vector, deque, list
 Container Adapters: stack, queue, priority_queue
 Ordered Associative Containers: [multi] set, [multi] map
 Một số bài toán ví dụ:
 Thuật toán DFS dùng vector < list<int> > chứa danh sách kề của các đỉnh và stack để
chứa danh sách các đỉnh lần lượt được thăm: https://onlinegdb.com/E2zXKgufI
 Thuật toán BFS dùng vector < list<int> > chứa danh sách kề của các đỉnh và queue để
chứa danh sách các đỉnh lần lượt được thăm: https://onlinegdb.com/94iqfiZZR
 ??? Thuật toán Dijkstra sử dụng priority_queue: https://onlinegdb.com/OKvIf8pOM

Bài 8: Bẫy lỗi và lập trình phòng ngừa


1. Assertion
 Assertion: một macro hay một chương trình con dùng trong quá trình phát triển ứng dụng,
cho phép chương trình tự kiểm tra khi chạy.
 Assertions chủ yếu được dùng trong quá trình phát triển hay bảo trì ứng dụng. Dịch thành
code khi phát triển, loại bỏ khỏi code trong sản phẩm để nâng cao hiệu năng của ctr.
 Sử dụng assert trong C/C++ (trong C thư viện <assert.h>, trong C++ thư viện <cassert>)
2. Xử lý ngoại lệ
 Xử lý exception được thực hiện thông qua 3 keywords: try, catch, throw. Các đoạn code
có khả năng gây ra lỗi cần phải được đặt trong khối lệnh try. Khi một exception được
throw, việc thực thi của block code đó sẽ chấm dứt, nhưng bản thân chương trình vẫn
còn sống. Nếu không có đoạn code xử lý bắt exception (khối catch), chương trình sẽ kết
thúc.
 Đặt khối lệnh catch sau khối lệnh try để bắt ngoại lệ.Lệnh catch sẽ chỉ bắt những
exception tương thích với kiểu đã được chỉ định.
try {
// protected code
} catch( ExceptionType e ) {
// code to handle ExceptionType exception
}
Đoạn code trên chỉ bắt ngoại lệ có kiểu là ExceptionType. Nếu muốn lệnh catch bắt
exception thuộc bất kỳ kiểu dữ liệu nào, sử dụng dấu “...”
try {
// protected code
} catch(...) {
// code to handle any
exception
}
std::exception là Exception chung nhất cho tất cả các exception khác trong C++
 Ví dụ:
Bài 9: Gỡ lỗi, kiểm thử và tinh chỉnh mã nguồn
1. Gỡ lỗi
 Có thể phân loại thành lỗi syntax, lỗi run-time và lỗi logic.
 Lỗi syntax: Các lỗi khiến chương trình bị sai cú pháp và không thể biên dịch được.
 Lỗi run-time: các lỗi chỉ phát hiện ra khi tiến hành chạy chương trình như Truy cập
phần tử ngoài mảng, Lỗi gán không hợp lệ: (a = b thay vì a == b), Sử dụng các biến
chưa được khởi tạo ban đầu, Các case thực hiện xuyên suốt cho nhau do thiếu
break, không giải phóng bộ nhớ, Xử lý file (quên đóng file hoặc mở/đóng file liên
tục)
 Lỗi logic: chương trình vẫn chạy đúng nhưng do tư duy sai, thuật toán sai dẫn đến
sai kết quả, là loại lỗi khó phát hiện nhất
 Công cụ nổi tiếng nhất để gỡ lỗi trong ngôn ngữ C/C++ là gdb
 Đặt điểm dừng (breakpoint) tại vị trí bất kỳ trong mã nguồn
 Thực thi từng câu lệnh sau điểm dừng
 Kiểm tra giá trị của các biến
 Phân tích các lỗi liên quan đến bộ nhớ (core dump)
2. Kiểm thử
 Phương pháp kiểm thử
 Black-Box (Kiểm thử hộp đen): Testing chỉ dựa trên việc phân tích các yêu cầu
testing sử dụng mô tả bên ngoài của phần mềm để kiểm thử, bao gồm các đặc tả
(specifications), yêu cầu (requirements) và thiết kế (design).
 Không có sự hiểu biết cấu trúc bên trong của phần mềm
 White-Box (Kiểm thử hộp trắng): Testing dựa trên việc phân tích các logic bên
trong
 WBT đòi hỏi kĩ thuật lập trình am hiểu cấu trúc bên trong của phần mềm
(các đường, luồng dữ liệu, chức năng, kết quả).
 Phương thức: Chọn các đầu vào và xem các đầu ra
 Grey-Box (Kiểm thử hộp xám): Là sự kết hợp của kiểm thử hộp đen và kiểm thử
hộp trắng khi mà người kiểm thử biết được một phần cấu trúc bên trong của phần
mềm
 Khác với kiểm thử hộp đen, là dạng kiểm thử tốt và có sự kết hợp các kĩ
thuật của cả kiểm thử hộp đen và hộp trắng
 Các mức độ kiểm thử (unit → tập các units = tphần → tập các tphần = sản phẩm →
system)
 Unit: kiểm thử các công việc nhỏ nhất của lập trình viên để có thể lập kế hoạch và
theo dõi hợp lý (function, procedure, module, object class,...)
 Component: kiểm thử tập hợp các units tạo thành 1 thành phần (program, package,
task, interacting object classes,...)
 Product: kiểm thử các thành phần tạo thành 1 sản phẩm (subsystem, application,...)
 System: kiểm thử toàn bộ hệ thống
 Các bước kiểm thử: Black-Box → White-Box → Unit level đến System level
 Các phương pháp đo độ bao phủ kiểm thử
 Statement Coverage: Statement Coverage đảm bảo rằng tất cả các dòng lệnh
trong mã nguồn đã được kiểm tra ít nhất một lần. (số câu lệnh)
 Branch Coverage: Branch Coverage đảm bảo rằng tất cả các nhánh chương trình
trong mã nguồn đã được kiểm tra ít nhất một lần. (số nhánh)
 Path Coverage: đảm bảo rằng tất cả các đường chạy (là tổ hợp của các nhánh)
chương trình trong mã nguồn đã được kiểm tra ít nhất một lần. (số dòng code)

3. Tinh chỉnh mã nguồn

You might also like