本章中,你将学习对象、函数和类型。我们将研究如何声明变量(有标识符的对象)和函数,获取对象的地址,并对这些对象指针的解引用。你已经看到了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 = %dn", a, b);
}

int main(void) {
    int a = 21;
    int b = 17;
    swap(a, b); 
    printf("main: a = %d, b = %dn", 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 = %dn", 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中的计数例子所示。这些对象在函数退出后持续存在。

#include <stdio.h>

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
}

对齐是按从弱到强(也叫严格)的顺序排列的。

对象类型

我们将介绍布尔类型、字符类型和数字类型(包括整数和浮点类型)。

布尔类型

声明为_Bool的对象只能存储0和1的值。这种布尔类型是在C99中引入的,并以下划线开始,以便在已经声明了自己的标识符名为bool或boolean的现有程序中加以区分。以下划线和大写字母或另一个下划线开头的标识符总是被保留。

如果你包含头文件<stdbool.h>,你也可以把这个类型拼成bool,并给它赋值为true(扩展为整数常数1)和false(扩展为整数常数0)。在这里,我们使用两种类型名称的拼写来声明两个布尔变量:

#include <stdbool.h>
_Bool flag1 = 0;
bool flag2 = false;

两种拼法都可以使用,但最好使用bool,因为这是语言的长期方向。

字符类型

C语言定义了三种字符类型:char、signed char和unsigned char。每个编译器的实现都会将char定义为具有相同的对齐方式、大小、范围、表示方式和行为,即signed char或nsigned char。无论做出什么样的选择,char都是独立的类型,与其他两种类型都不兼容。

char类型通常用于表示C语言程序中的字符数据。特别是,char类型的对象必须能够表示执行环境中所需要的最小字符集(称为基本执行字符集),包括大写和小写字母、10位小数、空格字符以及各种标点符号和控制字符。char类型不适合整数数据;使用signed char来表示小的有符号整数值,使用unsigned char来表示小的无符号值,是比较安全的。

基本的执行字符集适合许多传统的数据处理应用的需要,但它缺乏非英文字母是国际用户接受的障碍。为了解决这一需要,C标准委员会指定了一种新的、宽的类型,以允许大型字符集。你可以通过使用wchar_t类型将大字符集的字符表示为宽字符,它通常比基本字符占用更多空间。通常情况下,实现者选择16或32位来表示一个宽字符。C标准库提供了同时支持窄字符和宽字符类型的函数。

数值类型

C提供了几种数字类型,可以用来表示整数、枚举器和浮点值。第3章更详细地介绍了其中的一些类型,但这里是简单的介绍。

  • 整数类型

有符号的整数类型可以用来表示负数、正数和零。有符号的整数类型包括signed char、short int、int、long int和long long int。
除了int本身,在这些类型的声明中可以省略关键字int,所以你可以,例如,用long long而不是long long int来声明一个类型。
对于每个有符号的整数类型,都有相应的无符号整数类型,使用相同的存储量:unsigned char、unsigned short int、unsigned int、unsigned long int和unsigned long long int。无符号类型只能用来表示正数和零。

有符号和无符号整数类型用于表示各种大小的整数。每个平台(当前或历史)都决定了这些类型的大小,给定了一些约束条件。每个类型都有最小的可表示范围。这些类型按宽度排序,保证较宽的类型至少和较窄的类型一样大,这样,long long int类型的对象可以表示long int类型的对象可以表示的所有值,long int类型的对象可以表示int类型的对象可以表示的所有值,等等。各种整数类型的实际大小可以从<limits.h>头文件中指定的各种整数类型的最小和最大可表示值推断出来。

int类型通常具有执行环境的架构所建议的自然大小,因此在16位架构上,其大小为16位宽,在32位架构上为32位宽。你可以通过使用<stdint.h>或<inttypes.h>头文件的类型定义来指定实际宽度的整数,如uint32_t。这些头文件还为最宽的可用整数类型提供了类型定义:uintmax_t和intmax_t。
第3章详细介绍了整数类型。

  • 枚举类型

枚举,或称enum,允许你定义一个类型,在具有可枚举的常量值集的情况下,为整数值分配名称(枚举器)。下面是枚举的例子:

enum day { sun, mon, tue, wed, thu, fri, sat }; 
enum cardinal_points { north = 0, east = 90, south = 180, west = 270 };
enum months { jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec };

如果你没有用=操作符给第一个枚举器指定值,那么它的枚举常量的值就是0,而后面每个没有=的枚举器都会在前面的枚举常量的值上加1。因此,day枚举中sun的值是0,mon是1,以此类推。
你也可以给每个枚举器分配特定的值,如cardinal_points枚举所示。与枚举器一起使用=可能会产生具有重复值的枚举常数,如果你错误地认为所有的值都是唯一的,这可能是一个问题。months枚举将第一个枚举器设置为1,每个后续的枚举器如果没有被特别指定一个值,将被递增1。
枚举常量的实际值必须可以作为int来表示,但是它的类型是由实现定义的。例如,Visual C++使用有符号的int,而GCC使用无符号的int。

  • 浮点类型

C语言支持三种浮点类型:float、double和long double。浮点运算类似于实数运算,并经常被用作实数运算的模型。C语言支持多种浮点表示法,包括在大多数系统上支持IEEE浮点算术标准(IEEE 754-2008)。浮点表示法的选择取决于实现。第3章详细介绍了浮点类型。

  • void类型
    关键字void(本身)的意思是 "不能容纳任何值"。例如,你可以用它来表示函数不返回值,或者作为函数的唯一参数来表示该函数不接受参数。另一方面,派生类型void *意味着指针可以引用任何对象。我将在本章后面讨论派生类型。

函数类型

函数类型是派生类型。在这种情况下,该类型是由返回类型和其参数的数量和类型衍生出来的。函数的返回类型不能是数组类型。
当你声明函数时,你使用函数声明器来指定函数的名称和返回类型。如果声明器包括参数类型列表和定义,每个参数的声明必须包括标识符,除了只有void类型参数的参数列表,它不需要标识符。
下面是几个函数类型的声明:

int f(void);
int *fip();
void g(int i, int j);
void h(int, int);

首先,我们声明没有参数的函数f,返回int。接下来,我们声明一个没有指定参数的函数fip,它返回指向int的指针。最后,我们声明两个函数,g和h,每个函数都返回void,并接受两个int类型的参数。
如果标识符是宏,用标识符来指定参数(如这里的g)可能会有问题。然而,提供参数名称是自我记录代码的良好做法,所以通常不建议省略标识符(如对h的做法)。
在函数声明中,指定参数是可选的。然而,不这样做偶尔也会有问题。如果你用C++写fip的函数声明,它将声明不接受任何参数的函数,并返回int *。在C语言中,fip声明的是接受任何类型参数的函数,并返回一个int *。在C语言中,你不应该用空参数列表来声明函数。首先,这是语言的废弃功能,将来可能会被删除。其次,这段代码可能会被移植到C++中,所以要明确地列出参数类型,在没有参数的时候使用void。
带有参数类型列表的函数类型被称为函数原型。函数原型告知编译器一个函数所接受的参数的数量和类型。编译器使用这些信息来验证在函数定义和对函数的任何调用中是否使用了正确的参数数量和类型。

函数定义提供了该函数的实际实现。请看下面的函数定义:

int max(int a, int b)
{ return a > b ? a : b; }

返回类型指定为int;函数声明器为max(int a, int b);而函数主体为{ return a > b ? a : b; }。函数类型的指定不能包括任何类型限定符(参见第32页 "类型限定符")。函数体本身使用了条件运算符(?

内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/testing-/p/17390682.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!

相关课程