9 指针

9.0 为什么我们需要指针

我们已经学习过了函数,现在我们思考这样一个案例:

使用函数,交换两个函数外的变量的值

我们可以写出类似下面的解决方案:

void swap(int num1, int num2)
{
	cout << "交换前:" << endl;
	cout << "num1 = " << num1 << endl;
	cout << "num2 = " << num2 << endl;
 
	int temp = num1;
	num1 = num2;
	num2 = temp;
 
	cout << "交换后:" << endl;
	cout << "num1 = " << num1 << endl;
	cout << "num2 = " << num2 << endl;
 
	//return ; 当void函数声明时候,不需要返回值,可以不写return
}
 
int main() {
 
	int a = 10;
	int b = 20;
 
	swap(a, b);
 
	cout << "main中的 a = " << a << endl;
	cout << "main中的 b = " << b << endl;
 
	return 0;
}

程序输出如下:

交换前:
num1 = 10
num2 = 20
交换后:
num1 = 20
num2 = 10
main中的 a = 10
main中的 b = 20

从程序的输出,我们可以看出,在 swap() 函数中,我们确实成功地对两个变量的值,进行了交换,但当程序重新回到 main() 函数中运行,两个变量的值,似乎又变回去了。

为什么会这样呢?

我们说过, 函数调用时,实参传递给形参的是他们的值,而不是他们本身。 也就是就, num1num2 仅仅是值为 10 和 20 的两个 int 类型的变量而已,它们和已经与 ab 没有关系了。

内存发布图

那我们如何才能让两个变量,真正地在函数中被交换呢?这就需要指针的参与了。

9.1 什么是指针?

让我们回顾一下我们如何理解 变量变量名

我们可以把电脑的内存,看作一栋大楼,大楼中有大大小小的房间,房间中可以住不同类型的住户。那么,这些房间就是 变量 ,房间可以住的住户类型就是 变量类型 ,比如:供一个人居住的单人间,供两个人居住的双人间,甚至是供一个公司住的超大房间。而房间中的具体住户就是 变量的值 。那么,我们假设有一名住户,叫 “刘禹锡” ,他的房间有个名字,叫 “陋室” ,那么 “陋室” 这个名字,就可以代指这个房间。或者更常见的情况,“XX级XX班”、“XX系办公室”等等,这种用于指代一个变量的名字,这就是 变量名

那我们什么是 指针 呢?

我们在日常生活中,想要找到一个房间,除了可以通过这个房间叫什么名字,即变量名来找之外,我们常常使用房间的地址、门牌号来找。 而内存空间的每个内存单元也是有地址的,一个变量的地址,被称为这个变量的指针。

而用于存储一个对象的内存地址的变量,叫做指针变量。

9.2 指针变量的定义和使用

指针变量定义语法: 数据类型 * 指针变量名;

指针变量赋值语法: 指针变量名 = &变量名;

指针变量使用语法: *指针变量名;

两种新运算符:

  1. 取地址运算符 &
  2. 间接寻址运算符 *

示例:

int main() {
	int a = 10;             //定义整型变量a
	
	//指针定义语法: 数据类型 * 变量名 ;
	int* p;

	//指针变量赋值
	p = &a;                  //指针指向变量a的地址
	cout << &a << endl;      //打印数据a的地址
	cout << p << endl;       //打印指针变量p

	//通过*操作指针变量指向的内存
	cout << "*p = " << *p << endl;

	return 0;
}

总结1:定义指针时的 * 运算符,仅仅是一个类型说明符,表明变量是指针型变量。
总结2:定义指针时的数据类型,是指针变量指向的内存空间对应的对象的类型。
总结3:我们可以通过 & 运算符,获取变量的地址。
总结4:我们可以通过 * 运算符,对指针变量解引用,可以操作指针指向的内存。

说明:

  1. 野指针: 指针变量未赋值时,其值是不确定的,即指向的内存空间是不确定的。此时,对其进行“指向”操作,后果是难以预计的。
  2. 空指针: 给指针变量赋值0或NULL,可使指针不指向任何变量。
  3. 指针变量只能接收相同类型的变量的地址。如 int * p 只能接收 int 型变量的地址。

9.3 指针变量作为函数参数

普通变量可以作为函数的参数,而指针变量也可以作为函数的参数。

还记得我们这节课最开始时遇到的问题吗?现在,我们是不是已经可以轻松解决它了呢?

之前我们向 swap() 函数传入的是两个变量的值,现在我们向 swap() 函数传入的是两个变量的指针,不就可以操作函数外的那两个变量了吗?

用指针时的内存示意图

示例: 使用函数,交换两个函数外的变量的值。

#include<iostream>

using namespace std;

