Don't Panic
in C ☕ 6분 읽기

[C] C/C++ 핵심 — 컴파일 과정·포인터·동적할당·가상 함수

#C
목차

[C] 컴파일 과정

gcc를 통해 C언어로 작성된 코드가 컴파일되는 과정을 알아보자.

C 컴파일 과정 개요

이러한 과정을 거치면서, 결과물은 컴퓨터가 이해할 수 있는 바이너리 파일로 만들어진다. 이 파일을 실행하면 주기억장치(RAM)로 적재되어 시스템에서 동작하게 된다.

1. 전처리 과정

  • 헤더파일 삽입#include 구문을 만나면 헤더파일을 찾아 그 내용을 순차적으로 삽입
  • 매크로 치환 및 적용#define, #ifdef와 같은 전처리기 매크로 치환 및 처리

2. 컴파일 과정 (전단부 → 중단부 → 후단부)

단계설명
전단부언어 종속적인 부분 처리 — 어휘, 구문, 의미 분석
중단부SSA(Static Single Assignment) 기반으로 최적화 수행 — 프로그램 수행 속도 향상
후단부RTS(Runtime Support)로 아키텍처 최적화 수행 — 더 효율적인 명령어로 대체

3. 어셈블 과정

  • 컴파일이 끝나면 어셈블리 코드가 됨. 이 코드는 어셈블러에 의해 기계어가 된다.
  • 어셈블러로 생성되는 파일은 명령어와 데이터가 들어있는 ELF 바이너리 포맷 구조를 가짐 (링커가 여러 바이너리 파일을 하나의 실행 파일로 효과적으로 묶기 위해 명령어와 데이터 범위를 일정한 규칙을 갖고 형식화해 놓음)

4. 링킹 과정

오브젝트 파일들과 프로그램에서 사용된 C 라이브러리를 링크한다.

해당 링킹 과정을 거치면 실행 파일이 만들어진다.


[C] 구조체 메모리 크기 (Struct Memory Size)

typedef struct 선언 시, 변수 선언에 대한 메모리 공간 크기에 대해 알아보자.

기업 필기 테스트에서 자주 나오는 유형이다.

기본 자료형 크기

자료형크기
char1바이트
int4바이트
double8바이트

sizeof 메소드를 통해 해당 변수의 사이즈를 알 수 있다.

구조체 메모리 할당 규칙

  1. 4바이트 단위 구성 — 각각의 멤버를 저장하기 위해서는 기본 4바이트 단위로 구성된다. char 데이터 1개를 저장할 때도 이 데이터가 포함된 ‘4바이트’를 읽는다.
  2. 가장 큰 멤버의 크기에 영향 — 구조체 각 멤버 중에서 가장 큰 멤버의 크기에 영향을 받는다.

예제 1 — char + int

typedef struct student {
    char a;
    int b;
}S;

void main() {
    printf("메모리 크기 = %d/n", sizeof(S)); // 8
}

char는 1바이트고, int는 4바이트라서 5바이트가 필요하다.

하지만 메모리 공간은 5가 아닌 8이 출력된다. char형이지만 기본 4바이트 단위 구성으로 인해 3바이트의 여유공간이 생기기 때문이다.

char + int 메모리 배치

예제 2 — char + char + int

typedef struct student {
    char a;
    char b;
    int c;
}S;

char + char + int 메모리 배치

똑같이 8바이트가 필요하며, char형으로 선언된 a, b가 4바이트 안에 함께 들어가고 2바이트의 여유 공간이 생긴다.

예제 3 — 선언 순서가 다른 경우

typedef struct student {
    char a;
    int c;
    char b;
}S;

구성은 같지만, 선언된 순서에 따라 할당되는 메모리 공간이 달라진다.

선언 순서 다른 경우 메모리 배치

이 경우에는 총 12바이트가 필요하게 된다.

예제 4 — double 포함

typedef struct student {
    char a;
    int c;
    double b;
}S;

두 규칙이 모두 적용되는 상황이다. b가 double로 8바이트이므로 기본 공간이 8바이트로 설정된다. ac는 8바이트로 해결이 가능하기 때문에 16바이트로 해결이 가능하다.

double 포함 메모리 배치


[C] 포인터 (Pointer)

