C언어 기본 보충

제 11장

포인터 기본 개념

포인터 : 메모리에 있는 데이터의 주소를 가지고 있는 변수

변수는 컴퓨터 메모리에 저장된다.

메모리의 단위는 바이트이다.

메모리는 각 바이트마다 고유한 주소를 가지고 있다.

프로그램에서 변수를 만들면 컴파일러에 의하여 메모리 공간의 비어있는 위치를 차지한다.

변수가 메모리에 저장될 때 변수의 크기에 따라서 차지하는 메모리 공간의 크기가 달라진다.

주소 연산자 &

C언어에는 변수의 주소를 계산하는 연산자 & int i; 라고 변수를 정의했으며 변수 i주소는 &i하면 알 수 있다.

int *p; // 정수형 포인터 변수 p 선언 int i = 10;

p = &i; 포인터 변수 p에 변수 i의 주소값을 넣었다. 이 때 포인터 변수 p가 변수 i를 가리킨다고 한다.

image

간접참조연산자 *

* 연산자는 단항연산자로서 포인터가 가리키는 위치의 내용을 추출하는 연산자이다.

만약 p가 변수 i를 가리킨다고 하면 *p는 변수 i와 같다.

* 포인터를 통하여 변수를 간접 참조할 때 사용

& 변수의 주소를 구하여 포인터에 대입할 때 사용

함수가 호출될 때 인수의 보사본이 함수로 전달되면 값에 의한 호출이다. 만약 함수가 호출될 때 인수들의 원본이 함수로 전달되면 참조에 의한 호출이다. C에서는 값에 의한 호출만을 지원한다. 참조에 의한 호출은 포인터를 이용앟여 간접 구현이 가능하다.

널 포인터 사용

포인터가 아무것도 가리키고 있지 않을 때는 NULL(0)으로 설정하는 것이 바람직하다. NULL은 stdio.h에 0으로 정의되어 있다.

포인터를 배열처럼 사용

배열 이름이 바로 포인터다. 배열 이름은 배열이 시작되는 주소와 같다.

배열 이름을 포인터라고 생각하고 *a를 출력해보면 첫 번째 원소 a[0]의 값이 출력됨을 알 수 있다. 또한 a+i는 &a[i]와 같다. 또한 *(a+i)는 a[i]와 완전히 동일하다. 배열의 이름은 포인터 상수이므로 그 값이 변경될 수 없다.

a[] = {・・・} int *p; p = a

여기서 우리는 포인터 p에 배열의 이름 a를 대입하면 배열의 첫 번째 주소가 p에 대입되는 것과 똑같다. 이 문장이 끝나면 p와 a는 똑같은 곳을 가리키게 된다.

① 배열의 인덱스를 사용하여 참조하면 컴파일러는 a가 1000번지부터 시작한다고 가정할 떄, a[2]의 주소는 1000+4*2와 같이 계산된다.

②포인터를 사용 -> 매번 반복 떄마다 배열의 원소주소를 다시 계산할 필요가 없다.

함수 호출

  • 값에 의한 호출 call by value -> 복사본 전달
  • 참조에 의한 호출 call by reference -> 원본 전달

배열 매개 변수

C에서 배열은 자동으로 참조를 이용하여 함수에 전달된다. 즉 배열을 함수의 인수로 주면 자동으로 배열의 주소만 전달된다. 함수에 선언된 배열 매개 변수는 포인터라고 생각할 수 있따.

문자열

NULL 문자는 아스키 코드 값이 0인 문자이다. 문쟈열은 반드시 NULL 문자로 끝난다.

문자

‘A’와 “A” A는 하나의 문자, 문자에 대한 아스키코드와 같다. “A”는 문자열 A의 아스키코드에 문자열 끝을 나타내는 NULL 문자 추가

문자열 상수 : 문자열 상수는 프로그램이 사용하는 메모리 영역 중에서 텍스트 세그먼트라고 불리는 특수한 메모리 영역에 저장된다.

텍스트 세그먼트 : 값을 읽기만 하고 변경할 수 없는 메모리 영역 데이터 세그먼트 : 값을 변경할 수 있는 메모리 영역

