C语言_基础小记

数据类型

0x/0X 十六进制
0八进制

输出:%d,%o,%x//十进制,八进制,十六进制
若要显示各进制前缀:%#d,%#o,%#x

个人计算机常见的配置:
short占16位,int占16或32位(依计算机的字长决定),long占32位,long long占64位。

定义数据类型时,可以用L和U为后缀表示long和unsigned
若定义long:int a=2L/l;
若定义long long:int a=2LL/ll;
若定义unsigned:int a==2ull/Ull/LLU;
若定义short:int a=2h;

超过最大值时,unsigned int的变量会从0开始;而int类型的变量从范围内最小的那个数开始。

输出:
无符号数:%u
long:%ld
如果int和long大小相同,用%d
long类型的十六进制整数:%lx
long类型的八进制整数:%lo
short类型的十进制整数:%hd

指数计数法的浮点数:%e
如果系统支持十六进制格式的浮点数,可用a和A分别代替e和E
long double类型:%Lf%Le%La

类型的大小
char:1
int:4
long:8
long long:8
double:8
long double:16
地址:8

字符串和格式化输入/输出

sizeof():会把空字符也计算在内;
strlen():把有用的信息计算在内。

printf中的format格式 %zd

#define定义数值常量
const只读变量

scanf()标准输入函数:除了%c模式,其他输入时都会调过非空白字符前的所有空白字符。scanf()返回成功读取项的数量。
printf()标准输出函数。

运算符、表达式和语句

sizeof 返回 size_t 类型的值。这是一个无符号整数类型。输出是%zd。如果系统不支持%zd,可使用%u或%lu代替%zd。

typedef机制,允许程序员为现有类型创建别名。
用法:typedef double real; real就是double的别名。

a_post = a++; // 后缀递增
pre_b = ++b; // 前缀递增

a_post是a递增之前的值,而b_pre是b递增之后 的值。

优先级:

y = 2; 
n = 3; 
nextnum = (y + n++)*6;

nextnum = (2 + 3) * 6 = 5*6 = 30
n的值只有在被使用之后才会递增为4。根据优先级的规定,++只作用 于n,不作用与y + n。

类型转换
规则。
1.当类型转换出现在表达式时,无论是unsigned还是signed的char和short 都会被自动转换成int,如有必要会被转换成unsigned int(如果short与int的大 小相同,unsigned short就比int大。这种情况下,unsigned short会被转换成 unsigned int)。在K&R那时的C中,float会被自动转换成double(目前的C不 是这样)。由于都是从较小类型转换为较大类型,所以这些转换被称为升级 (promotion)。

2.涉及两种类型的运算,两个值会被分别转换成两种类型的更高级别。

3.类型的级别从高至低依次是long double、double、float、unsignedlong long、long long、unsigned long、long、unsigned int、int。例外的情况是,当 long 和 int 的大小相同时,unsigned int比long的级别高。之所以short和char类 型没有列出,是因为它们已经被升级到int或unsigned int。

4.在赋值表达式语句中,计算的最终结果会被转换成被赋值变量的类型。这个过程可能导致类型升级或降级(demotion)。所谓降级,是指把一种类型转换成更低级别的类型。

5.当作为函数参数传递时,char和short被转换成int,float被转换成 double。

声明参数就创建了被称为形式参数(formal argument或formal parameter,简称形参)的变量。
函数调用传递的值为实际参数(actual argument或actual parameter),简称实参。

C99标准规定 了:
对于actual argument或actual parameter使用术语argument(译为实参)
对于formal argument或formal parameter使用术语parameter(译为形参)

getchar()和putchar()函数

只处理字符。
ch = getchar();等效于scanf("%c", &ch);
putchar(ch);等效于printf("%c", ch);
这两个函数通常定义在 stdio.h头文件中

数组和指针

&date[0]:地址
*(date+1):值

注意:

int moredata[2] = { 300, 400 };
p3 = moredata;

结果:
*p3 = 300
(*p3)++ = 300
*p3 = 301

保护数组中的数据

define:符号常量
const:数组、指针、指向const的指针(不可以改变对应地址的值,但可以改变指针指向的位置)。

注意:
1.只能把非const数据的地址赋给普通指针;
2.含const的函数定义可以接受普通数组名和const数组名作为参数;
3.不含const的函数定义不能接受const参数。

const位置的区别:
1.

const double * pc = rates;

可以修改值也可以修改位置。

double * const pc = rates;

不可以修改位置,能修改值。
因为const放在了pc的旁边。

const double * const pc = rates;

既不能更改它所指向的地址,也不能修改指向地址上的值。

指针和多维数组

