前排声明一下,本篇是在学习 typedef 高级应用的时候发现对于指针的应用有很多不熟,然后查看了好多资料(参考链接在后面),于是又重新复习了一遍,并把它以自己通俗的语言整理出来,可能会有炒鸡多的不对,内容多来自书本和网络,希望各方大佬进行指正;同样的,在看这篇文章之前,不可全信,请务必持怀疑的态度去思考(我也不敢打保单一定是正确的,毕竟我也在学习中)
指针这东西,讨人喜爱的同时也惹人烦恼,反正对我而言,谈起指针就脑壳疼,所以别在我面前谈它,不然出来打一架;哈哈哈,开玩笑的
# 什么是指针?
首先你要明白什么是指针,指针是一个值为内存地址的变量(或数据对象),即,内存位置的直接地址;记住:指针变量的值并不是它所指向的内存位置所存储的值 。
# 指针是用来干什么的?
那么指针是用来干嘛的?简单的说,指针是用来操作内存的,因为内存中的每个位置都是由一个独一无二的地址标识,并且内存的每个位置都包含一个值,所以,我们常说的 “谁谁谁指向 xxx”,其实就是通过它的地址来找到所需的变量单元(也就是内存)然后再对它进行操作,注意,这个地址它不是固定的,它是由计算机随机安排的。
# 指针变量的声明
type * var-name;
在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型(就是 int、char 那些),var-name 是指针变量的名称。用来声明指针的星号 ***** 与乘法中使用的星号是相同的(符号一样)。但是,在这个语句中,星号是用来指定一个变量是指针 。
例如:
int *ip; /* 一个整型的指针(int *) */ | |
double *dp; /* 一个 double 型的指针(double *) */ | |
float *fp; /* 一个浮点型的指针(float *) */ | |
char *ch; /* 一个字符型的指针(char *) */ | |
…… |
在声明一个指针变量,计算机并不会自动分配用来存储指针所指向的数据的内存,其缺省值是随机的,所以我们必须对指针进行初始化,否则可能会带来严重的危害。
# 四、指针的初始化及赋值
在进行初始化之前,首先我们需要了解 &
、 *
这两个与指针相关的运算符,在 C 中,可以通过 & 运算符访问地址,通过 * 运算符获得地址上的值 。
在写程序过程中,为每个指针变量初始化赋值是个良好的习惯,会有效的防止指针指向未知的内存(也是野指针的一种);对指针进行初始化,要让它:①指向现有的内存、②配置成 NULL 指针、③或者给它分配动态内存
示例:
1、/* 指向现有内存 */
int q = 25; | |
int *ptr = &q; // 定义一个指针变量名为 ptr,并且 *ptr 是 int 类型的;同时,把 q 的地址赋给 ptr(就是我们常说的:把 ptr 指向 q) |
2、/* NULL 空指针 */
int *ptr = NULL; |
3、/* 分配所需的动态内存空间 */
int *ptr = (int *)calloc(n, sizeof(int)); // 在空闲内存池中分配连续内存 n * sizeof (int) 个字节的堆内存空间,并自动将这一块的内存之初始化为 0; | |
/* 注意区分 ‘malloc’‘calloc’; 若是用 malloc 函数,则一般还要调用 memset 函数对内存进行初始化, | |
因为 memset 函数只是向计算机申请了一块内存空间,并没有对这些内存的值进行初始化,虽然说不调 | |
用 memset 这个函数初始化也能通过编译,但是分配内存的值为一些垃圾数值,而且后面也有可能出错, | |
所以啊,用到指针就得给它初始化,否则别用,不然得不偿失 */ | |
int *ptr = (int *)calloc(n*sizeof(int)); | |
memset(ptr, 0, n*sizeof(int)); |
另外,稍微拓展一下:对于我们常见的利用 malloc 函数来为指针申请一段空间来存储数据,那么什么时候需要为指针申请空间,什么时候不需要呢?
要知道,C 库函数 void *malloc(size_t size)
是用来分配所需的内存空间,并返回一个指向它的指针;如果定义指针的时候,指针指向一个有空间的数据,这时就不需要分配空间;如果要给指针赋值,则需要分配内存空间 。
# 指针和数组
数组表示法其实是在变相地使用指针,数组名是数组首元素的地址 。
举个栗子:
/* 我们定义一个数组,把它初始化一下 */ | |
int array[3] = {0}; | |
/* 那么下面的语句是成立的 */ | |
// array == &array[0]; | |
// 你可以测试输出看一下它们的地址,你会发现是一样的 | |
int *temp = array; // 把数组的地址赋给指针 | |
printf("array = %x\n", temp); | |
printf("array[0] = %x\n", &array[0]); |
但是,指针和数组并不是相等的,虽然数组也可以像指针那样进行间接访问,但还是有很大差别的:
声明一个数组时,编译器将根据声明所指定的元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量,指向这段空间的起始位置。声明一个指针变量时,编译器只为指针本身保留内存空间,它并不为任何整形值分配内存空间;而且,指针变量并未被初始化为指向任何现有的内存空间,如果它是一个自动变量,他甚至根本不会被初始化。
同样的
temp + 2 == &array[2]; // 相同的地址 | |
*(temp + 2) == array[2]; // 相同的值 |
顺带一提,不要混淆 *(temp + 2)
和 *temp + 2
;间接运算符 *
的优先级是高于 +
的,所以 *temp + 2
相当于 (*temp)+ 2
。
*(temp + 2) //temp 第 3 个元素的值 | |
*temp + 2 //temp 第 1 个元素的值加 2 |
# 指针与 const
我们知道 const 关键字是用来声明常量的,用于限定一个变量为只读,所以在指针中,由于指针的特殊,有多种情况,这是因为要区分是限定指针本身为 const 还是限定指针指向的值为 const;在指针中,const 常见的用法是声明为函数形参的指针。
在分析之前,我们先来复习一下:
int *pi; //pi 是一个普通的指向整形的指针 |
然后,下面我们在此基础上,让指针跟 const 关键字结合来分析。
1、指向整形常量的指针 (int const *pci / const int *pci)
// 注意其实 const int *p 跟 int const *p 效果一样的 | |
void fun1( int const *p ) | |
{ | |
int temp = 3; | |
p = &temp; // 正确 | |
*p = 8; // 错误 | |
} |
在这种情况下,它的特点就是:你可以修改指针的值,但你不能修改它所指向的值 。
2、指向整形的常量指针 (int *const cpi)
void fun2( int *const p ) | |
{ | |
int temp = 3; | |
p = &temp; // 错误 | |
*p = 8; // 正确 | |
} |
而现在这种情况:此时指针是常量,它的值无法修改,但你可以修改它所指向的整形的值 。
然后,用个例子验证一下
#include<stdio.h> | |
int temp = 3; | |
void fun1( int const *p ) | |
{ | |
p = &temp; | |
// *p = 8; // 错误,编译器警报 | |
printf("fun1 -> p = %x\n", p); | |
printf("fun1 -> *p = %d\n", *p); | |
} | |
void fun2( int *const p ) | |
{ | |
// p = &temp; // 错误,编译器警报 | |
*p = 8; | |
printf("fun2 -> p = %x\n", p); | |
printf("fun2 -> *p = %d\n", *p); | |
} | |
int main(void) | |
{ | |
int value = 11; | |
printf("&temp = %x\n", &temp); | |
printf("&value = %x\n\n", &value); | |
fun1(&value); | |
printf("value1 = %d\n", value); | |
printf("value1 = %x\n\n", &value); | |
value = 11; | |
fun2(&value); | |
printf("value2 = %d\n", value); | |
printf("value2 = %x\n", &value); | |
return 0; | |
} |
note: 既然,在指针和形参声明中使用 const 有以上这两种情况,那么怎么区分呢?
其实细心点可以发现,const 放在 *
左侧任意位置,限定了指针指向的数据不能改变;const 放在 *
的右侧,限定了指针本身不能改变。
3、指向整形常量的常量指针 (int const *const cpci)
根据上面的,我们会想,在指针上能不能同时加 const 呢?可以,这就是第三种:指向整形常量的常量指针。
在这里,无论是指针本身还是它所指向的值都是常量,都不允许修改 。
你可能会问,上面的第一、二种不是常说的 “常量指针” 和 “指针常量” 吗?或许你说的是对的,但对于我目前找到的参考文献中,并没有找到对应出现的这两个词;正如文章开头所说的,请务必持怀疑的态度去思考,所以,我并不打算把上面的两个分点写上 “常量指针” 和 “指针常量”,毕竟,也有可能是大部分人为了叫的好听一点或者区分这个关系而命名的,但相对的,我想如果你理解了上面分点的那两个的特点及应用,比记 “常量指针” 和 “指针常量” 更好理解吧
# 指向指针的指针
int **p;
--- 我们从右往左看,首先从 p 开始,先与 *
结合,说明 p 是一个指针 *p
,然后再与 *
结合 **p
,说明指针所指向的元素是指针;然后再与 int 结合,说明该指针所指向的元素是整型数据。由于二级指针以及更高级的指针极少用在复杂的类型中,所以在这里最多只考虑一级指针。
例子:
int a = 12; | |
int *n = &a; | |
int **p = &n; |
然后,我们来分析一下
表达式 | 与之相对应的表达式 |
---|---|
a | == 12 |
n | == &a |
*n | == a or == 12 |
P | == &n |
*p | == n or == &a |
**p | == *n or == a or == 12 |
# 指针与结构体
先来放一个例子:
#include<stdio.h> | |
#define SIZE 10 | |
typedef struct | |
{ | |
char name[20]; | |
int num; | |
float score; | |
}Student_TypeDef; | |
Student_TypeDef stu = {"Tom", 13, 136.5}; | |
Student_TypeDef *pstu = &stu; | |
int main(void) | |
{ | |
// 读取结构体成员的值 | |
printf("%s的学号是%d,成绩是%.1f!\n", (*pstu).name, (*pstu).num, (*pstu).score); | |
printf("%s的学号是%d,成绩是%.1f!\n", pstu->name, pstu->num, pstu->score); | |
return 0; | |
} |
输出都是:
Tom的学号是13,成绩是136.5!
先来认识一下结构成员的访问:
直接访问:结构变量的成员是通过点操作符 .
访问的
间接访问:利用操作符 ->
(也称箭头操作符)通过结构体指针直接取得结构体成员
值得注意的是:通过结构体指针获取结构体成员,一般形式为:
(*pointer).memberName
或者:
pointer->memberName
第一种写法中, .
的优先级高于 *
, (*pointer)
两边的括号不能少。如果去掉括号写作 *pointer.memberName
,那么就等效于 *( pointer.memberName)
,这样意义就完全不对了。
然后我们结合上面的例子来分析一下,为什么在操作结构体指针的时候,这两种输出的情况是一样的
首先,① pstu = &stu;
指针 pstu 指向了定义为 Student_TypeDef 类型的结构体,这个结构变量名为 stu;和数组不同的是,结构变量名并不是结构的地址;数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,因此得在结构变量名前加上 &
运算符。② pointer->memberName
输出结构先来分析使用 ->
运算符的,也是最常用的方法;按照上面的,如果 pstu == &stu
,那么 pstu->num
即是 stu.num
;这里 pstu 是一个指针,但是 pstu->num
是该指针所指向结构的一个成员,所以 pstu->num
是一个 int 类型的变量,至于为什么不能写成 pstu.num
,因为 pstu 不是结构体。③ (*pointer).memberName
是等价于 pointer->memberName
的,如果 pstu == &stu
,那么 *pstu == stu
,因为我们从上面学习到 &
和 *
是一对互逆运算符;因此就有了这样的替代: stu.num == (*pstu).num
。
# 函数和指针
1、返回指针的函数
int *pf(void)
:首先执行的是函数调用操作符 () ,因为它的优先级高于间接访问操作符;因此, pf
是一个函数,它的返回值是一个指向整形的指针。
2、指向函数的指针(函数指针)
int (*pf)(void)
:这个声明有两对括号,每对的含义各不相同。第二对括号是函数调用操作符,但第一对括号只起聚组的作用;它迫使间接访问在函数调用之前进行,使 pf 成为一个函数指针,它所指向的函数返回一个整形值 (你也可以这样理解: int pam(void)
是我们常见的函数声明,它声明了 pam
是一个返回整形的函数;如果我们将 pam
替换为 *pf
,由于 pam
是函数,因此 *pf
也是函数;而如果 *pf
是函数,则 pf
就成为指向该类型的指针,也就是函数指针) 。
我们知道在声明一个数据指针时,必须声明指针所指向的数据类型。而声明一个函数指针时,必须声明指针指向的函数类型;为了指明函数类型,要指明函数签名,即函数的返回类型和形参类型。在声明了函数指针后,可以把类型匹配的函数地址赋给它。
下面来个例子熟悉一遍:
#include<stdio.h> | |
#include<stdlib.h> | |
#define NUM1 76 | |
#define NUM2 83 | |
int max( char x, char y ) | |
{ | |
return (x>y ? x:y); | |
} | |
int min( char x, char y ) | |
{ | |
return (x<y ? x:y); | |
} | |
double product( int x, int y) | |
{ | |
return (x * y); | |
} | |
char *pam( char *ptf) // 返回指针的函数 | |
{ | |
return ptf; | |
} | |
int main(void) | |
{ | |
char temp[] = "hello, world!"; | |
char *p = (char *)calloc(sizeof(temp), sizeof(temp[0])); // 申请空间 | |
int (*pf)( char a, char b ); // 指向函数的指针 | |
p = pam(temp); | |
printf("%s\n", p); | |
// pf = product; // 错误,编译器警报;product 与指针类型不匹配 | |
pf = max; | |
printf("max = %d\n", (*pf)(NUM1, NUM2)); | |
pf = min; | |
printf("min = %d\n", pf(NUM1, NUM2)); | |
return 0; | |
} |
从上面可以看到 (*pf)()
和 pf()
,两种其中一种都可以;先来看第一种:由于 pf
指向 max()
函数,那么 *pf
就相当于 max()
函数,表达式 (*pf)()
和 max()
相同;第二种:由于函数名是指针,那么指针和函数名可以互换使用。但是我们一般都是使用第一种,也就是 (*pf)()
,因为它明确指出是通过指针而非函数名来调用函数的,这样我们才好区分函数指针,如果我们用第二种,则看上去和函数调用无异。
# 参考
《C Primer Plus》
《C++ Primer Plus》
《C 和指针》
C 指针详解
C 语言指针的初始化和赋值
内存分配函数 malloc 与 calloc 的用法及区别
malloc 和 calloc 的区别
C 中指针变量何时需要初始化 malloc
【C++ 基础之二】常量指针和指针常量
C 语言结构体和指针
指针函数与函数指针(C 语言)