정적 데이터 멤버를 클래스 외부에서 Java와 달리 C ++에서 별도로 정의해야하는 이유는 무엇입니까?


41
class A {
  static int foo () {} // ok
  static int x; // <--- needed to be defined separately in .cpp file
};

A::x.cpp 파일 (또는 템플릿의 동일한 파일)에 별도로 정의 할 필요가 없습니다 . 왜 동시에 A::x선언하고 정의 할 수 없습니까?

역사적 이유로 금지되어 있습니까?

내 주요 질문은 static데이터 멤버가 동시에 선언 / 정의 된 경우 ( Java 와 동일 ) 모든 기능에 영향을 줍 니까?


가장 좋은 방법은 초기화 순서 문제를 피하기 위해 일반적으로 정적 변수를 정적 메서드 (로컬 정적)로 래핑하는 것이 좋습니다.
Tamás Szelei

2
이 규칙은 실제로 C ++ 11에서 약간 완화되었습니다. const 정적 멤버는 일반적으로 더 이상 정의 할 필요가 없습니다. 참조 : en.wikipedia.org/wiki/…
mirk

4
@afishwhoswimsaround : 모든 상황에 일반화 된 규칙을 지정하는 것은 좋은 생각이 아닙니다 (모범과 함께 모범 사례를 적용해야 함). 존재하지 않는 문제를 해결하려고합니다. 초기화 순서 문제는 생성자가 있고 다른 정적 스토리지 기간 오브젝트에 액세스하는 오브젝트에만 영향을줍니다. 'x'는 int이므로 첫 번째는 적용되지 않으므로 'x'는 비공개이므로 두 번째는 적용되지 않습니다. 셋째, 이것은 질문과 관련이 없습니다.
Martin York

1
스택 오버플로에 속합니까?
Monica와의 가벼움 경주

2
C ++ 17은 정적 데이터 멤버의 인라인 초기화를 허용합니다 (정수가 아닌 유형의 경우에도) inline static int x[] = {1, 2, 3};. en.cppreference.com/w/cpp/language/static#Static_data_members
Vladimir Reshetnikov를

답변:


15

당신이 고려한 한계는 의미론과 관련이 없으며 (초기화가 동일한 파일에 정의 된 경우 왜 변경해야합니까?) 오히려 이전 버전과의 호환성 때문에 쉽게 변경할 수없는 C ++ 컴파일 모델과 관련이 있다고 생각합니다. 너무 복잡해 지거나 (새 컴파일 모델과 기존 모델을 동시에 지원함) 기존 코드를 컴파일 할 수 없습니다 (새 컴파일 모델을 도입하고 기존 모델을 삭제하여).

C ++ 컴파일 모델은 (헤더) 파일을 포함하여 선언을 소스 파일로 가져 오는 C의 컴파일 모델에서 비롯됩니다. 이런 식으로 컴파일러는 포함 된 모든 파일과 해당 파일에서 포함 된 모든 파일을 재귀 적으로 포함하는 하나의 큰 소스 파일을 정확하게 볼 수 있습니다. 이것은 IMO에게 하나의 큰 장점, 즉 컴파일러를 쉽게 구현할 수 있다는 것입니다. 물론 포함 된 파일에 선언과 정의 모두를 쓸 수 있습니다. 헤더 파일에는 선언을, .c 또는 .cpp 파일에는 정의를 넣는 것이 좋습니다.

다른 한편으로, 다른 모듈에 정의 된 전역 심볼 의 선언가져 오는지 또는 다음에서 제공 하는 전역 심볼 의 정의컴파일하는 경우 컴파일러가 잘 알고있는 컴파일 모델을 가질 수 있습니다. 현재 모듈 . 후자의 경우에만 컴파일러가이 기호 (예 : 변수)를 현재 객체 파일에 넣어야합니다.

예를 들어 GNU Pascal에서는 다음 과 같이 a파일에 단위 를 작성할 수 있습니다 a.pas.

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

여기서 전역 변수는 동일한 소스 파일에서 선언되고 초기화됩니다.

그런 다음 a를 가져오고 전역 변수를 사용하는 다른 단위를 가질 수 있습니다 MyStaticVariable(예 : 단위 b ( b.pas)).

unit b;

interface

uses a;

procedure PrintB;

implementation

procedure PrintB;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

단위 c ( c.pas) :

unit c;

interface

uses a;

procedure PrintC;

implementation

procedure PrintC;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

마지막으로 메인 프로그램에서 단위 b와 c를 사용할 수 있습니다 m.pas.

