Tại sao tôi nên sử dụng một con trỏ thay vì chính đối tượng?

1664
gEdringer 2014-03-04 01:54.

Tôi đến từ nền tảng Java và đã bắt đầu làm việc với các đối tượng trong C ++. Nhưng một điều xảy ra với tôi là mọi người thường sử dụng con trỏ tới các đối tượng hơn là chính các đối tượng, ví dụ như khai báo này:

Object *myObject = new Object;

hơn là:

Object myObject;

Hoặc thay vì sử dụng một hàm, hãy nói testFunc()như thế này:

myObject.testFunc();

chúng ta phải viết:

myObject->testFunc();

Nhưng tôi không thể hiểu tại sao chúng ta nên làm theo cách này. Tôi cho rằng nó liên quan đến hiệu quả và tốc độ vì chúng ta có quyền truy cập trực tiếp vào địa chỉ bộ nhớ. Tôi nói đúng chứ?

20 answers

1621
Joseph Mansfield 2014-03-04 02:01.

Thật không may khi bạn thấy phân bổ động thường xuyên như vậy. Điều đó chỉ cho thấy có bao nhiêu lập trình viên C ++ tồi.

Theo một nghĩa nào đó, bạn có hai câu hỏi được gộp lại thành một. Đầu tiên là khi nào chúng ta nên sử dụng phân bổ động (using new)? Thứ hai là khi nào chúng ta nên sử dụng con trỏ?

Thông điệp quan trọng là bạn phải luôn sử dụng công cụ thích hợp cho công việc . Trong hầu hết mọi tình huống, có điều gì đó phù hợp và an toàn hơn là thực hiện phân bổ động thủ công và / hoặc sử dụng con trỏ thô.

Phân bổ động

Trong câu hỏi của bạn, bạn đã trình bày hai cách tạo một đối tượng. Sự khác biệt chính là thời lượng lưu trữ của đối tượng. Khi thực hiện Object myObject;trong một khối, đối tượng được tạo với thời lượng lưu trữ tự động, có nghĩa là nó sẽ tự động bị hủy khi vượt ra khỏi phạm vi. Khi bạn làm như vậy new Object(), đối tượng có thời lượng lưu trữ động, có nghĩa là nó vẫn tồn tại cho đến khi bạn rõ ràng delete. Bạn chỉ nên sử dụng thời lượng lưu trữ động khi cần. Có nghĩa là, bạn nên luôn thích tạo các đối tượng có thời lượng lưu trữ tự động khi có thể .

Hai trường hợp chính mà bạn có thể yêu cầu phân bổ động:

  1. Bạn cần đối tượng tồn tại lâu hơn phạm vi hiện tại - đối tượng cụ thể đó tại vị trí bộ nhớ cụ thể đó, không phải bản sao của nó. Nếu bạn ổn với việc sao chép / di chuyển đối tượng (hầu hết thời gian là như vậy), bạn nên thích một đối tượng tự động hơn.
  2. Bạn cần phân bổ nhiều bộ nhớ , có thể dễ dàng làm đầy ngăn xếp. Sẽ thật tuyệt nếu chúng ta không phải lo lắng về điều này (hầu hết thời gian bạn không nên phải làm như vậy), vì nó thực sự nằm ngoài tầm nhìn của C ++, nhưng thật không may, chúng ta phải đối mặt với thực tế của hệ thống chúng tôi đang phát triển cho.

Khi bạn thực sự yêu cầu phân bổ động, bạn nên đóng gói nó trong một con trỏ thông minh hoặc một số loại khác thực hiện RAII (như các vùng chứa tiêu chuẩn). Con trỏ thông minh cung cấp ngữ nghĩa quyền sở hữu của các đối tượng được phân bổ động. Hãy xem std::unique_ptrstd::shared_ptr, ví dụ. Nếu sử dụng chúng một cách hợp lý, bạn gần như hoàn toàn có thể tránh thực hiện việc quản lý bộ nhớ của riêng mình (xem Quy tắc số không ).

Con trỏ

Tuy nhiên, có nhiều cách sử dụng chung khác cho con trỏ thô ngoài phân bổ động, nhưng hầu hết đều có các lựa chọn thay thế mà bạn nên thích. Như trước đây, hãy luôn thích các lựa chọn thay thế trừ khi bạn thực sự cần các gợi ý .

  1. Bạn cần tham khảo ngữ nghĩa . Đôi khi bạn muốn chuyển một đối tượng bằng con trỏ (bất kể nó được cấp phát như thế nào) vì bạn muốn hàm mà bạn đang truyền nó có quyền truy cập vào đối tượng cụ thể đó (không phải bản sao của nó). Tuy nhiên, trong hầu hết các tình huống, bạn nên ưu tiên các loại tham chiếu hơn là con trỏ, bởi vì đây là những gì chúng được thiết kế cụ thể. Lưu ý rằng điều này không nhất thiết phải kéo dài thời gian tồn tại của đối tượng vượt ra ngoài phạm vi hiện tại, như trong tình huống 1 ở trên. Như trước đây, nếu bạn ổn với việc truyền một bản sao của đối tượng, bạn không cần ngữ nghĩa tham chiếu.

  2. Bạn cần đa hình . Bạn chỉ có thể gọi các hàm một cách đa hình (nghĩa là theo kiểu động của một đối tượng) thông qua một con trỏ hoặc tham chiếu đến đối tượng. Nếu đó là hành vi bạn cần, thì bạn cần sử dụng con trỏ hoặc tham chiếu. Một lần nữa, tài liệu tham khảo nên được ưu tiên.

  3. Bạn muốn trình bày rằng một đối tượng là tùy chọn bằng cách cho phép truyền a nullptrkhi đối tượng đang bị bỏ qua. Nếu đó là một đối số, bạn nên sử dụng các đối số mặc định hoặc quá tải hàm. Nếu không, bạn nên sử dụng kiểu đóng gói hành vi này, chẳng hạn như std::optional(được giới thiệu trong C ++ 17 - với các tiêu chuẩn C ++ trước đó, sử dụng boost::optional).

  4. Bạn muốn tách các đơn vị biên dịch để cải thiện thời gian biên dịch . Thuộc tính hữu ích của con trỏ là bạn chỉ yêu cầu một khai báo chuyển tiếp của kiểu trỏ đến (để thực sự sử dụng đối tượng, bạn sẽ cần một định nghĩa). Điều này cho phép bạn tách các phần của quá trình biên dịch của mình, điều này có thể cải thiện đáng kể thời gian biên dịch. Xem thành ngữ Pimpl .

  5. Bạn cần giao diện với thư viện C hoặc thư viện kiểu C. Tại thời điểm này, bạn buộc phải sử dụng các con trỏ thô. Điều tốt nhất bạn có thể làm là đảm bảo rằng bạn chỉ thả lỏng con trỏ thô của mình vào thời điểm cuối cùng có thể. Ví dụ, bạn có thể lấy một con trỏ thô từ một con trỏ thông minh bằng cách sử dụng gethàm thành viên của nó . Nếu một thư viện thực hiện một số phân bổ cho bạn mà nó mong đợi bạn phân bổ thông qua một trình điều khiển, bạn thường có thể bọc xử lý đó trong một con trỏ thông minh với một trình phân bổ tùy chỉnh sẽ phân bổ đối tượng một cách thích hợp.

