You are on page 1of 89

Part 1: Lập trình C

Part 2: Lập trình OOP C++

Part 3: Advance (File handle, Multithreading, IPC, Networking, GUI (Win32API, MFC), DLL, LIB)
Part 1: Lập trình C

1. Phân biệt Syntax-error, Runtime-error and Logical-error?

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:

error C2143: syntax error : missing ';' before '}

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

Unhandled exception at 0x012313a0 in example.exe: 0xC0000094: Integer division by zero.

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.

Ví dụ: viết macro tính bình phương của 1 số

#include <stdio.h>

#define SQUARE(X) X*X


void main()
{
int a = 2, b = 3;
int c = SQUARE(a + b);
printf("c = %d", c);

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:

#define SQUARE(X) ((X)*(X))

c = SQUARE(2+3) = ((2 + 3)*(2 + 3)) = (5*5) = 25

2. Sự khác nhau giữa hàm memcpy và strcpy?

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ú ý:

 Cả 2 hàm không xử lí được trường hợp overlap buffer.


 Mordern C++ (ver 11, 14, 17) không support hàm memcpy() và strcpy() vì các hàm này có
thể gây ra lỗi buffer overrun. C++ đưa ra các API cải tiến mới là strcpy_s(), memcpy_s()

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 dump(char* s, int sz);

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);

void dump(char* s, int sz)


{
char* p = s;
int i;
for (i = 0; i < sz; i++)
{
printf("%.2x ", *(p + i)); // hexa ASCII
}
for (i = 0; i < sz; i++)
{
printf("%c", (p + i) ? *(p + i) : ' '); // characters
}
printf("\n");
}

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

3. Truyền tham trị và tham biến cho hàm?

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:

Truyền tham trị (dùng trong cả C và C++)

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>

int sum(int a, int b)


{
a++;
b++;
return (a + b);
}
void main()
{
int x = 1, y = 2;
int s = 0;
s = sum(x, y);
printf("\n s = %d", s);
printf("\n x = %d", x);
printf("\n y = %d", y);
}

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.

Truyền tham biến


Phương pháp truyền tham biến là cách truyền địa chỉ của đối số cho các tham số tương ứng của
hàm được gọi. Với cách truyền tham biến, giá trị của đối số truyền vào có thể bị thay đổi bởi việc gọi
hàm.
Truyền tham biến chia ra thành 2 loại : truyền con trỏ (dùng trong C và C++) , truyền tham chiếu (chỉ
dùng trong C++)

Truyền con trỏ (dùng trong C và C++)


#include <stdio.h>

void swap(int* a, int* b)


{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
void main()
{
int x = 2, y = 3;
swap(&x, &y);
printf("\nx = %d", x);
printf("\ny = %d", y);
}

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.

Truyền tham chiếu (chỉ dùng trong C++)


#include <stdio.h>

void swap(int& a, int& b)


{
int temp;
temp = a;
a = b;
b = temp;
}
void main()
{
int x = 2, y = 3;
swap(x, y);
printf("\nx = %d", x);
printf("\ny = %d", y);
}

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.

4. Sự khác nhau giữa cấp phát tĩnh và cấp phát động?


4.1 Cấp phát bộ nhớ tĩnh
Khi khai báo các biến (int, float,..), mảng, chuỗi kí tự,(int a[20], char s[30]).. Trình biên dịch cấp phát
vùng nhớ (static memory) cho các biến này. Vì vùng nhớ giành cho các (biến, mảng, chuỗi,..) là static
memory nên việc gán con trỏ tới vùng nhớ tĩnh được gọi là cấp phát bộ nhớ tĩnh. Vùng nhớ được
cấp phát trong lúc biên dịch.
#include <stdio.h>

void main()
{
char s[] = "vncoding forum";
char* p = &s[0];
printf("%s", p);
}

Output:

vncoding forum

4.2 Cấp phát bộ nhớ động


Sử dụng hàm malloc(), calloc() để cấp phát bộ nhớ động. Giá trị trả về của malloc(), calloc() là con trỏ
trỏ tới vùng nhớ được cấp phát. Vùng nhớ được cấp phát trong lúc chạy chương trình (runtime)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main()
{
char* s = (char*)malloc(30);
if (s)
{
strcpy(s, "vncoding forum");
printf("%s", s);
free(s);
}
}

Output:

vncoding forum

5. Hằng con trỏ và con trỏ hằng?


Trong bài viết này, chúng ta sẽ đến với 2 khái niệm khó nhớ và hay gây nhầm lẫn là: hằng con trỏ và
con trỏ hằng.
5.1 Hằng con trỏ – Constant pointer
Khai báo:

<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:

Error C2734 'p': 'const' object must be initialized if not 'extern'

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:

Error C3892 'p': you cannot assign to a variable that is const

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_

float sum(float, float);

#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();
}

float sum(float a, float b)


{
return (a + b);
}

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()

7. Sự khác nhau giữa malloc() và calloc()?


Điểm giống nhau giữa malloc() và calloc()
Cả 2 hàm malloc() và calloc() đều được sử dụng để cấp phát vùng nhớ động cho chương trình. Nếu
cấp phát vùng nhớ thành công, hàm trả về con trỏ trỏ tới vùng nhớ được cấp phát. Hàm trả về NULL
nếu không đủ vùng nhớ.

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ể.