char *p = “HelloWorld”; p = “Goodbye”;

포인터 변수 p에 문자열 상수의 주소를 저장. 즉 포인터 변수 p가 문자열 상수 Goodbye를 가리키도록 변경.

문자열 입출력 라이브러리

int getchar(void) : 하나의 문자를 읽어서 반환 (함수 반환형이 int형인 이유는) 입력의 끝을 나타내는 EOF 문자를 체크하기 위해서이다.

void putchar(int c)변수 c에 저장된 문자를 출력한다. int getch(void) : 하나의 문자를 읽어서 반환한다(버퍼를 사용하지 않음) void putch(int c) : 변수 c에 저장된 문자를 출력한다 (버퍼를 사용하지 않음) scanf(“%c”, &c) : 하나의 문자를 읽어서 변수 c에 저장한다. printf(“%c”, c) : 변수 c에 저장된 문자를 출력한다.

구조체

프로그래머가 여러 개의 자료형들을 묶어서 새로운 자료형을 만들 수 있는 방법이다. 구조체 멤버 : 구조체에 포함되는 변수 구조체 선언은 변수 선언이 아니다. 구조체의 형태만 정의한 것. 즉 아직 데이터를 저장할 수는 없다.

구조체 멤버 참조

구조체 변수.멤버이름; // 구조체의 변수의 멤버를 참조한다. s1.number = 24; 만약 멤버가 문자열이라면 멤버에 값을 대입할 때 strcpy를 사용해야 한다,.

구조체를 멤버로 가지는 구조체

구조체를 다른 구조체에 대입하는 것이 가능하다. 따라서 개별 변수들을 사용하는 것보다 구조체가 편리한 이유. 그러나 구조체 변수와 구조체 변수를 서로 비교하는 것은 허용되지 않는다.

구조체 배열

구조체와 포인터

구조체에서 포인터가 사용되는 경우 ① 구조체를 가리키는 포인터 ② 포인터를 멤버로 가지는 구조체

① 구조체를 가리키는 포인터

구조체를 가리키는 포인터도 만들 수 있다.

struct student s = {20070001, "홍길동", 4.3};
struct student *p; // 구조체 student를 가리키는 포인터 선언

p = &s; // 구조체의 주소를 포인터에 대입

printf("학번 = %d 이름 = %s 학점 = %f \n", (*p).number, (*p).name, (*p).grade);

포인터를 이용하여 구조체의 멤버를 가리키는것은 자주 등장하기 때문에 이것을 위한 특수 연산자 ->가 있다. -> 연산자는 간접 멤버 연산자라고 불리는 것으로 구조체 포인터를 이용하여 멤버에 접근하기 위하여 사용된다. p->number; // (*p).number와 같다.

p->number의 의미는 포인터 p가 가리키는 구조체의 멤버 number라는 뜻이다.

image

② 포인터를 멤버로 가지는 구조체

struct date{
	int month;
    int day;
    int year;
}

struct student{
	int number;
    char name[20];
    double grade;
    struct date *dob; //포인터가 구조체의 멤버
}

int main(void){
	struct date d ={};  값 넣어서 초기화
    struct student = {}; 값 넣어서 초기화
    s.dob = &d; // 구조체 변수 s의 멤버인 포인터 dob에 구조체 d의 주소를 대입하였다.
}

자기 자신을 가리키는 포인터도 포함시킬 수 있다 -> 연결 리스트에서 사용

struct student{
	int number;
    char name[20];
    float height;
    struct student *next; // student 구조체를 가리키는 포인터 정의
}
linkedlist.c

struct student {
	int number;
    char name[10];
    double grade;
    struct student *next; 
} // 자기참조 구조체 정의

