Monday, July 31, 2017

PHP Client Library

Download
One annoying omission from the 1.x framework for .NET was support for FTP. This could be rectified by various libraries (some free, others commercial) that filled this gap. However, with Visual Studio 2005 and 2.0 of the .NET framework, FTP makes a welcome appearance.
As well as adding FTP, Microsoft has moved support for web, mail and FTP requests out of System.Web and intoSystem.Net which is a more logical approach.

This paper discusses the migration of PHP (PHP:Hypertext Preprocessor 4) to ASP.NET. It compares and contrasts the basic underlying syntax of PHP with Microsoft® Visual Basic® .NET, as well as the underlying functionality and architecture of the two systems.

While both PHP and ASP.NET allow you to build sophisticated and complex Web applications (like e-commerce sites, intranets, and corporate portals), PHP and ASP.NET have several major differences. Unlike PHP, ASP.NET is not a language or a parser but rather a set of technologies in the Microsoft .NET Framework for building Web applications and XML Web Services. Microsoft ASP.NET pages execute on the server like PHP and generate markup, such as HTML, WML or XML, which is sent to a desktop or to mobile applications. ASP.NET, though, is different in that it provides a robust, object-oriented, event-driven programming model for developing Web pages, while still maintaining the simplicity that PHP developers are accustomed to.





Picasa Google Image

Download

Introduction

The sample provides you an easy to way to create an Image Gallery from your Picasa account. This is achieved by making call to Picasa API (Google Data API) from your ASP.NET web application and displaying images in a gallery using a JQuery Plugin.

Prerequisites

Description

The source code is attached in this sample. I have used Default.aspx file to show image gallry, but you can use any aspx page such as Gallery.aspx in your ASP.NET project.

As a first step you need to include the css and js folder from the Galleriffic plugin source code into your ASP.NET project. Then you need include the stylesheets and javascripts in your aspx page and then design your aspx page to display the images as in an image gallery. This is detailed here - Creating picasa image gallery in ASP.NET.  

Once you have done necessary steps in your aspx page next step is to add the proper configuration in your Web.config file to include Google user name, album and password.

There can be two scenarios you can use to display the images

  1. You can display any public album which can belong to you or any other user. For this you will need google account username (yours or the username of person whose album you are accessing) and the album id.
  2. Accessing your own private album. Here you will need to provide your username, password and album id. You will need to authenticate with API using your credentials and then access the album.

 

For any of the above cases, it’s a good idea to have username and album information in Web.config as below

 

XML
Edit|Remove
<appSettings    <add key="albumid" value="5792668263385651889"/>       <add key="user" value="username@gmail.com"/>       <add key="password" value=""/>   </appSettings> 

 

Here, user should be either your google user name or in case of a public album of some user, that user's user name.

To get album id for the album that contains the photos you want to display, you can simply brose to the album and from the url in the address bar, you can get the album id. For example in case of https://plus.google.com/u/0/photos?tab=mq#photos/114107981519387242086/albums/5792668263385651889, the album id is 5792668263385651889.

In case you are displaying your private album you will need to uncomment the following line in the code:

C#
Edit|Remove
//service.setUserCredentials(userName, password);  //-- needed when you need to show albums with private visibility
After you make the necessary changes in the config file as above you are good to go.

Source Code Files

Attached

More Information

For complete stpe by step instructions on how I built this project refer to http://techyfreak.blogspot.com.au/2012/10/an-aspnet-picasa-image-gallery.html.

The same approach can be extended to show images from Twitter, Flickr, Facebook,  Photobucket etc as well.

Picasa Google Image

Download

Introduction

The sample provides you an easy to way to create an Image Gallery from your Picasa account. This is achieved by making call to Picasa API (Google Data API) from your ASP.NET web application and displaying images in a gallery using a JQuery Plugin.

Prerequisites

Description

The source code is attached in this sample. I have used Default.aspx file to show image gallry, but you can use any aspx page such as Gallery.aspx in your ASP.NET project.

As a first step you need to include the css and js folder from the Galleriffic plugin source code into your ASP.NET project. Then you need include the stylesheets and javascripts in your aspx page and then design your aspx page to display the images as in an image gallery. This is detailed here - Creating picasa image gallery in ASP.NET.  

Once you have done necessary steps in your aspx page next step is to add the proper configuration in your Web.config file to include Google user name, album and password.

There can be two scenarios you can use to display the images

  1. You can display any public album which can belong to you or any other user. For this you will need google account username (yours or the username of person whose album you are accessing) and the album id.
  2. Accessing your own private album. Here you will need to provide your username, password and album id. You will need to authenticate with API using your credentials and then access the album.

 

For any of the above cases, it’s a good idea to have username and album information in Web.config as below

 

XML
Edit|Remove
<appSettings    <add key="albumid" value="5792668263385651889"/>       <add key="user" value="username@gmail.com"/>       <add key="password" value=""/>   </appSettings> 

 

Here, user should be either your google user name or in case of a public album of some user, that user's user name.

To get album id for the album that contains the photos you want to display, you can simply brose to the album and from the url in the address bar, you can get the album id. For example in case of https://plus.google.com/u/0/photos?tab=mq#photos/114107981519387242086/albums/5792668263385651889, the album id is 5792668263385651889.

In case you are displaying your private album you will need to uncomment the following line in the code:

C#
Edit|Remove
//service.setUserCredentials(userName, password);  //-- needed when you need to show albums with private visibility
After you make the necessary changes in the config file as above you are good to go.

Source Code Files

Attached

More Information

For complete stpe by step instructions on how I built this project refer to http://techyfreak.blogspot.com.au/2012/10/an-aspnet-picasa-image-gallery.html.

The same approach can be extended to show images from Twitter, Flickr, Facebook,  Photobucket etc as well.

Pixelpx to EM Converter

Download

Introduction

It helps the user to provide the whole CSS file with Base value of conversion from "px" to "em" and hence converts all the pixel values to EM values 

Building the Sample

This sample is a easy one which includes the use of Javascript and Regex

Description

This sample will help one in converting all the values in px to EM by just providing the base conversion value. One has to just copy and paste the whole CSS file and it will provide you the output CSS in EM format.

Javascript replace() method is used with Regex searches all the px values and replace it with the corresponding EM value. for a specified value.

Only constraint is that the class names which are present in CSS file should not be in format example: ".bodySize2px", ".labelSize2px", ".paddingvalue10px" etc.

The problem this sample will solve is that many of the times in web application we need to do that, so for achieving this very quickly just add the HTML page provided in this solution to your web application and do the conversion on the fly. Although there are other option available on the World wide web but this i think will help to keep the things local to your system and achieve it very quickly.

For chossing base values you can refer the table below:

 

Pixels

Ems

Percents

Points

6px

0.375em

37.5%

5pt

7px

0.438em

43.8%

5pt

8px

0.500em

50.0%

6pt

9px

0.563em

56.3%

7pt

10px

0.625em

62.5%

8pt

11px

0.688em

68.8%

8pt

12px

0.750em

75.0%

9pt

13px

0.813em

81.3%

10pt

14px

0.875em

87.5%

11pt

15px

0.938em

93.8%

11pt

16px

1.000em

100.0%

12pt

17px

1.063em

106.3%

13pt

18px

1.125em

112.5%

14pt

19px

1.188em

118.8%

14pt

20px

1.250em

125.0%

15pt

21px

1.313em

131.3%

16pt

22px

1.375em

137.5%

17pt

23px

1.438em

143.8%

17pt

24px

1.500em

150.0%

18pt

 

 

More Information

For more information on this topic, see http://codetrainer.blogspot.in/2013/08/pixelpx-to-em-converter-it-helps-user.html

pointer

Download

Introduction

What problem does the sample solve?

Building the Sample

Are there special requirements or instructions for building the sample?

Description

How does this sample solve the problem?

You can include code snippets, images, videos.   

 

C#
스크립트 편집|Remove
Click here to add your code dsnippet.

Source Code Files

  • source code file name #1 - summary for this source code file.
  • source code file name #2 - summary for this source code file.

More Information

For more information on X, see ...?

 

포인터를 이해하기 앞서 
 

  앞서 3 강에서 이야기 하였지만 모든 데이터들은 메모리 상에 특정한 공간에 저장 되어 있습니다. 우리는 앞으로 편의를 위해, 메모리의 특정한 공간을 '방' 이라고 하겠습니다. 즉, 각 방에는 데이터들이 들어가게 되는 것 입니다. 보통 사람들은 한 방의 크기를 1 바이트 라고 생각합니다. 우리가 만약 int 형 변수를 정의한다면 4 바이트 이므로 메모리 상의 4 칸을 차지하게 됩니다. 그런데 말이죠. 프로그램 작동 시 컴퓨터는 각 방에 있는 데이터를 필요로 하게 됩니다. 따라서, 서로 구분하기 위해 각 방에 고유의 '주소(address)' 를 붙여 주었습니다. 우리가 아파트에서 각 집들을 호수로 구분하는 것 처럼 말입니다. 예를 들어 우리가 아래와 같은 int 변수 a 를 정의하였다면 특정한 방에 아래 그림 처럼 변수 a 가 정의됩니다.

 

int a = 123; // 메모리 4 칸을 차지하게 한다.



 

메모리에 123 이란 수가 있고 이 수는 메모리에 0x152839 (앞에 0x 는 이 수가 16 진수로 표시되었다는 것을 의미해요) 에 위치



이 때, 0x152839 는 제가 아무렇게나 정한 이 방의 시작 주소 입니다. 참고로, 0x 가 뭐냐고 물어보는 사람들이 있을 텐데, 이전 강좌에서도 이야기 하였지만 16 진수라고 표시한 것 입니다. 즉, 16 진수로 152839 (10 진수로 1386553) 라는 위치에서 부터 4 바이트의 공간을 차지하며 123 이라는 값이 저장되어 있게 하라는 뜻이지요.

그렇다면 아래와 같은 문장은 어떻게 수행 될까요?

 

 

a = 10;


사실 컴파일러는 위 문장을 아래와 같이 바꿔주게 됩니다. 

메모리 0x152839 위치에서 부터 4 바이트의 공간에 있는 데이터를 10 으로 바꾸어라! 

결과적으로, 컴퓨터 내부에서는 올바르게 수행되겠지요. 
참고적으로 말하는 이야기 이지만 현재 (아마 이 블로그에 접속하는 사람들 중 99% 이상이) 많은 사람들은 32 비트 운영체제를 사용하고 있습니다. 이 32 비트에서 작동되는 컴퓨터들은 모두 주소값의 크기가 32 비트 (즉, 4 바이트.. 까먹었다면 2-3 강 참조) 로 나타내집니다. 즉 주소값이 0x00000000 ~ 0xFFFFFFFF 까지의 값을 가진다는 것이지요. 어랏! 조금 똑똑하신 분들이라면 32 비트로 사용할 수 있는 주소값의 가지수는 2 의 32 승 바이트, 즉 RAM 은 최대 4 GB 까지 밖에 사용할 수 없다는 사실을 알 수 있습니다. 맞습니다. 이 때문에 32 비트 운영체제에서는 RAM 의 최대 크기가 4 GB 로 제한되지요(즉, 돈을 마이 들여서 RAM 10GB 로 만들어도 컴퓨터는 4 GB 까지 밖에 인식하지 못합니다. 어찌 이렇게 슬플수가..) 1

