C ++의 변수는 유형을 어떻게 저장합니까?


42

특정 유형의 변수를 정의하면 (내가 아는 한 변수 내용에 대한 데이터를 할당하는 경우) 변수 유형을 어떻게 추적합니까?


8
" 어떻게 추적 합니까? "에서 " it "로 누가 / 무엇을 언급 합니까? 컴파일러 또는 CPU 또는 언어 또는 프로그램과 같은 다른 것?
Erik Eidt


8
@ErikEidt IMO OP는 분명히 "변수 자체"를 "it"로 의미합니다. 물론이 질문에 대한 두 단어의 대답은 "그렇지 않습니다"입니다.
alephzero

2
좋은 질문입니다! 오늘날 관련성이 높은 모든 언어를 고려할 때 특히 관련이 있습니다.
Trevor Boyd Smith

@alephzero 그것은 분명히 주요한 질문이었습니다.
Luaan

답변:


105

변수 (또는 일반적으로 C의 의미에서 "개체")는 런타임에 해당 유형을 저장하지 않습니다. 머신 코드에 관한 한, 형식화되지 않은 메모리 만 있습니다. 대신이 데이터에 대한 작업은 데이터를 특정 유형 (예 : 플로트 또는 포인터)으로 해석합니다. 형식은 컴파일러에서만 사용됩니다.

예를 들어 구조체 또는 클래스 struct Foo { int x; float y; };와 변수 가있을 수 있습니다 Foo f {}. 필드 액세스는 어떻게 auto result = f.y;컴파일 할 수 있습니까? 컴파일러는 이것이 f유형의 객체임을 Foo알고 있으며 Foo-objects 의 레이아웃을 알고 있습니다. 플랫폼 별 세부 사항에 따라 "시작 지점에 포인터를 f가져간 다음 4 바이트를 추가 한 다음 4 바이트를로드하고이 데이터를 부동 소수점으로 해석 "으로 컴파일 될 수 있습니다 . 많은 기계 코드 명령어 세트 (x86-64 포함) ) 플로트 또는 정수를로드하는 다른 프로세서 명령이 있습니다.

C ++ 타입 시스템이 우리 타입을 추적 할 수없는 한 가지 예는 다음과 같은 공용체 union Bar { int as_int; float as_float; }입니다. 공용체는 다양한 유형의 개체를 하나 이상 포함합니다. 공용체에 객체를 저장하면 이것이 공용체의 활성 유형입니다. 우리는 그 유형을 유니온에서 다시 가져 오려고 시도해야합니다. 다른 것은 정의되지 않은 동작입니다. 액티브 타입이 무엇인지 프로그래밍하는 동안“알고”있거나 타입 태그 (보통 열거 형)를 별도로 저장하는 태그 된 유니온을 만들 수 있습니다 . 이것은 C의 일반적인 기술이지만 공용체와 유형 태그를 동기화해야하기 때문에 오류가 발생하기 쉽습니다. void*포인터는 노동 조합과 유사하지만 함수 포인터를 제외하고, 포인터 객체를 보유 할 수 있습니다.
C ++은 알려지지 않은 유형의 객체를 처리하는 두 가지 더 나은 메커니즘을 제공합니다. 객체 지향 기술을 사용하여 유형 삭제 를 수행 하거나 (실제 유형을 알 필요가 없도록 가상 메소드를 통해 객체와 상호 작용할 수 있음) 사용 std::variant, 형태 보증 된 노동 조합의 일종.

C ++가 객체의 유형을 저장하는 경우가 하나 있습니다. 객체의 클래스에 가상 메소드 ( "다형성 유형", 일명 인터페이스)가있는 경우. 가상 메소드 호출의 대상은 컴파일 타임에 알 수 없으며 런타임시 오브젝트의 동적 유형 ( "동적 디스패치")에 따라 분석됩니다. 대부분의 컴파일러는 객체의 시작 부분에 가상 함수 테이블 ( "vtable")을 저장하여이를 구현합니다. vtable은 런타임에 객체의 유형을 얻는 데 사용될 수도 있습니다. 그런 다음 컴파일 타임에 알려진 식의 정적 유형과 런타임에 객체의 동적 유형을 구분할 수 있습니다.

C ++을 사용하면 객체 typeid()를 제공 하는 연산자 로 객체의 동적 유형을 검사 할 수 있습니다 std::type_info. 컴파일러는 컴파일 타임에 객체 유형을 알고 있거나 필요한 유형 정보를 객체 내부에 저장하여 런타임에 검색 할 수 있습니다.


