0% found this document useful (0 votes)
23 views23 pages

Chapter 2

Uploaded by

ngoc26042005
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
23 views23 pages

Chapter 2

Uploaded by

ngoc26042005
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 23

CHAPTER 2.

BASICS OF C AND C++

C Keywords

The auto keyword is used to declare automatic variables, which are variables whose
lifetime is limited to the scope in which they are defined (usually within a function). In
auto
modern C, auto is generally not used explicitly, as variables inside functions are
automatically considered auto by default.
The enum keyword is used to define an enumeration, which is a user-defined type
consisting of a set of named integer constants. Enums make code more readable by
enum replacing literal integer values with meaningful names.

Example: enum Days {Sunday, Monday, Tuesday};


The extern keyword is used to declare variables or functions that are defined in
another file or outside the current scope. It tells the compiler that the variable or
extern function exists elsewhere, usually in another source file or in a library.

Example: extern int x; declares that x is defined elsewhere.


The goto keyword is used to transfer control to another part of the program, typically
by jumping to a labeled statement. It is generally discouraged due to making code
harder to follow and maintain.
goto
Example:
goto label;
label:
printf("Jumped to label\n");
The register keyword suggests that a variable should be stored in a CPU register
rather than RAM for faster access. However, modern compilers manage register
allocation automatically, so this keyword is rarely used in contemporary C
register
programming.

Example: register int x; suggests storing x in a register.


The static keyword has two main uses:
- Within functions: It keeps a variable's value between function calls, preserving its
state.
- Within files: It limits the visibility of a function or variable to the file in which it is
static
defined, making it invisible to other files.
Example:
- static int count; (preserves the value of count across function calls)
- static void helper(); (function is limited to the current file)
The typedef keyword is used to create new type names (aliases) for existing types. It
simplifies code and enhances readability, especially with complex types like structures
or pointers.
typedef
Example:
typedef unsigned int uint;
uint x; // equivalent to unsigned int x;
union A union is a special data type that allows storing different types of data in the same
memory location. All members of a union share the same memory, so only one
member can hold a value at a time.

Example:
union Data {
int i;
float f;
};
These keywords specify whether an integer type can represent both negative and
signed / positive values (signed) or only non-negative values (unsigned).
unsigne Example:
d signed int x; //(can hold both negative and positive integers)
unsigned int y; //(only non-negative integers)
The volatile keyword tells the compiler not to optimize the variable, as its value may be
changed unexpectedly (e.g., by external hardware, or in a multi-threaded program). It
ensures the variable is always read from memory rather than being cached.
volatile
Example: volatile int flag;
This is typically used for hardware access or flags that may be modified by an interrupt
service routine.

Compiler & Linker

- The compiler checks the source program for syntax errors and, if no error is
found, translates the program into equivalent machine language. The
equivalent machine language program is called an object program.
- Linker combines destination files (*.obj, *.lib) to generate an executable
file *.exe
- Errors that may occurs includes:
 Undeclared functions
 Duplicated declaration of variables or functions

Debug

- Run the program with F5


 Step over (run each statement), Step in (jump to the function called), Step out
(jump out of the function called)
- Debugging can be done via command line by using gdb (gdb stands for GNU
Debugger)
 Set breakpoints: Pause the execution of a program at specific points to inspect
its state.
 Step through code: Execute a program one line at a time to observe its behavior
and variables.
 Inspect variables: View and modify the values of variables while the program is
running.
 Call stack inspection: See the sequence of function calls that led to the current
point in the program.
 Control execution: Resume, stop, or continue execution based on the current
state of the program.
- To find errors:
 Operation, algorithm, logic errors not the syntax errors
 Errors which are not informed when executing the program
 These errors are difficult to find, thus it is required to debug in most of the cases

Program Execution
- Program Memory
 Non-volatile type
 Stores instructions in memory slots
 CPU reads and executes instructions
- Data MemoryVolatile type
 Stores temporary data in memory slots
 Very fast
 Cache, Registers, and RAM (SRAM, DRAM, DDRAM...)

The four standard streams

Memory allocation of C/C++ Programs

Variables and Data Types


- Statement = data + operator + …
- Data is represented via variables or constants with data type
- Number Systems
 Information in computer is represented in base 2, or binary number system
which has only 2 digits: 0 and 1
 Binary numbers can be stored in memory as a sequence of bits, called a word
 The number of words available in address space memory is referred to as the
memory space, or address space

Type compatibility and conversion