여기까지는 상당히 직관적이고 단순해서 이해하기 쉬웠을 것 입니다. 그런데 C 를 만든 사람은 아주 유용하면서도 골때리는 것을 하나 새롭게 만들었습니다. 바로 '포인터(pointer)' 입니다. 영어를 잘하는 분들은 이미 아시겠지만 '포인터' 라는 단어의 뜻이 '가리키는 것(가르켜지는 대상체를 말하는 것이 아닙니다)' 이란 의미를 가지고 있습니다. 

사실, 포인터는 우리가 앞에서 보았던 int 나 char 변수들과 다른 것이 전혀 아닙니다. 포인터도 '변수' 입니다. int 형 변수가 정수 데이터, float 형 변수가 실수 데이터를 보관했던 것 처럼, 포인터도 특정한 데이터를 보관하는 '변수' 입니다. 그렇다면 포인터는 무엇을 보관하고 있을 까요? 

바로, 특정한 데이터가 저장된 주소값을 보관하는 변수 입니다. 여기서 강조할 부분은 '주소값' 이라는 것 이지요. 여기서 그냥 머리에 박아 넣어 버립시다. 이전에 다른 책들에서 배운 내용을 싹 다 잊어 버리고 그냥 망치로 때려 넣듯이 박아버려요. 포인터에는 특정한 데이터가 저장된 주소값을 보관하는 변수 라고 말이지요. 크게 외치세요. '주소값!!!!!' 

암튼, 뇌가 완전히 세뇌되었다고 생각하면 다음 단계로 넘어가도록 하겠습니다. 아직도 이상한 잡념이 머리에 남아 있다면 크게 숨을 호흡하시고 주소값이라고 10 번만 외쳐 보세요.. 

자. 되었습니다. 이제 포인터의 세계로 출발해 봅시다. 뿅

   포인터 
 

 다시 한 번 정리하자면

 
"포인터" : 메모리 상에 위치한 특정한 데이터의 (시작)주소값을 보관하는 변수!

우리가 변수를 정의할 때 int 나 char 처럼 여러가지 형(type) 들이 있었습니다. 그런데 놀랍게도 포인터에서도 형이 있습니다. 이 말은 포인터가 메모리 상의 int 형 데이타의 주소값을 저장하는 포인터와, char 형 데이터의 주소값을 저장하는 포인터가 서로 다르다는 말입니다. 응?? 여러분의 머리속에는 아래와 같은 생각이 번개 처럼 스쳐 지나갈 것입니다. 

"야 이 Psi 같은 놈아. 아까 포인터는 주소값을 저장하는 거래며. 근데 우리가 쓰는 컴퓨터에선 주소값이 무조건 32 비트, 즉 4 바이트래며!! 그러면 포인터의 크기는 다 똑같은것 아냐? 근데 왜 포인터가 형(type)을 가지는 건데!! 아아아아악" 

휴우우. 진정좀 하시고. 여러분 말이 백번 맞습니다 - 단, 현재 까지 배운 내용을 가지고 생각하자면 말이지요. 포인터를 아주 조금만 배우면 왜 포인터에 형(type) 이 필요한지 알게 될 것입니다. 

C 언어에서 포인터는 다음과 같이 정의할 수 있습니다. 

(포인터에 주소값이 저장되는 데이터의 형) *(포인터의 이름);

혹은 아래와 같이 정의할 수 도 있습니다.

(포인터에 주소값이 저장되는 데이터의 형)* (포인터의 이름);

  예를 들어 p 라는 포인터가 int 데이터를 가리키고 싶다고 하면 ...

int *p; // 라고 하거나
int* p; // 로 하면 된다

라 하면 올바르게 됩니다. 즉 위 포인터 p 는 int 형 데이터의 주소값을 저장하는 변수가 되는 것 입니다. 와우! 

   & 연산자
 

그런데 말입니다. 아직도 2% 부족합니다. 포인터를 정의하였으면 값을 집어 넣어야 하는데, 도대체 우리가 데이터의 주소값을 어떻게 아냐는 말입니까? 하지만, 여러분의 이러한 욕구를 충족시키는 연산자가 C 에 (당연히) 있습니다. 바로 & 연산자 이지요. 

그런데, 아마 복습을 철저하게 잘하신 분들은 당황할 수 도 있습니다. 왜냐하면 & 가 AND 연산자이기 때문입니다. (4 강 참조) 그런데, & 연산자는 두 개의 피연산자를 필요로 했습니다. 즉,

a & b; //o.k
a & // NOT ok

와 같이 언제나 2 개가 필요 하다는 것이지요. 그런데, 여기에서 소개할  & 연산자는 오직 피연산자가 1 개인 연산자 입니다. (이러한 연산자를 단항(unary) 연산자라 합니다) 즉, 위의 AND 연산자와 완전히 다르 다는 것이지요. 이는 데이터의 주소값을 불러 옵니다. 사용은 그냥 아래와 같은 꼴로 사용해 주면 됩니다.

& (주소값을 계산할 데이터)

백설(說)이 불여일행(行). 한 번 프로그램을 짜 봅시다. 

/* & 연산자 */
#include <stdio.h>
int main()
{
    int a;
    a = 2;

    printf("%x \n", &a); 
    return 0;
}

성공적으로 컴파일 했다면


와 같이 나옵니다. 참고로, 여러분의 컴퓨터에 따라 결과가 다르게 나올 수 도 있습니다. 사실, 저와 정말 인연 이상의 무언가가 있지 않는 이상 전혀 다르게 나올 것 입니다. 더 놀라운 것은 실행할 때 마다 결과가 달라질 것입니다. 

2 번째 실행한 것


위와 같이 나오는 이유는 나중에 설명하겠지만 주목할 것은 '값' 이 출력되었다는 것 입니다. 

    printf("%x \n", &a);

위 문장에서 &a 의 값을 16 진수 (%x) 로 출력하라고 명령하였습니다. 근데요. 눈치가 있는 사람이라면 금방 알겠지만 위에서 출력된 결과는 4 바이트(16 진수로 8 자리)가 아닙니다! (여러분의 컴퓨터는 다를 수 있습니다. 아무튼..) 하지만 저는 32 비트 운영체제를 사용하고 있습니다. 그렇다면 뭐가 문제인가요? 사실, 문제는 없습니다. 단순히 앞의 0 이 잘린 것 이지요. 주소값은 언제나 4 바이트 크기, 즉 16 진수로 8 자리 인데 앞에 0 이 잘려서 출력이 안된 것일 뿐입니다. 따라서 변수 a 의 주소는 아마도 0x001EF8D4 가 될 것입니다. 

 아무튼 위 결과를 보면, 적어도 제 컴퓨터 상에선 int 변수 a 는 메모리 상에서 0x001EF8D4 를 시작으로 4 바이트의 공간을 차지하고 있었다는 사실을 알 수 있습니다. 

 자, 이제 & 연산자를 사용하여 특정한 데이터의 메모리 상의 주소값을 알 수 있다는 사실을 알았으니 배고픈 포인터에게 값을 넣어 봅시다. 

/* 포인터의 시작 */
#include <stdio.h>
int main()
{
    int *p;
    int a;

    p = &a;

    printf("포인터 p 에 들어 있는 값 : %x \n", p);
    printf("int 변수 a 가 저장된 주소 : %x \n", &a);

    return 0;
}

실행해 보면 많은 이들이 예상했던 것 처럼.... 


똑같이 나옵니다. 어찌 보면 당연한 일입니다. 

    p = &a;

에서 포인터 p 에 a 의 주소를 대입하였기 때문이죠. 참고로, 한 번 정의된 변수의 주소값은 바뀌지 않습니다. 따라서 아래 printf 에서 포인터 p 에 저장된 값과 변수 a 의 주소값이 동일하게 나오게 됩니다. 어때요. 쉽죠? 

   * 표  
 

 
  이제, 드디어 포인터의 핵심에 다다랐습니다. 현재 까지 우리가 배운 바로는 "포인터는 특정한 데이터의 주소값을 보관한다. 이 때 포인터는 주소값을 보관하는 데이터의 형에 * 를 붙임으로써 정의되고, & 연산자로 특정한 데이터의 메모리 상의 주소값을 알아올 수 있다" 까지 알고 있습니다. 참고로 아래에서 설명하는 * 는 앞서 포인터를 정의할 때 사용하였던 * 와 다른 의미를 지닌 다는 사실을 알고 있으세요. 

  앞서 & 연산자가 2% 부족한 부분을 채워준다고 했지만 안타깝게도 1% 가 남았습니다. 하지만 * 연산자가 이 1% 를 채워질 것 입니다.... 잠깐만. * 연산자?? 어디서 많이 들어본 것 같네요.. 맞아요. * 연산자도 & 처럼 피연산자를 2 개 가질 때 에는 곱셈 연산자로 사용됩니다. 즉

a * b; // a 와 b 를 곱한다.
a *; // Not OK

하지만 이 연산자는 위 & 처럼 1 개의 피연산자를 가지는 단항 연산자 입니다. * 연산자는 쉽게 풀이하자면

 "나(포인터)를 나에게 저장된 주소값에 위치한 데이터라고 생각해!"

  라는 의미의 연산자 입니다. 한 번 아래 예제를 봅시다. 

/* * 연산자의 이용 */
#include <stdio.h>
int main()
{
    int *p;
    int a;

    p = &a;
    a = 2;

    printf("a 의 값 : %d \n", a);
    printf("*p 의 값 : %d \n", *p);

    return 0;
}

성공적으로 컴파일 한다면 


가 됩니다. 

    int *p;
    int a;

일단 int 데이터를 가리키는 포인터 p 와 int 변수 a 를 각각 정의하였습니다. 평범한 문장 이지요. 

    p = &a;
    a = 2;

그리고 포인터 p 에 a 의 주소를 집어 넣었습니다. 그리고 a 에 2 를 대입하였습니다. 

    printf("a 의 값 : %d \n", a);
    printf("*p 의 값 : %d \n", *p);

일단 위의 문장은 단순 합니다. a 의 값을 출력하란 말이지요. 당연하게도 2 가 출력됩니다. 그런데, 아래에서 *p 의 값을 출력하라고 했습니다. * 의 의미는 앞서, "나를 나에 저장된 주소값에 해당하는 데이터로 생각하시오!" 로 하게 하는 연산자라고 하였습니다. 즉, *p 를 통해 "p 에 저장된 주소(변수 a 의 주소)에 해당하는 데이타, 즉 변수 a" 를 의미할 수 있게 되었습니다. 다시 말해 *p 와 변수 a 는 정확히 동일합니다. 즉, 위 두 문장은 아래 두 문장과 10000% 일치합니다.

   printf("a 의 값 : %d \n", a);
    printf("*p 의 값 : %d \n", a);

마지막으로 * 와 관련된 예제 하나를 더 살펴 봅시다.

/* * 연산자 */
#include <stdio.h>
int main()
{
    int *p;
    int a;

    p = &a;
    *p = 3;

    printf("a 의 값 : %d \n", a);
    printf("*p 의 값 : %d \n", *p);

    return 0;
}

성공적으로 컴파일 하였다면


아마 많은 여러분들이 예상했던 결과 이길 바랍니다^^

    p = &a;
    *p = 3;

