728x90

Nekoder


 

배열 주소 연속


배열의 주소는 메모리에서 연속된 공간을 차지한다.
즉, arr[0], arr[1], arr[2] ... 이런 식으로 각 원소의 주소가 일정한 간격을 유지하며 배치된다.

배열의 주소 구조 예제

int arr[3] = {10, 20, 30};
printf("%p\n", &arr[0]); // ex) 0x1000
printf("%p\n", &arr[1]); // ex) 0x1004 (int는 4바이트)
printf("%p\n", &arr[2]); // ex) 0x1008
  • int는 4바이트이므로, 주소는 4씩 증가한다.
  • char 배열은 1바이트씩, double 배열은 8바이트씩 증가한다.

배열이 연속된 주소를 가지는 이유

  • CPU가 캐시를 효율적으로 활용할 수 있도록 하기 위함.
  • 배열의 인덱스를 기반으로 빠르게 메모리 접근이 가능하도록 하기 위함.

포인터 배열


 

포인터 배열은 각 원소가 변수의 값이 아니라, 변수의 주소를 저장하는 배열이다.
즉, 포인터를 여러 개 저장하는 배열이라고 보면 된다.

포인터 배열 예제

int a = 10, b = 20, c = 30;
int *arr[3] = {&a, &b, &c}; // 포인터 배열
printf("%d\n", *arr[0]); // 10
printf("%d\n", *arr[1]); // 20
printf("%d\n", *arr[2]); // 30
  • arr는 int* 타입의 배열이며, 각 원소는 int 변수의 주소를 저장한다.
  • *arr[i]를 하면 해당 주소에 저장된 값을 가져올 수 있다.

포인터 배열이 유용한 경우

  • 2차원 배열을 포인터 배열로 표현할 때
  • 문자열 리스트를 저장할 때
  • 함수 포인터 배열을 만들 때

arr[var]이 warning이 나는 이유


 

  • 배열 인덱스는 항상 정수형이어야 함 (int, short, long 등)
  • 실수형(double, float)을 인덱스로 사용하면 warning 발생
  • scanf()로 입력받을 때는 항상 정수형을 사용해야 함
  • 문자(char)나 포인터(pointer)를 인덱스로 사용하면 오류 발생 가능
  • 배열 범위를 벗어나는 입력을 방지하기 위해 추가적인 체크가 필요함

포인터 간 +, - 연산


 

포인터끼리 덧셈(+)은 의미가 없어서 허용되지 않는다.
하지만 포인터끼리 뺄셈(-)은 같은 배열 내에서의 거리(차이)를 구하는 데 사용할 수 있다.

 

포인터 뺄셈 예제

int arr[5] = {10, 20, 30, 40, 50};
int *p1 = &arr[4];
int *p2 = &arr[1];
printf("%ld\n", p1 - p2); // 3 (4번째 요소 - 1번째 요소)
  • p1 - p2는 두 포인터가 몇 개의 요소 차이가 나는지 반환한다.
  • 이 결과는 바이트 단위가 아니라, 배열의 요소 개수 단위로 계산된다.

포인터 덧셈이 안 되는 이유

int *p1, *p2;
int *result = p1 + p2; // ERROR: 의미 없는 연산
  • 포인터끼리 더하면 메모리 주소를 알 수 없는 값이 되므로 컴파일러가 허용하지 않는다.(말 그대로 쓸 데가 없다. 어따 쓸건데?)

포인터 vs 배열의 차이


배열과 포인터는 비슷해 보이지만, 동작 방식이 다르다.

int arr[3] = {10, 20, 30};
int *ptr = arr;
  • arr은 상수 포인터처럼 동작하지만, 실제로는 배열 자체의 주소를 의미한다.
  • ptr은 포인터 변수이므로 다른 주소를 저장할 수도 있다.

 


함수 포인터 배열


 

함수를 저장하는 포인터 배열도 사용할 수 있다.

void f1() { printf("Function 1\n"); }
void f2() { printf("Function 2\n"); }

void (*funcArr[2])() = {f1, f2};
funcArr[0](); // Function 1
funcArr[1](); // Function 2
  • funcArr은 함수 포인터 배열이며, funcArr[i]()를 호출하면 해당 함수가 실행된다.

부동소수점 사용 소수 비교


종류 표현방식 특징 예제
정규수 (Normal Number) (-1)^S × 1.M × 2^E 일반적인 부동소수점 수 3.14, 1.0e-10
비정상수 (Denormal Number, DN) (-1)^S × 0.M × 2^E_min 연산이 느리지만 작은 값 표현 가능 1.0e-39, 5.0e-324
0 (Zero) 부호 비트만 다름 +0과 -0 구별됨 1.0 / -0.0 = -Inf
무한대 (Infinity, Inf) 특수한 지수 비트 값 오버플로우 발생 시 나옴 1.0 / 0.0 = Inf
NaN (Not a Number) 비트 패턴이 특정 값 정의되지 않은 연산 결과 0.0 / 0.0, sqrt(-1.0)

 

