Tại sao con trỏ không được khởi tạo bằng NULL theo mặc định?


118

Ai đó có thể vui lòng giải thích tại sao con trỏ không được khởi tạo thành NULLkhông?
Thí dụ:

  void test(){
     char *buf;
     if (!buf)
        // whatever
  }

Chương trình sẽ không bước vào bên trong if bởi vì bufkhông phải là null.

Tôi muốn biết tại sao, trong trường hợp nào chúng ta cần một biến có bật thùng rác, con trỏ đặc biệt định địa chỉ thùng rác trên bộ nhớ?


13
Chà, vì các loại cơ bản chưa được khởi tạo. Vì vậy, tôi giả sử câu hỏi "thực sự" của bạn: là tại sao các kiểu cơ bản không được khởi tạo?
GManNickG

11
"chương trình sẽ không bước vào if vì buf không null". Điều đó không chính xác. Vì bạn không biết buf gì , bạn không thể biết nó không phải là gì .
Drew Dormann

Trái ngược với một thứ như Java, C ++ mang lại nhiều trách nhiệm hơn cho nhà phát triển.
Rishi

số nguyên, con trỏ, mặc định là 0 nếu bạn sử dụng hàm tạo ().
Erik Aronesty

Bởi vì giả định rằng ai đó sử dụng C ++ biết họ đang làm gì, hơn nữa ai đó đang sử dụng một con trỏ thô trên một con trỏ thông minh biết (thậm chí còn hơn thế) họ đang làm gì!
Lofty Lion

Câu trả lời:


161

Tất cả chúng ta đều nhận thấy rằng con trỏ (và các loại POD khác) nên được khởi tạo.
Câu hỏi sau đó trở thành 'ai nên khởi tạo chúng'.

Về cơ bản có hai phương pháp:

  • Trình biên dịch khởi tạo chúng.
  • Nhà phát triển khởi tạo chúng.

Hãy giả sử rằng trình biên dịch khởi tạo bất kỳ biến nào không được nhà phát triển khởi tạo một cách rõ ràng. Sau đó, chúng tôi gặp phải các tình huống trong đó việc khởi tạo biến là không bình thường và lý do nhà phát triển không thực hiện điều đó tại điểm khai báo là họ cần thực hiện một số thao tác và sau đó gán.

Vì vậy, bây giờ chúng ta có tình huống rằng trình biên dịch đã thêm một lệnh bổ sung vào mã khởi tạo biến thành NULL, sau đó mã nhà phát triển được thêm vào để thực hiện việc khởi tạo chính xác. Hoặc trong các điều kiện khác, biến có thể không bao giờ được sử dụng. Rất nhiều nhà phát triển C ++ sẽ hét lên lỗi trong cả hai điều kiện với chi phí của hướng dẫn bổ sung đó.

Nó không chỉ là về thời gian. Nhưng cả không gian. Có rất nhiều môi trường mà cả hai tài nguyên đều ở mức cao và các nhà phát triển cũng không muốn từ bỏ.

NHƯNG : Bạn có thể mô phỏng tác động của việc buộc khởi tạo. Hầu hết các trình biên dịch sẽ cảnh báo bạn về các biến chưa được khởi tạo. Vì vậy tôi luôn chuyển mức cảnh báo của mình lên mức cao nhất có thể. Sau đó, yêu cầu trình biên dịch coi tất cả các cảnh báo là lỗi. Trong các điều kiện này, hầu hết các trình biên dịch sau đó sẽ tạo ra lỗi cho các biến chưa được khởi tạo nhưng được sử dụng và do đó sẽ ngăn không cho mã được tạo.


