Skip to main content

Week 1

Divisibility by 2^n ### Working Examples: Divisibility by Powers of 2 in Binary

Summary of Observations

In binary if LSB:

  1. Last bit $$(= 0 )$$: Divisible by 2.
  2. Last 2 bits $$(= 00)$$: Divisible by 4.
  3. Last 3 bits $$(= 000)$$: Divisible by 8.
  4. For $$(2^n)$$, the last $$(n)$$ bits $$(= 0)$$: Divisible by $$(2^n)$$.

This pattern works because binary inherently encodes numbers as sums of powers of 2, making divisibility checks straightforward!

Working Examples

1. Multiple of 2

A number is divisible by 2 if its last bit is 0.

  • $$(6_{10} = 110_2)$$:

    • Last bit = 0 → Divisible by 2.
    • Calculation: $$(6 \div 2 = 3)$$ (no remainder).
  • $$(7_{10} = 111_2)$$:

    • Last bit = 1 → Not divisible by 2.
    • Calculation: $$(7 \div 2 = 3)$$ (remainder 1).

2. Multiple of 4

A number is divisible by 4 if its last 2 bits are 00.

  • $$(12_{10} = 1100_2)$$:

    • Last 2 bits = 00 → Divisible by 4.
    • Calculation: $$(12 \div 4 = 3)$$ (no remainder).
  • $$(14_{10} = 1110_2)$$:

    • Last 2 bits = 10 → Not divisible by 4.
    • Calculation: $$(14 \div 4 = 3)$$ (remainder 2).

3. Multiple of 8

A number is divisible by 8 if its last 3 bits are 000.

  • $$(24_{10} = 11000_2)$$:

    • Last 3 bits = 000 → Divisible by 8.
    • Calculation: $$(24 \div 8 = 3)$$ (no remainder).
  • $$(26_{10} = 11010_2)$$:

    • Last 3 bits = 010 → Not divisible by 8.
    • Calculation: $$(26 \div 8 = 3)$$ (remainder 2).

4. Testing Higher Powers

A number is divisible by $$(2^n)$$ if its last $$(n)$$ bits are all 0.

  • $$(32_{10} = 100000_2)$$:

    • Last 5 bits = 00000 → Divisible by $$(2^5 = 32)$$.
    • Calculation: $$(32 \div 32 = 1)$$ (no remainder).
  • $$(36_{10} = 100100_2)$$:

    • Last 5 bits = 00100 → Not divisible by $$(2^5 = 32)$$.
    • Calculation: $$(36 \div 32 = 1)$$ (remainder 4).

Theory

Why Do Last Bits Determine Divisibility by Powers of 2?

In binary, numbers are represented as sums of powers of 2:

$[\text{Binary Number:} b_k b_{k-1} \dots b_2 b_1 b_0 = b_k \cdot 2^k + b_{k-1} \cdot 2^{k-1} + \dots + b_1 \cdot 2^1 + b_0 \cdot 2^0]$

The last $$(n)$$ bits of a binary number represent the portion that determines divisibility by $$(2^n)$$. If these bits are all 0, the entire number is divisible by $$(2^n)$$.

Intuition

1. Base and Place Values

  • Binary is a base-2 system, so each position corresponds to a power of 2.
  • The last $$(n)$$ bits contribute to values smaller than $$(2^n)$$, and if they are all 0, the number has no remainder when divided by $$(2^n)$$.

2. Example: Multiples of 4

  • $$(4 = 2^2)$$. The last 2 bits must be 0, meaning: $[ b_1 \cdot 2^1 + b_0 \cdot 2^0 = 0 ]$
  • This ensures that the number contains $$(2^2)$$ or higher powers, so it's divisible by 4.

3. Scaling to Higher Powers

  • For multiples of $$(2^n)$$, having $$(n)$$ trailing zeros ensures the number is built entirely from $$(2^n)$$ or larger powers (e.g., $$(2^n, 2^{n+1}, \dots)$$) with no smaller contributions.

Real-World Analogy

