Joel이 Stack Overflow 팟캐스트 #34 에서 지적했듯이 C 프로그래밍 언어 (일명: K & R)에는 C: a[5] == 5[a]
Joel은 포인터 연산 때문이라고 하지만 여전히 이해가 되지 않습니다. 왜 a[5] == 5[a]
입니까?
질문자 :Dinah
Joel이 Stack Overflow 팟캐스트 #34 에서 지적했듯이 C 프로그래밍 언어 (일명: K & R)에는 C: a[5] == 5[a]
Joel은 포인터 연산 때문이라고 하지만 여전히 이해가 되지 않습니다. 왜 a[5] == 5[a]
입니까?
C 표준은 []
연산자를 다음과 같이 정의합니다.
a[b] == *(a + b)
따라서 a[5]
는 다음과 같이 평가됩니다.
*(a + 5)
5[a]
는 다음과 같이 평가됩니다.
*(5 + a)
a
는 배열의 첫 번째 요소에 대한 포인터입니다. a[5]
5 개 요소있어서의 값이다 와 동일 a
*(a + 5)
, 초등 수학에서 우리는 그 (또한이 동일 알고 가환 ).
배열 액세스는 포인터로 정의되기 때문입니다. a[i]
는 가환성인 *(a + i)
를 의미하도록 정의됩니다.
나는 다른 답변에서 뭔가를 놓치고 있다고 생각합니다.
예, p[i]
는 정의상 *(p+i)
와 동일하며 (덧셈은 가환성이기 때문에) *(i+p)
와 동일하며 (다시 말하지만 []
연산자의 정의에 따라) 동일합니다. i[p]
.
(그리고 array[i]
에서 배열 이름은 암시적으로 배열의 첫 번째 요소에 대한 포인터로 변환됩니다.)
그러나 이 경우 덧셈의 가환성은 그다지 분명하지 않습니다.
두 피연산자가 동일한 유형이거나 공통 유형으로 승격된 다른 숫자 유형인 경우 x + y == y + x
와 같이 commutativity가 완벽하게 이해됩니다.
그러나 이 경우 우리는 포인터 산술에 대해 구체적으로 이야기하고 있습니다. 여기서 한 피연산자는 포인터이고 다른 피연산자는 정수입니다. (정수 + 정수는 다른 연산이고 포인터 + 포인터는 넌센스입니다.)
+
연산자( N1570 6.5.6)에 대한 C 표준의 설명은 다음과 같습니다.
또한 두 피연산자 중 하나는 산술 유형을 갖거나 하나의 피연산자는 완전한 객체 유형에 대한 포인터이고 다른 하나는 정수 유형을 가져야 합니다.
다음과 같이 쉽게 말할 수 있습니다.
또한 두 피연산자 중 하나는 산술 유형을 갖거나 왼쪽 피연산자는 완전한 객체 유형에 대한 포인터이고 오른쪽 피연산자 는 정수 유형을 가져야 합니다.
이 경우 i + p
와 i[p]
는 모두 불법입니다.
C++ 용어로, 우리는 실제로 다음과 같이 느슨하게 설명할 수 있는 +
pointer operator+(pointer p, integer i);
그리고
pointer operator+(integer i, pointer p);
그 중 첫 번째만 정말 필요합니다.
왜 이런 식입니까?
C++는 이 정의를 C에서 상속받았고(배열 인덱싱의 교환성은 B에 대한 1972년 사용자 참조 에 명시적으로 언급되어 있음) BCPL (1967년 설명서)에서 얻었습니다. 이전 언어(CPL? Algol?).
따라서 배열 인덱싱이 덧셈의 관점에서 정의되고 포인터와 정수의 덧셈이 가환적이라는 아이디어는 수십 년 전 C의 조상 언어로 거슬러 올라갑니다.
이러한 언어는 현대 C보다 훨씬 덜 강력한 유형이었습니다. 특히 포인터와 정수의 구분은 종종 무시되었습니다. unsigned
키워드가 언어에 추가되기 전에 포인터를 부호 없는 정수로 사용하기도 했습니다.) 따라서 피연산자의 유형이 다르기 때문에 덧셈을 비가환적으로 만드는 아이디어는 해당 언어의 설계자에게 발생하지 않았을 것입니다. . 사용자가 두 개의 "사물"을 추가하려는 경우 해당 "사물"이 정수이든 포인터이든 아니면 다른 무엇이든 이를 방지하는 것은 언어에 달려 있지 않습니다.
그리고 수년에 걸쳐 해당 규칙을 변경하면 기존 코드가 손상될 수 있습니다(1989년 ANSI C 표준이 좋은 기회였을 수도 있음).
포인터를 왼쪽에, 정수를 오른쪽에 두도록 요구하도록 C 및/또는 C++를 변경하면 기존 코드가 일부 손상될 수 있지만 실제 표현력은 손실되지 않습니다.
이제 우리는 arr[3]
과 3[arr]
정확히 같은 것을 의미하지만, 후자의 형태는 IOCCC 외부에 나타나지 않아야 합니다.
그리고 물론
("ABCD"[2] == 2["ABCD"]) && (2["ABCD"] == 'C') && ("ABCD"[2] == 'C')
그 주된 이유는 70년대에 C가 설계되었을 때 컴퓨터에 메모리가 많지 않았기 때문에(64KB가 많았습니다) C 컴파일러가 구문 검사를 많이 하지 않았기 때문입니다. 따라서 " X[Y]
"는 오히려 맹목적으로 " *(X+Y)
"로 번역되었습니다.
이것은 또한 " +=
" 및 " ++
" 구문을 설명합니다. A = B + C
" 형식의 모든 것은 동일한 컴파일 형식을 가졌습니다. 그러나 B가 A와 동일한 개체이면 어셈블리 수준 최적화를 사용할 수 있습니다. 그러나 컴파일러는 그것을 인식할 만큼 밝지 않았기 때문에 개발자는 ( A += C
) 해야 했습니다. 마찬가지로 C
가 1
이면 다른 어셈블리 수준 최적화를 사용할 수 있으며 컴파일러가 이를 인식하지 못하기 때문에 개발자는 이를 명시적으로 만들어야 했습니다. (최근에는 컴파일러가 수행하므로 이러한 구문은 요즘 거의 필요하지 않습니다)
sizeof
문제에 대해 아무도 언급하지 않은 것 같습니다.
포인터에는 정수만 추가할 수 있으며 두 개의 포인터를 함께 추가할 수는 없습니다. 그렇게 하면 정수에 포인터를 추가하거나 포인터에 정수를 추가할 때 컴파일러는 항상 고려해야 할 크기가 있는 비트를 알고 있습니다.
질문에 문자 그대로 대답합니다. x == x
라는 것은 항상 사실이 아닙니다.
double zero = 0.0; double a[] = { 0,0,0,0,0, zero/zero}; // NaN cout << (a[5] == 5[a] ? "true" : "false") << endl;
인쇄물
false
이 추악한 구문이 "유용"하거나 최소한 동일한 배열의 위치를 참조하는 인덱스 배열을 처리하려는 경우 매우 재미있게 사용할 수 있다는 것을 알게 되었습니다. 중첩된 대괄호를 대체하고 코드를 더 읽기 쉽게 만들 수 있습니다!
int a[] = { 2 , 3 , 3 , 2 , 4 }; int s = sizeof a / sizeof *a; // s == 5 for(int i = 0 ; i < s ; ++i) { cout << a[a[a[i]]] << endl; // ... is equivalent to ... cout << i[a][a][a] << endl; // but I prefer this one, it's easier to increase the level of indirection (without loop) }
물론 실제 코드에는 사용 사례가 없다고 확신하지만 어쨌든 흥미롭습니다. :)
좋은 질문/답변.
C 포인터와 배열이 동일 하지 않다는 점을 지적하고 싶습니다. 이 경우 차이가 필수적인 것은 아닙니다.
다음 선언을 고려하십시오.
int a[10]; int* p = a;
a.out
에서 기호 a
는 배열의 시작인 주소에 있고 기호 p
는 포인터가 저장된 주소에 있으며 해당 메모리 위치의 포인터 값은 배열의 시작입니다.
C의 포인터에 대해, 우리는
a[5] == *(a + 5)
그리고 또한
5[a] == *(5 + a)
a[5] == 5[a].
것은 사실입니다.
답은 아니지만 생각할 거리를 줄 뿐입니다. 클래스에 오버로드된 인덱스/첨자 연산자가 있는 경우 표현식 0[x]
는 작동하지 않습니다.
class Sub { public: int operator [](size_t nIndex) { return 0; } }; int main() { Sub s; s[0]; 0[s]; // ERROR }
int 클래스에 대한 액세스 권한이 없으므로 다음을 수행할 수 없습니다.
class int { int operator[](const Sub&); };
Ted Jensen의 A TUTORIAL ON POINTERS AND ARRAYS IN C 에 아주 좋은 설명이 있습니다.
Ted Jensen은 다음과 같이 설명했습니다.
사실, 이것은 사실입니다. 즉,
a[i]
쓰는 곳마다 문제 없이*(a + i)
로 대체될 수 있습니다. 실제로 컴파일러는 두 경우 모두 동일한 코드를 생성합니다. 따라서 포인터 산술은 배열 인덱싱과 동일하다는 것을 알 수 있습니다. 두 구문 모두 동일한 결과를 생성합니다.이것은 포인터와 배열이 같은 것이 아니라 그렇지 않다는 것을 말하는 것이 아닙니다. 배열의 주어진 요소를 식별하기 위해 배열 인덱싱을 사용하는 구문과 동일한 결과를 산출하는 포인터 산술을 사용하는 다른 구문의 두 가지 구문을 선택할 수 있다는 것만 말하고 있습니다.
이제 이 마지막 표현식을 보면 그 일부입니다.
(a + i)
는 + 연산자를 사용하는 간단한 덧셈이고 이러한 표현식이 가환적이라는 C의 규칙입니다. 즉 (a + i) 는(i + a)
동일합니다.*(a + i)
처럼 쉽게*(i + a)
작성할 수 있습니다. 그러나*(i + a)
i[a]
에서 올 수 있습니다! 이 모든 것에서 다음과 같은 흥미로운 사실이 나옵니다.char a[20];
쓰기
a[3] = 'x';
쓰는 것과 같다
3[a] = 'x';
질문에 대한 답을 알고 있지만 이 설명을 공유하지 않을 수 없었습니다.
컴파일러 설계의 원리를 기억합니다. a
는 int
배열이고 int
크기는 2바이트이고 a
기본 주소는 1000이라고 가정하겠습니다.
a[5]
작동 방식 ->
Base Address of your Array a + (5*size of(data type for array a)) ie 1000 + (5*2) = 1010
그래서,
마찬가지로 c 코드를 3-주소 코드로 5[a]
는 ->가 됩니다.
Base Address of your Array a + (size of(data type for array a)*5) ie 1000 + (2*5) = 1010
따라서 기본적으로 두 명령문은 메모리의 동일한 위치를 가리키므로 a[5] = 5[a]
입니다.
이 설명은 배열의 음수 인덱스가 C에서 작동하는 이유이기도 합니다.
즉 a[-5]
Base Address of your Array a + (-5 * size of(data type for array a)) ie 1000 + (-5*2) = 990
그것은 위치 990에서 나에게 개체를 반환합니다.
C 컴파일러에서
a[i] i[a] *(a+i)
배열의 요소를 참조하는 다른 방법입니다! (전혀 이상하지 않음)
이제 약간의 역사. 다른 언어 중에서 BCPL은 C의 초기 개발에 상당히 큰 영향을 미쳤습니다. 다음과 같이 BCPL에서 배열을 선언한 경우:
let V = vec 10
실제로 10워드가 아닌 11워드의 메모리를 할당했습니다. 일반적으로 V가 첫 번째였으며 바로 다음 워드의 주소를 포함했습니다. 따라서 C와 달리 이름 V는 해당 위치로 이동하여 배열의 0번째 요소 주소를 선택합니다. 따라서 BCPL에서 배열 간접 지정은 다음과 같이 표현됩니다.
let J = V!5
어레이의 기본 주소를 얻기 위해 V를 가져와야 했기 때문에 J = !(V + 5)
(BCPL 구문 사용)를 수행해야 했습니다. 따라서 V!5
와 5!V
는 동의어였습니다. 일화적인 관찰로 WAFL(Warwick Functional Language)은 BCPL로 작성되었으며 내 기억으로는 데이터 저장소로 사용되는 노드에 액세스하기 위해 전자보다 후자 구문을 사용하는 경향이 있었습니다. 물론 이것은 35~40년 전의 것이므로 내 기억이 약간 녹슬었습니다. :)
스토리지의 추가 단어를 없애고 컴파일러가 배열 이름을 지정할 때 배열의 기본 주소를 삽입하도록 하는 혁신은 나중에 나왔습니다. C 역사 문서에 따르면 이것은 구조가 C에 추가되었을 때 발생했습니다.
참고 !
BCPL에서 두 경우 모두 간접 참조를 수행하는 단항 접두사 연산자와 이진 중위 연산자였습니다. 이진 형식에는 간접 참조를 수행하기 전에 두 피연산자의 추가가 포함되었습니다. BCPL(및 B)의 단어 지향 특성을 감안할 때 이것은 실제로 많은 의미가 있습니다. "포인터와 정수"의 제한은 데이터 유형을 얻을 때 C에서 필요 sizeof
가 되었습니다.
C 배열에서 arr[3]
과 3[arr]
은 동일하며 해당 포인터 표기법은 *(arr + 3)
~ *(3 + arr)
입니다. 그러나 반대로 [arr]3
또는 [3]arr
(arr + 3)*
및 (3 + arr)*
가 유효한 표현식이 아니기 때문에 구문 오류가 발생합니다. 그 이유는 역참조 연산자는 주소 뒤가 아니라 표현식에 의해 생성된 주소 앞에 위치해야 하기 때문입니다.
뭐, 언어 지원이 있어야만 가능한 기능입니다.
컴파일러는 a[i]
를 *(a+i)
로 해석하고 표현식 5[a]
*(5+a)
평가합니다. 덧셈은 가환성이므로 둘 다 같음이 밝혀졌습니다. 따라서 표현식은 true
평가됩니다.
C에서
int a[]={10,20,30,40,50}; int *p=a; printf("%d\n",*p++);//output will be 10 printf("%d\n",*a++);//will give an error
포인터 p
는 "변수"이고 배열 이름 a
는 "니모닉" 또는 "동의어"이므로 p++
는 유효하지만 a++
는 유효하지 않습니다.
a[2]
2[a]
와 같습니다 *(a+2)
*(2+a)
로 내부적으로 계산된 "포인터 산술"이기 때문입니다.
C 컴파일러는 항상 포인터 표기법으로 배열 표기법을 변환하기 때문입니다. a[5] = *(a + 5)
또한 5[a] = *(5 + a) = *(a + 5)
따라서 둘 다 같습니다.
출처 : http:www.stackoverflow.com/questions/381542/with-arrays-why-is-it-the-case-that-a5-5a
저장 프로시저의 결과를 임시 테이블에 삽입 (0) | 2022.01.08 |
---|---|
인스턴스의 클래스 이름을 가져오시겠습니까? (0) | 2022.01.06 |
getter 및 setter/접근자를 사용하는 이유는 무엇입니까? (0) | 2022.01.06 |
객체를 멤버로 사용하여 일반 JavaScript 객체를 반복하는 방법 (0) | 2022.01.06 |
무엇인가요 ":-!!" C 코드에서? (0) | 2022.01.06 |