Ví dụ: Sử dụng malloc và calloc để cấp phát vùng nhớ


#include <stdio.h>
#include <stdlib.h>

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:

malloc() allocates memory successfully


calloc() allocates memory successfully

8. Sư khác nhau giữa ++x và x++ trong C/C++?


Cả 2 toán tử ++x và x++ dùng để tăng giá trị của x lên 1 đơn vị. Tuy nhiên, chúng có 1 điểm khác
nhau cơ bản:
Toán tử tăng trước ++x: tăng giá trị x trước khi thực hiện các phép toán khác trong cùng 1 câu lệnh
Ví dụ 1:
#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 = 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

9. Sự khác nhau giữa hàm strcpy() và strdup()?


Hàm strcpy(char* s1, char* s2) copy nội dung vùng nhớ s2 và vùng nhớ s1. Vùng nhớ s1 phải được
cấp phát tĩnh hoặc cấp phát động trước đó (malloc( ), new( )). Kích thước vùng nhớ s1 phải đủ để
chứa chuỗi s2. Nếu không đủ vùng nhớ, gây ra buffer overrun.
#include <stdio.h>
#include <string.h>

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

10. Alignment và padding struct là gì?


Alignment data và padding data
Data alignment là cách mà trình biên dịch sắp xếp dữ liệu trong memory. Việc sắp xếp dữ liệu liên
quan tới 2 khái niệm: data alignment và data padding.
Đối với những PC hiện nay, việc đọc/ghi dữ liệu trong bộ nhớ được với kích thước của WORD (OS 32
bit: kích thước WORD: 4 byte) hoặc lớn hơn. Data alignment là cách sắp xếp dữ liệu sao cho kích
thước bằng bội số của kích thước 1 word = 4k (byte). (k = 0, 1, 2,…). Để alignment data, đôi khi
chúng ta cần phải add thêm các byte giả (dummy) vào vị trí thích hợp để đảm bảo data alignment,
được gọi là padding data.

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.

Data Alignment trên Visual Studio C++


Khi chúng ta định nghĩa struct trong project Visual Studio C++, trình biên dịch (complier) sẽ cố gắng
allocate dữ liệu theo alignment data.

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.

a dummy1[0 dummy1[1] dummy1[2]


]
b b b b
c c d dummy2[0]

4 bytes

Struct packing và Alignment


Trình biên dịch tự động alignment data làm tăng performance của hệ thống. Tuy nhiên, kích thước
struct lớn hơn dự định ban đầu. (do padding thêm một số byte) → làm tăng kích thước chương
trình.

Để 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.

Mối liên hệ giữa alignment và packing data

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

packsize có các giá trị 1, 2, 4, 8, 16 bytes. (Defaut: 8 bytes)

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]

Cách 2: Dùng tiền xử lí #pragma pack([n]) (n = 1, 2, 4, 8, 16)

Ví dụ 3:
#include <stdio.h>

# pragma pack (1)


struct A
{
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
char d; // 1 byte
};
# pragma pack ()

void main()
{
printf("size = %d", sizeof(A));
}

Output:
size = 8

11. Sự khác nhau giữa mảng và chuỗi?

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.

Ví dụ: Khai báo mảng kí tự và chuỗi kí tự.


#include <stdio.h>
#include <string.h>

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

12. Sự khác nhau giữa struct và union?


Struct và Union là 2 cấu trúc dữ liệu do lập trình viên định nghĩa bao gồm các biến với kiểu dữ liệu
khác nhau.

Sự khác nhau giữa struct và union


Việc định nghĩa, khai báo biến, truy cập đến các thành phần của struct và union là giống nhau. Tuy
nhiên, giữa struct và union có một vài điểm khác nhau:
Struct Union
Size của struct ít nhất bằng tổng size của các Size của union bằng size của thành phần có size
thành phần của struct. Mình sử dụng từ “ít lớn nhất trong union.
nhất” là vì size struct còn phụ thuộc vào sizeof(A) = 10 (kích thước của thành phần lớn
alignment struct. nhất trong union)
sizeof(A) = 4 + 10 + 4 = 18 (trong trường hợp
struct alignment member = 1 byte)
struct A union A
{ {
int x; int x;
char s[10]; char s[10];
float f; float f;
}; };

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));

printf("\ndate = %d", dat.d);


printf("\nmonth = %d", dat.m);
printf("\nyear = %d", dat.y);
}

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;

printf("\nSize of union: %d", sizeof(date));


dat.d = 24;
dat.m = 9;
dat.y = 2014;

printf("\ndate = %d", dat.d);


printf("\nmonth = %d", dat.m);
printf("\nyear = %d", dat.y);
}

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.

13. Ưu nhược điểm khi sử dụng mảng?


Ưu điểm: Mảng có 1 số ưu điểm như sau.

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ó.

14. Hàm sprintf() được sử dụng trong trường hợp nào?


Hàm sprintf() được sử dụng để định dạng 1 chuỗi kí tự và lưu vào chuỗi kí tự
int sprintf(char *buffer, const char *format [,argument] ...);
buff: vùng nhớ lưu trữ chuỗi output.

argument: biến tùy chọn

%d hoặc %i : in ra số nguyên

%u : in ra số nguyên không dấu

