Seuraava alkoi mietityttämään:
/* struct_person.h */ struct person { const char *name; };
/* print_person.h */ void print_person(struct person *p);
/* print_person.c */ #include "struct_person.h" #include "print_person.h" void print_person(struct person *p) { puts(p->name); }
/* main.c */ #include <stdio.h> #include "struct_person.h" #include "print_person.h" void main() { struct person petteri = {"Petteri"}; print_person(&petteri); }
/* makefile */ print_person.o: print_person.c print_person.h struct_person.h gcc -c print_person.o main.o: main.c print_person.h struct_person.h gcc -c main.c run: main.o print_person.o gcc main.o print_person.o -o run
Jotta sain tuon toimimaan, minun pitää antaa preprosessorille struct_person.h sekä main.c:ssä että print_person.c:ssä. Aiheuttaako tämä mahdollisesti jotain konfliktia tai vaaratilannetta? Tai tehottomuutta? Onko parempaa ratkaisua? Ainakin oman käsityksen mukaan tuossa määritellään struct person kahdesti, joka vaikuttaa huonolta ja mahdollisesti vaaralliselta ratkaisulta.
Melko yleinen tapa on lisätä ehtolauseita header-tiedostoihin siten, että ne suoritetaan vain kerran:
#ifndef _STRUCT_PERSON_H #define _STRUCT_PERSON_H struct person { const char *name; }; #endif
Saman structin määritteleminen kahdessa eri c-tiedostossa saman headerin kautta on ihan ok.
Kiitos. Tuo selvensikin asian.
fergusq kirjoitti:
Saman structin määritteleminen kahdessa eri c-tiedostossa saman headerin kautta on ihan ok.
Kääntäjä ei välitä siitä, onko sama määrittely includetettu useampaan kertaan yhdestä otsikkotiedostosta vai onko se tuotu useista eri tiedostoista; jo olemassa olevan määrittelyn tekeminen uudestaan johtaa virheeseen. Virhe tapahtuu silloin, kun yhden käännösoperaation aikana ladataan määrittelyn tekevät otsikkotiedostot useammin kuin yhden kerran. Juuri sen takia include guardseja käytetään, että ne määrittelyt tapahtuvat vain yhden kerran. (Otsikkotiedon toisella latauskerralla include guardin vuoksi kaikki sen sisällä oleva koodi jätetään huomiotta.)
(Edelliset vastaukset eivät vastaa itse kysymykseen, koska kysymys ei ollut samasta structista samassa käännösyksikössä (samassa c-tiedostossa) vaan useassa eri käännösyksikössä (eri c-tiedostossa), jotka sitten linkitetään samaan ohjelmaan.)
Kuten olet huomannut, rakenne täytyy määritellä jokaisessa c-tiedostossa (tietenkin includen kautta), koska muutenhan koodia ei voi kääntää.
C:ssä erillisten tiedostojen käännöksen jälkeen ei ole mitään tarkastusmekanismia, joka tutkisi, ovatko eri tiedostot yhteensopivat. On siis mahdollista tehdä ja linkittää C-koodeja, joissa samalla nimellä on ristiriitaisia merkityksiä:
// f.c struct A { int x; }; void f(struct A* a) { a->x = 1111111111; }
// main.c #include <stdio.h> struct A { float x; // huom, eri tietotyyppi }; extern void f(struct A* a); int main() { struct A a; f(&a); printf("%f\n", a.x); // tulos: 46.552517 return 0; }
Ohjelman tuloste ei siis suinkaan ole 1111111111 vaan 46.552517 (x86-64-arkkitehtuurilla), koska rakenteeseen sijoitetut bitit tulkitaan väärin.
Tällaisen ongelman voi aiheuttaa vahingossa include-riveillä esimerkiksi silloin, kun otsikkotiedostossa olevaa määrittelyä muutetaan mutta Makefilestä puuttuu tarvittava riippuvuus ja jokin objekti jää kääntämättä uudestaan.
Ongelma voi ilmetä vakavammin esimerkiksi ohjelman kaatumisena, jos rikkoutuminen koskee vaikka osoittimia tai taulukon indeksejä tai jos rakenteen koko muuttuu.
Itse asiassa C:ssä on mahdollista tehdä vielä räikeämpi virhe ja vaihtaa koko funktion parametrin tyyppiä, koska C:ssä ei ole samannimisten funktioiden ylikuormitusta. C++ estää sentään tuon, mutta siinäkin on edellä kuvattu ongelma tietotyypin muuttumisen suhteen.
Alkuperäiseen kysymykseen liittyvä kysymys: onko niin, että C:ssä voi käsitellä samaa arvoa kahtena eri structina, kunhan structit on määritelty identtisesti?
#include <stdio.h> struct A { int x; }; struct B { int y; }; int main() { struct A a; struct B *b; a.x = 3; b = &a; printf("%d\n", b->y); }
Olen ennen ajatellut, että jotkut kääntäjät saattavat jostain omituisesta syystä säilöä A:n ja B:n arvoja eri tavalla muistissa, mutta nyt tajusinkin, että kääntäjänhän on kai pakko säilöä ne samalla tavalla, koska muuten tuo alkuperäisen viestin ongelma ei toimisi (saman structin on käännyttävä samalla tavalla joka kerta).
fergusq kirjoitti:
Alkuperäiseen kysymykseen liittyvä kysymys: onko niin, että C:ssä voi käsitellä samaa arvoa kahtena eri structina, kunhan structit on määritelty identtisesti?
Ainakin käytännössä kyllä. C:ssä samanlaiset tietueet tallentuvat lähtökohtaisesti samalla tavalla. Tätä hyödynnetään mm. alkeellisessa perinnässä, jolloin tietueen alussa kerrotaan sen tyyppi ja eri tietueet on koottu yhdistelmätyypiksi (union) tai osoitin muuten vain väännetään sitten sopivaksi.
struct MouseMoveEvent { int type; int x, y; }; struct KeyboardEvent { int type; int keycode; }; union Event { int type; struct MouseMoveEvent mousemove; struct KeyPressedEvent keypress; }; void handle(struct Event *e) { switch (e->type) { case EVENT_TYPE_MOUSE_MOVE: printf("mouse moved: %d, %d\n", e->mousemove.x, e->mousemove.y); break; case EVENT_TYPE_KEY_PRESSED: printf("key pressed: %d\n", e->keypress.key); break; default: printf("unknown event: %d\n", e->type); break; } }
fergusq kirjoitti:
Alkuperäiseen kysymykseen liittyvä kysymys: onko niin, että C:ssä voi käsitellä samaa arvoa kahtena eri structina, kunhan structit on määritelty identtisesti?
[..] kääntäjänhän on kai pakko säilöä ne samalla tavalla [..]
Standardin mukaan näin ei saa tehdä (ilman unionia). Edes castin lisääminen esimerkkiisi ei auta, eli b = (struct B *)&a;
on myös väärin. Syy ei ole se, että structit tallennettaisiin muistiin eri tavalla, vaan se, että tämän salliminen hankaloittaisi tai estäisi joitain optimointeja.
int f(struct A *a, struct B *b) { b->y = 1; a->x = 42; return b->y; }
Kääntäjällä on lupa olettaa, että a ja b osoittavat eri objekteihin ja optimoida viimeinen rivi muotoon return 1;
.
Poikkeuksena on, että char-pointterin kautta saa käsitellä minkä vain tyyppisiä arvoja. Tämä siksi, että memcpy yms. olisi mahdollista toteuttaa.
tl;dr: T-tyyppistä objektia saa käsitellä vain T- tai char-tyyppisen pointterin kautta.
Lisäys (2.8.): http://stackoverflow.com/questions/98650/what-is-the-strict-aliasing-rule
Aihe on jo aika vanha, joten et voi enää vastata siihen.