首页 > 技术随笔 > c++扫盲系列之–指针专题

c++扫盲系列之–指针专题

技术随笔

本文将讨论如下的内容:
1.指针的概念及其本质
2.指针的用法
3.容易混淆的概念
4.善用指针

 

1.指针的本质

在信息工程中指针是一个用来指示一个内存地址的计算机语言的变量或中央处理器(CPU)中寄存器.指针一般出现在比较近机器语言的语言,如汇编语言或C语言。纯面向对象的语言如Java一般避免用指针。指针一般指向一个变量或者函数。在使用一个指针时,一个程序既可以直接使用这个指针所储存的内存地址,又可以使用这个地址里储存的变量或函数的值。

c语言之所以强大,以及其自由性,很大部分体现在其灵活的指针运用上。因此,说指针是c语言的灵魂,一点都不为过。C++作为从C语言发展而来的一门高级语言,他并没有摒弃指针,而是积极的采纳他.同时又注入了面向对象的高级思想,这使得C++成为了全世界迄今为止最为优秀的语言.

C++中的指针一直被初学者视为很神秘的东西.其实指针的概念很简单.我们可以把他看作一种数据类型,定义一个指针变量就像定义一个int,char型一样的变量.只不过我们往int里面存整数,往char型里面存字符,而往指针里面呢,存放的是地址,内存地址.我们只要记住一点,在高级语言里

 

 image

2.指针的用法

既然我们说指针是一个变量类型,那么在继续深入了解指针之前我们先来了解一下什么叫做变量类型.

我们知道,所有的数据最终都是放到内存中才能被使用,也即是说,所有的数据,不管你是如何定义的,最终都将反映为一段内存空间.那么我们声明为某种变量类型之后有什么用呢?其实有两个用途.一个是限定内存占用大小.一般来说编程语言总要为数据设定不同的内存占用空间以满足不同应用的需求.尤其是在内存吃紧的嵌入式等应用,经常可是要用到bit型的,如果都用4个字节那是省事了,但是带来的空间消耗可是很大的.类型定义的第二个用途是约束操作.例如我int型的数据可以相乘,但是char型不可以不同的数据类型应该具有不同的操作.所以我们用类型定义来实现这种约束.

所以我们可以看出,指针无非存放的东西有点不同,其余的与其他数据类型没有什么不一样.指针变量本身占用了4个字节的空间来存放内存地址,即可以寻址到2的32次方即4G的内存地址空间.他有两个基本操作符,一个是*号(取地址中存放的值),一个是&(取变量地址).

我们先来看下面这段简单的代码

int  aint = 3;
int* pint;
pint = &aint;

当定义了一个int型变量之后,C++会该变量分配一块内存空间,然后把3转换成4个字节初始化这片内存空间.在这里,我们要知道,其实变量名就相当于某个寄存器的别名,也就是地址空间的名字.我们使用这个变量名.就像使用地址空间上面存放的值一样.如果我们要得到aint这个变量的地址,我们可以用&aint来获得.程序下面又定义了一个指针变量pint,他是用来存放aint变量的内存地址的.那么我们如何使用指针呢?一般来说我们不用到变量的地址,我们更关心的变量的值,即地址中所存的值.我们用*号操作符来获取aint的值.然后输出.

指针和指向的变量之间的关系如下图所示.

image

image

那么既然指针是一类数据类型,为什么我们不用一种统一的数据类型的名字,例如pointer来定义一个指针变量呢?这是由于指针存放东西的特殊性造成的,指针存放的是内存地址,需要的时候我们可以取出内存地址中的数据来.问题是,我们如何去取这块内存中的数据呢??取两个字节?取四个字节?还有,取出来的数据他有什么操作呢??他可以相乘吗?我们要知道机器是很笨的,他什么都不知道,他只知道我们给他设定的规则.

还记得我们上面说过的数据类型的两大用途吗?用”基本数据类型 *”这样一种方式来定义一个指针变量的意义正在于此.机器将按照我们设置的类型(int*)取出aint地址上的四个字节的数据,然后将数据格式化成int型数据,这样数据的操作就受到约束了.C++中还甚至允许我们定义指针类型为void*型,表明我们暂时只需要指针的首地址,不需要立即对其数据进行格式化.

*函数指针*

指针作为存放地址的变量,那么他自然可以存放任何东西的地址,例如函数.这就是函数指针了.函数指针的声明格式如下:

函数返回值类型(*指针变量名)(参数类型列表) = &函数

或者是

typedef 函数返回值类型 (*函数类型名)(参数类型列表)

函数类型名 函数指针 = &函数;

我们来看一个典型的应用

int getmax(int a,int b)
{
return a > b ? a : b;
}
typedef int (*Func)(int,int);
int main()
{
Func pFuncFormat1 = &getmax;
int (*pFuncFormat2)(int,int) = &getmax;
cout<<(*pFuncFormat1)(3,5);
cout<<(*pFuncFormat2)(3,5);
return 0;
} 

