c

포인터

haniru 2025. 11. 6. 23:27

1. gets 함수 - gets 함수는 글자를 무제한으로 받기 때문에 fgets를 쓰는게 좋다

(배열이 할당된 메모리 공간을 넘어서 다른 메모리 영역을 덮어쓰게 되는 버퍼오버플로우가 발생할 수 있다)

 

2. 포인터

10진수 주소와 16진수 주소를 나타냄

 

pa는 a의 주소값을 저장하고, a를 가리킨다. *pa가 가리키는 메모리 공간의 값(pa)을 10으로 변경해서 a의 값과 *pa (a가 가리키는 값) 둘다 10이 됐다.

 

 

포인터로 두 정수의 합과 평균을 구하는 코

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main(void) {
	// 1. 변수 선언 및 초기화
	int a = 10, b = 15, total; // 정수형 변수 선언 및 초기값 할당
	double avg;                // 실수형 변수 선언

	// 2. 포인터 선언
	int* pa, * pb;             // 정수형 변수를 가리키는 포인터 pa, pb 선언

	// 포인터 pt에 total 변수의 메모리 주소(&total)를 저장
	// pt가 가진 값(&total)과 total의 주소는 같지만,
	// pt 포인터 변수 자체가 저장된 주소(&pt)는 total의 주소(&total)와 다름.
	int* pt = &total;

	// 포인터 pg에 avg 변수의 메모리 주소(&avg)를 저장
	// pg가 가진 값(&avg)과 avg의 주소는 같지만,
	// pg 포인터 변수 자체가 저장된 주소(&pg)는 avg의 주소(&avg)와 다름.
	double* pg = &avg;

	// 3. 포인터에 주소 할당
	pa = &a; // pa에 변수 a의 메모리 주소를 저장 (pa는 a를 가리킨다)
	pb = &b; // pb에 변수 b의 메모리 주소를 저장 (pb는 b를 가리킨다)

	// 4. 포인터를 이용한 값 연산 및 저장
	// *pa: a의 값 (10), *pb: b의 값 (15)을 가져와 더함
	// 그 결과(25)를 *pt가 가리키는 total 변수에 저장
	*pt = *pa + *pb;

	// *pt: total의 값 (25)을 가져옴. 2.0으로 나누어 실수 연산 수행
	// 그 결과(12.5)를 *pg가 가리키는 avg 변수에 저장
	*pg = *pt / 2.0;

	// 5. 포인터를 통한 값 출력 (간접 접근)
	printf("두 정수의 값 : %d, %d\\n", *pa, *pb);
	printf("두 정수의 합 : %d\\n", *pt);     // *pt는 total 변수의 현재 값(25)을 출력
	printf("두 정수의 평균 : %.1f\\n", *pg);  // *pg는 avg 변수의 현재 값(12.5)을 출력

	// 6. 일반 변수를 통한 값 출력 (직접 접근)
	// 포인터를 사용한 연산 결과가 변수에 잘 반영되었는지 확인
	printf("두 정수의 합 : %d\\n", total);
	printf("두 정수의 평균 : %.1f\\n", avg);

	return 0;
}

 

포인터 연산을 활용1 - ary[i] 대신 *(ary + i) 처럼 포인터 연산을 사용할 수 있다

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main(void) {
	int ary[3];
	int i;

	*(ary + 0) = 10; // ary[0]
	*(ary + 1) = *(ary + 0) + 10; // ary[1]

	printf("세 번째 배열 요소에 키보드 입력 : ");
	scanf("%d", ary + 2); // ary[2] 에다가 키보드를 입력

	for (i = 0; i < 3; i++) {
		printf("%5d", *(ary + i));
	}

	return 0;
}

 

포인터 연산을 활용2 - 역참조 연산( *(a + i) ), 배열 인덱스 연산 (a[i]), 배열 이름 사용 (ary[i]) 이 세가지 방식으로 모두 동일한 배열 요소에 접근할 수 있다.

#include <stdlib.h>
#include <stdio.h>

