nhầm lẫn chuỗi, chuỗi và char *


141

Câu hỏi của tôi có thể được rút ra, chuỗi được trả về từ đâu stringstream.str().c_str()trong bộ nhớ và tại sao nó không thể được gán cho một const char*?

Ví dụ mã này sẽ giải thích nó tốt hơn tôi có thể

#include <string>
#include <sstream>
#include <iostream>

using namespace std;

int main()
{
    stringstream ss("this is a string\n");

    string str(ss.str());

    const char* cstr1 = str.c_str();

    const char* cstr2 = ss.str().c_str();

    cout << cstr1   // Prints correctly
        << cstr2;   // ERROR, prints out garbage

    system("PAUSE");

    return 0;
}

Giả định stringstream.str().c_str()có thể được gán const char*cho một lỗi dẫn đến một lỗi khiến tôi mất một thời gian để theo dõi.

Đối với điểm thưởng, bất cứ ai cũng có thể giải thích tại sao thay thế couttuyên bố bằng

cout << cstr            // Prints correctly
    << ss.str().c_str() // Prints correctly
    << cstr2;           // Prints correctly (???)

in dây có đúng không?

Tôi đang biên dịch trong Visual Studio 2008.

Câu trả lời:


201

stringstream.str()trả về một đối tượng chuỗi tạm thời bị hủy ở cuối biểu thức đầy đủ. Nếu bạn nhận được một con trỏ tới một chuỗi C từ đó ( stringstream.str().c_str()), nó sẽ trỏ đến một chuỗi bị xóa trong đó câu lệnh kết thúc. Đó là lý do tại sao mã của bạn in rác.

Bạn có thể sao chép đối tượng chuỗi tạm thời đó sang một số đối tượng chuỗi khác và lấy chuỗi C từ chuỗi đó:

const std::string tmp = stringstream.str();
const char* cstr = tmp.c_str();

Lưu ý rằng tôi đã tạo chuỗi tạm thời const, bởi vì bất kỳ thay đổi nào đối với chuỗi đó có thể khiến nó phân bổ lại và do đó hiển thị cstrkhông hợp lệ. Sẽ an toàn hơn khi không lưu trữ kết quả của cuộc gọi đến str()cstrchỉ sử dụng cho đến khi kết thúc biểu thức đầy đủ:

use_c_str( stringstream.str().c_str() );

Tất nhiên, cái sau có thể không dễ dàng và sao chép có thể quá đắt. Thay vào đó, những gì bạn có thể làm là liên kết tạm thời với một consttài liệu tham khảo. Điều này sẽ kéo dài thời gian tồn tại đến thời gian tham chiếu:

{
  const std::string& tmp = stringstream.str();   
  const char* cstr = tmp.c_str();
}

IMO đó là giải pháp tốt nhất. Thật không may, nó không được biết đến nhiều.


13
Cần lưu ý rằng thực hiện một bản sao (như trong ví dụ đầu tiên của bạn) sẽ không nhất thiết phải đưa ra bất kỳ chi phí nào - nếu str()được triển khai theo cách mà RVO có thể khởi động (rất có thể), trình biên dịch được phép xây dựng kết quả trực tiếp vào tmp, trốn tránh tạm thời; và bất kỳ trình biên dịch C ++ hiện đại nào cũng sẽ làm như vậy khi tối ưu hóa được kích hoạt. Tất nhiên, giải pháp liên kết tham chiếu liên kết đảm bảo không có bản sao, vì vậy có thể tốt hơn - nhưng tôi nghĩ nó vẫn đáng để làm rõ.
Pavel Minaev

1
"Tất nhiên, giải pháp liên kết tham chiếu đảm bảo không sao chép" <- không. Trong C ++ 03, hàm tạo sao chép cần có thể truy cập được và việc triển khai được phép sao chép trình khởi tạo và liên kết tham chiếu với bản sao.
Julian Schaub - litb

1
Ví dụ đầu tiên của bạn là sai. Giá trị được trả về bởi c_str () là nhất thời. Nó không thể được dựa vào sau khi kết thúc tuyên bố hiện tại. Do đó, bạn có thể sử dụng nó để truyền giá trị cho hàm nhưng bạn KHÔNG BAO GIỜ gán kết quả của c_str () cho biến cục bộ.
Martin York

2
@litb: Bạn đúng kỹ thuật. Con trỏ hợp lệ cho đến khi gọi phương thức phi chi phí tiếp theo trên chuỗi. Vấn đề là việc sử dụng vốn đã nguy hiểm. Có thể không phải cho nhà phát triển ban đầu (mặc dù trong trường hợp này là vậy) nhưng đặc biệt là các bản sửa lỗi bảo trì tiếp theo, loại mã này trở nên cực kỳ dễ vỡ. Nếu bạn muốn làm điều này, bạn nên bọc phạm vi con trỏ sao cho việc sử dụng nó càng ngắn càng tốt (tốt nhất là độ dài của biểu thức).
Martin York

1
@sbi: Ok, cảm ơn, điều đó rõ ràng hơn. Mặc dù vậy, nói đúng ra, vì var 'chuỗi str' không được sửa đổi trong đoạn mã trên, str.c_str () vẫn hoàn toàn hợp lệ, nhưng tôi đánh giá cao sự nguy hiểm tiềm tàng trong các trường hợp khác.
William Knight

13

Những gì bạn đang làm là tạo ra một tạm thời. Điều đó tạm thời tồn tại trong một phạm vi được xác định bởi trình biên dịch, sao cho nó đủ dài để đáp ứng các yêu cầu của nơi nó sẽ đến.

