不晕晕不乎乎的 C/C++指针1
Liam Huang23
1这只是一篇整理性的文章。
2山东大学 09级数学学院信息安全专业本科生。
3liamhuang0205@gmail.com
本文之缘起从略,无非是刘老师1学业不精,学习和使用指针时
总结
初级经济法重点总结下载党员个人总结TXt高中句型全总结.doc高中句型全总结.doc理论力学知识点总结pdf
出来的一点东西罢
了。好了,废话不说,直接进入正题。
1 指针的三个要素
所谓变量
C/C++里面的变量,实际上是在程序运行过程中内存空间中的一块固定区域。而变量的
名字就是这块区域的“代号”,在编写计算机程序的时候,我们可以通过这个“代号”对这
块固定区域进行一定的操作,比如赋值。
变量在哪里
我们知道,内存是一块很大的记忆体,计算机向其中存取数据是通过一个内存编址来进
行定位的。在一个 C/C++程序中,你可以使用取址运算符(第 2章)来得到一个变量的内
存编址。在 32位计算机中,内存编址通常是 32位(4Bytes)的,这相当于是一个 int型的
变量的长度。
1 int a = 0;
2 std::cout << &a << std::endl;
什么是指针
C/C++里面的指针是一个特殊的变量,具有一般变量的基本特性,也具有它自己的特别
性质。指针存储的数值被理解成是内存里的地址。同时指针和数组还有这千丝万缕的关系,
具有一些神奇的性质。
要想搞清楚一个指针,必须要搞清楚指针三方面的内容,即指针的类型、指针所指向的
类型、指针的值。2
在声明一个指针的时候,指针变量的名字前面必须要加上一个星号 (*),以表示这是一个
指针。以下声明几个指针,用以之后来判断 ptr作为指针的各种性质(情况)。
1 int *ptr;
2 char *ptr;
3 float **ptr;
4 double (*ptr)[5];
5 bool *(*ptr)[5];
1.1 指针的类型
所谓指针的类型,就是把指针声明语句中指针名字去掉的部分。之前有声明几个例子,
让刘老师带大家来看看各例中指针的类型。
1 int *ptr; //指针的类型为int *
2 char *ptr; //指针的类型为char *
3 float **ptr; //指针的类型为float **
4 double (*ptr)[5]; //指针的类型为double (*)[5]
5 bool *(*ptr)[5]; //指针的类型为bool *(*)[5]
1吾有多重人格,比如本色的是阿黄,假正经的时候是 Liam,道貌岸然的则是刘老师。
2这是刘老师自己总结的,学界不一定有这个说法(指三个要素),但大家可以放心地按照这种方式思考。
1
1.2 指针所指向的类型
我们知道,在 C/C++中,对于不同类型的变量系统会分配不同大小的存储空间。当通
过指针来访问指针所指向的内存空间时,声明指针时指针指向的类型决定了编译器将如何对
待那片区域中的数据。因此了解指针所指向的类型是必须的。
另外值得一提的是,指针的类型(这是说指针本身)和指针所指向的变量的类型 (这是说
指针指向的那个变量)是两个概念,必须加以区分。而混淆这两个概念往往是导致“迷糊”
的根源。
通过指针变量的声明得到指针所指向的类型很简单,从语法上看,只需要去掉指针声明
语句中指针变量的名字还有其前面紧挨着的那个星号即可。例如
1 int *ptr; //指针所指向的类型为int
2 char *ptr; //指针所指向的类型为char
3 float **ptr; //指针所指向的类型为float *
4 double (*ptr)[5]; //指针所指向的类型为double ()[5]
5 bool *(*ptr)[5]; //指针所指向的类型为bool *()[5]
1.3 指针的值——指针指向的内存空间的编号
指针也是一个变量,它的值就是它指向的变量所占据的内存空间的编号。准确的说,是
它指向的变量所占据第一块内存空间的内存编址。之前有介绍,一个 32位系统中的内存编
址是 32位的二进制数,所以指针的值也是一个 32位的二进制数。
值得注意的是,到目前为止,例子中声明的五个指针都是空指针,他们不指向任何内存
空间。这是因为在声明他们的时候,并没有给他们赋值,系统默认为他们赋值了空指针。
2 取址(&)和解引用(*)运算符
&和 *这两个运算符的名字已经在标题中给出了——&是取址运算符,*是解引用运算
符。
&a的运算结果是一个指针,指针的类型是变量 a的类型加上个 *,指针所指向的类型就
是变量 a的类型,指针所指向的地址就是 a的地址(也就是 &a)。
*ptr就是 ptr所指向的那个变量。换而言之,如果 ptr是指向变量 a的指针,那么 *ptr就
是变量 a本身。*ptr的类型就是变量 a的类型,*ptr所占据的内存空间就是变量 a在声明时
系统分配的内存空间。
1 int a = 12;
2 int b = 0;
3 int *p;
4 int *ptr;
5 p = &a; //从这一句开始
分析
定性数据统计分析pdf销售业绩分析模板建筑结构震害分析销售进度分析表京东商城竞争战略分析
。
6 *p = 24;
7 ptr = &p;
8 *ptr = &b;
9 **ptr = 36;
这是刘老师对上面的例子依次进行的分析:
• &a的结果是一个指针,类型是 int*,指向的类型是 int,指向的地址是 a的地址,这
被赋值给了指针 p。
• *p就是 a,所以此时 a == 24。
2
• &p的结果是一个指针,类型是 int**,指向的类型是 int*,指向的地址是 p的地址,这
被复制给了指针 ptr。
• &b的状况和 &a类似,*ptr就是 p。所以此时 p是指向 b的指针,而不再指向 a。
• **ptr就是 b,所以此时 b == 36, a == 24。
3 const int *p与 int *const p
3.1 const int与 int const
我们在使用 C/C++的时候,经常会遇到需要将变量指定为常量的时候:
1 int a = 0;
2 const int const_b = 0;
3 int const const_c = 0;
在上面的代码中
• a是一个普通的 int型变量。
• const_b也是一个 int型变量,但是被声明为常量。
• const_c和 const_b具有完全类似的性质。
请注意刘老师在对 const_c的解释中,用了完全相同这样的字眼,这说明在 C/C++中,
const int与 int const完全等效(其他类型的变量也有类似性质)。亦即,关键字 const与类型
名的前后关系是不重要的,在任何情况下都是!
3.2 const int *p
下面回到本节的标题,讨论一下 const int *p的含义。
• 首先 p与解引用运算符 *结合,表明 p是一个指针;
• 其次与 int结合,表明 p指向的是一个整型变量;
• 最后与 const结合,表明 p指向的变量是一个常量。
总体说来,p是一个指向常整型的指针。其类型为 const int *,指向的类型为 const int。而
int const *p有完全相同的分析。
3.3 int *const p
先来看一段 C++代码
1 int a = 0; //声明一个整型变量
2 const int const_b = 1; //声明一个整型常量
3 int *const p_a = &a; //声明一个即将要分析的指针
4 int const *p_b = &const_b;//声明一个指向常整型的指针
5
6 a += 10;
7 std::cout << a << std::endl;
8
3
9 (*p_a) += 10;
10 std::cout << (*p_a) << std::endl;
11
12 p_a = &const_b;
13 std::cout << (*p_a) << std::endl;
即使加上合适的头文件,这段代码在编译过程中也不能通过。编译器将会提示在上述代码的
第 12行有错误,说是 p_a是一个常量,它的值不能被改变!于是大家转过来看到 p_a的声明
• p_a先与 const结合,表明 p_a首先是一个常量;
• 接下来与解引用运算符结合,表明 p_a是一个指针常量(常指针);
• 再与 int结合,表明这是一个指向整型变量的常指针。
回顾一下 C/C++中关于常变量的性质,大家就会知道问题所在。一个常变量只能在声
明的时候进行赋值(初始化赋值),之后任何对于该常变量的值的修改都是非法的,所以编
译器报错!同理按照上一小节的分析,进行如下操作编译器也会报错。
1 (*p_b) += 10;
2 std::cout << (*p_b) << std::endl;
3.4 小结
本节中,刘老师介绍的实际上是指针常量和常量指针的区别。
所谓指针常量(例如本节中的 int *const p),又名常指针,首先是一个常量,只能在声明
的时候赋值(确定指针指向的空间),之后不能更改;常量指针(例如本节中的 int const *p)
则是指向常量的指针,该指针可以在之后更改值(即更改指向的变量),但不能更改该指针
指向的变量的值(因为它指向的是一个常量)。
类似地,大家还可以尝试着定义“常量常指针”,形如
1 int const *const ptr;
它的性质留给大家自己摸索!
4 指针的算术运算
4.1 加减一个整数
指针和整数进行加减运算和普通变量的加减运算是不同的,指针加减一个整数是以单元
为单位的。亦即 ptr = ptr + 1意味着 ptr指向了原来指向的位置的后一个单元(假设 ptr是一
个已经定义的指针)。而单元的大小则是由指针指向的类型大小决定的,单元的大小就等于
指针指向类型的大小。例如你可以用下面这串代码输出 int型数组中的全部数据。
1 int (*ptr)[10] = &a; //假定这是一个已经声明过的数组
2 for(int i = 0; i != 10; ++i)
3 {
4 std::cout << (*(ptr + i)) << std::endl;
5 }
4
4.2 两个指针相减
两个指针不能相加,但是同类型的两个指针却可以相减。同类型的两个指针相减得到的
是一个 int数值,表示两个地址的差值。这通常用在数组操作中,这里就不详说了。
5 数组与指针
上一节中用来输出 int数组的方法是不是有点奇怪?没关系,现在就是了解数组与指针之
间的关系的时候了。
5.1 数组名是一个常指针
人格分裂的数组名
刘老师语,一旦按照如下方式声明一个数组
1 int array[n];
那么数组名 array就开始了人格分裂。
• array代表整个数组,其类型是 int ()[n]
• array是一个常指针,其类型是 int *,指向的类型是 int,其值是编号为 0的元素的内存
编址。
不同表达式中的数组名 数组名人格分裂,大家却不能轻易地人格分裂。既然刘老师已
经人格分裂,那么刘老师就化为救苦救难的观世音菩萨来说一说不同表达式中数组名的角
色。下面是几个例子
• 在表达式 sizeof (array)中,数组名代表数组本身,此时 sizeof得到的结果是整个数组的
大小;
• 在表达式 sizeof (*array)中,数组名代表数组的第 0号元素的指针,此时 sizeof得到的是
数组中元素的大小。
• 在表达式 (array + i)中,数组名代表的是第 0号元素的指针,(array + i)则是第 i号元素
的指针。
总的说来,数组名如果单用则代表的是数组本身,其余情况都代表的是一个常指针,指
向编号为 0的元素。
数组名作为一个常指针
刘老师懒,依然使用上节输出 int数组的例子,且看下面这个“正常”的代码
1 for(int i = 0; i != 10; ++i)
2 {
3 std::cout << a[i] << std::endl;
4 }
以及这个“不正常”的代码
5
1 for(int i = 0; i != 10; ++i)
2 {
3 std::cout << (*(a + i)) << std::endl;
4 }
这两串代码的效果完全相同,且都和上一节的代码可以得到相同的输出结果。大家仔细看一
下本小节的第二串代码,找到第三行有 (*(a + i))的样式。看刘老师来分析一下
• a是数组名作为常指针,i是 int型数据,(a + i)首先与解引用运算符结合,表明这是一个
指针,(a + i)是指针与整数的加法;
• (a + i)解引用之后应当是一个与 a指向的变量类型相同的变量。
6 struct以及 class与指针
struct以及 class(C++中)都是用户自行定义的类型,为了方便说明,刘老师假定已经
定义了一个叫做 Liam的结构体以及一个叫做 Huang的类。
1 struct Liam
2 {
3 int a;
4 int b;
5 int c;
6 };
7
8 class Huang
9 {
10 public:
11 void Seta(int val){a = val;}
12 void Setb(int val){b = val;}
13 void Setc(int val){c = val;}
14 int Geta(){return a;}
15 int Getb(){return b;}
16 int Getc(){return c;}
17
18 private;
19 int a;
20 int b;
21 int c;
22 };
6.1 指向 Huang类型的指针
大家看一看刘老师这段代码,回忆一下结构体成员的调用方式。
1 Huang temp;
2
3 temp.a = 0;temp.b = 1; temp.c = 2;
4
5 std::cout << temp.a << ’\t’ << temp.b << ’\t’ << temp.c << std::
endl;
6
这段代码将会输出 temp中的 a, b, c的值。如果定义一个指向 temp的指针,这样的调用方式会
变成
1 Huang temp;
2 Huang *ptr;
3 ptr = &temp;
4
5 (*ptr).a = 0;(*ptr).b = 1; (*ptr).c = 2;
6
7 std::cout << (*ptr).a << ’\t’ << (*ptr).b << ’\t’ << (*ptr).c <<
std::endl;
6.2 指向 Liam类型的指针
Liam是一个类,除了具有和结构体一样的数据成员外,还有自己特有的成员函数。同样
大家来看一看这段代码,回忆一下类的成员函数的调用方式
1 Liam temp;
2
3 temp.Seta(0);temp.Setb(1); temp.Setc(2);
4
5 std::cout << temp.Geta() << ’\t’ << temp.Getb() << ’\t’ << temp.
Getc() << std::endl;
这段代码将会输出 temp中的 a, b, c的值。如果定义一个指向 temp的指针,这样的调用方式会
变成
1 Liam temp;
2 Liam *ptr;
3 ptr = &temp;
4
5 (*ptr).Seta(0);(*ptr).Setb(1); (*ptr).Setc(2);
6
7 std::cout << (*ptr).Geta() << ’\t’ << (*ptr).Getb() << ’\t’ <<
(*ptr).Getc() << std::endl;
6.3 为了方便使用指针而产生的小箭头“->”
C++的实现者们觉得通过指针用上面的方法调用类的成员太麻烦,于是定义了一个与
(*ptr).效果一样的表达方式 ptr ->。具体的效果如下(结构体指针也有同样用法)
1 Liam temp;
2 Liam *ptr;
3 ptr = &temp;
4
5 ptr -> Seta(0);ptr -> Setb(1); ptr -> Setc(2);
6
7 std::cout << ptr -> Geta() << ’\t’ << ptr -> Getb() << ’\t’ <<
ptr -> Getc() << std::endl;
7
6.4 一个注意事项
有的时候,你定义的指针并不是结构体的指针,而是结构体里面成员的指针
1 Huang temp;
2
3 temp.a = 0;temp.b = 1; temp.c = 2;
4
5 int *ptr = (int *) &temp; //注意这里有一个显示的类型转化
这个时候,ptr将会指向 temp的第一个数据成员——也就是 temp.a。如果你想访问 temp接下
来的两个成员,那么这样做是危险的
1 //这样做是危险的,不要这样做!!!
2
3 Huang temp;
4
5 temp.a = 0;temp.b = 1; temp.c = 2;
6
7 int *ptr = (int *) &temp;
8
9 std::cout << (*ptr) << ’\t’ << (*(ptr + 1)) << ’\t’ << (*(ptr +
2)) << std::endl;
原因有两个
• ptr每次加一,都向后移动了 sizeof(int),当结构体中的数据不完全是 int时这样做是错
误的;
• 这么做有一个隐含的假设,那就是结构体在储存时每个数据是连续储存的。而这个假
设实际上是不成立的,系统在储存结构体时,又是会为了对其而在成员之间加一些填
充字符(pad)。这样表达的话很可能会使得指针指向这些填充字符。
7 函数与指针
函数在内存中
不知道看到这里的大家有没有想过,当大家在创建一个函数并在其他地方调用的时候,
是怎样一个过程。事实上,当声明了一个函数,系统就将函数保存在了内存中,和变量一样
占用一定的内存空间。因此,事实上在进行函数调用的时候,系统将目标函数的内存编址
传递给调用函数,然后调用函数再对目标函数进行调用。
提到了内存编址,大家自然就会想到,是否也有指向函数的指针?
答案
八年级地理上册填图题岩土工程勘察试题省略号的作用及举例应急救援安全知识车间5s试题及答案
是肯定的!
7.1 函数的指针
让刘老师带大家先来看一看声明一个函数以及一个指向该函数的指针:
1 int max(int, int, int);
2 int (*ptrmax)(int, int, int);
3 ptrmax = &max;
在了解上面这个声明之前,刘老师认为最好是回顾一下通常是怎样定义一个指向“普通
变量”的指针的:
8
1 int num = 0;
2 int *ptr;
3 ptr = #
细心的同学或许已经发现,声明指向函数的指针与指向变量的指针具有极高的相似性!
甚至连取址符号 &在函数以及变量上的用法都完全一样。
于是,仿照刘老师分析指向变量的指针,对于 ptrmax,大家可以做如下分析:
• ptrmax的类型为“int (*)(int, int, int)”,即 ptrmax是一个指向一个具有三个整型变量并
且返回一个整型变量的函数的指针。
• ptrmax指向的类型为“int ()(int, int, int)”,即 ptrmax指向的(东西的)类型是一个具
有三个整型变量并且返回一个整型变量的函数。
• ptrmax的值——其指向的内存空间的编址是声明函数 max的时候系统分配的内存空间
的首地址
7.2 通过指向函数的指针调用函数
仍然先看一段 C++代码:
1 #include
2
3 using std::cout;
4 using std::endl;
5
6 int main()
7 {
8 int a = 0;
9 int b = 1;
10 int c = 2;
11 int max(int, int, int);//函数声明
12 int (*ptrmax)(int, int, int);//函数指针声明
13 ptrmax = &max;
14
15 cout<<”这是通过函数名调用函数的示例”
16 < b) ? a : b > c) ? ((a > b) ? a : b) : c;
26 }
编译并运行,一切正常!这说明函数指针与函数的关系,确实与变量指针与变量的关系在
很大程度上(差别马上会讲)是一致的。
9
7.3 小小偷一下懒
或许大家在使用的时候偶尔会犯懒,以致于写出下面这样的代码(众:明明是刘老师偷
懒!):
1 #include
2
3 using std::cout;
4 using std::endl;
5
6 int main()
7 {
8 int a = 0;
9 int b = 1;
10 int c = 2;
11 int max(int, int, int);//函数声明
12 int (*ptrmax)(int, int, int);//函数指针声明
13 ptrmax = &max;
14
15 cout<<”这是通过函数名调用函数的示例”
16 < b) ? a : b > c) ? ((a > b) ? a : b) : c;
27 }
编译并运行这段程序,你会发现结果和之前竟然完全一致!之前已经分析过了,ptrmax和
max的关系就是 int *与 int的关系。但这里似乎有 ptrmax == max!这是怎么回事呢?
如果细心的大家再试一试,将上面两例中第十五行的 max(a, b, c)更改为 (*max)(a, b, c)
(即像前例中函数指针的调用方式),你会发祥输出结果和之前完全相同。这说明函数名本
身和(刘老师声明的)函数指针有着相同的调用方式。原来,C语言在
设计
领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计
的时候,为了使
用上的方便:
• max的函数名与 ptrmax函数指针都是一样的,即都是函数指针。
• 为了读写的方便,C语言在实现的时候,将 max(a, b, c)和 (*max)(a, b, c)等效起来了。
• 为了统一函数指针 ptrmax与函数名(同样也作为函数指针)的调用方式,ptrmax也可
以直接像 ptrmax(a, b, c)这样调用函数。
• 赋值时,既可以写作 ptrmax = &max形式,也可以写作 ptrmax = &max的形式。
这也就是说,大家在学习 C/C++的时候对于函数名的任何使用方法都可以套用在函数
指针上——甚至包括作为调用函数的参数!
8 一个变态的总结
啊,这个标题不是说做这个总结的刘老师是变态,而是这个总结本身很变态。
10
什么,你说这个变态的总结是刘老师做的,所以刘老师也就是变态?
啊,你这个疯同学!
刘老师很懒,一直懒……所以总结就用这么一个题目来代替吧。请问分析下面这代码中
的指针 ptr
1 int *(*ptr(int))[3]
跋
首先向一路披荆斩棘来到这里的读者表示祝贺,你们应当对指针已经有了比较明晰的了
解,更详细的东西需要你们在未来的日子里自己摸索,刘老师没法陪伴了(众:谁要你阴魂
不散!);而那些没有看完的人,估计还是对指针晕晕乎乎的,将来他们晕指针的时候一定会
出现我的脸,披头散发扑向他们。
这篇文章本来准备写九节,用来向《九章算术》致敬。但实际上写到第八节已是强弩之
末,没法再往下写了,最后一节恐怕得另择吉日。刘老师在开头就说了,学业不精,如有问
题,还望大家不吝指出。
时候不早了,刘老师披头散发睡觉去了!
11