%o : in ra số hệ 8 (octal)

%x hoặc %X : in ra số hê 16 (hexa)

%f : in ra số dấu phẩy động

%c : in ra kí tự

%s : in ra string

%e hoặc %E: in dạng M*e^x

Một số kí tự đặc biệt:


‘/n’ : kí tự xuống dòng

‘/t’ : kí tự cách ra 1 tab (4 space)

‘/r’ : kí tự trở về đầu dòng in

‘/b’: kí tự lùi con trỏ màn hình về sau 1 kí tự

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;

// Format and print various data:


j = sprintf(buffer, " String: %s\n", s);
j += sprintf(buffer + j, " Character: %c\n", c);
j += sprintf(buffer + j, " Integer: %d\n", i);
j += sprintf(buffer + j, " Real: %f\n", fp);
// Note: sprintf is deprecated; consider using sprintf_s instead

printf("Output:\n%s\ncharacter count = %d\n", buffer, j);


}

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;

if ((buff = (char*)malloc(100 * sizeof(char))) == NULL)


{
printf("\nInsufficient memory for malloc()");
return;
}

if ((buff = (char*)realloc(buff, (50 + 100) * sizeof(char))) == NULL)


{
printf("\nInsufficient memory for realloc()");
}
}

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;

if ((buff = (char*)malloc(100 * sizeof(char))) == NULL)


{
printf("\nInsufficient memory for malloc()");
return;
}

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().

 Lưu ý khi sử dụng hàm strdup(const char* s)


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.
Ví dụ:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

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.

16. Phân biệt biến local, global, extern, static và const?


16.1 Biến local
 Là biến được khai báo trong hàm
 Biến chỉ tồn tại trong hàm mà biến được khai báo
Ví dụ:
#include <stdio.h>

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"

int X; // global variable

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.

16.4 Biến const


 Trong ngôn ngữ C, tiền xử lý #define được sử dụng để định nghĩa các macro, hằng số
 Trong ngôn ngữ C++, xuất hiện một số vấn đề: khi sử dụng #define, tiền xử lí sẽ thay thế
macro bằng giá trị hoặc logic đã định nghĩa. Vì biến #define chỉ tồn tại bên trong file mà nó
được định nghĩa, có thể xảy ra trường hợp định nghĩa tên biến giống nhưng khác về giá trị.
 Định nghĩa biến hằng trong C++, chúng ta nên sử dụng từ khóa const đi kèm. Khi sử dụng từ
khóa const, phải khởi tạo giá trị ban đầu cho biến.
17. Sự khác nhau giữa khai báo và định nghĩa biến, hàm, class?
Khai báo là việc cung cấp tên và kiểu (biến, object, hàm). Trình biên dịch dùng các thông tin khai báo
này để chấp nhận các tham chiếu tới tên (biến, object, hàm) đó.
Ví dụ: khai bao biến, hàm, object
extern int bar;
extern int g(int, int);
double f(int, double); // extern can be omitted for function declarations
class foo; // no extern allowed for type declarations

Đị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.

18. Mối liên hệ giữa con trỏ và mảng 1 chiều?


Xét ví dụ sau:
#include <stdio.h>

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

19. Mối liên hệ giữa con trỏ và mảng 2 chiều?


Xét ví dụ sau:
#include <stdio.h>
#define N 4
#define M 3

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:

<return_type> (*<pointer_name>) (function_arguments);

Ví dụ 1:
int (*fpFunc)(int x, int y); // declare a function pointer

20.2 Ứng dụng con trỏ hàm?


i. Sử dụng mảng con trỏ hàm để thay thế cho switch case {}
Ví dụ 1: Sử dụng switch case thực hiện phép cộng, trừ, nhân, chia
#include <stdio.h>
#include <assert.h>

float Plus(float a, float b);


float Minus(float a, float b);
float Multiply(float a, float b);
float Divide(float a, float b);
void Switch(float a, float b, char opCode);
void main()
{
Switch(4, 5.6, '+');
Switch(4, 6, '-');
Switch(4, 0, '/');
}

void Switch(float a, float b, char opCode)


{
float result;

// 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);
}

float Plus(float a, float b)


{
return a + b;
}

float Minus(float a, float b)


{
return a - b;
}

float Multiply(float a, float b)


{
return a * b;
}

float Divide(float a, float b)


{
assert(b != 0);
return a / b;
}

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

Ví dụ 2: sử dụng con trỏ hàm thay thế cho switch case


#include <stdio.h>
#include <assert.h>

float Plus(float a, float b);


float Minus(float a, float b);
float Multiply(float a, float b);
float Divide(float a, float b);
void Switch(float a, float b, float (*p2Func)(float, float));

void main()
{
Switch(4, 5.6, &Plus);
Switch(4, 6, &Minus);
Switch(4, 0, &Divide);
}

void Switch(float a, float b, float (*p2Func)(float, float))


{
float result = p2Func(a, b);
printf("result = %f\n", result);
}

float Plus(float a, float b)


{
return a + b;
}

float Minus(float a, float b)


{
return a - b;
}

float Multiply(float a, float b)


{
return a * b;
}

float Divide(float a, float b)


{
assert(b != 0);
return a / b;
}

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 compare(const void* a, const void* b)


{
return (*(int*)a - *(int*)b);
}

