본문 바로가기
1000권

9.Do it! c언어 입문

by 인듯아닌듯 2020. 8. 20.

메모리를 알맞은 공간에 저장하는 것이 중요한 이유는 공간을 효율적으로 사용하기위한 것 뿐만아니라, 정보전달에 있어서도 비용을 적게 들게하기위해서이다.

소스코드 설계도
라이브러리 제품
헤더파일 설명서

로 비유하곤 한다.

라이브러리 파일은 '실제 사용하는 내용' 만 실행 파일에 포함한다. 라이브러리 파일(lib)은 자신이 가지고 있는 함수들 중에 시 신제로 다른 소스 파일에서 사용한 함수의 기계어만 분리할 수 있도록 목적 파일을 재구성한 것입니다. 그래서 보통 라이브러리 파일은 목적 파일을 변환해서 만들어집니다. 이렇게 만들어진 라이브러리 피일을 프로그래머가 목적 파일 대신에 소스 목록에 넣어 두면 링크할 때 실제로 사용되는 함수들만 신행 파일에 포함시킵니다. 즉 라이브러리 파일에 10개의 함수 코드가 있어도 프로그램에서 그 중 1개만 사용한다.

라이브러리의 또 다른 장점은 라이브러리 파일은 컴파일된 파일이기 때문에 기계어로 변환된 상태라 소스코드를 볼 수 없어서, 다른 사람에게 소스코드를 공개하지 않아도 사용할 수 있도록 할 수 있다는 점이 있다.

헤더파일이 있는 이유는 기계어로 컴파일된 라이브러리, 목적파일을 불러오기 위해서는 함수의 프로토타입을 알려줘야 사용할 수 있기 때문에, 소스 코드 맨 위에 함수의 원형(사용 방법)을 표기해주는 것 이다.

전처리기라고 해서 '#'으로 불러오고 define과 include가 있다.