1.zippo[0]、zippo、*zippo
zippo[0]指向一个4 字节的数据对象。zippo[0]加1,其值加4;

数组名zippo 是一个内含2个int类型值的数组的地址,所以 zippo指向一个8字节的数据对象。因此,zippo加1,它所指向的地址加8字节。

*zippo和zippo[0]相同。

int (* pz)[2]; // pz指向一个内含两个int类型值的数组

*先与pz结合,因此声明的是一个指向数组(内含两个int类型的 值)的指针。

int * pax[2]; // pax是一个内含两个指针元素的数组,每个元素都指 向int的指针

由于[]优先级高,先与pax结合,所以pax成为一个内含两个元素的数 组。然后*表示pax数组内含两个指针。最后,int表示pax数组中的指针都指 向int类型的值。因此,这行代码声明了两个指向int的指针。

指针的兼容性

int * pt; 
int (*pa)[3]; 
int ar1[2][3];
int ar2[3][2];
int **p2; // 一个指向指针的指针
p2 = &pt; // both pointer-to-int * 
*p2 = ar2[0]; // 都是指向int的指针 
p2 = ar2; // 无效

pt 指向一个 int类型值,而ar1指向一个内含3个int类型元素的数组。
类似地,pa指向一个内含3个int类型元素的数组,所以它与ar1的类型兼容,
但是ar2指向一个内含2个int类型元素的数组,所以pa与ar2不兼容。

两个例子有些棘手:
变量p2是指向指针的指针,它指向的指针指向int,而ar2是指向数组的指针,该数组内含2个int类型的元素。
所以, p2和ar2的类型不同,不能把ar2赋给p2。但是,*p2是指向int的指针,与 ar2[0] 兼容。因为ar2[0]是指向该数组首元素(ar2[0][0])的指针,所以 ar2[0]也是指向int的指针。

把const指针赋给非const指针不安全,因为这样可以使用新的指针改变const指针指向的数据。编译器在编译代码时,可能会给出警告,执行这样的代码是未定义的。但是把非const指针赋给const指针没问题,前提是只进行一级解引用:

p2 = p1; // 有效 -- 把非const指针赋给const指针

但是进行两级解引用时,这样的赋值也不安全,例如,考虑下面的代码:

const int **pp2; 
int *p1; 
const int n = 13; 
pp2 = &p1; // 允许,但是这导致const限定符失效(根据第1行代码, 不能通过*pp2修改它所指向的内容) 
*pp2 = &n; // 有效,两者都声明为const,但是这将导致p1指向 n(*pp2已被修改)
*p1 = 10;//有效,但是这将改变n的值(但是根据第3行代码,不能修改n 的值)

不同编译器的结果可能不同。有的会改变n值,有的不会改变。但都会警告。

注意:
数组声明时,第一个[]可以空着,其余不能空。

复合字面量

相当于一种匿名变量。
所以必须在创建的同时使用它。

存储类别、链接和内存管理

存储类别

存储期(storage duration)描述对象,所谓存储期是指对象在内存中保留了多长时间。

标识符用于访问对象,可以用作用域(scope)和链接(linkage)描述标识符,标识符的作用域和链接表明了程序的哪些部分可以使用它。

不同的存储类别具有不同的存储期、作用域和链接。

标识符可以在源代码的多文件中共享、可用于特定文件的任意函数中、可仅限于特定函数中使用,甚至只在函数中的某部分使用。

对象可存在于程序的执行期,也可以仅存在于它所在函数的执行期。对于并发编程,对象可以在特定线程的 执行期存在。可以通过函数调用的方式显式分配和释放内存。

作用域

块作用域、函数作用域、函数原型作用域或文件作用域。

块作用域变量的可见范围是 从定义处到包含该定义的块的末尾。

函数作用域(function scope)仅用于goto语句的标签。这意味着即使一 个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。

函数原型作用域(function prototype scope)用于函数原型中的形参名(变量名)

文件作用域也称为全局变量。

编译器源代码文件和所有的头文件都看成是一个包含信息的单独文件。这个文件被称为翻译单元

链接

C 变量有 3 种链接属性:外部链接、内部链接 或无链接。

无链接变量:具有块作用域、函数作用域或函数原型作用域的变量。

具有文件作用域的变量可以是外部链接或内部链接。
外部链接变量可以在多文件程序中使用。“全局作用域”或“程序作用域”。

内部链接变量只能在一个翻译单元中使用。“文件作用域”。static

存储期

作用域和链接描述了标识符的可见性。存储期描述了通过这些标识符访 问的对象的生存期。

C对象有4种存储期:静态存储期、线程存储期、自动 存储期、动态分配存储期。

静态存储期:变量在程序的执行期间一直存在。