위에서도 마찬가지로 p 에 변수 a 의 주소를 집어 넣었습니다. 그리고 *p 를 통해 "나에 저장된 주소(변수 a 의 주소)에 해당하는 데이터(변수 a) 로 생각하시오" 를 의미하여 *p = 3 은 a = 3 과 동일한 의미를 지니게 되었습니다. 어때요. 간단하지요? 이로써 여러분은 포인터의 50% 이상을 이해하신 것 입니다~~! 짝짝짝짝

 자. 그럼 '포인터' 라는 말 자체의 의미를 생각해 봅시다. int 변수 a 와 포인터 p 의 메모리 상의 모습을 그리면 아래와 같습니다.

포인터 p 도, a 도 메모리 상에 각각 존재합니다만, 그 위치에는 a 의 경우 3 이 있지만 p 의 경우 a 의 주소값이 있습니다.

참고로 주소값은 제가 임의로 정한 것 입니다.


  즉, 포인터  p 는 * 를 통해 a 를 의미 할 수 (가리 킬 수) 있게 되었지요. 흔히 많은 책들은 포인터 p 가 변수 a 를 가리키고 있다 라고 말합니다. 사실 저는 이 말이 여러분에게 어렵게 다가올 까봐 여태까지 하고 있지 않았지만 아무튼, 포인터 p 에 어떤 변수 a 의 주소값이 저장되어 있다면 '포인터 p 는 변수 a 를 가리킨다' 라고 말합니다. 참고적으로 말하지만 포인터 또한 엄연한 변수 이기 때문에 특정한 메모리 공간을 차지합니다. 따라서 위 그림과 같이 포인터도 자기 자신만의 주소를 가지고 있지요. 

 이제 여러분들은 포인터에 왜 '형(type)' 이 필요한지 이야기 할 수 있는 단계가 되었습니다. (아마 여러분들 중 반 수 이상은 이미 짐작하고 계실 것 입니다.) 다시 말해 '형' 이 없다는 것은 포인터가 자신이 가리키고 있는 대상에 대해 어떠한 정보도 가지지 않아도 된다는 것 입니다. 여기서 포인터를 선언하기 위해 pointer 라는 저 만의 키워드를 이용했다고 합시다. (실제로 이런게 사용되는 것이 아닙니다;;;) 

int a;
pointer *p;
p = &a;
*p = 4;

자. 위 명령이 올바른 결과를 출력할까요? 포인터 p 에는 명백히 변수 a 의 '시작 주소' 가 들어 있습니다. '시작 주소' 란 말입니다. 즉, *p 라고 했을 때 포인터 p 에는 자신이 가리키는 대상의 시작 주소가 있지만 대상의 크기에 대한 정보가 없습니다. 헉! 컴퓨터는 *p 라고 했을 때 메모리에서 0x12345678 로 부터 (&a 가 0x12345678 이라 가정합시다) 몇 개의 바이트를 더 읽어 들어서
값을 변경해야 할 지 모른다는 말입니다. 결국 포인터는 쓸모 없게 됩니다. 

하지만, 우리가 여태까지 해왔던 것 처럼 

int a;
int *p;
p = &a;
*p = 4;

라고 한다면 어떨 까요? 컴퓨터는 0x12345678 로 부터 포인터 p 가 int * 라는 사실을 보고 "아하. 이 포인터는 int 데이터를 가리키는 구나!" 라고 알게 되어 정확히 4 바이트를 읽어 들어 값을 바꾸게 됩니다. 따라서 정확한 값이 출력될 수 있겠지요. 

여러분이 현재 까지 배운 내용에 대해 완벽하게 이해를 하고 있다면 아래와 같은 궁금증이 생길 것 입니다. 

"야. 그런데 말야. 주소값은 무조건 4 바이트 잖아? 그러면 int *p; 처럼 귀찮게 * 을 붙이지 말고 int p; 라고 한다음에 p = &a; 라고 해도 상관 없는거 아니야? 그지? 그지? 그지? " 

  훌륭한 생각입니다. 한 번 해봅시다. 

/* 될까? */
#include <stdio.h>
int main()
{
    int p;
    int a;

    p = &a;
    a = 3;

    printf("*p 의 값? : %d \n", *p);

    return 0;
}

안타깝게도 printf 부분에서 아래와 같은 오류가 출력됩니다.

error C2100: 간접 참조가 잘못되었습니다.

사실, 우리가 현재 까지 배운 내용을 바탕으로 이해해 보자면 위에서 왜 오류가 발생하는지 이해하기 힘듦니다. 그냥 단순히 * 연산자는 포인터들에게만 적용된다는 사실만을 알아 두시면 감사하겠습니다.

/* 포인터도 변수이다 */
#include <stdio.h>
int main()
{
    int a;
    int b;
    int *p;

    p = &a;
    *p = 2;
    p = &b;
    *p = 4;

    printf("a : %d \n", a);
    printf("b : %d \n", b);
    return 0;
}

성공적으로 컴파일 하였다면



    p = &a;
    *p = 2;
    p = &b;
    *p = 4;

  사실, 이런 예제까지 굳이 보여주어야 하나 하는 생각이 들었지만 그래도 혹시나 하는 마음에 했습니다. 앞에서도 말했듯이 포인터는 '변수' 입니다. 즉, 포인터에 들어간 주소값이 바뀔 수 있다는 것이지요. 위와 같이 처음에 a 를 가리켰다가, (즉 p 에 변수 a 의 주소값이 들어갔다가) 나중에 b 를 가리킬 수 (즉 p 에 변수 b 의 주소값이 들어감) 있다는 것 이지요. 뭐 특별히 중요한 예제는 아니였습니다만. 나중에 상수 포인터, 포인터 상수에 대해 이야기 하면서 다시 다루어 보도록 하겠습니다.

마지막으로, 강의를 마치며 여러분에게 포인터에 대해 완벽히 뇌리에 꽂힐 만한 동화를 들려드리겠습니다.

옛날 옛날에 .. 대략 2 년 전에 (뭐.. 전 여러분과 옛날의 정의가 다릅니다ㅋ) 변철수, 변수철, 포영희라는 세 명의 사람이 OO 아파트에 살고 있었습니다. 

int chul, sue; 
int *young;

그런데 말이죠. 포영희는 변철수를 너무나 좋아한 나머지 자기 집 대문 앞에 큰 글씨로 "우리집에 오는 것들은 모두 철수네 주세요" 라고 써 놓고 철수네 주소를 적어 놓았습니다

young = &chul;

어느날 택배 아저씨가 영희네 집에 물건을 배달하러 왔다가 영희의 메세지를 보고 철수네에 가져다 주게 됩니다.

*young = 3; // 사실 chul = 3 과 동일하다!

영희에 짝사랑이 계속 되다가 어느날 영희는 철수 보다 더 미남인 수철이를 보게 됩니다. 결국 영희는 마음이 변심하고 수철이를 좋아하기로 했죠. 영희는 자기 대문 앞에 있던 메세지를 떼 버리고 "우리집에 오는 것은 모두 수철이네 주세요." 라 쓰고 수철이네 주소를 적었습니다.

young = &sue;

며칠이 지나 택배 아저씨는 물건을 배달하러 영희네에 왔다가 메세지를 보고 이번엔 수철이네에 가져다 줍니다.

*young = 4; // 사실 sue = 4 와 동일하다

이렇게 순수한 사랑이 OO 아파트에서 모락 모락 피어났습니다..... 끝

return 0; // 종료를 나타내는 것인데, 아직 몰라도 되요. (정확히 말하면 리턴...) 

 

 

 안녕하세요 여러분! 지난 시간에 포인터의 기본 중의 기본이라 할 수 있는 것 들에 배워보았습니다. 다시 정리해 보자면 포인터는 특정한 데이터의 메모리 상의 (시작) 주소값을 보관하는 변수 입니다. 

  제가 C 언어를 배우면서 포인터를 배울 때 가장 많이 든 생각은 

"근데 말야. 이거  배워?" 

  이였습니다. 맞아요. 여러분들도 위와 같은 생각이 머리속에 끊임없이 맴돌 것 입니다. int a; 와 int *p; 가 있을 때 p 가 a 를 가리킨다고 하면 a = 3; 이라 하지 *p = 3; 과 같이 귀찮게 할 필요가 없잖아요. 하지만 나중에 가면 알겠지만 포인터는 C 언어에서 정말로 중요한 역할을 담당하게 될 것입니다. 포인터의 중요한 역할에 대해 지금 이야기 하는 것은 무리라고 생각합니다. 일단, 포인터가 뭔지만 알아 놓고 이걸 도대체 왜 배우는지에 대해선 나중에 이야기 하도록 합시다. 

   상수 포인터 
 
 

 
  이전에 11-1 강에서 상수에 대해 잠깐 언급한 것이 기억이 나시나요? 그 때 저는 어떠한 데이터를 상수로 만들기 위해 그 앞에 'const' 키워드를 붙여주면 된다고 했습니다. 예를 들어서 

const int a = 3; 

  과 같이 값이 3 인 int 변수 a 를 상수로 정의할 수 있습니다. const 는 단순히 말해서 '이 데이터의 내용은 절대로 바뀔 수 없다' 라는 의미의 키워드 입니다. 따라서, 위 문장의 의미는 '이 int 변수 a 의 값은 절대로 바뀌면 안된다!!!' 가 됩니다. 위와 같이 정의한 상수 a 를 아래 문장에

 
a = 4;

  와 같이 하려고 해도 컴파일 시에 오류가 발생하게 됩니다. 왜냐하면 a 는 상수로 선언이 되어 있으므로 값이 절대로 변경될 수 없기 때문이죠. 심지어 '값이 변경될 가능성이 있는 문장' 조차 허용되지 않습니다. 예를 들어

 
a = 3;

  이라고 한다면, a 의 값은 이미 3 이므로 a 의 값은 바뀌지 않습니다. 그런데 왠일? 컴파일 해보면 오류가 출력됩니다. 왜냐하면 위 문장은 a 의 값이 바뀔 '가능성' 이 있기 때문이죠. 즉, 컴파일러는 a 에 무슨 값이 들어가 있는지 신경 쓰지 않습니다. 그냥 무조건 가능성이 있다면 오류를 출력합니다. 

  여러분은 도대체 왜 상수를 사용하는지 의문을 가질 것 입니다. 하지만 상수는 프로그래밍 상에서 프로그래머들의 실수를 줄여주고, 실수를 했다고 해도 실수를 잡아내는데 중요한 역할을 하고 있습니다. 예를 들어 아래와 같은 문장을 봅시다.

 
const double PI = 3.141592;

  즉 double 형 변수 PI 를 3.141592 라는 값을 가지게 선언하였습니다. 왜 이렇게 해도 되냐면 실제로 PI 값은 절대로 바뀌지 않는 상수 이기 때문이죠. 따라서, 프로그래머가 밤에 졸면서 코딩을 하다가 아래와 같이 

 
PI = 10;

  PI 의 값을 문장을 집어 넣었다고 해도 컴파일 시 오류가 발생하여 프로그래머는 이를 고칠 수 있게 됩니다. 반면에 PI 를 그냥 double 형 변수로 선언했다고 해봅시다.

 
double PI = 3.141592;

  그렇다면 프로그래머가 아래와 같은 코드를 잠결에 집어 넣었다면 

