分清以下几个概念

  1. 指针数组 数组指针
  2. 指针函数 函数指针
  3. 指针常量 常量指针
  4. typedef
  5. #define
  6. 可变参数
  7. 字符串常量

内容主要来自《C专家编程》《C陷阱和缺陷》《C和指针》

常量和指针

首先看比较容易理解的常量指针和指针常量

int const a;
const int a;

首先看如上声明,const 在前还是在后表示的意思都是一样的,表示a是一个常量 类型为int,a的值初始化后就不能修改了

int const *pi;
const int *pi;

常量指针有2种表示方法意思都是一样的, 以上2个都表示常量指针, 可以修改指针的值但不能修改指针指向的值,这个在函数库的原型里可以见到比如printf和scanf, 后面的...表示可变参数后面会讲到

int	 printf(const char * __restrict, ...) 
int	 scanf(const char * __restrict, ...)

再来看看指针常量的声明,一般只有一种表示方法 如下

int * const pi;

表示pi 这个指针是常量,pi的值不能修改,但是pi指向的值是可以修改的

如果既不想改变指针的值也不想改变指针指向的值,可以声明如下

int const * const pi;

指针和数组的爱恨情仇

指针和数组他们之间没有任何关系

  • 指针就是指针,指针变量在32位系统下,永远占4个byte,其值为某一个内存的地址。指针可以指向任何地方,但是不是任何地方你都能通过这个指针变量访问到。
  • 数组就是数组,其大小与元素的类型和个数有关。定义数组时必须指定其元素的类型和个数。数组可以存放任何类型的数据,但不能存函数。

如何分辨数组指针和指针数组

  • 指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身决定。它是“存储指针的数组”的简称
  • 数组指针:首先它是一个指针,他指向一个数组。在32位系统下永远是占4个字节,至于它指向的数组占多少个字节,不知道。它是“指向数组的指针”的简称

考虑下,下面2个哪个是指针数组,哪个是数组指针

A). int *p1[10];
B). int (*p2)[10];

根据右左法则,先找到未定义标识符,p1右边是[]因为[]比*的优先级高,p1先与[]结合,所以它是一个数组。int *修饰的是数组的内容。
再看p2,因为"()"比"[]"优先级高,所以(*p2)表示这是一个指针,指针变量名为p2,int修饰的数组内容,即数组的每个元素。数组在这里没有名字,是匿名数组。所以p2是一个数组指针,指向一个包含10个int类型数据的数组。

多维数组和多级指针

考虑下面一个例子
&p[4][2]-&a[4][2]的值为多少

#include <stdio.h>
int main()
{
    int a[5][5];
    int (*p)[4];
    p = a;
    printf("a_ptr=%#p,p_ptr=%#p\n",&a[4][2],&p[4][2]);
    printf("%p,%d\n",&p[4][2]-&a[4][2],&p[4][2]-&a[4][2]);
    return 0;
}

运行结果如下:

文字解析,不如看图来的好

数组参数与指针参数

一维数组参数

能否向函数传递一个数组?

考虑以下程序

#include <stdio.h>

void fun(char a[10])
{
    char c = a[3];
}

int main(void)
{
    char b[10] = "abcdefg";
    fun(b[10]);
    return 0;
}

上面的调用,fun(b[10]);将b[10]这个数组传递到fun函数。这样显然不对。
b[0]代表是数组的一个元素,那么b[10]也是,只是这里越界了。但在编译阶段,编译器并不会真正计算b[10]的地址并取值,所在在编译阶段不会报错,但是编译器会给出警告:

//在我的Mac上编译出现这个
warning: incompatible integer to pointer conversion passing 'char' to parameter of type 'char *'; take the address with & [-Wint-conversion]  
warning: array index 10 is past the end of the array (which contains 10 elements) [-Warray-bounds]
//在作者的电脑上给出的警告是这样的  
warning C4047: 'function' : 'char *' differs in levels of indirection from 'char '
warning C4024: 'fun' : different types for formal and actual parameter 1

虽然编译没问题,但运行肯定有问题。
具体分析如下:

  1. b[10]并不存在,在编译的时候由于没有去实际地址取值,所以没有出错,但是在运行时,将计算b[10]的实际地址,并且取值。这时发生越界错误
  2. 编译器的警告已经告诉我们编译器需要的是一个char*类型的参数,而传递过去的是一个char类型的参数,这时候fun函数会将传入的char类型的数据当地址处理,同样会发生错误
  3. 对第二个错误的理解,fun函数明明传递的是一个数组啊,编译器怎么会说是char*类型呢?

将fun(b[10])改为fun(b),编译后不会出现警告了。

无法向函数传递一个数组

可以简单验证下,代码如下

#include <stdio.h>

void fun(char a[10])
{
    int i=sizeof(a);
    printf("i:%d\n",i);
    char c=a[3];
}

int main(void)
{
    char b[10] = "abcdefg";
    int j = sizeof(b);
    printf("j:%d\n",j);
    fun(b);
    return 0;
}


如果数组b真正传递到函数内部,那么i的值应该为10,但我测试后发现i的值为8(作者测的值为4)。造成这样的原因是这样一条规则:

  • C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针

这么做是有原因的。在 C 语言中,所有非数组形式的数据实参均以传值形式(对实参 做一份拷贝并传递给被调用的函数,函数不能修改作为实参的实际变量的值,而只能修改 传递给它的那份拷贝)调用。然而,如果要拷贝整个数组,无论在空间上还是在时间上, 其开销都是非常大的。更重要的是,在绝大部分情况下,你其实并不需要整个数组的拷贝, 你只想告诉函数在那一刻对哪个特定的数组感兴趣。这样的话,为了节省时间和空间,提 高程序运行的效率,于是就有了上述的规则。同样的,函数的返回值也不能是一个数组, 而只能是指针。这里要明确的一个概念就是:函数本身是没有类型的,只有函数的返回值 才有类型。很多书都把这点弄错了,甚至出现“XXX 类型的函数”这种说法。简直是荒唐 至极!

