2012-08-23 24 views
18

Sau khi nhìn thấy this question một vài phút trước, tôi tự hỏi tại sao các nhà thiết kế ngôn ngữ cho phép nó vì nó cho phép sửa đổi gián tiếp dữ liệu cá nhân. Ví dụ:Tại sao C++ cho phép các thành viên riêng được sửa đổi bằng cách sử dụng phương pháp này?

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 

Tôi đã thử nghiệm mã trên và thực sự dữ liệu cá nhân đã được sửa đổi. Có lời giải thích nào về lý do tại sao điều này được phép xảy ra hay đây chỉ là sự giám sát trong ngôn ngữ? Dường như trực tiếp làm suy yếu việc sử dụng các thành viên dữ liệu cá nhân.

+21

Trong C++, thời điểm bạn sử dụng phôi như thế, bạn đã nói với trình biên dịch bạn không quan tâm đến tính an toàn và kiểm tra kiểu của nó. Đây không phải là an ninh mạng; trình biên dịch sẽ cho phép bạn làm hỏng công cụ của riêng bạn nếu bạn nhấn mạnh, như mã của bạn. – tenfour

+0

@tenfour Có, tôi nhận ra rằng tôi hơi ngạc nhiên rằng điều đó là có thể. Tôi đã thay đổi C cast thành một static_cast và sau đó trình biên dịch đã phàn nàn. – mathematician1975

+2

bạn thực sự không nên sử dụng C phôi trong C++. Về cơ bản họ đang nói với trình biên dịch 'biến mất, tôi biết những gì tôi đang làm, và tôi chịu mọi trách nhiệm'. Họ thậm chí còn tồi tệ hơn reinterpret_cast (mà cũng có thể đã làm việc bằng cách này. Static_cast sẽ không hoạt động vì không có cách nào cũng được xác định để làm những gì bạn đang yêu cầu) –

Trả lời

31

Bởi vì, như Bjarne đặt nó, C++ được thiết kế để bảo vệ chống lại Murphy, chứ không phải Machiavelli.

Nói cách khác, nó phải bảo vệ bạn khỏi tai nạn - nhưng nếu bạn đi đến bất kỳ công việc nào để phá hoại nó (chẳng hạn như sử dụng dàn diễn viên), nó thậm chí sẽ không cố ngăn bạn lại.

Khi tôi nghĩ về nó, tôi có một suy nghĩ tương tự hơi khác: nó giống như khóa trên cửa phòng tắm. Nó cung cấp cho bạn một cảnh báo rằng bạn có thể không muốn để đi bộ trong đó ngay bây giờ, nhưng nó tầm thường để mở khóa cửa từ bên ngoài nếu bạn quyết định.

Chỉnh sửa: như câu hỏi @Xeo thảo luận, về lý do tại sao tiêu chuẩn cho biết "có cùng kiểm soát truy cập" thay vì "có tất cả kiểm soát truy cập công khai", câu trả lời dài và hơi quanh co.

Hãy bước trở lại vào đầu và xem xét một cấu trúc như:

struct X { 
    int a; 
    int b; 
}; 

C luôn luôn có một vài quy tắc cho một cấu trúc như thế này. Một là trong thể hiện của cấu trúc, địa chỉ của cấu trúc phải bằng địa chỉ a, vì vậy bạn có thể đưa con trỏ đến cấu trúc tới con trỏ tới int và truy cập a với kết quả được xác định rõ. Khác là các thành viên phải được sắp xếp theo cùng một thứ tự trong bộ nhớ khi chúng được định nghĩa trong cấu trúc (mặc dù trình biên dịch là miễn phí để chèn đệm giữa chúng).

Đối với C++, có ý định duy trì điều đó, đặc biệt là đối với các cấu trúc C hiện có. Đồng thời, có ý định rõ ràng là nếu trình biên dịch muốn thực thi private (và protected) vào thời gian chạy, sẽ dễ dàng thực hiện điều đó (hợp lý hiệu quả).

Do đó, một cái gì đó nhất định như:

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

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

Trình biên dịch nên được yêu cầu để duy trì các quy tắc tương tự như C đối với Y.aY.b với. Đồng thời, nếu nó sẽ thực thi truy cập vào thời gian chạy, nó có thể muốn di chuyển tất cả các biến công cộng cùng nhau trong bộ nhớ, vì vậy cách bố trí sẽ được nhiều hơn như:

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

Sau đó, khi nó được thực thi điều tại Theo thời gian của tôi, về cơ bản nó có thể làm điều gì đó giống như if (offset > 3 * sizeof(int)) access_violation();

