指针

我们在声明变量时,有两个要素即“对应地址+对应值”,变量的指针就是变量的地址

优点:提高效率,使用灵活,可实现动态存储分配

缺点:过于灵活,易出现危险的野指针

指针变量

访问变量的方式

1
2
int arr[3] = {10, 20, 30};
int *p = arr; // 数组名arr等价于&arr[0],直接指向第一个元素
  • 直接访问(通过变量名找到其所在的存储空间)

    1
    cout<<arr[1]<<endl;
  • 间接访问(将a的地址放入p,通过p去访问变量a)

    1
    cout<<*(p + 1)<< endl;   //&:取地址符  *:指针运算符,所声明的即指针变量

[!TIP]

直接访问(变量名 / 数组下标)是最基础、最直观的内存操作方式,胜在可读性高、出错风险低,适合简单变量操作、非性能敏感的普通场景;而间接访问(指针)的核心价值在于突破直接访问的局限 —— 能直接操作底层固定内存地址适配硬件开发、支持运行时动态创建可变长度数据、传递大对象时无需拷贝提升效率,是实现通用函数、高性能程序、底层 / 动态逻辑的核心方式;实际开发中,两者并非对立,而是互补:简单逻辑用直接访问保证易读,复杂场景(底层、动态内存、高性能传参)用间接访问实现核心功能。

定义指针变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int a = 10;
int *p1; // 定义int类型指针p1:p1只能指向int类型变量
p1 = &a; // 给p1赋值:让p1指向变量a(p1存储a的地址)

/* & 和 * 优先级相同,自右向左结合 */
// &*p1:先通过p1找到a的值(*p1=10),再取这个值的地址(&10=&a),最终p2和p1都指向a
int *p2 = &*p1;
// *&p1:先取p1自己的地址(&p1),再找到这个地址里存的内容(就是a的地址),最终p4和p1都指向a
int *p4 = *&p1;

cout << "p1 = " << p1 << endl; // 输出a的地址(如0x61fe14)
cout << "p2 = " << p2 << endl; // 输出和p1相同的地址
cout << "p4 = " << p4 << endl; // 输出和p1相同的地址
cout << "*p1 = " << *p1 << endl;// 输出10(a的值)
cout << "*p2 = " << *p2 << endl;// 输出10(a的值)
cout << "*p4 = " << *p4 << endl;// 输出10(a的值)

在使用时,指针变量前加 ‘*’ 表示指针变量所指向的目标

1
2
3
4
5
int  i;  
int *p1; //定义
p1 = &i; //赋值
i=2; //直接访问i,将i赋值为2
*p1=8; //使用指针,间接访问,更新i的赋值为8

双指针交换

交换两个指针变量的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
using namespace std;
int main(){
int *p1,*p2,a,b,*t;
cin>>a>>b;
p1 = &a;
p2 = &b;
if(a<b){
t = p1; //把p1存储的地址赋值给t
/*若用*t=*p1(把p1指向的值赋值给t指向的变量),就必须要对t进行初始化,否则就会变野指针*/
p1 = p2;
p2 = t;
}
cout<<"max="<<*p1<<",min="<<*p2<<endl;

return 0;
}

交换指向变量的值

1
2
3
4
5
6
7
/*--------------------在上一步的基础上--------------------*/
int t;//t现在不是指针
if(a<b){
t=*p1;//把p1指向的值赋值给t
*p1=*p2;//把p2指向的变量的值赋给p1指向的变量
*p2=t;//把t里的值赋值给p2
}

用指针做函数参数

将一个变量的地址传送给被调用函数的形参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
using namespace std;
int main(){
void swap(int *p1,int *p2); //函数声明
int *po1,*po2,a,b;
cin>>a>>b;
po1 = &a;
po2 = &b;
if(a<b)swap(po1,po2);
cout<<"max="<<a<<"min="<<b<<endl;
return 0;
}

void swap(int *p1,int *p2)
{
int t;
//这三步的写法要想清楚,如果不带指针运算符,那这里就只会交换指针的指向而不会实际修改值
t = *p1;
*p1=*p2;
*p2=t;
}