5
Bob Tabor nói, “Quá nhiều người chưa suy nghĩ đủ để khởi tạo!” Tính 'thân thiện' của nó để tự động khởi tạo tất cả các biến, nhưng nó mất thời gian và các chương trình chậm là 'không thân thiện'. Không thể chấp nhận một trang tính trải rộng hoặc các trình biên tập hiển thị malloc rác ngẫu nhiên được tìm thấy. C, một công cụ sắc bén cho người dùng được đào tạo (nguy hiểm nếu sử dụng sai) sẽ không mất thời gian khởi tạo các biến tự động. Có thể có một macro bánh xe huấn luyện cho các biến init, nhưng nhiều người nghĩ rằng tốt hơn hết là đứng lên, lưu tâm và chảy máu một chút. Nói tóm lại, bạn làm việc theo cách bạn luyện tập. Vì vậy, hãy thực hành như bạn đang có.
Bill IV,

2
Bạn sẽ ngạc nhiên về số lượng lỗi có thể tránh được chỉ bằng cách ai đó sửa tất cả quá trình khởi tạo của họ. Đây sẽ là công việc tẻ nhạt nếu nó không có cảnh báo trình biên dịch.
Jonathan Henson

4
@Loki, tôi đang gặp khó khăn khi làm theo quan điểm của bạn. Tôi chỉ cố gắng khen ngợi câu trả lời của bạn là hữu ích, tôi hy vọng bạn thu thập được điều đó. Nếu không, tôi xin lỗi.
Jonathan Henson

3
Nếu con trỏ lần đầu tiên được đặt thành NULL và sau đó được đặt thành bất kỳ giá trị nào, trình biên dịch sẽ có thể phát hiện điều này và tối ưu hóa lần khởi tạo NULL đầu tiên, phải không?
Korchkidu

1
@Korchkidu: Đôi khi. Tuy nhiên, một trong những vấn đề lớn là không có cách nào để nó cảnh báo bạn rằng bạn đã quên thực hiện khởi tạo, vì nó không thể biết rằng mặc định không hoàn hảo cho việc sử dụng của bạn.
Deduplicator

41

Trích dẫn Bjarne Stroustrup trong TC ++ PL (Phiên bản đặc biệt tr.22):

Việc triển khai tính năng không được áp đặt chi phí đáng kể cho các chương trình không yêu cầu tính năng đó.


và cũng không đưa ra tùy chọn. Có vẻ như
Jonathan

8
@ Jonathan không có gì ngăn cản bạn khởi tạo con trỏ thành null - hoặc 0 như tiêu chuẩn trong C ++.
stefanB

8
Có, nhưng Stroustrup có thể đã tạo ra cú pháp mặc định ưu tiên tính đúng đắn của chương trình hơn là hiệu suất bằng cách khởi tạo con trỏ bằng 0 và bắt lập trình viên phải yêu cầu rõ ràng con trỏ được hủy khởi tạo. Rốt cuộc, hầu hết mọi người thích đúng-nhưng-chậm hơn-nhanh-nhưng-sai, với lý do rằng việc tối ưu hóa một lượng nhỏ mã thường dễ dàng hơn so với sửa lỗi trong toàn bộ chương trình. Đặc biệt là khi phần lớn nó có thể được thực hiện bởi một trình biên dịch tốt.
Robert Tuck

1
Nó không phá vỡ khả năng tương thích. Ý tưởng đã được xem xét kết hợp với "int * x = __uninitialized" - an toàn theo mặc định, tốc độ theo ý định.
MSalters

4
Tôi thích những gì Dlàm. Nếu bạn không muốn khởi tạo, hãy sử dụng cú pháp này float f = void;hoặc int* ptr = void;. Bây giờ nó được khởi tạo theo mặc định nhưng nếu bạn thực sự cần, bạn có thể dừng trình biên dịch làm việc đó.
deft_code

23

Vì quá trình khởi tạo cần có thời gian. Và trong C ++, điều đầu tiên bạn nên làm với bất kỳ biến nào là khởi tạo nó một cách rõ ràng:

int * p = & some_int;

hoặc là:

int * p = 0;

hoặc là:

class A {
   public:
     A() : p( 0 ) {}  // initialise via constructor
   private:
     int * p;
};