Think of binary numbers as "buckets" of powers of 2:

  • If the lower buckets ($$(2^0, 2^1, \dots, 2^{n-1})$$) are empty (all 0s), the number is cleanly divisible by $$(2^n)$$. If there's even one drop (a 1 in these bits), divisibility is broken.

Summary

The last $$(n)$$ bits in binary indicate divisibility by $$(2^n)$$ because binary inherently represents values in powers of 2. Zeros in these positions mean no leftover contributions smaller than $$(2^n)$$, ensuring clean divisibility.


Q6 Identify issues with code.

Steps

Recap of Code:

void main() {
    char array[10] = {5, 10, 15, 20, 25, 30, 35, 40, 45, 50};
    char *p = array;

    p = p + (4);  // Line 4
    *p = *p + 5;  // Line 5

    p = p + (-5); // Line 6
    *p = *p + 5;  // Line 7

    p = p + (5);  // Line 8
    *p = *p + 5;  // Line 9
}

Which lines cause pointer faults?

Key Assumption:

  • Memory starts at &array[0] = 256, or 0x100 in hexadecimal.
  • Each char takes up 1 byte in memory.

Step-by-Step Analysis with Memory Addresses:

  1. Initial Setup:

    • Array contents: {5, 10, 15, 20, 25, 30, 35, 40, 45, 50}.
    • char *p = array means p initially points to array[0], which is at address 0x100.
  2. Line 4 (p = p + (4);):

    • p is moved 4 positions forward, so it points to array[4], which is at address 0x104.
  3. Line 5 (*p = *p + 5;):

    • *p modifies the value at array[4]. Initially, array[4] = 25. After adding 5, array[4] = 30.
    • After line 5: p = 0x104 and *p = 30.
  4. Line 6 (p = p + (-5);):

    • p moves back 5 positions, so it now points to array[0] at address 0x100.
  5. Line 7 (*p = *p + 5;):

    • *p modifies the value at array[0]. Initially, array[0] = 5. After adding 5, array[0] = 10.
    • After line 7: p = 0x100 and *p = 10.
  6. Line 8 (p = p + (5);):

    • p moves forward 5 positions, so it now points to array[5] at address 0x105.
  7. Line 9 (*p = *p + 5;):

    • *p modifies the value at array[5]. Initially, array[5] = 30. After adding 5, array[5] = 35.
    • After line 9: p = 0x105 and *p = 35.

Evaluating Each Option:

  1. After line 5: p = 0x104, *p = 30, a[4] is modified:

    • This is true. After line 5, p is at address 0x104, *p = 30, and a[4] (or array[4]) is modified.
  2. After line 5: p = 0x114, *p = 35, a[5] is modified:

    • This is false. After line 5, p is at 0x104, not 0x114, and *p = 30, not 35.
  3. After line 9: p = 0x103, *p = 30, a[3] is modified:

    • This is false. After line 9, p is at 0x105, not 0x103, and a[3] is not modified.
  4. After line 7: p = 0x0fe, *p = Unknown, Index out of bounds:

    • This is false. After line 7, p is at 0x100, not 0x0fe, and *p = 10, so it’s not out of bounds.
  5. After line 7: p = 0x100, *p = 0, a[0] is modified:

    • This is false. After line 7, p = 0x100, but *p = 10, not 0. So this statement is incorrect.
  6. After line 9: p = 0x114, *p = 35, a[5] is modified:

    • This is false. After line 9, p = 0x105, not 0x114, though a[5] is modified.
  7. After line 7: p = 0x0ff, *p = Unknown, Index out of bounds:

    • This is false. After line 7, p is at 0x100, not 0x0ff, so this statement is incorrect.

Conclusion:

The only correct statement is:

  • After line 5: p = 0x104, *p = 30, a[4] is modified.

This matches the memory and pointer operations as detailsed above.


1D arrays and Pointers

