Có 2 trường hợp xảy ra:1 Nếu vùng nhớ 0x0000ffff chưa được thằng nào dùng không có chương trình nào trong cùng máy tính dùng, thì sẽ lấy đc 1 giá trị ngẫu nhiên, lung tung nằm trong vùng
Trang 1TRƯỜNG ĐẠI HỌC BÁCH KHOA HÀ NỘI
VIỆN CÔNG NGHỆ THÔNG TIN VÀ TRUYỀN THÔNG
──────── * ───────
BÁO CÁO
MÔN: Kỹ thuật lập trình
đến C++, Kỹ thuật lập trình
Họ và tên sinh viên: Đinh Chí Công
Mã số sinh viên: 20193996
Lớp: 124162
Giảng viên hướng dẫn: ThS.Vũ Đức Vượng
Hà Nội, 06/2021
Trang 2M 甃⌀c l甃⌀c
Giới thiệu 3
Phần 1: Các vấn đề lý thú liên quan đến con trỏ 4
1.1 Một số vấn đề gặp phải 4
1.1.1 Lỗi truy cập bộ nhớ cấm: 4
1.1.2 Sử dụng dereferencer con trỏ làm tốn thời gian 4
1.2 Một số câu hỏi đặc biệt 4
Phần 2: Các vấn đề lý thú liên quan đến cấp phát bộ nhớ động 5
2.1 Một số vấn đề gặp phải 5
2.1.1 Hiện tượng phân mảnh bộ nhớ 5
2.2 Một số câu hỏi đặc biệt 6
Phần 3: Các vấn đề lý thú nữa liên quan đến C/C++ 6
3.1 Dấu ngoặc chết tróc 6
3.2 Các phép toán phối hợp loạn với nhau 7
3.3 Vấn đề bộ nhớ không an toàn (Memory Unsafety) 8
Phần 4: Các vấn đề lý thú liên quan đến kỹ thuật viết code hiệu quả 8
4.1 Đừng cố gắng tối ưu code từ lúc đầu 8
4.2 Trang bị nhiều kinh nghiệm tự tối ưu hóa code 9
4.3 Đặc tính của CPU cũng giúp mang lại hiệu quả đáng kể 9
4.4 Đảm bảo sức khỏe để có thể viết code chất lượng 10
Tài liệu tham khảo 14
Trang 3Giới thiệu
Trong suốt 1 kỳ học vừa qua, mặc dù gặp phải khó khăn do dịch bệnh khiến cho việc học tập phải thay đổi liên tục, gặp bất tiện trong việc gặp thầy và các bạn để trao đổi môn học Nhưng với sự hỗ trợ của công nghệ thông tin, thầy và các bạn em cảm thấy kỳ vừa rồi em vẫn tiếp tục thu nhận được rất nhiều kiến thức bổ ích mà sau đây em sẽ viết lại trong bài báo cáo của mình
Trang 4Phầần 1: Các vầấn đềầ lý thú liền quan đềấn con trỏ
1.1 Một số vấn đề gặp phải
1.1.1 Lỗi truy cập bộ nhớ cấm:
Khi em tạo ra con trỏ mà không khởi tạo hay gán nó với 1 địa chỉ là biến, giá trị trong biến con trỏ này là 1 giá trị vô định
Ví dụ khi em khai báo int *p; và quên không khởi tạo, thì sẽ trở tới 1 vùng nhớ lungp
tung (giả sử là vùng nhớ 0x0000ffff)
Nếu em tiếp tục thao tác kiểu lấy giá trị chứa trong , như là , thì chương trình của emp *p
sẽ access vào 0x0000ffff để lấy giá trị Có 2 trường hợp xảy ra:
1) Nếu vùng nhớ 0x0000ffff chưa được thằng nào dùng (không có chương trình nào trong cùng máy tính dùng), thì sẽ lấy đc 1 giá trị ngẫu nhiên, lung tung nằm trong vùng nhớ đó (sai logic)
2) Nếu vùng nhớ 0x0000ffff được 1 thằng khác dùng và chương trình của em không được quyền cao hơn (ví dụ quyền admin), thì sẽ bị crash ngay lập tức
Đây cũng là lý do vì sao có những lúc em truy cập vào 1 mảng không đc cấp phát hoặc vào index quá độ dài của mảng, nhưng có lúc thì bị crash và có lúc thì không
1.1.2 Sử dụng dereferencer con trỏ làm tốn thời gian
Thao tác gán địa chỉ vùng nhớ dữ liệu cho một con trỏ dereference tốn nhiều thời gian và
có thể gây hậu quả nghiêm trọng nếu vùng nhớ đích chưa được cấp phát
1.2 Một số câu hỏi đặc biệt
Sau 1 kỳ học môn Kỹ thuật Lập trình trên lớp, em có tổng kết lại một vài thắc mắc về con trỏ Nhờ có thầy, các bạn giúp đỡ cùng các nguồn tài liệu trên mạng, em đã có câu trả lời như sau
Câu hỏi: Không thể gán 2 mảng nhưng có thể gán 2 biến cấu trúc, dù các trường của cấu
trúc có thể là 1 mảng, xâu hay 1 struct khác Nhưng nếu 1 trường của cấu trúc là con trỏ thì sao?
Trả lời: Khi trong struct có con trỏ thì không được gán trực tiếp Bản chất việc gán 2 struct là compiler sẽ copy toàn bộ vùng nhớ của struct này sang struct kia
Nếu các biến trong struct là biến thì compiler copy giá trị của biến int này sang biến int
int kia
Khi ta cấp phát vùng nhớ cho con trỏ thì dữ liệu được cấp phát không được copy Chỉ có địa chỉ mà con trỏ đang trỏ đến là được copy
Trang 5Dẫn đến ta sẽ có hai struct khác nhau, nhưng con trỏ trong 2 struct đó lại cùng trỏ về một vùng nhớ Thao tác trên con trỏ của struct này sẽ thay đổi vùng nhớ của con trỏ trên struct khác Nếu ta không hiểu rõ điều này sẽ gây ra những lỗi khó hiểu và nguy hiểm
Phầần 2: Các vầấn đềầ lý thú liền quan đềấn cầấp phát b nh đ ng ộ ớ ộ 2.1 Một số vấn đề gặp phải
2.1.1 Hiện tượng phân mảnh bộ nhớ
Trong quá trình tìm hiểu em đã 1 vài lần gặp thuật ngữ "phân mảnh bộ nhớ" được sử dụng một vài lần trong trường hợp phân bổ bộ nhớ động C ++ Cùng với đó là một số câu hỏi về cách xử lý phân mảnh bộ nhớ, nhưng không thể tìm thấy câu hỏi trực tiếp liên quan đến vấn đề này Vì thế em muốn đề cập nó trong bài báo cáo này để thầy có thể xem qua và nếu sai mong thầy có thể phản hồi lại cho em
Các hiểu của em về phân mảnh bộ nhớ như sau
Nếu như em có một bộ nhớ trống "lớn" (32 byte):
Bây giờ, phân bổ một số trong đó (5 phân bổ):
aaaabbccccccddeeee
Bây giờ giải phóng bốn phân bổ đầu tiên:
eeee
Nếu ngay hiện tại phải phân bổ 16 byte nữa thì có vẻ hợp lý vì chúng to có gấp đôi số đó nhưng không thể do phân mảnh bộ nhớ
Theo em được biết trên các hệ thống có bộ nhớ ảo, sự phân mảnh ít gặp vấn đề hơn bởi vì phân bổ lớn chỉ cần được đặt liền kề trong không gian địa chỉ ảo , không phải trong không gian địa chỉ vật lý Vì vậy trong ví dụ của em, nếu tôi có bộ nhớ ảo với kích thước trang là 2 byte thì tôi có thể thực hiện phân bổ 16 byte của mình mà không gặp vấn đề gì
Bộ nhớ vật lý sẽ trông như thế này:
ffffffffffffffeeeeff
trong khi bộ nhớ ảo (lớn hơn nhiều) có thể trông như thế này:
eeeeffffffffffffffff
Dấu hiệu của sự phân mảnh bộ nhớ là khi em cố gắng phân bổ thêm một khối nhưng không thể mặc dù dường như có đủ bộ nhớ trống Một hậu quả khác có thể xảy ra là quá trình không thể giải phóng bộ nhớ trở lại hệ điều hành (vì có một số đối tượng vẫn được
sử dụng trong tất cả các khối được phân bổ từ hệ điều hành, mặc dù các khối đó hiện không được sử dụng)
=> Để ngăn chặn sự phân mảnh bộ nhớ trong C ++ hoạt động bằng cách phân bổ các đối tượng từ các khu vực khác nhau theo kích thước Vì vậy, nếu bạn sẽ tạo ra nhiều đối tượng và giải phóng tất cả chúng sau này, hãy phân bổ chúng từ một nhóm bộ nhớ Bất kỳ
Trang 6phân bổ nào khác mà bạn thực hiện ở giữa chúng sẽ không nằm trong nhóm, do đó sẽ không được đặt ở giữa chúng trong bộ nhớ, do đó, bộ nhớ sẽ không bị phân mảnh 2.2 Một số câu hỏi đặc biệt
Câu hỏi: C++ chỉ có new và delete để cập nhập và giải phóng mảng, nếu muốn tái cập
nhập lại bộ nhớ cho các mảng n chiều mà vẫn muốn giữ lại các giá trị đã có của mảng cũ thì làm thế nào ?
Trả lời: Vì khi sử dụng toán tử delete không có nghĩa là delete tất cả mọi thứ bên trong vùng nhớ mà con trỏ trỏ đến Toán tử new và delete chỉ mang ý nghĩa về "quyền sử dụng" vùng nhớ Toàn bộ dãy địa chỉ trên bộ nhớ ảo được quản lý bởi một chương trình mang tên "Hệ điều hành", và hệ điều hành có quyền trao lại quyền sử dụng một vùng nhớ nào đó (trên Stack hoặc trên Heap ) cho những chương trình đáng tin cậy trên máy tính
Và toán tử new dùng để làm hợp đồng sử dụng vùng nhớ trên Heap, nếu lấy vùng nhớ được cấp phát thông qua hợp đồng (make by new operator) để chương trình chạy, vậy khi sử dụng toán tử delete, đơn giản là đưa lại cho hệ điều hành Lúc này, Giá trị trên vùng nhớ đó có thể vẫn còn giữ nguyên do chưa có chương trình nào can thiệp vào Toán tử delete không tác động gì đến con trỏ
Tuy nhiên vì là có thể, tức là nếu bị chương trình khác can thiệp vào trước khi chúng ta tái cập nhập bộ nhớ thì các giả trị của mảng cũ chắc chắn không còn Trong trường hợp may mắn chưa có chương trình nào can thiệp, chúng ta có thể lấy lại các giá trị đã có của mảng cũ bằng cách dùng hàm realloc
Phầần 3: Các vầấn đềầ lý thú n a liền quan đềấn C/C++ ữ
3.1 Dấu ngoặc chết tróc
Trong ngôn ngữ C/C++, có một cái bảng gọi là…thứ tự ưu tiên của các phép toán Mà đa
số lập trình viên C/C++ đều không thể nhớ nó, nhưng em nghe các anh chị khóa trên kể lại những người tuyển dụng rất thích hỏi về vấn đề này
Mặc dù gặp rất nhiều trong quá trình code nhưng trong thời gian làm bài báo cáo này tần xuất code của em đã giảm xuống đáng kể vì ôn nhiều môn khác Nên em sẽ lấy một ví dụ
mà em đã gặp trên mạng:
1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
int a = 10, b = 5, c = 5;
int d;
d = b + c == a;
printf("%d", d); // Kết quả in ra 1
return 0;
}
Trang 7Kết quả in ra d = 1 Vì phép “ ” ưu tiên đầu tiên, nên+ b + c thực hiện trước cho ra 10
Sau đó phép “==” ưu tiên tiếp theo (10 == 10 đúng (true)) nên cuối cùng d = 1
Em còn đọc được có người comment dưới bài viết rằng “Khi đã trở thành developer, tuyệt đối không được thử trí thông minh của bản thân bằng những dòng code như thế này” Vì nó chỉ làm cho chúng ta và những người xung quan muốn trầm cảm hơn mà thôi
3.2 Các phép toán phối hợp loạn với nhau
Tiếp đến em gặp rất nhiều crash liên quan đến các toán hạng và em đã phải mất nhiều thời gian để nhận ra nó Đó là lý do em muốn đề cập tới trong bài báo cáo này
1
2
3
4
5
6
7
8
9
// Kết quả in ra trên màn hình là gì?
#include <stdio.h>
int main()
{
int a = 10, b = 10;
if (a = 5)
b ;
printf("%d, %d", a, b );
return 0;
}
Nếu không nắm vững các lý thuyết về độ ưu tiên của các toán tử như thầy đã giới thiệu trong buổi 2 thì rất có thể sẽ đưa ra kết quả (10,10), (5,10),(10,9)
Câu trả lời: 5, 9
Một bài toán khác, đây là bài toán em đọc được trên mạng và phản hồi của số đông mọi người đều cho ra kết quả sai như em
1
2
3
4
5
6
7
8
// Kết quả in ra màn hình là gì
#include <stdio.h>
int main()
{
int i = 10, j = 0;
if (i || (j = i + 10))
printf("%d", j);
return 0;
}
Câu trả lời: 0
Lý do em và phần đông đều sai vì trong câu điều kiện , vế bên trái của phép “ ” là (màif || i
i ở đây đang = 10 nghĩa là true) True nó có OR với thằng nào thì cũng là true cả, nên vế bên phải sẽ không được thực hiện, và đương nhiên vẫn dữ giá trị j 0
Trang 83.3 Vấn đề bộ nhớ không an toàn (Memory Unsafety)
Đây là một vấn đề em ít gặp phải Nhưng đọc các bài báo trên mạng em thấy các lỗ hổng bảo mật như Heartbleed, Zero-Day hay lỗ hổng bị khai thác bởi Ransomware như WannaCry đều có chung nguyên nhân gốc và thường gặp trong 2 ngôn ngữ lập trình phổ biến là C và C++ Một báo cáo từ Motherboard chỉ ra rằng vấn đề này xuất phát từ cái gọi
là “memory unsafety” tồn tại trong trong C/C++
Trong một chương trình có một danh sách 10 phần tử chứa các giá trị số (number) Về mặt lý thuyết, nếu trong chương trình mà truy cập vào phần tử thứ 11 thì chương trình phải hiển thị thông báo lỗi mới chuẩn Tuy nhiên trong các ngôn ngữ memory-unsafe như
C và C++ thì chương trình sẽ tìm kiếm phần tử thứ 11 ở chỗ mà nó cho rằng phần tử đó tồn tại ở đó (mặc dù trên thực tế nó không tồn tại) và truy cập vào memory ở vị trí đó Vấn đề này được gọi là “buffer overflow” và bị khai thác trong các lỗ hổng như
HeartBleed Và đó chưa phải là lỗ hổng duy nhất trong C/C++ Còn một số loại lỗ hổng
“memory unsafety” khác trong C/C++ như sau:
+ Type confusion: nhầm lẫn giữa giá trị tồn tại trong bộ nhớ và kiểu dữ liệu của nó + Use after free: truy cập bộ nhớ sau khi đã giải phóng nó
+ Use of uninitialized memory: truy cập và sử dụng bộ nhớ khi chưa khởi tạo giá trị cho nó
Các lỗ hổng này rất phổ biến trong các phần mềm được sử dụng rộng rãi như Firefox, Chrome, Windows, Android và iOS
Phầần 4: Các vầấn đềầ lý thú liền quan đềấn kỹỹ thu t viềất code hi u qu ậ ệ ả 4.1 Đừng cố gắng tối ưu code từ lúc đầu
Việc thực hiện tối ưu hóa code của mình từ rất sớm đôi khi chúng cũng làm cho mọi việc trở nên phức tạp hơn khi lúc nào bạn cũng phải nghĩ cách tối ưu cho code của mình Thậm chí có thể gây ra một vài vấn đề (Được em trích lại trong slide của thầy Vũ Đức Vượng):
1) Không thể xác định được những nút thắt trong chương trình trước khi chạy thử toàn bộ chương trình
2) Việc xác định quá các nút thắt trong chương trình sẽ gây ra các nút thắt mới khi chạy thử toàn bộ chương trình
Trang 93) Nếu vừa viết chương trình vừa tìm các tối ưu mã nguồn, có thể làm sai lệch mục tiêu của chương trình
Hãy tập trung viết cho code hoạt động đúng trước, sau đó mới bắt đầu tối ưu những đoạn code
4.2 Trang bị nhiều kinh nghiệm tự tối ưu hóa code
Một vài kinh nghiệm mà em đã học được:
Viết lại các biểu thức logic cần đảm bảo các tiêu chuẩn sau:
1) Không kiểm tra khi đã biết kết quả rồi
2) Sắp xếp thứ tự các phép kiểm tra theo tần xuất xảy ra kết quả đúng
3) So sánh hiệu năng của các lệnh có cấu trúc tương đương
4) Thay thế các biểu thức logic phức tạp bằng bảng tìm kiếm kết quả
Viết lại các vòng lặp hiệu quả:
Bằng các cách sau sẽ giúp vòng lặp hiệu quả hơn
1) Ghép các vòng lặp với nhau
2) Giảm thiểu các phép tính toán bên trong vòng lặp nếu có thể
Viết lại các biểu thức và tinh chỉnh việc biến đổi dữ liệu
Còn rất nhiều các kinh nghiệm nữa mà chúng ta cần khám phá thêm nhưng bên cạnh đó hiểu thêm về phần cứng cũng giúp ích rất nhiều cho tối ưu hóa code như phần sau 4.3 Đặc tính của CPU cũng giúp mang lại hiệu quả đáng kể
Để đảm bảo tốc độ truy xuất tối ưu, các bộ vi xử lý (CPU) 32-bit hiện nay yêu cầu dữ liệu sắp xếp và tính toán trên bộ nhớ theo từng offset 4-byte Yêu cầu này gọi là memory alignment
Khi biên dịch một đối tượng dữ liệu có kích thước dưới 4- byte, các trình biên dịch sẽ bổ sung thêm các byte trống để đảm bảo các dữ liệu được sắp xếp theo đúng quy luật Việc
bổ sung này có thể làm tăng đáng kể kích thước dữ liệu, đặc biệt đối với các cấu trúc dữ liệu như structure, class…
Theo nguyên tắc alignment 4-byte (hai biến “c” và “d” có kích thước 4 byte), các biến
“a” và “b” chỉ chiếm 1 byte và sau các biến này là biến int chiếm 4 byte, do đó trình biên dịch sẽ bổ sung 3 byte cho mỗi biến này Kết quả tính kích thước của lớp Test sẽ là 16 byte
class Test
{
bool a;
int c;
int d;
bool b;
};
Ta có thể sắp xếp lại các biến thành viên của lớp Test như sau theo chiều giảm dần kích thước Khi đó, hai biến “a” và “b” chiếm 2 byte, trình biên dịch chỉ cần bổ sung thêm 2
Trang 10byte sau biến “b” để đảm bảo tính sắp xếp 4-byte Kết quả tính kích thước sau khi sắp xếp lại class Test sẽ là 12 byte
class Test
{
int c;
int d;
bool a;
bool b;
};
4.4 Đảm bảo sức khỏe để có thể viết code chất lượng
Theo như em biết việc ngồi nhiều giờ trước màn hình máy tính để viết code sẽ gây ảnh hưởng rất xấu đến sức khỏe và điều này khiến cho hiệu quả công việc giảm đi, code cũng
vì thế mà giảm đi phần nào sự đặc sắc
4.3.1 Ngồi lâu trước máy tính ảnh hưởng đến thị lực:
Ngoài thời gian bắt buộc phải ngồi làm việc với máy tính, chúng ta không thể phủ nhận sức hấp dẫn của các sản phẩm công nghệ, đặc biệt là smartphone, laptop Việc ngồi sử dụng chúng trong hàng giờ đồng hồ ảnh hưởng nghiêm trọng đến thị lực đôi mắt, gây mỏi mắt, khô mắt, giảm thị lực, cận thị…
Nhận thấy các vấn đề đó, các bác sĩ ở bệnh viện Mayo (Mỹ) đã đưa ra quy tắc 20-20-20
Cụ thể là cứ 20 phút thì bạn nên rời mắt khỏi màn hình máy tính, nhìn ra xa khoảng 20 feet (khoảng 6m) ít nhất trong vòng 20 giây Thi thoảng bạn có thể nhắm mắt lại trong vài phút hoặc tập các bài tập cho mắt để mắt được nghỉ ngơi và sau đó có thể bắt đầu làm việc hiệu quả hơn Nếu mắt bị khô, bạn hãy chớp mắt thường xuyên hơn và có thể sử dụng một lọ thuốc nhỏ mắt bên mình
4.3.2 Ngồi lâu trước máy tính khiến da nhanh lão hóa
Thực tế cho thấy rằng, những người thường xuyên sử dụng máy tính và các thiết bị điện
tử có dấu hiệu lão hóa da nhanh hơn hẳn những người khác kể cả khi họ đã bổ sung các thực phẩm tươi, lành mạnh
Để khắc phục tình trạng này, bạn nên hạn chế ngồi máy tính nhiều nhất có thể, bảo vệ da đúng cách và cung cấp các dưỡng chất cần thiết cho da
4.3.3 Ngồi lâu trước máy tính khiến đau mỏi vai và lưng
Ngồi nhiều sẽ tạo ra sự hao mòn trong khớp xương của bạn, ảnh hưởng lớn đến dây chằng cột sống khi phải đặt một trọng lực lớn trên cơ vai và lưng trong thời gian dài Ngoài ra, khi ở đằng trước là máy tính thì tự nhiên bạn sẽ phải vươn cổ về phía trước trong khi tập trung, gây căng thẳng trên cổ và vai