1
k, nếu việc khởi tạo mất thời gian và tôi vẫn muốn nó, tehre có làm thế nào để làm cho con trỏ của tôi trở thành null mà không cần thiết lập thủ công không? thấy không, không phải vì tôi không muốn làm đúng, bởi vì tôi dường như sẽ không bao giờ sử dụng các con trỏ đơn nguyên với thùng rác trên địa chỉ của chúng
Jonathan

1
Bạn khởi tạo các thành viên lớp trong phương thức khởi tạo của lớp - đó là cách hoạt động của C ++.

3
@Jonathan: nhưng null cũng là thùng rác. Bạn không thể làm bất cứ điều gì hữu ích với con trỏ null. Tham khảo một trong những cũng giống như một lỗi. Tạo con trỏ có giá trị thích hợp, không phải giá trị rỗng.
DrPizza

2
Khởi tạo apointer tới Nnull có thể là một việc hợp lý. Và có một số thao tác bạn có thể thực hiện trên con trỏ null - bạn có thể kiểm tra chúng và có thể gọi xóa trên chúng.

4
Nếu bạn không bao giờ sử dụng một con trỏ mà không khởi tạo nó một cách rõ ràng, nó không quan trọng những gì nó chứa trước khi bạn cấp cho nó một giá trị và theo nguyên tắc C và C ++ chỉ trả tiền cho những gì bạn sử dụng, điều đó không được thực hiện tự động. Nếu có một giá trị mặc định có thể chấp nhận được (thường là con trỏ null), bạn nên khởi tạo nó. Bạn có thể khởi tạo nó hoặc để nó chưa khởi tạo, sự lựa chọn của bạn.
David Thornley

20

Bởi vì một trong những phương châm của C ++ là:


Bạn không trả tiền cho những gì bạn không sử dụng


Vì lý do này rất, sự operator[]của vectorlớp không kiểm tra xem chỉ số này nằm ngoài giới hạn, ví dụ.


12

Vì lý do lịch sử, chủ yếu là vì đây là cách nó được thực hiện trong C. Tại sao nó được thực hiện như vậy trong C, là một câu hỏi khác, nhưng tôi nghĩ rằng nguyên tắc chi phí bằng không đã liên quan bằng cách nào đó trong quyết định thiết kế này.


Tôi đoán vì C được coi là ngôn ngữ cấp thấp hơn với khả năng truy cập bộ nhớ dễ dàng (hay còn gọi là con trỏ) nên nó cho phép bạn tự do làm những gì bạn muốn và không áp đặt chi phí bằng cách khởi tạo mọi thứ. BTW Tôi nghĩ rằng nó phụ thuộc vào nền tảng này bởi vì tôi đã làm việc trên Linux dựa trên nền tảng di động mà khởi tạo tất cả các bộ nhớ nó với tỉ số 0 trước khi sử dụng vì vậy tất cả các biến sẽ được thiết lập là 0.
stefanB

8

Bên cạnh đó, chúng tôi có một cảnh báo khi bạn thổi nó: "có thể được sử dụng trước khi được gán một giá trị" hoặc rác tương tự tùy thuộc vào trình biên dịch của bạn.

Bạn biên dịch với các cảnh báo, phải không?


Và nó chỉ có thể là một xác nhận rằng việc theo dõi trình biên dịch có thể bị lỗi.
Deduplicator

6

Có rất ít tình huống mà trong đó một biến chưa được khởi tạo sẽ có ý nghĩa và việc khởi tạo mặc định có chi phí nhỏ, vậy tại sao lại làm như vậy?

C ++ không phải là C89. Chết tiệt, ngay cả C cũng không phải là C89. Bạn có thể kết hợp khai báo và mã, vì vậy bạn nên trì hoãn khai báo cho đến khi bạn có giá trị phù hợp để khởi tạo.