176
TemplateRex 2014-03-04 02:06.

Có nhiều trường hợp sử dụng cho con trỏ.

Hành vi đa hình . Đối với các kiểu đa hình, con trỏ (hoặc tham chiếu) được sử dụng để tránh cắt:

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

Tham chiếu ngữ nghĩa và tránh sao chép . Đối với các kiểu không đa hình, một con trỏ (hoặc một tham chiếu) sẽ tránh sao chép một đối tượng có thể đắt tiền

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

Lưu ý rằng C ++ 11 có ngữ nghĩa di chuyển có thể tránh nhiều bản sao của các đối tượng đắt tiền vào đối số hàm và làm giá trị trả về. Nhưng sử dụng một con trỏ chắc chắn sẽ tránh được những điều đó và sẽ cho phép nhiều con trỏ trên cùng một đối tượng (trong khi một đối tượng chỉ có thể được di chuyển từ một lần).

Mua lại tài nguyên . Tạo một con trỏ đến một tài nguyên bằng cách sử dụng newtoán tử là một mô hình chống lại trong C ++ hiện đại. Sử dụng một lớp tài nguyên đặc biệt (một trong những vùng chứa Chuẩn) hoặc một con trỏ thông minh ( std::unique_ptr<>hoặc std::shared_ptr<>). Xem xét:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

vs.

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

Một con trỏ thô chỉ nên được sử dụng như một "chế độ xem" và không liên quan đến quyền sở hữu theo bất kỳ cách nào, có thể là thông qua tạo trực tiếp hoặc ẩn thông qua các giá trị trả về. Xem thêm phần Hỏi & Đáp này từ Câu hỏi thường gặp về C ++ .

Kiểm soát thời gian tồn tại chi tiết hơn Mỗi khi một con trỏ được chia sẻ đang được sao chép (ví dụ như một đối số của hàm) thì tài nguyên mà nó trỏ tới đang được giữ nguyên. Các đối tượng thông thường (không được tạo bởi new, trực tiếp bởi bạn hoặc bên trong một lớp tài nguyên) sẽ bị phá hủy khi vượt ra khỏi phạm vi.

133
Gerasimos R 2014-03-07 08:40.

Có rất nhiều câu trả lời tuyệt vời cho câu hỏi này, bao gồm các trường hợp sử dụng quan trọng của khai báo chuyển tiếp, đa hình, v.v. nhưng tôi cảm thấy một phần "linh hồn" của câu hỏi của bạn chưa được trả lời - đó là ý nghĩa của các cú pháp khác nhau trên Java và C ++.

Hãy xem xét tình huống so sánh hai ngôn ngữ:

Java:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

Tương đương gần nhất với điều này, là:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

Hãy xem cách thay thế C ++:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

Cách tốt nhất để nghĩ về nó là - dù ít hay nhiều - Java (ngầm) xử lý các con trỏ tới các đối tượng, trong khi C ++ có thể xử lý các con trỏ tới các đối tượng hoặc chính các đối tượng. Có những ngoại lệ cho điều này - ví dụ: nếu bạn khai báo kiểu "nguyên thủy" của Java, chúng là các giá trị thực tế được sao chép chứ không phải con trỏ. Vì thế,

Java:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

Điều đó nói rằng, việc sử dụng con trỏ KHÔNG nhất thiết phải là cách đúng hay sai để xử lý mọi thứ; tuy nhiên những câu trả lời khác đã giải đáp thỏa đáng điều đó. Tuy nhiên, ý tưởng chung là trong C ++ bạn có nhiều quyền kiểm soát hơn đối với thời gian tồn tại của các đối tượng và nơi chúng sẽ sống.

