第 3 章 数据
# 第 3 章 数据
- 程序对数据进行操作
- 本章将对数据进行描述, 描述它的各种类型,描述它的特点以及如何声明它
- 本章还将描述变量的三个属性——作用域、链接属性和存储类型
- 这三个属性决定了一个变量的“可视性”(也就是它可以在什么地方使用)和“生命期”(它的值将保持多久)
# 3.1 基本数据类型
书本内容
# 3.1.1 整形家族
书本内容
# 整形字面值
书本内容
# 枚举类型
书本内容
# 3.1.2 浮点类型
书本内容
提示
FLT_MIN
、DBL_MIN
、LDBL_MIN
分别表示float
、double
、long double
所能储存的最小值(绝对值上的最小值)
# 3.1.3 指针
书本内容
# 指针常量
书本内容
# 字符串常量
书本内容
# 3.2 基本声明
书本内容
# 3.2.1 初始化
书本内容
# 3.2.2 声明简单数组
书本内容
# 3.2.3 声明指针
书本内容
# 3.2.4 隐式声明
书本内容
# 3.3 typedef
书本内容
# 3.4 常量
书本内容
# 3.5 作用域
书本内容
# 3.5.1 代码块作用域
书本内容
# 3.5.2 文件作用域
书本内容
# 3.5.3 原型作用域
书本内容
# 3.5.4 函数作用域
书本内容
# 3.6 链接属性
书本内容
extern 关键字在这里的解释有点让人费解
代码
1.c
#include <stdio.h> extern int a; void foo3(void); void foo4(void); int main(void) { foo3(); foo4(); printf("global a: %d\n", a); }
1
2
3
4
5
6
7
8
9
10
11
12
132.c
int a = 2;
13.c
#include <stdio.h> static int a = 3; void foo3(void) { extern int a; printf("foo3 a : %d\n", a); }
1
2
3
4
5
6
7
8
94.c
#include <stdio.h> static int a = 4; void foo4(void) { extern int a; printf("foo4 a : %d\n", a); }
1
2
3
4
5
6
7
8
运行结果
❯ cc 1.c 2.c 3.c 4.c && ./a.out && rm ./a.out foo3 a : 3 foo4 a : 4 global a: 2
1
2
3
4
代码说明
extern int a
只是声明a
为一个全局变量,但是并没有为a
分配任何的内存空间- 而在任何代码块外,
int a
会创建一个全局变量,会有实际的内存空间分配 extern int a
可以出现多次,只是说明a
是一个全局变量- 而在不同文件的任何代码块外,
int a
只能出现一次,即全局变量只能创建一次 - 在任何代码块外的
static int a
也相当于创建了一个文件全局变量,而且这个文件全局变量只在当前文件内有效,而且只从其所声明的位置到文件未尾有效- 所以,3.c 中的
static int a
和 2.c 中的int a
以及 4.c 中的static int a
并不会冲突 - 而且如书本中意思,应是如果先用
static int a
创建了一个链接属性为internal
的变量a
,再用extern int a
,则此时也不会改变a
的链接属性为external
- 即 3.c 中的
extern int a
指得是 3.c 中static int a
定义的a
- 而不是 2.c 中
int a
定义的 a
- 而不是 2.c 中
- 即 3.c 中的
- 而且
extern int a
不能出现在static int a
之前- 至少在当前
gcc
和clang
中如果这样做,会报错
- 至少在当前
static int a
的这种特性可用于实现 ADT 和黑盒
- 所以,3.c 中的
- 可以理解为文件作用域外还有一个全局作用域
- 而
static int a
相当于将a
的作用域由全局作用域改为了文件作用域 - 而且一个文件中如果有了
static int a
定义的文件作用域的a
,则全局作用域的a
便不在此文件中生效
- 而
🔲 待做事项
- 与 csapp 中和链接有关的内容相关联
不要直接在任何代码块外使用赋值语句
代码 5.c
float b; b = 1.1;
1
2
3编译错误
❯ gcc -S 5.c 5.c:3:1: warning: data definition has no type or storage class 3 | b = 1.1; | ^ 5.c:3:1: warning: type defaults to ‘int’ in declaration of ‘b’ [-Wimplicit-int] 5.c:3:1: error: conflicting types for ‘b’ 5.c:1:7: note: previous declaration of ‘b’ was here 1 | float b; |
1
2
3
4
5
6
7
8
9本质上是编译器将代码块外的赋值语句当成了变量声明语句,而且默认的声明类型为
int
任何代码块外放置的除变量声明外的其它表达式无实际意义
# 3.7 存储类型
书本内容
# 自动变量和静态变量的初始化
书本内容
# 3.8 static 关键字
书本内容
# 3.9 作用域、存储类型示例
书本内容
# 3.10 总结
书本内容
# 3.11 问题
#
问题 1
在你的机器上,字符的范围有多大?有哪些不同的整数类型?它们的范围又是如何?
答案
对于 linux 可在
/usr/lib/gcc/x86_64-linux-gnu/9/include/limits.h
中进行查看。linux 的内核版本(Ubuntu 20.04)
Linux version 5.8.0-63-generic (buildd@lgw01-amd64-035) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #71~20.04.1-Ubuntu SMP Thu Jul 15 17:46:08 UTC 2021
1除了常见的整型外,还增加了
long long int
和unsigned long long int
两种类型
可通过下列 C 文件,查看各个整形的表示范围
代码
#include <stdio.h>
#include <limits.h>
int main(void)
{
printf("The range of 'char' is %d ~ %d\n", SCHAR_MIN, SCHAR_MAX);
printf("The maximum of 'unsigned char' is %u\n", UCHAR_MAX);
printf("The range of 'short int' is %d ~ %d\n", SHRT_MIN, SHRT_MAX);
printf("The maximum of 'unsigned short int' is %u\n", USHRT_MAX);
printf("The range of 'int' is %d ~ %d\n", INT_MIN, INT_MAX);
printf("The maximum of 'unsigned int' is %u\n", UINT_MAX);
printf("The range of 'long int' is %ld ~ %ld\n", LONG_MIN, LONG_MAX);
printf("The maximum of 'unsigned long int' is %lu\n", ULONG_MAX);
printf("The range of 'long long int' is %lld ~ %lld\n", LLONG_MIN, LLONG_MAX);
printf("The maximum of 'unsigned long long int' is %llu\n", ULLONG_MAX);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 输出
The range of 'char' is -128 ~ 127
The maximum of 'unsigned char' is 255
The range of 'short int' is -32768 ~ 32767
The maximum of 'unsigned short int' is 65535
The range of 'int' is -2147483648 ~ 2147483647
The maximum of 'unsigned int' is 4294967295
The range of 'long int' is -9223372036854775808 ~ 9223372036854775807
The maximum of 'unsigned long int' is 18446744073709551615
The range of 'long long int' is -9223372036854775808 ~ 9223372036854775807
The maximum of 'unsigned long long int' is 18446744073709551615
2
3
4
5
6
7
8
9
10
#
问题 2
在你的机器上,各种不同类型的浮点数的范围是怎样的?
答案
对于 linux 可在
/usr/lib/gcc/x86_64-linux-gnu/9/include/float.h
中进行查看。linux 的内核版本(Ubuntu 20.04)
Linux version 5.8.0-63-generic (buildd@lgw01-amd64-035) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #71~20.04.1-Ubuntu SMP Thu Jul 15 17:46:08 UTC 2021
1
可通过下列 C 文件,查看各个浮点的表示范围
代码
#include <stdio.h>
#include <stdlib.h>
#include <float.h>
int main(void)
{
printf("The range of 'float' is %g ~ %g\n", FLT_MIN, FLT_MAX);
printf("The range of 'double' is %g ~ %g\n", DBL_MIN, DBL_MAX);
printf("The range of 'long double' is %Lg ~ %Lg\n", LDBL_MIN, LDBL_MAX);
return EXIT_SUCCESS;
}
2
3
4
5
6
7
8
9
10
11
- 输出
The range of 'float' is 1.17549e-38 ~ 3.40282e+38
The range of 'double' is 2.22507e-308 ~ 1.79769e+308
The range of 'long double' is 3.3621e-4932 ~ 1.18973e+4932
2
3
#
问题 3
假定你正编写一个程序,它必须运行于两台机器之上。这两台机器的缺省整型长度并不相同,一个是16位,另一个是32位。而这两台机器的长整型长度分别是32位和64位。程序所使用的有些变量的值并不太大,足以保存于任何一台机器的缺省整型变量中,但有些变量的值却较大,必须是32位的整型变量才能容纳它。一种可行的解决方案是用长整型表示所有的值,但在16位机器上,对于那些用16位足以容纳的值而言,时间和空间的浪费不可小视。在32位机器上,也存在时间和空间的浪费问题。
如果想让这些变量在任何一台机器上的长度都合适的话,你该如何声明它们呢?正确的方法是不应该在任何一台机器中编译程序前对程序进行修改。提示:试试包含一个头文件,里面包含每台机器特定的声明。
答案
- 声明整型变量名,使变量的类型必须有一个确定的长度(如
int8
、int16
、int32
) 。 - 为了尽可能的使用缺省的整形长度,使用类似
defint8
、defint16
或defint32
这样的名字 - 为每台机器创建一个名为
int_sizes.h
的文件,它包含一些typedef
声明,为所创建的类型名字选择最合适的整型长度在典型的 32 位机器上,
int_sizes.h
如下:typedef signed char int8; typedef short int int16; typedef int int32; typedef int defint8; typedef int defint16; typedef int defint32;
1
2
3
4
5
6在典型的 16 位机器上,
int_sizes.h
如下:typedef signed char int8; typedef int int16; typedef long int int32; typedef int defint8; typedef int defint16; typedef long int defint32;
1
2
3
4
5
6
#
问题 4
假定你有一个程序,它把一个long整型变量赋值给一个short整型变量。当你编译程序时会发生什么情况?当你运行程序时会发生什么情况?你认为其他编译器的结果是否也是如此?
答案
- 编译程序时,许多编译器会给出警告信息
- 标准规定:
- 如果要分配的值足够小,适合较短的变量,则保留其值
- 取决于具体的实现
- 一般会简单的丢弃高阶位
- 因此运行程序时一般不会报错,但可能无法达到预期的目的
- 不同编译器的实现可能不同,因此基本不具有可移植性
#
问题 5
假定你有一个程序,它把一个double变量赋值给一个float变量。当你编译程序时会发生什么情况?当你运行程序时会发生什么情况?
答案
- 当编译时,可能会出现一条警告消息
- 运行时的行为与整数几乎相同
- 如果值适合较小的变量,则赋值起作用
- 否则依赖于编译器具体的实现
- 但是,对于浮点值,当其指数大于较短类型所能容纳的值时,值“不适合”
- 如果指数适合,仍然有尾数,在这种情况下,该值被替换为可以在较短的变量中表示的最接近的值
- 但无论是舍入、截断还是执行其他操作,都取决于实现
#
问题 6
编写一个枚举声明,用于定义硬币的值。请使用符号PENNY、NICKEL等。
答案
enum Change {PENNY = 1, NICKEL = 5, DIME = 10,
QUARTER = 25, HALF_DOLLAR = 50, DOLLAR = 100};
2
#
问题 7
下列代码段会打印出什么东西?
enum Liquid { OUNCE = 1, CUP = 8, PINT = 16,
QUART = 32, GALLON = 128 };
enum Liquid jar;
...
jar = QUART;
printf( "%s\n", jar );
jar = jar + PINT;
printf( "%s\n", jar );
2
3
4
5
6
7
8
答案
jar
是枚举类型,其实质上整数类型,printf
使用格式代码%s
来打印整数,则无法判断输出,甚至会报如下错误segmentation fault (core dumped)
- 如果格式代码为
%d
,则最后的输出是:
32
48
2
#
问题 8
你所使用的C编译器是否允许程序修改字符串常量?是否存在编译器选项,允许或禁止你修改字符串常量?
答案
对于下列编译器来说,编译可通过,但运行时报错:
segmentation fault (core dumped)
❯ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 Copyright (C) 2019 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1
2
3
4
5参照字符串常量
#
问题 9
如果整数类型在正常情况下是有符号类型,那么 signed
关键字的目的何在呢?
答案
signed
关键字只对默认情况下为unsigned char
的char
类型有实际作用- 如果
char
默认情况下是signed char
,则signed
几乎没有任何实际作用,不过加上signed
,可以使得目标代码的含义更加明确
#
问题 10
一个无符号变量可不可以比相同长度的有符号变量容纳更大范围的值?
答案
- 不能。因为在长度相同的情况下,任何给定的
个值只有 个不同的组合,表示的数的范围是一定的。 - 无符号变量和有符号变量唯一的区别在于其最高位为 1 的一半是如何解释的
- 在无符号类型中,它们是一个更大的正数
- 在有符号类型中,它们是一个负数
#
问题 11
假如 int
和 float
类型都是 32
位长,你觉得哪种类型所能容纳的精确值更多一些?
答案
float
的范围比int
大,但如果它的位数比int
小,则它能容纳的精确值的数量并不一定比int
多- 因为给定一个特定的二制制值,
float
表示的值也就是固定
- 因为给定一个特定的二制制值,
- 即使
float
跟int
的范围一样大,如果根据 IEEE754 标准, 则其表示的精确值仍然会比int
类型少,- 因为其
0
有+0
和-0
之分,而且对于一些表示形式,其值为NaN(Not-a-Number)
- 因为其
#
#
问题 13
如果问题12中代码片段的声明中包含有 const
关键字,它们完成任务的方式又有何不同?
#
问题 14
在一个代码块内部声明的变量可以从该代码块的任何位置根据名字来访问,对还是错?
答案
- 错,有以下两个原因
- 只有在代码块的开始位置声明的变量才具有代码块作用域,如果在声明变量之前使用了该变量,则编译器会报错
- 如果代码块中有嵌套的代码块,则在嵌套代码块中声明的同名变量会隐藏在嵌套代码块外声明的变量
#
问题 15
假定函数 a
声明了一个自动整型变量 x
,你可以在其他函数内访问变量 x
,只要你 使用了下面这样的声明:
extern int x;
对还是错?
答案
错,在函数中声明的自动变量只具有代码块作用域,在该代码块外无法通过名字进行访问
#
问题 16
假定问题15中的变量 x
被声明为 static
。你的答案会不会有所变化?
答案
不会,因为 static
只是改变了变量的储存类型,并不会改变其作用域
#
问题 17
假定文件a.c的开始部分有下面这样的声明: int x;
;如果你希望从同一个源文件后面出现的函数中访问这个变量,需不需要添加额外的声明,如果需要的话,应该添加什么样的声明?
#
问题 18
假定问题17中的声明包含了关键字 static
。你的答案会不会有所变化?
答案
不会
- 因为
static
只是将其链接属性从external
变为了internal
,使得不能再从其它的文件中访问这个变量; - 但是
static
并没有改变其作用域,其仍然具有文件作用域
#
问题 19
假定文件a.c的开始部分有下面这样的声明: int x
;如果你希望从不同的源文件的函数中访问这个变量,需不需要添加额外的声明, 如果需要的话,应该添加什么样的声明?
答案
需要添加额外的声明,额外的声明如下:
extern int x
#
问题 20
假定问题19中的声明包含了关键字 static
。你的答案会不会有所变化?
答案
会,因为 static
关键字将 x
的链接属性从 external
变为了 internal
,这样则无法再从外部文件访问 x
#
问题 21
假定一个函数包含了一个自动变量,这个函数在同一行中被调用了两次。试问,在函数第2次调用开始时该变量的值和函数第1次调用即将结束时的值有无可能相同?
#
问题 22
当下面的声明出现于某个代码块内部和出现于任何代码块外部时,它们在行为上有何不同?
int a = 5;
答案
- 存储类型不同
- 声明在代码块内部的变量为自动变量,只会函数被调用时才会被初始化,而且函数每次调用都会重新初始化
- 声明在任何代码块外部的为静态变量,其只会在程序开始运行时被初始化一次
- 作用域不同
- 声明在代码块内部的变量具有代码块作用域
- 声明在任何代码块外部的变量具有文件作用域
- 链接属性不同
- 声明在代码块内部的变量链接属性为
none
- 声明在任何代码块外部的变量链接属性为
external
- 声明在代码块内部的变量链接属性为
#
问题 23
假定你想在同一个源文件中编写两个函数x和y,需要使用下面的变量:
名字 | 类型 | 存储类型 | 链接属性 | 作用域 | 初始化为 |
---|---|---|---|---|---|
a | int | static | external | x可以访问,y不能访问 | 1 |
b | char | static | internal | x和y都可以访问 | 2 |
c | int | automatic | none | x的局部变量 | 3 |
d | float | static | none | x的局部变量 | 4 |
你应该怎样编写这些变量?应该在什么地方编写?注意:所有初始化必须在声明中完成,而不是通过函数中的任何可执行语句来完成。
答案
- 将函数
y
的声明置于函数x
之前,将a
的声明置于两者之间, 即可实现a
的链接属性为external
,但却不能被函数y
访问的要求
static char b = 2;
void y(void)
{
}
int a = 1;
void x(void)
{
int c = 3;
static float d = 4;
}
2
3
4
5
6
7
8
9
10
11
12
13
#
问题 24
确认下面程序中存在的任何错误(你可能想动手编译一下,这样能够踏实一些)。在去除所有错误之后,确定所有标识符的存储类型、作用域和链接属性。每个变量的初始值会是什么?程序中存在许多同名的标识符,它们所代表的是相同的变量还是不同的变量?程序中的每个函数从哪个位置起可以被调用?
static int w = 5;
extern int x;
static float
func1( int a, int b, int c )
{
int c, d, e = 1;
...
{
int d, e, w;
...
{
int b, c, d;
static int y = 2;
...
}
}
...
{
register int a, d, x;
extern int y;
...
}
}
static int y;
float
func2( int a)
{
extern int y;
static int z;
...
}
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
答案
总共有两个错误
- 第 7 行声明的变量
c
与函数的形参(第 5 行)相冲突- 在 ANSI C 中会报错,在 K&R C 中则正常
- 第 26 行声明的
static int y
与 第 21 行声明的extern int y
相冲突- 因为第 21 行已将
y
的链接属性声明为extern
,第 24 行再使用static
尝试修改其链接属性为internal
,则编译器会报错 - 解决方法之一是将第 21 行的声明中的
extern
关键字去除,将其变为局部变量 - 解决方法之二是将第 26 行的声明提前到
func1
函数前,这样第 21 行声明指定的y
相当于是访问static int y
声明的y
,而不会再尝试去其它文件中查找 - 参考 Rationale of static declaration followed by non-static declaration allowed but not vice versa (opens new window)
- 参考链接属性
- 因为第 21 行已将
- 第 7 行声明的变量
去除所有错误后的代码如下:
static int w = 5;
extern int x;
static float
func1( int a, int b, int c )
{
int d, e = 1;
...
{
int d, e, w;
...
{
int b, c, d;
static int y = 2;
...
}
}
...
{
register int a, d, x;
int y;
...
}
}
static int y;
float func2( int a)
{
extern int y;
static int z;
...
}
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
- 各个标识符的存储类型,作用域,链接属性和初始值如下
- 函数形参的作用域以
ANSI C
为准
- 函数形参的作用域以
标识符名称(行号) | 存储类型 | 作用域 | 链接属性 | 初始值 |
---|---|---|---|---|
w(1) | static | 1-8, 17-31 | internal | 5 |
x(2) | static | 2-18, 23-31 | external | Note a |
func1(4) | -- | 4-31 | external | -- |
a(4) | auto | 5-18, 23 | none | Note b |
b(4), c(4) | auto | 5-11, 16-23 | none | Note b |
d(6) | auto | 6-8, 17-18, 23 | none | garbage |
e(6) | auto | 6-8, 17-23 | none | 1 |
d(9) | auto | 9-11, 16 | none | garbage |
e(9), w(9) | auto | 9-16 | none | garbage |
b(12), c(12), d(12) | auto | 12-15 | none | garbage |
y(13) | static | 13-15 | none | 2 |
a(19), d(19), x(19) | register | 19-22 | none | garbage |
y(20) | auto | 20-22 | none | garbage |
y(24) | static | 24-31 | internal | 0 |
func2(26) | -- | 26-31 | external | -- |
a(26) | auto | 27-31 | none | Note b |
y(28) | static | 28-31 | internal | 与 y(24) 相同 |
z(29) | static | 29-31 | none | 0 |
- Note a
- 如果该变量没有被其它声明初始化,则其初始值为
0
- 如果该变量没有被其它声明初始化,则其初始值为
- Note b
- 函数形参根据函数被调用时传递的参数进行初始化