三 指针

 

指针

指针代表什么?

我接触过的很多对C++心存敬畏之心的人(他们大多不懂C++)都跟我说“指针是非常容易出错的,但是一旦学会了指针也就学会了C++”。虽然我不同意这个观点,但或许这是从某个角度提出了指针在C++语言中的重要地位。

指针是什么?指针是一个占用四字节内存的特殊整型变量,它里面保存的是另一块内存的起始地址,并通过指针的静态类型标志这块内存是什么类型。看看下面的代码:

int main(void)

{

int * p=new int(5);

}

短短几句话,发生了多少动作呢?简单分析一下:

首先进入main函数前,为该函数分配了私有栈,栈的大小通常是固定的并且可以配置。进入函数后执行new int(5)使得在堆上分配4个字节,并且将这四个字节初始化为整形值5,然后栈顶指针向上移动四个字节,将这四个字节作为p变量的空间,这四个字节将保存刚才存放5的堆地址。当函数返回时,栈将被销毁,因此p将不再存在,但是new int(5)获得的堆上的4字节空间将依然存在,但是却没有办法再使用或者回收它,因此发生了最可怕的事情--内存泄漏。

野指针

好,刚才至少我们明白了指针也就是个4字节的变量。现在看下面的代码:

int * p1=NULL;

int * p2;

p1变量内部存放的值为0,这使得它不能指向任何有效地址。

p2 由于只是分配了4字节空间,并没有初始化,因此它里面的值应该是上次对该块内存使用后遗留下来的,是多少谁都不知道,或许是0,或许指向某处你绝对不想让它指向的地方,这称为野指针。

所以下面的代码就很危险,*p2=0 ;你都不知道你把什么内存给改写了,结果是无法预料的。野指针相当危险,所以比较好的做法是初始化为NULL。但是C++中很难有什么绝对遵守的准则,C++给了你很大的权限去选择不同的方案。如果在一个你确信不会出现问题的地方,并且性能是很关键的地方,我为什么多此一举要赋初值呢?我本人就有时候故意不这样做。

我对大家的建议是了解原理,然后自己控制,在对自己的控制能力没有信心的时候,遵守较安全的做法是明智的。

还有一种产生野指针的常见情况:

char* p=new char(‘b’);

...

delete p;

...

cout<<*p<<endl;

delete语句已经把p指向的堆上的一字节内存销毁了,但是并不会清空p的值,也就是说p仍然指向堆上的那个字节,然后cout<<p<<endl;会出现什么情况,无法预料。也许堆上的那个字节已经被改写,或者没有。下面的代码会对这种情况有所帮助:

char* p=new char(‘b’);

...

delete p;

p=NULL;

...

if(p!=NULL)//p仍然有效

cout<<*p<<endl;

但其实这是一个逻辑错误,既然delete p都执行了,无论如何,都不应该再使用p。修正逻辑才是治本,if(p!=NULL)只是打补丁的做法。

指针的类型

指针的静态类型

int* p=new int(5);这句话里我们的p变量的静态类型是int*,这就是告诉编译器p所指向的内存应该看作int变量,起始地址是p里面的值,大小是sizeof(int)

char* p=new char[100];

char* pChar=p;

int* pInt=p;

++pChar;

++pInt;

由于pChar的静态类型为char*,所以每次执行++,都会向后移动一个字节,由于pInt静态类型是int*,所以每次执行++,都会向后移动sizeof(int)个字节(通常为4字节)。

指针的动态类型

指针的动态类型是指在多态的情况下,静态类型为指向基类的指针,实际指向的子对象的类型就是该指针的动态类型。比如class B派生自class A,我们写了下面的代码:

A* p=new B();

这句话说明p的静态类型是A*,但是实际指向的对象类型是B,该指针得动态类型是B*。动态类型在多态运用中起到十分重要的作用,绝大多数设计模式都以此为基础,微软的著名技术COM也是基于此。后面在虚函数部分我们会详细讨论。

智能指针

通常如果我们通过new操作获得了一个指针,我们需要记住在不需要使用的时候使用delete操作。如:

void f()

{

string* p=new string(“hello,world”);

....

delete

p; }

但是,可能会遇到这种情况,在delete p被执行之前的语句里面出错而抛出了一个异常对象,f函数将立刻返回,delete p将不会被执行,这样内存就泄露了。遇到这种情况,我们有几个办法:

1)不要使用new/delete,改在栈内创建对象

void f()

{

string str(“hello,world”);

....

}

这是个好办法,而且速度很快,如果能用,尽量用这种.

2)写一个class,利用栈的机制来管理

class StringManager

{

public:

StringManger(string* pStr):_pStr(pStr)

{

}

~StringManager()

{

delete _pStr;

}

private:

string* _pStr;

};

void f()

{

StringManager manager(new string(“hello,world”));

....

}

无论函数f内部是否抛出异常,只要f函数返回,私有栈必然要销毁,那么栈上分配的StringManager对象的析构函数一定会被调用,所以delete _pStr语句一定会被执行。

这就是目前广为使用的智能指针的基本原理。

目前标准C++2003修正版中常用的智能指针有auto_ptrshared_ptr,我们公司的BFL类库中提供了其他的一些智能指针类。在后面我会逐步介绍,并分析优缺点,智能指针有其优点,但是并不是万能的药方,只有当你充分明白了它们的优缺点,才可以安全的用好它们。

指针用作参数

在前面我们介绍栈的时候,说过一个函数拥有一个私有栈,当函数执行时,会先将参数值拷贝到栈中,比如:

void f(int i,int* p)

{

*p=i;

}

int main(void)

{

int a(5);

int b;

f(a,&b);

return 0;

}

f函数执行时,通常从右到左的顺序拷贝pi到栈中,这样栈中有一个p的副本变量p’i的副本i’,然后通过*p’=i’ i’的值赋给了p’指向的变量b。这就是常说的传址和传值,对于b变量,是传址,对于a变量是传值。

这样使用指针会带来什么好处呢?

首先可以在函数f内部修改外面b变量的值,其次如果b变量不是简单类型,而是复杂如string的对象,只传递4字节的指针性能是非常快的。

我们经常见到类似这样的指针参数void f(int** p),指针的指针,为什么要这么用呢?看下面的示例代码:

void f(int** p)

{

*p=new int(5);

}

int main(void)

{

int* pValue=NULL;

f(&pValue);

....

delete pValue;

}

f函数在堆上分配了一块4字节整数区域,初始化为5,然后让外部的指针变量pValue指向这块堆上的内存。我们来分析一下:

一开始,pValue指针变量被创建,但是内容为0,即什么都不指向,然后将pValue指针变量所在的内存地址作为int** p传递给f函数,f函数将在自己的栈中保存p指针变量的地址副本,写成伪代码应该是:

void f(int** p)

{

int** p’=p;

*p’=new int(5);

}

*p’其实就是pValue,所以等价于外部pValue=new int(5);

然后函数f返回p’被销毁,但是pValue已经指向堆上的有效内存了。

请注意,涉及这样的函数应该写上注释,告诉用户是使用什么函数释放内存delete还是free或者其他,因为有可能用户看不到f内部实现的代码。

微软的COM总是使用这种策略。

指针和引用

引用很有可能就是常指针实现的,但是引用有特殊的约束。

引用不会为空,所以当函数接收一个引用参数时,不需要检测该引用所指定的对象是否为空,指针可以为空,所以当某函数接收指针作为参数时,你经常会看到这样的代码:assert(p!=NULL)

引用必须被初始化,而指针变量没有这个限制。

引用一旦被初始化后,只能代表初始化设定的对象,而指针是可以改变指向的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值