单纯的指针

最简单的指针带*的:野指针(确信)

1
int *p; //一只快乐的野指针,未初始化的指针,指向随机地址(野指针)

这个表示定义了指针,不过注意,前面的类型是要自己设定的,不能乱写
但是提到指针,就不得不提到&(取地址符)了

&的用法:

&表示取地址符,比如这样:

1
2
int a=1;
cout<<&a<<endl;//输出a的地址

这一句表示输出a变量对应的地址位置,详细见书上的解释,获取地址的开头方便输出

然后指针所能存储的恰好是地址,所以可以这么写:

1
int *p=&a;

表示p指向a对应的地址

指针的初始化

可以把指针设定为NULL,这里表示p不指向任何一个地址
要是没有初始化,p可能指向任何一个位置,可能导致代码错误,所以被称作野指针

1
2
3
4
5
6
7
int *p = NULL;  // 未初始化指针(野指针)
*p = 10; // 段错误

// 解引用前应检查
if(p != NULL) {
*p = 10;
}

指针的多种含义

在实际程序中,*p,可能有多种含义:

  • 表示p所指的变量,即*p=a
  • 表示p对指的变量的值,即a的值

下面是例子:

1
2
3
4
int a=1;
int *p=&a;
*p=1; //表示将p指向的变量(即a)赋值为1
cout<<*p<<endl;//表示输出p指向的变量(即a)的值

这里两种情况怎么理解呢?
首先加上*表示对指针解引用,即访问指针指向的内存位置

  • case 1:这里指把p所指的位置修改为1,所以表示p所指的变量
  • case 2:这里因为表示的是p所指的位置,所以我们在输出时就是从这个位置开始逐个输出,表示的就是所指对应变量对应的值了。

更详细的解释:
在C++里有两种值叫做左值(Location Value)右值(Read Value)

  • 左值表示一个内存位置,可以被赋值
  • 右值表示一个数据值,只能被读取

前者表示的是地址,被写入数据的那个变量,所以表示地址
后者只是获取其内部数据,表示的就是对应的值了

所以,当*p出现在赋值号左边时,它是左值,表示一个内存位置(即p指向的变量);当*p出现在其他表达式中(如赋值号右边、函数参数等),它是右值,表示该位置存储的值

这样基本用法就能理解了

指针表示一维数组

指针指向的是一个元素的位置,要是指向的是一个数组的开头呢?

指针指向一个元素的位置,如果指向一个数组的开头,就可以通过指针来访问数组

那么问题来了,我们该怎么获取中间元素的值呢?

这里我们就需要知道一下数组下标的工作原理了:

实际上程序会计算要往后访问几个位置,根据数据类型来决定往后跳几步。
所以我们可以这么理解a[3]
a开头位置加上3×步长,得到这个元素的地址开头,就能正常访问了,
这个过程相当于是一种规则,表示我们要往后几步

对于指针也是如此,首先我们已经保证了指针p所指向的变量类型和数组a的元素类型一致,所以我们知道了p的开头以后,我们只要往后加上对应的步长就能找到我们要访问的元素了

那么C++中规定了,指针+1表示往后一个步长,[n]表示往后n个步长,所以我们从这里理解了,为什么数组的下表是从0开始的,以及怎么利用指针来访问数组

诶那我们是不是发现了数组a怎么和p这么类似啊,都能通过[n]来访问元素
确实,实际上数组名在大多数情况下可以看作指向数组首元素的指针,我们用a+1也能访问元素,a表示的是a数组的开头地址,所以要用p遍历a的时候,我们只要p=a就能完成初始化,因为p存储的不正是地址吗?

所以我们在代码里一般可以这么写:

1
2
3
4
5
6
7
8
9
int a[10]={0,1,2,3,4,5,6,7,8,9};
int *p=a;
cout<<p<<endl; //a[0]的地址
cout<<p[1]<<endl; //a[1]的值
cout<<p+2<<endl; //a[2]的地址

for(int i=0;i<10;i++){ //遍历a数组
cout<<p+1<<" "<<p[i]<<endl; //两者等价
}