线程存储期:用于并发程序设计,程序执行可被分为多个线程。

自动存储期:块作用域的变量。

(变长数组稍有不同,它们的存储期从声明处到块的末尾,而不是从块的开始处到块的末尾。)

块作用域变量也能具有静态存储期。为了创建这样的变量,要把 变量声明在块中,且在声明前面加上关键字static:

在这里插入图片描述

自动变量

声明在块或函数头中的任何变量都属于自动存储类别。为了更清楚地 表达你的意图(例如,为了表明有意覆盖一个外部变量定义,或者强调不要把该变量改为其他存储类别),可以显式使用关键字auto。

关键字auto是存储类别说明符(storage-class specifier)。

块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量(当然,参数用于传递变量的值和地址给另一个函数,但是这是间 接的方法)。另一个函数可以使用同名变量,但是该变量是储存在不同内存 位置上的另一个变量。

变量具有自动存储期意味着,程序在进入该变量声明所在的块时变量存在,程序在退出该块时变量消失。原来该变量占用的内存位置现在可做他用。

寄存器变量

 register int quick

块作用域的静态变量

具有静态存储期、块作用域的局部变量。这些变量和自动变量一样,具有相同的作用域,但是程序离开它们所在的函数后,这些变量不会消失。
值的变化在函数外也不会消失。
也就是说,这种变量具有块作用域、无链接,但是具有静态存储期。

不能在函数的形参中使用static

“局部静态变量”是描述具有块作用域的静态变量的另一个术语。

这种存储类别被称为内部静态存储类别(internal static storage class)。这里的内部指的是函数内部,而非内部链接。

外部链接的静态变量

外部链接的静态变量具有文件作用域、外部链接和静态存储期。该类别有时称为外部存储类别(external storage class),属于该类别的变量称为外部变量(external variable)。

把变量的定义性声明(defining declaration)放在在所有函数的外面便创建了外部变量。

当然,为了指出该函数使用了外部变量,可以在函数中用关键字extern再次声明。

如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须用extern在该文件中声明该变量。

1.初始化外部变量
与自动变量不同的是,如果未初始化外部变量,它们会被自动初始化为 0。

内部链接的静态变量

该存储类别的变量具有静态存储期、文件作用域和内部链接。

在所有函数外部(这点与外部变量相同),用存储类别说明符static定义的变量具有 这种存储类别。

普通 的外部变量可用于同一程序中任意文件中的函数,但是内部链接的静态变量 只能用于同一个文件中的函数。

可以使用存储类别说明符 extern,在函数中重复声明任何具有文件作用域的变量。

多文件

复杂的C程序通常由多个单独的源代码文件组成。有时,这些文件可能要共享一个外部变量。

C通过在一个文件中进行定义式声明,然后在其他文件中进行引用式声明来实现共享。

也就是说,除了一个定义式声明外,其他声明都要使用extern关键字。而且,只有定义式声明才能初始化变量。

注意,如果外部变量定义在一个文件中,那么其他文件在使用该变量之前必须先声明它(用 extern关键字)。

也就是说,在某文件中对外部变量进 行定义式声明只是单方面允许其他文件使用该变量,其他文件在用extern声明之前不能直接使用它。

存储类别说明符

C 语言有6个关键字作为存储类别说明符:auto、register、static、extern、 _Thread_local和typedef。typedef关键字与任何内存存储无关,把它归于此类 有一些语法上的原因。

在绝大多数情况下,不能在声明中使用多个存储类别说明符,所以这意味着不能使用多个存储类别说明符作为typedef的 一部分。唯一例外的是_Thread_local,它可以和static或extern一起使用。

auto说明符表明变量是自动存储期,只能用于块作用域的变量声明中。 由于在块中声明的变量本身就具有自动存储期,所以使用auto主要是为了明 确表达要使用与外部变量同名的局部变量的意图。

register 说明符也只用于块作用域的变量,它把变量归为寄存器存储类别,请求最快速度访问该变量。同时,还保护了该变量的地址不被获取。

用 static 说明符创建的对象具有静态存储期,载入程序时创建对象,当程序结束时对象消失。如果static 用于文件作用域声明,作用域受限于该文 件。如果 static 用于块作用域声明,作用域则受限于该块。因此,只要程序 在运行对象就存在并保留其值,但是只有在执行块内的代码时,才能通过标识符访问。块作用域的静态变量无链接。文件作用域的静态变量具有内部链接。

extern 说明符表明声明的变量定义在别处。如果包含 extern 的声明具有文件作用域,则引用的变量必须具有外部链接。如果包含 extern 的声明具有块作用域,则引用的变量可能具有外部链接或内部链接,这接取决于该变量的定义式声明。

