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()
函数中运行,两个变量的值,似乎又变回去了。
为什么会这样呢?
我们说过, 函数调用时,实参传递给形参的是他们的值,而不是他们本身。 也就是就, num1
和 num2
仅仅是值为 10 和 20 的两个 int
类型的变量而已,它们和已经与 a
和 b
没有关系了。
那我们如何才能让两个变量,真正地在函数中被交换呢?这就需要指针的参与了。
9.1 什么是指针?
让我们回顾一下我们如何理解 变量 和 变量名 。
我们可以把电脑的内存,看作一栋大楼,大楼中有大大小小的房间,房间中可以住不同类型的住户。那么,这些房间就是 变量 ,房间可以住的住户类型就是 变量类型 ,比如:供一个人居住的单人间,供两个人居住的双人间,甚至是供一个公司住的超大房间。而房间中的具体住户就是 变量的值 。那么,我们假设有一名住户,叫 “刘禹锡” ,他的房间有个名字,叫 “陋室” ,那么 “陋室” 这个名字,就可以代指这个房间。或者更常见的情况,“XX级XX班”、“XX系办公室”等等,这种用于指代一个变量的名字,这就是 变量名 。
那我们什么是 指针 呢?
我们在日常生活中,想要找到一个房间,除了可以通过这个房间叫什么名字,即变量名来找之外,我们常常使用房间的地址、门牌号来找。 而内存空间的每个内存单元也是有地址的,一个变量的地址,被称为这个变量的指针。
而用于存储一个对象的内存地址的变量,叫做指针变量。
9.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:我们可以通过 * 运算符,对指针变量解引用,可以操作指针指向的内存。
说明:
- 野指针: 指针变量未赋值时,其值是不确定的,即指向的内存空间是不确定的。此时,对其进行“指向”操作,后果是难以预计的。
- 空指针: 给指针变量赋值0或NULL,可使指针不指向任何变量。
- 指针变量只能接收相同类型的变量的地址。如
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修饰指针
三种情况:
-
指针常量:指针是常量,即不可修改指针指向的内存空间,但可以修改指针指向的内存空间的数据(不能是常量)。
类型名 * const 指针变量名;
-
指向常量的指针变量:指针指向的内存空间中的数据不可修改(不一定是常量),但指针可以改为指向其他常量内存地址。
const 类型名 * 指针变量名;
-
指向常量的指针常量:指针不可更换其指向的地址,对应内存空间中的数据也不可修改。
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];
说明:
- 内存空间是有限的,如果需要的内存空间过大(一般是特别长的数组),超过了电脑可以分配的极限,此时将得到一个空指针(NULL)。如果需要创建特别大的数组,最好在创建后,判断是否创建成功。
- 用 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;
}
说明:
- 指针可以与整数进行加减操作。 例如,指针 p 指向数组元素
a[0]
,那么 (p+1) 指向数组元素a[1]
。即 (p+1) 中的 1 指的是一个单位(变量类型所占内存大小),而不是内存地址中的一字节。 - 因为实际上数组名就是数组首元素的指针,所以 数组名和指向数组首元素的指针有相似的用法 。例如,我们想要表示数组中第 i 个元素,可以用
a[i]
,也可以用p[i]
;可以用*(p+i)
,也可以用*(a+i)
。 - 指针可以进行大小比较,比较的是指向地址位置的大小。 如:
p = a; q = a + 9
,那么q > p
。 - 指针可以与指针相减,得到两指针地址间的差值。但两个指针之间不可以相加。 如在第三点中,
q - p
的值为 9 。 *
、++
和--
,三种运算符同一优先级,默认从右向左算。
示例: (使用断点分析代码执行步骤)
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 通过指针在函数间传递一维数组
指针可以作为函数的参数,而数组名其实就是指针,所以可以向函数传递数组。
三种使用方式:
void inverse(int * p, int n)
void inverse(int p[10], int n)
- 因为形参仅是数组首地址,不关心数组长度,因此可以省略数组长度:
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;