특정 유형의 변수를 정의하면 (내가 아는 한 변수 내용에 대한 데이터를 할당하는 경우) 변수 유형을 어떻게 추적합니까?
특정 유형의 변수를 정의하면 (내가 아는 한 변수 내용에 대한 데이터를 할당하는 경우) 변수 유형을 어떻게 추적합니까?
답변:
변수 (또는 일반적으로 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
. 컴파일러는 컴파일 타임에 객체 유형을 알고 있거나 필요한 유형 정보를 객체 내부에 저장하여 런타임에 검색 할 수 있습니다.
void*
).
typeid(e)
정적 유형의 표현식을 검사합니다 e
. 정적 유형이 다형성 유형 인 경우 표현식이 평가되고 해당 객체의 동적 유형이 검색됩니다. 알 수없는 유형의 메모리에서 typeid를 가리키고 유용한 정보를 얻을 수 없습니다. 예를 들어 공용체의 typeid는 공용체의 개체가 아닌 공용체를 설명합니다. a의 typeid void*
는 단지 void 포인터입니다. 그리고 a void*
를 참조하여 내용을 얻을 수는 없습니다 . C ++에서는 명시 적으로 프로그래밍하지 않으면 권투가 없습니다.
다른 답변은 기술적 측면을 잘 설명하고 있지만 일반적인 "머신 코드에 대한 생각 방법"을 추가하고 싶습니다.
컴파일 후의 머신 코드는 꽤 멍청하며 실제로 모든 것이 의도 한대로 작동한다고 가정합니다. 간단한 기능이 있다고 가정 해보십시오.
bool isEven(int i) { return i % 2 == 0; }
그것은 int를 취하고 bool을 뱉어냅니다.
컴파일 후에는 자동 오렌지 과즙 짜는기구와 같은 것으로 생각할 수 있습니다.
오렌지를 먹고 주스를 반환합니다. 들어오는 물체의 유형을 인식합니까? 아니요, 그들은 단지 오렌지색이어야합니다. 오렌지 대신 사과를 먹으면 어떻게 되나요? 아마도 깨질 것입니다. 책임있는 소유자가 이런 식으로 사용하려고하지 않기 때문에 중요하지 않습니다.
위의 기능은 비슷합니다. int를 취하도록 설계되었으며 다른 것을 먹었을 때 부적절하거나 무언가를 할 수 있습니다. 컴파일러 (일반적으로)는 결코 발생하지 않는지 확인하기 때문에 (보통) 중요하지 않습니다. 실제로 올바른 형식의 코드에서는 발생하지 않습니다. 컴파일러가 함수가 잘못된 유형의 값을 얻을 가능성을 감지하면 코드 컴파일을 거부하고 대신 유형 오류를 반환합니다.
주의해야 할 점은 컴파일러가 전달할 잘못된 형식의 코드 사례가 있다는 것입니다. 예를 들면 다음과 같습니다.
void*
에 orange*
포인터의 다른 쪽 끝에있는 사과가있을 때,말했듯이 컴파일 된 코드는 과즙 짜는 기계와 같습니다. 처리 방법을 모르고 명령을 실행합니다. 그리고 지침이 틀리면 깨집니다. 그렇기 때문에 C ++에서 위의 문제로 인해 제어되지 않은 충돌이 발생합니다.
void*
는 foo*
일반적인 산술 판촉, union
유형 punning, NULL
vs.에 강요합니다 . nullptr
심지어 나쁜 포인터를 갖는 것 조차 UB 등입니다. 그대로입니다.
void*
암시 적으로 변환하지 않습니다 foo*
, 그리고 union
형의 말장난이 지원되지 않습니다 (UB있다).
변수는 C와 같은 언어에서 여러 가지 기본 속성을 갖습니다.
소스 코드 에서 위치 (5)는 개념적이며이 위치는 이름 (1)로 나타납니다. 따라서 변수 선언은 값 (6)에 대한 위치와 공간을 만드는 데 사용되며 다른 소스 줄에서는 변수의 이름을 지정하여 해당 위치와 값을 참조합니다.
프로그램이 컴파일러에 의해 머신 코드로 변환되면 위치 (5)는 일부 메모리 또는 CPU 레지스터 위치이며 변수를 참조하는 모든 소스 코드 표현식은 해당 메모리를 참조하는 머신 코드 시퀀스로 변환됩니다. 또는 CPU 레지스터 위치.
따라서 변환이 완료되고 프로그램이 프로세서에서 실행될 때 변수 이름은 기계 코드 내에서 효과적으로 잊혀지고 컴파일러가 생성 한 명령은 해당 변수가 아닌 변수의 할당 된 위치 만 참조합니다. 이름). 디버깅하고 디버깅을 요청하는 경우 이름과 관련된 변수의 위치가 프로그램의 메타 데이터에 추가되지만 프로세서는 메타 데이터가 아닌 위치를 사용하여 머신 코드 명령어를 계속 볼 수 있습니다. (일부 이름은 링크,로드 및 동적 조회를 위해 프로그램 메타 데이터에 있으므로 지나치게 단순화되어 있습니다. 여전히 프로세서는 프로그램에 지시 된 기계어 코드 명령 만 실행합니다.이 기계어 코드에는 이름이 있습니다. 위치로 변환되었습니다.)
유형, 범위 및 수명에 대해서도 마찬가지입니다. 컴파일러에서 생성 된 머신 코드 명령어는 값을 저장하는 위치의 머신 버전을 알고 있습니다. 유형과 같은 다른 속성은 변수의 위치에 액세스하는 특정 명령으로 변환 된 소스 코드로 컴파일됩니다. 예를 들어, 해당 변수가 부호있는 8 비트 바이트 대 부호없는 8 비트 바이트 인 경우 변수를 참조하는 소스 코드의 표현식은 부호있는 바이트로드 대 부호없는 바이트로드로 변환됩니다. (C) 언어의 규칙을 충족시키는 데 필요합니다. 따라서 변수 유형은 소스 코드를 기계 명령어로 변환하여 인코딩됩니다.이 명령은 CPU가 변수의 위치를 사용할 때마다 메모리 또는 CPU 레지스터 위치를 해석하는 방법을 CPU에 명령합니다.
본질은 프로세서의 머신 코드 명령어 세트에서 명령어 (및 추가 명령어)를 통해 수행 할 작업을 CPU에 알려 주어야한다는 것입니다. 프로세서는 방금 수행했거나 들었던 내용에 대해 거의 기억하지 못합니다. 주어진 명령 만 실행하며 변수를 올바르게 조작하기위한 전체 명령 시퀀스 세트를 제공하는 것은 컴파일러 또는 어셈블리 언어 프로그래머의 작업입니다.
프로세서는 byte / word / int / long signed / unsigned, float, double 등과 같은 몇 가지 기본 데이터 유형을 직접 지원합니다. 예를 들어, 일반적으로 프로그램에서 논리 오류 일 수 있습니다. 변수와의 모든 상호 작용에서 프로세서에 지시하는 것은 프로그래밍 작업입니다.
이러한 기본 프리미티브 유형 외에도 데이터 구조로 사물을 인코딩하고 알고리즘을 사용하여 해당 프리미티브 측면에서 데이터 구조를 조작해야합니다.
C ++에서 다형성의 클래스 계층 구조에 포함 된 객체에는 일반적으로 객체의 시작 부분에 포인터가 있으며, 이는 가상 디스패치, 캐스팅 등에 도움이되는 클래스 별 데이터 구조를 나타냅니다.
요약하면, 프로세서는 저장 위치의 용도를 모르거나 기억하지 못합니다. CPU 레지스터 및 주 메모리에서 저장 장치를 조작하는 방법을 알려주는 프로그램의 기계 코드 명령어를 실행합니다. 프로그래밍은 스토리지를 의미있게 사용하고 프로그램 전체를 충실하게 실행하는 일관된 머신 코드 명령어 세트를 프로세서에 제공하는 소프트웨어 (및 프로그래머)의 임무입니다.
useT1(&unionArray[i].member1); useT2(&unionArray[j].member2); useT1(&unionArray[i].member1);
, 그 소리와 GCC는 포인터가 할 수 있다고 가정하는 경향이 unionArray[j].member2
액세스 할 수없는 unionArray[i].member1
모두가 같은에서 파생 된 경우에도 unionArray[]
.
특정 유형의 변수를 정의하면 변수 유형을 어떻게 추적합니까?
여기에는 두 가지 관련 단계가 있습니다.
C 컴파일러는 C 코드를 기계 언어로 컴파일합니다. 컴파일러에는 소스 파일 (및 라이브러리 및 기타 작업을 수행하는 데 필요한 모든 정보)에서 얻을 수있는 모든 정보가 있습니다. C 컴파일러는 무엇을 의미하는지 추적합니다. C 컴파일러는 변수를로 선언하면 char
char 임을 알고 있습니다 .
변수 이름, 유형 및 기타 정보를 나열하는 소위 "기호 테이블"을 사용하여이를 수행합니다. 다소 복잡한 데이터 구조이지만 사람이 읽을 수있는 이름의 의미를 추적하는 것으로 생각할 수 있습니다. 컴파일러의 이진 출력에서 이와 같은 변수 이름은 더 이상 나타나지 않습니다 (프로그래머가 요청할 수있는 선택적 디버그 정보를 무시하는 경우).
컴파일러의 출력 (컴파일 된 실행 파일)은 기계 언어로, OS에서 RAM으로로드하고 CPU에서 직접 실행합니다. 기계 언어에서는 "type"이라는 개념이 전혀 없습니다. RAM의 특정 위치에서 작동하는 명령 만 있습니다. 명령은 실제로 그들이 작동 고정 된 유형이 있습니까 (즉, 기계어 명령이 "RAM 위치로 0x100와 0x521에 저장이 2 개의 16 비트 정수를 추가"있을 수 있습니다),하지만 정보가없는 어디 그 시스템은 해당 위치의 바이트는 실제로 정수를 나타냅니다. 유형 오류로부터 보호가 없다 전혀 여기가.
char *ptr = 0x123
C 와 같이 ). 이 문맥에서 "포인터"라는 단어의 사용법은 분명해야한다고 생각합니다. 그렇지 않다면, 나에게 미리 알려주십시오. 그리고 나는 대답에 문장을 추가 할 것입니다.
C ++가 런타임에 유형을 저장하는 몇 가지 중요한 특수 사례가 있습니다.
기존 솔루션은 차별화 된 통합입니다. 여러 유형의 객체 중 하나를 포함하는 데이터 구조와 현재 포함 된 유형을 나타내는 필드입니다. 템플릿 버전은 C ++ 표준 라이브러리에로 std::variant
있습니다. 일반적으로 태그는 enum
이지만 데이터에 대한 모든 스토리지 비트가 필요하지 않은 경우 비트 필드 일 수 있습니다.
이것의 다른 일반적인 경우는 동적 타이핑입니다. 함수 class
가 있으면 virtual
프로그램은 해당 함수에 대한 포인터를 가상 함수 테이블 에 저장합니다.이 함수class
는 생성 될 때 의 각 인스턴스에 대해 초기화됩니다 . 일반적으로 이는 모든 클래스 인스턴스에 대해 하나의 가상 함수 테이블을 의미하며 각 인스턴스는 해당 테이블에 대한 포인터를 보유합니다. (테이블이 단일 포인터보다 훨씬 크기 때문에 시간과 메모리를 절약 할 수 있습니다.) virtual
포인터 나 참조를 통해 해당 함수를 호출 하면 프로그램이 가상 테이블에서 함수 포인터를 찾습니다. 컴파일 타임에 정확한 유형을 알고 있으면이 단계를 건너 뛸 수 있습니다. 이렇게하면 코드에서 기본 클래스 대신 파생 된 유형의 구현을 호출 할 수 있습니다.
여기 관련 만드는 것입니다 각각 ofstream
받는 포인터가 포함되어 ofstream
가상 테이블, 각 ifstream
받는 ifstream
가상 테이블을, 등등. 클래스 계층의 경우 가상 테이블 포인터는 프로그램에 클래스 객체의 유형을 알려주는 태그 역할을 할 수 있습니다!
언어 표준은 컴파일러를 설계하는 사람들에게 런타임에서 런타임을 구현하는 방법을 알려주지 않지만 이것이 예상 dynamic_cast
하고 typeof
작동 하는 방법 입니다.