본문 바로가기

Books/열혈강의 C 프로그래밍

[열혈C] 02. Part2 내용 정리 (포인터와 배열의 시작)


에러가 발생하면 기뻐해라!!!
지금의 에러가 그대들을 현명한 프로그래머로 이끌어 줄 것이다.

널(Null) 문자에 대한 이해

문자열을 표현할 때에는 문자열의 끝을 의미하는 '\0'을 문자열 끝에 삽입한다.

이 문자를 가리켜 널(Null) 문자라 하며, 아스키 코드 값은 0이다.



문자열 입력받기

scanf 함수는 공백(스페이스, tab, enter)을 기준으로 데이터 수를 결정짓는다.

따라서 My  Sweet Home이라고 입력되면 My, Sweet, Home 세 개의 문자열을 입력한 샘이다.



포인터

메모리 공간의 주소 값을 저장하는데 사용하는 변수를 포인터라 한다.

포인터가 변수라는 것을 강조하기 위해서 '포인터 변수'라는 표현을 쓴다.

  • 포인터는 const 키워드에 의해서 상수화되기도 한다. (뒤에서 자세히 설명)
  • 포인터 변수의 크기는 타입에 상관없이 4byte로 표현된다. (컴퓨터 주소 체계가 4byte로 표현된다는 것을 의미)
  • 포인터에도 타입(type: 형)이 존재한다. 가리키고자 하는 변수의 자료형에 따라 적절한 타입의 포인터를 선언해야 한다.

주소 값을 참조할 때 사용하는 연산자는 & 연산자이다.

예를 들어, &a 는 변수 a의 주소 값을 반환하라는 의미다.


포인터 변수 앞에 * 연산자를 붙이게 되면, 포인터가 가리키는 메모리 공간에 존재하는 값을 참조하겠다는 뜻이 된다.


포인터에 다양한 타입이 있는 이유

포인터에 타입이 존재하지 않는다면, 포인터를 이용해서 변수를 참조하는 경우 몇 바이트를 읽어들여야 할 지 알 수 없게 된다. 

포인터의 타입은 메모리를 참조하는 방법을 알려주는 역할을 한다.



포인터와 배열

배열의 이름도 포인터다.

  • 단 그 값을 바꿀 수 없는 상수라는 점이 일반적인 포인터와의 유일한 차이점이다.
  • 배열 이름은 첫 번째 요소의 주소 값을 나타낸다.
  • 배열 이름을 가리켜 "상수 포인터"라 한다.


포인터를 배열의 이름처럼 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
 
