C에서 구조체 멤버 접근에 `.`와 `->`가 모두 존재하는 역사적 배경과 설계 이유를 다룬 Stack Overflow 질문과 답변입니다.
제품
Stack Data Licensing 신뢰할 수 있고 출처가 명시된 콘텐츠로 최고 수준의 기술 전문성에 접근하세요.
Releases Stack Overflow와 Stack Internal에 추가되는 기능을 최신 상태로 확인하세요.
로딩 중…
* [둘러보기 사이트의 빠른 개요를 여기서 시작하세요](https://stackoverflow.com/tour)
* [도움말 센터 궁금한 모든 질문에 대한 자세한 답변](https://stackoverflow.com/help)
* [Meta 이 사이트의 운영 방식과 정책을 논의합니다](https://meta.stackoverflow.com/)
* [회사 소개 Stack Overflow 회사와 제품에 대해 더 알아보기](https://stackoverflow.co/)
* [Stack Overflow](https://stackoverflow.com/) [도움말](https://stackoverflow.com/help)[채팅](https://chat.stackoverflow.com/?tab=explore)
* [Meta Stack Overflow](https://meta.stackoverflow.com/)
1. [홈](https://stackoverflow.com/)
좋아하는 기술을 위한 커뮤니티입니다. 모든 Collectives 살펴보기
Stack Internal Stack Overflow for Teams는 이제 Stack Internal이라는 이름을 사용합니다. 직장에서 인간의 사고와 AI 자동화의 장점을 함께 활용하세요.
무료로 사용해 보기자세히 알아보기 3. Stack Internal 4. 직장에서 인간의 사고와 AI 자동화의 장점을 함께 활용하세요. 자세히 알아보기
신뢰할 수 있는 콘텐츠를 한곳에서 찾고, 가장 많이 사용하는 기술을 중심으로 협업하세요.
Stack Internal
업무 속 지식
직장에서 인간의 사고와 AI 자동화의 장점을 함께 활용하세요.
13년 6개월 전에 질문함
수정됨3년 전
조회수 252k회
339
점(.) 연산자는 구조체의 멤버에 접근하는 데 사용되고, C의 화살표 연산자(->)는 해당 포인터가 가리키는 구조체의 멤버에 접근하는 데 사용됩니다.
포인터 자체에는 점 연산자로 접근할 수 있는 멤버가 없습니다(실제로는 가상 메모리의 위치를 설명하는 숫자일 뿐이므로 멤버가 없습니다). 따라서 점 연산자가 포인터에 사용될 때 자동으로 포인터를 역참조하도록 정의했더라도 모호성은 없었을 것입니다(이 정보는 제가 알기로 컴파일 시점에 컴파일러가 알고 있습니다).
그렇다면 언어 설계자들은 왜 겉보기에 불필요한 이 연산자를 추가해 일을 더 복잡하게 만들기로 했을까요? 어떤 큰 설계 결정이 있었던 건가요?
팔로우
123k 50 50 gold badges 254 254 silver badges 306 306 bronze badges
2012년 11월 13일 17:58에 질문함
6,382 5 5 gold badges 29 29 silver badges 51 51 bronze badges
9
int는 ‘integer’의 약자입니다. 그렇다면 키워드 double도 더 짧지 않은 이유는 무엇인가요?junwanghe –junwanghe 2012-12-11 06:54:23 +00:00 2012년 12월 11일 6:54에 댓글 작성. 연산자가 * 연산자보다 우선순위가 높을까요? 그렇지 않았다면 *ptr.member 와 var.member 가 가능했을 것입니다.milleniumbug –milleniumbug 2012-12-18 20:30:57 +00:00 2012년 12월 18일 20:30에 댓글 작성정렬 기준: 기본값으로 재설정
441
당신의 질문을 두 가지 질문으로 해석하겠습니다. 1) 왜 ->가 존재하는가, 2) 왜 .가 자동으로 포인터를 역참조하지 않는가. 두 질문 모두에 대한 답은 역사적 뿌리를 가지고 있습니다.
왜 ->가 존재하는가?
C 언어의 아주 초기 버전들 중 하나(여기서는 1975년 5월 Unix 6판과 함께 제공된 "C Reference Manual"의 약자인 CRM이라고 부르겠습니다)에서, -> 연산자는 매우 독점적인 의미를 갖고 있었고 *와 .의 조합과 동의어가 아니었습니다.
CRM이 설명하는 C 언어는 여러 면에서 현대의 C와 매우 달랐습니다. CRM에서는 struct 멤버가 바이트 오프셋 이라는 전역 개념을 구현했고, 이것은 타입 제한 없이 어떤 주소 값에도 더할 수 있었습니다. 즉, 모든 struct의 모든 멤버 이름은 독립적인 전역 의미를 가졌고(따라서 고유해야 했습니다). 예를 들어 다음과 같이 선언할 수 있었습니다.
struct S {
int a;
int b;
};
그리고 이름 a는 오프셋 0을, 이름 b는 오프셋 2를 의미했습니다(int 타입의 크기가 2이고 패딩이 없다고 가정). 언어는 번역 단위 내의 모든 struct의 모든 멤버가 서로 고유한 이름을 가지거나, 같은 오프셋 값을 의미해야 한다고 요구했습니다. 예를 들어 같은 번역 단위 안에서 추가로 다음과 같이 선언할 수 있었습니다.
struct X {
int a;
int x;
};
이 경우 이름 a가 일관되게 오프셋 0을 의미하므로 문제가 없었습니다. 하지만 다음 추가 선언은
struct Y {
int b;
int a;
};
형식적으로는 올바르지 않았습니다. 왜냐하면 a를 오프셋 2로, b를 오프셋 0으로 “재정의”하려고 했기 때문입니다.
그리고 여기서 -> 연산자가 등장합니다. 모든 struct 멤버 이름이 자체적으로 완결된 전역 의미를 가졌기 때문에, 언어는 다음과 같은 식을 지원했습니다.
int i = 5;
i->b = 42; /* 주소 7에 있는 `int`에 42를 기록 */
100->a = 0; /* 주소 100에 있는 `int`에 0을 기록 */
첫 번째 대입은 컴파일러에 의해 “주소 5를 취하고, 여기에 오프셋 2를 더한 다음, 결과 주소에 있는 int 값에 42를 대입하라”로 해석되었습니다. 즉 위 코드는 주소 7에 있는 int 값에 42를 대입하게 됩니다. 이때 ->의 사용은 왼쪽 피연산자의 타입을 전혀 신경 쓰지 않았다는 점에 주목하세요. 왼쪽 피연산자는 숫자형 주소를 나타내는 rvalue(포인터든 정수든)로 해석되었습니다.
이런 종류의 묘기는 *와 .의 조합으로는 불가능했습니다. 다음과 같이 쓸 수는 없었습니다.
(*i).b = 42;
왜냐하면 *i는 이미 잘못된 식이기 때문입니다. * 연산자는 .와 분리되어 있기 때문에 피연산자에 더 엄격한 타입 요구 사항을 부과합니다. 이러한 한계를 우회할 수 있는 능력을 제공하기 위해 CRM은 왼쪽 피연산자의 타입과 독립적인 -> 연산자를 도입했습니다.
Keith가 댓글에서 지적했듯, ->와 *+. 조합의 이러한 차이가 바로 CRM 7.1.8에서 “요구 사항의 완화”라고 말하는 부분입니다. Except for the relaxation of the requirement that E1 be of pointer type, the expression E1−>MOS is exactly equivalent to (*E1).MOS
나중에 K&R C에서는 원래 CRM에 설명되어 있던 많은 기능이 크게 재작업되었습니다. “struct 멤버를 전역 오프셋 식별자로 보는” 아이디어는 완전히 제거되었습니다. 그리고 -> 연산자의 기능은 *와 . 조합의 기능과 완전히 동일해졌습니다.
왜 .가 자동으로 포인터를 역참조할 수 없는가?
다시 말해, CRM 버전의 언어에서 . 연산자의 왼쪽 피연산자는 lvalue 여야 했습니다. 이것이 그 피연산자에 부과된 유일한 요구 사항이었고(그리고 이것이 위에서 설명했듯 ->와 달랐던 점입니다). CRM은 .의 왼쪽 피연산자가 struct 타입일 것을 요구하지 않았다는 점에 주목하세요. 단지 그것이 lvalue, 즉 어떤 lvalue든 요구했을 뿐입니다. 이는 CRM 버전의 C에서는 다음과 같은 코드를 쓸 수 있었다는 뜻입니다.
struct S { int a, b; };
struct T { float x, y, z; };
struct T c;
c.b = 55;
이 경우 컴파일러는 c라는 연속적인 메모리 블록의 바이트 오프셋 2 위치에 있는 int 값에 55를 기록했을 것입니다. 비록 struct T 타입에는 b라는 필드가 없더라도 말입니다. 컴파일러는 c의 실제 타입은 전혀 신경 쓰지 않았습니다. 중요한 것은 c가 lvalue, 즉 어떤 종류의 쓰기 가능한 메모리 블록이라는 점뿐이었습니다.
이제 다음과 같이 했다고 가정해 봅시다.
S *s;
...
s.b = 42;
이 코드는 유효한 것으로 간주되었을 것입니다(s도 lvalue이므로). 그리고 컴파일러는 단순히 바이트 오프셋 2 위치에 포인터 s 자체 안으로 데이터를 기록하려고 시도했을 것입니다. 말할 필요도 없이 이런 것은 쉽게 메모리 오버런으로 이어질 수 있었지만, 당시 언어는 그런 문제에 크게 관심을 두지 않았습니다.
즉, 그 시기의 언어 버전에서는 당신이 제안한 . 연산자를 포인터 타입에 대해 오버로드하자는 아이디어가 작동할 수 없었습니다. . 연산자는 이미 포인터에 대해(정확히는 lvalue 포인터나, 더 나아가 어떤 lvalue에 대해서든) 매우 구체적인 의미를 가지고 있었기 때문입니다. 의심할 여지 없이 매우 기이한 기능이었지만, 당시에는 실제로 존재했습니다.
물론 이 기이한 기능이 재작업된 C 버전, 즉 K&R C에서 당신이 제안한 것처럼 포인터용 오버로드된 . 연산자를 도입하지 말아야 할 아주 강력한 이유는 아닙니다. 하지만 그렇게 하지는 않았습니다. 어쩌면 그 당시 지원해야 할 CRM 버전 C로 작성된 레거시 코드가 있었을지도 모릅니다.
(1975년 C Reference Manual의 URL은 안정적이지 않을 수 있습니다. 또 다른 사본이, 아마도 약간의 차이가 있을 수도 있지만, 여기에 있습니다.)
팔로우
community wiki
댓글에서 설명을 요청하거나 추가 맥락을 더하려면 회원가입하세요.
댓글 추가
Random832
왜 *i를 주소 5에 있는 어떤 기본 타입(int?)의 lvalue로 두지 않았나요? 그러면 (*i).b도 같은 방식으로 동작했을 텐데요.
2012-11-13T21:41:07.143Z+00:00
1
답글
AnT stands with Russia
@Leo: 어떤 사람들은 C 언어를 더 고수준의 어셈블러로 여깁니다. C 역사에서 그 시기에는 실제로 C가 더 고수준의 어셈블러였습니다.
2012-11-15T19:26:32.783Z+00:00
6
답글
AnT stands with Russia
@bradley.ayers: 저는 구조체 시작점으로부터 데이터 필드의 바이트 오프셋을 말하고 있습니다. int의 크기가 2바이트라면, 연속된 int형 멤버들은 오프셋 0, 2, 4, 6 등을 갖습니다.
2012-11-24T23:00:59.48Z+00:00
2
답글
icktoofay
아하. 그러니까 이것이 많은 UNIX 구조체(예: struct stat)가 필드 이름에 접두사(예: st_mode)를 붙이는 이유를 설명해 주는군요.
2013-01-19T02:20:22.323Z+00:00
42
답글
AnT stands with Russia
@perfectionm1ng: bell-labs.com이 Alcatel-Lucent에 인수되면서 원래 페이지가 사라진 것 같습니다. 다른 사이트로 링크를 업데이트했지만, 그것도 얼마나 오래 유지될지는 모르겠습니다. 어쨌든 "ritchie c reference manual"로 검색하면 보통 그 문서를 찾을 수 있습니다.
2013-10-09T16:37:05.763Z+00:00
7
답글
댓글 추가|댓글 6개 더 보기
59
이미 언급된 역사적 이유 외에도, 연산자 우선순위와 관련된 작은 문제가 하나 더 있습니다. 점 연산자는 별표 연산자보다 우선순위가 높으므로, 구조체 안에 구조체 포인터가 있고 그 안에 또 구조체 포인터가 있는 식이라면... 다음 두 표현은 서로 동등합니다.
(*(*(*a).b).c).d
a->b->c->d
하지만 두 번째가 훨씬 읽기 쉽습니다. 화살표 연산자는 점과 마찬가지로 가장 높은 우선순위를 가지며 왼쪽에서 오른쪽으로 결합합니다. 구조체 포인터와 구조체 모두에 대해 점 연산자를 사용하는 것보다 이것이 더 명확하다고 생각합니다. 선언을 보지 않고도, 그것도 다른 파일에 있을 수 있는 선언을 보지 않고도, 식만 보고 타입을 알 수 있기 때문입니다.
팔로우
2012년 11월 13일 18:25에 답변함
2,901 3 3 gold badges 26 26 silver badges 44 44 bronze badges
댓글 추가
Askaga
구조체와 구조체 포인터가 섞인 중첩 데이터 타입에서는 각 하위 멤버 접근마다 올바른 연산자를 선택해야 하므로 오히려 더 어려워질 수 있습니다. a.b->c->d나 a->b.c->d 같은 식이 될 수도 있죠(저는 freetype 라이브러리를 사용할 때 이 문제를 겪었고, 계속 소스 코드를 확인해야 했습니다). 그리고 이것은 포인터를 다룰 때 컴파일러가 자동으로 역참조하게 하는 것이 왜 불가능한지를 설명해 주지 않습니다.
2012-11-13T18:38:28.32Z+00:00
3
답글
Askaga
당신이 말한 사실들은 맞지만, 제 원래 질문에는 전혀 답하지 않습니다. 당신은 a->와 *(a). 표기의 동등성을 설명했고(이것은 다른 질문들에서 이미 여러 번 설명되었습니다), 언어 설계가 어느 정도 임의적이라는 모호한 진술도 했습니다. 저는 당신의 답변이 그다지 도움이 되지 않았기 때문에 비추천을 눌렀습니다.
2012-11-28T20:13:51.78Z+00:00
4
답글
Shahbaz
@effeffe, 질문자는 언어가 a.b.c.d를 (*(*(*a).b).c).d로 쉽게 해석할 수도 있었으므로 -> 연산자가 쓸모없어질 수 있었다고 말하고 있습니다. 그러므로 질문자의 버전(a.b.c.d)도 (a->b->c->d와 비교해) 똑같이 읽기 쉽습니다. 그래서 당신의 답변은 질문자의 질문에 답하지 못합니다.
2013-06-04T09:11:03.26Z+00:00
23
답글
cmaster - reinstate monica
cmaster - reinstate monica1년 이상 전
@Shahbaz 자바 프로그래머에게는 그럴 수 있겠지만, C/C++ 프로그래머는 a.b.c.d와 a->b->c->d를 매우 다른 것으로 이해합니다. 전자는 중첩된 하위 객체에 대한 단일 메모리 접근입니다(이 경우 메모리 객체는 하나뿐입니다). 후자는 네 개의 아마도 서로 다른 객체를 거쳐 포인터를 추적하는 세 번의 메모리 접근입니다. 이는 메모리 배치에서 엄청난 차이이며, 저는 C가 이 두 경우를 아주 눈에 띄게 구별하는 것이 옳다고 생각합니다.
2017-10-17T08:45:04.247Z+00:00
7
답글
cmaster - reinstate monica
cmaster - reinstate monica1년 이상 전
@Shahbaz 그 말을 자바 프로그래머를 모욕하려고 한 것은 아닙니다. 그들은 단지 포인터가 완전히 암묵적인 언어에 익숙할 뿐입니다. 제가 자바 프로그래머로 성장했다면 아마 저도 같은 방식으로 생각했을 겁니다... 어쨌든 저는 실제로 C에서 보는 연산자 오버로딩이 최적이라고는 생각하지 않습니다. 하지만 우리는 거의 모든 것에 연산자를 자유롭게 오버로드하는 수학자들에게 익숙해졌다는 점도 인정합니다. 사용 가능한 기호 집합이 꽤 제한적이기 때문에 그들의 동기도 이해합니다. 결국에는 어디에서 선을 그을지의 문제일 뿐인 것 같습니다...
2017-10-17T15:33:43.13Z+00:00
2
답글
댓글 추가|댓글 9개 더 보기
23
C는 또한 어떤 것도 애매하게 만들지 않는 데 꽤 능숙합니다.
물론 점을 오버로드해서 두 가지 의미를 모두 갖게 만들 수도 있겠지만, 화살표는 프로그래머가 자신이 포인터를 다루고 있다는 사실을 확실히 알 수 있게 해 줍니다. 컴파일러가 서로 호환되지 않는 두 타입을 섞는 것을 허용하지 않는 것과 비슷합니다.
팔로우
2014년 12월 3일 3:58에 답변함
3,005 18 18 silver badges 21 21 bronze badges
댓글 추가
jforberg
이것이 단순하고 올바른 답입니다. 제 생각에 C가 연산자 오버로딩을 대부분 피하는 것은 C의 가장 좋은 점 중 하나입니다.
2015-09-14T11:51:55.36Z+00:00
4
답글
Petr Skocik
C에는 애매하고 흐릿한 것이 많습니다. 암시적 타입 변환이 있고, 수학 연산자는 오버로드되어 있으며, 연쇄 인덱싱은 다차원 배열을 인덱싱하는지 포인터 배열을 인덱싱하는지에 따라 완전히 다르게 동작하고, 무엇이든 무엇을 숨기는 매크로일 수 있습니다(대문자 명명 관례가 도움이 되긴 하지만 C 자체가 보장해 주지는 않습니다).
2018-06-13T11:28:54.267Z+00:00
24
답글
CivFan
그 논리라면 왜 화살표가 필요한가요? 프로그래머가 구조체 내용에 접근하려면 (*a).b를 써야 한다면, 그것만으로도 자신이 포인터를 다루고 있다는 점을 더 확실히 알 수 있게 되지 않나요?
2020-09-09T20:37:20.293Z+00:00
1
답글
Khoa Vo
@CivFan (*a).b는 같은 의미가 아닙니다. b의 값을 가져오고 싶을 때는 a를 역참조할 필요가 없거나 원하지 않기 때문입니다.
2021-07-12T13:40:56.427Z+00:00
0
답글
Ingmar
이것은 단지 포인터만의 문제가 아닙니다. 더 고수준 개념으로 생각하면, 이것은 객체의 멤버에 접근하는 것 입니다. 제 생각에는 이것은 “int를 나타내는 메모리 셀에 숫자를 쓰는 것”과는 다른 일입니다.
2024-02-01T09:51:30.157Z+00:00
0
답글
댓글 추가
2
C에서 별도의 -> 연산자가 있어야 할 기술적인 이유는 없습니다. 하지만 이것은 명확성을 더해 줍니다. ->를 보면 그것이 포인터라는 것을 알 수 있고, null일 가능성이 있으므로 역참조하기 전에 null 검사가 필요할 수 있다는 것도 알 수 있습니다.
C++에는 어느 정도 포인터인 척하는 클래스들이 있습니다(std::unique_ptr, std::shared_ptr, std::optional). 이들은 포인터처럼 *와 ->를 지원하지만, 동시에 .으로 접근하는 자체 멤버 함수도 가지고 있습니다. 이런 방식으로 표기를 분리하면 멤버 이름 충돌 가능성을 피할 수 있고, 명확성도 높아집니다.
팔로우
2023년 4월 25일 12:31에 답변함
105k 13 13 gold badges 191 191 silver badges 319 319 bronze badges
댓글 추가
HolyBlackCat
저는 닫힌 질문이 이 질문의 중복으로 처리되었기 때문에 제 답변을 여기에서 복사해 왔습니다. 이런 복사는 권장되는 것처럼 보입니다.
2023-04-25T12:32:08.553Z+00:00
0
답글
Sylvain Chiron
함수 포인터에 대해서도 같은 null 검사 논리는 관련이 있으며, 함수와 동일한 문법을 사용할 수 있습니다. 더 일반적으로, a.b는 어떤 계산도 필요하지 않지만, a->b는 *(typeof(a->b) *)((char *)a + offset(typeof(*a), b))입니다. 따라서 예를 들어 a->b->c를 자주 사용한다면, 이런 계산을 반복하지 않도록 지역 변수에 저장하는 것이 좋습니다.
2023-09-22T22:55:28.64Z+00:00
0
답글
보호된 질문. 이 질문에 답변하려면 이 사이트에서 최소 10의 평판이 필요합니다(association bonus는 제외). 이 평판 요구 사항은 스팸과 답변이 아닌 활동으로부터 이 질문을 보호하는 데 도움이 됩니다.
질문해서 답을 얻으세요
질문을 올려 궁금한 점의 답을 찾아보세요.
관련 질문 살펴보기
이 태그가 달린 비슷한 질문을 살펴보세요.
[(거의) 1년이 된 Challenges](https://meta.stackexchange.com/questions/418261/almost-one-year-of-challenges)
[정책: 생성형 AI(예: ChatGPT)는 금지됩니다](https://meta.stackoverflow.com/questions/421831/policy-generative-ai-e-g-chatgpt-is-banned)
2객체의 포인터에서 왜 '.' 대신 '->'를 사용해야 하나요?
0왜 C에는 구조체 멤버를 가리키기 위해 .와 ->가 둘 다 있나요?
-3왜 C++는 포인터에는 "->" 기호를, 나머지 모든 것에는 "." 기호를 쓰게 하나요?
0C++는 타입을 아는데, 점과 화살표를 추론할 수 없나요?
-1언제 화살표를 쓰고 언제 점을 써야 하는지 이해가 안 됩니다
1C에서 구조체와 함께 동작하도록 두 개의 다른 연산자(.와 ->)가 정의된 이유는 무엇인가요?
140화살표 연산자 -> 대신 무엇을 사용할 수 있나요?
0왜 어떤 C 프로그램은 구조체의 일부를 직접 참조하지 않고 화살표 연산자로 가리키나요?
3왜 단일 포인터가 배열처럼 동작하고 역참조 연산자(*) 없이도 가리키는 요소에 접근할 수 있나요?
2C에서 "->" 없이 속성에 접근하는 것이 어떻게 가능한가요?
0e가 포인터라면 왜 -> 대신 . 연산자가 사용되나요?
질문 피드 이 RSS 피드를 구독하려면 이 URL을 복사해 RSS 리더에 붙여 넣으세요.
lang-c
사이트 디자인 / 로고 © 2026 Stack Exchange Inc; 사용자 기여는 CC BY-SA 라이선스로 제공됩니다. rev 2026.5.18.43150
이 웹사이트를 계속 사용하면 Stack Exchange가 귀하의 기기에 쿠키를 저장하고 쿠키 정책에 따라 정보를 공개하는 데 동의하는 것입니다. 이 창을 닫으면 기본 쿠키가 허용됩니다. 쿠키를 거부하려면 아래 옵션 중 하나를 선택하세요.
필수 쿠키만 사용
설정 사용자 지정
![]()