C++ 指针和引用详解
C++
是在 C
的基础上发展来的,C++
除了有 C
语言的指针外,还增加了一个新的概念——引用,初学者容易把指针和引用混淆一起。
要弄清楚这两个概念,先从变量说起。
变量
首先最重要的,变量的定义,当你申明一个变量的时候,计算机会将指定的一块内存空间和变量名进行绑定;这个定义很简单,但其实很抽象,例如:
int x = 5;
这是一句最简单的变量赋值语句了, 我们常说“x
等于5
”,其实这种说法是错误的,x
仅仅是变量的一个名字而已,它本身不等于任何值的。这条语句的正确翻译应该是:“将 5
赋值于名字叫做 x
的内存空间”,其本质是将值 5
赋值到一块内存空间,而这个内存空间名叫做 x
。
切记:x
只是简单的一个别名而已(变量名),x
不等于任何值。
变量在内存中的操作其实是需要经过2个步骤的:
1)找出与变量名相对应的内存地址。
2)根据找到的地址,取出该地址对应的内存空间里面的值进行操作。
指针
指针简介
指针是一个变量,其值为另一个变量的地址。
指针变量和任何变量一样,也有变量名,指针变量相对应的内存空间存储的值恰好是某个内存地址,这也是指针变量区别去其他变量的特征之一。
int *ptr; // 指针的类型是int *,指针所指向的类型是int
char *ptr; // 指针的类型是char *,指针所指向的的类型是char
int **ptr; // 指针的类型是 int **,指针所指向的的类型是 int *
int (*ptr)[3]; // 指针的类型是 int(*)[3],指针所指向的的类型是 int()[3],
// 注意不是指针数组,是单个指针,用数组的首地址来初始化
int *(*ptr)[4]; // 指针的类型是 int *(*)[4],指针所指向的的类型是 int *()[4]
指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C越来越熟悉时,你会发现,把与指针搅和在一起的“类型”这个概念分成“指针的类型”和“指针所指向的类型”两个概念,是精通指针的关键点之一。
- 我们说一个指针的值是 XX,就相当于说该指针指向了以 XX 为首地址的一片内存区域;
- 我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。
指针运算符 & 和 *
说到指针,就涉及到指针的两个运算符(&,*
):
- 取地址运算符
&
,&
是一元运算符,返回操作数的内存地址。例如,如果var
是一个整型变量,则&var
是它的地址。返回的结果是一个地址。 - 间接寻址运算符
*
,它是&
运算符的补充。*
也是一元运算符,返回操作数所指定地址的变量的值。返回的结果五花八门,*p
类型是该指针p
指向的类型。 - 运算时都是从右往左顺序进行的。
int a = 12;
int b;
int *p;
int **ptr;
p = &a; // &a 返回变量 a 的地址。p 是指针类型的变量,存储的是(指向)变量 a 的地址
*p = 24; // 访问指针 p 所指向地址的内存空间,也就是 *p 就是访问变量 a
ptr = &p; // &p的结果是个指针类型变量的地址,也就是说指针 ptr 指向指针变量 p 的地址
*ptr = &b; // 访问指针 ptr 所指向地址的内存空间,也就是访问指针 p,令指针 p 指向变量 b 的地址
**ptr = 34; // *ptr的结果是 ptr 所指向的东西,在这里是指针 p,对这个指针再做一次 * 运算,结果就是指针 p 所指向的东西,也就是访问整型变量 b。
指针的算术运算
指针的加减操作,不是把它所指向位置的值加减,而是把地址向后或向前移动。
地址移动的大小与指针所指向的类型大小相关。
-
指针
p
的类型为int *
,其所指向的类型为int
,所以p
加 1 会向后移动sizeof(int)
。#include<cstdio> int main() { int a = 1, *p, *q; p = &a; q = p + 1; printf("%p %p", p, q); }
0022feb4 0022feb8
-
指针
p
的类型为char *
,其所指向的类型为char
,所以p
加 1 会向后移动sizeof(char)
。#include<cstdio> int main() { char a = '1', *p, *q; p = &a; q = p + 1; printf("%p %p", p, q); }
0022feb7 0022feb8
-
指针只是一个地址,它的类型只影响用运算符
*
访问时从该位置向后读取多少个字节。指针ptr
的类型为int *
,其所指向的类型为int
,但是可以把一个其他类型变量的地址给该指针,ptr
加 1 会向后移动sizeof(int)
个字节,ptr
加 5 会向后移动5 * sizeof(int)
个字节。char a[20]; int *ptr = a; ptr++; // 向高地址方向增加了 4 个字节 ptr += 5; // 向高地址方向增加了 20 个字节
指针数组
声明一个具有 10 个指针的指针数组,里面每一个元素 (p[0],p[1],p[2],…,p[9])
都是一个指针。
int *p[10];
可以用一个 char *
类型(指向的类型为 char
)的指针数组来存储一个字符串列表:
#include <iostream>
using namespace std;
const int MAX = 4;
int main ()
{
const char *names[MAX] = {
"Zara Ali",
"Hina Ali",
"Nuha Ali",
"Sara Ali",
};
for (int i = 0; i < MAX; i++)
{
cout << "Value of names[" << i << "] = ";
cout << names[i] << endl;
}
return 0;
}
Value of names[0] = Zara Ali
Value of names[1] = Hina Ali
Value of names[2] = Nuha Ali
Value of names[3] = Sara Ali
指针与数组的关系
数组的数组名其实可以看成是一个指针。如下例:
int array[10] = {0,1,2,3,4,5,6,7,8,9};
int value;
value = array[0]; //也可写成:value = *array;
value = array[3]; //也可写成:value = *(array+3);
value = array[4]; //也可写成:value = *(array+4);
上例中,一般而言数组名 array
表数组本身,类型是 int [10]
,但如果把 array
看做指针的话,它指向数组的第 0
个单元,类型是 int *
,所指向的类型是数组单元的类型即 int
。因此 *array
等于 0
就一点也不奇怪了。同理,array+3
是一个指向数组第 3
个单元的指针,所以 *(array+3)
等于 3
。其它依此类推。
int (*p1)[4];
int *(*p2)[4];
int *p3[4];
cout << sizeof(p1) << endl; // 32位机器中为 4
cout << sizeof(p2) << endl; // 32位机器中为 4
cout << sizeof(p3) << endl; // 32位机器中为 16
指针与结构体的关系
struct MyStruct
{
int a;
int b;
int c;
}
MyStruct ss = {20,30,40};
MyStruct *ptr = &ss; // 指针类型是 MyStruct*, 它指向的类型是 MyStruct。
cout << ptr->a << endl;
cout << ptr->b << endl;
cout << ptr->c << endl;
指针与函数的关系
在一个程序中,不仅仅是变量需要分配内存,函数也一样,那么函数自然也可以有指针,是函数的入口地址。
函数指针声明只比函数声明多一个 *
和一对括号,例如:
int (*Psum)(int*,int) // 声明函数指针
其中把*Psum
括起来的括号一定不能少,不然编译器会认为你声明了一个叫 Psum
的函数,返回类型是 int*
。
#include<cstdio>
int (*Psum)(int*,int);
int sum(int *a,int n) // 指针也可以看成是一个数组,前面说过数组可以看成指针
{
int ans = 0;
for(int i = 1; i <= n; i++)
ans += a[i];
return ans;
}
int main()
{
int A[10], n;
scanf("%d", &n);
for(int i=1; i<=n; i++) {
scanf("%d", &A[i]);
}
Psum = sum; // 不用'&',因为这里函数后面没有括号视为是函数的地址
printf("%d\n", (*Psum)(A,n));
}
引用
引用相当于变量的别名,对引用的操作与对变量直接操作完全一样。
引用的声明方法:类型标识符 &引用名=目标变量名;
int m;
int &n = m;
上面的代码中,定义了引用 n
,它是变量 m
的引用,这样子目标变量就有两个别名,即它的原名称和引用名,且不能把该引用名作为其他变量名的别名。
引用的规则:
- 引用被创建的同时必须被初始化,而指针可以在任何时候被初始化;
- 不能有
NULL
引用,引用必须与合法的存储单元关联,也就是说定义时必须同时初始化,而指针可以是NULL
; - 一旦引用被初始化,就不能改变引用的关系,而指针可以随时改变所指的对象
int i = 5;
int j = 6;
int &k = i; // 定义变量 i 的引用,也就是给 i 一个别名 k
k = j; // k 和 i 的值都变成了 6;
上面 k = j
不能将 k
修改为 j
的引用,只是把 k
(i
) 的值改变成为 6
,因为引用一旦初始化,就不能改变引用的关系,
引用与指针的联系与区别
相同点:
- 都是地址的概念。
指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。
不同点:
- 引用必须被初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配存储空间。
- 标准没有规定引用要不要占用内存,也没有规定引用具体要怎么实现,具体随编译器 http://bbs.csdn.net/topics/320095541。
- 引用初始化后不能被改变,指针可以改变所指的对象。
- 不存在指向空值的引用,但是存在指向空值的指针。
- 指针可以有多级,但是引用只能是一级(
int **p
合法 而int &&a
是不合法的)。 sizeof引用
得到的是所指向的变量(对象)的大小,而sizeof指针
得到的是指针本身的大小(与机器有关,32 位机器就是 4 字节)。- 指针和引用的自增(++)运算意义不一样。
- 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄漏。
- 如创建链表节点时
ListNode* node = new ListNode();
- 如创建链表节点时
从概念上讲。指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。
而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。
总而言之,言而总之——它们的这些差别都可以归结为 “指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名,引用不能改变指向”。
指针传递和引用传递
当指针和引用作为函数的参数是如何传值的呢?
- 指针传递参数本质上是值传递的方式,它所传递的是一个地址值。
- 值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。
- 引用传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
引用传递和指针传递是不同的,虽然它们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用简介寻址运算符 *
。
参考
https://www.runoob.com/cplusplus/cpp-pointer-operators.html。
https://www.runoob.com/cplusplus/cpp-pointers.html。
https://www.jianshu.com/p/9d4f5ab84d40。
https://www.cnblogs.com/ggjucheng/archive/2011/12/13/2286391.html。