const 修饰指针
只写一种:常量指针const 数据类型 *变量名;
不能通过解引用的方法修改内存地址中的值(可以用原始的变量名修改)。
这种写法,指针指向的对象是可以改变的。
void* 指针
void*
表示可以接收任意数据类型的指针。
- 不能用void* 指针声明变量。
- 不能对void* 指针直接解引用(需要转换成其他类型的指针)。
- 其他类型变量赋值给void* 指针不需要转换。
- void* 指针赋值给其他类型的指针需要转换。
C++内存模型
重点关心 栈 and 堆 区。
管理方式:
栈是系统自动管理,在出作用域时,将被自动释放;堆需要手动释放,若程序中不释放,程序结束时由操作系统回收。
空间大小:
堆内存的大小受限于物理内存空间;
栈内存,一般就只有8M(可以修改系统参数,但没必要)。
不是很重要:
1.栈是系统提供的数据结构,计算机在底层提供了堆栈的支持,进栈和出栈都有专门的指令,效率比较高;堆是由C++函数库提供的。
2.栈不会产生碎片,堆频繁的分配和释放,会造成内存空间不连续,容易产生碎片,太多碎片会导致性能下降。
3.栈,以降序分配内存地址;堆,以升序分配内存地址。
动态分配内存new 和 delete
int *p = new int(5);
delete p;
- 动态分配出来的内存没有变量名,只能通过指向它的指针来操作内存中的数据。
- 如果动态分配的内存不用了,必须用delete释放它,否则有可能用尽系统的内存。
- 动态分配的内存生命周期与程序相同,程序退出时,如果没有释放,系统将自动回收。
- 就算指针的作用域已经失效,所指向的内存也不会自动释放。
- 用指针跟踪已经分配的内存时,不能跟丢。
二级指针
二级指针用于存放指针变量的地址。
声明二级指针的语法:数据类型** 指针名;
int a = 8;
int *p = &a;
int **pp = &p;
cout << **pp << endl;//8
int *p = 0;
{
int *pp = p;
pp = new int(3);
cout << pp << *pp << endl; // add 3
}
cout << p << *p << endl; // add 0
如果想在函数中修改p的值,需要把p的地址传给pp。
使用指针的目的:1) 传递地址;2) 存放动态分配的内存的地址。
在函数中,如果传递普通变量的地址,形参用指针;传递指针的地址,形参使用二级指针。
把普通变量的地址传入函数后可以在函数中修改这些变量的值;把指针的地址传入函数后可以在函数中修改指针的值。
空指针
在C/C++中,用0或者NULL都可以表示空指针。
声明指针后,在赋值之前,让它指向空,表示没有指向任何地址。
- 如果对空指针解引用,程序会崩溃。
- 如果对空指针使用delete运算符,系统将忽略该操作,不会出现异常。所以,内存被释放后,也应该把指针指向空。
- 在函数中,应该有判断形参是否为空指针的代码,目的是保证程序的健壮性。
为什么使用空指针访问会出现异常?
NULL指针分配的分区:范围是0x00000000到0x0000FFFF。这段空间是空闲的,对于空闲的空间而言,没有相应的物理存储器与之相对应,所以对这段空间来说,任何读写操作都是会引起异常的。空指针是程序无论在何时都没有物理存储器与之相对应的地址。为了保障“无论何时”这个条件,需要人为划分一个空指针的区域,固有上面NULL指针分区。
C++11的nullptr
用0和NULL表示空指针会产生歧义,C++11建议用nullptr表示空指针,也就是(void*)0。
NULL在C++中就是0,这是因为在C++中void*类型是不允许隐式转换成其他类型的,所以之前C++中使用0来表示空指针,但是在重载整形的情况下,会出现上述的问题。所以,C++11加入了nullptr,可以保证在任何情况下都表示空指针。因此建议nullptr代替NULL,NULL当作0使用。
在Linux平台,如果使用nullptr,编译需要加-std=c++11参数。
野指针
野指针就算指针指向的不是一个有效(合法)的地址。
在程序中,如果访问野指针,可能会造成程序的崩溃。
出现野指针的三种情况:
- 指针在定义的时候,没有初始化,值是乱指的。
- 如果用指针指向了动态分配的内存,内存被释放后,指针不会置空,但是,指向的地址已经失效。
- 指针指向的变量已经超越变量的作用域(变量的内存空间已经被系统回收)比如指向了函数中的返回值。
规避方法: - 指针在定义的时候,如果没地方指,就初始化为nullptr。
- 动态分配的内存被释放后,将其置为nullptr。
- 函数不要返回局部变量的地址。
一维数组和指针
sizeof(char) //1
sizeof(short) //2
sizeof(int) //4
sizeof(double) //8
地址+1并不是字节+1,而是地址+sizeof(type)
- 数组在内存中占用的空间是连续的。
- C++将数组名解释为数组第0个元素的地址。
- 数组第0个元素的地址和数组首地址的取值是相同的。
- 数组第n个元素的地址是:数组首地址+n
- C++编译器把数组名[下标] 解释为 *(数组首地址+下标)
多数情况下,C++将数组名解释为数组的第0个元素的地址,但是,将sizeof运算符用于数据名时,将返回整个数组占用内存空间的字节数。
可以修改指针的值,但是数组名是常量,不可修改。
一维数组用于函数的参数
C++编译器把 地址[下标] 解释为 *(地址+下标)
一维数组用于函数的内存时,只能传数组的地址,并且必须把数组长度也传进去,除非数组中有最后一个元素的标志。
void func(int* arr, int len);
void func(int arr[], int len);
在函数中,可以用数组表示法,也可以用指针表示法。
在函数中,不要对指针名用sizeof运算符,它不是数组名。
其中用int* arr 替换了 int arr[]。这证明这两个函数头都是正确的,因为在C++中,当(且仅当)用于函数头或函数原型中,int *arr 和 int arr[]的含义才是相同的。他们都意味着arr是一个int指针。然而,数组表示法(int arr[])提醒用户,arr不仅指向int,还指向int数组的第一个int。当指针指向数组的第一个元素时,本书使用数组表示法;而当指针指向一个独立的值时,使用指针表示法。别忘了,在其他的上下文中,int *arr 和 int arr[]的含义并不相同。
for(int i=0;i<len;++i)
{
//等价
cout << i << arr[i] << endl;
cout << i << *(arr + i) << endl;
}
用new动态创建一维数组
普通数组在栈上分配内存,栈很小;如果需要存放更多的元素,必须在堆上分配内存。
动态创建一维数组数据类型 *指针 = new 数据类型[数组长度];
释放一维数组delete [] 指针;
注意:
- 动态创建的数组没有数组名,不能用sizeof运算符。
- 可以用数组表示法和指针表示法两种方式使用动态创建的数组。
- 不要用
delete []
来释放不是new []
分配的内存。 - 不要用
delete []
来释放同一个内存块两次(否则等同于操作野指针)。 - 对空指针用
delete []
是安全的,释放内存后,应该把指针置为nullptr。 - 如果内存不足,调用new会产生异常,导致程序中止,如果在new关键字后面加上(std::nothrow) 选项,则返回nullptr,不会产生异常。
int* a = new (std::nothrow) int[1000000000001];
if(a == nullptr) cout<<"error"<<endl;
- 用
delete []
释放数组的时候,不需要指定数组大小,系统会自动跟踪已经分配数组的内存。
二维数组用于函数的参数
int* p; //整型指针
int* p[3]; //一维整型指针数组
int* p(); //函数p的返回值类型是整型地址
int (*p)(int,int); //p是指针函数,函数的返回值是整型
行指针(数组指针)
数据类型 (*行指针名)[行的大小,即数组的长度];
int (*p1)[3]; //p1是行指针,用于指向数组长度为3的int型数组
int (*p2)[5]; //p2是行指针,用于指向数组长度为5的int型数组
double (*p3)[5]; //p3是行指针,用于指向数组长度为5的double型数组
int a[10] = {1,2,3,4,5,6,7,8,9,0};
cout << a << endl; //0x61fde0
cout << &a << endl; //0x61fde0
cout << a+1 << endl; //0x61fde4
cout << &a+1 << endl; //0x61fe08
int *p1 = a;
int (*p2)[10] = &a;
cout << (*p1) << endl; //1
cout << (*p2)[0] << endl; //1
int bh[2][3] = {{11,12,13}, {21,22,23}};
int (*p)[3] = bh;
cout << bh << endl; //0x61fe00
cout << &bh << endl; //0x61fe00
cout << (*p) << endl; //0x61fe00
cout << (*p)[0] << endl;//11
cout << (*p)[3] << endl;//21
可以按照方块队来解释,a+1就是+4,访问下一个int;&a+1就是+40,访问下一列的第一个int,因为一行有十个元素,所以+40。
函数指针和回调函数
函数的二进制代码存放在内存四区中的代码段,函数的地址是它在内存中的起始地址。如果把函数的地址作为参数传递给函数,就可以在函数中灵活的调用其它函数。
使用函数指针的三个步骤:
a) 声明函数指针;
b) 让函数指针指向函数的地址;
c) 通过函数指针调用函数。
理解:
我们不可能在当前函数中直接调用另一个我们需要的函数(没准需要哪个函数,我也就不可能写一个确定的函数名,但是通过函数指针,只需要不同的参数,可以个性化需求,不需要提前知道函数名),因为在写函数的时候,根本不知道这个函数会被谁调用。
回调函数是把一个函数的代码嵌入到另一个函数中,调用者函数提供了主体的流程和框架,具体的功能可以由回调函数来实现。我们在写调用者函数的时候,只确定回调函数的种 类,不关系回调函数的功能。