2
Sau đó, mỗi giá trị sẽ cần được ghi hai lần - một lần theo quy trình thiết lập của trình biên dịch và một lần nữa bởi chương trình của người dùng. Thường không phải là một vấn đề lớn, nhưng nó sẽ tăng thêm (ví dụ: nếu bạn đang tạo một mảng gồm 1 triệu mục). Nếu bạn muốn tự động khởi tạo, bạn luôn có thể tạo các kiểu của riêng mình để làm điều đó; nhưng bằng cách này, bạn không bị buộc phải chấp nhận chi phí không cần thiết nếu bạn không muốn.
Jeremy Friesner

3

Một con trỏ chỉ là một loại khác. Nếu bạn tạo int, charhoặc bất kỳ loại POD nào khác, nó không được khởi tạo bằng 0, vậy tại sao con trỏ lại phải? Điều này có thể được coi là chi phí không cần thiết đối với một người viết một chương trình như thế này.

char* pBuf;
if (condition)
{
    pBuf = new char[50];
}
else
{
    pBuf = m_myMember->buf();
}

Nếu bạn biết bạn sẽ khởi tạo nó, tại sao chương trình phải chịu chi phí khi bạn tạo lần đầu pBufở đầu phương thức? Đây là nguyên tắc chi phí không.


1
mặt khác, bạn có thể làm điều kiện char * pBuf =? new char [50]: m_myMember-> buf (); Nó giống như một vấn đề về cú pháp sau đó là hiệu quả nhưng tôi vẫn đồng ý với bạn.
the_drow

1
@the_drow: Chà, người ta có thể làm cho nó phức tạp hơn vì vậy việc viết lại như vậy là không thể.
Deduplicator

2

Nếu bạn muốn một con trỏ luôn được khởi tạo thành NULL, bạn có thể sử dụng mẫu C ++ để mô phỏng chức năng đó:

template<typename T> class InitializedPointer
{
public:
    typedef T       TObj;
    typedef TObj    *PObj;
protected:
    PObj        m_pPointer;

public:
    // Constructors / Destructor
    inline InitializedPointer() { m_pPointer=0; }
    inline InitializedPointer(PObj InPointer) { m_pPointer = InPointer; }
    inline InitializedPointer(const InitializedPointer& oCopy)
    { m_pPointer = oCopy.m_pPointer; }
    inline ~InitializedPointer() { m_pPointer=0; }

    inline PObj GetPointer() const  { return (m_pPointer); }
    inline void SetPointer(PObj InPtr)  { m_pPointer = InPtr; }

    // Operator Overloads
    inline InitializedPointer& operator = (PObj InPtr)
    { SetPointer(InPtr); return(*this); }
    inline InitializedPointer& operator = (const InitializedPointer& InPtr)
    { SetPointer(InPtr.m_pPointer); return(*this); }
    inline PObj operator ->() const { return (m_pPointer); }
    inline TObj &operator *() const { return (*m_pPointer); }

    inline bool operator!=(PObj pOther) const
    { return(m_pPointer!=pOther); }
    inline bool operator==(PObj pOther) const
    { return(m_pPointer==pOther); }
    inline bool operator!=(const InitializedPointer& InPtr) const
    { return(m_pPointer!=InPtr.m_pPointer); }
    inline bool operator==(const InitializedPointer& InPtr) const
    { return(m_pPointer==InPtr.m_pPointer); }

    inline bool operator<=(PObj pOther) const
    { return(m_pPointer<=pOther); }
    inline bool operator>=(PObj pOther) const
    { return(m_pPointer>=pOther); }
    inline bool operator<=(const InitializedPointer& InPtr) const
    { return(m_pPointer<=InPtr.m_pPointer); }
    inline bool operator>=(const InitializedPointer& InPtr) const
    { return(m_pPointer>=InPtr.m_pPointer); }

    inline bool operator<(PObj pOther) const
    { return(m_pPointer<pOther); }
    inline bool operator>(PObj pOther) const
    { return(m_pPointer>pOther); }
    inline bool operator<(const InitializedPointer& InPtr) const
    { return(m_pPointer<InPtr.m_pPointer); }
    inline bool operator>(const InitializedPointer& InPtr) const
    { return(m_pPointer>InPtr.m_pPointer); }
};

