Чи хороша ідея вводити покажчики def?


75

Я переглянув якийсь код і помітив, що умова полягає у перетворенні типів покажчиків на зразок

SomeStruct* 

в

typedef SomeStruct* pSomeStruct;

Чи є в цьому якась заслуга?


1
Це поширене у C. Оскільки ви намагаєтеся приховати той факт, що це вказівник. Це об’єкт, який ви передаєте всім інтерфейсам у бібліотеці. У C ++ це не часто і не рекомендується, хоча це і нечувано.
Мартін Йорк,

4
Цікава стаття на цю загальну тему: Бесіди: Божевілля в літню ніч. Дивіться також Стиль кодування ядра Linux, щоб отримати крайній вигляд "не вводити вказівники (і не надавати вказівки типу для структур чи об'єднань)".
Джонатан Леффлер

Відповіді:


108

Це може бути доречним, коли сам вказівник можна розглядати як "чорний ящик", тобто фрагмент даних, внутрішнє представлення якого не повинно мати значення для коду.

По суті, якщо ваш код ніколи не розмежовує покажчик, а ви просто передаєте його навколо функцій API (іноді за посиланням), тоді typedef не тільки зменшує кількість *s у вашому коді, але й пропонує програмісту, що вказівник не повинен з вами насправді не втручаються.

Це також полегшує зміну API в майбутньому, якщо виникне необхідність. Наприклад, якщо ви перейдете до використання ідентифікатора, а не вказівника (або навпаки), існуючий код не зламається, оскільки вказівник ніколи не повинен був бути розменованим.


9
Приклад із реального світу: в OS X фреймворк CoreFoundation широко використовує цю техніку, називаючи їх "непрозорими" типами даних.
hbw

3
Також в OS X: pthread_t - непрозорий тип; це typedef'ed, щоб бути вказівником на 'struct _opaque_pthread_t', який сам є непрозорим масивом байтів.
Адам Розенфілд,

13
Приклад лічильника: FILE * fp? Насправді, я з вами погоджуюсь, але є достатній прецедент протилежного.
Джонатан Леффлер,

11
'Звичайно, якщо ви зробите це, ви не називаєте непрозорий тип "pFoo".
MSalters

3
@MattJoiner: причину FILE*не було визначено типом, оскільки непрозорий вказівник полягає в тому, щоб дозволити макроси, такі як, getc()і putc()безпосередньо маніпулювати структурою FILE та уникати накладних викликів функції в більшості випадків.
chqrlie

74

Не з мого досвіду. Якщо приховати ' *', код важко читається.


1
Не знаю про це. Я маю на увазі, якщо у вас є вказівник, скажімо, Deviceі ви typedefйого Device_Ptr(замість того, щоб використовувати Device *) або подібний, він цілком читається. Я бачу багато дозрілих і досить великих бібліотек, які роблять це. Особливо в C ++, якщо ви додаєте шаблони, це може пощадити ваші пальці.
rbaleksandar

@rbaleksandar знову це нормально, якщо Device * є непрозорим дескриптором.
Олексій

Як C n00b, я думаю, синтаксис вказівника C є мерзотою - подобається лише тим нещасним душам, котрі вже витратили 10+ років, навчаючись подобатися. Я б взяв myTypePtrабо взяв би myTypeRefцю зірку в будь-який день. : P
Christoffer Bubach

28

Єдиний раз, коли я використовую покажчик всередині typedef, це при роботі з покажчиками на функції:

typedef void (*SigCatcher(int, void (*)(int)))(int);

typedef void (*SigCatcher)(int);

SigCatcher old = signal(SIGINT, SIG_IGN);

В іншому випадку я вважаю їх більш заплутаними, ніж корисними.


Закреслена декларація є правильним типом для вказівника на signal()функцію, а не для вловлювача сигналу. Це можна зробити зрозумілішим (використовуючи виправлений SigCatcherтип вище), написавши:

 typedef SigCatcher (*SignalFunction)(int, SigCatcher);

Або, щоб оголосити signal()функцію:

 extern SigCatcher signal(int, SigCatcher);

Тобто a SignalFunction- це вказівник на функцію, яка приймає два аргументи (an intі a SigCatcher) і повертає a SigCatcher. А signal()сама є функцією, яка приймає два аргументи (an intі a SigCatcher) і повертає a SigCatcher.


