본문 바로가기
개인공부/C++ 기초플러스

C++ Primer 09

by 하고싶은건많은놈 2023. 2. 19.

학습목표

  • 분할 컴파일
  • 기억 존속 시간, 사용 범위, 링크
  • 위치 지정 new
  • 이름 공간

9.1 분할 컴파일

C++은 프로그램을 구성하는 함수들을 별개에 파일에 넣는 것을 권장함
파일들을 분할하여 컴파일한 후 하나의 최종 실행 프로그램으로 링크할 수 있는데, 이 때 어떤 하나의 파일을 수정할 경우 해당 파일만을 다시 컴파일하여 이미 컴파일되어있는 다른 파일들과 링크하는 방식으로 관리함

  • 규모가 큰 프로그램을 쉽게 관리할 수 있음
  • Unix와 Linux에서는 make라는 프로그램이 해당 기능을 제공함
  • 대부분의 통합개발 환경들도 Project 메뉴를 통해 비슷한 기능을 제공함

여러 파일에 동일한 데이터가 들어갈 경우, 수정의 편리함 및 오류 방지를 위해 해당 데이터들을 헤더 파일에 넣고 #include로 각각의 소스 파일로 포함시킴
따라서 원본 프로그램을 세 부분으로 나눌 수 있음

  • 중복으로 사용되는 데이터들이 들어있는 헤더 파일
  • 해당 데이터들을 사용하는 함수들의 코드가 들어있는 소스 코드 파일
  • 함수들을 호출하는 코드가 들어있는 소스 코드 파일

헤더 파일에는 함수 정의나 변수 선언을 넣으면 안됨

  • 함수 원형 / #define이나 const를 사용하여 정의하는 기호 상수
  • 구조체 선언 / 클래스 선언 / 템플릿 선언 / 인라인 함수
  • <>으로 헤더 파일을 지정할 경우 컴파일러는 호스트 시스템의 파일 시스템 영역을 탐색함 : 표준 헤더 파일에 사용
  • ""로 사용할 겨우 현재 작업 디렉토리나 소스 코드 디렉토리에서 탐색함 : 사용자 정의 헤더 파일에 사용
// coordin.h

#ifndef COORDIN_H_
#define COORDINI_H_

struct polar
{
	double distance;
	double angle;
};
struct rect
{
	double x;
	double y;
};

polar rect_to_polar(rect xypos);
void show_polar(polar dapos);

#endif
// file1.cpp

#include <iostream>
#include "coordin.h"
using namespace std;
int main()
{
	rect rplace;
	polar pplace;

	cout << "x값과 y값을 입력하십시오 : ";
	while (cin >> rplace.x >> rplace.y)
	{
		pplace = rect_to_polar(rplace);
		show_polar(pplace);
		cout << "x값과 y값을 입력하십시오(끝내려면 q를 입력) : ";
	}
	cout << "프로그램을 종료합니다.\n";
	return 0;
}
// file2.cpp

#include <iostream>
#include <cmath>
#include "coordin.h"

polar rect_to_polar(rect xypos)
{
	using namespace std;
	polar answer;

	answer.distance = sqrt(xypos.x * xypos.x + xypos.y * xypos.y);
	answer.angle = std::atan2(xypos.y, xypos.x);
	return answer;
}

void show_polar(polar dapos)
{
	using namespace std;
	const double Rad_to_deg = 57.29577951;

	cout << "거리 = " << dapos.distance;
	cout << ", 각도 = " << dapos.angle * Rad_to_deg;
	cout << "도\n";
}

C++에서는 C와는 달리 헤더 파일을 빼고, 소스 코드 파일들만 컴파일함
전처리기 지시자 #ifndef#endif를 이용하여 헤더 파일을 중복으로 포함시키는 실수를 방지함

C++ 표준에서는 파일이라는 용어 대신 더 보편성이 큰 번역 단위(translation unit)이라는 용어를 사용함
컴파일러에 따라 이름 장식(혹은 이름 맹글링)의 방식이 다르기 때문에 서로 다른 컴파일러로 만들어진 목적 파일이나 라이브러리들은 링크시 에러가 발생할 가능성이 큼



9.2 기억 존속 시간, 사용 범위, 링크