포인터: 특정 변수를 가리키는 역할을 하는 변수

main에서 한번 만들어둔 변수 값을 다른 함수에서 그대로 사용하거나 변경하고 싶은 경우가 있다. 다른 지역인 경우에는 해당 값을 임시 변수로 받아 반환하는 식으로 처리한다. 이때 효율적으로 처리할 수 있도록 포인터를 사용한다.

포인터 없이 처리하는 경우:

#include<stdio.h>

int ReturnPlusOne(int n) {
    printf("%d\n", n+1);
	return n + 1;
}

int main(void) {

	int number = 3;
	printf("%d\n", number);

	number = 5;
	printf("%d\n", number);

	ReturnPlusOne(number);
	printf("%d\n", number);

	return 0;
}

[출력 결과]

3
5
6
5

mainnumber와 function의 n은 다른 변수다.

포인터로 접근하면:

#include<stdio.h>

int ReturnPlusOne(int *n) {
	*n += 1;
}

int main(void) {

	int number = 3;
	printf("%d\n", number);

	number = 5;
	printf("%d\n", number);

	ReturnPlusOne(&number);
	printf("%d\n", number);

	return 0;
}

[출력 결과]

3
5
6

포인터를 활용해서 원했던 결과를 가져올 수 있다.

포인터 문법 정리

표현의미
int* p;int형 포인터로 p라는 이름의 변수를 선언
p = &num;p의 값에 num 변수의 주소값 대입
printf("%d", *p);p가 가리키는 주소에 있는 을 나타냄
printf("%d", p);p가 가리키고 있는 주소를 나타냄
#include<stdio.h>

int main(void) {

    int number = 5;
    int* p;
    p = &number;

    printf("%d\n", number);
    printf("%d\n", *p);
    printf("%d\n", p);
    printf("%d\n", &number);

	return 0;
}

[출력 결과]

5
5
주소값
주소값

가리키는 주소 vs 가리키는 주소에 있는 값의 차이다.

이중 포인터

포인터의 포인터, 즉 포인터의 메모리 주소를 저장하는 것을 말한다.

#include <stdio.h>

int main()
{
    int *numPtr1;     // 단일 포인터 선언
    int **numPtr2;    // 이중 포인터 선언
    int num1 = 10;

    numPtr1 = &num1;    // num1의 메모리 주소 저장

    numPtr2 = &numPtr1; // numPtr1의 메모리 주소 저장

    printf("%d\n", **numPtr2);    // 20: 포인터를 두 번 역참조하여 num1의 메모리 주소에 접근

    return 0;
}

[출력 결과]

10

포인터의 메모리 주소를 저장할 때는 이중 포인터를 활용해야 한다. 실제 값을 가져오기 위해 **numPtr2처럼 역참조 과정을 두 번 하여 가져올 수 있다.

이중 포인터 메모리 구조


[C] 동적할당

프로그램 실행 중에 동적으로 메모리를 할당하는 것. Heap 영역에 할당한다.

<stdlib.h> 헤더 파일을 include 해야 한다.

동적할당 함수 정리

함수시그니처설명
mallocvoid* malloc(size_t size)size_t 크기만큼 메모리 할당
callocvoid* calloc(size_t nelem, size_t elsize)배열 요소 개수 × 각 요소 사이즈만큼 할당 후 0으로 초기화
reallocvoid* realloc(void *ptr, size_t size)이미 할당받은 메모리에 추가로 메모리 할당 (이전 메모리 주소 없어짐)
freevoid free(void* ptr)할당된 메모리 해제

중요: 할당한 메모리는 반드시 해제하자. 해제하지 않으면 메모리 릭(누수) 발생!

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int arr[4] = { 4, 3, 2, 1 };
    int* pArr;

    // 동적할당 : int 타입의 사이즈 * 4만큼 메모리를 할당
    pArr = (int*)malloc(sizeof(int)*4);

    if(pArr == NULL) { // 할당할수 없는 경우
        printf("malloc error");
        exit(1);
    }

    for(int i = 0; i < 4; ++i) {
        pArr[i] = arr[i];
    }

    for(int i = 0; i < 4; ++i) {
        printf("%d \n", pArr[i]);
    }

    // 할당 메모리 해제
    free(pArr);

    return 0;
}
  • (int*) — malloc의 반환형이 void*이므로 형변환
  • sizeof(int) — sizeof는 괄호 안 자료형 타입을 바이트로 연산해줌
  • *4arr[4]가 가진 동일한 크기의 메모리를 할당하기 위해
  • free(pArr) — 다 사용하면 꼭 메모리 해제

