2-1. C언어의 해부학
- C언어의 기본적인 구성요소는 함수
- input - 함수 - 출력의 흐름
2-2. C 프로그램 훑어보기
include <stdio.h>
// 전처리기 -> 컴파일 하기 전에 처리한다라는 의미, 이미 만들어진 함수를 사용하기 위해
int main(void) /// main 함수 정의 시작 // statement
{ // scope의 시작 // statement
int a; // 변수의 선언 // statement
int b; // 변수의 선언 // statement
int c; // 변수의 선언 // statement
a = 1; // 변수에 값 대입 // statement
b = 2; // 변수에 값 대입 // statement
c = a + b; // 벼눗에 연산 결과 대입 // statement
printf("Result is %i", c); // 함수 호출 // statement
return 0; // 결과 값을 반환
} // scope의 끝 // statement
2-3. 변수가 편리한 이유
- 메모리를 주소를몰라도 변수명을 활용해 값들을 처리 할 수 있게 됨.
2-4. 자료형이 필요한 이유
(1) 첫번째 이유(for CPU)
- 사람은 정수와 실수를 구분하여 연산할 수 있지만, CPU는 0101로 이루어져있는 값을 가져오기때문에 정수와 실수를 구분할 수가 없다.
- 이를 미리 정수, 실수를 미리 알려주기 위해 자료형을 명시적으로 보여주는 것.
(2) 두번째 이유(for Memory)
- 자료형을 메모리에 미리 알려주어 자료형에 맞는 메모리 사이즈를 활용할 수가 있다.
2-5. 변수를 선언하는 방법
int main()
{
//int x, y, x; // 선언 -> 메모리를 잡기
//int x = 1, y = 2, z; // 선언과 할당을 동시에 할수도 있다.
//x = 1; // 할당
//y = 2; // 할당
// 통상 아래와 같은 방식으로 선언 및 할당을 함.
int x = 1, y = 2;
int z;
z = x + y; // 할당
return 0;
}
2-6. printf() 함수의 기본적인 사용법
- 입력 -> printf() -> 출력
- printf에서 f의 의미는 format이며 값을 출력하기 위해서 format을 지켜야 한다.
int main()
{
int a = 1, b = 2, c = 3;
// 몇가지 주요한 flag들이 존재한다.
// +,-, ,0, width, precision, specifier등이 있으며, 각각의 특성들은 직접 구현해보면... 자세히 알수 있다.
printf("%d %d %d", a, b, c);
return (0);
}
14-1. 구조체가 필요한 이유
- 자료형이 서로 다르지만 함께 사용하면 편리한 데이터 오브젝트들 끼리 모아 놓고 사용하기 위해
- template이며, 각각의 멤버들은 각 오브젝트별로 메모리에 순차적으로 차지하게 된다.
struct Patient
{
char name[MAX_NAME];
float height;
float weight;
int age;
}
- 메모리에 저장되는 형태는 아래와 같습니다.
14-2. 구조체의 기본적인 사용법
- 아래와 같은 기본 구조로 구조체를 선언합니다.
- 선언된 구조체는 template로서 초기화되기 전에는 어떠한 값도 가지지 않은 상태이며, 메모리에도 올려져 있지 않은 상태입니다.
struct person
{
char name[MAX];
int age;
float height;
};
- 초기화 방법 및 추가 설명
int main()
{
// 초기화 방법 1
struct person ts;
strcpy(ts.name, "taeskim");
ts.age = 100;
ts.height = 1000;
// 초기화 방법 2 -> 순서를 지켜야 함.
struct person ts2 = { "taeskim", 100, 1000 };
// 초기화 방법 3 -> Designated initializers 순서를 지킬 필요가 없음.
struct person ts3 = {
.age = 100;
.name = "taeskim";
.height = 150.0f;
}
// 구조체의 변수를 가르키는 포인터
struct person* someone;
someone = (struct person*)malloc(sizeof(struct person));
// Indirect member(ship) operator
// someone이라는 포인터를 통해 구조체의 변수에 접근하기 때문에 arrow operator를 사용한다.
someone->age = 1001;
// 물론 아래와 같이 표현할 수도 있음..
printf("%s %d\n", someone->name, (*someone).age);
// no tag -> 잠깐만 사용하고 말 구조체라면 이렇게 선언할 수 있다.
// 다만, 새로운 구조체 변수를 선언 후 사용하기 위해서는 apple2뒤에 새로운 변수명을 추가하거나 하는 번거로움이 있다.
struct {
char farm[MAX];
float price;
} apple, apple2;
// 새로운 타입인 것처럼 구조체를 선언하기
typedef struct {
char name[MAX];
char hobby[MAX];
} friend;
}
14-3. 구조체와 포인터, 구조체를 함수로 전달하는 방법
- 다음과 같이 선언해서 사용할 경우 t1을 t2에 재할당하게 되고 이는 내부에 있는 멤버들의 값도 복사 되어 들어가게 된다.
- 즉, 구조체의 포인터를 찍어 봤을 때 다른 값이 출력됨을 확인할 수 있다.
typedef struct
{
int age;
} t;
int main()
{
t t1;
t t2;
t1.age = 10;
t2 = t1;
return(0);
}
- 구조체를 함수의 인자로 전달하여 선언부에서 매개변수로 활용하게 된다면 몇가지 이슈가 발생하게 된다.
(1) 구조체 복사
- 구조체의 포인터에서 언급한 것처럼 구조체를 다른 구조체에 할당하게 되면 값이 복사되게 되고 서로 다른 메모리 영역을 차지하게 된다고 설명하였다.
- 함수의 인자로 전달하게 되면 해당 값을 복사하여 stack에 저장한 뒤 사용하게 된다.
(2) 성능 저하
- 1번의 이슈로 굉장히 큰 구조체를 복사하게 되면 성능의 저하를 야기할 수 있다.
(3) 배열을 인자로 가지는 경우
- 주소만 복사하고 값들을 복사하지 않는다.
--> 따라서 값을 전달하는 것이 아니라 포인터를 전달하자.
14-4. 구조체의 메모리 할당
- padding이 생기는 이유
- 컴퓨터의 메모리와 cpu가 데이터를 주고 받을 때의 단위는 1 word이며, x86인 경우 4bytes, x64인 경우 8bytes이다.
- x86은 32bit 칩셋의 품번.
typedef struct {
char a;
float b;
double c;
} human;
- 위와 같이 만들어진 템플릿을 활용해 초기화를 하게 되면 메모리상 재미있는 일이 일어난다.
human ts;
printf("%lld\n", (long long)&ts);
printf("%lld\n", (long long)&ts.a);
printf("%lld\n", (long long)&ts.b);
printf("%lld\n", (long long)&ts.c);
/*
출력되는 값
140732658305960
140732658305964
140732658305968
*/
- char는 1바이트인데 4바이트로 인식되는 것.
- 컴퓨터는 아래의 prerequisite를 가지고 있다.
- cpu와 메모리의 communication은 가장 많은 비용이 들기 때문에 최소화 하는것 이 좋다.
- 보통 word 단위로 하나, 구조체의 요소 중 가장 큰 사이즈를 기준으로 한다.
- 예를 들어, 위와 같은 경우 double의 8bytes를 기준으로 하고 char 는 1이기 때문에 3을 주가하여 4를 만들고, float는 4이기 때문에 이둘을 합쳐 8을 만든뒤 전송하게 되는 원리인 것이다.
- 그렇다면 더 재미있게 각각의 위치를 조금 변경시켜 보자.
typedef struct {
float b;
double c;
char a;
} human;
- 위과 같은 경우는 어떻게 될까?
- 사이즈가 24bytes임을 확인할 수 있다.
- 동일한 프로세스로 접근해서 계산해보자.
1. 가장 큰 요소의 크기를 찾는다. -> 8bytes
2. 각각의 요소를 8bytes틀에 맞추고 모자라는 경우 패딩을 넣는다. -> b의 4에서 4를 추가하여 8, a의 1에서 7을 추가하여 8
- packing을 통해 물론 padding을 제거 할 수도 있다 하지만.. 효율적이지 못하다.
- 아까와 비교해 그렇지 않다. 그렇다면 우리는 쓰지 않는 공간의 낭비를 위해 공간을 채우거나 요소의 배치를 효율적으로 할 필요가 생긴다.
- 쓰지 않는 공간을 없애는 방법은 아래의 방식을 취한다.
- 이를 packing한다 라고 한다. 더 자세한 내용은 이곳을 참고 하기 바란다.(https://www.ikpil.com/359)
typedef struct _MY_ST_D {
char str[10];
double cnt;
} __attribute__ ((packed)) MY_ST_D;
혹은
struct _MY_ST_D {
char str[10];
double cnt;
} __attribute__ ((packed));
typedef struct _MY_ST_A {
char str[10] __attribute__((aligned(4)));
char cnt[4];
} MY_ST_A;
14-5. 복합 리터럴
- 구조체를 초기화 한이후에는 아래와 같은 방식으로 값을 변경할 수 없다.
- 복합 리터럴로 저장된 값은 lvalue로서 포인터 값을 가질 수 가 있다.
struct myStruct
{
int age;
char name[MIN_LENGTH];
};
int main()
{
struct myStrcut s1 = { 12, "ts" };
s1 = { 13, "ts" }; // 불가능
struct myStruct s2 = { 13, "ts" };
s1 = s2; // 가능
s1 = (struct myStruct) { 15, "tt" }; // 가능 -> 복합 리터럴 해당 값들이 리터럴로 저장됨.
return (0);
}
14-6. 신축성 있는 배열 멤버
- 아래와 같이 선언해서 사용하게 되면 동적할당 배열처럼 사용할 수 있습니다.
- 특징
1) 동적 할당 전까지는 values는 메모리를 차지 하지 않는다.
2) count, average 다음에 values의 메모리 주소가 들어간다.
struct flex
{
size_t count;
double average;
double values[];
};
const size_t n = 3;
struct flex* pf = (strcut flex*)malloc(sizeof(struct flex) + n * sizeof(double));
14-7. 익명 구조체
- 익명 구조체를 사용하게 되면 접근시 dot 연산자의 사용을 줄일 수 있다.
struct name
{
char first[20];
char last[20];
}
struct person
{
int id;
struct names name; // nested structure member
}
struct person2
{
int id;
struct { char first[20]; char last[20]; }; // anonymous structure
}
int main()
{
struct person ted = { 123, {"Bill", "Gates"} };
struct person ted2 = { 125, "ts", "kim" };
struct person2 ted3 = { 123, {"Bill", "Gates"} };
printf("%s", ted.name.first);
printf("%s", ted2.name.first);
printf("%s", ted3.first);
return (0);
}
14-8. 공용체의 원리
- 서로 다른 데이터 타입이 같은 메모리 공간을 차지하게 됩니다.
- 메모리 주소를 찍어보면 동일한 곳을 가리키고 있다.
- 크기 또한 타입 중 가장 큰 8인 것을 확인할 수 있다.
union my_union
{
int i;
double d;
char c;
};
- 초기화 하는 방법은 아래와 같습니다.
union my_union uni1 = { 10 };
union my_union uni1 = { .c = 'A' };
union my_union uni1 = { .d = 1.23 .i = 100 }; // 이렇게 할 이유가 없음. 뒤에것으로 초기화.
14-9. 공용체와 구조체를 함께 사용하기
- 구조체와 함께 사용하게 되면 매우 편리하다.
- 구조체 하나가 두가지 구조체인것처럼 사용될 수 있다.
struct personal_owner
{
char rrn1[7];
char rrn1[8];
};
struct company_owner
{
char crn1[4];
char crn2[3];
char crn3[6];
};
union data
{
struct personal_owner po;
struct company_owner co;
};
struct car_data
{
char model[15];
int status;
union data ownerinfo;
/* 익명 공용체를 이렇게 사용할 수도 있음.
union data
{
struct personal_owner po;
struct company_owner co;
};
*/
};
14-10. 열거형
- define을 활용해서 symbolic constant를 만드는 방법이 있음 이는 문자열을 숫자로 바꿔주는 것
- 열거형도 이와 동일하게 컴퓨터상으로는 숫자로 저장되어 있는 것을 프로그래머의 가독성을 높여 주기 위해 문자로 대체해서 사용한다.
- 열거형을 변경이 불가능하기 때문에 정해진 값에 대해 실수를 줄일 수 있다.
int main()
{
enum spectrum { red, orange, yellow = 10, green, blue, violet };// 이렇게 입력되어 있더라도 컴퓨터상으로는 숫자로만 인식됨.
// 시작은 항상 0부터이지만 yellow가 10이기때문에 green 부터는 11로 시작된다.
//printf("%s", orange); // 이렇게 출력되지 않는다.
enum spectrum color;
// 1. loop
for (color = red; color <= violet; color++)// c++에서는 안됨.
printf("%d\n", color);
enum levels { low = 100, medium = 500, high = 2000 }; // 이곳에서만 점수에 대한 값들을 다루기 때문에 유지 관리가 유리하다.
int score = 800;
if (score > high)
printf("High score!\n");
else if (score > medium)
printf("Good job\n");
else if (score > low)
printf("Not bad\n");
return (0);
}
14-11. 함수 포인터의 원리
- 함수가 메모리의 어디에 저장되어 있는지를 살펴보는 함수 포인터
- 함수를 호출하게 되면 해당 주소에 있는 코드를 실행 시킨다라는 개념이기 때문에, 이름이 주소 입니다.
void f1()
{
return ();
}
int f2(char i)
{
return (1 + 1);
}
int main()
{
void (*pf1)() = f1;
// void (*pf1)() = &f1; 둘다 가능
int (*pf2)(char) = f2;
(*pf1)();
// pf1(); 둘다 가능
int a = pf2('A');
// int a = (*pf2)('A'); 둘다 가능
return (0);
}
- 프로그래머는 함수의 이름을 이용해서 프로그램을 작성하지만 컴파일러는 이름(식별자)들을 메모리에서의 주소로 번역합니다. 즉, 함수를 실행시킨다는 것은 메모리에서 함수의 주소 위치에 저장되어 있는 명령어들을 순차적으로 수행한다는 의미입니다. by 홍정모 교수님
14-12. 함수포인터의 사용방법
- 함수포인터를 통해 아래와 같은 사용할 수 있다.
/*
void toUpper(char *str)
{
while(*str)
{
*str = toupper(*str);
str++;
}
}
void toLower(char *str)
{
while(*str)
{
*str = tolower(*str);
str++;
}
}
*/
void updateStr(char *str, int (*pf)(int))
{
while(*str)
{
*str = (*pf)(*str);
str++;
}
}
int main()
{
char str[] = "Hello, world!";
updateStr(str, toupper);
updateStr(str, tolower);
return (0);
}
15-1. 비트단위 논리 연산자
- 비트 수준에서 정보를 조작하게 되면 정보를 아주 효율적으로 저장할 수 있게 된다.
int main()
{
unsigned char a = 6;
unsigned char b = 5;
printf("%hhu", a & b);
}
// 6을 이진수로 변환 00000110
// 5를 이진수로 변환 00000101
// &연산을 하게 되면 둘다 1인경우에 1로..
// 00000100이 되고 이는 4이다.
// |연산을 하게 되면 둘중에 하나만 1인경우도 1..
// 00000111이 되고 이는 7이다.
// ^연산을 하게 되면 둘다 1인경우에 0으로..
// 00000011이 되고 이는 3이다.
// ~연산을 하게 되면 0은 1로 1은 0으로..
// ~a의 경우 11111001이 되고 이는 249이다.
15-2. 쉬프트 연산자
- left shift : 곱셉 // 1 << 3 -> 00000001 -> 00001000
- right shift: 나눗셈 // 8 >> 1 -> 00001000 -> 00000100
- 하지만 아래와 같이 부호가 있는 경우에 대해서는 주의가 필요하다.