前排声明一下,本篇是在学习 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 语言)

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

夏沫の浅雨 微信支付

微信支付

夏沫の浅雨 支付宝

支付宝