值得收藏的 C语言 指针讲解文章
指针
本文介绍C语言的指针相关知识.
指针是什么?指针和其他的int, float等类似, 是一种类型. 有类型就有相应类型的变量和常量. 本文主要讨论变量的情况.
指针变量就是一种变量, 和其他种类的变量类似, 但指针和其他变量又有区别.
首先C语言作为一种类型语言, 每个变量都会有几个属性.
例如int a = 3, 变量名称就是a, 变量类型是int, 变量的值是3, 如果不提供初始值, 那么变量的值可能是一个随机值.
也就是说, 任何时候看到一个变量, 就会有这3个属性.
对于指针变量, 可以认为有4个属性.
可以看到指针变量的关键在于指针所指向的内存里面数据的类型.
例如int a = 3; int *b = &a;, 指针变量名称是b, 指针变量类型是指针, 变量b的值是变量a的内存地址. 变量b所指向的内存的数据类型是int. 指针变量多了一个"变量b所指向的内存的数据类型是int”, 本文将指针变量所指向的内存的数据类型称做指向类型.
任何时候看到一个指针就需要关注4点内容: 名称, 指针类型, 指针值, 指向类型. 搞清楚这几个内容, 就可以弄明白指针怎么回事, 当然还要记忆 一些例外的情形.
类型对于C语言来说, 搞清楚变量的类型相当重要, 涉及到指针的时候就更加重要. 看到一个指针变量后需要理解其指向类型.
例如char * const * (*next)(), next是一个指针, 那么其指向类型是什么? 这个声明/定义比较复杂, 日常编程可能就会碰到比较 复杂的情况, 所以要搞清楚指针首先要懂得怎么看一个声明/定义的变量的类型.
如果看到一个变量的声明或者定义, 那么就需要弄明白变量的类型. 在<<c专家编程>>这本书中有一部分内容专门讲解怎么分析 一个变量的类型, 值得参考.
理解类型的规则
char * const * (*next)()
按照上面的规则来理解next的类型
整个来说: next是一个指针, 指向一个函数, 函数的返回值也是一个指针, 指向一个类型为char的常量指针.
更详细的可以参考<<c专家编程>>
类型有什么用?C语言为类型语言, 即每个变量都有类型. 类型在变量的赋值, 函数传参, 编译检查等等方面都会用到.
类型可以确定数据的大小和操作.
例如int a = 3, 那么在内存中会存储一个数据3, 那么对于int类型具体来说.
那么对于指针来说, 其指向类型就非常重要, 指向类型就规定了指针的值所指向的内存的数据是什么类型, 也就是占用多大内存, 可以进行什么操作.
sizeof只要类型确定, 那么便可以用sizeof计算类型占用的内存大小, 这个是编译阶段便可以确定的.
对于指针类型来说, 所有指针类型占用的内存大小基本都是一样的, 例如在32bit的机器上占用4字节, 在64bit的机器上占用8字节.
下面代码的变量a和变量b都是指针类型, 但是指向类型不同. 因此sizeof(a)和sizeof(b)的值相等, 但是sizeof(*a)和sizeof(*b)不相等.
int *a;
double *b;
sizeof(a) == sizeof(b);
sizeof(*a) != sizeof(*b);
指针类型的操作可以对指针变量进行+操作.
double a[3] = {1, 2, 3};
double *b = a;
printf("b: %p, content: %f\n", b, *b);
printf("b+1: %p, content: %f\n", b+1, *(b+1));
int c[3] = {1, 2, 3};
int *d = d;
printf("d: %p, content: %d\n", d, *d);
printf("d+1: %p, content: %d\n", d+1, *(d+1));
运行结果
b: 0x7fff5f9ec7e0, content: 1.000000
b+1: 0x7fff5f9ec7e8, content: 2.000000
d: 0x7fff5f9ec7d0, content: 1
d+1: 0x7fff5f9ec7d4, content: 2
可以看到b+1的值比b要大8. d+1的值比b要大4. b+1实际上是指向a[1]的内存地址. d+1是指向c[1]的内存地址.
有如下公式成立, 指针做加法后的指针变量值和指向类型占用的内存大小相关.
指针变量 + 数字 = 指针变量值 + 数字 * sizeof(指向类型)
可以看到指向类型除了告诉你指针指向的内存里面的数据类型, 在指针变量的相关运算上也是有用的.
数组类型数组与指针有一定的相似, 同时又很不一样.
数组与指针的关键区别在于数组名是一个常量(和const常量不同). const常量表示变量的内容不会变化, 实际上还是一个变量. 这里所说的数组名为一个常量, 可以理解数组名称是一个内存地址值, 例如0×7fff5f9ec7d4.
以下面的例子来说, a本身不会占用内存, 占用内存的是a[0], a[1], a[2], 实际上a所表示的这块内存才是数组变量.
int a[10] = {0};
int *b = a;
int (*d)[10]= &a;
int c;
c = a[1];
c = b[1];
那么a[1]和b[1]的区别就在于数组是一个常量, 而不是变量(变量本身需要占用内存).
执行c = a[1]是直接从a表示的内存地址偏移4字节的内存中取数据. 仅包含一次内存读操作.
执行c = b[1]是首先从内存中取出变量b的值, 然后将变量b的值偏移4字节, 然后从这个地址的内存中取数据. 包含2次内存读操作. 第一次是读取变量b的值.
数组的其他几个需要注意的地方
可以看到有的时候a看作一个数组(例如sizeof(a)是计算数组的内存占用), 有时候a看作一个地址常量(例如计算sizeof(*a)和a+1的时候). 还有的时候完全是比较特殊的使用(例如&a得到的指向类型为int [10]的地址常量).
函数指针函数名本身也是一个地址常量, 其指向类型为一个函数. 实际指向的是函数在内存中的指令集合的起始位置.
int foo(int a)
{
return a;
}
int (*p_foo)(int a) = foo;
printf("%d, %d, %d\n", sizeof(foo), sizeof(*foo), sizeof(&foo));
printf("%d, %d\n", sizeof(p_foo), sizeof(*p_foo));
输出值如下:
1, 1, 8
8, 1
以上几点可以认为是函数的特殊情形, 直接记忆.
可以将函数的指令看作是一个unsigned char []的数组. 这样函数名就好像是一个数组名一样, 都是地址常量, 其指向类型为unsigned char类型. 但是函数指令的数组的长度是未知的, 因此编译器默认输出sizeof(foo)为1, sizeof(*foo)相当于是sizeof(unsigned char)为1.
强制类型转换很多时候涉及到指针和强制类型转换就会感觉比较麻烦, 实际上只要抓住类型这个关键点也可以很简单.
强制类型转换的关键是一段内存, 这段内存里面的数据你把它当作什么类型来看待.
double a = 23.456;
int *b = (int *) &a;
那么变量a有一段内存(8个字节), 里面存储了23.456(按标准浮点格式存储). 然后指针b指向这段内存, 而且指针b的指向类型是int, 因此指针b认为这段内存里面存储的是一个int类型的数据.
小结本文内容不算全面, 但关键点都有.
每次看到指针的时候, 记住4个特征, 不管如何进行类型转换, 多少级指针, 是否包含函数指针等, 看到指针 就思考下面4点, 尤其是3,4. 练习多了之后就会发现指针本身不是很难, 难的是怎么判断数据的类型.