指针与名称空间
本篇为机器人协会新生培训所用.
让我们来考虑一个问题: > 如何写一个函数来交换两个变量的值?
#include <stdio.h>
int swap_value(int a, int b);
int main(void) {
int a = 1, b = 2;
swap_value(a, b);
return 0;
}
int swap_value(int a, int b) {
int temp = a;
a = b;
b = temp;
return ???;
}
再来看一段代码:
#include <stdio.h>
void change_value(int value);
int main(void) {
int a = 1;
printf("now a is %d\n", a);
change_value(a);
printf("then a is %d\n", a);
return 0;
}
void change_value(int value) {
value += 1;
}
// 输出如下:
// now a is 1
// then a is 1
a
的值并没有改变, 因为C语言采取切仅采用 按值传递 的方式, 也即值在函数与函数之间传递时, 得到的仅仅是变量的备份, 并得到变量本身.
在 change_value(a)
中, 系统做了:
- 建立一个
int
类型的局部变量value
- 把它赋值为
a
- 把
value
加1
整个过程都没有对 a
本身进行操作.
如果我们非要用另外的一个函数去改变 a
的值该怎么做呢? 既然无法直接传递 a
那么能否传递一个可以链接 a
的东西, 然后通过这个东西去修改 a
呢?
答案当然是可以, 这个东西就叫做 指针(pointer)
给大家画了一张图:
图中的代码片段 *pt = &a;
出现了 *
和 &
, 两个新的操作符, &
表示变量的地址, *
我们稍后再说.
现在我们利用指针对 change_value(a)
进行修改
#include <stdio.h>
void change_value(int *value);
int main(void) {
int a = 1;
int *pt = &a;
printf("now a is %d\n", a);
change_value(pt);
printf("then a is %d\n", a);
return 0;
}
void change_value(int *value) {
*value += 1;
}
// 输出如下:
// now a is 1
// then a is 2
我们再来继续摆弄一下这个函数:
#include <stdio.h>
void change_value(int *value);
int main(void) {
int a = 1;
int *pt = &a;
printf("the address of a is %p\n", &a);
printf("the address of pt is %p\n", &pt);
printf("the value of a is %d\n", a);
printf("the value of pt is %p\n", pt);
printf("the value that pt points %d\n", *pt);
puts("now will run change_value(pt)");
change_value(pt);
puts("then");
printf("the address of a is %p\n", &a);
printf("the address of pt is %p\n", &pt);
printf("the value of a is %d\n", a);
printf("the value of pt is %p\n", pt);
printf("the value that pt points %d\n", *pt);
return 0;
}
void change_value(int *value) {
*value += 1;
}
// 输出如下:
// the address of a is 000000000061FE4C
// the address of pt is 000000000061FE40
// the value of a is 1
// the value of pt is 000000000061FE4C
// the value that pt points 1
// now will run change_value(pt)
// then
// the address of a is 000000000061FE4C
// the address of pt is 000000000061FE40
// the value of a is 2
// the value of pt is 000000000061FE4C
// the value that pt points 2
显然, *
是用来表示 pt
所指的那个地址上面的值, 可是, 真的这么简单吗?
回顾一下上面第7行的 int *pt = &a;
我们来推理一下
*
是用来表示pt
所指的那个地址上面的值pt
所指的那个地址是a
的地址*pt
表示a
的值&a
表示a
的地址*pt = &a
把a
的值变成了a
的地址
这个推理结果和输出 the value of a is 1
矛盾.
那么 *
到底是什么呢? 在大部分情况下, 它都表示 pt
所指的那个地址上面的值, 只有在声明变量的时候, 是用来表示 pt
是个指针. 我们也可以将
写成
不过下面那种写法在C语言中使用的比较少, 而在C++中使用的比较多, 不过这只是风格问题.
现在请动手完成我们最开始提出的问题, 写一个函数交换两个变量的值.
在指针的角度看数组
我们来回顾一下声明数组
再来看看他们的地址
#include <stdio.h>
int main(void) {
int nice_array[10] = {0};
char nicer_array[10] = "hello";
int aa = 0;
for (aa = 0; aa < 10; aa++) {
printf("the address of nice_array[%d] is %p\n", aa, &nice_array[aa]);
}
puts("--------- I am a split line -----------");
for (aa = 0; aa < 10; aa++) {
printf("the address of nicer_array[%d] is %p\n", aa, &nicer_array[aa]);
}
return 0;
}
// 输出如下:
// the address of nice_array[0] is 000000000061FE20
// the address of nice_array[1] is 000000000061FE24
// the address of nice_array[2] is 000000000061FE28
// the address of nice_array[3] is 000000000061FE2C
// the address of nice_array[4] is 000000000061FE30
// the address of nice_array[5] is 000000000061FE34
// the address of nice_array[6] is 000000000061FE38
// the address of nice_array[7] is 000000000061FE3C
// the address of nice_array[8] is 000000000061FE40
// the address of nice_array[9] is 000000000061FE44
// --------- I am a split line -----------
// the address of nicer_array[0] is 000000000061FE16
// the address of nicer_array[1] is 000000000061FE17
// the address of nicer_array[2] is 000000000061FE18
// the address of nicer_array[3] is 000000000061FE19
// the address of nicer_array[4] is 000000000061FE1A
// the address of nicer_array[5] is 000000000061FE1B
// the address of nicer_array[6] is 000000000061FE1C
// the address of nicer_array[7] is 000000000061FE1D
// the address of nicer_array[8] is 000000000061FE1E
// the address of nicer_array[9] is 000000000061FE1F
可以看到 nice_array
的内存时连续的. 而 nice_array
的地址步长为4, 这是因为 int
类型占4个字节(32位). 所以总的来说, 数组的内存时连续的.
再来看一个大胆的想法:
#include <stdio.h>
int main(void) {
int nice_array[10] = {0};
char nicer_array[10] = "hello";
int aa = 0;
for (aa = 0; aa < 10; aa++) {
printf("the address of nice_array[%d] is %p\n", aa, nice_array + aa);
}
puts("--------- I am a split line -----------");
for (aa = 0; aa < 10; aa++) {
printf("the address of nicer_array[%d] is %p\n", aa, nicer_array + aa);
}
return 0;
}
// 输出如下:
// the address of nice_array[0] is 000000000061FE20
// the address of nice_array[1] is 000000000061FE24
// the address of nice_array[2] is 000000000061FE28
// the address of nice_array[3] is 000000000061FE2C
// the address of nice_array[4] is 000000000061FE30
// the address of nice_array[5] is 000000000061FE34
// the address of nice_array[6] is 000000000061FE38
// the address of nice_array[7] is 000000000061FE3C
// the address of nice_array[8] is 000000000061FE40
// the address of nice_array[9] is 000000000061FE44
// --------- I am a split line -----------
// the address of nicer_array[0] is 000000000061FE16
// the address of nicer_array[1] is 000000000061FE17
// the address of nicer_array[2] is 000000000061FE18
// the address of nicer_array[3] is 000000000061FE19
// the address of nicer_array[4] is 000000000061FE1A
// the address of nicer_array[5] is 000000000061FE1B
// the address of nicer_array[6] is 000000000061FE1C
// the address of nicer_array[7] is 000000000061FE1D
// the address of nicer_array[8] is 000000000061FE1E
// the address of nicer_array[9] is 000000000061FE1F
这个结果回答了三个问题:
- 数组名既是数组首地址的指针
- 指针是可以进行加法运算的(请思考: 减法呢?乘除呢?)
- 声明指针时需要说明所指的数据类型, 因为不同的数据类型所占用的内存长度不同
于是我们又有了两个问题:
int
指针和char
有什么区别吗?- 既然指针的值是且仅是一个地址, 那么
int
指针怎么知道要读取四个字节的内存, 而char
又怎么知道只要读取一个字节?
变量存在时期
一般的变量仅在声明它的最近的大括号内有效, 这个大括号, 我们可以理解为变量的名称空间. 但是有两种特殊的变量修饰符需要我们注意,
static
static
修饰的变量会放在静态变量区, 其只在第一次使用时被初始化, 名称空间也不仅限于大括号中.
#include <stdio.h>
int it_will_add(void);
int main(void) {
int aa;
for (aa = 0; aa < 5; aa++) {
it_will_add();
}
return 0;
}
int it_will_add(void) {
static int static_value = 0;
int normal_value = 0;
printf("static_value is %d\n", static_value++);
printf("normal_value is %d\n", normal_value++);
printf("-----------------\n");
return 0;
}
// 输出如下:
// static_value is 0
// normal_value is 0
// -----------------
// static_value is 1
// normal_value is 0
// -----------------
// static_value is 2
// normal_value is 0
// -----------------
// static_value is 3
// normal_value is 0
// -----------------
// static_value is 4
// normal_value is 0
// -----------------
extern
另一个是 extern
, 用于引用再别的文件中声明的变量. 这东西, 在一个文件的时候没用, 在两个以上文件的工程才有作用. 还是先来看一个例子
// fileA.c
#include <stdio.h>
int say_hello(void);
int how_many_hello(void);
int say_hello_time = 0; // 声明变量为之分配储存空间
int say_hello(void) {
puts("hello");
say_hello_time++;
return 0;
}
int how_many_hello(void) {
printf("I have said %d \"hello\"\n", say_hello_time);
return 0;
}
我们在 fileA.c
中声明了 say_hello_time
这个变量. 接下来, 让我们在另外一个文件中引用这个变量
// fileB.c
#include <stdio.h>
extern int say_hello_time; // 引用外部变量
int main(void) {
puts("I am main");
say_hello();
how_many_hello();
say_hello();
how_many_hello();
puts("I can call ay_hello_time directly:");
printf("say_hello_time: %d\n", say_hello_time);
return 0;
}
// 输出如下:
// I am main
// hello
// I have said 1 "hello"
// hello
// I have said 2 "hello"
// I can call ay_hello_time directly:
// say_hello_time: 2
上面这段程序是最直接的写法, 但是不是最常用的写法. 在工程中, 一般我们并不会把函数原型放在 .c
文件中, 也不会把外部变量的声明和变量声明放在不同的文件中, 因为这样可能会出现声明冲突的问题. 取而代之, 我们将函数原型, 外部变量声明, 类型定义等需要被外部引用的定义写入 .h
文件中, 将函数主体, 变量声明写入 .c
文件中, 再将 .c
文件加入工程, 而 .h
文件则用 #include
引用.
因而我们可以改进成如下样子:
// fileA.h
#ifndef FILEA_H_
#define FILEA_H_
int say_hello(void);
int how_many_hello(void);
extern int say_hello_time; // 声明为外部变量
#endif
// fileA.c
#include <stdio.h>
#include "fileA.h"
int say_hello_time = 0; // 声明变量为之分配储存空间
int say_hello(void) {
puts("hello");
say_hello_time++;
return 0;
}
int how_many_hello(void) {
printf("I have said %d \"hello\"\n", say_hello_time);
return 0;
}
// fileB.c
#include <stdio.h>
#include "fileA.h"
int main(void) {
puts("I am main");
say_hello();
how_many_hello();
say_hello();
how_many_hello();
puts("I can call ay_hello_time directly:");
printf("say_hello_time: %d\n", say_hello_time);
return 0;
}
在 .h
文件中, 我们常用
来判断和建立文件标记, 以防止重复引用导致预处理器陷入死循环.
最后在强调一遍, 文件需要被添加在工程中. 在 C-Free5 中, 我们可以通过 工程-新建
来新建一个 Win32控制台
工程.