- Types are compatible  automatically converted


 Between integer types (Take note on range!!!)
 Between real types (Take note on accuracy!!!)
 Between integer and real (Take note on both range and accuracy!!!)
 Bool to integer and real: true  1, false  0
 Integer, real to bool: ≠ 0  true, 0  false
- If errors or warnings, it would require performing type casting

Type casting

- Type casting is used to convert data type of a value


- Syntax: (type) expression
- Examples:
(double) myage
(int) (balance + deposit)
- Attention: type casting does not change the way of calculation in an operation

Portability

- Compilers for microcontrollers may consider int as 16 bit or 32 bit number


depending on the microcontroller
- C Compiler may read it:
 From left to right
 From right to left

Endianness
- Endianness refers to the byte order used to store multi-byte data in memory. The
two primary types are Little-endian (LSB at the lowest address) and Big-endian
(MSB at the lowest address)
- Two types:
 Big-endian: The most significant byte is stored first.
 The 0x01020304 32-bit value is stored at the ptr address as follows:

 Examples of big-endian architectures are AVR32 and Motorola 68000


 Little-endian: The least significant byte is stored first.
 The 0x01020304 32-bit value is stored at the ptr address as follows:

 The x86 architecture is little-endian


 Bi-endian: Hardware supports switchable endianness.
 Hardware supports switchable endianness. Some examples are PowerPC,
ARMv3, and the preceding examples
- Endianness is particularly essential when exchanging data with other systems
- E.g.: If a developer sends the 0x01020304 32-bit integer as is, it may be read as
0x04030201 if the endianness of the receiver does not match the endianness of
the sender.
- This C++ snippet can be used to determine the endianness of a system:
#include <iostream>
int main() {
union {
uint32_t i;
uint8_t c[4];
} data;
data.i = 0x01020304;
if (data.c[0] == 0x01) {
std::cout << "Big-endian" << std::endl;
} else {
std::cout << "Little-endian" <<
std::endl;
}
}

Alignment

- Memory alignment refers to the arrangement of data in memory such that data
types are stored at addresses that are multiples of their size or a specific
alignment boundary. Proper alignment can improve the performance of programs
on some hardware, especially for accessing larger data types like integers, floating-
point numbers, and structs. Improper alignment can lead to inefficiencies or even
errors on some systems.
- Data Type Alignment
Different data types have different alignment requirements. The alignment
requirement of a type is typically the size of the type, though it can vary. For
example:
 A char (1 byte) usually has an alignment of 1 byte.
 A short (2 bytes) usually has an alignment of 2 bytes.
 An int (4 bytes) usually has an alignment of 4 bytes.
 A double (8 bytes) usually has an alignment of 8 bytes.
#include <stdio.h>
#include <stdalign.h>
struct Example {char c; int i;};
int main() {
printf("Alignment of char: %zu\n", alignof(char));
printf("Alignment of int: %zu\n", alignof(int));
printf("Alignment of Example struct: %zu\n", alignof(struct Example));
return 0;
}
- Processors don't read and write data in bytes but in memory words—chunks that
match their data address size
- Compilers align data automatically to achieve the most efficient data access

Fixed-width integer types

- To make the code portable, embedded developers often use fixed-size integer
types that explicitly specify the size of a data field
- The most used data types are as follows:

- size_t is a special data type to represent the offset and data sizes in an
architecture-independent way when using pointer

Derived Data Types


Enumeration

