C언어에서 (void *)0, (const void *)0, (void * const)0 세가지의 차이점

gurumong의 이미지

안녕하세요 ^^
공부중에 널포인터상수와 널포인터에 관해서 궁금증이 생겼는데요
(책을 보고 모르는게 생겼는데, 책의 내용을 이렇게 많이 인용해도 괜찬은지 모르겠네요, 혹시 아시는분 알려주시면 고맙겠습니다 ^^)

제가 가진 책에서 아래의 세가지 예시를 들어서 (void *)0, (const void *)0, (void * const)0 세가지의 차이점에 대해서 이렇게 설명하고 있습니다

int (*pf1)() = (void *)0;
int (*pf2)() = (void * const)0;
int (*pf3)() = (const void *)0; /*wrong*/

C언어 펀더멘탈 전웅 wrote:

"즉 처음 두 선언에서는 (void *)0과 (void * const)0 전체가 널 포인터 상수이므로 함수형 포인터로 적절히 변환되어 초기화에 사용된다. 하지만 세번째 선언에서는 0만 널 포인터 상수이고, 이에 적용된 캐스트 연산자에 의해 (const void *)은 const void *라는 데이터형을 갖는 널 포인터가 된다. 이때 const void *, 즉 pointer to const void라는 데이터형은 함수형으로 변환할수 없는 대상체형이기 때문에 이는 잘못된 초기화가 된다."

제가 이해하기로는 첫번째와 두번째는 널포인터 상수로써 함수포인터를 아무것도 가르키지 않는 널포인터로 만들어 버리지만 세번째는 (const void *)0; 자체가 const void*형의 데이터형을 갖는 널포인터가 되어 "대상체형 포인터와 함수형 포인터간의 변환은 허락되지 않는다"을 위반하기 때문에 잘못된것이라 이해했습니다

하지만 의문점이 const한정어를 쓴것이 어떻게 단순한 널포인터상수인것을 널포인터로 바꿔버리는 건지 이해하지를 못하고 있습니다
이에 대해서 간단하게라도 설명 부탁드립니다 ㅜ.ㅜ

아래는 이것을 설명하고 있는 페이지 전체입니다

C언어 펀더멘탈 전웅 wrote:

제목: 형한정어와 널 포인터 상수

포인터 문맥에서 (void *)0과 (const void *)0 그리고 (void * const)0의 차이를 알아보자.
생긴 모양을 봐서는 모두 널 포인터 상수를 의미하는 것 같지만, 약속에 의해 가장 첫번째와 마지막 형태만 널 포인터 상수가 될 수 있다. 형한정어라는 것은 값이 아닌 좌변값의 문맥에서만 그 의미가 유지된다. pointer to const void형으로 변환 하는 캐스트 연산자는 const가 포인터 자체가 아닌 가리켜지는 대상체를 한정하는데 사용되는 것이므로 값에 적용되더라도 형한정어가 유효하다. 하지만 constpointer to void형으로의 캐스트 연산자는 const가 포인터 자체에 적용되는 것이므로 값에 적용될 경우 형한정어는 사라진다. 이는 const int 형의 좌변값이 값으로 변할 때 const가 사라지고 값이 int 형을 가즌ㄴ 것과 동일한 원리이다. 따라서 (void *)0과 (void * const)0은 사실상 동일한 의미를 갖는다. 따라서 (void *)0이 널 포인터 상수로 취급되므로 (void * const)0 역시 널 포인터 상수로 취급된다.
하지만 (const void *)0은 이야기가 다르다. 앞에서 설명했듯이, 0 이라는 값을 갖는 정수 상수는 존재하지만 수식에서 (void *)라는 캐스트 연산자가 아닌 (const void *)라는 캐스트 연산자가 적용되고 있다. 따라서, 이번에는 정수 상수 0만 널 포인터 상수가 되며, 이 널 포인터 상수가 캐스트 연산자에 의해 const void *라는 포인터형으로 변환된다. 따라서 (const void *)0이라는 수식전체는 그냥 const void *형의 널 포인터가 된다. 이러한 차이를 실제 프로그램에서 살펴보자.