int main()
{
    int ary[3];
    int* a = ary;

    *a = 10;
    *(a + 1) = 20; // ary[1]
    a[2] = a[0] + a[1]; // ary[0] + ary[1] , *(a + 0) + *(a + 1)

    for (int i = 0; i < 3; i++) {
        printf("%-5d", a[i]);
    }
    printf("\\n");

    return 0;
}

 

포인터 연산 활용2 에서 조사식을 보면 ary[1], a[1], *(a+1) 값이 다 같은걸 볼 수 있다.

 

포인터의 산술연산 - 가리키는 자료형의 크기를 고려하여 요소 단위로 이동한다

포인터 뺄셈 규칙: 동일한 자료형을 가리키는 두 포인터끼리만 뺄셈이 가능하고, 뺄셈 결과는 두 포인터가 가리키는 메모리 주소간의 차이를 그들이 가리키는 자료형의 크기 단위로 나눈 값이다

pa 는 배열의 첫번째 요소(ary[0])을 가리키는데, pa + 3 으로 첫 번째 요소의 주소에서 int 자료형 크기(4바이트) 만큼 3번 이동한 것이다. 따라서 pb는 ary[3]의 주소를 저장한다

pa++ 로 pa 주소값을 int 자료형 크기(4바이트) 만큼 증가시킨다. 따라서 pa는 ary[1]의 주소로 이동한다.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int ary[5] = { 10, 20, 30, 40, 50 };
	int* pa = ary;
	int* pb = pa + 3;

	printf("pa : %u\\n", pa);
	printf("pb : %u\\n", pb);

	pa++;
	printf("pa : %u\\n", pa);

	printf("pb - pa : %u\\n", pb - pa); // 포인터 변수끼리의 산술 연산

	printf("앞에 있는 배열 요소의 값 출력 : ");
	if (pa < pb)
		printf("%d\\n", *pa);
	else printf("%d\\n", *pb);

    return 0;
}

 

배열의 함수 전달

함수에 배열을 전달할 때 호출하는 쪽에서는 배열의 이름만 넘겨도 되지만 호출받는 쪽(함수)에서는 배열의 주소를 받기 위해 매개변수를 포인터 변수(int* pa)로 선언해야 한다

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

void print_ary(int* pa);

int main() {
	int ary[5] = { 10, 20, 30, 40, 50 };

	print_ary(ary); // 배열을 함수로 전달, 배열을 넘길 때는 포인터 변수로 전달한다

	return 0;
}

// 전달 받은 배열을 함수에서 출력
void print_ary(int* pa) {
	for (int i = 0; i < 5; i++) {
		printf("%d ", pa[i]);
	}
}

 

배열 최댓값 찾기

배열의 시작 주소를 포인터로 함수에 전달한다.

주석처럼 int pa[]로 선언해도 컴파일러는 내부적으로 int* pa로 처리한다고 한다.

INT_MIN을 사용하면 가장 작은 값을 얻을 수 있다

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <limits.h>

void print_max(int* a); // void print_max(int pa[])

int main() {
	int array[7] = { 4, 5, 8, 1, 2, 3, 7 };

	print_max(array);

	return 0;
}

void print_max(int* a) { // void print_max(int pa[])
	int max = INT_MIN; // 변수의 Scope
	for (int i = 0; i < 7; i++) {
		if (max < a[i]) {
			max = a[i];
		}
	}

	printf("가장 큰 값: %d\\n", max);
}

 

다양한 크기의 배열 출력

배열의 주소와 그 크기를 인수로 받아 전달받은 크기만큼만 정확하게 요소를 순회하며 출력한다 (유연성, 재사용성)

C 함수가 배열의 크기를 자동으로 알 수 없어 (배열 이름은 주소만 전달하고 크기 정보는 소실된다고 한다) 크기 정보를 명시적으로 함께 전달해야 한다

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <limits.h>

void print_ary(int* pa, int size); // void print_max(int pa[])

