[C] C lang 구조체, 공용체, 함수 포인터
구조체, 구조체의 필요성
구조체란(structure)
1 . 클래스를 배우러 가는 과정에서 여러가지 형의 데이터만 모아둔 아이
2 . 클래스를 배운 뒤, 참조형이 아닌 값형인 커스텀 데이터형
C에서 구조체는 첫번쨰 개념과 같다.
- 데이터의 집합. 멤버함수는 없다
- 여러 자료형을 가진 변수들을 하나의 패키지로 만들어 둔 것.
v
typedef 이란?, typedef 사용법
typedef
typedef unsigned int size_t
- 이미 있는 자료형인 unsigned int에 새 별명 지어줌.
- 그 새로운 이름은 size_t
- 근데 그냥 unsigned int 써도 전혀 상관 없음.
- 서로 바꿔가며 써도 된다
- 물론 size_t의 경우는 각 구현마다 자료형이 달라질 수 있어서 typedef를 해놓은 것.
typedef 사용법
- 아무튼! 구조체 typedef를 쓰면 다른 자료형처럼 간결하게 변수 선언이 가능
- 어느 방법을 써도 크게 상관 없음
- 참고로 C에서 _t로 끝나는 자료형은 보통 이렇게 Typedef 한 것들
enum도 마찬가지
game_role_t를 만들어주면 이걸 사용해서 변수 선언이 가능해진다.
공용체도 사실 같은 방식. 공용체 선언하고 만들떄 그대로 쓴다.
코딩 표준 : 커스텀 자료형에 typedef를 쓰자.
- 가능하면 구조체, 열거형, 공용체에 typedef를 써서 제대로 된 자료형처럼 보이게 하자
- 강제사항은 아님.
구조체 변수 초기화 하기
구조체 변수 초기화하기
- 아까도 말했듯이 구조체 선언과 동시에 초기화 안 됨.
- 스택에 남아 있는 데이터를 그대로 사용
- 아까도 말했듯이 구조체 선언과 동시에 초기화 안 됨
- 스택에 남아있는 데이터를 그대로 사용
- 그냥 구조체는 지역변수를 여러개를 따로따로 사용하는 것과 마찬가지라고 생각하자
- 사실 기계는 구조체라는 개념 자체를 모른다.
- 그냥 프로그래밍 언어가 프로그래머 편하고 실수하지 말라고 제공해준 개념이다.
- 실제 하드웨어는 우측 기반으로 돈다(오른쪽 사진)
궁금하면 메모리/어셈블리 보면 됨
이 두개는 같은걸까?
컴파일러 입장에선 똑같은 코드로 인식하고 스택에 다 넣어준다.
또 다른 구조체 초기화 방법
date_t date = {0,};
- 아님 이렇게도 가능
- 배열 초기화 떄 썼던 방법
- 컴파일러 따라 그냥 date 가 차지하는 모든 메모리 공간을 0으로 채워주는 명령어로 바꿔줄 수도 있음.
- memset()
- 배열도 이렇게 초기화 되기도 한다.
요소 나열법으로 초기화
- 근데 되도록이면 이 방법은 추천하지 않음
실수할 여지가 있다.
나중에 다른 프로그래머가 구조체 멤버변수 순서를 바꾸고 변수 초기화 목록을 안 고치면 문제가 됨
- 그 버그가 생긴지도 모른 채 시간이 꽤 흐를 수 있다.
요소 나열법이 유용한 경우는 딱 한가지
- 멤버변수가 const일떄, 선언 시 초기화 안 하면 다시는 못함
- 근데 const멤버 변수는 잘 쓰지도 않고 쓰지 말라는 게 업계 표준
- const는 쓰는거 보다 안쓰는게 더 좋다(안 쓸때가 더 장점이 많다..)
구조체 매개변수
- 클래스랑 똑같네?
1년 더하는 코드(안됨)
구조체는 값형이다
- 값형이다. 참조형이 아님
- C에서는 자료형은 전부 똑같이 작동함
인자 전달도 기본 자료형과 똑같이 작동
- 완전히 똑같다.
그럼 원본을 바꾸고 싶다면?
구조체의 ‘포인터’
date_t *date
구조체 포인터에서 멤버의 값에 접근하기
void increase_year(date_t * date)
{
(*date).year = (*date).year+1;
}
매개변수 주소로 받아옴(포인터로). 실제 호출할 떄 그 date라는 구조체가 있었다면, 호출자에, 변수가 있었다면, date라는 그 앞에 &를 넣어줘 갖고 주소 변환해서 넣어주면 된다.
그럼 주소니까 값 가져오고 구조체 안 멤버변수 접근해서 증가시켜줌. 근데 * 넣어주는 이유? 연산자 우선순위 떄문
- () 필요한 이유?
- 연산자 우선순위 떄문
- .연산자의 우선순위는 1, *연산자 우선순위는 2
- 고로 괄호가 없다면? _date.year-> _(date.year)
date주소에서 .year를 가져오려고 하는 것. 포인터인데 주소.이 어디있나 그래서 위처럼 작성해야 한다.
근데 이러면 괄호떄문에 지저분해진다.
- 이걸 합친 간단한 연산자가 있다
- 화살표 연산자(->)
- 우선순위는 1순위.
(*date).year= (*date).year+1;
date->year = date-> year+1;
date->year++;
.은 어떤 구조체의 변수의 멤버변수에 접근하는 것. 근데 그 구조체가 구조체로 직접 값으로 들어온 게 아니라 주소로 들어오면 그 주소가 가리키는 구조체에 접근을 해서 그 다음에 멤버 변수 불러와야 함.
즉, date가 포인터라면 화살표 써야하고 date가 포인터가 아니라 값이라면 그냥 점(.)만 쓰는 것.
구조체 매개변수 베스트 프렉티스
1 . 값으로 전달 vs 주소로 전달
- 기본 자료형 전달시는 간단했음
- 기본 데이터 크기가 작으니 원본 바꿀 때만 주소로 전달하면 되었다.
- 구조체의 경우 데이터 크기가 클 수 있다.
- 구조체 안에 int형 멤버변수가 5만개면 50000*4 = 200kb
- 이럴 떄는 다 복사하는 게 성능이 느릴 수도 있다. - > 이럴떄 주소로 전달
- 포인터에 const 포인터를 붙이면 원본도 못 바꾸니 안전하다.
2 . 구조체 매개변수 vs 여러개의 개별 변수
- 딱히 정확한 규칙이 있지 않지만 보통 변수 많이 전달하는 대신, 구조체 하나 전달하라고 한다.
- 엄밀한 규칙은 없다. 구조체 안쓰고 매개변수 10개 넘게 넘기는 사람들도 많다.
- 근데 어떤 곳에서 4개까지는 괜찮고 5개가 넘어가면 그떄부터 구조체 만들라 하는 사람도 많다.
- 이유는 다양 x64 함수 규약이라던가 이런 걸 보면 좀 더 빠르다던가 더 큰 문제는 실수를 줄이기 위함.
- 한 4개까지는 낱개 변수로 그 이후에는 구조체로 넘기라는 규칙을 쓰는 회사도 있다.
- 이유는 다양
- 실수 줄이기 위해서도 있고
- 성능 빠르게 하기 떄문도 있고(특히, 주소로 전달시)
함수 반환값으로서의 구조체, 구조체 배열
함수 반환값으로서의 구조체
- 기본자료형이랑 똑같음(int 반환하면 int반환 되듯이 함수 반환하면 함수 반환)
- 그래서 반환값이 2개면 return으로 반환 못하니까 out매개변수 만들어서 씀.
- 근데 이걸 벗어나는게 return 구조체 만들어서 사용
- 여러개의 반환값 넘겨주고 싶으면 구조체 사용
- int형 하나 반환하는 것과 마찬가지로 복사에 의해 반환
- C언어의 함수는 반환값이 하나라고 했음
- 이렇게 구조체를 반환하면 실질적으로 여러개의 값을 반환하는 격
그럼
date = get_day()
가 되는 건 대입도 된다는 얘기인가?
대입도 가능하다
- 개념상 그냥 각 멤버 변수를 돌아가며 하나씩 대입한다고 봐도 된다.
date_t date1; //{2043,10,1}
date_t date2; //{0,0,0}
date2= date1
- 어떤 컴파일러들은 메모리를 통쨰로 그냥 복사해줌
- memcpy()라는 함수
- 동적 메모리 사용시 많이 쓰임
구조체로 배열도 만들 수 있다.
- 정말 기본 자료형이랑 다르지 않다고 생각 하는게 편함
- 구조체마다 자료크기가 딱 정해져 있으니 컴파일러가 다른 변수와 똑같이 처리 가능
가족 생일 저장하는 구조체 배열의 예
각 요소가 몇 바이트씩 떨어져 있다
요소 하나의 크기
- sizeof(int) + sizeof(int) + sizeof(int) = 12
실제로 결과가 같은지 확인
각 요소사이가 12바이트 떨어진 것도 확인
실제 메모리
family_birthdates가 이런 데이터 가진거 보고 배열이 시작하는 위치로 메모리를 본거. 그리고 실제 값들이 올바르게 들어간건지 보는 것이다. 실제 메모리도 12바이트 단위로 내가 집어넣은 생일이 하나씩 들어간 점이 보인다.
예시
typedef struct name {
char* lastname;
char* firstname;
} name_t;
char firstname[] = "Lulu";
char lastname[] = "Lee";
name_t name;
name_t clone;
name.lastname = lastname;
name.firstname = firstname;
clone = name;
name.lastname[0] = 'N';
printf("origin: %s %s\n", name.firstname, name.lastname);
printf("clone: %s %s\n", clone.firstname, clone.lastname);
-> origin :Lulu Nee clone: Lulu Nee 출력
얕은 복사, 깊은 복사
위 코드에서 원본만 바꿨는데 복사본 까지 바꿔서 출력이 된다.
포인터 변수에 대입한 값은 주소
- 변수를 그대로 대입했었다.
- 포인터의 경우 주소만 복사
105라는 주소가 저장되어있고 뒤에 100 주소 저장 name에 있던 105가져오고 마찬가지로 100 저장
lastname은 뒤에 105 부분, firstname은 앞의 100 부분의 주소를 가리키고 있다.
같은 스타일 C 문자열을 가리키니 하나를 바꾸면 당연히 다른 것도 바뀐다.
name.lastname[0] ="N";
- 둘 다 같은 주소를 가리키고 있음, 즉 둘다 “NEE”를 가리킴
이렇게 실제 데이터가 아니라 주소를 복사하는 걸 얕은 복사 라고 한다
깊은복사
- 이럴떈 깊은 복사를 하는게 맞는데 이건 대입만으로는 안 된다.
- 구조체 변수마다 독자적인 메모리 공간 만들어주고 거기에 문자열을 복사해야함.
- 동적 메모리 관련
파일 읽고 쓸 떄도 비슷한 문제 발생
먼저 구조체 만듬. 구조체의 배열 만들고 4개 저장하겠다고 선언. 처음 꺼에 teemo 이런식으로 쭉 선언
nums_name만큼 이름 가지고 있다. 주소로 들어가고 이게 시작주소로 들어가고 바이트 수 알려준다.
파일 읽고 쓸 떄도 비슷한 문제 발생
num_names가 2일꺼고 연 다음에 읽을거.
읽은 다음에 화면에 출력해보면 예외뜬다.(툴에서 띄울 듯 아마)
이름 저장한게 아닌가? 뭘 저장한거지
- 디버깅 하니 이상한 값들이 들어온다
파일을 열어보니 생각한 값이 아닌 다른 값이 들어감.
아래는 아까 저장할 떄 보던 숫자인데..?
포인터는 주소를 저장. 그래서 주소가..
size_t size;
size = sizeof(name_t);
- name_t의 크기를 보니 역시나 8바이트
즉. 주소 2개만 들어있음
- 파일에 저장할 떄 C는 문자열로 저장한 게 아니다.
- 그 문자열이 메모리 어느 주소에 있는데, 그 문자열이 메모리어느 주소에 있다. 그 주소를 저장한 것.
주소가 4바이트라면 8바이트를 데이터에 저장한 것. 그 주소 위치 였던 것임. 그랬을 떄 sizeof() 가 구조체하면 8바이트 밖에 안 나옴. 그래서 주소만 저장해 둠. 주소를 저장하면 주소를 프로그램 재 실행해서 주소를 읽어옴.
- 그럼 프로그램 재 실행 하면 문자열이 똑같은 위치에 들어가있는건가?
- 아니다. 프로그램 키고 끄는 순간 문자열은 다른 주소에 저장이 된다.
- 그래서 파일에 이 주소에 그 떄 문자열이 있었는데라고 그 문자열 주소를 읽어왔는데 다시 가보면 그 문자열 주소가 엉뚱한 것일 수 있다. 혹은 읽으면 안되는 메모리 일수있다.
주소라는 건 프로그램이 도는 순간 그때 그 순간에 어떤 위치에 그 문자열 배열이 들어가 있었을 뿐인 것이다.
- 다음에 실행시 그 위치에 있다는 보장이 없다.
- 문제는 포인터. 포인터를 없애야 🤔. 포인터가 실수하기 쉽다는 게 이런 이유들 때문
구조체 사용 시 포인터 저장의 문제
포인터 변수 없는 name_t 크기는 64
함수의 인자로 전달해도 64 -> 배열을 복사하는 법을 발견?
위처럼 바꾸고 실행해보면 잘 실행이 된다.
포인터만 없으면 된다. 포인터만
- 반드시 그럴 수 없겠지만
- 가능한 이렇게 한 덩어리 메모리에 모든 데이터가 들어가고 대입 가능한 구조체를 만드는 게 좋다.
- 즉, 포인터만 없으면 됨.
- 그래도 뭘 해야 편안
구조체를 다른 구조체의 멤버로 사용하기, 바이트 정렬
홈 파일안에 몇 바이트가 쓰인건가
계산하면 76바이트가 들어가야 정상
막상 계산 해보면 80바이트 출력
위 예시에서 키 나이 저장하는 메모리는 아래와 같이 구성
height, age도 2바이트 먹어야 하는데 4바이트
바이트 정렬 요구사항 때문에 구멍이 생김.
- 살펴보니 4바이트를 안 채운 애들이 공간을 먹음
- 이건 각 시스템 마다 메모리에 접근할 때 사용하는 주소에 대한 요구사항이 다르기 때문
- 시스템 상 제약이 있거나
- 효율성 때문
- 패딩이라고 표현하기도 함.
- 어떤 시스템은 n 바이트배수인 시작 주소에서만 메모리 접근 가능
- x86 시스템은 4바이트(워드 경계) 에서 읽어오는 게 효율적
- 이걸 4바이트 경계에 정렬된다(aligned)라고 한다.
- 따라서 컴파일러가 알아서 각 멤버의 시작위치를 경계에 맞춤.
- 그러기 위해 안 쓰는 바이트를 덧붙임(padding)
- 32비트 clang 윈도우는 4바이트 정렬을 하려고 함
- 따라서 어떤 아키텍쳐에서 저장한 파일을 다른 아키텍처에서 읽으면 잘못 읽힐 수도 있다.
현실로 비교해보면 줄 세우는 것과 비슷
패딩 줄이기
- 이렇게 하는 것 만으로도 없어짐
- 크기도 76
- 2개의 short형 변수가 4바이트로 합체됨.
#pragma pack 쓰는 방법도 있긴 함
- 크기도 우리가 원하는 76
- 그러나 표준은 아님
- 요즘 컴파일러들이 잘 지원해주는 것 뿐..
구조체 베스트 프렉티스
구조체를 파일이나 다른데 저장해야해서 바이트 크기가 정확히 맞아야 한다면?
- 보통 assert()를 사용
#include <assert.h> assert(sizeof(user_info_t)==76)
어쩔 수 없이 패딩이 생길거라면 구조체에 패딩을 명시적으로 넣기도 한다.
typedef struct{
unsigned int id;
name_t name;
float weight;
unsigned char height;
unsigned char age;
char unused[2];
}user_info_t
- 특히 데이터 전체 크기가 4바이트로 안 나눠 떨어질 때
비트 필드
- 구조체는 여러 데이터를 한 곳에 모아두는 데 좋았다. 거기서 무슨 패딩이 들어가고 그런 문제를 잡기 위해서 여러가지 것들을 했다.
- 근데 내가 정수형 데이터를 쓰는데 32비트가 필요가 없다. 그러면 뭐 16비트를 줄일 수도 있고, 8비트로 줄일수도 있고 이런 것들이 있다. 혹은 24비트 , 8바이트 이런식으로 나눈다던가.딱 8비트 16비트 정해져서 나눠지는 게 아니라 내가 표현하는 값은 6비트에 딱 들어간다. 그럼 6비트에 들어가고 4비트에 4비트 넣고 표현하고. 그리고 모든 변수를 4,6비트, 이런거 합쳐서 하나의 구조체로 만드는 것. 그게 바로 비트필드.
비트필드는 비트 플래그 개념과 유사하다.
비트플래그 : bool 여럿을 효율적으로 저장
8개 이하의 bool 값을 하나의 byte에 저장하는 방법
- C#에는 bool형이 있다.
- C는 없어서 int형
- 원래는 8*4 바이트 = 32바이트
- 근데 한 byte만 사용하니까 이득
구조체와 비트 플래그
비트플래그 구조체 사용 예
실제 메모리와 비교
bitflags_t라는 구조체 만들고 bitflags_t만들고 flags는 0으로 초기화를 함 0으로 다 지우고 디버그 뷰에도 0 나옴 이게 1바이트(8비트)라고 하고 b0부터 시작했으니 1,2,3,4를 1로 바꾸고 싶다. 1로 바뀌나. 위 flags 디버깅뷰는 바뀐다. 실제 메모리도 1 먹으면서 이런식으로 저장할 수 있는 방법
플래그 전체를 한번에 체크하려면?
- 지금 방식은 멤버 함수들끼리 비교만 가능
- 구조체 전체가 0(모든 플래그가 거짓)인지 비교하고 싶은데 안됨
근데 포인터를 알면 할 수 있다.
- 근데 이렇게 하면 안됨
- 이걸 제대로 좀 더 해주는 C언어 기능이 있다.
- 그게 공용체
공용체
- C에서 거의 안 쓰지만 가끔 쓰일때가 있기는 함.
- 공용체도 메모리를 어떻게 읽을거라는 내요오가 밀접
공용체란
- union
- 똑같은 메모리 위치를 다른 변수로 접근하는 방법
- 즉, 공용체 안에 있는 여러 변수들이 같은 메모리를 공유
- val, bit 두개로 접근이 가능하다.
메모리 공유만을 위한 공용체
- 한 메모리 공간을 용도에 따라 다른 기본 데이터형으로 읽을 때 사용
- 앞의 예보다 덜 유용
- 사용하기도 어렵고 실수하기도 쉽다.
함수 포인터
- 포인터란? 누가 물으면
- 메모리 주소를 저장하는 변수
switch문 사용한 사칙연산 프로그램
- 이 경우 switch말고 다른 방법 없나?
사칙연산은 피연산자 2개고 이름만 다른데..
- 언제나 변하는 피연산자 매개변수 전달하듯이
함수 호출할 떄는 직접 함수명을 썼는데 어셈블리는 그 함수의 주소로 점프
어셈블리어한텐 어떤 메모리 주소로 점프해서 실행하겠다라는 뜻
- 실행 중에 다른 코드로 이동하는 경우도 있다
함수에서 반환할 떄 돌아가야 하는 호출자 코드의 주소.
- 돌아갈 주소는 스택 메모리에 저장되어 있었다.
- 돌아갈 주소가 스택에 들어있다는 의미?
- 실행 도중 결정된다는 뜻.
호출하고 실행할 주소 들어가 있음. 어셈블리 명령어가 실행 될 떄 스택에 푸시하고 다 된 다음 돌아갈 떄 없애고 메모리 주소 보고 실행주소 여기고 호출이 누가 하느냐에 따라 바뀌는 주소. 즉 실행 도중 바뀔수 있는 주소.
어셈블리가 찾아서 넣어주는 주소가 아니라 실행 도중 바뀌는 주소(호출자에 따라)
즉, 모든 것이 다 메모리 주소.
그럼 실행 도중 조건에 따라 어떤 함수를 실행해 주려면 무슨 주소를 변수에 기억해야 하나? -> 그 함수의 시작 주소
실제 어셈블리 보면 모든 코드 앞에 메모리 주소가 있고 함수의 시작 코드도 마찬가지다.
함수를 매개변수로 전달할 때 필요한 것들, 함수 포인터 선언
함수 코드 시작 메모리 넣으면 될듯? 그걸 실행해 달라고 하면 되고
- 그래서 함수 포인터
- 즉 어떤 데이터가 저장된 주소가 아니라 그 함수 코드 시작하는 코드가 있는 그 주소다.
- 둘다 가능
- 보통 위에 것을 더 많이 쓰긴 함
이제 함수 호출하는 함수를 선언하자
double calculate(double, double, function*)
- 어떻게 선언하지 함수의 주소 저장하니 포인터긴 한데.
- 함수니까 자스마냥 function이라는 거나 자료형이 있지 않을까? - > 없다..
- 어떠한 함수도 담을 수 있는 자료형은 말이 안된다.
- 다양한 매개변수 목록가 반환형을 하나로 표현 불가
함수가 어떻게 실행되는지 생각해보자
- 매개변수로 전달된 함수가 무사히 실행되려면
- 그 함수에서 쓸 매개변수가 스택에 들어가 있어야한다.
double calculate(double, double, 매개변수로_전달된_함수*)
근데 여기서 앞의 두 double은 calculate()의 매개변수지 매개변수로전달된함수 의 매개변수가 아님 🤔
지금 모양에서는 컴파일러가 이 함수에 대해 유일하게 아는 정보는 함수 포인터이며, 그 크기는 4바이트
- 이 함수를 호출하려면 어떤 형의 매개변수를 몇개나 스택 메모리에 넣어야 하는지 모름
반환값이 있는지, 있으면 어떻게 가져다 써야하는지 모름
- 반환값이 있는지, 있다면 어떻게 가져다 써야하는지도 모른다.
함수가 어떻게 실행되는지 떠올려보자
- 그래서 함수 포인터를 이용해서 이 함수를 호출 시 컴파일러 입자엥선 생뚱맞은 상황이 된다.
- 왠지 모르지만 func가 double형 두개를 매개변수로 받고
- 왠지 모르지만 func가 반환한 값을 double형에 대입 가능
즉, 매개변수로 전달되는 함수는
함수의 매개변수로 전달되는 함수는 다음과 같은 내용도 있어야 한다.
- 자기 자신이 받아야 하는 매개변수 목룍
- 자기 자신이 반환하는 자료형
그러니 이런식의 모습은 아닐 거라는 점
double calculate(double, double, function*)
올바른 함수포인터는 이것
함수 포인터 선언
<반환형> (\*<변수명>)(<매개변수 목록="">) - 함수의 시작 주소를 저장하는 변수 - 함수의 매개변수 목로고가 반환형을 반드시 표기해야 함--- ### 함수 포인터 읽는 방법, 오른쪽-왼쪽 규칙(Right-Left Rule) #### 함수 포인터 읽는 법 - 오른쪽-왼쪽 규칙(RIght-LEft Rule) 이라고도 함 ``` double (*func) (double, double); ``` ![20221113_023250](https://user-images.githubusercontent.com/37941513/201487060-f42a5d37-9fa4-4b7e-a3a4-88082b382bad.png) 변수 func는 포인터다 ![20221113_030622](https://user-images.githubusercontent.com/37941513/201488422-a1ac643b-1403-464d-8eeb-a33b8e8efba5.png) 변수 func는 두개의 double형 매개변수 받는 함수의 포인터 결국 두개의 double형 매개변수 받고 double형 반환하는 함수의 포인터다. #### 함수 포인터 읽는 방법 ![20221113_031112](https://user-images.githubusercontent.com/37941513/201488593-f6ee029b-9a41-4280-8bfb-d695770deab4.png) ![20221113_031212](https://user-images.githubusercontent.com/37941513/201488638-ffbb3aae-bdc0-42c7-b827-fab164aafd70.png) ![20221113_031236](https://user-images.githubusercontent.com/37941513/201488655-6c5ccafe-10f4-4607-95ec-dbf849e0781f.png)
--- #### 좀 더 복잡한 경우 ![20221113_031927](https://user-images.githubusercontent.com/37941513/201488933-2f3c52d6-af63-4400-a693-2892766e3c7e.png) 코드를 분해해서 이해해야한다 ![20221113_032041](https://user-images.githubusercontent.com/37941513/201488989-a2f65f5e-70e5-4d8a-a33e-eac725b9ff82.png)
--- ### 배열의 포인터, 퀵 정렬, void 포인터 #### 배열의 포인터 - 지금까지 포인터를 사용해서 배열 접근 했다. ![20221113_032614](https://user-images.githubusercontent.com/37941513/201489180-bbccef81-f871-44c5-8f85-4d38090d7eae.png) - 그런데 배열 전체를 다 가리키는 포인터도 있다.(배열의 요소가 아닌) - 바로 배열의 포인터(pointer to array) ![20221113_032719](https://user-images.githubusercontent.com/37941513/201489209-1b0873fe-b38b-4467-b233-69c3965c8e3b.png) - 배열도 결국 어디 메모리 주소에 저장된 데이터이므로 그걸 포인터로 들고온다. #### 배열의 포인터 ![20221113_032953](https://user-images.githubusercontent.com/37941513/201489315-7462991a-0f13-4d16-9f8a-6214489b1f50.png) ![20221113_033107](https://user-images.githubusercontent.com/37941513/201489363-2831a076-01ae-4ce4-8b76-853c295ce443.png) 참고로 위 매개변수 받을 때 [] [ 10 ] 으로 2차원 받는게 표준이다. ![20221113_033158](https://user-images.githubusercontent.com/37941513/201489393-0450555e-851a-4405-9759-c49d11bc8a30.png) #### 함수 포인터 예: 퀵정렬 ![20221113_033248](https://user-images.githubusercontent.com/37941513/201489417-4950501d-a4f2-4285-a067-2b11b89d34c5.png) - 일반정렬은 o2로 되게 느리게 도는데 퀵 정렬은 log n으로 빠른 속도로 도는게 가능하다. - 이 함수가 존재하는 이유는 만약int나 float만 정렬하면 그 자료형밖에 못하지만 이 함수를 쓰면 구조체 같은 것들도 정렬이 가능해진다. - 결과적으로 어떤 데이터라도 처리가 가능하다. - ptr으로 정렬하고자 하는 배열이 들어온다. 그 배열의 시작 주소 - count는 그 배열에 들어가 있는 요소의 수 - comp라는 건 어디로 가라는 포인터. 그 포인터인데 매개변수 2개를 받는 포인터이므로 함수 포인터다. ![20221113_115545](https://user-images.githubusercontent.com/37941513/201503545-10b2f548-6211-403e-85f8-48bdc6b717c9.png) #### void 포인터 - 범용적 포인터 - 어떤 포인터라도 대입 가능 - 그 반대라도 그냥 대입 가능 - 따라서 어떤 변수의 주소라도 곧바로 대입 가능 - 매개변수형으로 void\* 를 사용시 어떤 포인터도 받을 수 있는 함수 탄생(int형 포인터든 float형이든 구조체 포인터든 ) - 단, 다음과 같은 경우 다른 포인터로 캐스팅 또는 대입해서 써야함 - 역참조(몇 바이트 읽을지 모르기 떄문) - 포인터 산술 연산(몇 바이트 이동할 지 모르기 때문) #### void의 예 ![20221113_120355](https://user-images.githubusercontent.com/37941513/201503648-48a9f07c-1db0-476e-b9f5-507ee65f1918.png)
--- #### 다른 언어에도 함수포인터가 있다 C# ![20221113_120555](https://user-images.githubusercontent.com/37941513/201503746-c198f019-ee15-440f-bb6e-85656a61c319.png) javascript ![20221113_120719](https://user-images.githubusercontent.com/37941513/201503747-46276905-dd17-40c1-bde6-ef58fd36d59c.png)
--- #### 정리 1. 구조체 2. typedef 3. 비트필드 4. 공용체 5. 함수 포인터 6. 오른쪽-왼쪽 규칙 7. 퀵 정렬 8. void\* 구조체를 배우면서 왜 실수 막는데 좋은지, 어떻게 데이터를 묶는지, 메모리에 데이터가 어떻게 들어가고 패딩이 어떻게 들어가고 콤팩트 시키게 만드는지를 봤다. 또 구조체 배열을 통해 복사라던지 이런 점. 이런걸 이해하지 않으면 구조체가 왜 값형인지, 클래스가 왜 참조형인지 그리고 C++같은데 가면 왜 클래스 구조체를 값형으로 다 전달할 수 있는지, 어떤 경우에 참조형이 되는지 이걸 다 이해 가능하다. _그냥 클래스는 참조형_ 이고 _그냥 기본형은 값형_ 이라고 하는 걸 왜 그렇게 도는지 알 수 있었어야 한다.
구조체의 추가로 비트필드라는 것도 배웠따. 구조체에 각 데이터 멤버들 원래대로 하면 데이터 형에 따라 비트수가 결정되느넫, 그것보다 더 적은 데이터를 쓰고 싶다. 그럼 적은 데이터를 넣어서 훨씬 더 촘촘하게 크기가 작은 구조체를 만들겠다라는 뜻.
함수 포인터는 포인터의 종류 중 하나이며, 함수 포인터 이해하려면 함수 호출되는 방식을 알아야 했고 스택 메모리가 어떻게 도는지 알아야 했고, 굳이 어셈블리어 보면서 여기까지 온 이유가 함수포인터 내부적으로 도는지 알기 위함.
함수 포인터는 개념상으로는 어렵지 않음. 이 함수 쓸 것이므로 포인터를 가지고 있다가 그냥 호출해 달라 요청하면 된다.(매개변수 넣어서) 그런데 매개변수 어떤거 받는지 선언
반환형 어떻게 할지 선언하고 그 함수 포인터 선언해주는 문구 자체가 암호처럼 보이던걸 오른쪽 왼쪽 규칙으로 읽었다. 이건 영어권에서 시작해서 영어 문법이라 생각하면 됨. 함수 선언만 읽을떄는 오른쪽 왼쪽 규칙 해서 영어처럼 읽는게 제일 베스트일 것
함수 포인터 배운 김에 퀵정렬도 배웠다. 정렬중 제일 쉬운건 버블정렬 퀵정렬은 이미 라이브러리가 제공되는 함수. 그 라이브러리는 어떤 데이터형에도 쓸 수 있으면 좋겠다. 그럼 퀵정렬이 알아야 하는건 배열하나 받고 배열이 몇 바이트이닞 알고 요소가 몇개인지 알고, 그러면 거기서 모든 요소들 쌍 하나씩 뽑아서 시작위치들, 두 쌍의 시작위치들을 비교함수에 전달. 그럼 qsort()에 전달해주고 비교함수에서 이 2개 데이터 주소 오면 그 데이터를 내가 원하는 포인터로 원래는 void 포인트였으니까 포인터로 대입해서 필요한 데이터필드로 접근해서 어떤게 먼저 나와야 하느닞를 알려준다.
void\* 포인터는 어떤 데이터형도 받을 수 있는 매개변수, 그건 void포인터 밖에 못한다. void포인터 받고 함수에서 변경, 캐스팅 해가면서 변환해가면서 쓰는 것. 그 변환이 잘못된거면 위험하다. 퀵정렬 하면서 비슷한게 기수정렬(radix sort)이라 하는데 퀵정렬의 일부로 보기도 함. 퀵정렬은 순수 데이터 비교지만 기수정렬은 기준이 있을 때 비교 한번으로 그 기준에 맞춰 나오게 하는 것. 기수정렬이 실제 기수정렬 배울떄 그 똑같은 기수정렬은 아니지만 데이터 패킹하면 기수정렬이 가능하다.
-------- 매개변수>변수명>반환형>