A data type allows you to define a set enum Color {Red, Green, Blue};
of named integral constants. enum WeekDay{
It is typically used when you have a Mon = 2,
fixed set of related constants that are Tue, Wed, Thu, Fri, Sat,
Sun = 1};
logically grouped together.
-----------------------------------------
-
How enum Works: #include <stdio.h>
- Each name in an enum enum Day {Sunday, Monday, Tuesday,
corresponds to an integer value. Wednesday, Thursday, Friday, Saturday};
- By default, the first enumerator is int main() {
assigned the value 0, and each enum Day today;
subsequent enumerator's value is today = Wednesday;
incremented by 1. printf("The value of today
- You can also explicitly assign (Wednesday) is: %d\n", today);
values to the enumerators. return 0;
}
By default, Sunday is assigned 0, Monday
1, Tuesday 2, and so on.
The value of Wednesday is 3, since it is
the 4th item in the enum starting from 0.

#define in C and C++

#define preprocessor directive is used to // Syntax:


define macros in C and C++ #define MACRO_NAME replacement_text
-----------------------------------------------------------------
A macro is a fragment of code which has ----
#include <stdio.h>
been given a name
#define PI 3.14159
#define SQUARE(x) ((x) * (x))
How #define Works: int main() {
- The #define directive defines a printf("Value of PI: %f\n", PI);
constant or a macro that can be int num = 5;
used throughout the code. printf("Square of %d: %d\n", num,
- The preprocessor replaces all SQUARE(num));
instances of the macro with its return 0;
definition before compiling the }
program. // Value of PI: 3.141590
// Square of 5: 25
Pointer
int i = 1;
int* p = &i;// p has the address of i
*p = 2;// i = 2
int j;
p = &j;// now p has the address of j
*p = 3;// j = 3, i remains 2
void main() {
int i = 0;
int* p = &i; // p refers to the address of i
int j = *p; // j = 0
*p = 2;// now i = 2
p = &j;// now p contains the address of j
*p = 3;// now j = 3, i remains 2
double d = i; // OK, int is compatible to double
p = &d;// error, int* isn’t compatible to double*
p = (*int)&d; // no compile error, but dangerous, // meaningless type
conversion!
double* pd=0; // p contains the address 0
*pd = 0;// no compile error, but fatal error
pd = &d;// OK
double* pd2; // p refers to an uncertain address*pd2= 0;// fatal error
pd2= &d;// OK, pd and pd2refer to the same addr.
}
- Pointer is a variable which contain the address of the first byte of another variable, it is
used to access that data indirectly
- If the pointer is declared without initialization, its address is undefined (random)
- The pointer’s value (which is a memory location address) can be changed, the pointer
can represent different variable at different moment
- “&” returns a pointer which points to a variable it is usually used to assign a value to a
pointer
- “*” applies to a pointer to provide a variable whose address is the pointer’s value so the
variable’s value can be read or changed
- Never use the operator “*”, if the pointer has not been assigned to a memory address
that can be controlled by the program

Array
// Declaration without initialization:
int a[3];
enum {index = 5};
double b[index];
const int N = 2;
char c[N]; // C++ only
// Declaration with number of elements and initialization of the elements
int d[3]= {1, 2, 3};
double e[5]= {1, 2, 3};
char f[4]= {0};
// Number of elements is defined automatically
int a[]= {1, 2, 3, 4, 5};
double b[]= {1, 2, 3};
double c[]= {0};
char s[]= {'a'};
// Declaration of multi-dimension array
double M[2][3];
int X[][2]={{1,2},{3,4},{5,6}};
short T[2][2]={1,2,3,4,5,6};
Pointer and Array
void main() {
int a[5]; // a has 5 elements with uncertain values
int* p;
p = a; // p refers to a[0]
p = &a[0]; // the same as above
*p = 1; // a[0]=1
++p; // now p points to a[1]
*p = 2; // a[1]=2
p++; // now p points to a[2]
*p = 3; // a[2]=3
p += 2; // now p points to a[4]
*p = 5; // a[4] = 5
++p; // OK, no problem until we dereference it*p = 6; // Now is a BIG BIG
problem!
a = p; // error, a is like a constant pointer
}
-------------------------------------------------------------------------------
--------
void main() {
int a[5]; // a has 5 elements with
// uncertain values
int* p = a; // p points to a[0]
p[0] = 1; // a[0]=1
p[1] = 2; // a[1]=2
p+= 2; // now p points to a[2]
p[0] = 3; // a[2]=3
p[1] = 4; // a[3]=4
p[3] = 6;// a[5]=6, Now is a BIG BIG problem!
}
- An array is a set of data with the same type organized in sequence in the memory  the
array elements
- An array element can be accessed via array variable together with respected index or by
using a pointer (address of each element)
- Number of array elements is fixed (must be constant when declaring array), it can never
be changed
- An array variable (statics) is a constant pointer which is assigned with the first element
in the array
- Possible to initialize element values, one should never assign one array to another. If it
is required to copy an array, a function must be used
- Never access an array with an index which is out of range, i.e., if N is the array size, the
accessible range is 0 .. N-1
- Pointer is never an array; it can only carry array address and be used to manage the
array (either dynamic or static)

Reference (C++)

- When a variable is declared as reference, it becomes an alternative name (alias)


for an existing variable.
- Mainly used to provide parameters and return values of a function in general,
and for overloaded operators in particular
void main() { Reference vs. Pointer
double d = 2.0; - Both are an alias for an object,
double& r = d; // r represents d and usually implemented to hold an
double *p1 = &d, *p2 = &r; address of an object
r = 1.0; // OK, d = 1.0
- Reference differs from a pointer
double& r2; // error, r has to be
assigned to a var.double& r3 = 0;//
that:
error, too  A reference can be accessed with
double d2 = 0; the same syntax as the name of
r = d2; // r = 0, d=0 an object
r = 1.0; // r = d = 1, d2 =0  A reference always refers to the
} object to which it was initialized
 There is no ‘‘null reference’’, and