int main(void){
	struct student s1 = { 30, "Kim", 4.3, NULL};
    struct student s2 = { 31, "Park", 3.7, NULL}; // 구조체 s1과 s2 생성
    
    struct student *first = NULL;
    struct student *current = NULL;/ 구조체 student를 가리키는 포인터 선언
    
    first = &s1; // first는 s1 구조체를 가리킨다
    s1.next  = &s2; //s1 구조체의 next는 s2 구조체를 가리킨다.
    s2.next = NULL; // s2 구조체의 next에는 NULL을 넣어준다.
    
    current = first; //first에서 시작하여, 연결된 모든 구조체를 방문하면서 멤버의 값을 출력한다.
    while(current != NULL)
    {
    	printf("학생의 번호 = %d 이름 = %s, 성적 = %f \n", current->number, current->name, current->grade);
        current = current -> next;
    }
}

포인터 first는 연결 리스트에서 첫 번째 구조체의 주소를 저장하는 데 사용된다. 이 첫 번째 구조체의 주소가 있어야만 다른 연결된 구조체들을 찾아갈 수 있다. 두 개의 구조체를 선언하였으며 첫 번째 구조체의 주소가 first에 대입된다. 그런 다음 첫 번쨰 구조체의 next에 두 번째 구조체의 주소를 저장한다. 두 번쨰 구조체의 next에는 다음에 연결될 구조체가 없으므로 NULL을 대입한다. 이러한 구조가 연결 리스트이다. 포인터를 사용하여 데이터들을 서로 연결 고리로 만들어 놓은 구조가 연결 리스트이다.

연결 리스트에서는 첫 번쨰 구조체의 주소만 알면 다른 구조체들에 접근할 수 있다. 즉 first의 주소에 먼저 접근한 뒤 멤버 next를 찾아 다음 구조체의 주소를 얻는다. 여기서 또 next에 저장된 값을 이용하여 다음 구조체의 주소를 얻는다.이런 식으로 반복하다보면 원하는 주소를 찾을 수 있다. 맨 마지막 구조체의 next는 NULL이 된다.

포인터 current는 루프에서 사용되는 구조체 포인터로 처음에는 first로 설정된다. 루프는 첫 번째 구조체부터 차례대로 접근한다. 첫 번쨰 구조체부터 차례대로 구조체의 멤버값을 출력한다. 출력이 끝나면 next에 저장된 다음 구조체의 주소를 current에 대입하여 다음 구조체로 넘어간다. 이 while 루프는 current가 NULL이 될 떄까지 계속된다. current가 NULL이 되면 이전 구조체의 next 값이 NULL이었다는 것을 의미하고, 즉 이전 구조체가 마지막 구조체라는 것을 의미한다. 따라서 루프는 종료된다.

문자열 배열과 문자 포인터의 차이점

구조체 멤버로서 char name[10]; 가 있고 다른 구조체의 멤버로서 char *pname;가 있을 경우 각자 s,p로 구조체 변수를 선언하였을 때 구조체 변수 s의 경우 10 바이트의 공간이 구조체의 내부 공간에 할당된다. 구조체 변수 p의 경우 내부에는 포인터 pname을 위한 4바이트의 공간만 할당된다. 즉 문자열을 위한 공간은 구조체 변수 p에는 없다.

구조체와 함수

구조체가 인수나 반환값으로 사용될 때는 “값에 의한 호출” 원칙이 적용된다. 따라서 함수에는 구조체의 복사본이 인수로 전달되므로 함수 안에서 인수의 값이 변경되더라도 원본 구조체에 영향을 주지 않는다. 구조체의 크기가 클 경우 상당한 시간이 소요 되므로 구조체를 직접 전달하거나 반환하는 것 보다 구조체의 포인터를 사용하는 것이 적합하다. 하지만 원본 값이 훼손될 여지가 있다. 따라서 함수로 포인터가 전달되고 원본을 변경할 필요가 없으면 가급적 const를 적어주도록 하자.

ex)

int equal(struct student const *p1, struct student const *p2)
{
	if(p1 -> number == p2 -> number)
    	return 1;
    else 
    	return 0;
}

구조체를 함수의 반환값으로 넘길 경우 -> 얼마든지 가능. 반환값의 형을 구조체로 표시해주면 된다. 반환 값으로 구조체를 사용하면 한 번에 여러 개의 값을 반환 할 수 있다. 이 때도 물론 원본이 아닌 복사본이 전달.

