2012-08-23 2 views
18

После просмотра this question несколько минут назад я задавался вопросом, почему дизайнеры языка допускают это, поскольку это позволяет косвенную модификацию частных данных. В качестве примераПочему C++ позволяет частным членам изменять этот подход?

class TestClass { 
    private: 
    int cc; 
    public: 
    TestClass(int i) : cc(i) {}; 
}; 

TestClass cc(5); 
int* pp = (int*)&cc; 
*pp = 70;    // private member has been modified 

Я протестировал вышеуказанный код и действительно личные данные были изменены. Есть ли объяснение, почему это разрешено, или это просто надзор на языке? Кажется, это прямо подрывает использование частных данных.

+21

В C++, в тот момент, когда вы используете подобные ролики, вы сказали компилятору, что вы не заботитесь о его типе безопасности и проверках. Это не безопасность сети; компилятор позволит вам испортить ваши собственные вещи, если вы настаиваете, как и ваш код. – tenfour

+0

@tenfour Да, я понимаю, что я был немного удивлен, что это возможно. Я изменил C-cast на static_cast, а затем компилятор действительно жаловался. – mathematician1975

+2

вы действительно не должны использовать C-cast в C++. В основном они говорят, что компилятор «уходит, я знаю, что я делаю, и я беру на себя всю ответственность». Они еще хуже, чем reinterpret_cast (что также должно было сработать кстати. Static_cast не будет работать, потому что нет четко определенного способа делать то, что вы просите) –

ответ

31

Потому что, как выразился Бьярне, C++ предназначен для защиты от Мерфи, а не Макиавелли.

Другими словами, он должен защищать вас от несчастных случаев, но если вы идете на любую работу, чтобы подорвать ее (например, используя приведение), она даже не попытается остановить вас.

Когда я думаю об этом, у меня есть несколько другая аналогия в виду: это как замок на двери в ванной. Это дает вам предупреждение, что вы, вероятно, не должны хотите, чтобы идти туда прямо сейчас, но это тривиально, чтобы открыть дверь снаружи, если вы решите.

Редактировать: что касается вопроса @Xeo, обсуждается, почему в стандарте говорится, что «имеет тот же контроль доступа», а не «имеет общий контроль доступа», ответ длинный и немного извилистый.

Давайте шаг назад к началу и рассмотрим, как-структуру:

struct X { 
    int a; 
    int b; 
}; 

C всегда было несколько правил для структуры, как это. Один из них заключается в том, что в экземпляре структуры адрес самой структуры должен быть равен адресу a, поэтому вы можете наложить указатель на структуру на указатель на int и получить доступ к a с четко определенными результатами. Другим является то, что члены должны быть упорядочены в том же порядке в памяти, что и они определены в структуре (хотя компилятор может свободно вставлять между ними).

Для C++ было намерение поддерживать это, особенно для существующих структур C. В то же время было очевидное намерение, что , если, компилятор хотел ввести в действие privateprotected) во время выполнения, это должно быть легко сделать (разумно эффективно).

Поэтому, учитывая что-то вроде:

struct Y { 
    int a; 
    int b; 
private: 
    int c; 
    int d; 
public: 
    int e; 

    // code to use `c` and `d` goes here. 
}; 

Компилятор должен быть обязан поддерживать одни и те же правила, что и C относительно Y.a и Y.b. В то же время, если это будет обеспечивать доступ во время выполнения, он может хотеть, чтобы переместить все публичные переменные вместе в памяти, так что макет будет больше похож:

struct Z { 
    int a; 
    int b; 
    int e; 
private: 
    int c; 
    int d; 
    // code to use `c` and `d` goes here. 
}; 

Тогда, когда это исполнение вещей во время выполнения, он может в основном сделать что-то вроде if (offset > 3 * sizeof(int)) access_violation();

Насколько мне известно, никто никогда не делал этого, и я не уверен, что остальная часть стандарта действительно позволяет это, но похоже, что, по крайней мере, сформированный зародыш идеи вдоль этой линии.

Для применения обоих из них, С ++ 98 сказал Y::a и Y::b должен был быть в том порядке, в памяти, и Y::a должны были быть в начале структуры (т.е. С-подобные правила). Но из-за промежуточных спецификаторов доступа Y::c и Y::e больше не должны были быть относительно друг друга.Другими словами, все последовательные переменные, определенные без спецификатора доступа между ними, были сгруппированы вместе, компилятор был свободен для перегруппировки этих групп (но все же должен был сохранить первый в начале).

Это было хорошо, пока какой-нибудь рывок (то есть я) не указал, что способ написания правил имел еще одну небольшую проблему. Если бы я написал код вроде:

struct A { 
    int a; 
public: 
    int b; 
public: 
    int c; 
public: 
    int d; 
}; 

... у вас получилось немного самоуничтожения. С одной стороны, это была официально структура POD, поэтому правила C-like должны были применяться, но поскольку у вас были (по общему признанию, бессмысленные) спецификаторы доступа между членами, это также давало компилятору разрешение на переупорядочение членов, таким образом нарушая правила C, подобные им.

Чтобы исправить это, они немного переформулировали стандарт, чтобы он говорил о членах, имеющих одинаковый доступ, а не о том, существует ли между ними спецификатор доступа. Да, они могли бы просто постановили, что правила будут применяться только к публичным членам, но, похоже, никто не видел ничего, что можно было бы извлечь из этого. Учитывая, что это модифицировало существующий стандарт с большим количеством кода, который использовался в течение довольно долгого времени, они выбрали наименьшее изменение, которое они могли бы сделать, что все равно вылечило бы проблему.