컴파일러는 먼저 현재 자신이 작업하는 경로에서 해당 파일을 찾고(#include "libft.h"), 만약 파일이 현재 작업 경로에 없으면, 표준라이브러리가 있는 경로에서 파일을 찾는다. 표준라이브러리를 사용할 때는 관습적으로 '<'stdio.h'>'를 사용한다.

int main(){
    int result = 0;
    int data = 5;
    result = --result && (data = 0);
    result = result-- && (data = 0);    
    result = result-- || (data = 0);
    //                result, data
    //result = (--result && (data = 0)); 0, 0
    //result = (result-- && (data = 0)); 0, 5
    //result = (result-- || (data = 0)); 0, 0
}

대입 연산자와 비교 연산자를 실수하는 경우가 많다. 이에 대한 대책으로 상수를 먼저 입력하자. 그렇게하면, 잘못 코딩하여도 에러가 적절하게 나올 수 있다.

if (3 == data) //good
if (3 = data)// bad, but error can be occured

unsigned int에서 2의보수는 0이 되고, signed int 에서 2의보수는 *=-1 이 된다.

1.지역변수가 전역변수보다 먼저 처리된다.

2.전역변수의 이름이 지역변수든, 또 다른 소스코드의 전역변수든 겹치면 안된다.

해결방식

2-1.변수이름에 접두어 'g_'를 붙이기 ex:g_data

2-2.static 전역변수로 선언하기

함수 많이 호출하면 속도가 느려진다. 대신 함수를 적절하게 사용하면, 유지보수 측면에서 좋다.

if은 프로그램의 흐름을 바꾸기때문에, 많이 사용하면 속도에 영향을 준다. 다만 컴파일하는 과정에서 if의 사용횟수와 관계없어지기도한다.

ex 컴파일로 번역하면 동일한 형태의 기계어가 만들어진다.

if(A > 0){
    if(B > 0){
    }
}

if(A > 0 && B > 0){
}

big endian vs little endian

과거 컴퓨터들은 구조설계방식이 CISC, RISC로 나뉘어 있었다. 많은 차이점 중에 개발자가 알아야하는 차이점은 메모리 정렬방식이 다르다는 것이다.

비트 단위의 메모리 정렬은 CISC 방식이나 RISC 방식 모두 같기 때문에 상관없지만, 여러 개의 바이트가 모여서 표현되는 메모리를 정렬할 때는 이 두 방식에 차이가 있습니다.

CSIC big endian(오름차순) unix, linux
RSIC little endian(내림차순) window

단, Java은 VM에서 하드웨어와 관계없이 메모리를 big endian방식으로 재배열한다.

char str[6] = "happy" 도 가능한 표현이다.

즉 배열 이름은 변수처럼 보이지만 내부를 들여다보면 실제로는 상수화된 주소이기 때문에 변경할 수 없다는 뜻 입니다.

배열은 일반 변수들을 묶어 놓은 개념이기 때문에 변수가 자신이 위치한 주소를 변경할 수 없듯이 배열도 자신이 위치한 메모리 주소를 변경할 수 없습니다. 따라서 배열의 시작 주소도 변경할 수 없겠지요.

그런데 실행 파일에 있는 명령들은 CPU가 직접 실행할 수 없습니다. CPU가 이 명령들을 실행하려면 먼저 운영체제가 실행 파일의 명령들을 읽어서 메모리에 재구성하게 되는데, 이것을 프로세스(Process)라고 합니다. 이렇게 메모리에 프로세스가 구성되면 CPU는 프로세스에 저장된 명령들을 실행할 수 있습니다.

출처:do it! c언어 입문

push -> 스택포인터 감소, pop -> 스택포인터 증가 : stack은 데이터가 거꾸로 자란다.

Q. 이게 리틀엔디언이여서, 그런건가 아니면, 그것과 상관없는 걸까? 상관없다고하면 더 문제가 될 것 같음..

정적메모리 할당의 한계

컴파일러의 설정을 변경하지 않았다면 프로세스 안에서 지역 변수가 저장되는 기본 스택메모리 크기는 1Mbyte를 넘을 수 없다.(error : stack overflow) pixel에 관련해서 정적메모리로 저장하면, 가로 * 세로 * 4byte <= 1Mbyte 를 충족시키기 어렵다. 매우작은 사진만 저장이 가능하다.

동적메모리 힙은 Gbyte 단위까지 할당할 수 있기 때문에 메모리를 할당할 때 크기 문제가 거의 발생하지 않습니다.

malloc함수에서 2Gbyte이상의 큰 메모리를 한번에 할당하는 경우에 실패하여, NULL을 반환할 수 있다.

page 404에서 스택세그먼트에서 스택프레임을 어셈블리어의 명령어와 함께 잘 설명한 그림이 있다. 꼭 확인할 것!

구조체와 배열 = 데이터의 그룹화

typedef

1. 미리 설정한 자료형의 크기를 한번에 바꿀 수 있다.

2. 복잡해보이는 문법을 쉽게 정의한다.

구조체의 경우에는 다양한 크기의 메모리를 하나의 그룹으로 묶어 사용하다보니 구조체 요소를 사용할 때 실행 속도가 떨어지는 문제가 있습니다. 그래서 구조체의 멤버를 일정한 크기로 정렬하여 실행속도를 더 빠르게 하는 개념이 c언어 컴파일러에 추가되었다. 이를 '구조체 멤버 정렬("Struct member Alignment)라고 한다.

요즘 컴파일러들은 1,2,4,8바이트 정렬중에서 8바이트 정렬을 기본 값으로 하고 있다.

구조체의 요소는 같은 크기끼리 모아 주는 것이 좋다.

구조체 자료형을 선언할 때, 같은 크기의 요소들끼리 모아 주는 것만으로도 8byte 정렬에 의해서 낭비되는 메모리를 줄일 수 있다.

링크드리스트 개념의 출발

동적할당되는 메모리 만큼 포인터 변수가 필요하고, 동적할당된 메모리에는 값이 들어갈 수 있어야한다.

따라서 포인터 변수와 메모리 변수를 하나(구조체)로 묶어서 요청이 들어올 때마다 할당시키고 포인터의 변수가 구조체를 가리키도록 이어주면 링크드리스트가 된다.

함수 포인터

작성한 소스 코드가 컴파일러에 의해 기계어로 번역되면 실행 파일이 됩니다. 이 실행 파일은 CPU가 처리할 수 있는 기계어 명령문' 단위로 이루어집니다. 그리고 해당 프로그램이 실행되어 프로세스 형태로 메모리에 저장되면 프로그램의 명령문들은 코드 세그먼트(CS, Code Segment)에 옮겨집니다.

결국 이 말은 명령문들도 메모리에 저장되어 있기 때문에 각 명령문마다 주소를 갖는다는 뜻입니다. 메모리의 주소 값을 알고 있다면 당연히 포인터를 사용할 수 있겠죠? 포인터 개념을 사용하면 특정 명령문이 저장된 메모리의 주소로 바로 이동해서 그 명령문을 실행할 수 있습니다. 하지만 제멋대로 메모리의 위치를 이동하면서 명령문을 실행하면 C 언어의 스택 프레임이 엉망이되기 때문에 스택 프레임이 유지될 수 있도록 함수 단위로만 이동해야 합니다.

코드 세그먼트에 있는 명령문의 주소를 저장해서 포인터로 사용하는 방법을 함수 포인터라고 한다.

함수 포인터-> 콜백 함수 -> 상속의 오버라이딩, 오버로딩

ifdef, endif 조건부 컴파일 : 예외처리를 하되 특수한 경우에만 발휘되도록 하기 위한 수단(왜냐하면 예외처리도 실행속도에 있어서 비용이 들기 때문이다.)

union : 극한의 메모리관리

enum : 숫자를 단어로 define해야할 것이 많을 때, 하는 방법

비트연산자를 사용하지 않고 비트값을 이용하는 방법(bit_0 : 1) && union으로 unsigned char로도 사용하기!

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

struct BitType
{
    unsigned char bit_0 : 1;
    unsigned char bit_1 : 1;
    unsigned char bit_2 : 1;
    unsigned char bit_3 : 1;
    unsigned char bit_4 : 1;
    unsigned char bit_5 : 1;
    unsigned char bit_6 : 1;
    unsigned char bit_7 : 1;
};

union BitData
{
    struct BitType bit_data;
    unsigned char byte_data;
};
int main()
{
    struct BitType data;
    memset(&data, 0, 1);
    data.bit_0 = 1;
    unsigned int temp;
    temp = (unsigned int)data; //캐스팅을 해줘도 불가능함
    memcpy(&temp, &data, 1); //memory로 카피해줘야함
    printf("%d\n",temp);
    printf("%d\n",data);
    printf("%d\n",sizeof(data)); //1
    return (1);
}

바이너리파일 텍스트파일

바이너리 텍스트
sizeof strlen
memcpy strcpy
fwrite fprintf
   

(tmi : memcpy가 strcpy보다 빠르다.)

결론적으로 어떤 속성을 사용할지를 결정하면 그에 맞는 함수를 사용해서 프로그래밍해야원하는 결과를 얻을 수 있습니다. 그리고 어떤 속성을 사용하든지 데이터 자체가 변경되는 것이 아니라 데이터를 해석하는 개념이 달라지기 때문에 프로그램 개발 상황에 맞게 잘 판단해서 사용하면 됩니다.

fwrite와 fread의 정상적인 반환값은 반복횟수 값이므로, 실행이 제대로 되었는지 판단할 때 사용하기도한다.

if(5 == fread(&data, sizeof(int), 5, p_file)){ }
if(1 == fwrite(&data, sizeof(int), 1, p_file)){ }