C Language
앨리어싱 및 효과적인 유형
수색…
비고
앨리어싱 규칙을 위반하거나 효과적인 유형의 객체를 위반하는 것은 서로 다른 두 가지이며 혼란스러워서는 안됩니다.
앨리어싱 은
a == b
동일한 객체를 참조a
두 포인터a
와b
의 특성입니다.데이터 오브젝트의 유효 유형 은 C가 해당 오브젝트에서 수행 할 수있는 조작을 판별하는 데 사용됩니다. 특히, 두 포인터가 서로에 대해 별명을 지정할 수 있는지 여부를 판별하는 데 유효한 유형이 사용됩니다.
하나의 포인터를 통해 객체를 변경하기 때문에 앨리어싱, 최적화 문제가 될 수 말하자면, 다른 포인터를 통해 볼 오브젝트를 변경할 수 a
b
. C 컴파일러가 포인터가 유형과 출처에 관계없이 항상 별칭을 지정할 수 있다고 가정하면 많은 최적화 기회가 손실되고 많은 프로그램이 느리게 실행됩니다.
C의 엄격한 앨리어싱 규칙은 컴파일러의 케이스가 어떤 오브젝트가 서로 별칭을 지정하는지 (또는 지정하지 않는지)를 가정 할 수 있음을 나타냅니다. 데이터 포인터에는 항상 염두에 두어야 할 엄지 손가락 규칙이 두 가지 있습니다.
달리 언급하지 않는 한, 동일한 기본 유형을 가진 두 개의 포인터가 별명을 지정할 수 있습니다.
두 가지 유형 중 적어도 하나가 문자 유형이 아닌 한, 다른 기본 유형을 가진 두 개의 포인터는 별명을 지정할 수 없습니다.
여기에 기본 형식은 우리가 같은 유형의 자격을 치워 버리고 있다는 것을 의미 const
하는 경우 예를 들어, a
인 double*
및 b
것입니다 const double*
, 컴파일러의 변경이 가정 일반적으로해야한다 *a
변경 될 수 있습니다 *b
.
두 번째 규칙을 위반하면 치명적인 결과가 발생할 수 있습니다. 여기에서 엄격한 앨리어싱 규칙을 위반한다는 것은 실제로 컴파일러에 다른 유형의 두 포인터 a
와 b
를 제시한다는 것을 의미합니다. 실제로는 동일한 객체를 가리 킵니다. 컴파일러는 항상 다른 개체에 그 두 점을 가정 할 수있다, 그리고 그 생각이 업데이트되지 않습니다 *b
당신을 통해 오브젝트를 변경 한 경우 *a
.
그렇게하면 프로그램의 동작이 정의되지 않게됩니다. 따라서 C는 실수로 이러한 상황이 발생하지 않도록 포인터 변환에 매우 심각한 제한을가합니다.
소스 또는 타겟 유형이
void
가void
, 기본 유형이 다른 포인터 간의 모든 포인터 변환은 명시 적 이어야합니다.
즉, 대상 유형에 const
와 같은 한정자를 추가하는 변환을 수행하지 않으면 캐스트 가 필요합니다.
일반적으로 포인터 변환과 캐스트 변환을 피하면 앨리어싱 문제를 방지 할 수 있습니다. 당신이 정말로 필요로하지 않는 한, 이러한 경우는 매우 특별합니다. 가능한 한 피하는 것이 좋습니다.
문자 유형은 비 문자 유형을 통해 액세스 할 수 없습니다.
객체가 정적, 스레드 또는 자동 저장 지속 기간으로 정의되고 문자 유형이 char
, unsigned char
또는 signed char
인 경우 비 문자 유형으로 액세스 할 수 없습니다. 아래의 예에서, char
배열 형식으로 재 해석 int
, 상기 동작은 모든 비 참조에 정의되지 int
포인터 b
.
int main( void )
{
char a[100];
int* b = ( int* )&a;
*b = 1;
static char c[100];
b = ( int* )&c;
*b = 2;
_Thread_local char d[100];
b = ( int* )&d;
*b = 3;
}
이것은 "유효 유형"규칙을 위반하여 정의되지 않았으므로 유효한 유형의 데이터 객체는 문자 유형이 아닌 다른 유형을 통해 액세스 할 수 없습니다. 다른 유형은 int
이므로 여기에는 허용되지 않습니다.
정렬과 포인터 크기가 잘 맞더라도이 규칙은 면제되지 않지만 동작은 여전히 정의되지 않습니다.
이것은 특히 표준 C에서 malloc
이나 유사한 함수에 의해 수신 된 버퍼를 사용하는 것처럼 다른 유형의 포인터를 통해 사용할 수있는 문자 유형의 버퍼 객체를 예약 할 방법이 없다는 것을 의미합니다.
위의 예와 같은 목표를 달성하는 올바른 방법은 union
를 사용하는 것입니다.
typedef union bufType bufType;
union bufType {
char c[sizeof(int[25])];
int i[25];
};
int main( void )
{
bufType a = { .c = { 0 } }; // reserve a buffer and initialize
int* b = a.i; // no cast necessary
*b = 1;
static bufType a = { .c = { 0 } };
int* b = a.i;
*b = 2;
_Thread_local bufType a = { .c = { 0 } };
int* b = a.i;
*b = 3;
}
여기서 union
은 처음부터 컴파일러가 다른 뷰를 통해 버퍼에 액세스 할 수 있음을 확인합니다. 이것은 또한 버퍼가 이미 int
타입이고 포인터 변환이 필요없는 "view" ai
를 가지므로 이점이 있습니다.
유효 유형
데이터 오브젝트의 유효 유형 은 연관되어있는 마지막 유형 정보입니다 (있는 경우).
// a normal variable, effective type uint32_t, and this type never changes
uint32_t a = 0.0;
// effective type of *pa is uint32_t, too, simply
// because *pa is the object a
uint32_t* pa = &a;
// the object pointed to by q has no effective type, yet
void* q = malloc(sizeof uint32_t);
// the object pointed to by q still has no effective type,
// because nobody has written to it
uint32_t* qb = q;
// *qb now has effective type uint32_t because a uint32_t value was written
*qb = 37;
// the object pointed to by r has no effective type, yet, although
// it is initialized
void* r = calloc(1, sizeof uint32_t);
// the object pointed to by r still has no effective type,
// because nobody has written to or read from it
uint32_t* rc = r;
// *rc now has effective type uint32_t because a value is read
// from it with that type. The read operation is valid because we used calloc.
// Now the object pointed to by r (which is the same as *rc) has
// gained an effective type, although we didn't change its value.
uint32_t c = *rc;
// the object pointed to by s has no effective type, yet.
void* s = malloc(sizeof uint32_t);
// the object pointed to by s now has effective type uint32_t
// because an uint32_t value is copied into it.
memcpy(s, r, sizeof uint32_t);
후자의 경우, 그 객체에 대한 uint32_t*
포인터가있을 필요는 없다는 사실을 관찰하십시오. 우리가 다른 uint32_t
객체를 복사했다는 사실만으로도 충분합니다.
엄격한 앨리어싱 규칙을 위반합니다.
다음 코드에서는 float
와 uint32_t
의 크기가 동일하다는 것을 단순하게 가정합니다.
void fun(uint32_t* u, float* f) {
float a = *f
*u = 22;
float b = *f;
print("%g should equal %g\n", a, b);
}
u
와 f
는 서로 다른 기본 유형을 가지므로 컴파일러는 서로 다른 객체를 가리킨다 고 가정 할 수 있습니다. *f
가 a
와 b
의 두 초기화 사이에서 바뀔 수있는 가능성은 없으므로 컴파일러는 코드를 다음과 같은 것으로 최적화 할 수 있습니다.
void fun(uint32_t* u, float* f) {
float a = *f
*u = 22;
print("%g should equal %g\n", a, a);
}
즉, *f
의 두 번째로드 연산은 완전히 최적화 될 수 있습니다.
이 함수를 "보통"
float fval = 4;
uint32_t uval = 77;
fun(&uval, &fval);
모두 잘되고 뭔가 좋아.
4는 4와 같아야합니다.
인쇄됩니다. 그러나 우리가 속여서 동일한 포인터를 전달하면 그것을 변환 한 후,
float fval = 4;
uint32_t* up = (uint32_t*)&fval;
fun(up, &fval);
엄격한 앨리어싱 규칙을 위반합니다. 그러면 동작이 정의되지 않게됩니다. 컴파일러가 두 번째 액세스 또는 완전히 다른 것을 최적화 한 경우 출력이 위와 같을 수 있으므로 프로그램이 완전히 신뢰할 수없는 상태가됩니다.
자격을 제한하다
동일한 유형의 포인터 인수가 2 개있는 경우 컴파일러는 어떤 가정도 할 수 없으며 항상 *e
변경 사항이 *f
변경 될 수 있다고 가정해야합니다.
void fun(float* e, float* f) {
float a = *f
*e = 22;
float b = *f;
print("is %g equal to %g?\n", a, b);
}
float fval = 4;
float eval = 77;
fun(&eval, &fval);
모두 잘되고 뭔가 좋아.
4가 4와 같은가?
인쇄됩니다. 동일한 포인터를 전달하면 프로그램은 여전히 올바른 작업을 수행하고 인쇄합니다.
4가 22와 같습니까?
e
와 f
가 같은 데이터 객체를 가리 키지 않는다는 것을 외부 정보를 통해 알면 비효율적 일 수 있습니다. 포인터 매개 변수에 restrict
한정자를 추가하여 해당 지식을 반영 할 수 있습니다.
void fan(float*restrict e, float*restrict f) {
float a = *f
*e = 22;
float b = *f;
print("is %g equal to %g?\n", a, b);
}
그렇다면 컴파일러는 항상 e
와 f
가 다른 객체를 가리킨다 고 가정합니다.
바이트 변경
객체가 유효한 유형을 가지면 다른 유형이 문자 유형, char
, signed char
또는 unsigned char
가 아닌 한 다른 유형의 포인터를 통해 수정하려고 시도하면 안됩니다.
#include <inttypes.h>
#include <stdio.h>
int main(void) {
uint32_t a = 57;
// conversion from incompatible types needs a cast !
unsigned char* ap = (unsigned char*)&a;
for (size_t i = 0; i < sizeof a; ++i) {
/* set each byte of a to 42 */
ap[i] = 42;
}
printf("a now has value %" PRIu32 "\n", a);
}
이것은 인쇄하는 유효한 프로그램입니다.
707406378 값이 있습니다.
이 이유는 다음과 같습니다.
-
unsigned char
유형으로 표시된 개별 바이트에 대한 액세스가 이루어 지므로 각 수정 사항이 잘 정의됩니다. -
a
및 through*ap
별칭을 통해 객체에 대한 두 개의보기가 있지만ap
는 문자 유형에 대한 포인터이기 때문에 엄격한 별칭 지정 규칙이 적용되지 않습니다. 따라서 컴파일러는a
의 값이for
루프에서 변경되었을 수 있다고 가정해야합니다. 의 수정 된 값a
변경된 바이트로 구성되어야한다. - 의 타입
a
,uint32_t
패딩 비트가 없다. 표현의 모든 비트 (여기서는707406378
)는 트랩 표현이 될 수 없습니다.