Lấy điểm chính - Object * object = new Object()cấu trúc thực sự là những gì gần nhất với ngữ nghĩa Java điển hình (hoặc C # cho vấn đề đó).

82
user3391320 2014-03-07 21:30.

Lời nói đầu

Java không giống C ++, trái ngược với sự cường điệu. Máy thổi phồng Java muốn bạn tin rằng vì Java có cú pháp giống C ++ nên các ngôn ngữ tương tự nhau. Không có gì có thể được thêm từ sự thật. Thông tin sai lệch này là một phần lý do tại sao các lập trình viên Java sử dụng C ++ và sử dụng cú pháp giống Java mà không hiểu ý nghĩa của mã của họ.

Tiếp tục chúng tôi đi

Nhưng tôi không thể hiểu tại sao chúng ta nên làm theo cách này. Tôi cho rằng nó liên quan đến hiệu quả và tốc độ vì chúng ta có quyền truy cập trực tiếp vào địa chỉ bộ nhớ. Tôi nói đúng chứ?

Thực ra thì ngược lại. Heap chậm hơn nhiều so với ngăn xếp, vì ngăn xếp rất đơn giản so với đống. Các biến lưu trữ tự động (hay còn gọi là biến ngăn xếp) có các trình hủy của chúng được gọi khi chúng vượt ra khỏi phạm vi. Ví dụ:

{
    std::string s;
}
// s is destroyed here

Mặt khác, nếu bạn sử dụng một con trỏ được cấp phát động, trình hủy của nó phải được gọi theo cách thủ công. deletegọi hàm hủy này cho bạn.

{
    std::string* s = new std::string;
}
delete s; // destructor called

Điều này không liên quan gì đến newcú pháp phổ biến trong C # và Java. Chúng được sử dụng cho các mục đích hoàn toàn khác nhau.

Lợi ích của phân bổ động

1. Bạn không cần phải biết trước kích thước của mảng

Một trong những vấn đề đầu tiên mà nhiều lập trình viên C ++ gặp phải là khi họ chấp nhận đầu vào tùy ý từ người dùng, bạn chỉ có thể phân bổ kích thước cố định cho một biến ngăn xếp. Bạn cũng không thể thay đổi kích thước của mảng. Ví dụ:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

Tất nhiên, nếu bạn đã sử dụng std::stringthay thế, hãy std::stringtự thay đổi kích thước bên trong để điều đó không thành vấn đề. Nhưng về cơ bản giải pháp cho vấn đề này là phân bổ động. Bạn có thể cấp phát bộ nhớ động dựa trên đầu vào của người dùng, ví dụ:

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

Lưu ý phụ : Một sai lầm mà nhiều người mới bắt đầu mắc phải là việc sử dụng các mảng có độ dài thay đổi. Đây là một phần mở rộng GNU và cũng là một phần mở rộng trong Clang vì chúng phản chiếu nhiều phần mở rộng của GCC. Vì vậy int arr[n]không nên dựa vào những điều sau đây .

Bởi vì đống lớn hơn nhiều so với ngăn xếp, người ta có thể tùy ý phân bổ / phân bổ lại bao nhiêu bộ nhớ tùy ý, trong khi ngăn xếp có một giới hạn.

2. Mảng không phải là con trỏ

Bạn hỏi đây là lợi ích như thế nào? Câu trả lời sẽ trở nên rõ ràng khi bạn hiểu được sự nhầm lẫn / huyền thoại đằng sau mảng và con trỏ. Người ta thường cho rằng chúng giống nhau, nhưng thực tế không phải vậy. Lầm tưởng này xuất phát từ thực tế là các con trỏ có thể được chỉ định giống như mảng và do mảng phân rã thành con trỏ ở cấp cao nhất trong khai báo hàm. Tuy nhiên, khi một mảng phân rã thành một con trỏ, con trỏ sẽ mất sizeofthông tin của nó . Vì vậy, sizeof(pointer)sẽ cung cấp kích thước của con trỏ theo byte, thường là 8 byte trên hệ thống 64 bit.

Bạn không thể gán cho mảng, chỉ khởi tạo chúng. Ví dụ:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

Mặt khác, bạn có thể làm bất cứ điều gì bạn muốn với con trỏ. Thật không may, vì sự phân biệt giữa con trỏ và mảng được phân biệt bằng tay trong Java và C #, người mới bắt đầu không hiểu sự khác biệt.

3. Tính đa hình

Java và C # có các phương tiện cho phép bạn coi các đối tượng như một đối tượng khác, ví dụ như sử dụng astừ khóa. Vì vậy, nếu ai đó muốn coi một Entityđối tượng như một Playerđối tượng, người ta có thể làm Player player = Entity as Player;Điều này rất hữu ích nếu bạn định gọi các hàm trên một vùng chứa đồng nhất chỉ áp dụng cho một kiểu cụ thể. Chức năng có thể đạt được theo cách tương tự dưới đây:

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

Vì vậy, giả sử nếu chỉ có Tam giác có chức năng Xoay, sẽ là một lỗi trình biên dịch nếu bạn cố gắng gọi nó trên tất cả các đối tượng của lớp. Sử dụng dynamic_cast, bạn có thể mô phỏng astừ khóa. Nói rõ hơn, nếu một phép ép kiểu không thành công, nó sẽ trả về một con trỏ không hợp lệ. Vì vậy, !testvề cơ bản là một cách viết tắt để kiểm tra xem testcó phải là NULL hay con trỏ không hợp lệ, có nghĩa là quá trình ép kiểu không thành công.

Lợi ích của biến tự động

Sau khi nhìn thấy tất cả những điều tuyệt vời mà phân bổ động có thể làm, bạn có thể tự hỏi tại sao không ai KHÔNG sử dụng phân bổ động mọi lúc? Tôi đã nói với bạn một lý do, đống chậm. Và nếu bạn không cần tất cả bộ nhớ đó, bạn không nên lạm dụng nó. Vì vậy, đây là một số nhược điểm không theo thứ tự cụ thể:

  • Nó dễ xảy ra lỗi. Cấp phát bộ nhớ thủ công rất nguy hiểm và bạn dễ bị rò rỉ. Nếu bạn không thành thạo trong việc sử dụng trình gỡ lỗi hoặc valgrind(một công cụ rò rỉ bộ nhớ), bạn có thể bứt tóc ra khỏi đầu. May mắn thay, thành ngữ RAII và con trỏ thông minh làm giảm bớt điều này một chút, nhưng bạn phải quen thuộc với các thực hành như Quy tắc Ba và Quy tắc Năm. Đó là rất nhiều thông tin để tham gia, và những người mới bắt đầu nếu không biết hoặc không quan tâm sẽ rơi vào cái bẫy này.

  • Nó không phải là cần thiết. Không giống như Java và C #, việc sử dụng newtừ khóa ở khắp mọi nơi là khá dễ hiểu, trong C ++, bạn chỉ nên sử dụng nó nếu cần. Một cụm từ phổ biến là, mọi thứ sẽ giống như một cái đinh nếu bạn có một cái búa. Trong khi những người mới bắt đầu bắt đầu với C ++ sợ con trỏ và học cách sử dụng các biến ngăn xếp theo thói quen thì các lập trình viên Java và C # lại bắt đầu bằng cách sử dụng con trỏ mà không hiểu nó! Đó thực sự là bước nhầm chân. Bạn phải từ bỏ mọi thứ bạn biết vì cú pháp là một chuyện, học ngôn ngữ là chuyện khác.

1. (N) RVO - Aka, (Đã đặt tên) Tối ưu hóa giá trị lợi nhuận

Một tối ưu hóa mà nhiều trình biên dịch thực hiện là những thứ được gọi là tối ưu hóa giá trị trả về và loại bỏ . Những thứ này có thể loại bỏ các bản sao không cần thiết, rất hữu ích cho các đối tượng rất lớn, chẳng hạn như một vectơ chứa nhiều phần tử. Thông thường, thực tế phổ biến là sử dụng con trỏ để chuyển quyền sở hữu hơn là sao chép các đối tượng lớn để di chuyển chúng. Điều này dẫn đến sự ra đời của ngữ nghĩa chuyển độngcon trỏ thông minh .

Nếu bạn đang sử dụng con trỏ, (N) RVO KHÔNG xảy ra. Việc tận dụng (N) RVO sẽ có lợi hơn và ít mắc lỗi hơn là trả về hoặc chuyển con trỏ nếu bạn lo lắng về việc tối ưu hóa. Rò rỉ lỗi có thể xảy ra nếu người gọi hàm chịu trách nhiệm nhập deletemột đối tượng được cấp phát động và như vậy. Có thể khó theo dõi quyền sở hữu của một đối tượng nếu các con trỏ đang được chuyển đi xung quanh như một củ khoai tây nóng. Chỉ cần sử dụng các biến ngăn xếp vì nó đơn giản hơn và tốt hơn.

80
Burnt Toast 2014-03-04 04:34.

Một lý do chính đáng khác để sử dụng con trỏ là cho các khai báo chuyển tiếp . Trong một dự án đủ lớn, chúng thực sự có thể tăng tốc thời gian biên dịch.

23
Kirill Gamazkov 2014-03-08 00:00.

C ++ cung cấp cho bạn ba cách để truyền một đối tượng: bằng con trỏ, bằng tham chiếu và theo giá trị. Java giới hạn bạn với cái sau (ngoại lệ duy nhất là các kiểu nguyên thủy như int, boolean, v.v.). Nếu bạn muốn sử dụng C ++ không chỉ như một món đồ chơi kỳ lạ, thì tốt hơn hết bạn nên biết sự khác biệt giữa ba cách này.

Java giả vờ rằng không có vấn đề như 'ai và khi nào nên hủy nó?'. Câu trả lời là: The Garbage Collector, Great and Awful. Tuy nhiên, nó không thể bảo vệ 100% chống rò rỉ bộ nhớ (vâng, java có thể làm rò rỉ bộ nhớ ). Trên thực tế, GC cho bạn cảm giác an toàn sai lầm. Chiếc SUV của bạn càng lớn, con đường đến nơi sơ tán càng dài.

C ++ giúp bạn đối mặt với việc quản lý vòng đời của đối tượng. Chà, có nhiều phương tiện để đối phó với điều đó ( họ con trỏ thông minh , QObject trong Qt, v.v.), nhưng không có phương tiện nào có thể được sử dụng theo cách 'cháy và quên' như GC: bạn nên luôn ghi nhớ xử lý bộ nhớ. Bạn không chỉ nên quan tâm đến việc phá hủy một đối tượng mà còn phải tránh hủy cùng một đối tượng nhiều hơn một lần.

Chưa hết sợ hãi? Ok: tham chiếu theo chu kỳ - tự xử lý chúng, con người. Và hãy nhớ rằng: giết từng đối tượng chính xác một lần, chúng tôi trong thời gian chạy C ++ không thích những kẻ gây rối với xác chết, bỏ mặc xác chết.

Vì vậy, trở lại câu hỏi của bạn.

Khi bạn chuyển đối tượng của mình xung quanh theo giá trị, không phải bằng con trỏ hoặc bằng tham chiếu, bạn sao chép đối tượng (toàn bộ đối tượng, cho dù đó là một vài byte hay một kết xuất cơ sở dữ liệu khổng lồ - bạn đủ thông minh để tránh điều này xảy ra sau này ' t bạn?) mỗi khi bạn làm '='. Và để truy cập các thành viên của đối tượng, bạn sử dụng '.' (dấu chấm).

Khi bạn chuyển đối tượng của mình bằng con trỏ, bạn chỉ sao chép một vài byte (4 trên hệ thống 32 bit, 8 trên hệ thống 64 bit), cụ thể là - địa chỉ của đối tượng này. Và để hiển thị điều này cho mọi người, bạn sử dụng toán tử '->' ưa thích này khi bạn truy cập các thành viên. Hoặc bạn có thể sử dụng kết hợp '*' và '.'.

Khi bạn sử dụng tham chiếu, bạn sẽ nhận được con trỏ giả vờ là một giá trị. Đó là một con trỏ, nhưng bạn truy cập các thành viên thông qua '.'.

Và, để bạn suy nghĩ thêm một lần nữa: khi bạn khai báo một số biến được phân tách bằng dấu phẩy, thì (hãy xem các tay):

  • Loại được trao cho mọi người
  • Công cụ sửa đổi giá trị / con trỏ / tham chiếu là riêng lẻ

Thí dụ:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one
21
Karthik Kalyanasundaram 2014-03-04 02:00.

Trong C ++, các đối tượng được cấp phát trên ngăn xếp (sử dụng Object object;câu lệnh trong một khối) sẽ chỉ sống trong phạm vi mà chúng được khai báo. Khi khối mã kết thúc thực thi, đối tượng được khai báo sẽ bị hủy. Trong khi nếu bạn phân bổ bộ nhớ trên heap, bằng cách sử dụng Object* obj = new Object(), chúng tiếp tục sống trong heap cho đến khi bạn gọi delete obj.

Tôi sẽ tạo một đối tượng trên heap khi tôi muốn sử dụng đối tượng không chỉ trong khối mã đã khai báo / cấp phát nó.

20
marcinj 2014-03-04 02:19.

Nhưng tôi không thể hiểu tại sao chúng ta nên sử dụng nó như thế này?

Tôi sẽ so sánh cách nó hoạt động bên trong thân hàm nếu bạn sử dụng:

Object myObject;

Bên trong hàm, ý chí của bạn myObjectsẽ bị hủy khi hàm này trả về. Vì vậy, điều này rất hữu ích nếu bạn không cần đối tượng bên ngoài chức năng của mình. Đối tượng này sẽ được đưa vào ngăn xếp luồng hiện tại.

Nếu bạn viết bên trong nội dung hàm:

 Object *myObject = new Object;

thì cá thể lớp Đối tượng được trỏ bởi myObjectsẽ không bị hủy khi hàm kết thúc và việc cấp phát nằm trên heap.

Bây giờ nếu bạn là lập trình viên Java, thì ví dụ thứ hai gần hơn với cách cấp phát đối tượng hoạt động trong java. Dòng này: Object *myObject = new Object;tương đương với java: Object myObject = new Object();. Sự khác biệt là trong java myObject sẽ được thu thập rác, trong khi dưới c ++ nó sẽ không được giải phóng, bạn phải ở đâu đó gọi rõ ràng là `delete myObject; ' nếu không bạn sẽ giới thiệu rò rỉ bộ nhớ.

Kể từ c ++ 11, bạn có thể sử dụng các cách phân bổ động an toàn new Object:, bằng cách lưu trữ các giá trị trong shared_ptr / unique_ptr.

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

Ngoài ra, các đối tượng thường được lưu trữ trong các vùng chứa, như map-s hoặc vector-s, chúng sẽ tự động quản lý toàn bộ thời gian của các đối tượng của bạn.

13
in need of help 2014-03-04 02:05.

Về mặt kỹ thuật, đó là một vấn đề cấp phát bộ nhớ, tuy nhiên đây là hai khía cạnh thực tế hơn của vấn đề này. Nó liên quan đến hai điều: 1) Phạm vi, khi bạn xác định một đối tượng mà không có con trỏ, bạn sẽ không thể truy cập nó sau khi khối mã mà nó được xác định, trong khi nếu bạn xác định một con trỏ bằng "mới" thì bạn có thể truy cập nó từ bất kỳ nơi nào bạn có con trỏ tới bộ nhớ này cho đến khi bạn gọi "xóa" trên cùng một con trỏ. 2) Nếu bạn muốn truyền các đối số cho một hàm, bạn muốn chuyển một con trỏ hoặc một tham chiếu để hiệu quả hơn. Khi bạn truyền một Đối tượng thì đối tượng sẽ được sao chép, nếu đây là một đối tượng sử dụng nhiều bộ nhớ thì điều này có thể ngốn CPU (ví dụ: bạn sao chép một vectơ đầy dữ liệu). Khi bạn truyền một con trỏ, tất cả những gì bạn truyền là một int (tùy thuộc vào việc triển khai nhưng hầu hết chúng là một int).

Ngoài ra, bạn cần hiểu rằng "mới" phân bổ bộ nhớ trên heap cần được giải phóng tại một số điểm. Khi bạn không phải sử dụng "mới", tôi khuyên bạn nên sử dụng định nghĩa đối tượng thông thường "trên ngăn xếp".

6
ST3 2014-03-09 00:48.

Câu hỏi chính là Tại sao tôi nên sử dụng một con trỏ thay vì chính đối tượng? Và câu trả lời của tôi, bạn (hầu như) không bao giờ nên sử dụng con trỏ thay vì đối tượng, vì C ++ có tham chiếu , nó an toàn hơn khi con trỏ và đảm bảo hiệu suất tương tự như con trỏ.

Một điều khác bạn đã đề cập trong câu hỏi của mình:

Object *myObject = new Object;

Làm thế nào nó hoạt động? Nó tạo ra con trỏ Objectkiểu, phân bổ bộ nhớ để phù hợp với một đối tượng và gọi hàm tạo mặc định, nghe hay đấy, phải không? Nhưng thực ra nó không tốt lắm, nếu bạn cấp phát động bộ nhớ (từ khóa được sử dụng new), bạn cũng phải giải phóng bộ nhớ theo cách thủ công, điều đó có nghĩa là trong mã bạn nên có:

delete myObject;

Điều này gọi hàm hủy và giải phóng bộ nhớ, trông dễ dàng, tuy nhiên trong các dự án lớn có thể khó phát hiện xem một luồng có giải phóng bộ nhớ hay không, nhưng vì mục đích đó, bạn có thể thử các con trỏ được chia sẻ , những con trỏ này làm giảm một chút hiệu suất, nhưng nó dễ làm việc hơn nhiều chúng.


Và bây giờ một số phần giới thiệu đã kết thúc và quay lại câu hỏi.

Bạn có thể sử dụng con trỏ thay vì các đối tượng để có được hiệu suất tốt hơn trong khi chuyển dữ liệu giữa các hàm.

Hãy xem, bạn có std::string(nó cũng là một đối tượng) và nó chứa rất nhiều dữ liệu, ví dụ: big XML, bây giờ bạn cần phải phân tích cú pháp nó, nhưng đối với nó, bạn có hàm void foo(...)có thể được khai báo theo những cách khác nhau:

  1. void foo(std::string xml); Trong trường hợp này, bạn sẽ sao chép tất cả dữ liệu từ biến của mình sang ngăn xếp hàm, điều này sẽ mất một thời gian, do đó hiệu suất của bạn sẽ thấp.
  2. void foo(std::string* xml); Trong trường hợp này, bạn sẽ truyền con trỏ tới đối tượng, cùng tốc độ với việc truyền size_tbiến, tuy nhiên khai báo này dễ xảy ra lỗi, vì bạn có thể truyền NULLcon trỏ hoặc con trỏ không hợp lệ. Con trỏ thường được sử dụng Cvì nó không có tham chiếu.
  3. void foo(std::string& xml); Ở đây bạn truyền tham chiếu, về cơ bản nó giống như truyền con trỏ, nhưng trình biên dịch thực hiện một số thứ và bạn không thể chuyển tham chiếu không hợp lệ (thực tế có thể tạo ra tình huống với tham chiếu không hợp lệ, nhưng nó đang lừa trình biên dịch).
  4. void foo(const std::string* xml); Ở đây cũng giống như thứ hai, chỉ là giá trị con trỏ không thể thay đổi.
  5. void foo(const std::string& xml); Ở đây tương tự như thứ ba, nhưng không thể thay đổi giá trị đối tượng.

Tôi muốn đề cập thêm điều gì nữa, bạn có thể sử dụng 5 cách này để chuyển dữ liệu cho dù bạn đã chọn cách phân bổ nào (với newhoặc thông thường ).


Một điều khác cần đề cập, khi bạn tạo đối tượng theo cách thông thường , bạn phân bổ bộ nhớ trong ngăn xếp, nhưng trong khi tạo nó, newbạn phân bổ heap. Việc cấp phát ngăn xếp nhanh hơn nhiều, nhưng nó hơi nhỏ đối với các mảng dữ liệu thực sự lớn, vì vậy nếu bạn cần đối tượng lớn, bạn nên sử dụng heap, vì bạn có thể bị tràn ngăn xếp, nhưng thông thường vấn đề này được giải quyết bằng cách sử dụng vùng chứa STL và hãy nhớ std::stringcũng là container, một số bạn đã quên nó :)

5
Quest 2014-03-04 02:02.

Hãy nói rằng bạn có class Achứa class BKhi bạn muốn gọi một số chức năng của class Bbên ngoài class Abạn chỉ đơn giản là sẽ có được một con trỏ đến lớp học này và bạn có thể làm bất cứ điều gì bạn muốn và nó cũng sẽ thay đổi bối cảnh class Btrong của bạnclass A

Nhưng hãy cẩn thận với đối tượng động

5
Rohit 2014-03-04 02:18.

Có nhiều lợi ích khi sử dụng con trỏ để phản đối -

  1. Hiệu quả (như bạn đã chỉ ra). Truyền các đối tượng cho các hàm có nghĩa là tạo các bản sao mới của đối tượng.
  2. Làm việc với các đối tượng từ thư viện của bên thứ ba. Nếu đối tượng của bạn thuộc mã của bên thứ ba và các tác giả dự định chỉ sử dụng các đối tượng của họ thông qua con trỏ (không có trình tạo bản sao, v.v.) thì cách duy nhất bạn có thể truyền xung quanh đối tượng này là sử dụng con trỏ. Việc vượt qua giá trị có thể gây ra vấn đề. (Vấn đề sao chép sâu / sao chép nông).
  3. nếu đối tượng sở hữu một tài nguyên và bạn muốn rằng quyền sở hữu đó không được lưu trữ với các đối tượng khác.
4
cmollis 2014-03-08 14:45.

Điều này đã được thảo luận từ lâu, nhưng trong Java mọi thứ đều là một con trỏ. Nó không phân biệt giữa phân bổ ngăn xếp và phân bổ heap (tất cả các đối tượng được cấp phát trên heap), vì vậy bạn không nhận ra mình đang sử dụng con trỏ. Trong C ++, bạn có thể kết hợp cả hai, tùy thuộc vào yêu cầu bộ nhớ của bạn. Hiệu suất và việc sử dụng bộ nhớ là xác định hơn trong C ++ (duh).

3
Palak Jain 2017-04-16 07:07.
Object *myObject = new Object;

Làm điều này sẽ tạo ra một tham chiếu đến một Đối tượng (trên heap) phải được xóa một cách rõ ràng để tránh rò rỉ bộ nhớ .

Object myObject;

Thực hiện việc này sẽ tạo một đối tượng (myObject) thuộc loại tự động (trên ngăn xếp) sẽ tự động bị xóa khi đối tượng (myObject) vượt ra khỏi phạm vi.

2
RioRicoRick 2014-03-05 10:37.

Một con trỏ tham chiếu trực tiếp đến vị trí bộ nhớ của một đối tượng. Java không có gì giống như thế này. Java có các tham chiếu tham chiếu đến vị trí của đối tượng thông qua các bảng băm. Bạn không thể làm bất cứ điều gì như số học con trỏ trong Java với các tham chiếu này.

Để trả lời câu hỏi của bạn, đó chỉ là sở thích của bạn. Tôi thích sử dụng cú pháp giống Java hơn.

0
lasan 2016-06-02 00:08.

Với con trỏ ,

  • có thể trực tiếp nói chuyện với bộ nhớ.

  • có thể ngăn chặn việc rò rỉ nhiều bộ nhớ của một chương trình bằng cách thao tác với con trỏ.

0
Noname 2017-01-12 10:03.

Một lý do để sử dụng con trỏ là giao diện với các hàm C. Một lý do khác là để tiết kiệm bộ nhớ; ví dụ: thay vì truyền một đối tượng chứa nhiều dữ liệu và có một hàm tạo sao chép chuyên sâu của bộ xử lý, chỉ cần chuyển một con trỏ đến đối tượng, tiết kiệm bộ nhớ và tốc độ đặc biệt nếu bạn đang ở trong một vòng lặp, tuy nhiên tham chiếu sẽ tốt hơn trong trường hợp đó, trừ khi bạn đang sử dụng mảng C-style.

0
seccpur 2018-02-19 07:11.

Ở những nơi mà việc sử dụng bộ nhớ ở mức cao, con trỏ rất tiện dụng. Ví dụ: hãy xem xét một thuật toán minimax, trong đó hàng ngàn nút sẽ được tạo bằng cách sử dụng quy trình đệ quy và sau đó sử dụng chúng để đánh giá bước đi tốt nhất tiếp theo trong trò chơi, khả năng phân bổ hoặc đặt lại (như trong con trỏ thông minh) làm giảm đáng kể mức tiêu thụ bộ nhớ. Trong khi đó, biến không phải là con trỏ tiếp tục chiếm không gian cho đến khi nó được gọi đệ quy trả về một giá trị.

0
user18853 2018-03-16 00:25.

Tôi sẽ bao gồm một trường hợp sử dụng quan trọng của con trỏ. Khi bạn đang lưu trữ một số đối tượng trong lớp cơ sở, nhưng nó có thể là đa hình.

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

Vì vậy, trong trường hợp này, bạn không thể khai báo bObj như một đối tượng trực tiếp, bạn phải có con trỏ.

0
RollerSimmer 2020-08-21 05:59.

Điểm mạnh chính của con trỏ đối tượng trong C ++ là cho phép các mảng đa hình và bản đồ của các con trỏ của cùng một lớp cha. Ví dụ, nó cho phép đặt vẹt đuôi dài, gà, robins, đà điểu, v.v. vào một mảng Chim.

Ngoài ra, các đối tượng được cấp phát động linh hoạt hơn và có thể sử dụng bộ nhớ HEAP trong khi đối tượng được cấp phát cục bộ sẽ sử dụng bộ nhớ STACK trừ khi nó là bộ nhớ tĩnh. Có các đối tượng lớn trên ngăn xếp, đặc biệt là khi sử dụng đệ quy, chắc chắn sẽ dẫn đến tràn ngăn xếp.

Related questions

MORE COOL STUFF

Cate Blanchett chia tay chồng sau 3 ngày bên nhau và vẫn kết hôn với anh ấy 25 năm sau

Cate Blanchett chia tay chồng sau 3 ngày bên nhau và vẫn kết hôn với anh ấy 25 năm sau

Cate Blanchett đã bất chấp những lời khuyên hẹn hò điển hình khi cô gặp chồng mình.

Tại sao Michael Sheen là một diễn viên phi lợi nhuận

Tại sao Michael Sheen là một diễn viên phi lợi nhuận

Michael Sheen là một diễn viên phi lợi nhuận nhưng chính xác thì điều đó có nghĩa là gì?

Hallmark Star Colin Egglesfield Các món ăn gây xúc động mạnh đối với người hâm mộ tại RomaDrama Live! [Loại trừ]

Hallmark Star Colin Egglesfield Các món ăn gây xúc động mạnh đối với người hâm mộ tại RomaDrama Live! [Loại trừ]

Ngôi sao của Hallmark Colin Egglesfield chia sẻ về những cuộc gặp gỡ với người hâm mộ ly kỳ tại RomaDrama Live! cộng với chương trình INSPIRE của anh ấy tại đại hội.

Tại sao bạn không thể phát trực tuyến 'chương trình truyền hình phía Bắc'

Tại sao bạn không thể phát trực tuyến 'chương trình truyền hình phía Bắc'

Bạn sẽ phải phủi sạch đầu đĩa Blu-ray hoặc DVD để xem tại sao Northern Exposure trở thành một trong những chương trình nổi tiếng nhất của thập niên 90.

Where in the World Are You? Take our GeoGuesser Quiz

Where in the World Are You? Take our GeoGuesser Quiz

The world is a huge place, yet some GeoGuessr players know locations in mere seconds. Are you one of GeoGuessr's gifted elite? Take our quiz to find out!

8 công dụng tuyệt vời của Baking Soda và Giấm

8 công dụng tuyệt vời của Baking Soda và Giấm

Bạn biết đấy, hai sản phẩm này là nguồn điện để làm sạch, riêng chúng. Nhưng cùng với nhau, chúng có một loạt công dụng hoàn toàn khác.

Hạn hán, biến đổi khí hậu đe dọa tương lai của thủy điện Hoa Kỳ

Hạn hán, biến đổi khí hậu đe dọa tương lai của thủy điện Hoa Kỳ

Thủy điện rất cần thiết cho lưới điện của Hoa Kỳ, nhưng nó chỉ tạo ra năng lượng khi có nước di chuyển. Bao nhiêu nhà máy thủy điện có thể gặp nguy hiểm khi các hồ và sông cạn kiệt?

Quyên góp tóc của bạn để giúp giữ nước sạch của chúng tôi

Quyên góp tóc của bạn để giúp giữ nước sạch của chúng tôi

Tóc tỉa từ các tiệm và các khoản quyên góp cá nhân có thể được tái sử dụng như những tấm thảm thấm dầu và giúp bảo vệ môi trường.

BoJack Horseman vẫn gan ruột và gan dạ hơn bao giờ hết trong phần 3

BoJack Horseman vẫn gan ruột và gan dạ hơn bao giờ hết trong phần 3

BoJack Horseman (Ảnh: Netflix) BoJack Horseman bắt đầu mùa thứ ba với sự xuất hiện của nhân vật tiêu biểu, người vẫn đang theo đuổi những lời khen ngợi từ giới phê bình trong một nỗ lực tuyệt vọng để tìm ra ý nghĩa trong cuộc sống của mình. Mùa thứ hai về số phận bi kịch của Raphael Bob-Waksberg dày đặc những trò đùa cũng như tuyệt vọng.

Xem cách tính năng lái tự động cập nhật của Tesla bùng phát như thế nào khi bạn bỏ qua các cảnh báo của nó

Xem cách tính năng lái tự động cập nhật của Tesla bùng phát như thế nào khi bạn bỏ qua các cảnh báo của nó

Bản nâng cấp Phiên bản 8.0 gần đây của Tesla đối với hệ thống Lái xe tự động được thiết kế để cải thiện hệ thống và mối quan hệ của nó với người lái.

UnREAL, Chương trình Truyền hình Về Truyền hình Thực tế Chúng tôi Không biết Chúng tôi Cần

UnREAL, Chương trình Truyền hình Về Truyền hình Thực tế Chúng tôi Không biết Chúng tôi Cần

Chắc chắn khi con cái chúng ta xem lại các chương trình chúng ta đã xem vào khoảng năm 2015, chúng sẽ thấy UnREAL, một chương trình truyền hình mới chiếu vào tối Thứ Hai của Lifetime — bạn sẽ thấy đúng lúc, sẽ phát sóng ngay sau khi The Bachelor kết thúc — như một chương trình không thể được thực hiện vào bất kỳ thời điểm nào khác trong lịch sử truyền hình, nhưng một chương trình cảm thấy cần thiết để xem bây giờ. UnREAL có sự tham gia của Shiri Appleby trong vai Rachel, một nhà sản xuất truyền hình cho một chương trình về cơ bản là The Bachelor, mặc dù trong thế giới này, nó được gọi là Vĩnh viễn.

Sau tất cả, Josh Trank sẽ không đạo diễn bộ phim tuyển tập Star Wars thứ hai

Sau tất cả, Josh Trank sẽ không đạo diễn bộ phim tuyển tập Star Wars thứ hai

Thông báo chính thức đến trực tiếp từ trang web của Star Wars: Fantastic Four, đạo diễn Josh Trank, một người bí ẩn không xuất hiện tại đại hội Star Wars Celebration gần đây, sẽ không chỉ đạo phần hai Star Wars Anthology. Đây là tuyên bố được đăng ngày hôm qua: Tất cả thực sự là ngôn ngữ rất lịch sự, nhưng một bài báo của Hollywood Reporter cho thấy tất cả đều không tốt ở hậu trường, gọi sự ra đi là "một vụ sa thải" một phần dựa trên "hành vi bất thường" của Trank trong khi quay Fantastic Four: The Bài báo trích dẫn các nguồn gọi là Trank “đôi khi thiếu quyết đoán và thiếu sáng tạo,” và lưu ý rằng Fantastic Four, khởi chiếu vào ngày 7 tháng 8, đã được khởi động lại vào cuối tháng 4.

Edwin McCain ra mắt Grand Ole Opry: Quay cảnh hậu trường với nhạc sĩ 'I'll Be'

Edwin McCain ra mắt Grand Ole Opry: Quay cảnh hậu trường với nhạc sĩ 'I'll Be'

McCain, người đang làm việc cho một album mới, lần đầu tiên bước vào vòng kết nối vào tối thứ Sáu ở Nashville

Nicky Hilton Forced to Borrow Paris' 'I Love Paris' Sweatshirt After 'Airline Loses All [My] Luggage'

Nicky Hilton Forced to Borrow Paris' 'I Love Paris' Sweatshirt After 'Airline Loses All [My] Luggage'

Nicky Hilton Rothschild's luggage got lost, but luckily she has an incredible closet to shop: Sister Paris Hilton's!

Kate Middleton dành một ngày bên bờ nước ở London, cùng với Jennifer Lopez, Julianne Hough và hơn thế nữa

Kate Middleton dành một ngày bên bờ nước ở London, cùng với Jennifer Lopez, Julianne Hough và hơn thế nữa

Kate Middleton dành một ngày bên bờ nước ở London, cùng với Jennifer Lopez, Julianne Hough và hơn thế nữa. Từ Hollywood đến New York và mọi nơi ở giữa, hãy xem các ngôi sao yêu thích của bạn đang làm gì!

17 tuổi bị đâm chết trong khi 4 người khác bị thương trong một cuộc tấn công bằng dao trên sông Wisconsin

17 tuổi bị đâm chết trong khi 4 người khác bị thương trong một cuộc tấn công bằng dao trên sông Wisconsin

Các nhà điều tra đang xem xét liệu nhóm và nghi phạm có biết nhau trước vụ tấn công hay không

Tôi viết như thế nào

Tôi viết như thế nào

Đối với tôi, mọi thứ là về dòng đầu tiên đó và nó sẽ đưa bạn đến đâu. Một số nhà văn bị điều khiển bởi cốt truyện, sự sắp xếp tinh tế của các quân cờ, trong khi những người khác bị lôi cuốn bởi một nhân vật và khả năng thực hiện một cuộc hành trình với một người bạn hư cấu mới.

Đường băng hạ cánh

Đường băng hạ cánh

Cuối hè đầu thu là mùa hoài niệm. Những chiếc đèn đường chiếu ánh sáng của chúng qua những con đường đẫm mưa, và những chiếc lá dưới chân - màu đỏ cam tắt trong bóng chạng vạng - là lời nhắc nhở về những ngày đã qua.

Hãy tưởng tượng tạo ra một chiến lược nội dung thực sự CHUYỂN ĐỔI. Nó có thể.

Hãy tưởng tượng tạo ra một chiến lược nội dung thực sự CHUYỂN ĐỔI. Nó có thể.

Vào năm 2021, tôi khuyến khích bạn suy nghĩ lại mọi thứ bạn biết về khách hàng mà bạn phục vụ và những câu chuyện bạn kể cho họ. Lùi lại.

Sự mất mát của voi ma mút đã mở ra trái tim tôi để yêu

Sự mất mát của voi ma mút đã mở ra trái tim tôi để yêu

Vào ngày sinh nhật thứ 9 của Felix The Cat, tôi nhớ về một trong những mất mát lớn nhất trong cuộc đời trưởng thành của tôi - Sophie của tôi vào năm 2013. Tôi đã viết bài luận này và chia sẻ nó trên nền tảng này một thời gian ngắn vào năm 2013.

Language