int main(void)
{
    int arr[3= {012};
    int *ptr;
 
    ptr = arr;
 
    printf("%d, %d, %d \n", ptr[0], ptr[1], ptr[2]);
    return 0;
}
cs


포인터는 제한된 형태의 연산이 가능하다.

포인터 연산 - 포인터 값을 증가 혹은 감소시키는 것이다. (곱셈, 나눗셈은 불가능)

1
2
3
4
// 아무것도 가리키지 않는 형태 = 널(Null) 포인터
int* ptr1 = 0;
char* ptr2 = 0;
double* ptr3 = 0;
cs


포인터 연산에 따른 실질적인 값의 변화는 포인터 타입에 따라 다르다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 배열을 다룰 때 범위를 넘어선 접근을 하지 않도록 주의해야 한다.
 
#include <stdio.h>
int main(void)
{
       int arr[5= {1, 2, 3, 4, 5};
       
       int* pArr = arr;
       printf("%d\n"*pArr);
       printf("%d\n"*(++pArr));
       printf("%d\n"*(++pArr));
       printf("%d\n"*(pArr + 1));
       printf("%d\n"*(pArr + 2));
       
       return 0;
}
cs




arr[i] = *(arr+i)

배열의 이름을 포인터처럼 사용할 수 있는 방법

arr이 포인터이거나 배열 이름인 경우  arr[i] == *(arr+i)

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(void)
{
       int arr[2= { 1,2 };
       int* pArr = arr;
       // 배열 이름을 통한 출력(배열 이름을 포인터처럼 사용)
       printf("%d %d \n", arr[0], *(arr + 1));
       // 포인터 변수를 통한 출력(포인터를 배열처럼 사용)
       printf("%d %d \n", pArr[0], *(pArr + 1));
       return 0;
}
cs




문자열 상수를 가리키는 포인터

배열 str1은 문자열 전체를 저장

포인터 str2는 메모리상에 저장되어 있는 문자열 상수 "ABCD"를 단순히 가리킴

1
2
char str1[5= "abcd";
char *str2 = "ABCD";
cs


1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main(void)
{      
       char str1[5= "abcd";     // 문자열 변수 선언
       char *str2 = "ABCD";       // 문자열 상수 선언
       printf("%s \n", str1);
       printf("%s \n", str2);
       str1[0= 'x';             // 문자열 변수 변경, 따라서 OK
       //str2[0] = 'x';           // 문자열 상수 변경, Error 발생   
       printf("%s \n", str1);
       printf("%s \n", str2);
       return 0;
}
cs


문자열 상수는 메모리 공간에 할당되면 주소를 반환한다.


똑같은 문자열을 선언하면 한번만 메모리 공간에 할당된다.

  • 좋은 컴파일러는 똑같은 코드라 할지라도 메모리를 효율적으로 사용하기 위해서 최적화(Optimization)라는 과정을 거친다.
  • 문자열이 상수면 내용 변경이 불가능하다. 이러한 상황에서 따로 만들면 메모리만 차지하기 때문에, 이는 메모리 공간을 효율적으로 사용하기 위해 컴파일러가 진행한 최적화의 결과이다. (단, 일부 컴파일러에만 해당)
  • Turbo C/C++ 컴파일러는 문자열 상수 조작도 허용하고 같은 문자열 상수라 할지라도 두 번 메모리 공간에 할당한다. (최적화 수행 X)
1
2
3
4
5
6
7
8
#include <stdio.h>
int main(void)
{
    char *str1 = "Good!";
    char *str2 = "Good!";
    printf("%d, %d \n", str1, str2);
    return 0;
}
cs



포인터 배열

배열 요소로 포인터를 지니는 포인터 배열


1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(void)
{
    int a = 10, b = 20, c = 30;
    int* arr[3= {&a, &b, &c};
    printf("%d \n"*arr[0]);
    printf("%d \n"*arr[1]);
    printf("%d \n"*arr[2]);
    return 0;
}
cs



문자열 배열 (char형 포인터 배열)

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main(void)
{
    char* arr[3= {"Fervent-lecture",
                    "TCP/IP",
                    "Socket Programming"
    };
    printf("%s \n", arr[0]);
    printf("%s \n", arr[1]);
    printf("%s \n", arr[2]);
    return 0;
}
cs





포인터와 함수

함수의 인자로 배열 전달하기

  1. 함수의 인자 전달 방식
    인자 전달의 기본 방식은 복사다. 즉, 함수 호출 시 전달되는 값을 매개 변수라는 것을 통해서 전달 받는데, 이때 값의 복사가 일어난다.


    함수 호출 시 배열을 통째로 복사하여 넘겨주는 방법은 존재하지 않는다. 이에 대안은 포인터를 이용하는 것이다. (구조체를 정의하면 배열을 통째로 복사해서 넘겨줄 수 있다)

  2. 배열을 함수의 인자로 전달하는 방식
    배열을 통째로 전달하는 것이 불가능하다면, 배열의 주소 값을 인자로 전달해서 이를 통해서 접근하도록 유도하는 방법을 생각해 볼 수 있다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <stdio.h>
     
    void fct(int *arr2);
     
    int main(void)
    {
        int arr1[2= {12};
        fct(arr1);
        printf("%d \n", arr1[0]);
        return 0;
     
    void fct(int *arr2)
    {
        printf("%d \n", arr2[0]);
        arr2[0]=3;
    }
    cs

  3. 배열의 길이를 얻는 방법(공식) : sizeof(arr)/sizeof(int)
    ArrAdder 호출 시 첫 번째 인자(배열의 주소)만 전달할 경우 함수 내에서 배열의 길이를 알 방법이 없다. 그래서 반드시 배열의 길이를 두 번째 인자로 전달해야 한다.

  4. 배열을 인자로 전달받는 함수의 또 다른 선언
    ArrAdder 호출 시 첫 번째 인자(배열의 주소)만 전달할 경우 함수 내에서 배열의 길이를 알 방법이 없다. 그래서 반드시 배열의 길이를 두 번째 인자로 전달해야 한다.
    둘 다 int형 포인터 변수이다.
    다만 함수의 매개 변수를 선언하는데 있어서 인자로 배열이 전달된다는 것을 좀 더 명확히 할 수 있도록 "int pArr[]"이라는 선언을 허용하는 것 뿐이다. 

    공부하는 동안에는 배열을 인자로 전달받는 매개 변수 선언 시 "int *arr" 와 같은 선언을 주로 사용하기 바란다. "int arr[]" 와 같은 선언은 매개 변수 선언 시 예외적으로 허용되는 방법에 지나지 않는다. (다른 사람이 어떠한 방식으로 구현하더라도 이해할 수 있도록 설명해준 것이다)



Call-By-Value 와 Call-By-Reference

함수의 호출 방식

  • Call-By-Value (값에 의한 호출)
    가장 일반적인 함수 호출의 형태 "값의 복사"
  • Call-By-Reference (참조에 의한 호출)
    함수 호출 시 변수의 주소를 전달해서, 인자로 전달된 주소가 가리키는 변수의 조작을 함수 내에서 가능하게 하는 것.


이제는 알 수 있다! scanf 함수 호출 시 &를 붙이는 이유

scanf 함수는 내부적으로 사용자로부터 정수를 입력받은 다음, 변수 val에 접근해서 값을 대입해야 한다. 결국 Call-By-Reference에 해당한다.



포인터와 const 키워드

상수를 만들 때 사용하는 const 키워드

포인터를 상수화

  1. 포인터 자체를 상수화시킨다
    p가 가리키는 변수의 값을 못 바꾸게 하겠다는 의미
    즉, 포인터 p를 통해서 변수 a의 값을 변경하는 것만 막겠다는 의미
    따라서 변수 a의 조작은 문제되지 않는다.
    1
    2
    3
    4
    int a = 10;
    const int* p = &a;
    *= 30;    // Error
    = 30;     // OK!
    cs



  2. 포인터가 가리키는 변수를 상수화시킨다
    p가 지니는 주소 값을 변경할 수 없다는 뜻
    따라서 포인터가 가리키는 변수의 조작은 전혀 문제 되지 않는다.
    1
    2
    3
    4
    5
    int a = 10;
    int b = 20;
    int* const p = &a;
    = &b;    // Error
    *= 30;   // OK!
    cs

    포인터도 상수화시키고 포인터가 가리키는 데이터도 상수화시키려면?

    1
    2
    3
    4
    5
    int a = 10;
    int b = 20;
    const int* const p = &a;
    = &b;    // Error
    *= 30;   // Error
    cs


  3. const를 사용하는 이유?
    const 키워드를 많이 사용하게 되면 프로그램의 구성은 안정적이 된다.

    const 사용전  
    컴파일하고 실행하는 과정에서 오류메시지를 보여주지 않음
    다만 결과 값이 이상할 뿐....
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <stdio.h>
     
    float PI = 3.14;
     
    int main(void)
    {
        float rad;
        PI = 3.07;    // 분명히 실수!
        scanf("%f"&rad);
        printf("원의 넓이는 %f \n", rad*rad*PI);
        return 0;
    }
    cs

    const 사용 후  
    자신의 실수를 컴파일러에 의해서 발견  
    이렇게 실수를 컴파일러가 발견할 수 있도록 프로그램을 작성하는 것은 아주 중요하다.
    훌륭한 프로그래밍 습관은 아주 사소한 것에서부터 시작된다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <stdio.h>
     
    const float PI = 3.14;    // PI값 변경 시 오류 발생
     
    int main(void)
    {
        float rad;
        PI = 3.07;    // 분명히 실수!
        scanf("%f"&rad);
        printf("원의 넓이는 %f \n", rad*rad*PI);
        return 0;
    }
    cs




열혈강의 C 프로그래밍
국내도서
저자 : 윤성우
출판 : 프리렉 2003.12.15
상세보기