Professional Documents
Culture Documents
Part 3: Advance (File handle, Multithreading, IPC, Networking, GUI (Win32API, MFC), DLL, LIB)
Part 1: Lập trình C
Syntax-error: Lỗi cú pháp được phát hiện ngay khi biên dịch chương trình. Trình biên dịch sẽ thông
báo lỗi tại cửa sổ Output.
Ví dụ:
#include <stdio.h>
void main()
{
printf("Hello world")
}
Output:
Runtime-error: Chương trình đã được biên dịch thành công, gặp lỗi khi chạy chương trình do đầu
vào hoặc đầu ra có giá trị không mong muốn.
#include <stdio.h>
void main()
{
int a, b = 5, c = 0;
a = b / c;
}
Output
Logical-error: Chương trình đã được biên dịch và chạy không gặp lỗi Runtime-error. Nhưng kết quả
đầu ra không đúng theo yêu cầu do logic xử lý bài toán bị sai.
#include <stdio.h>
Kết quả mong muốn: c= SQUARE(2+3) = SQUARE(5) = 25. Tuy nhiên kết quả c= SQUARE(2+3) =
2+3*2+3 = 11. Macro SQUARE(X) đúng phải được define như sau:
Memcpy va strcpy đều sử dụng để copy dữ liệu từ vùng nhớ này sang vùng nhớ khác. Tuy nhiên,
chúng có một số điểm khác nhau:
strcpy() memcpy()
Copy chuỗi kí tự nguồn sang chuỗi đích. Hàm Copy chính xác số byte dữ liệu từ vùng nhớ
strcpy copy kí tự đến khi gặp kí tự NULL thì nguồn sang vùng nhớ đích.
dừng lại.
Chú ý:
Xem thêm ví dụ sau để hiểu thêm về sự khác nhau của 2 hàm strcpy() và memcpy()
#include <stdio.h>
#include <string.h>
#include <conio.h>
void main()
{
char s[] = { 'v', 'n', '\0', 'c', 'o', 'd', 'i', 'n', 'g' };
char strcpy_buf[5];
char memcpy_buf[5];
memset(strcpy_buf, NULL, 9);
memset(memcpy_buf, NULL, 9);
strcpy(strcpy_buf, s);
memcpy(memcpy_buf, s, 9);
printf("strcpy_buf = ");
dump(strcpy_buf, 9);
printf("memcpy_buf = ");
dump(memcpy_buf, 9);
Output:
strcpy_buf = 76 6e 00 00 00 00 00 00 00 vn
memcpy_buf = 76 6e 00 63 6f 64 69 6e 67 vn coding
Khi một hàm mà được định nghĩa và khai báo trong chương trình thì hàm main(), các hàm khác và
thậm chí hàm đó có thể gọi đến chính nó (đệ quy). Trong C/C++, có các cách truyền đối số
(arguments) cho hàm như sau:
Khi truyền đối số kiểu tham trị, chương trình biên dịch sẽ copy giá trị của đối số để gán cho tham số
của hàm (không tác động trực tiếp đến biến số truyền vào).
#include <stdio.h>
Output:
s=5
x=1
y=2
Trong ví dụ này, ta định nghĩa và khai báo hàm sum() có kiểu trả về là int, 2 tham số a,b kiểu int. Khi
gặp câu lệnh gọi hàm sum() trong hàm main(). Chương trình sẽ tạo ra 2 biến Local a,b trong hàm
sum(). Giá trị của đối số x,y truyền vào sẽ được copy và gán tương ứng cho 2 tham số a,b
(a = 1,b = 2). Các câu lệnh tiếp theo trong hàm sum() chỉ thao tác trên 2 biến Local a,b. Do vậy kết
quả là giá trị biến x = 1 , y = 2.
Output:
x=2
y=3
Hàm swap() làm nhiệm vụ đổi chỗ 2 biến nguyên x,y truyền vào. Đối số truyền vào ở đây là địa chỉ 2
biến x,y (&x , &y). Trong hàm swap(), biến con trỏ a,b sẽ trỏ tới địa chỉ của biến x,y (a = &x, b = &y).
Và *a và *b chính là giá trị của 2 biến x,y. Các câu lệnh tiếp theo:
temp = *a;
*a = *b;
*b = temp;
Ta thấy, hàm swap() đã thay đổi giá trị của đối số truyền vào.
Output:
x=3
y=2
Hàm swap() cũng làm nhiệm vụ hoán đổi vị trí giữa 2 biến nguyên truyền vào. Trong C++ cho phép
sử dụng biến tham chiếu.
+ Biến tham chiếu không được cấp phát bộ nhớ, không có địa chỉ riêng.
+ Nó dùng làm bí danh cho một biến (kiểu giá trị) nào đó và nó sử dụng vùng nhớ của biến này.
Trong hàm swap(), chương trình sẽ thực hiện lệnh gán sau:
int &a = x;
int &b = y;
a,b ở đây là bí danh của biến x,y. Tức là biến a,b sẽ dùng chung vùng nhớ với biến số x,y.
Các cậu lệnh được thao tác trên biến a,b hay cũng chính là thao tác trên biến x,y. Do vậy, hàm
swap() sẽ thay đổi giá trị của đối số truyền vào.
void main()
{
char s[] = "vncoding forum";
char* p = &s[0];
printf("%s", p);
}
Output:
vncoding forum
void main()
{
char* s = (char*)malloc(30);
if (s)
{
strcpy(s, "vncoding forum");
printf("%s", s);
free(s);
}
}
Output:
vncoding forum
<Kiểu dữ liệu> * const <Tên con trỏ> = <Địa chỉ khởi tạo> ;
Đặc điểm:
– Cần gán ngay giá trị địa chỉ khởi tạo cho hằng con trỏ tại câu lệnh khai báo ban đầu.
– Không thể thay đổi địa chỉ đã được khởi gán cho hằng con trỏ ( sẽ gây ra lỗi).
– Có thể thay đổi giá trị tại địa chỉ đã khởi gián ban đầu.
Ví dụ 1:
#include "stdio.h"
void main()
{
int* const p; // error
}
Output:
Ví dụ 2:
#include <stdio.h>
void main()
{
int m = 2;
int* const p = &m;
p++; // error
}
Chương trình dịch sẽ thông báo lỗi vì hằng con trỏ không thể thay đổi địa chỉ trỏ tới.
Output:
Ví dụ 3:
#include <stdio.h>
void main()
{
int m = 2;
int* const p = &m;
printf("\n*p = %d", *p);
m++;
printf("\n*p = %d", *p);
}
Câu lệnh m++ chỉ làm thay đổi giá trị tại địa chỉ &m.
Output:
*p = 2
*p = 3
Con trỏ hằng – Pointer to constant
Khai báo:
const <Kiểu dữ liệu> * <Tên con trỏ>;
Đặc điểm:
– Không được phép dùng trực tiếp con trỏ hằng để thay đổi giá trị tại vùng nhớ mà con trỏ hằng
đang trỏ đến.
– Con trỏ hằng có thể thể thay đổi địa chỉ trỏ tới (hay nói cách khác: nó có thể trỏ đến các ô nhớ
khác nhau).
Ví dụ 4:
#include <stdio.h>
void main()
{
int m = 3;
const int* p;
p = &m;
m++;
(*p)++; // error
}
Câu lệnh m++ hoàn toàn hợp lệ, còn câu lệnh (*p)++ gây ra lỗi thay đổi giá trị tại vùng nhớ.
Output:
Error C3892 'p': you cannot assign to a variable that is const
Ví dụ 5:
#include <stdio.h>
void main()
{
int m = 3, n = 5;
const int* p;
p = &m;
printf("\n*p = %d", *p);
p = &n;
printf("\n*p = %d", *p);
}
Output:
*p = 3
*p = 5
6. Sự khác nhau giữa #include “filename” and #include < filename >?
Lệnh #include <filename> và #include “filename” là tương đương nhau, để include header file của
thư viện ngôn ngữ C hoặc header file do lập trình viên tự định nghĩa.
Sự khác nhau giữa #include <filename> and #include “filename” nằm ở khâu tìm kiếm file header
của tiền xử lý trước quá trình biên dịch.
#include <filename>: tiền xử lý (pre-processor) sẽ chỉ tìm kiếm file header (.h) trong thư mục chứa
file header của thư viện ngôn ngữ C (thường là thư mục trong bộ cài IDE).
#include “filename”: Trước tiên, tiền xử lý (pre-processor) tìm kiếm file header(.h) trong thư mục
đặt project C/C++. Nếu không tìm thấy, tiền xử lý tìm kiếm file header (.h) trong thư mục chứa file
header của thư viện ngôn ngữ C (thường là thư mục trong bộ cài IDE).
Tóm lại,
Khi cần sử dụng thư viện file header (.h), chúng ta nên sử dụng #include <filename>. Tất nhiên, sử
dụng #include “filename” không sai, nhưng tiền xử lí sẽ mất thời gian tìm kiếm trong thư mục
project trước khi tìm kiếm trong thư mục header của IDE.
Khi cần gọi file header (.h) tự định nghĩa, chúng ta nên sử dụng #include “filename”. Chú ý: nếu file
header (.h) được đặt ở đường dẫn khác thư mục project, chúng ta cần phải chỉ định đường dẫn
tương đối cho file header: #include “..\…\filename.h”
Ví dụ: Khai báo file header của thư viện và file header tự định nghĩa
File sum.h
#ifndef _SUM_H_
#define _SUM_H_
#endif
File sum.c
#include <stdio.h>
#include "sum.h"
void main(void)
{
float a = 4.5, b = 6.7;
printf("sum(%f, %f) = %f", a, b, sum(a, b));
getch();
}
Output:
#include <stdio.h> là thư viện cho hàm printf()
#include <conio.h> là thư viện cho hàm getch()
#include “sum.h” được định nghĩa khai bao hàm sum()
Hàm malloc() và calloc() trả về NULL trong các trường hợp sau:
Kích thước vùng nhớ cần cấp phát vượt quá kích thước vật lý của hệ thống
Tại thời điểm gọi hàm malloc() và calloc(), tạm thời không đủ vùng nhớ để cấp phát. Nhưng
application có thể gọi lại nhiều lần malloc() và calloc() để cấp phát vùng nhớ thành công.
Điểm khác nhau giữa hàm malloc() và calloc()
malloc() calloc()
malloc viết tắt của memory allocation calloc viết tắt của contiguous allocation
malloc nhận 1 tham số truyền vào là số byte calloc nhận 2 tham số truyền vào là số block và
của vùng nhớ cần cấp phát kích thước mỗi block (byte)
void *malloc(size_t n); Hàm trả về con trỏ trỏ tới vùng nhớ được cấp
Hàm trả về con trỏ trỏ tới vùng nhớ nếu cấp phát và vùng nhớ được khởi tạo bằng giá trị 0.
phát thành công, trả về NULL nếu cấp phát fail Trả về NULL nếu cấp phát fail
Hàm malloc() nhanh hơn hàm calloc() Hàm calloc() tốn thêm thời gian khởi tạo vùng
nhớ. Tuy nhiên, sự khác biệt này không đáng
kể.
void main(void)
{
int* p1, * p2;
p1 = (int*)malloc(10 * sizeof(int));
if (p1)
printf("\nmalloc() allocates memory successfully");
else
printf("\nFail in allocate memory");
p2 = (int*)calloc(10, sizeof(int));
if (p2)
printf("\ncalloc() allocates memory successfully");
else
printf("\nFail in allocate memory");
}
Output:
void main()
{
int x = 5;
int y = 0;
y += ++x + 5;
printf("\n x = %d, y = %d", x, y);
}
Output:
x = 6, y = 11
Toán tử tăng sau x++: tăng giá trị x sau khi thực hiện các phép toán khác trong cùng 1 câu lệnh
Ví dụ 2:
#include <stdio.h>
void main()
{
int x = 5;
int y = 0;
y += x++ + 5;
printf("\n x = %d, y = %d", x, y);
}
Output:
x = 6, y = 10
void main(void)
{
char s[20];
char p[] = "vncoding.net";
strcpy(s, p);
printf("\ns = %s", s);
}
Output:
s = vncoding.net
Hàm strdup(const char* s) tạo ra vùng nhớ mới (gọi hàm malloc( ) để cấp phát vùng nhớ) để lưu
chuỗi s. Sau đó, trả về con trỏ trỏ tới vùng nhớ được cấp phát. Nếu cấp phát fail, hàm trả về NULL.
Chú ý: phải free vùng nhớ trả về bởi hàm strdup.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main(void)
{
char* s = strdup("vncoding.net");
if (s)
{
printf("\ns = %s", s);
free(s);
}
}
Output:
s = vncoding.net
Data Alignment làm tăng performance do việc đọc/ghi thao tác trên block data có kích thước bằng
bội số của WORD.
Trình biên dịch sẽ lấy kiểu dữ liệu có kích thước lớn nhất trong struct làm kích thước đường biên cho
việc alignment data.
Ví dụ 1:
#include <stdio.h>
struct A
{
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
char d; // 1 byte
};
void main()
{
printf("size = %d", sizeof(A));
}
Output
Khi mới học lập trình C/C++, mình nghĩ kích thước của struct A bằng: 1+4+2+1 = 8 (byte). Wrong!!!
Kích thước của struct A chỉ bằng tổng các thành phần cộng lại khi bạn sử dụng packing. Mình sẽ trình
bày ở mục bên dưới. Thực tế kích thước của struct A bằng 12. Lý do, trình biên dịch đã padding data
(add thêm một số byte giả trong lúc biên dịch để đảm bảo data alignment) như sau:
struct A
{
char a; // 1 byte
char dummy1[3]; // padding 3 bytes
int b; // 4 bytes
short c; // 2 bytes
char d; // 1 byte
char dummy2[1]; // padding 1 byte
};
Output
Trình biên dịch chọn kích thước kiểu int (4 bytes) để align data.
3 byte dummy1[3] được add thêm vào để: sizeof(a) + sizeof(dummy1) = 4 bytes
biến int b có kích thước 4 byte nên không cần padding.
1 byte dummy2[1] được add thêm vào để: sizeof(c) + sizeof(d) + sizeof(dummy2) = 4 bytes.
4 bytes
Để kiểm soát kích thước của struct mà vẫn đảm bảo performace, một khái niệm mới struct packing
ra đời.
Nếu packsize >= kích thước đường biên, giá trị packsize bị bỏ qua và trình biên dịch lấy kích thước
đường biên để align struct
Nếu packsize < kích thước đường biên, trình biên dịch align struct dựa vào packsize.
Packsize là kích thước đóng gói struct. Được setting bằng 2 cách:
Cách 1: Chọn properties → Code Generation → Struct Member Alignment
Như ví dụ 1, packsize là default (8 bytes), nên trình biên dịch lấy sizeof(int) = 4 bytes làm đường
biên. Dưới đây, mình đưa thêm một ví dụ packsize = 2.
Ví dụ 2: source code giống ví dụ 1 + packsize = 2, kết quả size = 10. Trình biên dịch lấy 2 bytes làm
đường biên để align data như sau:
struct A
{
char a; // 1 byte
char dummy1[3]; // padding 3 bytes
int b; // 4 bytes
short c; // 2 bytes
char d; // 1 byte
char dummy2[1]; // padding 1 byte
};
a dummy1[0]
b b
b b
c c
d dummy2[0]
Ví dụ 3:
#include <stdio.h>
void main()
{
printf("size = %d", sizeof(A));
}
Output:
size = 8
Mảng Chuỗi
Khai báo <kiểu dữ liệu> tên mảng[<kích Chuỗi là kiểu dữ liệu đặc biệt của mảng. Chuỗi
thước>]. Trong đó, kiểu dữ liệu có thể là kiểu là mảng kí tự (kiểu char) + kí tự NULL (‘\0’) ở
dữ liệu nguyên thủy: int, float, double, long, phần tử cuối của chuỗi. Kí tự NULL sẽ được
char,… hoặc struct, class,… trình biên dịch tự động thêm vào trong quá
trình biên dịch.
void main(void)
{
char s1[] = { 'v', 'n', 'c', 'o', 'd', 'i', 'n', 'g' };
char s2[] = "vncoding";
int sz1 = sizeof(s1);
int sz2 = sizeof(s2);
int len = strlen(s2);
printf("\nsize of s1 = %d", sz1);
printf("\nsize of s2 = %d", sz2);
printf("\nlenth of s2 = %d", len);
}
Giải thích:
s1 là mảng kí tự gồm các kí tự (v, n, c, o, d, i, n, g)
s2 là chuỗi kí tự gồm các kí tự (v, n, c, o, d, i, n, g) và kí tự NULL ẩn (chúng ta ko nhìn thấy) sẽ
được trình biên dịch thêm vào trong quá trình biên dịch.
Toán tử sizeof lấy kích thước (theo byte) của s1 và s2. s1 chứa 8 hằng kí tự tương ứng với
kích thước 8 (byte). s2 chứa 8 hằng kí tự + 1 kí tự NULL tương ứng với kích thước 9 (byte).
Hàm strlen() lấy độ dài của chuỗi kí tự (ko tính kí tự NULL). Do vậy, độ dài là 8 (kí tự). Ngôn
ngữ C có support nhiều built-in function thao tác và xử lí chuỗi kí tự:
Output:
size of s1 = 8
size of s2 = 9
lenth of s2 = 8
Tại cùng 1 thời điểm run-time, có thể truy cập Tại cùng 1 thời điểm run-time, chỉ có thể truy
vào tất cả các thành phần của struct cập 1 thành phần của union
Ví dụ struct và union
Ví dụ 1: Biểu diễn thời gian ngày tháng năm, tính size của struct và hiển thị từng thành phần của
struct
#include <stdio.h>
struct date
{
int d;
int m;
long y;
};
void main()
{
date dat = { 4, 4, 2016 };
printf("\nSize of struct: %d", sizeof(date));
Output:
Size of struct: 12
date = 4
month = 4
year = 2016
Ví dụ 2: Biểu diễn thời gian ngày tháng năm bằng union, tính size của union.
#include <stdio.h>
union date
{
int d;
int m;
int y;
};
void main()
{
date dat;
Giải thích:
Kích thước của union = kích thước lớn nhất của thành phần của union = sizeof(int) = 4
Vùng nhớ giành cho union date là 4 byte. Vùng nhớ này sẽ chứa giá trị 24 khi dat.d = 24
được thực hiện. Tiếp đó, 9 sẽ được copy đè vào vùng nhớ này khi dat.m = 9 được thực hiện.
Cuối cùng, 2014 được copy đè vào vùng nhớ khi dat.y = 2014 được thực hiện.
Output:
Size of union: 4
date = 2014
month = 2014
year = 2014
Ví dụ 3:
#include <stdio.h>
union date
{
int d;
int m;
int y;
};
void main()
{
date dat;
printf("\nSize of union: %d", sizeof(date));
dat.d = 24;
printf("\ndate = %d", dat.d);
dat.m = 9;
printf("\nmonth = %d", dat.m);
dat.y = 2014;
printf("\nyear = %d", dat.y);
}
Output:
Size of union: 4
date = 24
month = 9
year = 2014
Ta thấy, tại một thời điểm trong chương trình chỉ có 1 thành phần của union được sử dụng.
Dễ hiểu và dễ sử dụng: chỉ cần khai báo <kiểu dữ liệu> tên mảng[kích thước].
Truy cập đến các phần tử trong mảng nhanh: chúng ta có thể truy cập tới bất kì phần tử nào trong
mảng bằng cách chỉ định chỉ số cho phần tử đó. Ví dụ: mảng A[100] gồm 100 phân tử ( từ A[0] đến
A[99]), để truy xuất tới phần tử thứ i ta chỉ cần gọi giá trị A[i]. Thời gian truy cập phần tử A[0] và thời
gian truy cập phần tử A[1000] là như nhau.
Hạn chế: Bên cạnh những ưu điểm, mảng còn tồn tại một số hạn chế sau.
Kích thước của mảng phải là cố định: Trong cấp phát mảng tĩnh, mảng cần được khai báo với kích
thước xác định trước khi chạy chương trình. (vùng nhớ cho mảng được cấp phát khi biên dịch).
Trong cấp phát động, vùng nhớ được cấp phát khi chạy chương trình. Như các bạn đã biết, vùng nhớ
giành cho mỗi chương trình thường không dự đoán được trước. Nếu khai báo mảng với kích thước
lớn, không sử dụng hết sẽ gây lãng phí bộ nhớ, ngược lại nếu kích thước vùng nhớ không đủ dùng,
chúng ta không thể mở rộng vùng nhớ thêm được, dẫn đến buffer overrun ( tràn vùng nhớ).
Các byte vùng nhớ cấp phát mảng được sắp xếp liên tục: trong trường hợp vùng nhớ cho chương
trình đang bị phân mảnh, chương trình sẽ báo lỗi khi chúng ta khai báo hoặc cấp phát cho mảng với
kích thước lớn vì lý do: không đủ vùng nhớ liên tục cho mảng ( mặc dù tổng dung lượng vùng nhớ
phân mảnh là đủ).
Việc chèn và xóa phần tử của mảng mất nhiều thời gian: vì vùng nhớ cấp phát cho mảng được sắp
xếp liên tục nên việc chèn một phần tử mới vào hoặc xóa phần tử trong mảng trở lên khó khăn. Ví
dụ: cho mảng A[100], chúng ta muốn chèn thêm phần tử mới vào vị trí i, tất cả các phần tử thứ i trở
đi phải dịch sang vị trí kế tiếp để chèn giá trị vào vị trí thứ i. Việc xóa phần tử trong mảng cũng tương
tự như vậy, dịch tất cả các phần tử từ vị trí thứ i+1 sang vị trí liền trước nó.
%d hoặc %i : in ra số nguyên
%o : in ra số hệ 8 (octal)
%x hoặc %X : in ra số hê 16 (hexa)
%c : in ra kí tự
%s : in ra string
Hàm sprintf() được dùng để tạo ra chuỗi từ các kiểu dữ liệu nguyên thủy khác nhau (char*, int, float,
…)
Hàm sprintf() trả về số lượng kí tự được ghi ra chuỗi buff.
Ví dụ:
#include <stdio.h>
void main()
{
char buffer[200], s[] = "computer", c = 'l';
int i = 35, j;
float fp = 1.7320534f;
Output:
Output:
String: computer
Character: l
Integer: 35
Real: 1.732053
character count = 79
15. Memory leak là gì? Cách tránh memory leak? Cách detect memory leak?
15.1 Memory leak là gì?
Leak memory được định nghĩa như việc lập trình viên quên không giải phóng (free hoặc delete) vùng
nhớ đã cấp phát sau khi sử dụng xong. Một lượng nhỏ memory leak không được phát hiện ngay ban
đầu. Lượng leak memory sẽ tăng dần theo thời gian. Điều này làm cho ứng dụng giảm performance,
hoặc thậm chí gây chết (crash) chương trình khi sử dụng hết memory. Hoặc tệ hơn nữa, việc leak
memory của 1 ứng dụng sử dụng hết vùng nhớ của các ứng dụng khác, làm cho các ứng dụng khác
có thể bị crash.
15.2 Cách tránh memory leak?
Chắc chắn phải gọi hàm free() hoặc toán tử delete khi cấp phát bộ nhớ động bằng hàm
malloc(), calloc(), toán tử new
Ví dụ:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define SIZE 4
void main()
{
int* p = (int*)malloc(SIZE * sizeof(int));
if (p == NULL)
return;
memset(p, 0x00, SIZE * sizeof(int));
for ( int i = 0; i < SIZE; i++)
{
printf("%d ", p[i]);
}
}
Không sử dụng 1 con trỏ để trỏ tới nhiều vùng nhớ khác nhau, dẫn đến mất dấu (địa chỉ)
vùng nhớ đã được cấp phát, không thể giải phóng được vùng nhớ đó.
Ví dụ:
#include <string.h>
#include <stdlib.h>
#define SIZE 4
void main()
{
int* p = (int*)malloc(SIZE * sizeof(int));
p = (int*)malloc(SIZE * sizeof(int));
}
Giải thích:
Đoạn code trên cấp phát 2 vùng nhớ, địa chỉ vùng nhớ thứ 1 bị mất dấu, con trỏ p chỉ lưu địa chỉ
vùng nhớ thứ 2
Lưu ý khi sử dụng realloc(): không sử dụng cùng 1 biến con trỏ cho cả tham số input và
output
void *realloc(void *memblock, size_t size);
Ví dụ 1:
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
void main(void)
{
char* buff;
int size;
Giải thích:
Câu lệnh malloc() cấp phát vùng nhớ 100 byte được trỏ tới bởi con trỏ buff.
Câu lệnh realloc() extend thêm 50 bytes vùng nhớ đã được cấp phát ở trên.
Tuy nhiên, ở đây có 1 common mistake trong trường hợp hàm realloc() trả về NULL do không đủ
vùng nhớ, con trỏ buff bị gán NULL. Dẫn đến địa chỉ vùng nhớ 100 bytes được cấp phát bởi
malloc() bị mất dấu, không thể giải phóng được vùng nhớ 100 bytes, dẫn đến memory leak. Để
giải quyết vấn đề này. Bạn xem ví dụ 2 dưới đây
Ví dụ 2:
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
void main(void)
{
char* buff, * old_buff;
int size;
old_buff = buff;
if ((buff = (char*)realloc(buff, 50 + 100 * sizeof(char))) == NULL)
{
free(old_buff);
printf("\nInsufficient memory for realloc()");
}
}
Giải thích:
Dùng con trỏ old_buff để lưu địa chỉ vùng nhớ 100 bytes trước khi gọi hàm realloc().
void main(void)
{
char* s = _strdup("vncoding.net");
if (s)
{
printf("\ns = %s", s);
free(s);
}
}
Output:
vncoding.net
15.3 Detect memory leak?
Tiến hành review source code sau khi coding
Dùng tool phân tích source code tĩnh (Cppcheck, Coverity, SonarQube).
Có một vài trường hợp, leak memory chỉ được phát hiện khi chạy run-time. Khi đó bạn có
thể sử dụng tool UMDH (User Mode Memory Leak) của Microsoft. Tool này khá hiệu quả, có
thể chỉ ra được line of code gây ra memory leak.
void Display();
int main()
{
Display();
printf("\n i = %d", i); // error
}
void Display()
{
int i;
for (i = 0; i < 10; i++)
printf("\n %d", i);
}
Giải thích:
Compiler thông báo lỗi C2065 'i': undeclared identifier vì biến i khai báo trong hàm Display(),
nên phạm vị hoạt động của biến i chỉ trong hàm Display(). Hàm main() không sử dụng được biến
i.
16.2 Biến global và extern
Biến global là biến được khai báo bên ngoài tất cả các hàm và có giá trị với tất cả các hàm
trong chương trình. Tức là các hàm trong chương trình đều có quyền read/write vào biến
global.
Biến global tồn tại đến khi nào chương trình kết thúc.
Có thể định nghĩa 1 biến global trong 1 file (.c/.cpp/.h) và truy cập biến này từ 1 file
(.c/.cpp/.h) khác. Để làm điều này, biến phải được khai báo ở cả 2 file và từ khóa extern
được thêm trong lần khai báo thứ 2.
Ví dụ:
Header1.h
#ifndef _HEADER_H_
#define _HEADER_H_
extern int X;
void display_x();
#endif
File1.cpp
#include "header1.h"
void main()
{
X = 5;
display_x();
}
File2.cpp
#include <stdio.h>
#include "Header1.h"
void display_x()
{
printf("X = %d", X);
}
Giải thích:
X=5
16.3 Biến static
Biến static có thể là global hoặc local. Cả hai đều được khai báo với từ khóa static đi kèm.
Biến local static là biến có thể duy trì giá trị từ lần gọi hàm thứ nhất đến các lần gọi hàm tiếp
theo. Biến local static tồn tại đến khi chương trình kết thúc.
Khi tạo 1 biến local static trong hàm, chúng ta nên khởi tạo giá trị cho chúng. Nếu không giá
trị biến được gán mặc định bằng 0.
Biến global static là biến global mà chỉ có thể truy cập từ file (.c/.cpp) mà biến đó được định
nghĩa.
Định nghĩa là việc implement tên biến, hàm, object. Linker sử dụng thông tin này để liên kết các
tham chiếu tới những biến, hàm, object tương ứng
int bar;
int g(int lhs, int rhs) { return lhs * rhs; }
double f(int i, double d) { return i + d; }
class foo {};
Việc định nghĩa biến/hàm/object phải chính xác. Nếu bạn quên không định nghĩa biến/hàm/object
đã được khai báo, linker không tìm được liên kết tham chiếu và báo lỗi missing symbol. Nếu bạn
định nghĩa nhiều hơn 1, linker không biết liên kết tham chiếu nào và báo lỗi duplicated symbol.
void main()
{
int A[] = { 1, 2, 3, 4, 5 };
int i;
for (i = 0; i < 5; i++)
printf("\nA[%d] = %d", i, *(A + i));
}
Giải thích:
A là địa chỉ của phần tử đầu tiên của mảng A, A+1 : địa chỉ của phần tử thứ 2,…
Giá trị của các phần tử của mảng:
*(A+0) = A[0] = 1
*(A+1) = A[1] = 2
…
*(A+4) = A[4] = 5
Vậy: Muốn lấy giá trị của phần tử trong mảng 1 chiều ta có thể dùng: *(A+i) , i là chỉ số mảng.
Output:
A[0] = 1
A[1] = 2
A[2] = 3
A[3] = 4
A[4] = 5
void main()
{
int A[N][M] = { 1, 2, 3,
4, 0, 5,
8, 9, 2,
3, 9, 3 };
int i, j;
for (i = 0; i < N; i++)
for (j = 0; j < M; j++)
{
printf("\nA[%d][%d] = %d", i, j, *(*(A + i) + j));
printf("\nAddress of element %d = %d", i + j, *(A + i) + j);
}
}
Giải thích:
Lấy giá trị của các phần tử mảng 2 chiều ta dùng biểu thức sau: *(*(A + i) + j)
Lấy địa chỉ của phần tử mảng 2 chiều ta dùng biểu thức sau: *(A + i) + j
(trong đó i,j là chỉ số hàng và cột)
Output:
A[0][0] = 1
Address of element 0 = 15726380
A[0][1] = 2
Address of element 1 = 15726384
A[0][2] = 3
Address of element 2 = 15726388
A[1][0] = 4
Address of element 1 = 15726392
A[1][1] = 0
Address of element 2 = 15726396
A[1][2] = 5
Address of element 3 = 15726400
A[2][0] = 8
Address of element 2 = 15726404
A[2][1] = 9
Address of element 3 = 15726408
A[2][2] = 2
Address of element 4 = 15726412
A[3][0] = 3
Address of element 3 = 15726416
A[3][1] = 9
Address of element 4 = 15726420
A[3][2] = 3
Address of element 5 = 15726424
20. Con trỏ hàm là gì? Khi nào dùng con trỏ hàm?
20.1 Con trỏ hàm là gì?
Như các bạn đã biết, con trỏ lưu địa chỉ của biến. Tương tự, con trỏ hàm lưu địa chỉ của hàm.
Khai báo:
Ví dụ 1:
int (*fpFunc)(int x, int y); // declare a function pointer
// execute operation
switch (opCode)
{
case '+':
result = Plus(a, b);
break;
case '-':
result = Minus(a, b);
break;
case '*':
result = Multiply(a, b);
break;
case '/':
result = Divide(a, b);
break;
}
printf("%f %c %f = %f\n", a, opCode, b, result);
}
Giải thích: Hàm assert() đánh giá biến ‘b’ nếu b = 0, exception message xuất hiện.
Output:
4.000000 + 5.600000 = 9.600000
4.000000 - 6.000000 = -2.000000
Assertion failed: b != 0, file C:\ConsoleApplication1.cpp, line 57
void main()
{
Switch(4, 5.6, &Plus);
Switch(4, 6, &Minus);
Switch(4, 0, &Divide);
}
Giải thích:
Hàm Switch() đã được modify bằng cách thay tham số cuối bằng con trỏ hàm. Và đối số
truyền vào là địa chỉ hàm: &Plus, &Minus,…
Chú ý:
Mỗi con trỏ hàm được khai báo với tham số (số lượng + kiểu dữ liệu) và giá trị trả về
Khi bạn muốn sử dụng 1 con trỏ hàm trỏ tới nhiều hàm (như ví dụ trên), thì tham số
(số lượng + kiểu dữ liệu) và giá trị trả về của con trỏ hàm và các hàm được trỏ tới
phải giống nhau.
Output:
result = 9.600000
result = -2.000000
Assertion failed: b != 0, file C:\ConsoleApplication1.cpp, line 40
ii. Con trỏ hàm được dùng làm đối số (argument) của hàm, hoặc có thể trả về con trỏ hàm để
giảm code thừa
Ví dụ 3: Trong thư viện hàm ngôn ngữ C, hàm qsort() được dùng để sắp xếp mảng theo thứ
tự tăng dần hoặc giảm dần. Với con trỏ hàm và con trỏ void, có thể sử dụng hàm qsort() với
bất kì kiểu dữ liệu nào
#include <stdio.h>
#include <stdlib.h>
int main()
{
int arr[] = { 10, 5, 15, 12, 90, 80 };
int n = sizeof(arr) / sizeof(arr[0]), i;
Output:
5 10 12 15 80 90
int *ptr[10];
#define LENGTH 30
void main(void)
{
char strarr[][LENGTH] = { "C/C++", "Java", "C#", "Python", "PHP", "Java Script" };
strsort(strarr, 6);
strshow(strarr, 6);
}
}
}
}
Output:
str[1] = C#
str[2] = C/C++
str[3] = Java
str[4] = Java Script
str[5] = PHP
str[6] = Python
Qua ví dụ ở trên, chúng ta thấy để sắp xếp 1 mảng các chuỗi yêu cầu swap các chuỗi với nhau ( sử
dụng hàm strcpy( ) hoán đổi nội dung giữa các chuỗi với nhau). Khi gặp các bài toán với kích thước
dữ liệu lớn (chuỗi, struct,…), việc copy dữ liệu làm giảm performance của chương trình.
→ Dùng con trỏ trỏ tới vùng nhớ của dữ liệu (chuỗi, struct,…), sau đó swap con trỏ.
Ví dụ 2: sắp xếp chuỗi theo thứ tự alpha, beta, … sử dụng mảng con trỏ
#include <stdio.h>
#include <string.h>
#define LENGTH 30
#define SIZE 10
strsort(pstr, 6);
strshow(pstr, 6);
}
Giải thích:
for (i = 0; i < 6; i++)
{
pstr[i] = strarr[i];
}
Gán phần tử mảng con trỏ tới mỗi chuỗi trong mảng chuỗi.
if (strcmp(str[j], str[j - 1]) < 0)
{
temp = str[j];
str[j] = str[j - 1];
str[j - 1] = temp;
}
Hoán vị con trỏ. Sau khi thực hiện xong hàm strsort(). Giá trị mảng con trỏ như sau:
Output:
str[1] = C#
str[2] = C/C++
str[3] = Java
str[4] = Java Script
str[5] = PHP
str[6] = Python
void main(void)
{
printf("The string: %s\n", str1);
memcpy(str1 + 2, str1, 6);
printf("New string: %s\n", str1);
Output:
Hàm memmove() đảm bảo kết quả đúng như mong muốn (“vncodi” ghi đè lên “coding”).
Không thể sử dụng trực tiếp dữ liệu mà con trỏ void trỏ tới bằng toán tử (*), mà cần biến đổi
con trỏ void* thành kiểu dữ liệu tương ứng.
Ví dụ 1: Gán con trỏ void cho con trỏ int
#include<stdio.h>
int main()
{
int a = 10;
void* p = &a;
int* ptr = p; // error
printf("%u\n", *ptr);
}
Output:
Trình biên dịch báo lỗi dòng int* ptr = p;
Error C2440 'initializing': cannot convert from 'void *' to 'int *'
int main()
{
int a = 10;
void* p = &a;
int* ptr = (int*)p;
printf("%u\n", *ptr);
}
Output:
10
Ở ví dụ này, ta đã fix lỗi ở ví dụ 1 bằng cách ép kiểu con trỏ (void*) thành (int*). Lúc này complier sẽ
hiểu là con trỏ p trỏ tới kiểu int.
Ví dụ 3: minh họa con trỏ void có thể trỏ tới các kiểu dữ liệu khác như struct, class
#include <stdio.h>
typedef struct
{
int date;
int month;
int year;
}DATE;
typedef struct
{
int hour;
int minute;
int second;
}TIME;
void main()
{
DATE d = { 16, 9, 1989 };
TIME t = { 14, 30, 0 };
showTime((void*)&d, 'd');
showTime((void*)&t, 't');
}
Giải thích:
Đoạn code trên hiển thị thông tin của struct DATE và TIME. Thông thường, chúng ta sẽ phải viết 2
hàm hiển thị cho 2 struct. Tuy nhiên, chúng ta có thể sử dụng tham số kiểu void* để ép kiểu khi
truyền đối số là kiểu DATE và TIME.
Khi vào hàm showTime(), dựa vào biến type để ép từ kiểu void* về kiểu dữ liệu tương ứng. Điều này
làm giảm thiểu việc viết nhiều function cùng chức năng nhưng khác kiểu dữ liệu đầu vào.
Output:
date = 16/09/1989
time = 14:30:00
Giải thích:
- Biến int num có giá trị 123 được lưu tại địa chỉ &num = 0x1235F
- Con trỏ pInt1 trỏ tới biến num, giá trị pInt1 = &num = 0x1235F, *pInt1 = num = 123
- Con trỏ double pInt2 trỏ tới con trỏ pInt1, giá trị pInt2 = &pInt1 = 0x5094F, *pInt2 = pInt1 =
&num = 0x1235F, **pInt2 = *pInt1 = num = 123
void main()
{
int* pInt;
bool bRes = memalloc(&pInt, 100);
if (bRes)
{
printf("\nMemmory allocation is successful");
}
memfree(&pInt);
}
void memfree(int** p)
{
if (*p)
{
free(*p);
*p = NULL;
}
}
Giải thích:
Sau khi thực hiện hàm memalloc(), con trỏ pInt trỏ tới vùng nhớ 100 byte. Sau khi thực hiện hàm
memfree(), giải phóng vùng nhớ trỏ bởi pInt và pInt được gán bằng NULL.
Output:
void main()
{
int* p = (int*)malloc(100);
int* q = p;
free(p);
p = NULL;
}
Giải thích:
Cấp phát vùng nhớ 100 byte trỏ tới bởi con trỏ p và q.
- Sau câu lệnh free(p), thì con trỏ p và q trỏ vào vùng nhớ bị free, và được gọi là dangling pointer.
- Sau câu lệnh gán p = NULL, con trỏ p không còn là dangling pointer, con trỏ q vẫn là dangling
pointer.
Ví dụ 2: hàm trả về vùng nhớ stack
#include <stdio.h>
int* getVal();
void main()
{
int* p = getVal();
}
int* getVal()
{
int x = 5;
return &x;
}
Giải thích:
Hàm getVal() trả về địa chỉ biến local x, sau câu lệnh return, chương trình free vùng nhớ stack chứa
biến local x. Con trỏ p trỏ với vùng nhớ invalid, p được gọi là dangling pointer.
NOTE: qua ví dụ trên, nếu tiếp tục sử dụng dangling pointer để truy cập vùng nhớ sẽ gây ra các lỗi về
memory như access violation. Để tránh dangling pointer, chúng ta cần lưu ý mấy điểm sau:
Khởi tạo NULL khi khai báo con trỏ
Sau khi free() hoặc delete memory, gán NULL cho con trỏ.
Empty
Cấp phát bộ nhớ động ( malloc, new)
HEAP được lưu trong heap
BSS Uninitialized data (BSS)
Data Initialized data (DS)
Low address -> Text Binary code
STACK
Vùng nhớ stack được sắp xếp ở địa chỉ cao, có thể tăng hoặc giảm.
Stack chứa các biến local được khai báo trong hàm.
Stack frame được tạo trong stack khi hàm được gọi.
Mỗi hàm có 1 stack frame riêng.
Stack frame chứa các đối số truyền vào hàm và giá trị trả về của hàm
Stack chứa cấu trúc LIFO (Last In – First Out). Các biến của hàm được push vào stack khi gọi hàm,
và pop-up khi kết thúc hàm.
SP (Stack Pointer) chứa địa chỉ của stack.
Ví dụ 1: biến local được lưu trong stack
void main()
{
int num;
}
Biến num là biến local được lưu trong vùng nhớ stack.
Heap
Được sử dụng để cấp phát vùng nhớ khi run-time (chương trình chạy)
Heap được quản lý bởi API (malloc, realloc, free).
Heap được chia sẻ giữa các DLL (Dynamic Link Library) trong cùng 1 process.
Ví dụ 2: vùng nhớ cấp phát malloc()
void main()
{
int* p = (int*)malloc(100);
}
Chương trình cấp phát vùng nhớ heap 100 byte khi run-time.
void main()
{
static int num;
}
Biến global gCount và biến static num được khai báo và không khởi tạo, được lưu vào BSS.
void main()
{
static int num = 10;
}
Text
Chứa binary code (source code được biên dịch thành mã máy).
Text segment cho phép permission read để tránh cho binary code bị modify.
void main(void)
{
int a = 5, b = 7;
float c = 5.6, d = 4.5;
printf("\nMAX(%d, %d) = %d", a, b, MAX(a, b));
printf("\nMAX(%f, %f) = %f", c, d, MAX(c, d));
}
Output:
MAX(5, 7) = 7
MAX(5.600000, 4.500000) = 5.600000
27.2 Hàm?
Hàm là 1 khối lệnh để thực hiện chức năng nào đó
Hàm có tham số truyền vào và giá trị trả về.
Ví dụ 2: viết hàm tìm giá trị lớn nhất của 2 số
#include <stdio.h>
void main(void)
{
int a = 5, b = 7;
printf("\nMAX(%d, %d) = %d", a, b, MAX(a, b));
}
Output:
MAX(5, 7) = 7
Macro Hàm
Việc định nghĩa macro khó hơn định nghĩa hàm. Việc định nghĩa đơn giản hơn
Dễ bị side effect. (xem ví dụ 3)
Không thể debug tìm lỗi của macro trong thời Có thể debug được, dễ cho việc tìm lỗi
gian thực thi.
Macro không cần quan tâm kiểu dữ liệu của Phải chỉ rõ kiểu dữ liệu của tham số và giá trị
tham số và kiểu trả về. Như ví dụ trên, chúng ta trả về
có thể truyền kiểu int, float.
Macro tạo ra các inline code, thời gian xử lí Chương trình mất time dịch từ vùng nhớ hàm
inline code ngắn hơn thời gian gọi hàm được lưu trữ sang vùng nhớ goi hàm.
Giả sử macro được gọi 20 lần trong chương Giả sử 1 hàm được gọi 20 lần, sẽ chỉ có 1 bản
trình, 20 dòng code sẽ được chèn vào chương copy của hàm trong chương trình. Kích thước
trình trong quá trình tiền xử lí. Điều này làm chương trình nhỏ hơn sử dụng macro.
cho kích thước của chương trình
(.EXE, .DLL, .LIB,…) phình to ra.
NOTE: Tùy thuộc vào tiêu chí thời gian thực thi hay kích thước chương trình, mà bạn quyết định
chọn macro hay hàm trong chương trình của mình. Đối với khối chức năng đơn giản ít dòng code,
nên sử dụng macro.
Ví dụ 3: side effect khi sử dụng macro
#include <stdio.h>
void main(void)
{
printf("SQUARE(%d) = %d", 3 + 5, SQUARE(3 + 5));
}
Giải thích:
Kết quả mong muốn sẽ là 8*8 = 64. Nhưng thực tế, kết quả như sau: 3+5*3+5 = 3 + 15 + 5 = 23.
Khi bạn định nghĩa macro, phải chú ý dấu ngoặc. Macro SQUARE được update lại như sau:
#define SQUARE(X) ((X)*(X))
#define SZ 10
void main(void)
{
int i;
int* p = (int*)malloc(SZ * sizeof(int));
free(p);
}
Giải thích:
Nếu câu lệnh cấp phát malloc() trả về NULL, chương trình trên bị crash ở dòng code p[i] = i * i; vì
access con trỏ NULL.
Do vậy cần check giá trị trả về hàm malloc, xem ví dụ 2.
Ví dụ 2: check giá trị trả về hàm malloc()
#include <stdio.h>
#include <stdlib.h>
#define SZ 10
void main(void)
{
int i;
int* p = (int*)malloc(SZ * sizeof(int));
if (p == NULL)
{
printf("Error in memory allocation");
return;
}
free(p);
}
Giải thích:
Nếu hàm malloc() trả về NULL, in ra thông báo và kết thúc hàm. Đây chỉ là ví dụ minh họa xử lý
trong trường hợp NULL. Tùy vào design source code, mà sẽ có cách xử lí khác nhau như khi hàm
malloc() trả về NULL, thực hiện retry lại hàm malloc() để cấp memory.
Không khởi tạo vùng nhớ sau khi cấp phát
Ngôn ngữ C sử dụng hàm malloc() cấp phát vùng nhớ động theo block. Có thể quên không khởi tạo
vùng nhớ hoặc tưởng nhầm rằng, hàm malloc() khởi tạo giá trị vùng nhớ là 0. Việc sử dụng vùng
nhớ không được khởi tạo có thể gây ra một số bug tiềm ẩn.
Ví dụ 3: không khởi tạo vùng nhớ sau khi cấp phát
#include <stdio.h>
#include <stdlib.h>
#define SZ 10
void main(void)
{
int i;
int* p = (int*)malloc(SZ * sizeof(int));
if (p == NULL)
{
printf("Error in memory allocation");
return;
}
free(p);
}
Giải thích:
Do chưa khởi tạo sau khi cấp phát vùng nhớ, giá trị p[i] là giá trị rác (garbage value), dẫn đến phép
toán p[i] = p[i] * i cho kết quả sai.
Cần khởi tạo giá trị sau khi cấp phát vùng nhớ bằng hàm memset() hoặc dùng hàm calloc() thay cho
hàm malloc().
Double free vùng nhớ
Hàm free(p) giải phóng vùng nhớ được cấp phát, giá trị con trỏ p vẫn chứa địa chỉ vùng nhớ đã giải
phóng. Nếu tiếp tục gọi hàm free(p) sẽ gây lỗi heap, vì giải phóng vùng nhớ trái phép.
Ví dụ 4: gọi hàm free() 2 lần
#include <stdio.h>
#include <stdlib.h>
#define SZ 10
void main(void)
{
int i;
int* p = (int*)malloc(SZ * sizeof(int));
if (p == NULL)
{
printf("Error in memory allocation");
return;
}
free(p);
free(p);
}
Giải thích:
Việc free() 2 lần gây ra lỗi heap corruption. Nên gán p = NULL sau lệnh free(), sẽ giúp hạn chế lỗi.
Trong trường hợp free() 2 lần, hàm free(p) không được thực hiện nếu p = NULL.
Free() vùng nhớ không được tạo ra bởi hàm malloc(), calloc(), realloc().
Chỉ sử dụng hàm free() để giải phóng vùng nhớ được cấp phát bởi malloc(), calloc(), realloc(). Nếu
cố sử dụng hàm free() để giải phóng vùng nhớ biến, mảng tĩnh,…, sẽ gây ra lỗi segmentation fault.
Ví dụ 5: free vùng nhớ biến local
#include <stdio.h>
#include <stdlib.h>
void main(void)
{
int i;
int* p = &i;
free(p);
}
void main(void)
{
int* p = (int*)malloc(10 * sizeof(int));
/* do something*/
/* not free() */
}
void main(void)
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
printf("Error in memory allocation");
return;
}
printf("size of pointer: %d", sizeof(p));
}
Output:
size of pointer: 4
Để tính toán kích thước vùng nhớ động, chúng ta có 2 cách: sử dụng hàm _msize() hoặc lưu kích
thước vùng nhớ khi cấp phát vào phần tử đầu tiên.
Ví dụ 8: Sử dụng hàm _msize() tính kích thước bộ nhớ động
#include <stdio.h>
#include <stdlib.h>
void main(void)
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
printf("Error in memory allocation");
return;
}
printf("size of memory: %d", _msize(p));
}
Output:
size of memory: 40
Ví dụ 9: lưu kích thước khi cấp phát vào phần tử đầu tiên
#include <stdio.h>
#include <stdlib.h>
int* arrAlloc(int);
void main(void)
{
int sz = 10;
int* p = arrAlloc(sz);
int* temp = p + 1;
// Initialize array
for (size_t i = 1; i < sz + 1; i++)
{
p[i] = i;
}
return p;
}
Giải thích:
int* p = (int*)malloc((sz + 1) * sizeof(int));
p[0] = sz;
Con trỏ p trỏ tới vùng nhớ gồm 11 phần tử như sau
10 1 2 3 4 5 6 7 8 9 10
p temp
Output:
temp[0] = 1
temp[1] = 2
temp[2] = 3
temp[3] = 4
temp[4] = 5
temp[5] = 6
temp[6] = 7
temp[7] = 8
temp[8] = 9
temp[9] = 10
void main(void)
{
int* p = (int*)malloc(0 * sizeof(int));
if (!p)
{
printf("Error in memory allocation");
return;
}
p[0] = 1;
free(p);
p = NULL;
}
Output:
Lỗi heap corruption tại câu lệnh p[0] = 1
Truy cập vào phần tử nằm ngoài vùng nhớ cấp phát
Một số developer mắc phải lỗi access vào phần tử nằm ngoài vùng nhớ cấp phát, gây ra lỗi access
violation. Để tránh lỗi này chúng ta có thể thêm các điều kiện kiểm tra trước khi access array.
Ví dụ 12: kiểm tra điều kiện tránh lỗi access violation
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = NULL;
int n = 10;
int pos = 0;
p = (int*)malloc(sizeof(int) * n);
if (p == NULL)
{
return -1;
}
do {
printf("Enter the array index = ");
scanf("%d", &pos);
} while (pos >= n && pos < 0);
return 0;
}
Giải thích:
Vòng lặp do while() chỉ cho phép người dùng nhập giá trị index hợp lệ
Output:
int main()
{
int* p = NULL;
int n = 10;
p = (int*)malloc(sizeof(int) * n);
if (p == NULL)
{
return -1;
}
p++;
free(p);
p = NULL;
return 0;
}
Giải thích:
Sau khi cấp phát p = 0x00f25020, sau lệnh p++ p = p + 4 = 0x00f25024. Địa chỉ vùng nhớ bị thay
đổi, vùng nhớ 0x00f25024 là vùng nhớ invalid, dẫn đến câu lệnh free(p) gây ra lỗi access violation.
Để tránh lỗi này, chúng ta chỉ nên thao tác tính toán trên con trỏ temp thay vì trên con trỏ gốc. Xem
ví dụ 14.
Ví dụ 14: Thao tác tính toán trên con trỏ temp
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = NULL;
int n = 10;
p = (int*)malloc(sizeof(int) * n);
if (p == NULL)
{
return -1;
}
int* temp = p;
temp++;
free(p);
p = NULL;
return 0;
}
Dangling pointer
Cả 2 con trỏ p1, p2 cùng trỏ vào 1 vùng nhớ. Nếu gọi hàm free(p1), sau đó vẫn thao tác tính toán
trên con trỏ p2, dẫn đến lỗi heap corruption. Con trỏ p2 được gọi là dangling pointer.
Ví dụ 15: Dangling pointer
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p1 = NULL;
int* p2 = NULL;
p1 = (int*)malloc(sizeof(int));
if (p1 == NULL)
{
return -1;
}
*p1 = 100;
printf(" *piData1 = %d\n", *p1);
p2 = p1;
printf(" *piData1 = %d\n", *p2);
free(p1);
*p2 = 50;
printf(" *piData2 = %d\n", *p2);
return 0;
}
Giải thích:
Cả 2 con trỏ cùng trỏ vào 1 vùng nhớ động, free(p1) giải phóng vùng nhớ, p2 trỏ vào vùng nhớ
invalid *p2 = 50 gây ra lỗi heap corruption hoặc access violation.
29. Khi nào nên khai báo con trỏ hàm trong struct?
Ngôn ngữ C, chúng ta không để khai báo function trong struct, nhưng chúng ta có thể khai báo con
trỏ hàm trong struct, dựa vào con trỏ hàm, chúng ta có thể gọi hàm khi cần.
29.1 Step khai báo con trỏ hàm strong struct
Khai báo con trỏ hàm
typedef void (*pfnMessage)(const char*, float fResult);
typedef float (*pfnCalculator)(float, float);
29.2 Cách sử dụng gọi gọi con trỏ hàm trong struct
Ví dụ: gọi con trỏ hàm trong struct để tính toán phép + - * /
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
// Addition
float Addition(float a, float b)
{
return (a + b);
}
// Subtraction
float Subtraction(float a, float b)
{
return (a - b);
}
// Multiplication
float Multiplication(float a, float b)
{
return (a * b);
}
// Division
float Division(float a, float b)
{
assert(b != 0);
return (a / b);
}
// Display message
void Message(const char* pcMessage, float fResult)
{
printf("\n\n %s = %f\n\n\n\n", pcMessage, fResult);
}
// Arithmetic operation
void PerformCalculation(float x, float y, sArithMaticOperation* funptr, const char*
pcMessage)
{
float result = funptr->ArithmaticOperation(x, y);
funptr->DisplayMessage(pcMessage, result);
funptr->iResult = result;
}
int main(void)
{
char szMessage[32] = { 0 };
int iChoice = 0;
float fData1 = 0.0f;
float fData2 = 0.0f;
sArithMaticOperation* psArithmaticOperation = NULL;
psArithmaticOperation =
(sArithMaticOperation*)malloc(sizeof(sArithMaticOperation));
if (psArithmaticOperation == NULL)
{
return -1;
}
psArithmaticOperation->DisplayMessage = &Message;
while (1)
{
printf("\n\n 1.Add \n\
2.Sub \n\
3.Mul \n\
4.Div \n\n\n");
printf(" Enter the operation Choice = ");
scanf("%d", &iChoice);
switch (iChoice)
{
case 1:
printf("\n Enter the numbers : ");
scanf("%f", &fData1);
printf("\n Enter the numbers : ");
scanf("%f", &fData2);
psArithmaticOperation->ArithmaticOperation = &Addition;
strcpy(szMessage, "Addition of two Number = ");
break;
case 2:
printf("\n Enter the numbers : ");
scanf("%f", &fData1);
printf("\n Enter the numbers : ");
scanf("%f", &fData2);
psArithmaticOperation->ArithmaticOperation = &Subtraction;
strcpy(szMessage, "Subtraction of two Number = ");
break;
case 3:
printf("\n Enter the numbers : ");
scanf("%f", &fData1);
printf("\n Enter the numbers : ");
scanf("%f", &fData2);
psArithmaticOperation->ArithmaticOperation = &Multiplication;
strcpy(szMessage, "Multiplication of two Number = ");
break;
case 4:
printf("\n Enter the numbers : ");
scanf("%f", &fData1);
printf("\n Enter the numbers : ");
scanf("%f", &fData2);
psArithmaticOperation->ArithmaticOperation = &Division;
strcpy(szMessage, "Division of two Number = ");
break;
default:
printf(" \n Invalid Choice :\n\n");
exit(0);
}
PerformCalculation(fData1, fData2, psArithmaticOperation, szMessage);
}
//Free the allocated memory
free(psArithmaticOperation);
return 0;
}
Giải thích:
float Addition(float a, float b)
psArithmaticOperation->DisplayMessage = &Message;
Truy cập vào con trỏ hàm DisplayMessage và trỏ tới hàm Message().
psArithmaticOperation->ArithmaticOperation = &Addition;
psArithmaticOperation->ArithmaticOperation = &Subtraction;
psArithmaticOperation->ArithmaticOperation = &Multiplication;
psArithmaticOperation->ArithmaticOperation = &Division;
Tùy vào lựa chọn user, con trỏ hàm ArithmaticOperation trỏ tới hàm tương ứng.
PerformCalculation(fData1, fData2, psArithmaticOperation, szMessage);
Gọi hàm PerformCalculation(), thực thi 2 hàm: hàm Addition hoặc Subtraction hoặc Multiplication
hoặc Division, hàm Message() để hiển thị kết quả tính toán
Output:
class Test {
int x;
};
int main()
{
Test t;
t.x = 20;
return 0;
}
Output:
Error C2248 'Test::x': cannot access private member declared in class 'Test'
Ví dụ 2: truy xuất member public của struct
#include <stdio.h>
struct Test {
int x;
};
int main()
{
Test t;
t.x = 20;
return 0;
}
Khi struct kế thừa từ class/struct, mặc định việc kế thừa là public. Còn class kế thừa class/struct,
mặc định việc kế thừa là private.
Ví dụ 3: class kế thừa từ class
#include <stdio.h>
class Base {
public:
int x;
};
int main()
{
Derived d;
d.x = 20;
return 0;
}
Giải thích:
class Derived : Base { };
Câu lệnh tương đương với câu lệnh sau:
class Derived : private Base { };
Chương trình biên dịch thông báo lỗi vì truy cập vào member private ‘x’
Error C2247 'Base::x' not accessible because 'Derived' uses 'private' to inherit from 'Base'
class Base {
public:
int x;
};
int main()
{
Derived d;
d.x = 20;
return 0;
}
Giải thích:
struct Derived : Base { };
Việc truy xuất vào member public ‘x’ là hoàn toàn hợp lệ
malloc() new
malloc được định nghĩa trong thư viện ngôn Toán tử new chỉ được support trong ngôn ngữ
ngữ C, có thể sử dụng trong C++ C++
malloc() không gọi hàm tạo Toán tử new gọi hàm tạo
class Test {
int a;
public:
int* ptr;
Test()
{
cout << "Constructor is called!" << endl;
}
};
int main()
{
Test* a = (Test*)malloc(sizeof(Test));
cout << "Object of class Test is "
<< "created using malloc()!"
<< endl;
return 0;
}
Giải thích:
Hàm malloc() không gọi hàm tạo, toán tử new gọi hàm tạo khi cấp phát bộ nhớ động cho con trỏ
object.
Output:
class construct
{
public:
int a, b;
construct()
{
cout << "constructor is called" << endl;
a = 10;
b = 20;
}
};
int main()
{
construct obj;
cout << "a = " << obj.a << endl
<< "b = " << obj.b;
return 1;
}
Giải thích:
construct obj;
Hàm tạo mặc định của class construct được gọi và khởi tạo giá trị ‘a’ và ‘b’
Output:
constructor is called
a = 10
b = 20
Hàm tạo có tham số làm hàm tạo nhận các giá trị truyền vào làm đối số. Hàm tạo có tham số
được sử dụng khi cần khởi tạo nhiều đối tượng với các giá trị khác nhau.
Ví dụ 2: hàm tạo có tham số
#include <iostream>
using namespace std;
class Point
{
private:
int x, y;
public:
Point(int x1, int y1)
{
cout << "Constructor is called" << endl;
x = x1;
y = y1;
}
int getX()
{
return x;
}
int getY()
{
return y;
}
};
int main()
{
// Constructor called
Point p1(10, 15);
cout << "p1.x = " << p1.getX() << ", p1.y = " << p1.getY();
return 0;
}
Giải thích:
Point p1(10, 15);
Khởi tạo object p1 với đối số (10, 15), hàm tạo tham số Point(10, 15) được gọi.
Output:
Constructor is called
p1.x = 10, p1.y = 15
class Point
{
private:
int x, y;
public:
Point(int x1, int y1)
{
cout << "Parameter constructor is called" << endl;
x = x1; y = y1;
}
// Copy constructor
Point(const Point& p2)
{
cout << "Copy constructor is called" << endl;
x = p2.x; y = p2.y;
}
int main()
{
Point p1(10, 15);
Point p2 = p1;
cout << "p1.x = " << p1.getX() << ", p1.y = " << p1.getY() << endl;
cout << "p2.x = " << p2.getX() << ", p2.y = " << p2.getY() << endl;
return 0;
}
Giải thích:
Point p1(10, 15);
Khai báo object ‘p1’, hàm tạo tham số Point(10, 15) được gọi để khởi tạo object ‘p1’.
Point p2 = p1;
Khai báo object ‘p2’, gán p2 = p1, hàm tạo copy được gọi để gán các giá trị object ‘p1’ cho ‘p2’.
Output:
Hàm tạo copy mặc định thực hiện copy từng member.
Ví dụ 2: hàm tạo copy mặc định
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;
class Student
{
public:
// conventional constructor
Student(const char* pName = "no name", int ssId = 0)
: name(pName), id(ssId)
{
cout << "Constructed " << name << endl;
}
// copy constructor
Student(const Student& s)
: name("Copy of " + s.name), id(s.id)
{
cout << "Constructed " << name << endl;
}
~Student() { cout << "Destructing " << name << endl; }
protected:
string name;
int id;
};
class Tutor
{
protected:
Student student;
int id;
public:
Tutor(Student& s)
: student(s), id(0)
{
cout << "Constructing Tutor object" << endl;
}
};
void fn(Tutor tutor)
{
cout << "In function fn()" << endl;
}
return 0;
}
Giải thích:
Student obj("Linda");
Khởi tạo object ‘obj’ với tham số “Linda”, hàm tạo Student() được gọi in ra “Constructed Linda”.
Tutor tutor(obj);
Khởi tạo object ‘tutor’ với tham số là object ‘obj’, hàm tạo Tutor() gọi hàm tạo copy của class
Student và in ra “Constructed Copy of Linda”, trong hàm tạo Tutor() in ra “Constructing Tutor
object”.
fn(tutor);
Gọi hàm fn(), vì chúng ta define hàm tạo copy cho class Tutor, hàm tạo copy mặc định được gọi để
tạo ra bản copy của object ‘tutor’. Hàm tạo copy mặc định Tutor sẽ gọi hàm tạo copy Student(), hiển
thị “Constructed Copy of Copy of Linda ”. Kết thúc hàm fn(), hàm hủy được gọi để hủy bản copy của
object ‘tutor’, in ra “Destructing Copy of Copy of Linda”. Kết thúc hàm main(), hàm hủy được gọi tự
động để hủy object ‘tutor’
Output:
Constructed Linda
Constructed Copy of Linda
Constructing Tutor object
Calling fn()
Constructed Copy of Copy of Linda
In function fn()
Destructing Copy of Copy of Linda
Back in main()
Destructing Copy of Linda
Destructing Linda
class Point
{
private:
int x, y;
public:
Point(int x1, int y1)
{
cout << "Parameter constructor is called" << endl;
x = x1; y = y1;
}
// Copy constructor
Point(const Point& p2)
{
cout << "Copy constructor is called" << endl;
x = p2.x; y = p2.y;
}
cout << "p1.x = " << p1.getX() << ", p1.y = " << p1.getY() << endl;
cout << "p2.x = " << p2.getX() << ", p2.y = " << p2.getY() << endl;
return 0;
}
Giải thích:
Point p1(10, 15);
Khai báo object ‘p1’, hàm tạo tham số Point(10, 15) được gọi để khởi tạo object ‘p1’.
Point p2 = p1;
Khai báo object ‘p2’, gán p2 = p1, hàm tạo copy được gọi để gán các giá trị object ‘p1’ cho ‘p2’.
Output:
Object được truyền vào phương thức của class theo kiểu tham trị
Ví dụ 2: truyền biến object vào phương thức
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;
class Student
{
public:
// conventional constructor
Student(const char* pName = "no name", int ssId = 0)
: name(pName), id(ssId)
{
cout << "Constructed " << name << endl;
}
// copy constructor
Student(const Student& s)
: name("Copy of " + s.name), id(s.id)
{
cout << "Constructed " << name << endl;
}
~Student() { cout << "Destructing " << name << endl; }
protected:
string name;
int id;
};
// fn - receives its argument by value
void fn(Student copy)
{
cout << "In function fn()" << endl;
}
int main(int nNumberofArgs, char* pszArgs[])
{
Student obj("Linda", 1234);
cout << "Calling fn()" << endl;
fn(obj);
cout << "Back in main()" << endl;
return 0;
}
Giải thích:
Student(const char* pName = "no name", int ssId = 0)
: name(pName), id(ssId)
Nếu khai báo object không có parameter, thì hàm tạo sử dụng giá trị “no name” và “0” để gán cho
‘name’ và ‘id’.
Student obj("Linda", 1234);
Khởi tạo object ‘obj’ với giá trị truyền vào “Linda” và “1234”, hàm tạo được gọi.
fn(obj);
Khi gọi hàm fn(obj), C++ gọi hàm tạo copy để tạo ra bản copy của object ‘obj’ để truyền vào hàm
fn(). Hàm hủy được gọi khi kết thúc hàm fn() để hủy bản copy của object ‘obj’.
Kết thúc hàm main, hàm hủy được gọi lần nữa để hủy object ‘obj’.
Output:
Constructed Linda
Calling fn()
Constructed Copy of Linda
In function fn()
Destructing Copy of Linda
Back in main()
Destructing Linda
Khi phương thức của lớp trả về object kiểu giá trị
Ví dụ 2: hàm tạo copy được gọi khi hàm trả về object kiểu giá trị
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;
class Student
{
public:
// conventional constructor
Student(const char* pName = "no name", int ssId = 0)
: name(pName), id(ssId)
{
cout << "Constructed " << name << endl;
}
// copy constructor
Student(const Student& s)
: name("Copy of " + s.name), id(s.id)
{
cout << "Constructed " << name << endl;
}
~Student() { cout << "Destructing " << name << endl; }
protected:
string name;
int id;
};
Student fn()
{
Student copy("Peter", 23456);
cout << "In function fn()" << endl;
return copy;
}
return 0;
}
Giải thích:
Student obj;
Khai báo object ‘obj’, gọi hàm tạo Student() với tham số mặc định “no name” và 0. In ra màn hình
“Constructed no name”
obj = fn();
Gọi hàm fn()
Student fn()
{
Student copy("Peter", 23456);
cout << "In function fn()" << endl;
return copy;
}
Trong hàm fn(), khởi tạo object ‘copy’ với tham số “Peter” và 23456. In ra màn hình “Constructed
Peter”, “In function fn()”. Khi gọi câu lệnh return copy; chương trình tạo ra temporary object và gọi
hàm tạo copy, name = “Copy of Peter”, in ra màn hình “Constructed Copy of Peter”. Hàm hủy được
gọi tự động để hủy object ‘copy’, in ra màn hình “Destructing Peter”. Hàm hủy được gọi tự động để
hủy temporary object, in ra màn hình “Destructing Copy of Peter”.
obj = fn();
object ‘obj’ có name = “Copy of Peter”, id = 23456.
Kết thúc hàm main(), hàm hủy được gọi tự động để hủy object ‘obj’, in ra màn hình “Destructing
Copy of Peter”.
Output:
Constructed no name
Constructed Peter
In function fn()
Constructed Copy of Peter
Destructing Peter
Destructing Copy of Peter
Back in main()
Destructing Copy of Peter
Việc gọi hàm tạo copy để copy object khá tốn thời gian, giảm performance chương trình. Để tránh
việc tạo temprorary object, cách đơn giản nhất là truyền object vào cho hàm hoặc là trả về object
kiểu tham chiếu.
Ví dụ 3: hàm trả về object kiểu tham chiếu
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;
class Student
{
public:
// conventional constructor
Student(const char* pName = "no name", int ssId = 0)
: name(pName), id(ssId)
{
cout << "Constructed " << name << endl;
}
// copy constructor
Student(const Student& s)
: name("Copy of " + s.name), id(s.id)
{
cout << "Constructed " << name << endl;
}
~Student() { cout << "Destructing " << name << endl; }
protected:
string name;
int id;
};
Student& fn()
{
Student copy("Peter", 23456);
cout << "In function fn()" << endl;
return copy;
}
return 0;
}
Giải thích:
Gọi hàm fn(), trong hàm fn() khởi tạo object ‘copy’ và trả về tham chiếu đến object ‘copy’, mà
không tạo ra thêm temporary object, không gọi hàm tạo copy, mà tham chiếu ‘obj’ trỏ tới object
‘copy’.
Output:
Constructed Peter
In function fn()
Destructing Peter
Back in main()
class Student
{
public:
// conventional constructor
Student(const char* pName = "no name", int ssId = 0)
{
name = _strdup(pName);
id = ssId;
cout << "Constructed " << name << endl;
}
~Student()
{
cout << "Destructing " << ((name != NULL) ? name : "(NULL)") << endl;
free(name);
name = NULL;
}
protected:
char* name;
int id;
};
return 0;
}
Giải thích:
Student obj("Peter", 12345);
Gọi hàm tạo cấp phát bộ nhớ động bằng hàm _strdup() và trỏ bởi con trỏ ‘name’.
Student copy = obj;
Hàm tạo copy mặc định được gọi để copy từng member của object ‘obj’ sang object ‘copy’.
copy.id = obj.id = 12345
copy.name = obj.name = 0x00a8e0f8, cùng trỏ vào 1 vùng nhớ
copy.name
"Peter"
0x00a8e0f8
obj.name
Kết thúc hàm main(), hàm hủy được gọi 2 lần để hủy object ‘obj’ và ‘copy’. Lần gọi hàm hủy đầu tiên,
giải phóng vùng nhớ obj.name = 0x00a8e0f8, gán obj.name = NULL, con trỏ copy.name là dangling
pointer, trỏ tới vùng nhớ invalid. Lần gọi hàm hủy thứ 2, chương trình báo lỗi heap corruption do
free 2 lần cùng 1 vùng nhớ.
copy.name
Garbage value
obj.name 0x00a8e0f8
= NULL
class Student
{
public:
// conventional constructor
Student(const char* pName = "no name", int ssId = 0)
{
name = _strdup(pName);
id = ssId;
cout << "Constructed " << name << endl;
}
// copy constructor
Student(const Student& s)
{
name = _strdup(s.name);
id = s.id;
cout << "Copy of Constructed " << name << endl;
}
~Student()
{
cout << "Destructing " << ((name != NULL) ? name : "(NULL)") << endl;
free(name);
name = NULL;
}
protected:
char* name;
int id;
};
return 0;
}
Giải thích:
Student obj("Peter", 12345);
Gọi hàm tạo cấp phát bộ nhớ động bằng hàm _strdup() và trỏ bởi con trỏ ‘name’.
Student copy = obj;
Hàm tạo copy được gọi để khởi tạo object ‘copy’. Điểm khác so với ví dụ 1, là cấp phát vùng nhớ
riêng cho con trỏ copy.name
copy.nam
"Peter"
e
0x00a8e0f8
obj.name "Peter"
0x013c8f58
Khi kết thúc hàm main(), hàm hủy được gọi 2 lần để hủy object ‘obj’ và ‘copy’, gọi hàm free() để giải
phóng vùng nhớ trỏ bởi obj.name và copy.name
Output:
Constructed Peter
Copy of Constructed Peter
Back in main()
Destructing Peter
Destructing Peter
38. Shadow copy và deep copy?
Hàm tạo copy mặc định thực hiện shadow copy, chỉ có hàm tạo copy do developer define mới có thể
thực hiện được deep copy.
Xem 2 ví dụ sau để hiểu sự khác nhau giữa shadow copy và deep copy
Ví dụ 1: Shadow copy
#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <iostream>
using namespace std;
class Student
{
public:
// conventional constructor
Student(const char* pName = "no name", int ssId = 0)
{
name = _strdup(pName);
id = ssId;
cout << "Constructed " << name << endl;
}
~Student()
{
cout << "Destructing " << ((name != NULL) ? name : "(NULL)") << endl;
free(name);
name = NULL;
}
protected:
char* name;
int id;
};
return 0;
}
Giải thích:
Student obj("Peter", 12345);
Gọi hàm tạo cấp phát bộ nhớ động bằng hàm _strdup() và trỏ bởi con trỏ ‘name’.
Student copy = obj;
Hàm tạo copy mặc định được gọi để copy từng member của object ‘obj’ sang object ‘copy’.
copy.id = obj.id = 12345
copy.name = obj.name = 0x00a8e0f8, cùng trỏ vào 1 vùng nhớ
copy.name
"Peter"
0x00a8e0f8
obj.name
Kết thúc hàm main(), hàm hủy được gọi 2 lần để hủy object ‘obj’ và ‘copy’. Lần gọi hàm hủy đầu tiên,
giải phóng vùng nhớ obj.name = 0x00a8e0f8, gán obj.name = NULL, con trỏ copy.name là dangling
pointer, trỏ tới vùng nhớ invalid. Lần gọi hàm hủy thứ 2, chương trình báo lỗi heap corruption do
free 2 lần cùng 1 vùng nhớ.
copy.name
Garbage value
obj.name 0x00a8e0f8
= NULL
Ví dụ 2: Deep copy
#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <iostream>
using namespace std;
class Student
{
public:
// conventional constructor
Student(const char* pName = "no name", int ssId = 0)
{
name = _strdup(pName);
id = ssId;
cout << "Constructed " << name << endl;
}
// copy constructor
Student(const Student& s)
{
name = _strdup(s.name);
id = s.id;
cout << "Copy of Constructed " << name << endl;
}
~Student()
{
cout << "Destructing " << ((name != NULL) ? name : "(NULL)") << endl;
free(name);
name = NULL;
}
protected:
char* name;
int id;
};
return 0;
}
Giải thích:
Student obj("Peter", 12345);
Gọi hàm tạo cấp phát bộ nhớ động bằng hàm _strdup() và trỏ bởi con trỏ ‘name’.
Student copy = obj;
Hàm tạo copy được gọi để khởi tạo object ‘copy’. Điểm khác so với ví dụ 1, là cấp phát vùng nhớ
riêng cho con trỏ copy.name
copy.nam
"Peter"
e
0x00a8e0f8
obj.name "Peter"
0x013c8f58
Khi kết thúc hàm main(), hàm hủy được gọi 2 lần để hủy object ‘obj’ và ‘copy’, gọi hàm free() để giải
phóng vùng nhớ trỏ bởi obj.name và copy.name
Output:
Constructed Peter
Copy of Constructed Peter
Back in main()
Destructing Peter
Destructing Peter
class CMySingleton
{
public:
static CMySingleton& GetInstance()
{
static CMySingleton singleton;
return singleton;
}
return 0;
}
Giải thích:
CMySingleton& object = CMySingleton::GetInstance();
Hàm GetInstance() trả về tham chiếu đến object static ‘singleton’.
CMySingleton object1;
Compiler báo lỗi do không truy cập vào hàm tạo private để khởi tạo đối tượng
CMySingleton object2(object);
Compiler báo lỗi do không truy cập được vào hàm copy private.
object1 = object;
Compiler báo lỗi do toán tử gán kiểu private.
40. Tại sao đối số của hàm tạo copy phải là kiểu tham chiếu?
Nhắc lại cách khai báo hàm tạo class A
A(const A&)
Hàm tạo copy cũng là hàm thông thường. Nếu truyền đối số kiểu giá trị, hàm tạo copy gọi đến chính
nó, tạo thành đệ quy vô tận, gây ra crash chương trình.
41. Tại sao đối số của hàm tạo copy nên là const?
Khai báo hàm tạo class A
A(const A&)
Việc khai báo const, tránh việc đối số object bị thay đổi giá trị.
42. Toán tử gán là gì?
Toán tử gán (cho class) là một trường hợp đặc biệt so với các toán tử khác. Nếu trong lớp chưa
định nghĩa một phương thức toán tử gán thì compiler sẽ tạo một toán tử mặc định để thực hiện
câu lệnh gán 2 object của class.
Trong đa số các trường hợp, khi class không có các thành phần con trỏ hay tham chiếu thì toán
tử gán mặc định là đủ dùng và không cần define một phương thức toán tử gán cho lớp.
Ví dụ 1: Sử dụng toán tử gán mặc định
#include <iostream>
using namespace std;
class Test
{
private:
int x;
float y;
public:
Test() {}
Test(int n, float m)
{
x = n;
y = m;
}
void show()
{
cout << "x = " << x << ", y = " << y;
}
};
int main()
{
Test obj1(10, 10.1);
Test obj2;
obj2 = obj1;
obj2.show();
return 0;
}
Giải thích:
obj2 = obj1;
Trong chương trình không define toán tử gán, compiler gọi toán tử gán mặc định copy object ‘obj1’
vào ‘obj2’ theo từng bit một.
Trong class ‘Test’ không có thành phần con trỏ hay tham chiếu, việc sử dụng hàm tạo mặc định là
đủ.
Kết quả:
x = 10, y = 10.1
Khi trong class có thành phần con trỏ, tham chiếu, cần phải define toán tử gán riêng cho class.
Ví dụ 2: Sử dụng hàm tạo mặc định khi class có thành phần con trỏ
#include <iostream>
using namespace std;
class Test
{
private:
int x;
float y;
char* name;
public:
Test() {}
Test(int n, float m, const char s[])
{
x = n;
y = m;
name = _strdup(s);
}
~Test()
{
free(name);
name = NULL;
}
void show()
{
cout << "x = " << x << ", y = " << y << ", name = " << name;
}
};
int main()
{
Test obj1(10, 10.1, "vncoding");
Test obj2;
obj2 = obj1;
obj2.show();
return 0;
}
Giải thích:
Test obj1(10, 10.1, "vncoding");
Gọi hàm tạo để khởi tạo object ‘obj1’
Chú ý: trong hàm tạo có đối số Test(int n, float m, const char s[]), hàm _strdup(“vncoding”) cấp phát
bộ nhớ động và có giá trị là chuỗi “vncoding”
Khi bạn debug sẽ thấy giá trị object ‘obj1’ như sau:
{x=10 y=10.1000004 name=0x014ae788 "vncoding" }
(0x014ae788 là địa chỉ vùng nhớ động được cấp phát bởi hàm _strdup())
Test obj2;
Tạo object ‘obj2’, hàm tạo Test() không đối số được gọi.
obj2 = obj1;
Toán tử gán mặc định được gọi. Khi bạn debug sẽ thấy giá trị object ‘obj2’ như sau:
{x=10 y=10.1000004 name=0x014ae788 "vncoding" }
Chúng ta thấy con trỏ obj2.name không được cấp phát vùng nhớ mới, mà chỉ trỏ đến vùng nhớ
obj1.name = 0x014ae788, giống như shadow copy của hàm tạo copy.
obj2.show();
Show ra màn hình console: x = 10, y = 10.1, name = vncoding
return 0;
Gọi câu lệnh return, hàm hủy gọi để hủy lần lượt object ‘obj1’, ‘obj2’.
Lần gọi hàm hủy đầu tiên, vùng nhớ obj1.name = 0x014ae788 bị hủy, con trỏ obj2.name trở thành
dangling pointer.
Lần gọi hàm hủy tiếp theo, chương trình báo lỗi heap corruption vì free() vùng nhớ invalid.
Do vậy, đối với class chưa con trỏ hay tham chiếu, cần định nghĩa toán tử gán riêng cho class. Xem ví
dụ 3 minh họa cho việc định nghĩa toán tử gán.
Ví dụ 3: Định nghĩa toán tử gán khi class có thành phần con trỏ
#include <iostream>
using namespace std;
class Test
{
private:
int x;
float y;
char* name;
public:
Test() {}
Test(int n, float m, const char s[])
{
x = n;
y = m;
name = _strdup(s);
}
~Test()
{
free(name);
name = NULL;
}
const Test& operator = (const Test& obj)
{
this->x = obj.x;
this->y = obj.y;
this->name = _strdup(obj.name);
return *this;
}
void show()
{
cout << "x = " << x << ", y = " << y << ", name = " << name;
}
};
int main()
{
Test obj1(10, 10.1, "vncoding");
Test obj2;
obj2 = obj1;
obj2.show();
return 0;
}
Giải thích:
Test obj1(10, 10.1, "vncoding");
Gọi hàm tạo để khởi tạo object ‘obj1’
Chú ý: trong hàm tạo có đối số Test(int n, float m, const char s[]), hàm _strdup(“vncoding”) cấp phát
bộ nhớ động và có giá trị là chuỗi “vncoding”
Khi bạn debug sẽ thấy giá trị object ‘obj1’ như sau:
{x=10 y=10.1000004 name=0x014ae788 "vncoding" }
(0x014ae788 là địa chỉ vùng nhớ động được cấp phát bởi hàm _strdup())
Test obj2;
Tạo object ‘obj2’, hàm tạo Test() không đối số được gọi.
obj2 = obj1;
Toán tử gán định nghĩa được gọi, gán giá trị object ‘obj1’ cho ‘obj2’, riêng con trỏ name, cấp phát
vùng nhớ mới với giá trị “vncoding”. Khi bạn debug sẽ thấy giá trị object ‘obj2’ như sau:
{x=10 y=10.1000004 name=0x014ae000 "vncoding" }
Chúng ta thấy, vùng nhớ obj1.name và obj2.name là 2 vùng nhớ riêng biệt, giống với Deep Copy
trong hàm tạo copy.
obj2.show();
Show ra màn hình console: x = 10, y = 10.1, name = vncoding
return 0;
Gọi câu lệnh return, hàm hủy gọi để hủy lần lượt object ‘obj1’, ‘obj2’. Cụ thể là hủy vùng nhớ trỏ bởi
obj1.name và obj2.name. Và tất nhiên việc gọi hàm hủy không xảy ra lỗi như ví dụ 2.
44. Hàm tạo class có thể gọi 1 hàm tạo khác trong cùng 1 class để khởi tạo object không?
45. Hàm tạo copy có thể nhận object của cùng class làm tham số không? Nếu không tại sao?
46. Hàm tạo và hàm hủy có thể khai báo kiểu const không?
47. Có thể tạo hàm tạo copy kiểu private không?
48. Hàm hủy là gì? Hàm hủy được gọi khi nào?
49. Hàm hủy ảo là gì? Khi nào sử dụng hàm hủy ảo?
50. Có thể overload hàm hủy không?
51. Khi nào nên viết hàm hủy?
52. Con trỏ this là gì? Khi nào nên sử dụng con trỏ this?
53. Tính đa hình là gì? Sự khác nhau giữa các kiểu đa hình trong C++?
54. Namesapce là gì? Sử dụng namespace như thế nào?
55. Static member trong C++?
56. Ưu nhược điểm hàm inline?
57. Function overloading là gì?
58. Sự khác nhau giữa function overloading và operator overloading?
59. Function overriding là gì?
60. Sự khác nhau giữa function overloading và function overriding?
61. Khi nào nên sử dụng con trỏ, khi nào nên sử dụng tham chiếu?
62. Virtual function là gì?
63. Pure virtual function là gì?
64. Sự khác nhau giữa virtual function và pure virtual function?
65. Làm thế nào access private member của class?
66. Hàm virtual có thể là kiểu private được không?
67. Hàm friend, class friend?
68. Abstract class?
69. Diamond problem?
70. Template class là gì? Khi nào sử dụng template class?
71. Khi nào nên sử dụng static_cast, dynamic_cast, const_cast và reinterpret_cast?
72. Smart pointer là gì?
73. Lamda function là gì?
74. STL là gì? Khi nào sử dụng STL?
75. How to convert std::string to char*?
76. Shared pointer?
77. File handling C++?
78. Exception handling?
79.
Part 3: