65.9K
CodeProject 正在变化。 阅读更多。
Home

可爱的指针

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.41/5 (22投票s)

2003年6月6日

6分钟阅读

viewsIcon

94179

本文的主题是指针。我将在下面描述一些与使用指针相关的问题、bug 和技术解决方案。本文对于初学者以及正在学习 C++ 的其他编程语言程序员会很有用。

引言

本文的主题是指针。我将在下面描述一些与使用指针相关的问题、bug 和技术解决方案。本文对于初学者以及正在学习 C 和 C++ 的其他编程语言程序员会很有用。

糟糕的指针

有很多程序员认为指针是一种糟糕的结构,就像“go to”操作符一样。指针是如此糟糕,以至于在 Basic、Java、C# 中找不到它们。但事实并非如此。所有在 Windows 或 Unix 下执行的应用程序都使用指针!许多 API 函数接收指针并返回指针,因此如果你使用 API,你就在使用指针。

例如,如果我在 Visual Basic 程序中声明这样的 API 函数接口

Private Declare Sub SetLocalTime Lib "kernel32" (localTime As SYSTEMTIME)

Basic 指令 Call SetLocalTime(tmLocal),将向 API 函数发送指向 SYSTEMTIME 结构的指针。

为什么许多语言不将指针作为语言结构支持?——因为指针很危险。很容易犯下编译器无法发现的错误。更可能的是,在调试程序时,你会发现不了错误。如果你的程序能正常运行,那只是因为它没有被合适的用户启动。好的用户总能找到让你的程序崩溃的方法。

这是一个非常常见的与指针相关的错误示例

char * CloneName(char *pSomeName)
{
     char *pTmpName = new char[strlen(pSomeName)]; // Error! 
     strcpy(pTmpName, pSomeName); 
     return pTmpName;
}

此函数必须克隆一个字符串。在此示例中,字符串副本后面的一字节内存将被破坏。正确的分配指令是 new char[strlen(pSomeName) +1]。C 和 C++ 中的字符串以零代码结束。这个错误可能立即、偶尔或永远不会使你的程序崩溃!一切都取决于字符串后面的一字节。

好的指针

指针是 C++ 的语言结构。从历史上看,这种语言延续了 C 语言的传统,C 语言最初是作为汇编语言的良好替代品而创建的。指针允许程序员非常有效地管理内存分配。如果你工作准确,一切都会顺利。

什么是指针?

指针是一个特殊的变量,用于存储某个内存地址。因此 sizeof(pointer) 很小,并且取决于操作系统。对于 Win32,它等于 4 字节。指针具有“指向某种类型的指针”类型。指针可以转换为整数值,整数值可以转换为指针。这在 Windows API 函数中广泛使用。

这是“主要”Windows 函数的标题。它向窗口发送消息。

LRESULT SendMessage(
     HWND hWnd, // handle of destination window
     UINT Msg, // message to send
     WPARAM wParam, // first message parameter
     LPARAM lParam // second message parameter
);

WPARAMLPARAM 是整数类型。但许多消息将它们用作指针。例如,我想在句柄为 hWnd 的某个窗口中打印文本。我这样做

SendMessage(hWnd, WM_SETTEXT, 0, (LPARAM)"Some Text");

“Some Text”是一个静态文本常量。它在内存中有一个地址,类型为 char*。此示例展示了从 char* 到整数的转换(LPARAM 是一个长整数)。

指针也是数组。数组类型实际上与指针类型不同。但它与指针非常接近,并且易于转换为指针。所有动态数组都是指针。

指针和数组

这是一篇文章。我不想重写一些 C++ 书籍。所以,我只在这里展示一个有趣的例子。它展示了使用二维自动数组和二维动态数组之间的区别。

#include <iostream.h> 

void OutAutoElm(int nRow, int nCol, int *pAutoArray, int nSize);
void OutDynElm(int nRow, int nCol, int **pDynArray); 

int main(int argc, char* argv[])
{
     const int nSize = 5; // Size of matrix 
   // Auto Array. Allocate memory in the stack.
     int AutoArray[nSize][nSize]; 
     int **DynArray; // Dynamic Array pointer.
     // Memory allocation for Dynamic Array in the heap.
     DynArray = new int*[nSize];
     for(int i=0; i< nSize; i++){ 
          DynArray[i] = new int[nSize]; 
     } 
   // Assign some element of AutoArray
     AutoArray[2][3] = 7; 
   // and call output function
     OutAutoElm(2, 3, (int *)AutoArray, nSize); 
     DynArray[3][4] = 9; // Assign some element of DynamicArray
     OutDynElm(3, 4, DynArray); // and call output function
     AutoArray[5][0] = 10; 
      // Error! Outside of the array. The last element is [4][4]
      // But the program executed in my system without any errors.

     // Release memory of Dynamic Array 
      for(i=0; i< nSize; i++){ 
          delete[] DynArray[i]; 
     } 
     delete[] DynArray; 
     return 0; 
}
void OutAutoElm(int nRow, int nCol, int *pAutoArray, int nSize)
{
    // What a strange expression!
    int nValue = *(pAutoArray + nRow*nSize + nCol); 
    cout << "AutoArray["<<nRow<<"]
        ["<<nCol<<"]="<< nValue 
        << endl;
}
void OutDynElm(int nRow, int nCol, int **pDynArray)
{
     int nValue = pDynArray[nRow][nCol]; // Looks Normal
     cout << "DynArray["<<nRow<<"]
     ["<<nCol<<"]="<< nValue 
     << endl;
}

一个非常有趣的例子!AutoArray[2][3] = 7DynArray[3][4] = 9 看起来是相同的指令。但其中一个是 *(AutoArray + 2 * 5 + 3) = 7,另一个是 *(*(DynArray+3)+4) = 9;参见图 1。