3
매우 포괄적입니다.
중복 제거기

9
다형성 객체의 유형에 액세스하려면 컴파일러는 여전히 객체가 특정 상속 패밀리에 속한다는 것을 알아야합니다 (즉, 객체가 아닌 객체에 대한 형식화 된 참조 / 포인터가 있음 void*).
Ruslan

5
첫 번째 문장이 사실이 아니기 때문에 +0 ​​마지막 두 단락이 그것을 정정합니다.
Marcin

3
일반적으로 다형성 객체의 시작 부분에 저장되는 것은 테이블 자체가 아니라 가상 메소드 테이블에 대한 포인터입니다.
Peter Green

3
@ v.oddou 내 단락에서 몇 가지 세부 사항을 무시했습니다. typeid(e)정적 유형의 표현식을 검사합니다 e. 정적 유형이 다형성 유형 인 경우 표현식이 평가되고 해당 객체의 동적 유형이 검색됩니다. 알 수없는 유형의 메모리에서 typeid를 가리키고 유용한 정보를 얻을 수 없습니다. 예를 들어 공용체의 typeid는 공용체의 개체가 아닌 공용체를 설명합니다. a의 typeid void*는 단지 void 포인터입니다. 그리고 a void*를 참조하여 내용을 얻을 수는 없습니다 . C ++에서는 명시 적으로 프로그래밍하지 않으면 권투가 없습니다.
amon

51

다른 답변은 기술적 측면을 잘 설명하고 있지만 일반적인 "머신 코드에 대한 생각 방법"을 추가하고 싶습니다.

컴파일 후의 머신 코드는 꽤 멍청하며 실제로 모든 것이 의도 한대로 작동한다고 가정합니다. 간단한 기능이 있다고 가정 해보십시오.

bool isEven(int i) { return i % 2 == 0; }

그것은 int를 취하고 bool을 뱉어냅니다.

컴파일 후에는 자동 오렌지 과즙 짜는기구와 같은 것으로 생각할 수 있습니다.

자동 오렌지 과즙

오렌지를 먹고 주스를 반환합니다. 들어오는 물체의 유형을 인식합니까? 아니요, 그들은 단지 오렌지색이어야합니다. 오렌지 대신 사과를 먹으면 어떻게 되나요? 아마도 깨질 것입니다. 책임있는 소유자가 이런 식으로 사용하려고하지 않기 때문에 중요하지 않습니다.

위의 기능은 비슷합니다. int를 취하도록 설계되었으며 다른 것을 먹었을 때 부적절하거나 무언가를 할 수 있습니다. 컴파일러 (일반적으로)는 결코 발생하지 않는지 확인하기 때문에 (보통) 중요하지 않습니다. 실제로 올바른 형식의 코드에서는 발생하지 않습니다. 컴파일러가 함수가 잘못된 유형의 값을 얻을 가능성을 감지하면 코드 컴파일을 거부하고 대신 유형 오류를 반환합니다.

주의해야 할 점은 컴파일러가 전달할 잘못된 형식의 코드 사례가 있다는 것입니다. 예를 들면 다음과 같습니다.

  • 잘못된 유형의 캐스팅 : 명시 적 캐스트가 올바른 것으로 가정하고, 그가 캐스팅되지 않도록 프로그래머에 있습니다 void*orange*포인터의 다른 쪽 끝에있는 사과가있을 때,
  • 널 포인터, 댕글 링 포인터 또는 사용 후 범위와 같은 메모리 관리 문제; 컴파일러는 대부분을 찾을 수 없습니다.
  • 내가 놓친 다른 것이 있다고 확신합니다.

말했듯이 컴파일 된 코드는 과즙 짜는 기계와 같습니다. 처리 방법을 모르고 명령을 실행합니다. 그리고 지침이 틀리면 깨집니다. 그렇기 때문에 C ++에서 위의 문제로 인해 제어되지 않은 충돌이 발생합니다.


4
컴파일러 함수에 올바른 유형의 객체가 전달되었는지 확인 하려고 하지만 C와 C ++는 컴파일러가 모든 경우에이를 입증하기에는 너무 복잡합니다. 따라서 과즙 짜는기구에 대한 사과와 오렌지의 비교는 매우 유익합니다.
Calchas

@Calchas 귀하의 의견에 감사드립니다! 이 문장은 실제로 지나치게 단순화되었습니다. 가능한 문제에 대해 조금 자세히 설명했지만 실제로 질문과 관련이 있습니다.
Frax