存储类别和函数

函数也有存储类别,可以是外部函数(默认)或静态函数。

C99 新增了 第 3 种类别——内联函数

外部函数可以被其他文件的函数访问,但是静态函数只能用于其定义所在的文件。

通常的做法是:用 extern 关键字声明定义在其他文件中的函数。这样做是为了表明当前文件中使用的函数被定义在别处。

除非使用static关键字, 否则一般函数声明都默认为extern。

以static存储类别说明符创建的函数属于特定模块私有。

srand((unsigned int) time(0));/*随机种子*/

roll=rand()%sides+1//产生1~sides+1的随机数

分配内存:malloc()和free(),calloc()

malloc()要与free()配套使用。
free()函数的参数是之前malloc()返回的地址,该函数释放之前malloc()分配的内存。

malloc()和free()的原型都在stdlib.h 头文件中。

double * temp = (double *) malloc( n * sizeof(double)); 
...
/* free(temp); // 假设忘记使用free() */
long * newmem; 
newmem = (long *)calloc(100, sizeof (long));

calloc()函数还有一个特性:它把块中的所有位都设置为0(注意,在某 些硬件系统中,不是把所有位都设置为0来表示浮点值0)。

free()函数也可用于释放calloc()分配的内存。

动态内存分配和变长数组

int vlamal()
{
int n; 
int * pi; 
scanf("%d", &n); 
pi = (int *) malloc (n * sizeof(int)); 
int ar[n];// 变长数组 
pi[2] = ar[2] = -5; ... 
}

变长数组是自动存储类型。因此,程序在离开变长数组定义所在的块时(该例中,即vlamal()函数结束时),变长数组占用的内存空间 会被自动释放,不必使用 free()。

free()所用的指针变量可以与 malloc()的指针变量不同,但是两个指针必须储存相同的地址。但是,不能释放同一块内存两次。

如果编译器不支持变长数组特性,就只能固 定二维数组的维度,如下所示:

int n = 5; 
int m = 6; 
int ar2[n][m]; // n×m的变长数组(VLA)
int (* p2)[6]; // C99之前的写法
int (* p3)[m]; // 要求支持变长数组 
p2 = (int (*)[6]) malloc(n * 6 * sizeof(int)); // n×6 数组 
p3 = (int (*)[m]) malloc(n * m * sizeof(int)); // n×m 数组(要求支持变长数 组)
ar2[1][2] = p2[1][2] = 12; 

先复习一下指针声明。由于malloc()函数返回一个指针,所以p2必须是 一个指向合适类型的指针。
第1个指针声明:
int (* p2)[6]; // C99之前的写法
表明p2指向一个内含6个int类型值的数组。因此,p2[i]代表一个由6个整 数构成的元素,p2[i][j]代表一个整数。

第2个指针声明用一个变量指定p3所指向数组的大小。
因此,p3代表一个指向变长数组的指针,这行代码不能在C90标准中运行

存储类别和动态内存分配

程序把它可用的内存分为3部分:一部分供具有外部链接、内部链接和无链接的静态变量使用;一部分供自动变量使用;一部分供动态内存分配(内存堆、自由区域)。

静态存储类别所用的内存数量在编译时确定,只要程序还在运行,就可访问储存在该部分的数据。该类别的变量在程序开始执行时被创建,在程序结束时被销毁。

自动存储类别的变量在程序进入变量定义所在块时存在,在程序离开块时消失。因此,随着程序调用函数和函数结束,自动变量所用的内存数量也相应地增加和减少。这部分的内存通常作为栈来处理,这意味着新创建的变量按顺序加入内存,然后以相反的顺序销毁。

动态分配的内存在调用 malloc()或相关函数时存在,在调用 free()后释放。这部分的内存由程序员管理,而不是一套规则。所以内存块可以在一个 函数中创建,在另一个函数中销毁。正是因为这样,这部分的内存用于动态内存分配会支离破碎。也就是说,未使用的内存块分散在已使用的内存块之 间。

另外,使用动态内存通常比使用栈内存慢。

总而言之,程序把静态对象、自动对象和动态分配的对象储存在不同的区域。

ANSI C类型限定符

通常用类型和存储类别来描述一个变量。

C90 还新增了两个属性: 恒常性(constancy)和易变性(volatility)。
这两个属性可以分别用关键字 const 和 volatile 来声明,是限定类型(qualified type)。

C99标准新增了第3个限定符:restrict,用于提高编译器优化。

C11 标准新增了第4个限定符:_Atomic。
C11提供一个可选库,由stdatomic.h管 理,以支持并发程序设计,而且_Atomic是可选支持项。