int main() {
	int ary1[5] = { 10, 20, 30, 40, 50 };
	int ary2[7] = { 10,20,30,40,50,60,70 };

	print_ary(ary1, 5);
	printf("\\n");
	print_ary(ary2, 7);

	return 0;
}

void print_ary(int* pa, int size) { // void print_max(int pa[])
	for (int i = 0; i < size; i++) {
		printf("%d ", pa[i]);
	}
}

 

최대값 구하기

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <limits.h>

// 함수 선언
void input_ary(double *pa, int size);
double find_max(double *pa, int size);

int main() {
    double ary[5];
    double max;
    int size = sizeof(ary) / sizeof(ary[0]);  // 배열 길이 계산 (call-by-value로 전달될 예정)

    // ary의 시작 주소가 함수에 전달됨 → call-by-reference
    input_ary(ary, size);  
    
    // ary 주소를 참조하여 최댓값 계산 → call-by-reference
    max = find_max(ary, size);
    
    printf("배열의 최댓값 : %.1lf\\n", max);

    return 0;  // 프로그램 종료 → program counter가 OS로 돌아감
}

// ----------------------------------------

void input_ary(double* pa, int size) {
    // pa는 ary의 시작 주소를 가리킴 (call-by-reference)
    printf("값을 입력해주세요: ");
    
    // 전수조사(반복문): 배열 전체에 대해 입력 수행
    for (int i = 0; i < size; i++) {
        // &pa[i]는 pa가 가리키는 배열의 i번째 원소 주소
        scanf("%lf", &pa[i]);
    }
    // 함수 종료 시 스택 프레임이 pop됨 → context switch(함수 레벨)
}

// ----------------------------------------

double find_max(double* pa, int size) {
    double max = INT_MIN;  // 초기 최솟값 설정
    
    // 전수조사(반복문): 모든 요소를 확인하여 최댓값 찾음
    for (int i = 0; i < size; i++) {
        if (pa[i] > max) {
            max = pa[i];  // call-by-reference 덕분에 실제 배열값 참조 가능
        }
    }

    // max를 call-by-value로 main에 반환
    return max;
}

 

대소문자 구하기

아스키 코드값을 알고있지 않아도 'a' 값과 'A' 값의 차(32)로 대소문자 변환이 가능하다

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main() {
	char small;
	char cap = 'G';

	if ((cap >= 'A') && (cap <= 'Z')) {
		small = cap + ('a' - 'A');
	}

	printf("대문자: %c\\n", cap);
	printf("소문자: %c\\n", small);

	return 0;
}

 

인코딩 방식은 다음과 같이 있다

문자 인코딩 방식의 특징 보충 설명

인코딩 특징 바이트 크기 (Byte) 한국어 환경에서의 역할
아스키코드 (ASCII) 영문, 숫자, 기호 등 **7비트(Bit)**를 사용하는 가장 기본적인 표준. 영어 1바이트 모든 현대 인코딩의 기반이 됩니다.
유니코드 (Unicode) 전 세계의 모든 문자를 하나의 표준 번호(코드 포인트)로 정의. 코드 포인트 정의 전 세계 언어를 통일적으로 처리하는 표준입니다.
UTF-8 유니코드를 저장하는 가변 길이 인코딩 방식. 영어 1바이트, 한글 3바이트 웹과 리눅스 등 대부분의 현대 시스템에서 표준으로 사용됩니다.
UTF-16 유니코드를 저장하는 인코딩 방식. 영어 2바이트, 한글 2바이트 주로 Windows 환경이나 자바(Java) 등 일부 애플리케이션의 내부 처리에서 사용됩니다.
EUC-KR (CP949) 한글 인코딩 표준이었으나, MS에 의해 확장됨. 영어 1바이트, 한글 2바이트 과거 한국어 Windows 시스템과 MS Office(엑셀 등)에서 사용되던 CP949 인코딩의 기반이 되었습니다.

 

대문자, 소문자, 숫자, 특수문자 갯수 세기