Kiến thức của tôi không ai làm được điều này, và tôi không chắc phần còn lại của tiêu chuẩn thực sự cho phép nó, nhưng dường như có ít nhất là một nửa hình thành mầm của một ý tưởng dọc theo dòng đó.

Để thực thi cả hai, C++ 98 cho biết Y::aY::b phải theo thứ tự đó trong bộ nhớ và Y::a phải ở đầu cấu trúc (ví dụ: quy tắc giống C). Tuy nhiên, do các thông số truy cập can thiệp, Y::cY::e không còn phải theo thứ tự tương ứng với nhau.Nói cách khác, tất cả các biến liên tiếp được xác định mà không có một specifier truy cập giữa chúng được nhóm lại với nhau, trình biên dịch được tự do sắp xếp lại các nhóm đó (nhưng vẫn phải giữ lại các đầu tiên ở đầu).

Điều đó là tốt cho đến khi một số kẻ giật (tức là tôi) đã chỉ ra rằng cách các quy tắc được viết có một vấn đề nhỏ khác. Nếu tôi đã viết mã như:

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

... bạn đã kết thúc với một chút tự mâu thuẫn. Một mặt, điều này vẫn chính thức là cấu trúc POD, vì vậy các quy tắc giống như C được cho là sẽ áp dụng - nhưng vì bạn đã thừa nhận vô số các specifier giữa các thành viên, nó cũng cho phép trình biên dịch sắp xếp lại các thành viên, phá vỡ các quy tắc giống như C mà họ dự định.

Để khắc phục điều đó, họ đã nói lại tiêu chuẩn một chút để nói về các thành viên đều có cùng quyền truy cập, thay vì có hay không có một thông số truy cập giữa chúng. Có, họ có thể chỉ ra lệnh rằng các quy tắc sẽ chỉ áp dụng cho các thành viên công cộng, nhưng nó sẽ xuất hiện mà không ai nhìn thấy bất cứ điều gì để đạt được từ đó. Cho rằng điều này đã được sửa đổi một tiêu chuẩn hiện có với rất nhiều mã đã được sử dụng trong một thời gian, lựa chọn cho sự thay đổi nhỏ nhất mà họ có thể làm mà vẫn sẽ chữa được vấn đề.

+0

Thực ra, điều này không đúng. Tiêu chuẩn hoàn toàn xác định rõ dàn diễn viên cụ thể này. – Puppy

+4

@DeadMG: và làm thế nào để bạn nghĩ rằng mâu thuẫn với bất cứ điều gì tôi nói? Tôi rất đặc biệt đã làm * không * nói kết quả là không xác định, hoặc bất cứ điều gì gần gũi - chỉ rằng bạn phải sử dụng một diễn viên. –

+0

Vâng, có một chút bất tiện khi ngụ ý rằng tiêu chuẩn có ý định bảo vệ chống lại Murphy trong trường hợp này, khi ý định của nó khá rõ ràng không phải bất kỳ thứ gì, cũng không phải là sự lật đổ các quy tắc pháp lý của Tiêu chuẩn. Không có quy tắc Murphy/Machiavelli ở đây - đó là một diễn viên đơn giản, được xác định rõ ràng và đó là điều đó. – Puppy

0

Điều này là do bạn đang thao tác bộ nhớ trong đó lớp của bạn nằm trong bộ nhớ. Trong trường hợp của bạn nó chỉ xảy ra để lưu trữ các thành viên tư nhân tại vị trí bộ nhớ này, do đó bạn thay đổi nó. Nó không phải là một ý tưởng rất tốt để làm bởi vì bây giờ bạn biết làm thế nào các đối tượng sẽ được lưu trữ trong bộ nhớ.

14

Vì khả năng tương thích ngược với C, nơi bạn có thể thực hiện tương tự.


Đối với tất cả mọi người tự hỏi, đây là lý do tại sao đây không phải là UB và thực sự là phép theo tiêu chuẩn:

Thứ nhất, TestClass là một lớp tiêu chuẩn bố trí (§9 [class] p7):

A lớp bố cục tiêu chuẩn là một lớp học:

  • không có thành viên dữ liệu không tĩnh thuộc loại không bố cục tiêu chuẩn (hoặc mảng loại như vậy) hoặc tham chiếu, // OK: thành viên dữ liệu không tĩnh thuộc loại 'int'
  • không có ảo chức năng (10.3) và không có lớp cơ sở ảo (10.1), // OK
  • có cùng điều khiển truy cập (Điều 11) cho tất cả thành viên dữ liệu không tĩnh, // OK, tất cả thành viên dữ liệu không tĩnh (1) là 'riêng tư'
  • không có lớp cơ sở bố cục phi tiêu chuẩn, // OK, không có lớp cơ sở
  • hoặc không có thành viên dữ liệu không tĩnh trong lớp dẫn xuất nhất và nhiều nhất một lớp cơ sở với thành viên dữ liệu không tĩnh hoặc không có lớp cơ sở với thành viên dữ liệu không tĩnh và // OK.
  • không có lớp cơ sở cùng loại với thành viên dữ liệu không tĩnh đầu tiên. // OK, không có lớp cơ sở một lần nữa