정규수는 일반적인 부동소수점 수고, **비정상수(DN)**는 아주 작은 값을 표현하기 위해 사용.
비정상수는 정규수보다 작은 값을 저장할 수 있지만, 연산 속도가 느려지고 정확도가 떨어질 수 있음.

0은 +0과 -0을 구별하며, 0으로 나누면 무한대가 발생할 수 있음.
무한대(Inf)는 오버플로우 발생 시 나타나는 값이고, NaN은 잘못된 연산 결과를 의미.

이런 특수한 값들은 부동소수점 연산에서 오류를 방지하고, 작은 값이 0으로 사라지는 문제(언더플로우)를 해결하는 데 중요.


연산자 우선순위


우선순위 연산자 설명 결합 방향
1 ( ), [ ], -> , . 함수 호출, 배열 접근, 구조체 멤버 접근 >
2 ! , ~ ,++, --, + , - , * , & , sizeof, (type) NOT(논리, 비트), (후위, 전위) 증가, 감소, 부호, 역참조, 주소, 크기 확인, 형 변환 <
3 *, /, % 곱셈, 나눗셈, 나머지 >
4 +, - 덧셈, 뺄셈 >
5 <<, >> 비트 시프트 >
6 <, >, <=, >= 비교 연산자 >
7 ==, != 동등 연산자 >
8 & 비트 AND >
9 ^ 비트 XOR >
10 | 비트 OR >
11 && 로직 AND >
12 || 로직 OR >
13 ? : 3항 연산자 <
14 =, +=, -=, *=, /=, %=, >>=, <<=, &=, ^=, |= 대입 연산자 <
15 , 쉼표 연산자 (순차 평가) >

Declaration vs. Definition


Declaration vs. Definition

 

가장 큰 차이 = Memory Allocation(메모리 할당)

Declaration(선언)

메모리 할당 X

컴파일러에게 변수,함수 등의 존재나 형태를 알리는 것

 

Definition(정의)

메모리 할당 O

선언된 변수, 함수 등의 각 요소를 실제로 구현하는 것

#include <stdio.h>

// Declaration
struct food;

int swap(int *a, int *b);

int main() {
    int *a;
    int *b;

// Definition
    struct food {
    	int sushi;
        int cake;
        int bread;
    } FOOD;
    FOOD.sushi = 3;
    
    int c = 10;
    int d = 15;

    a = &c;
    b = &d;

    printf("%d\n", FOOD.sushi);

    swap(a, b);

    return 0;
}

int swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

 

추가로 warning 보게끔 설정 후 컴파일

gcc -Wall -Wextra -o program program.c

 

아래처럼 컴파일 시 'warning' 출력.

이유는 int 함수인 swap에서 리턴을 하지 않았다.(리턴 할 필요가 없는 함수)

이럴 땐 void 사용. 안해서 warning

program.c: In function 'swap':
program.c:31:1: warning: control reaches end of non-void function [-Wreturn-tyoe]
 }
 ^

프로그램 실행

$ ./program

정상 동작

3
15
10

 


Compile


Preprocessing

$ gcc -E hello.c -o hello.i

 

The preprocessing stage is handled by the preprocessor, not the compiler. It prepares the source code before the actual compilation begins by processing directives that start with #.

 

  • #include <stdio.h> → Inserts the contents of stdio.h into the source code.
  • #define → Replaces macros with their defined values.
  • Removes all comments from the code.

 

The output of this stage is typically a file like hello.i, which contains the processed source code.

 

Compilation

$ gcc -S hello.i -o hello.s

 

The compilation stage takes the preprocessed code (hello.i) and converts it into assembly code (hello.s).

 

Key tasks in this stage:

 

  • Parses and checks the C code for syntax errors.
  • Performs code optimization.
  • Translates the code into platform-specific assembly instructions.

 

 

Assembling

$ gcc -c hello.s -o hello.o

 

An assembler converts the assembly code into machine code (object file).

Key tasks:

  • Translates .s file to binary object code.

 

Linking

$ gcc hello.o -o hello

 

The linker combines one or more object files and external libraries into a single executable.

Key tasks:

  • Resolves symbol references (e.g., function calls).
  • Connects standard libraries (like printf from libc).

Library

 

A library is a file that contains reusable code such as functions and variables to avoid repetitive coding and reduce the size of a program.

Category Static Dynamic
Link Time Complie-time Runtime
Extension .a .so
Size Large Small
Modification Recompilation required Replace library file
Speed Fast(code already included) Slightly slow(loaded at runtime)
Memory Usage High Low
728x90