指针的基本概念
1)变量的地址
变量是内存变量的简称,在C++中,系统会给变量分配一块内存,内存是有地址的。
0x00000001 |
---|
0x00000002 |
0x00000003 |
0x00000004 |
0x00000005 |
0x00000006 |
0x00000007 |
0x00000008 |
0x00000009 |
0x0000000A |
0x0000000B |
…… |
0xFFFFFFFF |
C++用运算符&获取变量在内存中的起始地址。
语法:&变量名
2)指针变量
指针变量简称指针,他是一种特殊的变量,专用于存放变量在内存中的起始地址。
语法:数据类型 *变量名;
数据类型必须是合法的C++数据类型(int、char、double或其他自定义数据类型)。
星号*与乘法中的星号是相同的,但是,在这个场景中,星号用于表示这个变量是指针。
3)对指针赋值
不管是整型、浮点型、字符型,还是其他数据类型的变量,它的地址都是一个十六进制数。我们用整型指针存放整型变量的地址,用字符型指针存放字符型变量的地址,用浮点型指针存放浮点型变量的地址,用自定义数据类型指针存放自定义数据类型变量的地址。
语法:指针=&变量名;
4)指针占用的内存
指针也是变量,是变量就要占用内存空间。
在64位操作系统中,不管什么类型的指针,占用内存都是8字节。
在C++中,指针是复合数据类型,复合数据类型是指基于其他类型而定义的数据类型,在程序中,int是整型变量,int *是整型指针类型,int *可以用于sizeof运算符,可以用于数据类型的强制转换,总的来说,把int *当成一种数据类型就对了。
使用指针
声明变量后,在没有赋值之前,里面是乱七八糟的值,这时候不能使用指针。
指针存放变量的地址,因此,指针名表示的是地址(就像变量名可以表示变量的值一样)。
运算符被称为间接值或解除引用(解引用),将它用于指针,可以的得到该地址的内存中存储的值,也是乘法符号,C++根据上下文来确定所指的是乘法还是解引用。
变量和指向变量的指针就像同一枚硬币的两面。
int no = 38;
int *ptr = &no;
程序在存储数据的时候,必须跟踪三种基本属性:
- 程序存储在哪里;
- 数据是什么类型;
- 数据的值是多少。
用两种策略可以达到以上目的;
声明一个普通变量,声明时指出数据类型和变量名(符号名),系统在内部跟踪该内存单元。
声明一个指针变量,存储的值是地址,而不是值本身,程序直接访问该内存单元。
指针用于函数的参数
如果把函数的形参声明为指针,调用的时候把实参地址传进去,形参中存放的是实参的地址,在函数中
通过解引用的方法直接操作内存中的数据,可以修改实数的值,这种方法被通俗的称为地址传递或传地址。
值传递:函数的形参是普通变量。
传地址的意义如下:
- 可以在函数中修改实参的值。
- 减少内存拷贝,提升性能。
使用常量
常量是程序中固定不变的数据。
1)宏常量
一般在 main 函数的上面声明,用大写命名。
语法:#define 常量名 值
2)const修饰的变量
在程序的任何地方都可以声明。
语法:const 数据类型 常量名 = 值
3)常量的特点
程序中不允许改变常量的值,否则编译的时候会报错。
4)示例
#include <iostream> //包含头文件
#define MONTHS 12 //一年中的月份数
#define PI 3.14159 //圆周率
using namespace std; //指定缺省的命名空间
//main函数u,程序从这里开始执行,每个程序只能有一个 main 函数。
int main()
{
const days = 7; //一星期的天数
cout << "一年有" << MONTHS << "个月" << endl;
}
用const修饰指针
1)常量指针
语法:const 数据类型 *变量名;
不能通过解引用的方法修改内存地址中的值(用原始的变量名是可以修改的)。
注意:
- 指向的变量(对象)可以改变(之前是指向变量a的,后来可以改为指向变量b)。
- 一般用于修饰函数的形参,表示不希望在函数里修改内存地址中的值。
- 如果用于形参、虽然指向的对象可以改变,但那么做没有任何意义。
- 如果形参的值不需要改变,建议加上const修饰,程序可读性更好。
int a = 3, b = 8;
const int* p = &a;
//不能通过解引用赋值修改值,编译会报错 错误 C3892
//*p = 13;
//只能通过变量修改值
a = 13;
cout << "a=" << a << ",*p=" << *p << endl;
p = &b;
cout << "b=" << b << ",*p=" << *p << endl;
void func(const int* no,const string *str)
{
//参数加上const,解引用赋值,编译时 同样会报错
//*no = 8;
//*str = "我有一只小小鸟.";
cout << "亲爱的" << *no << "号" << *str << endl;
}
2)指针常量
语法:数据类型 *const 变量名;
指向的变量(对象)不可改变。
注意:
- 在定义的同时必须初始化,否则没有意义。
- 可以通过解引用的方法修改内存地址中的值。
- C++编译器把指针常量做了一些特别的处理,改投换面之后,有了一个新的名字,叫做引用。
3)常指针常量
语法:const 数据类型 *const 变量名;
指向的变量(对象)不可改变,不能通过解引用的方法修改内存地址中的值。
//常指针常量
int a = 3,b = 8;
const int* const pa = &a;
//既不能改变指针指向,也不能解引用改变值,下列两种写法都是错误的
//*pa = 13;
//pa = &b;
常量指针:指针指向可以改,指针指向的值不可以改。
指针常量:指针指向不可以改,指针的指向的值可以更改。
常指针常量:指针指向不可以改,指针指向的值不可以更改。
void 关键字
在C++中,void表示为无类型,主要有三个用途:
函数的返回值使用
void
,表示函数没有返回值。void func (int a,int b) { //函数体代码 return; }
函数的参数填
void
,表示函数不需要参数(或者让参数列表空着)。int func( void ) { //函数体代码 return 0; }
函数的形参用
void *
,表示接受任意数据类型的指针。注意:
- 不能用
void
声明变量,它不能代表一个真实的变量。 - 不能对
void *
指针直接解引用(需要转化为其他类型的指针)。 - 把其他类型的指针赋值给
void *
指针不需要转换。 - 把
void *
指针赋值给其他类型的指针需要转换。
- 不能用
动态分配内存new和delete
C++内存空间
使用堆区内存有四个步骤:
1)声明一个指针;
2)用new运算符向系统申请一块内存,让指针指向这块内存;
3)通过对指针的解引用的方法,像使用变量一样使用这块内存;
4)如果这块内存不用了,用delete运算符释放它;
申请内存的语法:new 数据类型(初始值);
//C++11支持 {}
如果申请成功,返回一个地址;如果申请失败,返回一个空地址(暂时不考虑失败的情况)
是否内存的语法:delete 地址;
注意:
- 动态分配出来的内存没有变量名,只能通过指向它的指针来操作内存中的数据。
- 如果动态分配的内存用不了,必须使用
delete
释放它,否则可能用尽系统的内存。 - 动态分配的内存的生命周期与程序相同,程序退出时,如果没有是否,系统将自动回收。
- 就算指针的作用域已失效,所指向的内存也不会释放。
- 用指针跟踪已分配的内存时,不能跟丢。
二级指针
指针是指针变量的简称,也是变量。是变量就有地址。
指针用于存放普通变量的地址。
二级指针用于存放指针变量的地址。
声明二级指针的语法:数据类型** 指针名;
使用指针有两个目的:1)传递地址;2)存放动态分配的内存的地址。
在函数中,如果传递普通变量的地址,形参用指针;传递指针的地址,形参用二级指针。
空指针
在C++和C中,用0或NULL都可以表示空指针。
声明指针后,在赋值之前,让它指向空,表示没有任何地址。
1)使用空指针的后果
如果对空指针解引用,程序会崩溃。
如果对空指针使用delete运算符,系统忽略该操作,不会出现异常。所以内存释放后,也应该把指针指向空。
为什么空指针访问会出现异常?
NULL指针分配的分区:其范围是从 0x00000000
到 0x0000FFFF
。这段代码是空闲的,对于空闲的空间而言,没有相对应的物理存储器,与之相对应,所以对这段代码来说,任何读写操作都会引起异常的。空指针是程序无论在核实都没有物理存储器与之相对应的地址。为了保障“无论何时”这个条件,需要人为划分一个空指针的区域,固有上面NULL指针分区。
2)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
参数
野指针
野指针就是指针指向的不是一个有效(合法)的地址。
在程序中,如果访问野指针,可能会造成程序的崩溃。
出现野指针的情况主要有三种:
- 1)指针在定义的时候,如果没有初始化,它的值是不确定的(乱指一气)。
- 2)如果指针指向了动态分配的内存,内存被释放后,指针不会置空,但是,指向的指针已失效。
- 3)指针指向的变量超越变量的作用域(变量的内存空间已被回收),作用域指的是是局部变量还是全局变量。
规避方法:
1)指针在定义的时候,如果没地方指,就初始化为nullptr。
2)动态分配的内存释放后,将其置位nullptr。
3)函数不要返回局部变量的地址。
注意:野指针的危害比空指针要大很多,如果访问野指针,可能会造成程序的崩溃。是可能,表示一定,程序的表现是不稳定,增加了调试的难度。
一维数组与指针
1) 指针的算术
- 将一个整型变量加1后,其值将增加1。
- 但是,将指针变量(地址的值)加1后,增加的量等于它指向的数据类型的字节数。
2) 数组的地址
- a) 数组在内存中占用的空间是连续的。
- b) C++数组名解释为数组的第0个元素的地址。
- c) 数组第0个元素的地址和数组首地址的取值是相同的。
- d) 数组第n个元素的地址是:
数组首地址+n
- e) C++编译器把
数组名[下标]
解释为*(数组首地址+下标)
3) 数组的本质
- 数组是占用连续空间的一块内存,数组名被解释为数组第0个元素的地址。C++操作内存有两种方法:数组解释法和指针表示法,他们说等价的。
4)数组名不一定会被解释为地址
- 在多数情况下,C++将数组名解释成第0个元素的地址,但是,将sizeof运算符用于数组名,返回的是整个数组的内存空间的字节数。
- 可以修改指针的值,但数组是常量,不可修改。
一维数组用于函数的参数
1)指针的数组表示
- 在C++内部,用指针来处理数组。
- C++编译器把
数组名[下标]
解释为*(数组首地址+下标)
- C++编译器把
地址[下标]
解释为*(地址+下标)
2)一维数组用于函数的参数
一维数组用于函数的参数时,只能传数组的地址,并且必须把数组长度也传进去,除非数组中有最后一个元素的标志。
书写的方法有两种:
void func(int* arr,int len)
void func(int* arr[],int len)
注意:
在函数中,可以用数组表示法,也可以用指针表示法。
在函数中,不要对指针名用sizeof运算符,由于64位操作系统指针大小是永远是8,并不是数组占用内存的大小。
用 new 动态创建一维数组
普通数组在栈上分配内存,栈很小;如果需要存放更多的元素,必须在堆上分配内存。
动态创建一维数组的语法:数据类型 *指针=new 数据类型[数组长度]
;
释放一维数组的语法:delete []指针;
注意:
- 动态创建的数组没有数组名,不能用 sizeof 运算符。
- 可以用数组比色法和指针表示法两种方式使用动态创建的数组。
- 必须使用
delete[]
来释放内存(不能只用delete
)。 - 不用用
delete[]
来释放不是new[]
分配的内存。 - 不要用
delete[]
释放同一个内存块两次(否则等同于操作野指针)。 - 对空指针用
delete[]
是安全的(释放内存后,应该把指针置空nullptr)。 - 声明普通数组的时候,数组长度可以用变量,相当于在栈上动态创建数组,并且不需要释放。
- 如果内存不足,调用
new
会产生异常,导致程序终止;如果在new关键字后面加(std::nothrow
)选项,则返回nullptr
,不会产生异常。 - 为什么用
delete[]
释放数组的时候,不需要指定数组的大小?因为系统会自动跟踪分配的内存。
二维数组用于函数的参数
int* p; //整型指针。
int* p[3]; //一维整型指针数组,元素是3个整型指针(p[0]、p[1]、p[2])。
int* p(); //函数p的返回值类型是整型的地址。
int (*p)(int, int); //p是函数指针,指针指向的函数具有两个int型的形参。函数的返回值是整型。
1)行指针(数组指针)
声明行指针的语法:数据类型 (*行指针名)[行的大小]; //行的大小即数组长度。
//行指针的举例
int(*p1)[3]; //p1是行指针,用于指向数组长度为3的int型数组。
int(*p2)[5]; //p2是行指针,用于指向数组长度为5的int型数组。
double(*p3)[5]; //p3是行指针,用于指向数组长度为5的double型数组。
一维数组名被解释为数组的第0个元素地址。
对一维数组名取地址得到的是数组的地址,是行地址。
2)二维数组名是行地址
int bh[2][3] = {{11,12,13},{21,22,23}};
bh 是二维数组名,该数组有2元素,每一个元素又是一个数组长度为3的整型数组
。
bh被解释为数组长度为三的整型数组
类型的地址。
如果存放bh的值,要用数组长度为3的整型数组
类型的指针。
int (*p)[3] = bh
3)把二维数组传递给函数
如果要把bh传给函数,函数的声明如下:
void func(int (*p)[3],int len);
void func(int p[][3],int len);