Notes about C ( work in progress )
From the Beej's guideAbout
- Originally created to construct programs for the Unix operating system, and a flavor of C called ANSI C was also used in Plan9.
- Also was then used to rewrite the Unix kernel.
- Also was then used to rewrite the Unix kernel.
- Compilers for C come built in with most operating system, eg: gcc, clang.
Language
-
Anything between the digraphs
/*
and*/
is a comment. Also//
-
#include
tells the C Preprocessor to include a specified file. -
Two stages in compilation of C code
- Preprocessor: Anything that starts with the pound sign `#` or "octothorpe" is handled by the Preprocessor ( Common _preprocessor directives_ )
- Preprocessor -> processes `code starting with #` -> output -> Compiler -> compiles -> `assembly code`
- Compiler: Rest all will be handled by the compiler.
-
Basic point but the
stdio.h
header file allows to perform bunch of I/O functionality. -
The main function is called automatically when the program starts
executing.
- Nothing else will be executed before main()
-
Languages that typically aren’t compiled are called interpreted languages. But as we mentioned with Java and Python, they also have a compilation step. And there’s no rule saying that C can’t be interpreted. (There are C interpreters out there!) In short, it’s a bunch of gray areas. Compilation in general is just taking source code and turning it into another, more easily-executed form.
Variable
- A variable is a name for some data that's stored in memory at some address.
- Looking at variables this way helps to understand pointers better. (later)
- Variable type needs to be defined when declaring, and that type will stay unchanged until it falls out of scope.
- Always explicitly initialize variables to some values before you use them.
-
printf()
%d -> integer
%s -> string
%f -> float
%zu -> size_t
%p -> pointer
-
C originally did not have boolean
-
In C,
0
meansfalse
, and any other number for eg.1
or-44
meanstrue
-
If you
#include<stdbool.h>
, you can also use thebool
keyword.
-
In C,
-
C has ternary operator
-
y += x > 10 ? 17 : 37
: meansif (x > 10) y += 17; else y += 37;
-
-
i++ & i--
the value of the expression is first computed with the value as-is. -
++i & --i
this happens before the expression is evaluated. -
sizeof
operator is used to get the size ( in bytes ) of a particular expression.-
C has a special data type to represent the return value of
sizeof
. It'ssize_t
. - All we know is that it's an unsigned integer type.
-
%zu
is the format specifier for typesize_t
- Also this is a compiler-time operation.
-
C has a special data type to represent the return value of
-
switch
only works with equality comparisons with constant numbers.- evaluates an expression to an integer value, jumps to the case that corresponds to that value. Execution continues from the point.
-
If a
break
statement is encountered, then execution jumps out of theswitch
.-
If no
break
is applied, it will fall through. It will execute all the cases without abreak
.
-
If no
- is
switch
faster; maybe, maybe not?!! -
but
switch
cannot do things like>
or<=
and floating point and other types.
-
A not-uncommon use of
while
loops is for infinite loop where you repeat while true.while(1) { printf("1 is always true, so this repeats forever\n"); }
-
All three parts of a
for
loop are optional.- An empty for loop
for(;;)
will run forever.
- An empty for loop
Functions
- Arguments and return value types have to be predeclared.
-
A parameter is a special type of local variable into which the
arguments are copied.
- Thus, a parameter is a copy of the argument, not the argument itself.
- Functions will be defined before being called.
-
Function parameters
-
Passing by value -> basically means we copy the value of the
argument into the parameter of the function.
-
Unless returned, no value in the parent for eg.
main
changes.
-
Unless returned, no value in the parent for eg.
-
Passing by value -> basically means we copy the value of the
argument into the parameter of the function.
-
Function prototypes: looks something like
int foo(void);
where we have created a prototype before defining the function and it's okay to call it before definition. -
Use
void
keyword as parameter instead of an empty parameter for function definition.
Pointers
-
When you have a data type (like your typical
int
) that uses more than a byte of memory, the bytes that make up the data are always adjacent to one another in memory. Sometimes they’re in the order that you expect, and sometimes they’re not46. While C doesn’t guarantee any particular memory order (it’s platform-dependent), it’s still generally possible to write code in a way that’s platform-independent where you don’t have to even consider these pesky byte orderings. - A pointer holds the address of a data.
- It tells us where in memory a data point is stored.
- A dereference operator is used to get the original data indirectly from the pointer variable.
-
&i
ampersand will get the address of the variablei
, it cannot be on the left side of a statement. -
The main use of pointers is when you might want to return more than
a single value from the execution of a function. ( while we know
functions don't allow multiple returns like in Go. )
- Pointers will allow you to point that to that address, data and be able to mutate it.
- A question: do we always use pointers in case of complex functions? ( mutating multiple values for eg. )
-
When using pointers,
j
for eg. isthe address
while*j
will give thevalue
at that address. -
Any pointer variable of any pointer type can be set to a special
value called
NULL
. This indicates that this pointer doesn’t point to anything.int *p; p = NULL;
-
Despite being called
the billion dollar mistake by its creator49, the
NULL
pointer is a good sentinel value50 and general indicator that a pointer hasn’t yet been initialized. -
(Of course, like other variables, the pointer points to garbage
unless you explicitly assign it to point to an address or
NULL
.)
Array
- Array length can be read inside a function with the array passed as a argument.
-
sizeof(int)
is basically calling the sizeof fn to check on the size of the int array without creating an array. - When initializing an array when you don't need to fill the array to the size. But you can't definitely add more.
Strings
-
char *s = "Hello, world!";
is a pointer initialization to the first character in the string, but is immutable.s[0] = 'z';
is not allowed. -
Quite the opposite when you
char s[] = "Hello, world!";
. -
C follows a different route with implementing strings in the
language, wherein it stores the bytes of a string, and mark the end
of a string with a special bye called the terminator.
- A pointer to the first character in the string
- A zero-valued byte ( or NUL character ) somewhere in memory after the pointer that indicates the end of the string.
- A NUL character can be written in C as
\0
.
Structs
- Structs is a convenient to handle data. Multiple parameters can be replaced with a single type.
- It basically allows a User to create types, to define data models.
-
struct car { ... }
is how you define a struct type. -
struct car honda
is how you define a variable of struct type car. -
struct car honda = {"honda city", 123, 44
} you could initialize it this way but imagine if the struct series of the data is changed, that would break the whole code. -
Instead you could add
struct car honda = {.name="honda city", .speed=123, .price=44
}` -
There are basically two cases when you’d want to pass a pointer to
the
struct
as the argument to a function:-
You need the function to be able to make changes to the
struct
that was passed in, and have those changes show in the caller. -
The
struct
is somewhat large and it’s more expensive to copy that onto the stack than it is to just copy a pointer69
-
You need the function to be able to make changes to the
-
We just need to write the body. One attempt might be:
void set_price(struct car *c, float new_price) { c.price = new_price; // ERROR!! }
-
That won’t work because the dot operator only works on
struct
s… it doesn’t work on pointers tostruct
s. -
Ok, so we can dereference the
struct
to de-pointer it to get to thestruct
itself. Dereferencing astruct car*
results in thestruct car
that the pointer points to, which we should be able to use the dot operator on:
void set_price(struct car *c, float new_price) {
(*c).price = new_price; // Works, but is ugly and non-idiomatic :(
}
-
So this is where the arrow operator comes in. The Arrow operator
helps refer to fields in pointers to
structs
.void set_price(struct car *c, float new_price) { c->price = new_price; // This looks so much better! }
File I/O
-
Before diving into anything else,
-
stdin
: Standard Input, generally the keyboard by default. -
stdout
: Standard Output, generally the screen by default. -
stderr
: Standard Error, generally the screen by default, as well. - These are the core I/O streams assigned to all programs or processes running on a Unix system.
-
-
Reading Text files:
- Streams are generally categorized in two different ways: text and binary.
-
There is a special character defined as a macro:
EOF
. This is whatfgetc()
will return when the end of the file has been reached and you’ve attempted to read another character fgetc
- to read a character a timefgets
- to read lines
-
Writing Files
-
fputc, fputs, fprintf
are basically the one-to-one similar to the readers.
-
-
Binary
fwrite and fread
-
Caveat: It's not ideal to just
fwrite()
an entire struct out to a file when you don't know where the padding will end up. -
Will need to come back to reading about the [[Endianness]] of an architecture.
-
So the problem is resolved using Protocol Buffers created by
Google.
- I have come across protocol buffers before this but never knew the purpose.
typedef
-
Take an existing type and make an alias for it with
typedef
. -
typedef int apple
=>apple x = 10
. Where we are defining a variable x of type"apple"
which is in turn the same as type `"int". -
Application:
-
Scoping
- typedef follows regular scoping rules.
- For this reason, it's quite common to find typedef at global scope in a file so that all functions can use the new type at will.
-
typedef
ing a struct.-
typedef struct animal animal;
=> allowsanimal z;
-
-
Scoping
void
Pointer>
-
Sometimes it’s useful to have a pointer to a thing
that you don’t know the type of.
-
There are basically two use cases for this.
-
A function is going to operate on something byte-by-byte. For
example,
memcpy()
copies bytes of memory from one
pointer to another, but those pointers can point to any type.
memcpy()
takes advantage of the fact that if you
iterate through char*
s, you’re iterating through
the bytes of an object no matter what type the object is. More
on this in the
Multibyte Values
subsection.
-
Another function is calling a function you passed to it (a
callback), and it’s passing you data. You know the type of the
data, but the function calling you doesn’t. So it passes you
void*
s—’cause it doesn’t know the type—and you
convert those to the type you need. The built-in
qsort()
84
and
bsearch()
85
use this technique.
-
You can just using
number_of_elements
*
sizeof(type)
to define how many elements of what size
to be moved, let's say if you are using memcpy()
.
-
If we didn't have
void
pointer, we would have had
to use function definitions for each of the data type
memcpy()
functions.
- You cannot do pointer arithmetic on a
void*
.
- You cannot dereference a
void*
.
-
You cannot use the arrow operator on a
void*
, since
it’s also a dereference. 4. You cannot use array notation on a
void*
, since it’s also a dereference, as well86
-
Important piece of example code ```c void
my_memcpy(void dest, void
src, int byte_count) { // Convert voids to chars char s = src, *d = dest;
// Now that we have char*s, we can dereference and copy them while
(byte_count--) {
*d++ = *s++;
}
// Most of these functions return the destination, just in case //
that's useful to the caller. return dest; }
- possible char unsigned size
-
A function is going to operate on something byte-by-byte. For
example,
memcpy()
copies bytes of memory from one pointer to another, but those pointers can point to any type.memcpy()
takes advantage of the fact that if you iterate throughchar*
s, you’re iterating through the bytes of an object no matter what type the object is. More on this in the Multibyte Values subsection. -
Another function is calling a function you passed to it (a
callback), and it’s passing you data. You know the type of the
data, but the function calling you doesn’t. So it passes you
void*
s—’cause it doesn’t know the type—and you convert those to the type you need. The built-inqsort()
84 andbsearch()
85 use this technique.
number_of_elements
*
sizeof(type)
to define how many elements of what size
to be moved, let's say if you are using memcpy()
.
void
pointer, we would have had
to use function definitions for each of the data type
memcpy()
functions.
- You cannot do pointer arithmetic on a
void*
. - You cannot dereference a
void*
. -
You cannot use the arrow operator on a
void*
, since it’s also a dereference. 4. You cannot use array notation on avoid*
, since it’s also a dereference, as well86
Important piece of example code ```c void my_memcpy(void dest, void src, int byte_count) { // Convert voids to chars char s = src, *d = dest;
// Now that we have char*s, we can dereference and copy them while (byte_count--) {
*d++ = *s++;
}
// Most of these functions return the destination, just in case // that's useful to the caller. return dest; }
char type |
Minimum | Maximum |
---|---|---|
signed char |
-128 |
127 |
unsigned char |
0 |
255 |