本章中,你将学习对象、函数和类型。我们将研究如何声明变量(有标识符的对象)和函数,获取对象的地址,并解除对这些对象指针的引用。你已经看到了C语言程序员可用的一些类型, C语言中的类型不是对象就是函数。
对象、函数、类型和指针
对象是你可以表示数值的存储。准确地说,C标准(ISO/IEC 9899:2018)将对象定义为 "执行环境中的数据存储区域,其内容可以代表数值",并补充说明,"当被引用时,对象可以被解释为具有特定类型"。变量是对象的例子。
变量会声明的类型,告诉你它的值代表哪种对象。例如类型为int的对象包含一个整数值。类型很重要,因为代表一种类型的对象的比特集合,如果被解释为不同类型的对象,可能会有不同的值。例如,数字1在IEEE 754(IEEE浮点运算标准)中由比特模式0x3f800000(IEEE 754-2008)表示。但是,如果你把这个相同的比特模式解释为一个整数,你会得到1,065,353,216的值,而不是1。
函数不是对象,但确实有类型。
C语言也有指针,它在地址--内存中存储对象或函数的位置。指针类型是由引用类型的函数或对象类型派生出来的。从被引用类型T派生出来的指针类型被称为对T的指针。
声明变量
声明变量时,需要指定类型,并提供名称用来引用该变量。
可以一行声明多个变量,但如果变量是指针或数组,或者变量是不同的类型,这样做就会引起混乱。下面的声明都是正确的。
char *src, c;
int x, y[5];
int m[12], n[15][3], o[21];
第一行声明了两个变量,src和c,它们的类型不同。src变量的类型为char *,而c的类型为char。第二行再次声明了两个类型不同的变量,x和y。变量x的类型是int,y是由五个元素组成的数组,类型是int。第三行声明了三个数组-m、n和o-具有不同的尺寸和元素数。
一行一种类型的声明可读性会更好:
char *src; // src has a type of char *
char c; // c has a type of char
int x; // x has a type int
int y[5]; // y is an array of 5 elements of type int
int m[12]; // m is an array of 12 elements of type int
int n[15][3]; // n is an array of 15 arrays of 3 elements of type int
int o[21]; // o is an array of 21 elements of type int
实例:交换值1
在{ }字符之间有代码块称为复合语句。我们在主函数中定义了两个变量,a和b。我们声明这些变量的类型为int,并将它们分别初始化为21和17。每个变量都必须有一个声明。然后主函数调用swap函数来尝试交换这两个整数的值。
#include <stdio.h>
void swap(int a, int b) {
int t = a;
a = b;
b = t;
printf("swap: a = %d, b = %d\n", a, b);
}
int main(void) {
int a = 21;
int b = 17;
swap(a, b);
printf("main: a = %d, b = %d\n", a, b);
return 0;
}
局部变量,如清单2-1中的a和b,具有自动存储期限,这意味着它们一直存在,直到执行离开它们被定义的块。我们将尝试交换存储在这两个变量中的值。
swap函数声明了两个参数,a和b,你用它们来向这个函数传递参数。我们还在交换函数中声明了int类型的临时变量t,并将其初始化为a的值。这个变量用于临时保存a中存储的值,以便在交换过程中不会丢失。
执行结果
$ ./a.out
swap: a = 17, b = 21
main: a = 21, b = 17
变量a和b分别被初始化为21和17。在swap函数中对printf的第一次调用显示这两个值被交换了,但在main中对printf的第二次调用显示原始值没有变化。
C语言是传值的语言,传参时参数的值被复制到一个单独的变量中,以便在函数中使用。swap函数将你作为参数传递的对象的值分配给各自的参数。当函数中的参数值发生变化时,调用方的值不会受到影响,因为它们是不同的对象。因此,在第二次调用printf时,变量a和b在main中保留了它们的原始值。
实例:交换值2
我们使用指示符(*)来声明指针
#include <stdio.h>
int swap(int *_a, int *_b) {
int tmp = *_a;
*_a = *_b;
*_b = tmp;
}
int main(void) {
int a = 21;
int b = 17;
swap(&a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
清单2-3:修改后的使用指针的交换函数
当在函数声明或定义中使用时,作为指针声明器的一部分,表示参数是指向特定类型的对象或函数的指针。注意_a表示指针,_a表示指针指向的值。&获取操作符的地址。
执行结果
$ ./a.out
a = 17, b = 21
变量a和b分别被初始化为21和17。然后代码将这些对象的地址作为参数传递给交换函数。
在swap函数中,参数_a和_b现在都被声明为int的指针类型,并且包含了从调用函数(在本例中,main)传递给swap的参数的副本。这些地址副本仍然指向完全相同的对象,所以当它们所引用的对象的值在交换函数中被交换时,在main中声明的原始对象的内容也被访问并被交换。这种方法通过生成对象地址,通过值传递这些地址,然后通过地址来访问原始对象,即传址。
范围
对象、函数、宏和其他C语言标识符都有范围,它限定了它们可以被访问的连续区域。C语言有四种类型的范围:文件、块、函数原型和函数。
对象或函数标识符的范围是由它的声明位置决定的。如果声明在任何块或参数列表之外为文件范围;如果声明出现在块内或参数内,只能在该块内访问。
如果声明出现在函数原型的参数声明列表中(不是函数定义的一部分),那么标识符具有函数原型作用域,它终止于函数声明末端; 函数范围是指函数定义的开头{和结尾}之间的区域。标签名是唯一一种函数作用域的标识符。标签是标识符,后面有一个冒号,用来标识函数中的一个语句,控制权可以被转移到这个语句中。
作用域可以被嵌套,有内部和外部作用域。内层作用域可以访问外层作用域,但反之不行。如果你在内层作用域和外层作用域中都声明了同一个标识符,那么在外层作用域中声明的标识符会被内层作用域中的标识符所隐藏,后者具有优先权。
存储期限
有四种存储期限可供选择:自动、静态、线程和分配。你已经看到,自动存储期限的对象被声明在块中或作为函数参数。这些对象的生命周期从它们被声明的块开始执行时开始,到块的执行结束时结束。
范围和寿命是完全不同的概念。范围适用于标识符,而寿命适用于对象。标识符的范围是代码区域,在这个区域中,标识符所表示的对象可以通过它的名字被访问。对象的生命周期是该对象存在的时间段。
在文件作用域中声明的对象具有静态存储期限。这些对象的生命期是程序的整个执行过程,它们的存储值在程序启动前被初始化。你也可以通过使用存储类指定符static,将块作用域中的变量声明为具有静态存储期限,如清单2-6中的计数例子所示。这些对象在函数退出后持续存在。
void increment(void) {
static unsigned int counter = 0;
counter++;
printf("%d ", counter);
}
int main(void) {
for (int i = 0; i < 5; i++) {
increment();
}
return 0;
}
这个程序输出1 2 3 4 5。我们在程序启动时将静态变量counter初始化为0,并在每次调用increment函数时将其递增。计数器的生命周期是程序的整个执行过程,它将在整个生命周期内保留其最后存储的值。你可以通过用文件范围声明计数器来实现同样的行为。然而在可能的情况下,限制对象的范围是一种良好的软件工程实践。
静态对象必须用常量值而不是变量来初始化。
int *func(int i) {
const int j = i; // ok
static int k = j; // error
return &k;
}
常量值指的是字面常量(例如,1、'a'或0xFF)、枚举成员以及alignof或sizeof等运算符的结果。
线程存储持续时间用于并发编程,动态分配的内存。
对齐
对象类型有对齐要求,对象可能被分配的地址进行限制。对齐代表了给定对象可以被分配的连续地址之间的字节数。CPU在访问对齐的数据(例如,数据地址是数据大小的倍数)和未对齐的数据时可能有不同的行为。
一些机器指令可以在非字的边界上执行多字节访问,但可能会有性能上的损失。字是自然的、固定大小的数据单位,由指令集或处理器的硬件处理。一些平台不能访问未对齐的内存。对齐要求可能取决于CPU字的大小(通常为16、32或64位)。
一般来说,C语言程序员不需要关心对齐要求,因为编译器为其各种类型选择合适的对齐方式。对于所有的标准类型,包括数组和结构,从malloc动态分配的内存都需要充分对齐。然而,在极少数情况下,你可能需要覆盖编译器的默认选择;例如,在必须从二幂地址边界开始的内存缓存行边界上对齐数据,或者满足其他系统特定的要求。传统上,这些要求是通过linker命令来满足的,或者通过malloc对内存进行整体分配,然后将用户地址向上舍入,或者通过涉及其他非标准设施的类似操作。
C11引入了一种简单的、向前兼容的机制来指定对齐方式。对齐是以size_t类型的值表示的。每个有效的对齐值都是的2整数次方。每个对象都有默认的对齐要求:更严格的对齐(更大的2次方)可以通过对齐指定器(_Alignas)来请求。你可以在声明的指定器中包个对齐方式的指定器。清单2-7使用对齐指定符来确保good_buff是正确对齐的(bad_buff对于成员访问表达式可能有不正确的对齐)。
struct S {
int i; double d; char c;
};
int main(void) {
unsigned char bad_buff[sizeof(struct S)];
_Alignas(struct S) unsigned char good_buff[sizeof(struct S)];
struct S *bad_s_ptr = (struct S *)bad_buff; // wrong pointer alignment
struct S *good_s_ptr = (struct S *)good_buff; // correct pointer alignment
}
对齐是按从弱到强(也叫严格)的顺序排列的。
网友评论