PI = 10;

  컴파일러는 이를 오류로 처리하지 않습니다. 이는 엄청나게 큰일이 아닐 수 없죠. 만일 고객들에게 원의 넓이를 계산하는 프로그램을 만들어 주었는데 잘못해서 이상한 값이 나오면 어떻겠습니까? 물론 위와 같이 간단한 오류는 잡아내기 쉽지만 프로그램이 커지만 커질 수록 위와 같은 오류를 잡아내는 것은 여간 힘든 일이 아닙니다. 따라서, 우리는 '절대로 바뀌지 않을 것 같은 값에는 무조건 const 키워드를 붙여주는 습관' 을 기르는 것이 중요합니다.

  아무튼. 이번에는 포인터에도 const 를 붙일 수 있는지 생각해 봅시다. 

/* 상수 포인터? */
#include <stdio.h>
int main()
{
    int a;
    int b;
    const int* pa = &a;

    *pa = 3; // 올바르지 않은 문장
    pa = &b; // 올바른 문장
    return 0;
}

컴파일 해보면 오류가 발생합니다. 

 
error C2166: l-value가 const 개체를 지정합니다.

일단, 위 오류가 왜 발생하였는지에 대해 이야기 하기 앞서서 아래 문장이 무슨 의미를 가지는지 살펴 봅시다. 

    const int* pa = &a; //int* pa 와 같이 정의해도 int *pa 와 같다는 사실은 다 알고 있죠?

 여러분은 위 문장을 보면 다음과 같은 생각이 떠오를 것입니다. "저 포인터는 const int 형을 가리키는 포인터인데, 어떻게 int 형 변수 a 의 주소값이 대입 될 수 있지? 그러면 안되는 거 아니야?". 하지만, 제가 앞에서 누누히 강조해 왔듯이 'const' 라는 키워드는 '이 데이터의 값은 절대로 바뀌면 안된다' 라고 일러주는 키워드라고 하였습니다. 다시 말해, const int a 라는 변수는 그냥 int 형 변수 a 인데 값이 절대로 바뀌면 안되는 변수일 뿐입니다. 따라서, const int a 변수도 그냥 int 형이라 말할 수 있습니다. (다만 '변'수가 아닐 뿐) 

  따라서 const int* 의 의미는 const int 형 변수를 가리킨다는 것이 아닙니다. int 형 변수를 가리키는데 '그 값을 절대로 바꾸지 말라' 라는 의미이죠. 즉, pa 는 어떠한 int 형 변수를 가리키고 있습니다. 그런데 const 가 붙었으므로 pa 가 가리키는 변수의 값은 절대로 바뀌면 안되게 됩니다. 여기서 'pa 가' 라는 부분을 강조한 이유는 a 자체는 변수 이므로 값이 자유롭게 변경될 수 있기 때문입니다. 하지만 pa 를 통해서 a 를 간접적으로 가리킬 때 에는 컴퓨터가 '아, 내가 const 인 변수를 가리키고 있구나' 로 생각하기 때문에(const int* 로 포인터를 정의하였으므로) 값을 바꿀 수 없게 됩니다. 

  결과적으로 아래의 문장은 오류를 출력합니다. 

 
  *pa = 3; // 올바르지 않은 문장

  물론 a = 3; 과 같은 문장은 오류를 출려하지 않습니다. 앞에서도 말했듯이 변수 a 자체는 const 가 아니기 때문이죠. 

 
    pa = &b; // 올바른 문장

  그렇다면 위 문장은 옳은 문장 입니다. 왜 일까요? (아마 당연하다고 생각하면 여러분은 훌륭한 학생들 입니다) 이는 아래 예제와 함께 설명하도록 하겠습니다.
 
/* 상수 포인터? */
#include <stdio.h>
int main()
{
    int a;
    int b;
    int* const pa = &a;

    *pa = 3; // 올바른 문장
    pa = &b; // 올바르지 않은 문장

    return 0;
}   

역시 컴파일 해보면 

 error C2166: l-value가 const 개체를 지정합니다.

앞서 보았던 오류와 동일한 오류가 뜹니다. 그런데 위치가 다릅니다. 앞에서는 위 문장에서 오류가 발생했는데 이번엔 아래에서 발생합니다. 일단, 포인터의 정의 부분 부터 이야기 해봅시다.

   int* const pa = &a;

차근차근 봐 보면, 우리는 int* 를 가리키는 pa 라는 포인터를 정의하였습니다. 그런데 이번에는 const 키워드가 int* 앞에 있는 것이 아니라 int* 와 pa 사이에 놓이고 있습니다. 뭐지? 하지만 이 것은 const 키워드의 의미를 그대로 생각해 보면 간단합니다. pa 의 값이 바뀐 안된다는 것이지요. 그런데 제일 처음에 포인터를 배울 때 강조했듯이, 포인터에는 가리키는 데이터의 주소값, 즉 위 경우 a 의 주소값이 pa 저장되는 것이지요. 따라서, 이 pa 가 const 라는 의미는 pa 의 값이 절대로 바뀔 수 없다는 것인데, pa 는 포인터가 가리키는 변수의 주소값이 들어 있으므로 결과적으로 pa 가 처음에 가리키는 것 (a) 말고 다른 것은 절대로 건드릴 수 없다는 것 입니다.

    pa = &b; // 올바르지 않은 문장

결론적으로 위 문장은 오류를 뿜게 됩니다. 왜냐하면 pa 가 다른 변수를 가리키기 때문이죠 (즉 pa 에 저장된 주소값을 바꾸므로) 반면에 위의 예제에서 오류가 났던 문장은 올바르게 돌아갑니다.

    *pa = 3; // 올바른 문장

왜냐하면 pa 가 가리키는 값을 바꾸면 안된다는 말은 안했기 때문이죠. (그냥 int* )

한 번 위에 나와있던 것을 모두 합쳐 보면

/* 상수 포인터? */
#include <stdio.h>
int main()
{
    int a;
    int b;
    const int* const pa = &a;

    *pa = 3; // 올바르지 않은 문장
    pa = &b; // 올바르지 않은 문장

    return 0;
}   

와 같이 되겠지요. 어때요? 쉽죠? 

   포인터의 덧셈
 

 
  이번에는 포인터의 덧셈과 뺄셈에 대해서 다루어 보도록 하겠습니다. 앞에서도 강조하였지만 지금 하는 작업들이 무의미해 보이고 쓸모 없어 보이지만 나중에 정말로 중요하게 다루어 집니다. 조금만 힘내세요 (아마도 C 언어에서 가장 재미 없는 부분일듯.) 

/* 포인터의 덧셈 */
#include <stdio.h>
int main()
{
    int a;
    int* pa;
    pa = &a; 
    
    printf("pa 의 값 : %d \n", pa);
    printf("(pa + 1) 의 값 : %d \n", pa + 1);

    return 0;
}

성공적으로 컴파일 해보면

여러분의 출력 결과는 위에 나온 결과와 다를 수 있습니다. 다만, 두 수의 차이는 4 일 것입니다.

  아마 여러분은 출력된 결과를 보면서 깜짝 놀랐을 것입니다. 우리는 분명히 

    printf("(pa + 1) 의 값 : %d \n", pa + 1);

에서 pa + 1 의 값을 출력하라고 명시하였습니다. 제가 앞에서도 이야기 하였듯이 pa 에는 자신이 가리키는 변수의 주소값이 들어갑니다. 따라서, pa + 1 을 하면 1244812 에 1 이 더해진 1244813 가 아니라, 4 가 더해진 1244816 이 출력되었습니다. 이게 도대체 무슨 일입니까? 1244812 + 1 = 1244816 이라고요? 

  위 해괴한 계산 결과를 해결하기 앞서, 우리는 포인터의 형이 int* 라는 것을 알 수 있었습니다. 그런데, int 가 4 바이트 이니까.... 설마? 

  일단, 위 추측을 확인해보기 위해 int  포인터 말고도 크기가 다른 char 이다 double 등에 대해서도 해봅시다. 

/* 과연? */
#include <stdio.h>
int main()
{
    int a;
    char b;
    double c;
    int* pa = &a; 
    char* pb = &b;
    double* pc = &c;

    printf("pa 의 값 : %d \n", pa);
    printf("(pa + 1) 의 값 : %d \n", pa + 1);
    printf("pb 의 값 : %d \n", pb);
    printf("(pb + 1) 의 값 : %d \n", pb + 1);
    printf("pc 의 값 : %d \n", pc);
    printf("(pc + 1) 의 값 : %d \n", pc + 1);

    return 0;
}

성공적으로 컴파일 후 실행해 보면

여러분의 출력 결과는 위에 나온 결과와 다를 수 있습니다.


  우왕.  우리의 예상과 정확하게 맞아 떨어졌습니다. pb 의 경우 1 이 더해졌고, pc 의 경우 8 이 더해졌습니다. 그런데, char 은 1 바이트, double 은 8 바이트 이므로 모두 우리가 예상한 결과와 일치합니다. 놀랍군요. 하지만 머리 한 켠에는 또다른 의문이 남습니다. 왜 하라는 대로 안하고 포인터가 가리키는 형의 크기 만큼 더할까요. 사실 이에 대한 해답은 대략 2 분 31 초 뒤에 나옵니다. 

 훌륭한 학생이라면 여러가지 모험을 해볼 것 입니다. 예를 들어 '포인터의 뺄셈은 허용되는지, 포인터 끼리 더해도 되는지 등등..' 말이죠. 우리도 한 번 궁금증을 해결해 봅시다. 

  일단 직관적으로 포인터 뺄셈은 허용될 것 같습니다. 왜냐하면 뺄셈은 본질적으로 덧셈과 다를 바 없기 때문이죠. (1 - 1 = 1 + (-1) ) 아무튼 해 보면 덧셈과 유사한 결과가 나타납니다. 

/* 포인터 뺄셈 */
#include <stdio.h>
int main()
{
    int a;
    int* pa = &a; 

    printf("pa 의 값 : %d \n", pa);
    printf("(pa - 1) 의 값 : %d \n", pa - 1);

    return 0;
}

성공적으로 컴파일 후 실행 해보면

여러분의 출력 결과는 위에 나온 결과와 다를 수 있습니다.


  역시 우리의 예상대로 4 가 빼졌습니다. 

/* 포인터끼리의 덧셈 */
#include <stdio.h>
int main()
{
    int a;
    int* pa = &a; 
    int b;
    int *pb = &b;
    int *pc = pa + pb;

    return 0;
}

아마 컴파일 해보면 아래와 같은 오류를 만날 수 있습니다.

error C2110: '+' : 두 포인터를 더할 수 없습니다.

왜 C 에서는 두 포인터끼리의 덧셈을 허용하지 않는 것일까요? 사실, 포인터끼리의 덧셈은 아무런 의미가 없을 뿐더러 필요 하지도 않습니다. 두 변수의 메모리 주소를 더해서 나오는 값은 이전에 포인터들이 가리키던 두 개의 변수와 아무런 관련이 없는 메모리 속의 임의의 지점 입니다. 아무런 의미가 없는 프로그램 상에 상관없는 지점을 말이죠. 무언가, 설명이 불충분한 느낌이 들지만 아무튼 포인터 끼리의 덧셈은 아무런 의미가 없기 때문에 C 언어에선 수행할 수 없습니다. 그렇다면, 포인터에 정수를 더하는 것은 왜 되는 것일까요. 아까도 말했듯이 이에 대해선 아래에서 설명해드리겠습니다. (이제 1분 정도 남았군요.) 

  그런데 한 가지 놀라운 점은 포인터끼리의 뺄셈은 가능하다는 것입니다. 왜 그런지에 대한 설명은 나중에 합시다. 