5
기계어 코드에 대한 은유! 당신의 은유는 그림으로도 10 배 향상되었습니다!
Trevor Boyd Smith

2
"내가 빠진 것이있을 것입니다." - 물론이야! C void*foo*일반적인 산술 판촉, union유형 punning, NULLvs.에 강요합니다 . nullptr심지어 나쁜 포인터를 갖는 것 조차 UB 등입니다. 그대로입니다.
케빈

@ 케빈 질문에 C ++로만 태그되어 있기 때문에 여기에 C를 추가 할 필요가 없다고 생각합니다. 그리고 C ++에서 void*암시 적으로 변환하지 않습니다 foo*, 그리고 union형의 말장난이 지원되지 않습니다 (UB있다).
Ruslan

3

변수는 C와 같은 언어에서 여러 가지 기본 속성을 갖습니다.

  1. 이름
  2. 유형
  3. 범위
  4. 평생
  5. 장소
  6. 가치

소스 코드 에서 위치 (5)는 개념적이며이 위치는 이름 (1)로 나타납니다. 따라서 변수 선언은 값 (6)에 대한 위치와 공간을 만드는 데 사용되며 다른 소스 줄에서는 변수의 이름을 지정하여 해당 위치와 값을 참조합니다.

프로그램이 컴파일러에 의해 머신 코드로 변환되면 위치 (5)는 일부 메모리 또는 CPU 레지스터 위치이며 변수를 참조하는 모든 소스 코드 표현식은 해당 메모리를 참조하는 머신 코드 시퀀스로 변환됩니다. 또는 CPU 레지스터 위치.

따라서 변환이 완료되고 프로그램이 프로세서에서 실행될 때 변수 이름은 기계 코드 내에서 효과적으로 잊혀지고 컴파일러가 생성 한 명령은 해당 변수가 아닌 변수의 할당 된 위치 만 참조합니다. 이름). 디버깅하고 디버깅을 요청하는 경우 이름과 관련된 변수의 위치가 프로그램의 메타 데이터에 추가되지만 프로세서는 메타 데이터가 아닌 위치를 사용하여 머신 코드 명령어를 계속 볼 수 있습니다. (일부 이름은 링크,로드 및 동적 조회를 위해 프로그램 메타 데이터에 있으므로 지나치게 단순화되어 있습니다. 여전히 프로세서는 프로그램에 지시 된 기계어 코드 명령 만 실행합니다.이 기계어 코드에는 이름이 있습니다. 위치로 변환되었습니다.)

유형, 범위 및 수명에 대해서도 마찬가지입니다. 컴파일러에서 생성 된 머신 코드 명령어는 값을 저장하는 위치의 머신 버전을 알고 있습니다. 유형과 같은 다른 속성은 변수의 위치에 액세스하는 특정 명령으로 변환 된 소스 코드로 컴파일됩니다. 예를 들어, 해당 변수가 부호있는 8 비트 바이트 대 부호없는 8 비트 바이트 인 경우 변수를 참조하는 소스 코드의 표현식은 부호있는 바이트로드 대 부호없는 바이트로드로 변환됩니다. (C) 언어의 규칙을 충족시키는 데 필요합니다. 따라서 변수 유형은 소스 코드를 기계 명령어로 변환하여 인코딩됩니다.이 명령은 CPU가 변수의 위치를 ​​사용할 때마다 메모리 또는 CPU 레지스터 위치를 해석하는 방법을 CPU에 명령합니다.

본질은 프로세서의 머신 코드 명령어 세트에서 명령어 (및 추가 명령어)를 통해 수행 할 작업을 CPU에 알려 주어야한다는 것입니다. 프로세서는 방금 수행했거나 들었던 내용에 대해 거의 기억하지 못합니다. 주어진 명령 만 실행하며 변수를 올바르게 조작하기위한 전체 명령 시퀀스 세트를 제공하는 것은 컴파일러 또는 어셈블리 언어 프로그래머의 작업입니다.

프로세서는 byte / word / int / long signed / unsigned, float, double 등과 같은 몇 가지 기본 데이터 유형을 직접 지원합니다. 예를 들어, 일반적으로 프로그램에서 논리 오류 일 수 있습니다. 변수와의 모든 상호 작용에서 프로세서에 지시하는 것은 프로그래밍 작업입니다.

이러한 기본 프리미티브 유형 외에도 데이터 구조로 사물을 인코딩하고 알고리즘을 사용하여 해당 프리미티브 측면에서 데이터 구조를 조작해야합니다.