C++은 네가지 유형으로 데이터를 저장하며, 유형마다 데이터를 존속시키는 시간에서 차이가 발생함

  • 자동 기억 존속 시간(automatic storage duration) : 함수 매개변수 및 함수 정의 안에 선언된 변수는 자동 변수로써 자동 기억 존속 시간을 가짐
    프로그램이 함수나 블록 안으로 들어갈 때 생성, 프로그램이 해당 함수나 블록을 떠나면 해제되며 C++은 두 종류의 자동 변수를 가짐
  • 정적 기억 존속 시간(static storage duration) : 함수 정의 바깥에서 정의된 변수 또는 static을 사용한 정적 변수는 정적 기억 존속 시간을 가짐
    프로그램이 실행되는 전체 시간동안 존속하며, C++은 세 종류의 정적 변수를 가짐
  • 쓰레드 존속 시간(Thread Storage Duration(C++11) : 멀티코어 프로세스를 사용하여 연산 작업을 쓰레드 단위로 쪼개서 처리함
  • 동적 기억 존속 시간(dynamic storage duration) : new연산자를 사용하여 대입된 메모리는 동적 기억 존속 시간을 가지며, 이는 delete연산자로 해제되거나 프로그램이 종료할 때까지 존속됨
    해당 메모리를 자유 공간(free store)이라고 부르기도 함

사용 범위와 링크

사용 범위(scope) : 어떠한 이름이 하나의 파일(혹은 번역 단위) 내에서 얼마나 널리 알려지는가를 나타냄
링크(linkage) : 서로 다른 번역 단위들이 이름을 공유하는 것

  • 외부 링크(external linkage)를 가진 이름은 여러 파일들이 공유할 수 있음
  • 내부 링크(internal linkage)를 가진 이름은 한 파일 안에 있는 함수들만 공유할 수 있음
  • 자동 변수는 공유되지 않기 때문에 링크를 갖지 않음

C++ 변수는 종류에 따라 다른 사용 범위를 가짐

  • 지역 사용 범위(local scope) 또는 블록 사용 범위(block scope) : 해당 변수를 정의한 블록 안에만 알려짐
  • 파일 사용 범위(file scope) 또는 전역 사용 범위(global scope) : 변수가 정의된 지점부터 그 아래로 파일 전체에 걸쳐 알려짐
  • 함수 원형 사용 범위(function prototype scope) : 매개변수 리스트를 둘러싸고있는 괄호 안에만 알려짐
  • 클래스 사용 범위(class scope) : 클래스 안에 선언된 멤버가 해당됨
  • 이름 공간 사용 범위(namespace scope) : 어떠한 이름 공간 안에 선언된 변수가 해당되며, 전역 사용 범위는 이름 공간 사용 범위의 특별한 경우임

C++의 함수는 다른 함수에 의해 호출되어야 하므로 지역 사용 범위는 가질 수 없음


자동 변수

함수 매개변수 및 함수 안에서 선언된 변수는 기본적으로 자동 기억 존속 시간을 가짐

  • 지역 사용 범위를 가지며, 링크는 없음
  • 각 변수는 프로그램이 해당 변수가 정의되어있는 블록에 들어갈 때 대입되며, 함수 종료시 메모리에서 사라짐
  • 단, 사용 범위는 블록 내에서도 변수가 선언된 지점부터 시작됨
// autoscp.cpp

#include <iostream>
void oil(int x);

int main()
{
	using namespace std;

	int texas = 31;
	int year = 2011;
	cout << "main()에서, texas = " << texas << ", &texas = ";
	cout << &texas << endl;
	cout << "main()에서, year = " << year << ", &year = ";
	cout << &year << endl;
	oil(texas);
	cout << "main()에서, texas = " << texas << ", &texas = ";
	cout << &texas << endl;
	cout << "main()에서, year = " << year << ", &year = ";
	cout << &year << endl;
	return 0;
}

void oil(int x)
{
	using namespace std;
	int texas = 5;

	cout << "oil()에서, texas = " << texas << ", &texas = ";
	cout << &texas << endl;
	cout << "oil()에서, x = " << x << ", &x = ";
	cout << &x << endl;
	{
		int texas = 113;
		cout << "블록에서, texas = " << texas << ", &texas = ";
		cout << &texas << endl;
		cout << "블록에서, x = " << x << ", &x = ";
		cout << &x << endl;
	}
	cout << "블록을 통과한 후, texas = " << texas;
	cout << ", &texas = " << &texas << endl;
}

블록의 앞뒤로 같은 이름의 변수가 있을 경우, 신규 정의가 이전 정의의 앞을 가려 이전 정의는 블록 안쪽에 있는 동안에는 사용되지 못함

자동 변수는 그 선언에 도달된 시점에서 값을 알 수 있을 경우 어떠한 표현식을 사용하더라도 초기화할 수 있음

C++컴파일러는 일반적으로 메모리의 일부를 예약해두고, 변수들의 생성과 소멸을 스택으로 관리함

  • 스택의 기본 크기는 상이하나 일반적으로 사용자가 선택할 수 있음
  • 스택은 LIFO(후입선출) 설계로 매개변수의 전달 과정이 간단함
  • 프로그램은 메모리의 시작 위치인 스택의 바닥을 지시하는 포인터와 비어있는 메모리 위치인 스택의 꼭대기를 지시하는 포인터를 사용하여 스택을 관리함
  • 새로 생성되는 데이터는 먼저 생성된 데이터의 위에 쌓이고, 데이터 사용을 마치면 스택에서 제거됨
    • 정확히는 이미 입력된 데이터는 제거되지 않고, 대신 포인터가 데이터가 저장되기 이전의 위치로 돌아가 값을 덮어쓸 수 있도록 함

기존의 C에서는 register 키워드로 컴파일러가 CPU 레지스터를 사용하여 자동 변수를 저장할 것을 요구했음
그러나 C+11에서는 어떤 한 변수가 자동적임을 명시하는 용도로만 사용되며, 이는 auto를 사용하면 되기 때문에 기존 코드를 인식하기 위한 목적으만 사용됨


정적 변수

정적 변수는 프로그램이 실행되는동안 변하지 않기 때문에 스택을 사용하는 대신, 모든 정적 변수를 수용할 수 있을만큼 넉넉한 크기의 메모리 블록을 대입함
정적 변수는 명시적으로 초기화하지 않은 경우 0으로 초기화됨

  • 외부 링크를 가지는 정적 변수 : 어떠한 블록에도 속하지 않는 완전한 바깥에 선언
  • 내부 링크를 가지는 정적 변수 : 어떠한 블록에도 속하지 않는 바깥에서 선언하되, 기억 공간 제한자 static을 붙임
  • 링크가 없는 정적 변수 : 블록 안에서 static 사용

정적 변수는 제로 초기화될 수 있으며, 제로 초기화와 상수 표현 초기화를 합해 정적 초기화라고 부름

  • 정적 초기화 : 컴파일러가 파일(혹은 변환 유닛)을 처리할 때 변수가 초기화됨
  • 동적 초기화 : 변수가 파일 처리 이후에 초기화됨

정적 변수는 초기화 명시 유무에 관계없이 우선 제로 초기화되며, 이후 상수 표기를 평가하여 상수 표시 초기화를 진행함
충분한 정보가 없을 경우에는 동적 초기화됨


정적 존속 시간 / 외부 링크

외부 변수는 전역 변수로 불리기도 하며, 정적 저장 기간과 파일 범위를 지님
외부 변수는 그 변수를 사용하는 모든 각각의 파일에서 선언되어야 하지만, C++은 하나의 변수에 대해 오직 하나의 정의를 부여하는 단일 정의 원칙(odr)를 원칙으로 명시함

double up;extern char gr = 'z';는 모두 정의에 속함
extern int abc;는 선언에 속함
정의는 한 곳에서 한번만 해야하며, 선언은 해당 변수의 정의가 들어있지 않은 파일에서 변수를 사용하기 위해 사용함

// external.cpp

#include <iostream>
using namespace std;

double warming = 0.3;

void update(double dt);
void local();

int main()
{
	cout << "전역 warming은 " << warming << "도입니다. \n";
	update(0.1);
	cout << "전역 warming은 " << warming << "도입니다. \n";
	local();
	cout << "전역 warming은 " << warming << "도입니다. \n";
	return 0;
}
// support.cpp

#include <iostream>
extern double warming;

void update(double dt);
void local();

using std::cout;

void update(double dt)
{
	extern double warming;
	warming += dt;
	cout << "전역 warming이 " << warming;
	cout << "도로 갱신되었습니다. \n";
}

void local()
{
	double warming = 0.8;

	cout << "지역 warming은 " << warming << "도입니다. \n";
	cout << "그러나, 전역 warming은 " << ::warming;
	cout << "도입니다. \n";
}

C++의 사용 범위 결정 연산자(scope resolution operator)인 :: 사용시 해당 변수의 전역 버전을 사용하라는 뜻이 됨

데이터에 대한 불필요한 접근을 잘 막을수록 데이터의 무결성이 보전되므로, 대부분의 경우 데이터를 무차별적으로 전역 변수로 만드는 것보다는 지역 변수로 만들어 꼭 필요한 함수에만 데이터를 전달하는 것이좋음
그러나 여러 함수가 공통으로 사용하는 데이터 블록에는 전역 변수가 유용할 수 있으며, 특히 const를 사용한 상수 데이터를 나타내는 데에 유용함


정적 존속 기간 / 내부 링크

파일 사용 범위가 있는 변수에 static 제한자 적용시 내부 링크를 부여함
서로 다른 파일에서 같은 이름의 변수를 사용하려고 시도할 시, 단일 변수 정의의 규칙에 위배되어 에러가 발생함
그러나 하나의 파일에서 static 외부 변수로 선언을 한다면 해당 변수는 그 파일에서만 사용하는 변수로 인식되어 규칙에 위배되지 않음

하나의 파일 안에 들어있는 함수들 사이에서 데이터를 공유하기 위해 static변수를 사용함
단, 이름 공간이 대안을 제공할 수 있으며 C++ 표준은 이를 더 권장함

// twofile1.cpp

#include <iostream>
int tom = 3;
int dick = 30;
static int harry = 300;

void remote_access();

int main()
{
	using namespace std;
	cout << "main()이 보고하는 세 변수의 주소 : \n";
	cout << &tom << " = &tom, " << &dick << " = &dick, ";
	cout << &harry << " = &harry\n";
	remote_access();
	return 0;
}
// twofile2.cpp

#include <iostream>
extern int tom;
static int dick = 10;
int harry = 200;

void remote_access()
{
	using namespace std;
	cout << "remote_access()가 보고하는 세 변수의 주소 : \n";
	cout << &tom << " = &tom, " << &dick << " = &dick, ";
	cout << &harry << " = &harry\n";
}

정적 기억 존속 시간 / 링크 없음

// static.cpp

#include <iostream>
const int ArSize = 10;

void strcount(const char * str);

int main()
{
	using namespace std;

	char input[ArSize];
	char next;

	cout << "영문으로 한 행을 입력하십시오:\n";
	cin.get(input, ArSize);
	while (cin)
	{
		cin.get(next);
		while (next != '\n')
			cin.get(next);
		strcount(input);
		cout << "다음 행을 입력하십시오(끝내려면 빈 행을 입력) : \n";
		cin.get(input, ArSize);
	}
	cout << "프로그램을 종료합니다.\n";
	return 0;
}

void strcount(const char * str)
{
	using namespace std;
	static int total = 0;
	int count = 0;
	
	cout << "\"" << str << "\"에는";
	while (*str++)
		count++;
	total += count;
	cout << count << "개의 문자가 있습니다.\n";
	cout << "지금까지 총 " << total << "개의 문자를 입력하셨습니다. \n";
}

블록 안에서 정의되는 변수에 static 제한자를 사용할 경우 함수의 호출과 호출 사이에서도 값을 보존할 수 있음
정적 지역 변수는 프로그램 시작시 한번만 초기화되고, 그 이후의 함수 호출시에는 다시 초기화되지 않음


제한자

기억 공간 제한자(storage class specifier)와 cv제한자는 기억 공간에 대한 추가 정보를 제공함

  • auto(C++11에서는 빠짐) : C++11이전에는 어떤 변수가 자동 변수라는 선언에 사용됨
  • register : 레지스터 기억 공간을 지정하는 선언에 사용됨
  • static : 파일 범위 선언에 사용시 내부 링크, 지역 선언에 사용시 지역 변수를 위한 정적 기억 존속 시간을 가리킴
  • extern : 참조 선언을 나타냄
  • thread_local(C++11에서 추가됨) : 변수의 존속 시간이 변수를 포함하는 쓰레드의 존속 시간임
  • mutable : 특정 구조체 또는 클래스가 const로 선언되어 있다 하더라도 해당 구조체의 특정 멤버를 변경할 수 있음을 나타냄

하나의 선언에는 하나의 제한자만 사용할 수 있으나, thread_localstatic 또는 extern과 함께 사용할 수 있음

cv제한자에는 constvolatile이 있음

  • const : 메모리가 초기화된 후에는 프로그램이 해당 메모리를 변경할 수 없음
    • 외부 링크가 디폴트인 전역 변수와는 달리 const 전역 변수는 내부 링크가 디폴트이기 때문에 static 제한자를 사용한 것과 같이 취급함
    • 이 때 앞에 extern 제한자를 붙이면 내부 링크를 가리면서 외부 링크로 만들 수 있으며, 보통의 변수와는 달리 반드시 초기화해야함
    • 함수나 블록 안에서 const 선언시 블록 사용 범위를 가지기 때문에 해당 블록 안에 있는 코드를 실행하고있을 때만 선언한 상수를 사용 가능함
  • volatile : 프로그램 코드가 변경하지 않더라도 특정 메모리 위치의 값이 변경될 수 있음을 나타냄
    컴파일러의 최적화 능력을 개선하는데 사용

함수와 링크

C와 마찬가지로 C++은 하나의 함수 안에서 다른 함수를 정의할 수 없기 때문에, 모든 함수는 자동적으로 정적 기억 존속 시간을 가짐
따라서 모든 함수는 프로그램이 실행되는 동안 계속 존재함
기본적으로 함수는 외부 링크를 가지며 여러 파일이 함수를 공유할 수 있음
static 제한자로 함수에 내부 링크를 부여할 수도 있으며, 이 경우 같은 이름의 외부 함수 정의보다 우선순위가 높음

C++의 단일 정의 규칙에 의해 인라인이 아닌 함수의 경우 프로그램은 정확이 단 하나의 정의만을 가져야 함
따라서 파일들 중 하나만 함수 정의를 가질 수 있으며, 다른 파일들은 해당 함수의 원형을 가져야 함
단, 인라인 함수는 이 규칙을 따르지 않기 때문에 헤더파일에 인라인 함수의 정의를 넣을 수 있음


언어 링크

링커는 구별되는 각 함수마다 서로 다른 기호 이름을 요구함
따라서 C++ 컴파일러는 오버로딩된 함수들에 대해 서로 다른 기호 이름을 생성하기 위해 이름 맹글링 또는 이름 장식 과정을 거침
즉, spliff(int)_spiff_i, spiff(double, double)_spiff_d_d로 변환하는 방식이며 이러한 접근방식을 C++ 언어 링크(C++ language linkage)라고 함

C에서의 언어 링크와 C++에서의 언어 링크를 혼용하여 쓸 때(C++ 프로그램에서 C라이브러리에 미리 컴파일된 함수를 사용하고 싶을 때)는 exttern "C" void spiff(int);와 같이 명시적으로 지정하여 사용함


기억 공간 형식과 동적 대입

new 연산자에 의해 대입된 메모리인 동적 메모리(dynamic memory)에는 기억 공간 형식이 적용되지 않음
동적 메모리는 자동 메모리와는 달리 LIFO가 아니기 때문에 대입 순서와 해제 순서는 newdelete를 언제 어떻게 사용하느냐에 달림
단, 동적 메모리를 추적하는데 사용되는 자동 및 정적 포인터 변수에는 기억 공간 형식이 적용됨

일반적으로 new로 대입한 메모리는 프로그램 종료시 해제되지만, 다소 취약한 운영체제에서 큰 메모리 블록을 요청하는 등의 경우가 생기면 자동으로 삭제되지 않을 수 있기 때문에 delete를 사용하는 것이 권장됨

동적 메모리 대입을 사용해 변수를 초기화할 수 있음
int *pi = new int (6);, double * pd = new double (99.99)의 형식을 사용
단, 순차적인 구조체 및 변수 초기화는 C++11에서만 가능한 중괄호를 사용한 리스트 초기화를 사용해야함

struct where (double x; double y; double z;);
where * one = new where {2.5, 5.3, 7.2};
int * ar = new int [4] {2, 4, 6, 7};
int *pin = new int {6};

위와 같이 단일 값을 가지는 변수에 대해서도 중괄호 초기화가 가능함

new가 필요한 메모리의 양을 확보할 수 없는 경우 과거에는 newnull포인터를 리턴하여 문제를 처리하였으나, 최근에는 std::bad_alloc예외를 반환함

new 연산자는 void * operator new(std::size_t); 함수를, new[] 연산자는 void * operator new[](std::size_t); 함수를 호출함
이 함수들을 대입 함수라고 부르며, 대입 함수는 전역 이름 공간의 일부임
deletevoid operator delete(void *);를, delete[]void operator delete[](void *);라는 해제 함수를 호출함
여기서의 std::size_ttypedef

int * pi = new int;int * pi = new(sizoef(int));로, int * pa = new int[40];int * pa = new(40 * sizeof(int));로 변환할 수 있으며 이처럼 new 연산자를 이용한 선언은 초기값을 설정할 수 있음
따라서 new 연산자를 사용하면 단순히 new() 함수를 호출하는 것보다 더 많은 일을 할 수 있음

일반적으로 new 연산자는 할당할 메모리 블록을 힙에서 찾음
그러나 위치 지정 new를 사용하면 사용할 위치를 사용자가 지정할 수 있음

  • 메모리 관리 절차 설정, 특정 메모리 위치에 있는 객체 생성, 특정 주소를 통해 접근하는 하드웨어를 다루기 등에 이 기능을 사용할 수 있음
  • new 헤더 파일을 포함시켜야 사용할 수 있음
// newplace.cpp

#include <iostream>
#include <new>

const int BUF = 512;
const int N = 5;
char buffer[BUF];

int main()
{
	using namespace std;

	double *pd1, *pd2;
	int i;
	cout << "new와 위치 지정 new의 첫번째 호출 : \n";
	pd1 = new double[N];
	pd2 = new (buffer) double[N];

	for (i = 0; i < N; i++)
		pd2[i] = pd1[i] = 1000 + 20.0 * i;
	cout << "메모리 주소 : \n" << pd1 << " : 힙;	"
		 << (void *) buffer << " : 정적" << endl;
	cout << "메모리 내용 : \n";
	for (i = 0; i < N; i++)
	{
		cout << &pd1[i] << "에 " << pd1[i] << ";	";
		cout << &pd2[i] << "에 " << pd2[i] << endl;
	}

	cout << "\nnew와 위치 지정 new의 두번째 호출 : \n";
	double *pd3, *pd4;
	pd3 = new double[N];
	pd4 = new (buffer) double[N];
	for (i = 0; i < N; i++)
		pd4[i] = pd3[i] = 1000 + 40.0 * i;
	cout << "메모리 내용 : \n";
	for (i = 0; i < N; i++)
	{
		cout << &pd3[i] << "에 " << pd3[i] << ";	";
		cout << &pd4[i] << "에 " << pd4[i] << endl;
	}

	cout << "\nnew와 위치 지정 new의 세번째 호출 : \n";
	delete [] pd1;
	pd1 = new double[N];
	pd2 = new (buffer + N * sizeof(double)) double[N];
	for (i = 0; i < N; i++)
		pd2[i] = pd1[i] = 1000 + 60.0 * i;
	cout << "메모리 내용 : \n";
	for (i = 0; i < N; i++)
	{
		cout << &pd1[i] << "에 " << pd1[i] << ";	";
		cout << &pd2[i] << "에 " << pd2[i] << endl;
	}
	delete [] pd1;
	delete [] pd3;
	return 0;
}

위치 지정 new는 메모리가 이미 점유되어있는지를 고려하지 않고 자신에게 전달된 주소를 그대로 사용함
delete는 보통의 new에 의해 대입된 힙 메모리를 지시하는 포인터에만 사용할 수 있기 때문에 정적 메모리를 delete로 해제할 수 없음

int *pi = new(buffer) int;new(sizeof(int), buffer)를 호출하고, 이렇게 오버로딩된 함수를 표준배치 new라고 함



9.3 이름 공간

서로 다른 라이브러리에 List, Tree, Node라는 클래스가 서로 호환되지 않는 방식으로 정의되어있을 때, 한 라이브러리의 List 클래스와 다른 라이브러리의 Tree클래스 사용시 두 클래스가 각각 자기 버전에 속하는 Node를 기대하게되고 이러한 충돌을 이름 공간 문제라고 함
따라서 C++ 표준은 이름 사용 범위를 더 잘 제어할 수 있도록 이름 공간이라는 기능을 제공함

구식 C++ 이름 공간

선언 영역(declarative region) : 선언을 할 수 있는 영역
전역 변수는 그것이 선언된 파일이며 함수 안에 어떤 변수를 선언할 시 해당 변수의 선언 영역은 변수가 선언된 블록이 됨

잠재 사용 범위(potential scope) : 변수를 선언한 지점부터 선언 영역의 끝까지
변수는 첫 정의 위에서는 사용할 수 없기 때문에 잠재 사용 범위는 선언 영역보다 좁음
단, 같은 이름으로 선언된 또 다른 변수에 의해 앞이 가려져 잠재 사용 범위 이내임에도 불구하고 변수가 보이지 않을 수 있음

사용 범위(scope) : 변수를 실제로 볼 수 있는 프로그램의 영역


새로운 이름 공간 기능

하나의 이름 공간에 속한 이름은 동일한 이름으로 다름 이름 공간에 선언된 이름과 충돌하지 않음
C++에서는 namespace 키워드를 사용하여 이름이 명명된 이름 공간을 만들 수 있으며, 이를 통해 이름을 선언하는 영역을 따로 제공할 수 있음

  • 이름 공간은 전역 위치 또는 다름 이름 공간 안에도 놓일 수 있음
  • 단, 블록 안에는 놓일 수 없으므로 하나의 이름 공간에 선언된 이름은 기본적으로 외부 링크를 가짐(상수 참조가 아닌 경우)
  • 전역 이름 공간(global namespace)이 기본적으로 파일 수준의 선언 영역에 존재하기 때문에 전역 변수라고 부르는 것들은 전역 이름 공간의 일부가 됨
  • 사용 범위 결정 연산자 ::를 이용해 어떤 이름을 주어진 이름 공간으로 제한할 수 있으며, 이름 공간이 지정된 이름을 제한된 이름(qualified name)이라고 함

using 선언과 using 지시자

using 선언 : 하나의 특별한 식별자를 사용할 수 있게 만들어줌

  • 함수 안에서 해당 이름을 지역 선언 영역에 추가하기 때문에 같은 이름으로 지역 변수를 만들 수 없음
  • 선언된 변수는 같은 이름의 전역 변수를 가릴 수 있음
  • 외부 위치에서 선언시 해당 이름이 전역 이름 공간에 추가됨

using 지시자 : 그 이름 공간 전체에 접근할 수 있게 해줌

  • 이름 공간 앞에 using namespace 키워드를 붙여 사용
  • 사용범위 결정 연산자 없이도 해당 이름 공간에 속한 모든 이름을 사용할 수 있음
  • 함수 안에 넣을시 이름들을 해당 함수 안에서만 사용할 수 있음

using 선언과 using 지시자는 이름 충돌의 가능성을 증가시킬 수 있음
하나의 using 선언을 사용하는 것은 using 선언이 놓이는 위치에 해당 이름을 선언하는 것과 같음
반면 using 지시자를 사용하는 것은 using 선언과 그 이름 공간 자체를 둘 다 포함하는 최소한의 선언 영역에 그 이름들을 선언하는 것과 같음

using 지시자를 사용하여 그 이름 공간에 있는 이름을 선언 영역에 불러오면 해당 이름의 지역 버전이 이름 공간 버전의 앞을 가림
이럴 경우에는 이름 공간의 이름이 지역 이름과 충돌시에도 경고 메세지가 발생하지 않음
반면 using 선언을 사용한 이름 공간의 이름이 지역 이름과 충돌하면 컴파일러가 그 사실을 알려줌
그렇기 때문에 일반적으로는 using 지시자보다 using 선언을 사용하는 것이 더 안전함

namespace elements
{
	namespace fire
	{
		int flame;
		...
	}
	float water;
}
elemetns::fire::flame = 10;
using namespace elements:: fire;

// 중첩된 이름 공간 선언 사용 가능

namespace myth
{
	using Jill:fetch;
	using namespace elements;
	using std::cout;
	using std::cin;
}
std::cin >> myth::fetch;
std::cout << Jill::fetch;
using namespace myth;
cin >> fetch; 

// 이름 공간 안에 using 지시자 및 선언 사용 가능

using 지시자는 과도적(transitive)임

  • A op B, B op C가 A op C를 의미하는 경우 op는 과도적임
  • 어떤 이름 공간에 대한 대용 이름(alias)를 만들어 중첩된 이름 공간을 보다 간단하게 사용할 수 있음

이름 공간의 이름을 생략시 이름을 명명하지 않은 이름 공간이 만들어짐

  • 이름을 명명하지 않은 공간에 있는 이름들은 전역 변수와 비슷함
  • 이 경우 using 선언을 명시적으로 사용하여 해당 이름 공간 안에 들어있는 이름들을 다른곳에서 사용할 수 있도록 만들 수 없음
  • 따라서 내부 링크를 가지는 정적 변수 대신 사용할 수 있음

이름 공간 예제

// namesp.h

#include <string>
namespace pers
{
	struct Person
	{
		std::string fname;
		std::string lname;
	};
	void getPerson(Person &);
	void showPerson(const Person &);
}

namespace debts
{
	using namespace pers;
	struct Debt
	{
		Person name;
		double amount;
	};
	void getDebt(Debt &);
	void showDebt(const Debt &);
	double sumDebts(const Debt ar[], int n);
}

두 개의 이름 공간을 정의하고, 각 이름 공간 안에 구조체의 정의 및 함수 원형들을 선언함

// namesp.cpp

#include <iostream>
#include "namesp.h"

namespace pers
{
	using std::cout;
	using std::cin;
	void getPerson(Person & rp)
	{
		cout << "이름을 입력하십시오 : ";
		cin >> rp.fname;
		cout << "성씨를 입력하십시오 : ";
		cin >> rp.lname;
	}
	void showPerson(const Person & rp)
	{
		std::cout << rp.lname << ", " << rp.fname;
	}
}

namespace debts
{
	void getDebt(Debt & rd)
	{
		getPerson(rd.name);
		std::cout << "부채를 입력하십시오 : ";
		std::cin >> rd.amount;
	}
	void showDebt(const Debt & rd)
	{
		pers::showPerson(rd.name);
		std::cout << " : $" << rd.amount << std::endl;
	}
	
	double sumDebts(const Debt ar[], int n)
	{
		double total = 0;
		for (int i = 0; i < n; i++)
			total += ar[i].amount;
		return total;
	}
}

namesp.h 헤더파일에 있는 이름 공간에 함수 정의를 추가함

// usenmsp.cpp

#include <iostream>
#include "namesp.h"

void other(void);
void another(void);

int main(void)
{
	using debts::Debt;
	using debts::showDebt;
	Debt golf = { {"Benny", "Goatsniff"}, 120.0};
	showDebt(golf);
	other();
	another();

	return 0;
}

void other(void)
{
	using std::cout;
	using std::endl;
	using namespace debts;
	Person dg = {"Doodles", "Glister"};
	showPerson(dg);
	cout << endl;
	Debt zippy[3];
	int i;
	for (i = 0; i < 3; i++)
		getDebt(zippy[i]);
	for (i = 0; i < 3; i++)
		showDebt(zippy[i]);
	cout << "부채 총액 : $" << sumDebts(zippy, 3) << endl;
	return ;
}

void another(void)
{
	using pers::Person;
	Person collector = {"Milo", "Rightshift"};
	pers::showPerson(collector);
	std::cout << std::endl;
}

using 선언을 사용할 경우 리턴형이나 함수 시그내처등의 정보는 알려주지 않고, 단지 이름만 알려줌


이름 공간의 미래

이름 공간의 가이드라인

  • 외부 전역 변수 대신 이름이 명명된 이름 공간에 있는 변수를 사용
  • 정적 전역 변수 대신 이름이 명명되지 않은 이름 공간에 있는 변수를 사용
  • 함수 또는 클래스 라이브러리 개발시 하나의 이름 공간에 넣어야함
  • using 지시자는 옛날 코드를 이름 공간 용도로 변환하는 임시 수단으로만 사용
  • using 지시자를 헤더 파일에 사용하지 말고, 사용하더라도 모든 #include 전처리기 뒤에 놓아야 함
  • 사용 범위 결정 연산자 또는 using 선언을 사용하여 이름을 선택적으로 들여와야함
  • using 선언에 대해 전역 범위 대신 선택적으로 지역 범위를 사용해야함


연습문제

  1. a) 자동 변수
    b) 하나의 파일에 외부 변수로 정의, 다른 파일에서 extern 키워드로 참조 선언
    c) static 키워드로 내부 링크를 가지는 정적 변수로 정의 혹은 이름이 명명되지 않은 이름 공간에 정의
    d) 함수 안에서 static 키워드를 붙여 지역 정적 변수로 정의
  2. using 선언은 이름 공간에 들어있는 하나의 이름에만 적용
    using 지시자는 해당 이름 공간 전체에 접근할 수 있게 해줌
  3. 3.