若想通过函数调用得到n个想要改变的值:

  1. 在主调函数里设n个变量,用n个指针指向他们;
  2. 设置函数(包含n个指针形参,在函数中去该百年这n个形参的值)
  3. 在调用时,在主调函数里将这n个指针变量作为实参,将地址传给函数的形参;
  4. 在执行函数时通过形参指针变量,改变所指的n个变量的值;

[!NOTE]

不能试图通过改变形参指针变量的值而使实参指针变量的值改变;

函数的调用可以(而且只可以)得到一个返回值,

但是用指针变量作参数,可以得到多个变化了的值。

例:对输入的两个函数按大小顺序输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<iostream>
using namespace std;
int main(){
void swap(int *p1,int *p2);//函数声明
int *po1,*po2,a,b;
cin>>a>>b;
po1 = &a;
po2 = &b;
if(a<b)swap(po1,po2);
cout<<a<<b<<endl;

return 0;
}
/*这里可以直接用void来定义是因为swap函数中直接通过指针修改了原变量的值,不需要通过return返回值*/
void swap(int *p1,int *p2){
int t;
t = *p1; //把p1指向的值存到t
*p1 = *p2; //把p2指向的值赋值给p1指向的变量
*p2 = t; //把t里的a值赋值给p2指向的变量
}

数组与指针

数组元素的指针就是数组元素的地址;

在C和C++中数组名代表数组在内存中的起始地址,因此对于int a[10],*p,则p=&a[0],p=a;这两句是一样的意思;p = p +1;即表示指向数组的下一个元素

引用一个数组元素的方法:(以输出数组中的全部元素为例)

  • 下标法:a[i] 【直观】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include<iostream>
    using namespace std;
    int main(){
    int a[10];
    int i;
    for(i=0;i<10;i++)cin>>a[i];
    cout<<endl;
    for(i=0;i<10;i++)cout<<a[i]<<" "<<endl;
    return 0;
    }
  • 指针法:p=p+i; 【改变指针p的指向,适用于对数组进行遍历 速度快】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include<iostream>
    using namespace std;
    int main(){
    int a[10];
    int i,*p=a;
    for(i=0;i<10;i++)cin>>*(p+i);
    cout<<endl;
    for(p=a;p<(a+10);p++)cout<<*p<<" "<<endl; //主要从这就能看出来
    return 0;
    }
  • 地址法:* (a+i)或* (p+i)【通过计算偏移量直接找到目标位置,不改变当前p的值,适用于随机访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include<iostream>
    using namespace std;
    int main(){
    int a[10];
    int i,*p=a;
    for(i=0;i<10;i++)cin>>*(a+i); //通过数组名计算地址,也可以像上一种那样用*(p+i)
    cout<<endl;
    for(p=a;p<(a+10);p++)cout<<*(a+i)<<" "<<endl; //不移动指针p
    return 0;
    }

指针运算

指针运算符与自增/自减运算符优先级相同,按照从右向左结合

  • p++ 指针指向下一个元素
  • *p++ 等价于 *(p++) 即先将p所指的元素取出,再使p自增指向下一个元素
  • *(++p) 先将p自增1指向下一个元素,然后再取值
  • (*p)++ 取出p所指的数组元素,将其值增加1(p自身的值不变,就是地址没变呀)

号外:i +=2;的意思是先算i+2,然后把新值赋给i

字符串与指针

【例】将字符串str1复制为字符串str2

1
2
3
4
5
6
7
8
char str1[] = "I Love China",str[20],*p1;*p2;
p1 = str1;
p2 = str2;
for (;*p1!='\0';p1++,p2++)*p2 = *p1; //同位复制
*p2 = '\0'; //补结束符
p1 = str1;//指回开头
p2 = str2;
cout<<p2<<endl;//这里取得是字符串首地址,会自动从该地址输出到结束符,如果是*p2就只会输出单个字符

函数与指针

函数的入口地址就称为函数的指针;

当一个指针变量指向函数入口地址,那么可以通过这个指针变量调用函数。

  • 定义形式: 函数类型 (*指针变量名)(函数参数) 声明的时候省略

  • 在对函数指针赋值时应写成p=max;即只将函数名赋给p,因为函数名代表函数入口地址,不能写成后面带括号的,那样就变成调用了

在应用时,一个函数指针变量可以调用不同函数

