[CS] 면접 대비 언어 (C/CPP)
[C] 컴파일 과정
- gcc를 통해 C언어로 작성된 코드가 컴파일되는 과정을 알아보자
이러한 과정을 거치면서, 결과물은 컴퓨터가 이해할 수 있는 바이너리 파일로 만들어진다. 이 파일을 실행하면 주기억장치(RAM)로 적재되어 시스템에서 동작하게 되는 것이다.
1. 전처리 과정
헤더파일 삽입 (#include 구문을 만나면 헤더파일을 찾아 그 내용을 순차적으로 삽입)
매크로 치환 및 적용 (#define, #ifdef와 같은 전처리기 매크로 치환 및 처리)
2. 컴파일 과정 (전단부 - 중단부 - 후단부)
전단부 (언어 종속적인 부분 처리 - 어휘, 구문, 의미 분석)
중단부 (SSA 기반으로 최적화 수행 - 프로그램 수행 속도 향상으로 성능 높이기 위함)
후단부 (RTS로 아키텍처 최적화 수행 - 더 효율적인 명령어로 대체해서 성능 높이기 위함)
3. 어셈블 과정
컴파일이 끝나면 어셈블리 코드가 됨. 이 코드는 어셈블러에 의해 기계어가 된다.
어셈블러로 생성되는 파일은 명령어와 데이터가 들어있는 ELF 바이너리 포맷 구조를 가짐 (링커가 여러 바이너리 파일을 하나의 실행 파일로 효과적으로 묶기 위해 명령어와 데이터 범위를 일정한 규칙을 갖고 형식화 해놓음)
4. 링킹 과정
- 오브젝트 파일들과 프로그램에서 사용된 C 라이브러리를 링크함
해당 링킹 과정을 거치면 실행파일이 드디어 만들어짐
[C] 구조체 메모리 크기 (Struct Memory Size)
typedef struct 선언 시, 변수 선언에 대한 메모리 공간 크기에 대해 알아보자
기업 필기 테스트에서 자주 나오는 유형이기도 함
char : 1바이트
int : 4바이트
double : 8바이트
sizeof 메소드를 통해 해당 변수의 사이즈를 알 수 있음
크기 계산
typedef struct student {
char a;
int b;
}S;
void main() {
printf("메모리 크기 = %d/n", sizeof(S)); // 8
}
char는 1바이트고, int는 4바이트라서 5바이트가 필요하다.
하지만 메모리 공간은 5가 아닌 8이 찍힐 것이다.
Why?
구조체가 메모리 공간을 잡는 원리에는 크게 두가지 규칙이 있다.
각각의 멤버를 저장하기 위해서는 기본 4바이트 단위로 구성된다. (4의 배수 단위) 즉, char 데이터 1개를 저장할 때 이 1개의 데이터를 읽어오기 위해서 1바이트를 읽어오는 것이 아니라 이 데이터가 포함된 ‘4바이트’를 읽는다.
구조체 각 멤버 중에서 가장 큰 멤버의 크기에 영향을 받는다.
이 규칙이 적용된 메모리 공간은 아래와 같을 것이다.
a는 char형이지만, 기본 4바이트 단위 구성으로 인해 3바이트의 여유공간이 생긴다.
typedef struct student {
char a;
char b;
int c;
}S;
똑같이 8바이트가 필요하며, char형으로 선언된 a,b가 4바이트 안에 함께 들어가고 2바이트의 여유 공간이 생긴다
이제부터 헷갈리는 경우다.
typedef struct student {
char a;
int c;
char b;
}S;
구성은 같지만, 순서가 다르다.
자료타입은 일치하지만, 선언된 순서에 따라 할당되는 메모리 공간이 아래와 같이 달라진다.
이 경우에는 총 12바이트가 필요하게 된다.
typedef struct student {
char a;
int c;
double b;
}S;
두 규칙이 모두 적용되는 상황이다. b가 double로 8바이트이므로 기본 공간이 8바이트로 설정된다. 하지만 a와 c는 8바이트로 해결이 가능하기 때문에 16바이트로 해결이 가능하다.
[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
main의 number와 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 = # : p의 값에 num 변수의 주소값 대입
printf(“%d”, *p); : 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 주소값 주소값
가리키는 주소 - 가리키는 주소에 있는 값의 차이다.
이중 포인터
포인터의 포인터, 즉 포인터의 메모리 주소를 저장하는 것을 말한다.
#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처럼 역참조 과정을 두번하여 가져올 수 있다.