指针
一、指针的基本概念
1、变量的地址
变量是内存变量的简称,在C++中,每定义一个变量,系统就会给变量分配一块内存,内存是有地址的。
C++用运算符&获取变量在内存的起始地址。地址常用十六进制表示
语法:&变量名。
例:
#include <iostream>
using namespace std;
int main()
{
int a;
char b;
cout << "变量a的地址是" << &a << endl;
cout << "变量b的地址是" << &b << endl;
//正确表示为:
//cout << "变量a的地址是" << (long long)&a << endl;
//cout << "变量b的地址是" << (long long)&b << endl;
return 0;
}
运行结果如下:
变量a的地址是000000AD2CCFFBC4
变量b的地址是烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫?^
原因:int整型变量a能正确输出内存地址。
char字符型变量不能正确输出:cout有bug,显示字符型变量的地址会把它当成字符串来显示。
其他的数据类型(bool、float等)均能正常表示。
2、指针变量
指针变量简称指针,它是一种特殊的变量,专用于存放变量在内存中的起始地址。
语法:数据类型* 变量名 或 数据类型 *变量名。
数据类型必须是合法的C++数据类型(int、char、double或其他自定义的数据类型)。整型指针、字符型指针。
3、对指针赋值
不管是整型、字符型、浮点型,还是其他自定义数据类型的变量,它的地址都是一个十六进制数。用整型指针存放整型变量的地址;用字符型指针存放字符型变量的地址等等。
语法:指针 = &变量名。
#include <iostream>
using namespace std;
int main()
{
int a;
cout << "变量a的地址是" << (long long)&a << endl;
int* pa = &a;
cout << "变量a的地址是" << (long long)pa << endl;
return 0;
}
运行结果为:
变量a的地址是148649343220
变量a的地址是148649343220
注意:
- 对指针的赋值操作也通俗的被称为”指向某变量“,被指向的变量的数据类型被称为”基类型“。
如int* pa = &a; 可解释为:让指针pa指向a,pa的基类型是int。 - 如果指针的数据类型与基类型不符,编译会出现警告。但是,可以强制转换它们的类型。
4、指针占用的内存
指针也是变量,是变量就要占用内存空间。
在64位的操作系统中,不管是什么类型的指针,占用的内存都是8字节。
在32位的操作系统中,不管是什么类型的指针,占用的内存都是4字节。
注解: 在C++中,指针是复合数据类型,复合数据类型是指基于其它类型而定义的数据类型,在程序中,int是整型类型,int* 是整型指针类型,int* 可以用于声明变量,可以用于sizeof运算符,可以用于数据类型的强制转换,总的来说,把int*当成一种数据类型就是了。
二、使用指针
- 声明指针变量后,在没有赋值之前,是不能使用指针!乱码!
- 指针变量存放的是变量的地址,因此,指针名表示的是地址(就像变量名可以表示变量的值一样)。例如pa是指针,*pa不是指针;int *pa= 8。(大部分同学会弄错)
- *运算符被称为间接值或解除引用(解引值)运算符,将它用于指针,可以得到存放在该地址的变量的值。
- 变量和指向变量的指针是统一枚硬币的两面。
int a = 8;
int *pa = &a;
a与*pa的值都是8,pa与&a的值都是变量a的地址。
- 程序在存储数据的时候,必须跟踪这三种基本属性:
- 数据存储在哪里;
- 存储数据的类型;
- 数据的值是多少。
用两种策略可以达到以上目的:
- 声明一个普通变量,声明时指出数据类型和变量名(符号名),系统在内部跟踪该内存单元。
- 声明一个指针变量,指针存储的是变量的地址,而不是值本身,程序可利用*解除运算符访问或者修改变量。
三、指针用于函数的参数
1、 值传递
值传递:函数的形参是普通变量。例 函数为:void function1(int a){ 内容 };
2、 地址传递
地址传递:如果把函数的形参声明为指针,调用的时候就可以把实参的地址传进去,形参中存放的是实参的地址,在函数中可以通过解引用符号*直接操作内存数据的值,进而改变实参的值,这种方法通常被称为地址传递或传地址。
例如:在main主函数中 function1 (&a);
函数为 void function1 (int * pa){ 函数体 } 。
3、 地址传递的意义
- 可以在函数中修改实参的值;
- 减少内存拷贝,提升性能;(任何一个指针变量只占用8个字节,但是如果是一般值传递的函数则会根据数据类型的不同,系统分配对应的内存空间)
- 一般函数只能返回一个值,由于函数运行结束后,内部变量也会随之消失,不会返回两个或者更多值。
如下程序是找出三个值的最大、最小值,并输出。
#include <iostream>
using namespace std;
void funct1(int x, int y, int z, int* max, int* min)
{
*max = x >= y ? x : y;
*min = x < y ? x : y;
*max = *max >= z ? *max : z;
*min = *min < z ? *min : z;
}
int main()
{
int a = 2, b = 5, c = 8, m, n;
funct1(a, b, c, &m, &n);
cout << "最大值为:" << m << "\n" << "最小值为:" << n << endl;
return 0;
}
分析:函数的前三个变量x、y、z为普通的值传递,max、min为地址传递。max存放的是变量m的地址,min存放的是变量n的地址,可通过解除引用符*来修改main函数中对应实参的值。
运行结果如下:
最大值为:8
最小值为:2
四、用const修饰指针
有三种类型:常量指针、指针常量、常指针常量,其中需重点掌握常量指针。
1、 常量指针
语法:const 数据类型* 指针;如 const int* pt;
不能通过解引用的方法来修改指向变量的值,但可以更改指向的变量。
注意:
- 指向的变量可以改变。
(例:const int* pt=&a; pt=&b;一开始pt指向变量a的地址,之后指向为b的地址); - 一般用于函数的形参,表示不希望在函数中修改main函数中实参的值。
(例:void funct(const int *a,const int * b) { 函数内容}); - 如果用于形参,虽然指向的对象可以改变,但这并没有意义;
- 如果形参的值不希望被改变,建议加上const修饰,程序可读性会更好。
2、 指针常量
语法:数据类型 *const 指针;如int *const pt=&a。
不能改变指向的对象(变量名),但可以利用解引用运算符 * 修改存放在内存地址的值。
注意:
- 在定义的同时,必须进行初始化,否则会报错。这是因为指针常量与变量一开始就”锁死“。
- 可以通过解引用的方法修改指向对象的值。
- C++编译器把指针常量做了特殊的处理,改头换面后,有一个新的名字,叫引用。
3、 常指针常量
语法:const 数据类型 *const 指针;
指向的对象不能改变,也不能通过解引用运算符 * 来改变内存空间的值。
五、 二级指针
1. 定义:
二级指针也属于指针。
指针是指针变量的简称,也是变量,是变量就会在内存中分配地址。
指针用于存放普通变量的地址;
2. 用法:
二级指针用于存放一级指针的地址。
3. 语法:
声明二级指针的语法:数据类型** 指针名。
4. 目的:
使用指针的目的:
- 传递地址;
- 存放动态分配的内存变量的地址。
4. 注意事项:
- 在函数中,如果传递普通变量的地址,形参用指针;如果传递指针的地址,形参用二级指针。
- 把普通变量的地址传入函数中,可以通过指针来操作主函数main实参变量的值;
若把指针的地址传入函数中,可以通过二级指针来操作main函数一级指针(指针)的值。
例:
#include <iostream>
using namespace std;
void fun(int **pp)
{
cout << "二级指针pp的地址:" << pp << ", * pp的值为:" << *pp << endl;
*pp = new int(9);
cout << "二级指针pp的地址:" << pp << ", * pp的值为:" << *pp << endl;
cout << "**pp的值为" << **pp << endl;
}
int main()
{
int* p=0;//指针为空指针,指向0或者NULL,可看下一章节内容
cout << "p的值为:" << p << endl;//p存放的是0的地址
fun(&p);
/* {
int** pp = &p;
cout << "二级指针pp的地址:" << pp << ", * pp的值为:" << *pp << endl;
*pp = new int(9);
cout << "二级指针pp的地址:" << pp << ", * pp的值为:" << *pp << endl;
}*/
cout << "一级指针p的地址: " << p << ", * p的值为: " << *p << endl;
return 0;
}
运行结果如下:
p的值为:0000000000000000
二级指针pp的地址:000000593DCFFCD8, * pp的值为:0000000000000000
二级指针pp的地址:000000593DCFFCD8, * pp的值为:000001C756A20850
**pp的值为9
一级指针p的地址: 000001C756A20850, * p的值为: 9
分析:
若想在函数中修改主函数main的实参,须在main中传递指针的地址(&p),函数中形参用二级指针(**pp)。
*pp存放的是p的地址(*pp== p);
**pp 与 *p存放的值相同,为变量的值(**pp==*p==变量);
pp存放的是 *p的地址(pp == * p );
p存放的是变量的地址(p ==&变量)。(括号的内容是为了便于理解)
六、空指针
1、定义
在C/C++中,用0或NULL都可以表示空指针。
声明指针后,在赋值之前,让它指向空,表示没有指向任何地址。
2、使用空指针的后果:
- 如果对空指针进行解引用,程序会崩溃;
- 如果对空指针使用delete运算符,系统会忽略此操作,不会出现异常。所以,内存被释放后,也应把指针指向空。
- 在函数中,应该有判断形参是否为空指针的代码,目的是保证程序的健壮性。
例:
#include <iostream>
using namespace std;
void fun(int* m,string* str )
{
if ((m == 0) || (str == 0)) return;
cout << "有请" << m << "号" << str << "闪亮登场" << endl;
}
int main()
{
int* a = 0;
string* name = 0;
fun(a,name);
return 0;
}
运行结果如下:
C:\code\day\x64\Debug\day.exe (进程 12804)已退出,代码为 0。
按任意键关闭此窗口. . .
分析:第一行输出为空,第二行的“代码为0”表示程序正常退出。如果代码为其余值,则说明程序奔溃退出。
3、C++11的nullptr
用0或NULL表示空指针可能会产生歧义,C++11建议用nullptr表示空指针,也就是(void*)0。(int* a = nullptr;)
注意:在Linux平台下,如果使用nullptr,编译需要加-std=c++11参数。
七、野指针
1、定义:
野指针就是指针指向的不是一个有效(合法)的地址。
在程序中,如果访问野指针,可能会造成程序的奔溃。
2、出现原因:
- 指针在定义的时候,如果没有进行初始化,它的值是不确定的;
- 如果用指针指向了动态分配的内存,内存被释放后,指针不会指空,但是,指向的地址已失效;
#include <iostream>
using namespace std;
int main()
{
int* p = new int(8);
cout << "指针p指向的地址:" << p << endl;
delete p;
cout << "指针p指向的地址:" << p << endl;
return 0;
}
运行结果如下:
指针p指向的地址:000001AA2D91DE30
指针p指向的地址:0000000000008123
C:\code\day\x64\Debug\day.exe (进程 25380)已退出,代码为 0。
按任意键关闭此窗口
- 指针指向的变量已超越变量的作用域(变量的内存空间已被系统回收)。如果让指针指向了函数的局部变量,或者把函数的局部变量的地址作为返回值赋给了指针都属于这种情况。
#include <iostream>
using namespace std;
int* fun()
{
int a = 8;
cout << "a的地址:" << &a <<" a的值为:"<<a << endl;
return &a;
}
int main()
{
int* p = fun();
cout << "p的地址:" << p << " *p的值为:" << *p << endl;
return 0;
}
运行结果如下:
a的地址:000000D07DEFF7D4 a的值为:8
p的地址:000000D07DEFF7D4 *p的值为:-858993460
C:\code\day\x64\Debug\day.exe (进程 18660)已退出,代码为 0。
按任意键关闭此窗口. . .
分析:指针还是存放的是变量a的地址,但是对指针解引用就不是变量a的值了!
3、规避方法:
- 指针在定义的时候,如果没地方指向,就初始化nullptr;(int* p=nullptr)
- 动态分配的内存被释放后,将其只指向为空指针;
- 函数不要返回局部变量的地址。
注意:野指针的危害比空指针要大得多,在程序中,如果访问野指针,可能在程序的崩溃,是可能,不是一定,程序的表现是不稳定,增加了调试程序的难度。
八、函数指针和回调函数
1、函数指针
函数的二进制代码存放在内存四区的代码段,函数的地址是它存放在内存的起始地址,如果把函数的地址作为参数传递给另一个函数,就可以在该函数灵活调用其他函数。
使用函数指针的三个步骤:
- 声明函数指针;
- 让函数指针指向被调函数的地址;
- 通过函数指针调用函数。
int (*pfa)(int,string);//声明函数指针
pfa=fun1; //让函数指针指向被调函数的地址
pfa(a,str1); //通过函数指针调用函数
1) 声明函数指针;
声明函数指针时,必须提供指针的类型。同样,声明函数指针时,也必须提供函数类型,函数的类型是返回值(数据类型)和参数列表(形参类型和数量),函数名和形参名不是。
假设函数的原型是:
int fun1(int a,string str1);
int fun2(int b,string str2);
int fun1(int c,string str3);
bool fun4(int a,string str);
bool fun5(int a);
//前三个函数类型同属一种,第四个一种,第五个一种。
对应的三种函数指针的声明为:
int (*pfa)(int,string)
bool (*pfb)(int,string)
bool (*pfc)(int)
//pfa、pfb、pfc为函数指针名,可随便起名
2) 让函数指针指向被调函数的地址
pfa=fun1; //让函数指针指向被调函数的地址
3) 通过函数指针调用函数
pfa(a,str1); //通过函数指针调用函数
2、回调函数
在函数中调用另一个函数,称为函数的回调;
被调函数称为回调函数。回调函数就是把一个函数嵌入到另一个函数中。
调用者函数提供了主体的流程和框架,具体的功能可由回调函数实现。
只需关心回调函数的种类,不关心回调函数的功能。
#include <iostream>
using namespace std;
void mine()
{
cout << "先来一个小目标" << endl;
}
void you()
{
cout << "一边赚钱,一边享受" << endl;
}
void show(void (*pt)())
{
cout << "年轻努力赚钱" << endl;//流程之前的准备工作
pt(); //个性化部分,因人而异
cout << "最后发现钱没赚找,还不年轻了" << endl;//流程之后的收尾工作
}
int main()
{
show(mine);
cout << endl;
show(you);
return 0;
}
运行结果如下:
年轻努力赚钱
先来一个小目标
最后发现钱没赚找,还不年轻了
年轻努力赚钱
一边赚钱,一边享受
最后发现钱没赚找,还不年轻了
C:\code\day\x64\Debug\day.exe (进程 19040)已退出,代码为 0。
按任意键关闭此窗口. . .
分析:函数mine()和you()是个性化部分;函数show()为通用模板,里面需填写个性内容,才算完整。
注意:
- 一个函数(a)不可能直接调用另一个函数(b),这是因为在写a函数时,根本不知道a函数会被谁调用,也不知道函数b的函数名,这是两方面的原因;
- 在C++中,通过函数传递不同的参数可以让函数实现不同的功能,这仅仅适用于简单函数,对于复杂函数只能传递函数的地址,这就用到函数指针了。
给回调函数传递参数的形式:
- 由调用者提供实参(在调用函数增加)
void mine(int m)
{
cout << m << endl;
}
void show(void (*pt)(int))
{
int a = 3;
pt(3);
}
运行结果如下:
3
C:\code\day\x64\Debug\day.exe (进程 17812)已退出,代码为 0。
按任意键关闭此窗口. . .
- 把实参从外面传进去(在main函数中增加)
void mine(int m)
{
cout << m << endl;
}
void show(void (*pt)(int),int a)
{
pt(a);
}
int main()
{
show(mine,8);
return 0;
}
运行结果如下:
8
C:\code\day\x64\Debug\day.exe (进程 29956)已退出,代码为 0。
按任意键关闭此窗口. . .