we may assume that a reference
refers to an object

Typedef

- typedef create a new name for an Before


available type, it does not define a struct car{
new type char engine[50];
- Usage: provide a distinguish name, char fuel_type[10];
suitable to a specific application, int fuel_tank_cap;
int seating_cap;
and easy to modify later
float city_mileage;
};
int main(){
struct car c1;
}

After
typedef struct car{ // Old type
char engine[50];
char fuel_type[10];
int fuel_tank_cap;
int seating_cap;
float city_mileage;
}car; // New type
int main(){
car c1;
}

Struct

Accessing Member Variables with // `Person` structure declaration


Arrow Notation struct Person{
// member variables
Member variables of a structure can be char* name;
int age;
accessed using a pointer with arrow ( -> )
};
notation. The given code block initializes a // `person1` intialization
Person pointer type named struct Person person1 = {"Jerry", 29};
person1Pointer. Inside the printf() // `person1Pointer` intialization to
statement, the name member variable of memory address to `person1`
person1 is accessed using arrow ( -> ) struct Person* person1Pointer =
notation. &person1;
// accessing `name` through
`person1Pointer`
printf("My name is %s", person1Pointer-
>name);
// OUTPUT: My name is Jerry
Passing Structures to Functions // Person structure declaration
struct Person{
Structures can be used as parameters of // member variables
functions by using the struct keyword char* name;
int age;
followed by the structure name in the
};
function definition. The given code block // declaring Person type parameter
defines a function signature named void myFunc(struct Person person1);
myFunc() with a Person parameter named
person1 .
Passing Structure Pointers To // Person structure declaration
Functions struct Person{
// member variables
Structure pointers can be paramters of char* name;
int age;
functions by using the struct keyword, the
};
structure name, and the pointer symbol // Person pointer parameter declaration
(*) in the function definition. The given void myFunc(struct
code block defines a function signature Person*person1Pointer);
named myFunc() with a Person pointer
parameter named person1Pointer .
- (struct) is used to group related data described an object, data type can be same
or different
- Structure is defined by declaration of member variables. The definition is not to
define specific variables, thus, does not initialize the variables
- Size of structure ≥ total size of all the member variables
- A member variable of a structure can be accessed via structure variable name,
(.) operator, and member variable name
- Structures can be nested, the structure can contain an array, an array can also
have elements which are structures, etc.
- Variables with the same structure type can be assigned to each other, or one can
be used to initialize another (different from arrays)
- Pointers can also be used to access structure via (*.) and (->)
- Two structure types which are defined the same are still different structures

Union
A union in C/C++ is a user-defined data type
that allows storing different data types in the
same memory location, but only one member
can hold a value at a time. All members of a
union share the same memory, meaning the size
of a union is determined by the size of its
largest member. Unions are used to optimize
memory usage when a variable may hold different
types of data at different times. For example, it is
often used in low-level programming, such as
handling hardware registers or implementing type
conversions. However, accessing a member other
than the one most recently written to can lead to
undefined behavior.
// Example of Union
enum SignalType {BINARY_8, BINARY_16,
ANALOG_1, ANALOG_2};
// Example of Union
union SignalValue {
#include <stdio.h>
unsigned short word;
union test1 {
unsigned char byte;
int x;
float real;
int y;
double lreal;
} Test1;
};
union test2 {
struct Signal {
int x;
SignalType type;
char y;
SignalValue value;
} Test2;
};
union test3 {
void main() {
int arr[10];
SignalValue B,W;
char y;
B.byte = 0x01;
} Test3;
W.word = 0x0101;
int main(){
unsigned char b = W.byte; // OK, the
printf(“sizeof(test1) = %lu,
lower byte
sizeof(test2) = %lu,
float f = W.real; // meaningless
sizeof(test3) = %lu, sizeof(Test1),
Signal DI1 = {BINARY_8, 0x11};
sizeof(Test2), sizeof(Test3));return
Signal AI1 = {ANALOG_1,{0}};
0;
Signal AI2;
}
AI2.type = ANALOG_2;
AI2.value.lreal = 145.67;
}
- A union is a variable that may hold (at different times) objects of different types and
sizes, with the compiler keeping track of size and alignment requirements
- Unions provide a way to manipulate different kinds of data in a single location of
storage, without embedding any machine-dependent information in the program
- Members of a union are independent
- Size of a union ≥ size of the largest variable
- Similar declaration to that of struct but different meaning
- Same way of accessing member variables as struct, i.e. direct variables or via a
pointer
- A union can contain struct or vice versa, a union can also contain arrays or elements
of an array can be unions