void swap(int* num1, int* num2)
{
    cout << "交换前:" << endl;
    cout << "num1 = " << *num1 << endl;
    cout << "num2 = " << *num2 << endl;

    int temp = *num1;
    *num1 = *num2;
    *num2 = temp;

    cout << "交换后:" << endl;
    cout << "num1 = " << *num1 << endl;
    cout << "num2 = " << *num2 << endl;
}

int main() {

    int a = 10;
    int b = 20;

    swap(&a, &b);

    cout << "main中的 a = " << a << endl;
    cout << "main中的 b = " << b << endl;

    return 0;
}

交换前:
num1 = 10
num2 = 20
交换后:
num1 = 20
num2 = 10
main中的 a = 20
main中的 b = 10

现在,我们的程序已经可以正常完成它的功能了。

9.4 const修饰指针

三种情况:

  1. 指针常量:指针是常量,即不可修改指针指向的内存空间,但可以修改指针指向的内存空间的数据(不能是常量)。

    类型名 * const 指针变量名;

  2. 指向常量的指针变量:指针指向的内存空间中的数据不可修改(不一定是常量),但指针可以改为指向其他常量内存地址。

    const 类型名 * 指针变量名;

  3. 指向常量的指针常量:指针不可更换其指向的地址,对应内存空间中的数据也不可修改。

    const 类型名 * const 指针变量名;

示例:

int main() {
    int a = 10;
    int b = 20;
    const int c = 30;
    const int d = 40;

    //指针常量
    int *const p1 = &a;
    *p1 = 40;       // 正确
    // p1 = &b;     // 错误,指针常量不能更改指向的地址
    // p1 = &c;     // 错误,必须使用指向常量的指针,才能指向常量

    //指向常量的指针变量
    const int *p2 = &c;
    // *p2 = 40;    // 错误,不可以通过指向常量的指针修改指向的内存空间的数据。
    p2 = &a;        // 正确,指向常量的指针可以指向变量
    // *p2 = 40;    // 错误,不可以通过指向常量的指针修改指向的内存空间的数据。
    p2 = &d;        // 正确

    //指向常量的指针常量
    const int *const p3 = &c;
    // p3 = &d;      //错误,不可更改指针常量指向的地址
    // *p3 = 10;     //错误,不可以通过指向常量的指针修改指向的内存空间的数据。

    return 0;
}

9.5 指针作为函数返回值(指针型函数)

语法:

类型说明符 * 函数名(参数列表){
    函数体
}

示例: 利用指针型函数,取两数中的最大值。

#include <iostream>

using namespace std;

int *max(int *p1, int *p2) {
    return *p1 > *p2 ? p1 : p2;
}

int main() {
    int a = 10, b = 25;
    cout << *max(&a, &b) << endl;
    return 0;
}

警告:使用指针作为函数返回值时,应该注意变量的生命周期,不可将指针型函数内部定义的变量的指针返回。因为在函数执行完毕后,此函数内定义的数据将被系统释放,不再能被程序引用,否则会发生内存错误。这种指针也是 野指针 ,也被称为 迷途指针悬垂指针

9.6 函数指针变量

在C++中,数组是占用一段连续内存区域的数据,数组名是此内存空间的起始地址。与之相似,函数其实是占用一段连续内存空间的指令,函数名是该内存空间的首地址。因此,我们可以联想到,我们应该可以使用指针,间接访问一个函数。

定义: 类型标识符 (* 指针变量名)(参数列表);

赋值: 指针变量名 = 函数名;

使用: 指针变量名(实参列表)(*指针变量名)(实参列表)

示例:

#include<iostream>

using namespace std;

int add(int a, int b) {
    return a+b;
}

int main() {
    int a = 10, b=15;
    int (* p)(int,int);
    // 注意不能写括号
    p= add;
    // 两种写法都可以
    cout << p(a,b) << endl;
    cout << (*p)(a,b) << endl;
    return 0;
}

9.7 函数指针变量作为函数参数

与普通变量一样,函数指针变量也可以作为函数的参数。

示例: 使用二分法求不同函数的解。

#include <iostream>
#include <cmath>

using namespace std;

// 以下是三个待求解的方程
double f1(double x){
    return x*x*x + x*x -3*x+1;
}

double f2(double x){
    return x*x-2*x-8;
}

double f3(double x){
    return x*x*x + 2*x*x +2*x+1;
}

// 二分法计算方程的解
double divide(double (* p)(double ), double x1, double x2){
    double x0;
    do {
        x0 = (x1 +x2)/2;
        if (p(x1) * p(x0)>0){
            x1= x0;
        } else{
            x2=x0;
        }
    } while (fabs(p(x0))>1e-6);
    return x0;
}

int main() {
    cout << "f1方程的解为:"<< divide(f1,-2,2)<<endl;
    cout << "f2方程的解为:"<< divide(f2,-3,3)<<endl;
    cout << "f3方程的解为:"<< divide(f3,-2,3)<<endl;
    return 0;
}

