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

- 하지만 아래와 같이 부호가 있는 경우에 대해서는 주의가 필요하다.

+ Recent posts