Control flow

Making decisions (branching statement)

if..else: decision statement to select 1


or 2 cases
switch..case: decision statement to
select multiple cases
break: jumping instruction to end (early) a
scope
continue: skips the current iteration of the
loop and continues with the next iteration
return: jumping instruction and end
(early) a function
goto: jumping to a label (should not use!)

Decide on one case: use if


if(npoints >= 60) cout<< "Passed";
if(npoints>= 80 && npoints<= 90){
grade = ‘A’;
cout<< grade;
}

Decide on two case: use if..else


if(npoints >= 90) cout << 'A';
else if (npoints>= 80)cout<< 'B';
else if (npoints>= 70)cout<< 'C';
else if (npoints>= 60)cout<< 'D';
else cout << 'F';

switch..case

switch (expression)
{
case /* constant-expression */:
/* code */
break;

default:
break;
}

Control flow: loop

- while (condition) { }
- do { } while (condition)
- for (int i; condtion;
post_action) { }

Range-based for loop for ( range_declaration :


- Added since C++ 11 range_expression){
- Executes a for loop over a range loop_statement
}
------------------------------------------
----
int main(){
// Iterating over whole array
std::vector<int> v =
{0, 1, 2, 3, 4, 5};
for (auto i : v)
std::cout << i << ' ';
}
------------------------------------------
----
int main(){
// Iterating over arrayint a[] = {0,
1, 2, 3, 4, 5};
for (int n : a) std::cout << n << ' ';
}

printf()
int printf ( const char * format, ... );

A format specifier follows this prototype:


%[flags][width][.precision][length]specifier
Returning value:
- On success, the total number of characters written is returned
- If a writing error occurs, the error indicator (ferror) is set and a negative number
is returned
- If a multibyte character encoding error occurs while writing wide characters,
errno is set to EILSEQ and a negative number is returned.

