征服C指针

疑惑

我这种菜鸟在学习指针的时候往往会有一些困惑

  • 什么是 “指向int的指针”? 指针不是地址吗?怎么还有指向类型的指针呢?
  • scanf在使用 d% 的情况下,变量之前需要加上取地址符 & 才能进行传递,为什么在使用 %s 的时候就可以不加 & 了呢
  • 学习得到将数组名赋值给指针的时候,将指针和数组完全混为一谈:
    • 将没有分配内存区域的指针当做数组进行访问
    • 将指针赋给数组

导致这一系列事件的原因是:

  • C语言砌块的语法
  • 数组和指针之间微妙的兼容性

有经验的程序员会有些疑问:

  • C的声明中,[] 比 * 的优先级高,因此 char *s[10] 这样的声明意为 “指向char的指针的数组” ?
  • 搞不明白double (*p)[3]; 和 void (*func)(int a); 这样的声明到底怎么阅读。
  • int *a中,声明a为 ”指向int的指针”。可是表达式中的指针变量前*却代表其他意思。明明是同样的符号,意义为何不同?
  • int *a和inta[] 在什么情况下可以互换?
  • 空的[] 可以在什么地方使用,它又代表什么意思呢?

目标读者和内容结构􏰕􏰄􏰯􏱶􏱾􏱍􏰌􏱞􏰆

本书的目标读者为:

  • 粗略的学过C语言的基础,但对指针不太理解的人
  • 平时能自如的使用C语言,但实际对指针理解不够深入的人

本书的内容:

  • 1、从基础开始–预备知识
  • 2、做个试验–C是怎样使用内存的
  • 3、解密C的语法–它到底是怎么回事
  • 4、数组和指针的常用用法
  • 5、数据结构–真正的指针使用方法
  • 6、补充

通过printf来亲眼目睹地址的实际值,这不失为理解指针的一个非常简单有效的方式。

对于那些 “尝试学习了C语言,但对指针还不太理解”的人来说,通过自己的机器实际的输出指针的值,可以将对简单的领会地址的概念

阅读本书,让我们做到 知其然知其所以然。

第一章 从基础开始

C是什么样的语言

C语言是一门什么语言?

  • 为了解决眼前问题,由开发现场的人发明的。
  • 虽然使用方便,但看上去不怎么顺眼
  • 如果不熟悉的人稀里糊涂的使用了它,难免会带来悲剧的语言。

C语言的出现

C原本是为了开发Unix才做系统内而设计的语言,但早起打的Unix是由汇编开发出来的。后来经过发展,出现的C语言

不完备和不统一的语法


C语言是开发现场的人们根据自身的需要开发出的的语言所以具备极高的实用性。但反过来从人类工程学的角度来看,他就不是那么完美了。


比如:

1
2
3
if (a = 5) { // 本来应该写成==的地方却写成了=
/* code */
}

相信大家都犯过这种错误吧。

或者:

1
2
3
for (i - 0; i < count; i++) {
/* code */
}

将赋值的‘=’号写成‘-’;或者在使用switch case的时候,也经常发生忘记写breaak的错误。

幸运的是,如今的编译器对于容易犯的语法错误,在很多地方可以给我们警告提示。
因此,不但不能无视这些警告,相反应该提高编译器的警告级别,让编译器替我们指出尽可能多的错误。

C语言是在使用中成长起来的语言,因此,由于很多历史原因遗留的一些奇怪的问题。
具有代表性的有 位运算符“&”和“|”的优先顺序问题

通常,如“==”的比较运算符的优先级要低于那些做计算的运算符。因此:

1
if(a < b + 3)

这样的表达式中,虽然可以不适用括号来写,但是当时用了位运算符的时候,就行不通了。
想要进行 将a和MASK进行按位与运算后的结果,再和b做比较运算

1
if (a & MASK == b)

按照上面的写法,因为&运算符的优先级低于==运算符,所以被解释成了下面这样

1
if (a & (MASK == b))

这是因为在没有“&&”和“||”运算符的时代,使用“&“和”|”来代替而留下的后遗症。

C的理念


C是危险的语言。
尤其是,在几乎所有的C语言实现中,运行时的检查总是不充分的。
比如:数组越界,在C的大部分处理中,总是悄悄的将数据写入,从而破坏了完全不想管的内存区域。


C是抱着“程序员万能”的理念设计出来的。在C的设计中名优先考虑的是:

  • 如何才能简单的实现编译器(而不是让使用C的人们能够简单化的编程)
  • 如何才能让程序员写出能够生成高效率的执行代码的程序(而不是考虑优化编译器,使编译器生成高效率的执行代码)
    而安全问题完全被忽略了。但无论怎样,C语言原本就是“仅仅为了自己使用”而开发出来的语言。

C的主体

下面的单词中,哪些是C语言中规定的保留字:

1
if printf main malloc sizeof

答案是 if 和 sizeof。

“printf和malloc不必多说,连main也不是C的保留字吗?”
请查一查手头的C语言参考书。相信大部分的C入门书籍中都有C语言保留字的列表

相对于把输入输出作为语言自身功能的一部分,C语言将printf() 这样的输入输出功能从语言的主体中分离出来,让它单纯的成为库函数。对于编译器来说,printf() 函数和其他由程序员写的函数并没有什么不同。