int main()
{
int arr[] = { 10, 5, 15, 12, 90, 80 };
int n = sizeof(arr) / sizeof(arr[0]), i;

qsort(arr, n, sizeof(int), compare);

for (i = 0; i < n; i++)


printf("%d ", arr[i]);
return 0;
}

Output:
5 10 12 15 80 90

21. Mảng con trỏ và ứng dụng?


21.1 Mảng con trỏ là gì?
Mảng con trỏ là mảng chứa các con trỏ. Mỗi phần tử mảng chứa địa chỉ của mỗi con trỏ
Khai báo:

<kiểu dữ liệu> *<tên mảng>[kích thước mảng]

Ví dụ: mảng con trỏ chứa 10 con trỏ kiểu int

int *ptr[10];

21.2 Ứng dụng mảng con trỏ?


Ví dụ 1: sắp xếp chuỗi theo thứ tự anpha, beta, …
#include <stdio.h>
#include <string.h>

#define LENGTH 30

void strsort(char str[][LENGTH], int num);


void strshow(char str[][LENGTH], int num);

void main(void)
{
char strarr[][LENGTH] = { "C/C++", "Java", "C#", "Python", "PHP", "Java Script" };
strsort(strarr, 6);
strshow(strarr, 6);
}

void strsort(char str[][LENGTH], int num)


{
int i, j;
char temp[LENGTH];
for (i = 0; i < num; i++)
{
for (j = num - 1; j > i; j--)
{
if (strcmp(str[j], str[j - 1]) < 0)
{
strcpy(temp, str[j]);
strcpy(str[j], str[j - 1]);
strcpy(str[j - 1], temp);
}

}
}
}

void strshow(char str[][LENGTH], int num)


{
int i;
for (i = 0; i < num; i++)
{
printf("\nstr[%d] = %s", i + 1, str[i]);
}
}

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

void strsort(char** str, int num);


void strshow(char** str, int num);
void main(void)
{
char strarr[][LENGTH] = { "C/C++", "Java", "C#", "Python", "PHP", "Java Script" };
char* pstr[SIZE];
int i;
for (i = 0; i < 6; i++)
{
pstr[i] = strarr[i];
}

strsort(pstr, 6);
strshow(pstr, 6);
}

void strsort(char** str, int num)


{
int i, j;
char* temp;
for (i = 0; i < num; i++)
{
for (j = num - 1; j > i; j--)
{
if (strcmp(str[j], str[j - 1]) < 0)
{
temp = str[j];
str[j] = str[j - 1];
str[j - 1] = temp;
}
}
}
}

void strshow(char** str, int num)


{
int i;
for (i = 0; i < num; i++)
{
printf("\nstr[%d] = %s", i + 1, str[i]);
}
}

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

22. Sự khác nhau giữa hàm memcpy() và memmove()?


Cả 2 hàm memcpy() và memmove() được sử dụng để copy N byte dữ liệu từ vùng nhớ này sang
vùng nhớ khác. Tuy nhiên, trong trường hợp vùng nhớ nguồn overlap với vùng nhớ đích:
 Hàm memmove đảm bảo việc copy dữ liệu và output là chính xác .
 Hàm memcpy KHÔNG đảm bảo việc copy dữ liệu và output là chính xác.
Ví dụ sau sẽ giúp các bạn hiểu thêm về sự khác nhau của 2 hàm trong trường hợp vùng nhớ bị
overlap
#include <stdio.h>
#include <string.h>

char str1[] = "vncoding.net";


char str2[] = "vncoding.net";

void main(void)
{
printf("The string: %s\n", str1);
memcpy(str1 + 2, str1, 6);
printf("New string: %s\n", str1);

printf("The string: %s\n", str2);


memmove(str2 + 2, str2, 6);
printf("New string: %s\n", str2);
}

Output:

The string: vncoding.net


New string: vnvncoco.net
The string: vncoding.net
New string: vnvncodi.net

Hàm memmove() đảm bảo kết quả đúng như mong muốn (“vncodi” ghi đè lên “coding”).

23. Con trỏ void và ứng dụng?


 Con trỏ void là con trỏ có thể lưu địa chỉ của mọi kiểu biến dữ liệu.
int a;
float b;
char c;
void* p;
p = &a; // address of a
p = &b; // address of b
p = &c; // address of c

 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 *'

Để fix lỗi trên, chúng ta có ví dụ sau


Ví dụ 2: gán con trỏ void cho con trỏ int
#include<stdio.h>

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 showTime(void* p, char type);

void main()
{
DATE d = { 16, 9, 1989 };
TIME t = { 14, 30, 0 };
showTime((void*)&d, 'd');
showTime((void*)&t, 't');
}

void showTime(void* p, char type)


{
DATE* pDate;
TIME* pTime;
switch (type)
{
case 'd':
pDate = (DATE*)p;
printf("date = %.02d/%.02d/%.04d\n", pDate->date, pDate->month, pDate->year);
break;
case 't':
pTime = (TIME*)p;
printf("time = %.02d:%.02d:%.02d\n", pTime->hour, pTime->minute, pTime-
>second);
break;
default:
printf("Default case");
}
}

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

24. Double pointer (pointer to pointer) và ứng dụng?