1
Nếu tôi đang thực hiện điều này, tôi sẽ không bận tâm với te copy ctor hoặc op chuyển nhượng - các giá trị mặc định khá ổn. Và trình hủy của bạn là vô nghĩa. Tất nhiên, bạn cũng có thể kiểm tra các con trỏ bằng cách sử dụng less than operater et all) trong một số trường hợp) vì vậy bạn nên cung cấp chúng.

OK, ít hơn là tầm thường để thực hiện. Tôi đã có bộ hủy để nếu đối tượng đi ra ngoài phạm vi (tức là cục bộ được xác định trong kính con của một hàm) nhưng vẫn chiếm không gian trên ngăn xếp, bộ nhớ sẽ không bị bỏ lại như một con trỏ treo lơ lửng. Nhưng anh bạn nghiêm túc, tôi đã viết điều này trong vòng chưa đầy 5 phút. Nó không có nghĩa là phải hoàn hảo.
Adisak

OK đã thêm tất cả các toán tử so sánh. Các ghi đè mặc định có thể là thừa nhưng chúng ở đây rõ ràng vì đây là một ví dụ.
Adisak

1
Tôi không thể hiểu làm thế nào điều này sẽ làm cho tất cả các con trỏ trống nếu không nhập chúng theo cách thủ công, bạn có thể giải thích bạn đã làm gì ở đây không?
Jonathan

1
@Jonathan: Về cơ bản đây là "con trỏ thông minh" không làm gì khác hơn là đặt con trỏ thành null. IE thay vì Foo *a, bạn sử dụng InitializedPointer<Foo> a- Một bài tập hoàn toàn mang tính học thuật cũng như Foo *a=0ít đánh máy hơn. Tuy nhiên, đoạn mã trên rất hữu ích từ quan điểm giáo dục. Với một chút sửa đổi (đối với ctor / dtor "giữ chỗ" và hoạt động chuyển nhượng), nó có thể dễ dàng được mở rộng sang nhiều loại con trỏ thông minh khác nhau bao gồm con trỏ phạm vi (không có trên bộ hủy) và con trỏ được đếm tham chiếu bằng cách thêm inc / hoạt động dec khi m_pPointer được đặt hoặc xóa.
Adisak

2

Lưu ý rằng dữ liệu tĩnh được khởi tạo bằng 0 (trừ khi bạn nói khác).

Và có, bạn nên luôn khai báo các biến của mình càng muộn càng tốt và với giá trị ban đầu. Mã như

int j;
char *foo;

nên đặt chuông báo thức khi bạn đọc nó. Tôi không biết liệu có bất kỳ lint nào có thể thuyết phục được cá chép về điều đó không vì nó hợp pháp 100%.


đó là ĐẢM BẢO, hay chỉ là một thực tế phổ biến được sử dụng bởi các trình biên dịch ngày nay?
gha.st

1
biến tĩnh được khởi tạo thành 0, điều này cũng làm đúng cho con trỏ (tức là, đặt chúng thành NULL, không phải tất cả các bit 0). Hành vi này được đảm bảo bởi tiêu chuẩn.
Alok Singhal

1
khởi tạo dữ liệu tĩnh về 0 được đảm bảo bởi tiêu chuẩn C và C ++, nó không chỉ là thông lệ phổ biến
groovingandi

1
có lẽ bởi vì một số người muốn đảm bảo rằng ngăn xếp của họ được căn chỉnh một cách đẹp mắt nên họ khai báo trước tất cả các biến ở đầu hàm? Có lẽ họ đang viết bằng phương ngữ ac mà YÊU CẦU điều này?
KitsuneYMG

1