C ++에서 다형성의 클래스 계층 구조에 포함 된 객체에는 일반적으로 객체의 시작 부분에 포인터가 있으며, 이는 가상 디스패치, 캐스팅 등에 도움이되는 클래스 별 데이터 구조를 나타냅니다.

요약하면, 프로세서는 저장 위치의 용도를 모르거나 기억하지 못합니다. CPU 레지스터 및 주 메모리에서 저장 장치를 조작하는 방법을 알려주는 프로그램의 기계 코드 명령어를 실행합니다. 프로그래밍은 스토리지를 의미있게 사용하고 프로그램 전체를 충실하게 실행하는 일관된 머신 코드 명령어 세트를 프로세서에 제공하는 소프트웨어 (및 프로그래머)의 임무입니다.


1
"번역이 완료 될 때 이름을 잊어 버렸습니다"... "연결은 이름 ("정의되지 않은 기호 xy ")을 통해 이루어지며 동적 연결을 통해 런타임에 발생할 수 있습니다. blog.fesnel.com/blog/2009/08/19/…를 참조하십시오 . 디버그 기호도 제거되지 않음 : 동적 연결을위한 함수 (전역 변수) 이름이 필요합니다. 따라서 내부 객체의 이름 만 잊을 수 있습니다. 그건 그렇고, 좋은 변수 속성 목록.
피터-복원 모니카

@ PeterA.Schneider는 링커와 로더가 소스 코드에서 온 (전역) 함수 및 변수의 이름을 사용하고 사용한다는 것을 큰 그림에서 절대적으로 맞습니다.
Erik Eidt

또 다른 문제는 일부 컴파일러는 표준에 따라 컴파일러 가 작성된 것과 같이 앨리어싱을 포함하지 않는 경우에도 다른 유형과 관련된 작업을 순서가 지정되지 않은 것으로 간주 할 수 있도록 컴파일러가 특정 일을 별칭으로 간주하지 않도록하는 규칙을 해석한다는 것입니다 . 같은 것을 감안할 때 useT1(&unionArray[i].member1); useT2(&unionArray[j].member2); useT1(&unionArray[i].member1);, 그 소리와 GCC는 포인터가 할 수 있다고 가정하는 경향이 unionArray[j].member2액세스 할 수없는 unionArray[i].member1모두가 같은에서 파생 된 경우에도 unionArray[].
supercat

컴파일러가 언어 사양을 올바르게 해석하는지 여부에 관계없이 프로그램을 수행하는 기계 코드 명령어 시퀀스를 생성해야합니다. 이는 소스 코드의 각 변수 액세스에 대한 (모듈로 최적화 및 기타 여러 요인) 프로세서에 저장 위치에 사용할 크기 및 데이터 해석을 알려주는 일부 기계 코드 명령어를 생성해야 함을 의미합니다. 프로세서는 변수에 대해 아무것도 기억하지 않으므로 변수에 액세스해야 할 때마다 변수를 정확히 수행하는 방법을 지시해야합니다.
Erik Eidt

2

특정 유형의 변수를 정의하면 변수 유형을 어떻게 추적합니까?

여기에는 두 가지 관련 단계가 있습니다.

  • 컴파일 시간

C 컴파일러는 C 코드를 기계 언어로 컴파일합니다. 컴파일러에는 소스 파일 (및 라이브러리 및 기타 작업을 수행하는 데 필요한 모든 정보)에서 얻을 수있는 모든 정보가 있습니다. C 컴파일러는 무엇을 의미하는지 추적합니다. C 컴파일러는 변수를로 선언하면 charchar 임을 알고 있습니다 .

변수 이름, 유형 및 기타 정보를 나열하는 소위 "기호 테이블"을 사용하여이를 수행합니다. 다소 복잡한 데이터 구조이지만 사람이 읽을 수있는 이름의 의미를 추적하는 것으로 생각할 수 있습니다. 컴파일러의 이진 출력에서 ​​이와 같은 변수 이름은 더 이상 나타나지 않습니다 (프로그래머가 요청할 수있는 선택적 디버그 정보를 무시하는 경우).

  • 실행 시간

컴파일러의 출력 (컴파일 된 실행 파일)은 기계 언어로, OS에서 RAM으로로드하고 CPU에서 직접 실행합니다. 기계 언어에서는 "type"이라는 개념이 전혀 없습니다. RAM의 특정 위치에서 작동하는 명령 만 있습니다. 명령은 실제로 그들이 작동 고정 된 유형이 있습니까 (즉, 기계어 명령이 "RAM 위치로 0x100와 0x521에 저장이 2 개의 16 비트 정수를 추가"있을 수 있습니다),하지만 정보가없는 어디 그 시스템은 해당 위치의 바이트는 실제로 정수를 나타냅니다. 유형 오류로부터 보호가 없다 전혀 여기가.


