指针是C++的一个非常强大的特性,它能使我们直接访问计算机的内存,指针可以用来引用一个数组,一个字符串,一个整数或者任何其他变量。这种强大的功能使得指针在C++程序设计中是非常普遍的,而同时,指针的知识又显得有那么些“繁杂”,有必要清晰地做个总结。
什么是指针
指针,就是内存地址。我们一般会声明一个变量是整数int
,浮点double
,或者字符char
等等,指针变量(通常简称指针)和他们本质上没什么区别,不过它保存的是一个内存地址。
先来看看指针变量的声明:
int *pValue; // int *pValue与int* pValue是一样的,找一种适合自己的写法就好
声明指针变量,要在变量名前方一个*
,这里的int
表示这个指针(也就是内存地址)所保存的变量的类型。注意,内存地址本身可以理解为是没有数据类型的,所以指针声明中的类型完全依赖于所赋值的内存地址保存的数据的类型。比如下面的代码,我将指针变量pValue
的声明和初始化放一起了:
int value = 10;
int *pValue = &value;
需要注意的是第二行,&value
中的&
是取址运算符,表示将已经定义的变量value
所在的内存地址赋值给指针变量pValue
。
那所谓内存地址长什么样子呢?我下面将内存地址打印出来,大家感受一下,顺便再区分一下指针变量和普通变量的区别:
int value = 10;
int *pValue = &value;
std::cout << pValue << std::endl; // 指针的值,0x7fff5e9f4b28
std::cout << *pValue << std::endl; // 指针所指向(即相应的内存地址所保存)的值,10
std::cout << &value << std::endl; // 变量value所在的内存地址的值,0x7fff5e9f4b28
需要注意的是第3行中,*
此时的作用是“间接引用”,表示读取指针pValue
所存储的变量。*
也叫“间接引用运算符”,和之前在指针变量声明中*
的作用是不一样的。这里大家一定要小心*pValue
并非指针,pValue
才是,比如给指针赋值的时候,下面的语法才是对的:
int value = 10;
int *pValue;
pValue = &value; // 这是对的,而*pValue = &value是错的
现在,已经知道*
在C++中的3中使用方式:
- 乘法运算符:
int a = b * c
- 声明指针变量:
int *pValue;
- 间接引用运算符:
std::cout << *pValue;
上面对于*
的总结,也是提醒大家,当看到*pValue
这种形式时,先要判断到底是指针的声明还是间接引用。另外需要注意的一点是,不能像声明整型变量那样,用一条语句声明2个指针。比如:
int i, j; // 一条语句声明2个整型变量i, j
int *pValue1,pValue2; //这条语句实际上声明了指针pValue1,和整型数据pValue2,而pValue2不是指针
指针实现实参的引用传递
指针也可以作为实参传递给函数。C++中,向函数传递实参一共有三种方式:按值传递;按引用传递;按指针传递。我在这里顺带一并总结了。
按值传递
声明并定义函数:
#include <iostream>
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int a = 1, b = 0;
swap(a, b);
std::cout << "a = " << a << ", " << "b = " << b << std::endl;
return 0;
}
实参a, b
直接以数值的形式传入函数。这个程序运行的结果: a = 1, b = 0
. 可见a和b的值只是在函数内部发生了交换,换句话说,这里传入函数的是实参的拷贝,而并未改变实参本身。拿上面的例子来说,想要“彻底”实现两个数的交换,就要用到“按引用传递”的方式了。
按引用传递
引用变量的语法形式为& + 变量名
,此处与取址运算符形式上很像,要注意区分。所谓引用就是参数的别名。如果声明的函数中形参是引用,那么形参就是原变量的别名,而并非是拷贝:
#include <iostream>
void swap(int &n1, int &n2) {
int temp = n1;
n1 = n2;
n2 = temp;
}
int main() {
int a = 1, b = 0;
swap(a, b);
std::cout << "a = " << a << ", " << "b = " << b << std::endl;
return 0;
}
函数第11行,实参a和b实际上分别与形参n1, n2
形成了“共享”。此时的n1, n2
你可以理解为就是a,b的别名。上面程序运行的结果:a = 0, b = 1
.
按指针传递
顾名思义,形参为指针,而传入的实参也是指针。还是上面的例子:
#include <iostream>
void swap(int *n1, int *n2) {
int temp = *n1;
*n1 = *n2;
*n2 = temp;
}
int main() {
int a = 1, b = 0;
swap(&a, &b);
std::cout << "a = " << a << ", " << "b = " << b << std::endl;
return 0;
}
将整型变量a,b的地址&a, &b
作为实参传入函数,而函数swap
的实际作用是交换这两个地址所存储的数值。形象的说,以前n1
房子住1,n2
房子住0;现在反了,n2
房子住1,n1
房子住0;a和b的值自然也就跟着变化。这个程序的运行结果:a = 0, b = 1
.
用关键词总结一下这3种方式:
- 按值:拷贝
- 按引用:共享
- 按指针:传入的是“房子”
此外,我也在这里对&
符号的应用做个小结:
&
出现在函数的声明中(即形参),例如int func(int &num)
,则表示这个函数的实参是按引用传入的;&
出现在函数的调用中(即实参),例如std::cout << func(&num)
,则表示这个函数的实参是按指针传入的,实参是值num
的地址;
数组的指针
C++中,数组就是指针。比如我定义一个数组int a[] = {1, 2, 3}
,那么数组变量a
实际上是数组中元素a[0]
的地址(即a = &a[0]
)。不信我们看看:
int a[] = {1, 2, 3};
std::cout << a << std::endl; // 0x7fff5c163b4c
std::cout << &a[0] << std::endl; // 0x7fff5c163b4c
这一点是所有指针和数组问题的核心,明白了这一点,其他好多内容都可以自己推导出来。
比如,可以由数组变量得到其中某个元素的地址。对于整型数组a来说,a[i]
的地址是a + i
。
还是上面的例子,我向访问a[1]
,可以这样做:
int a[] = {1, 2, 3};
std::cout << *(a + 1) << std::endl; // 2
根据这样的规则,你告诉我*a + 1
是多少,*(a + 1)
又是多少?显然,这两个表达式是不一样的,*a + 1
表示a[0] + 1
的值,而*(a + 1)
则表示a[1]
的值。
此外,我们知道C++函数中不能把数组当做返回值,但是数组和指针的性质则可以帮助我们设计返回值是指针的函数,使得其返回的其实就是数组。不过需要注意的是,一旦数组声明,数组的位置(指针)就不能变了,也就是说数组其实是“常量指针”。
常量指针
常量指针有3种形式:
const int *pValue = 10
:指针不是常量,但指针指向的数据是常量;int* const pValue = 10
:指针是常量,但指向的数据是可以变的;const int* const pValue = 10
:指针和指针指向的数据都是常量;
把握一点,const在谁前头,谁就被定义为常量了。
动态内存分配
动态内存分配的目的是给变量分配更持久的内存空间。我们都知道,在函数中定义的变量是局部变量,当函数运行完毕,那么调用栈中的局部变量也就被丢弃了。而有时,可能这个变量还是有用的。为了处理这种情况,可以使用动态内存分配。动态内存分配是以关键字new
实现的。比如:
int *pValue = new int
这里,给指针pValue
所指向的整型数据分配了内存空间。再比如,也可以为数组分配内存:
int *list = new int[10]
当然,为数组动态内存分配还有一个好处,那就是可以创建动态数组(即数组的长度可以是变量)。动态数组是个非常有用的概念,比如,我现在要做这样一件事情:设计一个函数,函数的输入为斐波那契数列的长度,返回斐波那契数列的所有元素。代码如下:
#include <iostream>
int *fibonacci(int num) {
int *list = new int [num];
list[0] = 0;
list[1] = 1;
for (int i = 2; i < num; i++) {
list[i] = list[i - 1] + list[i - 2];
}
return list;
}
int main() {
int num = 10;
int *list = fibonacci(num);
for (int i = 0; i < num; i++) {
std::cout << list[i] << std::endl;
}
return 0;
}
可以看到,在第4行建立的动态数组,这种动态内存分配后的永久性也保证了,指针list
的正确返回。
结语
好了,至此指针的基本知识算是介绍完了(之后,我觉得有没有介绍的重要知识的话,本文还会持续更新)。在实际编码的过程中,指针的应用是千变万化的,但是无论怎样变化,掌握最基本的知识,再做高层次的推理总是没有问题的。