program M;

uses b, c;

begin
  PrintB;
  PrintC;
  PrintB
end.

이 파일들을 따로 컴파일 할 수 있습니다 :

$ gpc -c a.pas
$ gpc -c b.pas
$ gpc -c c.pas
$ gpc -c m.pas

다음을 사용하여 실행 파일을 생성하십시오.

$ gpc -o m m.o a.o b.o c.o

그것을 실행하십시오 :

$ ./m
1
2
3

여기서의 트릭은 컴파일러가 프로그램 모듈에서 uses 지시문을 볼 때 (예 : b.pas에서 a를 사용) 해당 .pas 파일을 포함하지 않지만 .gpi 파일, 즉 사전 컴파일 된 파일을 찾는다는 것입니다. 인터페이스 파일 ( 문서 참조 ) 이러한 .gpi파일은 .o각 모듈이 컴파일 될 때 파일 과 함께 컴파일러에 의해 생성됩니다 . 따라서 전역 기호 MyStaticVariable는 객체 파일에서 한 번만 정의됩니다 a.o.

Java는 비슷한 방식으로 작동합니다. 그런 다음 컴파일러가 클래스 A를 클래스 B로 가져올 때 클래스 파일에서 A를 찾고 파일이 필요하지 않습니다 A.java. 따라서 클래스 A에 대한 모든 정의 및 초기화를 하나의 소스 파일에 넣을 수 있습니다.

C ++로 돌아가서 C ++에서 정적 데이터 멤버를 별도의 파일로 정의해야하는 이유는 링커 또는 컴파일러가 사용하는 다른 도구에 의해 부과 된 제한보다 C ++ 컴파일 모델과 더 관련이 있습니다. C ++에서 일부 기호를 가져 오면 현재 컴파일 단위의 일부로 선언을 작성하는 것을 의미합니다. 이는 템플릿이 컴파일되는 방식 때문에 무엇보다 중요합니다. 그러나 이것은 포함 된 파일에서 전역 기호 (함수, 변수, 메소드, 정적 데이터 멤버)를 정의 할 수 없거나 정의해서는 안된다는 것을 의미합니다. 그렇지 않으면 이러한 기호가 컴파일 된 객체 파일에서 다중 정의 될 수 있습니다.


42

정적 멤버는 클래스의 모든 인스턴스간에 공유되므로 한 곳에서만 정의해야합니다. 실제로는 일부 액세스 제한이있는 전역 변수입니다.

헤더에서 정의하려고하면 해당 헤더를 포함하는 모든 모듈에서 정의되며 모든 중복 정의를 찾을 때 링크하는 동안 오류가 발생합니다.

예, 이것은 적어도 부분적으로 cfront에서 나온 역사적 문제입니다. 일종의 숨겨진 "static_members_of_everything.cpp"를 생성하고 링크하는 컴파일러를 작성할 수 있습니다. 그러나 이전 버전과의 호환성을 손상시킬 수 있으며 그렇게하면 실제 이점이 없습니다.


2
내 질문은 현재 행동의 이유가 아니라 그러한 언어 문법에 대한 정당성입니다. 다시 말해, static변수가 같은 장소 (예 : Java)에서 선언 / 정의되면 무엇이 잘못 될 수 있다고 가정 해 봅시다 .
iammilind

8
@iammilind이 답변의 설명으로 인해 문법이 필요하다는 것을 이해하지 못한다고 생각합니다. 왜? C (및 C ++)의 컴파일 모델로 인해 c 및 cpp 파일은 별도의 프로그램과 같이 개별적으로 컴파일되는 실제 코드 파일이며 함께 실행되어 전체 실행 파일을 만듭니다. 헤더는 실제로 컴파일러의 코드가 아니며 c 및 cpp 파일 내부에 복사하여 붙여 넣을 텍스트 일뿐입니다. 이제 무언가가 여러 번 정의되면 컴파일 할 수 없으며 같은 이름의 여러 로컬 변수가있는 경우 컴파일하지 않는 것과 같은 방법입니다.
클라 임

1
@Klaim, static회원은 template어떻습니까? 모든 헤더 파일에서 볼 수 있어야합니다. 나는이 대답에 반대하지는 않지만 내 질문과도 일치하지 않습니다.
iammilind