typedef

말그대로 프로그래머가 새로운 자료형(type)을 정의(define)하는 것이다. 이 키워드는 C의 기본 자료형을 확장시키는 역할을 한다. 구조체로 새로운 자료형을 만들면 ` typedef struct point POINT; ` 이렇게 만든다면, struct point를 새로운 타입인 POINT로 정의하는 것이다. 그러면 struct를 앞에 붙일 필요 없이 POINT a,b;로 선언해줄 수 있다.

여기서 구조체의 선언과 typedef를 같이 사용할 수 있다.

typedef struct point{
	int x;
    iny y;
} POINT;

typedef의 장점

① 이식성을 높여준다 (자신의 코드를 컴퓨터 하드웨어에 독립적으로 만들 수 있다) ② #define과의 차이점 : typedef는 컴파일러가 직접 처리. #define은 문자열을 단순히 다른 문자열로 대치 ③ 문서화의 역할 : 주석을 붙이는 것과 같은 효과가 있다.

포인터 활용

이중 포인터

포인터 p의 주소를 다른 포인터 q에 넣으면. 포인터 q가 포인터 p를 가리키게 된다. 이것을 포인터의 포인터 또는 이중포이넡라고 한다.

정수형 포인터를 가리키는 이중 포인터는 다음과 같이 선언 int **q; // int형 포인터에 대한 이중 포인터 선언

이중포인터의 해석 방법 : 정수를 가리키는 포인터를 가리키는 포인터라는 의미로 해석하자.

int i = 100; // i 는 int형 변수
int *p = &i; // p는 i를 가리키는 포인터
int \**q = &p; // q는 포인터 p를 가리키는 이중 포인터

이중포인터가 많이 사용되는 상황은 외부에서 정의된 포인터 값을 함수의 인수로 받아서 변경하려고 하는 경우


void set_pointer(char **Q); //**
char *proverb = "All that glisters is not gold.";

int main(void)
{
	char *p = "zzz";
    set_pointer(&p); // 포인터 p의 주소를 전달한다.
    printf("%s \n", p);
    return 0;
}
void set_pointer(char **q) //** 이중 포인터 q를 통하여 외부의 포인터p를 변경한다.
{
	*q = proverb;
}

포인터 배열

포인터들을 모아서 배열 만든 것 즉 배열의 원소가 포인터다.

포인터 배열 중에서 가장 많이 사용되는 형태는 문자형 포인터 배열이다. 각 행들의 길이가 가변적으로 변할 수 있어서 레그드 배열 이라고도 불린다. (ragged array)

ex)

char *fruits[] = {
	"apple",
    "blueerry",
    "orange",
    "melon"
};

배열 포인터

배열포인터는 배열을 가리키는 포인터이다. int (* pa)[10]; 괄호가 있으므로 pa는 먼저 포인터가 되고, 어떤 포인터냐면 int [10]을 가리키는 포인터가 된다. 포인터의 배열을 주로 사용한다.

함수 포인터

함수 포인터의 배열

뒤의 내용 조금 생략

15장 전처리기 생략

16장 스트림과 파일 입출력 생략

17장 동적 메모리와 연결 리스트

정적 메모리 할당 : 프로그램이 시작되기 전에 미리 정해진 크기의 메모리를 할당 받는 것 동적 메모리 할당 : 프로그램이 실행 도중에 동적으로 메모리를 할당 받는 것을 말한다.

int *score; int i;

(1)동적 메모리 할당 : score = (int )malloc(100sizeof(int)); (2) 동적 메모리 사용 : for(i=0; i< 100; i++){score[i] = 0;} (3) 동적 메모리 반납 : free(score);

malloc()이 할당한 동적 메모리는 초괴화되어 있지 않다. 조금이라도 실행시간을 단축하기 위해서이다. 0으로 초기화된 동적 메모리를 원한다면 calloc()을 사용하면 된다.

calloc void *calloc(항목의 갯수, 항목의 크기); void *realloc(기존의 동적 메모리, 새로운 크기);

연결리스트

