https://www.geeksforgeeks.org/compiling-a-c-program-behind-the-scenes/
https://www.scaler.com/topics/execution-of-c-program/
https://www.cs.odu.edu/~zeil/cs252/s22/Public/compilation/index.html
-
Computers understand only one language and that language consists of sets of instructions made of ones and zeros. This computer language is appropriately called machine language.
-
A particular computer's machine language program that allows a user to input two numbers, adds the two numbers together, and displays the total could include these machine code instructions:
-
Programming directly in machine language using only 0s and 1s is tedious and error-prone. To make programming easier, middle-level languages (like C) and high-level languages (like C++) have been developed, allowing programmers to write code in a more human-readable form.
-
Since computers can only understand machine language, such code must be translated before execution. This translation is done by compilers, interpreters, or assemblers.
-
C is designed as a compiled language, meaning its source code is converted into machine code for efficient execution. This requires a development toolchain, with the compiler and linker at its core.
=> What is the compilation process ?
- The compilation is the process of converting the source code of the C language into machine code.
- C is a mid-level language, it needs a compiler to convert it into an executable code so that the program can be run on our machine.
We first need a compiler and a code editor to compile and run a C Program. The below example is of an Ubuntu machine with GCC compiler.
-
[Step 1] Create =>
Source Code Creation- We first create a C program using an editor and save the file as filename.c In linux, we can use vi to create a file from the terminal using the command:
vi filename.c - In windows, we can use the Notepad to do the same. Then write a simple hello world program and save it.
- We first create a C program using an editor and save the file as filename.c In linux, we can use vi to create a file from the terminal using the command:
-
[Step 2] Build =>
Compilation process using GCC compiler- We use the following command in the terminal for compiling our filename.c source file:
gcc filename.c –o filename - We can pass many instructions to the GCC compiler to different tasks such as:
- -Wall enables all compiler’s warning messages.
- -o specifies the output file name (default is a.out if omitted).
- If there are no errors in our C program, the executable file of the C program will be generated.
- We use the following command in the terminal for compiling our filename.c source file:
-
[Step 3] Run =>
Execution Process -
After compilation executable is generated and we run the generated executable using the below command:
./filename// for linuxfilename// for windows
-
The program will be executed, and the output will be shown in the terminal.
C code: When you first write the code in the C language, that source code is sent to the Preprocessing section.
-
We first create a C program using an editor and save the file as filename.c.
- In linux, we can use vi to create a file from the terminal using the command:
vi filename.c - In windows, we can use the Notepad to do the same. Then write a simple hello world program and save it.
- In linux, we can use vi to create a file from the terminal using the command:
-
Example code (main.c)
#include <stdio.h>
int main() {
printf("hello");
return 0;
}gcc -E main.c -o main.i # Preprocessing → produces .i
gcc -S main.i -o main.s # Compilation → produces .s (assembly)
gcc -c main.s -o main.o # Assembly → produces .o (object code)
gcc main.o -o main # Linking → produces executableHere, you manually create each file (.i, .s, .o), which means you are handling the build process step by step.
This is very useful for learning or debugging.
Preprocessing: In this section our source code is attached to the preprocessor file. Different types of header files are used like the studio.h, math.h, etc. Our C source code is attached to these types of files and the final C Source generates. (some of the preprocessor directives are #include,#define). After generating the preprocessed C source code, the C source code is sent to the compiler section.
- This is the first phase through which source code is passed. This phase includes:
- Removal of Comments
- Expansion of Macros (#define, macros)
- Expansion of the included files (#include)
- Conditional compilation
- Command:
gcc -E main.c -o main.i- Input: main.c
- Output: main.i (preprocessor file, C code)
- main.i contains expanded code:
- #include<stdio.h> is missing instead we see lots of code. So header files have been expanded and included in our source file.
- Macros expanded.
Compiler: The preprocessed source code moves to the compiler, and an assembly-level code is generated by the compiler after the compilation of the whole C source code program. All the different files which have the C program must be saved with the .c extension. For the compiler to understand whether the program file is a C program file or not, it is necessary to have the '.c' extension. Suppose there is a program file named as first.c, The file first.c will be the source file which will consist of the C source code of the program. Now, when the file is compiled, the first thing the C compiler does is to search for any error. If there is no error, the C compiler will report for no error, after that the compiler will store the file as a .obj file of the same name, which is termed as the object file. So by this process, the compiler will create the first.obj. Although this .obj file will not be executable. After the compilation, the process is continued by the assembler section.
-
The compiler Converts preprocessed C code into assembly code. => This file is in assembly-level instructions which the assembler can understand.
This step is to compile main.i and produce an intermediate compiled output file main.s (in assembly-level instructions).
- What is Assembly code ? [assembly code represents a correspondence between program and machine code] + Is a human-readable representation of machine instructions. + Each instruction in assembly corresponds 1-to-1 with a CPU instruction (almost direct mapping). + Example: MOV AX, 5 ADD AX, 3 => This means: move value 5 into register AX, then add 3. + So assembly = "English-like" symbolic form of machine code. It’s easier for humans to read but not executable yet. => Assembly code: readable by humans, acts as a "bridge" between C code and machine code.
- Command:
gcc -S main.i -o main.s- Input: main.i
- Output: main.s (assembly code)
Assembler: This part usually generates the Object code, after taking the assembly-level code from the compiler. This object code is quite similar to the machine code or the set of binary digits. After this assembler part, The Linker continues the process, producing an executable.exe file at the end.
-
Translates assembly code into object code (.o or .obj file) in machine language.
In this phase the main.s is taken as input and turned into main.o by the assembler. This file contains machine-level instructions.
At this phase, only existing code is converted into machine language, and the function calls like printf() are not resolved.
- What is Object code ? [object code represents pure machine code (ie. binary)] + Is pure machine code (binary format: 0s and 1s). + Directly understandable by the CPU. + Stored in .o or .obj files. + Example (hex view of object file): B8 05 00 ; machine code for MOV AX, 5 83 C0 03 ; machine code for ADD AX, 3 + Not human-readable. => Object code: unreadable binary, directly executable by the processor (after linking). -
Object file ≠ Executable file
- It contains machine code, but it is incomplete because many symbols are still undefined (e.g., printf from the C standard library).
- You can think of an object file as an “unfinished puzzle piece.”
- Example:
- main.o contains the machine code for the function main().
- But it only calls printf as a reference, without including the actual code for printf.
=> This file contains machine-level instructions.
- Command:
gcc -c main.s -o main.o- Input: main.s
- Output: main.o (object file, machine code but incomplete)
- main.o:
- Binary file, not human-readable.
- Contains compiled machine code for main, but still needs printf resolved.
Linker: Before getting started with this, we should know that the library functions are a part of the C software but not of any C program. Hence, the compiler has no idea about the working of the function, whether it is a printf function or scanf function. The information for each of these functions is kept in the corresponding library, which the compiler ought to be able to connect. The linker does this task. So, when the #include is written, it includes the studio.h library, which is basically used for giving access to the Standard Output and Input. The basic goal of the linker is to link the object file to the library functions so that the programme may be run as an executable file (.exe). In this Linker process, the first.exe file will be created and this file is in an executable format. After this process, the next step is the loader process.
- The linker combines all object files and libraries to create a complete executable file.
- It resolves all undefined symbols (external references). E.g., printf() comes from the standard C library
- Example: when it finds printf in main.o, the linker looks up the actual code for printf in the C standard library (libc) and links it in.
- The output of the linker is the executable file (e.g., a.out on Linux, program.exe on Windows).
Linker step = build time (creating the executable). (This is still not running the program, just building the final binary.)
- Linking can be of two types:
- Static Linking: All the code is copied to the single file and then executable file is created.
- Dynamic Linking: Only the names of the shared libraries is added to the code and then it is referred during the execution.
GCC by default does dynamic linking, so printf() is dynamically linked in above program.
- Command:
gcc main.o -o main- Input: main.o
- Output: main (executable)
- main:
- Fully linked binary.
- Linker pulls in printf() from the C standard library (libc).
- This is the file you can run.
read in https://www.geeksforgeeks.org/c/compiling-a-c-program-behind-the-scenes/
gcc main.c -o main-
GCC internally does everything:
- Preprocessing → .i
- Compilation → .s
- Assembly → .o
- Linking → hello (executable)
-
The intermediate files .i, .s, .o are deleted (unless you use -save-temps).
-
This is the “one-shot build”, quick and commonly used.
-
By executing the below command while compiling the code, we get all intermediate files in the current directory along with the executable.
gcc -Wall -save-temps main.c –o main -
The following screenshot shows all generated intermediate files.
https://stackoverflow.com/questions/69212665/how-computer-cpu-executes-a-software-application
https://cs.stackexchange.com/questions/47410/how-is-a-program-executed-at-the-cpu-level
https://www.reddit.com/r/computerscience/comments/1g68ijl/how_exactly_does_a_cpu_run_code/
https://levelup.gitconnected.com/how-your-program-actually-runs-from-c-to-cpu-86e7d7ba299a
https://protect.bju.edu/cps/docs/cps110/textbook/ch01s01.html
https://cs.stackexchange.com/questions/47410/how-is-a-program-executed-at-the-cpu-level
https://www.cs.emory.edu/~cheung/Courses/170/Syllabus/01/intro-computer2.html
- Before diving into how programs are loaded and executed, it is essential to examine the CPU’s fundamental components—registers and buses—that enable and coordinate the execution cycle.
- [Registers Used in Program Execution]
Registers are small, high-speed storage locations within the CPU that temporarily hold data and instructions. Each register has a specific role in managing program execution:
- i. Instruction Register (IR): This register holds the instruction that the CPU is currently executing. It stores the instruction fetched from memory and passes it to the control unit to carry out the required operation.
- ii. Program Counter (PC): This register keeps track of the memory address of the next instruction to be executed. It automatically updates to the next instruction after the current one is executed, ensuring the program runs sequentially.
- iii. Accumulator (AC): The accumulator temporarily holds the results of arithmetic and logic operations performed by the CPU. It’s often used for intermediate calculations.
- iv. Memory Address Register (MAR): The MAR holds the address in memory where data is to be fetched from or written to. When the CPU needs to read or write data, the MAR is used to point to the correct location in memory.
- v. Memory Buffer Register (MBR): This register temporarily stores the data being transferred to or from memory. When data is fetched from memory, it's placed in the MBR before being passed to the appropriate part of the CPU.
- vi. Status Register (SR): The status register contains flags that represent the state of the CPU after performing an operation, such as zero (if the result is zero) or carry (if there was a carry in the operation).
- [Data Buses Used in Program Execution]
- i. Address Bus: Transmits the address from the MAR to the Memory Unit to fetch or store data.
- ii. Data Bus: Transfers the data between memory and the MBR or between the MBR and other registers like the AC.
- _iii. Control Bus: Sends signals to control memory operations, including Read and Write operations, allowing the CPU to interact with memory. _
- After compilation executable is generated and we run the generated executable using the below command.
- On Linux/Unix:
- After linking, the output is an executable named hello (by default it has no .exe extension).
- To run it, you must type ./ to specify the current directory:
./hello
- On Windows:
- The linker produces hello.exe.
- Windows recognizes .exe as an executable, so you can simply type:
helloor double-click the file to run it.No ./ is needed because Windows automatically looks for .exe in the current folder or in the system PATH.
- On Linux/Unix:
- When the user invokes the program, control is transferred to the Operating System (OS).
- The OS activates the loader, which prepares the program for execution in memory.
Loader: Whenever the command is given for the execution of a particular program, The loader plays an important role. With the help of the loader, the .exe file is loaded in the RAM and the CPU is informed of the starting point of the address of the program where it is loaded.
- i. The loader copies the executable file from disk → RAM:
- The loader reads the executable file from secondary storage (disk).
- It loads the program’s code and data sections into RAM.
- ii. Allocate memory regions into standard segments:
- Memory Layout of a Process in RAM
- Text Segment (Code Segment)
- Contains machine code instructions (compiled program code).
- Typically marked as read-only to prevent accidental modification.
- Data Segment Split into two parts.
- Initialized Data Segment: stores global and static variables with an initial value. (e.g., int x = 10;)
- Uninitialized Data Segment (BSS): stores global and static variables without an initial value. (e.g., int y;) → initialized to 0 by default.
- Heap
- Used for dynamic memory allocation (malloc, new).
- Grows upward during execution as more memory is requested.
- Stack
- Stores function call information (local variables, return addresses, parameters).
- Grows downward.
- Each function call creates a stack frame that gets destroyed when the function exits.
- Libraries (Shared Objects / DLLs)
- If the program uses external libraries, the loader maps them into memory.
This memory segmentation provides isolation and efficient management of program execution
- Text Segment (Code Segment)
- Memory Layout of a Process in RAM
- iii. Set the entry point: The OS initializes the Program Counter (PC) with the address of the program’s entry point (usually the main function).
=> At this stage, the program is ready, and execution control is passed to the CPU.
-
Once the program is loaded into memory, the CPU starts executing instructions sequentially.
-
The Central Processing Unit (CPU) is the brain of the computer, responsible for executing instructions of a computer program. This process is carried out through a cycle known as the fetch-decode-execute cycle, also referred to as the instruction cycle. This cycle is a set of operations that the CPU performs to execute each machine language instruction of a program.
-
i. Fetch the First Instruction
The first step in this cycle is the fetch operation. The CPU fetches the instruction from the main memory (RAM) into its own internal memory, the Instruction Register (IR). The Program Counter (PC) keeps track of the memory address of the next instruction to be fetched. After fetching, the PC is incremented to point to the next instruction.
- PC (Program Counter) holds the address of the next instruction.
- MAR (Memory Address Register) loads that address.
- The Address Bus sends the address to memory, and the Control Bus triggers a Read operation.
- The instruction is fetched into the MBR (Memory Buffer Register) via the Data Bus and then moved to the IR (Instruction Register).
- PC is incremented to point to the next instruction.
-
ii. Decode
The second step is the decode operation. The Control Unit (CU) of the CPU decodes the fetched instruction to determine what operation needs to be performed. This could be an arithmetic operation, a data movement operation, or a control operation. The CU uses the opcode (operation code) of the instruction to determine the type of operation.
- The Control Unit decodes the instruction in the IR, determining what needs to be done.
-
iii. Execute
The final step is the execute operation. The CPU performs the operation as determined by the decode step. This could involve the Arithmetic Logic Unit (ALU) if it's an arithmetic or logical operation, or it could involve moving data from one register to another, or from memory to a register. If the operation is a control operation, it could involve changing the sequence of execution of the program, such as in the case of a jump instruction.
- Depending on the instruction type:
- Arithmetic/Logic → The ALU performs the operation (e.g., add, subtract, AND, OR).
- Memory operations → The CPU uses MAR and MBR to read/write data in RAM.
- I/O operations → The CPU communicates with devices via system buses.
- Results may be stored in registers (e.g., the Accumulator, AC) or written back to memory.
- Depending on the instruction type:
-
iv. Repeat
This cycle repeats for each instruction in the program until the program ends. The speed at which the CPU can complete this cycle, often measured in cycles per second or hertz, is a key factor in the overall performance of a computer system. The CPU's ability to execute multiple instructions simultaneously, known as parallel processing, can also significantly enhance performance.
- The CPU loops back to Fetch.
- This cycle repeats billions of times per second (depending on the clock speed), enabling complex tasks like file deletion, graphics rendering, or web browsing—everything still boils down to math and logic at the hardware level.
- When the program finishes (e.g., after return 0; in main), control is returned to the operating system. For example, in C programs, this happens after the return statement in main (e.g., return 0;)
- The OS deallocates memory and releases system resources.
https://www.programiz.com/c-programming/c-preprocessor-macros
-
Macros are shortcuts or placeholders that the preprocessor replaces before the code is compiled. They are defined using #define and can be used to create constants or code snippets.
-
Macros can be classified into four types in C++:
- Object-Like Macros
- Function-Like Macros
- Conditional Macros
- Predefined Macros
- EX1
// C++ program to illustrate the object like macros
#include <iostream>
using namespace std;
// Define a constant for the value of PI
#define PI 3.14159
int main()
{
double radius = 4.0;
// Calculate the area of the circle
double area = PI * radius * radius;
cout << "Area of circle with radius " << radius
<< " is " << area;
return 0;
}Output: Area of circle with radius 4 is 50.2654
-EX2
#include <stdio.h>
#define MESSAGE "Hello World" // macro definition
#define TRUE 1 // macro definition
#define FALSE 0 // macro definition
#define SUM (3 + 5) // macro definition
int main() {
printf("String: %s\n", MESSAGE);
printf("Custom boolean TRUE: %d\n", TRUE);
printf("Custom boolean FALSE: %d\n", FALSE);
printf("Arithmetic: 3+5=%d\n", SUM);
return 0;
}These macros look like functions, but they are just text replacements.
// C++ program to illustrate the function like macros
#include <iostream>
using namespace std;
// Define a macro to print a value
#define PRINT(x) cout << "Value is: " << x
int main()
{
int value = 42;
// Print the value using the PRINT macro
PRINT(value);
return 0;
}Output: Value is: 42
These are used to control which parts of the code should be compiled, based on whether something is defined or not.
#include <iostream>
// Define a macro named DEBUG
#define DEBUG
int main() {
int x = 5, y = 10;
int sum = x + y;
// This block will only be compiled if DEBUG is defined
#ifdef DEBUG
std::cout << "[DEBUG] x = " << x << std::endl;
std::cout << "[DEBUG] y = " << y << std::endl;
std::cout << "[DEBUG] sum = " << sum << std::endl;
#endif
// Always compiled
std::cout << "Sum: " << sum << std::endl;
return 0;
}Output:
[DEBUG] x = 5
[DEBUG] y = 10
[DEBUG] sum = 15
Sum: 15
We can remove, or redefine, a macro that we set up previously with the #undef directive.
Macro definition is typically done at the top of the document, but macro undefining and redefining is done inside the rest of the document.
#include <stdio.h>
#define MESSAGE "Hello World"
int main() {
printf("String: %s\n", MESSAGE);
#undef MESSAGE // remove macro
#define MESSAGE "Hello there" // redefine macro
printf("String: %s\n", MESSAGE);
return 0;
}Predefined macros are special macros that are already built into the C++ compiler. You don't need to define them — they are automatically available in your program.
- The following are some commonly used predefined macros in C++:
__LINE__: This macro expands to the current line number in the source code.__FILE__: This macro expands to the name of the current source file.__DATE__: This macro expands to a string that represents the date of compilation.__TIME__: This macro expands to a string that represents the time of compilation.
// C++ program to illustrate the predefined macros
#include <iostream>
using namespace std;
int main()
{
// Display the current line number and the source file
// name
cout << "This is line " << __LINE__ << " in file "
<< __FILE__ << "\n";
// Display the compilation date
cout << "Compiled on " << __DATE__;
return 0;
}Output:
This is line 10 in file ./Solution.cpp
Compiled on Nov 7 2023
The stdarg.h header defines a variable type va_list and three macros which can be used to get the arguments in a function when the number of arguments are not known i.e. variable number of arguments.
Input
#include <stdio.h>
#include <stdarg.h>
int sum(int count, ...) {
va_list args; // args là 1 con trỏ, dùng để các lưu địa chỉ các tham số truyền vào
va_start(args, count); // va_start () tạo vùng nhớ, địa chỉ đầu tiên của nó là địa chỉ biến count đc lưu trong args
int result = 0;
for (int i = 0; i < count; i++) {
result += va_arg(args, int); // va_arg () dịch chuyển đến địa chỉ tiếp theo, và lấy giá trị tại địa chỉ đó
}
va_end(args); // va_ end () giải phóng vùng nhớ
return result;
}
int main() {
printf("Sum: %d\n", sum(4, 1, 2, 3, 4));
return 0;
}Output
Sum: 10
Input
#include <stdio.h>
#include <stdarg.h>
typedef enum { TEMPERATURE_SENSOR, PRESSURE_SENSOR } SensorType;
void processSensorData(SensorType type, ...) {
va_list args;
va_start(args, type);
switch (type) {
case TEMPERATURE_SENSOR: {
int numArgs = va_arg(args, int);
int sensorId = va_arg(args, int);
float temperature = va_arg(args, double); // float được promote thành double
printf("Temperature Sensor ID: %d, Reading: %.2f degrees\n", sensorId, temperature);
if (numArgs > 2) {
// Xử lý thêm tham số nếu có
char* additionalInfo = va_arg(args, char*);
printf("Additional Info: %s\n", additionalInfo);
}
break;
}
case PRESSURE_SENSOR: {
int numArgs = va_arg(args, int);
int sensorId = va_arg(args, int);
int pressure = va_arg(args, int);
printf("Pressure Sensor ID: %d, Reading: %d Pa\n", sensorId, pressure);
if (numArgs > 2) {
// Xử lý thêm tham số nếu có
char* unit = va_arg(args, char*);
printf("Unit: %s\n", unit);
}
break;
}
}
va_end(args);
}
int main() {
processSensorData(TEMPERATURE_SENSOR, 3, 1, 36.5, "Room Temperature");
processSensorData(PRESSURE_SENSOR, 2, 2, 101325);
return 0;
}Output
Temperature Sensor ID: 1, Reading: 36.50 degrees
Additional Info: Room Temperature
Pressure Sensor ID: 2, Reading: 101325 Pa
-
Provides a macro called assert
-
This macro can be used to verify assumptions made by the program.
-
If this assumption is false, nothing happens and the program continues to execute.
-
If this assumption is false, The program stops to execute and print a diagnostic message.
-
Using for debugging, use #define NDEBUG to turn off debug mode.
-
Macro is used for debugging
#define LOG(condition, cmd) assert(condition && #cmd);
#include <assert.h>
#define ASSERT_IN_RANGE(val, min, max) assert((val) >= (min) && (val) <= (max))
void setLevel(int level) {
ASSERT_IN_RANGE(level, 1, 10);
// Thiết lập cấp độ
}#include <assert.h>
#include <stdint.h>
#define ASSERT_SIZE(type, size) assert(sizeof(type) == (size))
void checkTypeSizes() {
ASSERT_SIZE(uint32_t, 4);
ASSERT_SIZE(uint16_t, 2);
// Kiểm tra các kích thước kiểu dữ liệu khác
}void *ptr_void;
Input
#include <stdio.h>
#include <stdlib.h>
int sum(int a, int b) {
return a+b;
}
int main() {
char array[] = "Hello";
int value = 5;
double test = 15.7;
char letter = 'A';
void *ptr = &value;
printf("value is: %d\n", *(int*)(ptr));
ptr = &test;
printf("value is: %f\n", *(double*)(ptr));
ptr = &letter;
printf("value is: %c\n", *(char*)(ptr));
ptr = (void*)sum;
printf("sum: %d\n", ((int (*)(int,int))ptr)(5,6));
void *ptr1[] = {&value, &test, &letter , (void*)sum, array};
printf("value: %d\n", *(int*)ptr1[0]);
printf("value: %s\n", ((char*)ptr1[4]));
return 0;
}Output
value is: 5
value is: 15.700000
value is: A
sum: 11
________________________________
value: 5
value: Hello
https://daynhauhoc.com/t/con-tro-ham-function-pointers/31959
Function pointer is a pointer that points to a function.
(vì function nằm ở code segment - vùng nhớ read-only khi thực thi chương trình -> Function pointer là 1 pointer to const)
Function pointers syntax <return_type> (*<name_of_pointer>)( <data_type_of_parameters> );
Input
#include <stdio.h>
void greetEnglish() {
printf("Hello!\n");
}
void greetFrench() {
printf("Bonjour!\n");
}
int main() {
void (*ptrToFunc)(); // Khai báo con trỏ hàm
ptrToFunc = greetEnglish; // Gán địa chỉ của hàm greetEnglish cho con trỏ hàm
// Gọi hàm thông qua con trỏ hàm -> In ra: Hello!
(*ptrToFunc)();// cách 1
ptrToFunc();// cách 2
ptrToFunc = greetFrench; // Gán địa chỉ của hàm greetFrench cho con trỏ hàm
// Gọi hàm thông qua con trỏ hàm -> In ra: Bonjour!
(*ptrToFunc)(); // cách 1
ptrToFunc(); // cách 2
return 0;
}Output
Hello!
Hello!
Bonjour!
Bonjour!
Input
#include <stdio.h>
void sum(int a, int b) {
printf("Sum of %d and %d is: %d\n",a,b, a+b);
}
void subtract(int a, int b) {
printf("Subtract of %d by %d is: %d \n", a, b, a-b);
}
void multiple(int a, int b) {
printf("Multiple of %d and %d is: %d \n", a, b, a*b );
}
void divide(int a, int b) {
if (b == 0) {
printf("Mau so phai khac 0\n");
return;
}
printf("%d divided by %d is: %f \n",a,b, (double)a / (double)b);
}
void calculator(void (*ptr)(int, int), int a, int b) {
ptr(a,b);
}
int main() {
// Cách 1: dùng hàm calculator thay địa chỉ hàm cần tính cho con trỏ hàm
printf("\tProgram calculate: \n");
calculator(sum,5,2);
calculator(subtract,5,2);
calculator(multiple,5,2);
calculator(divide,5,2);
printf ("______________________________\n");
// Cách 2: tạo 1 mảng chứa các con trỏ hàm cho từng hàm riêng biệt
printf("\tProgram calculate: \n");
void (*ptr[])(int, int) = {sum, subtract, multiple, divide};
ptr[0](5,2);
ptr[1](5,2);
ptr[2](5,2);
ptr[3](5,2);
return 0;
}Output
Program calculate:
Sum of 5 and 2 is: 7
Subtract of 5 by 2 is: 3
Multiple of 5 and 2 is: 10
5 divided by 2 is: 2.500000
______________________________
Program calculate:
Sum of 5 and 2 is: 7
Subtract of 5 by 2 is: 3
Multiple of 5 and 2 is: 10
5 divided by 2 is: 2.500000
Input
#include <stdio.h>
#include <string.h>
void bubbleSort(int arr[], int n) {
int i, j, temp;
for (i = 0; i < n-1; i++) {
for (j = i+1; j < n; j++) {
if (arr[i] > arr[j]) {
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr)/sizeof(arr[0]);
bubbleSort(arr, n);
printf("Sorted array: \n");
for (int i=0; i < n; i++)
printf("%d ", arr[i]);
return 0;
}Input
#include <stdio.h>
#include <string.h>
typedef struct {
char ten[50];
float diemTrungBinh;
int id;
} SinhVien;
int stringCompare(const char *str1, const char *str2) {
while (*str1 && (*str1 == *str2)) {
str1++;
str2++;
}
return *(const unsigned char*)str1 - *(const unsigned char*)str2;
}
// Hàm so sánh theo tên
int compareByName(const void *a, const void *b) {
SinhVien *sv1 = (SinhVien *)a;
SinhVien *sv2 = (SinhVien *)b;
return stringCompare(sv1->ten, sv2->ten);
}
// Hàm so sánh theo điểm trung bình
int compareByDiemTrungBinh(const void *a, const void *b) {
SinhVien *sv1 = (SinhVien *)a;
SinhVien *sv2 = (SinhVien *)b;
if (sv1->diemTrungBinh > sv2->diemTrungBinh) return 1;
return 0;
}
// Hàm so sánh theo ID
int compareByID(const void *a, const void *b) {
SinhVien *sv1 = (SinhVien *)a;
SinhVien *sv2 = (SinhVien *)b;
return sv1->id - sv2->id;
}
// Hàm sắp xếp chung
void sort(SinhVien array[], size_t size, int (*compareFunc)(const void *, const void *)) {
int i, j;
SinhVien temp;
for (i = 0; i < size-1; i++)
for (j = i+1; j < size; j++)
if (compareFunc(array+i, array+j)>0) {
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
void display(SinhVien *array, size_t size) {
for (size_t i = 0; i < size; i++) {
printf("ID: %d, Ten: %s, Diem Trung Binh: %.2f\n", array[i].id, array[i].ten, array[i].diemTrungBinh);
}
printf("\n");
}
int main() {
SinhVien danhSachSV[] = {
{
.ten = "Hoang",
.diemTrungBinh = 7.5,
.id = 100
},
{
.ten = "Tuan",
.diemTrungBinh = 4.5,
.id = 101
},
{
.ten = "Vy",
.diemTrungBinh = 6.8,
.id = 102},
{
.ten = "Ngan",
.diemTrungBinh = 5.6,
.id = 10
},
};
size_t size = sizeof(danhSachSV) / sizeof(danhSachSV[0]);
// Sắp xếp theo tên
sort(danhSachSV, size, compareByName);
display(danhSachSV, size);
// Sắp xếp theo điểm trung bình
sort(danhSachSV, size, compareByDiemTrungBinh);
display(danhSachSV, size);
// Sắp xếp theo ID
sort(danhSachSV, size, compareByID);
display(danhSachSV, size);
return 0;
}#include <stdio.h>
typedef struct {
void (*start)(int gpio);
void (*stop)(int gpio);
void (*changeSpeed)(int gpio, int speed);
} MotorController;
typedef int PIN;
// Các hàm chung
void startMotor(PIN pin) {
printf("Start motor at PIN %d\n", pin);
}
void stopMotor(PIN pin) {
printf("Stop motor at PIN %d\n", pin);
}
void changeSpeedMotor(PIN pin, int speed) {
printf("Change speed at PIN %d: %d\n", pin, speed);
}
// Macro để khởi tạo GPIO và MotorController
#define INIT_MOTOR(motorName, pinNumber) \
PIN PIN_##motorName = pinNumber; \
MotorController motorName = {startMotor, stopMotor, changeSpeedMotor};
int main() {
// Sử dụng macro để khởi tạo
INIT_MOTOR(motorA, 1);
INIT_MOTOR(motorB, 2);
// Sử dụng motorA
motorA.start(g_motorA);
motorA.changeSpeed(g_motorA, 50);
motorA.stop(g_motorA);
// Sử dụng motorB
motorB.start(g_motorB);
motorB.changeSpeed(g_motorB, 75);
motorB.stop(g_motorB);
return 0;
}Ex:
#include <stdio.h>
#include <stdlib.h>
int main() {
int value = 5;
int const *ptr_const = &value;
//*ptr_const = 7; // wrong
//ptr_const++; // right
printf("value: %d\n", *ptr_const);
value = 9;
printf("value: %d\n", *ptr_const);
return 0;
}Ex:
#include <stdio.h>
#include <stdlib.h>
int main() {
int value = 5;
int test = 15;
int *const const_ptr = &value;
printf("value: %d\n", *const_ptr);
*const_ptr = 7;
printf("value: %d\n", *const_ptr);
//const_ptr = &test; // wrong
return 0;
}#include <stdio.h>
int main() {
int value = 42;
int *ptr1 = &value; // Con trỏ thường trỏ đến một biến
int **ptr2 = &ptr1; // Con trỏ đến con trỏ
/*
**ptr2 = &ptr1
ptr2 = &ptr1;
*ptr2 = ptr1 = &value;
**ptr2 = *ptr1 = value
*/
printf("address of value: %p\n", &value);
printf("value of ptr1: %p\n", ptr1);
printf("address of ptr1: %p\n", &ptr1);
printf("value of ptr2: %p\n", ptr2);
printf("dereference ptr2 first time: %p\n", *ptr2);
printf("dereference ptr2 second time: %d\n", **ptr2);
return 0;
}#include <stdio.h>
int main() {
int *ptr = NULL; // Gán giá trị NULL cho con trỏ 0x0000000
if (ptr == NULL) {
printf("Pointer is NULL\n");
} else {
printf("Pointer is not NULL\n");
}
int score_game = 5;
if (ptr == NULL) {
ptr = &score_game;
*ptr = 30;
ptr = NULL;
}
return 0;
}- Syntax:
extern data_type variable_name;
#include <stdio.h>
void exampleFunction() {
static int count = 0; // Biến static giữ giá trị qua các lần gọi hàm
count++;
printf("Count: %d\n", count);
}
int main() {
exampleFunction(); // In ra "Count: 1"
exampleFunction(); // In ra "Count: 2"
exampleFunction(); // In ra "Count: 3"
return 0;
}File motor.c
#include <stdio.h>
#include "motor.h"
void startMotor(PIN pin) {
printf("Start motor at PIN %d\n", pin);
}
void stopMotor(PIN pin) {
printf("Stop motor at PIN %d\n", pin);
}
void changeSpeedMotor(PIN pin, int speed) {
printf("Change speed at PIN %d: %d\n", pin, speed);
}
void init_motor(MotorController *motorName) {
motorName->start = startMotor;
motorName->stop = stopMotor;
motorName->changeSpeed = changeSpeedMotor;
}File motor.h
#ifndef __MOTOR_H
#define __MOTOR_H
typedef struct {
void (*start)(int gpio);
void (*stop)(int gpio);
void (*changeSpeed)(int gpio, int speed);
} MotorController;
typedef int PIN;
static void startMotor(PIN pin);
static void stopMotor(PIN pin);
static void changeSpeedMotor(PIN pin, int speed);
void init_motor(MotorController *motorName);
#endif#include <iostream>
typedef enum {
red = 0,
blue,
green,
purple,
black,
yellow
} Pen_Color;
void print_color_pen(Pen_Color color) {
switch (color) {
case red:
std::cout << "Red\n";
break;
case blue:
std::cout << "Blue\n";
break;
case green:
std::cout << "Green\n";
break;
default:
break;
}
}
class PEN {
public:
Pen_Color pen_color;
static int pen_length;
PEN(Pen_Color color);
Pen_Color get_color()
{
return pen_color;
}
void set_length(int length)
{
pen_length = length;
}
};
int PEN::pen_length;
PEN::PEN(Pen_Color color) {
pen_color = color;
pen_length = 10;
}
int main(int argc, char const *argv[]) {
PEN blue_pen(blue);
PEN red_pen(red);
PEN green_pen(green);
blue_pen.set_length(9);
std::cout << "Color: ";
print_color_pen(blue_pen.get_color());
std::cout << "Length: " << blue_pen.pen_length << '\n';
std::cout << "Color: ";
print_color_pen(red_pen.get_color());
std::cout << "Length: " << red_pen.pen_length << '\n';
std::cout << "Color: ";
print_color_pen(green_pen.get_color());
std::cout << "Length: " << green_pen.pen_length << '\n';
return 0;
}#include "stm32f10x.h"
volatile int i = 0;
int a = 100;
int main() {
while(1) {
i = *((int*) 0x20000000);
if (i > 0) break;
}
a = 200;
}
#include <stdio.h>
#include <time.h>
int main() {
// Lưu thời điểm bắt đầu
clock_t start_time = clock();
int i;
// Đoạn mã của chương trình
for (i = 0; i < 2000000; ++i) {
// Thực hiện một số công việc bất kỳ
}
// Lưu thời điểm kết thúc
clock_t end_time = clock();
// Tính thời gian chạy bằng miligiây
double time_taken = ((double)(end_time - start_time)) / CLOCKS_PER_SEC;
printf("Thoi gian chay cua chuong trinh: %f giay\n", time_taken);
return 0;
}
#include <stdio.h>
void delay() {
double start;
while (start < 60000000) start++;
}
char letter = 'A';
char first_sentence[] = "HELLO";
char second_sentence[] = "FASHION COTHES";
char third_sentence[] = "SUITABLE PRICE";
int letter_A[8][8] = { {0,0,1,0,0,0,0,0},
{0,1,0,1,0,0,0,0},
{1,0,0,0,1,0,0,0},
{1,1,1,1,1,0,0,0},
{1,0,0,0,1,0,0,0},
{1,0,0,0,1,0,0,0},
{1,0,0,0,1,0,0,0},
{1,0,0,0,1,0,0,0}, };
int letter_H[8][8] = { {1,0,0,0,1,0,0,0},
{1,0,0,0,1,0,0,0},
{1,0,0,0,1,0,0,0},
{1,1,1,1,1,0,0,0},
{1,0,0,0,1,0,0,0},
{1,0,0,0,1,0,0,0},
{1,0,0,0,1,0,0,0},
{1,0,0,0,1,0,0,0}, };
int letter_L[8][8] = { {1,0,0,0,0,0,0,0},
{1,0,0,0,0,0,0,0},
{1,0,0,0,0,0,0,0},
{1,0,0,0,0,0,0,0},
{1,0,0,0,0,0,0,0},
{1,0,0,0,0,0,0,0},
{1,0,0,0,0,0,0,0},
{1,1,1,1,1,0,0,0}, };
// H, e, l,o, F, a, ....
int button = 0;
typedef enum {
FIRST,
SECOND,
THIRD
} Sentence;
int main() {
Sentence sentence = FIRST;
while(1) {
switch (sentence) {
case FIRST:
for (int index = 0; index < sizeof(first_sentence); index++) {
if (first_sentence[index] == 'H') {
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
if (letter_H[i][j] == 1) {
printf("Turn on led at [%d][%d]\n", i,j);
if (button == 1) { goto exit_loops; }
}
}
// tat den
}
}
if (first_sentence[index] == 'e') {
// in ra chu e
}
}
printf("first sentence is done\n");
delay();
goto logic;
case SECOND:
for (int index = 0; index < sizeof(second_sentence); index++) {
if (second_sentence[index] == 'A') {
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
if (letter_A[i][j] == 1) {
printf("Turn on led at [%d][%d]\n", i,j);
if (button == 1) { goto exit_loops; }
}
}
// tat den led
}
}
if (second_sentence[index] == 'F') {
// in ra chu F
}
}
printf("second sentence is done\n");
delay();
goto logic;
case THIRD:
for (int index = 0; index < sizeof(third_sentence); index++) {
if (third_sentence[index] == 'L') {
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
if (letter_L[i][j] == 1) {
printf("Turn on led at [%d][%d]\n", i,j);
if (button == 1) { goto exit_loops; }
}
}
// tat den led
}
}
if (third_sentence[index] == 'E') {
// in ra chu H
}
}
printf("third sentence is done\n");
delay();
//button = 1;
goto logic;
}
logic:
if (sentence == FIRST) { sentence = SECOND; }
else if (sentence == SECOND) { sentence = THIRD; }
else if (sentence == THIRD) { sentence = FIRST; }
goto exit;
exit_loops:
printf("Stop!\n");
break;
exit:;
}
return 0;
}Bitmask in C is a powerful way to basically work with bits and functions upon boolean logic. It can be used to store data compactly and with much more efficiency in certain cases.
A bit is the smallest unit of data which can either store a 0 or 1 inside it. All the data in the computer is stored using these bits. These 2 possible values can also be represented as boolean values that are True or False. Using this we can apply boolean logic to manipulate data stored on the computer.
Bitmasking is a technique that involves bit manipulation. It is basically like putting a mask over certain bits and hiding the other un-useful bits, so as to make the program much more efficient and optimize the memory. A bitmask is a sequence of bits that can also be known as a bitset or bit field and is used to perform bitwise operations on the given data. There are basically 6 bitwise operators in C that can be used to manipulate bits which are as follows:
+ & (Bitwise AND Operator)
+ | (Bitwise OR Operator)
+ ^ (Bitwise XOR Operator)
+ ~ (Bitwise NOT Operator)
+ >> (RIght Shift Operator)
+ << (Left Shift Operator)
Using these operators, we perform different bit masking techniques according to the requirements. Let's discuss these techniques and how to implement them.
In C programming, both structures and unions are used to group different types of data under a single name, but they behave in different ways. The main difference lies in how they store data. The below table lists the primary differences between the C structures and unions:
A structure in C is a collection of variables, possibly of different types, under a single name. Each member of the structure is allocated its own memory space, and the size of the structure is the sum of the sizes of all its members.
struct name {
member1 definition;
member2 definition;
…
memberN definition;
};
Input
#include <stdio.h>
struct Student {
char name[50];
int age;
float grade;
};
int main() {
// Create a structure variable
struct Student s1 = {"Geek", 20, 85.5};
// Access structure members
printf("%s\n", s1.name);
printf("%d\n", s1.age);
printf("%.2f\n", s1.grade);
printf("Size: %d bytes", sizeof(s1));
return 0;
}Output
Geek
20
85.50
Size: 60 bytes
A union in C is similar to a structure, but with a key difference: all members of a union share the same memory location. This means only one member of the union can store a value at any given time. The size of a union is determined by the size of its largest member.
union name {
member1 definition;
member2 definition;
…
memberN definition;
};
Input
#include <stdio.h>
#include <stdint.h>
typedef union Data {
uint8_t arr1[5];
uint8_t arr2[3];
uint8_t arr3[6];
}Data;
void display(uint8_t arr[], int size)
{
for (int i = 0; i < size; i++)
{
printf("arr[%d]: %d\n",i, arr[i]);
}
printf("----------\n");
}Input
#include <stdio.h>
union Data {
int i;
double d;
char c;
};
int main() {
// Create a union variable
union Data data;
// Store an integer in the union
data.i = 100;
printf("%d", data.i);
// Store a double in the union (this will
// overwrite the integer value)
data.d = 99.99;
printf("%.2f", data.d);
// Store a character in the union (this will
// overwrite the double value)
data.c = 'A';
printf("%c", data.c);
printf("Size: %d", sizeof(data));
return 0;
}Output
100
99.99
A
Size: 8
#include <stdio.h>
#include <stdint.h>
#include <string.h>
typedef union {
struct {
uint8_t id[2];
uint8_t data[4];
uint8_t check_sum[2];
} data;
uint8_t frame[8];
} Data_Frame;
int main(int argc, char const *argv[])
{
Data_Frame transmitter_data;
strcpy(transmitter_data.data.id, "10");
strcpy(transmitter_data.data.data, "1234");
strcpy(transmitter_data.data.check_sum, "70");
Data_Frame receiver_data;
strcpy(receiver_data.frame, transmitter_data.frame);
return 0;
}[] How C program structure the memory area? https://www.scaler.com/topics/c/memory-layout-in-c/
- Contains executable instructions
- Sharable
- Read-only
| Global variables & Static variables | Initialized (≠0) by programmer |
| Global variables & Static variables | Uninitialized or Initialized (=0) by programmer |
- malloc/ calloc/ realloc/ free
- new/ delete
Forget to deallocate memory in Heap -> cause Memory leak
#include <stdlib.h>
int main() {
int *arr_malloc, *arr_calloc;
size_t size = 5;
arr_malloc = (int*)malloc(size * sizeof(int)); // Sử dụng malloc
arr_calloc = (int*)calloc(size, sizeof(int)); // Sử dụng calloc
// ...
// Giải phóng bộ nhớ
free(arr_malloc);
free(arr_calloc);
return 0;
}#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[]) {
int soluongkytu = 0;
char* ten = (char*) malloc(sizeof(char) * soluongkytu);
for (int i = 0; i < 3; i++) {
printf("Nhap so luong ky tu trong ten: \n");
scanf("%d", &soluongkytu);
ten = realloc(ten, sizeof(char) * soluongkytu);
printf("Nhap ten cua ban: \n");
scanf("%s", ten);
printf("Hello %s\n", ten);
}
return 0;
}#include <stdio.h>
#include <stdlib.h>
void test1() {
int array[3];
for (int i = 0; i < 3; i++) {
printf("address of array[%d]: %p\n", i, (array+i));
}
printf("----------------------\n");
}
void test2() {
int *array = (int*)malloc(3*sizeof(int));
for (int i = 0; i < 3; i++) {
printf("address of array[%d]: %p\n", i, (array+i));
}
printf("----------------------\n");
//free(array);
}
int main(int argc, char const *argv[]) {
test1(); test1();
test2(); test2();
return 0;
}| LIFO structure (Last In First Out)
| Grows in the direction opposite to heap
| Function frame
https://www.w3schools.com/js/js_json_intro.asp
https://www.geeksforgeeks.org/cjson-json-file-write-read-modify-in-c/
+ JSON stands for JavaScript Object Notation
+ JSON is a text format for storing and transporting data; which is used for data exchange between applications and web services.
+ JSON is "self-describing", easy to read and write for humans and machines alike.
-
In JSON, values must be one of the following data types:
- a string (must be written in double quotes) -> Ex: {"name":"John"}
- a number (must be an integer or a floating point) -> Ex: {"age":30}
- an object (Values in JSON can be objects) -> Ex: { "employee":{"name":"John", "age":30, "city":"New York"} }
- an array -> Ex: { "employees":["John", "Anna", "Peter"] }
- a boolean -> Ex: {"sale":true}
- null -> Ex: {"middlename":null}
-
JSON values cannot be one of the following data types:
- a function
- a date
- undefined
{ "name": "Bob Johnson", "age": 35, "city": "Chicago" },
{ "name": "John Doe", "age": 30, "city": "New York", "occupation": "Software Engineer", "isStudent": false },
{
"name": "Jane Smith",
"age": null,
"city": "Los Angeles",
"contact": { "email": "jane.smith@example.com", "phone": "555-1234" }
}#include <stdio.h>
#include <cjson/cJSON.h>
int main() {
// create a cJSON object
cJSON *json = cJSON_CreateObject();
cJSON_AddStringToObject(json, "name", "John Doe");
cJSON_AddNumberToObject(json, "age", 30);
cJSON_AddStringToObject(json, "email", "john.doe@example.com");
// convert the cJSON object to a JSON string
char *json_str = cJSON_Print(json);
// write the JSON string to a file
FILE *fp = fopen("data.json", "w");
if (fp == NULL) {
printf("Error: Unable to open the file.\n");
return 1;
}
printf("%s\n", json_str);
fputs(json_str, fp);
fclose
// free the JSON string and cJSON object
cJSON_free(json_str);
cJSON_Delete(json);
return 0;
}#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stddef.h>
#include <ctype.h>
#include <stdbool.h>
typedef enum {
JSON_NULL,
JSON_BOOLEAN,
JSON_NUMBER,
JSON_STRING,
JSON_ARRAY,
JSON_OBJECT
} JsonType;
typedef struct JsonValue {
JsonType type;
union {
int boolean; double number; char *string;
struct {
struct JsonValue *values;
size_t count; // số lượng element
} array;
struct {
char **keys;
struct JsonValue *values;
size_t count; // số cặp key-value
} object;
} value;
} JsonValue;
JsonValue *parse_json(const char **json);
void free_json_value(JsonValue *json_value);
static void skip_whitespace(const char **json) {
while (isspace(**json)) {
(*json)++;
}
}
JsonValue *parse_null(const char **json) {
skip_whitespace(json);
if (strncmp(*json, "null", 4) == 0) { // khi bắt gặp đc chữ n -> nó lấy thêm 3 phần tử tiếp theo -> so sánh với null
JsonValue *value = (JsonValue *) malloc(sizeof(JsonValue));
value->type = JSON_NULL;
*json += 4;
return value;
}
return NULL;
}
JsonValue *parse_boolean(const char **json) {
skip_whitespace(json);
JsonValue *value = (JsonValue *) malloc(sizeof(JsonValue));
if (strncmp(*json, "true", 4) == 0) {
value->type = JSON_BOOLEAN;
value->value.boolean = true;
*json += 4;
} else if (strncmp(*json, "false", 5) == 0) {
value->type = JSON_BOOLEAN;
value->value.boolean = false;
*json += 5;
} else {
free(value);
return NULL;
}
return value;
}
JsonValue *parse_number(const char **json) {
skip_whitespace(json);
char *end; //:')
double num = strtod(*json, &end);
if (end != *json) {
JsonValue *value = (JsonValue *) malloc(sizeof(JsonValue));
value->type = JSON_NUMBER;
value->value.number = num;
*json = end;
return value;
}
return NULL;
}
JsonValue *parse_string(const char **json) {
skip_whitespace(json);
if (**json == '\"') {
(*json)++;
const char *start = *json;
while (**json != '\"' && **json != '\0') {
(*json)++;
}
if (**json == '\"') {
size_t length = *json - start; // 3
char *str = (char *) malloc((length + 1) * sizeof(char));
strncpy(str, start, length);
str[length] = '\0';
JsonValue *value = (JsonValue *) malloc(sizeof(JsonValue));
value->type = JSON_STRING;
value->value.string = str;
(*json)++;
return value;
}
}
return NULL;
}
JsonValue *parse_array(const char **json) {
skip_whitespace(json);
if (**json == '[') {
(*json)++;
skip_whitespace(json);
JsonValue *array_value = (JsonValue *)malloc(sizeof(JsonValue));
array_value->type = JSON_ARRAY;
array_value->value.array.count = 0;
array_value->value.array.values = NULL;
/*
double arr[2] = {};
arr[0] = 30;
arr[1] = 70;
*/
while (**json != ']' && **json != '\0') {
JsonValue *element = parse_json(json); // 70
if (element) {
array_value->value.array.count++;
array_value->value.array.values = (JsonValue *)realloc(array_value->value.array.values, array_value->value.array.count * sizeof(JsonValue));
array_value->value.array.values[array_value->value.array.count - 1] = *element;
free(element);
} else {
break;
}
skip_whitespace(json);
if (**json == ',') {
(*json)++;
}
}
if (**json == ']') {
(*json)++;
return array_value;
} else {
free_json_value(array_value);
return NULL;
}
}
return NULL;
}
JsonValue *parse_object(const char **json) {
skip_whitespace(json);
if (**json == '{') {
(*json)++;
skip_whitespace(json);
JsonValue *object_value = (JsonValue *)malloc(sizeof(JsonValue));
object_value->type = JSON_OBJECT;
object_value->value.object.count = 0;
object_value->value.object.keys = NULL;
object_value->value.object.values = NULL;
while (**json != '}' && **json != '\0') {
JsonValue *key = parse_string(json);
if (key) {
skip_whitespace(json);
if (**json == ':') {
(*json)++;
JsonValue *value = parse_json(json);
if (value) {
object_value->value.object.count++;
object_value->value.object.keys = (char **)realloc(object_value->value.object.keys, object_value->value.object.count * sizeof(char *));
object_value->value.object.keys[object_value->value.object.count - 1] = key->value.string;
object_value->value.object.values = (JsonValue *)realloc(object_value->value.object.values, object_value->value.object.count * sizeof(JsonValue));
object_value->value.object.values[object_value->value.object.count - 1] = *value;
free(value);
} else {
free_json_value(key);
break;
}
} else {
free_json_value(key);
break;
}
} else {
break;
}
skip_whitespace(json);
if (**json == ',') {
(*json)++;
}
}
if (**json == '}') {
(*json)++;
return object_value;
} else {
free_json_value(object_value);
return NULL;
}
}
return NULL;
}
JsonValue *parse_json(const char **json) { // làm việc với giá trị json_str_value
while (isspace(**json)) {
(*json)++;
}
switch (**json) {
case 'n':
return parse_null(json);
case 't':
case 'f':
return parse_boolean(json);
case '\"':
return parse_string(json);
case '[':
return parse_array(json);
case '{':
return parse_object(json);
default:
if (isdigit(**json) || **json == '-') return parse_number(json);
else return NULL; // Lỗi phân tích cú pháp
}
}
void free_json_value(JsonValue *json_value) {
if (json_value == NULL) return;
switch (json_value->type) {
case JSON_STRING:
free(json_value->value.string);
break;
case JSON_ARRAY:
for (size_t i = 0; i < json_value->value.array.count; i++) {
free_json_value(&json_value->value.array.values[i]);
}
free(json_value->value.array.values);
break;
case JSON_OBJECT:
for (size_t i = 0; i < json_value->value.object.count; i++) {
free(json_value->value.object.keys[i]);
free_json_value(&json_value->value.object.values[i]);
}
free(json_value->value.object.keys);
free(json_value->value.object.values);
break;
default:
break;
}
}
void test(JsonValue* json_value){
if (json_value != NULL && json_value->type == JSON_OBJECT) {
// Truy cập giá trị của các trường trong đối tượng JSON
size_t num_fields = json_value->value.object.count;
size_t num_fields2 = json_value->value.object.values->value.object.count;
for (size_t i = 0; i < num_fields; ++i) {
char* key = json_value->value.object.keys[i];
JsonValue* value = &json_value->value.object.values[i];
JsonType type = (int)(json_value->value.object.values[i].type);
if(type == JSON_STRING) printf("%s: %s\n", key, value->value.string);
if(type == JSON_NUMBER) printf("%s: %f\n", key, value->value.number);
if(type == JSON_BOOLEAN) printf("%s: %s\n", key, value->value.boolean ? "True":"False");
if(type == JSON_OBJECT){
printf("%s: \n", key);
test(value);
}
if(type == JSON_ARRAY){
printf("%s: ", key);
for (int i = 0; i < value->value.array.count; i++) {
test(value->value.array.values + i);
}
printf("\n");
}
}
} else {
if(json_value->type == JSON_STRING) printf("%s ", json_value->value.string);
if(json_value->type == JSON_NUMBER) printf("%f ", json_value->value.number);
if(json_value->type == JSON_BOOLEAN) printf("%s ", json_value->value.boolean ? "True":"False");
if(json_value->type == JSON_OBJECT){
printf("%s: \n", json_value->value.object.keys);
test(json_value->value.object.values);
}
}
}
int main(int argc, char const *argv[]) {
// Chuỗi JSON đầu vào
const char* json_str = "{"
"\"1001\":{"
"\"SoPhong\":3,"
"\"NguoiThue\":{"
"\"Ten\":\"Nguyen Van A\","
"\"CCCD\":\"1920517781\","
"\"Tuoi\":26,"
"\"ThuongTru\":{"
"\"Duong\":\"73 Ba Huyen Thanh Quan\","
"\"Phuong_Xa\":\"Phuong 6\","
"\"Tinh_TP\":\"Ho Chi Minh\""
"}"
"},"
"\"SoNguoiO\":{"
"\"1\":\"Nguyen Van A\","
"\"2\":\"Nguyen Van B\","
"\"3\":\"Nguyen Van C\""
"},"
"\"TienDien\": [24, 56, 98],"
"\"TienNuoc\":30.000"
"},"
"\"1002\":{"
"\"SoPhong\":5,"
"\"NguoiThue\":{"
"\"Ten\":\"Phan Hoang Trung\","
"\"CCCD\":\"012345678912\","
"\"Tuoi\":24,"
"\"ThuongTru\":{"
"\"Duong\":\"53 Le Dai Hanh\","
"\"Phuong_Xa\":\"Phuong 11\","
"\"Tinh_TP\":\"Ho Chi Minh\""
"}"
"},"
"\"SoNguoiO\":{"
"\"1\":\"Phan Van Nhat\","
"\"2\":\"Phan Van Nhi\","
"\"2\":\"Phan Van Tam\","
"\"3\":\"Phan Van Tu\""
"},"
"\"TienDien\":23.000,"
"\"TienNuoc\":40.000"
"}"
"}";
// Phân tích cú pháp chuỗi JSON
JsonValue* json_value = parse_json(&json_str);
test(json_value);
// Kiểm tra kết quả phân tích cú pháp
// Giải phóng bộ nhớ được cấp phát cho JsonValue
free_json_value(json_value);
//printf("test = %x", '\"');
// hienthi(5);
return 0;
}https://techacademy.edu.vn/danh-sach-lien-ket-don-c/
https://www.tutorialspoint.com/data_structures_algorithms/linked_list_algorithms.htm
- A linked list is a linear data structure that stores a collection of data elements dynamically.
- Nodes represent those data elements, and links or pointers connect each node.
- Each node consists of two fields, the information stored in a linked list and a pointer that stores the address of its next node.
- The last node contains null in its second field because it will point to no node.
- A linked list can grow and shrink its size, as per the requirement.
- It does not waste memory space.
tyedef struct Node {
int value;
struct Node* next;
} Node;
Node* createNode (int value) {
Node* ptr = (Node*)malloc(sizeof(Node));
ptr->value = value;
ptr->next = NULL;
return ptr;
};In this operation, we are adding an element at the beginning of the list.
- START
- Create a node to store the data
- Check if the list is empty
- If the list is empty, add the data to the node and assign the head pointer to it.
- If the list is not empty, add the data to a node and link to the current head. Assign the head to the newly added node.
- END
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct node {
int data;
struct node *next;
};
struct node *head = NULL;
struct node *current = NULL;
// display the list
void printList(){
struct node *p = head;
printf("\n[");
//start from the beginning
while(p != NULL) {
printf(" %d ",p->data);
p = p->next;
}
printf("]");
}
//insertion at the beginning
void insertatbegin(int data){
//create a link
struct node *ptr = (struct node*) malloc(sizeof(struct node));
ptr->data = data;
// point it to old first node
ptr->next = head;
//point first to new first node
head = ptr;
}
void main(){
int k=0;
insertatbegin(12);
insertatbegin(22);
insertatbegin(30);
insertatbegin(44);
insertatbegin(50);
printf("Linked List: ");
// print list
printList();
}
In this operation, we are adding an element at the ending of the list.
- START
- Create a new node and assign the data
- Find the last node
- Point the last node to new node
- END
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct node {
int data;
struct node *next;
};
struct node *head = NULL;
struct node *current = NULL;
// display the list
void printList(){
struct node *p = head;
printf("\n[");
//start from the beginning
while(p != NULL) {
printf(" %d ",p->data);
p = p->next;
}
printf("]");
}
//insertion at the beginning
void insertatbegin(int data){
//create a link
struct node *lk = (struct node*) malloc(sizeof(struct node));
lk->data = data;
// point it to old first node
lk->next = head;
//point first to new first node
head = lk;
}
void insertatend(int data){
//create a link
struct node *lk = (struct node*) malloc(sizeof(struct node));
lk->data = data;
lk->next = NULL;
struct node *linkedlist = head;
// point it to old first node
while(linkedlist->next != NULL)
linkedlist = linkedlist->next;
//point first to new first node
linkedlist->next = lk;
}
void main(){
int k=0;
insertatbegin(12);
insertatend(22);
insertatend(30);
insertatend(44);
insertatend(50);
printf("Linked List: ");
// print list
printList();
}
In this operation, we are adding an element at any position within the list.
- START
- Create a new node and assign data to it
- Iterate until the node at position is found
- Point first to new first node
- END
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct node {
int data;
struct node *next;
};
struct node *head = NULL;
struct node *current = NULL;
// display the list
void printList(){
struct node *p = head;
printf("\n[");
//start from the beginning
while(p != NULL) {
printf(" %d ",p->data);
p = p->next;
}
printf("]");
}
//insertion at the beginning
void insertatbegin(int data){
//create a link
struct node *lk = (struct node*) malloc(sizeof(struct node));
lk->data = data;
// point it to old first node
lk->next = head;
//point first to new first node
head = lk;
}
void insertafternode(struct node *list, int data){
struct node *lk = (struct node*) malloc(sizeof(struct node));
lk->data = data;
lk->next = list->next;
list->next = lk;
}
void main(){
int k=0;
insertatbegin(12);
insertatbegin(22);
insertafternode(head->next, 30);
printf("Linked List: ");
// print list
printList();
}
- A stack is a linear data structure in which the insertion of a new element and removal of an existing element takes place at the same end represented as the top of the stack.
- It follows the principle of LIFO (Last In First Out) - This means the last element inserted inside the stack is removed first.
- push() to insert an element into the stack
- pop() to remove an element from the stack
- peek() Get the value of the top element without removing it.
- isempty() Check if the stack is empty.
- isfull() Check if the stack is full.
- size() returns the size of stack.
How does it work?
- The operations work as follows:
- A pointer called TOP is used to keep track of the top element in the stack.
- When initializing the stack, we set its value to -1 so that we can check if the stack is empty by comparing TOP == -1.
- On pushing an element, we increase the value of TOP and place the new element in the position pointed to by TOP.
- On popping an element, we return the element pointed to by TOP and reduce its value.
- Before pushing, we check if the stack is already full
- Before popping, we check if the stack is already empty
int peek() {
return stack[top];
}bool isempty() {
if(top == -1)
return true;
else
return false;
}
bool isfull() {
if(top == MAXSIZE)
return true;
else
return false;
}void push(int data) {
if(!isFull()) {
top = top + 1;
stack[top] = data;
} else {
printf("Stack overflow\n");
}
}int pop(int data) {
if(!isempty()) {
data = stack[top];
top = top - 1;
return data;
} else {
printf("Stack underflow\n");
}
}#include <stdio.h>
#include <stdlib.h>
typedef struct Stack {
int* items; // mảng để lưu giá trị của từng ô
int size;
int top;
} Stack;
void initialize( Stack *stack, int size) {
stack->items = (int*) malloc(sizeof(int) * size);
stack->size = size;
stack->top = -1;
}
int is_empty( Stack stack) {
return stack.top == -1;
}
int is_full( Stack stack) {
return stack.top == stack.size - 1;
}
void push( Stack *stack, int value) {
if (!is_full(*stack)) {
stack->items[++stack->top] = value;
} else {
printf("Stack overflow\n");
}
}
int pop( Stack *stack) {
if (!is_empty(*stack)) {
return stack->items[stack->top--];
} else {
printf("Stack underflow\n");
return -1;
}
}
int top( Stack stack) {
if (!is_empty(stack)) {
return stack.items[stack.top];
} else {
printf("Stack is empty\n");
return -1;
}
}
int main() {
Stack stack1;
initialize(&stack1, 5);
push(&stack1, 10);
push(&stack1, 20);
push(&stack1, 30);
push(&stack1, 40);
push(&stack1, 50);
push(&stack1, 60);
printf("Top element: %d\n", top(stack1)); // 50
printf("Pop element: %d\n", pop(&stack1)); // 50
printf("Pop element: %d\n", pop(&stack1)); // 40
printf("Top element: %d\n", top(stack1)); // 30
return 0;
}#include <stdio.h>
#include <stdlib.h>
void push();
void pop();
void display();
struct node {
int data;
struct node* next;
};
struct node* temp; // Variable to store the top of the stack
int main() {
printf("LINKED LIST IMPLEMENTATION USING STACKS\n\n");
do {
printf("1. Insert\n2. Delete\n3. Display\n4. Exit\n\n");
printf("Enter your choice:");
int choice; scanf("%d", &choice);
switch (choice) {
case 1:
push();
break;
case 2:
pop();
break;
case 3:
display();
break;
case 4:
exit(0);
break;
default:
printf("Please re-enter!\n");
break;
}
} while (choice != 4);
return 0;
}
void push() {
int data;
struct node* pointer = (struct node*)malloc(sizeof(struct node));
if (pointer == NULL) printf("Stack overflow\n");
else {
printf("Enter the element to be inserted: ");
scanf("%d", &data);
if (temp == NULL) {
pointer->data = data;
pointer->next = NULL;
temp = pointer;
} else {
pointer->data = data;
pointer->next = temp;
temp = pointer;
}
}
}
void pop() {
int item;
struct node* pointer;
if (temp == NULL) {
printf("Stack underflow\n");
}
else {
item = temp->data;
pointer = temp;
temp = temp->next;
free(pointer);
printf("The deleted item is %d\n", item);
}
}
void display() {
struct node* pointer;
pointer = temp;
if (pointer == NULL) {
printf("Stack underflow\n");
}
else {
printf("The elements of the stack are:\n");
while (pointer != NULL) {
printf("%d\n", pointer->data);
pointer = pointer->next;
}
}
}https://www.programiz.com/dsa/queue
https://www.scaler.com/topics/data-structures/queue-in-data-structure/
- Queue follows the First In First Out (FIFO) rule - the item that goes in first is the item that comes out first.
- Enqueue: Add an element to the end of the queue
- Dequeue: Remove an element from the front of the queue
- IsEmpty: Check if the queue is empty
- IsFull: Check if the queue is full
- Peek: Get the value of the front of the queue without removing it
How does it work?
-
Queue operations work as follows:
- two pointers FRONT and REAR
- FRONT track the first element of the queue
- REAR track the last element of the queue
- initially, set value of FRONT and REAR to -1
#include <stdio.h>
#include <stdlib.h>
typedef struct Queue {
int* items; // mảng để lưu giá trị của từng ô
int size;
int front, rear; // để xác định phần tử nào đứng đầu, phần tử nào đứng cuối hàng
} Queue;
void initialize(Queue *queue, int size) {
queue->items = (int*) malloc(sizeof(int)* size);
queue->front = -1;
queue->rear = -1;
queue->size = size;
}
int is_empty(Queue queue) {
return queue.front == -1;
}
int is_full(Queue queue) {
return (queue.rear + 1) % queue.size == queue.front;
}
void enqueue(Queue *queue, int value) {
if (!is_full(*queue)) {
if (is_empty(*queue)) {
queue->front = queue->rear = 0;
} else {
queue->rear = (queue->rear + 1) % queue->size;
}
queue->items[queue->rear] = value;
} else {
printf("Queue overflow\n");
}
}
int dequeue(Queue *queue) {
if (!is_empty(*queue)) {
int dequeued_value = queue->items[queue->front];
if (queue->front == queue->rear) {
queue->front = queue->rear = -1;
} else {
queue->front = (queue->front + 1) % queue->size;
}
return dequeued_value;
} else {
printf("Queue underflow\n");
return -1;
}
}
int front(Queue queue) {
if (!is_empty(queue)) {
return queue.items[queue.front];
} else {
printf("Queue is empty\n");
return -1;
}
}
int main() {
Queue queue;
initialize(&queue, 3);
enqueue(&queue, 10);
enqueue(&queue, 20);
enqueue(&queue, 30);
printf("Front element: %d\n", front(queue));
printf("Dequeue element: %d\n", dequeue(&queue));
printf("Dequeue element: %d\n", dequeue(&queue));
printf("Front element: %d\n", front(queue));
enqueue(&queue, 40);
enqueue(&queue, 50);
printf("Dequeue element: %d\n", dequeue(&queue));
printf("Front element: %d\n", front(queue));
return 0;
}How does Binary Search work
#include <stdio.h>
int binarySearch(int arr[], int l, int r, int x) {
while (l <= r) {
int m = l + (r - l) / 2;
if (arr[m] == x) return m; // If found at mid, then return it
if (arr[m] < x) l = m + 1; // If x greater, ignore left half
else r = m - 1; // If x is smaller, ignore right half
}
// If we reach here, then element was not present
return -1;
}
int main(void) {
int arr[] = { 2, 3, 4, 10, 40 };
int n = sizeof(arr) / sizeof(arr[0]);
int x = 10;
int result = binarySearch(arr, 0, n - 1, x);
(result == -1) ? printf("Element is not present in array")
: printf("Element is present at index %d", result);
return 0;
}#include <stdio.h>
int binarySearch(int array[], int x, int low, int high) {
if (high >= low) {
int mid = low + (high - low) / 2;
if (array[mid] == x) return mid; // If found at mid, then return it
if (array[mid] > x) return binarySearch(array, x, low, mid - 1); // Search the left half
return binarySearch(array, x, mid + 1, high); // Search the right half
}
return -1;
}
int main(void) {
int array[] = {3, 4, 5, 6, 7, 8, 9};
int n = sizeof(array) / sizeof(array[0]);
int x = 4;
int result = binarySearch(array, x, 0, n - 1);
if (result == -1) printf("Not found");
else printf("Element is found at index %d", result);
}#include <stdio.h>
#include <stdlib.h>
int binarySearch(int* arr, int l, int r, int x) {
if (r >= l) {
int mid = l + (r - l) / 2;
if (arr[mid] == x) return mid;
if (arr[mid] > x) return binarySearch(arr, l, mid - 1, x);
return binarySearch(arr, mid + 1, r, x);
}
return -1;
}
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
void bubbleSort(int arr[], int n) {
int i, j;
for (i = 0; i < n - 1; i++) {
for (j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1])
swap(&arr[j], &arr[j + 1]);
}
}
}
int main() {
int n, x, i;
printf("Nhap so phan tu cua mang: "); scanf("%d", &n);
int* arr = (int*)malloc(n * sizeof(int));
printf("Nhap cac phan tu cua mang: ");
for (i = 0; i < n; i++) { scanf("%d", &arr[i]); }
bubbleSort(arr, n);
for (int i = 0; i < n; i++) { printf("i = %d\n", arr[i]); }
printf("Nhap gia tri can tim: "); scanf("%d", &x);
int result = binarySearch(arr, 0, n - 1, x);
if (result == -1) printf("Khong tim thay %d trong mang.\n", x);
else printf("Tim thay %d tai vi tri %d trong mang.\n", x, result);
free(arr);
return 0;
}https://www.geeksforgeeks.org/basics-file-handling-c/
https://www.programiz.com/c-programming/c-file-input-output#google_vignette
- Classes are an expanded concept of data structures: like data structures, they can contain data members, but they can also contain functions as members.
- An object is an instantiation of a class. In terms of variables, a class would be the type, and an object would be the variable.
- Classes are defined using either keyword class or keyword struct, with the following syntax:
class class_name { access_specifier_1: member1; access_specifier_2: member2; ... } object_names;
- Where class_name is a valid identifier for the class, object_names is an optional list of names for objects of this class. The body of the declaration can contain members, which can either be data or function declarations, and optionally access specifiers.
- Classes have the same format as plain data structures, except that they can also include functions and have these new things called access specifiers. An access specifier is one of the following three keywords: private, public or protected. These specifiers modify the access rights for the members that follow them:
- private members of a class are accessible only from within other members of the same class (or from their "friends").
- protected members are accessible from other members of the same class (or from their "friends"), but also from members of their derived classes.
- public members are accessible from anywhere where the object is visible.
- By default, all members of a class declared with the class keyword have private access for all its members. Therefore, any member that is declared before any other access specifier has private access automatically.
- Ex:
class Rectangle { int width, height; public: void set_values (int,int); int area (void); } rect;
- Declares a class (i.e., a type) called Rectangle and an object (i.e., a variable) of this class, called rect.
- This class contains four members:
- two data members of type int (member width and member height) with private access (because private is the default access level)
- two member functions with public access: the functions set_values and area, of which for now we have only included their declaration, but not their definition.
- Notice the difference between the class name and the object name:
- Rectangle was the class name (i.e., the type)
- rect was an object of type Rectangle.
- After the declarations of Rectangle and rect, any of the public members of object rect can be accessed as if they were normal functions or normal variables, by simply inserting a dot (.) between object name and member name. This follows the same syntax as accessing the members of plain data structures. For example:
rect.set_values (3,4); myarea = rect.area();
- The only members of rect that cannot be accessed from outside the class are width and height, since they have private access and they can only be referred to from within other members of that same class.
- Here is the complete example of class Rectangle:
Input
// classes example
#include <iostream>
using namespace std;
class Rectangle {
int width, height;
public:
void set_values (int,int);
int area() {return width*height;}
};
void Rectangle::set_values (int x, int y) {
width = x;
height = y;
}
int main () {
Rectangle rect;
rect.set_values (3,4);
cout << "area: " << rect.area();
return 0;
}Output area: 12
- This example reintroduces the scope operator (::, two colons), seen in earlier chapters in relation to namespaces. Here it is used in the definition of function set_values to define a member of a class outside the class itself.
- Notice that the definition of the member function area has been included directly within the definition of class Rectangle given its extreme simplicity. Conversely, set_values it is merely declared with its prototype within the class, but its definition is outside it. In this outside definition, the operator of scope (::) is used to specify that the function being defined is a member of the class Rectangle and not a regular non-member function.
- The scope operator (::) specifies the class to which the member being defined belongs, granting exactly the same scope properties as if this function definition was directly included within the class definition. For example, the function set_values in the previous example has access to the variables width and height, which are private members of class Rectangle, and thus only accessible from other members of the class, such as this.
- The only difference between defining a member function completely within the class definition or to just include its declaration in the function and define it later outside the class, is that in the first case the function is automatically considered an inline member function by the compiler, while in the second it is a normal (not-inline) class member function. This causes no differences in behavior, but only on possible compiler optimizations.
- Members width and height have private access (remember that if nothing else is specified, all members of a class defined with keyword class have private access). By declaring them private, access from outside the class is not allowed. This makes sense, since we have already defined a member function to set values for those members within the object: the member function set_values. Therefore, the rest of the program does not need to have direct access to them. Perhaps in a so simple example as this, it is difficult to see how restricting access to these variables may be useful, but in greater projects it may be very important that values cannot be modified in an unexpected way (unexpected from the point of view of the object).
- The most important property of a class is that it is a type, and as such, we can declare multiple objects of it. For example, following with the previous example of class Rectangle, we could have declared the object rectb in addition to object rect:
Input
// example: one class, two objects
#include <iostream>
using namespace std;
class Rectangle {
int width, height;
public:
void set_values (int,int);
int area () {return width*height;}
};
void Rectangle::set_values (int x, int y) {
width = x;
height = y;
}
int main () {
Rectangle rect, rectb;
rect.set_values (3,4);
rectb.set_values (5,6);
cout << "rect area: " << rect.area() << endl;
cout << "rectb area: " << rectb.area() << endl;
return 0;
}Output
rect area: 12
rectb area: 30
- In this particular case, the class (type of the objects) is Rectangle, of which there are two instances (i.e., objects): rect and rectb. Each one of them has its own member variables and member functions.
- Notice that the call to rect.area() does not give the same result as the call to rectb.area(). This is because each object of class Rectangle has its own variables width and height, as they -in some way- have also their own function members set_value and area that operate on the object's own member variables.
- Classes allow programming using object-oriented paradigms: Data and functions are both members of the object, reducing the need to pass and carry handlers or other state variables as arguments to functions, because they are part of the object whose member is called. Notice that no arguments were passed on the calls to rect.area or rectb.area. Those member functions directly used the data members of their respective objects rect and rectb.
https://www.w3schools.com/cpp/cpp_constructors.asp
https://www.w3schools.com/cpp/cpp_constructors_overloading.asp
- What would happen in the previous example if we called the member function area before having called set_values? An undetermined result, since the members width and height had never been assigned a value.
- In order to avoid that, a class can include a special function called its constructor, which is automatically called whenever a new object of this class is created, allowing the class to initialize member variables or allocate storage.
- This constructor function is declared just like a regular member function, but with a name that matches the class name and without any return type; not even void.
- The Rectangle class above can easily be improved by implementing a constructor:
Input
// example: class constructor
#include <iostream>
using namespace std;
class Rectangle {
int width, height;
public:
Rectangle (int,int);
int area () {return (width*height);}
};
Rectangle::Rectangle (int a, int b) {
width = a;
height = b;
}
int main () {
Rectangle rect (3,4);
Rectangle rectb (5,6);
cout << "rect area: " << rect.area() << endl;
cout << "rectb area: " << rectb.area() << endl;
return 0;
}Output
rect area: 12
rectb area: 30
-
The results of this example are identical to those of the previous example. But now, class Rectangle has no member function set_values, and has instead a constructor that performs a similar action: it initializes the values of width and height with the arguments passed to it.
-
Notice how these arguments are passed to the constructor at the moment at which the objects of this class are created:
Rectangle rect (3,4); Rectangle rectb (5,6); -
Constructors cannot be called explicitly as if they were regular member functions. They are only executed once, when a new object of that class is created.
-
Notice how neither the constructor prototype declaration (within the class) nor the latter constructor definition, have return values; not even void: Constructors never return values, they simply initialize the object.
- Like any other function, a constructor can also be overloaded with different versions taking different parameters: with a different number of parameters and/or parameters of different types. The compiler will automatically call the one whose parameters match the arguments:
Input
// overloading class constructors
#include <iostream>
using namespace std;
class Rectangle {
int width, height;
public:
Rectangle ();
Rectangle (int,int);
int area (void) {return (width*height);}
};
Rectangle::Rectangle () {
width = 5;
height = 5;
}
Rectangle::Rectangle (int a, int b) {
width = a;
height = b;
}
int main () {
Rectangle rect (3,4);
Rectangle rectb;
cout << "rect area: " << rect.area() << endl;
cout << "rectb area: " << rectb.area() << endl;
return 0;
}Output
rect area: 12
rectb area: 25
-
In the above example, two objects of class Rectangle are constructed: rect and rectb. rect is constructed with two arguments, like in the example before.
-
But this example also introduces a special kind constructor: the default constructor. The default constructor is the constructor that takes no parameters, and it is special because it is called when an object is declared but is not initialized with any arguments. In the example above, the default constructor is called for rectb. Note how rectb is not even constructed with an empty set of parentheses - in fact, empty parentheses cannot be used to call the default constructor:
Rectangle rectb; // ok, default constructor called Rectangle rectc(); // oops, default constructor NOT called -
This is because the empty set of parentheses would make of rectc a function declaration instead of an object declaration: It would be a function that takes no arguments and returns a value of type Rectangle.
https://www.geeksforgeeks.org/cpp/object-oriented-programming-in-cpp/
#include <iostream>
#include <string>
using namespace std;
class Student {
private:
string Name;
double GPA;
int StudentID;
public:
Student(string name) {
Name = name;
static int id = 1000;
StudentID = id;
++id;
}
string getName() { return Name; }
double getGPA() { return GPA; }
int getID() { return StudentID; }
void setName(string name) { Name = name; };
void setGPA(double gpa) { GPA = gpa; };
void setStudentID(int studentID) { StudentID = studentID; }
};
int main() {
Student student1("Trung"); student1.setGPA(8);
Student student2("Thai"); student2.setGPA(9);
Student student3("Thao"); student3.setGPA(10);
cout << "Name: " << student1.getName() << "\tGPA: " << student1.getGPA() << "\tID: " << student1.getID() << endl;
cout << "Name: " << student2.getName() << "\tGPA: " << student2.getGPA() << "\tID: " << student2.getID() << endl;
cout << "Name: " << student3.getName() << "\tGPA: " << student3.getGPA() << "\tID: " << student3.getID() << endl;
return 0;
}#include <iostream>
#include <string>
using namespace std;
class Person {
protected:
string Name;
int Age;
string Home_Address;
public:
string getName() { return Name; }
void setName(string name) { Name = name; }
int getAge() { return Age; }
void setAge(int age) { Age = age; }
string getAddress() { return Home_Address; }
void setAddress(string address) { Home_Address = address; }
void displayInfo() {
cout << "Name: " << Name << "\tAge: " << Age << "\tAddress: " << Home_Address << endl;
}
};
class Student : public Person {
private:
string School_Name;
double GPA;
int StudentID;
public:
Student() {
static int id = 1000;
StudentID = id;
++id;
}
string getSchoolName() { return School_Name; }
void setSchoolName(string school_name) { School_Name = school_name; }
double getGPA() { return GPA; }
void setGPA(double gpa) { GPA = gpa; }
int getID() { return StudentID; }
void displayInfo() { // overriding
cout << "Name: " << Name << "\tAge: " << Age << "\tAddress: " << Home_Address << "\tSchool name: " << School_Name << "\tGPA: " << GPA << endl;
}
};
int main() {
Person person1;
person1.setName("Trung");
person1.setAge(20);
person1.setAddress("HCM");
person1.displayInfo();
Student student1;
student1.setName("Trunggg");
student1.setAge(24);
student1.setAddress("HCMM");
student1.setGPA(8.1);
student1.setSchoolName("DinhTienHoang");
student1.displayInfo();
return 0;
}Input
#include <iostream>
#include <string>
using namespace std;
class Calc {
public:
int sum(int a, int b) {
return a+b;
}
int sum(int a, int b, int c) {
return a+b+c;
}
double sum(double a, double b) {
return a+b;
}
};
int main() {
Calc* ptr = new Calc();
cout << "Sum: " << ptr->sum(1,2) << endl;
cout << "Sum: " << ptr->sum(2.5,3.7) << endl;
cout << "Sum: " << ptr->sum(1,2,3) << endl;
return 0;
}Output
Sum: 3
Sum: 6.2
Sum: 6
Input
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
void show() { cout << "hello person" << endl; }
virtual void displayInfo() { cout << "HELLO PERSON"<< endl; } // thêm virtual vào chỗ này
};
class Student : public Person {
public:
void show() { cout << "hello student" << endl; }
void displayInfo() { cout << "HELLO STUDENT"<< endl; }
};
int main() {
Person *ptr = NULL;
Person person1;
Student student1;
ptr = &person1;
ptr->show();
ptr->displayInfo();
cout << "-----------------------" << endl;
ptr = &student1;
ptr->show();
ptr->displayInfo();
return 0;
}Output
hello person
HELLO PERSON
hello person
HELLO STUDENT
Input
#include <iostream>
#include <string>
using namespace std;
class Parent {
public:
virtual string test() { return "Hello Parent"; } // thêm virtual vào chỗ này
void displayInfo() {cout << test() << endl; }
};
class Baby : public Parent {
public:
string test() { return "Hello Baby"; }
};
int main() {
Parent parent;
parent.displayInfo();
cout << "-----------------------" << endl;
Baby baby;
baby.displayInfo();
return 0;
}Output
Hello Parent
-----------------------
Hello Baby
Input
#include <iostream>
#include <string>
using namespace std;
class Buffalo {
public:
virtual void action(){cout << "Hello Buffalo\n";}; // thêm virtual vào chỗ này
};
class YoungBuffalo : public Buffalo {
public:
void action(){ cout << "Hello Young Buffalo\n";};
};
void takeAnBuffalo(Buffalo* buffalo){
buffalo->action();
}
int main() {
Buffalo *buffalo = new Buffalo();
Buffalo *youngBuffalo = new YoungBuffalo();
takeAnBuffalo(buffalo);
cout << "-----------------------" << endl;
takeAnBuffalo(youngBuffalo);
}Output
Hello Buffalo
-----------------------
Hello Young Buffalo
Input
#include <iostream>
#include <string>
#include <cmath>
using namespace std;
class GiaiPhuongTrinh {
private:
double a, b, c;
int x1, x2;
double delta;
void tinhNghiem() {
delta = b*b - 4*a*c;
if (delta < 0) {
delta = -1;
}
else if (delta == 0) {
x1 = x2 = -b/ (2*a);
}
else if (delta > 0) {
delta = 1;
x1 = (-b + sqrt(delta))/(2*a);
x2 = (-b - sqrt(delta))/(2*a);
}
}
public:
void enterNumber(double num_a, double num_b, double num_c);
void printResult();
};
void GiaiPhuongTrinh::enterNumber(double num_a, double num_b, double num_c) {
a = num_a;
b = num_b;
c = num_c;
}
void GiaiPhuongTrinh::printResult() {
tinhNghiem();
switch ((int)delta) {
case -1:
cout << "PT vo nghiem" << endl;
break;
case 0:
cout << "PT co nghiem kep: " << x1 << endl;
break;
default:
cout << "PT co 2 nghiem phan biet:" << endl;
cout << "x1 = " << x1 << "\tx2 = " << x2 << endl;
break;
}
}
int main() {
GiaiPhuongTrinh phuongtrinh1;
phuongtrinh1.enterNumber(1,-3,2);
phuongtrinh1.printResult();
return 0;
}Output
PT co 2 nghiem phan biet:
x1 = 2 x2 = 1
https://www.geeksforgeeks.org/vector-in-cpp-stl/
https://www.programiz.com/cpp-programming/vectors
https://topdev.vn/blog/vector-trong-c/
Vectors are the same as dynamic arrays with the ability to resize itself automatically when an element is inserted or deleted
1, Declaration and Initialization
vector<data_type> vector_name;
Ex: #include #include using namespace std;
int main() {
vector<int> vector1; // Khai báo vector rỗng
// Method 1: Create and Initialize a Vector
vector<int> vector2 = {1, 2, 3, 4, 5}; // Initializer list
vector<int> vector3 {1, 2, 3, 4, 5}; // Uniform initialization
// Method 2: Create a Vector with Size
int n = 20;
vector<int> v3(n);
int val = 10;
vector<int> v4(m, val); //Khai báo vector có sẵn m phần tử có cùng giá trị val
return 0;
}
2, C++ Vector Functions
a. Capacity__________________________________________________________________________________________________________________________
.size(): returns the number of elements present in the vector // kích thước - trả về số lượng phần tử ĐANG ĐƯỢC SỬ DỤNG trong vector
.capacity(): check the overall size of a vector // dung lượng - trả về số lượng phần tử ĐƯỢC CẤP PHÁT cho vector nằm TRONG BỘ NHỚ
.empty(): returns 1 (true) if the vector is empty
.resize(n): change the number of elements present in the vector
Ex:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> vec1;
for (int i = 1; i <= 10; i++)
vec1.push_back(i);
cout << "Size of our vector: " << vec1.size() << endl;
cout << "nCapacity of our vector: " << vec1.capacity() << endl;
cout << "nMax_Size of our vector: " << vec1.max_size() << endl;
// resizes the vector size to 4
vec1.resize(4);
// prints the vector size after resize()
cout << "nSize of our vector after resize: " << vec1.size() << endl;
// checks if the vector is empty or not
if (vec1.empty() == false) cout << "nVector is not empty";
else cout << "nVector is empty";
return 0;
}b. Access Elements of a Vector_______________________________________________________________________________________________________
.at(): access the element from the specified index
.front(): returns the first element of the vector
.back(): returns the last element of the vector
c. Add Elements to a Vector___________________________________________________________________________________________________________
.push_back(): This method pushes elements to the back of a vector.
vector<int> v = {1,2,3,4};
v.push_back(5);
int n = v.size();
cout << "The last element is: " << v[n - 1];
// Output: The last element is: 5
.insert(): This method inserts new elements before the element at the specified position.
vector<int> v = {1,2,3,4};
v.insert(v.begin(),0); // Insert at beginning
v.insert(v.end(),6); // Insert at end
cout << "The first element is: " << v[0] << "\n";
// Output: The first element is: 0
cout << "The last element is: " << v[5] << "\n";
// Output: The last element is: 6
Ex:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector <int> arr1 = {2,5,7,4,9};
arr1.at(0) = 3; // 3 5 7 4 9
arr1.resize(7); // 3 5 7 4 9 0 0
arr1.push_back(10); // 3 5 7 4 9 0 0 10
// Duyet vector bang chi so
for (int i = 0; i < arr1.size(); i++) {
cout << "Value: " << arr1.at(i) << endl;
}
cout << "-----------" << endl;
// Duyet vector bang ranged-base for loop
for (const int& i : arr1) {
cout << "Value: " << i << endl;
}
for (int i : arr1) {
cout << "Value: " << i << endl;
}
return 0;
}d. Delete Elements from C++ Vectors
.pop_back()
.erase()
.clear(): removes all the elements of the vector
Ex:
#include <iostream>
#include <vector>
using namespace std;
int main(){
vector<string> v = {"Da Nang", "Ngay 5"};
cout << "Size of vector : " << v.size() << endl;
v.push_back("Thang 2");
v.push_back("Nam 2024");
cout << "Size of vector : " << v.size() << endl;
cout << "Access vector : ";
for(int i = 0; i < v.size(); i++){
cout << v[i] << ", ";
}
v.pop_back();
cout << "\nSize of vector : " << v.size() << endl;
}3, C++ Vector Iterators Vector iterators are used to point to the memory address of a vector element. In some ways, they act like pointers in C++.
Syntax: vector<T>::iterator iteratorName;
a. begin() function
vector<int> num = {1, 2, 3};
vector<int>::iterator iter;
// iter points to num[0]
iter = num.begin();
b. end() function
vector<int> num = {1, 2, 3};
vector<int>::iterator iter;
// iter points to the last element of num
iter = num.end() - 1;
c. Ex
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> num {1, 2, 3, 4, 5};
vector<int>::iterator iter;
// use iterator with for loop
for (iter = num.begin(); iter != num.end(); ++iter) {
cout << *iter << " ";
}
return 0;
}
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> vec1;
for (int i = 1; i <= 10; i++)
vec1.push_back(i);
cout << "Understanding begin() and end() function: " << endl;
for (auto i = vec1.begin(); i != vec1.end(); ++i)
cout << *i << " ";
return 0;
}4, Vector Và Mảng 1 Chiều
#include <iostream>
#include <vector>
using namespace std;
int main(){
int n, tmp; cout << "Nhap so luong phan tu : ";
cin >> n;
vector<int> v(n);
for(int i = 0; i < n; i++){
cout << "Nhap phan tu thu " << i + 1 << " : ";
cin >> v[i];
}
cout << "Day so vua nhap : \n";
for(int i = 0; i < v.size(); i++){
cout << v[i] << " ";
}
return 0;
}5, Vector Và Mảng 2 Chiều
Cách 1:
#include <iostream>
#include <vector>
using namespace std;
int main(){
int n, m;
cout << "Nhap hang, cot : ";
cin >> n >> m;
vector<vector<int>> v;
for(int i = 0; i < n; i++){
vector<int> row;
for(int j = 0; j < m; j++){
cout << "Nhap phan tu hang " << i + 1 << ", cot " << j + 1 << " : ";
int tmp; cin >> tmp;
row.push_back(tmp);
}
v.push_back(row);
}
cout << "\nMang 2 chieu vua nhap : \n";
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
cout << v[i][j] << " ";
}
cout << endl;
}
return 0;
}Cách 2:
#include <iostream>
#include <vector>
using namespace std;
int main(){
int n, m;
cout << "Nhap hang, cot : ";
cin >> n >> m;
vector<vector<int>> v(n, vector<int>(m));
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
cout << "Nhap phan tu hang " << i + 1 << ", cot " << j + 1 << " : ";
cin >> v[i][j];
}
}
cout << "\nMang 2 chieu vua nhap : \n";
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
cout << v[i][j] << " ";
}
cout << endl;
}
return 0;
}https://www.programiz.com/cpp-programming/list
https://websitehcm.com/list-trong-c/
https://laptrinhcanban.com/cpp/lap-trinh-cpp-co-ban/list-trong-cpp/list-trong-cpp-la-gi/
https://www.startertutorials.com/blog/generic-programming-cpp.html
https://www.scaler.com/topics/cpp/generic-programming-in-cpp/
https://albertoferrari.github.io/generics/cpp_generic_programming.pdf
https://www.codeguru.com/cplusplus/exploring-the-c-generics-programming-model/
#include <iostream>
using namespace std;
// One function works for all data types.
// This would work even for user defined types
// if operator '>' is overloaded
template <typename T>
T myMax(T x, T y) {
return (x > y) ? x : y;
}
int main() {
cout << myMax<int>(3, 7) << endl; // call myMax for int
cout << myMax<double>(3.5, 7.5) << endl; // call myMax for double
cout << myMax<char>('g', 'e') << endl; // call myMax for char
return 0;
} #include <iostream>
using namespace std;
class Person {
private:
int age;
string name;
public:
Person(string _name, int _age) {
age = _age;
name = _name;
}
void toString() {
cout << name << " is " << age << " years old."<< endl;
}
};
template < class T >
void printTheData(T &obj) {
obj.toString();
}
int main() {
Person p1 = Person("Tommy Vercetti", 21);
printTheData(p1);
return 0;
}#include <iostream>
using namespace std;
template < class T >
class Container {
private:
T data;
public:
static int count;
Container() {
count++;
}
static void displayStaticVariable() {
cout << count << endl;
}
};
template < class T >
int Container < T > ::count = 0;
int main() {
Container < int > obj1;
Container < float > obj2;
Container < int > obj3;
Container < int > ::displayStaticVariable();
Container < float > ::displayStaticVariable();
return 0;
}#include <iostream>
using namespace std;
template <typename T>
class Array {
private:
T* ptr;
int size;
public:
Array(T arr[], int s);
void print();
};
template <typename T>
Array<T>::Array(T arr[], int s) {
ptr = new T[s];
size = s;
for (int i = 0; i < size; i++)
ptr[i] = arr[i];
}
template <typename T>
void Array<T>::print() {
for (int i = 0; i < size; i++)
cout << " " << *(ptr + i);
cout << endl;
}
int main() {
int arr[5] = { 1, 2, 3, 4, 5 };
Array<int> a(arr, 5);
a.print();
return 0;
} #include <iostream>
using namespace std;
template <typename E>class MyStack{
private:
int SIZE;
int tos;
E *items;
public:
MyStack(int=10);
~MyStack(){ delete[] items;}
void push(const E&);
E pop();
};
template<typename E>MyStack<E>::
MyStack(int s):SIZE(s>0?s:10),tos(-1),items(new E[SIZE]){}
template<typename E> void MyStack<E>::push(const E &value){
if (tos == SIZE - 1)
cout<<"Stack is full"<<endl;
else
items[++tos] = value;
}
template<typename E> E MyStack<E>::pop() {
E item;
if (tos == -1)
cout<<"Stack is empty"<<endl;
else
item = items[tos--];
return item;
}
int main() {
MyStack<string> strStack(4);
strStack.push("January");
strStack.push("February");
strStack.push("March");
strStack.push("April");
cout<<strStack.pop()<<endl;
cout<<strStack.pop()<<endl;
cout<<strStack.pop()<<endl;
cout<<strStack.pop()<<endl;
cout<<strStack.pop()<<endl; // stack is empty now
return 0;
}#include <iostream>
using namespace std;
template <class T, class U>
class A {
T x;
U y;
public:
A() {
cout << "Constructor Called" << endl;
}
};
int main() {
A<char, char> a;
A<int, double> b;
return 0;
}
https://www.scaler.com/topics/cpp/dynamic-memory-allocation-in-cpp/
https://www.geeksforgeeks.org/new-and-delete-operators-in-cpp-for-dynamic-memory/
https://daynhauhoc.com/t/cap-phat-dong-dynamic-memory-allocation/31145
+ Static memory allocation (Compile-time Memory Allocation) // Cấp phát bộ nhớ tĩnh
Xảy ra trên các biến tĩnh và biến toàn cục.
Vùng nhớ của các loại biến này được cấp phát một lần khi chương trình bắt đầu chạy và vẫn tồn tại trong suốt thời gian tồn tại của chương trình.
Kích thước của biến/mảng phải được biết tại thời điểm biên dịch chương trình.
+ Automatic memory allocation (Run-time Memory Allocation) // Cấp phát bộ nhớ tự động
Xảy ra trên các tham số hàm và biến cục bộ.
Vùng nhớ của các loại biến này được cấp phát khi chương trình đi vào khối lệnh và được giải phóng khi khối lệnh bị thoát.
Kích thước của biến/mảng phải được biết tại thời điểm biên dịch chương trình.
+ Dynamic memory allocation // Cấp phát bộ nhớ động
a. The "new" operator in C++
Syntax: data_type* ptr_var = new data_type;
b. The "delete" Operator in C++
Syntax: delete ptr_var;
#include <iostream>
using namespace std;
int main() {
// declare an int pointer
int* pointInt;
// declare a float pointer
float* pointFloat;
// dynamically allocate memory
pointInt = new int;
pointFloat = new float;
// assigning value to the memory
*pointInt = 45;
*pointFloat = 45.45f;
cout << *pointInt << endl;
cout << *pointFloat << endl;
// deallocate the memory
delete pointInt;
delete pointFloat;
return 0;
}// C++ Program to store GPA of n number of students and display it
// where n is the number of students entered by the user
#include <iostream>
using namespace std;
int main() {
int num;
cout << "Enter total number of students: ";
cin >> num;
float* ptr;
// memory allocation of num number of floats
ptr = new float[num];
cout << "Enter GPA of students." << endl;
for (int i = 0; i < num; ++i) {
cout << "Student" << i + 1 << ": ";
cin >> *(ptr + i);
}
cout << "\nDisplaying GPA of students." << endl;
for (int i = 0; i < num; ++i) {
cout << "Student" << i + 1 << ": " << *(ptr + i) << endl;
}
// ptr memory is released
delete[] ptr;
return 0;
}#include <iostream>
using namespace std;
class Student {
private:
int age;
public:
// constructor initializes age to 12
Student() : age(12) {}
void getAge() {
cout << "Age = " << age << endl;
}
};
int main() {
// dynamically declare Student object
Student* ptr = new Student();
// call getAge() function
ptr->getAge();
// ptr memory is released
delete ptr;
return 0;
}https://www.geeksforgeeks.org/smart-pointers-cpp/
https://www.geeksforgeeks.org/auto_ptr-unique_ptr-shared_ptr-weak_ptr-in-cpp/
https://viblo.asia/p/cc-smart-pointer-MG24BKzBJz3
https://viblo.asia/p/tim-hieu-smartpointer-trong-c-phan-1-4dbZND8Q5YM
#include <iostream>
using namespace std;
class SmartPtr {
private:
int* ptr;
public:
SmartPtr(int* p = NULL) { ptr = p; }
~SmartPtr() { delete (ptr); }
int& operator*() { return *ptr; } // Overloading dereferencing operator
int getValue() { return *ptr; }
void setValue(int value) { *ptr = value; }
};
int main() {
SmartPtr ptr1(new int);
ptr1.setValue(50);
cout << "Value: " << ptr1.getValue() << endl;
cout << "Value: " << *ptr1 << endl;
cout << "_____________________________" << endl;
SmartPtr ptr2(new int);
*ptr2 = 20;
cout << "Value: " << ptr2.getValue() << endl;
cout << "Value: " << *ptr2 << endl;
return 0;
}#include <iostream>
#include <memory>
using namespace std;
template < typename T >
class HinhChuNhat {
private:
T ChieuDai, ChieuRong;
public:
HinhChuNhat(T dai, T rong){
ChieuDai = dai;
ChieuRong = rong;
cout << "Constructor called. " << endl;
}
void tinhDienTich() {
cout << "Dien tich: " << ChieuDai * ChieuRong << endl;
}
~HinhChuNhat() {
cout << "Destructor called " << endl;
}
};
int main() {
unique_ptr <HinhChuNhat<float>> ptr1(new HinhChuNhat<float>(10.5,5.2));
(*ptr1).tinhDienTich(); // Dien tich: 54.6
//unique_ptr <HinhChuNhat> ptr2(ptr1); // Khong cho phep vì giát trị ptr1 thực chất là địa chỉ của object HinhChuNhat<float>(10.5,5.2) mà mỗi unique ptr chỉ đc trỏ đến 1 object
unique_ptr <HinhChuNhat<float>> ptr2 = move(ptr1); // gan object HinhChuNhat(10,5) cho ptr2, sau do remove ptr1
(*ptr2).tinhDienTich(); // Dien tich: 54.6
// (*ptr1).tinhDienTich(); -> lỗi, 1 unique chỉ đi 1 object, ptr1 đã bị remove
return 0;
}#include <iostream>
#include <memory>
using namespace std;
class HinhChuNhat {
private:
int ChieuDai;
int ChieuRong;
public:
HinhChuNhat(int dai, int rong){
ChieuDai = dai;
ChieuRong = rong;
cout << "Constructor called. " << endl;
}
void tinhDienTich() {
cout << "Dien tich: " << ChieuDai * ChieuRong << endl;
}
~HinhChuNhat() {
cout << "Destructor called " << endl;
}
};
int main() {
shared_ptr <HinhChuNhat> ptr1 (new HinhChuNhat(40,10));
(*ptr1).tinhDienTich();
shared_ptr <HinhChuNhat> ptr2 (ptr1);
shared_ptr <HinhChuNhat> ptr3;
ptr3 = ptr2;
(*ptr2).tinhDienTich();
(*ptr1).tinhDienTich();
(*ptr3).tinhDienTich();
cout << "Count: " << ptr1.use_count() << endl; // số con trỏ, trỏ đến địa chỉ object HinhChuNhat(40,10)
cout << "Count: " << ptr2.use_count() << endl; // số con trỏ, trỏ đến địa chỉ object HinhChuNhat(40,10)
cout << "Count: " << ptr3.use_count() << endl; // số con trỏ, trỏ đến địa chỉ object HinhChuNhat(40,10)
ptr3.reset();
// (*ptr3).tinhDienTich(); -> không hợp lệ, ctr bị lỗi
cout << "Count: " << ptr1.use_count() << endl;
cout << "Count: " << ptr3.use_count() << endl; // ptr3 đang không trỏ đến 1 object nào cả
return 0;
}#include <iostream>
#include <memory>
using namespace std;
class HinhChuNhat {
private:
int ChieuDai;
int ChieuRong;
public:
HinhChuNhat(int dai, int rong){
ChieuDai = dai;
ChieuRong = rong;
cout << "Constructor called. " << endl;
}
void tinhDienTich() {
cout << "Dien tich: " << ChieuDai * ChieuRong << endl;
}
~HinhChuNhat() {
cout << "Destructor called " << endl;
}
};
int main() {
shared_ptr <HinhChuNhat> ptr1 (new HinhChuNhat(40,10));
shared_ptr <HinhChuNhat> ptr2(ptr1);
weak_ptr <HinhChuNhat> ptr3 (ptr1);
weak_ptr <HinhChuNhat> ptr4 (ptr2);
// ________________________________________________________________________________________________________________________________________
cout << "\nCase 1" << endl; // Khi chưa reset, giá trị của ptr1 và ptr2 vẫn còn.
if (auto ptr_lock = ptr3.lock()) ptr_lock->tinhDienTich(); // diện tích = 400
else cout << "Object has been deallocated" << endl;
if (auto ptr_lock = ptr4.lock()) ptr_lock->tinhDienTich(); // diện tích = 400
else cout << "Object has been deallocated" << endl;
cout << "Count: " <<ptr1.use_count() << endl;
cout << "Count: " <<ptr2.use_count() << endl;
// ________________________________________________________________________________________________________________________________________
cout << "\nCase 2" << endl; // Khi chỉ reset giá trị ptr1 -> vẫn còn tồn tại 1 share pointer là ptr2 trỏ đến object -> lock() vẫn sẽ trả về một shared_ptr hợp lệ có thể sử dụng để truy cập đối tượng.
ptr1.reset();
if (auto ptr_lock = ptr3.lock()) ptr_lock->tinhDienTich(); // diện tích = 400
else cout << "Object has been deallocated" << endl;
cout << "Count: " <<ptr1.use_count() << endl; // tuy nhiên count ở đây đã = 0
cout << "Count: " <<ptr2.use_count() << endl;
// ________________________________________________________________________________________________________________________________________
cout << "\nCase 3 " << endl; // Cả ptr1 và ptr2 đã bị reset-> shared_ptr đã bị giải phóng -> lock() sẽ trả về một shared_ptr rỗng.
ptr2.reset();
if (auto ptr_lock = ptr4.lock()) ptr_lock->tinhDienTich();
else cout << "Object has been deallocated" << endl; // "Object has been deallocated"
cout << "Count: " <<ptr1.use_count() << endl; // count = 0
cout << "Count: " <<ptr2.use_count() << endl; // count = 0
return 0;
}#include <iostream>
#include <functional>
#define PI 3.14
using namespace std;
void processFunction(int a, int b, const function<void(int, int)>& func) {
cout << "Processing numbers: " << a << " and " << b << endl;
func(a, b);
}
int main() {
int a = 10;
const double g = 9.8;
processFunction(7, 9, [a](int x, int y) {
cout << "Product: " << x * y + a << endl;
});
processFunction(7, 9, [g](int x, int y) {
cout << "Product: " << x + y + g << endl;
});
processFunction(7, 9, [](int x, int y) {
cout << "Product: " << x - y + PI << endl;
});
return 0;
}https://viblo.asia/p/lam-quen-voi-multithreading-trong-c-qm6RWQYXGeJE
https://viblo.asia/p/lam-quen-voi-multithreading-p2-AQ3vVka1RbOr
https://viblo.asia/p/concurrency-in-c11-LzD5dOLzljY
https://toilangthangit.wordpress.com/
https://zalopay-oss.github.io/go-advanced/ch1-basic/ch1-05-concurrency-parallelism.html
- Thời gian đầu, CPU chỉ có một nhân duy nhất, các ngôn ngữ khi đó sẽ theo mô hình lập trình tuần tự.
- Xử lý tuần tự là khả năng xử lý chỉ một tác vụ trong một khoảng thời gian, các tác vụ sẽ được thực thi theo thứ tự hết tác vụ này sẽ thực thi tiếp tác vụ khác.
- Dưới đây là ví dụ cho mô hình xử lý tuần tự (sequential processing):
- Ngày nay, với sự phát triển của công nghệ, mô hình lập trình đồng thời Concurrency đối với các hệ thống lõi đơn và mô hình lập trình song song Parallelism đối với các hệ thống đa lõi và đa xử lý ngày càng phổ biến với những ưu điểm nổi bật.
- Đối với hệ thống lõi đơn(máy tính có một processor với một unit/core) thì ĐA LUỒNG (Multithreading) thực chất xử lý task tuần tự.
- Bản chất tại một thời điểm nhân CPU chỉ có thể xử lý một tác vụ, vậy làm sao 1 máy tính có CPU 1 nhân có thể làm được việc xử lý đồng thời nhiều tác vụ của các tiến trình cùng lúc.
- Như câu nói của Rob Pike: Concurrency is about dealing with lots of things at once-Rob Pike, ông đã sử dụng từ dealing (phân chia xử lý) để nói đến khái niệm concurrency.
- Thật như vậy, nhân CPU không bao giờ đợi xử lý xong một tác vụ rồi mới xử lý tiếp tác vụ khác, mà nhân CPU đã chia các tác vụ lớn thành các tác vụ nhỏ hơn và sắp xếp xen kẽ lẫn nhau. Nhân CPU xẽ tận dụng thời gian rảnh của tác vụ này để đi làm tác vụ khác, một lúc thì làm tác vụ nhỏ này, một lúc khác thì làm tác vụ nhỏ khác. Việc này được gọi là task switching. Như vậy chúng ta sẽ cảm thấy máy tính xử lý nhiều việc cùng lúc tại cùng thời điểm. Nhưng bản chất bên dưới nhân CPU thì nó chỉ có thể thực thi một tác vụ nhỏ trong tác vụ lớn tại thời điểm đó.
- Dưới đây là ví dụ về việc chia nhỏ tác vụ và xử lý trong mô hình concurrency
Trong đó, màu đỏ: task 1; màu xanh: task 2; màu xám: context switch
Context switch: Hệ điều hành sẽ lưu trạng thái CPU và instruction pointer của task hiện tại; tìm task sẽ được thực hiện tiếp; load trạng thái CPU cho việc thực hiện task tiếp theo)
- Đối với hệ thống đa lõi và đa xử lý (máy tính có nhiều processor | máy tính có nhiều core trong một processors) thì ĐA LUỒNG (Multithreading) thực chất là các LUỒNG (thread) xử lý song song các task khác nhau cùng một lúc, các tác vụ này hoàn toàn độc lập với nhau trên lõi hoặc bộ vi xử lý khác nhau. Thay vì một nhân CPU chúng ta chỉ có thể xử lý một tác vụ nhỏ tại một thời điểm thì khi số nhân CPU có nhiều hơn chúng ta có thể xử lý các tác vụ song song với nhau cùng lúc trên các nhân CPU.
- Dưới đây là ví dụ cho mô hình xử lý song song các tác vụ cùng một thời điểm.
- Trong thực tế, trên mỗi nhân của CPU vẫn xảy ra quá trình xử lý đồng thời miễn là tại một thời điểm không có xảy ra việc xử lý cùng một tác vụ trên hai nhân CPU khác nhau, mô hình trên vẽ lại như sau:
- Ở multiprocessing, sẽ có nhiều process (tiến trình). Mỗi tiến trình chỉ có một thread (luồng).
- Các process giao tiếp với nhau thông qua Interprocess Communication (signals, sockets, files, pipes, message queues, …).
- Dưới đây là ví dụ cho việc giao tiếp giữa 2 process chạy đồng thời
- Ở multithreading, một process chứa hai hay nhiều thread. Việc giao tiếp giữa các thread này sẽ qua Shared memory.
- Dưới đây là ví dụ cho việc giao tiếp giữa 2 thread qua shared memory
- Lợi ích của Multithreading trong C++ là:
- Tăng hiệu năng và thời gian thực thi.
- Tận dụng tài nguyên hệ thống: Bằng cách sử dụng multithreading, chương trình có thể tận dụng được nhiều bộ xử lý (cores) của máy tính. Mỗi luồng (thread) có thể chạy trên một bộ xử lý riêng, đồng thời thực hiện các tác vụ độc lập. Điều này giúp tối ưu hóa sử dụng tài nguyên hệ thống và đảm bảo rằng máy tính hoạt động ở mức tối đa.
- Xử lý đồng thời các tác vụ độc lập: Multithreading cho phép chương trình chạy đồng thời các tác vụ độc lập nhau. Điều này rất hữu ích trong các tình huống cần xử lý song song các công việc như xử lý dữ liệu đồng thời, phản hồi các sự kiện đồng thời, hoặc thực hiện các tác vụ background trong khi chương trình vẫn tiếp tục hoạt động.
- Tăng tính tương tác và sự phản hồi của ứng dụng: Multithreading cho phép chương trình chạy các tác vụ không liên quan song song với nhau. Điều này tạo ra một trải nghiệm tương tác mượt mà hơn, giúp người dùng cảm nhận được sự phản hồi nhanh chóng của ứng dụng và không bị gián đoạn.
ThreadsRace conditionsMutexesDeadlockCondition variablesAsynchonous tasksAtomicity
- Mỗi chương trình C++ đều có tối thiểu một thread, nó được chạy trong runtime: thread đó là main().
- Sau đó chương trình có thể khởi chạy các thread khác nữa với từng tác vụ nhất định phục vụ cho một mục đích nào đó.
- Những thread này chạy đồng bộ với các thread khác cũng như thread được khỏi tạo ban đầu. Khi tác vụ của một thread chạy xong và trả về một kết quả nào đó, thread đó sẽ kết thúc; tương tự như chương trình sẽ kết thúc khi chương trình nhận giá trị trả về từ main().
- Như các bạn cũng đã biết, nếu chúng ta có một đối tượng std::thread cho một thread nào đó, chúng ta cần đợi cho đến khi nó chạy xong.
- Nhưng trước khi việc đó xảy ra, mình cần làm là khởi tạo một thread.
// Đầu tiên include header thread
#include <thread>
// Khi muốn khởi tạo 1 thread, tạo 1 thread object:
thread t_empty;- Hàm khởi tạo mặc định của thread class được sử dụng. Chúng ta không chuyền bất cứ 1 thông tin nào vào thread. Tức là không có gì được chạy trong thread này.
- Chúng ta phải khởi tạo thread. Nó có thể được hoàn thành bằng cách khác. Khi bạn tạo 1 thread, bạn có thể truyền 1 con trỏ hàm vào khởi tạo của nó -> 1 thread được khởi tạo, function sẽ bắt đầu chạy, nó chạy trong 1 thread riêng biệt
#include <iostream>
#include <thread>
using namespace std;
void threadFunc() {
cout << "Welcome to Multithreading" << endl;
}
int main() {
//truyền 1 function tới thread
thread funcTest1(threadFunc);
}- Việc biên dịch đoạn code trên không có vấn đề gì, tuy nhiên bạn sẽ lập tức đối mặt với 1 runtime error
- Lý do là: Main thread tạo 1 thread mới là funcTest1 với paramaters threadFunc. Main thread không đợi funcTes1 huỷ, nó tiếp tục hoạt động và sẽ kết thúc, nhưng funcTest vẫn chạy -> Nó sẽ dẫn đễn lỗi. TẤT CẢ CÁC THREAD PHẢI HUỶ TRƯỚC KHI MAIN THREAD HUỶ. Vậy làm các nào khắc phục vấn đề này ?
- Thread class cung cấp method join(), hàm này chỉ return khi tất cả các thread kết thúc, điều đó có nghĩa là main thread sẽ đợi đến khi tất cả các thread con hoàn thành công việc của nó.
Ex 1
#include <iostream>
#include <thread>
using namespace std;
void threadFunc() {
cout << "Welcome to Multithreading" << endl;
}
int main() {
//truyền 1 function tới thread
thread funcTest1(threadFunc);
//main sẽ block đến khi funcTest1 kết thúc
funcTest1.join();
}Ex 2
#include <iostream>
#include <thread>
void printHello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
// Tạo một luồng t1 với hàm printHello
std::thread t1(printHello);
// Hàm join() được gọi để chờ luồng t1 kết thúc trước khi chương trình kết thúc.
t1.join();
return 0;
}Ex 3:
struct func {
int& i;
func(int& i_):i(i_){}
void operator()() {
for(unsigned j=0;j<1000000;++j) {
do_something(i); // point1: Potential access to dangling reference
}
}
};
void oops() {
int some_local_state=0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.join(); // point2: wait for thread to finish
}Việc sử dụng my_thread.join() nhằm mục đích cho thread này chạy xong trước khi kết thúc function oop() và do đó, nó cũng sẽ chạy trước khi biến some_local_state bị huỷ. Nếu biến này bị huỷ trước khi thread chạy xong, sẽ dẫn đến việc ở lần chạy tiếp theo, do_something(i) (point1) sẽ truy cập vào một biến đã bị huỷ (some_local_state). Điều này có thể dẫn đến những kết quả không mong muốn của chương trình. Lưu ý là chúng ta chỉ được gọi join() một lần với mỗi một thread cụ thể, bởi vì hành động này dọn dẹp các tài nguyên, bộ nhớ liên quan tới thread đó. Một khi chúng ta đã gọi hàm join(), hàm joinable() sẽ trả về giá trị false.
- Có thể khởi tạo thread không chỉ với 1 function mà có thể dùng với 1 object hoặc 1 function của class.
Ex 1:
class myFunctor {
public:
void operator()() {
cout << "This is my function object" << endl;
}
};
int main() {
// 1. Khởi tạo thread bằng cách truyền object của class myFunctor vào hàm khởi tạo của thread
myFunctor myFunc1;
thread functorTest1(myFunc1);
if (functorTest1.joinable()) functorTest1.join();
}Ex 2:
class myFunctor {
public:
void publicFunction() {
cout << "public function of myFunctor class is called" << endl;
}
};
int main() {
// 2. Khởi tạo thread với 1 public function của class, phải định nghĩa function và truyền object của class định nghĩa function đó
myFunctor myFunc2;
thread functorTest2(&myFunctor::publicFunction,myFunc2);
if (functorTest2.joinable()) functorTest2.join();
}- Sau khi hàm join return, thread trở lên không thể join lại. 1 joinable thread là 1 thread mà đại diện cho 1 execution mà chưa join.
- 1 thread không là joinable khi nó được khởi tạo mặc định hoặc được moved/assigned tới 1 thread khác hoặc joind hoặc detach hàm đã được ọi Not joinable thread có thể huỷ 1 cách an toàn. Hàm joinable() để checks thread có là joinable thread hay không (trả về true nếu thread là joinable and false nếu ngược lại). Nên sử dụng hàm này trước khi call hàm join();
//truyền 1 function tới thread
thread funcTest1(threadFunc);
//check if thread is joinable
if (funcTest1.joinable()) {
//main is blocked until funcTest1 is not finished
funcTest1.join();
}- Đặc biệt ở đây có nghĩa là gì? Ví dụ:
- Điều gì xảy ra nếu chương trình chúng ta xảy ra lỗi sau khi thread đã khởi chạy và trước khi chúng ta gọi hàm join().
- Ở trường hợp này, chương trình quăng exception rồi bị chấm dứt (terminate) trước khi hàm join() được gọi (tham khảo phần References).
- Do vậy để tránh trường hợp này chúng ta sẽ sử dụng các cách sau:
-
i. Dùng try/catch
Ngoài việc gọi hàm join() trong trường hợp lý tưởng không có exception, chúng ta cũng sẽ gọi hàm join() khi exception xảy ra:
struct func; void f() { int some_local_state=0; func my_func(some_local_state); std::thread t(my_func); try { do_something_in_current_thread(); } catch(...) { t.join(); //point1 throw; } t.join(); //point2 }
Việc dùng try/catch sẽ đảm bảo cho việc truy cập vào biến some_local_state sẽ kết thúc trước khi hàm kết thúc, dù là kết thúc bình thương (point2) hay do exception (point1).
-
ii. Dùng RAII (Resource Acquisition Is Initialization)
Nếu việc sử dụng try/catch hơi dài dòng, thì chúng ta có thể sử dụng cách này, nó cung cấp cho chúng ta một class thực hiện việc gọi join() trong destructor:
class thread_guard { std::thread& t; public: explicit thread_guard(std::thread& t_): t(t_){} ~thread_guard() { if(t.joinable()) t.join(); } thread_guard(thread_guard const&)=delete; thread_guard& operator=(thread_guard const&)=delete; }; struct func; void f() { int some_local_state=0; func my_func(some_local_state); std::thread t(my_func); thread_guard g(t); do_something_in_current_thread(); }
-
- Ở cách này, khi hàm f() kết thúc, những đối tượng trong function này sẽ được huỷ theo thứ tự ngược, có nghĩa là đối tượng thread_guard với tên g sẽ gọi hàm huỷ trước, sau đó, hàm join() sẽ được gọi trong hàm destructor này. Việc này được tiến hành ngay cả khi hàm do_something_in_current_thread() quăng exception nào đó.
- Nếu chúng ta không cần đợi một thread chạy xong trước khi chương trình/hàm kết thúc, mình có thể sử dụng hàm detach(). Một khi hàm này được gọi, nó sẽ không có bất kỳ liên kết nào nữa với đối tượng std::thread, do đó đảm bảo việc std::terminate() sẽ không được gọi khi đối tượng std::thread bị huỷ, mặc dù thread này chạy ngầm.
- Thread trở thành not joinable sau khi hàm detach được gọi.
- Việc gọi detach() ở đối tượng std::thread có nghĩa tách một thread chạy độc lập/chạy ngầm. Một khi thread chạy ngầm, quyền sở hữu (ownership) và điều khiển sẽ do C++ Runtime Library thực hiện, việc này sẽ đảm bảo các tài nguyên liên qua đến thread đó sẽ được lấy lại một cách chính xác khi thread kết thúc.
- Một thread khi đã detach còn được gọi là daemon thread (ở một số sách nói về khái niệm của daemon process trong UNIX – tiến trình chạy ngầm trong hệ thống mà không hề có User interface). Những thread loại này thường chạy xuyên suốt vòng đời của ứng dụng, thực hiện các tác vụ chạy ngầm như theo dõi hệ thống, dọn dẹp tài nguyên…
- Việc khi nào sử dụng detach() phụ thuộc vào từng task cụ thể, để từ đó ta có những cơ chế để biết được thread đó đã hoàn thành hay chưa – hay là được sử dụng cho những task kiểu “fire and forget“.
//detach funcTest1 from main thread funcTest1.detach(); if (funcTest1.joinable()) { //main is blocked until funcTest1 is not finished funcTest1.join(); } else cout << "functTest1 is detached" << endl;
- mainthread không đợi các thread con bị huỷ!
Ex 1:
#include <iostream>
#include <thread>
using namespace std;
void printSomeValues(int val, string str, double dval) {
cout << val << " " << str <<" " << dval << endl;
}
int main() {
string str = "Hello";
thread paramPass(printSomeValues, 5, str, 3.2); //5, str and 3.2 are passed to printSomeValues function
if (paramPass.joinable()) paramPass.join();
}Output Ex 1
5 Hello 3.2
Ex 2: // Ví dụ về việc sử dụng nhiều luồng để tăng hiệu suất của chương trình
#include <iostream>
#include <thread>
#include <vector>
void printSum(int start, int end, int* sum) {
for (int i = start; i <= end; i++) {
(*sum) += i;
}
}
int main() {
int sum = 0;
std::vector<std::thread> threads;
// Sử dụng 10 luồng để tính tổng của các số từ 1 đến 1000. Mỗi luồng tính tổng cho một phần của dãy số.
for (int i = 0; i < 10; i++) {
threads.push_back(std::thread(printSum, i * 100 + 1, (i + 1) * 100, &sum));
}
// Sau đó gộp kết quả lại -> Sử dụng multithreading như vậy có thể tăng hiệu suất của chương trình.
for (int i = 0; i < 10; i++) {
threads[i].join();
}
std::cout << "Sum = " << sum << std::endl;
return 0;
}Ex 3:
#include <iostream>
#include <thread>
using namespace std;
class myFunctorParam {
public:
void operator()(int* arr, int length) {
cout << "An array of length " << length << " is passed to thread" << endl;
for (int i = 0; i != length; ++i) cout << arr[i] << " ";
cout << endl << endl;
}
void changeSign(int* arr, int length) {
cout << "An arrray of length " << length << " is passed to thread" << endl;
for (int i = 0; i != length; ++i) cout << arr[i] << " ";
cout << "\nChanging sign of all elements of initial array" << endl;
for (int i = 0; i != length; ++i) {
arr[i] *= -1;
cout << arr[i] << " ";
}
}
};
int main() {
int arr2[5] = { -1, -3, -5, -7, 0 };
myFunctorParam objParamPass;
thread test(objParamPass, arr2, 5);
thread test2(&myFunctorParam::changeSign, &objParamPass, arr2, 5);
if (test.joinable()) test.join();
if (test2.joinable()) test2.join();
}Output Ex 2
An array of length 5 is passed to thread
-1 -3 -5 -7 0
An arrray of length 5 is passed to thread
-1 -3 -5 -7 0
Changing sign of all elements of initial array
1 3 5 7 0
- Chúng ta muốn viết một hàm tạo một thread (luồng) để chạy ngầm nhưng cần chuyển lại quyền sở hữu thread mới này cho hàm gọi thay vì ngồi đợi cho thread chạy xong.
- Chúng ta cần tạo một thread và chuyển lại quyền sở hữu cho một hàm – mà hàm đó cần đợi cho tác vụ hoàn thành.
- Cả 2 trường hợp này, ta cần thưc hiện chuyển quyền sở hữu thread cho một thread khác hoặc là một function (hàm).
- Sử dụng hàm std::move() của đối tượng std::thread
Chuyển quyền sở hữu cho một thread khác
- Ở ví dụ trên, ta đã thực hiện việc chuyển quyền sở hữu của thread t1 -> t2 bằng std::move(). Lúc này t1 không còn bất cứ liên kết với bất cứ thread nào – trở thành empty thread. Do đó, nó có thể được gán để chạy một thread mới.
- Chúng ta có thể xây dựng một class thread_guard như ở mục 1.3 (RAII) tại phần 1 của bài viết; giúp cho class này sở hữu quyền thực sự một thread. Điều này giúp chúng ta rất lớn, nó giúp ta tránh được kết quả không mong muốn trong quá trình chạy khi mà một đối tượng của class RAII tồn tại lâu hơn một thread mà nó đang tham chiếu – có nghĩa là không một thread nào khác có thể join hay detach thread một khi quyền sở hữu đã được chuyển sang đối tượng khác.
Chuyển quyền sở hữu ngay tại constructor của class scoped_thread ở 1 – 2
- Việc hỗ trợ chuyển quyền sở hữu ở std::thread cho phép các containers (vùng chứa) nếu các containers đó là move-aware container (như là std::vector). Việc đặt các đối tượng thread vào std::vector là một bước hướng tới việc quản lý các thread một cách tự động, thay vì phải tạo từng biến cho mỗi thread rồi sau đó join từng cái riêng lẻ.
- Giả sử chúng ta có một vấn đề phức tạp cần giải quyết, vậy chúng ta cần bao nhiêu thread để giải quyết một vấn đề nào đó?
-
C++ Standard Library hỗ trợ chúng ta trong việc xác định số lượng threads có thể chạy lúc runtime bằng std::thread::hardware_concurrency(). Hàm này sẽ trả về số thread có thể chạy đồng thời của một chương trình.
-
Lưu ý: đây chỉ là con số tham khảo bời vì nó có thể trả về 0 nếu giá trị không được xác định rõ hoặc do lý do gì đó không thể tính toán được. Nhưng nó có thể hữu dụng giúp chúng ta trong việc chia các task giữa các thread.
Ví dụ về std::thread::hardware_concurrency()
Output
- Xảy ra khi có hai hay nhiều yêu cầu có thể truy cập dữ liệu được chia sẻ và chúng cố gắng thay đổi nó cùng một lúc. Vì thuật toán lập lịch luồng có thể hoán đổi giữa các luồng bất kỳ lúc nào, nên bạn không biết thứ tự mà các luồng sẽ cố gắng truy cập vào dữ liệu được chia sẻ. Do đó, kết quả của sự thay đổi dữ liệu lại phụ thuộc vào thuật toán lập lịch luồng, tức là cả hai luồng đang "chạy đua" để truy cập/thay đổi dữ liệu.
- Vì lẽ đó, Race condition có thể gây ra lỗi không mong muốn trong lập trình, vì thế chúng ta cần phải tìm ra cách để giải quyết việc tranh chấp này. Hay chí ít là phải xác định được yêu cầu nào có quyền thao tác với dữ liệu. Đó là lí do Mutex ra đời.
- Hay còn gọi là "loại trừ lẫn nhau" được tạo ra nhằm ngăn chặn Race condition. Với mục tiêu một luồng không bao giờ được truy cập vào tài nguyên mà một luồng thực thi đang nắm giữ.
- Tài nguyên được chia sẻ là một đối tượng dữ liệu, mà hai hoặc nhiều luồng đồng thời đang cố gắng sửa đổi. Thuật toán Mutex đảm bảo rằng nếu một quy trình đang chuẩn bị thao tác sửa đổi trên một đối tượng dữ liệu thì không một quy trình/luồng nào khác được phép truy cập hay sửa đổi cho đến khi nó hoàn tất và giải phóng đối tượng để các quy trình khác có thể tiếp tục.
- Trong C++, chúng ta sử dụng std::mutex để khởi tạo một đối tượng mutex, sau đó gọi hàm lock() và unlock() để chỉ cho phép 1 thread tại một thời điểm có quyền truy cập vào đối tượng/ dữ liệu.
- Dưới đây là một ví dụ đơn giản về cách sử dụng mutex để đồng bộ hóa truy cập vào biến được chia sẻ giữa các luồng:
Ex 1:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // Khai báo một mutex global
int sharedVariable = 0; // Biến được chia sẻ giữa các luồng
// Hàm thực hiện công việc cho luồng
void threadFunction() {
// Khóa mutex trước khi truy cập vào biến được chia sẻ
mtx.lock();
sharedVariable++; // Thực hiện một hoạt động trên biến được chia sẻ
std::cout << "Giá trị biến được chia sẻ: " << sharedVariable << std::endl;
// Mở khóa mutex sau khi thực hiện xong
mtx.unlock();
}
int main() {
// Tạo và bắt đầu hai luồng
std::thread t1(threadFunction);
std::thread t2(threadFunction);
// Đợi cho các luồng kết thúc
t1.join();
t2.join();
return 0;
}- Trong ví dụ này, std::mutex mtx; là một biến mutex được khai báo global. Trong hàm threadFunction, mutex được khóa bằng cách sử dụng mtx.lock() trước khi truy cập vào biến sharedVariable, và được mở khóa bằng cách sử dụng mtx.unlock() sau khi đã thực hiện xong các hoạt động trên biến đó.
Ex 3:
#include <iostream>
#include <thread>
#include <mutex>
int dem = 0;
using namespace std;
mutex mtx;
void delay(int s) {
for (size_t i = 0; i < 0xffff; i++) {
for (size_t j = 0; j < 5*s; j++) { }
}
}
void funcA() {
while (1) {
mtx.lock();
dem++;
mtx.unlock();
cout<<"funcA: "<<dem<<endl;
delay(4000);
}
}
void funcB(){
while (1) {
mtx.lock();
dem++;
mtx.unlock();
cout<<"funcB: "<<dem<<endl;
delay(1000);
}
}
int main(int argc, char const *argv[]) {
thread t1(funcA);
thread t2(funcB);
t1.join();
t2.join();
return 0;
}Ex 4:
// C++ program to illustrate the thread synchronization using mutex
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
// Create object for mutex
mutex mtx;
// Shared resource
int number = 0;
// function to increment the number
void increment(){
// Lock the thread using lock
mtx.lock();
// increment number by 1 for 1000000 times
for(int i=0; i<1000000; i++){
number++;
}
// Release the lock using unlock()
mtx.unlock();
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
// Print the number after the execution of both threads
std::cout<<"Number after execution of t1 and t2 is "<<number;
return 0;
} - Tuy nhiên, vì các thread khác phải chờ đến khi unlock được gọi mới có quyền truy cập nên sẽ dẫn tới một vấn đề được gọi là deadlock. Ngoài ra, việc này khiến chúng ta phải luôn ghi nhớ phải gọi unlock mỗi khi kết thúc một hàm, hay thậm khi cả có lỗi xảy ra. Do đó, C++ Standard có hỗ trợ việc gọi unlock() một cách tự động.
a, std::unique_lock:
- std::unique_lock cho phép bạn khóa và mở khóa mutex một cách linh hoạt.
- Nó có thể được sử dụng để khóa một mutex trong một phạm vi cụ thể và tự động mở khóa khi ra khỏi phạm vi đó.
- std::unique_lock có thể được sử dụng cho các tình huống cần đặt khóa trong thời gian dài hoặc cần mở khóa trước thời gian kết thúc.
b, std::shared_lock:
- std::shared_lock cũng được sử dụng để khóa mutex, nhưng nó hỗ trợ việc chia sẻ truy cập giữa các luồng.
- Nó cho phép nhiều luồng cùng đọc một tài nguyên mà không cần phải chờ đợi khi một luồng khác đang đọc (khóa đọc).
- Khi một luồng muốn ghi vào tài nguyên (khóa ghi), nó phải chờ cho đến khi tất cả các luồng đang đọc kết thúc.
c, Ví dụ cách sử dụng std::unique_lock và std::shared_lock:
Ex 1
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // Mutex được chia sẻ
void reader() {
std::shared_lock<std::mutex> lock(mtx);
std::cout << "Luồng đọc..." << std::endl;
}
void writer() {
std::unique_lock<std::mutex> lock(mtx);
std::cout << "Luồng ghi..." << std::endl;
}
int main() {
std::thread t1(reader);
std::thread t2(writer);
std::thread t3(reader);
t1.join();
t2.join();
t3.join();
return 0;
}- Trong ví dụ này, reader() sử dụng std::shared_lock để cho phép nhiều luồng đọc cùng một lúc, trong khi writer() sử dụng std::unique_lock để đảm bảo rằng chỉ có một luồng được phép ghi vào tài nguyên tại một thời điểm.
Ex 2
#include <iostream>
#include <shared_mutex>
#include <mutex>
#include <thread>
using namespace std;
// creating a shared_mutex object
shared_mutex mutx;
int shared_data = 11;
// callable with shared lock
void readData() {
shared_lock<shared_mutex> lock(mutx);
cout << "Thread " << this_thread::get_id() << ": ";
cout << shared_data << endl;
}
// callable with unique_lock
void writeData(int n) {
unique_lock<shared_mutex> lock(mutx);
shared_data = n;
cout << "Thread" << this_thread::get_id() << ": \n";
}
int main() {
thread t1(readData);
thread t2(writeData, 128);
thread t3(writeData, 10);
thread t4(readData);
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}- Deadlock là trạng thái mà 2 hay nhiều thread sẽ chờ đợi lẫn nhau vì mỗi thread (vd là A) giữ một tài nguyên và chờ đợi tài nguyên từ thread khác (vd là B) – trong khi thread B này cũng đang cần tài nguyên từ A để mở khoá mutex.
=> Ở ví dụ này, khi chạy chương trình sẽ xảy ra deadlock do mỗi thread đang đợi thread khác để giải phóng khoá mutex. Dưới đây là kết quả chạy chương trình
- là một phần của thư viện chuẩn C++ (C++11 trở đi) được sử dụng để đồng bộ hóa giữa các luồng thông qua sự kích hoạt (notify) và chờ đợi (wait) của các sự kiện.
- Nó cho phép một hoặc nhiều luồng đợi cho đến khi một điều kiện cụ thể được đáp ứng trước khi tiếp tục thực hiện các công việc khác.
- Hiểu đơn giản nó đóng vai trò như cái điện thoại, nhằm đồng bộ thứ tự thực thi của các threads trong chương trình.
- Để sử dụng std::condition_variable, bạn cần kết hợp nó với một hoặc nhiều mutex. Thông thường, việc kết hợp std::condition_variable với std::unique_lock là phổ biến.
- Dưới đây là một ví dụ về cách sử dụng std::condition_variable để đồng bộ hóa giữa hai luồng:
Ex 1:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void threadFunction1() {
for (int i = 0; i < 5; ++i) {
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::unique_lock<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one();
}
}
void threadFunction2() {
for (int i = 0; i < 5; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; });
ready = false; // Đặt lại cờ trạng thái
std::cout << "Luồng 2 nhận được thông báo và tiếp tục thực hiện công việc." << std::endl;
}
}
int main() {
std::thread t1(threadFunction1);
std::thread t2(threadFunction2);
t1.join();
t2.join();
return 0;
}- Trong ví dụ này, threadFunction1 sẽ chạy một vòng lặp trong đó nó sẽ thực hiện việc chuẩn bị dữ liệu và gửi thông báo cho threadFunction2 sau mỗi giây. Trong khi threadFunction2 sẽ chờ đợi thông báo và sau đó tiếp tục thực hiện công việc của nó. Điều này sẽ diễn ra trong 5 lần lặp, mỗi lần đợi một thông báo mới từ threadFunction1.
Ex 2:
// C++ program to illustrate the use of shared_mutex
#include <iostream>
#include <shared_mutex>
#include <mutex>
#include <thread>
#include <condition_variable>
using namespace std;
// creating a shared_mutex object
mutex mutx;
condition_variable cv;
int data_test = 0;
void delay(int s){
for (size_t i = 0; i < 0xffff; i++) {
for (size_t j = 0; j < 5*s; j++) { }
}
}
void writeData(){
for (int i = 0; i < 5; i++) {
delay(5000);
unique_lock<mutex> lock(mutx);
data_test = data_test+i;
cv.notify_one();
}
}
void readData(){
for (size_t i = 0; i < 5; i++) {
unique_lock<mutex> lock(mutx);
cv.wait(lock);
cout<<"data: "<<data_test<<endl;
}
}
// driver code
int main() {
thread t1(writeData);
thread t2(readData);
t1.join();
t2.join();
return 0;
}Ex 3:
#include <iostream>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <chrono>
#include <queue>
using namespace std;
condition_variable cond_var;
mutex m;
int main() {
int value = 100;
bool notified = false;
thread reporter([&]() {
unique_lock<mutex> lock(m);
while (!notified) {
cond_var.wait(lock);
}
cout << "The value is " << value << endl;
});
thread assigner([&]() {
value = 20;
notified = true;
cond_var.notify_one();
});
reporter.join();
assigner.join();
return 0;
}Ex 4:
- Chúng ta có một hàng đợi, producer thread sẽ có nhiệm vụ thêm phần tử vào hàng đợi này.
- Cũng trong lúc đó, ta có consumer thread cũng đang chờ để xử lý khi hàng đợi này có dữ liệu từ producer thread.
- producer sẽ thêm phần tử vào trong hàng đợi, sau đó thông báo cho consumer biết (thông qua notify_one()), và nó sẽ thực hiện in ra giá trị vừa được thêm vào.
- Giả sử bạn có một một chương trình, trong chương trình này có một hàm để tính giai thừa. Giả định rằng hàm này chạy rất tốn thời gian, mà bạn muốn tách ra một thread riêng để chạy. Khi thread thực thi xong, bạn làm thế nào để nhận kết quả của hàm này trả về? (cách cũ bạn có thể xài condition variable và mutex – nhưng ở đây mình không sử dụng cách này).
- Sử dụng std::async() kết hợp std::future (bất đồng bộ)
- Trong lập trình, bất đồng bộ thường ám chỉ việc thực hiện một tác vụ mà không cần chờ đợi kết quả của tác vụ trước đó hoàn thành.
- Các tác vụ bất đồng bộ thường được thực hiện song song và có thể hoàn thành trong thời gian khác nhau.
- Bất đồng bộ thường được sử dụng trong các tình huống khi bạn muốn tiếp tục thực hiện các tác vụ khác mà không cần chờ đợi kết quả từ các tác vụ trước đó.
Ex 1:
- Ở ví dụ trên, chúng ta gọi std::async() và nó trả về một std::future.
- Để lấy được giá trị trả về, ta sử dụng hàm get() từ std::future để nhận giá trị trả về của hàm factorial().
- Lưu ý: get() chỉ được gọi một lần, nếu không sẽ gây crash chương trình.
- Hàm async() với tham số std::launch:async sẽ tạo một thread riêng biệt, tách biệt khỏi main thread.
Ex 2:
#include <iostream>
#include <thread>
#include <chrono>
// Hàm thực hiện công việc bất đồng bộ
void asynchronousTask() {
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Công việc bất đồng bộ đã hoàn thành!" << std::endl;
}
int main() {
// Tạo một luồng để thực hiện công việc bất đồng bộ
std::thread asyncThread(asynchronousTask);
std::cout << "Chương trình đang tiếp tục thực hiện công việc khác..." << std::endl;
// Đợi cho luồng kết thúc
asyncThread.join();
std::cout << "Chương trình đã hoàn thành!" << std::endl;
return 0;
}- Trong ví dụ này, hàm asynchronousTask được gọi trong một luồng riêng biệt bằng cách sử dụng std::thread. Hàm này thực hiện một công việc bất đồng bộ bằng cách chờ 2 giây và sau đó in ra một thông báo. Trong khi đó, chương trình tiếp tục thực hiện các công việc khác mà không cần chờ đợi luồng bất đồng bộ hoàn thành.
- Điều này chứng minh rằng công việc bất đồng bộ đã tiếp tục thực hiện mà không cần chờ đợi luồng hoàn thành, và chương trình tiếp tục thực hiện các công việc khác.
Ex 3:
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
// Hàm thực hiện công việc bất đồng bộ và trả về một kết quả
int asynchronousTask() {
std::this_thread::sleep_for(std::chrono::seconds(2));
return 42;
}
int main() {
// Tạo một tác vụ bất đồng bộ và lấy kết quả bằng future
std::future<int> resultFuture = std::async(std::launch::async, asynchronousTask);
// Thực hiện một số công việc khác
std::cout << "Chương trình đang tiếp tục thực hiện công việc khác..." << std::endl;
// Chờ và lấy kết quả từ tác vụ bất đồng bộ
int result = resultFuture.get(); // Đợi và lấy kết quả từ tác vụ
std::cout << "Kết quả từ tác vụ bất đồng bộ là: " << result << std::endl;
return 0;
}- Trong ví dụ này, chúng ta sử dụng std::async để tạo một tác vụ bất đồng bộ và std::future để lấy kết quả từ tác vụ đó. Phương thức get() được sử dụng để đợi và lấy kết quả từ tác vụ. Khi tác vụ hoàn thành, kết quả được trả về và được gán cho biến result.


