Một lý do có thể khác tại sao, là tại thời gian liên kết các con trỏ được cấp một địa chỉ, nhưng việc định địa chỉ / hủy tham chiếu gián tiếp một con trỏ là trách nhiệm của lập trình viên. Thông thường, trình biên dịch không quan tâm ít hơn, nhưng gánh nặng được chuyển cho lập trình viên để quản lý các con trỏ và đảm bảo rằng không xảy ra rò rỉ bộ nhớ.

Thực sự, tóm lại, chúng được khởi tạo theo nghĩa là tại thời điểm liên kết, biến con trỏ được cấp một địa chỉ. Trong mã ví dụ của bạn ở trên, điều đó được đảm bảo sẽ xảy ra sự cố hoặc tạo ra một SIGSEGV.

Vì lợi ích của sự tỉnh táo, hãy luôn khởi tạo con trỏ tới NULL, theo cách đó nếu có bất kỳ nỗ lực nào để tham khảo nó mà không có mallochoặc newsẽ gợi ý cho lập trình viên lý do tại sao chương trình hoạt động sai.

Hy vọng điều này sẽ hữu ích và có ý nghĩa,


0

Chà, nếu C ++ đã khởi tạo con trỏ, thì những người C phàn nàn "C ++ chậm hơn C" sẽ có điều gì đó thực sự đáng lo ngại;)


Đó không phải là lý do của tôi. Lý do của tôi là nếu phần cứng có 512 byte ROM và 128 byte RAM và một lệnh bổ sung đến không một con trỏ thậm chí là một byte thì đó là một tỷ lệ khá lớn của toàn bộ chương trình. Tôi cần byte đó!
Jerry Jeremiah

0

C ++ xuất phát từ nền tảng C - và có một số lý do trở lại từ điều này:

C, thậm chí nhiều hơn C ++ là một ngôn ngữ thay thế hợp ngữ. Nó không làm bất cứ điều gì bạn không yêu cầu nó làm. Vì vậy: Nếu bạn muốn NULL - hãy làm điều đó!

Ngoài ra, nếu bạn vô hiệu hóa mọi thứ bằng một ngôn ngữ bình thường như C, các câu hỏi về tính nhất quán sẽ tự động xuất hiện: Nếu bạn làm sai điều gì đó - nó có nên tự động được điền không? Điều gì về một cấu trúc được tạo trên ngăn xếp? tất cả các byte có nên được làm bằng không? Còn các biến toàn cục thì sao? còn câu lệnh như "(* 0x18);" điều đó không có nghĩa là vị trí bộ nhớ 0x18 nên được làm bằng không?


Trên thực tế, trong C, nếu bạn muốn cấp phát bộ nhớ bằng 0, bạn có thể sử dụng calloc().
David Thornley

1
chỉ là quan điểm của tôi - nếu bạn muốn làm điều đó, bạn có thể, nhưng nó không được thực hiện cho bạn một cách tự động
gha.st

0

Những gợi ý bạn nói đến là gì?

Để an toàn ngoại lệ, luôn luôn sử dụng auto_ptr, shared_ptr, weak_ptrvà các biến thể khác của họ.
Dấu hiệu của mã tốt là mã không bao gồm một lệnh gọi đến delete.


3
Kể từ C ++ 11, tránh xa auto_ptrvà thay thế unique_ptr.
Deduplicator

-2

Oh Boy. Câu trả lời thực sự là rất dễ dàng để xóa bộ nhớ, đây là cách khởi tạo cơ bản cho một con trỏ. Điều này cũng không liên quan gì đến việc khởi tạo chính đối tượng.

Xem xét các cảnh báo mà hầu hết các trình biên dịch đưa ra ở mức cao nhất, tôi không thể tưởng tượng được việc lập trình ở mức cao nhất và coi chúng là lỗi. Vì việc bật chúng lên chưa bao giờ giúp tôi tiết kiệm dù chỉ một lỗi trong số lượng lớn mã được tạo ra, tôi không thể khuyến nghị điều này.


Nếu con trỏ không được dự kiến sẽ trở thành NULL, khởi tạo nó để đó chỉ là càng nhiều lỗi.
Deduplicator
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.