前言
C++语言之所以强大和灵活,很大一部分体现在其灵活的指针运用上。指针是C++语言的灵魂。但是指针的灵活性也导致了其难以控制,有很多bug也是基于指针而产生的。
首先来说明变量的地址:变量是内存变量的简称,在C++中,每定义一个变量,系统就会给变量分配一块内存,内存是有地址的。(我们将内存分为小小的内存单元,每个内存单元为一个字节,又为每个字节规定一个编号,这个编号就是地址)
指针的基本概念
指针是一个变量,一个特殊变量,用来存放一个变量在内存中的起始地址。在C++中,每定义一个变量,系统会分配一块内存
对于指针我们关心这四个概念:指针类型、指针所指向变量的类型、指针的值或者叫做所指向的内存区,指针本身所占用的内存区。
指针的定义语法:数据类型 *变量名;
其中数据类型指的是指针变量所指向的变量的数据类型,*表示这是一个指针变量,变量名表示的是指针变量的名字。
例子:int *p1;//表示p1是一个指针变量,它的值是某个整型变量的地址
指针类型
指针也是变量,是变量就会占用内存空间。指针变量的类型分为整形指针、字符指针、数组指针等等。在64位的操作系统中,不管什么类型的指针,占用的内存都是8字节。32位是4个字节。
由于指针是一种独立的变量类型,它存储的值是内存地址,所以指针也是有类型的,指针的类型是指指针本身的类型。从语法角度看,把指针声明语句中的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。
int *ptr; //指针的类型是int *
char *ptr; //指针的类型是char *
int **ptr; //指针的类型是 int **
int (*ptr)[3]; //指针的类型是 int(*)[3]
int *(*ptr)[4]; //指针的类型是 int *(*)[4]
划分指针类型的原因:
- 指针的类型决定了指针在被解引用时所访问的权限。
- 指针的类型决定了指针向前或者向后退一步走的“距离”
指针所指向的类型
知道指针所指向的数据类型的作用:有一个变量在内存中,系统不会记录在内存中的开始位置和结束位置,在程序中变量的地址是变量在内存中的起始位置,再知道变量占用的内存空间就可以准确定位变量的位置,总的来说数据类型决定了占用内存的大小,决定了系统如何操作数据。
从语法上看,你只需把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例如:
int *ptr; //指针所指向的类型是int
char *ptr; //指针所指向的的类型是char
int **ptr; //指针所指向的的类型是 int *
int (*ptr)[3]; //指针所指向的的类型是 int()[3]
int *(*ptr)[4]; //指针所指向的的类型是 int *()[4]
指针的值
指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为32位程序里内存地址全都是32位长。
指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。
指针所指向的内存区和指针所指向的类型是两个完全不同的概念。声明指针后,指针所指向的类型已经有了,但如果指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。
在这里要记住,指针的值就是指针的所指向的内存地址的首地址,而指针的指向则是以XX为首地址的一片内存区域。
指针本身所占用的内存
在64位的操作系统中,不管什么类型的指针,占用的内存都是8字节。32位是4个字节。
不管什么类型的变量,它们的地址都是一个十六进制的数。
指针的使用
声明指针变量之后,在没有赋值之前,里面是乱七八糟的值,不能使用指针。
指针存放变量的地址,因此指针名表示的是地址。
指针使用的语法:指针名=&变量名 //&表示的是取地址
例子:
int* p;
int a = 3;
p = &a;
cout << "p=" << p << endl;
指针用于函数的参数
指针用于函数的参数即把变量地址作为参数传给函数。
把变量的地址作为参数传给函数的优点是:
- 在函数中可以通过解引用方式修改实参的值。
*运算符被称为解引用运算符
- 减少内存的拷贝,提高效率。
解引用:*p //p是指针名
值传递:在函数中修改形参值,不能改变实参。
用const修饰指针
用const修饰指针有三种:
-
常量指针
const 数据类型 *变量名
常量指针不能通过解引用方法修改内存地址中的值(但可通过原始变量名修改),常量指针指向的变量可改变。 -
指针常量
数据类型 const *变量名
指针常量要赋初值,其指向对象不可改变。一般用于修饰函数形参,表示希望在函数只修改内存地址中的值。 -
常指针常量
const 数据类型 const *变量名
常指针常量即不能通过解引用的方法修改内存地址中的值,其指向对象也不可改变。
**注:**如果函数的形参的值不需要改变建议加上const让别人明白。
void关键字
void表示无类型。void关键字有三种用途:
- 函数返回值用void表示函数没有返回值。
- 函数参数填void表示函数不需要输入参数(括号中什么不填也可表示此意义)。
- 函数形参用void*,表示接受任意数据类型的指针。
**注:**不能用void声明变量。
C++内存模型
C++内存分配从代码段到内核空间是从低到高的。
栈区:内存分配是向下减小的。
堆区:内存分配是向上增大的。
栈和堆的区别:
- 管理方式不同:栈是编译器自动管理的,堆需要手动释放内存。
- 空间大小不同:栈很小,一般只有8M,堆的内存空间比栈大,受限于物理内存空间。
动态分配内存new和delete
若需要处理大量的数据,必须使用堆区内存。
使用堆区内存的四个步骤:
- 声明一个指针。
- 用new运算符向内存申请一块内存,让指针指向这块内存。
- 通过对指针解引用的方法,像使用变量一样使用这块内存。
- 如果这块内存不用了,用delete运算符释放这块内存。
申请内存的语法:new 数据类型(初始值);
释放内存的语法:delete 地址
注:
- 动态分配出来的内存,没有变量名,只能通过指向它的指针来操作内存中的数据。
- 动态分配的内存不用了,必须用delete释放它,否则有可能用尽系统的内存。
二级指针
用于存放指针变量的地址。
空指针
在C和C++中,用0或NULL都可以表示空指针。
声明指针后,在赋值之前,让它指向空,表示没有指向任何地址。
在函数中,应该有判断形参是否为空指针的代码,目的是保证程序的健壮性。
用0和NULL表示空指针会产生歧义,C++11建议用nullptr表示空指针,也就是(void*)0。
野指针
所谓野指针即指针指向的不是一个有效合法的地址。
访问野指针可能会导致程序崩溃。
造成野指针的三种情况:
- 指针在定义的时候,如果没有初始化,它的值是不确定的。
- 指针指向了动态分配的内存,内存被释放后,指针不会置空,但指向的地址已经失效。
- 指针指向的变量已超过变量的作用域。
规避方法:
- 指针在定义的时候,如果没有地方指就初始化为nullptr。
- 动态分配的内存被释放后,将其置为nullptr。
- 函数不要返回局部变量的地址。
函数指针和回调函数
函数指针主要用于函数的回调。
使用函数指针的三个步骤:
- 声明函数指针
- 让函数指针指向函数的地址
- 通过函数指针调用函数
函数指针的声明语法:函数类型(*函数指针名)(函数形参表); //函数类型是指返回值和参数列表
例子:int (*p)(int,string);
#include <iostream> //包含头文件
using namespace std;
void func(int no, string str)
{
cout << "亲爱的" << no << "号:" << str << endl;
}
int main()
{
int bh = 3;
string message = "我是一只啥啥鸟";
func(bh, message);
void (*pfunc)(int, string); //声明函数指针
pfunc = func;
pfunc(bh, message);//通过函数指针调用函数
}
在C++中,函数名就是函数的地址
回调函数是把一个函数的代码嵌入到另一个函数中,调用者函数提供了主体的流程和框架,具体功能可以由回调函数实现。
给回调函数传递参数有两种方法:
- 由调用者函数提供实参
- 把实参从外面传进去
回调函数在多线程和网络通讯中很常用