/* 포인터의 대입 */
#include <stdio.h>
int main()
{
    int a;
    int* pa = &a; 
    int* pb;

    *pa = 3;
    pb = pa; 

    printf("pa 가 가리키고 있는 것 : %d \n", *pa);
    printf("pb 가 가리키고 있는 것 : %d \n", *pb);

    return 0;
}

성공적으로 컴파일 해보면 


와 같이 나옵니다. 뭐 당연한 일이지요. 

    pb = pa;

부분에서 pa 에 저장되어 있는 값 (즉, pa 가 가리키고 있는 변수의 주소값) 을 pb 에 대입하였습니다. 따라서 pb 도 pa 가 가리키던 것의 주소값을 가지게 되는 것이지요. 결과적으로 pb 와 pa 모두 a 를 가리키게 됩니다. 주의해야 될 점은 pa 와 pb 가 형이 같아야 한다는 점 입니다. 다시 말해 pa 가 int* 면 pb 도 int* 여야 합니다. 만일 형이 다르다면 형변환을 해주어야 하는데 이에 대한 이야기는 나중에 합시다. 

   배열과 포인터
 

  아마 이 단원을 읽다 보면 쇼크를 받을 지도 모르므로 심장이 약하신 분들은 의사와 함께 하십시오.(참고로 저의 경우 많이 놀라서 잠을 잘 못잤습니다) 

  제가 C 언어를 배우면서 가장 감탄하고도 쇼킹했던 부분이 바로 여기였습니다. 물론, 모든 사람들이 그다지 놀라워 하는 것은 아니지만 저한테는 신선한 충격이였습니다. 아마 이 단원을 배운다면 앞서 '포인터의 연산은 왜 이따구로 하는 거야!' 에 대한 답안을 찾을 수 있을 것 입니다. 

  이전 강좌에서 (11 강) 저는 여러분에게 배열에 대해 이야기 했었습니다. 그 때, 배열은 '변수가 여러개 모인 것으로 생각할 수 있다' 라고 이야기 했었지요. 그런데 말이죠. 또다른 놀라운 특징이 있습니다. 바로 배열들의 각 원소는 메모리 상에 연속되게 놓인 다는 점입니다. 뭐, 놀랍지 않다면 말고요. 어쨋든, 

int arr[10] = {1,2,3,4,5,6,7,8,9,10};

  이라는 배열을 정의한다면 메모리 상에서 다음과 같이 나타납니다. 

10 개의 방이 있고 각 방에 1 부터 10 까지 들어가 있습니다. 물론 각 방의 주소값은 다 있죠


  즉, 위와 같이 메모리 상에 연속된 형태로 나타난다는 점이지요. 한 개의 원소는 int 형 변수이기 때문에 4 바이트를 차지하게 됩니다. 물론, 위 사실을 믿지 못하시는 분들은 아래와 같이 컴퓨터를 통해 직접 확인해 볼 수 있습니다. 

/* 배열의 존재 상태? */
#include <stdio.h>
int main()
{
    int arr[10]={1,2,3,4,5,6,7,8,9,10};
    int i;

    for(i=0;i<10;i++)
    {
        printf("arr[%d] 의 주소값 : %x \n", i, &arr[i]);
    }
    return 0;
}

  성공적으로 컴파일 하면


 와 같이 나타납니다. 여러분의 결과와 주소값은 약간 다를 수 있지만, 어쨋든 4 씩 증가하면 된 것입니다. 

 아마 여기쯤 왔다면 여러분의 머리를 스쳐지나가는 생각이 들 것입니다! 아! 포인터로도 배열의 원소에 쉽게 접근이 가능하겠구나! (이 생각이 떠오르지 않는 사람은 아마 이 글을 다시 처음부터 읽으셔야 합니다.) 배열의 시작 부분을 가리키는 포인터를 정의한 뒤에 포인터에 1 을 더하면 그 다음 원소를 가리키겠군! 그리고 2 를 더한 그 다음 다음 원소를 가리킨다!!

 위와 같은 일이 가능한 이유는 포인터는 자신이 가리키는 데이타의 '형' 의 크기를 곱한 만큼 덧셈을 수행하기 때문이죠. 즉 p 라는 포인터가 int a; 를 가리킨다면 p + 1 을 할 때 p 의 주소값에 사실은 1*4 가 더해지고, p + 3 을 하면 p 의 주소값에 3*4 인 12 가 더해진다는 것입니다. 

 한 번 이 아이디어를 적용시켜서 배열의 원소를 가리키는 포인터를 만들어봅시다.

/* 과연? */
#include <stdio.h>
int main()
{
    int arr[10]={1,2,3,4,5,6,7,8,9,10};
    int* parr; 
    int i;
    parr = &arr[0];

    for(i=0;i<10;i++)
    {
        printf("arr[%d] 의 주소값 : %x ", i, &arr[i]);
        printf("(parr + %d) 의 값 : %x ", i, (parr + i));

        if(&arr[i] == (parr + i)) 
        {
        /* 만일 (parr + i) 가 성공적으로 arr[i] 를 가리킨다면 */
            printf(" --> 일치 \n");
        }
        else
        {
            printf("--> 불일치\n");
        }
    }
    return 0;
}

  성공적으로 컴파일 하였다면


 정확히 모두 일치가 나옵니다. 위 소스코드가 이해가 안되는 분들이 있을 까봐 살짝 설명을 드리기는 하겠습니다. 

      parr = &arr[0];

 parr 이라는 int 형을 가리키는 포인터는 arr[0] 이라는 int 형 변수를 가리킵니다. (배열의 각 원소는 하나의 변수로 생각할 수 있다는 사실은 까먹지 않았죠? )

        printf("arr[%d] 의 주소값 : %x ", i, &arr[i]);
        printf("(parr + %d) 의 값 : %x ", i, (parr + i));

  이제, arr[i] 의 주소값과 (parr + i) 의 값을 출력해봅니다. 만일 parr + i 의 값이 arr[i] 의 주소값과 같다면 하단의 if-else 에서 
'일치' 가 출력되고 다르다면 '불일치' 가 출력되게 됩니다. 그런데, 이미 예상하고 있던 바이지만 parr 이 int 형이므로 + i 를 하면 주소값에는 사실상 4*i 가 더해지게 되는 것이지요. 이 때 arr[i] 의 주소값도 i 가 하나씩 커질 때 마다 4 씩 증가하므로 (int 형 배열이므로) 결과적으로 모든 결과가 일치하게 되는 것 입니다. 

 이렇게 포인터에 정수를 더하는 것 만으로도 배열의 각 원소를 가리킬 수 있습니다. 그렇다면 * 를 이용하여 원소들과 똑같은 역할을 할 수 있게 되겠군요. 마치 아래 예제 처럼 말이지요. 

/* 우왕 */
#include <stdio.h>
int main()
{
    int arr[10]={1,2,3,4,5,6,7,8,9,10};
    int* parr; 
    
    parr = &arr[0];

    printf("arr[3] = %d , *(parr + 3) = %d \n", arr[3], *(parr + 3));
    return 0;
}

  성공적으로 컴파일 하였다면


  와 같이 동일하게 접근할 수 있게 됩니다. 

  즉 parr + 3 을 수행하면, arr[3] 의 주소값이 되고, 거기에 * 를 붙여주면 * 의 연산자의 역할이 '그 주소값에 해당하는 데이터를 의미해라!' 라는 뜻이므로 *(parr + 3) 은 arr[3] 과 동일하게 된다는 것입니다. 어때요? 놀랍지요. 포인터의 덧셈이 왜 그렇게 수행되는지 속 시원하게 해결되는 것 같나요?

   배열의 이름의 비밀
 

 
  아마 여러분들 중 대다수는 배열을 처음 배울 때 다음과 같은 실수를 하신 경험이 있을 것 입니다. (나만 그런가?)

#include <stdio.h>
int main()
{
    int arr[3]={1,2,3};

    printf("%d", arr);
}

  그러곤 1 도, 2 도, 3 도, 아닌 이상한 값이 나오는 것을 보고 당황하셨겠죠.. 그런데, 놀랍게도 그 때 출력되는 값은 아래와 같습니다. 

#include <stdio.h>
int main()
{
    int arr[3]={1,2,3};

    printf("arr 의 정체 : %x \n", arr);
    printf("arr[0] 의 주소값 : %x \n", &arr[0]);

    return 0;
    
}


성공적으로 컴파일 하면

출력 결과는 여러분과 다를 수 있으나 두 값이 같다는 점에 주목하세요


와 같이 나옵니다. 와우~ 놀랍게도 arr 과 arr[0] 의 주소값이 동일합니다. 

따라서 배열에서 배열의 이름은 배열의 시작 주소값을 지칭하고 있다는 사실을 알 수 있습니다. 다시 말해, 배열의 이름은 배열의 시작 주소를 가리키는 포인터라고 생각할 수 있겠군요. 그렇다면 다음과 같은 연산이 가능할까요? 

/* 될까? */
#include <stdio.h>
int main()
{

    int arr[2]={1,2};
    int brr[2]={3,4};

    arr = brr; // 안됨

    return 0;
    
}

  컴파일 해보면 아래와 같은 오류가 뜹니다. 

error C2106: '=' : 왼쪽 피연산자는 l-value이어야 합니다.

  arr 을 다른 배열을 가리키게 주소값을 바꿀 수 없다는 뜻이군요... 여러분들은 왜 그래야만 하는지 이해가 가나요? 사실 arr 은 포인터 상수 형태로 되어 있습니다. 이 경우에는 int* const 꼴의 포인터가 되지요. 사실 이러한 이유는 컴퓨터 메모리의 구조와 밀접한 관련이 있지만 여기서는 그 이야기는 하지 않도록 하겠습니다. 

   [] 연산자의 역할
 

 
  여러분들 중에서 많은 분들은 [] 가 연산자였다는 사실을 보고 깜짝 놀랐을 것 입니다. 그런데, 제가 사실 오래 전에 암묵적으로 알려 주었었죠.. 다음 그림은 4 강에서 연산 순위에 대해 이야기 하였을 때 가져왔던 그림입니다. 

* www.winapi.com 에서 가져온 자료 입니다.


  위의 표에서 아는 연산자가 별로 없다고 걱정하지는 마세요!우리는 앞으로 C 언어를 공부하면서 위의 거의 모든 연산자들을 다 배울 것 입니다. 표 최상단을 보면 [ ] 라 생긴 것이 보일 것 입니다. 아무튼.. [] 도 연산자였습니다. 우리는 막연히 배열을 처음에 배우면서 "아, 배열의 n 번째 원소를 지정하려면 [n-1] 이라고 해야 되는 구나!" 라고 생각하였는데 [ ] 도 + , - 와 같은 특정한 역할을 하는 연산자였던 것입니다. 

 그런데, 우리는 앞서 포인터와 배열의 놀라운 관계를 배웠기 때문에 [ ] 연산자의 역할을 대충 짐작할 수 있습니다. 