Question 8: Find x = *((short*)array+13) where array = 0x1000, Little Endian

  1. Memory Layout: The memory contents are laid out byte by byte as shown in the question. Given that the system is little-endian, the least significant byte is stored first.

    Memory contents (in bytes):

    Address       0   1   2   3
    0x1000       00  01  02  03
    0x1004       04  05  06  07
    0x1008       08  09  0a  0b
    0x100c       0c  0d  0e  0f
    0x1010       10  11  12  13
    0x1014       14  15  16  17
    
  2. Pointer Arithmetic:

    • *((short*)array + 13) means we are treating the memory starting from array (address 0x1000) as an array of short values (2 bytes each).
    • Moving 13 positions forward (short* type moves by 2 bytes) means we are accessing the bytes at address 0x1000 + 13 * 2 = 0x101A.
  3. Little Endian Interpretation:

    • The value at address 0x101A is formed by the two bytes at 0x101A and 0x101B (little-endian means the least significant byte is stored first).
    • From the memory contents, at 0x101A, the value is 1a, and at 0x101B, the value is 1b.
  4. Forming the Result:

    • In little-endian, the least significant byte (1a) comes first, followed by the more significant byte (1b).
    • Therefore, the value is 0x1b1a (in hex).

Thus, x equals 0x1b1a in hexadecimal.

If you are given a multidimensional array.

Q2: Let's break down the expression char* arrayc = &array; *(short*)(array_c + 2);

Lets try to explain it in terms of pointer manipulation and how many bytes will be read Key idea:The +2 in the expression is pointer math based on the type of array_c a char* The number of bytes you read once the pointer is calculated is based on the cast type

Components of the Expression:

  1. array_c + 2:

    • array_c is a pointer to char
    • Adding 2 to array_c moves the pointer 2 bytes ahead because pointer arithmetic takes the size of the data type into account (in this case, char is 1 byte).
    • After this addition, the pointer now points to the third byte of the char array (array_c[2]).
  2. (short*):

    • This casts the resulting pointer from array_c + 2 (which is of type char*) to a short*.
  3. *(short*):

    • The * dereferences the short* pointer, meaning it reads the value stored at the memory location array_c + 2, but since the pointer has been cast to a short*, it reads 2 bytes of data starting from the location array_c + 2.
2D Arrays

Q2. Explain array accesses array[i][j] for int a[16][16] in terms of pointer arithmetic.

In the expression array[i][j] for a 2D array int array[16][16], we can break down how the memory is accessed and calculate which byte is being accessed relative to the start of the array (array[0][0]).

Steps

Background:

  1. Memory Layout for 2D Arrays:

    • In C and C++, 2D arrays like array[16][16] are stored in row-major order. This means that elements of each row are stored contiguously in memory.
    • For an array of integers (int), the size of each element (in most systems) is 4 bytes since an int typically occupies 4 bytes of memory.
  2. Array Access:

    • The expression array[i][j] accesses the element at row i and column j of the 2D array.
    • The total number of columns in the array is 16, meaning each row contains 16 integers.
Memory layout for int array[16][16] (Row-Major Order):

     array[0][0]  array[0][1]  array[0][2]  ...  array[0][15]
     array[1][0]  array[1][1]  array[1][2]  ...  array[1][15]
     array[2][0]  array[2][1]  array[2][2]  ...  array[2][15]
     ...
     array[15][0] array[15][1] array[15][2] ...  array[15][15]
  1. Calculating the Memory Offset:
    • Each row has 16 integers, and each integer takes 4 bytes.
    • To access array[i][j], you first need to skip i rows and then skip j elements within the ith row.

Memory Address Calculation:

  • The total number of elements to skip before reaching array[i][j] is: $$[\text{Offset in elements} = i \times 16 + j]$$ This gives you the total number of elements before array[i][j].

  • To convert this to a byte offset, multiply by the size of each element (which is 4 bytes for int): $$[\text{Byte offset} = (i \times 16 + j) \times 4]$$ This is the number of bytes from the start of array[0][0] to the element array[i][j].

Example:

  • If you want to access array[2][3], the byte offset is: $$[\text{Byte offset} = (2 \times 16 + 3) \times 4 = (32 + 3) \times 4 = 35 \times 4 = 140 \text{ bytes}]$$ So, array[2][3] is 140 bytes from the start of array[0][0].

Week 2

How many bits are required

