写在前面:
本文章旨在总结备份、方便以后查询,由于是个人总结,如有不对,欢迎指正;另外,内容大部分来自网络、书籍、和各类手册,如若侵权请告知,马上删帖致歉。
前排声明一下,不然可能会被打,本篇是在学习 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)
既然,在指针和形参声明中使用 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语言)