C 语言入门 / C 变量、声明和常量
C 语言支持灵活的变量类型和结构,以及常用的算术和数学函数,以及一些 C 语言特有的有趣运算符。本章将详细介绍它们,最后简要讨论预处理器命令。
C 语言包含以下基本数据类型
类型 | 用途 | 大小(位) | 范围 |
---|---|---|---|
char | 字符 | 8 | -128 到 127 |
unsigned char | 字符 | 8 | 0 到 255 |
short | 整数 | 16 | -32,768 到 32,767 |
unsigned short | 整数 | 16 | 0 到 65,535 |
int | 整数 | 32 | –2,147,483,648 到 2,147,483,647 |
unsigned int | 整数 | 32 | 0 到 4,294,967,295 |
long | 整数 | 32 | -2,147,483,648 到 2,147,483,647 |
unsigned long | 整数 | 32 | 0 到 4,294,967,295 |
float | 实数 | 32 | 1.2 × 10−38 到 3.4 × 1038 |
double | 实数 | 64 | 2.2 × 10−308 到 1.8 × 10308 |
long double | 实数 | 128 | 3.4 × 10−4932 到 1.2 × 104932 |
这些是代表值。定义在不同的实现之间往往有所不同。例如,在某些系统中,“int” 为 16 位,“long double” 可能为 64 位。唯一保证的是优先级
short <= int <= long float <= double <= long double
C 语言的一个特点是,虽然存在“unsigned char” 数据类型,但出于某种原因,许多处理单个字符的函数要求变量被声明为“int” 或“unsigned int”。
声明的形式为
int myval, tmp1, tmp2;
unsigned int marker1 = 1, marker2 = 10;
float magnitude, phase;
变量名至少可以是 31 个字符长,虽然现代编译器始终支持更长的名称。变量名可以由字母、数字和 "_"(下划线)字符组成;第一个字符必须是字母。虽然可以在变量名中使用大写字母,但传统的 C 语言用法将大写字母保留用于常量名称。以 "_" 开头的字符也是合法的,但通常保留用于标记内部库名称。
C 语言允许在同一语句中声明多个变量,用逗号分隔声明。可以在声明时初始化变量。声明的常数值可以用多种格式声明
128 decimal int 256u decimal unsigned int 512l decimal long int 0xAF hex int 0173 octal int 0.243 float 0.1732f float 15.75E2 float 'a' character "giday" string
C 语言中定义了许多特殊字符
'\a' alarm (beep) character '\p' backspace '\f' formfeed '\n' newline '\r' carriage return '\t' horizontal tab '\v' vertical tab '\\' backslash '\?' question mark '\'' single quote '\"' double quote '\0NN' character code in octal '\xNN' character code in hex '\0' null character
可以使用“define” C 预处理器声明指定“符号常量”
#define PI 3.141592654
还有一个“const” 声明,它定义一个只读变量,例如 ROM 中的内存位置
const int a;
- 可以声明和初始化数组
int myarray[10];
unsigned int list[5] = { 10, 15, 12, 19, 23 };
float rdata[128], grid[5][5];
所有 C 语言数组的起始索引都是 0,因此“list” 的索引为 0 到 4。“rdata” 中的元素将按如下方式访问
for( i = 0; i <= 127; i = i + 1 )
{
printf ( "%f\n", rdata[i] );
}
C 语言不会对数组访问进行严格的边界检查。很容易超出数组的边界,唯一的症状是程序行为非常奇怪。
- 字符数组尤其重要,它们用于存储
字符串
char s[128];
strcpy( s, "This is a test!");
字符串“This is a test!” 通过“strcpy()” 函数初始化“s”,该函数将在后面的章节中讨论。存储的字符串将包含一个结束“空”字符(ASCII 码为 0 的字符,用“\0” 表示)。C 语言函数使用空字符来确定字符串的结尾,记住空字符的存在很重要。
好奇的读者可能想知道为什么要使用“strcpy()” 函数来初始化字符串。这样做似乎更容易
char s[128] = "This is a test!";
事实上,这是一个荒谬的操作,要解释原因,必须介绍“指针”的概念。
- C 语言程序可以定义包含变量或
数组地址的指针。例如,可以定义一个名为
int *ptr;
-- 的指针,它给出变量的地址,而不是变量本身。然后可以使用以下语句将值放入该位置
*ptr = 345;
相反地,可以使用“&” 获取变量的地址
int tmp;
somefunc( &tmp );
总结一下
- 指针的声明形式为:“*myptr”。
- 如果“myvar” 是一个变量,那么“&myvar” 就是指向该变量的指针。
- 如果“myptr” 是一个指针,那么“*myptr” 将给出该指针的变量数据。
指针很有用,因为它们允许函数通过参数变量返回值。否则,函数只会获取变量包含的数据,而无法访问变量本身。
C 语言的一个奇特性质是,数组的名称实际上指定了指向数组中第一个元素的指针。例如,给出字符串声明
char s[256];
-- 那么函数调用
somefunc( s )
-- 实际上会将字符数组的地址传递给函数,并且函数将能够修改它。但是
s[12]
-- 给出索引为 12 的数组值。请记住,这是第 13 个元素,因为索引始终从 0 开始。
C 语言中字符串还有更多奇特性质。另一个有趣的地方是,字符串字面量实际上会评估为指向它所定义字符串的指针。这意味着在以下操作中
char *p;
p = "Life, the Universe, & Everything!";
-- “p” 最终成为指向 C 编译器存储字符串字面量的内存的指针,而“p[0]” 将评估为“L”。类似地,以下操作
char ch;
ch = "Life, the Universe, & Everything!"[0];
-- 将把字符“L” 放入变量“ch” 中。
这很好,但为什么要关心呢?之所以关心是因为这解释了为什么操作
char s[128] = "This is a test!";
-- 是荒谬的。这个语句告诉 C 编译器保留 128 字节的内存,并将名为“s” 的指针指向它们。然后它保留另一个内存块来存储“This is a test!”,并将“s” 指向该块。这意味着最初分配的 128 字节内存块现在处于空闲状态且无法使用,并且程序实际上正在访问存储“This is a test!” 的内存。
这在一段时间内似乎可以正常工作,直到程序尝试将更多字节存储到该块中,而这些字节超过了为“This is a test!” 保留的 16 个字节。由于 C 语言对边界检查很差,这可能会造成各种问题。
这就是通常需要“strcpy()” 的原因。对于不会被修改或不会被用来存储比初始化时更多数据的字符串,不需要这样做,在这种情况下,以下语句可以正常工作
char *p;
p = "Life, the Universe, & Everything! ";
当将字符串作为参数传递给函数时,这些问题会变得特别棘手。以下示例展示了如何避免这些陷阱
/* strparm.c */
#include <stdio.h>
#include <string.h>
char *strtest( char *a, char *b );
int main ()
{
char a[256],
b[256],
c[256];
strcpy( a, "STRING A: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" );
strcpy( b, "STRING B: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" );
strcpy( c, "STRING C: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" );
printf( "Initial values of strings:\n" );
printf( "\n" );
printf( " a = %s\n", a );
printf( " b = %s\n", b );
printf( " c = %s\n", c );
printf( "\n" );
strcpy( c, strtest( a, b ));
printf( "Final values of strings:\n" );
printf( "\n" );
printf( " a = %s\n", a );
printf( " b = %s\n", b );
printf( " c = %s\n", c );
printf( "\n" );
return 0;
}
char *strtest( char *x, char *y )
{
printf( "Values passed to function:\n" );
printf( "\n" );
printf( " x = %s\n", x );
printf( " y = %s\n", y );
printf( "\n" );
strcpy( y, "NEWSTRING B: abcdefghijklmnopqrstuvwxyz0123456789" );
return "NEWSTRING C: abcdefghijklmnopqrstuvwxyz0123456789";
}
- 可以在 C 语言中定义“结构”,它们是不同数据元素的集合
/* struct.c */
#include <stdio.h>
#include <string.h>
struct person /* Define structure type. */
{
char name[50];
int age;
float wage;
};
void display( struct person );
int main()
{
struct person m; /* Declare an instance of it. */
strcpy( m.name, "Coyote, Wile E." ); /* Initialize it. */
m.age = 41;
m.wage = 25.50f;
display( m );
return 0;
}
void display( struct person p )
{
printf( "Name: %s\n", p.name );
printf( "Age: %d\n", p.age );
printf( "Wage: %4.2f\n", p.wage );
}
这个程序有一些有趣的特性
- 必须通过“struct” 声明定义结构,然后才能声明任何结构本身。在本例中,我们定义了一个名为“person” 的结构类型。
- 然后,通过定义结构类型 (“struct person”) 来声明结构的实例 (“m”)。
- 使用“点” 表示法访问结构的元素 (“m.name”、“m.age” 和“m.wage”)。
只要结构类型相同,就可以使用单个赋值语句将一个结构复制到另一个结构
struct person m, n;
...
m = n;
也可以声明结构数组
struct person group[10];
...
strcpy( group[5].name, "McQuack, Launchpad" );
-- 甚至可以在结构声明中嵌入结构
struct trip_rec
{
struct person traveler;
char dest[50];
int date[3];
int duration;
float cost;
}
-- 在这种情况下,嵌套结构将按如下方式访问
struct trip_rec t1;
...
strcpy( t1.traveler.name, "Martian, Marvin" );
结构的名称定义了一个变量,而不是一个地址。如果将结构的名称传递给函数,则该函数只会在其结构的本地副本上工作。要返回值,必须指定一个地址
setstruct( &mystruct );
有一个捷径可以用来获取结构的元素,使用指向结构的指针而不是结构本身。如果“sptr” 是指向“person” 类型结构的指针,则可以按如下方式访问其字段
strcpy( sptr->name, "Leghorn, Foghorn" );
sptr->age = 50;
sptr->wage = 12.98;
- C 语言包含一个与结构类似的概念,称为“联合”。联合的声明方式与结构非常相似。例如
union usample
{
char ch;
int x;
}
不同之处在于,联合可以存储这两个值中的任何一个,但不能同时存储。可以在上面定义的联合实例中存储一个“char” 值或一个“int” 值,但不能同时存储两者。只为联合分配足够的空间来存储其中声明的最大项目的价值,并且使用相同的内存来存储所有声明项目的价值。联合不经常使用,这里不再进一步讨论。
- 以下示例程序展示了结构的实际应用。它测试了一组用于对三维向量执行操作的函数
vadd(): Add two vectors. vsub(): Subtract two vectors. vdot(): Vector dot product. vcross(): Vector cross product. vnorm(): Norm (magnitude) of vector. vangle(): Angle between two vectors. vprint(): Print out vector.
程序如下
/* vector.c */
#include <stdio.h>
#include <math.h>
#define PI 3.141592654
struct v
{
double i, j, k;
};
void vadd( struct v, struct v, struct v* );
void vprint( struct v );
void vsub( struct v, struct v, struct v* );
double vnorm( struct v );
double vdot( struct v, struct v );
double vangle( struct v, struct v );
void vcross( struct v, struct v, struct v* );
int main()
{
struct v v1 = { 1, 2, 3 }, v2 = { 30, 50, 100 }, v3;
double a;
printf( "Sample Vector 1: " );
vprint( v1 );
printf( "Sample Vector 2: " );
vprint( v2 );
vadd( v1, v2, &v3 );
printf( "Vector Add: " );
vprint( v3 );
vsub( v1, v2, &v3 );
printf( "Vector Subtract: " );
vprint( v3 );
vcross( v1, v2, &v3 );
printf( "Cross Product: " );
vprint( v3 );
printf( "\n" );
printf( "Vector 1 Norm: %f\n", vnorm( v1 ) );
printf( "Vector 2 Norm: %f\n", vnorm( v2 ) );
printf( "Dot Product: %f\n", vdot( v1, v2 ) );
a = 180 * vangle( v1, v2) / PI ;
printf( "Angle: %3f degrees.\n", a );
return 0;
}
void vadd( struct v a, struct v b, struct v *c ) /* Add vectors. */
{
c->i = a.i + b.i;
c->j = a.j + b.j;
c->k = a.k + b.k;
}
double vangle( struct v a, struct v b ) /* Get angle between vectors. */
{
double c;
c = vdot( a, b ) / ( vnorm( a ) * vnorm( b ) );
return acos( c );
}
void vcross( struct v a, struct v b, struct v *c ) /* Cross product. */
{
c->i = a.j * b.k - a.k * b.j;
c->j = a.k * b.i - a.i * b.k;
c->k = a.i * b.j - a.j * b.i;
}
double vdot( struct v a, struct v b ) /* Dot product of vectors. */
{
return a.i * b.i + a.j * b.j + a.k * b.k;
}
double vnorm ( struct v a ) /* Norm of vectors. */
{
return sqrt( a.i * a.i + a.j * a.j + a.k * a.k );
}
void vprint ( struct v a ) /* Print vector. */
{
printf( " I = %6.2f J = %6.2f K = %6.2f\n", a.i, a.j, a.k );
}
void vsub ( struct v a, struct v b, struct v *c ) /* Subtract vectors. */
{
c->i = a.i - b.i;
c->j = a.j - b.j;
c->k = a.k - b.k;
}
- 现在应该清楚局部变量和全局变量的概念了。它
还可以将局部变量声明为“static”,这意味着它会从函数的一次调用保留到下一次调用。例如
#include <stdio.h>
void testfunc( void );
int main()
{
int ctr;
for( ctr = 1; ctr <= 8; ++ctr )
{
testfunc();
}
return 0;
}
void testfunc( void )
{
static int v;
printf( "%d\n", 2*v );
++v;
}
这会打印
0 2 4 6 8 10 12 14
-- 因为整数的初始值为 0。不要依赖默认值!
- 还有另外两个变量声明应该被识别出来,虽然
很少有理由使用它们:“register”,它声明一个变量应该分配给 CPU 寄存器,“volatile”,它告诉编译器变量的内容可能会自发改变。
这些声明比表面上看到的要复杂。 “register” 声明是可选的:如果可以,变量将被加载到 CPU 寄存器中,如果不行,它将按正常方式加载到内存中。由于一个好的优化编译器会尽力利用 CPU 寄存器,因此这通常不是一个很有用的操作。
乍一看,“volatile” 声明似乎很荒谬,就像“halt and catch fire” 这样的“笑话” 计算机命令一样。实际上,它用于描述可以独立于程序操作改变的硬件寄存器,例如实时时钟的寄存器。
- C 语言在数据类型之间的转换方面相当灵活。在许多情况下,
类型转换会透明地发生。例如,如果将一个“char” 转换为“short” 数据类型,或者将一个“int” 转换为“long” 数据类型,则转换后的数据类型可以轻松地容纳原始数据类型中的任何值。
从更大的数据类型转换为更小的数据类型可能会导致奇特的错误。对于有符号和无符号数据类型之间的转换也是如此。因此,应该小心地处理类型转换,通常最好是显式地进行转换,使用“强制转换” 操作。例如,可以如下将“int” 值强制转换为“float” 值
int a;
float b;
...
b = (float)a;
- 可以在 C 语言中定义自定义“枚举” 类型。例如
enum day
{
saturday, sunday, monday, tuesday, wednesday, thursday, friday
};
-- 定义枚举类型“day”,它包含一周中各天的值。实际上,这些值仅仅是一组连续整数值相关联的文本常量。默认情况下,集合从 0 开始,并依次递增,因此这里的“saturday” 的值为 0,“sunday” 的值为 1,依此类推。如果需要,可以指定任何一组数字分配
enum temps
{
zero = 0, freeze = 32, boil = 220
};
当然,使用一组 “#define” 指令也可以实现同样的功能,但这是一种更简洁的解决方案。一旦类型定义完成,就可以像下面这样声明该类型的变量:
enum day today = wednesday;
变量 “today” 将作为一个 “int” 类型变量,并允许对 “int” 类型变量进行有效的操作。再次提醒,C 语言在边界检查方面做得不多,不建议依赖 C 编译器发出警告。
- 最后,可以使用 “typedef” 声明来定义自定义数据类型。
typedef ch[128] str;
然后,可以像下面这样声明该类型的变量:
str name;