Q6 How many bits are required to represent the number -1897? in 2’s complement and sign-magnitude? ?

Steps

Group of answer choices

12 (2’s complement), 12 (Sign-mag)
12 (2’s complement), 12 (Sign-mag)
11 (2’s complement), 13 (Sign-mag)
13 (2’s complement), 11 (Sign-mag)
14 (2’s complement), 12 (Sign-mag)

Let's break down how many bits are required to represent the number -1897 in both 2’s complement and sign-magnitude.

1. 2’s Complement Representation:

In 2’s complement, to represent a negative number, we first find its binary form as a positive number, then invert all the bits and add 1.

Step-by-step process:

  • Convert 1897 (the absolute value) into binary:
    • 1897 in binary is 11101101001 (which is 11 bits long for the positive number).
  • In 2's complement, to represent a negative number, we need an additional bit to denote the sign. Therefore, we need 12 bits to represent -1897 in 2’s complement.

Hence, 12 bits are needed for 2's complement.

2. Sign-Magnitude Representation:

In sign-magnitude, one bit is used for the sign (0 for positive, 1 for negative), and the remaining bits represent the magnitude of the number (the absolute value).

  • Since the absolute value of 1897 is 11101101001 (11 bits long), we need an additional bit to represent the sign.
  • Therefore, 12 bits are required for sign-magnitude (1 bit for the sign and 11 bits for the magnitude).

Summary of Bits Required:

  • 2's complement: 12 bits
  • Sign-magnitude: 12 bits

Correct Answer:

  • 12 (2’s complement), 12 (Sign-mag)

So, the correct option is:

12 (2’s complement), 12 (Sign-mag).

Sure! Let's break this down step-by-step, showing exactly how the expression works in binary. The goal is to set every 10th bit in a 32-bit integer to 1, starting from the least significant bit (LSB).

Write 1-line expression

Setting every 10th bit of int x to 1 (starting from the LSB) e.g., every 4th bit …0001001001 ? Legal ops: ! ~ & ^ | << >>

Steps

Step 1: Understanding the Problem

We are dealing with a 32-bit integer. For each bit position, the index starts at 0 from the LSB (rightmost bit) to the MSB (leftmost bit).

  • The 10th bit is at position 9 (since we start counting from 0).
  • The 20th bit is at position 19.
  • The 30th bit is at position 29.

So we want to set these specific bits to 1.

Step 2: Creating the Bit Mask

We need to create a mask that has 1s in the 10th, 20th, and 30th bit positions (binary positions 9, 19, and 29, respectively).

This is done using left shifts (<<) and OR (|) operations.

  • 1 << 9: This shifts the number 1 (which is 00000001 in binary) to the left by 9 positions.

    • The result is 00000000 00000000 00000010 00000000 (in 32 bits).
  • 1 << 19: This shifts the number 1 to the left by 19 positions.

    • The result is 00000000 00000100 00000000 00000000 (in 32 bits).
  • 1 << 29: This shifts the number 1 to the left by 29 positions.

    • The result is 00100000 00000000 00000000 00000000 (in 32 bits).

Now, we combine these using the OR (|) operation.

  • OR operation in binary combines two bits as follows:
    • 0 | 0 = 0
    • 0 | 1 = 1
    • 1 | 0 = 1
    • 1 | 1 = 1

So, the result of the bitwise OR between these shifted values is:

  00000000 00000000 00000010 00000000  (1 << 9)
| 00000000 00000100 00000000 00000000  (1 << 19)
| 00100000 00000000 00000000 00000000  (1 << 29)
-----------------------------------------
  00100000 00000100 00000010 00000000  (final mask)

This gives us the binary mask with 1s at the 9th, 19th, and 29th positions, and 0s elsewhere.

Step 3: Applying the Mask to x

Once the mask is created, we need to apply it to the integer x using the bitwise OR (|) operation. This ensures that the bits in the positions we are interested in (9, 19, and 29) are set to 1, while the rest of the bits in x remain unchanged.

For example, if x is:

  x = 00000000 00000000 00000000 00000000  (initial value of x)