上面这段代码首先定义了一个普通的比较大小的函数,然后在主函数中以两种风格定义了两个同样的函数指针,指向该函数.这样我们就在这个函数指针中存放了GetMax函数的入口地址了,注意这里跟普通的变量有些不一样.对GetMax函数用不用&号都是可以的,在使用时用不用*号也是可以的,效果是一样的.但是习惯上我们会采用上面这种风格来写程序代码比较好懂一点.还有要注意的是,函数指针一定要与函数声明的签名(也就是参数表)一致,还有返回值也要一致.

3.容易混淆的概念

**指针数组和数组指针的区别

这个也是C++里面比较容易混淆的一个定义.

你能看出下面的定义有什么区别吗?

1.int *PointerArray[3];

2.int (*ArrayPointer)[3];

1中定义的是一个指针数组.首先让我们先来了解一下什么叫做数组.数组就是同一类型元素的聚合.例如int array[3];定义了3个int型元素,并将其聚合在array这个数据结构中.数组本质上也是指针.即array变量本身存放的是上面这个数据结构的首地址.当我们用[]索引符号去访问数组对象时,其实就相当于调用指针操作符*取地址上的数据(注意,指针也可以应用[]索引符,或者数组"名"也可以应用*操作符,这也再一次说明了数组和指针具有一定的关系).那数组和指针有什么不同呢?我们说数组是一种常指针(关于常指针和指针常量我们待会再解释).了解到这一点之后我们再来看下指针数组是什么东西就很好理解了.指针数组说白了就是存放指针变量的数组.我们可以把int* 看作一种新类型,和int,char等基本类型等价的一种类型,然后再去看上面这个例子就很清楚了.

字符型的指针数组一般用于字符串操作较多.例如我们可以这样定义

char *PointerArray[3] = {"Hello","World","!"}

三个指针分别指向了三个字符串,长度不相等.但要注意,这里定义之后每个指针就变成了指针常量了.即通过该指针对这些字符串进行修改是非法的.一般的用法是

PointerArray[i] = new char[N]; (N为常数)

然后再进行赋值操作.

再来解释数组指针.数组指针其实就是指向数组的指针,定义中的下标号3表示的是该指针所指向的一维数组的长度(注意,任意维数组都可以看成是由一维数组构成的).我们来看下面的例子

int array[3][5];

int (*ArrayPointer)[5];

ArrayPointer = array;

然后我们就可以像使用array数组一样使用ArrayPointer了.如果记不住的话我们可以将上面的定义分开来看.把他看成(*ArrayPointer)和int [5]两部分,如下图所示:

image image

                    (数组指针)                 (指针数组)

注意,是(*ArrayPointer)才相当于int [5],而不是ArrayPointer,所以我们可以用(*ArrayPointer)[3]来访问array[0][3].或者用(*ArrayPointer+1)[3]来访问array[1][3].当然也可以像二维数组那样直接用两个索引号来访问.

数组指针一般用于动态分配二维数组,如下

char (*CharArray)[5] = new char[N][5]; (N为常数)

注意,二维数组动态分配时必须指定他的一维长度.

**常指针和指针常量的区别

常指针就像我们平时说常整数一样,意思是指针的内容不能再被修改了.而我们知道指针内存放的是变量的地址,也就是说一旦将某变量的地址赋值给了该指针,那么以后就不能将其指向其他的变量了.他的定义如下:

type * const PointerName = &Variable;

对于常量必须在声明时即初始化,除非是类的数据成员,那么可以在构造函数的时候才进行初始化,如下面

class Test
{
const int c;
public:
Test(int x):c(x){}
};

指针常量比较难从字面上理解其意思.我们只好记住他了,他表示的是指针所指向的内容是不能通过指针来修改的(虽然通过一些小伎俩是可以进行"不合法"修改的).

他的定义方式有两种

type const * PointerName = &Variable;
const type * PointerName = &Variable;

下面的例子中试图对指针常量进行修改将编译失败

char Variable = '3';
char const * PointerName;
PointerName = &Variable;
//*PointerName = '4'; //编译不通过

与常指针不同的是指针常量允许声明和赋值分开.

**指针与引用的区别

引用就像是变量的别名一样.他的定义如下:

int Variable;
int &Ref = Variable;

我们可以看到,引用使用指针里的取地址符作为标识,他表示引用与变量使用的是同一块内存的数据.引用和指针有几个不同的地方,一个就是引用必须声明即赋初值,而且不能再引用其他的变量了.而指针是可以随时指向其他变量的(常指针除外).既然引用就像别名一样,那么为什么我们还需要这个东西呢?引用一般是用于传递参数的比较多,这样可以节省内存和拷贝构造函数等的开销.

4.善用指针

指针是操作底层必备的东西,是C++里面最为灵活的东西.但也带来安全性的问题.不正确的使用指针将会导致内存泄露,内存悬挂,野指针(不安全指向)等问题的出现,严重威胁到软件系统的稳定性和安全性.现在有很多人在致力于编写智能指针的库,使用智能指针,我们将可以在很大程度上避免上述的问题.如boost库中就设计了智能指针,具体请参考相关的内容.

本博客遵循CC协议2.5,即署名-非商业性使用-相同方式共享
写作很辛苦,转载请注明作者以及原文链接~
如果你喜欢我的文章,你可以订阅我的博客:-D点击订阅我的文章

  1. X﹏X 到现在还没有评论~
  1. 暂时没有trackbacks.