C99 为类型限定符增加了一个新属性:它们现在是幂等的 (idempotent)!
这个属性听起来很强大,其实意思是可以在一条声明中多 次使用同一个限定符,多余的限定符将被忽略。

const类型限定符

1.在指针和形参声明中使用const
注意:

const放在*左侧任意位置,限定了指针指向的数据不能改变;
const放在*的右侧,限定了指针本身不能改变

const 关键字的常见用法是声明为函数形参的指针。

void display(const int array[], int limit);

在函数原型和函数头,形参声明const int array[]与const int * array相同, 所以该声明表明不能更改array指向的数据。

2.对全局数据使用const

  1. 可以采用两个策略。第一,遵循外部变量的常用规则,即在一个文件中使用定义式声明,在其他文件中使用引用式声明(用extern关键字);
  2. 把const变量放在一个头文件中,然后在其他文件中包含该头文件。

volatile类型限定符

volatile 限定符告知计算机,代理(而不是变量所在的程序)可以改变 该变量的值。

如果声明中没有volatile关键字,编译器会假定变量的值在使用 过程中不变,然后再尝试优化代码。

可以同时用const和volatile限定一个值。
例如,通常用const把硬件时钟设置为程序不能更改的变量,但是可以通过代理改变,这时用 volatile。
只能在声明中同时使用这两个限定符,它们的顺序不重要,如下所示。

限定的数据除了被当前程序修改外还可以被其他进程修改。该限定符的目的是警告编译器不要进行假定的优化。

restrict类型限定符

restrict 关键字允许编译器优化某部分代码以更好地支持计算。它只能 用于指针,表明该指针是访问数据对象的唯一且初始的方式。

_Atomic类型限定符(C11)

并发程序设计把程序执行分成可以同时执行的多个线程。这给程序设计 带来了新的挑战,包括如何管理访问相同数据的不同线程。C11通过包含可 选的头文件stdatomic.h和threads.h,提供了一些可选的(不是必须实现的) 管理方法。值得注意的是,要通过各种宏函数来访问原子类型。当一个线程 对一个原子类型的对象执行原子操作时,其他线程不能访问该对象。

旧关键字的新位置

void ofmouth(int * const a1, int * restrict a2, int n); // 以前的风格
void ofmouth(int a1[const], int a2[restrict], int n); // C99允许
double stick(double ar[static 20]);//引入新风格

static 的这种用法表明,函数调用中的实际参数应该是一个指向数组首元素的指针,且该数组至少有20个元素。

关键概念

C 提供多种管理内存的模型。除了熟悉这些模型外,还要学会如何选择不同的类别。大多数情况下,最好选择自动变量。

如果要使用其他类别,应该有充分的理由。

通常,使用自动变量、函数形参和返回值进行函数间的通信比使用全局变量安全。

但是,保持不变的数据适合用全局变量。

应该尽量理解静态内存、自动内存和动态分配内存的属性。

尤其要注意:静态内存的数量在编译时确定;静态数据在载入程序时被载入内存。

在程序运行时,自动变量被分配或释放,所以自动变量占用的内存数量随着程序的运行会不断变化。可以把自动内存看作是可重复利用的工作区。

动态分配的内存也会增加和减少,但是这个过程由函数调用控制,不是自动进行 的。

结构和其他数据形式

建立超级数组

struct book { 
char title[MAXTITL]; 
char author[MAXAUTL]; 
float value; };

struct book library;//把library声明为一个使用book结构布局的结构变量。

struct book { 
char title[MAXTITL]; 
char author[AXAUTL]; 
float value; } library; /* 声明的右右花括号后跟变量名*/

struct { /* 无结构标记 */ 
char title[MAXTITL]; 
char author[MAXAUTL];
 float value; } library;

定义结构变量

初始化结构

struct book library = { 
"The Pious Pirate and the Devious Damsel", 
"Renee Vivotte", 
1.95 };

访问结构成员

library.float

结构的初始化器

C99和C11提供

1.struct book surprise = { .value = 10.99};
2. 可以按照任意顺序使用指定初始化器:

struct book gift = { 
 .value = 25.99,
 .author = "James Broadfool", 
 .title = "Rue for the Toad"}; 

3.与数组类似,在指定初始化器后面的普通初始化器,为指定成员后面的成员提供初始值。另外,对特定成员的最后一次赋值才是它实际获得的值。 例如:

 struct book gift= {
 .value = 18.90, 
 .author = "Philionna Pestle", 
 0.25}; 

赋给value的值是0.25,因为它在结构声明中紧跟在author成员之后。新 值0.25取代了之前的18.9。

指向结构的指针

