可爱的指针






4.41/5 (22投票s)
2003年6月6日
6分钟阅读

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 );
WPARAM
和 LPARAM
是整数类型。但许多消息将它们用作指针。例如,我想在句柄为 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] = 7
和 DynArray[3][4] = 9
看起来是相同的指令。但其中一个是 *(AutoArray + 2 * 5 + 3) = 7
,另一个是 *(*(DynArray+3)+4) = 9
;参见图 1。
危险
使用指针时有一些常见的错误。其中大多数都非常危险,因为它们可以在你的系统中执行而不会出现运行时错误。没有人知道它们何时何地会使系统崩溃。
- 在没有内存分配的情况下使用指针。
示例
char *Str; cin >> Str;
- 数组溢出。参见前几段中的示例。
- 将值发送给等待指针的函数。
示例(流行的初学者错误)
int nSomeInt = 0; scanf("%d", nSomeInt); // send the value
scanf
定义为int scanf(const char *, ...)
。编译器无法测试变量的类型。因此,你不会收到错误或警告消息。正确的解决方案是int nSomeInt = 0; scanf("%d", &nSomeInt); // send the pointer
- 指针释放错误。常见的错误是内存泄漏。我们使用
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.
- 类型转换错误。我们可以将指针转换为错误的类型并使用它。
示例
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! }
- 奇怪的分配。这不是我的幻想。我遇到过同样的代码!
void SomeFun(int a); .... int main(){ SomeFun(*(new int) ); // Temporary variable with memory allocation. // Deleting memory is impossible. }
这里我只描述了 C 和 C++ 语言中常见的普通错误。当我们使用类、继承、多重继承、模板和其他 OOP 结构时,我们有更多机会在使用指针时犯错。保持乐观!
建议
有一些规则可以防止你犯很多错误。
- 只有在你确实需要时才使用指针。使用指针的常见情况有:创建动态数组,在一个函数中创建对象并在另一个函数中删除它,从库函数接收指针。如果你可以使用自动变量或引用来代替,则不要使用指针。
- 如果在定义指针时没有分配内存,请将其设置为
NULL
。空指针比未初始化的指针更适合调试。示例
int *pSome = NULL;
- 始终测试函数返回的指针是否为
NULL
。示例
if ( ( pFile = fopen("SomeFile","r") ) == NULL){ cerr << " SomeFile Open Error!" << end; }
- 始终使用断言宏测试传入的指针。它仅在调试模式下有效,在发布模式下将被忽略。
示例
void SomeFun(SomeType *pSomePointer){ ASSERT(pSomePointer); . . . }
- 切勿使用 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; }
如你所见,此示例中没有指针、
new
和delete
操作。相同的 MFC 类称为CString
和CArray
。 - 如果你使用标准字符串或容器类并需要其数据的指针。你可以轻松获取它。所有类都有相应的方法或操作符。
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 函数或其他函数。
- 当你需要将一些大对象发送给函数时,请使用引用而不是指针。至少你无法更改引用的内存地址。
- 使用新的类型转换操作符
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!"
- 在可能的情况下使用常量修饰符
示例
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
- 请记住,每个 "
new SomeType
" 操作符都需要 "delete PointerSomeType
" 操作符,每个 "new SomeType[]
" 操作符都需要 "delete[] PointerSomeType
" 操作符。 - 请记住,如果你的程序运行正确,这并不意味着它内部没有指针错误。错误可能在另一台计算机上的另一个时间出现。务必小心!
公告
本文只描述了使用指针的简单问题。我计划在下一篇文章中继续这个主题,该文章将命名为“指针和类”。