@iammilind 템플릿은 실제 코드가 아니며 코드를 생성하는 코드입니다. 템플릿의 각 인스턴스에는 컴파일러에서 제공하는 각 정적 선언의 정적 인스턴스가 하나만 있습니다. 여전히 인스턴스를 정의해야하지만 인스턴스의 템플릿을 정의 할 때 위에서 언급 한 것처럼 실제 코드는 아닙니다. 템플릿은 말 그대로 컴파일러가 코드를 생성하기위한 코드 템플릿입니다.
Klaim

2
@iammilind : 템플릿은 일반적으로 정적 변수를 포함하여 모든 객체 파일에서 인스턴스화됩니다. ELF 오브젝트 파일이있는 Linux에서 컴파일러는 인스턴스화를 약한 기호 로 표시합니다. 즉, 링커는 동일한 인스턴스화의 여러 사본을 결합합니다. 헤더 파일에 정적 변수를 정의하는 데 동일한 기술을 사용할 수 있으므로 수행되지 않은 이유는 아마도 역사적 이유와 컴파일 성능 고려 사항의 조합 일 것입니다. 다음 C ++ 표준에 모듈이 통합되면 전체 컴파일 모델이 수정 될 것 입니다.
han

6

그 이유는 객체 파일과 연결 모델이 여러 객체 파일에서 여러 정의의 병합을 지원하지 않는 환경에서 C ++ 언어를 구현할 수 있기 때문입니다.

클래스 선언 (좋은 이유로 선언이라고 함)은 여러 번역 단위로 가져옵니다. 선언에 정적 변수에 대한 정의가 포함 된 경우 여러 번역 단위로 여러 정의가 생길 수 있습니다 (이 이름에는 외부 연결이 있음을 기억하십시오).

이러한 상황은 가능하지만 링커는 불평없이 여러 정의를 처리해야합니다.

(심볼의 종류 또는 배치 된 섹션의 종류에 따라 수행 할 수없는 경우 이는 하나의 정의 규칙과 충돌합니다.)


6

C ++과 Java에는 큰 차이가 있습니다.

Java는 모든 런타임 환경에 모든 것을 생성하는 자체 가상 머신에서 작동합니다. 정의가 두 번 이상 표시되면 런타임 환경이 완전히 알고있는 동일한 객체에 작용합니다.

C ++에는 "최종 지식 소유자"가 없습니다. C ++, C, Fortran Pascal 등은 소스 코드 (CPP 파일)에서 중간 형식 (OBJ 파일 또는 ".o"파일)으로 "번역기"입니다. OS) 여기서 명령문은 기계 명령어로 변환되고 이름은 기호 테이블이 매개하는 간접 주소가됩니다.

프로그램은 컴파일러가 아니라 다른 프로그램 ( "링커")에 의해 만들어집니다. 다른 프로그램 ( "링커")은 모든 OBJ를 (언어에 관계없이) 결합합니다. 효과적인 정의.

링커가 작동하는 방식에 따라 정의 (변수의 물리적 공간을 만드는 것)는 고유해야합니다.

C ++은 자체적으로 링크되지 않으며 링커는 C ++ 사양에 의해 발행되지 않습니다. 링커는 OS 모듈이 구축되는 방식 (일반적으로 C 및 ASM)으로 인해 존재합니다. C ++은 그것을 그대로 사용해야합니다.

이제 : 헤더 파일은 여러 CPP 파일에 "붙여 넣기"되어야합니다. 모든 CPP 파일은 다른 모든 CPP 파일과 독립적으로 번역됩니다. 서로 다른 CPP 파일을 번역하는 컴파일러는 모두 동일한 정의를 수신 하여 정의 된 객체에 대한 " 생성 코드 "를 모든 결과 OBJ에 배치합니다.

컴파일러는 모든 OBJ가 함께 사용되어 단일 프로그램을 구성하거나 다른 독립적 인 프로그램을 형성하기 위해 함께 사용되는지 알지 못합니다.

링커는 정의가 존재하는 방법과 이유와 정의의 출처를 알지 못합니다 (C ++에 대해서도 알지 못함 : 모든 "정적 언어"는 정의 할 링크와 참조를 생성 할 수 있음). 주어진 결과 주소에서 "정의 된"지정된 "기호"에 대한 참조가 있다는 것을 알뿐입니다.

주어진 심볼에 대해 여러 정의가 있고 (정의와 참조를 혼동하지 마십시오) 링커는 그와 함께해야 할 일에 대한 지식이 없습니다 (언어에 구애받지 않음).

