星号的秘密
1、乘法运算符
2、定义指针
int *p = 0; 还是 int* p = 0;?
后一种比较容易这样理解:定义了一个变量p,它是指针型的(更详细一点,是指向int的指针型),相比而言,前面一种定义似乎是定义了*P这个奇怪的东西。但是后面一种写法会带来一个容易产生的误解:
int* p1, p2;
这儿给人的感觉似乎是定义了两个指针型变量p1和p2,但是,事实上,这种直觉是错误的,正确的理解方式是int *p1, p2;即p1是指针型的,而p2确是整型的。
在MS VC++ 6.0中,是按照后面一种格式写的。
3、何谓指针?
指针仅仅表示一个内存中的某个地址?
非也,注意到,我们在定义指针的时候,都关联了一个类型,如int,char,或者是string等等,如果说指针仅仅表示一个内存中的地址,那何必要关联这么多变化的东西呢?完全可以DWORD p=0;这样解决问题。
关联了的数据类型是作何用的呢?
它可以指示编译器怎样解释特定地址上内存的内容,以及该内存区域应该跨越多少内存单元。如 int *p;
编译器可以从这个定义中获得信息:1、p指向的内存存放的是整型数据,2、由于该内存区域只存放了一个数据,跨越的内存区域为4个字节,即p+1的效果是跳过了四个字节。
另一个复杂一点的例子,如
struct a
{int x1;
short x2;
a *next;
}
定义指针 a *p;那么编译器对这个指针又作何解释呢?
1、p指向的内存区域依次存放了三种类型的数据,分别是int,short和一个指针型数据。
2、p指向的内存区域跨越了12个字节,即p+1的效果是跳过了12个字节。(为何不是10?对齐的原因)
但是,C++中定义了一种特殊的指针,它去处了一般指针中对内存区域内容以及大小的解释,以满足特定定的需要,如我们只需要某块内存的首地址,不需要考虑其中的数据类型以及大小。这种形式为 void *; 这种类型的指针可以被任意数据类型的指针赋值,如上面的a* 型,void *q = p; 唯一例外的是,不能把函数指针赋给它。
4、关于const修饰符
当const遇到指针,麻烦事就来了,看:const int* p; int* const p; const int* const p;
这三个表达式,第一个表示p是一个指针,p本身平凡无比,但是p所指向的对象是一个特殊的对象--整型常量;第二个表示:这个p指针不是一个普通的指针,它是个常量指针,即只能对其初始化,而不能赋值,另外,这个指针所指向的对象是一平凡的int型变量;第三个则结合了前两者:指针和指向的对象都非同寻常,都是常量。
有了const,赋值的问题就变得麻烦起来,
首先,对于 const int* p;这儿由于p指向的对象是个常量,所以在通过p来引用这个对象的时候不可对其进行赋值!对于一个常量对象,不可以用普通的指针指向,而必须用这种指向常量的指针,原因很简单,通过普通指针可以改变指向的那个值,但是对于一个非常量对象,即普通变量,可不可以将其地址赋给指向常量的指针呢?是可以的,但是一旦这样指向之后,由于这个指针本身定义的是指向常量的指针,因而编译器统一认为其是指向变量的,因而此时不可以通过该指针修改所指向的对象的值。
第二,对于 int* const p;这儿p本身是个常量指针,所以根本就不能赋值,所以不存在赋值的问题。不可以用常量对其进行初始化,因为这个指针不是指向常量的;只能用变量对其初始化。
第三,对于 const int* const p;这儿,只能初始化,不能赋值。可以利用常量进行初始化;也可以利用变量对其初始化,不过不可以利用该指针对该变量进行赋值。
const int* p这种指向常量对象的指针常用来用作某些函数的形参,用意是从编译器的角度防止用户在函数中将传递进去的参数修改,虽然用户本身也可以避免,但是这样更可靠一点--当用户不小心作出修改实参的行为时,编译器发现并阻止这种行为。
this指针是const xx* const型的。
5、函数与指针
指向函数的指针:可以利用它代替函数名来调用函数。
如何定义函数指针,由于一个程序中可以用多个函数名相同的情形(即函数的重载),因而,定义函数指针的时候,必须包含函数的参数,这样才能准确地将指针指向某函数。
定义:int (*p)(const char*, int); 表示p是一个指向函数的指针,该函数的两个参数为const char* 和int,另外该函数返回int型值。
容易混淆的是:int *p(const char *, int); 缺少了一个括号,此时编译器的解释是 int* p(const char*, int);即其含义是一个函数的声明,函数名为p,返回一个指向int型的指针。那么 int* (*p)(const char*, int);则是定义了一个函数指针p,它指向一个函数,该函数的两个参数为const char*和int,该函数返回一个指向int型的指针
函数指针的初始化与初始化:
函数名如同数组名,编译器将其解释为指向该类型函数的指针,故而,可以领用函数名,或者&函数名对函数指针进行初始化或者赋值,另外,可以用另一个函数指针对该指针进行初始化以及赋值。重要的一点是指针与函数名,指针与指针必须具有完全相同的参数表和返回类型(必须完全完全一样,任何一点不同都不可以)。不存在隐式的类型转换,用户必须保证完全的一致性。
初始化或者赋值为0,表示不指向任何函数。
利用函数指针调用函数是可以p(x,y)这样调用,也可以(*p)(x,y)这样调用,前提是p已经正确的赋值或者初始化。
函数返回指针:可以返回一个非基本类型的对象。
6、数组与指针
int a[3] = {1,2,3};
考虑 a,a[0], &a, 以及 &a[0]这三个表达式的含义:
首先这三个表达式的数值结果是一样的--数组的首地址(即数组中第0个元素的地址),但是编译器对三者的解释不同:
对于a,编译器将其解释为一个指针,指向的是一个整型数据,因而利用a+1即指向数组中的第一的元素,a+2指向第二个元素。
对于a这个指针有些特殊的性质:
a不是一个普通的指针,它同时是一个数组名,即关联了一个数组,因而某些性质上与普通的指针不同。
普通的指针可以被赋值,即可以用一个地址或者另一个指针修改当前指针的指向,然而对于a这种关联了一个数组的指针,如果允许这样赋值的话,那么数组中的元素将无法被访问,所以不允许对数组名代表的指针进行赋值。在这一点上a相当于指针常量,即只能被初始化,不可以进行赋值。
虽然a不可以被赋值,但是将a赋给其他的元素是完全可以的,这一点同普通的指针没有不同。
综上,a相当于一个指针常量。(type* const型的)
本质上a[i]操作被编译器解释为*(a+i)操作,即[]运算符是通过数组名指针实现的,因而&a[0]的含义即&(*a),显然对一个指针先*(解引用),再&(引用),等价于什么都没做,还是这个指针本身,因而a完全等价于&a[o],--(&a[0])[i]等价于a[i],形式有点诡异,呵呵。而对于&a这个表达式,奇怪的是这个也是数组的首地址,那么就是说,这个数组的首地址中存放了一个指针常量(即数组名),但是数组的首地址中不是存放的一个int型的数字吗?这是怎么回事呢?难道一个地址能存放两个东西?
暂时无法解释,可以这样认为编译器发现这种&和数组名的结合运算时,即返回数组首地址,只不过,这是,这仅仅是个纯粹的地址,它不再具有指针的特性,即编译器不再将其解释为指针,用户不可以通过+1运算来访问下一个数组元素。它的+1就是数学上的+1。
当数组变为多维,问题变成怎么样了呢?
考虑二维数组 int b[4][3] = {{1,2,3}, {4,5,6}, {7,8,9}, {10,11,12}}; b,&b, b[0], &b[0], &b[0][0]几个表达式的含义:
首先,c++中数组元素的存放是以行序为主序,即第一段存放第一行的数据,第二段存放第二行的数据,....,如此。
首先考虑数组名b,编译器同样将数组名b解释为一个指针,但是显然这个指针不是普通的指针。这个b指向数组所有元素的首地址,这一点是勿庸置疑的,那么这个指针一步跨越的内存是多大呢?在本例中,b一步跨越12个字节,即b一步跨越数组中的一行元素,实际上b是一个指针的指针,或者说指向指针的指针,即b所指向的内容是一个指针,(同样对于b+1,b+2),b[i][j]这种访问方式本质上即:先通过+i,将指针跳跃到第i行,从而获得了指向第i行首地址的指针b[i],然后通过这个指针,再通过+j,跳跃j步,到达了第j个元素,即找到第i行,第j列的元素。
所以b是指针的指针,b[i]是指针,这儿,b[i]类似于一维中的a。
那么&b呢?&b仍然是数组的首地址,但是跟一维类似的是,这是个纯粹的地址,不再具有指针的特性,它的+1就是数学上的+1,不可以利用+1来访问下一个元素。同样的道理对于&b[i],&运算符加上之后,本来是作为指针的b[i]被剥夺的指针的资格,返回一个纯粹的地址。
实际上,由于[]本质上是对指针的解引用,那么我们访问数组元素时可以不拘于a[i][j]这种方式,可以这样:(*(a+i))[j], 或者*(a[i]+j),或者 *(*(a+i)+j),这几种写法是等价的。
对于&b[i][j]呢?我们把b[i][j]换一种写法,写成*(*(b+i)+j),这样问题就容易看清楚了,原来的*b[i][j]就等价于&(*(*(b+i)+j)),我们可以把最外层的括号脱掉,就成了*(b+i)+j,即b[i]+j,显然这是一个指针,指向第i行,第j列元素的指针,对该指针的解释是一次跨越一个int型的数据。
让我们再变态一点,考虑三维的情形,虽然三维的数组不多见,还是考虑一下吧,毕竟空间的坐标是用三维表示的。
int c[2][3][4] = {{{1,2,3,4},{5,6,7,8},{9,10,11,12}}, {{13,14,15,16},{17,18,19,20},{21,22,23,24}}};
首先,数组名c,编译器将c解释为一个指针,指向数组的首地址,由于行序是主序,所以,该指针一步跨越12个整型数,共48个字节,实际上即跨越了一个二维数组。
对于&c,跟一维二维的情形类似,是一个纯粹的地址.
c[i]呢?可以推测,c[i]与二维中的b类似,即指向指针的指针,c[i]一步跨越4个整数,16个字节。c[i]是指向指针的指针,那么c便是指向指向指针的指针的指针(晕~)。
c[i]亦等价于*(c+i)
至于c[i][j],这才是真正的int型的指针,即指向真实数据的指针,一步跨越一个int型,4个字节。跟二维类似,对于&c[i][j],编译器返回一个地址,虽然跟c[i][j]的值一样,但是只是一个纯粹的地址,跨越单元为一个字节。
对于c[i][j][k],不需要废话,对于&c[i][j][k],这是一个地址吗?这是一个指针吗?我们还是要借助[]的另一种表示方法:c[i][j][k]等价于*(*(*(c+i)+j)+k),那么&c[i][j][k]就等价于*(*(c+i)+j)+k,即c[i][j]+k,即指向第(i,j,k)个元素的指针,一步跨越单元为一个int型。
让我们来看一看,寻找第(i,j,k)个元素有哪些写法:
1、c[i][j][k]
2、*(c[i][j]+k)
3、*(*(c[i]+j)+k)
4、*(*(*(c+i)+j)+k)
5、(*(c+i))[j][k]
6、(*(*(c+i)+j))[k]
7、*((*(c+i))[j]+k)
8、(*(c[i]+j))[k]
可见,共八种写法,实际上就是一共有三个解引用,选择用[]还是用*,这样,总共有8个组合。(那么二维的就是4种,一维的2种)
7、typedef与指针
typedef似乎很简单,如typedef int integer;然而,这些简单的typedef语句容易让人产生一种误解,typedef就是一种宏替换,把后面的自定义类型替换成前面的已知类型,事实是这样的吗?显然不是!
考虑这样的问题:如何定义一个指向整型的指针类型?如何定义一个函数指针类型?
第一个问题很简单:typedef int* int_pointer;即可,对于第二个问题,似乎就没有那么简单了,首先,看函数指针的定义方法:int (*p)(const&, int); 这个p指向的函数必须返回int,形参必须是const&和int。现在要将这种指针类型命名为func_pointer,其定义的方法如下:
typedef int (*func_pointer)(const&, int);
可以这样来理解:typedef int integer;将typedef去掉,那就是个变量的定义,这儿即定义了一个int型的变量integer,考虑这个integer是什么类型的,那么这个typedef语句就是将integer定义为这个类型的。将typedef int (*func_pointer)(const&, int);中的typedef去掉,就成了一个函数指针定义,即func_pointer被定义为函数指针类型变量,那么原来的typedef即将func_pointer定义为函数指针类型。
8、函数,数组与指针
int (*testCases[10])();
这个表达式是什么意思?指针,数组,函数糅合在了一起问题变得复杂起来。它定义了数组,testCases[10],数组中的元素是函数指针,函数指针的类型是 int (*)();
怎么来理解这种定义呢?首先考虑数组的定义,数组的定义一般模式是:
类型 数组名[大小];
考虑这个表达式,似乎是定义了一个数组,但是数组名[大小]被夹在了中间,那么类型是什么呢,发现类型并不是简单的数据类型,而是一个函数指针类型int (*p)(),这个函数没有参数,返回int型。从而这个表达式的含义是:定义了一个函数指针型的数组,大小是10。
可以利用typedef来简化这种定义:
typedef int (*PFV)();
PFV testCases[10];
其实int (*testCases[10])();这儿我们定义了一个函数指针数组,数组是主体。
下面考虑这样的问题:如何定义一个指向数组的指针?
指向数组的指针,好像比较新鲜,所谓指向数组的指针,即指针的一步跨越是一个数组,跟指向整型的指针一步跨越一个整型一个道理。事实上前面已经碰到了指向数组的指针,如二维数组名,实际上就是一个指向数组的指针,它一次跨越一行的数据,实际上即是跨越了一个一维数组,而三维数组名呢,也是一个指向数组的指针,它一次跨越的是低维组成的一个二维数组。
数组指针(即指向数组的指针)的定义:
int (*ptr)[3]; 这个表达式定义了一个数组指针ptr,ptr一次跨越一个由3个int型组成的一维数组。发现其定义的方式与函数指针定义的方式很相似,只是把()换作了[]。
更进一步,如果要定义一个指向数组的指针,而数组中的元素不是简单的int型,而是比较复杂的类型,那该如何定义呢?事实上数组指针这种东西就已经够稀有的了,一般编程绝对不会用到,我们只需要能读懂一些比较复杂的东西就行了,自己没有必要构造这么复杂的类型。
比较复杂的表达式:
1、int (*(*(*p())[])())[];
首先,根据p()判断p是一个函数,再根据p()前面的*号判断该函数返回一个指针,下面就看这个指针指向的是什么类新了,我们可以把*p()替换成一个*pointer,这个pointer就是函数p返回的指针,那么就成了int (*(*(*pointer)[])())[];再根据(*pointer)[],这说明了指针pointer是指向的一个数组,那么这个数组中的元素是什么类型呢?由于数组名实际上就是个指针,我们把(*pointer)[](即(*p())[])替换成一个array,这样就成了 int (*(*array)())[];发现array是一个函数指针,从而数组中的每个元素是函数指针,而这个函数呢,又返回一个指针类型,把(*array)()用func代替,就成了int (*func)[];这说明了func函数返回的是指向数组的指针,数组中的元素是int型。
这个表达式够酷!!!
2、p = (int( * (*)[20])[10])q;
这是一个强制类型转换,q被强制类型转换成一个这样的指针类型,这个指针呢直线一个具有20个元素的数组,这个数组中的元素也是指针,是指向另外一种数组,这种数组是含有10个int型数据的一维数组。
可见,分析复杂的表达式时(所谓复杂,即糅合了指针,数组,函数三样,缺少了一样就不会复杂了),从括号的最里层做起,最里层的东西是复杂表达式的“根节点”,然后一层一层脱,脱的时候,是这样的,比如里层是个数组,那么就是说这个数组的元素是什么呢,那就是外层的东西,如果里层是个有返回值的函数,那么就是说这个函数返回什么值呢?那就是外层的东西,就这样一层一层地把表达式解析清楚。
关于typedef还有一些要说的:
typedef int (*PFV)(); 这是定义了一个函数指针,那么PFV p;就可以定义了一个指向函数的指针。
typedef int (*p[10])(); 这是把p定义为函数指针数组,那么 p array;语句就可以定义了一个函数指针数组,数组名即为array,array这个数组含10个元素。
typedef int (*parray)[3];这是定义了一个指向整型数组的指针,那么 parray ptr;就定义了一个指向数组的指针。如何对这个ptr赋值或者初始化呢?事实上,是通过二维数组名来对其进行赋值(初始化)的,因为二维数组名作为指针来讲,就是一个指向数组的指针,一次跨越一个数组。
typedef int a[3][3]; 这个语句什么意思呢?这是把a定义为一个3*3的整型数组类型。当a b = {1}时就完成了一个3×3的整型数组的定义初始化的工作。
同样,简单一点 typedef int a[3];这个语句是把a定义为一个一维数组类型。
typedef void func(int); 这个语句定义了一个函数类型。通过这个typedef,我们可以比较清晰地定义出函数指针,func* p;即可。
typedef char* string;
const string str;
这个str是什么类型的呢?const char * str,即指向常量的指针类型?事实上,答案有些不可思议,str是一个常量指针,而不是指针常量,即const修饰符针对的是指针,而不是char。
9、引用与指针
引用类似与指针常量,只可初始化,不可赋值。别名(alias)是引用(reference)的另一种叫法。通过引用可以间接地操操纵对象。
常量引用,即类似与指向常量的常量指针,对常量引用的初始化,有一点特殊,可以用常量,变量,甚至是常数对其进行初始化。
对于用变量初始化常量引用,那么不能通过这个引用修改这个变量,但是本来的变量名可以。这一点,类似变量地址赋给常量指针。通过常量指针不可以修改变量,但是变量自身的变量名可以。
可以有指针的引用,如 int a = 1; int* p = &a; int* &r = p; 那么r就成了指针p的引用。如果是const int* p = &a;说明是常量指针,那么定义引用的时候,就要如此定义,const int* &r = p;这个语句说明r是一个指针的引用,这个指针是个指向常量的指针变量,而并不意味着这个引用是个常量引用。那如果说这个指针不仅仅是常量指针,而且是个指针常量,即const int* const p;那么定义引用时要注意应该const int* const &r = p;这样表明该引用是个常量引用。这里有一个问题当用一个变量的地址初始化引用时如,int a = 22; int* const &pi_ref = &a;需要注意应该定以为常量引用,因为&a不是变量名,而是类似常数。而若const int a = 22;则应该const int* const &pi_ref = &a;即中间的const是用来定义常量引用的,而前面的const反映的是引用指向的对象(指针)是指向的的const对象。
我们有对象名,或者对象的指针,这些都可以操纵对象,为何要引入引用的概念呢?
事实上,引用最常用的是用作函数的形参。要在函数中操纵一个外部对象的时候,利用引用是一个好办法。
关于引用
首先,引用只可初始化,不可被赋值,因为,被初始化后的引用,就成了被引用的对象的别名,再行赋值,就不是对引用本身的赋值了,而是对所引用的对象的赋值了。