문자열을 한글자씩 반복해서 검사하고, 각 문자가 어떤 종류에 속하는지 아스키 코드 범위를 이용하여 분리한다

strlen 안 쓰고 null 문자가 나타날 때까지 for문 돌려서 갯수 세는 방법도 있다고 한다

fgets 대신 scanf 쓰면 개행이 있는지 체크를 따로 안해도 된

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>

int main() {
	char str[80];

	int small = 0;
	int cap = 0;
	int number = 0;
	int special = 0;

	printf("문자를 입력해 주세요: ", str);

	fgets(str, 80, stdin);

	printf("총 길이 : %d ", strlen(str));

	for (int i = 0; i < strlen(str); i++) {
		char s = str[i];

		if (s >= 'a' && s <= 'z') {
			small++;
		}
		else if (s >= 'A' && s <= 'Z') {
			cap++;
		}
		else if (s >= '0' && s <= '9') {
			number++;
		}
		else if (s != '\\n') {
			printf("특수문자가 찍히는 문자 : %c..", s);
			special++;
		}
	}

	printf("알파벳 대문자: %d\\n", cap);
	printf("알파벳 소문자: %d\\n", small);
	printf("숫자: %d\\n", number);
	printf("특수문자: %d\\n", special);

	return 0;
}

 

더보기

fgets와 scanf의 개행 문자 처리 차이

fgets 대신 scanf 쓰면 개행이 있는지 체크를 따로 안 해도 되는듯

이것은 fgets와 scanf가 사용자 입력의 줄 바꿈(\n) 문자를 처리하는 방식이 다르기 때문에 발생하는 현상입니다.

📝 차이점 설명

함수 개행 문자 (\n) 처리 방식 문자열 포함 여부 직전 코드에 미치는 영향
fgets 줄 바꿈 문자(\n)를 문자열의 일부로 함께 읽어와 저장합니다. 포함 마지막 \n을 특수문자 카운트에서 제외(if (s != '\n'))해야 합니다.
scanf scanf("%s", str) 형식으로 사용할 경우, 줄 바꿈 문자(\n)를 읽기 전에 멈추고 버립니다. 불포함 문자열 끝에 \n이 없으므로, 특수문자 카운트에서 $\textbf{따로 제외할 필요가 없습니다.}$ (fgets의 단점을 해결)

💡 scanf 사용 시 주의점

scanf를 사용할 경우 \n 처리에 대한 걱정은 줄어들지만, 다음과 같은 치명적인 단점이 있습니다.

  1. 공백 문자 처리 불가: scanf("%s", str)는 **공백(Space)**을 만나면 그 앞까지만 읽고 멈춥니다. 따라서 "Hello World"를 입력하면 str에는 "Hello"만 저장됩니다.
  2. 버퍼 오버플로우 위험: scanf는 문자열의 최대 길이를 지정하는 안전 장치가 없어, 사용자가 배열 크기보다 긴 문자열을 입력하면 버퍼 오버플로우가 발생하여 보안 문제가 생길 수 있습니다.

결론: 문자열에 공백이 포함될 가능성이 있다면 fgets가 더 안전하고 적합하며, fgets의 특징인 \n 제거 처리(str[strlen(str) - 1] = '\0';와 같은 코드)만 추가해주면 됩니다.

 

포인터의 순차적 접근

반복문을 돌면서 포인터 pa 가체를 증가시켜 메모리 주소를 이동시키고, 이를 통해 배열의 모든 요소를 차례대로 읽어낸다

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
    int ary[3] = { 10, 20, 30 };
    int* pa = ary;
    int i;
    
    printf("배열의 값 : ");
    
    for (i = 0; i < 3; i++) {
        printf("%d ", *pa);
        pa++;
    }
    
    return 0;
}

'c' 카테고리의 다른 글

이중포인터 N차원 배열  (1) 2025.11.10
문자열  (0) 2025.11.07
배열  (0) 2025.11.05
함수와 배열 일부  (0) 2025.11.04
반복문 (while, for)  (0) 2025.11.03