C Language Syntax: A Complete Reference for CSE Students
C syntax comes down to a small set of rules: every program starts at main, every statement ends with a semicolon, every variable has a declared type, and memory is yours to manage through pointers. Learn those four ideas well and the rest of the language is detail you can look up.
This is a working reference for CSE students preparing for semester finals, viva boards and job interviews. It covers program structure, compiling with gcc, data types and their sizes, printf and scanf, operators, control flow, functions, arrays, strings, pointers, structs and malloc. Every code block here is a complete snippet that compiles, and I've kept the explanations short enough to revise from the night before an exam.
Key takeaways
- Type sizes aren't fixed by the standard. C only sets minimums:
intis at least 16 bits andlongat least 32. Usesizeofto check on your machine. - Always compile with warnings on:
gcc -Wall -Wextra -std=c17 file.c -o file. Most beginner bugs show up as warnings first. scanfneeds addresses. Write&xfor plain variables, but not for arrays like acharbuffer.- An array name decays to a pointer to its first element in most expressions, which is why arrays passed to functions lose their size.
- Every
mallocneeds a matchingfree, and you should check the returned pointer forNULLbefore using it.
The structure of a C program
A C program is a set of functions, and execution begins in main. Here's the smallest useful program:
#include <stdio.h>
int main(void)
{
printf("Hello, Dhaka!\n");
return 0;
}
Line by line:
#include <stdio.h>is a preprocessor directive. It pulls in the declarations forprintf,scanfand other standard I/O functions.int main(void)declares thatmaintakes no arguments and returns anintto the operating system. Examiners sometimes acceptvoid main(), but it isn't standard C. Don't write it.- Curly braces group statements into a block.
return 0;tells the OS the program finished successfully. A non-zero value signals an error.
Comments are either /* block */ or // line. The line form has been standard since C99.
Compiling C with gcc
You write source code in a .c file, and a compiler turns it into an executable. On Linux, macOS (through the command line tools) or Windows (through MinGW or WSL), the usual choice is GCC:
gcc -Wall -Wextra -std=c17 hello.c -o hello
./hello
-Wall -Wextra turns on the warnings that catch most mistakes. -o hello names the output file; without it you get a.out. -std=c17 pins the language version. That flag matters more than it used to: GCC 15 changed its default from -std=gnu17 to -std=gnu23, so older code that uses bool or true as ordinary identifiers can break on a newer compiler. Pinning the standard keeps lab machines and your laptop in agreement.
Behind that one command, four stages run: preprocessing (handles #include and #define), compilation to assembly, assembly to an object file, and linking into an executable. Viva boards like asking for these four in order. You can stop after each with -E, -S and -c.
Data types and their sizes
C's basic types are char, int, float and double, plus the modifiers short, long, signed and unsigned. The exact sizes are implementation-defined. The C standard only guarantees minimum widths, listed in section 5.2.5.3.2 of the C23 working draft (N3220): a byte (CHAR_BIT) is at least 8 bits, short and int at least 16, long at least 32 and long long at least 64. sizeof(char) is always 1.
In practice, sizes depend on the platform. The first column below is what sizeof printed on my x86-64 Linux machine with GCC 13. The second comes from Microsoft's data type ranges page for its C/C++ compiler.
| Type | Standard minimum | 64-bit Linux, GCC (bytes) | Windows, MSVC (bytes) | printf specifier |
|---|---|---|---|---|
char | 8 bits | 1 | 1 | %c |
short | 16 bits | 2 | 2 | %hd |
int | 16 bits | 4 | 4 | %d |
long | 32 bits | 8 | 4 | %ld |
long long | 64 bits | 8 | 8 | %lld |
float | not fixed | 4 | 4 | %f |
double | not fixed | 8 | 8 | %f (printf), %lf (scanf) |
long double | not fixed | 16 | 8 | %Lf |
Look at long: 8 bytes on one system, 4 on the other. That's the reason exam answers that say "int is 2 bytes" (a leftover from Turbo C on 16-bit DOS) or "long is always 8 bytes" are both wrong as general statements. When you need an exact width, include <stdint.h> and use int32_t or uint64_t. To check your own machine:
#include <stdio.h>
int main(void)
{
printf("int: %zu, long: %zu, pointer: %zu\n",
sizeof(int), sizeof(long), sizeof(void *));
return 0;
}
sizeof yields a size_t, and the printf manual defines %zu as the matching specifier.
Variables and constants
Declare every variable with a type before you use it. You can initialize it on the same line:
#include <stdio.h>
#define MAX_STUDENTS 60 /* preprocessor constant */
int main(void)
{
const double PI = 3.14159; /* typed constant */
int roll = 17;
char grade = 'A';
float cgpa = 3.75f;
printf("%d %c %.2f %d %.5f\n", roll, grade, cgpa, MAX_STUDENTS, PI);
return 0;
}
Rules for names: letters, digits and underscores only, can't start with a digit, can't be a keyword, and they're case-sensitive (Roll and roll are different variables). A local variable that you don't initialize holds an indeterminate value, not zero. Global and static variables start at zero.
#define does plain text replacement before compilation and has no type. const creates a real typed object the compiler can check. Prefer const unless you need the value in a place that requires a compile-time constant, like a case label.
Input and output with printf and scanf
printf takes values; scanf takes addresses, because it has to write into your variables. That one difference causes more lab crashes than anything else.
#include <stdio.h>
int main(void)
{
int age;
double salary;
char name[50];
printf("Name, age, salary: ");
if (scanf("%49s %d %lf", name, &age, &salary) != 3) {
printf("Invalid input\n");
return 1;
}
printf("%s is %d and earns %.2f\n", name, age, salary);
return 0;
}
Three things to notice. name has no &, since an array name already works as an address. %49s stops scanf from writing past the 50-byte buffer, leaving room for the terminating '\0'. And the return value tells you how many items were read, so checking it catches bad input. %s stops at the first space, so it reads one word. For a full line, use fgets(name, sizeof name, stdin).
Handy printf formatting: %5d pads to width 5, %-5d left-aligns, %.2f prints two decimal places, %x prints hex and %p prints a pointer (cast it to void *). Print a literal percent sign with %%.
Operators and precedence
When an expression mixes operators, precedence decides which binds first, and associativity breaks ties between operators at the same level. This condensed table runs from highest to lowest:
| Level | Operators | Associativity |
|---|---|---|
| Postfix | () [] -> . x++ x-- | Left to right |
| Unary | ++x --x + - ! ~ * & sizeof (type) | Right to left |
| Multiplicative | * / % | Left to right |
| Additive | + - | Left to right |
| Shift | << >> | Left to right |
| Relational | < <= > >= | Left to right |
| Equality | == != | Left to right |
| Bitwise | &, then ^, then | | Left to right |
| Logical | &&, then || | Left to right |
| Conditional | ?: | Right to left |
| Assignment | = += -= *= /= %= and others | Right to left |
| Comma | , | Left to right |
Classic exam traps:
7 / 2is3, because both operands are integers. Write7 / 2.0or cast with(double)7 / 2to get3.5.%works only on integers. With a negative left operand, the result takes that operand's sign:-7 % 3is-1.x & 1 == 0parses asx & (1 == 0), because==binds tighter than&. Add parentheses.&&and||short-circuit. Inp != NULL && *p > 0, the dereference only runs if the pointer is valid.- Expressions like
i = i++ + ++i;modifyitwice without sequencing. That's undefined behaviour, and there's no "correct" output to memorize, whatever a guidebook says.
Decisions with if, else and switch
#include <stdio.h>
int main(void)
{
int marks = 72;
int day = 3;
if (marks >= 80) {
printf("A+\n");
} else if (marks >= 70) {
printf("A\n");
} else {
printf("Below A\n");
}
switch (day) {
case 1:
printf("Sunday\n");
break;
case 2:
printf("Monday\n");
break;
case 3:
printf("Tuesday\n");
break;
default:
printf("Another day\n");
}
return 0;
}
In C, zero is false and any non-zero value is true. switch works only with integer types (including char), and each case label must be a constant. Forget a break and execution falls through into the next case. Sometimes that's intentional, but usually it's a bug.
Loops: for, while and do-while
#include <stdio.h>
int main(void)
{
int i, n = 5, sum = 0;
for (i = 1; i <= n; i++) {
sum += i;
}
printf("Sum: %d\n", sum);
i = n;
while (i > 0) {
printf("%d ", i);
i--;
}
printf("\n");
do {
printf("Runs at least once\n");
} while (0);
return 0;
}
Use for when you know the count, while when you loop until a condition changes, and do-while when the body must run at least once, like a menu. Note the semicolon after while (0) in a do-while; leaving it out is a compile error. break exits the innermost loop, and continue skips to its next iteration.
Functions and prototypes
Every function has a return type, a name, parameters and a body. If you call it before its definition, declare a prototype first so the compiler can check the arguments:
#include <stdio.h>
int add(int a, int b); /* prototype */
void swap(int *a, int *b);
int main(void)
{
int x = 3, y = 8;
printf("%d\n", add(x, y));
swap(&x, &y);
printf("x=%d y=%d\n", x, y);
return 0;
}
int add(int a, int b)
{
return a + b;
}
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
C passes every argument by value, meaning the function gets a copy. That's why swap takes pointers: the copies are addresses, and writing through them changes the caller's variables. "Call by reference" in C is really passing a pointer by value. Examiners like hearing that distinction.
Arrays and strings
An array is a fixed-size block of elements of one type, indexed from 0. A string is a char array that ends with the null character '\0'.
#include <stdio.h>
#include <string.h>
int main(void)
{
int marks[5] = {70, 85, 90, 60, 75};
int total = 0;
size_t count = sizeof marks / sizeof marks[0];
for (size_t i = 0; i < count; i++) {
total += marks[i];
}
printf("Average: %.1f\n", (double)total / count);
char city[20] = "Chattogram";
char copy[20];
strcpy(copy, city);
printf("%s has %zu letters\n", copy, strlen(copy));
if (strcmp(city, copy) == 0) {
printf("Same string\n");
}
return 0;
}
Nothing in C checks array bounds. Writing marks[5] in the example above touches memory past the end, and the program might crash, print garbage or appear to work. Compare strings with strcmp, never ==, which compares addresses. And strlen doesn't count the '\0', so "Chattogram" needs 11 bytes of storage, not 10.
Pointers and pointer arithmetic
Pointers store memory addresses. & takes an address, and * dereferences one to reach the value stored there.
#include <stdio.h>
int main(void)
{
int a[4] = {10, 20, 30, 40};
int *p = a; /* same as &a[0] */
printf("%d\n", *p); /* 10 */
printf("%d\n", *(p + 2)); /* 30 */
p++;
printf("%d\n", *p); /* 20 */
printf("%td\n", p - a); /* 1 element apart */
int *q = NULL;
if (q == NULL) {
printf("q points nowhere\n");
}
return 0;
}
Pointer arithmetic counts in elements, not bytes. If int is 4 bytes, p + 1 moves the address forward by 4. That's also why a[i] and *(a + i) mean exactly the same thing. Subtracting two pointers into the same array gives a ptrdiff_t, printed with %td.
Know the difference between these declarations, because it comes up in almost every C interview:
const int *p: you can't change the value throughp, but you can pointpelsewhere.int *const p:palways points to the same place, but the value can change.int **pp: a pointer to a pointer toint.
Structures
A struct groups related values of different types into one type:
#include <stdio.h>
#include <string.h>
struct Student {
int id;
char name[40];
float cgpa;
};
void print_student(const struct Student *s)
{
printf("%d %s %.2f\n", s->id, s->name, s->cgpa);
}
int main(void)
{
struct Student s1 = {101, "Rahim", 3.60f};
struct Student s2;
s2.id = 102;
strcpy(s2.name, "Karim");
s2.cgpa = 3.85f;
print_student(&s1);
print_student(&s2);
return 0;
}
Use . on a struct variable and -> on a pointer to a struct; s->id is shorthand for (*s).id. Passing a pointer avoids copying the whole struct, and const promises the function won't modify it. sizeof(struct Student) can be larger than the sum of its members because the compiler may add padding for alignment.
Dynamic memory with malloc and free
When you don't know a size until runtime, allocate memory on the heap with malloc from <stdlib.h> and release it with free:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int n;
printf("How many numbers? ");
if (scanf("%d", &n) != 1 || n <= 0) {
return 1;
}
int *nums = malloc(n * sizeof *nums);
if (nums == NULL) {
printf("Out of memory\n");
return 1;
}
for (int i = 0; i < n; i++) {
nums[i] = i * i;
}
printf("Last square: %d\n", nums[n - 1]);
free(nums);
nums = NULL;
return 0;
}
malloc returns uninitialized memory, or NULL on failure. calloc(n, size) zeroes the memory, and realloc resizes an existing block. In C you don't need to cast the result of malloc; C++ requires the cast, which is where the habit comes from. Writing sizeof *nums instead of sizeof(int) keeps the line correct if you later change the type. Setting the pointer to NULL after free guards against accidentally using it again.
Common beginner mistakes in C
These are the errors I'd look for first when a lab program misbehaves:
- Missing
&inscanffor anint,floatordouble. GCC with-Wallwarns about this. - Using
=instead of==in a condition.if (x = 5)assigns 5 and is always true. - Off-by-one loops like
for (i = 0; i <= 5; i++)on a 5-element array. - Wrong format specifier, such as
%dfor adoubleor%fforscanfinto adouble(it needs%lf). - A stray semicolon after
if (...)orfor (...), which makes the body an empty statement. - Returning the address of a local variable from a function. The variable no longer exists after the function returns.
- Forgetting
free, or calling it twice on the same pointer. - Leaving out
breakinswitchcases.
If a program crashes and the cause isn't obvious, compile with gcc -g -fsanitize=address,undefined. The sanitizers report out-of-bounds access and many kinds of undefined behaviour with the exact line number, which beats guessing with extra printf calls. The same step-by-step habit of isolating a problem applies well beyond C, as I describe in system analysis techniques for modern IT professionals.
Where C fits after your exams
C is the language of operating system kernels, embedded firmware and the runtimes under languages like Python and PHP. Even if your job ends up being web or cloud work, understanding memory and pointers makes it easier to reason about performance and bugs in higher-level code. If you're heading toward backend work, the PHP patterns in the DGePay PHP client guide and the serverless setup in this Cloudflare Workers and D1 build show where those fundamentals end up. When you apply for that first job, list C with a project you actually built, and see the ATS resume guide for IT professionals for how to present it.
FAQ
What is the size of int in C?
It depends on the compiler and platform. The standard only requires at least 16 bits. On mainstream 32-bit and 64-bit systems today, including GCC on Linux and MSVC on Windows, int is 4 bytes. Old 16-bit compilers like Turbo C used 2 bytes. Check with sizeof(int).
What's the difference between %d and %i in scanf?
In printf they behave the same. In scanf, %d reads a decimal integer, while %i also accepts octal (leading 0) and hexadecimal (leading 0x). So typing 010 gives 10 with %d and 8 with %i.
Why use int main(void) instead of void main()?
The standard defines main as returning int, and the return value reports success or failure to the operating system. void main() is a compiler extension that some old textbooks use, and GCC flags it with a -Wmain warning under -Wall.
What is the difference between an array and a pointer?
An array is a block of storage with a fixed size; a pointer is a variable that holds an address. An array name converts to a pointer to its first element in most expressions, but sizeof on an array gives the whole array's size, and you can't assign a new address to an array name.
Which C standard should I learn?
Write code that works under C99 and C11, since most textbooks, online judges and lab setups target them, and compile with an explicit -std= flag. Newer standards like C23 add features on top, but the core syntax in this guide hasn't changed.
If you're a student or team lead with a question about C, systems work or IT careers in Bangladesh, you can reach me here.