We apply the OR operation between x and the mask:

  00000000 00000000 00000000 00000000   (x)
| 00100000 00000100 00000010 00000000   (mask)
-------------------------------------------
  00100000 00000100 00000010 00000000   (result)

Now, the bits at positions 9, 19, and 29 are set to 1 in x.

Final Expression

The full expression to achieve this in a 32-bit integer x is:

x = x | (1 << 9 | 1 << 19 | 1 << 29);

Conclusion

  • 1 << 9 shifts 1 to the 9th position (10th bit).
  • 1 << 19 shifts 1 to the 19th position (20th bit).
  • 1 << 29 shifts 1 to the 29th position (30th bit).
  • We combine these with the bitwise OR to set all the necessary bits in x.

By using this approach, you can set every 10th bit (starting from the LSB) in a 32-bit integer x to 1.

What is the size of struct ?

Steps

typedef struct{
        short  fld0;
        double fld1;
        float  fld2[6];
      } my_struct;
      
      my_struct s; 
      my_struct s_array[3];

// Consider int: 4bytes, char: 1byte, float: 4bytes 
// double: 8bytes long: 8bytes pointer: 8bytes 

Explanation of sizeof(my_struct) and s_array Memory Layout

To determine the size of the structure my_struct and the memory occupied by the array s_array, we must consider the alignment and padding rules of the system architecture.


Step 1: Analyze my_struct Fields

Given typedef struct:

  • short fld0: Size is 2 bytes.
    • Typically aligned to its size (2 bytes), but due to alignment of the double field next, padding will be added.
  • double fld1: Size is 8 bytes.
    • Typically aligned to 8 bytes.
  • float fld2[6]: Size is ( 6 \times 4 = 24 ) bytes.
    • Typically aligned to 4 bytes.

Step 2: Memory Layout of my_struct

Layout of my_struct with padding:

+------------+---------+
| fld0       | padding |  (2 bytes for short + 6 bytes padding)
+------------+---------+
| fld1                  |  (8 bytes for double)
+-----------------------+
| fld2[0]               |  (4 bytes for each float)
| fld2[1]               |
| fld2[2]               |
| fld2[3]               |
| fld2[4]               |
| fld2[5]               |
+-----------------------+
  1. fld0 (short): Starts at offset 0, uses 2 bytes.
    • Next field (fld1) requires alignment to 8 bytes, so 6 bytes of padding are added after fld0.
  2. fld1 (double): Starts at offset 8, uses 8 bytes.
  3. fld2[6] (float array): Starts at offset 16, uses ( 6 \times 4 = 24 ) bytes.

Total Size of my_struct:

  • Size of fields = ( 2 + 6 ) (padding for fld0) ( + 8 + 24 = 40 ) bytes.
  • No additional padding is needed because the total size is already aligned to the largest field size (8 bytes for double).

sizeof(my_struct) = 40 bytes


Step 3: Size of s_array

  • s_array[3] contains 3 instances of my_struct.
  • Total size of s_array = ( 3 \times sizeof(my_struct) = 3 \times 40 = 120 ) bytes.

ASCII Art Layout

Single my_struct (40 bytes):

Offset   Field          Size
0        fld0           2 bytes
2        padding        6 bytes
8        fld1           8 bytes
16       fld2[0]        4 bytes
20       fld2[1]        4 bytes
24       fld2[2]        4 bytes
28       fld2[3]        4 bytes
32       fld2[4]        4 bytes
36       fld2[5]        4 bytes

Array s_array[3] (120 bytes):

Each struct is laid out consecutively in memory:

Struct 0:
0        fld0           2 bytes
2        padding        6 bytes
8        fld1           8 bytes
16       fld2[0]        4 bytes
...      fld2[5]        4 bytes
40       End of struct 0

Struct 1:
40       fld0           2 bytes
...      (same layout)
80       End of struct 1

Struct 2:
80       fld0           2 bytes
...      (same layout)
120      End of struct 2

Final Answers

  • sizeof(my_struct): 40 bytes
  • Memory occupied by s_array[3]: 120 bytes
---