/* [] 연산자 */
#include <stdio.h>
int main()
{
    int arr[5]={1,2,3,4,5};

    printf("a[3] : %d \n", arr[3]);
    printf("*(a+3) : %d \n", *(arr+3));
    return 0;
}

 성공적으로 컴파일 했다면 


  음... 이미 앞에서 다룬 내용을 모두 이해했더라면 위 정도쯤은 쉽게 이해할 수 있을 것입니다. 사실 컴퓨터는 C 에서 [] 라는 연산자가 쓰이면 자동적으로 위 처럼 포인터 형태로 바꾸어서 처리하게 됩니다. 즉, 우리가 arr[3] 이라 사용한 것은 사실 *(arr + 3) 으로 바뀌어서 처리가 된다는 뜻이지요. 

  따라서 다음과 같이 신기한 연산도 가능합니다. 

/* 신기한 [] 사용 */
#include <stdio.h>
int main()
{
    int arr[5]={1,2,3,4,5};

    printf("3[arr] : %d \n", 3[arr]);
    printf("*(3+a) : %d \n", *(arr+3));
    return 0;
}

  성공적으로 컴파일 하면


  3[arr] 은 무언가 조금 '깨는' 표현 입니다. 사실 이렇게 사용한다면 가독성도 떨어지고 한 번에 이해도 되지 않기에 99.9999% 의 프로그래머들은 arr[3] 으로 사용할 것입니다. 하지만, 앞에서도 [] 는 연산자로 3[arr] 을 *(3+arr) 로 바꿔주기 때문에 arr[3] 과 동일한 결과를 출력할 수 있게 되지요. 

   포인터의 정의
 
 

 
  앞에서 말하기를 int 를 가리키는 포인터를 정의하기 위해 다음의 두 문장을 모두 사용할 수 있다고 했습니다.

    int* p;
    int *p;

  그런데 말이죠. 제 강좌 말도 다른 곳에서 C 언어를 공부했던 사람들이라면 아래와 같은 형식을 훨씬 많이 쓴다는 사실을 알 수 있었을 것입니다.

    int *p;

  왜 일까요? 우리가 int 형 변수를 여러개 한 번에 선언하려 했을 때 int a,b,c,d; 라 하잖아요. 포인터 변수를 여러개 선언 하려면 아래와 같이 해야 합니다. 

   int *p, *q, *r;

  물론

    int* p, *q, *r;

  게 해도 됩니다. 다만, 

    int* p;

   꼴로 한다면 다음과 같이 실수 할 확률이 매우 커지게 됩니다. 왜냐하면 아래와 같이 한다면

    int* p,q,r;

 p 만 int 를 가리키는 포인터 이고, q, r 은 평범한 int 형 변수가 됩니다. 따라서, 앞으로 저는 제 강좌에서 모든 포인터들은 

    int *p;

 
  꼴로 선언 하도록 하겠습니다. 

 

안녕하세요 여러분~! 이전 강좌는 잘 보시고 계시는지요? 아마도 이번 강좌가 최대의 난관일 듯 하네요. 이번 강좌를 잘 이해하냐, 이해 못하냐에 따라서 C 언어가 쉽다/어렵다가 완전히 좌우됩니다. 그러니 지금 졸린 사람들은 잠을 자고 쌩쌩할 때 오시길 바랍니다. (아마도 이 부분이 C 언어에서 가장 어려울 부분이 될 듯 하네요.. 저도 최대한 쉽게 설명하기 위해 노력하겠습니다^^)

  잠깐 지난 시간에 배웠던 것을 머리속으로 상기시켜봅시다. 일단, 

  • 배열은 포인터와 밀접한 관련이 있다
  • 1 차원 배열의 이름은 첫 번째 원소를 가리킨다. 즉, int arr[3]; 이라는 배열이 있다면 arr 와 &arr[0] 은 같다. 그런데, arr[0] 이 int 형이므로 arr 은 int*  형 이다. (왜냐하면 int 형을 가리키는 포인터는 int* 이니까) 

  이 두 가지 사실을 머리속에 잘 들어 있겠지요. 만일 위 두 문장을 읽으면서 조금이라도 의구심이 드는 사람은 바로 뒤로가기를 눌러서 이전 강좌를 보시기 바랍니다. 

   1 차원 배열 가리키기 
 

 
  일단, 강의의 시작은 간단한 것으로 해보겠습니다. 이전해도 말했듯이 (벌써 몇 번째 반복하고 있는지는 모르겠지만 그 만큼 중요하니까) int arr[10]; 이라는 배열을 만든다면 arr 이 arr[0] 을 가리킨다고 했습니다. 그렇다면 다른 int* 포인터가 이 배열을 가리킬 수 있지 않을까요? 한 번 프로그램을 짜봅시다.

 
#include <stdio.h>
int main()
{
    int arr[3] = {1,2,3};
    int *parr; 

    parr = arr; 
    /* parr = &arr[0]; 도 동일하다! */

    printf("arr[1] : %d \n", arr[1]);
    printf("parr[1] : %d \n", parr[1]);
    return 0;
}

성공적으로 컴파일 한다면


 일단, 중점적으로 볼 부분은 아래와 같습니다. 

 
    parr = arr;

  바로 arr 에 저장되어 있는 값을 parr 에 대입하는 부분이지요. 앞에서 말했듯이 arr 은 int 를 가리키는 포인터 입니다. 이 때, arr 에 저장된 값, 즉 배열의 첫 번째 원소의 주소를 parr 에 대입하고 있습니다. 다시 말해 위 문장은 주석에도 잘 나와 있듯이 아래와 같은 문장이 됩니다. 

    parr = &arr[0]

  따라서, parr 을 통해서 arr 을 이용했을 때와 동일하게 배열의 원소에 마음껏 접근할 수 있게 되는 것이 됩니다. 위 모습을 한 번 그림으로 나타내보면 (아마도 여러분들은 지금 수준이라면 머리속으로 다 그릴 수 있어야 할 것입니다) 

arr[0] 은 arr 과 parr 모두가 가리키게 됩니다.

참고적으로 한 방의 크기는 그림의 단순화를 위해 4 바이트로 하였습니다.


 
/* 포인터 이용하기 */
#include <stdio.h>
int main()
{
    int arr[10] = {100,98,97,95,89,76,92,96,100,99};

    int* parr = arr;
    int sum = 0;

    while(parr - arr <= 9)
    {
        sum += (*parr);
        parr ++;
    }

    printf("내 시험 점수 평균 : %d \n", sum/10);
    return 0;
}

  성공적으로 컴파일 하면


  일단, 포인터를 이용한 간단한 예제를 다루어보겠습니다. 
  
 
    int* parr = arr;

  먼저, int 형 1 차원 배열을 가리킬 수 있는 int* 포인터를 정의하였습니다. 그리고, 이 parr 은 배열 arr 을 가리키게 됩니다. 

 
    while(parr - arr <= 9)
    {
        sum += (*parr);
        parr ++;
    }

  그 다음 while 문을 살펴봅시다. while 문을 오래전에 배워서 기억이 안난다면 다시 뒤로 돌아가세요! 이 while 문은 parr - arr 이 9 이하일 동안 돌아가게 됩니다. sum 에 parr 이 가리키는 원소의 값을 더했습니다. += 연산자의 의미는 아시죠? sum += (*parr); 문장은 sum = sum + *parr 와 같다는 것 알고 계시지요? 

 
        parr ++;

  parr 을 1 증가시켰습니다. 이전 강좌에서도 이야기 하였지만 포인터 연산에서 1 증가시킨다면, parr 에 저장된 주소값에 1 이 더해지는 것이 아니라 1 * (포인터가 가리키는 타입의 크기) 가 더해진다는 것이지요. 즉, int 형 포인터 이므로 4 가 더해지므로, 결과적으로 배열의 그 다음 원소를 가리킬 수 있게 됩니다. 암튼, 위 작업을 반복하면 arr 배열의 모든 원소들의 합을 구하게 됩니다. while 문에서 9 이하일 동안만 반복하는 이유는, parr - arr >= 10 이 된다면 parr[10 이상의 값] 을 접근하게 되므로 오류를 뿜게 됩니다. 

  여기서 궁금한 것이 없나요? 우리가 왜 굳이 parr 을 따로 선언하였을까요? 우리는 arr 이 arr[0] 을 가리킨다는 사실을 알고 있으므로 arr 을 증가시켜서 *(arr) 으로 접근해도 되지 않을까요? 한 번, arr 의 값을 변경할 수 있는지 없는지 살펴봅시다.

 
/* 배열명 */
#include <stdio.h>
int main()
{
    int arr[10] = {100,98,97,95,89,76,92,96,100,99};
     
    arr ++; // 오류!! 배열 이름은 const 
    return 0;
}


  컴파일 해보면

error C2105: '++'에 l-value가 필요합니다.

  와 같은 오류를 만나게 됩니다. 이 말은 arr 이 상수이므로 연산할 수 없다는 말이 됩니다. 배열이름이 상수라고요? 맞습니다. 물론 *arr = 10; 과 같이 arr[0] 의 값은 바꿀 수 있지만 arr++ 와 같은 연산(즉, arr 이 다른 것을 가리키는 것)은 허용되지 않는 것을 보아서 const 키워드가 어느쪽에 붙은 포인터인지 짐작할 수 있을 것 입니다. 즉, 배열의 이름은 

  (뭐시기 뭐시기) *const (배열 이름) 

 과 같은 형태임을 알 수 있습니다. 결과적으로 배열의 이름은 죽어다 깨어나도 언제나 배열의 첫번째 원소를 가리키게 됩니다. 배열의 이름이 결코 다른 값들을 가리킬 수 없도록 C 에서 제한을 두는 이유는, 제 생각에 프로그래머가 실수로 배열의 이름이 가리키는 값을 잘못 바꿔서 배열을 '아무도 가리키지 않는 메모리 상의 미아'가 됨을 막으려고 한 것이 아닐까요?

   포인터의 포인터
 

  똑똑한 분들이라면 이러한 것들에 대해서도 생각해 보신 적이 있을 것입니다. 물론, 안하셔도 상관 없고요.. 저의 경우 포인터 처음 배울 때 그것 마저 이해하기도 힘들어서 한참 버벅거렸습니다 :) 아무튼. 지금 머리속으로 예상하시는 대로 포인터의 포인터는 다음과 같이 정의합니다. 

      int **p;

  위는 'int 를 가리키는 포인터를 가리키는 포인터' 라고 할 수 있습니다. 쉽게 머리에 와닿지 않죠? 당연합니다. 이전 강좌의 내용도 어려워 죽겠는데 위 내용까지 머리속에 쑤셔 넣으려면 얼마나 힘들겠어요? 그래서, 한 번 예제를 봅시다. 

/* 포인터의 포인터 */
#include <stdio.h>
int main()
{
    int a;
    int *pa;
    int **ppa;

    pa = &a;
    ppa = &pa;

    a = 3;

    printf("a : %d // *pa : %d // **ppa : %d \n", a, *pa, **ppa);
    printf("&a : %d // pa : %d // *ppa : %d \n", &a, pa, *ppa);
    printf("&pa : %d // ppa : %d \n", &pa, ppa);
    
    return 0;
}

