天天看点

对 C指针的理解(大杂烩)

写在前面: 

本文章旨在总结备份、方便以后查询,由于是个人总结,如有不对,欢迎指正;另外,内容大部分来自网络、书籍、和各类手册,如若侵权请告知,马上删帖致歉。

前排声明一下,不然可能会被打,本篇是在学习 typedef高级应用的时候发现对于指针的应用有很多不熟,然后查看了好多资料(参考链接在后面),于是又重新复习了一遍,并把它以自己通俗的语言整理出来,可能会有炒鸡多的不对,内容多来自书本和网络,希望各方大佬进行指正;同样的,在看这篇文章之前,不可全信,请务必持怀疑的态度去思考(我也不敢打保单一定是正确的,毕竟我也在学习中)

目录

一、什么是指针?

二、指针是用来干什么的?

三、指针变量的声明

四、指针的初始化及赋值

五、指针和数组

六、指针与 const

七、指向指针的指针

八、指针与结构体

九、函数和指针

指针这东西,讨人喜爱的同时也惹人烦恼,反正对我而言,谈起指针就脑壳疼,所以别在我面前谈它,不然出来打一架;哈哈哈,开玩笑的

一、什么是指针?

首先你要明白什么是指针,指针是一个值为内存地址的变量(或数据对象),即,内存位置的直接地址;记住:指针变量的值并不是它所指向的内存位置所存储的值

二、指针是用来干什么的?

那么指针是用来干嘛的?简单的说,指针是用来操作内存的,因为内存中的每个位置都是由一个独一无二的地址标识,并且内存的每个位置都包含一个值,所以,我们常说的“谁谁谁指向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、

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;
}
           

输出:(运行环境 –> Dev-C++ 5.11)

对 C指针的理解(大杂烩)

既然,在指针和形参声明中使用 const有以上这两种情况,那么怎么区分呢?

其实细心点可以发现,const放在 " * "左侧任意位置,限定了指针指向的数据不能改变;const放在 " * "的右侧,限定了指针本身不能改变。

3、根据上面的,我们会想,在指针上能不能同时加 const,可以,这就是第三种:指向整形常量的常量指针(int const *const cpci)

在这里,无论是指针本身还是它所指向的值都是常量,都不允许修改

你可能会问,上面的第一、二种不是常说的“常量指针”和“指针常量”吗?或许你说的是对的,但对于我目前找到的参考文献中,并没有找到对应出现的这两个词;正如文章开头所说的,请务必持怀疑的态度去思考,所以,我并不打算把上面的两个分点写上“常量指针”和“指针常量”,毕竟,也有可能是大部分人为了叫的好听一点或者区分这个关系而命名的,但相对的,我想如果你理解了上面分点的那两个的特点及应用,比记“常量指针”和“指针常量”更好理解吧

七、指向指针的指针

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语言)