Data Types, Operators, and Control Flow

Data Types

// Primitive
int 		012(octal)    0xFB(hex)		 
float		double 		  long double
char
bool (from <stdbool.h>)

signed	unsigned   //(for "char" and "int" types only)
short		long 			long long 

// Derived
arrays, enum, union, struct


// Pointers
int*
void (only pointers and function return type)
etc...


/* Literal Suffixes (Case-insensitive)
int = u, l, ll
real = f, l
*/

Need for long type

Systems that used 16-bits to store integer values continued using the same amount even on 32-bit systems where Memory Address Register (MAR) is larger and hence can address more locations than on a 16-bit storage, we needed a type that can be utilized depending on machine architecture, hence long came into existence.

Modifier Rules

  • If no data type is specified, it is always int by default.
static a = 7;
signed b = 8;
const c = 9;
  • int and char are always signed by default.

  • No modifiers for float are allowed.

  • Only modifier allowed on double is long double.

Type Casting and Integer Promotions

Explicit Cast: Higher data type to lower one. float to int (often leads to truncate or loss of data).

//Explicit conversion syntax

(int)a

(float) 9/5

(unsigned int) 4.3 - 2

1.141f	1.141F

Implicit Cast: Lower data type to higher one. signed to unsigned (performed by compiler implicitly).

//Very famous pitfall
#include <stdio.h>
 
int main(){
   float c = 5.0;
   printf ("Temperature in Fahrenheit is %.2f", (9/5)*c + 32);
   return 0;
}

//Output: 37.0

//Fix: make any one of 9 or 5 as double
#include <stdio.h>
 
int main(){
   float c = 5.0;
   printf ("Temperature in Fahrenheit is %.2f", (9.0 / 5)*c + 32);
   return 0;
}

//Output: 41.0

Integer Promotions: Integer data types like char and enum can be promoted to int or unsigned. Link: https://www.geeksforgeeks.org/integer-promotions-in-c/

2s Complement

  • It is totally a “hack”, entirely observation based, no first principles. It always works!
  • We need to represent negative numbers too in those bit patterns only and we do so by not touching the positive numbers (whose MSB is 0) and taking their 2s complement as negative numbers.
  • We get only one representation for 0 (that’s good), and we also get a number 2 ^ (n) (where n is number of bits we are using to represent the numbers) whose both positive and negative representations are same, so we take it as negative.
  • So, positive numbers have MSB as 0 in their representaion and negative have MSB as 1.

Float and Double

float is a 32-bit IEEE 754 single precision Floating Point Number: 1 bit for the sign, 8 bits for the exponent, and 23 for the value, i.e. float has 7 decimal digits of precision.

double is a 64-bit IEEE 754 double precision Floating Point Number: 1 bit for the sign, 11 bits for the exponent, and 52 bits for the value, i.e. double has 15 decimal digits of precision.

float a = .2;	//valid
float b = 2.;	//valid
//By default every real constant in a program is double

float a = 0.1;
a == 0.1	//false, double is more precise than float

a == 0.1f	//true (f/F)

0.1l	//long double literal (l/L)

0.1 + 0.1 + 0.1 == 0.3	//false, non-terminating recurring decimal on RHS, LHS expression's result has limited precision 

Character

char and int are interchangeable in C as char is internally represented as ASCII integer values from 0 to 255. The range of char is much less than that of int and hence care must be taken on casts.

size_t

  • Its size must be equal to the largest data type size on that machine, typically unsigned long long.
  • always a positive (unsigned) number
  • sizeof() operator returns this
  • Functions like malloc(), memcpy(), etc… has arguments and strlen() returns this type.

Operators

Arithmetic

  • If the size of the result lies outside the range of the type of variable we’re storing it in, it’ll truncate to largest value.
unsigned char c;
c = 255 + 255;		//sets c = 254

//signed char c would set c as -2
  • Division truncates often.
2/3		// 0
9/5		// 1
  • In modulo (%) operation, sign of the numerator always carries over to the result.
-3 % 2  // -1

-3 % -2  // -1

2 % -3  // 1
  • Division works the same as maths after C99.
-3 / -1   // 3
-2 / 1	  // -2
3 / -2	  // -1

Logical

  • Ouput is an int. Either 0 or non-zero value.
  • Can’t use compound assignments with logical operators like &&== or ||==.
  • Short-circuit logic applies to both && and ||.
sizeof(x && y) 	// 4


