Chapter 2
Chapter 2
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:
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.
- 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
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...)
Type casting
Portability
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:
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
- 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
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.
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++)
Typedef
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
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
switch..case
switch (expression)
{
case /* constant-expression */:
/* code */
break;
default:
break;
}
- while (condition) { }
- do { } while (condition)
- for (int i; condtion;
post_action) { }
printf()
int printf ( const char * format, ... );
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;
}
Preprocessor
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
Macro Substitution
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
- 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);
- 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
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
- 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
Conditional inclusion
Debugging
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
- 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