[C++对象模型][2]指针与引用
一 概括
指针和引用,在C++的软件开发中非常常见,如果能恰当的使用它们能够极大的提高整个软件的效率,但是很多的C++学习者对它们的各种使用情况并不是都了解,这就导致了实际的软件开发中经常会内存泄漏,异常抛出,程序崩溃等问题。对于C和C++的初学者,那更是被它们搞的迷迷糊糊。本篇作为[深入C++]系列的第一节,我们就带领大家把指针和引用这个基本功练好。
二 指针
指针,指针的定义是什么呢?好像要想给个直接的定义还是很难的哦,所以我们这里用它的语法结合图来认识它。
int i = 10;int *p = NULL;p = &i;int j = *p; int **pP = NULL; pP = &p;
在上面的几条语句中,&用来定义引用变量或对变量取其地址,*用来定义指针或得到指针所指向的变量,其中p为定义的指针变量,它指向int变量i,而pP为二级指针变量,它指向指针变量p。相应的示意图如下:
C++是对C的扩展,我们首先来看指针在C中的使用,下面的经典实例来自林锐的《高质量编程》,记住函数的默认参数传递方式为按值传递,即实参被传入函数内时进行了拷贝,函数内其实是对拷贝对象的操作,还有当函数使用return返回变量时,其实返回的是原对象的一个拷贝,此时的实参和原对象有可能是一般变量也有可能是指针变量。
#pragma once
#include <cstring>
#include <cstdio>
#include <cstdlib>
// -----------------------------------------------
void GetMemory1(char *p, int num)
{
p = (char*)malloc(num);
}
void Test1(void)
{
char *str = NULL;
GetMemory1(str, 100);
strcpy(str, "hello world");
printf(str);
}
// -----------------------------------------------
void GetMemory2(char **p, int num)
{
*p = (char*)malloc(num);
}
void Test2(void)
{
char * str = NULL;
GetMemory2(&str, 100);
strcpy(str, "hello world");
printf(str);
free(str);
}
// -----------------------------------------------
char* GetMemory3(void)
{
char p[] ="hello world";
return p;
}
void Test3(void)
{
char* str = NULL;
str = GetMemory3();
printf(str);
}
// -----------------------------------------------
char* GetMemory4(void)
{
char *p = "hello world";
return p;
}
void Test4()
{
char* str = NULL;
str = GetMemory4();
printf(str);
}
// -----------------------------------------------
char* GetMemory5(void)
{
char *p = (char*)malloc(100);
strcpy(p,"hello world");
return p;
}
void Test5()
{
char* str = NULL;
str = GetMemory5();
printf(str);
free(str);
}
// -----------------------------------------------
void Test6( void )
{
char * str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world" );
printf(str);
}
}
// -----------------------------------------------
void TestPointerAndReference()
{
// -----------------------------------------------
// 请问运行Test1函数会有什么样的结果?
//
// 答:程序崩溃。同时有内存泄漏。
//
// 因为在GetMemory1函数的调用过程中,其实是对实参指针p做了拷贝,拷贝为局部变量,
// 在函数内的操作是对局部变量的操作,局部变量与实参是两个不同的变量,相互不影响。
//
// 所以,当GetMemory1调用结束时,Test1函数中的 str一直都是 NULL。
// strcpy(str, "hello world");将使程序崩溃。
//
//Test1();
// -----------------------------------------------
// 请问运行Test2函数会有什么样的结果?
//
// 答:(1)能够输出hello world; (2)但是调用结束要对内存释放,否则内存泄漏;
//
Test2();
// -----------------------------------------------
// 请问运行Test3函数会有什么样的结果?
//
// 答:可能是乱码。
//
// 因为GetMemory3返回的是指向“栈内存”的指针,
// 该指针的地址不是 NULL,但其原现的内容已经被清除,新内容不可知。
//
Test3();
// -----------------------------------------------
// 请问运行Test4函数会有什么样的结果?
//
// 答:(1)能够输出hello world; (2) 此时的str指向了常量区,不需要程序员释放,程序结束自动释放。
//
Test4();
// -----------------------------------------------
// 请问运行Test5函数会有什么样的结果?
//
// 答:(1)能够输出hello world; (2)但是调用结束要对内存释放,否则内存泄漏;
//
Test5();
// -----------------------------------------------
// 请问运行Test6函数会有什么样的结果?
//
// 答:篡改动态内存区的内容,后果难以预料,非常危险。
//
// 因为free(str);之后,str成为野指针,
// if(str != NULL)语句不起作用。
//
Test6();
}
三 C++指针与引用
引用,其实是变量的别名,与变量是同一个东东。例如 int i = 10; int &a = i; int &b = i; 这样 a,b为i的引用,即a,b为i的别名,还有 int * pi = new int(10); int *& pa = pi; int *& pb = pi; 此时pa,pb为pi的别名。在C++中引入了引用概念后,我们不仅可以定义引用变量,相应的函数的传递方式也增加了按引用传递,当参数以引用方式传递时,函数调用时不对实参进行拷贝,传入函数内的变量与实参是同一个变量。下面的实例演示了指针和引用在C++的使用。
#pragma once
#include <iostream>
class Point
{
public:
Point(int x, int y)
{
_x = x;
_y = y;
}
void SetX(int x)
{
_x = x;
}
void SetY(int y)
{
_y = y;
}
void PrintPoint()
{
std::cout << "The Point is : "<< '(' << _x << ',' << _y << ')' << std::endl;
}
void PrintPointAdress()
{
std::cout << "The Point's adress is : " << this << std::endl;
}
private:
int _x;
int _y;
};
// 默认按值传递,当传入时对对像进行了拷贝,函数内只是对所拷贝值的修改,所以实参没被修改。
void ChangeValue(Point pt, int x, int y)
{
pt.SetX(x);
pt.SetY(y);
}
// 按引用传递,函数内外同一值,所以修改了实参。
void ChangeValueByReference(Point& pt, int x, int y)
{
pt.SetX(x);
pt.SetY(y);
}
// 通过传递指针,虽然实参指针传入时也产生了拷贝,但是在函数内通过指针任然修改了指针所指的值。
void ChangeValueByPointer(Point *pt, int x, int y)
{
pt->SetX(x);
pt->SetY(y);
}
void TestChangeValue()
{
Point pt(10,10);
pt.PrintPoint();
ChangeValue(pt,100,100);
pt.PrintPoint();
ChangeValueByReference(pt,200,200);
pt.PrintPoint();
ChangeValueByPointer(&pt,300,300);
pt.PrintPoint();
}
// 按引用传递,所以指针可以被返回。
void ChangePointerByReference(Point *& pPt, int x, int y)
{
pPt = new Point(x, y);
}
// 对二级指针拷贝,但是二级指针指向的一级指针被返回。
void ChangePointerByTwoLevelPointer(Point **pPt, int x, int y)
{
*pPt = new Point(x, y);
}
void TestChangePointer()
{
Point *pPt = NULL;
ChangePointerByReference(pPt, 1000,1000);
pPt->PrintPoint();
pPt->PrintPointAdress();
delete pPt;
pPt = NULL;
int *p = new int(10);
//int *p2 = new int(10);
//int *p3 = new int(10);
ChangePointerByTwoLevelPointer(&pPt, 2000,2000);
pPt->PrintPoint();
pPt->PrintPointAdress();
delete pPt;
pPt = NULL;
}
void TestPointerAndReference2()
{
TestChangeValue();
TestChangePointer();
}
运行结果如下:
四 函数参数传递方式,函数中return语句和拷贝构造函数的关系
通过上面的2个实例,如果还有人对函数的参数传递方式和return有疑问的啊,可以对下面的代码亲自debug。
#pragma once
#include <iostream>
class CopyAndAssign
{
public:
CopyAndAssign(int i)
{
x = i;
}
CopyAndAssign(const CopyAndAssign& ca)
{
std::cout << "拷贝构造!" << std::endl;
x = ca.x;
}
CopyAndAssign& operator=(const CopyAndAssign& ca)
{
std::cout << "赋值操作符" << std::endl;
x = ca.x;
return *this;
}
private:
int x;
};
CopyAndAssign ReturnCopyAndAssign()
{
CopyAndAssign temp(20); // 构造
return temp;
}
void CopyAndAssignAsParameter(CopyAndAssign ca)
{
}
CopyAndAssign& ReturnCopyAndAssignByReference()
{
CopyAndAssign temp(20); // 构造
return temp;
}
void CopyAndAssignAsParameterByReference(CopyAndAssign& ca)
{
}
void TestCopyAndAssign()
{
CopyAndAssign c1(10); // 构造
CopyAndAssignAsParameter(c1); // 拷贝构造
ReturnCopyAndAssign(); // 拷贝构造
CopyAndAssignAsParameterByReference(c1);
ReturnCopyAndAssignByReference();
}
亲自debug,效果会更好,运行结果如下:
五 总结
1) 指针也是变量,它存储其他变量的地址。例如int *p = new int(10); p是指针变量,p实际是存储了一个int变量的地址。
2)引用其实是一个别名,跟原对象是同一个东东。例如 std::string str = "hello"; std::string & strR = str;此时strR跟str其实是同一个东东,strR可以看成是str的一个小名。
3)函数默认的传参方式为按值传递,即当实参传入是其实是做了拷贝,函数内其实是对所拷贝对象的操作。例如 void Increase(int x) { x++; } 调用时 int i = 10; Increase(i); Increase函数内部其实是对i的一个拷贝(我们假设为ii)进行++。所以在函数调用结束后原来的i的值仍然保持不变。
4)函数的传参方式可以显示的指定按引用来传递,按引用传递时,函数内即对实参的操作,没有拷贝操作,所以函数内对实参的修改,当然后调用结束后反映到实参上。例如void Increase(int & x) { x++;} 调用 int i = 10; Increase(i);此时Increase内部的++即是对i的操作,所以函数调用结束后i的值被修改。
5)函数中如果有return返回变量时,其实所返回的也是一个拷贝。所以当使用return返回对象时一定要考虑所返回对象的拷贝构造函数是否能够满足要求。
六 使用注意
1) malloc/free一起使用。
2)new/delete一起使用。
3)对于new中有[]时,相应的必须使用delete[]来释放。
4)用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
5)对指针的使用前,应该检查是否为空,空指针可能导致程序崩溃。
6)非内置类型的参数传递,使用const引用代替一般变量。
七 谢谢!