Чи працює цей typedef з меншими знаками пунктуації? "typedef void SigCatcher (int, void (*) (int)) (int)"
sigjuice

Ні; gcc говорить "помилка: SigCatcher оголошено функцією, що повертає функцію".
Джонатан Леффлер

Я щойно натрапив на ще простішу декларацію typedef'd сигналу () freebsd.org/cgi/man.cgi?query=signal
sigjuice

@Sigjuice: навіть не переходячи на сторінку BSD (що я зробив лише після того, як відредагував матеріал), я побачив величезні проблеми з моєю відповіддю, які тепер виправлені. І те, що BSD називає, sig_tвідповідає моєму SigCatcher, і так, це signal()надзвичайно спрощує декларацію .
Джонатан Леффлер,

1
круто. Еквівалентна декларація з явним * не виглядає так погано, IMHO. typedef void SigCatcher (int); зовнішній сигнал SigCatcher * (int, SigCatcher *);
sigjuice

16

Це може допомогти вам уникнути деяких помилок. Наприклад, у наступному коді:

int* pointer1, pointer2;

pointer2 - це не int * , це простий int . Але з typedefs цього не станеться:

typedef int* pInt;
pInt pointer1, pointer2;

Вони обидва зараз int * .


13
Якщо ви ставите * зворушливий покажчик1 замість того, щоб торкнутися "int", у оголошенні набагато більше сенсу, що pointer2 - це не вказівник, а лише int.
dreamlax

3
Я думаю, програмісти C стверджуватимуть, що використання оператора розмежування посилань при оголошенні змінної вказівника - це більше ідіоматична річ. Для вивчення в K&R вони кажуть: "'int * ip' призначений мнемонікою; там сказано, що вираз '* ip' - це int."
hbw

2
Але всі знають цей синтаксис, і все-таки найкраще оголосити та ініціалізувати одну змінну за раз ... Я рідко, якщо коли-небудь пишу int * pointer1, * pointer2; тому що в наступному рядку у вас є два приємні вказівники з невизначеним значенням.
Даніель Даранас,

2
@Daniel Daranas:int *p1 = NULL, *p2 = NULL;
Matt Joiner

2
Типовий приклад захисту поганої практики з ще більшою кількістю поганої практики. Ніколи не оголошуйте більше однієї змінної в одному рядку, тому що ніколи немає причин для цього, крапка. Це призведе лише до помилок. І цілі суперечки, пов’язані із «вказівником», - це нісенітниця, адже якщо ви пишете хороший код, то місце розташування *не має значення.
Лундін

15

Моя відповідь - чітке "Ні".

Чому?

Ну, по-перше, ви просто обмінюєте одного персонажа *на іншого p. Це нульовий виграш. Одне лише це повинно заважати вам робити це, оскільки робити погані зайві речі, які безглуздо, завжди погано.

По- друге, і це важлива причина, несе це означає , що не хорошо приховувати . Якщо я передаю щось такій функції*

void foo(SomeType bar);

void baz() {
    SomeType myBar = getSomeType();
    foo(myBar);
}

Я не сподіваюся, що значення слова myBarбуде змінено, передавши його foo(). Врешті-решт, я передаю значення, тому foo()лише коли-небудь бачу копію myBarтак? Не тоді, коли SomeTypeпсевдонім означає якийсь вказівник!

Це стосується як вказівників на С, так і розумних вказівників на С ++: якщо ви приховуєте той факт, що вони є вказівниками для ваших користувачів, ви створите абсолютно непотрібну плутанину. Тож, будь ласка, не псевдонім своїх покажчиків.

(Я вважаю, що звичка визначати типи вказівників - це просто помилкова спроба приховати, скільки зірок є у програміста http://wiki.c2.com/?ThreeStarProgrammer .)


+1. Інша причина полягає в тому, що не буде типової системи захисту від void *vp=/*...*/; foo(vp);або foo(0);. (Typedefs для перелічень та числові typedefs для речей, які ніколи не повинні відображатися, оскільки числа мають подібні проблеми).
PSkocik,

5

Це питання стилю. Цей код ви часто бачите у файлах заголовків Windows. Хоча вони, як правило, віддають перевагу версії з великими літерами, а не префіксом з малої літери p.

Особисто я уникаю такого використання typedef. Набагато зрозуміліше, коли користувач прямо заявляє, що хоче Foo *, ніж PFoo. На сьогоднішній день Typedef найкраще підходять для читання STL :)