使用->运算符。该运算符由一个连接号 (-)后跟一个大于号(>)组成。
如果him == &barney,那么him->income 即是 barney.income
如果him == &fellow[0],那么him ->income 即是 fellow[0].income

如果him == &fellow[0],那么*him == fellow[0],因为&和*是一对互逆运算符。因此, 可以做以下替代:

fellow[0].income == (*him).income

总之,如果him是指向guy类型结构barney的指针,下面的关系恒成立:

barney.income == (*him).income == him->income // 假设 him == &barney

其他结构特性

1.可以直接把一个结构赋值给另一个结构;
2.结构可以作为参数传递;
3.结构可以作为返回值。

结构和结构指针的选择

1.把指针作为参数有两个优点:无论是以前还是现在的C实现都能使用这种方法,而且执行起来很快,只需要传递一个地址。缺点是无法保护数据。 被调函数中的某些操作可能会意外影响原来结构中的数据。不过,ANSI C 新增的const限定符解决了这个问题。
(适用:追求效率)

  1. 把结构作为参数传递的优点是,函数处理的是原始数据的副本,这保护了原始数据。另外,代码风格也更清楚。
    (适用:小型结构)

结构中的字符数组和字符指针

指针应该只用来在程序中管理那些已分配和在别处分配的字符串。

如果要用结构储存字符串,用字符数组作为成员比较简单。用指向 char 的指针也行,但是误用会导致严重的问题。

结构、指针和malloc()

如果使用malloc()分配内存并使用指针储存该地址,那么在结构中使用指针处理字符串就比较合理。

复合字面量和结构(C99)

是把类型名放在圆括号中,后面紧跟一个用花括号括起来的初始化列表。
struct book类型的复合字面量: (struct book) {“The Idiot”, “Fyodor Dostoyevsky”, 6.99}

伸缩型数组成员(C99)

最后一个数组成员具有一些特性。
第1个特性是, 该数组不会立即存在。
第2个特性是,使用这个伸缩型数组成员可以编写合适的代码,就好像它确实存在并具有所需数目的元素一样。

声明一个伸缩型数组成员有如下规则:
伸缩型数组成员必须是结构的最后一个成员;
结构中必须至少有一个成员;
伸缩数组的声明类似于普通数组,只是它的方括号中是空的。

匿名结构(C11)

struct person 
{
int id;
struct {char first[20]; char last[20];}; // 匿名结构
};

初始化ted的方式相同:

 struct person ted = {8483, {"Ted", "Grass"}};

在访问ted时简化了步骤,只需把first看作是person的成员那样使用它:

puts(ted.first);

使用结构数组的函数

struct funds jones[N] =
{
{}{}
}

把结构内容保存到文件中

储存在一个结构中的整套信息被称为记录(record),
单独的项被称为字段(field)

更好的方案是使用fread()和fwrite()函数读写结构大小的单元。回忆一 下,这两个函数使用与程序相同的二进制表示法。例如:

 fwrite(&primer, sizeof(struct book), 1, pbooks); 

定位到 primer 结构变量开始的位置,并把结构中所有的字节都拷贝到与 pbooks 相关的文件中。
sizeof(struct book)告诉函数待拷贝的一块数据的大小,
1 表明一次拷贝一块数据。
带相同参数的fread()函数从文件中拷贝一块结构大小的数据到&primer指向的位置。
简而言之,这两个函数一次读写整个记录,而不是一个字段。

程序要点

以"a+b"模式打开文件。
a+部分允许程序读取整个文件并在文件的末尾添加内容。
b 是 ANSI的一种标识方法,表明程序将使用二进制文件 格式。
对于不接受b模式的UNIX系统,可以省略b,因为UNIX只有一种文件形式。

联合简介

联合(union)是一种数据类型

能在同一个内存空间中储存不同的数据类型(不是同时储存)。

其典型的用法是:
设计一种表以储存既无规律、事先也不知道顺序的混合类型。
使用联合类型的数组,其中的联合都大小相等,每个联合可以储存各种数据类型。

创建方式:
需要一个联合模板和联合变量。
可以用一个步骤定义联合,
也可以用联合标记分两步定义。

带标记的联合模板:

union hold 
{ 
int digit; 
double bigfl; 
char letter; 
}

声明的联合只能储存一个int类型的值或一个double类型的值或char类型的值。
下面定义了3个与hold类型相关的变量:
union hold fit; // hold类型的联合变量
union hold save[10]; // 内含10个联合变量的数组
union hold * pu; // 指向hold类型联合变量的指针

第1个声明创建了一个单独的联合变量fit。编译器分配足够的空间以便它能储存联合声明中占用最大字节的类型。
在本例中,占用空间最大的是double类型的数据。
在我们的系统中,double类型占64位,即8字节。