#include <iostream>
int main()
{
	double x;
	std::cout << "값을 입력하십시오 : ";
	while (!(std::cin >> x))
	{
		std::cout << "불량 입력. 수를 입력하십시오 : ";
		std::cin.clear();
		while (std::cin.get() != '\n')
			continue;
	}
	std::cout << "값 = " << x << std::endl;
	return 0;
}
  1. #include <iostream>
    int main()
    {
    	using std::cout;
    	using std::cin;
    	using std::endl;
    
    	double x;
    	cout << "값을 입력하십시오 : ";
    	while (!(cin >> x))
    	{
    		cout << "불량 입력. 수를 입력하십시오 : ";
    		cin.clear();
    		while (cin.get() != '\n')
    			continue;
    	}
    	cout << "값 = " << x << endl;
    	return 0;
    }
  2. average() 함수를 내부 링크를 가지는 정적 함수로 정의하거나 이름이 명명되지 않은 이름 공간에 적당한 average() 함수를 정의
  3. 6.
10
4
0
other(): 10, 1
another(): 10, -4
  1. 1
    4, 1, 2
    2
    2
    4, 1, 2
    2

'개인공부 > C++ 기초플러스' 카테고리의 다른 글

C++ Primer 10  (0) 2023.02.19
C++ Primer 09 Exercise  (0) 2023.02.19
C++ Primer 08 Exercise  (0) 2023.02.19
C++ Primer 08  (0) 2023.02.19
C++ Primer 07 Exercise  (0) 2023.02.19

댓글