Và với điều đó, bạn có thể được phép reinterpret_cast lớp để loại thành viên đầu tiên của mình (§9.2 [class.mem] p20):

Một con trỏ tới một đối tượng cấu trúc bố cục chuẩn, được chuyển đổi hợp lý bằng cách sử dụng một reinterpret_cast, trỏ đến thành viên ban đầu của nó (hoặc nếu thành viên đó là một trường bit, sau đó đến đơn vị mà nó cư trú) và ngược lại.

Trong trường hợp của bạn, dàn diễn viên kiểu C (int*) giải quyết thành reinterpret_cast (§5.4 [expr.cast] p4).

+1

Điều này không trả lời câu hỏi ... tại sao tiêu chuẩn cho phép bạn làm điều này nếu thành viên đầu tiên là riêng tư? Nghĩ lại thì, tại sao tiêu chuẩn lại chủ động hỗ trợ tính năng này? : P – tenfour

+1

@tenfour: Tính tương thích ngược với C, như thường lệ. Tuy nhiên, đừng hỏi tôi lý do tại sao họ làm cho nó "có cùng quyền kiểm soát truy cập" và không "có toàn quyền kiểm soát truy cập công cộng". : s Bạn đã đúng, mặc dù, tôi đã thêm một câu trả lời nhỏ bé đến đỉnh. :) – Xeo

+0

@Xeo vì nó ít hạn chế hơn? –

1

Trình biên dịch sẽ cho bạn lỗi nếu bạn đã thử int *pp = &cc.cc, trình biên dịch sẽ cho bạn biết rằng bạn không thể truy cập thành viên riêng tư.

Trong mã của bạn, bạn đang diễn giải lại địa chỉ của cc dưới dạng con trỏ trỏ đến int. Bạn đã viết nó theo kiểu C, phong cách C++ sẽ là int* pp = reinterpret_cast<int*>(&cc);. Các reinterpret_cast luôn luôn là một cảnh báo rằng bạn đang làm một diễn viên giữa hai con trỏ không liên quan. Trong trường hợp này, bạn phải chắc chắn rằng bạn đang làm đúng. Bạn phải biết bộ nhớ cơ bản (layout). Trình biên dịch không ngăn cản bạn làm như vậy, bởi vì điều này nếu thường xuyên cần thiết.

Khi thực hiện dàn diễn viên, bạn vứt bỏ mọi kiến ​​thức về lớp học. Từ bây giờ trên trình biên dịch chỉ thấy một con trỏ int. Tất nhiên bạn có thể truy cập vào bộ nhớ con trỏ trỏ đến. Trong trường hợp của bạn, trên nền tảng của bạn trình biên dịch đã xảy ra để đặt cc trong n byte đầu tiên của một đối tượng TestClass, do đó, một con trỏ TestClass cũng trỏ đến thành viên cc.

2

Lý do chính đáng là cho phép khả năng tương thích với C nhưng an toàn truy cập phụ trên lớp C++.

xem xét:

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 
}; 

Bằng cách yêu cầu rằng C có thể nhìn thấy S và C++ - có thể nhìn thấy S được bố trí tương thích, S có thể được sử dụng trên các ranh giới ngôn ngữ với C++ bên có an toàn truy cập lớn hơn . Sự lật đổ an toàn truy cập reinterpret_cast là một hệ quả không may nhưng cần thiết.

Ngoài ra, việc hạn chế có tất cả thành viên có cùng quyền kiểm soát truy cập là do việc triển khai được phép sắp xếp lại các thành viên liên quan đến các thành viên có quyền kiểm soát truy cập khác nhau. Có lẽ một số triển khai đặt các thành viên với cùng một kiểm soát truy cập với nhau, vì lợi ích của tidiness; nó cũng có thể được sử dụng để giảm đệm, mặc dù tôi không biết bất kỳ trình biên dịch nào thực hiện điều đó.

1

Toàn bộ mục đích của reinterpret_cast (và dàn diễn viên kiểu C thậm chí còn mạnh hơn reinterpret_cast) là cung cấp đường thoát hiểm xung quanh các biện pháp an toàn.

Các vấn đề liên quan