c和指针

1. 快速上手

1.1.1 空白和注释

空白 - 清晰的显示程序的结构

空行将程序的不同部分分割开来;制表符(tab)用于缩进语句,更好的显示程序的结构等等。

C是一种自由格式的语言,并没有规则要求你必须怎样书写语句。

然而,在编写程序时遵循一些约定是非常值得的,它可以使你的代码更加容易阅读和修改。

注释 - 告诉读者程序能做什么,以及怎么做

1
2
3
4
/*
** 这个程序从标准输入中读取输入行并在标准输出中打印这些行
** 每个输入行的后面一行是该行内容的一部分
*/

在有些语言中允许通过/**/来注释掉不需要的代码,但是在C中注释并不能嵌套

从逻辑上删除一段代码更好的办法是:

1
2
3
#if 0
statements
#endif

在 #if 和 #endif 之间的程序段就可以从程序中去除。

1.1.2 预处理指令

由预处理器读入源代码并用预处理器解释的指令。

#include 和 #define

1
2
3
#include <stdio.h>
#include <stdlib.h>
#define MAX_COLS 20

在这段程序中,预处理器将 stdio.h 的内容逐字写到源文件预处理指令的位置。

TIP:

如果有许多不同的文件需要相同的一些声明,可以在单独的文件中编写这些声明,然后用 #include 将这个文件包含到需要声明的文件源代码中去。

另一种预处理器指令是 #define ,它把名字 MAX_COLS 定义为 20,当这个名字出现在源文件的任何地方时,它就会被替换为定义的值。这些名字一般都 大写 。这样做的好处是易于修改 MAX_COLS 的值而不必去源代码挨个替换。

函数原型

1
2
int read_column_numbers( int columns[], int max);
void rearrange( char *output, char const *input, int n_columns, int const columns[]);

这些声明为 函数原型 。他们告诉编译器这些以后将在源文件中定义的函数特征,便于准确性检查。

原型以一个类型名开头,表示返回值的类型。跟在其后的是函数名,在后面是函数期望接受的参数。

columns 是一个数组,output 和 input是一个指针,带有 const 的参数表示函数将不会修改传递的这两个参数。

void表示无返回值,在某些语言里,无返回值的函数称为过程。

TIP:

假如一个程序的函数被多个子文件使用,那么子文件中要有 #include somefunc指令来包含这些函数的原型

1.1.3 main函数

1
2
3
4
5
6
7
8
9
10
11
12
int main( void )
{
char input[MAX_INPUT];
/*
** 读取,处理和打印剩余的输入行。
*/
while( gets( input ) != NULL ){
printf( "Original input : %s \n", input );
}

return EXIT_SUCCESS;
}

软件开销的最大指出并非在于编写,而是在于维护。

gets函数

gets 函数从标准输入读取一行文本并把它存储于作为参数传入的数组中。

一行输入由一串字符组成,由一个换行符结尾。

当 gets 读取到换行符时,会在该行的末尾存储一个 NUL 值。

当 gets 调用但是找不到输入行时,它就会返回一个 NULL 值。

字符串常量

字符串常量就是用 “ “ 括起来的一串字符

例如 “Hello”

在内存中占据六个字节,分别为 H,e,l,l,o 和 NUL。

TIP:

​ NULL在 stdio.h 中定义,并不存在预定义的符号 NUL ,如果想使用它而不是字符常量 ‘\0’ ,就得自行定义。

print函数

print 函数接受多个参数

第一个参数为字符串,描述输出的格式

剩余参数就是需要打印的值。格式常常以一些字符串常量形式出现。

格式 含义
%d 以十进制的形式打印一个整形值
%o 以八进制的形式打印一个整形值
%x 以十六进制的形式打印一个整形值
%g 打印一个浮点值
%c 打印一个字符
%s 打印一个字符串
\n 换行

在最后循环结束时,main 函数返回值 EXIT_SUCCESS ( 在 stdlib.h 中定义 )。该值向操作系统提示程序成功执行,右花括号标志着 main 函数体的结束。

1.1.4 自定义函数

1
2
3
4
5
/*
** 读取列标号,如果超出规定范围则不予理会
*/

int read_column_numbers( int columns[], int max);

参数数组并没有申明长度,这是一个伟大的特性,它允许同一个函数操作任意长度的一维数组。

但是也有弊处,在于当你需要知道数组的长度时,就必须用其他参数传入

