# C programming part 1 - Basics but important
Things to note:
1) C expression has a value
Expressions in C has a value. This value can be used in various ways.
In case expression is assignment, value of the expression is the value
of the left operand.
Instead of,
c = getchar();
while (c != EOF) {
c = getchar();
}
you can write,
while ((c = getchar()) != EOF) {
// Do something
}
where, we use expression in inside, which has value of c after assignment.
2) Chained logical expressions evaluation in C
let's take,
if (c == ' ' || c == '\n' || c = '\t') {;}
The expression is evaluated from left to right and evaluation is stopped
the moment trueness or falseness of result is found.
This is important if expressions connected over || or && are doing something more
than just comparison above. It is not necessary that all expressions will be
evaluated. So this needs to be kept in mind. Same applicable with && (logical AND)
3) Mental model of function and variable
We can model everything as a "Symbol" in C.
Function is a symbol, a variable is a symbol.
Think - function is nothing function a variable whose value depend on input arguments
and some processing on them via code execution.
Similarly, variable is nothing but a function whose value is simply what is stored at that
memory location, or function which simply returns a stored value.
So function and variable = symbol. This is how compiler and linkers also think and model.
4) return statement in function can follow any expression.
int function1()
{
statements;
return (any expression);
/*
Example:
return (c = getchar()); // reads char, assigns to c, return c;
*/
}
5) Vocabulary:
5.1) "Function prototype" and "Function declaration" both mean, they tell compiler
what function looks like at high level while compilation and put references to external
functions in the code. (Which will be linked later during linking phase)
5.2)
Function parameter --> function arguments seen by the functions.
Function arguments --> What is being passed by caller to function.
Function parameter and argument are same things referred differently by caller and callee.
5.3) Symbolic constant = fancy name for C MACROs.
5.4) Symbol (function or variable as per mental model) definition
Definition means --> memory is allocated and we do some code execution.
6) C programs function calls are always "call by value"
Call by reference is invalid concept for C. Many articles and book may interpret is
incorrectly as passing a pointer to function.
While people say passing pointer to a variable to function as "pass by reference",
this is incorrect as per C programming, because when we are passing pointer to
variable to function, we are still passing a value, and that value is the address of
the variable (obtained using & operator). C function access that variable using local pointer
which is initialized with this address indirectly.
So, in effect C program access variables indirectly using pointers, hence call by reference is
incorrect term to use here.
Other languages such as Fortran support this "pass by reference" concept.
There is nice article about it:
https://medium.com/@isaaccway228/the-technically-correct-definition-of-pass-by-reference-is-nonsense-d89ca22180a5
7) const keyword in C
"const" just tell that value "should" not be changed.
Language does not put protection from changing it indirectly, you can
always define pointer to it and modify.
Usually this protection is done at system level using MPU during program boot by linux OS.
8) There are only 4 data types in C.
- char
- int
- float
- double (double precision float)
Rest of the stuff are called "qualifiers"
Qualifiers are -
unsigned, signed, long, long long etc.
- Sizes of C data types are machine dependent and determined by compilers.
Check <limits.h> and <float.h> for sizes.
- As per C spec, int and float should be at minimum 16bit is the requirement.
9) ' ' is way to represent chars in C
'c' yields value of c in machine's character set (usually ASCII).
'\xA' --> inserts hexadecimal value using escape sequence.
10) enumerations constants in C
- enums are integers --> sizeof(int) memory is allocated.
- Advantage: debuggers can recognize these values and associate them with symbolic name,
which helps while debugging over JTAG (source level debugging)
- enums give better ways to initialize value -> name association, consider below,
enum days {
monday = 1,
tuesday,
Wednesday,
so on...
};
tuesday, Wednesday and so on are automatically initialized to 2,3, and so on.
This is helpful to initialize value -> symbolic name association.
- enum variables can take any value (int range), compilers do not check it.
- At high level, enums are just a means to get rid of numeric magic values and deal
with them using english words, so that debugger can also recognize them and show them
symbolically instead of magic numbers.
- What is then difference between enums and MACROs ?
MACRO (Symbolic constants) are pre processor stuff.
enum is runtime stuff, hence debugger can understand them.
Note: debugger can never understand MACROs.
11) Global external variables in C must be initialized with constant expression.
int myGlobal = (1 + 4 > 5);
This initializes myGlobal = 0;
Why ?
Global variables are set in .data section before program even starts executing.
You can not assign variable with non constant expression because program is not
ready to run at this phase. Constant expressions are evaluated by the compiler.
12) associativity and precedence of the C operators.
Many times, its hard to work with them. Use ( ) wherever needed to avoid any confusion.
13) Implicit Type conversions in C
- Generally, lower type is converted to longer type.
a (int) = b (char) --> b is converted to int implicitly
- Comparison between signed and unsigned type is machine dependent. Hence avoid it.
- Implicit type conversion can loose the information when high type is converted to low type,
hence care must be taken in such cases.
14) "cast" is a operator.
- Cast is the unary operator.
Example,
b = (int)c; --> converts c to int.
- cast operator is used for explicit type conversion.
15) Use ++ and -- operators innovatively.
- ++ and -- increment and decrement operators are unary.
- When these operators are used on variables/functional (symbols),
their value is updated after variable is used in that "statement".
- These operators can be used innovatively to reduce code line size. Maybe use
while indexing array in the loop arr[i++] = (expression);
16) Conditional expression in C
(Expression1) ? (expression2) : (expression3);
- This is indeed an expression and has value.
- Value of conditional expression is either exp2 or exp3 based on value of exp1
- value of conditional expression can be assigned to other variable/expression,
ans = (exp1) ? (exp2) : (exp3);
Special considerations for conditional expressions:
- Use conditional expression to compact the code length.
- Also sometimes, simple conditional expression can be converted to "branch free" assembly
instruction by the compiler based on the CPU ISA (Instruction Set Architecture).
Hence, these expressions can help accelerate branch predictions and CPU pipelines.
Hence, prefer them if possible to improve code performance as well.
17) C language does not define order of evaluation of functions in expressions.
c = f() + g();
There is no rule which one (f or g) called first. This is completely upto the compiler.
18) C statement.
C statement is nothing but "expression" terminated by ; (semicolon)
19) comma (,) operator in C
Expressions separated by comma (,) operators are evaluated left -> right
This operator can be used in innovative way.
Comma operator can be used to combine multiple closely related expressions in
single C statement. For example consider below.
// consider snippet of swapping the string.
temp = s[i], s[i] = s[j], s[j] = temp;
comma operator can also be used to write multiple expressions/statements in single
line inside for loop or while loop to compact the code and improve the readability of the code.
- Value of expression using comma operator is value of rightmost expression.
20) Use of goto and labels.
- goto is used to take direct exit from multiple or deeply nested loops.
It is used to handle error cases in more structured way. (Linux kernel code) uses it
more extensively.
- Disadvantage of goto statement is that it can break the CPU pipeline as branch prediction
logic of the CPU may perform bad with lot of direct branches in the code using goto.
- In case of nested loops, how to propagate error upwards ?
Take some variable (maybe bool) and set that during error case, Check that variable
in all loop conditions which can help break all the upper looks if any loop sets
that variable. This is one way to avoid goto constructs in your code if you want to
optimize and do goto less programming to make CPU branch logic happy.
flag = 1;
for (; exp && flag ;)
for (; exp && flag;)
for (;exp && flag;)
if (exp)
flag = 0; // exit from all loops controlled via "flag"
21) continue and break
"continue" is only used in loop. Immediately evaluate loop condition and goto next iteration.
"break" is used both in loops and switch statement. Immediately exit from the loop.
Things to try out and understand:
1) verify (2) assembly and try out.
2) Verify (9) '\xA' inserting hex/decimals in strings.
3) Exercise (12) associativity and precedence. Look at assembly and find ways to remember
them permanently.
4) Check (14) check how (cast) operator is implemented by looking into assembly.
5) Understand comma (,) operator.
Check that value of expression using comma operator is value of rightmost expression.
In general play with , operator.
--------------------------------------------------------------------------------------------
# C programming part 2
1) In C programming, if return type of the function is omitted (not mentioned) then
default "int" is assumed. Hence, following is allowed.
my_function(int a)
{
return a;
}
This function will return int.
2) Functions can return nothing.
return statement may not follow the expression.
It is allowed for functions to return nothing.
int my_function(int a, int b)
{
int c = a + b;
return;
}
It is allowed. return value in X0 (ARM architecture) will be garbage in this case.
3) Function declaration/prototype can come inside other functions or outside function
in the file.
int function1()
{
int function2(int, int);
return function2(2,3);
}
OR
int function2(int, int);
int function1()
{
return function2(2,3);
}
These both constructs are allowed.
4) Functions in C are always external and global.
If we consider variables, we have automatic variables which are local to function, and
external variables (global variables). But functions are always external/global.
But, if we define function as static functions, we can limit the scope to current file only.
5) External variables (Global variables)
- We can interchangeably use term external variable and global variables.
- "extern" here means that outside function. Variable declared outside any function
is called external variable.
- External variables are globally accessible unless they are defined as static.
- If external variable (variable defined outside the function) is defined as static,
their scope is within the file.
- External variables can be used to share data between multiple functions. Think it as
alternative to function arguments to pass data.
- Too many external/global variable usage in the program may create multiple complex data
links between functions and variables, hence they should be carefully used.
- (important) Scope of the external variable is between it is defined till end of file. Hence to use
external variable in other file "extern" declaration is required in other file.
- extern with arrays.
while declaring array as extern on other file, we can skip number of elements. Like below,
extern int arr[]; --> works
but while defining array, its size must be specified.
int arr[MAX_SIZE]; --> correct definition of array.
Visual representation of extern variables:
File1.c
int sp = 0;
double val[MAXVAL];
(Storage is allocated in this file)
File2.c
extern int sp;
extern double val[]; --> extern declaration in second file.
function1();
function2()
6) Commutative operators.
+, *. == are commutative operators.
/ (division), - (subtraction), << are non commutative.
Operator is commutative if swapping the operands gives same result.
7) More subtle points related to "Static" storage class.
When we define a "external static" variable
- Its scope is inside current file
- This can be thought as global variable which is global only inside the current file.
- This variable gets storage assigned in .data section of the program.
When we define a "static variable" variable inside the function:
- Scope is only inside the function.
- This can be also thought as a global variable whose scope is limited to function only.
- Storage is allocated on .data section. This static variable defined inside function
is allocated on .data and not on the stack like auto variables. This point is important.
In both of these cases static variables get memory in .data (Global variable space)
8) "Register" variables
register int a;
- These are automatic variables directly allocated on CPU registers.
- We can no take address of them. Obviously, CPU registers do not have address, hence
& operator on register variable is compilation error.
- Compiler is free to ignore it. That is, there is no guarantee that compiler will assign
storage for them on CPU registers. This is because CPU registers are limited in numbers.
9) Variables inside the block (That is inside { and }) has their own scope and namespace.
Variables defined inside block will have scope limited inside block only (this includes both
static and automatic variables). These variables have their own namespace.
Lets consider below:
extern int i;
int function1(int a) {
int i = 5;
a = i; --> This "i" is different than "Extern/global i"
return i;
}
So, from the language point of view, preference is first given to inner most variable in the block,
then compiler goes outside to search for variable names.
10) Think: char array and string.
Consider below:
char pattern[] = "This is string";
What happends here ?
pattern is constant pointer (array name).
"This is string" is also pointer to string literal.
Here, array pattern is initialized to pointer to string constant.
No actual copy happen. Its just pointer assignment.
11) Some misconception about recursion in C.
Note that recursion does not save space, it only make code (.text) compact.
Internally, while execution more and more stack will be used during recursion.
Hence, recursive implementation saves on .text (code part) but does not save runtime memory.
This understanding and intuition is very important.
12) World of preprocessor:
Preprocessor run even before code starts compilation. Thats why name pre processor.
a) #include "file.h"
include file need not to be file. It can be anything. Its upto the compiler what it means.
like #include <stdio.h> may mean something to include, but not necessarily a file.
b) Lets consider basic macro (symbolic constant)
#define name text
here, name is called --> token
text --> this can be any arbitrary text ended by newline. If you want to continue on
second line use '\'
Scope of symbolic constant is from point of its definition till end of the file.
So, if you want macro to be valid in other file, it needs to be defined in some header
file and include that header file in other file. Note that macros are not global, they are
always local to file where they are defined.
c) Macro functions
Think macros as simply text replacement stuff.
#define MAX(A,B) ((A) > (B) ? (A) : (B))
make sure to add necessary parentheses in macro function arguments. Because they are purely
text replacement. What if user gives below input to macro function.
MAX(a + b, c + d) == a + b > c + d ? a + b : c + d;
with parentheses,
MAC(a + b, c + d) == (a + b) > (c + d) ? (a + b) : (c + d); --> this looks more safe.
d) Think macro functions as equivalent to inline functions. --> faster to execute.
Hence important design rule --> For small and repeated used functions, use macro functions.
This will avoid function calling and provide more readability and reusability to your code.
e) Quoted strings concatenation
#define dprint(expr) printf(#expr "some data")
# --> converts expr in macro to quoted string.
so dprint(Google) becomes printf("Google" "some data")
which in turn become printf("Google some data")
This approach can be used in logging mechanism for your module.
Similarly there is "##" --> token pasting operator.
#define CONCAT(a,b) a##b
CONCAT(x,y) --> expands to --> xy
#define MASK_ENUM(name) STATE_##name
MASK_ENUM(IDLE) --> STATE_IDLE
This concept is used at multiple places in bit operations while accessing IP registers in
firmware engineering.
f) Conditional inclusions
#if (any expression)
// include this code for compilation
#endif
defined(MACRO_NAME) is expression.
#if defined(DEBUG)
#endif
#ifdef and #ifndef are special forms of conditional inclusions preprocessor directive.
--------------------------------------------------------------------------------------------
# C programming part 3 - pointers and arrays
1) What is the pointer ?
It is group of memory locations (byte addressible) which can hold the address.
It can be 32bit or 64bit size based on CPU architecture.
e.g AArch64 --> pointer is 64bit ARM32 --> 32bit size pointer.
2) Mental model of '&' operator
& operator is address of operator. This is unary operator.
Give address of symbol on which it is applied on.
Note: & can not be applied to register variables.
& also can not be applied to expressions.
& can be applied to anything which has a valid address in the system memory.
3) mental model of * (dereference) operator.
* is used to access the object/element pointer is pointing to.
Call it either dereference operator or access operator.
Better to call it - access operator.
Do not call it evaluate, it does not evaluate, it accesses the object.
Meaning of - evaluate and access is different.
So, when you say *ptr --> we access the object pointed by pointer ptr by using dereference
operator.
Think *ptr as access to object. So one can do *ptr++ or ++(*ptr)
So it ptr pointer to variable v, then *ptr represents access to variable v.
(*ptr)++ increments the variable v. (*ptr) = v
4) Unary operator associate left to right.
Think about (*p)++ vs *p++
5) In C programming, there is strong relationship between pointer and array.
array name is a constant pointer to first data element. Basically, it is a pointer.
[ ] This can be thought as dereference operator used in case of arrays.
array[ i ] means *(array + i)
So array name with index is same as *(array + i)
This is nothing but alternative way --> pointer + offset way.
So, lets say we have pointer to string char *ptr, then
ptr[0] = *(ptr + 1) --> first character.
ptr[1] = *(ptr + 2) --> second character
So, [] indexing operator can be used with pointers for ease of accessing instead of
pointer and offset notation.
6) Pointer arithmetic
Think that pointer arithmetic works considering object size pointer points to.
a) Adding constant to pointer
lets say pointer ptr pointing to integer (size 4 bytes).
ptr + 1 --> points to 1 object ahead --> address is ptr + 4 bytes.
ptr + 4 --> points to 4 integer objects ahead --> 4 * 4 = ptr + 16 bytes.
b) Illegal stuff
Division and multiplication does not make sense and are invalid on pointers.
Even if you want to do it, typecast pointer to unsigned int, do operations, cast back
it to pointer. But this is a hack, worth noting as it may be useful in certain conditions.
Adding or subtracting float variable from pointer is illegal, does not make sense.
Addition of two pointer variables is not allowed.Think what sense it make if we add two addresses
of system memory ?
c) subtracting constant from pointer
(ptr - n) --> points to n objects behind current pointed object.
This means negative indexes in array is allowed.
array[-1] is equivalent to *(array - 1) which is valid given that memory location
access is allowed and in valid memory range.
d) Subtraction of two pointers.
ptr2 - ptr1 --> gives how many objects are there in between two memory locations.
Why ? Think this in reverse way.
ptr1 + (number of objects) = ptr2
Hence ptr2 - ptr1 = number of objects that can be fitted in the memory range.
So bottom line -> Adding or subtracting constants from pointers actually moves pointer
by size of the object it is designed to point to.
This is important analogy to remember if you want to understand pointer arithmetic better.
7) Passing array to the function.
function1(&array[0]) is equivalent to function1(array)
function1(&array[2]) is equivalent to function1(array + 2)
function1(int array[]) is equivalent to function1(int *array)
Think array name as constant pointer.
8) Additional arithmetic and logical operators on pointers.
-, +, ==, !=, < , >, <=, >= all work well and legal on pointers provided they make sense
with the context.
What do I mean by context here ?
You can not compare 2 random pointers, even language allowes it. it does not make sense.
But, you can compare 2 pointers pointing to elements of same array, they are logically related.
So any situation when there is some relation with respect to addresses, pointers to these addresses
can logically compared or operated upon in safe manner.
Think as pointer is just a normal variable which holds the address of some memory location and C
language gives special handling to it at language level. At the end of the day, at CPU assembly
level everything is just numbers in CPU registers.
9) Mutable verses immutable strings.
Consider below:
char message[] = "This is a string";
char *pmessage = "This is a string"
message is array, so a constant pointer to first char of string.
pointer variable message can not be changed. You can not do message++.
This actually creates message array on the stack and initialize it with string content.
So you can modify the content of the string.
However on the other side, pmessage is pointer to first char of string constant.
pmessge++ is allowed.
Quoted strings in C are string constants. They live in the
literal pool of the memory (.text section), you may not change their value. Also OS may add
some protections on it based on MMU/MPU/firewalls during boot. Hence values pointed by pmessage
may not be changed.
So summary:
1st one --> mutable string creation in array
2nd one --> immutable string access via char pointer.
There is a difference, remember it.
10) Pushing and poping from the stack : Common pattern
Lets say pointer p points to stack,
*p++ = val; --> push value on the stack.
val = *--p; --> pop value from the stack.
11) Array of pointers
Think pointers as just variables.
So, pointer can be array element.
char *array[10] --> defined array of 10 char/string pointers.
Lets say when you want to sort string based on some criteria,
you run sorting algorithm on array of strings and swap them in the array.
So effectively, you are only operating and swapping the pointers to strings and
not the actual strings. This saves memory a lot.
12) Note: Pointer is only C language concept, at the low level at CPU ISA is it just
an address.
value = (unsigned int) ptr;
You can cast pointer to unsigned int and play with it as usual.
13) multidimensional arrays
Consider 2D array,
int days[2][13]
Mental model of 2D array:
- It is array of array.
- Read from right, First it is array of 13 ints, then it is array of 2 such arrays.
right indicate the most granular size, as we shift left it abstracts up.
- Another way to think: It is structure of 2 elements, each element of this structure is array of 13 ints.
First subscript (leftmost) indicate how many objects offset into this structure (each object is array of 13 elements),
and second subscript (rightmost) indicate how much offset in an int array.
days[0] --> first array days[1] --> second array.
Same analogy as structures can be applied to 2D arrays.
int array[][13] --> works.
int array[][] --> does not work.
Why ? You must define most granular object numbers first, because that defines offset steps for 2D arrays which is must.
Only difference from structure analogy here is, each object in 2D array must be of same size, while
in structure you can have members of various sizes.
14) Tricky stuff involving 2D array.
Case 1: int array[10][20];
Case 2: int *array2[10];
Case 1 defines 2D array.
Strict length check is enforced by the language.
*(array[1] + i) = array[1][i] --> works, but array[i][j] is preferred.
In this case array[i] --> pointer to 1D array.
Cool stuff: *(array[3] + 21) = array[4][0] i.e things are sequencial in the memory.
Case2:
Less strict length checking.
array2[3][4] --> allowed. --> but there is nothing bounding in the second dimention.
*(array2[3] + i) = array2[3][i]
15) C command line arguments
- C execution environment (Operating system) passes command line arguments.
argc --> argument count --> int
argv --> argument vector --> Pointer to array of string pointers. Or array of string pointers.
argv[0] --> points to string of program name
argv[i] --> ith argument.
argv[argc] = NULL as per the language specification.
Prototype of main:
int main(int argc, char *argv[])
{
;
}
16) Pointers to functions
int (*function)(void *, void *) {}
function is the function pointer with takes two void parameters and returns int.
Dereference or access function pointed by this pointer.
val = (*function)(&var1, &var2);
Remember: function name = addess of that function. So function pointer can be initialized as,
function = my_function; thats it. No need to use & operator.
17) Important concept: typecasting address value as function pointer.
lets day we have address as 0x1234. We need to jump to it as function with 2 int arguments
Typecast address as function:
(int (*)(int, int))0x1234
then dereference it.
*((int (*)(int, int))0x1234)(4, 5);
This is very cool.
Read it as,
typecast 0x1234 to function pointer to function which takes 2 int values and return int,
and then dereference it (access the function) with input arguments as 4 and 5.
Internally in Assembly language, address 0x1234 is put into some register and instruction BLR is used.
--------------------------------------------------------------------------------------------
# C programming part 4 - structures and unions
1) What is a structure ?
It is a collection of one or more variables possibly of different types grouped together
under a single name for convenience in handling.
Structures help to organize the complicated data under one name.
2) Check and think.
Can we assign one structure to other directly like,
s1 = s2 ;
It will copy entire data from structure2 to structure1.
Can we pass structure by value to function ? how ?
Mental model structure as "complex data type" made up of multiple fundamental data type variables.
You can also think structures as "objects"
3) structure tag
Consider below structure declaration,
struct tag_name {
int member1;
char member2;
}
tag_name is called tag or structure name or complex data type in other words.
tag --> shorthand for this type of variable grouping used later to instantiate the object of that type.
Structure tag is always optional. You can instantiate objects as struct { ... } obj1; There is no need to give name (tag) to structure.
4) Another mental model for structure is to view them as "Way of viewing the memory".
Consider below structure definition:
struct {int a; char b} var1;
This means look at the memory starting at variable var1 as first one int (4 bytes ) and then 1 char (1 byte)
Basically structure describe how this complex variable looks and sits in the memory and how to access it with offsets.
Notice in above example, we have not used any tag (identification) to structure.
5) Model/think structure declaration as a data "type"
struct { ... } a, b, c;
defines three variables a b and c of type "struct { ... }"
6) Note: struct initialization are always a constant expressions.
7) Legal operations on structures:
- Copy one structure object to another.
- Assignment as a unit.
- Taking the address of structure instance with "&" operator
- Accessing members of structures using "." operator.
8) Passing and returning structures from functions
Think structure like normal variable or type.
Example:
struct point addpoint(struct point p1, struct point p2) {
p1.x = p1.x + p2.x;
p1.y = p1.y + p2.y
return p1;
}
Notice: We are not using function pointers here, we are passing and returning full
structures from functions here.
9) Use pointer to structure while passing large structure to functions
struct point origin; *pp;
pp = &origin;
(*pp).x --> access member x of structure.
Note that () are needed as precedence of "." is more.
alternative way to access member of structure via pointer,
pp->x --> access member x of structure.
so pp->x is equivalent to (*pp).x
arrow -> is shorthand to access structure member via pointer.
Remember that "." binds very tightly, hence use () wherever necessary.
10) Think structures in C as "objects" --> all pointer arithmetic apply.
Like incrementing pointer to structure increments pointer variable by total size of one structure element.
11) Array of structure
struct key {
char *word;
int count;
};
struct key keytab[NKEYS];
This defines array of structures.
12) Tip: Use sizeof() operator in C to find out number of elements in the array of structure.
#define NKEY (sizeof(keytab) / sizeof(struct key))
Note: sizeof can not be used at #if (condition)
13) Size of the structure is compiler dependent and based on alignment requirement on CPU and system.
Structure members are aligned with necessary padding by the compiler.
14) Self referential structures
Structure can contain pointer to itself. But not the instance of itself, that is not allowed.
This is called self referential structures. This concept is used in trees and linked list
struct list_node {
int val;
struct list_node *next; // this is self referential concept.
};
15) Dynamic memory allocation for structure object/instances
We use malloc() in C. Malloc returns void, hence need to cast it.
While allocating memory, we ensure that alignment requirements from all members in the
structures are satisfied. That is alignment of structure is alignment of the worst element.
Hence, malloc() will return memory with size that takes care of structure alignment correctly.
16) typedefs in C programming
typedef creates a new data type name in C.
Note that it is only "name" and not new data type.
Think typedefs are Macros(symbolic constants) which are recognized by the compiler.
The new name defined by typedef is the last word of the typedef definition.
Examples:
typedef char * string;
string is the new name for the type (char *)
So, you can write - string my_string; in more readable way.
Where typedefs are actually more useful ?
Wherever there is complex data type declaration and you need to instantiate objects
of that data type more frequently. like below:
typedef struct tnode {
int val;
int val2;
} TreeNode;
TreeNode is the new type name for structure, so that we can simply instantiate this type object
as - TreeNode my_node;
Another example while defining function pointers. This is where their true difference from the Macros come.
typedef int (*Function_ptr)(char *, char *);
This defines typedef for function pointer.
Function_ptr my_ptr; --> defined function pointer in simple words.
This is where compiler is handling typedef, quite different than preprocessor.
What are 2 main applications of typedef ?
1) Portability
You can define typedef for fundamental data types which are machine dependent like float, int
So when we move from one machine to another machine, only typedef definition needs to change.
This is extensively used in linux kernel code. Example - u8, size_t etc.
This make your code more portable.
2) Better readability
Consider writing complex function pointer declaration. typedefs are always better to deal with.
17) Unions in C programming
Think unions as a way to manipulate and store/access different type of data in single storage.
Union give a tap or way of accessing data at single storage with different views, these views can be
thought as int (4 bytes), char (1 byte) etc.
Example:
union u_tag {
int ival;
float fval;
char *sval;
};
Size of the union variable is the size of biggest member in it. Also alignment of union
variable is alignment of worst member in it.
in above union example ival, fval, sval members start from the same address, their type
define how much data to access, like ival will read 4 bytes, fval will read 4 bytes and
sval will read size of pointer (usually 64 bit on Aaarch64 processor).
So effectively while accessing union variable, you have multiple views to look into that
storage. Hence, it is programmers responsibility to control which type is currently stored.
Operations permitted on unions:
1) Assignment or copying as a unit
2) Taking address using '&' operator.
3) Accessing its members.
Note: Initialization of unions can be done only with data type of its first element. That
is in our example it is int data type.
18) bit fields in C programming
bit fields in C programming is C abstraction and alternative for bit manipulation on variables.
Consider below:
flag |= EXTERN | STATIC; --> setting bits of flag varible.
This uses bitwise operators to manipulate bits of a variable.
C provides way to do same using bit fields.
struct tag_name {
unsigned int is_extern : 1;
unsigned int is_static : 1;
} flags;
flags.is_extern = 1;
flags.is_static = 1;
This is equivalent way to do same using C bitfields.
Important note:
bitfields in C are implementation defined. They can overlap processor word boundaries.
In above example, it is not necessary that is_extern bit field is bit 0, it can be any bit.
It is fully compiler implementation defined. C only gives abstract interface.
- Never use bit fields to write drivers. Manipulating HW registers.
- Usually avoid using structure based bit fields in your programs as these are not portable.
- Note: we can not use '&' address of operator on bit fields -> bits do not have addresses.
--------------------------------------------------------------------------------------------
END. Thank you for reading. Happy talking to hardware via C.
No comments:
Post a Comment