우연히 "바이트 코드 지향 언어"로 C # 또는 Java를 참조하는 경우 포인터가 절대로 생략되지 않습니다. C #과 Java에서는 포인터가 훨씬 일반적입니다 (따라서 Java에서 가장 일반적인 오류 중 하나는 "NullPointerException"입니다). 그것들이 "참조"라는 것은 용어의 문제 일뿐입니다.
피터-복원 모니카

@ PeterA.Schneider, 물론 NullPOINTERException이 있지만 언급 한 언어 (Java, ruby, 아마도 C #, 심지어 Perl과 같은)의 참조와 포인터 사이에는 명확한 구분이 있습니다. 타입 시스템, 가비지 콜렉션, 자동 메모리 관리 등; 일반적으로 메모리 위치를 명시 적으로 명시 할 수도 없습니다 ( char *ptr = 0x123C 와 같이 ). 이 문맥에서 "포인터"라는 단어의 사용법은 분명해야한다고 생각합니다. 그렇지 않다면, 나에게 미리 알려주십시오. 그리고 나는 대답에 문장을 추가 할 것입니다.
AnoE

C ++에서 포인터는 "타입 시스템과 함께 간다";-). (실제로 Java의 일반 제네릭은 C ++보다 강력하게 형식화되지 않습니다.) 가비지 콜렉션은 C ++에서 명령하지 않기로 결정한 기능이지만 구현에서 제공 할 수 있으며 포인터에 사용하는 단어와는 아무런 관련이 없습니다.
피터-복원 모니카

좋아, @ PeterA.Schneider, 나는 우리가 여기서 레벨을 얻고 있다고 생각하지 않습니다. 포인터를 언급 한 단락을 제거했지만 어쨌든 대답을 위해 아무것도하지 않았습니다.
AnoE

1

C ++가 런타임에 유형을 저장하는 몇 가지 중요한 특수 사례가 있습니다.

기존 솔루션은 차별화 된 통합입니다. 여러 유형의 객체 중 하나를 포함하는 데이터 구조와 현재 포함 된 유형을 나타내는 필드입니다. 템플릿 버전은 C ++ 표준 라이브러리에로 std::variant있습니다. 일반적으로 태그는 enum이지만 데이터에 대한 모든 스토리지 비트가 필요하지 않은 경우 비트 필드 일 수 있습니다.

이것의 다른 일반적인 경우는 동적 타이핑입니다. 함수 class가 있으면 virtual프로그램은 해당 함수에 대한 포인터를 가상 함수 테이블 에 저장합니다.이 함수class 는 생성 될 때 의 각 인스턴스에 대해 초기화됩니다 . 일반적으로 이는 모든 클래스 인스턴스에 대해 하나의 가상 함수 테이블을 의미하며 각 인스턴스는 해당 테이블에 대한 포인터를 보유합니다. (테이블이 단일 포인터보다 훨씬 크기 때문에 시간과 메모리를 절약 할 수 있습니다.) virtual포인터 나 참조를 통해 해당 함수를 호출 하면 프로그램이 가상 테이블에서 함수 포인터를 찾습니다. 컴파일 타임에 정확한 유형을 알고 있으면이 단계를 건너 뛸 수 있습니다. 이렇게하면 코드에서 기본 클래스 대신 파생 된 유형의 구현을 호출 할 수 있습니다.

여기 관련 만드는 것입니다 각각 ofstream받는 포인터가 포함되어 ofstream가상 테이블, 각 ifstream받는 ifstream가상 테이블을, 등등. 클래스 계층의 경우 가상 테이블 포인터는 프로그램에 클래스 객체의 유형을 알려주는 태그 역할을 할 수 있습니다!

언어 표준은 컴파일러를 설계하는 사람들에게 런타임에서 런타임을 구현하는 방법을 알려주지 않지만 이것이 예상 dynamic_cast하고 typeof작동 하는 방법 입니다.


"언어 표준은 코더들에게 말하지 않는다"당신은 아마도 문제의 "코더들"은 gcc, clang, msvc 등을 쓰는 사람들 이며 C ++을 컴파일하기 위해 그것들을 사용 하는 사람들 이 아니라는 것을 강조해야 할 것이다 .
Caleth

@Caleth 좋은 제안!
Davislor
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.