+0

Собственно, это не совсем так. Стандарт полностью соответствует этому конкретному исполнению. – Puppy

+4

@DeadMG: и как вы думаете, что противоречит всему, что я сказал? Я очень конкретно не * не сказал, что результат был неопределенным, или что-то близкое - только то, что вам нужно использовать бросок. –

+0

Ну, это немного неискренне, чтобы подразумевать, что Стандарт намеревается защитить от Мерфи в этом случае, когда его намерения совершенно не похожи ни на что подобное, равно как и не подрывают законные нормы Стандарта. Здесь нет правила Мерфи/Макиавелли - это простой, четко определенный актерский состав, и все. – Puppy

0

Это потому, что вы управляете памятью, в которой находится ваш класс в памяти. В вашем случае просто сохранить частный член в этой ячейке памяти, чтобы вы его изменили. Это не очень хорошая идея, потому что теперь вы знаете, как объект будет храниться в памяти.

14

Из-за обратной совместимости с C, где вы можете сделать то же самое.


Для всех людей задаются вопросом, вот почему это не UB и фактически разрешено стандартом:

Во-первых, TestClass класс стандартного макета (§9 [class] p7):

A стандарт-макет класса является классом, который:

  • не имеет нестатические элементы данных класса типа нестандартного расположения (или массив таких видов) или ссылки, // OK: нестатическая элемент данных имеет тип «INT»
  • не имеет виртуального функции (10.3) и нет виртуальных базовых классов (10.1), // OK
  • имеет такое же управление доступом (раздел 11) для всех нестатических элементов данных, // ОК, все нестатические элементы данных (1) являются «частными»
  • не имеет базовых классов нестандартной компоновки, // ОК, нет базовых классов
  • либо не имеет нестатических элементов данных в самом производном классе, но и не более одного базового класса с нестатическими членами данных или не имеет базовых классов с нестатическими элементами данных и // ОК, базовые классы снова отсутствуют
  • не имеет базовых классов того же типа, что и первый нестатический элемент данных. // OK, нет базовых классов снова

И с этим, вы не можете позволено reinterpret_cast класс к типу его первого элемента (§9.2 [class.mem] p20):

указатель структурный объект стандартной компоновки, соответствующим образом преобразованный с использованием reinterpret_cast, указывает на его начальный член (или если этот элемент является битовым полем, а затем блоку, в котором он находится) и наоборот.

В вашем случае, C-стиль (int*) литые решает к reinterpret_cast (§5.4 [expr.cast] p4).

+1

Это не отвечает на вопрос ... почему стандарт позволяет это сделать, если первый член является приватным? Подумайте об этом, почему стандарт проактивно поддерживает эту функцию? : P – tenfour

+1

@tenfour: обратная совместимость с C, как всегда. Не спрашивайте меня, хотя почему они сделали так, что «имеют один и тот же контроль доступа», а не «имеют все средства контроля доступа». : s Вы были правы, однако, я добавил крошечный крошечный ответ на вершину. :) – Xeo

+0

@Xeo, потому что он менее ограничительный? –

1

Компилятор дал бы вам ошибку, если бы вы попытались int *pp = &cc.cc, компилятор сказал бы вам, что вы не можете получить доступ к частному члену.

В коде вы переинтерпретируете адрес cc как указатель на int. Вы написали это стиль стиля C, стиль стиля C++ был бы int* pp = reinterpret_cast<int*>(&cc);. Reinterpret_cast всегда является предупреждением о том, что вы выполняете бросок между двумя указателями, которые не связаны. В таком случае вы должны убедиться, что вы поступаете правильно. Вы должны знать базовую память (макет). Компилятор не мешает вам делать это, потому что это часто требуется.

При выполнении броска вы выбрасываете все знания о классе. Теперь компилятор видит только указатель int. Конечно, вы можете получить доступ к памяти, на которую указывает указатель. В вашем случае на вашей платформе компилятор выполнил загрузку cc в первые n байтов объекта TestClass, поэтому указатель TestClass также указывает на член cc.

2

Причиной является совместимость с C, но дополнительная безопасность доступа на уровне C++.

Рассмотрим:

struct S { 
#ifdef __cplusplus 
private: 
#endif // __cplusplus 
    int i, j; 
#ifdef __cplusplus 
public: 
    int get_i() const { return i; } 
    int get_j() const { return j; } 
#endif // __cplusplus 
}; 

Требуя, что С-видимой S и C++ - видимый S быть макет-совместимый, S можно использовать через границу языка со стороной C++, имеющие более высокий уровень безопасности доступа , reinterpret_cast Подрыв безопасности доступа является неудачным, но необходимым следствием.

В качестве стороннего ограничения ограничение наличия всех членов с одним и тем же контролем доступа заключается в том, что реализации разрешено изменять элементы по отношению к элементам с различным контролем доступа. Предположительно, некоторые реализации ставят членов с одним контролем доступа вместе, для удобства; его также можно использовать для уменьшения заполнения, хотя я не знаю ни одного компилятора, который это делает.

1

Целевая цель reinterpret_cast (и литье в стиле C еще более мощное, чем reinterpret_cast) - обеспечить путь выхода из зоны безопасности.

Смежные вопросы