分清以下几个概念
- 指针数组 数组指针
- 指针函数 函数指针
- 指针常量 常量指针
- typedef
- #define
- 可变参数
- 字符串常量
内容主要来自《C专家编程》《C陷阱和缺陷》《C和指针》
常量和指针
首先看比较容易理解的常量指针和指针常量
1 | int const a; |
首先看如上声明,const 在前还是在后表示的意思都是一样的,表示a是一个常量 类型为int,a的值初始化后就不能修改了
1 | int const *pi; |
常量指针有2种表示方法意思都是一样的, 以上2个都表示常量指针,
可以修改指针的值但不能修改指针指向的值,这个在函数库的原型里可以见到比如printf和scanf,
后面的…表示可变参数后面会讲到
1 | int printf(const char * __restrict, ...) |
再来看看指针常量的声明,一般只有一种表示方法
如下
1 | int * const pi; |
表示pi 这个指针是常量,pi的值不能修改,但是pi指向的值是可以修改的
如果既不想改变指针的值也不想改变指针指向的值,可以声明如下
1 | int const * const pi; |
指针和数组的爱恨情仇
指针和数组他们之间没有任何关系
- 指针就是指针,指针变量在32位系统下,永远占4个byte,其值为某一个内存的地址。指针可以指向任何地方,但是不是任何地方你都能通过这个指针变量访问到。
- 数组就是数组,其大小与元素的类型和个数有关。定义数组时必须指定其元素的类型和个数。数组可以存放任何类型的数据,但不能存函数。
如何分辨数组指针和指针数组
- 指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身决定。它是“存储指针的数组”的简称
- 数组指针:首先它是一个指针,他指向一个数组。在32位系统下永远是占4个字节,至于它指向的数组占多少个字节,不知道。它是“指向数组的指针”的简称
考虑下,下面2个哪个是指针数组,哪个是数组指针根据右左法则,先找到未定义标识符,p1右边是[]因为[]比
1
2A). int *p1[10];
B). int (*p2)[10];*
的优先级高,p1先与[]结合,所以它是一个数组。int *
修饰的是数组的内容。
再看p2,因为”()”比”[]”优先级高,所以(*p2)
表示这是一个指针,指针变量名为p2,int修饰的数组内容,即数组的每个元素。数组在这里没有名字,是匿名数组。所以p2是一个数组指针,指向一个包含10个int类型数据的数组。
多维数组和多级指针
考虑下面一个例子
&p[4][2]-&a[4][2]的值为多少
1 | #include <stdio.h> |
运行结果如下:
文字解析,不如看图来的好
数组参数与指针参数
一维数组参数
能否向函数传递一个数组?
考虑以下程序
1 | #include <stdio.h> |
上面的调用,fun(b[10]);将b[10]这个数组传递到fun函数。这样显然不对。
b[0]代表是数组的一个元素,那么b[10]也是,只是这里越界了。但在编译阶段,编译器并不会真正计算b[10]的地址并取值,所在在编译阶段不会报错,但是编译器会给出警告:
1 | //在我的Mac上编译出现这个 |
虽然编译没问题,但运行肯定有问题。
具体分析如下:
- b[10]并不存在,在编译的时候由于没有去实际地址取值,所以没有出错,但是在运行时,将计算b[10]的实际地址,并且取值。这时发生越界错误
- 编译器的警告已经告诉我们编译器需要的是一个char*类型的参数,而传递过去的是一个char类型的参数,这时候fun函数会将传入的char类型的数据当地址处理,同样会发生错误
- 对第二个错误的理解,fun函数明明传递的是一个数组啊,编译器怎么会说是char*类型呢?
将fun(b[10])改为fun(b),编译后不会出现警告了。
无法向函数传递一个数组
可以简单验证下,代码如下
1 | #include <stdio.h> |
如果数组b真正传递到函数内部,那么i的值应该为10,但我测试后发现i的值为8(作者测的值为4)。造成这样的原因是这样一条规则:
- C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针
这么做是有原因的。在 C 语言中,所有非数组形式的数据实参均以传值形式(对实参
做一份拷贝并传递给被调用的函数,函数不能修改作为实参的实际变量的值,而只能修改
传递给它的那份拷贝)调用。然而,如果要拷贝整个数组,无论在空间上还是在时间上,
其开销都是非常大的。更重要的是,在绝大部分情况下,你其实并不需要整个数组的拷贝,
你只想告诉函数在那一刻对哪个特定的数组感兴趣。这样的话,为了节省时间和空间,提
高程序运行的效率,于是就有了上述的规则。同样的,函数的返回值也不能是一个数组,
而只能是指针。这里要明确的一个概念就是:函数本身是没有类型的,只有函数的返回值
才有类型。很多书都把这点弄错了,甚至出现“XXX 类型的函数”这种说法。简直是荒唐
至极!
所以我们写代码的时候,可以写成这样
1 | #include <stdio.h> |
或者也可以
1 | void fun(char *p) |
函数和指针
指针函数首先是一个函数,返回的类型是一个指针形式的
函数指针是一个指针,指向的是一个函数
首先看下函数声明
任何C变量的声明都由两部分组成:
- 类型以及一组类似表达式的声明符(declarator)。声明符从表面上看与表达式有些类似,对它求值应该返回一个声明中给定类型的结果
最简单的声明符就是单个变量:
1 | float f, g; |
这个声明的含义:
- 当对其求值时,表达式f和g的类型为浮点数类型(float)。
因为声明符与表达式的相似,所以我们也可以在声明符中任意使用括号:
1 | float ((f)); |
这个声明的含义是:
- 当对其求值时,((f))的类型为浮点型,由此可以推知,f也是浮点类型。
同样的逻辑也适合用于函数和指针类型的声明,例如:
1 | float ff(); |
这个声明的含义是:
- 表达式ff()求值结果是一个浮点数,也就是说,ff是一个返回值为浮点数类型的函数。
类似的:
1 | float *pf; |
- 这个声明的含义是*pf是一个浮点数,也就是说,pf是一个指向浮点数的指针
以上形式组合起来:
1 | float *g(), (*h)(); |
- 表示
*g()
与(*h)()
是浮点表达式。因为()结合优先级高于*,*g()
也就是*(g()): g是一个函数,该函数的返回值类型为指向浮点数的指针。同理,h是一个函数指针,h所指向函数的返回值为浮点类型。
有了以上的说明 函数指针 指针函数也就能明白了
1 | int *func(int a, int b); |
表示 func是一个函数 其返回值是int *,所以是指针函数
1 | int (*func)(int a, intb); |
首先 func是一个指针 该指针指向一个函数 返回值是int 类型的,在使用的时候需要把一个函数的地址传给他
函数指针的使用经常用于回调函数
下面就是一个简单的回调函数使用
1 | #include <stdio.h> |
typedef的使用
typedef 常常用于为类型创建一个新的别名
比如:
1 | typedef char Line[12]; |
使用typedef 主要是简化代码比如上面的回调函数
1 | typedef int (*callback)(char *p) |
使用的时候就可是使用如下方法
1 | callback call; |
typedef的另外一个用处就是定义机器无关的类型,促进跨平台
宏定义
宏只是简单的字符串替换
通过使用 -E 参数查看宏展开后的结果
1 | gcc -E main.c -o main.i |
比如如下代码
1 | #include <stdio.h> |
可变参数
1 | #include <stdio.h> |
字符串常量
字符串常量用于表达式中,为指针常量
对于如下的程序
1 | #include <stdio.h> |
运行结果是 e
字符串常量实际上是个指针,加1的话结果是个指针,指向第二个字符,所以结果是e