scanf 函数

1
scanf( "%d", &columns[num] );

scanf 类似于 printf ,能接受多个参数,第一个是一个格式字符串,用于描述期望的输入类型。

剩余的几个都是变量,用于储存输入的值。

如果读取失败,如类型不对或者找不到,则返回 0 。

读取成功则返回 1 。

警告

所有标量参数的前面一定要加上一个 “ & “ 字符,但是引用值就不用加。加也没什么不对。

几乎所有的格式码( %c 除外 )输入值之前的空白( 空格,制表符,换行符 )会被跳过,值后面的空白会被表示为该值的结束。

所以 %s 并不能读取空白。

printf 和 scanf 格式代码并不完全一样!

格式 含义 变量类型
%d 读取一个整形值 int
%ld 读取一个长整型值 long
%f 读取一个实型值(浮点数) float
%lf 读取一个双精度实型值 double
%c 读取一个字符 char
%s 从输入读取一个字符串 char 型数组

TIP

标准并未硬性规定 C 编译器对数组下标的有效性进行检查。

puts函数

是 gets 函数的输出版本,把指定的字符串写到标准输入中去并在末尾添上一个换行符。

1
2
3
4
if ( num % 2 != 0 ) {
puts( "Last column number is not paired" );
exit( EXIT_FAILURE );
}

getchar函数

从标准输入中读取一个字符并且返回它的值。

1
2
3
4
5
6
7
8
9
10
/*
** 这样等同于,并避免了一些冗余的语句。
*/
int ch;
while( ( ch = getchar() ) != EOF && ch != '\n');

ch = getchar()
while(ch != EOF && ch != '\n')
ch = getchar();

scanf 在读取时,只读取需要的字符。

这个语句就剔除了当前输入行的剩余字符,避免被解释为第一行从而出错。

putchar函数

能够接受一个整型参数并在标准输出中打印该字符

TIP:

ch 为什么被声明为整型,而事实上我们用它来读取字符。

EOF 是一个整型值,它的位数比整型要多,把ch声明为整型可以避免其他字符意外的被解释为 EOF 。

字符只是小整型数,用它来容纳一个字符值并不会引发问题。

1.2 编译

编译即将我们书写的代码转变为机器可识别的机器语言的过程。

分为 预处理,编译,汇编,链接四步。

预处理 .c ->.i

即预处理器执行预处理指令

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

编译 .i -> .s

检查语法,如果没有错误的话,将源代码转为汇编代码

1
gcc test.c -S-o test.s

汇编 .s -> .o

将汇编语言转变为机器语言

生成 .o 的目标文件( Object File )

1
gcc test.s -c -o test.o

链接 .o -> .out/.exe

将相关的 .o 文件与库文件一起链接起来生成可执行文件

1
gcc test.o -o a.out

我们只需要执行gcc test.o -o a.out就可完成全过程

1.3 总结

本章的目的是描述足够的 C 语言的基础知识,使我们对 C 语言有一个整体的印象。

注释从 /* 开始 */ 结束,用于在程序中添加描述性的说明。

#include 用来使一个函数库头文件的内容由编译器处理,#define 允许给字面值常量取符号名。

所有的 C 程序都有一个 main 函数,它是程序执行的起点。函数的标量通过传值的方式进行传递,数组名参数句有传址调用的语义。

字符串是一串由 NUL 字节结尾的字符,并且有一组库函数专门操纵字符串。

printf,scanf 用于格式化输出输入。getchar 和 putchar 分别执行非格式化字符的输入和输出。

1.4 警告的总结

  • 在 scanf 函数的标量参数前未添加 & 字符。
  • 机械地把 print 函数的格式代码照搬于 scanf 函数。

1.5 编程提示的总结

  1. 使用 #include 指令避免重复声明。
  2. 使用 #define 指令给常量值取名。
  3. 在 #include 文件中放置函数原型。
  4. 在使用下标时要检查他们的值是否越界。
  5. 在 while 和 if 中蕴含赋值操作。

2. 基本概念

2.1 环境

ANSI C 的任何一种实现中,都存在着两种环境,翻译环境执行环境

翻译环境:在这个环境里,源代码被转换为可执行的机器指令。

执行环境:在这个环境里代码得到执行。

交叉编译器能够在一台机器上运行,但是可执行代码可以运行在不同的环境中。