第2个 声明创建了一个数组save,内含10个元素,每个元素都是8字节。

第3个声明创建了一个指针,该指针变量储存hold类型联合变量的地址。

初始化联合。
需要注意的是,联合只能储存一个值,这与结构不同。
有 3 种初始化的方法:
把一个联合初始化为另一个同类型的联合;
初始化联合的第1个元素;
或者根据C99标准,使用指定初始化器:

union hold valA;
valA.letter = 'R'; 
union hold valB = valA; // 用另一个联合来初始化 
union hold valC = {88}; // 初始化联合的digit 成员 
union hold valD = {.bigfl = 118.2}; // 指定初始化器

使用联合

下面是联合的一些用法:

fit.digit = 23; //把 23 储存在 fit,占2字节 
fit.bigfl = 2.0; // 清除23,储存 2.0,占8字节 
fit.letter = 'h'; // 清除2.0,储存h,占1字节

在联合中,一次只储存一个值。
和用指针访问结构使用->运算符一样,用指针访问联合时也要使用->运算符:

pu = &fit;
x = pu->digit; // 相当于 x = fit.digit

匿名联合(C11)

union 
{ 
struct owner owncar; 
struct leasecompany leasecar; 
};

总结:结构和联合运算符

成员运算符:.
一般注释: 该运算符与结构或联合名一起使用,指定结构或联合的一个成员。
如果 name是一个结构的名称, member是该结构模版指定的一个成员名,下面标识了该结构的这个成员:
name.member
name.member的类型就是member的类型。联合使用成员运算符的方式与结构相同。
示例:

 struct
  { 
  int code; 
  float cost; 
  } item; 
  item.code = 1265; 

间接成员运算符:->
一般注释: 该运算符和指向结构或联合的指针一起使用,标识结构或联合的一个成员。
假设ptrstr是指向结构的指针,member是该结构模版指定的一个成员, 那么:
ptrstr->member 标识了指向结构的成员。
联合使用间接成员运算符的方式与结构相同。
示例:

struct
 { 
 int code; 
 float cost; 
 } item, * ptrst; 
 * ptrst = &item; 
 * ptrst->code = 3451; 

最后一条语句把一个int类型的值赋给item的code成员。
如下3个表达式是等价的:

ptrst->code 
item.code 
(*ptrst).code

枚举类型

可以用枚举类型(enumerated type)声明符号名称来表示整型常量。
使用enum关键字,可以创建一个新“类型”并指定它可具有的值(实际上,enum 常量是int类型,因此,只要能使用int类型的地方就可以使用枚举类型)。
枚举类型的目的是提高程序的可读性。它的语法与结构的语法相同。

enum spectrum {red, orange, yellow, green, blue, violet}; 
enum spectrum color;
int c; 
color = blue; 
if (color == yellow) 
...; 
for (color = red; color <= violet; color++) 
...;

注意:
必须把上面例子中的color声明为int类型,才能C和C++都兼容。

enum常量

blue和red到底是什么?
从技术层面看,它们是int类型的常量。
例如,假定有前面的枚举声明,可以这样写:

printf("red = %d, orange = %d\n", red, orange); 

其输出如下: red = 0, orange = 1

red成为一个有名称的常量,代表整数0。
类似地,其他标识符都是有名称的常量,分别代表1~5。

只要是能使用整型常量的地方就可以使用枚举常量。
例如,在声明数组时,可以用枚举常量表示数组的大小;
在switch语句中,可以把枚举常量作为标签。

默认值

默认情况下,枚举列表中的常量都被赋予0、1、2等。因此,下面的声明中nina的值是3:

enum kids {nippy, slats, skippy, nina, liz};

赋值

在枚举声明中,可以为枚举常量指定整数值:

enum levels {low = 100, medium = 500, high = 2000}; 

如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后面的常量会被赋予后续的值。
例如,假设有如下的声明: enum feline {cat, lynx = 10, puma, tiger};
那么,cat的值是0(默认),lynx、puma和tiger的值分别是10、11、 12

共享名称空间

这意味着在相同作用域中变量和标记的名称可以相同,不会引起冲突,但是不能在相同作用域中声明两个同名标签或同名变量。
例如,在C中,下面的代码不会产生冲突:

struct rect { double x; double y; }; 
int rect; // 在C中不会产生冲突 

尽管如此,以两种不同的方式使用相同的标识符会造成混乱。另外, C++不允许这样做,因为它把标记名和变量名放在相同的名称空间中。

typedef简介