注意:数组名a在大多数情况下可以转换为指针,但数组名不是指针,它是整个数组的标识符。在sizeof(a)时,它返回整个数组的大小,而不是指针大小

值得注意的是,因为我们这里只是规定了向后移动几步,没有规定范围,所以理论上可能超出数组规定的范围访问,但是这会导致段错误,因为程序理论上没有权限访问这个位置的值
错误示例:

1
2
3
4
// 指针越界访问
int arr[5];
int *p = arr;
p[10] = 5; //未定义行为

理解了指针和数组的关系,也就理解了指针的本质,我们接下来就可以理解指针运算了。

指针运算

要是直接加减常数,表示的是移动多少个位置,要是是两个指针相减,表示的是中间差了几个单位,当然,我们也可以进行大小比较,表示指针在一个数组中的前后位置关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 指针与整数的加减
int arr[10];
int *p = arr;
p = p + 3; // 移动3个int位置
p = p - 2; // 移动回去2个位置

// 指针之间的减法(得到元素个数)
int *p1 = &arr[2];
int *p2 = &arr[5];
printf("%ld\n", p2 - p1); // 3

// 指针比较
if(p1 < p2) { // 同一数组内比较有意义
// ...
}

二级指针

1
2
3
4
5
6
int **p;
int *p1;
int a=1;
p1=&a;
p=&p1;
cout<<**p1<<endl;

表示指向存储指针位置的指针,分析时一层一层去掉就行了

要是等级更高也是类似的理解,比如实际上可以创建一个三级的指针

1
2
3
4
5
6
7
8
int ***p;
int **p1;
int *p2;
int a=1;
p2=&a;
p1=&p2;
p=&p1;
cout<<***p<<endl;

指针与函数

首先函数中可以直接传入指针变量,这里可以理解为直接把对应的开头地址传过来了

比如我们写一个swap函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
void swap(int *p1,int *p2){
int temp;
temp=*p1;
*p1=*p2;
*p2=temp;
}
int main(){
int a=1,b=2;
int *p1=&a,*p2=&b;
swap(&a,&b);
//也可以写swap(p1,p2);
cout<<a<<" "<<b<<endl;
}

这里操作完后,主函数里a,b的值也会互换,因为我们传入的是地址,在函数内我们又对这个地址对应的位置进行了操作,那么就能修改这个位置的值了,然后注意在函数调用时要传入的是地址,因为要和函数所需参数(指针)相对应。

注意,以下几种写法都是错误的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void swap_w1(int *p1,int *p2){
int *temp;
*temp=*p1; //错误,temp是野指针,未初始化,不能解引用
*p1=*p2;
*p2=*temp;
}
void swap_w2(int *p1,int *p2){
int *temp;
temp=p1; //错误,只交换了p1和p2对应的地址,但是函数结束这两个就被删除了
p1=p2; //只交换了p1和p2这两个指针变量本身(局部变量)
p2=temp; //没有交换它们指向的值
}
int main(){
int a=1,b=2;
int *p1=&a,*p2=&b;
swap(*p1,*p2); //错误:*p1表示的是对应的地址对应的值
cout<<a<<" "<<b<<endl;
}

最后这个为什么是错的?因为不是赋值语句,表示的是右值,是对应的值
*p1表示的是a的值,即1,类型是int,而函数需要int*,类型不匹配

那么中间的函数为什么是错误的?
我们来详细分析一下:

1
2
3
函数开始时          函数结束时          回到主函数(删除了作为形参的p1,p2)
p1->a=1 p1->b=2 a=1
p2->b=2 p2->a=1 b=2

所以没有任何变化,这个函数既没有修改p1,p2对应的位置的值,也法修改a和b对应的地址位置,所以没有产生任何变化。

传入数组也是类似的:

1
2
3
void f(int a[],int n){
//指定a是指针即可,反正传入首个元素的地址就行了,遍历方法见上面
}

然后因为数组传入都是传入首位置的,所以在函数内对数组操作是会改变原数组的

我知道大家一直有一个问题,为什么要这么大费周折的学习指针呢?
其实在这一块就给出了一个答案:可以让函数有多个返回值