성공적으로 컴파일 했다면

여러분의 결과는 약간 다를 수 있습니다. 다만, 같은 행에 있는 값들이 모두 같음을 주목하세요


 
  일단 위에 보시다 싶이 같은 행에 있는 값들은 모두 같습니다. 사실, 위 예제는 그리 어려운 것이 아닙니다. 포인터에 제대로 이해만 했다면 말이죠. 일단 ppa 는 int* 를 가리키는 포인터 이기 때문에 

 

    ppa = &pa;


 와 같이 이전의 포인터에서 했던 것 처럼 똑같이 해주면 됩니다. ppa 에는 pa 의 주소값이 들어가게 되죠. 
  

    printf("&pa : %d // ppa : %d \n", &pa, ppa);


  따라서 우리는 위의 문장이 같은 값을 출력함을 알 수 있습니다. 위의 실행한 결과를 보아도 둘다 1636564 를 출력했잖아요. 

    printf("&a : %d // pa : %d // *ppa : %d \n", &a, pa, *ppa);


그리고 이제 아래에서 두 번째 문장을 봐 봅시다. pa 가 a 를 가리키고 있으므로 pa 에는 a 의 주소값이 들어갑니다. 따라서, &a 와 pa 는 같은 값이 되겠지요. 그러면 *ppa 는 어떨까요? ppa 가 pa 를 가리키고 있으므로 *ppa 를 하면 pa 를 지칭하는 것이 됩니다. 따라서 역시 pa 의 값, 즉 &a 의 값이 출력되게 됩니다.

    printf("a : %d // *pa : %d // **ppa : %d \n", a, *pa, **ppa);


마지막으로 위의 문장을 살펴 봅시다. pa 가 a 를 가리키고 있으므로 *pa 를 하면 a 를 지칭하는 것이 되어 a 의 값이 출력됩니다. 그렇다면 **ppa 는 어떨까요? 이를 다시 써 보면 *(*ppa) 가 되는데, *ppa 는 pa 를 지칭하는 것이기 때문에 *pa 가 되서, 결국 a 를 지칭하는 것이 됩니다. 따라서, 역시 a 의 값이 출력되겠지요. 어때요? 간단하죠? 

위 관계를 그림으로 그리면 다음과 같습니다.

 

a 의 주소값을 pa 가, pa 의 주소값을 ppa 가 가지고 있습니다.

 

   2 차원 배열의 [] 연산자
 

 
  2 차원 배열의 [] 연산자에 대해선 제가 지난번 강좌에서 '생각 해보기' 문제로 내었던 것 같은데, 생각해보셨는지요? 일단 이전의 기억을 더듬에서 다음과 같은 배열이 컴퓨터 메모리 상에 어떻게 표현되는지 생각 해보도록 합시다. 

    int a[2][3];


  물론, 이 배열은 2 차원 배열이므로 평면위에 표시된다고 생각할 수 도 있지만, 컴퓨터 메모리 구조는 1 차원 적이기 때문에 1 차원으로 바꿔서 생각해봅시다. 되었나요? 그렇다면, 제가 그림을 보여드리죠.

a 는 a[0] 을 가리키며, a[0] 은 a[0][0] 을 가리킵니다. a[1] 은 a[1][0] 을 가리킵니다.

실제로 프로그램을 짜서 실행해 보면 메모리 상에 위와 같이 나타남을 알 수 있습니다. 한 번 해보세요~


  일단, 위 그림에서 왼쪽에 메모리 상의 배열의 모습이 표현된 것은 여러분이 쉽게 이해하실 수 있스리라 믿습니다. 다만, 제가 설명해야 할 부분은 오른쪽에 큼지막하게 화살표로 가리키고 있는 부분이지요. 먼저 아래의 예제를 봅시다. 

/* 정말로? */
#include <stdio.h>
int main()
{
    int arr[2][3];
    
    printf("arr[0] : %x \n", arr[0]);
    printf("&arr[0][0] : %x \n", &arr[0][0]);

    printf("arr[1] : %x \n", arr[1]);
    printf("&arr[1][0] : %x \n", &arr[1][0]);

    return 0;
}

성공적으로 컴파일 했다면

표현된 주소값은 여러분과 다를 수 있습니다.


 오옷.. arr[0] 에 저장되어 있는 값이 arr[0][0] 의 주소값과 같고, arr[1] 에 저장되어 있는 값이 arr[1][0]의 주소값과 같습니다. 이것을 통해 알 수 있는 사실은..? 아마도 다 알겠지만, arr[0] 은 arr[0][0] 을 가리키는 포인터 이고, arr[1] 은 arr[1][0] 을 가리키는 포인터라는 뜻이 되겠지요. 이 때, arr[0][0] 의 형이 int 이므로 arr[0] 은 int* 형이 되겠고, 마찬가지로 arr[1] 도 int* 형이 되겠지요. 

/* a? */
#include <stdio.h>
int main()
{
    int arr[2][3];
    
    printf("&arr[0] : %x \n", &arr[0]);
    printf("arr : %x \n", arr);

    return 0;
}

성공적으로 컴파일 했다면 

출력 결과가 여러분의 출력결과와 다를 수 있지만 값이 같음을 주목하세요


    흠.. 이번에는 arr[0] 의 주소값과 arr 에 저장된 값이 같군요.. 근데요, 이러한 결과는 어디서 본 것 같지 않으세요? 맞아요. 실제로 우리가 만들었던 1 차원 int 배열에서도 배열의 이름이 배열의 첫 번째 원소를 가리키고 있었습니다. 그런데 위 2차원 배열의 경우에도 arr 이 arr[0] 을 가리키고 있으므로 동일하다고 볼 수 있습니다. 그렇다면, arr[0] 이 int* 형 이므로 arr[0] 을 가리키는 arr 은 int** 형 일까요?  아마 여러분 머리속엔 지금 이러한 생각이 떠오를 것입니다.

"당연하지.. 그걸 말이라 묻냐? 니가 위에서 설명했잖아. int* 를 가리키는 포인터는 int** 이라고 " 

  그런데 생각을 해보세요. 그렇게 당연하다면 제가 질문을 했겠습니까? (^^) 답변은 "아니오" 입니다.

아마 여러분은 '내가 위에서 뭘 배웠지?' 라는 생각이 들면서 혼돈에 빠질 것입니다. 당연하지요.. 제가 이야기를 안한 부분이니까요. 저도 처음에 이 부분을 배웠을 땐 이전까지 배웠던 사실이 쓰나미 처럼 밀려나가고 머리속에 하애지는 현상을 겪었습니다. 아무튼.. 왜 그런지 설명해보죠. 

   포인터의 형(type) 을 결정짓는 두 가지 요소  
 

먼저 포인터의 형을 결정하는 두 가지 요소에 대해 이야기 하기 전에, 위에서 배열의 이름이 왜 int** 형이 될 수 없는지에 대해 먼저 이야기 해봅시다. 만일 int** 형이 될 수 있다면 맨 위에서 했던 것 처럼 int** 포인터가 배열의 이름을 가리킨다면 배열의 원소에 자유롭게 접근할 수 있어야만 할 것입니다. 

/* 과연 될까? */
#include <stdio.h>
int main()
{
    int arr[2][3]={{1,2,3},{4,5,6}};
    int **parr;

    parr = arr;

    printf("arr[1][1] : %d \n", arr[1][1]);
    printf("parr[1][1] : %d \n", parr[1][1]);

    return 0;
}

  그런데 컴파일 시에 아래와 같은 경고가 기분을 나쁘게 하네요.

  warning C4047: '=' : 'int **'의 간접 참조 수준이 'int (*)[3]'과(와) 다릅니다.

  아무튼, 무시하고 실행해봅시다. 

 
  헉! 예전에 보았던 친근한 오류가 뜹니다. 무슨 뜻일까요? 예전에 배열에 대해 공부하였을 때(11-1 강) '초기화 되지 않은 값' 에 대해서 이야기한 적이 있었을 것입니다. 이 때, int arr[3]; 이라 했는데 arr[10]= 2; 와 같이 허가되지 않은 공간에 접근하기만 해도 위와 같은 오류가 발생한다고 했습니다. 
  
  위 예제의 경우도 마찬가지 입니다. parr[1][1] 에서 이상한 메모리 공간의 값에 접근하였기에 발생한 일이지요. 그렇다면 왜? 왜? 이상한 공간에 접근하였을까요? 

  먼저, 일차원 배열에서 배열의 형과, 시작 주소값을 안다고 칠 때, n 번째 원소의 시작 주소값을 알아내는 공식을 생각해봅시다. 만일 이 배열의 형을 int 로 가정하고, 시작 주소를 x 라고 할 때, (참고적으로 다 아시겠지만 int 는 4 바이트1) n 번째 원소에 접근한다면 (x + 4 곱하기 (n - 1) 로 나타낼 수 있죠)


  와 같이 나타낼 수 있습니다. 왜냐구요? 아마, 여러분들이 스스로 생각해보세요 :)

  이번에는 이차원 배열을 나타내봅시다. 이 이차원 배열이 int arr[a][b]; 라고 선언되었다면 (여기서 a 와 b 는 당연히 정수겠죠) 아래와 같이 2차원 평면에 놓여 있다고 생각할 수 있습니다. 
 
arr[0][0], arr[0][1] .... arr[a-1][b-1] 까지 쭈르륵 나열된 모습

참고적으로 행은 '가로' 이고, 열은 '세로' 입니다.


  메모리는 선형(1차원) 이므로 절대로 위와 같이 배열될 일은 없겠지요. 위 이차원 배열을 메모리에 나타내기 위해서는 각 행 부터 읽어주면 됩니다. 즉, 위 배열은 아래와 같이 메모리에 배열됩니다. 

물론 메모리 상에서는 실제로 선형으로 존재한다는 것 아시죠!

