一、哪裡遇到了這個問題
在進行MNN機器學習框架的MFC應用開發的時候遇到了這個問題,在視窗控制元件程式碼段 “MNN_Inference_BarCode_MFCDlg.cpp” 當中需要進行輸入圖片的讀取。透過opnecv2庫建立cv:Mat物件,具體程式碼如下,是一個按鈕的控制元件程式碼。重點關注其中指標操作的內容
//按鈕1,用於選擇圖片檔案
void CMNNInferenceBarCodeMFCDlg::OnBnClickedButton1()
{
CFileDialog fileDlg(TRUE, _T("*.txt"), NULL, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, _T(" Png Files (*.png)|*.png| Jpg Files (*.jpg)|*.jpg| bmp Files (*.bmp)|*.bmp| BMP Files (*.BMP)|*.BMP| All Files (*.*)|*.*||"));
if (fileDlg.DoModal() == IDOK)
{
NIU::m_configureinfo_Dlg.picture_filepath = fileDlg.GetPathName(); // 儲存圖片檔案路徑
SetDlgItemText(IDC_EDIT1, NIU::m_configureinfo_Dlg.picture_filepath);
LoadImagePaths(); // 載入圖片路徑
// 將CString轉換為std::string,用於後續載入影像
std::string imagePathStr(CT2A(NIU::m_configureinfo_Dlg.picture_filepath.GetString()));
// 使用OpenCV載入影像並獲取尺寸
cv::Mat image = cv::imread(imagePathStr);
if (!image.empty())
{
// 儲存影像的寬度和高度到類的成員變數
NIU::m_imageinfo_Dlg.img_width = image.cols; // 影像的寬度
NIU::m_imageinfo_Dlg.img_height = image.rows; // 影像的高度
// 指向影像資料,這裡使用成員變數而不是區域性變數是為了保證變數的生命週期,避免當OnBnClickedButton1()函式結束之後,變數被銷燬導致指標成為懸空指標
// 將載入的影像儲存到成員變數 m_imageMat 中
m_imageMat = image; //請仔細看這裡!!!!!!!!!!!!!!
// 指向影像資料
NIU::m_imageinfo_Dlg.img_data_pt = m_imageMat.data;
// 顯示影像在圖片控制元件上
DisplayImage(m_imageMat);
}
else
{
AfxMessageBox(_T("無法載入影像檔案,請檢查檔案路徑。"));
CButton* check1 = (CButton*)GetDlgItem(IDC_CHECK1);
CButton* check2 = (CButton*)GetDlgItem(IDC_CHECK2);
check1->SetCheck(BST_UNCHECKED); // 取消選中 Check1
check2->SetCheck(BST_UNCHECKED); // 取消選中 Check2
}
}
}
程式碼當中的img_data_pt為另外一份程式碼 “AIEngineCommon.cpp” 當中類的一個成員,具體如下:
class NIU
{
public:
// 用於儲存輸入影像資訊的結構體
typedef struct ImageInfo
{
Coordinate_VOC roi_coord; // [輸入]ROI座標
void* img_data_pt; // [輸入]圖片儲存地址
int img_height; // [輸入]影像高度
int img_width; // [輸入]影像寬度
prep_type_t prep_type; // [輸入]前處理型別
};
//宣告靜態變數
static ImageInfo m_imageinfo_Dlg;
}
可以看到在opencv讀取完影像之後,透過 “m_imageMat.data” 這個影像指標指向影像畫素資料的首地址,然後將這個指標賦值給NIU類當中的 “img_data_pt” 指標。這樣方便影像儲存地址可以跨檔案呼叫。
這裡需要注意的是這個指向影像畫素資料的首地址指標,要麼是屬於全域性變數,要麼是資料類當中的成員變數,千萬不能是這個函式當中的區域性變數。這裡設計到兩個概念,分別是:
-
指標有效性
(1)指標有效性是非常重要的概念,涉及到指標是否指向了合法的記憶體地址。
(2)初始化:指標在使用前應該被初始化。未初始化的指標可能指向任意記憶體區域,這會導致未定義行為。
(3)分配記憶體:在使用指標之前,通常需要為其分配記憶體。例如,在C++中,可以使用 new 關鍵字為指標分配記憶體。
(4)釋放記憶體:當指標不再需要時,應該釋放它所指向的記憶體。在C++中,這通常是透過 delete 關鍵字完成的。如果忘記釋放記憶體,可能會導致記憶體洩漏。
(5)生命週期:指標的有效性與它所指向的資料的生命週期有關。如果資料被銷燬(例如,一個區域性變數離開了它的作用域),那麼指向它的指標就變得無效。
(6)野指標:如果指標被釋放了記憶體,但沒有將其設定為 nullptr,那麼它就變成了野指標。野指標指向的是一個不再有效的記憶體地址,試圖訪問它可能會導致程式崩潰。
(7)懸掛指標:當指標指向的記憶體被釋放後,如果再次被分配給另一個物件,那麼原來的指標就變成了懸掛指標。如果透過懸掛指標訪問資料,可能會訪問到錯誤的資料。
(8)指標與物件的關係:指標的有效性也與它所指向的物件的狀態有關。如果物件被修改,那麼指標可能需要更新以反映這種變化。
(9)多執行緒環境:在多執行緒環境中,指標的有效性更加複雜,因為多個執行緒可能同時訪問和修改指標和它所指向的資料。
(10)智慧指標:為了避免手動管理記憶體,可以使用智慧指標(如C++中的 std::unique_ptr、std::shared_ptr),它們可以自動管理記憶體的分配和釋放。 -
變數生命週期
(1)區域性變數:
定義在函式或程式碼塊內部的變數稱為區域性變數。
它們的生命週期從定義時開始,到函式或程式碼塊執行結束時結束。
區域性變數在函式呼叫結束後會被銷燬,它們通常儲存在棧(stack)上。(2)全域性變數:
全域性變數是在函式外部定義的變數,它們在整個程式的執行期間都是可見的。
它們的生命週期從程式開始執行時開始,到程式結束時結束。
全域性變數通常儲存在資料段(data segment)或BSS段(如果未初始化)。(3)靜態變數:
靜態變數是使用 static 關鍵字宣告的變數,它們的生命週期貫穿整個程式的執行期間。
靜態區域性變數只在定義它們的函式或程式碼塊中可見,每次函式呼叫時它們都會保留上一次的值。
靜態全域性變數則在整個程式中可見,但它們的作用域可能被限制在定義它們的檔案內。(4)動態分配的變數:
使用動態記憶體分配(如C++中的 new 或C中的 malloc)建立的變數,它們的生命週期由程式設計師控制。
必須使用相應的釋放函式(如 delete 或 free)來手動管理這些變數的生命週期,否則可能會導致記憶體洩漏。(5)執行緒區域性變數:
在多執行緒環境中,執行緒區域性變數是每個執行緒獨有的,它們線上程的生命週期內有效。
執行緒結束時,執行緒區域性變數會被銷燬。(6)物件的成員變數:
物件的成員變數(也稱為屬性或欄位)的生命週期與物件本身相同。
當物件被建立時,成員變數被初始化;當物件被銷燬時,成員變數也會隨之銷燬。(7)自動變數:
在某些程式語言中,如C和C++,自動變數是區域性變數的一種,它們在進入作用域時自動建立,在離開作用域時自動銷燬。(8)暫存器變數:
暫存器變數是儲存在CPU暫存器中的變數,它們通常用於最佳化效能,因為訪問暫存器比訪問記憶體更快。
暫存器變數的生命週期通常與它們所在的作用域相同。(9)常量:
常量是一旦初始化後其值就不能被改變的變數。
它們的生命週期可以是區域性的、全域性的、靜態的等,這取決於它們是如何宣告的。
因此,如果上述 “img_data_pt” 指標被賦值的物件是一個區域性變數,比如:
// 使用OpenCV載入影像並獲取尺寸
cv::Mat image = cv::imread(imagePathStr);
if (!image.empty())
{
NIU::m_imageinfo_Dlg.img_width = image.cols; // 影像的寬度
NIU::m_imageinfo_Dlg.img_height = image.rows; // 影像的高度
m_imageMat = image;
NIU::m_imageinfo_Dlg.img_data_pt = image.data;
// 顯示影像在圖片控制元件上
DisplayImage(m_imageMat);
}
那麼當該函式結束的時候,CV::Mat image例項就會被銷燬,導致 “img_data_pt” 被賦值的是未知的,導致了 指標懸空 的問題。