初步了解
所谓指针,就是指向某个地址的东西。最简单的例子就是指南针,它总是指向地理南方,地磁北方。我们也可以将网络上的url【就是你浏览器地址栏的那一串东东】当作一个指针,因为它是某个资源的地址。
C语言中的指针也是同样的意思,而它指向的地址就是某个变量的内存地址,因此也被叫做“指向某个变量的指针”。
在研究指针之前,我们先来康康内存地址在C语言中长啥样
如何查看变量的地址?
int a = 10;
int b = 11;
// 通过 & 运算符
cout << &a << " " << &b << endl;
// 输出结果 0x61ff0c 0x61ff08【请务必记住它们大概的样子】
如何查看该地址所占空间大小?
int a = 10;
cout << sizeof &a << endl;
// 输出结果: 4
知道了地址的样子,我们就稍微作个简单对比:
范畴 | 地址 | 内容 | 名称 |
---|---|---|---|
网图 | www.baidu.com/某比基尼图…【略】 | 【请自行想象或者动手百度】 | 萝莉比基尼 |
C/C++ | 0x61ff0c | 10 | a |
变量地址的作用
假设你知道了一个宝藏的地址,那么你总会想要知道里面有什么内容,当然啦,该宝藏也应该要有个名字才行,比如“秦始皇陵”
那么问题来了,你要怎样根据这个地址,找到里面的内容呢?
emmm…反正在C语言中,可以通过 指针运算符* 来实现
int a = 10;
// &a: 表示a的地址
// *地址: 表示读取该地址中存储的内容
cout << *&a << endl;
// 输出结果: 10
显然,*&a与 a 实质上是一样的。你肯定想问,既然一样,为啥还要写得这么麻烦?
请先把这个问题吞下,后面再让你消化。
指针变量
这个概念也很好理解,他就是用来存储地址的一个变量。从上面的例子中我们可以看到,地址是一串比较长的东西,因此我们就会想着用另外一个东西来表示。这另外一个东西就是指针变量。而它作为一个变量,那么必然也有自己的内存地址,可以从下图中直观感受一下:
上图中的指针变量存储的内容是变量a的地址,这种情况下,我们就称其为指向变量a的指针变量。
问题1:如何定义指针变量?
方法: int *pointer;
一些概念与操作直接看下图:
经过这一顿操作(pointer = &a)之后,我们就可以说指针变量pointer指向了变量a。
更多时候,我们会在定义指针变量的时候直接初始化,也就是写成 int *pointer = &a
这种形式。
问题2:如何通过pointer去访问它所指向的变量?
方法: *pointer;
文字解释:*pointer表示 pointer所指向的存储单元的内容,这个内容就是变量。
int a = 10;
int *pointer = &a;
// 在这里 *pointer 这个整体就表示变量a, 因此a能做的事情, *pointer 也能做
图示:
这里特别说明一下,变量存储的值是在一片连续的存储单元中(每个存储单元占1Byte),如果它超过1个字节,那就需要多个存储单元了,但我们要知道它在哪儿的话,只需要记住它的第一个存储单元的位置就可以了,因为一个简单变量的存储空间是连续的。
小贴士:
请尽量在初始化指针变量的时候赋值,如果不知道赋什么值,请赐它一个NULL。
被赋值为NULL的指针变量,就是传说中的空指针。相信不少java老兄已经蚌埠住了,一定要hole住啊。
空指针是内存中地址为0的区域,是不允许被访问的。非要去访问的话,就给你报错。
关于pointer++
这里说明一下目前所知的运算符的优先级:从高到低
- 后置+±-
- 前置+±-,!,*,&
- 算数运算符
- 关系运算符
- && 和 ||
- 赋值运算符
问题:(*pointer)++ 与 *pointer++ 是否相同?
根据上述的优先级判断,显然是不同的。但是有另外一个问题:pointer存储的是一个内存地址,对它进行++运算,得到的结果是什么呢?【请先不要看下图,冥想一会儿再看】
譬如上图中,已知pointer当前存储的值是0x0012FF75
你很可能会凭着自己超乎寻常的数学知识算出来 pointer++ 的值是 0x0012FF76.
事实真是如此吗?
不是的,指针在做++运算时,会根据自己的基类型去判断要加多少。这里pointer的基类型是int,因此它的++实质上会+4。而效果就是读到该变量存储空间下面的第一个存储单元,也就是会变成0x0012FF79【现在知道基类型的作用了吧!】
简单来记忆的话,对指针进行 + n 操作,就是跨过n个基类型存储空间的长度。比方说基类型是int,那么对 pointer + n 来说就是在图中跨过n个黄色的区域【每个黄色区域占4byte】。
数组与指针
一些例子
例1
int main() {
int a[5] = {1, 2, 3, 4, 5}
cout << a << endl; // 0x61fefc
cout << *a << endl; // 1
cout << &a[0] << endl; // 0x61fefc
cout << a[0] << endl; // 1
return 0;
}
我们已经知道a表示的是一个地址,根据第二,第三个输出结果,我们就可以推断出,a所表示的地址就是其第一个元素所在的地址。
也就是说,数组名相当于指向数组第一个元素的指针。
对于上面的例子来说就是 a <=> &a[0]
注意: a是一个地址常量,不是变量,无法被赋值。【这与java以及其他语言有区别】
例2
int main() {
int a[5] = {1, 2, 3, 4, 5};
int b[5] = {7, 8, 9};
cout << a << " " << b << endl;
a = b; // 这里是不能做赋值运算的,如果你的IDE够好,它就会提示你这一行有错误
// 提示内容:Array type 'int [5]' is not assignable
cout << a;
return 0;
}
例3
int main() {
int a[5] = {1, 2, 3, 4, 5};
int *p = NULL;
cout << a << endl; // 输出:0x61fef8
p = a; // 由例1可知a就是a[0]的地址,因此可以直接赋值给指针变量P
cout << p << endl; // 输出:0x61fef8
cout << *p << endl; // 输出:1
cout << *p++ << endl; // 输出:1
cout << *p++ << endl; // 输出:2
return 0;
}
如果第四个输出(即第一次*p++的输出)的结果与你想象的不一致,那么这不是你不理解指针,而是你没理解后置++运算。这里稍稍提示,就不展开啦。
顺便给出指针在数组中++的运动图示
小结:
定义int a[10]; int *p
则:p = a <=> p = &a[0]
数组访问方面:
p + i <=> a + i <=> &a[i]
*(p + i) <=> *(a + i) <=> a[i]
表示形式上:
p[i] <=> *(p+i)
小贴士:
- a++是没有意义的,因为a是常量,不能被赋值,但是 p++ 会引起p变化
- p可以指向数组范围以外的地址,但a这么做的话就会越界【比如长度为10的数组,你硬要获取a[10];由于使用p来获取数组中的值,不会产生越界异常,因此在使用的时候也要格外小心。
字符串指针
例4:copy
int main() {
char a[] = "hello kitty", b[15];
char *p1, *p2;
for (p1 = a, p2 = b; *p1 != '\0'; p1++, p2++) {
*p2 = *p1;
}
*p2 = '\0';
cout << "a=>" << a << endl;
cout << "b=>" << b << endl;
return 0;
}
例5:关于字符数组的打印
int main() {
char a[6] = {'h', 'e', 'l', 'l', 'o'};
char *p = a;
cout << "value=>" << a << endl; // value=>hello
cout << "value=>" << p << endl; // value=>hello
cout << "address=>" << static_cast<void *> (a) << endl; // address=>0x61ff06
cout << "address=>" << static_cast<void *> (p) << endl; // address=>0x61ff06
return 0;
}
这个例子说明了cout对字符数组做了特殊处理,如果要打印地址的话可以仿照上面的第三或第四个输出语句。
二维数组的指针
在开始之前,我们先说明一个规范:ISO/IEC9899:2011
在该规范中说了一段话:如果一个数组的名称没有出现在&符号之后,那么这个数组名称的类型将被转化成指针类型。
也就是说int a[10];
只要a不出现在&符号之后,那么a就可以表示成指针类型【注意,它不是指针变量】。
那么问题来了,倘若a出现在&之后,也就是 &a,这又表示什么呢?
答:&a 会返回一个指向a数组整体的指针【该指针依然指向a的第一个元素地址,但指针类型不同】。比如a中的元素是int类型,那么a就可以当作是int类型的指针。而 &a 的基类型则是其他类型,可以写作:int (*p)[10]
,表示指向了一个包含10个int类型元素的数组变量的指针变量,那么这个类型占多大空间呢?整个数组占多少空间,它就占多少空间。
直观的,可以看下图:
虽然 a 和 &a 都指向 a[0] 的地址,但它们基类型所占空间大小不同。
显然,a + 1 => 0x61fef8; 而 &a + 1 => 0x62fff0
思考: 如果要输出 *(&a),那么结果会是什么呢?
回忆一下上文中刚学习指针变量的时候遇到的一个问题:如何通过pointer去访问它所指向的变量?【这里我们将再次把那张图来画一下,加深记忆】
顺便会议一下:变量地址的作用【*地址: 表示读取该地址中存储的内容】
显然 &a 指向的地址就是数组a,因此就可以画出下面这张十分熟悉的图
这里使用红色字体表明与先前例子中的区别,但有一点是不变的,那就是可以通过*pointer
来间接读取它所指向的变量/常量的值。
另外一种解释:根据规范来,对于*E
E为一个指针,那么*E
返回的结果就是E
所指向的内容。
这样就很明确了,*(&a) 所指向的内容就是 &a, 而 &a 就是数组 a,因此 *(&a) <=> a
这个结论,我们在上文早已得知,这里不厌其烦地再露一次脸,体现一下重要性。
有了上面的论述,我们就很容易将一维数组扩展到二维数组了。这里不再文字描述,直接上图:
总结一下:
- 数组名相当于指向数组第一个元素的指针
- &E 相当于提升基类型
- *E 相当于降低基类型
可以观看这个视频来测试一下自己理解了多少:北大计算机概论——二维数组指针练习
必会操作:通过指针遍历二维数组
int main() {
int a[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
int *p;
// 这里注意一下,虽然我们通常会把二维数组写成矩阵的形式,但它在内存中的排列是线性的
for (p = &a[0][0]; p < &a[0][0] + 12; ++p) {
cout << p << " " << *p << endl;
}
return 0;
}
指针与函数
指针变量做函数参数
#include <iostream>
using namespace std;
void Rank(int *p1, int *p2) {
int temp;
if (*p1 < *p2) {
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
}
int main() {
int a, b, *p1, *p2;
cin >> a >> b;
p1 = &a;
p2 = &b;
Rank(p1, p2);
cout << a << " " << b << endl;
return 0;
}
这在java中其实就是引用传递的概念。
指针常量与常量指针
指针作为函数的返回类型
函数的定义举例:int *function(int param1, int param2)
这里的内容比较简单,但是有一点必须要说明,还是举个例子:
#include <iostream>
using namespace std;
int *getInt() {
int value = 10;
return &value;
}
int main() {
int *p;
p = getInt();
cout << *p << endl;
return 0;
}
你以为会输出20吗?天真了,注意一下 getInt() 方法中 value 的作用范围,这个局部变量在函数调用完后就不存在了,因此return &value; 这个操作是无厘头的。如果你有一款成熟的IDE,那么它就会提示你:Address of stack memory associated with local variable ‘value’ returned。
静态局部变量
静态局部变量的值在函数调用结束后不消失,而保留着,其占用的存储单元不释放,在下一次该函数调用时,仍然可以继续使用该变量。
定义举例: static int a = 1;
要注意,这一步操作只会执行一次,无论它在循环中出现多少次,或者在方法的调用中重复出现多少次。
本文主要参考了李戈老师的北大计算机概论课程,有兴趣的童鞋可以去小破站观摩~