危险

使用指针时有一些常见的错误。其中大多数都非常危险,因为它们可以在你的系统中执行而不会出现运行时错误。没有人知道它们何时何地会使系统崩溃。

  1. 在没有内存分配的情况下使用指针。

    示例

         char *Str;
         cin >> Str;
  2. 数组溢出。参见前几段中的示例。
  3. 将值发送给等待指针的函数。

    示例(流行的初学者错误)

         int nSomeInt = 0;
         scanf("%d", nSomeInt); // send the value

    scanf 定义为 int scanf(const char *, ...)。编译器无法测试变量的类型。因此,你不会收到错误或警告消息。正确的解决方案是

         int nSomeInt = 0;
         scanf("%d", &nSomeInt); // send the pointer
  4. 指针释放错误。常见的错误是内存泄漏。我们使用 new 语句而没有 delete 语句。下面显示了一些其他错误

    示例 1

         int *pArray = new int[10];
         ... 
         delete pArray; // must be delete[] pArray

    示例 2

         int a = 0;
         int*p = &a;
         delete p; // Nothing for release! 
                 //Use delete only when 
                 //instruction "new" was used!

    示例 3

         int *a = new int;
         int *b = a;
         delete a;
         delete b; // Error. 
                 //The memory was cleared 
                 //by previous delete.
  5. 类型转换错误。我们可以将指针转换为错误的类型并使用它。

    示例

         class A{};
         class B{
         public: 
              B():M(5){}
              int M;
         };
         int main(int argc, char* argv[])
         {
              A* pA = new A;
              B* pB = new B;
              cout << ((B*)pA)->M << endl; //Error! There is no M in A!
         }
  6. 奇怪的分配。这不是我的幻想。我遇到过同样的代码!
         void SomeFun(int a);
         ....
         int main(){
              SomeFun(*(new int) ); 
           // Temporary variable with memory allocation.
                // Deleting memory is impossible.
         }

这里我只描述了 C 和 C++ 语言中常见的普通错误。当我们使用类、继承、多重继承、模板和其他 OOP 结构时,我们有更多机会在使用指针时犯错。保持乐观!

建议

有一些规则可以防止你犯很多错误。

  1. 只有在你确实需要时才使用指针。使用指针的常见情况有:创建动态数组,在一个函数中创建对象并在另一个函数中删除它,从库函数接收指针。如果你可以使用自动变量或引用来代替,则不要使用指针。
  2. 如果在定义指针时没有分配内存,请将其设置为 NULL。空指针比未初始化的指针更适合调试。

    示例

         int *pSome = NULL;
  3. 始终测试函数返回的指针是否为 NULL

    示例

         if ( ( pFile = fopen("SomeFile","r") ) == NULL){
              cerr << " SomeFile Open Error!" << end;
         }
  4. 始终使用断言宏测试传入的指针。它仅在调试模式下有效,在发布模式下将被忽略。

    示例

         void SomeFun(SomeType *pSomePointer){
              ASSERT(pSomePointer);
              . . .
         }
  5. 切勿使用 C 风格字符串和 C 风格数组。始终使用库类代替。

    STL 使用示例

         #include <string>
         #include <vector>
         #include <iostream>
         using namespace std;
         int main(int argc, char* argv[])
         {
          // Some string object, use instead char *
              string sName1 = "Jeanne"; 
          // Some array of strings. Use instead char** 
              vector<string> sNames; 
              sNames.push_back(sName1);
              sNames.push_back("Ann");
              sNames.push_back("George");
              for(int i=0; i < sNames.size(); i++){
                   cout << sNames[i] << endl;
              }
              return 0;
         }

    如你所见,此示例中没有指针、newdelete 操作。相同的 MFC 类称为 CStringCArray

  6. 如果你使用标准字符串或容器类并需要其数据的指针。你可以轻松获取它。所有类都有相应的方法或操作符。

    MFC 示例

         CString sSomeStr; 
         (LPCTSTR) sSomeStr; // char * pointer to the string buffer
         CArray <int,int> SomeArray;
         SomeArray.GetData( ); // int * pointer to the array buffer
         STL examples:
         string sSomeStr;
         sSomeStr.c_str(); // char * pointer to the string buffer
         vector <int> SomeArray;
         &SomeArray[0]; // int * pointer to the array buffer

    记住危险!仅在你确实需要时才使用此类转换。例如,如果你需要将指针发送到库函数。它可以是 Win API 函数或其他函数。

  7. 当你需要将一些大对象发送给函数时,请使用引用而不是指针。至少你无法更改引用的内存地址。

  8. 使用新的类型转换操作符 static_cast 代替旧式转换。

    示例

         A* pA = new A;
         B* pB = new B;
         cout << ((B*)pA)->M << endl; // Compiler said "OK!"
         cout << static_cast<B*>(pA)->M << endl; // Compile said "Error!"
  9. 在可能的情况下使用常量修饰符

    示例

         int a = 5;
         const int* p1 = &a; // You cannot change pointed value
         int* const p2 = &a; // You cannot change pointer
         const int* const p2 = &a; // You cannot change anything
  10. 请记住,每个 "new SomeType" 操作符都需要 "delete PointerSomeType" 操作符,每个 "new SomeType[]" 操作符都需要 "delete[] PointerSomeType" 操作符。
  11. 请记住,如果你的程序运行正确,这并不意味着它内部没有指针错误。错误可能在另一台计算机上的另一个时间出现。务必小心!

公告

本文只描述了使用指针的简单问题。我计划在下一篇文章中继续这个主题,该文章将命名为“指针和类”。

链接

© . All rights reserved.