1.既然指针的本质都是变量的内存首地址,即一个 int 类型的整数,那为什么还要有各种不同类型的指针?
上面的问题,就是为了引出指针解引用的。
pa中存储的是a变量的内存地址,那如何通过地址去获取a的值呢?
这个操作就叫做解引用,在 C 语言中通过运算符 就可以拿到一个指针所指地址的内容了。
比如pa就能获得a的值。
我们说指针存储的是变量内存的首地址,那编译器怎么知道该从首地址开始取多少个字节呢?
这就是指针类型发挥作用的时候,编译器会根据指针的所指元素的类型去判断应该取多少个字节。
如果是 int 型的指针,那么编译器就会产生提取四个字节的指令,char 则只提取一个字节,以此类推。
2.指针申明
基本指针
首先,从最基本的指针开始:
int *p
:这是一个指向int
类型变量的指针。p
存储的是一个整数的地址,而不是整数的值。
二级指针
接下来是二级指针:
int **a
:这是一个指向int *
类型变量的指针。换句话说,a
存储的是一个指向整数的指针的地址。
拆解解释
为了更容易理解,我们可以将 int **a
拆成两部分:
*a
:表示a
是一个指针变量。int *
:表示a
这个指针变量存储的是一个int *
类型变量的地址。
更高等级的指针
我们可以用同样的方法解释更高级的指针,比如三级指针:
int ***b
:这是一个指向int **
类型变量的指针,也就是说,b
存储的是一个指向int *
类型变量的指针的地址。
拆解 int ***b
:
*b
:表示b
是一个指针变量。int **
:表示b
这个指针变量存储的是一个int **
类型变量的地址。
具体例子
让我们来看一个具体的例子来说明多级指针的含义:
int x = 10; // 一个整数变量 x,值为 10
int *p = &x; // 一个指针 p,指向 x 的地址
int **a = &p; // 一个二级指针 a,指向 p 的地址
在内存中,这些变量可能看起来像这样:
x
的地址是0x100
,值是10
p
的地址是0x200
,值是0x100
(指向x
)a
的地址是0x300
,值是0x200
(指向p
)
当我们使用 **a
时,我们实际上在访问 x
的值:
*a
是p
的地址,即0x200
**a
是x
的值,即10
int p;
这是一个普通的整型变量。
即 p is int.
普通指针
int *p
首先从 p 处开始,先与 * 结合,所以说明 p 是一个指针。然后再与 int 结合,说明指针所指向的内容的类型为int型,所以 p 是一个指向整型数据的指针。
即 p is pointer to int.
数组
int p[3];
首先从 p 处开始,先与[]结合,说明 p 是一个数组。然后与 int 结合,说明数组里的元素是整型的,所以 p 是一个由整型数据组成的数组。
即:p is arry(size 3) of int.
指针数组
int *p[3];
首先从 p 处开始,先与 [] 结合,因为其优先级比高( [] 在c语言中属于后缀运算符和 () 等同为最高优先级),所以 p 是一个数组。然后再与 * 结合,说明数组里的元素是指针类型。之后再与int结合,说明指针所指向的内容的类型是整型的,所以 p 是一个指向 int 的指针数组。
英文即: p is arry(size 3) of pointer to int.
数组指针
int (*p)[3];
首先从 p 处开始,先与 * 结合(因为 * 是被括号包围的),说明 p 是一个指针。然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组。之后再与 int 结合,说明数组里的元素是整型的,大小是 3,所以 p 是一个指向 int 数组(大小为3)的指针。
英文即:p is pointer to arry(size 3) of int.
二级指针
int **p;
首先从 p 开始,先与 * 结合,说明 p 是一个指针。然后再与 * 结合,说明指针所指向的元素还是指针。之后再与 int 结合,说明该指针所指向的元素是整型数据。由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针。
英文即: p is pointer to pointer to int.
函数声明
int p(int);
从 p 处起,先与 () 结合,说明 p 是一个函数。然后进入 () 里分析,说明该函数有一个整型变量的参数,之后再与外面的 int 结合,说明函数的返回值是一个整型数据。
英文即: p is function(int) returning int.
函数指针
int (*p)(int);
从 p 处开始,先与指针结合,说明 p 是一个指针。然后与()结合,说明指针指向的是一个函数。之后再与()里的int结合,说明函数有一个int型的参数,再与最外层的int结合,说明函数的返回类型是整型,所以 p 是一个指向有一个整型参数且返回类型为整型的函数的指针。
英文即: p is pointer to function(int) returning int.
复杂声明
int* (*p(int))[3];
从 p 开始,先与()结合,说明 p 是一个函数。然后进入()里面,与int结合,说明函数有一个整型变量参数。然后再与外面的 * 结合,说明函数返回的是一个指针。
之后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组。接着再*与结合,说明数组里的元素是指针,最后再与int结合,说明指针指向的内容是整型数据。
所以 p 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数。
p in function(int) returning pointer to arry(size 3) of pointer to int.
3.Void指针
void 指针最大的用处就是在 C 语言中实现泛型编程,因为任何指针都可以被赋给 void 指针,void 指针也可以被转换回原来的指针类型, 并且这个过程指针实际所指向的地址并不会发生变化。 比如:
int num;
int *pi = #
printf("address of pi: %p\n", pi);
void* pv = pi;
pi = (int*) pv;
printf("address of pi: %p\n", pi);
两次输出的值是一样的
不能对 void 指针解引用
比如:
int num;
void *pv = (void*)#
*pv = 4; // 错误
因为解引用的本质就是编译器根据指针所指的类型,然后从指针所指向的内存连续取 N 个字节,然后将这 N 个字节按照指针的类型去解释。
比如 int *型指针,那么这里 N 就是 4,然后按照 int 的编码方式去解释数字。
但是 void,编译器是不知道它到底指向的是 int、double、或者是一个结构体,所以编译器没法对 void 型指针解引用
4. 指针和引用
指针和引用在 C++ 中都用于间接访问变量,但它们有一些区别:
指针是一个变量,它保存了另一个变量的内存地址;引用是另一个变量的别名,与原变量共享内存地址。
指针(除指针常量)可以被重新赋值,指向不同的变量;引用在初始化后不能更改,始终指向同一个变量。
指针可以为 nullptr,表示不指向任何变量;引用必须绑定到一个变量,不能为 nullptr。
使用指针需要对其进行解引用以获取或修改其指向的变量的值;引用可以直接使用,无需解引用。
5. 值传递(Value Passing)
值传递是将实参的值传递给形参。在这种情况下,函数内对形参的修改不会影响到实参。
示例:
#include <iostream>void swap_value(int a, int b) {int temp = a;a = b;b = temp;
}int main() {int x = 10;int y = 20;swap_value(x, y);std::cout << "x: " << x << ", y: " << y << std::endl; // 输出:x: 10, y: 20return 0;
}
6. 引用传递(Reference Passing)
引用传递是将实参的引用传递给形参。在这种情况下,函数内对形参的修改会影响到实参。
示例:
#include <iostream>void swap_reference(int &a, int &b) {int temp = a;a = b;b = temp;
}int main() {int x = 10;int y = 20;swap_reference(x, y);std::cout << "x: " << x << ", y: " << y << std::endl; // 输出:x: 20, y: 10return 0;
}
7. 指针传递(Pointer Passing)
指针传递是将实参的地址传递给形参。在这种情况下,函数内对形参的修改会影响到实参。
示例:
#include <iostream>void swap_pointer(int *a, int *b) {int temp = *a;*a = *b;*b = temp;
}int main() {int x = 10;int y = 20;swap_pointer(&x, &y);std::cout << "x: " << x << ", y: " << y << std::endl; // 输出:x: 20, y: 10return 0;
}