24.1 Double pointer là gì?
Chúng ta đã biết con trỏ lưu trữ địa chỉ của biến. Khi con trỏ lưu trữ địa chỉ của 1 con trỏ thì được
gọi là double pointer hoặc pointer to pointer.
Khai báo:
<kiểu dữ liệu>** <tên con trỏ>;
Ví dụ 1: khai báo double pointer
int** pInt;
char** pStr;
float** pFt;
Để hiểu double pointer, chúng ta cùng xem ví dụ 2 và hình minh họa dưới đây
Ví dụ 2: khai báo double pointer và phép gán.
int num = 123;
int* pInt1 = &num;
int** pInt2 = &pInt1;

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

pInt2 pInt1 num

0x5094F 0x1235F 123

0x6092F 0x5094F 0x1235F

24.2 Ứng dụng double pointer?


Một trong những ứng dụng phổ biến nhất double pointer là thay đổi giá trị của con trỏ được truyền
vào như 1 đối số của hàm. Xét ví dụ sau
Ví dụ 3: Viết hàm cấp phát bộ nhớ động sử dụng tham số double pointer
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>

bool memalloc(int**, int);


void memfree(int**);

void main()
{
int* pInt;
bool bRes = memalloc(&pInt, 100);
if (bRes)
{
printf("\nMemmory allocation is successful");
}
memfree(&pInt);
}

bool memalloc(int** p, int sz)


{
*p = (int*)malloc(sz * sizeof(int));
if (*p == NULL)
{
*p = NULL;
return false;
}
memset(*p, 0x00, sz * sizeof(int));
return true;
}

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:

Memmory allocation is successful

25. Dangling pointer?


Dangling pointer là con trỏ trỏ vào vùng nhớ invalid hoặc trỏ vào vùng nhớ ban đầu là valid, nhưng
sau đó không còn valid nữa vì bị free.
Ví dụ 1: con trỏ trỏ vào vùng nhớ bị free
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>

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ỏ.

26. Layout of memory in C program?


Về cơ bản, memory layout của chương trình C bao gồm 5 segment: STACK, HEAP, BSS (Block Started
by Symbol), DS (Data Segment) và Text Segment. Mỗi segment có permission read/write/execute
riêng. Nếu chương trình truy cập vùng nhớ trái phép, gây lỗi segment fault.
Segment fault là lỗi cơ bản khiến cho chương trình bị crash, OS tạo ra file core dump. Developer dựa
file core dump để tìm root cause.
Dưới đây là memory layout của chương trình C

High address -> Environment  


Hàm và biến được khai báo trong hàm
STACK
  được lưu trong stack
 

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.

BSS (Uninitialized Data segment)


 Chứa các biến không được khởi tạo (global và static)
 Tất cả các biến trong segment này được khởi tạo là 0, con trỏ được khởi tạo là NULL.
 Loader chương trình cấp phát vùng nhớ BSS khi load chương trình.
Ví dụ 3: khai báo biến global và static không được khởi tạo.
int gCount;

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.

DS (Initialized Data Segment)


 Chứa các biến được khởi tạo khi khai báo (global và static)
 Kích thước của DS được xác định bởi kích thước của các giá trị trong source code và không thay
đổi được tại thời điểm run-time.
 DS có permission read/write, có thể thay đổi giá trị của biến trong DS tại thời điểm run-time.
Ví dụ 4: khai báo biến global và static được khởi tạo.
int gCount = 0;

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.

27. Ưu nhược điểm của macro và hàm?


Để so sánh được ưu nhược điểm của macro và hàm, chúng ta nhắc lại khái niệm macro và hàm.
27.1 Macro
 Marco là 1 tên bất kì (do lập trình viên đặt tên) trỏ tới 1 khối lệnh thực hiện một chức năng nào
đó.
 Trong quá trình tiền xử lí (pre-processor), các macro được sử dụng trong chương trình được
thay thế bởi các khối câu lệnh tương ứng.
 Định nghĩa macro bằng lệnh #define
Ví dụ 1: tạo macro tìm giá trị lớn nhất của 2 số
#include <stdio.h>

#define MAX(A, B) ((A) > (B) ? (A) : (B))

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>

int MAX(int, int);

void main(void)
{
int a = 5, b = 7;
printf("\nMAX(%d, %d) = %d", a, b, MAX(a, b));
}

int MAX(int a, int b)


{
return (a > b ? a : b);
}

Output:
MAX(5, 7) = 7

27.3 So sánh ưu nhược điểm giữa hàm và macro?

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>

#define SQUARE(X) (X*X)

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))

28. 11 lỗi hay gặp khi cấp phát bộ nhớ động


Con trỏ và cấp phát bộ nhớ động là điểm mạnh của ngôn ngữ C, giúp tối ưu tài nguyên memory. Tuy
nhiên, khi sử dụng cấp phát bộ nhớ động, phát sinh khá nhiều bug cho lập trình viên chưa có kinh
nghiệm. Dưới đây là 1 số lỗi hay gặp khi sử dụng cấp phát bộ nhớ động
 Không check giá trị trả về của hàm malloc, calloc, realloc
Hàm malloc() trả về con trỏ trỏ tới vùng nhớ được cấp phát. Trong trường hợp không đủ vùng nhớ,
hàm malloc() trả về NULL, việc truy cập vào con trỏ NULL gây ra lỗi segmentation fault.
Ví dụ 1: không 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));