独立环境即不存在操作系统的环境。如微波炉控制器(嵌入式系统)

宿主环境即存在操作系统的环境。

2.1.1 翻译

翻译即1.2中“编译”的全过程

  • 源代码通过编译转换为目标代码
  • 一或多个目标代码由链接器捆绑在一起形成单一完整的可执行程序
编译
  • 预处理器在源代码上进行一些文本操作
  • 源代码经过 解析,判断语句的意思
  • 优化器对目标代码进行进一步优化(如果加入了优化选项

image-20201214173524510

一 文件名约定

尽管标准并没有指定文件取名规则,但是大多数环境都有必须遵守的文件名命名约定。

C 源代码通常保存在.c扩展名命名的文件中,头文件通常具有扩展名.h。

至于目标文件名,不同的环境可能有不同的约定。UNIX系统中,扩展名为.o,MS-DOS系统中,它们的扩展名是.obj。

二 编译和链接

用于编译和链接 C 程序的特定指令在不同的系统中各不相同。

我们用 gcc 作为示例

  1. 编译并链接一个完全包含于一个或多个源文件的 C 程序
1
gcc main.c sort.c lookup.c

当编译的文件超过一个时,目标文件不会删除,允许对程序做成更改后,只对进行过改动的源文件进行编译。

  1. 编译一个 C 源文件,并把它和现存的目标文件链接在一起
1
gcc main.o sort.o lookup.c
  1. 编译一个或多个 C 源文件,并产生一个目标文件,以后再链接
1
2
3
4
gcc -c program.c sort.c

// 链接
gcc program.o sort.o

以上的所有指令都支持加上 -o name 来将可执行文件保存在“name”文件中。而不是“a”(默认名)中。

-lname来指定链接的库为“name”。

2.1.2 执行

程序的执行过程也有很多阶段。

首先,程序必须载入到内存中。在宿主环境中,这个任务由操作系统完成。在独立环境中,必须手工烧录。

然后,程序的执行就开始了。在宿主环境中,一般会有一个小型的启动程序与程序连接在一起,负责处理一系列日常事务,如收集命令行参数以便程序访问它们。

现在,便开始执行程序代码。在绝大多数机器里,程序会使用一个运行时 堆栈,它用于存储函数的局部变量和返回地址。程序同时也可以使用 静态 内存,存储于静态内存中的变量在程序结束前会一直保留他们的值。

程序执行的最后一个阶段是程序的终止。

2.2 词法规则

词法规则就像英语中的拼写规则,决定你在源程序中如何形成单独的字符片段,也就是标记( token )。

一个 C 程序由声明和函数组成。函数定义了需要执行的工作,声明则是描述了函数和(或)函数将要操作的数据类型(有时候是数据本身

)。注释可以散布于源文件的各个地方。

2.2.1 字符

标准并没有规定 C 环境中使用哪种特定的字符集,但是必须包括大小写字母和一些符号。

因为有些字符集对一些符号的缺失,标准定义了 三字符集 即三个字母表示一个字符。

通常我们要避免??出现在程序中,因为这是三字符集的开头,很容易被解释为其他字符而产生错误。

当我们要使用标准中带有意义的特定字符,这时候就需要 转义字符 来解决这个问题。转义序列由一个\和其他字符组成。

image-20201214191844718

2.2.2 注释

C 语言的注释是/**/内,在执行编译时会被预处理器拿掉。

2.2.3 自由形式的源代码

C 是一种自由形式的语言,相邻的标记之间必须出现一至多个空白字符(或者注释),不然可能被解释为单个标记。

这种代码书写的极度自由有利有弊。

书写代码: 肥皂盒哲学

2.2.4 标识符

标识符就是变量,函数,类型的名字,它们由大小写字母,数字和下划线组成,但不能用数字开头。C 是大小写敏感的语言。标识符长度没有限制。

image-20201214193738471

2.2.5 程序的形式

一个 C 程序可能保存在一个或多个源文件中,一个 C 程序的源文件应该包含一组相关的函数。这样就使得抽象数据类型成为可能。

2.3 程序风格

一种代码风格是合理利用空格以强调程序的结构。

  1. 空行用于分隔不同的逻辑代码段,它们是按照功能分段的。
  2. if和相关语句的括号是这些语句的一部分,而不是它们测试的表达式的一部分。所以,在括号和表达式之间留下一个空格,使表达式看上去更突出。函数原型也是如此。
  3. 在绝大多数操作符的使用中,中间都隔以空格,这可以使表达式的可读性更佳。有时在复杂表达式中,我会省略空格以助于显示子表达式的分组。
  4. 嵌套于其他语句的语句将缩进,以显示层次,使用 Tab 键而不是空格。
  5. 绝大部分注释都是成块出现的,这样它们从视觉上在代码中很突出。便于浏览或者跳过。
  6. 在函数的定义中,返回类型出现于独立的一行中,而函数的名字则在下一行的起始处。

2.4 总结

一个 C 程序的源代码保存在一个或多个源文件中,但一个函数只能完整地出现在一个源文件中,要把相关的函数放在同一个文件内。每个源文件都编译,产生对应的目标文件,由链接器链接在一起产生可执行程序。编译和最终运行程序的机器可能相同可能不同。

程序必须要载入内存中才能执行。在宿主环境中,由操作系统完成这一步。在独立环境中,程序往往永久存储于 ROM 中。经过初始化的静态变量在程序执行前获得值。程序的执行起点是 main 函数。绝大多数环境使用堆栈来存储局部变量和其他数据。

C编译器所使用的字符集必须包括某些特定的字符,如果缺少这些字符可以通过三字母词来代替。转义序列使得某些无法打印的字符得以表达。

注释从/*开始*/结束,不允许嵌套。注释会被预处理器清除。标识符由字母数字和下划线组成,不能以数字开头。关键字被系统保留。C 是自由形式的代码,但是要有清楚的风格来编写源代码,这样有便于程序的阅读和维护。

2.5 警告的总结

  1. 字符串常量中的字符被错误的解释为三字母词
  2. 编写糟糕的注释
  3. 注释不适当的结束。

2.6 编程提示的总结

良好程序风格和文档将使程序更容易阅读和维护!

3. 数据

程序对数据进行操作。

本节描述数据的各种类型,特点以及如何声明这些数据。

还将描述变量的三个属性——作用域,链接属性和存储属性。这三个属性决定一个变量的“可视性”(可以在什么地方使用)和“生命周期”(值将保存多久)

3.1 基本数据类型

在 C 语言中,仅有4种基本数据类型——整型,浮点型,指针和聚合类型(如数组和结构)。所有的其他类型都是由这些类型派生而来。

3.1.1 整型家族

整型家族包括字符,短整型,整型和长整型,它们都分为有符号无符号两种版本。

听上去“长整型”所能表示的值应当比“短整形”大,但这个假设并不一定正确。规定整型值相互之间大小的规则很简单:

长整型至少应该和整型一样长,而整型至少应该和短整型一样长。

image-20201215151401875

image-20201215151422088

一、整形字面值

字面值这个属于是字面值常量的缩写。即以字面形式输入源代码的值。这是一种实体,指定了自身的值,并且不允许发生改变。ANSI C允许 命名常量 (named constant,声明为 const 的变量)的创建。这种变量被初始化后,值就不能再改变。

在程序中出现整型字面值时,它属于整型家族哪一种取决于如何书写。如果在字符值的后面添加一个后缀,可以改变缺省的规则。添加 L 或者 l,可以使得这个整数被解释为 long 整型值,字符 U 或者 u 用于把数据指定为 unsigned 整型值。

源代码中,最自然的表达为十进制。如:

123,555,234,-3

十进制整型字面值可能是 int、long 或 unsigned long,在缺省的情况下,它是最短类型但能完整容纳这个值。

也可以用八进制来表示,只要数值以 0 开头。用十六进制表示则是用 0x 开头。如:

0173 0177777 00060

0x7b 0xFFFF 0xabcdef00

缺省情况下同十进制。

另外还有字符常量。它们的类型总是 int。你不能在它们后面添加 unsigned 或 long 后缀。字符常量就是用一个单引号包围起来的单个字符(或字符转义序列或三字母词),如:

‘M’ ‘\n’ ‘??(‘ ‘\377’

标准也允许’abc’这类的多字节字符常量,但是它们的实现在不同环境中可能不一样。所以不鼓励使用。

如果在多字节字符常量面前有一个 L ,那么它就是 宽字符常量 。如:

L’X’ L’e^’

当运行的环境支持一种宽字符集时,就可能使用它们。

二、枚举类型

枚举类型就是指它的值为符号常量而不是字面值的类型,声明:

enum Jar_Type { CUP, PINT, QUART, HALF_GALLON, GALLON };

这条语句声明了一个类型,称为 Jar_Type。这种类型的变量按下列方式声明:

enum Jar_Type milk_jug, gas_can, medicine_bottle;

如果某种特别的枚举类型只用声明一次,则

enum Jar_Type { CUP, PINT, QUART, HALF_GALLON, GALLON } milk_jug, gas_can, medicine_bottle;

这种类型的变量实际上以整型的方式存储,这些符号名实际值都是整型值。这里 CUP 是 0,PINT 是 1,以此类推。适当的时候,可以为这些符号名指定特定的整型值。如下所示:

enum Jar_Type { CUP = 8, PINT = 16, QUART = 32, HALF_GALLON = 64, GALLON = 128 };

只对部分命名也合法,如果符号名没有显式指定,那么它的值比前一个值大 1。

TIP:

符号名被当作整型处理,意味着可以给类型变量赋- 623 这样的字面值,也可以把 HALF_GALLON 这个值赋给任何一个整型变量,但是这样方式使用枚举将使枚举本身的含义被削弱。

3.1.2 浮点类型

诸如 3.14159 和 6.023*10^23 这样的数值无法按照整数存储。第一个数并非整数,而第二个数远远超出了计算机整数所能表达的范围。但是可以用浮点数来存储。通常以一个小数及一个以某个假定数为基数的指数(例如以2为基数的科学计数法。浮点数的存储—yiyide266)。

浮点数家族包括 float, double 和 long double 类型。通常,这些类型分别提供单精度,双精度以及在某些支持扩展精度的机器上提供扩展精度。所有浮点类型至少能容纳从 10^-37 到 10^37 之间的任何值。

头文件 float.h 定义了 FLT_MAX、DBL_MAX 和 LDBL_MAX,分别表示 float,double 和 long double 所能储存的最大值。(相应X_MIN则对应了最小值)。这个文件还定义了一些浮点数值实现相关的特性名字,如浮点数使用的基数,不同长度浮点数有效数字的位数。

浮点数字面值总是写成十进制的形式,它必须要有一个小数点或一个指数,也可以两者都有。

浮点数字面值缺省情况下都是 double 类型的,除非后面跟一个 L 或 l 表示它是一个 long double 类型的值,或者跟一个 F 或 f 表示它是一个 float 类型的值。

3.1.3 指针

指针是 C 语言如此流行的重要原因。指针可以有效地实现 tree 和 list 这类数据结构。但同时 C 对指针使用的不加限制正是许多令人欲哭无泪和药业切齿的错误的根源。

变量的值存储于计算机的内存中,每个变量占据一个特定的位置。每个内存位置都由 地址 唯一确定并引用。指针是地址的另一个名字。指针变量就是一个其值为另一个内存地址的变量。C 语言有一些操作符,可以获得一个变量的地址,也可以通过一个指针变量取得它所指向的值或数据结构。

一、指针常量(pointer constant)

指针常量与非指针常量在本质上是不同的,因为编译器负责把变量随机赋值给计算机内存中的位置,程序员事先并不知道这个值。所以指针常量表达为数值字面值的形式几乎没有用处,所以也没有特地的定义这个概念。

二、字符串常量(string literal)

许多人对 C 语言不存在字符串类型感到奇怪,不过 C 语言提供了字符串常量。事实上,C 语言存在字符串的概念:它就是一串以 NUL 字节结尾的零个或多个字符。字符串通常存储在字符数组中,这也是 C 语言没有显式的字符串类型的原因。由于 NUL 字节适用于终结字符串的,所以在字符串内部不能有 NUL 字节。不过在一般情况下,这个限制不会有问题,因为 NUL 并不是一个可以打印的字节。

字符串常量的书写方式是用一对双引号包围一串字符,如下所示:

“Hello” “world\n” “”

最后一个例子说明字符串常量可以是空的(不像字符常量)。尽管如此即使是空的字符串依然存在作为终止符的 NUL 字节。

TIP

在字符串常量的存储形式中,所有的字符和 NUL 终止符都存储在内存的某个位置。C 中相同值的不同字符串是分开存储的,因此有些编译器会允许程序修改字符串常量。

ANSI C 则声明如果修改,效果是未定义的,它也允许编译器把一个字符串常量存储在一个地方,即使程序中多次出现。因此修改字符串是完全不建议的,如果要这么做需要将其存在数组中。

字符串常量和指针放在一起,是因为程序在使用字符串常量时会生成一个“指向字符的常量指针”。当一个字符串常量出现在表达式中时,表达式所使用的值就是这些字符所储存的地址。但是不能把字符串常量赋值给一个字符数组。因为字符串常量的直接值是一个指针,而不是字符本身。

当字符串作为等号右侧存在时,它实际上是第一个字符的地址,当被需求为字符串时,就会从该地址读取并依次直到 NUL 。

3.2 基本声明

基本的数据类型还远远不够,你还应该知道怎样声明变量。变量声明的基本形式是:

说明符(一个或多个) 声明表达式列表

对于简单的类型,声明表达式列表就是被声明的标识符列表。对于更为复杂的类型,声明表达式列表中的每个条目实际上是一个表达式,显示被声明的名字的可能用途。

说明符(specifier)包含了一些关键字,用于描述被声明的标识符的基本类型。说明符也可以用于改变标识符的缺省存储类型和作用域。

例如声明

1
2
int i;
char j, k, l;

说明符也可以是用于修改变量的长度或者是否有符号数的关键字。这些关键字是:

short long signed unsigned

同时在声明整型变量时,如果声明中已经至少有了一个其他的说明符,关键字int可以省略。

1
2
3
unsigned short int  a;
unsigned short a;
/* 两式的效果是相等的 */

image-20201215221405479

该表显示了变量声明的所有类型,同一个框内的所有声明都是等同的。signed 关键字一般只用于 char 类型,因为其他整型类型在缺省的情况下都是有符号数。至于 char 是否是 signed,则因编译器而异。

浮点类型在这方面要简单一点,因为除了 long double 之外其余几个说明符(short,signed,unsigned)都是不可用的。

3.2.1 初始化

在一个声明中,你可以给标量变量指定一个初始值,方法是在变量名后面跟一个等号(赋值号)。例如:

1
int j = 15;

3.2.2 声明简单数组

为了声明一个一维数组,在数组名后面要跟一对方括号,方括号里面是一个整数,指定数组中元素的个数。例如:

1
int  values[20];

对于这个数组显而易见的解释是:我们声明了一个整型数组,数组包含二十个整型元素。这种解释是正确的,但是我们有一种更好的办法来阅读这个声明。名字 values 加一个下标,产生一个类型为 int 的值(共有20个整型值)。这个“声明表达式”显示了一个表达式的标识符产生了一个基本类型值,在本例中为 int。

C 数组值得注意的地方是,编译器并不检查程序对数组下标的引用是否在数组的合法范围内。这种不加检查的行为好处在于不用检查正确数组以减少时间浪费,另一方面使得无效的下标无法被检查出来。一个良好的经验法则是:

​ 如果下标是从正确的值计算得来,则不用检测;如果是根据某种方法从用户输入的数据产生而来,那么它必须 经过检测,确保处于有效范围内。

3.2.3 声明指针

声明表达式也可用于声明指针。C 语言中的声明中,先给出一个基本类型,紧随其后是一个标识符列表,这些标识符组成表达式,用于产生基本类型的变量。例如:

int *a;

这条语句表示 表达式*a产生的结果类型是 int 。知道了 * 操作符执行的是间接访问操作以后,可以推断 a 肯定是一个指向 int 的指针。

警告

C 本质是一种自由形式的语言,这很容易诱导你把星号写在靠近类型的一侧,如:

int* a;

这个声明与前面的声明具有相同的意思,而且看上去更加清楚,但是事实上 * 是表达式 *a 的一部分。

int* a, b, c;

人们很自然的以为这条语句声明了三个指针,实际上只有 a 是一个指针。

声明指针变量时可以指定初始值,如

char *message = "hello, world!";

这条语句把 message 声明为指向字符的指针,并把字符串常量第一个字符的地址对该指针进行初始化。

警告

这种类型的声明面临的一个危险是容易误会它的意思。看上去初始值是赋给 *message,事实上它是赋给 message 本身的,换句话说,前面的一个声明相当于

1
2
char *message;
message = "Hello, world!";

这是一个不容易理解的声明,简单来说,char *message = "hello, world!";是一个特殊的语句,C 会把地址交给 message,需要调用这个字符串时也是通过地址即可,*message实际指向的是字符串的第一个字符。

C 语言——字符串指针

3.2.4 隐式声明

这并不是一个好的主意,但是 C 确实有这样的特性。

1
2
3
4
5
6
7
8
int a[10];
int c;
b[10];
d;

f(x) {
return x + 1;
}

对于 K&R 编译器,能够判断出三四个声明是整型。当 f 函数缺少返回类型,编译器就会默认返回整型。x 没有类型名,也会被默认为整型。

3.3 typedef

C 语言支持一种叫做 typedef 的机制,它允许你为各种数据类型定义新名字,typedef 的声明和普通类型的声明基本相同,只是把 typedef 这个关键字卸载了声明前面。例如:

1
2
3
4
char  *ptr_to_char;
typedef char *ptr_to_char;
/* 这样,即可用 ptr_to_char 来声明 */
ptr_to_char a;

使用 typedef 可以减少声明又臭又长的危险,尤其是结构体这种复杂的声明。而且也便于修改程序所使用的数据类型,只修改 typedef 一个的数据类型即可。

morixinguan——为什么很多人编程喜欢用typedef?

iommer——数据结构中 为什么要用typedef int datatype ,而不直接用int

提示

typedef 的作用让我们想到 #define,但事实上,#define 并无法正确的处理指针类型。

3.4 常量

ANSI C 允许你声明常量,常量的样子和变量完全一样,只是它们的值不能修改。你可以用 const 关键字来声明常量。例如:

1
2
3
int const a;
const int a;
/* 两种格式效果完全相同 */

声明的整数 a 都无法更改,所以你无法把任何东西赋值给它。如此一来,怎么样才能进行值的初始化?

首先,可以在声明时进行初始化:

int const a = 15;

其次,在函数中声明的形参在函数被调用时会得到实参的值。

当涉及指针变量时,情况就很有趣了,因为两样东西都有可能成为常量——指针变量和它所指向的实体。如

1
2
3
4
5
6
7
8
9
10
11
int *pi;
int const *pci;
int * const cpi;
int const * const cpci

/*
** pi 是一个指向整型的指针
** pci 是一个指向整型常量的指针,可以修改指针的值(即地址),但是不能修改所指向的值。
** cpi 声明cpi是一个指向常型的常量指针,此时指针是常量,它的值无法修改(始终指向这个地址),但你可以修改它所指向的整型的值。
** cpci 说明这个指针的值无法被更改,这个指针指向的整型也不能更改。
*/
TIP

使用 const 关键字,有诸多好处,因为变量的值不会被更改,这使得你对于这个变量的意图更让其他阅读的人得到更清晰的认识,而且当这个值被意外修改时,编译器能够发现这个问题。

#define 也是一种创建名称常量的机制,在多数情况下用 #define 比 const 常量更好,因为只要允许使用字面值常量的地方都可以使用前者,比如数组长度。const 变量只能用于允许使用变量的地方。

TIP

名字变量非常有用,因为它们可以给字面值起符号名,用名字变量定义数组的长度或者限制循环的计数器可以提高程序的可维护性。如果一个值需要更改,只需要更改声明,这一点也相当的方便。

3.5 作用域

当变量被程序的某个部分声明时,只会在程序的一定区域才可以访问到。这个区域由标识符的作用域决定(scope)。这意味着两点,其他函数无法通过变量的名字访问它们,因为变量在作用于之外就不再有效,其次,意味着只要分属不同的作用域,就可以为变量起相同的名字

3.5.1 代码块作用域

位于一对花括号之间的所有语句被称为一个代码块。任何在代码块的开始位置声明的标识符都具有代码块作用域(block scope),它们可以被代码块中的所有语句访问。当代码块中存在嵌套时,内部可以访问外层的作用域,在内部声明的同名变量会覆盖外层。

TIP: 应当避免在嵌套的代码中使用相同的变量名,我们并没有很好的理由使用它们,它们容易造成调试或维护时混淆。

不同代码块的变量可能使用相同的内存地址。

3.5.2 文件作用域

在所有代码块之外声明的标识符都具有文件作用域,是指对于整个文件的全局变量的作用域。这些全局变量可以被其他文件访问到,但是static声明的变量并不能。

3.5.3 原型作用域

原型作用域(prototype scope)只适用于函数原型中声明的参数名。

3.5.4 函数作用域

适用于语句标签,这个标签用于goto语句。基本上可以简化为一个规则,一个函数中的所有语句标签必须唯一。

3.6 链接属性

评论