사실 위에서도 비슷한 그림이 나오지만 또 그린 이유는 머리에 완전히 박아 두라는 의미 입니다.

  즉, arr[0][0] 부터 arr[0][1] .... arr[a-1][b-1] 순으로 저장되게 되지요. 그렇다면 위 배열의 시작주소를 x 라 하고, int 형 배열이고, arr[c][d] 라는 원소에 접근한다고 칩시다. 그렇다면 이 원소의 주소값은 어떻게 계산될까요? 

  일단, 위 원소는 (c+1) 번째 행의 (d+1) 번째 열에 위치해 있다고 생각할 수 있습니다. (예를 들어서 arr[0][2] 는 1 번째 행의 3 번째 열에 위치해 있다) 그러면, 먼저 (c+1) 행의 시작 주소를 계산해봅시다. 간단히 생각해보아도 x + c * b * 4 라는 사실을 알 수 있습니다. 왜냐하면 4 를 곱해준 것은 int 이기 때문이고, (c+1) 행 앞에 c 개의 행들이 있는데, 각 행들의 길이가 b 이기 때문이죠. 
  
  그리고 이 원소가 (d+1) 번째에 있다는 사실을 적용하면 ((c+1) 행 시작주소) + d * 4 라고 계산될 수 있습니다. 결과적으로 (x + 4 곱하기 b 곱하기 c + 4 곱하기 d 가 됩니다)


  가 됩니다. 참고적으로 이야기 하자면, 수학에서 곱하기 기호가 매우 자주 등장하므로 생각하는 경향이 있는데, 저도 매번 곱하기 기호를 쓰기 불편하므로 생략하도록 하겠습니다. 위 식은 아래의 식과 동일합니다. ( x + 4bc + 4d 로 간추립니다)


  주목할 점은 식에 b 가 들어간다는 것입니다. (1 차원 배열에서는 배열의 크기에 관한 정보가 없어도 배열의 원소에 접근할 수 있었는데 말이죠) 다시 말해, 처음 배열 arr[a][b] 를 정의했을 때의 b 가 원소의 주소값을 계산하기 위해 필요하다는 것입니다. 우리는 이전의 예제에서 int** 로 배열의 이름을 나타낼 수 있다고 생각하였습니다. 하지만 이렇게 선언된 parr 으로 컴퓨터가 parr[1][1] 원소를 참조하려고 하면 컴퓨터는 b 값을 알 수 없기 때문에 제대로된 연산을 수행할 수 없게됩니다. 
  따라서, 이차원 배열을 가리키는 포인터는 반드시 b 값에 대한 정보를 포함하고 있어야 합니다. 

  결론적으로 포인터 형을 결정하는 것은 다음 두 가지로 요약할 수 있습니다.
  1. 가리키는 것에 대한 정보 (예를 들어, int* 이면 int 를 가리킨다, char** 이면 char* 을 가리킨다 등등)
  2. 1 증가시 커지는 크기 (2 차원 배열에서는 b * (형의 크기) 를 의미한다)
  여기서 1 증가시 커지는 크기가 2 차원 배열에서는 b * (형의 크기) 를 의미하는지 궁금한 사람들이 있을 것입니다. 한 번 해봅시다.

/* 1 증가하면 ? */
#include <stdio.h>
int main()
{
    int arr[2][3]={{1,2,3},{4,5,6}};
    
    printf("arr : %x , arr + 1 : %x \n", arr, arr+1);

    return 0;
}

  성공적으로 컴파일 한다면

  
  16 진수의 연산과 친숙하지 않더라도 0x2CF7D8 - 0x2CF7CC (참고로 예전에도 이야기 했듯이 제 강좌에서 16 진수로 나타내었다는 사실을 명시하기 위해 앞에 0x 를 붙인다고 했습니다.) 를 계산해 보면 0xC 가 나옵니다. 0xC , 즉 십진수로 12 입니다. 근데, 위 배열의 b 값은 3 이고 int 의 크기는 4 바이트 이므로, 3 * 4 = 12 가 딱 맞게 되는 것이지요. 

  왜 그럴까요? 사실, 그 이유는 단순합니다. 거의 맨 위의 그림을 보면 이차원 배열에서 a 가 a[0] 을 가리키고 있는 그림을 볼 수 있습니다. 만일 1 차원 배열 b[3] 이 있을 때 b + 1 을 하면 b[1] 을 가리키잖아요? 2 차원 배열도 동일하게 a 가 1 증가하면 a[1] 을 가리키게 됩니다. 다시말해 두 번째 행의 시작 주소값을 가리키는 포인터2를 가리키게 된다는 것이지요. 

/* 드디어! 배우는 배열의 포인터 */
#include <stdio.h>
int main()
{
    int arr[2][3]={{1,2,3},{4,5,6}};
    int (*parr)[3];  // 괄호를 꼭 붙이세요

    parr = arr; // parr 이 arr 을 가리키게 한다. 

    printf("parr[1][2] : %d , arr[1][2] : %d \n", parr[1][2], arr[1][2]);

    return 0;
}

  성공적으로 컴파일 한다면 


    드디어, 2 차원 배열을 가리키는 포인터에 대해 이야기 하겠습니다. 1 차원 배열을 가리키는 포인터는 간단합니다. (아마 아시겠죠?) 그런데, 2 차원 배열을 가리키는 포인터는 배열의 크기에 관한 정보가 있어야 한다고 했습니다. 2 차원 배열을 가리키는 포인터는 아래와 같이 써주면 됩니다. 

  (배열의 형) ( *(포인터 이름) )[2 차원 배열의 열 개수] ;

  이렇게 포인터를 정의하였을 때 앞서 이야기한 포인터의 조건을 잘 만족하는지 보도록 합시다. 일단, (배열의 형) 을 통해서 원소의 크기에 대한 정보를 알 수 있습니다. 즉, 가리키는 것에 대한 정보를 알 수 있게 됩니다. (조건 1 만족). 또한, [2 차원 배열의 열 개수] 를 통해서 1 증가시 커지는 크기도 알게 됩니다. 바로 (배열의 형 크기 - 예를 들어 int 는 4, char 은 1) * (2 차원 배열의 열 개수) 만큼 커지게 됩니다. 

    int (*parr)[3]; 

  위와 같이 정의한 포인터 parr 을 해석해 보면, int 형 이차원 배열을 가리키는데, 그 배열의 열의 개수가 3 개 이군요! 라는 사실을 알 수 있습니다 (정확히 말하면, int* 를 가리키는데, 1 증가시 3 이 커진다 라는 의미입니다) .

  주의할 점은 괄호로 꼭 묶어주어야 한다는 것입니다. 만일 괄호로 묶지 않는다면 다른 결과가 나오니 주의하세요.

/* 배열 포인터 */
#include <stdio.h>
int main()
{
    int arr[2][3]={{1,2,3},{4,5,6}};
    int brr[10][3];
    int crr[2][5];

    int (*parr)[3];

    parr = arr; // O.K
    parr = brr; // O.K
    parr = crr; // 오류!!!! 

    return 0;
}

  앞서, 2 차원 배열에서 원소의 주소값을 계산하는 식을 기억하시는지요? 다시, 불러오면 (x + 4bc + 4d 이지요!)


  여기서 보아야 할 점은 a 의 값이 필요 없다는 것입니다. 다시말해, b 값만 알고 있다면 a 값과 무관하게 원소에 접근할 수 있다는 것이지요. 위 예제에서도 보시다 싶이, parr 이 arr[2][3] 과 brr[10][3] 을 가리킬 수 있습니다. 왜냐하면 b 값, 즉 열의 개수가 동일한 이차원 배열이기 때문이죠. 반면에 carr 은 arr 과 b 값, 즉 3 과 5 가 다르기 때문에 parr 에 crr 을 대입하면 오류가 생깁니다. 만일 crr 을 parr 에 대입했다고 쳐도, 컴퓨터는 parr 을 이용하여 carr 을 참조할 때, 열의 개수가 3 인 배열로 생각하기 때문에 이상한 결과가 나타납니다. 

   포인터 배열
 

 
  포인터 배열, 말그대로 '포인터들의 배열' 입니다. 위에서 설명한 배열 포인터는 '배열을 가리키는 포인터' 였죠. 두 용어가 상당히 헷깔리는데, 그냥 '언제나 진짜는 뒷부분' 이라고 생각하시면 됩니다. 즉, 포인터 배열은 정말로 배열이고, 배열 포인터는 정말로 포인터 였죠. 

/* 포인터 배열 */
#include <stdio.h>
int main()
{
    int *arr[3];
    int a = 1, b = 2, c = 3;
    arr[0] = &a;
    arr[1] = &b;
    arr[2] = &c;

    printf("a : %d, *arr[0] : %d \n", a, *arr[0]);
    printf("b : %d, *arr[1] : %d \n", b, *arr[1]);
    printf("b : %d, *arr[2] : %d \n", c, *arr[2]);

    printf("&a : %d, arr[0] : %d \n", &a, arr[0]);
    return 0;
}

  성공적으로 컴파일 한다면
  

마지막 출력결과는 여러분과 상이할 수 있으나 두 값이 같음을 주목하세요


  일단, arr 배열의 정의 부분을 봐봅시다. 

    int *arr[3];

  위 정의가 마음에 와닿나요? 사실, 저는 처음에 배울 때 별로 와닿지 않았습니다. 사실, 이전에도 말했듯이 위 정의는 아래의 정의와 동일합니다. 

    int* arr[3];

  이제, 이해가 되시는지요? 우리가 배열의 형을 int, char 등등으로 하듯이, 배열의 형을 역시 int* 으로도 할 수 있습니다. 다시말해, 배열의 각각의 원소는 'int 를 가리키는 포인터' 형으로 선언된 것입니다. 따라서, int 배열에서 각각의 원소를 int 형 변수로 취급했던 것처럼 int* 배열에서 각각의 원소를 포인터로 취급할 수 있습니다. 마치, 아래처럼 말이지요. 

    arr[0] = &a;
    arr[1] = &b;
    arr[2] = &c;

  각각의 원소는 각각 int 형 변수 a,b,c 를 가리키게 됩니다. 이를 그림으로 표현하면 아래와 같습니다.

arr[0] 에는 a 의 주소값, arr[1] 에는 b 의 주소값, arr[2] 에는 c 의 주소값

  arr[0] 에는 변수 a 의 주소가, arr[1] 에는 변수 b 의 주소, arr[2] 에는 변수 c 의 주소가 각각 들어가게 됩니다. 이는 마지막 printf 문장에서도 출력된 결과로 확인 할 수 있습니다. 

  사실, 포인터 배열에 관한 내용은 짧게 끝냈습니다. 하지만, C 언어에서 상당히 중요하게 다루어지는 개념입니다. 아직 여러분이 그 부분에 대해 이야기할 단계가 되지 않았다고 보아, 기본적인 개념만 알려 드린 것입니다. 꼭 잊지 마시길 바랍니다. 

  자. 이제 배열을 향한 대장정이 끝이 났습니다. 여기까지 부담없이 이해하셨다면 여러분은 C 언어의 성지를 넘게 된 것입니다! 사실, 여러분은 이 포인터를 무려 3 강의를 연달아 들으면서 '도대체 이걸 왜하냐?' 라는 생각이 머리속에 끝없이 멤돌았을 것입니다. 물론, 앞에서도 이야기 했지만 포인터는 다음 단계에서 배울 내용에 필수적인 존재입니다. 사실, 지금은 아무짝에도 쓸모 없는 것 같지만... 

  여기까지 스크롤을 내리면서도 마음 한 구석에 응어리가 있는 분들은 과감하게 포인터 강좌를 처음부터 읽어 보세요. 저의 경우 포인터만 책 수십권을 찾아보고 인터넷에서 수십개의 자료를 찾아가며 익혔습니다. 그래도 궁금한 내용들은 꼬오옥 댓글을 달아주세요. 저는 정말 아무리 이상하고 괴상한 질문도 환영하니.. 꼭 궁금한 내용을 물어봐주세요 :) 

생각 해 볼 문제
 (사실, 잘 몰라도 걱정 안하셔도 됩니다. 어느 순간 답안이 머리속에 떠오를 테니까요)


3 차원 배열의, 배열이름과 동일한 포인터는 어떻게 정의될 것인가? (난이도 : 中)
(참조 :  2 차원 배열에선 int (*arr)[4]; 와 같은 꼴이었다) 

포인터 간의 형변환은 무엇을 의미하는가? 그리고, C 언어에서 포인터 간의 형변환이 위험한 것인가? (난이도 : 中)
(참고적으로, 포인터간의 형 변환은 아직 이야기 한 적이 없으나 한 번 시도는 해보세요)

 

 

qkralsrb