typedef stl::map<stl::wstring,CAdapt<CComPtr<IFoo>> NameToFooMap;

5

Це (як і стільки відповідей) залежить.

У C це дуже часто, оскільки ви намагаєтеся замаскувати, що об'єкт є покажчиком. Ви намагаєтеся зрозуміти, що це об’єкт, яким маніпулюють усі ваші функції (ми знаємо, що це вказівник знизу, але він представляє об’єкт, яким ви маніпулюєте).

MYDB   db = MYDBcreateDB("Plop://djdjdjjdjd");

MYDBDoSomthingWithDB(db,5,6,7);
CallLocalFuc(db); // if db is not a pointer things could be complicated.
MYDBdestroyDB(db);

Під MYDB, ймовірно, є вказівник на якийсь об'єкт.

У C ++ це більше не потрібно.
Головним чином тому, що ми можемо передавати речі за допомогою посилання, а методи включені в декларацію класу.

MyDB   db("Plop://djdjdjjdjd");

db.DoSomthingWithDB(5,6,7);
CallLocalFuc(db);   // This time we can call be reference.
db.destroyDB();     // Or let the destructor handle it.

5

Ні.

Це зробить ваше життя нещасним, як тільки ви його змішаєте const

typedef foo *fooptr;
const fooptr bar1;
const foo *bar2

Є bar1і bar2однотипні?

І так, я просто цитую Гуру Трави Саттера. Багато правди вона говорила. ;)

- Редагувати -

Додавання посилання на цитовану статтю.

http://www.drdobbs.com/conversationsa-midsummer-nights-madness/184403835


Не могли б ви додати посилання на згадану вами статтю? До того ж він чоловік! :-)
Фабіо каже "

1
@Azat Ibrakov Це незначний момент, але хоча Герб, безумовно, хлопець, гуру, про якого він пише, - жінка. Тому доречно посилатися на неї в жіночому роді. Так, я знаю, що вона фактично є альтернативним его для Херба, але якщо ви прочитаєте всю серію, включаючи історію, розказану в перших 20-ти статтях, для неї має сенс бути такою, якою вона є.
dgnuff

4

Typedef використовується, щоб зробити код більш читабельним, але створення вказівника як typedef збільшить плутанину. Краще уникати вказівників typedef.


4

Дискусія розпочалась, припускаючи, що мова, що цікавить, - C. Впливи на С ++ не розглядались.

Використання aa покажчика typedef для нетегованої структури

Питання Розмір структури, яка визначається як вказівник, викликає цікавий побічний світ щодо використання typedefпокажчиків для (структури).

Розглянемо визначення типу бетонної (непрозорої) конструкції:

typedef struct { int field1; double field2; } *Information;

Деталі учасників є цілком дотичними до цього обговорення; важливо лише те, що це не тип непрозорого типу typedef struct tag *tag;(і ви не можете визначити такі непрозорі типи за typedefдопомогою тегу).

Поставлене питання полягає в тому, "як можна знайти розмір цієї конструкції"?

Коротка відповідь - "лише через змінну типу". Немає тегу для використання sizeof(struct tag). sizeof(*Information)Наприклад, ви не можете корисно писати , і sizeof(Information *)це розмір вказівника на тип вказівника, а не розмір типу структури.

Насправді, якщо ви хочете розподілити таку структуру, ви не можете створити її, крім динамічного розподілу (або сурогатних методів, що імітують динамічне розподіл). Немає способу створити локальну змінну типу структури, вказівники якої викликаються Information, а також неможливо створити staticзмінну області файлу (глобальну або ) типу структури, а також немає можливості вбудувати таку структуру (як на відміну від вказівника на таку структуру) на іншу структуру або тип об’єднання.

Ви можете - повинні - написати:

Information info = malloc(sizeof(*info));

Окрім того, що вказівник прихований у typedef, це є гарною практикою - якщо тип infoзмін змінюється, розподіл розміру залишатиметься точним. Але в цьому випадку це також єдиний спосіб отримати розмір конструкції та розподілити структуру. І немає іншого способу створити екземпляр структури.

Це шкідливо?

Це залежить від ваших цілей.

Це непрозорий тип - деталі структури повинні бути визначені, коли тип вказівника - typedefd.

