CPU와 main memory의 발전으로, 이 책에 있는 예제들에 비해 실제 프로그램은 훨씬 크다. 10만줄 넘어가는 것도 흔하고, 더 큰 프로그램들도 있다.(이 책 쓰인 연도를 생각하면 지금은 더...)
C가 큰 프로그램을 만들기 위해 design되진 않았지만, 많은 큰 프로그램들이 C로 쓰였다.
이런 큰 프로그램을 만든다는건 작은 프로그램을 작성할때와는 확실히 다르다.
style에 더 집중해야하는데, 그 이유는 많은 사람들이 함께 작업하기 때문이다. documentation과 유지보수에 대해서도 계획해야한다.
chapter 15에서 큰 프로그램을 만드는 것의 language detail에 집중했다면, 이 챕터에선 좋은 program design에 집중할 것이다. (완벽하게 cover할 순 없겠지만 짧게나마 중요한 부분을 cover..)
C program을 desing할때, 이를 여러 독립된 modules로 보는 것이 유용하다.
module은 service의 집합이다.
clients가 이 modules을 사용할 수 있다.
module은 각 service의 기능을 설명하는 interface를 가지고 있다.
module의 detail(services의 source code같은 것)은 module의 implementation에 저장된다.
C에서, service는 function을,
interface는 header file을,
implementation은 (module's service의 definition을 포함하는..) source file을 나타낸다.
client는 프로그램 내의 다른 part(다른 source file)를 나타낸다.
C library는 바로 이 modules의 집합이다.
각 header file은 해당 module의 interface를 나타낸다.
프로그램을 모듈로 나눴을때 장점
1. Abstraction
: 잘 design됐다면, abstraction으로 대할 수 있다. 즉, 그 module이 어떻게 작동하는지 detail을 몰라도, 무엇을 하는지를 알고있다면 써먹을 수 있다.
팀이 하나의 프로그램을 만들때도 이를 이용하면 쉽다. 각각 본인이 맡은 implementation을 만들고, 서로의 모듈을 합의된 interface에 의해 사용하기만 하면 되기때문에 독립적인 작업을 통해 하나의 큰 프로그램을 만들 수 있다.
2. Reusability
: 한번 만들어진 모듈은 다른 프로그램에서 재사용하기 좋다.
3. Maintainability
: (비교적) 수정하기 간단하다. 수정하려는 부분이 주로 한 모듈의 implementation에 해당할테니 그 부분만 수정하고 recompile해서 relink하면 된다.
real world program들은 서비스 기간이 길기 때문에 이 특징이 가장 중요하다고 볼 수 있다. (bug fixing, enhancement, etc...)
이렇게 modular design의 중요성을 알았으면 이제 간단하게 알아볼 차례..
이거 말고도 더 공부하고 싶으면
Fundamentals of Software Enginieering, 2nd edition(Ghezzi, Jazayerim Mandrioli) 같은
software engineering 교재를 보면 됨
잘 design된 모듈은 아래 두가지 특징을 가지고 있다.
high cohesion과 low coupling을 위해, modules은 주로 아래 카테고리로 분류된다.
Data pool
: 관련있는 variables이나 constants의 집합이다.(주로 just header file)
디자인 관점에서 좋은 아이디어는 아니지만, 유용할 때가 있다.
C library의 예시로 <float.h>
, <limits.h>
가 있다.
Library
: 관련있는 functions의 집합이다.
예를들어 <string.h>
는 library of string handling functions의 interface이다.
Abstract object
: hidden data structure의 작업을 수행하는 functions의 집합이다.
(C 용어로 object는 "a block of memory that can store a value" 이지만, 여기서는 object를 다른 의미로 사용하겠다, "object is a collection of data bundled with operations on the data". 만약 data가 숨겨져있다면(hidden) object는 abstract이다.)
이 전에 다룬 stack module이 absract object이다.
Abstract data type(ADT)
: representation이 숨겨진 type이다.
clients가 해당 type의 variable을 선언할 순 있지만 그 구조에 대해선 알 지 못한다.
그리고 그 variable에 operation을 수행하려면 해당 ADT module이 제공하는 함수를 무조건 호출해야 한다.
ADT는 modern programming에서 중요한 역할을 한다.
3번이랑 4번 둘이 차이가 뭐지?
: 3번은 하나의 object(위에서 말한 다른 의미의 object)에 불과하고, 4번은 말 그대로 여러 instances를 만들 수 있는 type이다. 이 점이 다르다.
잘 design된 module은 주로 정보를 clients로부터 숨긴다.
stack module을 쓰는데있어서 clients가 stack을 array에 저장하는지~ structure에 저장하는지~ linked list에 저장하는지~ 알 필요가 없기 때문이다.
이렇게 의도적으로 cilents에게 information을 숨기는 것을 information hiding이라고 한다.
이는 두가지 장점을 가지고 있다.
Security
: clients가 내부 작업을 망칠 수 없게한다. stack module에 operation을 진행하려면 해당 모듈의 함수를 사용해야만 한다.
Flexibility
: module 내부의 작업을 수정하는 것이 그리 어렵지 않다.
static
storage class가 information hiding에 주로 쓰인다.
static
을 사용해서 선언한다.
#define PUBLIC /*empty*/
#define PRIVATE static
이렇게 macro를 선언해서 의미를 알려주기도 한다.
abstract object인 module은 심각한 단점이 있다. 바로 object의 multiple instances를 만들지 못하는 것이다. (이전 stack 예시에서 하나 이상의 stack을 만들지 못함)
그러기 위해서 우리는 새로운 type을 만들어야 한다.
(각 type은 "stack 본체"와 "top"값 두개 다 가지고 있어야 하니 structure로 묶어버리면 좋다.)
Stack
type을 정의해서 "사용하는 예시")
Stack s1, s2; //이렇게 instances를 만들어 내도록 해야 함
make_empty(&s1);
make_empty(&s2);
push(&s1, 1);
push(&s2, 2);
pop(&s1);
s1, s2가 무엇인진 중요하지 않다(structure? pointer?).
clients에게 s1과 s2는 특정 operation(ex. make_empty)에 반응하는 abstraction일 뿐이다.
이런 기능을 수행하기위해 header file(interface)을 바꿔보자..
각 함수는 parameter로 Stack
(or Stack *
)을 입력받아야 한다.(그래야 그 instance의 작업을 수행할 수 있으니까)
#define STACK_SIZE 100
typedef struct {
int contents[STACK_SIZE];
int top;
} Stack; //그냥 간단하게 일단 구조체로 구현
void make_empty(Stack *s);
bool is_empty(const Stack *s);
bool is_full(const Stack *s);
void push(Stack *s, int i);
int pop(Stack *s);
make_empty, push, pop은 stack을 수정해야하기 때문에 포인터로 입력받았다.
is_empty, is_full은 포인터로 입력받지 않아도 되지만, 실행 속도 향상을 위해 그냥 포인터로 입력받았다.(그냥 structure로 받으면 내용이 전부 copy되기 때문에 오래걸림)
바로 위에서 선언한 stack도 Abstract Data Type은 아니다.
stack.h
에서 Stack
type이 무엇인지 알 수 있고, 이를 clients에서 직접 접근해 사용하더라도 문제가 없기 때문이다.
Stack s1;
s1.top = 0;
//해당 type의 함수가 아닌 직접 접근해서 사용..
//이런 식으로 쓰면, stack이 오염됨
//그리고 나중에 stack이 저장되는 방식 바꾸면 client에도 영향이 감
그러므로 clients가 stack이 represent되는 방식을 알 수 없게 해야한다.
(즉 cilent에서 직접 접근할 수 없게 해야한다.)
(ADT 만든다고 typedef
이용해 type을 선언한 것이기 때문에 static 붙이는건 여기선 생각 X)
encapsulating types 을 하는 것에 C는 제한된 지원을 해준다.
(C++, JAVA 같은 새로운 C-based language는 이 목적에 맞게 더 잘 지원해준다.)
설계하다보니 ADT module을 만들어야되는데, 만들려고해보니 잘 안돼서 incomplete type 성질을 가져와서 쓰는게 아닌가 싶네
아님 incomplete type이 원래 이 목적으로 만들어진건가?
어쨌든 C++, JAVA에선 이런 기능(Information hiding 등)을 훨씬 잘 지원해줌
encapsulation을 위한 유일한 tool은 incomplete type이다.
(p.448, p.451에도 잠깐 소개됨)
<C standard>
incomplete type
: types that describe objects but lack information needed to determine their sizes
struct t;
: incomplete declaration of t
t가 structure tag이라는건 알려주지만 해당 structure의 members는 알려주지 않는다.
그래서 compiler는 해당 type의 size를 알 수 없다.
이 incomplete type을 프로그램의 다른 곳에서 complete 시켜야 한다.(그게 incomplete type으로 선언하는 의도)
incomplete type의 제한사항
: compiler가 해당 type의 size를 (complete하기 전까지는) 모르기 때문에 variable을 declare하는데 사용될 수 없다.
struct t s; //WRONG
하지만 incomplete type을 가리키는 pointer type을 정의하는 것은 perfectly leagl
typedef struct t *T; //Legal
이제 이 변수를 선언하거나, 함수의 argument로 넘겨주거나, pointer operation을 사용하는 것도 허용된다.
(pointer의 size는 그게 가리키는 것에 의해 결정되는게 아니므로 이런 행동을 허용하는 것이다.)
하지만 ->
operator를 사용하는 것은 금지다.
왜냐하면 compiler는 t의 member에 대해선 아는게 없기 때문이다.
19.3에선 encapsulation이 제대로 안됐으니, 이번엔 incomplete type
을 사용해서 제대로 해보자.
stack은 3가지 방법을 이용해서 각각 구현해 볼 것이다.(Fixed array, Dynamic array, Linked list)
/* StackADT.h (version 1) */
#ifndef STACKADT_H
#define STACKADT_H
#include <stdbool.h>
typedef struct stack_type *Stack; //incomplete type을 가리키는 pointer type
//위에서 말했듯이, incomplete type의 변수를 선언하거나 하는건 안되지만,
//incomplete type을 가리키는 pointer type을 만들거나, 그 pointer type의 변수를 선언하는건 OK
Stack create(void);
void destroy(Stack s);
void make_empty(Stack s);
bool is_empty(Stack s);
bool is_full(Stack s);
void push(Stack s, int i);
int pop(Stack s);
#endif
이를 include하는 clients는 Stack
으로 변수를 선언하거나, 여기 있는 함수를 호출하는건 가능하다.
하지만 structure의 member에는 접근할 수 없다.(다른 파일에 define돼있기 때문)
추가로, 위에 보면 create
와 destroy
function이 있다.
다른 module이라면 보통 이런 함수가 필요하지 않은데 ADT라면 필요하다.
create는 memory를 동적할당 해주고, destroy는 memory를 release해주는 역할을 한다.
ADT라면 header file에 encapsulation을 위해 incomplete type을 가리키는 pointer type을 만든다.
그리고 clinets가 선언하는건 그 pointer type의 변수이기때문에, 당연히 메모리 할당은 필요하다.
(해제도 필요..)
/* stackclient.c */
#include <stdio.h>
#include "stackADT.h"
int main(void)
{
Stack s1, s2; //stack_type의 포인터 생성, member에 직접 접근할 수 없음
int n;
s1 = create(); //memory를 할당받음, s1은 이제 할당받은 메모리를 가리킴
s2 = create(); //stack instance 하나 더 생성
push(s1, 1);
n = pop(s1);
destroy(s1); //s1이 가리키는 메모리 deallocate
return 0;
}
stackADT1.c
를 Fixed-Length Array를 이용해 구현해보자
(간단하게 좀 필수적인 부분만 구현.. 자세한건 p.495)
/* stackADT1.c */
#include <stdio.h>
#include <stdlib.h>
#include "stackADT.h"
#define STACK_SIZE 100
struct stack_type {
int contents[STACK_SIZE];
int top;
};
static void terminate(const char *message)
{
printf("%s\n", message);
exit(EXIT_FAILURE);
}
Stack create(void)
{
Stack s = malloc(sizeof(struct stack_type));
if (s == NULL)
terminate("Error in create: stack could not be created.");
s->top = 0; //clients와 다르게 여기선 structure의 정의가 있으므로 접근 가능
return s;
}
void destroy(Stack s)
{
free(s);
}
.
.
.
바로 위에서 구현한 implementation에 한가지 아쉬운 점이 있다면 int
type만 stack에 넣을 수 있다는 것이다.
다른 type도 입력가능하도록 code를 수정하기 쉽게 바꿔보자.
typedef
를 이용해서 stack에 들어갈 type을 정의하는 것이다.
/* stackADT.h (version 2) */
#ifndef STACKADT_H
#define STACKADT_H
#include <stdbool.h>
typedef int Item; //Item 이라는 type을 만든다. 다른 type이 필요하면 여기서 int만 수정해주면 됨
//code는 이 부분만 수정하는거고.. 다른 연관된 파일들의 recompile도 물론 필요함
typedef struct stack_type *Stack;
Stack create(void);
void destroy(Stack s);
void make_empty(Stack s);
bool is_empty(Stack s);
bool is_full(Stack s);
void push(Stack s, Item i); //int가 있던 자리는 Item 으로 바꿔준다.
Item pop(Stack s);
#endif
그리고 stackADT1.c
의 structure 선언 부분도 바꿔줘야 한다.
struct stack_type {
Item contents[STACK_SIZE];
int top;
};
위에서 Fixed array로 했으니까 이번엔 Dynamic array로 stackADT2.c
를 구현해보자.
(Fixed array는 크기가 모두 하나로 고정됐었으니까 이번엔 원하는 크기만큼 stack size를 할당해주자.)
(간단하게 좀 필수적인 부분만 구현.. 자세한건 p.498)
/* stackADT2.c */
#include <stdio.h>
#include <stdlib.h>
#include "stackADT2.h"
struct stack_type {
Item *contents;
int top;
int size; //stack full을 확인하기 위해 stack의 size도 하나 만듦.
//이전에는 stack size가 모두 같았지만, 이젠 각자 다르므로 따로 정의한 것
};
.
.
.
Stack create(int size)
{
Stack s = malloc(sizeof(struct stack_type));
if (s == NULL)
terminate("Error in create: stack could not be created.");
s->contents = malloc(size * sizeof(Item));
if (s->contents == NULL) {
free(s);
terminate("Error in create: stack could not be created.");
}
s->top = 0;
s->size = size;
return s;
}
void destroy(Stack s)
{
free(s->contents); //얘(stack)도 동적할당 받았으니까 먼저 free해주고 s를 free해야됨.
free(s);
}
bool is_full(Stack s)
{
return s->top == s->size;
}
.
.
.
바로 위 dynamic array도 미리 크기를 정해줘야된다는게 불편할 수 있음
그럴 필요가 없는 linked list를 이용해서 stack을 구현해보자. stackADT3.c
(간단하게 좀 필수적인 부분만 구현.. 자세한건 p.500)
/* stackADT3.c */
#include <stdio.h>
#include <stdlib.h>
#include "stackADT.h"
struct node {
Item data;
struct node *next;
};
struct stack_type {
struct node *top;
};
.
.
.
Stack create(void)
{
Stack s = malloc(sizeof(struct stack_type));
if (s == NULL)
terminate("Error in create: stack could not be created.");
s->top = NULL;
return s;
}
void destroy(Stack s)
{
make_empty(s);
free(s); //stack_type object도 deallocate
}
void push(Stack s, Item i)
{
struct node *new node = malloc(sizeof(struct node));
if (new_node == NULL)
terminate("Error in push: stack is full.");
new_node->data = i;
new_node->next = s->top;
s->top = new_node;
//chapter 17에서 linked list 만들때처럼 첫 node에 새로운 node를 insert
}
Item pop(Stack s)
{
struct node *old_top;
Item i;
if (is_empty(s))
terminate("Error in pop: stack is empty.");
old_top = s->top;
i = old_top->data;
s->top = old_top->next;
free(old_top);
return i;
}
.
.
.
중간에 structure 두번 선언한게 좀 과해보일 수 있다.
근데 저렇게 하는게 우리가 원래 하던 방식이다.
위에 보면 fixed array는 stack을 구성하는 array를 member로 가지는게 stack_type
structure 였고,
dynamic array도 stack을 구성하는 array의 시작점을 가리키는 포인터를 member로 가지는게 stack_type
strucuture였다.
이 경우는 위와 다르게 stack을 구성하는 요소들을 먼저 선언해둘 필요가 있어서(왜냐하면 int
같이 원래 있는게 아니니까) 처음 structure로 stack의 구성요소인 node를 만든 것이고,
그 다음 위에서 했던 것처럼 해당 node를 가리키는 pointer를 stack_type
structure에 포함시킨 것이다.
그런데 과해보이는 이유는 아마 stack_type
structure에 member가 하나밖에 없기 때문일 것이다. dynamic array도 member가 Item *contents;
하나밖에 없었다면 이렇게 보였을 것이다.
결국 stack_type
구조체에 member가 하나라도 더 있으면 된단 얘기가 된다..
지금은 member가 하나밖에 없지만 linked list로 만드는 경우도 dynamic array나 fixed array처럼 'stack에 item이 몇개가 들어있나' 같은 걸 세는 변수를 추가할 수도 있으므로 이렇게 선언하는게 맞다.
추가로, 위 code에서 stack_type
structure를 없애버리고 interface("stackADT.h"
)를 수정해서 다이렉트로 Stack
이 struct node *
이 되도록 하면 안되겠냐고 생각할 수도 있다.
그러면 번거롭기도하고,
애초에 그렇게 해버리면 stack을 수정하기위해 Stack
을 parameter로 받아서 쓰던 함수들이 Stack *
을 parameter 받게 수정해야 한다.(이게 이해 잘 안되면 section 17.6에 pointer to pointer 보고 오기.. 저렇게 수정해버리면 가리키는 node 수정하기 위해선 pointer to pointer가 필요한게 맞음)
위처럼 수정하면 Stack type이 top의 역할을 해야하는데, top은 insertion이나 deletion마다 수정돼야한다.
즉, Stack을 수정해야하므로 Stack*을 parameter로 받아야 한다.
위의 다른 구현 예시들은 stack을 구조체로 구현했고, 그 구조체에 content나 top 같은 정보들이 다 들어있었다.
그래서 그 구조체를 가리키는 pointer로 instance를 표현했기때문에, 그냥 Stack만 parameter로 받아도 그 구조체(stack)이 수정가능했다.
제일 아래의 경우는 linked list에서 top의 역할(포인터로 구현돼서 따라다녀야됨)로 인해 이런 오해가 발생하는 것이다.
ADT에도 몇가지 문제점은 있다. 이를 논의해보자.
지금까지 다룬 stack ADT module은 이해하기 쉽고 짧은 이름의 function을 사용했다.
만약 ADT가 두개 이상 있다면 그런 이름은 충돌할 가능성이 크다. (예를들어 두 module이 다 create
function을 필요로 할 수 있다.)
그래서 ADT 이름을 포함하는 function name을 지어야 할 필요가 있다.
ex) 그냥 create
대신에 stack_create
stack ADT에선 error가 발생했을때 (1)error메시지 띄우고 종료하는 방식으로 error를 다뤘다.
이는 나쁜 방식은 아니지만, (2)종료하는 대신 error를 복구하는 방법도 있다.
예를들어 push
나 pop
이 bool
을 return 하게 만들어서 성공 실패 여부를 볼 수 있다.
push
는 return type을 쉽게 bool
로 바꿀 수 있다.
pop
은 이미 return type이 있어서, 성공하면 일반적인 pointer를, 아니라면 NULL
pointer를 반환하는 방식을 사용할 수 있다.
추가로, C standard library에 assert
라고 하는 parameterized macro가 있다.(Section 24.1)
얘는 특정 조건을 충족하지 않으면 프로그램이 종료되도록 하는데, 이를 우리가 썼던 if
문과 ternimate
함수의 대체제로 사용할 수 있다.
19.4 중간에 Item
이란걸 만듦으로써, 다른 type을 stack에 넣을 수 있도록 코드를 수정 할때 쉽게 하도록 했다.
그런데도 문제점이 있다면, 다른 type을 가지는 2개의 stack을 만들 수 없다는 것이다.(그러려면 ADT를 거의 복사하듯이 해서 또 만들어야됨)
우리는 하나의 "generic" stack type을 만들고 싶다.
하나의 고정된 type이 아니라, integer의 stack, double의 stack 등을 동시에 선언할 수 있는..
다양한 방법이 있지만 완벽하게 커버할 수 있는 방법은 없다.
The most common approach는 Item type으로 void *
을 사용하는 것이다.
여기도 두가지 단점이 있는데
(1) pointer 형태로 나타낼 수 없는 데이터는 포함시킬 수 없다.(ex. string 같은건되지만 int는 안된다.)
(2) error checking이 가능하지 않다.
원래 버전에서 한 stack에 다른 type 들어오면 알 수 있었지만, 이젠 알 수 없다.
위에서 말한 문제들은 새로운 C-based language인 C++
, Java
, C#
에서는 더 명확하게 다뤄진다.
이 언어들의 특징에 대해 알아보자.
이름 충돌 문제는 class내에서 함수를 선언함으로써 해결할 수 있다.
stack ADT는 Stack
class로 나타내지며, stack 함수들도 이 class내에 존재하게 된다.
그리고 그 함수들은 stack object에 적용될때만 인정된다.
위 언어들의 error handling 방법에는 exception handling이라는 것이 있다.
push
나 pop
같은 함수들이 error 조건을 확인하면 exception을 "throw"할 수 있다.
그럼 client의 code는 해당 exception을 "catching"함으로써 error를 처리할 수 있게된다.
Generic ADT를 정의할 수 있는 특별한 특징도 있는데, template를 사용하는 방법이다.
item type을 기술하지 않고 남겨둔다.
초반에 C가 큰 프로그램을 작성하기 위해 design된게 아니라고 했는데, 그럼 UNIX는 큰 프로그램이 아니란건가?
C가 design된 당시엔 아니었지만, 요즘 기준으로 보자면 작은 프로그램이 맞다.(10,000줄 수준)
C lirary에 다른 abstract data types이 있나?
정확히 말하자면 없지만, ADT에 가까운건 있다.
<stdio.h>
에 정의된 FILE
type이다.
file에 대한 작업을 하려면 FILE *fp;
와 같이 FILE *
type의 변수를 선언해야 한다.
그리고 file-handling functions에 해당 변수를 넘겨주며 작업한다.
FILE
은 abstraction이다. 이 type을 사용하기 위해 이것이 무엇인지 알 필요가 없다.
짐작하자면 structure이긴 한데, C standard에서 structure라고 명확히 하진 않았다.
결국 C compiler마다 해당 type이 정의가 다를 수 있으므로 자세한건 알 필요가 없다.
<stdio.h>
를 들여다 보면 그 정의가 있겠지만, 그걸 직접 접근해서 사용하는건 좋은 생각이 아니다.(말했듯이 compiler마다 정의가 다를 수 있으니까)
incomplete structure type 외에 다른 incomplete type이 있나?
extern int a[];
array의 length를 모르므로 incomplete type이다. 아마 다른 파일에 정의돼있을 것이다.
int a[] = {1, 2, 3};
얘도 처음엔 incomplete type이지만(길이 명시 안돼있으니), initializer에 의해 complete된다.
추가로 member 명시 없이 선언한 union tag나 FAM도 incomplete type이다.
마지막으로 void
도 incomplet type이다. 얘는 다른 애들과 다른게 complete 될 수 없다는 것이 특징이다. 그래서 이 type의 변수를 declare하는 것은 불가능하다.
incomplete type에 대한 다른 restriction이 있나?
1. sizeof
operator를 적용할 수 없다.
2. structure나 union의 member가 될 수 없다.(FAM 제외)
3. array의 element가 될 수 없다.
4. function "definition"의 parameter가 될 수 없다.
(declaration에선 허용한다.)
(definition에 array parameter가 오면 (compiler가) pointer type이 되도록 조정함으로써 incomplete type이 오는걸 막는다.)
추가로 위 본문에 말했던 restriction 복습
1. variable declare하는데 사용할 수 없다.
2. 해당 incomplete type을 가리키는 pointer를 선언하는 것은 legal
3. 다른 어딘가에서 complete 해줘야...