int (*pf1)() = (void *)0;
int (*pf2)() = (void * const)0;
int (*pf3)() = (const void *)0; /*wrong*/

즉 처음 두 선언에서는 (void *)0과 (void * const)0 전체가 널 포인터 상수이므로 함수형 포인터로 적절히 변환되어 초기화에 사용된다. 하지만 세번째 선언에서는 0만 널 포인터 상수이고, 이에 적용된 캐스트 연산자에 의해 (const void *)은 const void *라는 데이터형을 갖는 널 포인터가 된다. 이때 const void *, 즉 pointer to const void라는 데이터형은 함수형으로 변환할수 없는 대상체형이기 때문에 이는 잘못된 초기화가 된다.

winner의 이미지

woong.org 에 가보시죠.

const 한정어를 쓴다는 것 자체가 대상체를 pointer 가 가리킨다고 봐야 하는 것 같습니다.
Pointer 이름 앞의 const 는 pointer 가 가리키는 대상이 변경되지 않음을 말하지만
Data type 앞 뒤에 붙는 const 는 대상체가 변경되지 않음을 말하니까요.
즉 이 때의 void 는 generic 대상체의 의미를 갖게 됩니다.

하지만 처음 두개의 선언에서의 void 는 아직 대상체가 아닌 함수를 가리키는 형태가 될 수 있다.

뭐 이정도가 아닐까 싶네요.

gurumong의 이미지

woong.org가 닫히고 열리지 않은지 2주 조금 넘었나 덜됬나 그쯤 됬는데.
혼자 고심하며 기다리다 기다리다 이곳에 질문을 올렸거든요 ^^;

답변을 들어도 잘 이해하지 못하겠습니다
한정어 const가 캐스트 연산자에 사용되어도 그 의미는 변하지 않는다 라고 추측되는데요
const의 예외적인 쓰임에 대해서는 들어본적이 없습니다
const void와 void로 서로 다른 데이터형에서 유도되었다는 차이 뿐이고
그런 차이가 있다 하더라도 셋다 동일하게 일반포인터형으로의 변환인데
두개는 그냥 널포인터상수이고 다른 하나는 널포인터가 된다고 하는게이해되지 않습니다

doldori의 이미지

먼저 몇 가지 사항을 기억해야 합니다.

(1) 표준에 의하면 널 포인터 상수의 정의는 다음과 같습니다.

6.3.2.3/3
An integer constant expression with the value 0, or such an expression cast to type void *,
is called a null pointer constant. [...]

(2) 함수 포인터의 초기값으로 널 포인터 상수를 쓸 수 있습니다. 이때 널 포인터 상수는 해당 함수 포인터형으로 변환됩니다.

(3) 대상체 포인터형으로부터 함수 포인터형으로 변환할 수 없습니다.

이에 따르면 첫번째의 (void*)0은 널 포인터 상수이며 이 값으로 함수 포인터를 초기화할 수 있고,
두번째도 마찬가지 이유로 적법합니다.
그러나 세번째의 경우 (const void*)0은 널 포인터 상수가 const void*로 변환된 대상체 포인터형이므로
이를 함수 포인터형의 초기값으로 쓸 수 없습니다.

책의 인용 부분을 자세히 읽어보시면 같은 얘기의 반복이라는 것을 아실 것입니다.

gurumong의 이미지

아직도 잘 모르겠네요 ㅜ.ㅜ

(void *)가 아니기 때문에 널포인터 상수의 정의를 벗어나서 수식 전체가 널포인터가 되어버리는건가요?
그러니까 (const void *) 에서 쓰인 const는 (void *)와 다르게 쓰여서 C표준에서 정하는 널포인터상수 정의를 벗어나게 하는 것 이외에 별 다른 의미가 없는건가요?
다른 형지정자를 써서 (volatile void *)0 도 그럼 널포인터가 되는것인가요?

(const void *)0가 널포인터'상수'가 아닌 널포인터가 되니 아래와 같은 쓰임도 표준에서 허락되나요?
i = *(const int *)((const void *)0 + 0x1000)

그냥 캐스트 연산자는 다른 데이터형으로의 대입같은 목적을 위해서 값을 데이터형에 들어맞도록 겉에서 보이는 형을 단순히 바꾸는거라 생각했는데
이 이건 어떻게 이해 해야할지 모르겠네요 ㅜ.ㅜ