//Short-circuiting in logical operators
foo() && bar()		// if foo() returns 0, bar() won't be called
foo() || bar() 		// if foo() returns a non-zero value, bar() won't be called

Relational

  • Ouput is an int. Either 0 or non-zero value.
sizeof(x == y)		// 4

Bitwise

  • using them on negative numbers is undefined behaviour.
-2 << 1 	//undefined
  • shifting a number more than its number of bits is undefined behaviour.
1 << 33		//undefined, if int is 32-bits
  • n left shifts = multiplication by 2 ^ n and n right shifts = division by 2 ^ n.
  • Arithmetic shift preserves sign-bit (rotates all except sign-bit), whereas logical shift doesn’t preserve sign-bit.

Pre and Post Unary

  • only l-value must be passed as operands, no r-value or even expressions.
int a = 8;
a++;	//correct

8++;	//compile-time error

int a = 1, b= 2;
++(a+b);	 //compile-time error

++(-i);		//compile-time error
(++i)++;	//compile-time error

int t = -i;
++t;	  //correct
  • Precedence of postfix is higher than that of prefix.
postfix           >      prefix = dereferncing

LR associative			   RL associative
  • Return value of pre and post differs (obviously)
int a = 8;

return a++;		//returns 8

return ++a;		//returns 9

Comma

Two uses:

  1. As an operator: act as a sequence point, lowest precedence.
//As an operator it evaluates both sides starting from left, sets sewuence point after each and returns the rightmost evaluation's result

int a = (1, 2, 5, 10);		// a = 10

int b = (foo(), bar());		// a = bar()


//we can also do this:
printf("Hello"),
printf("World"),
if(a == b) { ... }
  1. As a separator: used in function parameter separation, variable declaration, etc…
int a = 1, 2, 3;	//acts as a separator as it is expected during declaration, compile-time error saying expected identifier at 2's place

int a;
a = 1, 2, 3;	//acts as an separator after setting a = 1

int a = (1, 2, 3); 		//acts as an operator and sets value as 3


return a, b, c;		//c is returned
  • Comma as an operator can’t operate on l-values.
(a, b) = 10;	//compile-time error

Sequence Points: All side-effects (changes and computations on variables) have been finished before a sequence point. We have them naturally in &&, ||, ?:, start of loops, etc.. or we can make them using ,. Cases such as foo() + bar() or printf("%d %d", foo(), bar()) don’t have any sequence points and hence we don’t have a defined order of execution among operands or parameters of a function.

Assignment

//what's the return type of an assignment operation? Ans: the value it is assigning.

int a = (b = 4);	//a is set as 4

if(a = 0)	 //false
if(b = 4) 	 //true
if(c = 'c')	 //true

Conditional

// it has a return type and operands must have same return type (foo and bar) else suprises 
int a = (b == 1) ? foo() : bar();

a > b ? (c=10) : (d=10);	//use brackets otherwise error

sizeof

  • evaluated at compile-time
  • returns size_t type
  • can be applies to any primitive or aggregate type
  • no expression is evaluated inside
int a = 1, b = 2;

printf("%d", sizeof(a+=b));		// 4 
printf("%d", a);				// 1 
  • brackets are optional
sizeof a+b;		//valid

Precedence, Associativity, and Order of Evaluation

Link: here

//no chaining in C, unlike Python

if(a < b < c){ ... }		//no errors

//evaluated as: (a < b) < c	

Control Statements

Loops


for(;;); 	//valid

for(foo();foo();foo()){
	foo();
}

//TO-DO: sequence of execution in a 'for' loop
  • while is slightly dangerous, we may forget update clause or continue may appear before updation.
  • do-while loop always runs atleast once.

Switch Case

  • fall-through in nature, don’t forget to break; after every case.
  • default: is optional and its position of default: never matters.
  • anything else inside switch but not in any case is never executed.
  • case labels must be integer literals or constant expressions only, int, char, and enum.
  • two case labels can’t have the same values.
  • range based case labels: low ... high, low and high must only be integer literals. Errors will occur if overlapping case values or high < low.
switch (var) {
	case 1:
		break;
	case 'a':
		break;
	case 'b' ... 'c':
		break;
	case 1+2:
		break;
	case 'a'||1:
		break;
	default:
		break;
	}

goto, continue, and break

goto label;

label: 
continue;		

//skips current iteration in any loop as soon as it's encountered, used only with loops
break;

//goes out of loop as soon as it is encountered, to be used only with switch and loops