所以我们写代码的时候,可以写成这样

#include <stdio.h>
void fun(char a[])
{
    char c=a[3];
}

int main(void)
{
    char b[10] = "abcdefg";
    fun(b);
    return 0;
}

或者也可以

void fun(char *p)
{
    char c=p[3];
}

函数和指针

指针函数首先是一个函数,返回的类型是一个指针形式的
函数指针是一个指针,指向的是一个函数

首先看下函数声明 任何C变量的声明都由两部分组成:

  • 类型以及一组类似表达式的声明符(declarator)。声明符从表面上看与表达式有些类似,对它求值应该返回一个声明中给定类型的结果

最简单的声明符就是单个变量:

float f, g;

这个声明的含义:

  • 当对其求值时,表达式f和g的类型为浮点数类型(float)。

因为声明符与表达式的相似,所以我们也可以在声明符中任意使用括号:

float ((f));

这个声明的含义是:

  • 当对其求值时,((f))的类型为浮点型,由此可以推知,f也是浮点类型。

同样的逻辑也适合用于函数和指针类型的声明,例如:

float ff();

这个声明的含义是:

  • 表达式ff()求值结果是一个浮点数,也就是说,ff是一个返回值为浮点数类型的函数。

类似的:

float *pf;
  • 这个声明的含义是*pf是一个浮点数,也就是说,pf是一个指向浮点数的指针

以上形式组合起来:

float *g(), (*h)();
  • 表示*g()与(*h)()是浮点表达式。因为()结合优先级高于*,*g()也就是*(g()): g是一个函数,该函数的返回值类型为指向浮点数的指针。同理,h是一个函数指针,h所指向函数的返回值为浮点类型。

有了以上的说明 函数指针 指针函数也就能明白了

int *func(int a, int b);

表示 func是一个函数 其返回值是int *,所以是指针函数

int (*func)(int a, intb);

首先 func是一个指针 该指针指向一个函数 返回值是int 类型的,在使用的时候需要把一个函数的地址传给他

函数指针的使用经常用于回调函数
下面就是一个简单的回调函数使用

#include <stdio.h>
typedef int (*callback)(char *p);

int fun1(char *str){
    printf("回调 func1: %s\n",str);
    return 0;
}

int fun2(char *str){
    printf("回调 func2: %s\n",str);
    return 0;
}

int t1(callback p_callback, char *str){
    p_callback(str);
    return 0;
}


int main() {
    char *str = "hi!";
    t1(fun1, str);
    return 0;
}

typedef的使用

typedef 常常用于为类型创建一个新的别名
比如:

typedef char Line[12];
Line text; // 这行的意思就是 char text[12];

使用typedef 主要是简化代码比如上面的回调函数

typedef int (*callback)(char *p)   

使用的时候就可是使用如下方法

callback call;  

typedef的另外一个用处就是定义机器无关的类型,促进跨平台

宏定义

宏只是简单的字符串替换
通过使用 -E 参数查看宏展开后的结果

gcc -E main.c -o main.i

比如如下代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MALLOC(size,type) ((type*)malloc(sizeof(type) * (size)))
#define PRINT(FORMAT,VALUE)\
printf("The value of"#VALUE"is " FORMAT"\n",VALUE)
#define M 10
#define SQ(x) ((x)*(x))
#define DEBUG printf("FIle %s line %d :"\
                    "x = %d, y = %d \n",\
                    __FILE__, __LINE__,10,20)
#define MAX(a,b) ((a)>(b)?(a):(b))
#define ERROR_LOG(module)   fprintf(stderr,"error: "module"\n")
int main() {
    int n = M;
    printf("%d\n",n);
    DEBUG;
    int b = 121/SQ(2);
    printf("%d\n",b);
    int *p = MALLOC(10,int);
    int a = 0;
    int c = 1;
    int d = MAX(a++,c++);
    int x = 1;
    PRINT("%d",x+20);
    ERROR_LOG("add");
    return 0;
}

那么展开后的结果如下
省略一大段
int main() {
    int n = 10;
    printf("%d\n",n);
    printf("FIle %s line %d :" "x = %d, y = %d \n", "main.c", 17,10,20);
    int b = 121/((2)*(2));
    printf("%d\n",b);
    int *p = ((int*)malloc(sizeof(int) * (10)));
    int a = 0;
    int c = 1;
    int d = ((a++)>(c++)?(a++):(c++));
    int x = 1;
    printf("The value of""x+20""is " "%d""\n",x+20);
    fprintf(__stderrp,"error: ""add""\n");
    return 0;
}

可变参数

#include <stdio.h>
#include <stdarg.h>

int fun(int n, ...)
{
    va_list var_arg;
    int sum = 0;
    int i;

    va_start(var_arg, n); 
    for(i = 0; i < n; i+=1)
    {   
        sum += va_arg(var_arg, int);
    }   

    va_end(var_arg);
    return sum;
}

int main()
{
    int n = 5;
    int sum = fun(n,1,2,3,4,5);
    printf("%d\n",sum);
    return 0;
}

字符串常量

字符串常量用于表达式中,为指针常量
对于如下的程序

#include <stdio.h>
int main() {
    char *str = "hello"+1;
    printf("%c\n",*str);
}

运行结果是 e
字符串常量实际上是个指针,加1的话结果是个指针,指向第二个字符,所以结果是e