assert()
The assert() macro in C/C++ is
#include <assert.h> // or <cassert> in C++
used as a debugging aid to check
assumptions made by the program assert(expression);
during runtime. --------------------------------------------------------------------
Use Cases: --
- Commonly used in debugging // Example
to catch programming errors #include <assert.h>
or validate assumptions during #include <stdio.h>
int main() {
development.
int x = 5;
- Often disabled in production assert(x > 0); // Passes, program
builds for better performance. continues
Disabling assert: assert(x == 10); // Fails, program
- If the macro NDEBUG is defined terminates with an error message
(e.g., using #define NDEBUG), printf("This won't be printed if the
all assert() calls are ignored assertion fails.\n");
by the compiler. return 0;
}

Formatting I/O in C++

- A few standard ios class functions are:


 width(): The width method is used to set the required field width. The output
will be displayed in the given width
 precision(): The precision method is used to set the number of the decimal
point to a float value
 fill(): The fill method is used to set a character to fill in the blank space of a
field
 setf(): The setf method is used to set various flags for formatting output
 unsetf(): The unsetf method is used To remove the flag setting
- Examples:
cout.precision(8);
cout << 1234.56789 << ' ' << 1234.56789 << ' ' << 123456 << '\n;
cout.precision(4);
cout << 1234.56789 << ' ' << 1234.56789 << ' ' << 123456 << '\n;
// This produces:
// 1234.5679 1234.5679 123456
// 1235 1235 123456
cout.width(4);
cout.fill('#');
cout << "ab"; // print ##ab
// Results
// 1234.5679 1234.5679 123456
// 1235 1235 123456
// ##ab
Other Input/Output standard libraries
iostrea iostream stands for standard input-output stream. This header file contains
m definitions of objects like cin, cout, cerr, etc
iomanip stands for input-output manipulators. The methods declared in these files are
iomani
used for manipulating streams. This file contains definitions of setw, setprecision,
p
etc
fstrea This header file mainly describes the file stream. This header file is used to handle the
m data being read from a file as input or data being written into the file as output
This header file defines a type called stringstream that allows a string to be treated
sstrea
as a stream, and thus allowing extraction or insertion operations from/to strings in the
m
same way as they are performed on cin and cout

Preprocessor

- Preprocessor statements appear before the program is compiled


- It is not a part of the compiler, but is a separate step in the compilation process
- Tasks to do:
 Combine other files into the file being compiled
 Define constant, macro
 Compile the program with conditions
 Implement precompile statements with conditions
- Precompile statements start with # (hash)
 Only space characters can be put in front of pre-processor statements in that
line

The preprocessor in C and C++ is a tool that processes code before it is passed
to the compiler. It handles preprocessor directives, which are instructions that
start with a # symbol. These directives are not part of the C or C++ language itself
but are commands for the preprocessor to prepare the code for compilation.
File Inclusion #include <stdio.h> // Includes standard input/output
(#include): library
Macro Definition #define PI 3.14159
(#define): printf("Value of PI: %f", PI); // PI is replaced with
3.14159

File Inclusion: #include

- #include is usually used with header files, e.g.:


 #include searching follows an implementation-defined rule to find the file
 #include “myheader.h” searching for the file typically begins where the source
program was found
- These source lines are replaced by the content of the file, i.e. standard.h or
myheader.h

Macro Substitution

- Keyword #define can be used to define


 Constants
 Macros

Constant definition
#define BUFFERSIZE 256
#define MIN_VALUE -32
#define PI 3.14159

- Attention:
 There are neither = nor ; in the line
 Name defined by #define can be removed by using #undef(it may be redefined
later)
 Substitutions are made only for tokens, and do not take place within quoted
strings, e.g. there is no substitution for PI in printf("PI")
- UPPERCASE WORD is used as the constant name to differentiate with variable,
function names

#define, const and enum

- It is possible to replace #define with constant or enum


#define ARRAYSIZE 10
const int ArraySize = 10;
double array[ARRAYSIZE]; /* Valid. */
#define PI 3.14159
#define ARRAYSIZE 10
const double Pi = 3.14159; /* Preferred */
enum {ARRAYSIZE = 10}; /* Preferred */
Macro definition

- #define is usually used to create macros


#define max(x,y) ((x)>(y) ? (x) : (y))

- Macro looks like a function call; however it is not really a function. Macro name
(max) is replaced by statement with respect to operands before the program is
compiled
int a = 4, b = -7, c;
c = max(a,b);

is replaced by: c = ((a)>(b) ? (a) : (b));


Why use Macro?

- Speed:
 Implementation as a function, the code is copied to the program before being
compiled
 It is not really effective with program running on PC
- Common code
 Macro enable to work with all kinds of data type (int, double…)
//Computes the square of a number x
Macro examples #define SQR(x) ((x)*(x))
//Determines the sign of a number x
#define SQR(x) ((x)*(x)) #define SGN(x) (((x)<0) ? -1 : 1)
#define SGN(x) (((x)<0) ? -1 : 1) //Computes the absolute value of x
#define ABS(x) (((x)<0) ? -(x) : (x)) #define ABS(x) (((x)<0) ? -(x) : (x))
#define ISDIGIT(x) ((x) >= '0' && (x) <= //Checks if x is a digit character (from '0'
'9') to '9')
#define NELEMS(array) #define ISDIGIT(x) ((x) >= '0' && (x) <= '9')
sizeof(array)/sizeof(array[0])) //Calculates the number of elements in an
#define CLAMP(val,low,high) \ array
((val)<(low) ? (low) : (val) > (high) ? #define NELEMS(array)
(high) : (val)) (sizeof(array)/sizeof(array[0]))
#define ROUND(val) \ ((val)>0 ? (int) //Restricts val to the range [low, high]
((val)+0.5) : -(int)(0.5-(val))) #define CLAMP(val,low,high) \ ((val)<(low)?
(low):(val)>(high)?(high):(val))
//Rounds val to the nearest integer
#define ROUND(val) \ ((val)>0 ? (int)((val)
+0.5) : -(int)(0.5-(val)))

Disadvantages of Macro

- Be careful with some pitfalls when using macro


 Inappropriate usage of parentheses
 Side effect of ++, -- operator
 No data type verification

Macro pitfalls

#define SQR(x) x * x
- Example:
int a = 7;
b = SQR(a+1);
will be replaced by:
b = a+1 * a+1;
- Solution: use parentheses to avoid the confusion

Long Macro statement

- If the macro line is too long, it should be broken into multiple line by using \
#define ERROR(condition, message) \
if (condition) printf(message)

Pre-defined Macros

Contains the current line number of the program in the compilation. It


__LINE__
gives the line number where it is called.
__FILE__ Holds the file name of the currently executing program in the computer
Gives the date at which source code of this program is converted into
__DATE__
object code
__TIME__ Gives the time at which program was compiled
__STDC__ Is used to confirm the compiler standard

#define PRINT_DEBUG(expr, type) \