#define类似,但是两者有3处不同:
1.与#define不同,typedef创建的符号名只受限于类型,不能用于值。
2.typedef由编译器解释,不是预处理器。
3.在其受限范围内,typedef比#define更灵活。

typedef unsigned char BYTE;

用BYTE代替unsigned char表明你打算用BYTE类型的变量表示数字,而不是字符码。
使用typedef还能提高程序的可移植性。

sizeof运算符和time()函数的返回类型:size_t类型。

C标准提供以下通用原型: time_t time(time_t *);
time_t 在一个系统中是 unsigned long,在另一个系统中可以是 unsigned long long。
只要包含time.h头文件,程序就能访问合适的定义。

#define BYTE unsigned char

还可以把typedef用于结构。

使用typedef的第2个原因是:typedef常用于给复杂的类型命名。

使用typedef时要记住,typedef并没有创建任何新类型,它只是为某个已存在的类型增加了一个方便使用的标签。

其他复杂的声明

1.数组名后面的[]和函数名后面的()具有相同的优先级。它们比*(解引 用运算符)的优先级高。
因此下面声明的risk是一个指针数组,不是指向数组的指针:

int * risks[10];

2…[]和()的优先级相同,由于都是从左往右结合。
所以下面的声明中, 在应用方括号之前,*先与rusks结合。
因此rusks是一个指向数组的指针,该数组内含10个int类型的元素:

int (* rusks)[10];

3.[]和()都是从左往右结合。
因此下面声明的goods是一个由12个内含50 个int类型值的数组组成的二维数组,不是一个有50个内含12个int类型值的数组组成的二维数组:

int goods[12][50];

把以上规则应用于下面的声明:

int * oof[3][4]; 

[3]比*的优先级高,由于从左往右结合,所以[3]先与oof结合。因此, oof首先是一个内含3个元素的数组。然后再与[4]结合,所以oof的每个元素 都是内含4个元素的数组。*说明这些元素都是指针。最后,int表明了这4个 元素都是指向int的指针。

因此,这条声明要表达的是:foo是一个内含3个元 素的数组,其中每个元素是由4个指向int的指针组成的数组。简而言之,oof 是一个3×4的二维数组,每个元素都是指向int的指针。编译器要为12个指针 预留存储空间。

现在来看下面的声明:

int (* uuf)[3][4];

圆括号使得*先与uuf结合,说明uuf是一个指针,所以uuf是一个指向3×4的int类型二维数组的指针。编译器要为一个指针预留存储空间。

根据这些规则,还可以声明:

char * fump(int); // 返回字符指针的函数 
char (* frump)(int); // 指向函数的指针,该函数的返回类型为char 
char (* flump[3])(int); // 内含3个指针的数组,每个指针都指向返回类型为char的函数 这3个函数都接受int类型的参数。 

可以使用typedef建立一系列相关类型:

typedef int arr5[5]; 
typedef arr5 * p_arr5; 
typedef p_arr5 arrp10[10]; 
arr5 togs; // togs 是一个内含5个int类型值的数组 
p_arr5 p2; // p2 是一个指向数组的指针,该数组内含5个int类型的值 
arrp10 ap; // ap 是一个内含10个指针的数组,每个指针都指向一个内含5个int类型值的数组 

如果把这些放入结构中,声明会更复杂。

函数和指针

void ToUpper(char *); // 把字符串中的字符转换成大写字符
void (*pf)(char *); // pf 是一个指向函数的指针

提示要声明一个指向特定类型函数的指针,可以先声明一个该类型的函数, 然后把函数名替换成(*pf)形式的表达式。然后,pf就成为指向该类型函数的指针。

void ToUpper(char *); 
void ToLower(char *); 
int round(double); 
void (*pf)(char *); 
pf = ToUpper; // 有效,ToUpper是该类型函数的地址 
pf = ToLower; //有效,ToUpper是该类型函数的地址 
pf = round; // 无效,round与指针类型不匹配 
pf = ToLower(); // 无效,ToLower()不是地址
#include <math.h> /* 提供sin()函数的原型:double sin(double) */
 ... 
double (*pdf)(double);
double x; 
pdf = sin; 
x = (*pdf)(1.2); // 调用sin(1.2) 
x = pdf(1.2); // 同样调用 sin(1.2)

位操作

二进制数、位和字节

通常,1字节包含8位。

C语言用字节(byte)表示储存系统字符集所需的大小,所以C字节可能是8位、9位、16位或其他值。

可以从左往右给这8位分别编号为7~0。
在1字节中,编号是7的位被称为高阶位 (high-order bit),
编号是0的位被称为低阶位(low-order bit)。

通常unsigned char用1字节表示的范围是0~255,
而signed char用1字节表示的范围是-128~+127。

其他

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值