for (i = 0; i < SZ; i++)


{
p[i] = i * i;
}

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;
}

for (i = 0; i < SZ; i++)


{
p[i] = i * i;
}

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;
}

for (i = 0; i < SZ; i++)


{
p[i] = p[i] * i;
}

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);
}

 Không giải phóng vùng nhớ được cấp phát


Việc không giải phóng vùng nhớ sẽ gây memory leak.
Ví dụ 6: cấp phát mà không giải phóng vùng nhớ
#include <stdio.h>
#include <stdlib.h>

void main(void)
{
int* p = (int*)malloc(10 * sizeof(int));

/* do something*/
/* not free() */
}

 Tính kích thước của mảng được cấp phát động


Một số developer sử dụng toán tử sizeof() tính kích thước mảng nhớ động, dẫn đến kết quả sai. Vì
toán tử sizeof() chỉ được sử dụng để tính kích thước mảng tĩnh, không sử dụng tính kích thước
mảng động. Nếu bạn sử dụng mảng động, sizeof(p) trả về kích thước con trỏ p.
Ví dụ 7: sử dụng toán tử sizeof() tính kích thước mảng độ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 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;

// Access length of array via pointer temp


int len = temp[-1];
for (size_t i = 0; i < len; i++)
{
printf("temp[%d] = %d\n", i, temp[i]);
}
// Free memory
free(p);
p = NULL;
}

int* arrAlloc(int sz)


{
int* p = (int*)malloc((sz + 1) * sizeof(int));
if (p == NULL)
{
printf("Error in memory allocation");
return NULL;
}
p[0] = sz;

// 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;

Cấp phát thêm 1 phần tử để lưu kích thước mảng


int* p = arrAlloc(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

 Truyền kích thước 0 vào hàm malloc()


Truyền kích thước 0 cho hàm malloc(), hàm malloc() cấp phát vùng nhớ 0 byte trong vùng nhớ
heap, và trả về con trỏ khác NULL. Điều này gây ra bug, khi truy cập vùng nhớ 0 byte. Xem ví dụ sau:
Ví dụ 10: cấp phát vùng nhớ động 0 byte
#include <stdio.h>
#include <stdlib.h>

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

 Không count số lần cấp phát bộ nhớ động


Sử dụng biến global gCnt1 và gCnt2 để count số lượng cấp phát bộ nhớ động và số lần giải phóng
bộ nhớ. Mỗi lần cấp phát thành công, tăng biến gCnt1 lên 1 đơn vị. Tương tự, mỗi lần giải phóng bộ
nhớ, tăng biến gCnt2 lên 1 đơn vị. Đến cuối chương trình nếu biến gCnt1 = gCnt2, chương trình ko
bị leak memory.
Ví dụ 11: implement hàm check memory leak

static unsigned int gCnt1 = 0;


static unsigned int gCnt2 = 0;

void* Memory_Allocate(size_t size)


{
void* p = NULL;
p = malloc(size);
if (NULL != p)
{
++gCnt1;
}
else
{
printf("Error in memory allocation");
}
return (p);
}
void Memory_Deallocate(void* p)
{
if (p != NULL)
{
free(p);
++gCnt2;
}
}
int Check_Memory_Leak(void)
{
int iRet = 0;
if (gCnt1 != gCnt2)
{
iRet = -1;
}
else
{
iRet = 0;
}
return iRet;
}

 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;
}

for (pos = 0; pos < n; pos++)


{
p[pos] = 10;
}

do {
printf("Enter the array index = ");
scanf("%d", &pos);
} while (pos >= n && pos < 0);

printf("p[%d] = %d", pos, p[pos]);


free(p);
p = NULL;

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:

Enter the array index = 12


Enter the array index = 11
Enter the array index = 10
Enter the array index = 9
p[9] = 10

 Modify con trỏ gốc


Sau khi cấp phát vùng nhớ, nếu bạn thay đổi giá trị biến con trỏ, tức là bạn thay đổi địa chỉ vùng
nhớ được cấp phát, không lưu địa chỉ vùng nhớ cấp phát ban đầu  không free vùng nhớ 
memory leak. Nếu cố free(), gây ra lỗi access violation.
Ví dụ 13: lỗi access violation do free vùng nhớ trái phép
#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;
}

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);

 Định nghĩa struct bao gồm con trỏ hàm


typedef struct S_sArithMaticOperation
{
float iResult;
pfnMessage DisplayMessage;
pfnCalculator ArithmaticOperation;
} sArithMaticOperation;

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>

//function pointer to point display message


typedef void (*pfnMessage)(const char*, float fResult);
//function pointer to point arithmetic function
typedef float (*pfnCalculator)(float, float);
//structure of function pointer

typedef struct S_sArithMaticOperation


{
float iResult;
pfnMessage DisplayMessage;
pfnCalculator ArithmaticOperation;
} sArithMaticOperation;

// 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)

float Subtraction(float a, float b)

float Multiplication(float a, float b)

float Division(float a, float b)

Các hàm tính toán cộng trừ nhân chia 2 số thực a và b