注意一下函数的返回值,不能返回指针形参的地址,因为正如前面所说,函数结束后,这个位置就被删除了

1
2
3
4
int* bad_func() {
int x = 10; // 局部变量
return &x; // 函数结束x被删除
}

指针与二维数组

遍历方法类似,因为二维数组一般是连续的,所以可以p+i一直加下去来遍历,不过我们这里要讲解一下一个新的概念——行指针

行指针写法:

1
int (*p)[4];

根据之前的学习我们知道,这其实也是规定了一种遍历规则,*p指向的是一个长度为4的一维数组的开头位置,要是+1的话表示跳过4个长度,然后加上括号表示是p指向数组,如果写成int *p[4],则表示一个长度为4的指针数组。

那么我们就可以通过p来访问这个二维数组了,

首先p表示的是对应哪一行,*p是这一个数组对应的开头,就变成一维数组的访问了
例如:

1
2
3
4
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int (*p)[4];
p=a;
cout<<*(*(p+1)+1)<<endl;

和前面一位数组的理解类似二维数组名a也可以看作指向第一行(整个一维数组)的指针,指向的是第一行这个一维数组,这里的调用相当于进行了两次一维数组的查询操作,先确定行再确定列。

在行上,我们有:

1
2
3
4
5
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int (*p)[4];
p=a;
cout<<p[0]<<endl; // 第一行的首地址,即&a[0][0]
cout<<p+1<<endl; // 第二行的首地址,即&a[1][0]

再加上列,我们就有四种组合了,本质上是等价的

1
2
3
4
cout<<p[1][1]<<endl;
cout<<(*(p+1))[1]<<endl;
cout<<*(p[1]+1)<<endl;
cout<<*(*(p+1)+1)<<endl;

行指针和函数

因为在行指针里,每次+1跳过几个位置的规则是十分重要的,所以在把行指针传入函数时,两者的第二给参数,即规则,得完全一致,不然程序会报错

1
2
3
void f(int arr[][4]){
//
}

或者:

1
2
3
void f(int (*arr)[4]){
//
}

只有规则相同才能调用

动态分配数组空间

一维数组

1
2
int *p=new int[n];
delete[] p;

或者

1
2
int *p=(int*)malloc(sizeof(int)*n);
free(p);

相当于每个位置都给了一个int的空间
注意:malloc返回void*,需要强制类型转换。

例如,下面这段代码是错误的:

1
int *p = malloc(sizeof(int) * 100);

二维数组

在C语言中,动态分配二维数组(不连续)

1
2
3
4
5
6
7
8
int **matrix = new int *[ROW];
for (int i = 0; i < ROW; ++i) {
matrix[i] = new int[COL];
}
for (i = 0; i < m; i++) {
delete[] matrix[i];
}
delete[] matrix;

或者

1
2
3
4
5
6
7
8
int **matrix = (int**) malloc ( ROW * sizeof( int* ));
for int(i = 0 ; i < COL ; ++i) {
matrix[i] = (int*) malloc ( COl * sizeof( int ));
}
for (i = 0; i < m; i++) {
free(matrix[i]);
}
free(matrix);

这里的二级指针存储的是(每一行开头的元素的地址)的地址
为什么要这么麻烦呢?但是逐个分配的好处是方便删除,也可以实现各行长度不同(锯齿数组)
在这种情况下,我们就不能+1来查询了(因为行之间不连续),但可以通过行指针数组来访问每一行。

如果希望分配连续的二维数组,可以使用以下方法:

1
2
3
int (*p)[COL] = (int(*)[COL])malloc(ROW * COL * sizeof(int));
// 使用...
free(p);

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 方法一:一次分配所有内存(连续)
int rows = 3, cols = 4;
int *data = malloc(rows * cols * sizeof(int));
int **matrix = malloc(rows * sizeof(int*));

for(int i = 0; i < rows; i++) {
matrix[i] = data + i * cols;
}

// 释放
free(matrix);
free(data);

// 方法二:用行指针(更接近静态数组)
int (*p)[cols] = malloc(rows * cols * sizeof(int));
// 可以直接用 p[i][j] 访问
free(p);

