분할 컴파일
파일들을 개별적으로 분할하여 컴파일한 후에, 그것들을 하나의 최종 실행 프로그램으로 링크 할 수 있다.
사용하는 C++ 컴파일러에 따르는 컴파일 과정의 세세한 측면이 아니라, 설계와 같은 일반적인 측면을 집중적으로 살펴보자.
예를 들어, 구조체 선언된 프로그램을 쪼개어, 두 개의 지원 함수를 하나의 별도 파일에 넣기로 가정하자.
원본 파일에서 main()의 끝 이후를 잘라내 별도의 파일에 저장한다고 해서 간단히 해결될 일이 아니다. 그 이유는 main()과, 별도의 파일에 넣은 두 함수가 같은 구조체 선언을 사용하기 때문이다. 따라서 양쪽 파일에 모두 그 구조체 선언을 넣을 필요가 있다.
새로운 문제가 발생하는 것을 원하는 사람은 아무도 없다. C와 C++의 개발자들도 그것을 원하지 않는다. 그래서 그들은 이런 상황을 깨끗하게 처리하기 위해 #include 기능을 제공한다.
일을 포함시킬 때 <coordin.h> 대신에 " coordin.h"를 사용한 것에 주목하라. 파일 이름이 괄호로 묶여 있으면, 컴파일러는 표준 헤더 파일들이 들어 있는 호스트 시스템의 파일 시스템 영역에서 그것을 찾는다. 그러나 큰따옴표로 묶여 있으면, 컴파일러는 먼저 현재 작업 디렉토리나 소스코드 디렉토리에서 그것을 찾는다.
기억 존속 시간, 사용 범위, 링크
- 자동 기억 존속 시간(automatic storage duration): 함수 매개변수를 포함하여, 함수 정의 안에 선언된 변수는 자동 기억 존속 시간을 가진다.
- 정적 기억 존속 시간(static storage duration): 함수 정의의 바깥에서 정의된 변수 또는 키워드 static을 사용하여 정의된 변수는, 정적 기억 존속 시건을 가진다.
- 쓰레드 존속 시간(Thread Storage Duration(C++11)): 요즘은 멀티코어 프로세서가 많이 사용되는데, 이것은 여러 작업을 동시에 처리할 수 있는 CPU를 의미하낟. 멀티코어 프로세서를 사용하여 연산 작업을 쓰레드(thread) 단위로 쪼개서 처리할 수 있다.
- 동적 기억 존속 시간(dynamic storage duration): new 연산자를 사용하여 대입된 메모리는, delete 연산자로 해제되거나 프로그램이 종료할 때까지, 둘중 어느 것이 먼저 일어날 때까지 존속한다. 이 메모리는 동적 기억 존속 시간을 가진다. 때로는 이 메모리를 자유 공간(free store)이라 부른다.
자동 변수
int main()
{
int teledeli = 5; // teledeli가 대입된다.
{
int websight = -2; // websight가 대입된다.
}// websight 사용 범위의 종료
}// teledeli가 만료되다.
안쪽 블록에 있는 변수의 이름을 websight 대신에 teledeli라고 지정하며, 바깥쪽 블록의 변수 이름과 안쪽 블록의 변수 이름을 같게 하면 어떻게 될까? 이러한 경우에, 프로그램은 안쪽 블록에 있는 구문들을 실행하는 동안에는 teledeli라는 이름이 안쪽 블록에서 정의된 지역 변수를 의미한다고 간주한다. 이러한 상황을 가리켜, 신규 정의가 이전 정의의 앞을 가린다고 말한다. 신규 정의가 사용 범위 안으로 들어오고, 이전 정의는 사용 범위 밖으로 잠시 물러난다. 그러나 프로그램이 안쪽 플록을 벗어나면 원래의 정의가 다시 사용 범위 안으로 복귀한다.
자동 변수와 스택
일반적인 방법은 메모리에 일부를 예약해 두고 변수들의 생성과 소멸을 스택으로 관리하는 것이다. 새로 생성되는 데이터는 먼저 생성된 데이터의 의해 (같은 위치가 아니라 인접한 위치에) 쌓이고, 프로그램이 데이터의 사용을 마치면 그 데이터는 스택에서 제거된다. 스택의 기본 크기는 C++마다 다르지만, 일반적으로 사용자가 스택의 크기를 선택할 수 있다.
레지스터 변수
본래 C는 register 키워드를 제공함으로서 컴파일러가 CPU 레지스터를 사용해서 자동 변수를 저장할 것을 제안한다.
register int count_faster;
C++11에서는 비록 이러한 것들이 사라졌지만 register 키워드를 어떤 한 변수가 자동적임을 명시하도록 남겨두었다. 이것이 auto를 사용하는 본래의 목적이지만, register 키워드를 유지하는 가장 중요한 목적은 이 키워드를 사용하는 기존 코드가 인식이 불가능해지는 것을 방지하기 위함이다.
정적 변수
C와 마찬가지로 C++는, 세 가지 유형의 링크(외부 링크, 내부 링크, 링크 없음)를 가지는 정적 변수를 제공하낟. 세 가지 유형 모두 프로그램이 실행되는 전체 시간동안 존속한다.
세 가지 유형의 정적 변수들이 어떻게 만드는지 알아보고, 계속해서 그들의 속성을 살펴보자.
외부 링크를 가지는 정적 변수를 만들려면, 어떠한 블록에도 속하지 않는 완전한 바깥에 그것을 선언하라.
내부 링크를 가지는 정적 변수를 만들려면, 어떠한 블록에도 속하지 않는 바깥에서 그것을 선언하되, static이라는 기억 공간 제한자를 선언 앞에 붙여라.
링크가 없는 정적 변수를 만들려면, static이라는 제한자를 사용하여 블록 안에서 그것을 선언하라
모든 정적 변수들은 다음의 초기화 형태를 따른다. 초기화되지 않은 정적 변수들은 모든 변수가 0으로 세팅된다. 이것을 제로 초기화(zero-initlalized)라고 부른다.
정적 존속 시간, 외부 링크(단일 정의 원칙)
한편으로는 외부 변수가 그 변수를 사용하는 모든 각각의 파일에서 선언되어야 할 것이다. 또 다른 한편으로는, C++에서는 하나의 변수에 대하여 오직 하나의 정의를 부여하는 "단일 정의 원칙"을 명시하고 있다. 이러한 요구사항을 충족시키기 위해서, C++은 두 종류의 변수 선언을 한다.
첫째는 선언을 정의하는 것 또한 단순하게 정의로, 대입되는 변수에 대하여 저장소를 제공한다.
둘째 참조 선언 혹은 단순히 선언하는 것인데, 이 경우엔 기존의 변수를 의미하므로 저장소를 생성하지 않는다. 첨조 선언은 extren이라는 키워드를 사용하고 초기화를 제공하지 않는다. 그렇지 않을 경우, 선언은 정의이며 저장소가 대입되도록 한다.
double up; //정의, up은 0
extern in blem; // blem은 다른 지역에서 정의된다.
extern char gr = 'z'; // 초기화되었으므로 정의이다.
만약 여러 파일에서 외부 변수가 사용될 경우, 오직 한 개의 파일이 그 변수에 대한 정의를 지닐 수 있다.(유일 정의 원칙). 그러나, 그 변수를 사용하는 다른 모든 파일들은 그 변수를 extern이라는 키워드를 사용해서 선언해 줄 필요가 있다.
// file01.cpp
extern int cats = 20; // 초기화되므로 정의에 해당된다.
int dogs = 22; // 정의에 해당된다.
int fleas; // 정의에 해당된다.
// file02.cpp
// file01.cpp로부터 고양이와 개를 사용한다.
extern int cats; // 그들이 사용하기 때문에 정의에 해당되지 않는다
extern int dogs; // extern에 해당되며 초기화 되지 않는다.
// file98.cpp
// file01.cpp로부터 cats, dogs, fleas를 사용한다.
extern int cats;
extern int dogs;
extern int fleas;
유일 정의 원칙은 주어진 이름에 오직 하나의 변수만이 가능하다는 것을 의미하지 안흔낟는 점을 주목해야 한다. 예를 들면, 동일한 이름을 함께 사용하지만 다른 함수에서 정의된 자동 변수는 별개의 변수이며, 서로 독립적임과 동시에 각자 고유의 개별적인 주소를 지닌다.
전역과 지역(global과 local)
전역 변수에는 모든 함수가 접근할 수 있고, 매개변수를 전달할 필요도 없기 때문에 전역 변수에 당장 호감이 간다. 그러나 이터럼 접근이 쉬운 것이 오히려 프로그램의 신뢰성을 떨어뜨린다. 경험에 의하면 데이터에 대한 불필요한 접근을 잘 막으면 막을수록 데이터의 무결성이 보전된다. 대부분의 경우에, 데이터를 무차별적으로 전역 변수로 만들기보다는, 지역 변수로 만들어 꼭 필요한 함수에만 데이터를 전달하는 것이 좋다.
그러나 전역 변수가 유용할 때도 있다. 예를 들어, 월 이름의 배열이나 원소 원자량의 배열처럼 여러 함수가 공통으로 사용하는 데이터 블록을 만들 수 있다. const 키워드를 사용하면 상수 데이터가 변경되지 않도록 할 수 있기 때문에, 전역 변수는 특히 상수 데이터를 나타내는데 유용하다.
정적 존속 기간, 내부 링크
서로 다른 파일에서 서로 다른 변수들에게 같은 이름을 사용하기를 원한다면 어떻게 하면 될까? extern을 빼기만 하면 될까?
아니다. 왜냐하면 단일 변수를 정의해야하는 규칙에 위배되기 때문이다.
그러나, 만일 다른 파일에서 같은 이름으로 정의된 외부 변수를 하나의 파일에서 static외부 변수로 선언을 한다면, static으로 선언된 변수는 그 파일에서만 사용되는 변수로 인식 된다.
// file1
int errors = 20; // 외부 선언
// file2
static int errors = 5; //file2에만 사용된다.
cout << errors; //file2에서 정의된 errors를 사용한다.
정적 기억 존속 시간, 링크 없음
블록안에 정의되는 변수에 static 제한자를 적용하여 만든다. 블록 안에서 static을 사용하면, static이 지역 변수를 정적 기억 존속 시간을 갖게 된다. 이것은, 그런 변수들이 블록 안에서만 알려지지만 그 블록이 확동하지 않는 동안에도 계속 존재한다는 것을 의미한다. 그러므로 정적 지역 변수는 함수의 호출과 호출 사이에서도 값을 보존할 수 있다.
제한자
기억 공간 제한자(storage class specifier)와 cv제한자라 부르는 몇 가지 C++ 키워드가 기억 공간에 대해서 추가 정보를 제공한다. 기억 공간 제한자의 목록은 다음과 같다.
auto // C++11에서는 제한자에서 빠짐
register
static
extern
thread_local // C++11에서 추가됨
mutable
thread_local 키워드는 변수의 존속 시간이 변수를 포함하는 쓰레드의 존속 시간이다. 정적 변수의 존속 시간은 프로그램의 존속 시간과 같지만, thread_local변수는 존속 시간이 쓰레드의 존속 시간과 같다. mutable 키워드는 const 관점에서 설명할 수 있으므로, mutable을 설명하기 전에 cv-제한자를 먼저 살펴보기로 하겠다.
Cv-제한자
다음은 cv-제한자이다.
const
volatile
volatile 키워드는 프로그램 코드가 변경하지 않더라도 특정 메모리 위치에 있는 값이 변경될 수 있다는 것을 나타낸다. 예를 들면, 직렬 포트로부터 들어오는 시간이나 정보가 저장되어 있는 어떤 하드웨어 위치를 지시하는 포인터를 가질 경우, 프로그램이 아니라 하드웨어가 그 메모리의 내용을 변경한다.
mutable
mutable의 설명으로 돌아가자. mutable은 특정 구조체(또는 클래스)가 const로 선언되어 있다 하더라도 그 구조체의 특정 멤버를 변경할 수 있음을 나타내는 데 사용할 수 있다. 예를 들어, 다음과 같은 코드 단편이 있다고 가정해 보자.
struct data
{
char name[30];
mutable int accesses;
};
const data veep = {"Claybourne", 0};
strcpy(veep.name, "Joye Joux"); // 혀용되지 않는다.
veep.accesses++; // 허용된다
veep에 적용한 const 제한자는 프로그램이 veep의 멤버를 변경하지 못하게 한다. 그러나 accesses멤버에 적용한 mutable 제한자는 accesses가 그러한 제한을 적용받지 않게 조치한다.
const에 대한 보충
C++에서는 const 제한자가 디폴트 기억 공간을 약간 바꾼다. 전역 변수는 외부 링크를 갖도록 디폴트로 내정되어 있지만, const 전역 변수는 내부 링크를 갖도록 디폴트로 내정된다. 즉, 다음 코드 단편에서와 같이, C++는 전역 const 정의를 마치 static 제한자가 사용된 것처럼 취급한다.
const int fingers = 10; // static const int fingers;와 같다.
int main()
{
...
}
C++는 상수 데이터형에 대한 규칙을 사용하기 쉽게 변경하였다. 예를 들어, 어떤 헤더 파일 안에 넣고 싶은 상수들의 집합으 가지고 있고, 하나의 다중 파일 프로그램을 구성하는 여러 파일이 그 헤더 파일을 모두 사용한다고 가정하자. 전처리기가 그 헤더 파일의 내용을 각각의 소스 파일에 포함시키고 나면, 각 소스 파일에는 다음과 같은 정의가 들어갈 것이다.
const int fingers = 10;
const char * warning = "Wak!";
만약 전역 const 선언이 보통의 변수들처럼 외부 링크를 가지고 있다면, 전역 변수는 하나의 파일에서만 정의될 수 있기 때문에 이것은 분명 에러가 된다. 즉, 하나의 파일에만이 위와 같은 선언을 가질 수 있고, 다른 파일들은 모두 extern 키워드를 사용해야 한다. 그리고 extern 키워드를 가지고 있지 않은 선언만 값을 초기화 할 수 있다.
그러므로 정의들의 집합을 하나의 파일에 넣고, 선언들의 다른 집합은 다른 파일들에 넣을 필요가 있다. 그 대신에, 외부적으로 정의된 const 데이터는 내부 링크를 갖기 때문에, 모든 파일에 같은 선언을 사용할 수 있다.
또한 내부 링크는 각 파일들이 상수 집합을 공유하지 않고 자신만의 상수 집합을 따로 가진다는 것을 의미한다. 각각의 정의는 그들이 들어 있는 파일에 개별적이다.
상수 정의들을 헤더 파일에 넣는 것이 좋은 이유가 바로 이거다. 동일한 헤더 파일을 두 개의 소스 코드 파일에 넣으면, 두 파일은 동일한 상수 집합을 갖게 된다.
만일 어떠한 이유로 어떠한 상수를 위부 링크로 만들어야 한다면, extern 키워드를 사용항여 디폴트로 되어있는 내부 링크를 가릴 수 있다. 그리고 그 상수를 사용하는 모든 파일에서 extern 키워드를 사용하여 그 상수를 선언해야 한다. 이것은 보통의 외부 변수와는 다르다. 왜냐하면 보통의 위부 변수를 정의할 때에는 extern 키워드를 사용하지 않고, 그 변수를 사용하는 다른 파일에서 extern 키워드를 사요하기 때문이다. 또한 보통의 변수와는 다르게, extern const 값은 초기화할 수 있다.
함수와 링크
함수 역시 링크 속성을 가진다. C와 마찬가지로 C++는 하나의 함수 안에서 다른 함수를 정의할 수 없다. 따라서 모든 함수는 자동적으로 정적 기억 존속 시간을 가진다.
함수는 외부 링크를 가진다. 이 말은 여러 파일이 함수를 공유할 수 있다는 뜻이다. 실제로는 함수 원형에 extern을 정용하여 그 함수가 다른 파일에 정의되어 있다는 것을 나타댈 수도 있지만, 그것은 선택적이다. 또한 static 키워드를 사용하여 함수에 내부 링크를 부여할 수 있다. 이것은 그 함수의 사용 범위를 하나의 파일로 제한한다.
위치 지정 new 연산자
일반적으로, new 연산자는 사용자가 요청한 메모리 크기를 충분히 다룰 수 있을 만큼의 메모리 블록을 힙에서 찾는다. 그런데 new 연산자는, 사용할 위치를 사용자가 지정할 수 있는 위치 지정 new라는 한 가지 변형이 있다.
위치 지정 new 기능을 사용하려면, 먼저 new 헤더 파일을 포함시켜야 하낟. 이 해더 파일은 이 버전의 new를 위한 원형을 제공한다. 그러고 나서 원하는 주소를 제공하는 매개변수와 함께 new를 사용한다.
#include <new>
struct dhaff
{
char dross[20];
int slag;
};
char buffer1[50];
char buffer2[500];
int main()
{
chaff *p1, *p2;
int *p3, *p4;
// new의 일반 형식
p1 = new chaff; // 구조체를 힙에 놓는다.
p3 = new int [20]; // int 배열을 힙에 놓는다.
// 위치 지정 new의 두 형식
p2 = new (buffer1) chaff; // 구조체를 buffer1에 놓는다.
p4 = new (buffer2) int[20]; // int 배열을 buffer2에 놓는다.
}
간단하게, 이 예제는 위치 지정 new를 위한 메모리 공간을 제공하기 위해 두 개의 정적 배열을 사용한다. 그래서 이 코드는 chaff 구조체를 위한 공간을 buffer1에 대입하고, int형 20개짜리 배열을 위한 공간을 buffer2애 대입한다.
위치 지정 new에 의해 사용된 메모리를 delete를 사용하여 해제하지 않는다. 실제로, 이 경우에는 그렇게 할 수 없다. buffer에 의해 지정된 메모리는 정적 메모리 이다. delete는 보통의 new에 의해 대입된 힙 메모리를 지시하는 포인터에만 사용할 수 있다. 즉, buffer 배열은 delete의 관할권 밖에 놓여있다. 그래서 다음 구문은 실행시 에러를 발생 시킬 것이다.
delete [] pd2; // 동작하지 않는다.
new의 배치 형식
일반적인 new가 하나의 매개변수를 가지고 new 함수를 불러오듯이 표준배치 new는 두 가지 매개변수를 가지고 new 함수를 불러온다.
int * pi = new int; // new(sizeof(int))를 호출한다.
int * pi = new(buffer) int; // new(sizeof(int), buffer)를 호출한다.
int * p3 = new(buffer) int [40]; // new(40*sizeof(int), buffer)를 호출한다.
표준 배치 new 함수는 대체될 수는 없지만 오버로딩될 수는 있다.
구식 C++ 이름 공간
C++가 이미 가지고 있었던 이름 공간 기능을 복습하고, 몇 가지 용어를 소개한다. 이름 공간이라는 개념을 파악하는 데 이것이 도움이 될 것이다.
첫째, 선언 영역(declarative region)이라는 용어를 이해할 필요가 있다.
둘째, 잠재 사용 범위(potential scope)라는 용어를 이해할 필요가 있다. 어떤 변수의 잠재 사용 범위는 그 변수를 선언한 지점부터 선언 영역의 끝까지이다.
변수는 처음으로 그것을 정의한 지점의 위에는 사용할 수 없기 때문에, 잠재 사용 범위는 선언 영역보다 좁다. 하지만 잠재 사용 범위에서 변수가 보이지 않을 수도 있다.
예를 들어, 어떤 변수는 내포된 선언 영역에 같은 이름으로 선언된 또 다른 변수에 의해 앞이 가려질 수 있다. 가령, 함수 안에서 선언된 지역변수는, 같은 파일에 선언된 전역 변수의 앞을 가린다. 변수의 실제로 볼 수 있는 프로그램의 영역을 사용 범위(scope)라고 한다.
새로운 이름 공간 기능
새로운 종류의 선언 영역을 정의함으로써 이름이 명명된 이름 공간을 만들 수 있는 기능이 C++에 새로 추가 되었다. 그것의 주된 목적은 이름을 선언하는 영역을 따로 제공하는 것이다. 하나의 이름 공간에 속한 이름은, 동일한 이름으로 다른 이름 공간에 선언된 이름과 충돌하지 않는다.
다음과 같은 코드는 namespace라는 새로운 키워드를 사용하여 Jack과 Jill이라는 두 개의 이름 공간을 만든다.
namespace Jack {
double pail;
void fetch();
int pal;
struct Well {...};
}
namespace Jill {
double bucket(double n) {...};
void fetch;
int pal;
struct Hill {...};
}
사용자가 정의하는 이름 공간 외에, 전역 이름 공간(globle namespace)라는 또 하나의 이름 공간이 있다. 이것은 파일 수준의 선언 영역에 해당한다.
물론, 어떤 주어진 이름 공간에 속해 있는 이름에 접근할 수 있는 방법이 필요하다. 가장 간단한 방법은 사용 범위 결정 연산자 ::을 사용하여, 어떤 이름을 주어진 이름 공간으로 제한하는(qualify) 것이다.
Jack::pail = 12.34;
Jill::Hill mole;
Jack::fetch();
pill과 같이, 특별한 이롬 공간이 지정되지 않는 이름을 제한되지 않은 이름(unqualified name)이라 하고, Jack::pail처럼 이름 공간이 지정된 이름을 제한된 이름(qualified name)이라 한다.
using 선언과 using 지시자
사용할 때마다 매번 이름을 제한한다는 것은 성가시므로, C++는 이름 공간에 속해있는 이름을 간편하게 사용할 수 있는, using 선언과 using 지시자라는 두 가지 방법을 제공한다. using 선언은 하나의 특별한 식별자를 사용할 수 있게 만들고, using 지시자는 그 이름 공간 전체에 접근할 수 있게 만든다.
using Jill;;fetch; // using 선언
using namespace Jack //Jack에 속한 모든 이름을 사용할 수 있게 만든다.
일반적으로 말해서, 사용할 수 있게 하려는 이름이 무엇인지 분명하게 나타내 주므로 using 지시자보다는 using 선언을 사용하는 것이 더 안전하다.
이름 공간에 대한 보충
이름 공간 선언을 다음과 같이 중첩시킬 수 있다.
namespace elements
{
namespace fire
{
int flame;
}
float water;
}
이 경우에는 flame 변수를 elements::fire::flame으로 참조한다. 마찬가지로, 다음과 같은 using 지시자를 사용하면 내부의 이름을 사용할 수 있다.
using namespace elelmemnt::fire;
이름을 명명하지 않은 이름 공간
이름 공간의 이름을 생략하면 이름을 명명하지 않은 이름 공간이 만들어진다.
namespace
{
int ice;
int bandycoot;
}
어떤 한 파일 안에 있는 이름을 명명하지 않은 이름 공간으로 부터 이름 공간을 선언한 경우를 제외하고는 이름을 사용할 수 없다. 이름 공간 선언은 정적 변수를 사용하는 데 있어서 내부 연결을 가능하게 하는 대안을 제사한다. 예를 들어 다음과 같은 코드가 있다고 가정하자.
static int counts;
이렇게 하는 대신에 이름 공간 접근법을 사용하는 것은 다음과 같은 것이다.
namespace
{
int counts
}
'C++ > C++ 기초 플러스 6판(Book)' 카테고리의 다른 글
[C++] 클래스의 활용 (0) | 2025.03.08 |
---|---|
[C++] 객체와 클래스 (0) | 2025.03.07 |
[C++] 함수의 활용 (0) | 2025.03.05 |
[C++] C++의 프로그래밍 모듈 (0) | 2025.03.04 |
[C++] 분기 구문과 논리 연산자 (0) | 2025.03.04 |