Це тип, який можна використовувати лише з динамічним розподілом пам’яті.

Це тип безіменний. Вказівник на тип структури має ім'я, а сам тип структури - ні.

Якщо ви хочете застосувати динамічне розподіл, це, здається, спосіб це зробити.

Однак загалом це, швидше за все, спричинить розгубленість і гнів, ніж просвітлення.

Резюме

Загалом, це погана ідея використовувати typedefдля визначення вказівника на тип безступінчастої структури.


3

Якщо ви зробите це, ви не зможете створити STL-контейнери const pSomeStruct, оскільки компілятор читає:

list<const pSomeStruct> structs;

як

list<SomeStruct * const> structs;

який не є законним контейнером STL, оскільки елементи не можна призначити.

Дивіться це питання .


Звичайно, але ніколи не можна було б займатися написанням list<const ordinary_type>, оскільки це б і так не працювало, тому немає плутанини.

1
Я не впевнений, що ви маєте на увазі ordinary_type. list<const SomeStruct*>є цілком дійсним, як const SomeStruct*і CopyAssignable.
Ден Хук,

1

API Win32 робить це практично з усіма структурами (якщо не з усіма)

POINT => *LPPOINT
WNDCLASSEX => *LPWNDCLASSEX
RECT => *LPRECT
PRINT_INFO_2 => *LPPRINT_INFO_2

Приємно, наскільки це послідовно, але, на мій погляд, це не додає ніякої елегантності.


Це тому, що Win32 - це C API, і це часто зустрічається в C, а не в C ++
Мартін Йорк,

2
Питання позначене як C, так і C ++, в чому саме ваша думка?
dreamlax

1
Слід зазначити, що це також не є звичайною практикою в C, Win API є майже єдиною відомою бібліотекою C, яка робить це. Він також використовує угорські позначення. Це дуже незрозуміла стилістична бібліотека, це не те, що ви повинні виховувати як канон.
Лундін

1
Це позначення сходить до походження Windows 30 років тому в 16-бітовій сегментованій архітектурі Intel. LPрозшифровується як довгий покажчик, а скорочене позначення LPPOINT ptr;було нічим іншим, як зручною абревіатурою POINT FAR *ptr;, що FARрозширюється до farта пізніше до __far. Все це досить неелегантно, але раніше було ще гірше.
chqrlie

1

Мета typedef - приховати деталі реалізації, але typedef-ing властивості вказівника приховує занадто багато і ускладнює читання / розуміння коду. Тож не робіть цього.


Якщо ви хочете приховати деталі реалізації (що часто добре робити), не приховуйте частину вказівника. Візьмемо, наприклад, прототип для стандартного FILEінтерфейсу:

FILE *fopen(const char *filename, const char *mode);
char *fgets(char *s, int size, FILE *stream);

тут fopen повертає покажчик на якусь структуру FILE(для якої ви не знаєте деталей реалізації). Можливо, FILEце не такий вдалий приклад, тому що в цьому випадку він міг працювати з певним типом pFILE, який приховував той факт, що це вказівник.

pFILE fopen(const char *filename, const char *mode);
char *fgets(char *s, int size, pFILE stream);

Однак це спрацює лише тому, що ви ніколи не балуєтесь із вмістом, на який вказують безпосередньо. У той момент, коли ви набираєте десь вказівник, який ви де-небудь модифікуєте, код стає дуже важким для читання з мого досвіду.


0

Деякий час тому я відповів би "ні" на це запитання. Зараз, із зростанням розумних покажчиків, покажчики не завжди визначаються зірочкою «*». Отже, немає нічого очевидного в тому, що тип є вказівником чи ні.

Тож зараз я б сказав: добре вводити покажчики def, якщо чітко дається зрозуміти, що це "тип покажчика". Це означає, що ви повинні використовувати префікс / суфікс спеціально для нього. Ні, наприклад, "p" не є достатнім префіксом. Я б, мабуть, пішов із "ptr".


Якщо ви не маєте наміру використовувати його як вказівник, у цьому випадку ви можете назвати його як завгодно. Якщо він просто використовується для передачі речей (наприклад, C FILE *), чи має значення саме те, що це таке?
Девід Торнлі,

Влучне зауваження. Я намагався додати деякі плюси до списку мінусів, які були сказані проти вказівників набору тексту ...
Бенуа
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.