1
2
3
4
5
6
7
8
int a,b,c,n;
int(*p)(int,int);
cin>>a>>b;
cin>>n;
if(n==1)p=max;
if(n==2)p=min;
c=p(a,b);
if(n==1)cout<<"max="<<c<<endl;

也可用指向函数的指针作为函数的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*这里直接用例子去理解*/
#include <iostream>
using namespace std;

// 步骤1:定义普通函数(加法、减法)—— 这是被指针指向的目标函数
int add(int a, int b) {
return a + b;
}

int sub(int a, int b) {
return a - b;
}

// 步骤2:定义接收函数指针的函数
// 形参说明:int (*func)(int, int) → 声明一个函数指针func,指向“接收两个int、返回int的函数”
int calc(int a, int b, int (*func)(int, int)) {
// 通过函数指针调用目标函数(加法/减法)
return func(a, b);
}

int main() {
int x = 10, y = 5;

// 步骤3:传递函数名(函数名就是函数的地址,等价于&add/&sub)
// 调用calc,传入add函数的地址 → 执行加法
cout << "10 + 5 = " << calc(x, y, add) << endl;

// 调用calc,传入sub函数的地址 → 执行减法
cout << "10 - 5 = " << calc(x, y, sub) << endl;

return 0;
}

void指针类型

它不指向任何具体类型的数据,只表示一个地址;

可以用强制类型转换将其转换成其它类型的指针,从而实现定义一个指针访问多种类型的数据;

它必须转换为指定一个确定类型的数据,才能访问其中的数据;

注意void 指针*:不是指向对象的类型,即这种指针不能指向一个实体。【可以把非void型的指针赋给void型指针变量,但不能把void指针直接赋给非void型指针变量,必须先强制进行类型转换】

动态分配内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*先写一个C++的例子*/
#include <iostream>
using namespace std;

int main() {
// 1. 动态申请int内存,指针p绑定(不用强制类型转换,不用查NULL)
int *p = new int; //和C相比最大的区别
// 2. 操作动态内存
*p = 200;
cout << "动态内存的值:" << *p << endl; // 输出200
// 3. 释放内存
delete p;
p = NULL;
return 0;
}

/*再写一个C的例子*/
#include <stdio.h>
#include <stdlib.h> // malloc/free的头文件

int main() {
// 1. 动态申请4字节内存(存int),返回内存首地址,用指针p绑定
int *p = (int*)malloc(sizeof(int));
if (p == NULL) { // 必做:检查申请是否成功(内存不足时返回NULL)
printf("内存申请失败\n");
return 1;
}
// 2. 通过指针操作动态内存(没有变量名,只能用*p)
*p = 100; // 往动态内存里存值100
printf("动态内存的值:%d\n", *p); // 输出100
// 3. 手动释放内存(必须!否则内存泄漏)
free(p);
p = NULL; // 防止野指针(可选,但推荐)
return 0;
}

小结

  • 区分数组指针与指针数组

  • 区分函数&指针

    int (*p)(参数): p为指向函数的指针,该函数带回一个整型值

    int *p(参数): p为返回值为指针的函数,该指针指向整型值

  • int **p: p是一个指向指针的指针

  • const int *p: p是int型的指针常量,其指向的目标不能改变【等价于int const *p】

  • int * const p: p是int型的常量指针

  • p2 = p1 = &a; //将变量a的地址赋给p1,再将 p1的值赋给p2

  • 指针变量可以有空值,即该指针变量不指向任何变量【int *p = nullptr;】

    若NULL指针不指向具体对象,则它仍是空指针,此时,解引用 *q会触发未定义行为,程序必然崩溃;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int main()
    {
    int y=2;
    int *q=NULL;
    q = &y; //不在这里指明具体对象就会触发未定义行为
    *q = 6; //如果没有上面一步,就会提示在给未知空间进行赋值
    cout << *q << endl;
    return 0;
    }
    /*
    因此对于空指针的使用流程即:初始化为空指针→指向具体对象(通过取地址符获取相应的内存地址并赋值给指针)→安全解引用(就是用*这个操作符,这样就可以修改内存中的数据了)
    */

两个指针相减常用于数组中,这样结果即为两个指针间的元素个数,也可以用于比较(比如前一个元素小于后一个元素这样),但若两个指针指向不同的数组比较就没有意义。