typedef struct S_sArithMaticOperation
{
float iResult;
pfnMessage DisplayMessage;
pfnCalculator ArithmaticOperation;
} sArithMaticOperation;
Struct sArithMaticOperation gồm biến iResult lưu kết quả trả về từ phép toán, con trỏ hàm
ArithmaticOperation trỏ tới hàm Addition, Subtraction, Multiplication, Division theo sự lựa chọn của
user. Con trỏ hàm DisplayMessage trỏ tới hàm Message để hiển thị kết quả tính toán.
psArithmaticOperation = (sArithMaticOperation*)malloc(sizeof(sArithMaticOperation));
Khai báo và cấp phát vùng nhớ cho biến con trỏ struct psArithmaticOperation.

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:

Part 2: Lập trình OOP C++


30. Sự khác nhau giữa C và C++
C C++
C là subset của C++ C++ được phát triển từ ngôn ngữ C
C là ngôn ngữ hướng thủ tục hay cấu trúc C++ là ngôn ngữ hướng đối tượng OOP có các
thuộc tính: kế thừa, đa hình, đóng gói,…
C không có biến tham chiếu C++ có biến tham chiếu
C chỉ support con trỏ C++ support con trỏ và tham chiếu
C sử dụng malloc, calloc, realloc, free để cấp C++ sử dụng toán tử new và delete để cấp phát
phát và giải phóng bộ nhớ động và giải phóng bộ nhớ động
C không support function và operator C++ support function và operator overloading
overloading (chồng hàm và chồng toán tử) (chồng hàm và chồng toán tử)
Tuy nhiên: có thể sử dụng con trỏ hàm để thực
hiện function overloading trong C
C không support namespace C++ support namespace để tránh xung đột
C không support template C++ support template
31. Liệt kê các thuộc tính của OPP C++
 Tính kế thừa
Tính kế thừa cho phép định nghĩa class có thể kế thừa tất cả các phương thức và thuộc tính của
một class khác. Class kế thừa từ class khác được gọi là class dẫn xuất, class được kế thừa được
gọi là class cơ sở hoặc class cha.
Lợi ích của tính kết thừa
 Re-use code, dễ maintain chương trình và mở rộng (scale) ứng dụng
 Cho phép thêm các thuộc tính vào class mà không cần modify thuộc tính khác trong
class
 Tính chất bắc cầu, nếu class B kế thừa từ class A, thì sub-class B tự động kế thừa từ class
A
 Tính đa hình
Tính đa hình cho phép function overriding và overloading. Đa hình có đa hình tĩnh và đa hình
động
 Tính đóng gói
Tính đóng gói ẩn thuộc tính (dữ liệu) của object, user không thể biết object chứa các thuộc tính
gì, user không thể truy xuất trực tiếp vào các thuộc tính này, phải thông qua phương thức class.
Tính đóng gói đảm bảo sercurity cho application.
 Tính trừu tượng
Bổ sung sau

32. Sự khác nhau giữa struct và class trong C++?


Trong C++, struct giống class ngoại trừ 1 số điểm. Điều quan trọng nhất là sercurity. Struct không
bảo mật và không ẩn thuộc tính và implementation, class ẩn thuộc tính và implementation. Dưới
đây là mô tả chi tiết về sự khác nhau giữa struct và class
 Nếu không chỉ định từ khóa quyền truy xuất (private, public, protected), các member của class là
private, còn member của struct là public
Ví dụ 1: không thể truy xuất vào member private của class
#include <stdio.h>

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;
};

class Derived : Base { };

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'

Ví dụ 4: struct kế thừa từ class


#include <stdio.h>

class Base {
public:
int x;
};

struct Derived : Base { };

int main()
{
Derived d;
d.x = 20;

return 0;
}

Giải thích:
struct Derived : Base { };

Câu lệnh tương đương với câu lệnh sau:


struct Derived : public Base { };

Việc truy xuất vào member public ‘x’ là hoàn toàn hợp lệ

33. Sự khác nhau giữa malloc và toán tử new?


Hàm malloc() và toán tử new được sử dụng để cấp phát bộ nhớ động trong vùng nhớ heap. Tuy
nhiên, có một vài điểm khác biệt như sau:

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

Ví dụ: cấp phát bộ nhớ động bằng malloc và new


#include <iostream>
using namespace std;

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;

Test* b = new Test;


cout << "Object of class Test is "
<< "created using new operator!"
<< 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:

Object of class Test is created using malloc()!


Constructor is called!
Object of class Test is created using new operator!

34. Hàm tạo là gì?


 Hàm tạo được sử dụng để khởi tạo các giá trị thành phần của đối tượng.
 Hàm tạo được khai báo giống như một phương thức với tên phương thức trùng với tên lớp và
không có giá trị trả về. Có thể có nhiều hàm tạo trong cùng 1 lớp.
 Khi 1 lớp có nhiều hàm tạo, việc tạo các đối tượng phải kèm theo các tham số phù hợp với một
trong các hàm tạo đã khai báo.
 Khi người dùng không khai báo bất kì một hàm tạo nào cho lớp thì trình biên dịch sẽ tự động
sinh ra cho lớp một hàm tạo mặc định không có tham số.
 kKông được gọi hàm tạo trực tiếp, mà hàm tạo được gọi tự động khi khai báo đối tượng.
Có 3 loại hàm tạo: hàm tạo mặc định, hàm tạo có tham số, hàm tạo copy
 Hàm tạo mặc định là hàm tạo không nhận bất kì đối số nào
