2023 Exam B
QUESTION 1
- A
#include <stdio.h>
struct s;
typedef struct s * s_type;
int main(void) {
s_type var;
printf("The address of var is %p\n", var);
return 0;
}
- B
#include <stdio.h>
#include <stdlib.h>
struct s;
typedef struct s * s_type;
int main(void) {
s_type var = (s_type) malloc(sizeof(struct s));
printf("The address of var is %p\n", var);
return 0;
}
For each \(A\) and \(B\) codes:
-
Will the code compile?
-
Will the code run?
Justify your answer.
Analysis of Code A
-
Will the code compile?
Yes, the code will compile. The structs
is declared as an incomplete type, but this is allowed when defining a pointer to it (s_type
). The code does not attempt to dereference the pointer or usesizeof
on the incomplete type, so there are no compilation errors. -
Will the code run?
Yes, the code will run and execute to completion. However, note thatvar
is an uninitialized pointer, so printing its value with%p
results in undefined behavior (it may print garbage,NULL
, or potentially cause issues, but the program will still run).
Analysis of Code B
-
Will the code compile?
No, the code will not compile. The expressionsizeof(struct s)
is invalid becausestruct s
is an incomplete type (only declared, not defined), and C does not allowsizeof
on incomplete types. This will result in a compilation error. -
Will the code run?
No, the code will not run because it fails to compile.
QUESTION 2
Implement a function count
in JAVA Python, and C++ with the following restrictions:
-
Function
count
takes a collection of elements, you can choose which collection to use for each language, and returns an integer value. -
Function
count
must return how many elements in the collection makes a functionaccept
returntrue
. -
Function
accept
takes one argument (with the same type as the elements in the collection taken bycount
); it will returntrue
if the element satisfies the condition or property stated byaccept
, otherwise it will returnfalse
. -
The collection on which
count
operates can be empty but it cannot beNULL
orNone
. -
If you use a function argument to represent the
accept
function, this cannot beNULL
. -
The function must be generic, it must work with any type for the elements in the collection; if you use an argument to pass a function
accept
, this cannot beNULL
orNone
. -
You can add a
size
argument if you are using an array in either C or C++.
For each implementation you need to explain advantages and disadvantages of the implementation.
Java
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
public class Count {
public static <T> int count(T[] elements, Predicate<T> accept) {
if (elements == null) {
throw new IllegalArgumentException("Collection cannot be null");
}
if (accept == null) {
throw new IllegalArgumentException("Accept function cannot be null");
}
int counter = 0;
for (int i = 0; i < elements.length; i++) {
if (elements[i] != null && accept.test(elements[i])) {
counter++;
}
}
return counter;
}
}
Advantage:
It checks if the array or function is null and skips null items in the array so it won't crash if the function can't handle nulls.
It uses generics, so you can use it with numbers, strings, or anything, easily with short code like lambdas.
Disadvantages
It only takes arrays, not lists or other collections, so you need to change your data to an array and it doesn't work on LinkedList for example
It relies on accept
method, so it is user's responsibility to define it well
Python
def count(elements, accept):
if elements is None:
raise ValueError("Collection cannot be None")
if accept is None:
raise ValueError("Accept function cannot be None")
counter = 0
for e in elements:
if e is not None and accept(e):
counter += 1
return counter
Advantages
It works on any collections like lists or sets, not just array, so it's easy to use with different collection
It checks if the collection or function is None and raises an error, which stops crashes
It skips None items before checking them, so it won't crash if the function can't handle None.
Disadvantages
It doesn't check if "elements" can be iterated so it could crash if you pass something wrong like a number.
We cannot check if array and elem are actually the type we require
It relies on accept
method, so it is user's responsibility to define it well
C++
#include <cassert>
template<typename T>
using Predicate = bool (*)(T);
template<typename T>
int count(T elements[], const int size, Predicate<T> accept) {
assert(elements != nullptr);
assert(size >= 0);
assert(accept != nullptr);
int counter = 0;
for (int i = 0; i < size; ++i) {
if (elements[i] != nullptr && accept(elements[i])) {
++counter;
}
}
return counter;
}
#include <cassert>
#include <type_traits> // For std::is_pointer_v
template<typename T>
using Predicate = bool (*)(T);
template<typename T>
int count(T elements[], const int size, Predicate<T> accept) {
if (elements == nullptr) throw std::invalid_argument("elements cannot be null");
if (size < 0) throw std::invalid_argument("size cannot be negative");
if (accept == nullptr) throw std::invalid_argument("accept cannot be null");
int counter = 0;
for (int i = 0; i < size; ++i) {
if constexpr (std::is_pointer_v<T>) {//If T is a pointer, it will have null element
if (elements[i] != nullptr && accept(elements[i])) {
++counter;
}
} else { //If T is not a pointer like int, it won't have null element
if (accept(elements[i])) {
++counter;
}
}
}
return counter;
}
Advantages
It uses templates to handle different types like ints or strings, making it flexible
It uses asserts to check for null array, negative size, or null function and skips null items in the array, which avoids crushing due to error argument
Disadvantages
It only takes arrays, not lists or other collections, so you need to change your data to an array
There is no way to verify size is indeed the size of the array. If size is too large, it is likely to cause illegal memory access
It relies on accept
method, so it is user's responsibility to define it well
Question 3
Is a linearly organized collection of values implemented over linked nodes, an Abstract Data Type? Justify your answer.
No
An abstract data type just outlines what kind of data you're dealing with and what actions you can take on it, but it ignores the details like how the data is actually stored in memory or how those actions are implemented
But a linked list is a specific implements—using nodes connected by pointers (each node holds a value and a link to the next). This is a concrete implementation
Question 4
Is the definition of the Copy-Constructor correct?
class my_class {
public:
//Empty constructor, initializes the value field with 0
my_class(): value(0) {}
//Constructor taking a value for the value field
my_class(int v): value(v) {}
//Copy-Constructor
my_class(my_class other) {
value = other.value;
}
private:
int value;
};
Justify your answer
No, the copy constructor should pass by reference. If we pass by value, to copy the value of an object, the copy constructor is needed, then we will have a circular requirement
QUESTION 5
Given the two following Python modules:
a.py
def function_in_a():
print("Hello, I'm A!\n")
import b
function_in_a()
b.function_in_b()
b.py
def function_in_b():
print("Hello, I'm B!\n")
if __name__ == "__main__":
import a
a.function_in_a()
function_in_b()
What would be the output of running:
-
python3 a.py
-
python3 b.py
Justify your answer for each case.
- Output of
python3 a.py
Hello, I'm A!
Hello, I'm B!
Justification: When running a.py directly, it's the main module (__name__ == "__main__"
). It defines function_in_a
, imports b.py (which defines function_in_b
but skips its if __name__ == "__main__"
block since b is imported, not main). Then a.py calls function_in_a()
(prints A's message) and b.function_in_b()
(prints B's message).
- Output of
python3 b.py
Hello, I'm A!
Hello, I'm B!
Hello, I'm A!
Hello, I'm B!
Justification: When running b.py directly, it's the main module (__name__ == "__main__"
). It defines function_in_b
, then enters the if
block: imports a.py, which executes a.py's module-level code—defining function_in_a
, importing b.py again (as a separate module "b", which defines its own function_in_b
but skips the if
block), calling function_in_a()
(prints A's message once), and calling the imported b.function_in_b()
(prints B's message once). Back in b.py's if
block, it then calls a.function_in_a()
(which prints A's message again) and its own function_in_b()
(prints B's message again).
QUESTION 6
Explain why pointer arithmetic in void pointers is considered an undefined behavior?
Void pointers (void*
) don't have a specific type size associated with them (unlike int*
where you know to add sizeof(int) for p + 1
). Without knowing how many bytes to increment/decrement, pointer arithmetic (like adding offsets) can't be reliably performed, so the C/C++ standards classify it as undefined behavior to avoid unpredictable results or crashes.
QUESTION 7
In C++, given a generic node
class with a value
field, holding the node's value, and a next
field, holding a pointer to the next node and the following functions:
- Constructor
node(T value)
That initializes the node's value to value
, and the node's next
reference to NULL
.
- Setter functions
void set_value(T new_value);
void set_next(node<T> * new_next);
Which respectively sets the node's value and node's next reference with new values. - Getter functions
T get_value();
node<T> * get_next();
Which respectively returns the node's value and node's next reference.
A generic list
class with a head
field declared as node<T> * head;
and a size
field declared as int list_size
.
Complete the following code (specified by a //TODO: complete comment
):
//Empty constructor: constructs a new, empty, list.
list() {
//TODO: complete
}
//Destructor
~list() {
//TODO: complete
}
//Removes the value at a specific position
T remove_at(int pos) {
assert(pos >= 0);
assert(pos < list_size);
//TODO: Complete
}
//Returns the value at a specific position
T at(int pos) {
assert(pos >= 0);
assert(pos < list_size);
node<T> * current = head;
int i = 0;
while (i < pos) {
//TODO: complete
}
return current.get_value();
}
int size() {
return list_size;
}
Answer
class list {
public:
node<T>* head;
int list_size;
// Empty constructor: constructs a new, empty, list.
list() {
head = nullptr;
list_size = 0;
}
// Destructor
~list() {
node<T>* current = head;
while (current != nullptr) {
node<T>* next_node = current->get_next();
delete current;
current = next_node;
}
}
// Removes the value at a specific position
T remove_at(int pos) {
assert(pos >= 0);
assert(pos < list_size);
node<T>* current = head;
T removed_value;
if (pos == 0) {
// Removing the head node
removed_value = head->get_value();
head = head->get_next();
delete current;
} else {
// Traverse to the node just before the one to be removed
node<T>* prev = nullptr;
for (int i = 0; i < pos; ++i) {
prev = current;
current = current->get_next();
}
// `current` is now the node to remove
removed_value = current->get_value();
prev->set_next(current->get_next());
delete current;
}
list_size--;
return removed_value;
}
// Returns the value at a specific position
T at(int pos) {
assert(pos >= 0);
assert(pos < list_size);
node<T>* current = head;
int i = 0;
while (i < pos) {
current = current->get_next();
i++;
}
return current->get_value();
}
int size() {
return list_size;
}
};
QUESTION 8
Can a program always be considered an algorithm?
Justify your answer.
No The program Is a set of instruction that can be executed
But the algorithm should be a finite set of instruction that can be executed, which means if a program have infinite instruction is not the algorithm
For example
while(True):
print("Hello, world")