상자를 노드(node)라고 부른다. 연결 리스트는 이들 노드들의 집합니앋. 노드는 데이터 필드(data field)와 링크 필드(link field)로 구성 링크 필드는 다른 노드를 가리키는 포인터가 저장된다. 이 포인터를 이용하여 다음 노드로 건너갈 수 있다. 연결 리스트에서는 연결 리스트의 첫 번째 노드를 가리키고 있는 변수가 필요한데 이것을 헤드 포인터라고 한다. 연결리스트의 이름은 바로 이 헤드 포인터의 이름과 같다고 생각하면 된다.

즉 첫 번쨰 노드를 가리키는 포인터를 헤드 포인터라고 하고 맨 마지막의 노드의 링크필드는 NULL이다.

자기참조 구조체

 typedef struct NODE {
 	int data;
	struct NODE *link; -> 현재 구조체를 가리킬 수 있는 포인터
} NODE;
	
NODE *p1;
p1 = (NODE*) malloc(sizeof(NODE));
p1 ->data = 10;
p1 -> link = NULL;

NOde *p2;
p2 = (NODE*)malloc(sizeof(NODE));
p2->data = 20;
p2->link = NULL;
p1->link = p2;

free(p1);
free(p2);

연결리스트의 연산

연결리스트가 가지고 있는 모든 노드를 한 번씩 차례대로 방문하는 연산을 순회(traversal)라고 한다. 연결 리스트가 가지고 있는 데이터를 출력할 때, 노드의 개수를 셀 때, 노드가 가진 데이터들의 합계를 계산할 때 모두 순회 연산을 사용한다

연결 리스트를 순회하려면 먼저 노드를 가리키는 포인터 하나가 필요하다. 이 포인터는 처음에는 연결 리스트의 첫 번째 노드를 가리킨다. 첫 번째 노드를 방문하고 난 후에 이 포인터를 첫 번째 노드의 링크를 따라 이동시켜서 두 번째 노드를 가리키게 한다 두 번째 노드를 방문하고 난 후에 이 포인터를 두 번쨰 노드의 링크를 따라 이동시켜서 세 번째 노드를 가리키게 한다. 이런 방식으로 연결 리스트의 마지막 노드까지 가서 포인터의 값이 NULL이 되면 종료하면 된다.

리스트의 데이터값 출력

print_list()

void print_list(NODE *plist)
{
	NODE *p;
    
    p = plist;
    
    printf("(");
    
    while(p) // while p는 while(p!=NULL)이나 마찬가지이다.
    {
    	printf("%d", p->data);
        p = p->link; // p를 연결리스트를 따라서 앞으로 한 노드 전진하도록 하는 문장이다. 
    }
    printf(")\n");
}

p->link에는 다음 노드를 가리키는 포인터가 들어 있고 따라서 이것을 p에 대입하면 p가 다음 노드를 가리키게 된다. 만약 이것을 생략한다면 p가 변경되지 않아서 무한 반복이 된다. 연결 리스트의 끝에 도달하면 마지막 노드의 link 값이 NULL이므로 p에는 NULL이 대입 되고 따라서 while(p)문장에서 p가 0이 되어서 반복이 끝나게 된다.

노드의 개수 세기

이번에는 단순히 반복문 안에서 노드의 개수를 나타내는 변수인 length만을 증가시킨다.
void get_length(NODE *plist)
{
	NODE *p;
    int length = 0;
    
    p = plist;
    
     
    while(p) 
    {
    	length++;
        p = p->link;  
    }
    printf("리스트의 길이는 %d\n",length);
    return length;
}

합계 구하기

노드가 정수값을 저장하고 있다고 가정하고 순회 연산을 이용하여 연결 리스트 안에 저장되어 있는 정수들의 합을 계산하는 함수 작성

int get_sum(NODE *plist)
{
	NODE *p;
    int sum = 0;
    
    p = plist;
    
     
    while(p) 
    {
    	sum += p->datal
        p = p->link;  
    }
    printf("리스트의 길이는 %d\n",sum;
    return sum;
}

참고자료

프로그래밍 프로젝트

Gunny 더블링크드 리스트 구현

0%