winner의 이미지

Null pointer 는 역참조해서는 안됩니다.
따라서 위와 같은 쓰임은 잘못된 방식이죠.
원래 대상체가 int 에 의해서 유도된 것이 아니라 pointer 이므로
const void * 에서 const int * 로의 변환역시
잘못된 것입니다. 하지만 거꾸로 말해서 원래 대상체가 int 라면 적법한 변환이겠죠.

Cast 의 제약은 논리를 통해서 설명됩니다.

대상체의 성질을 한정하는 한정어가 붙은 cast 연산자는 더이상 대상체가 아닌 함수 pointer 를 가리킬 수
없도록 하는 것이 합리적이다라는 것이죠.

Null pointer 상수로 함수 pointer 를 초기화하는데 한정어가 붙어있다면 그것이 함수의
어떤 성질을 말하는 것일까요?

이것을 그냥 무시하고 0 이나 (void *) 한정어처럼 처리하는 것이 올바를까요?

Programmer 가 pointer 가 가리키는 대상체의 성질을 한정하기 위하여 한정어를 썼다고 본다면
이것을 함수 pointer 에 적용하는 것은 programmer 가 실수를 했다고 보는 것이 올바른 판단일겁니다.

lovewar의 이미지

상수 혼자 사용될 수는 없는 것이고 상수가 대입되면 null pointer가 되니깐 연관해서 생각해야 할 것 같습니다.

그래서 위 내용 3개가 모두 같다(null pointer로 초기화 된다)고 여겨집니다(여겨진다는 말 자체가 예매하군요).

아래 내용은 6.3 Conversions의 6.3.2.3 Pointers에서 인용하였습니다.

Quote:

An integer constant expression with the value 0, or such an expression cast to type
void *, is called a null pointer constant. If a null pointer constant is converted to a
pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal
to a pointer to any object or function.

Conversion of a null pointer to another pointer type yields a null pointer of that type.
Any two null pointers shall compare equal.

전웅의 이미지

아, 제 홈페이지(www.woong.org)는 현재 서버의 물리적 고장으로 수리
중입니다 - 이 기회에 학교 서버에서 제 개인 서버로 옮겨타려고 계획하고
있습니다.

int (*pf1)() = (void *)0;
int (*pf2)() = (void * const)0;
int (*pf3)() = (const void *)0; /*wrong*/

를 이해하는데 가장 핵심이 되는 내용은

- null pointer constant 의 정의와 의미
- 캐스트 연산에서 type qualifier 의 의미
- (const T *) 와 (T * const) 의 차이

입니다.

위에 보인 예 자체는 실제 프로그래밍 과정에서 크게 중요하지 않지만,
위와 같은 기본적인 내용을 얼마나 올바르게 이해하고 있느냐를 판단할
수 있는 예이기에 책에서 다뤘던 내용입니다 - 개정판에서는 구체적 설명
은 웹 보충 자료로 빠질 계획입니다.

null pointer constant 의 정의는 "0 값을 갖는 정수 상수 혹은 void *
형으로 변환된 그와 같은 수식"입니다. 여기까지 이해하시기에는 큰 문제가
없으리라 생각합니다. 따라서

(int *)0
(int *)(void *)0

에서 첫 줄에서는 0, 그 아래에서는 (void *)0 부분이 null pointer
constant 이며, (int *) 로의 변환까지 포함하면 int * 형의 null pointer
되는 것입니다. null pointer constant 는 함수 포인터로의 변환에도 아무
문제가 없지만 일단 대상체 포인터로 변환된 null pointer 는 함수 포인터
로의 변환이 불가능하다는 내용 역시 이해하고 계신 것으로 판단합니다.

이제 캐스트 연산에서 type qualifier 에 대해서 살펴 보겠습니다. type
qualifier 는 lvalue 문맥에서만 의미를 갖습니다. 캐스트 연산은 lvalue
문맥이 아니기 때문에 (캐스트 연산 결과의 주소를 취하는 것은 불가능
하지요) const T 형으로의 변환은 그냥 T 형으로의 변환과 다르지
않습니다. 이 사실을 기억하고 (void * const)0 를 보겠습니다. (void *
const) 가 의미하는 type 은 const pointer to void 가 됩니다. 따라서
const pointer to void 로의 변환은 pointer to void 로의 변환가 차이가
없습니다. 결국,

