• 1.8.1 링킹 에러
  • 1.8.2 중복 정의
  • 1.8.3 헤더 가드(Header Guards)

1.8.1 링킹 에러

 

Linking Error

프로그램을 만들다 보면 LNK2019, LNK1120이라는 링킹 에러를 만나는 경우가 종종 생긴다. 위와 같은 에러가 생기는 이유는 특정 함수에 대하여 선언은 존재하지만 정의가 존재하지 않는 경우이다. 

 

add function header

위처럼 add 함수에 대한 선언을 헤더 파일로 작성하고서 include를 통해 main 함수가 작성된 코드에서 add 함수를 사용했다면 전방 선언이 된 효과를 가지게 된다. 선언이 되어 있으므로 컴파일러는 통과를 시키고 빌드가 이루어지는데, 결국 해당 함수의 정의가 담긴 cpp 파일이 없다면 함수 호출 뒤에 어떤 연산을 해야 할지 모르는 상황이 발생하면서 링킹 에러가 생긴다. 따라서 링킹 에러가 생긴다면 혹시나 정의를 빼먹은 함수가 없는지 확인해보자!


1.8.2 중복 정의

 

위와는 다르게 과도한 정의로 인한 에러가 생기는 경우가 있다.

function already has a body

이처럼 C2084 에러가 잡히면 Description 의미 그대로 이미 중복 선언이 된 경우이다. 일반적으로 많은 함수를 선언과 정의를 해서 헤더 파일로 가지고 있다면 여기저기서 include를 통해 함수를 재사용하고 있을 것이다. 다음의 예시를 보자.

 

add header
dosomething header

add 함수가 있는 헤더 파일을 만들고 add 함수를 끌어다 쓰는 doSomething 함수를 만들어 다른 헤더 파일에 만들었다.

 

main 함수

그리고 main 함수가 담긴 cpp 파일이 위처럼 작성되어 있다. 여기서 #include 전처리기가 작동하는 방식을 이해하면 왜 중복 정의인지 알 수가 있는데, #include 전처리기는 해당 파일을 복사해서 가지고 오는 것처럼 작동한다. 따라서 위의 코드는 아래와 같은 상황이 벌어진다.

 

add 중복


1.8.3 헤더 가드(Header Guards)

 

아마 헤더 파일을 만드는 순간 #pragma once라는 전처리기를 보았을 것이다. 해당 전처리기는 이미 한번 정의된 함수라면 다시 정의하지 않도록 해주는 전처리기로 프로그램이 커질수록 해당 전처리기의 도움을 많이 받는다. 같은 방식으로 동작하는 것이 아래의 코드가 되겠다.

 

#ifndef  MY_ADD
#define  MY_ADD

int add(int a, int b)
{
	return a + b;
}

#endif // ! MY_ADD

 

다음과 같이 코드가 작성 되어 있다면 가장 앞에 #pragma once 전처리기를 둔 것과 같은 효과를 지닌다. #ifndef MY_ADD를 통해 만약 MY_ADD가 정의되어 있지 않다면 #endif 바로 위까지 코드를 읽어드리는 것이다. 이런 경우 #define MY_ADD가 최상단에서 정의가 되고 add 함수를 정의하기 때문에 추후 해당 헤더 파일을 다시 include하더라도 MY_ADD가 정의되어 있기 때문에 다시 add 함수를 정의하는 불상사는 발생하지 않는다.

  • 1.7.1 선언(Declaration)과 정의(Definition)
  • 1.7.2 헤더(Header) 파일 만들기
  • 1.7.3 Peek Definition
  • 1.7.4 Close All But This

1.7.1 선언(Declaration)과 정의(Definition)

 

만드는 프로그램이 커질수록 중요해지는 개념이 바로 선언과 정의이다. 컴파일러는 주어진 코드를 순차적으로 읽기 때문에 사용하고자 하는 함수에 대해 미리 알고 있어야 한다. 다음의 예시를 보자.

 

identifier not found

분명 눈으로 보기에는 add 함수와 subtract 함수가 main 함수 아래에 존재하지만 컴파일러는 identifier not found라는 에러를 출력하며 해당 함수들을 찾지 못하고 있다. 앞에서 이야기했듯이 컴파일러는 순차적으로 코드를 읽기 때문에 main 함수에 진입하였을 때, add 함수와 subtract 함수가 무엇인지 알 수 없었고 에러를 출력한 것이다.

 