큰 도시를 형성하기 위해 여러 도시를 통합하는 것과 같습니다. 두 개의 " 타임 스퀘어 "와 외부에서 " 타임 스퀘어 " 로 이동하도록 요청하는 사람들이 많으면 순수한 기술적 기준을 결정할 수 없습니다. ( 이름을 할당 한 정치에 대한 지식이 없어도 정확한 이름을 지정할 수 있습니다.)


3
전역 기호와 관련하여 Java와 C ++의 차이점은 가상 머신이있는 Java가 아니라 C ++ 컴파일 모델과 관련되어 있습니다. 이와 관련하여 Pascal과 C ++을 같은 범주에 두지 않습니다. 오히려 C와 C ++를 자바와 파스칼 (아마도 OCaml, Scala, Ada 등)과는 반대로 "가져온 선언이 기본 소스 파일과 함께 포함되고 컴파일되는 언어"로 가져온 선언은 내 보낸 심볼에 대한 정보가 포함 된 사전 컴파일 된 파일에서 컴파일러에 의해 조회됩니다 ".
Giorgio

1
@ Giorgio : Java에 대한 언급은 환영받지 못할 수도 있지만 Emilio의 대답은 문제의 요지, 즉 별도의 컴파일 후 객체 파일 / 링커 단계에 도달하면 대부분 옳다고 생각합니다.
ixache

5

그렇지 않으면 컴파일러가 변수를 넣을 위치를 모르기 때문에 필요합니다. 각 cpp 파일은 개별적으로 컴파일되며 다른 파일에 대해서는 알지 못합니다. 링커는 변수, 함수 등을 해결합니다. 개인적으로 vtable과 정적 멤버의 차이점이 무엇인지 알지 못합니다 (vtable이 정의 된 파일을 선택할 필요는 없습니다).

나는 주로 컴파일러 작성자가 그렇게하는 것이 더 쉽다고 생각합니다. 클래스 / 구조체 외부의 정적 변수는 일관성상의 이유로 또는 컴파일러 작성자에게 '구현하기 쉽기 때문에'표준에서 해당 제한을 정의했습니다.


2

이유를 찾은 것 같습니다. static별도의 공간에서 변수를 정의하면 변수를 임의의 값으로 초기화 할 수 있습니다. 초기화되지 않은 경우 기본값은 0입니다.

C ++ 11 이전에는 클래스 초기화가 C ++에서 허용되지 않았습니다. 따라서 다음 과 같이 쓸 수 없습니다 .

struct X
{
  static int i = 4;
};

따라서 변수를 초기화하려면 다음과 같이 클래스 외부에 변수를 작성해야합니다.

struct X
{
  static int i;
};
int X::i = 4;

다른 답변에서도 언급했듯이 int X::i이제는 전역이며 많은 파일에서 전역을 선언하면 여러 심볼 링크 오류가 발생합니다.

따라서 static별도의 번역 단위 내에서 클래스 변수 를 선언해야합니다 . 그러나 여전히 다음과 같은 방법으로 컴파일러가 여러 심볼을 만들지 않도록 지시해야한다고 주장 할 수 있습니다

static int X::i = 4;
^^^^^^

0

A :: x는 전역 변수이지만 네임 스페이스는 A이고 액세스 제한이 있습니다.

누군가 다른 전역 변수와 마찬가지로 여전히 선언해야하며 나머지 A 코드를 포함하는 프로젝트에 정적으로 링크 된 프로젝트에서도 수행 될 수 있습니다.

나는 이것을 모두 나쁜 디자인이라고 부를 것이지만, 이런 식으로 악용 할 수있는 몇 가지 기능이 있습니다.

  1. 생성자 호출 순서 ... int에는 중요하지 않지만 다른 정적 또는 전역 변수에 액세스 할 수있는보다 복잡한 멤버에게는 중요 할 수 있습니다.

  2. 정적 이니셜 라이저-클라이언트가 어떤 A :: x를 초기화해야하는지 결정할 수 있습니다.

  3. c ++ 및 c에서 포인터를 통해 메모리에 완전히 액세스 할 수 있으므로 변수의 실제 위치가 중요합니다. 링크 객체에서 변수가있는 위치를 기반으로 악용 할 수있는 매우 나쁜 것들이 있습니다.

나는 이것이 이런 상황이 "왜"인지 의심한다. 아마도 C가 C ++로 바뀌는 것과 아마도 이전 언어와의 호환성을 방해하는 이전 버전과의 호환성 문제 일 것입니다.


2
이것은 이전의 6 가지 답변에서 제시되고 설명 된 포인트를 넘어서는 실질적인 내용을 제공하지 않는 것 같습니다
gnat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.