(void *)0
(void * const)0

이 둘은 동일하다는 뜻이 됩니다. 따라서 (void * const)0 역시 null
pointer "constant" 이며, 함수 포인터로의 대입에 문제가 없습니다.

마지막으로 (const void *)0 에 대해서 생각해 보겠습니다. (const void *)
가 의미하는 데이터형은 pointer to const void 입니다. 즉, 이 경우 (위의
설명과 비교했을 때) T 자체가 pointer to const void 가 되기 때문에
(void * const)0 와는 다르며, 결국 (void *) 와도 다르기 때문에 null
pointer constant 의 "정의"에 의해 null pointer constant 가 될 수
없습니다. 즉,

(int *)0

에서와 마찬가지로 0 만이 null pointer constant 가 되며, 그러한 null
pointer constant 를 pointer to const void 형으로 변환해 얻은 그냥 null
pointer 가 되는 것입니다. 따라서 함수 포인터로의 대입이 불가능해지는
것입니다.

응용/적용을 했을 때에는 다소 비직관적으로 보이는 결과를 얻게 되지만,
정의/의미를 기초로 차근히 따라갔을 때에는 당연한 결과가 되는 셈입니다.

혹시나 아직도 가려운 부분이 남아 있다면 알려주시기 바랍니다.

그럼...

--
Jun, Woong (woong at icu.ac.kr)
Web: http://www.woong.org (서버 공사중)

--
Jun, Woong (woong at gmail.com)
http://www.woong.org

gurumong의 이미지

자세히 설명해주셔서 빠짐없이 모두 이해한거 같습니다 ^^
그런데 잘 알고 나니 조금 의문스러운것이 있습니다

(const void*)0;
(int *)0;
이와 같이 만들어진 널포인터는 어떤곳에 쓰일수 있나요?
실제 메모리상에 존재하는 포인터형 대상체도 아니거니와 널포인터라 역참조도 불가능한데요
조금 생각해보면 각각 const한정어가 붙은 대상체의 포인터와 int형의 대상체를 가르키는 포인터를 널포인터로 만들기 위해 쓰일수 있을꺼 같은데
하지만 그런 경우 모두다 void* 일반포인터나 0 널포인터상수를 대입연산자를 사용해서 가능하지 않습니까?

딱히 쓸모없는게 맞지요? (왠지 허무한...)

전웅의 이미지

> 자세히 설명해주셔서 빠짐없이 모두 이해한거 같습니다 ^^
> 그런데 잘 알고 나니 조금 의문스러운것이 있습니다
>
> (const void*)0;
> (int *)0;
>
> 이와 같이 만들어진 널포인터는 어떤곳에 쓰일수 있나요?
> 실제 메모리상에 존재하는 포인터형 대상체도 아니거니와 널포인터라 역참조도 불가능한데요
> 조금 생각해보면 각각 const한정어가 붙은 대상체의 포인터와 int형의 대상체를 가르키는 포인터를 널포인터로 만들기 위해 쓰일수 있을꺼 같은데
> 하지만 그런 경우 모두다 void* 일반포인터나 0 널포인터상수를 대입연산자를 사용해서 가능하지 않습니까?
>
> 딱히 쓸모없는게 맞지요? (왠지 허무한...)
>

좋은 질문입니다 - 상투적으로 하는 말 아닙니다. ^^

제 책을 보시면 0 이나 (void *)0 형태의 null pointer constant 만으로
적절한 type 의 null pointer 를 만들 수 없기에 반드시 주의해서 직접
캐스팅을 해줘야 하는 경우에 대한 설명을 했던 것으로 기억합니다.
(아닌가? 오래되어서 기억이 가물가물합니다. 없으면 말씀하세요 ^^)

--
Jun, Woong (woong at icu.ac.kr)
Web: http://www.woong.org (서버 공사중)

--
Jun, Woong (woong at gmail.com)
http://www.woong.org

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.