目录
# 指针的定义
何为指针?
简而言之,指针是一个指向性的地址。
例如:现在定义了一个整型变量a,编译系统则会分配4个字节的空间,此时这四个字节有一个编号(带类型的),这个编号就是变量a的地址 ¹。
当对变量a进行赋值操作时,它的地址并不会改变,改变的是空间里的所储存的数据,此时通过指针,也就是“地址”,就能锁定到这块空间,从而访问 ² 到“地址”里面的存储的数据,进行运算或者函数。
¹ C语言中的地址与我们所直观认为的有所区别,一串地址例如地址1010,我们并不能直接得到该单元的数据,因为缺少了数据类型。此时虽然锁定了指定的储存单元,但是无法确定是从一个字节中(字符数据)提取信息,还是两个字节(短整型),或者是四个字节(整型)。因此,C语言中所说的指针,不仅包含了地址信息,也包含了它所指向的数据的类型信息。
一个完整的指针包含地址和其类型。
² 对变量的访问:分为直接访问和间接访问。
其中直接访问是直接按变量名进行访问,譬如语句:
a=b+c;
就是直接取出b和c中的值,将其相加后,将结果输送到a所在的单元之中。
间接访问是指定义一个特殊的变量用于储存地址(指针变量),通过取出地址然后找到地址所代表的单元,从中取出变量的信息。
通过这种方法,可以避免改变变量本来存储的信息。
简而言之,指针变量的值是地址/指针本身
# 指针的类型
与变量的数据类型相似,指针也分为整型指针/浮点指针等
具体有大致如下:
//声明指针
char *pc = NULL; //字符指针
int *pi = NULL; //整形指针
short *ps =NULL; //短整型指针
long *pl = NULL; //长整型指针
float *pf = NULL; //单精度浮点型指针
double *pd = NULL; //双精度浮点型指针
……
#NULL为空指针#
# 指针的基本语法和应用
· 指针变量
为了能够更好的访问内存空间,我们可以通过&
(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个变量就是指针变量。
定义指针变量的语法为:类型名 * 指针变量名
int num = 10; //在内存中开辟一块空间
int *p = # //这里我们对变量a,取出它的地址,可以使用&操作符。
/* num变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
中,p就是一个之指针变量。*/
* 指针的定义语法与变量的定义语法类似,但指针尽量初始化(否则容易使定义的指针变为“野指针” ¹ )
· 引用指针
(1)赋值
p=&a; //把a的地址赋给指针变量p
指针变量p的值是变量a的地址,p指向a
* 取地址符 & ,&a是变量a的地址
(2)引用指针所指变量
int *p = &a ;
*p = 1 ;
表示:将1赋值给指针p所指向的变量(即变量a),相当于“a=1;”
· 指针的加减运算
(1)指针 ± 整数 :
char* 指针 + 1,意思是跳过一个字符,也就是向后走1个字节
short* 指针 + 1,向后走2个字节
int* 指针 + 1,意思是跳过1个整形,也就是向后走4个字节
double* 指针 + 1,意思是跳过一个double,也就是向后走8个字节
……
指针类型决定了指针的步长(向前 / 向后 跳过多少字节)
(2)同类型指针减法运算
指针类型与指针类型相减后,表示两个指针之间元素个数。
* 注意 指针+指针 没有意义
* 两个指针相减的前提是:指针指向的同一块连续的空间
· 指针变量参与函数
(1)函数声明
void function ( int * p1,int * p2) ;
* 对 function 函数的声明,需求两个整形指针变量p1,p2
(2)返回指针
int * function() {
int num = 100 ;
return & num ;
}
* 函数function返回一个指针变量
¹ 野指针
野指针就是指针指向的位置是未知的(随机的、不正确的、没有明确限制的)
其带来的后果就是指针指向了一个未知 的地址,难以运用指针进行进一步赋值和运算,因此代码中要尽量避免野指针的产生。
i.野指针的产生原因:
int *p ; //局部变量指针未初始化,默认为随机值
* 指针没有初始化,默认随机
int arr[10] = {0};
int *p = arr ;
for ( int i = 0 ; i <= 11 ; i++ ){ //当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
* 指针的越界访问(例如指针指向范围超过数组的范围,指针p此时变为野指针)
int * function()
{
int x = 1 ;
return & x ; //return函数后,x内存回归操作系统
int main() {
int * p = function() ;
*p = 200 ;
return 0 ;
}
* 指针所指向的空间被释放后,指针变为野指针
ii.规避野指针的产生
- 指针初始化(已知指向时明确初始化 / 未知初始化为NULL);
- 注意指针范围,防止越界;
- 指针指向空间释放,及时置NULL;
- 避免返回局部变量的地址;
- 指针使用之前检查 有效性 ;
// 检查指针的有效性:
//检查指针的有效性
int main() {
int *p = NULL ; //未知指向初始化为NULL
int a = 10;
p = & a; //明确初始化
if (p != NULL){ //为空指针不访问(无效指针)
*p = 20 ; //不为空再访问
}
return 0;
}
原理:空指针 NULL 位于内核区域不能直接访问。
# 指针引用数组
· 定义数组指针:
在定义数组元素的指针时,通过下面语句对其进行初始化:
int * p = & a[0] ;
其作用是将数组首元素 a[0] 的地址赋给指针变量 p
· 数组元素指针的算数运算
与指针的运算相类似,当指针已经指向一个数组元素时,可以对指针进行下列运算:
加一个整数(如 p+1); // 表示指针指向同一数组的下一个元素
减一个整数(如 p -1); // 表示指针指向同一数组的上一个元素
类似的也有自加和自减( p++ p-- ++p --p )
两个指针相减 p1-p2 ; // 表示之间有多少个元素
*指针相加无意义
· 指针引用数组元素
数组元素的指针就是数组元素地址
对于一个给定的数组,引用起元素既可以使用下标法(如 a [5] ),也可以使用指针法。
//下面两行代码等价
p = & a[0] ; //p的值是 a[0] 的地址
p = a ; //p的值是数组 a 的首元素(即 a[0])的地址
通过两种方法遍历数组
//下标表示
for ( i = 0 ; i < 10 ; i++ ){
scanf ( " %d ", & a[i] ) ;
}
for ( i = 0 ; i < 10 ; i++ ){
printf ( " %d ", a[i] ) ;
}
//指针表示
int * p , i ;
for ( i = 0 ; i < 10 ; i++ ){
scanf ( " %d ", & a[i] ) ;
}
for ( p = a ; p < ( a+10 ) ; p++ ){
printf ( " %d ", * p ) ;
}
语句: * ( arr + i ) 与 arr [ i ] 无条件等价
· 指针引用多维数组
二维数组的地址
定义二维数组 arr [ i ][ j ] ,其中元素地址可以描绘为 & [ i ][ j ] 。
由上文介绍的等价语句,地址同时可以写为如下:
a [ i ][ j ] == * ( a[ i ] + j ) == * ( * ( a + i ) + j ) .
即表示为:a [ i ][ j ] 与 * ( * ( a + i ) + j ) 等价。
示例:
#include <stdio.h>
int main() {
int a[3][4] = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24};
printf("%d,%d\n", a, *a); //0行起始地址 与 0行0列元素地址
printf("%d,%d\n", a[0], *(a + 0)); //0行0列元素地址
printf("%d,%d\n", &a[0], &a[0][0]); //0行起始地址 与 0行0列元素地址
printf("%d,%d\n", a[1], a + 1); //1行0列元素地址 与 1行起始地址
printf("%d,%d\n", &a[1][0], *(a + 1) + 0); //1行0列元素地址
printf("%d,%d\n", a[2], *a + 2); //2行0列元素地址
printf("%d,%d\n", &a[2], a + 2); //2行起始地址
printf("%d,%d\n", a[1][2], *(*(a + 1) + 2)); //1行2列元素的值
printf("%d,%d\n", *a[2], *(*(a + 2) + 0)); //2行0列元素的值
return 0;
}
运行结果:
6422000,6422000
6422000,6422000
6422000,6422000
6422016,6422016
6422016,6422016
6422032,6422008
6422032,6422032
14,14
18,18
输出结果中每一行两个相同,意味着两处代码等价;
# 指针引用字符串
· 字符串的引用方式
方式一:
用字符数组存放一个字符串,可以通过数组名+下标引用其中一个字符,也可以用数组名+“ %s ”声明输出。
char string [] = "C Primer Plus" ;
printf (" %s \n ",string ) ;
printf (" %s \n ",string [5] ) ;
字符数组的存储方式如下:
C | P | r | i | m | e | r | P | l | u | s | \0 |
如代码 string[5] ,等价于 *(string + 5)。
string+5 是一个地址,它指向了字符 “ i ” 。
方式二:
用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常量。
char * string = "C Primer Plus" ;
printf (" %s \n ",string ) ;
其中,将字符串的第一个元素的地址 赋给指针变量 string ,使 string 指向字符串第一个字符,也称为指针指向了字符串。
此时对指针变量进行再赋值,string 指向新的字符串,无法再引用原来的字符串。
· 使用字符指针变量 与 字符数组的区别
(1)字符数组由元素组成,每个元素一个字符,字符指针变量中存放地址;
(2)允许对字符指针变量赋值,但不能对数组名赋值;
(3)对于字符串数组,初始化是将字符赋给各个元素;对于指针变量,初始化是将第一个字符的地址赋给指针变量;
(4)指针变量的值可以改变,字符数组名代表了其首元素地址,不能改变;
(5)字符数组中的各个元素可以被更改,但字符指针变量所指向的字符串变量中的内容不可取代,不能被再赋值;
(6)用指针变量指向一个格式字符串,可以用它代替 printf 函数中的格式字符串。
# 指向函数的指针
· 什么是函数的指针
如果定义了一个函数,该函数会存在一个“入口”,存在一个入口地址,每次调用函数时,从函数名得到函数的起始地址,并执行函数代码。
换而言之,函数名就是函数的指针,他代表函数的起始地址。
因此,如果定义了一个指针p,它指向该函数的起始地址,也就指向了该函数。
· 用函数指针变量调用函数
语法: 类型名 ( * 指针 )( 函数参数列表 )
例如:int ( * p ) ( int a , int b )
//示例
int function (int a , int b );
int c ;
c = ( * p ) ( a , b ) ;
c = function ( a , b ) ;
*其中两次函数调用等价
· 嵌套函数
用指向函数的指针作为函数参数
//示例
void function ( int ( * x1 ) ( int ) , int ( * x2 ) ( int , int ) ) ;
//嵌套函数的声明
作用 / 好处 :在多次调用 function 函数的时候,若每次内部调用的函数都不相同,这时使用指针变量就显得更加方便了,原因是在每次调用function的时候只需要给出不同的函数名作为此次调用的实参即可,而function函数本身没有进行任何修改。
示例:输入整数a,b;由用户输入1/2/3,输入1执行取大,输入2执行取小,输入3执行求和.
#include <stdio.h>
int main() {
int function(int x, int y, int (*p)(int, int)); //function选择函数声明
int max(int, int); //取大函数声明
int min(int, int); //取小函数声明
int sum(int, int); //求和函数声明
int a, b, n;
scanf("%d %d %d", &a, &b, &n);
if (n == 1)
function(a, b, max); //执行取大函数
else if (n == 2)
function(a, b, min); //执行取小函数
else if (n == 3)
function(a, b, sum); //执行求和函数
return 0;
}
int function(int x, int y, int (*p)(int, int)) {
int res;
res = (*p)(x, y);
printf("%d", res);
} //定义function
int max(int x, int y) {
int z;
if (x > y)
z = x;
else
z = y;
printf("max=");
return (z);
} //定义取大函数
int min(int x, int y) {
int z;
if (x < y)
z = x;
else
z = y;
printf("min=");
return (z);
} //定义取小函数
int sum(int x, int y) {
int z;
z = x + y;
printf("sum=");
return (z);
} //定义求和函数
# 指针数组
· 什么是指针数组
一个数组,若其中元素都是指针类型的数据,就称为指针数组,相当于每一个元素都是一个地址。
*区分 指针数组 与 引用数组的指针 :
前者元素是地址,后者只是用外层指针存储了第一个元素的地址
· 语法
类型名 * 数组名 [ 数组长度 ]
//示例
char * name [ ]
* 注意区分
指针数组 与 指向一维数组的指针
int * p [ 4 ] int ( * p ) [ 4 ]
# 多重指针
简而言之是一个指针指向了另外一个指针
即指针指向的变量同时也是一个地址指向另一个变量
· 语法
//示例
char ** p
· 运用
int a = 1;
int *pa = &a ; // pa 就是指针变量,一级指针变量,表示指针指向的 a 是 int
int* *ppa = & pa ; // ppa 就二级指针,表示 pp 指向的 p 的类型是 int*
· 多重指针
多指几次,俗称 套娃
比如:五⭐程序员(整活代码)
#include <stdio.h>
int main() {
int n = 123; // int
int *oneStar = &n; // int *
int **twoStar = &oneStar; // int **
int ***threeStar = &twoStar; // int ***
int ****fourStar = &threeStar; // int ****
int *****fiveStar = &fourStar; // int *****
printf("n = %d", ***** fiveStar); // 五次取值,还原为int
return 0;
}
最终输出结果经过层层解引用,输出 n=123
# 声明顺序(优先级)
对于数组、函数、指针,它们遵循下列的运算数序,优先级从高到低:
1、括号( )
2、函数声明的 ( ) 与 数组声明的( )优先级相同
3、指针 声明的 *