많은 사람들이 처음에는 이러한 컴파일 에러를 넘어가기 위해서 main 함수 위에 add 함수와 subtract 함수를 옮겨서 해결하는데, 프로그램을 계속 만들다 보면 사용하는 함수들이 커져가기 마련이다. 함수를 옮기지 않고 해결하는 방법이 바로 선언을 사용하는 것이다.

 

Forward Declaration

위처럼 함수의 최소한의 정보(입력과 출력에 대한 자료형)를 가지고 작성해주는 것이 선언이며, main 함수 뒤에 정의를 두고서 main 함수 앞에 선언을 하는 경우 전방 선언이라고도 한다. 정의는 말그대로 함수가 어떤 작동을 하는지 코드로 작성한 것을 말한다.


1.7.2 헤더(Header) 파일 만들기

 

앞에서는 전방 선언을 통해서 main 함수가 사용할 함수가 무엇인지 컴파일러에게 미리 알려주는 방식을 택하였다. 하지만 이러한 방법도 다루는 함수가 양이 많아질 경우 위아래로 스크롤을 많이 움직이는 비효율적인 작업을 하게 된다. 이를 해결하기 위해 함수의 정의와 선언에 대해 개별적으로 파일을 만들고 main 함수가 담긴 파일이 헤더 파일을 include하는 방식을 사용한다.

Header files and source files

add라는 함수를 예시로 사용하기 위해 위처럼 파일을 만든다. Header Files 폴더에 함수의 선언을 담을 헤더 파일을 생성하고 Source Files 폴더에는 함수의 정의를 담을 cpp 파일을 만들었다.

 

add.h

헤더 파일을 생성하는 경우 자동으로 #pragma once라는 헤더 가드 전처리기가 생성된다. 파일에는 add 함수의 선언만 작성하였다.

 

add.cpp

cpp 파일에는 add 함수의 정의를 작성하였다.

 

main 함수가 담긴 파일

main 함수가 담긴 파일은 #include 전처리기를 통하여 add.h를 추가해준다.  이를 통해 전방 선언과 똑같은 효과를 가져올 수가 있다. 만약 Header Files 폴더가 아닌 새 폴더에 헤더 파일을 만들어서 관리하는 경우 해당 폴더의 절대 경로를 "" 사이에 넣어줘야한다.


1.7.3 Peek Definition

 

위처럼 선언과 정의가 파일 자체로 분리가 되고 많은 함수를 만들어서 다루다 보면 해당 함수가 사용되는 부분에서 이게 무슨 함수인지를 보고 싶은 경우가 생길 것이다. 정의 파일로 직접 가서 볼 수도 있지만 사용되는 곳에서 바로 볼 수도 있다.

 

Peek Definition

Peek Definition 기능을 사용하면 아래와 같이 사용하는 곳에서 함수의 정의를 빠르게 볼 수 있다.

 

Definitions

예시의 경우 add 함수가 총 세 곳에 정의되어 있었다. add.cpp를 제외한 모든 파일은 빌드에서 제외된 상태였으므로 위처럼 존재해도 실행하는 것에는 지장이 없었다.


1.7.4 Close All But This

 

Peek Definition만큼 유용한 기능이 Close All But This이다.

 

Close All But This

현재 캡처에는 총 4개의 파일이 열려 있는데, 다루는 파일이 많아질수록 지저분하게 많은 파일을 열게 된다. 이를 한 번에 닫아주고 내가 보고 있는 파일만 남기는 기능이 바로 Close All But This이다.

 

After Close All But This

 

 

  • 1.6.1 리터럴(Literal)
  • 1.6.2 연산자(Operator)와 피연산자(Operand)

1.6.1 리터럴(Literal)

 

리터럴은 C++에서 언급되는 상수 중의 하나로 직접적으로 입력하는 데이터이다.

#include <iostream>

using namespace std;

int main()
{
	// Assignment operator =
	int x = 2; // x is a variable, 2 is a literal.
	
	cout << 1 + 2 << endl; // 1 and 2 are literal.

	// Unary operator -
	cout << -x << endl;

	// Binary operator +
	cout << 1 + 2 << endl;

	// Ternary operator ? :
	int y = (x > 0) ? 1 : 2;
	cout << y << endl;

	return 0;
}

 

위의 코드에서 x를 2로 초기화하는데, 여기서 입력하게 되는 2를 리터럴이라고 한다. 저 2라는 값에 3이라는 값을 넣을 방법이 없는 것처럼 바꿀 수 없는 것을 리터럴이라고 한다.


1.6.2 연산자(Operator)와 피연산자(Operand)

 