printf(__FILE__ "[%d](" #expr "): \%" type##Conv "\n", __LINE__, (expr))
- defines a macro named PRINT_DEBUG
- PRINT DEBUG(expr, type) is the name of the macro, with two parameters: expr
(an expression to be evaluated and printed) and type (a format specifier for the
printf function)
- #expr is the stringizing operator. It converts the expr into a string literal. For
example, if expr is x + y, #expr would result in "x + y"
- type##Conv is a token concatenation operation, which combines type and
Conv. For example, if type is "d", it becomes "dConv". This is a trick to use a
custom conversion specifier (perhaps for specialized printing, though "Conv"
itself may need to be defined elsewhere)
- The printf function uses this format specifier along with the value of the expr
(evaluated as (expr))
//Example //Output
int x = 5; main.c[12](x + 3): 8
PRINT_DEBUG(x + 3, d);

Conditional inclusion

- Conditional preprocessor statements in C (#if, #elif, #else, #endif, #ifdef,


#ifndef)
- Goals:
 To add debugging codes in the program
 To add the code which is not standard
 To avoid insert header files multiple times

Debugging

- Compile the program in debug mode #define DEBUG


- Debugging code can be inserted
#ifdef DEBUG
printf("Pointer %#x points to value %f", pd, *pd);
#endif

Non-standard code

- Applied in the case that codes are used for different microprocessors
#ifdef __WIN32__
return WaitForSingleObject(Handle,0)== WAIT_OBJECT_0;
#elif defined(__QNX__)||defined(__linux__)
if(flock(fd,LOCK_EX|LOCK_NB) == -1)
return 0;
else
return 1;
#endif
- Applied in the case that codes are used for different microprocessors

One time inclusion of header files

- A header file should be included only once (although a few file may use it)
- Duplicated inclusion will result in repeated definition of variables, functions, labels,
and thus the program cannot be compiled successfully
- Solution: using “header guards”
#ifndef A_HEADER_H_
#define A_HEADER_H_
/* Contents of header file is here. */
#endif
Header guard example: htu21d.h
A header guard is a preprocessor technique used to ensure that a header file is included only
once in a program, preventing multiple definition errors or unnecessary redundancy in code. This
technique is especially important when writing C or C++ code that uses header files (.h files) to
declare functions, classes, or constants.
#ifndef HTU21D_h #ifndef stands for "if not defined"
#define HTU21D_h  Checks whether the symbol HTU21D_h
... has already been defined earlier in the
#define HTU21D_ADDRESS 0x40 //chip i2c program
address
 This is the first step of the header guard.
#define HTU21D_USER_REGISTER_WRITE 0xE6
//write user register
If the header file is included multiple
#define HTU21D_USER_REGISTER_READ 0xE7 times in different parts of the code, this
//read user register check prevents its contents from being
... processed more than once
class HTU21D { #define HTU21D_h:
public:  If the symbol HTU21D_h has not been
HTU21D(HTU21D_RESOLUTION = defined yet, this line defines it
HTU21D_RES_RH12_TEMP14);  This marks the header file as
#if defined(ESP8266) "processed" for the remainder of the
bool begin(uint8_t sda = SDA, uint8_t compilation, so any future inclusion of the
scl = SCL);
same file will be ignored by the
#else
bool begin(void); preprocessor.
#endif The Code in Between (class HTU21D, #define
float statements, etc.):
readHumidity(HTU21D_HUMD_OPERATION_MODE =  This is the actual content of the header
HTU21D_TRIGGER_HUMD_MEASURE_HOLD); file. It might contain class definitions,
float function prototypes, constants, or any
readTemperature(HTU21D_TEMP_OPERATION_MODE other declarations.
= HTU21D_TRIGGER_TEMP_MEASURE_HOLD);  In this case, the header file defines
... constants for chip I2C address and user
}; register addresses, as well as a class
#endif
HTU21D with several methods.
#endif:
 Marks the end of the conditional
preprocessor block that started with
#ifndef. It signifies the end of the header
guard.
Bit masking

- Bit operators are used for 2 main purposes:


 Save memory by storing status flags in one byte
 Interface with hardware register
- In these 2 cases, it is required the capability of modifying each bit and check each
bit status.
- C enables to create macro, which can set, reset (clear) bit or reverse bit status,
named masking
- Step 1: Create one integer which represents the status of each bit or a group of
bits
Common Bitwise Operators Used in Bit
Masking:
1. AND (&): Used to clear specific bits (set to
Bit masking is a technique used in 0).
programming to manipulate specific bits of a 2. OR (|): Used to set specific bits (set to 1).
value (usually integers) by using bitwise 3. XOR (^): Used to toggle specific bits (flip
operators. The idea is to "mask" certain bits between 1 and 0).
while leaving others unchanged, and to use 4. NOT (~): Used to invert all the bits (flip all
bitwise operations to extract, set, or clear 1s to 0s and vice versa).
certain bits within a number. 5. Shift left (<<): Used to move bits to the left
(multiply by powers of 2).
6. Shift right (>>): Used to move bits to the
right (divide by powers of 2).
How Bit Masking Works: Simple Example:
A bit mask is a binary value where some bits Let's say we have a number and want to
are set to 1 and others to 0. The mask is then perform bit masking on it. Consider the
used with bitwise operators to isolate, change, following number represented in binary:
or modify specific bits in the target value.
unsigned char num = 0b10110101;//Binary:
1011 0101 (which is 181 in decimal)
Example 1: Clear a specific bit (use AND
operator) Example 2: Set a specific bit (use OR
If we want to clear (set to 0) the 4th bit of num operator)
(from the right), we can use the AND operator If we want to set (make 1) the 2nd bit of num
with a mask. (from the right), we can use the OR operator.
1. Create a mask where the 4th bit is 0 (to 1. Create a mask where the 2nd bit is 1, and
clear it), and all other bits are 1: all other bits are 0:
unsigned char mask = 0b11110111;//Binary: unsigned char mask = 0b00000010;// Binary:
1111 0111 0000 0010
2. Perform the AND operation between num 2. Perform the OR operation between num
and the mask: and the mask:
num = num & mask; num = num | mask;
3. Result: 3. Result:
//After the operation, num will be: //After the operation, num will be:
num = 0b10110101 & 0b11110111; num = 0b10110101 | 0b00000010;
num = 0b10110101 & 0b11110111 = 0b10110101; num = 0b10110101 | 0b00000010 = 0b10110111;
//Now, the 4th bit is cleared, and the //Now, the 2nd bit is set to 1, and the
result is: result is:
num = 0b10110101;//The 4th bit is now 0 num = 0b10110111;//The 2nd bit is now 1
(cleared). (set).

Operation with Bit masking Macro for each bit


Bit Field example 1 A bit field in C allows you to define a structure
with fields that use a specific number of bits, rather
struct Disk_Register{ than using the default size of the data type. This is
unsigned int ready:1;//1 bit field named useful for memory optimization or when dealing
"ready" with hardware registers, flags, or packed data.
unsigned int error:1;//1 bit field named ------------------------------------------------------------------------
"error" ----
unsigned int wr_prot:1; struct Disk_Register {
unsigned int dsk_spinning:1; unsigned int ready:1;// 1 bit field for
unsigned int command:4; // 4 bits field "ready"
named "command"unsigned int error_code:8 ; unsigned int error:1;// 1 bit field for
unsigned int sector_no:16; "error"
}; unsigned int wr_prot:1;// 1 bit field
void printBits(int a){ for "wr_prot" (write protection)
int i; unsigned int dsk_spinning:1;// 1 bit
for (i=31;i>=0;i--){ field for "dsk_spinning"
if ((a&(1<<i))!=0) unsigned int command:4;// 4 bits field
putchar('1'); for "command"
else unsigned int error_code:8;// 8 bits
putchar('0'); field for "error_code"
} unsigned int sector_no:16;// 16 bits
field for "sector_no"
}
};
int main( int argc, char* argv[] ){ This C code demonstrates the manipulation of a
struct Disk_Register r; structure Disk_Register using bit-level operations and
printf( "sizeof(r) = %lu\n", printing the structure's bits by accessing its memory
sizeof(r) ); directly.
int *p = (int *) &r; // Make p point to
r - Pointer to Structure: The pointer p is used to
*p = 0; // Clear all 32 bits in r access and manipulate the memory of the
structure as an integer, which allows for direct
printBits( *p ); // Print the bits in r
bit-level manipulation.
putchar('\n'); - Bit Manipulation: By setting specific fields in the
r.error = 1; // Set the error bit (bit structure (like error and dsk_spinning), individual
#30) bits in the structure are changed, and the
printBits( *p ); // Print the bits in r printBits function allows us to observe these
putchar('\n'); changes.
r.dsk_spinning = 1; // Set the disk - Bit Fields: The bit fields in the Disk_Register
spinning bit (#28) structure are stored in the memory as contiguous
printBits( *p ); // Print the bits in r bits, and their positions can be modified directly
putchar('\n'); by accessing the structure's memory as an
} integer.

You might also like