JOE'S BLOG

好记性不如烂键盘

0%

C语言中几个基本概念

分清以下几个概念

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

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

常量和指针

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

1
2
int const a;
const int a;

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

1
2
int const *pi;
const int *pi;

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

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

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

1
int * const pi;

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

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

1
int const * const pi;

指针和数组的爱恨情仇

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

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

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

  • 指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身决定。它是“存储指针的数组”的简称
  • 数组指针:首先它是一个指针,他指向一个数组。在32位系统下永远是占4个字节,至于它指向的数组占多少个字节,不知道。它是“指向数组的指针”的简称 考虑下,下面2个哪个是指针数组,哪个是数组指针
    1
    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]的值为多少

1
2
3
4
5
6
7
8
9
10
#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;
}

运行结果如下:

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

数组参数与指针参数

一维数组参数

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

考虑以下程序

1
2
3
4
5
6
7
8
9
10
11
12
13
#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]的地址并取值,所在在编译阶段不会报错,但是编译器会给出警告:

1
2
3
4
5
6
7
//在我的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),编译后不会出现警告了。

无法向函数传递一个数组

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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 类型的函数”这种说法。简直是荒唐
至极!

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

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
void fun(char a[])
{
char c=a[3];
}

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

或者也可以

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

函数和指针

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

首先看下函数声明
任何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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#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 常常用于为类型创建一个新的别名
比如:

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

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

1
typedef int (*callback)(char *p)   

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

1
callback call;  

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

宏定义

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

1
gcc -E main.c -o main.i

比如如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#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;
}

可变参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#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;
}

字符串常量

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

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

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