Ai đó đã đề cập đến nó trong IRC như là vấn đề cắt lát.
Ai đó đã đề cập đến nó trong IRC như là vấn đề cắt lát.
Câu trả lời:
"Cắt lát" là nơi bạn gán một đối tượng của lớp dẫn xuất cho một thể hiện của lớp cơ sở, do đó làm mất một phần thông tin - một số thông tin bị "cắt" đi.
Ví dụ,
class A {
int foo;
};
class B : public A {
int bar;
};
Vì vậy, một đối tượng của loại B
có hai thành viên dữ liệu foo
và bar
.
Sau đó, nếu bạn đã viết này:
B b;
A a = b;
Sau đó, thông tin b
về thành viên bar
bị mất trong a
.
A a = b;
a
bây giờ là đối tượng của loại A
có bản sao B::foo
. Tôi nghĩ sẽ là sai lầm khi bỏ nó lại bây giờ.
B b1; B b2; A& b2_ref = b2; b2 = b1
. Bạn có thể nghĩ rằng bạn đã sao chép b1
vào b2
, nhưng bạn đã không! Bạn đã sao chép một phần của b1
để b2
(một phần của b1
mà B
thừa hưởng từ A
), và để lại các phần khác của b2
không thay đổi. b2
bây giờ là một sinh vật frankenstein bao gồm một vài bit b1
theo sau bởi một số khối b2
. Ừ! Downvote vì tôi nghĩ rằng câu trả lời là rất sai.
B b1; B b2; A& b2_ref = b2; b2_ref = b1
" Vấn đề thực sự xảy ra nếu bạn " ... xuất phát từ một lớp có toán tử gán không ảo. A
Thậm chí là dự định cho phái sinh? Nó không có chức năng ảo. Nếu bạn xuất phát từ một loại, bạn phải đối phó với thực tế là các hàm thành viên của nó có thể được gọi!
Hầu hết các câu trả lời ở đây không giải thích được vấn đề thực sự với việc cắt lát là gì. Họ chỉ giải thích các trường hợp lành tính của cắt lát, không phải là những người phản bội. Giả sử, giống như các câu trả lời khác, rằng bạn đang giao dịch với hai lớp A
và B
, B
xuất phát (công khai) từ đâu A
.
Trong tình huống này, C ++ cho phép bạn vượt qua một thể hiện của B
để A
's toán tử gán (và cũng để các nhà xây dựng bản sao). Điều này hoạt động vì một thể hiện của B
có thể được chuyển đổi thành a const A&
, đó là điều mà các toán tử gán và các nhà xây dựng sao chép mong đợi các đối số của chúng là.
B b;
A a = b;
Không có gì xấu xảy ra ở đó - bạn đã yêu cầu một ví dụ trong A
đó là bản sao B
và đó chính xác là những gì bạn nhận được. Chắc chắn, a
sẽ không chứa một số b
thành viên, nhưng làm thế nào? Rốt cuộc A
, đó không phải là một B
, vì vậy thậm chí còn chưa từng nghe về những thành viên này, nói gì đến việc có thể lưu trữ chúng.
B b1;
B b2;
A& a_ref = b2;
a_ref = b1;
//b2 now contains a mixture of b1 and b2!
Bạn có thể nghĩ rằng đó b2
sẽ là một bản sao b1
sau đó. Nhưng, than ôi, không phải vậy! Nếu bạn kiểm tra nó, bạn sẽ phát hiện ra đó b2
là một sinh vật Frankenstein, được tạo ra từ một số khối b1
(khối B
được thừa hưởng từ A
) và một số khối b2
(khối chỉ B
chứa). Ôi!
Chuyện gì đã xảy ra? Chà, theo mặc định, C ++ không coi các toán tử gán như virtual
. Do đó, dòng a_ref = b1
sẽ gọi toán tử gán A
, không phải của B
. Điều này là do, đối với các hàm không ảo, loại khai báo (chính thức: tĩnh ) A&
xác định hàm nào được gọi, trái ngược với loại thực tế (chính thức: động ) (sẽ là B
, vì a_ref
tham chiếu một thể hiện của B
) . Bây giờ, A
toán tử gán chỉ rõ ràng chỉ biết về các thành viên được khai báo A
, vì vậy nó sẽ chỉ sao chép các thành viên đó, khiến các thành viên được thêm vào B
không thay đổi.
Chỉ gán cho các bộ phận của một đối tượng thường không có ý nghĩa gì, nhưng thật không may, C ++ không cung cấp cách tích hợp nào để cấm điều này. Bạn có thể, tuy nhiên, cuộn của riêng bạn. Bước đầu tiên là làm cho toán tử gán gán ảo . Điều này sẽ đảm bảo rằng nó luôn là toán tử gán của loại thực tế được gọi, không phải là kiểu khai báo . Bước thứ hai là sử dụng dynamic_cast
để xác minh rằng đối tượng được gán có loại tương thích. Bước thứ ba là để làm việc chuyển nhượng thực tế trong một thành viên (được bảo vệ!) assign()
, Vì B
's assign()
có thể sẽ muốn sử dụng A
là assign()
để sao chép A
's, các thành viên.
class A {
public:
virtual A& operator= (const A& a) {
assign(a);
return *this;
}
protected:
void assign(const A& a) {
// copy members of A from a to this
}
};
class B : public A {
public:
virtual B& operator= (const A& a) {
if (const B* b = dynamic_cast<const B*>(&a))
assign(*b);
else
throw bad_assignment();
return *this;
}
protected:
void assign(const B& b) {
A::assign(b); // Let A's assign() copy members of A from b to this
// copy members of B from b to this
}
};
Lưu ý rằng, để thuận tiện trong sạch, B
là operator=
covariantly đè kiểu trả về, vì nó biết rằng nó đang trở về một thể hiện của B
.
derived
giá trị nào cũng có thể được trao cho mã mong đợi một base
giá trị, hoặc bất kỳ tham chiếu dẫn xuất nào cũng có thể được sử dụng làm tham chiếu cơ sở. Tôi muốn thấy một ngôn ngữ với một hệ thống loại giải quyết cả hai khái niệm riêng biệt. Có nhiều trường hợp trong đó một tham chiếu dẫn xuất nên có thể thay thế cho một tham chiếu cơ sở, nhưng các trường hợp dẫn xuất không nên thay thế cho các tham chiếu cơ sở; cũng có nhiều trường hợp các trường hợp nên chuyển đổi nhưng tài liệu tham khảo không nên thay thế.
Nếu bạn có một lớp cơ sở A
và một lớp dẫn xuất B
, thì bạn có thể làm như sau.
void wantAnA(A myA)
{
// work with myA
}
B derived;
// work with the object "derived"
wantAnA(derived);
Bây giờ phương thức wantAnA
cần một bản sao của derived
. Tuy nhiên, đối tượng derived
không thể được sao chép hoàn toàn, vì lớp B
có thể phát minh ra các biến thành viên bổ sung không có trong lớp cơ sở của nó A
.
Do đó, để gọi wantAnA
, trình biên dịch sẽ "cắt" tất cả các thành viên bổ sung của lớp dẫn xuất. Kết quả có thể là một đối tượng bạn không muốn tạo, bởi vì
A
-object (tất cả các hành vi đặc biệt của lớp B
bị mất).wantAnA
(như tên của nó ngụ ý!) Muốn có A
, thì đó là những gì nó nhận được. Và một ví dụ A
, sẽ, uh, hành xử như một A
. Làm thế nào là đáng ngạc nhiên?
derived
loại A
. Đúc ngầm định luôn là nguồn gốc của hành vi không mong muốn trong C ++, bởi vì thường rất khó hiểu khi nhìn vào mã cục bộ mà một diễn viên đã diễn ra.
Đây là tất cả các câu trả lời tốt. Tôi chỉ muốn thêm một ví dụ thực thi khi chuyển các đối tượng theo giá trị so với tham chiếu:
#include <iostream>
using namespace std;
// Base class
class A {
public:
A() {}
A(const A& a) {
cout << "'A' copy constructor" << endl;
}
virtual void run() const { cout << "I am an 'A'" << endl; }
};
// Derived class
class B: public A {
public:
B():A() {}
B(const B& a):A(a) {
cout << "'B' copy constructor" << endl;
}
virtual void run() const { cout << "I am a 'B'" << endl; }
};
void g(const A & a) {
a.run();
}
void h(const A a) {
a.run();
}
int main() {
cout << "Call by reference" << endl;
g(B());
cout << endl << "Call by copy" << endl;
h(B());
}
Đầu ra là:
Call by reference
I am a 'B'
Call by copy
'A' copy constructor
I am an 'A'
Trận đấu thứ ba trong google cho "C ++ cắt" cung cấp cho tôi bài viết Wikipedia này http://en.wikipedia.org/wiki/Object_slicing và điều này (nóng, nhưng một vài bài đăng đầu tiên xác định vấn đề): http://bytes.com/ diễn đàn / chủ đề163565.html
Vì vậy, đó là khi bạn gán một đối tượng của một lớp con cho siêu lớp. Siêu lớp không biết gì về thông tin bổ sung trong lớp con và không có chỗ để lưu trữ, vì vậy thông tin bổ sung sẽ bị "cắt".
Nếu các liên kết đó không cung cấp đủ thông tin cho "câu trả lời tốt", vui lòng chỉnh sửa câu hỏi của bạn để cho chúng tôi biết bạn đang tìm kiếm gì hơn.
Vấn đề cắt lát là nghiêm trọng vì nó có thể dẫn đến hỏng bộ nhớ và rất khó để đảm bảo chương trình không bị ảnh hưởng. Để thiết kế ngôn ngữ này, chỉ có thể truy cập các lớp hỗ trợ kế thừa bằng cách tham chiếu (không phải theo giá trị). Ngôn ngữ lập trình D có thuộc tính này.
Xem xét lớp A và lớp B xuất phát từ A. Tham nhũng bộ nhớ có thể xảy ra nếu phần A có con trỏ p và đối tượng B trỏ p đến dữ liệu bổ sung của B. Sau đó, khi dữ liệu bổ sung bị cắt, p sẽ trỏ đến rác.
Derived
nó hoàn toàn có thể chuyển đổi thành Base
.) Điều này rõ ràng là đối nghịch với Nguyên tắc Đóng mở và gánh nặng bảo trì lớn.
Trong C ++, một đối tượng lớp dẫn xuất có thể được gán cho một đối tượng lớp cơ sở, nhưng cách khác là không thể.
class Base { int x, y; };
class Derived : public Base { int z, w; };
int main()
{
Derived d;
Base b = d; // Object Slicing, z and w of d are sliced off
}
Cắt đối tượng xảy ra khi một đối tượng lớp dẫn xuất được gán cho một đối tượng lớp cơ sở, các thuộc tính bổ sung của đối tượng lớp dẫn xuất được cắt ra để tạo thành đối tượng lớp cơ sở.
Vấn đề cắt trong C ++ phát sinh từ ngữ nghĩa giá trị của các đối tượng của nó, chủ yếu vẫn là do khả năng tương thích với các cấu trúc C. Bạn cần sử dụng tham chiếu rõ ràng hoặc cú pháp con trỏ để đạt được hành vi đối tượng "bình thường" được tìm thấy trong hầu hết các ngôn ngữ khác làm đối tượng, tức là, các đối tượng luôn được truyền qua xung quanh bởi tham chiếu.
Các câu trả lời ngắn gọn là bạn cắt đối tượng bằng cách gán một đối tượng dẫn xuất cho một đối tượng cơ sở theo giá trị , tức là đối tượng còn lại chỉ là một phần của đối tượng dẫn xuất. Để duy trì ngữ nghĩa giá trị, cắt lát là một hành vi hợp lý và có những cách sử dụng tương đối hiếm, không tồn tại trong hầu hết các ngôn ngữ khác. Một số người coi nó là một tính năng của C ++, trong khi nhiều người coi nó là một trong những điều kỳ quặc / không phù hợp với C ++.
struct
, khả năng tương thích hoặc không có ý nghĩa khác mà bất kỳ linh mục OOP ngẫu nhiên nào nói với bạn.
Base
phải lấy chính xác sizeof(Base)
byte trong bộ nhớ, với sự liên kết có thể, có thể, đó là lý do tại sao "gán" (sao chép trên ngăn xếp ) sẽ không sao chép các thành viên lớp dẫn xuất, phần bù của chúng nằm ngoài sizeof. Để tránh "mất dữ liệu", chỉ cần sử dụng con trỏ, giống như bất kỳ ai khác, vì bộ nhớ con trỏ được cố định về vị trí và kích thước, trong khi ngăn xếp rất dễ bay hơi
Vậy ... Tại sao mất thông tin dẫn xuất là xấu? ... bởi vì tác giả của lớp dẫn xuất có thể đã thay đổi cách biểu diễn sao cho việc cắt bỏ thông tin bổ sung sẽ thay đổi giá trị được đại diện bởi đối tượng. Điều này có thể xảy ra nếu lớp dẫn xuất nếu được sử dụng để lưu trữ một biểu diễn hiệu quả hơn cho các hoạt động nhất định, nhưng tốn kém để chuyển đổi trở lại biểu diễn cơ sở.
Cũng nghĩ rằng ai đó cũng nên đề cập đến những gì bạn nên làm để tránh cắt lát ... Nhận bản sao Tiêu chuẩn mã hóa C ++, 101 hướng dẫn quy tắc và thực tiễn tốt nhất. Đối phó với cắt lát là # 54.
Nó gợi ý một mô hình hơi phức tạp để giải quyết hoàn toàn vấn đề: có một trình tạo bản sao được bảo vệ, một DoClone thuần được bảo vệ và một bản sao công khai với một xác nhận sẽ cho bạn biết nếu một lớp dẫn xuất (xa hơn) không thực hiện đúng DoClone. (Phương pháp Clone tạo một bản sao sâu thích hợp của đối tượng đa hình.)
Bạn cũng có thể đánh dấu hàm tạo sao chép trên cơ sở rõ ràng cho phép cắt lát rõ ràng nếu muốn.
1. ĐỊNH NGH OFA CỦA VẤN ĐỀ SLICING
Nếu D là lớp dẫn xuất của lớp cơ sở B, thì bạn có thể gán một đối tượng có kiểu Xuất phát cho một biến (hoặc tham số) của kiểu Base.
THÍ DỤ
class Pet
{
public:
string name;
};
class Dog : public Pet
{
public:
string breed;
};
int main()
{
Dog dog;
Pet pet;
dog.name = "Tommy";
dog.breed = "Kangal Dog";
pet = dog;
cout << pet.breed; //ERROR
Mặc dù việc chuyển nhượng trên được cho phép, giá trị được gán cho vật nuôi biến sẽ mất trường giống của nó. Đây được gọi là vấn đề cắt lát .
2. CÁCH CỐ ĐỊNH VẤN ĐỀ SLICING
Để đánh bại vấn đề, chúng tôi sử dụng các con trỏ tới các biến động.
THÍ DỤ
Pet *ptrP;
Dog *ptrD;
ptrD = new Dog;
ptrD->name = "Tommy";
ptrD->breed = "Kangal Dog";
ptrP = ptrD;
cout << ((Dog *)ptrP)->breed;
Trong trường hợp này, không có thành viên dữ liệu hoặc hàm thành viên nào của biến động được chỉ ra bởi ptrD (đối tượng lớp con cháu) sẽ bị mất. Ngoài ra, nếu bạn cần sử dụng các chức năng, chức năng phải là một chức năng ảo.
dog
không phải là một phần của lớp Pet
( breed
thành viên dữ liệu) không được sao chép trong biến pet
? Mã này chỉ quan tâm đến các Pet
thành viên dữ liệu - rõ ràng. Cắt lát chắc chắn là một "vấn đề" nếu không mong muốn, nhưng tôi không thấy điều đó ở đây.
((Dog *)ptrP)
" Tôi đề nghị sử dụngstatic_cast<Dog*>(ptrP)
Dog::breed
) không có cách nào L ERI liên quan đến SLICING không?
Dường như đối với tôi, việc cắt lát không phải là vấn đề quá lớn ngoài khi các lớp học và chương trình của bạn được thiết kế / thiết kế kém.
Nếu tôi truyền một đối tượng lớp con dưới dạng tham số cho một phương thức, trong đó có tham số kiểu siêu lớp, tôi chắc chắn sẽ nhận thức được điều đó và biết bên trong, phương thức được gọi sẽ chỉ làm việc với đối tượng siêu lớp (còn gọi là lớp cơ sở).
Đối với tôi, dường như chỉ có sự kỳ vọng không hợp lý rằng việc cung cấp một lớp con nơi yêu cầu một lớp cơ sở, bằng cách nào đó sẽ dẫn đến kết quả cụ thể của lớp con, sẽ khiến cho việc cắt lát trở thành một vấn đề. Thiết kế của nó kém trong việc sử dụng phương thức hoặc triển khai lớp con kém. Tôi đoán nó thường là kết quả của việc hy sinh thiết kế OOP tốt để ủng hộ hiệu quả hoặc tăng hiệu suất.
OK, tôi sẽ thử sau khi đọc nhiều bài viết giải thích việc cắt đối tượng nhưng không làm thế nào nó trở nên có vấn đề.
Kịch bản xấu xa có thể dẫn đến tham nhũng bộ nhớ là như sau:
Cắt lát có nghĩa là dữ liệu được thêm bởi một lớp con bị loại bỏ khi một đối tượng của lớp con được truyền hoặc trả về bởi giá trị hoặc từ một hàm mong đợi một đối tượng lớp cơ sở.
Giải thích: Xem xét khai báo lớp sau:
class baseclass
{
...
baseclass & operator =(const baseclass&);
baseclass(const baseclass&);
}
void function( )
{
baseclass obj1=m;
obj1=m;
}
Vì các hàm sao chép cơ sở không biết gì về phần dẫn xuất, chỉ phần cơ sở của phần dẫn xuất được sao chép. Điều này thường được gọi là cắt lát.
class A
{
int x;
};
class B
{
B( ) : x(1), c('a') { }
int x;
char c;
};
int main( )
{
A a;
B b;
a = b; // b.c == 'a' is "sliced" off
return 0;
}
khi một đối tượng lớp dẫn xuất được gán cho một đối tượng lớp cơ sở, các thuộc tính bổ sung của đối tượng lớp dẫn xuất được cắt ra (loại bỏ) tạo thành đối tượng lớp cơ sở.
class Base {
int x;
};
class Derived : public Base {
int z;
};
int main()
{
Derived d;
Base b = d; // Object Slicing, z of d is sliced off
}
Khi một đối tượng lớp Derogen được gán cho Đối tượng lớp cơ sở, tất cả các thành viên của đối tượng lớp dẫn xuất được sao chép vào đối tượng lớp cơ sở ngoại trừ các thành viên không có trong lớp cơ sở. Các thành viên này được cắt bởi trình biên dịch. Điều này được gọi là cắt lát đối tượng.
Đây là một ví dụ:
#include<bits/stdc++.h>
using namespace std;
class Base
{
public:
int a;
int b;
int c;
Base()
{
a=10;
b=20;
c=30;
}
};
class Derived : public Base
{
public:
int d;
int e;
Derived()
{
d=40;
e=50;
}
};
int main()
{
Derived d;
cout<<d.a<<"\n";
cout<<d.b<<"\n";
cout<<d.c<<"\n";
cout<<d.d<<"\n";
cout<<d.e<<"\n";
Base b = d;
cout<<b.a<<"\n";
cout<<b.b<<"\n";
cout<<b.c<<"\n";
cout<<b.d<<"\n";
cout<<b.e<<"\n";
return 0;
}
Nó sẽ tạo ra:
[Error] 'class Base' has no member named 'd'
[Error] 'class Base' has no member named 'e'
Tôi chỉ chạy qua vấn đề cắt lát và nhanh chóng hạ cánh ở đây. Vì vậy, hãy để tôi thêm hai xu của tôi vào đây.
Chúng ta hãy lấy một ví dụ từ "mã sản xuất" (hoặc một cái gì đó gần giống):
Hãy nói rằng chúng ta có một cái gì đó gửi hành động. Một UI trung tâm điều khiển chẳng hạn.
UI này cần có được một danh sách những thứ hiện có thể được gửi đi. Vì vậy, chúng tôi định nghĩa một lớp có chứa thông tin công văn. Hãy gọi nó là Action
. Vì vậy, an Action
có một số biến thành viên. Để đơn giản, chúng ta chỉ có 2, là a std::string name
và a std::function<void()> f
. Sau đó, nó có một void activate()
cái mà chỉ thực hiện các f
thành viên.
Vì vậy, UI được std::vector<Action>
cung cấp. Hãy tưởng tượng một số chức năng như:
void push_back(Action toAdd);
Bây giờ chúng tôi đã thiết lập giao diện của giao diện người dùng. Không có vấn đề cho đến nay. Nhưng một số anh chàng khác làm việc trong dự án này đột nhiên quyết định rằng có những hành động chuyên biệt cần thêm thông tin trong Action
đối tượng. Vì lý do gì bao giờ. Điều đó cũng có thể được giải quyết với các vụ bắt giữ lambda. Ví dụ này không được lấy 1-1 từ mã.
Vì vậy, anh chàng xuất phát từ Action
để thêm hương vị của riêng mình.
Anh ta chuyển một ví dụ của lớp học pha chế tại nhà của mình cho push_back
nhưng sau đó chương trình trở nên hay.
Vậy chuyện gì đã xảy ra?
Như bạn có thể đoán: đối tượng đã bị cắt.
Thông tin bổ sung từ ví dụ đã bị mất và f
hiện có xu hướng không xác định hành vi.
Tôi hy vọng ví dụ này mang lại ánh sáng cho những người thực sự không thể tưởng tượng mọi thứ khi nói về A
s và B
s được bắt nguồn theo một cách nào đó.