C언어 기초 문법 빠르게 복습하기 (2)

    6) 배열

    배열이란?

    : 같은 자료형의 값을 순서대로 여러 개 저장할 수 있는 변수 묶음

    int scores[5];  // 정수 5개 저장하는 배열 선언

    - 위의 배열은 scores[0] ~ scores[4]까지 총 5칸 (5개 요소)

    - scores[i] 로 접근 

     

    - 배열 선언 및 초기화

    int a[5] = {10, 20, 30, 40, 50};

     

    (크기 생략)

    int a[] = {10, 20, 30};  // 크기 3으로 자동 설정

     

    - 배열 전체를 출력하는 예제 (반복문 사용)

    #include <stdio.h>
    
    int main(void) {
        int a[5] = {1, 2, 3, 4, 5};
    
        for (int i = 0; i < 5; i++) {
            printf("a[%d] = %d\n", i, a[i]);
        }
    
        return 0;
    }

     

    - 사용자 입력을 받아서 배열을 구성하는 예제

    int a[5];
    for (int i = 0; i < 5; i++) {
        printf("%d번째 값 입력: ", i + 1);
        scanf("%d", &a[i]);
    }

     

    ** scanf("%d", &a[i]); 의 형태로 사용함.

     

    <연습 문제>

    사용자로부터 정수 5개를 입력받아 배열에 저장하고, 최댓값과 그 인덱스를 출력하라.

    - 힌트: 배열 선언, for문으로 입력받기, max와 maxIndex 변수 사용

     

    <출력 예시>

    5개의 정수를 입력하세요:
    10 60 30 40 20
    
    최댓값: 60
    인덱스: 1

     

     

    <정답>

    #include <stdio.h>
    
    int main(void) {
    	int arr[5];
    
    	printf("정수 5개를 입력하시오\n");
    	for (int i = 0; i < 5; i++)
    		scanf("%d", &arr[i]);
    
    	int max = arr[0];
    	int maxIndex = 0;
    
    	for (int i = 1; i < 5; i++){
    		if(arr[i]>max){
    		max = arr[i];
    		maxIndex = i;
    		}
    	}
    
    	printf("최댓값: %d\n", max);
    	printf("인덱스: %d\n", maxIndex);
    
    	system("pause");
    	return 0;
    }

     

    <연습문제>

    정수 5개를 입력받아서 배열에 저장하고, 그 배열을 함수에 전달하여 전체 합을 계산하라.

    #include <stdio.h>
    
    int sumArray(int arr[], int size) {
    	int sum = 0;
    	for (int i = 0; i < size; i++) {
    		sum += arr[i];
    	}
    
    	return sum;
    }
    
    
    int main(void) {
    	int arr[5];
    
    	printf("정수 5개를 입력하시오\n");
    	for (int i = 0; i < 5; i++)
    		scanf("%d", &arr[i]);
    
    	int total = sumArray(arr, 5);
    
    	printf("총합: %d\n", total);
    
    	system("pause");
    	return 0;
    }

     

     

    7) 포인터 << 집중 학습 

    (※포인터를 복습하며 개인적으로 정리하기 위한 글이지, 교재 순서대로 정리된 입문용 글이 아닙니다.

    포인터를 아예 처음 접하는 분에게는 예제의 순서가 중구난방일 수 있습니다.)

     

    포인터란?

    : 어떤 변수의 주소를 저장하는 변수

    - 일반적인 변수는 값을 저장하지만, 포인터 변수는 주소를 저장한다.

    int *p = &x;

    *p 는 주소 p가 가리키는 곳의 값, &x는 x의 주소

     

    - 핵심 문법 

    int *p; 정수형 값을 가리키는 포인터 선언
    p = &x; x의 주소를 p에 저장
    *p p가 가리키는 주소에 있는 값을 읽거나 바꿀때

     

    - 예제: 포인터의 기본 동작

    #include <stdio.h>
    
    int main(void) {
        int x = 10;
        int *p = &x;
    
        printf("x의 값: %d\n", x);      // 10
        printf("x의 주소: %p\n", &x);   // 예: 0x7ff...
        printf("p의 값(=x의 주소): %p\n", p);
        printf("*p = x의 값: %d\n", *p);  // 10
    
        *p = 20;  // x의 값을 바꿈!
        printf("x의 값(변경 후): %d\n", x);  // 20
    
        return 0;
    }
    x:  10       ← 변수 자체
    &p: 0x123    ← 포인터 p의 주소
    p:  &x       ← x의 주소 저장됨
    *p: x의 값   ← x가 가리키는 값을 의미

     

    *은 값에 접근하는 것, &는 주소에 접근하는 것

     

    - 예제: 포인터 두 개와 값 바꾸기 (헷갈리지 않도록)

    #include <stdio.h>
    
    int main(void) {
        int a = 10;
        int b = 20;
    
        int *p1 = &a;   // p1은 a를 가리킴
        int *p2 = &b;   // p2는 b를 가리킴
    
        *p1 = 100;      // a의 값을 바꿈
        p1 = p2;        // 이제 p1도 b를 가리킴
        *p1 = 200;      // b의 값을 바꿈
    
        printf("a = %d\n", a);  // ?
        printf("b = %d\n", b);  // ?
    
        return 0;
    }

     

    <생각해보기>

    1) *p1 = 100; 이후 a의 값은?

    2)  p1 = p2; 이후 p1은 무엇을 가리키나?

    3) *p1 = 200; 이후 b의 값은?

     

    <풀이>

    처음에는 a = 10, b = 20

    p1 값은 a의 주소, p2 값은 b의 주소임

    포인터 p1을 이용해서 a값을 100으로 바꿈

    p2를 p1에 대입, p2의 값 즉 b의 주소를 p1에 저장하게 됨

    p1을 이용해서 b 값을 200으로 바꿈

    최종적으로 a는 100, b는 200

     

    <참고>

    - 위의 예제에서는 포인터 선언과 주소 대입을 동시에 함.

      int *p1 = &a;
    int *p1;    // 포인터 선언만 먼저
    p1 = &a;    // 주소 대입 따로

     

    이렇게 해도 같음.

     

    <연습문제>

    포인터로 배열 순회하며 평균 구하기

    - 정수 5개를 배열에 입력받고 

    - 포인터 int *p를 사용해 배열을 순회하면서 

    - 총합을 구하고 

    - 평균을 출력하라 (소수점 첫째 짜리까지)

    ** 반드시 포인터를 이용해 순회하라 - 배열 인덱스 arr[i]을 사용하지 않아야 함!

     

    <정답>

    #include <stdio.h>
    
    int main(void) {
    	int arr[5];
    	int *p = arr;
    	int sum = 0;
    
    	printf("정수 5개를 입력하세요: ");
    	for (int i = 0; i < 5; i++)
    		scanf("%d", &arr[i]);
    
    	for (int i = 0; i < 5; i++) {
    		sum += *(p + i); //arr[i]와 같음, 배열을 i로 순회하며 모두 더함
    	}
    	
    	printf("총합: %d\n", sum);
    	printf("평균: %.1f\n", sum / 5.0); //실수로 나눔
    
    	system("pause");
    	return 0;
    
    }

     

     

    - 함수에 포인터를 인자로 넘기기

    : 배열이든 변수든, 원본 데이터를 함수에서 바꾸고 싶을 때 사용함

     

    기본적으로 C에서 함수의 인자는 "값 복사", "값에 의한 전달 (Call bt Value)"

    void setZero(int x) {
        x = 0;
    }

    이건 main()에 있는 x를 바꾸는 게 아니라, 복사본만 바꿈.

    원본엔 영향 없음.

     

    하지만 포인터를 쓰면?

    void setZero(int *px) {
        *px = 0;
    }

    *px는 실제 x가 있는 주소에 접근해서 값을 변경, 즉 원본을 바꿀 수 있음.

    "참조에 의한 전달 (Call by Reference)"

     

    - 정리

    값에 의한 전달  복사본만 다룸
    참조에 의한 전달 주소를 넘겨 원본을 바꿈

     

     

    - 예제: 함수로 변수 바꾸기

    #include <stdio.h>
    
    void setZero(int *px) {
        *px = 0;
    }
    
    int main(void) {
        int a = 100;
    
        setZero(&a);   // 주소를 넘김
    
        printf("a = %d\n", a);  // 출력: 0
        return 0;
    }

     

    <연습문제>

    정수 하나를 입력받고 함수 void doubleValue(int *p)를 만들어서 그 값을 2배로 만들기

    main()에서 printf()로 변경된 값을 출력하기

     

    <정답>

    #include <stdio.h>
    
    void doubleValue(int *p) {
    	*p *= 2;
    }
    
    int main(void) {
    	int a;
    	int *v = &a;
    
    	printf("정수를 하나 입력하시오: ");
    	scanf("%d", &a);
    
    	//함수 호출: a의 주소를 보냄
    	doubleValue(v);
    
    	//a의 값을 출력
    	printf("변경된 값: %d\n", a);
    	system("pause");
    	return 0;
    
    }

     

    - 포인터 연산자와 연산 규칙

    포인터 연산자 종류: 사칙연산 중에서는 덧셈, 뺄셈 연산만 허용

    +) 증감연산자, 주소참조연산자, 대입연산자, 비교연산자 사용 가능

    유형 종류 활용 예시 (p: 포인터, v: 일반 변수)
    산술 연산자 +, -, ++, -- *p+1, *p-1, *p--, *++p, ++*p, --*p
    주소 참조 연산자 &, * int *p, p=&a
    대입 연산자 =, +=, -= p=&a, v += (*p)++
    비교 연산자 ==, != p==NULL, p!=NULL

     

    포인터 타입별 증감 연산 (*p++ 또는 *p--)

    : 기본적으로 포인터를 선언한 자료형의 크기만큼 더하거나 빼라는 의미, 증감값은 반드시 정수

    (자료형별 크기)

    char 1바이트
    short int 2바이트
    int 4바이트
    float 4바이트
    double 8바이트

     

    포인터 증감 연산자 활용 (반드시 구분해서 기억해야 함!)

    : 메모리 주소값에 대한 변화 VS 포인터가 가리키는 주소에 저장된 데이터값에 대한 변화?

    (주의) 나란히 있을 경우 ++의 우선순위가 *보다 높음

    하지만 ++이 p의 뒤에 선언되었을 경우 *p 수행 후 p++ 수행

    괄호 안에 있는 것을 먼저 수행

    종류 설명 값 변동 요인
    *p++ *p 수행 --> p++ 수행 (주소값 1 증가) 주소값
    (*p)++ *p 수행 --> ++ 수행 (데이터값 1 증가) 데이터값
    *++p p++ 수행 --> *p 수행 (주소값 1 증가한 데이터값 참조) 주소값, 데이터값
    ++*p ++ 수행 --> *p 수행 (데이터값 1 증가 후 그 데이터값 참조) 데이터값

     

    <예제>

    char 형 포인터에 증감 연산자 사용 (책 11-7)

    (C언어 일취월장 교재를 참고했습니다)

    #include <stdio.h>
    
    //예제 11-7: char형 포인터에 증감 연산자 사용
    int main(void) {
    	char c;
    	char *pc;
    	pc = &c;
    
    	int i;
    	int *pi;
    	pi = &i;
    
    	double d;
    	double *pd;
    	pd = &d;
    
    	printf("포인터 증감 연산자 사용 전 주소값\n");
    	printf("char형 포인터 주소값: %d\n", pc);
    	printf("int형 포인터 주소값: %d\n", pi);
    	printf("double형 포인터 주소값: %d\n", pd);
    
    	//*p++은 *p (값 - 데이터형 - 참조) 후 p++ (그만큼 주소값 증가) 이다.
    	*pc++; //1바이트 증가
    	*pi++; //4바이트 증가
    	*pd++; //8바이트 증가
    
    	printf("\n포인터 *p++ 연산자 수행 후 주소값\n");
    	printf("char형 포인터 주소값: %d\n", pc);
    	printf("int형 포인터 주소값: %d\n", pi);
    	printf("double형 포인터 주소값: %d\n", pd);
    	
    	system("pause");
    	return 0;
    	
    }

     

    - 문자열과 포인터

    문자열: 문자 배열이면서, 끝에 '\0'이 붙은 것

    char s[] = "hello";  
    // 실제로는: {'h', 'e', 'l', 'l', 'o', '\0'}

     

    문자열을 가리키는 포인터

    char *p = "hello";

     

    - "hello"는 문자열 상수

    - p는 그 문자열의 시작 주소를 가리킴

     

    선언 방법에 따른 차이

    char s[] = "hi"; 문자열 복사해서 배열에 저장 s는 내부에 복사된 데이터가 있고 수정 가능
    ex) s[0] = 'H';
    char *p = "hi"; 문자열 상수의 주소만 가짐 p는 상수 영역을 가리켜서 수정하면 오류 발생 가능

     

    "문자열은 포인터처럼 다룬다"

    (문자열 이름 = 시작 주소 = 포인터)

    즉, 배열 이름이 배열 시작 주소로 자동 변환됨 --> *, +, [i] 등 포인터 문법을 사용 가능 

    배열의 이름은 첫 번째 배열 요소를 가리키는 포인터와 의미가 같다!

    char name[] = "hi";  // == {'h', 'i', '\0'}

     

    name == &name[0], 포인터랑 똑같이 동작!

     

    - 문자열에 접근하기 위한 표현

    name[0] 'h'
    *name 'h'
    name[1] 'e'
    *(name + 1) 'e'
    printf("%s", name); "hello"
    printf("%s", name + 2); "llo"

     

    "포인터로 선언한 문자열도 배열처럼 사용할 수 있다"

    char *p = "hello";
    
    printf("%c", p[0]);      // 'h'
    printf("%c", *(p + 1));  // 'e'

    포인터인데 배열처럼 [] 를 사용할 수 있음

     

    <핵심정리>

    arr[i] 배열 문법 배열에서 기본
    *(arr+i) 포인터 문법 배열도 OK
    p[i] 포인터를 배열처럼 포인터도 OK
    *(p+i) 포인터 정석 문법 포인터 전용

    => 이것이 배열은 포인터처럼, 포인터를 배열처럼 다룰 수 있다는 의미!

     

    <예시>

    char arr[] = "abc";
    char *p = arr;
    
    // 이 네 개는 모두 같은 문자 'b'를 출력함!
    printf("%c\n", arr[1]);     // 배열 문법
    printf("%c\n", *(arr + 1)); // 포인터 문법
    printf("%c\n", p[1]);       // 포인터를 배열처럼
    printf("%c\n", *(p + 1));   // 정석 포인터

     

    <확인 문제>

    아래 각 줄의 출력 결과를 예측하시오.

    #include <stdio.h>
    
    int main(void) {
        char str[] = "world";
        char *p = str;
    
        printf("1. %c\n", str[3]);     // ?
        printf("2. %c\n", *(str + 3)); // ?
        printf("3. %c\n", p[3]);       // ?
        printf("4. %c\n", *(p + 3));   // ?
        printf("5. %s\n", p + 2);      // ?
        return 0;
    }

     

    <정답>

    1. l

    2. l

    3. l

    4. l

    5. rld

    (p는 "world"의 시작 주소 'w', p+2는 'r'부터 시작하는 주소, %s는 그 주소부터 끝까지 출력하니까 "rld"가 나옴)

     

    <더 자세히>

       주소     값
    +--------+------+
    |  p     | 0x100|  ← p는 문자열 시작 주소
    +--------+------+
    
      메모리:
      0x100 → 'w'
      0x101 → 'o'
      0x102 → 'r'
      0x103 → 'l'
      0x104 → 'd'
      0x105 → '\0'

     

    *p
    p 주소

     

    *p는 'w'

    *p     == 'w'        // 첫 글자
    *(p+1) == 'o'        // 두 번째 글자
    *(p+2) == 'r'        // 세 번째 글자

     

    p는 "world" 문자열의 시작 주소

    p+1은 "o"부터 시작하는 다음 주소

    printf("%s\n", p);     // world
    printf("%s\n", p + 1); // orld
    printf("%s\n", p + 2); // rld

     

    p+1 "orld" 시작 주소
    *(p+1) 두 번째 글자 'o'
    %s 주소에서 '\0'이 나올 때까지 문자열을 출력함
    (= 그 주소부터 문자열 끝까지 출력해줌)

     

    -  문자열 수정

    어떻게 선언했는가에 따라 다름!

    배열로 선언한 경우: 수정 가능

    char s[] = "hello";
    s[0] = 'H';      // 가능!

     

    포인터로 선언한 문자열: 수정 금지 (오류)

    char *p = "hello";
    p[0] = 'H';      // 오류 or 실행 중 에러 (Segmentation Fault)

    why? "hello"는 상수 영역(읽기전용)에 저장됨.

    *p는 그걸 가리키기만 하기 때문에 수정할 경우 에러 발생.

     

    - 문자열 복사, 수정 함수 (배열에 저장된 문자열 대상으로 동작)

    #include <string.h>
    
    char s[20];
    strcpy(s, "apple");
    strcat(s, " pie");

     

    <예제> 

    포인터에 배열 대입 후 증감연산 

    (책 11-9) << 오류 발견. 수정 예정입니다

    #include <stdio.h>
    
    //예제 11-9: 포인터에 배열 대입 후 증감 연산
    int main(void) {
    	int a[] = { 100, 200, 300 };
    	int *pa;
    
    	pa = a; //중요!! 배열을 대입할 때는 & 를 사용하지 않음.
    
    	printf("int a[] = {100, 200, 300}일 때 포인터 증감 연산\n");
    
    	printf("\n포인터 *++pa의 증감 연산을 수행한 주소값과 데이터값\n");
    	printf("포인터 현재 주소값: %d\n", pa);
    	printf("현재 포인터 주소의 데이터값: %d\n", *a);
    	printf("포인터 *++pa 수행 후 데이터값: %d\n", *++pa);
    	printf("포인터 *++pa 수행 후 주소값: %d\n", pa);
    
    	printf("\n포인터 *--pa의 증감 연산을 수행한 주소값과 데이터값\n");
    	printf("포인터 현재 주소값: %d\n", pa);
    	printf("포인터 *--pa 수행 후 데이터값: %d\n", *--pa);
    	printf("포인터 *--pa 수행 후 주소값: %d\n", pa);
    
    	printf("\n포인터 ++*pa의 증감 연산을 수행한 주소값과 데이터값\n");
    	printf("포인터 현재 주소값: %d\n", pa);
    	printf("포인터 ++*pa 수행 후 데이터값: %d\n", ++*pa);
    	printf("포인터 ++*pa 수행 후 주소값: %d\n", pa);
    	
    	system("pause");
    	return 0;
    	
    }

    *++pa 수행 후 주소값 4바이트 증가

    *--pa 수행 후 주소값 4바이트 감소

    ++*pa 수행 후 주소값 변동 없음 (데이터값에 대한 연산이기 때문)

     

    <예제>

    포인터와 배열을 사용하여 배열의 주소값과 포인터에 증감 연산을 수행, 해당 배열 요소의 값을 출력하기

    (책 11-10)

    #include <stdio.h>
    #define R 5 //매크로 상수 선언
    // R이라는 이름을 5로 대체하라 (유지보수 용이)
    
    int main(void) {
    	int a[] = { 100, 200, 300, 400, 500 };
    	int cnt;
    
    	//배열 요소의 주소값 출력
    	for (cnt = 0; cnt < R; cnt++)
    		printf(" %d번째 배열 요소 a[%d] = &a[%d]\n", cnt + 1, cnt, &a[cnt]);
    	
    	//주소값과 배열 요소의 값 출력
    	// 이때 a는 정수가 아니라 주소이므로, %u 를 사용함
    	// unsigned int (부호 없는 정수), 하지만 %p를 사용하는 것이 현재 표준
    	printf("\n배열의 현재 주소값: a는 %u\n\n", a);
    	printf("배열의 첫 번째 요소 값: *a는 %u\n\n", *a);
    
    	printf("증감 연산 후 주소값: a+1은 %u\n\n", a + 1);
    	printf("배열의 첫 번째 요소 값: *(a+1)은 %u\n\n", *(a+1));
    
    	system("pause");
    	return 0;
    }

     

    <예제>

    포인터를 사용하여 배열 요소의 합계를 산출하기

    (책 11-11)

    #include <stdio.h>
    #define R 5 
    
    //예제 11-11: 포인터로 배열 요소 합계 산출
    
    int main(void) {
    	int a[R] = { 100, 200, 300, 400, 500 };
    	int* pa, cnt, hap = 0;
    
    	pa = a; //배열이므로 & 사용 X
    
    	for (cnt = 0; cnt < R; cnt++) {
    		hap += *pa++; //값의 자료형 참조 후 그만큼 주소값 증가(=다음 요소로 이동하며 요소 누적 합을 구함)
    		printf("%d번째 배열 요소: %d\n", cnt + 1, a[cnt]);
    		printf("포인터 주소: %d\n", pa);
    	}
    
    	printf("\n%d개의 배열 요소 합계: %d\n", cnt, hap);
    
    
    	system("pause");
    	return 0;
    }

     

     


    여기까지가 포인터 기초 입니다. 

    다음 편에서 포인터 활용을 다루고,

    중간고사 이후에 문자와 문자열, 구조체, 동적 메모리 할당 등의 개념을 기초부터 다루겠습니다.

     

     

    댓글