Ngay sau khi câu lệnh const char* cstr2 = ss.str().c_str();hoàn tất, trình biên dịch sẽ thấy không có lý do gì để giữ chuỗi tạm thời và nó bị phá hủy, và do đó, bạn const char *đang trỏ đến bộ nhớ miễn phí.

Câu lệnh của bạn string str(ss.str());có nghĩa là tạm thời được sử dụng trong hàm tạo cho stringbiến strmà bạn đã đặt trên ngăn xếp cục bộ và tồn tại trong khoảng thời gian bạn mong đợi: cho đến khi kết thúc khối hoặc hàm bạn đã viết. Do đó, const char *bên trong vẫn là bộ nhớ tốt khi bạn thử cout.


6

Trong dòng này:

const char* cstr2 = ss.str().c_str();

ss.str()sẽ tạo một bản sao của nội dung của chuỗi. Khi bạn gọi c_str()trên cùng một đường dây, bạn sẽ tham chiếu dữ liệu hợp pháp, nhưng sau đường dây đó, chuỗi sẽ bị hủy, khiến bạn char*chỉ đến bộ nhớ không dấu.


5

Đối tượng std :: string được trả về bởi ss.str () là một đối tượng tạm thời sẽ có thời gian sống giới hạn trong biểu thức. Vì vậy, bạn không thể gán một con trỏ cho một đối tượng tạm thời mà không nhận được rác.

Bây giờ, có một ngoại lệ: nếu bạn sử dụng tham chiếu const để lấy đối tượng tạm thời, việc sử dụng nó cho thời gian sử dụng rộng hơn là hợp pháp. Ví dụ bạn nên làm:

#include <string>
#include <sstream>
#include <iostream>

using namespace std;

int main()
{
    stringstream ss("this is a string\n");

    string str(ss.str());

    const char* cstr1 = str.c_str();

    const std::string& resultstr = ss.str();
    const char* cstr2 = resultstr.c_str();

    cout << cstr1       // Prints correctly
        << cstr2;       // No more error : cstr2 points to resultstr memory that is still alive as we used the const reference to keep it for a time.

    system("PAUSE");

    return 0;
}

Bằng cách đó bạn có được chuỗi trong một thời gian dài hơn.

Bây giờ, bạn phải biết rằng có một loại tối ưu hóa được gọi là RVO nói rằng nếu trình biên dịch thấy một khởi tạo thông qua một lệnh gọi hàm và hàm đó trả về tạm thời, nó sẽ không thực hiện sao chép mà chỉ làm cho giá trị được gán là tạm thời . Theo cách đó, bạn không cần phải thực sự sử dụng một tài liệu tham khảo, chỉ khi bạn muốn chắc chắn rằng nó sẽ không sao chép rằng nó cần thiết. Làm như vậy:

 std::string resultstr = ss.str();
 const char* cstr2 = resultstr.c_str();

sẽ tốt hơn và đơn giản hơn


5

Các ss.str()tạm thời bị phá hủy sau khi khởi tạo của cstr2hoàn tất. Vì vậy, khi bạn in nó với cout, chuỗi c được liên kết với std::stringtạm thời đó đã bị phá hủy từ lâu, và do đó bạn sẽ gặp may nếu nó gặp sự cố và khẳng định, và không may mắn nếu nó in rác hoặc có vẻ không hoạt động.

const char* cstr2 = ss.str().c_str();

cstr1Tuy nhiên, chuỗi C nơi trỏ đến được liên kết với một chuỗi vẫn tồn tại tại thời điểm bạn thực hiện cout- vì vậy nó sẽ in chính xác kết quả.

Trong đoạn mã sau, mã đầu tiên cstrlà đúng (tôi giả sử nó nằm cstr1trong mã thật?). Cái thứ hai in chuỗi c được liên kết với đối tượng chuỗi tạm thời ss.str(). Đối tượng bị hủy ở cuối đánh giá biểu thức đầy đủ mà nó xuất hiện. Biểu thức đầy đủ là toàn bộ cout << ...biểu thức - vì vậy trong khi chuỗi c là đầu ra, đối tượng chuỗi liên kết vẫn tồn tại. Vì cstr2- đó là sự xấu hoàn toàn mà nó thành công. Nó có thể nhất là bên trong chọn cùng một vị trí lưu trữ cho tạm thời mới mà nó đã chọn cho tạm thời được sử dụng để khởi tạo cstr2. Nó cũng có thể sụp đổ.

cout << cstr            // Prints correctly
    << ss.str().c_str() // Prints correctly
    << cstr2;           // Prints correctly (???)

Sự trở lại của c_str()thường sẽ chỉ trỏ đến bộ đệm chuỗi bên trong - nhưng đó không phải là một yêu cầu. Chuỗi có thể tạo thành một bộ đệm nếu việc triển khai bên trong của nó không liền kề nhau (điều đó hoàn toàn có thể - nhưng trong Tiêu chuẩn C ++ tiếp theo, các chuỗi cần được lưu trữ liên tục).

Trong GCC, các chuỗi sử dụng đếm tham chiếu và sao chép trên ghi. Do đó, bạn sẽ thấy rằng những điều sau đây đúng (ít nhất là trên phiên bản GCC của tôi)

string a = "hello";
string b(a);
assert(a.c_str() == b.c_str());

Hai chuỗi chia sẻ cùng một bộ đệm ở đây. Tại thời điểm bạn thay đổi một trong số chúng, bộ đệm sẽ được sao chép và mỗi bộ đệm sẽ giữ bản sao riêng của nó. Các triển khai chuỗi khác làm những việc khác nhau, mặc dù.

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.