[C++] 얕은 복사 vs 깊은 복사

shallow copydeep copy가 어떻게 다른지 알아보자.

구분설명
얕은 복사 (Shallow Copy)한 객체의 모든 멤버 변수의 값을 다른 객체로 복사
깊은 복사 (Deep Copy)모든 멤버 변수의 값뿐만 아니라, 포인터 변수가 가리키는 모든 객체에 대해서도 복사
struct Test {
    char *ptr;
};

void shallow_copy(Test &src, Test &dest) {
    dest.ptr = src.ptr;
}

void deep_copy(Test &src, Test &dest) {
    dest.ptr = (char*)malloc(strlen(src.ptr) + 1);
    strcpy(dest.ptr, src.ptr);
}

shallow_copy를 사용하면, 객체 생성과 삭제에 관련된 많은 프로그래밍 오류가 프로그램 실행 시간에 발생할 수 있다.

즉, 얕은 복사는 프로그래머가 스스로 무엇을 하는 지
잘 이해하고 있는 상황에서 주의하여 사용해야 한다

대부분 얕은 복사는 실제 데이터를 복제하지 않고서 복잡한 자료구조에 관한 정보를 전달할 때 사용한다. 실무에서는 거의 사용되지 않으며, 대부분 깊은 복사를 사용해야 한다.


[C++] 가상 함수 (Virtual Function)

C++에서 자식 클래스에서 재정의(오버라이딩)할 것으로 기대하는 멤버 함수를 의미한다. 멤버 함수 앞에 virtual 키워드를 사용하여 선언하며, 실행 시간에 함수의 다형성을 구현할 때 사용한다.

선언 규칙

  • 클래스의 public 영역에 선언해야 한다.
  • 가상 함수는 static일 수 없다.
  • 실행 시간 다형성을 얻기 위해, 기본 클래스의 포인터 또는 참조를 통해 접근해야 한다.
  • 가상 함수는 반환형과 매개변수가 자식 클래스에서도 일치해야 한다.
class parent {
public :
    virtual void v_print() {
        cout << "parent" << "\n";
    }
    void print() {
        cout << "parent" << "\n";
    }
};

class child : public parent {
public :
    void v_print() {
        cout << "child" << "\n";
    }
    void print() {
        cout << "child" << "\n";
    }
};

int main() {
    parent* p;
    child c;
    p = &c;

    p->v_print();
    p->print();

    return 0;
}

[출력 결과]

child
parent

parent 클래스를 가리키는 포인터 p를 선언하고 child 클래스의 객체 c를 선언한 상태다. 포인터 pc 객체를 가리키고 있다. (몸체는 parent 클래스지만, 현재 실제 객체는 child 클래스)

바인딩 방식 비교

함수결정 시점결과
v_print() — virtual런타임 (후기 바인딩)child 출력
print() — 일반컴파일 타임 (전기 바인딩)parent 출력

print()는 컴파일 시간에 이미 parent가 호출되는 것으로 결정이 끝난다.


[C++] 입출력 실행속도 줄이는 법

C++로 알고리즘 문제를 풀 때, cin, cout은 실행속도가 느리다. 최적화 방법을 이용하면 실행속도를 크게 단축할 수 있다.

  • 최적화 시 거의 절반의 시간이 단축된다.
int main(void)
{
    ios_base :: sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
}

ios_base는 C++에서 사용하는 iostream의 cin, cout 등을 함축한다.

sync_with_stdio(false)는 C언어의 stdio.h와의 동기화를 끊어, printf, scanf, getchar, fgets, puts, putchar 등을 false로 동기화하지 않음을 뜻한다.

주의사항

  • cin/scanfcout/printf를 같이 쓰면 문제가 발생하므로 조심하자.
  • 싱글 스레드 환경에서만 효율적이다 (즉, 알고리즘 문제 풀이 용도). 실무에서는 사용하지 말자.
  • 크게 차이가 나지 않는다면 그냥 printf/scanf를 써도 된다.