Ví dụ 1: hàm tạo mặc định
#include <iostream>
using namespace std;

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

 Hàm tạo copy


Là hàm tạo có tham số là một đối tượng của chính lớp đó, để tạo ra bản sao của chính đối tượng
tham số. Trong 1 lớp luôn luôn có 1 hàm tạo sao chép. Nếu trong trường hợp người sử dụng không
khai báo hàm thiết lập sao chép cho lớp thì trình biên dịch sẽ tự động sinh một hàm thiết lập sao
chép mặc định cho lớp.
35. Hàm tạo copy là gì?
Là hàm tạo có tham số là một đối tượng của chính lớp đó. Trong 1 lớp luôn luôn có 1 hàm tạo sao
chép. Nếu trong trường hợp người sử dụng không khai báo hàm sao chép cho lớp thì trình biên dịch
sẽ tự động sinh một hàm thiết lập sao chép mặc định cho lớp.
Ví dụ 1: khai báo hàm tạo copy
#include<iostream>
using namespace std;

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 getX() { return x; }


int getY() { return 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:

Parameter constructor is called


Copy constructor is called
p1.x = 10, p1.y = 15
p2.x = 10, p2.y = 15

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;
}

int main(int argcs, char* pArgs[])


{
Student obj("Linda");
Tutor tutor(obj);
cout << "Calling fn()" << endl;
fn(tutor);
cout << "Back in main()" << 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

36. Khi nào hàm tạo copy được gọi?


Trong C++, hàm tạo copy được gọi trong những trường hợp sau:
 Khi object được khởi tạo bằng object khác trong cùng 1 class.
Ví dụ 1:
#include<iostream>
using namespace std;

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 getX() { return x; }


int getY() { return 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:

Parameter constructor is called


Copy constructor is called
p1.x = 10, p1.y = 15
p2.x = 10, p2.y = 15

 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;
}

int main(int nNumberofArgs, char* pszArgs[])


{
Student obj;
obj = fn();
cout << "Back in main()" << endl;

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;
}

int main(int nNumberofArgs, char* pszArgs[])


{

Student& obj = fn();


cout << "Back in main()" << endl;

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()

37. Khi nào nên định nghĩa hàm tạo copy?


Trình biên dịch C++ cung cấp hàm tạo mặc định. Khi chúng ta không define hàm tạo copy và thực
hiện khởi tạo object ‘A’ bằng 1 object ‘B’ đã khởi tạo, hàm tạo copy mặc định được gọi và thực hiện
copy từng member của class từ object ‘B’ sang object ‘A’.
Khi class có thành phần con trỏ được khởi tạo lúc run-time, hàm tạo copy mặc định copy con trỏ của
object ‘B’ sang object ‘A’. 2 biến con trỏ trong object ‘A’ và ‘B’ cùng trỏ vào vùng nhớ chung. Khi
delete object ‘A’ hoặc ‘B’, object còn lại là dangling pointer, dẫn đến leak memory.
Vậy trong trường hợp class chứa con trỏ, khởi tạo run-time, cần khai báo hàm tạo copy.
Ví dụ 1: Sử dụng hàm tạo copy mặc định
#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;
};

int main(int nNumberofArgs, char* pszArgs[])


{

Student obj("Peter", 12345);


Student copy = obj;
cout << "Back in main()" << endl;

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: define hàm tạo 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;
};

int main(int nNumberofArgs, char* pszArgs[])


{

Student obj("Peter", 12345);


Student copy = obj;
cout << "Back in main()" << endl;

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;
};

int main(int nNumberofArgs, char* pszArgs[])


{

Student obj("Peter", 12345);


Student copy = obj;
cout << "Back in main()" << endl;

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;
};

int main(int nNumberofArgs, char* pszArgs[])


{

Student obj("Peter", 12345);


Student copy = obj;
cout << "Back in main()" << endl;

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

39. Có thể tạo hàm tạo copy private?


Chúng ta có thể tạo hàm tạo copy kiểu private, không cho phép copy object. Thường được ứng dụng
khi implement singleton pattern trong design pattern. Singleton pattern là pattern mà chỉ có 1 object
của class. Để tránh việc khai báo nhiều hơn 1 object, chúng ta khai báo hàm tạo, hàm tạo copy, toán
tử gán kiểu private. Object duy nhất được tạo thông qua hàm GetInstance() như sau:
Ví dụ 1: Implement Singleton pattern
#include <iostream>
using namespace std;

class CMySingleton
{
public:
static CMySingleton& GetInstance()
{
static CMySingleton singleton;
return singleton;
}

// Other non-static member functions


private:
CMySingleton() {} // Private constructor
~CMySingleton() {}
CMySingleton(const CMySingleton&); // Prevent copy-construction
CMySingleton& operator=(const CMySingleton&); // Prevent assignment
};

int main(int argc, char* argv[])


{
// create a single instance of the class
CMySingleton& object = CMySingleton::GetInstance();

// compile fail due to private constructor


CMySingleton object1;
// compile fail due to private copy constructor
CMySingleton object2(object);

// compile fail due to private assignment operator


object1 = object;

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.

43. So sánh hàm tạo copy và toán tử gán?

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:

You might also like