C是只能使用标量的语言

对于标量这个词,大家可能有些陌生。
简单地说,标量就是指char、int、double和枚举等树枝类型以及指针。相对的,像数组,结构体和共用体这样的将多个标量进行组合的类型,我们应称之为聚合类型。
提问:

1
if (str == "abc")

这样的代码为什么不能执行预期的动作呢?
对于这样的疑问,通常给出的答案是“这个表达式不是在比较字符串的内容,它只是在比较指针”其实还可以给出另一个答案:


字符串其实就是char类型的数组,也就是说它不是标量,当然在C里面不能用==进行比较了。


如今的C用过以下几个追加的功能,已经能够让我们整合的使用聚合类型了。

  • 结构体的一次性赋值
  • 将结构体作为函数参数值传递
  • 将结构体作为函数返回值返回
  • auto变量的初始化

关于指针

指针究竟是什么

关于“指针”一词,在C语言程序设计中有这样的说明:

指针是一种保存变量地址的变量,在C中频繁的使用。

C语言标准中出现过这句话:
指针类型可由函数类型。对象类型或不完全的类型派生,派生指针类型的类型称为引用类型。指针类型描述一个对象,该类的对象的值提供对该引用类型实体的引用由引用类型T派生的指针类型有时称为“(指向)T的指针”。从引用类型构造指针的过程称为“指针类型的派生”。这些构造派生类型的方法可以递归地应用

第一句话出现了指针类型。
“指针类型”其实不是单独存在的,它是由其它类型派生而成的。以上对标准内容的引用中也提到 “由引用类型T派生的指针类型有时称为‘(指向)T的指针’”

也就是说,实际上存在的类型是“指向int/double/char的指针类型”等等。

因为“指针类型”是类型,所以它和innt类型、double类型一样,也存在“指针类型变量”和“指针类型的值”。糟糕的是,”指针类型”、“指针类型变量”和“指针类型的值”经常被简单的统称为“指针”,所以非常容易造成歧义,这一点需要提高警惕。

要点:
先有“指针类型”。因为有了类型,才有了此类型的变量,才有了值。

比如,在C中,使用int类型表示整数。因为int是类型,所以存在用于保存innt类型的变量,当然也存在int类型的值。

指针类型同样如此,既存在指针类型的变量,也存在指针类型的值。

因此,几乎所有的处理程序中,所谓的“指针类型的值”,实际是指内存的地址。

和指针的第一次接触

下面我们通过实际代码来尝试输出指针的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int main(int argc, char *argv[]) {

int a = 5;
int b = 10;
int *a_p = NULL;

printf("&a = %p\n", &a);
printf("&b = %p\n", &b);

a_p = &a;

printf("&a_p = %p\n", a_p);
printf("a_p = %d\n", *a_p);

*a_p = 19;

printf("&a_p = %p\n", a_p);
printf("a_p = %d\n", *a_p);

return 0;
}

变量不一定按照声明的顺序保存在内存中

要点:

  • 对变量使用&运算符,可以取得该变量的地址。这个地址称为指向该变量的指针。
  • 指针变量保存了指向其他变量的地址的情况下,可以说指针变量指向其变量。
  • 对指针变量运用运算符,就等于同于它指向的变量。如果指针变量指向变量,则 x = x.

补充:
在说明地址概念的时候,通常使用十六进制来表示地址。
如果想要了解地址的真面目,把地址实际的表示出来才是最好的的方式。

指针和地址之间的关系

几乎所有的程序中,所谓的“指针类型的值”,实际是指内存的地址。
对于这句话,有人也许会产生疑问:
一:

  • 归根结底,指针就是地址,地址就是内存中那个被分配的“门牌号”。所以,指针类型和int类型应该是一回事吧。
    实际上,从某种意义来看,这种认识也不无道理。
    其实在很多运行环境中,int类型和指针类型的长度并不相同。

二:

  • 指针就是地址吧,那么,指向int的指针、指向double的指针,他们有什么区别么?,有必要区分它们吗?
    实际上,从某种意义来看,这种说法也有一定道理。

对于大部分的运行环境来说,当程序运行时,不管是指向int的指针,还是指向double的指针,都保持相同的表现形式(偶尔也会有一些运行环境,它们对于指向char的指针和指向int的指针有着不一样的内部表示和位数)。

不仅如此,C还为我们准备了可以指向任何类型的指针类型:void* 类型:

1
2
3
int a = 5;
void a_p;
a_p = &a;

这并不会报错,但是会出现警告:
void* 类型的指针无效

所以需要将地址的数据类型强转为int类型:

1
*(int*)a_p = &a;

在大部分的运行环境里,不管是指向int的指针,还是指向double的指针,在运行时都是相同的事物。可是,通过在int类型的变量去地址锁喉利用指针间接取出来的值,不出意外肯定是int类型。为什么? 因为int和double的内部表示完全不同。

因此,如今的运行环境,像下面这样取得指向double类型变量的指针,之后将其赋给指向int的指针变量,编译器必定会提示警告。
伪代码:

1
2
3
int int_p;
double double_p;
int_p = &double_value;

将指向double变量的指针赋予指向int的指针变量会出现警告。