这样分配的内存是连续的,可以用p[i][j]访问,且p是一个行指针,p+1会跳过一行(COL个int)

指针与字符串

C++语言中字符串常以字符数组表示,也可以用指针

1
2
3
char str[] = "Hello";
char *p = str;
printf("%s\n", p); // 输出Hello

字符串常量可以用指针指向:

1
char *p = "Hello"; // p指向字符串常量,注意字符串常量不可修改

const与指针

这里介绍基本的const用法,以及基本的const指针

const表示的是常量,表示后面的是不可修改的

首先const int *p1表示p1的类型是const int,所以是指向常量的指针
第二个int *const p2表示p2的类型是constp2 的值是不变的,指向同一个位置,但是他所指向的地址的值却是可以改变的
第三个表示指向不变,且指向的是常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 四种const指针
const int *p1; // 指向常量的指针,值不能改
int const *p2; // 同上
int * const p3; // 常量指针,指向不能改
const int * const p4; // 指向常量的常量指针

// 示例
int a = 5, b = 10;
const int *p = &a;
// *p = 6; // ❌ 错误
p = &b; // ✅ 正确

int * const q = &a;
*q = 6; // ✅ 正确
// q = &b; // ❌ 错误

指针数组与行指针区分

指针数组,每一个位置都是一个指针,指向一个位置
在指针数组,+1表示移动到指针数组的下一个位置,比如names+1表示name[1]的首地址,再加上*表示一维数组,有相似之处,但是和行指针在访问规则上还是有区别的

1
2
3
4
5
6
7
8
// 指针数组:数组的每个元素都是指针
char *names[] = {"Alice", "Bob", "Charlie"};
// names[0] 指向 "Alice"
// names[1] 指向 "Bob"

// 数组指针:指向整个数组的指针
int arr[3][4];
int (*p)[4] = arr; // 指向含有4个int的数组

指针相关题目

下面是程C课上提到的题目,我们来检测一下你有没有理解

例1:

1
2
3
4
5
6
7
8
9
10
int a=10,b=20,c=30;
void fun(int* x,int* y,int* z){
  cout<<++*x<<','<<*y++<<","<<*(z++)<<endl;
}
signed main(){
  for(int i=0;i<3;i++){
    fun(&a,&b,&c);
  }
  cout<<a<<","<<b<<","<<c<<endl;
}

这一题我们需要学习一下C++中的运算符的优先级,从高到低可以理解为:

后缀运算>前缀运算=单目运算>双目运算

双目运算的规则还是比较清晰的,这里就不解释了
详细解释可以看这个网站
C++ 运算符优先级

所以函数中的*y++表示的就是*(y++),先将地址使用,再后移一格,相当于没有对原先位置产生变化,所以得到答案:

1
2
3
4
11,20,30
12,20,30
13,20,30
13,20,30

例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void fun(int* x,int* y,int* z){
  *z=*x+*y;
}
signed main(){
  int a[3][3]={1,1};
  int *p1,*p2,*p3;
  p1=a[0];
  p2=a[0]+1;
  p3=a[0]+2;
  for(int i=2;i<=9;i++){
    fun(p1++,p2++,p3++);
  }
  for(int i=0;i<3;++i){
    for(int j=0;j<3;j++){
      cout<<a[i][j]<<" ";
    }
    cout<<endl;
  }
}

这道题的意图还是比较明确的,即利用二维数组空间的连续性,计算斐波那契数列
注意主函数中的用法,取的是a[0]的地址,因为p1不是行指针,只能指向一个一位数组的开头,所以写成p1=a[0]

答案:

1
2
3
1 1 2 
3 5 8
13 21 34

例3:

1
2
3
int a[5] = {1, 2, 3, 4, 5};
int *ptr = (int *)(&a + 1);
printf("%d", *(ptr - 1));

答案:输出5

此题需要注意的是’(&a + 1)’这里是指向a这个一维数组的地址,那么就是行指针了,所以+1要按照行指针的规则来执行,接下来再强制转化为正常指针,所以-1按照正常指针的规则

如果你对上面的解释还不是很清楚,也欢迎来私信我和我交流