9.8 动态存储分配

在我们之前的写的代码中,我们使用的所有变量(包括数组),编译器都明确知道这个变量占存储空间大小,这种定义变量的方式叫做 静态分配内存

静态分配的存储空间,会在变量(或数组)的生命周期结束时,自动释放。这就避免了程序员忘记回收分配出去的内存空间,导致 内存泄漏 ,进而导致电脑运行卡顿,甚至崩溃的可能。

但缺点也很明显。静态分配的数组,数组长度必须是常量,而这很不方便。例如:每个班级的人数不一致,如果我们使用静态分配的数组,我们要么需要为适配不同班级而去修改程序代码,很麻烦;要么以人数最多的班级为准,而这又会造成内存空间的浪费。另外,静态分配内存在栈中,栈空间相对较小,我们无法在其中创建较大数组(例如长度数千的数组)。

要解决这些问题,我们就要用到 动态存储分配 。动态存储分配需要使用 new 运算符。

普通变量使用语法: 指针变量名 = new 类型名(初值);

int * p;
p = new int(1);

数组变量使用语法: 指针变量名 = new 类型名[元素个数];

int * p;
p = new int[100];

说明:

  1. 内存空间是有限的,如果需要的内存空间过大(一般是特别长的数组),超过了电脑可以分配的极限,此时将得到一个空指针(NULL)。如果需要创建特别大的数组,最好在创建后,判断是否创建成功。
  2. 用 new 运算符分配的内存空间,存放在堆中,系统不会自动释放不需要的变量,程序员必须在这些变量使用完毕后将其销毁,不然会导致电脑运行缓慢,甚至电脑崩溃等严重后果!!!

释放普通变量语法: delete 指针名;

释放数组语法: delete []指针名;

9.9 指针和一维数组

数组,在内存中,是一块若干相同类型数据相连而形成的内存区域,而数组名,就是这块内存区域的起始地址。

而指针,就是存储内存区域地址的变量。因此,实际上数组名就是数组首元素的指针。

指针指向数组首地址的语法:

指针变量名 = 数组名;指针变量名 = &数组名[0];

示例:

int main() {

	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };

	int * p = arr;  //指向数组的指针
	//p = &arr[0];  //与上一行意义相同

	cout << "第一个元素: " << arr[0] << endl;
	cout << "指针访问第一个元素: " << *p << endl;

	for (int i = 0; i < 10; i++)
	{
		//利用指针遍历数组
		cout << *p << endl;
		p++;
	}

	return 0;
}

说明:

  1. 指针可以与整数进行加减操作。 例如,指针 p 指向数组元素 a[0] ,那么 (p+1) 指向数组元素 a[1] 。即 (p+1) 中的 1 指的是一个单位(变量类型所占内存大小),而不是内存地址中的一字节。
  2. 因为实际上数组名就是数组首元素的指针,所以 数组名和指向数组首元素的指针有相似的用法 。例如,我们想要表示数组中第 i 个元素,可以用 a[i] ,也可以用 p[i] ;可以用 *(p+i) ,也可以用 *(a+i)
  3. 指针可以进行大小比较,比较的是指向地址位置的大小。 如:p = a; q = a + 9 ,那么 q > p
  4. 指针可以与指针相减,得到两指针地址间的差值。但两个指针之间不可以相加。 如在第三点中, q - p 的值为 9 。
  5. *++-- ,三种运算符同一优先级,默认从右向左算。

示例: (使用断点分析代码执行步骤)

int main() {
    int a[4] = { 1,2,3,4 };
    int * p = a;
    cout << *p++ << endl;
    cout << (*p)++ << endl;
    cout << ++*p << endl;
    cout << ++(*p) << endl;
    cout << *++p << endl;
    cout << *(++p) << endl;

    return 0;
}

9.10 通过指针在函数间传递一维数组

指针可以作为函数的参数,而数组名其实就是指针,所以可以向函数传递数组。

三种使用方式:

  1. void inverse(int * p, int n)
  2. void inverse(int p[10], int n)
  3. 因为形参仅是数组首地址,不关心数组长度,因此可以省略数组长度: void inverse(int p[], int n)

9.11 指针与二维数组

9.12 指针数组

常规变量可以组成数组,而指针变量,作为变量也可以组成数组,这被称为 指针数组

定义语法: 类型名 * 数组名[数组长度];

指针数组常用来指向若干个字符串,使字符串的处理更加方便灵活。

示例:

char * week[] = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"};

9.13 指向指针的指针

指针也是存在内存当中的,那么和普通变量一样,也可以用指针指向指针。

定义语法: 类型标识符 ** 指针变量名;

示例:

int x = 8;
int *p = &x;
int **ptr = &p;