作用:可以通过指针间接访问内存
指针类型* 指针变量名 = 地址初值
int a;
int* p = &a;//p的初值为变量a 的地址
int b, * p1 = &b;//p1初始化是变量b已有地址值
由于指针数据的特殊性,他的初始化和赋值运算是有约束条件的,只能使用以下四种值:
(1)0值常量表达式:
int a, b = 0;
int p1 = null; //指针允许0值常量表达式
p1 = 0;//指针允许0值常量表达式
下面三中形式是错误的:
int* p1 = a;//错误 地址初值不能是变量
int p1 = b;//错误 整形变量不能作为指针,即使值为0
p1 = 4000;//错误,指针允许0值常量表达式
(2)相同指向类型的对象的地址
int a, * p1;
double b, * p2;
p1 = &a;
p2 = &b;
p1 = &b;//错误p1和f指向类型不同
(3)相同指向类型的另一个有效指针
int x, * px = &x;
int* py = px;//相同指向类型的另一个指针
(4)对象存储空间后面下一个有效地址,如数组下一个元素的地址
int a[10], * px = &a[2];
int* py = &a[++i];
空指针:指针变量指向内存编号为0的空间
用途:初始化指针变量
注意:空指针指向的内存是不可以访问的
例如:int *p=NULL;----空指针
野指针:指变量指向非法的内存空间
int *p=(int *)0x1100; //指针变量p指向内存地址编号为0x1100的空间
注意:空指针和野指针都不是我们申请的空间,因此不要访问
指针运算都是作用在连续存储空间上才有意义。
(1)指针加减整数运算
int x[10], * p = &x[5];
p + 1 //指向内存空间中x[5]后面的第1个int 型存储单元
p - 1 //-------------------前面-----1个
(2)指针变量自增自减运算
int x[10], * p = &x[5];
p++ //p指向x[5]后面的第1个int型内存单元
++p //同上
p-- //p指向x[5]前面的第1个int型内存单元
--p //同上
(3)两个指针相减运算
设p1, p2是相同类型的两个指针,则p2 - p1的结果是两指针之间对象
的个数,如果p2指针地址大于p1则结果为正,否则为负
int x[5], * p1 = &x[0], * p2 = &x[4];
int n;
n = p2 - p1;//n 的值为4 即为他们之间间隔的元素的个数`
运算方法:(p2储存的地址编码-p1储存的地址编码)/4 若是double类型则除以8 char类型除以1,short类型除以2.
(4)指针的运算关系
设p1、p2是同一个指向类型的两个指针,则p1和p2可以进行关系运算,
用于比较这两个地址的位置关系即哪一个是靠前或者靠后的元素
int x[4], * p1 = &x[0], * p2 = &x[4];
p2 > p1; //表达式为真
(1)一个指针变量可以指向只读型对象,称为指向const对象的指针定义形式是
const 指向类型 *指针变量,…
不允许通过指针来改变所指向的const对象的值,不能通过间接引用来改变它所指向的对象的值
const int a = 10, b = 20;
const int* p;
p = &a;//正确, p不是只读的,把a的地址赋给p,给p赋值是允许的
p = &b;//正确, p不是只读的
*p = 40;//把42赋给p所指向的对象。错误,*p是只读的
(2)不能把一个const对象的地址赋给一个非const对象的指针,例如:
const double p1 = 3.14;
double* p2 = &p1;//错误
const double* p3 = &p1;//正确
(3)允许把非const对象的地址赋给指向const对象的指针,不能使用指向const对象的指针修改指向对象,然而如果该指针指向的是一个非const对象,可以用其他方法修改其所指向的对象
const double pi = 3.14;
const double* cptrf = π//正确
double f = 3.14;//f是double类型(非const类型)
cptr = &f;//正确,允许将f的地址赋给cptrf
f = 1.68;//正确,允许修改f的值
*cptrf = 10.3;//错误
(4)const指针
一个指针变量可以是只读的,成为const指针它的定义形式:
指针类型* const 指针变量, …;
注意观察将const放在变量名的前面,与上面的形式不同
int a = 10, b = 20;
int* const pc = &a;//pc是const指针
pc = &b;//错误pc 是只读的
pc = pc;//错误pc是只读的
pc++;//错误pc是只读的
*pc = 100;//正确,a被修改
对于用const修饰的总结:
根据上述代码:1.pc是指向int型对象的const指针
不能使pc再被赋值指向其他对象,任何企图给const指针赋值的操作都会导致编译错误,但是可以通过pc间接引用修改该对象的值。
2.const 指向类型 *指针变量,这种类型时,可以修改指向,但是不能修改值
(1)数组的首地址
数组有若干个元素组成,每个元素都有相应的地址,通过取地址运算符&可以得到每个元素的地址,数组的地址就是这一整块存储空间中,第一个元素的地址即a[0]
int a[10];
int* p = &a[0];//定义指向一维数组元素的指针,用a数组的地址来初始化p,称p指向a
p = &a[5];//指向a[5] 重新给p赋值,指针数组元素的地址跟取变量的地址是一样的效果
下面两个语句是等价的:
p = a;
p = &a[0];//a和&a[0]都代表数组首地址
(2)指向一维数组的指针变量
定义指向一维数组元素的指针变量时,指向类型应该与数组元素类型一致
int a[10], * p1;
double f[10], * p2;
p1 = a;//正确
p2 = f;//正确
p1 = f;//错误,指向类型不同不能赋值
a是一个一维数组,p是指针变量,且p=a;下面我们来访问一个数组元素 a[i];
(1)数组下标法:a[i];
(2)指针下标法:p[i]; p里面已经放了数组的地址了,因此数组名和p 是等价的所以 p[i]与a[i]含义相同
(3)地址引用法:(a+i); a表示的是下标为0 的元素的地址 (a+i) 即为a这个数组往后数第 i 个元素的地址 即第 i 个元素的地址 那么(a+i)相当于地址再加一个星号表示的就是这个地址对应的存储单元,或者说对应的对像即为 a[i]这个元素
(4)指针引用法:*(p+i);将a替换成p跟(3)含义相同
如下代码可以用来访问数组
int main()
{
int a[5],i;
for (i = 0; i < 5; i++)
cin >> *(a + i);
for (i = 0; i < 5; i++)
cout << *(a + i) << " ";
}
int main()
{
int a[5], * p;
for (p = a; p < a + 5; p++)
cin >> *p;
for (p = a; p < a + 5; p++)
cout << *p << " ";
}
char* p = “C Language”;
如上述代码
指针p指向了首字符的地址,是虽然同数组一样都指向首地址,但是其输出却不同
char str[] = "C Language", * p = str;//p指向字符串的指针相当于p指向了str[0]
cout << p << endl;//输出:C Language 跟cout<
cout << p + 2 << endl;//输出:Language 从字符L开始输出直到结束
cout << &str[7] << endl;//输出:age 从第7个元素开始输出
会输出指向元素及其后的字符
注意:
指针可以指向数组,这使得数组访问多了一种方式,单指针不能代替数组存储大批量元素
char s[100] = “Computer”;
s是数组名不能赋值,自增自减运算
char* p = “Computer”;
p是一个指针,他存放的是这个字符串的首地址
p是一个指针变量,他能指向这个字符串也能指向其他东西可以进行赋值和自增自减
1、存储内容不同
2、运算方式不同
3、赋值操作不同
s一旦赋初值之后就不能再用其他字符来赋值,然而p却能重新指向其他字符
二级指针:是一个指针变量,指向一个一级指针,并保存该一级指针的地址。
由于一级指针已经很熟悉,这里不再赘述,这里我们只谈谈二级指针
下面先简单使用一个二级指针。
int main()
{
int a = 10;
int b = 20;
int *p = &a;//储存变量a的地址
int** s = &p;//储存指针p的地址
//一次解引用*s 此时类型int*
*s = &b;
//二次解引用**s 此时类型int
**s = 100;//间接改变b的值
return 0;
}
解释:s为指向一级指针p的二级指针,p为指向变量a的一级指针。
1.一次解引用
s 的类型变为int(代表着一级指针p),间接改变p指向b。
2.二次解引用
s的类型变成了int (代表着变量b),改变s的值,间接改变b的值。
(1)举例:对于下面给的这段代码,将给出三者的关系图解
int main()
{
//普通变量
int a1 = 1;
int a2 = 1;
int a3 = 1;
//一级指针
int* p1 = &a1;
int* p2 = &a2;
int* p3 = &a3;
//二级指针
int **ptr = &p1;
return 0;
}
表达式 | 移动的字节数/值的变化 | 类型 |
---|---|---|
ptr+1 | 4*(sizeof(int*)*1) | int** |
*ptr+1 | 4*(sizeof(int)*1) | int* |
**ptr+1 | a1+1 | int |
1.ptr+1表示ptr指向了p2(int**)
2.ptr+1(先对ptr进行一次解引用)表示操控一级指针p1指向a2(int*)
3.**ptr+1(对ptr进行两次解引用) 表示a1+1(int)
以此类推,其他类型如char,short,double类型皆类似
表达式 | 移动的字节数/值的变化 | 类型 |
---|---|---|
ptr+1 | 4*(sizeof(char*)*1) | char** |
*ptr+1 | (sizeof(char)*1) | char* |
**ptr+1 | a1+1 | char |
表达式 | 移动的字节数/值的变化 | 类型 |
---|---|---|
ptr+1 | 4*(sizeof(short*)*1) | short** |
*ptr+1 | 2*(sizeof(short)*1) | short* |
**ptr+1 | a1+1 | short |
表达式 | 移动的字节数/值的变化 | 类型 |
---|---|---|
ptr+1 | 4*(sizeof(double*)*1) | double** |
*ptr+1 | 8*(sizeof(double)*1) | double* |
**ptr+1 | a1+1 | double |
对于上述表格中移动字节数/值的变化解释一下:
1.二级指针类型移动的是一级指针所占字节数都为4*(sizeof()*1)
2.一级指针类型移动的是当前类型int为4,short为2,char为1,double为8
3.为当前类型时,就是修改变量的值
作为函数参数,它使得被调函数除了返回值以外,能够将更多的运算结果返回到主调函数中。指针是重要的函数参数传递工具。
函数形参可以是指针类型,一般形式为:
*注:指针作为函数的形参时,其实参应该是相应类型的数据的地址。
设计一个swap函数,用于交换两个变量的值,有如下两个例子:
1)指针作为形参
2)局部变量作为形参
总结:上述两例子分别为地址传递和值传递。
1.地址传递中由指针指向要修改变量的地址,通过修改指针来修改想要改变的变量
2.而值传递只传递了想要修改的变量的值,但是子函数中本身就有局部变量,其生命周期在函数结束时就结束了所以并没有修改原变量的值。
最后再看一个例子
通过将指针作为函数参数的方法,可以返回多个运算结果,有避免使用了全局变量。
例:计算 a 和 b 的平方和,自然对数和,几何平均数、和的平方根。
首先看一段代码
void fun(int *p){
int b=100;
p=&b;
}
int main(){
int a=10;
int *q;
q=&a;
cout<<*q;
fun(q);
cout<<*q;
return 0;
}
输出:10 10
为什么?
因为在这段代码中,虽然函数 fun 中修改了指针 p 的值,使其指向了一个局部变量 b 的地址,但在函数 fun 结束后,b 的生命周期也结束了,它所占用的内存会被释放。因此,在函数 fun 执行完毕后,指针 p 不再指向有效的内存地址,即它成为了悬挂指针。
当回到 main 函数并打印 *q 的值时,由于 q 仍然指向先前定义的 a 变量,而不是被 fun 中的修改所影响。因此,无论fun 中对指针的操作如何,最终都不会对变量 a 的值产生任何影响。
所以,两次输出结果一样,都是打印变量 a 的初始值 10。
那么该如何解决此问题?
这里将引入二级指针做参数
void fun(int **p){
int b=100;
*p=&b;
}
int main(){
int a=10;
int *q;
q=&a;
cout<<*q;
fun(&q);
cout<<*q;
return 0;
}
输出:10 100
原因:在这段代码中,函数 fun 的参数是一个指针的指针 **p,而不是单纯的指针 *p。通过将指针的地址传递给 fun 函数,并在函数内部通过解引用并修改指针的值,可以影响原始指针的值。
具体来说,函数 fun 中的 *p=&b; 语句将 *p 解引用得到 q 的指向的变量 a 的地址,并将其修改为 b 的地址。因此,在函数 fun 执行完毕后,q 指向了 b 的地址。
回到 main 函数并打印 *q 的值时,由于 q 指向了 b 的地址,所以打印的结果是 100。通过在函数 fun 中修改指针指向的地址,间接修改了原始指针 q 的值。
因此,这段代码可以通过指针的指针实现修改变量 a 的值。
通过上述例子,我们可以看到,在某些情况下,函数参数传递一级指针时,在函数体内对指针做变动,也不会对原始指针产生变化,而传递二级指针时,则可以,这是为什么呢?
在传递一级指针时,只有对指针所指向的内存变量做操作才是有效的;
在传递二级指针时,只有对指针的指向做改变才是有效的;
在链表或者树的操作中,也需要用到二级指针,
比如创建链表的头指针:
在始化链表函数中,传入头指针,并在函数中为该指针分配空间,此时就应该使用二级指针,如void initLinklist(Node **head);
而在添加删除结点的过程中,我们并没有改变函数参数指针的指向,而是通过传入的指针如Node *head,找到要删除结点的位置,并未对该指针做改变,因此退出函数后,该指针无影响。