연산자는 우리가 평소에 다루던 +, -, *를 의미하고 피연산자는 연산자가 행하는 연산 수행에 사용되는 것들을 의미한다. 연산자는 단항 연산자(unary operator), 이항 연산자(binary operator), 삼항 연산자(ternary operator)가 있다.

 

-의 경우 숫자 앞에 쓰이므로 하나의 피연산자로 행해지는 연산이다. 이처럼 피연산자가 하나만 사용될 경우 단항 연산자라 한다. +와 *는 연산자의 양쪽에 피연산자가 필요하므로 이항 연산자가 된다.

 

C++에서 정의된 삼항 연산자는 조건문을 비교하여 그 결과에 따라 값을 선택하는 연산을 수행한다. 위의 코드에서는 x가 0보다 큰지에 대한 판단을 하고 참이면 1을 거짓이면 2를 반환하도록 구현되어 있다.

  • 1.5.1 지역 변수(Local variables)
  • 1.5.2 지역 변수로 보는 매개변수와 인자의 차이

1.5.1 지역 변수(Local variables)

변수가 생성되고 없어지는 것엔 영역이 존재한다. 메모리에 할당된 지역 변수는 할당이 이루어진 곳을 묶고 있는 중괄호가 끝나는 지점까지만 존재한다. 이러한 영역을 지역 영역(local scope)이라고 한다. 다음의 예제를 보자.

 

#include <iostream>

using namespace std;

int main()
{
	// 구분을 하기 위한 식별자인데 같으면 문제가 생김
	int x = 0;
	//int x = 1;

	cout << x << " " << &x << endl;
	{
		// 앞의 x와 다르다.
		// 다른 메모리에 대한 식별자 공간 자체가 다르다.
		//int x = 0;
		x = 1;
		cout << x << " " << &x << endl;
	}

	cout << x << " " << &x << endl;

	{
		int x = 2;
		cout << x << " " << &x << endl;
	}

	return 0;
}

 

우선 같은 중괄호에 같은 이름(같은 식별자)으로 생성된 지역 변수를 보자.

redefinition

같은 중괄호에 같은 식별자의 변수는 존재할 수 없다. 애초에 식별자가 식별을 위해 존재하는 것이기 때문에 다른 메모리에 같은 식별자로 두는 것은 허용되지 않는다.

 

다른 중괄호에 묶인 경우 같은 식별자의 변수를 사용할 수 있다. 이것이 가능한 이유가 지역 범위가 있기 때문이다. 

local variables and local scope

메모리 주소를 출력한 결과 전부 다르다는 것을 알 수 있다. 첫번째와 세번째 출력 결과가 같은 이유는 같은 지역 변수의 메모리 주소를 출력하기 때문이다.

 

이번에는 새로 지역 변수를 할당하는 것이 아닌 값만 바꾸는 행위를 하는 코드의 결과를 보자.

x 값 변경

첫 중괄호에서 새로운 지역 변수 x를 할당하는 것이 아닌 x를 1로 만들고서 메모리 주소를 출력하기만 했다. 메모리 주소는 변하지 않았고 기존의 x의 값만 바꾼 것을 알 수가 있다.

 

지역 변수는 영역을 벗어나면 사용할 수 없게 된다.
지역 변수가 차지하고 있던 메모리는 그 지역 변수가 영역을 벗어날 때 "스택(stack)" 메모리로 반납된다.
반납된 메모리는 다음 지역 변수가 사용할 수 있도록 대기한다.

1.5.2 지역 변수로 보는 매개변수와 인자의 차이

지역 변수 예제를 통해 매개변수와 인자의 차이를 알 수가 있다. 다음 코드의 결과를 살펴보자.

 

#include <iostream>

using namespace std;

void doSomething(int x)
{
	x = 123;
	cout << x << "\t" << &x << endl; // #2
}

int main()
{
	int x = 0;
	cout << x << "\t" << &x << endl; // #1

	doSomething(x);
	cout << x << "\t" << &x << endl; // #3

	return 0;
}

 

arguments and parameters

main 함수에서 생성한 x를 인자로 사용하는 doSometing 함수이다. 인자의 주소는 끝이 A54이지만 doSomething 함수에서 사용한 x의 메모리 주소는 A30으로 끝난다. 이를 통해 인자의 메모리 주소와 매개변수의 메모리 주소는 다르다는 것을 알 수 있다. 그렇기 때문에 x의 값을 123으로 변경했어도 main 함수에서 할당한 x의 값은 변하지 않음을 알 수 있다.

+ Recent posts