Note

C语言超级无敌速成

2022年08月11日 Thursday · 56 min read

使用内存

C被设计为一种低级语言,可以轻松访问内存位置并执行与内存相关的操作。

例如,scanf() 函数将用户输入的值放在变量的位置或地址处。这是通过使用&符号实现的。

例如:

int num;
printf("Enter a number: ");

scanf("%d", &num);

printf("%d", num);

&num是变量num的地址。

内存地址以十六进制数给出。十六进制,是一个基数为16的数字系统,它使用数字0到9和字母A到F(16个字符)代表一组四个二进制数字,其值可以从0到15。

读取32位内存的8位十六进制数字要比尝试破译32位1和0的二进制代码容易得多。

以下程序显示变量i和k的内存地址:

void test(int k);

int main() {
  int i = 0;
    
  printf("The address of i is %x\n", &i);
  test(i);
  printf("The address of i is %x\n", &i);
  test(i);

  return 0;
}

void test(int k) {
  printf("The address of k is %x\n", &k);
}

在printf语句中,%x是十六进制格式说明符。

程序输出因运行会有所不同,但看起来类似于:

The address of i is 846dd754
The address of k is 846dd758
The address of i is 846dd754
The address of k is 846dd758

从变量声明到变量作用域结束的地址都保持不变。

什么是指针?

取地址运算符(&):返回操作数的内存地址。

解引用操作符(*):返回位于操作数所指定地址的变量的值。

指针在C编程中非常重要,因为它们使你可以轻松地处理内存位置。

指针是数组,字符串以及其他数据结构和算法的基础。

指针是一个变量,其中包含另一个变量的地址。换句话说,它“指向”分配给变量的位置,并且可以间接访问该变量。

指针使用*符号声明,语法如下:

指针数据类型 *标识符 

实际的指针数据类型是十六进制数,但是在声明指针时,必须指出它将指向的数据类型。

星号 * 声明一个指针,并应出现在用于指针变量的标识符旁边。

以下程序演示了变量,指针和地址:

int j = 63;
int *p = NULL;
p = &j; 

printf("The address of j is %x\n", &j);
printf("p contains address %x\n", p);
printf("The value of j is %d\n", j);
printf("p is pointing to the value %d\n", *p); 

关于此程序,需要注意以下几点:

在将指针分配给有效位置之前,应将其初始化为NULL。

可以使用&符号为指针分配变量的地址。

要查看指针指向的内容,请再次使用 *,如 *p 中所示。

在这种情况下,* 被称为间接或取消引用运算符。该过程称为取消引用。

程序输出类似于:

The address of j is ff3652cc
p contains address ff3652cc
The value of j is 63
p is pointing to the value 63 

一些算法使用指向指针的指针。这种类型的变量声明使用**,并且可以分配另一个指针的地址,如下所示:

int x = 12;
int *p = NULL
int **ptr = NULL;
p = &x;
ptr = &p;

表达式中的指针

指针可以与任何变量一样在表达式中使用。算术运算符可以应用于指针所指向的任何对象。

例如:

int x = 5;
int y;
int *p = NULL;
p = &x;

y = *p + 2; /* y 被赋予 7 */
y += *p;     /* y 被赋予 12 */
*p = y;       /* x 被赋予 12 */
(*p)++;      /* x 增加到 13 */

printf("p指向值 %d\n", *p); 

运行结果:

p指向值 13

注意,++运算符需要括号才能增加所指向的值。使用--运算符时也是如此。

指针和数组

指针对于数组特别有用。

当定义各一个数组时, 系统会在内存中为该数组分配一个存储空间, 其数组的名称就是数组在内存中的首地址.若再定义各一个指针变量,并将数组的首地址传给指针变量,则该指针就指向了这个一维数组.

例如:

int *p, a[5];
p=a;

这里a是数组名, 也就是数组的首地址, 将它赋给指针变量p, 也就是将数组a的首地址赋给p.

也可以写成如下形式:

int *p, a[5];
p = &a[0];

使用指针,我们可以指向第一个元素,然后使用地址算法遍历数组: +用于向前移动到存储位置 -用于向后移动到存储位置

int a[5] = {22, 33, 44, 55, 66};
int *ptr = NULL;
int i;

ptr = a;
for (i = 0; i < 5; i++) {
  printf("%d ", *(ptr + i));
}

程序输出为:22 33 44 55 66

数组的一个重要概念是,数组名称充当指向数组第一个元素的指针。

因此,语句 ptr = a 可以认为是 ptr =&a[0]。

更多地址算法

地址运算也可以视为指针运算,因为操作涉及指针。

除了使用+和–来引用下一个和上一个存储器位置之外,还可以使用赋值运算符更改指针包含的地址。

例如:

int a[5] = {22, 33, 44, 55, 66};
int *ptr = NULL;

ptr = a;  /* point to the first array element */
printf("%d  %x\n", *ptr, ptr);  /* 22 */
ptr++;
printf("%d  %x\n", *ptr, ptr);  /* 33 */
ptr += 3;
printf("%d  %x\n", *ptr, ptr);  /* 66 */
ptr--;
printf("%d  %x\n", *ptr, ptr);  /* 55 */
ptr -= 2;
printf("%d  %x\n", *ptr, ptr);  /* 33 */ 

程序输出类似于:

22 febd4760
33 febd4764
66 febd4770
55 febd476c
33 febd4764 

你也可以使用 ==,< 和 > 运算符比较指针地址。

指针与函数

指针大大扩展了功能的可能性。我们不再局限于返回一个值。使用指针参数,你的函数可以更改实际数据,而不是数据副本。

要更改变量的实际值,调用语句将地址传递给函数中的指针参数。

例如,以下程序交换两个值:

void swap (int *num1, int *num2);

int main() {
  int x = 25;
  int y = 100;

  printf("x is %d, y is %d\n", x, y); 
  swap(&x, &y);
  printf("x is %d, y is %d\n", x, y); 

  return 0;
}
 
void swap (int *num1, int *num2) {
  int temp;

  temp = *num1;
  *num1 = *num2;
  *num2 = temp;
}

程序交换变量的实际值,因为函数使用指针按地址访问它们。

具有数组参数的函数

数组不能通过值传递给函数。但是,数组名是一个指针,因此仅将数组名传递给函数就是将指针传递给数组。

例如:

int add_up (int *a, int num_elements);

int main() {
  int orders[5] = {100, 220, 37, 16, 98};

  printf("Total orders is %d\n", add_up(orders, 5)); 

  return 0;
}

int add_up (int *a, int num_elements) {
  int total = 0;
  int k;

  for (k = 0; k < num_elements; k++) {
    total += a[k];
  }

  return (total);
} 

输出:

Total orders is 471

返回数组的函数

正如可以将指向数组的指针传递给函数一样,可以返回指向数组的指针,如以下程序所示:

int * get_evens();

int main() {
  int *a;
  int k;

  a = get_evens(); /* get first 5 even numbers */
  for (k = 0; k < 5; k++)
    printf("%d\n", a[k]); 

  return 0;
}

int * get_evens() {
  static int nums[5];
  int k;
  int even = 0;

  for (k = 0; k < 5; k++) {
    nums[k] = even += 2;
  }

  return (nums);
}

注意,声明了一个指针(而不是数组)来存储该函数返回的值。

还要注意,当从函数传递局部变量时,需要在函数中将其声明为静态变量。

请记住,a[k] 与 *(a + k)相同。

该代码的输出是什么?

#include<stdio.h>
int * test() {
static int x[4];
for(int i=0;i<4;i++){
x[i] = i%2;
}
return x;
}

int main() {
int * arr = test();
printf("%d", *(arr+3));
}
输出结果为 1

字符串

C中的字符串是一个以NULL字符'\0'结尾的字符数组。

字符串声明可以通过多种方式进行,每种方式都有其各自的考虑因素。

例如:

char str_name[str_len] = "string"; 

这将创建一个由str_len个字符组成的名为str_name的字符串,并将其初始化为值“ string”。

提供字符串文字以初始化字符串时,编译器会自动将NULL字符'\0'添加到char数组。

因此,必须声明数组大小至少比预期的字符串长度长一个字符。

下面的语句创建包含NULL字符的字符串。如果声明不包含char数组大小,则将根据初始化中字符串的长度加上'\0'的值来计算:

char str1[6] = "hello";
char str2[ ] = "world";  /* size 6 */

字符串也可以声明为一组字符:

char str3[6] = {'h', 'e', 'l', 'l', 'o', '\0'};
char str4[ ] = {'h', 'e', 'l', 'l', 'o', '\0'}; /* size 6 */ 

使用这种方法,必须显式添加NULL字符。注意,这些字符用单引号引起来。

与任何数组一样,字符串的名称充当指针。

字符串文字是用双引号引起来的文本。

诸如 'b' 之类的字符单引号引起来,不能视为字符串。

字符串指针声明,例如 char *str ="stuff"; 被认为是常量,不能从其初始值更改。

为了安全,方便地使用字符串,可以使用下面显示的“标准库”字符串函数。不要忘记引入<string.h>头文件。

strlen() -获取字符串的长度

strcat()-合并两个字符串

strcpy()-将一个字符串复制到另一个

strlwr()-将字符串转换为小写

trupr()-将字符串转换为大写

strrev()-反向字符串

strcmp()-比较两个字符串

字符串输入

程序通常是交互式的,要求用户输入。

为了从用户那里检索一行文本或其他字符串,C提供了scanf(),gets()和fgets()函数。

你可以使用scanf()根据格式说明符读取输入。

例如:

char first_name[25];
int age;
printf("Enter your first name and age: \n");
scanf("%s %d", first_name, &age); 

当使用scanf()读取字符串时,不需要&访问变量地址,因为数组名称充当指针。

scanf()到达空格时停止读取输入。

要读取带空格的字符串,请使用gets()函数。它读取输入,直到到达终止换行符(按Enter键)。

例如:

char full_name[50];
printf("Enter your full name: ");
gets(full_name);

gets()的更安全替代方法是fgets(),它最多读取指定数量的字符。

这种方法有助于防止缓冲区溢出(当字符串数组的大小不足以容纳键入的文本时发生)。

例如:

char full_name[50];
printf("Enter your full name: ");
fgets(full_name, 50, stdin);

fgets() 参数是字符串名称,要读取的字符数以及指向要从中读取字符串的指针。 stdin表示从标准输入(即键盘)中读取。

get 和 fgets 之间的另一个区别是换行符由fgets存储。

fgets() 仅从stdin读取n-1个字符,因为必须有用于'\0'的空间。

字符串输出

字符串输出由fputs(),putf()和printf()函数处理。

fputs 函数

fputs 函数的作用是想指定的文件写入一个字符串,其中字符串可以是字符串常量,也可以是字符组名、指针或变量。

fputs 一般形式如下:

fputs(字符串, 文件指针)

例如:

#include <stdio.h>
int main()
{
  FILE *wj=NULL;
  wj=fopen("E:\\文件夹1\\生成.txt","w+")
  char city[40];
  printf("Enter your favorite city: ");
  gets(city);
    //fgets(要写入的数组,大小,要读取的文件)//一般用法
  // fgets(city, 40, stdin);//从stdin输入中读取40位写入city数组中 此用法与gets(city)同义
//超过40位以外的不读取!!
  fputs(city, wj);//将city里的数据写入到wj
    puts(city);//在终端显示city里面的数据
   // fputs(city,stdout);//与puts相同
  printf(" is a fun city.");

  return 0;
}

puts() 函数仅接受字符串参数,也可以用于显示输出。

但是,puts() 将在输出中添加换行符。

例如:

#include <stdio.h>
int main()
{
  char city[40];
  printf("Enter your favorite city: ");
  gets(city);
  // Note: for safety, use
  // fgets(city, 40, stdin);

  puts(city);

  return 0;
}

sprintf 和 sscanf 函数

可以使用 sprintf() 函数创建格式化的字符串。这对于用其他数据类型来构建字符串很有用。

例如:

#include <stdio.h>
int main()
{
  char info[100];
  char dept[ ] = "HR";
  int emp = 75;
  sprintf(info, "The %s dept has %d employees.", dept, emp);
  printf("%s\n", info);

  return 0;
}

结果是:

The HR dept has 75 employees.

另一个有用的函数是 sscanf(),用于扫描字符串中的值。

该函数从字符串中读取值,并将其存储在相应的变量地址中。

例如:

#include <stdio.h>
int main()
{
  char info[ ] = "Snoqualmie WA 13190";
  char city[50];
  char state[50];
  int population;
  sscanf(info, "%s %s %d", city, state, &population);
  printf("%d people live in %s, %s.", population, city, state);

  return 0;
}

结果是:

13190 people live in Snoqualmie, WA.

string.h库(字符串函数)

string.h库包含许多字符串函数。

程序顶部使用语句#include <string.h>使你可以访问以下内容:

  • strlen(str) 返回存储在str中的字符串的长度,不包括NULL字符。

  • strcat(str1, str2) 将str2追加(连接)到str1的末尾,并返回指向str1的指针。

  • strcpy(str1, str2) 将str2复制(覆盖)到str1。此功能对于为字符串分配新值很有用。

  • strncat(str1, str2, n) 将str2的前n个字符追加(连接)到str1的末尾,并返回指向str1的指针。

  • strncpy(str1, str2, n) 将str2的前n个字符复制(覆盖)到str1。

  • strcmp(str1, str2) 当str1等于str2时返回0,在 str1 <str2 时返回小于0,在 str1> str2 时返回大于0。

  • strncmp(str1, str2, n) 当str1的前n个字符等于str2的前n个字符时,返回0;当str1 <str2时,小于0;当str1> str2时,大于0。

  • strchr(str1, c) 返回指向str1中首次出现的char c的指针,如果找不到字符,则返回NULL。

  • strrchr(str1,c) 反向搜索str1并返回一个指向char c在str1中位置的指针;如果找不到字符,则返回NULL。

  • strstr(str1,str2) 返回指向str1中首次出现的str2的指针,如果未找到str2,则返回NULL。

下面的程序演示了string.h函数:

#include <stdio.h>
#include <string.h>

int main()
{
  char s1[ ] = "The grey fox";
  char s2[ ] = " jumped.";
    
  strcat(s1, s2);
  printf("%s\n", s1);
  printf("Length of s1 is %d\n", strlen(s1));
  strcpy(s1, s2);
  printf("s1 is now %s \n", s1);

  return 0;
}

运行结果:

The grey fox jumped.
Length of s1 is 20
s1 is now  jumped. 

将字符串转换为数字

将数字字符串转换为数值是C编程中的常见任务,通常用于防止运行时错误。

读取字符串比期望数值更容易出错,用户可能不小心键入“ o”而不是“ 0”(零)。

stdio.h库包含以下用于将字符串转换为数字的函数:

  • int atoi(str) 代表ASCII转成整数。将str转换为等效的int值。如果第一个字符不是数字或未遇到任何数字,则返回0。

  • double atof(str)表示ASCII转成浮动。将str转换为等效的double值。如果第一个字符不是数字或未遇到数字,则返回0.0。

  • long int atol(str) 表示ASCII转成long int。将str转换为等效的长整数值。如果第一个字符不是数字或未遇到任何数字,则返回0。

例如:

#include <stdio.h>
int main()
{
  char input[10];
  int num;
    
  printf("Enter a number: ");
  gets(input);
  num = atoi(input);

  return 0;
}

注意,atoi() 缺少错误处理,如果要确保已完成正确的错误处理,建议使用strtol()。

字符串数组

二维数组可用于存储相关的字符串。

以下语句,该语句声明一个包含3个元素的数组,每个元素包含15个字符:

char trip[3][15] = {
  "suitcase",
  "passport",
  "ticket"
};

尽管字符串长度有所不同,但必须声明一个足够大的大小以容纳最长的字符串。另外,访问这些元素可能非常麻烦。

引用trip[0]表示“suitcase”容易出错。相反,你必须将[0][0]处的元素视为“ s”,将[2][3]处的元素视为“ k”,依此类推。

处理相关字符串集合的一种更简单,更直观的方法是使用指针数组,如以下程序所示:

char *trip[ ] = {
  "suitcase",因为每个元素的长度都可以变化,所以与二维网格结构相比,字符串指针数组的结构更加参差不齐。
  "passport",
  "ticket"
};

printf("Please bring the following:\n");
for (int i = 0; i < 3; i++) {
  printf("%s\n", trip[ i ]);
}

因为每个元素的长度都可以变化,所以与二维网格结构相比,字符串指针数组的结构更加参差不齐。

使用这种方法,字符串长度没有限制。更重要的是,可以通过指向每个字符串的第一个字符的指针来引用项目。

请记住,像 char * items [3]; 这样的声明;仅保留三个指针的空间;这些指针引用了实际的字符串。

函数指针

由于指针可以指向任何存储器位置中的地址,因此它们也可以指向可执行代码的开头。

函数指针或函数指针指向内存中函数的可执行代码。函数指针可以存储在数组中,也可以作为参数传递给其他函数。

函数指针声明使用 * 就像使用任何指针一样:

return_type (*函数名)(参数) 

(*函数名) 周围的括号很重要。没有括号,编译器会认为函数在返回指针。

声明函数指针后,必须将其分配给函数。

下面的简短程序声明一个函数,声明一个函数指针,将该函数指针分配给该函数,然后通过该指针调用该函数:

#include <stdio.h>
void say_hello(int num_times); /* function */

int main() {
  void (*funptr)(int);  /* function pointer */
  funptr = say_hello;  /* 看注意 */
  funptr(3);  /* function call */
    
  return 0;
}

void say_hello(int num_times) {
  int k;
  for (k = 0; k < num_times; k++)
    printf("Hello\n");
}

注意

函数名称指向可执行代码的开头,就像数组名称指向其第一个元素一样。

因此,尽管诸如 funptr = &say_hello(*funptr)(3) 之类的语句是正确的,但在函数分配和函数调用中不必包括地址运算符&和间接运算符*。

函数指针数组

函数指针数组可以替换开关或if语句以选择动作,如以下程序所示:

#include <stdio.h>

int add(int num1, int num2);
int subtract(int num1, int num2);
int multiply(int num1, int num2);
int divide(int num1, int num2);

int main() 
{
  int x, y, choice, result;
  int (*op[4])(int, int);

  op[0] = add;
  op[1] = subtract;
  op[2] = multiply;
  op[3] = divide;
  printf("Enter two integers: ");
  scanf("%d%d", &x, &y);
  printf("Enter 0 to add, 1 to subtract, 2 to multiply, or 3 to divide: ");
  scanf("%d", &choice);
  result = op[choice](x, y);
  printf("%d", result);
    
  return 0;
}

int add(int x, int y) {
  return(x + y);
}

int subtract(int x, int y) {
  return(x - y);
}

int multiply(int x, int y) {
  return(x * y);
}

int divide(int x, int y) {
  if (y != 0)
    return (x / y);
  else
    return 0;
}

int (*op[4])(int, int); 语句声明函数指针的数组。每个数组元素必须具有相同的参数和返回类型。

在这种情况下,分配给数组的函数具有两个int参数并返回一个int。

空指针

空指针用于引用内存中的任何地址类型,并具有类似于以下内容的声明:

void *ptr;

以下程序将相同的指针用于三种不同的数据类型:

int x = 33;
float y = 12.4;
char c = 'a';
void *ptr;
  
ptr = &x;
printf("void ptr points to %d\n", *((int *)ptr));
ptr = &y;
printf("void ptr points to %f\n", *((float *)ptr));
ptr = &c;
printf("void ptr points to %c", *((char *)ptr));
  • *(int )ptr是表示把ptr强制转换成一个int型的指针。

  • **(int )ptr就是取ptr指向的内容的意思,跟*ptr的那个*作用一样

  • **void指针可以保存各种其它指针类型。**大多数时候它被用来存储数据结构。

取消引用空指针时,必须先使用类型将指针转换为适当的数据类型,然后再使用*取消引用。

不能使用空指针执行指针运算。

使用空指针的函数

空指针通常用于函数声明。

例如:

void * square (const void *); 

使用 void * 返回类型允许任何返回类型。同样,void * 的参数可以接受任何参数类型。

如果要使用参数传递的数据而不更改它,则将其声明为const

你可以省略参数名称,以进一步使声明与其实现的隔离。通过这种方式声明一个函数,可以根据需要自定义,而不必更改声明。

#include <stdio.h>

void* square (const void* num);

int main() {
  int x, sq_int;
  x = 6;
  sq_int = square(&x);
  printf("%d squared is %d\n", x, sq_int);

  return 0;
}

void* square (const void *num) {
  int result;
  result = (*(int *)num) * (*(int *)num);
  return result;
}

square 函数需要 int 类型,这就是为什么将num void指针强制转换为int的原因。

函数指针作为参数

使用函数指针的另一种方法是将其作为参数传递给另一个函数。

用作参数的函数指针有时称为回调函数,因为接收函数会“回调它”。

stdlib.h头文件中的 qsort() 函数使用此技术。

quicksort是一种广泛用于对数组进行排序的算法。

要在程序中实现排序,只需包含stdlib.h文件,然后编写一个与qsort中使用的声明匹配的比较函数:

函数声明:

void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))

qsort声明解释:

base 指向要排序的数组的第一个元素的指针。。

nitems 由 base 指向的数组中元素的个数。

size 数组中每个元素的大小,以字节为单位。

compar 用来比较两个元素的函数,即函数指针(回调函数)

回调函数:

回调函数就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就说这是回调函数。

compar参数

compar参数指向一个比较两个元素的函数。比较函数的原型应该像下面这样。注意两个形参必须是const void *型,同时在调用compar 函数(compar实质为函数指针,这里称它所指向的函数也为compar)时,传入的实参也必须转换成const void *型。在compar函数内部会将const void *型转换成实际类型。

​ int compar(const void *p1, const void *p2);

​ 如果compar返回值小于0(< 0),那么p1所指向元素会被排在p2所指向元素的左面;

如果compar返回值等于0(= 0),那么p1所指向元素与p2所指向元素的顺序不确定;

如果compar返回值大于0(> 0),那么p1所指向元素会被排在p2所指向元素的右面。

以下程序使用qsort从低到高对一个整数数组进行排序:

#include <stdio.h>
#include <stdlib.h>

int compare (const void *, const void *); 

int main() {
  int arr[5] = {52, 23, 56, 19, 4};
  int num, width, i;
  
  num = sizeof(arr)/sizeof(arr[0]);
  width = sizeof(arr[0]);
  qsort((void *)arr, num, width, compare);
  for (i = 0; i < 5; i++)
    printf("%d ", arr[ i ]);
    
  return 0;
}

int compare (const void *elem1, const void *elem2) {
  if ((*(int *)elem1) == (*(int *)elem2))
    return 0;
  else if ((*(int *)elem1) < (*(int *)elem2))
    return -1;
  else
    return 1;
}

我们在qsort调用中使用了函数名,因为函数名充当了指针。用作参数的函数指针有时称为:回调函数。

结构体

在一些情况下,程序员可以将一些有关的变量组织起来定义成一个结构, 这样来表示一个有机的整体或一个新的类型,因此程序就可以像处理内部的基本数据那样对结构进行各种操作。

“结构体”是一种构造类型,它是由若干“成员”组成的,其中的每一个成员可以是一个基本数据类型或者又是一个构造类型。既然结构体是一种新的类型,就需要先对其进行构造,这里这种操作称为声明一个结构体。

声明结构体的过程就好比生产商品的过程,只有商品生产出来才可以使用该商品。

加入在程序中使用“商品”这样一个类型,一般的商品具有产品名称,形状,颜色,功能,价格和产地等。

声明结构体时使用的关键字是 struct, 其一般形式为:

struct 结构体名
{
     成员列表
}

例如:

struct course {
  int id;
  char title[40];
  float hours; 
}; 

此struct语句定义了一个新的数据类型,命名为course,它具有三个成员。

结构成员可以是任何数据类型,包括基本类型,字符串,数组,指针,甚至其他结构。

不要忘了在结构声明后加上分号。

结构也称为复合或聚合数据类型。

结构体变量的定义

要声明结构数据类型的变量,请使用关键字struct,后跟struct标记,然后变量名。

例如,下面的语句声明一个结构数据类型,然后使用Student结构声明变量s1和s2:

struct student {
  int age;
  int grade;
  char name[40];
};

/* declare two variables */
struct student s1;
struct student s2; 

结构变量存储在连续的内存块中。就像基本数据类型一样,必须使用sizeof运算符来获取结构所需的字节数。

声明结构体变量

也可以在声明中通过在花括号内按顺序列出初始值来初始化struct变量:

struct student s1 = {19, 9, "John"};
struct student s2 = {22, 10, "Batman"};

如果要在声明后使用花括号初始化结构,则还需要写明类型转换,如以下语句所示:

struct student s1;
s1 = (struct student) {19, 9, "John"};

初始化结构相应成员:

struct student s1 
= { .grade = 9, .age = 19, .name = "John"}; 

在上面的示例中,.grade表示结构体的grade成员。同理 .age 和 .name 对应结构体 age 和 name 成员。

结构体变量的引用

你可以使用 **.(点运算符)**来访问struct变量的成员。

语法:

结构体变量.成员名

例如,要将值分配给s1结构变量age成员:

s1.age = 19;

你还可以将一个结构分配给相同类型的另一个结构:

struct student s1 = {19, 9, "Jason"};
struct student s2;
//....
s2 = s1; 

以下代码演示了如何使用结构:

#include <stdio.h>
#include <string.h>

struct course {
  int id;
  char title[40];
  float hours; 
};

int main() {
  struct course cs1 = {341279, "Intro to C++", 12.5};
  struct course cs2;

  /* initialize cs2 */
  cs2.id = 341281;
  strcpy(cs2.title, "Advanced C++");
  cs2.hours = 14.25;
   
  /* display course info */
  printf("%d\t%s\t%4.2f\n", cs1.id, cs1.title, cs1.hours);
  printf("%d\t%s\t%4.2f\n", cs2.id, cs2.title, cs2.hours);
  
  return 0;
}

字符串分配需要使用 string.h 库中的strcpy()。

使用typedef

typedef关键字 创建一个类型定义,该定义可简化代码并使程序更易于阅读。

typedef 通常与结构一起使用,因为它消除了在声明变量时使用关键字struct的需要。

例如:

typedef struct {
  int id;
  char title[40];
  float hours; 
} course;

course cs1;
course cs2; 

注意,不再使用结构标签,而是在结构声明之前显示typedef名称。

现在,变量声明中不再需要使用struct一词,从而使代码更简洁,更易于阅读。

结构与结构

结构的成员也可以是结构。

例如:

typedef struct {
  int x;
  int y;
} point;

typedef struct {
  float radius;
  point center;
} circle; 

嵌套花括号用于初始化结构成员。点运算符两次用于访问成员的成员,如语句中所示:

circle c = {4.5, {1, 3}};
printf("%3.1f %d,%d", c.radius, c.center.x, c.center.y);
/* 4.5  1,3 */

必须先出现一个结构定义,然后才能在另一个结构中使用它。

指向结构的指针

像指向变量的指针一样,也可以定义指向结构的指针。

struct myStruct *struct_ptr;

定义一个指向myStruct结构的指针。

struct_ptr = &struct_var;

将结构变量 struct_var 的地址存储在指针struct_ptr中。

struct_ptr -> struct_mem;

访问结构成员struct_mem的值。

例如:

struct student{
  char name[50];
  int number;
  int age;
};

// Struct pointer as a function parameter
void showStudentData(struct student *st) {
  printf("\nStudent:\n");
  printf("Name: %s\n", st->name);
  printf("Number: %d\n", st->number);
  printf("Age: %d\n", st->age);
}

struct student st1 = {"Krishna", 5, 21};
showStudentData(&st1);

-> 运算符允许通过指针访问结构的成员。

(*st).age与st->age相同。
同样,当使用typedef命名结构时,仅使用typedef名称以及*和指针名称来声明指针。

结构作为函数参数

一个函数可以具有结构参数,当仅需要结构变量的副本时,该结构参数将按值接受参数。

要使函数更改struct变量中的实际值,则需要使用指针参数。

例如:

#include <stdio.h>
#include <string.h>

typedef struct {
  int id;
  char title[40];
  float hours; 
} course;

void update_course(course *class);
void display_course(course class);

int main() {
  course cs2;
  update_course(&cs2);
  display_course(cs2);
  return 0;
}

void update_course(course *class) {
  strcpy(class->title, "C++ Fundamentals");
  class->id = 111;
  class->hours = 12.30;
}

void display_course(course class) {
  printf("%d\t%s\t%3.2f\n", class.id, class.title, class.hours);
}

如你所见,update_course() 将指针作为参数,而display_course()将按值获取结构。

结构数组

数组可以存储任何数据类型的元素,包括结构。

声明结构数组后,可以使用索引号访问元素。

然后使用点运算符访问元素的成员,如程序中所示:

#include <stdio.h>

typedef struct {
  int h;
  int w;
  int l;
} box;

int main() {
  box boxes[3] = {{2, 6, 8}, {4, 6, 6}, {2, 6, 9}};
  int k, volume;
  
  for (k = 0; k < 3; k++) {
    volume = boxes[k].h*boxes[k].w*boxes[k].l;
    printf("box %d volume %d\n", k, volume);
  }
  return 0;
}

结构数组用于数据结构,例如链表,二进制树等。

共用体

共用体看起来像结构体,只不过关键字由struct变成了union。

  • 共用体和结构体的区别在于:结构体定义了一个由多个数据成员组成的特殊类型,而共用体定义了一块为所有数据成员共享的内存。

  • 共用体也称为联合体,它使几种不同类型的变量存放到同一段内存单元中。所以共用体在同一时刻只能有一个值,它属于某一个数据成员。由于所有成员位于同一块内存,因此共用体的大小就等于最大成员的大小。

定义共用体的类型变量一般形式为:

union 共用体名
{
    成员列表
}变量列表;

例如:

union val {
  int int_num;
  float fl_num;
  char str[20]; 
};

声明 union(共用体) 之后,可以声明 union(共用体) 变量。你甚至可以将一个共用体分配给另一个相同类型的共用体(联合体):

union val u1;
union val u2;
u2 = u1; 

访问共用体成员

你可以使用 . (点符号) 来访问共用体的成员。

语法:

共用体变量.成员名

尝试访问不占用内存位置的成员会产生意外的结果。

以下程序演示了如何访问共用体成员:

union val {
  int int_num;
  float fl_num;
  char str[20]; 
};
  
union val test;

test.int_num = 123;
test.fl_num = 98.76;
strcpy(test.str, "hello");

printf("%d\n", test.int_num);
printf("%f\n", test.fl_num);
printf("%s\n", test.str);

结果:

1819043176
1143141483620823940762435584.000000
hello

最后一个赋值将覆盖先前的赋值,这就是为什么str存储一个值并且访问int_num和fl_num毫无意义的原因。

结构体内共用体

因为在结构中可以具有一个成员来跟踪哪个联合成员存储值,所以通常在结构内使用联合。

例如,在以下程序中,车辆结构使用车辆识别号(VIN)或分配的ID,但不能同时使用两者:

typedef struct {
  char make[20];
  int model_year;
  int id_type; /* 0 for id_num, 1 for VIN */
  union {
    int id_num;
    char VIN[20]; 
  } id;
} vehicle;

vehicle car1;
strcpy(car1.make, "Ford");
car1.model_year = 2017;
car1.id_type = 0;
car1.id.id_num = 123098; 

注意,该联合在结构内部声明

这样做时,声明的末尾需要一个联合名(共用体名)。

id_type跟踪哪个联合成员存储一个值。以下语句显示car1数据,并使用id_type确定要读取的联合成员:

/* display vehicle data */
printf("Make: %s\n", car1.make);
printf("Model Year: %d\n", car1.model_year);
if (car1.id_type == 0)
  printf("ID: %d\n", car1.id.id_num);
else
  printf("ID: %s\n", car1.id.VIN); 

共用体也可以包含结构。

共用体指针

指向共用体的指针指向分配给共用体的内存位置。

通过使用关键字 union 和 union 标签以及*和指针名称来声明共用体指针。

例如:

union val {
  int int_num;
  float fl_num;
  char str[20]; 
};

union val info;
union val *ptr = NULL;
ptr = &info;
ptr->int_num = 10;
printf("info.int_num is %d", info.int_num); 

如果要通过指针访问共用体成员,则需要->运算符。

(*ptr).int_num 与 ptr->int_num 相同

共用体作为函数参数

一个函数可以具有共用体参数,当需要共用体变量的副本时,该参数可以按值接受参数。

函数要更改共用体存储位置中的实际值,需要使用指针参数。

例如:

union id {
  int id_num;
  char name[20]; 
};

void set_id(union id *item) {
  item->id_num = 42;
}

void show_id(union id item) {
  printf("ID is %d", item.id_num);
} 

共用体数组

数组可以存储任何数据类型的元素,包括共用体。

使用共用体时,请记住,共用体中只有一个成员可以存储每个数组元素的数据,这一点很重要。

在声明共用体数组之后,可以使用索引号访问元素。然后使用点运算符访问共用体(联合)的成员,如程序中所示:

union val {
  int int_num;
  float fl_num;
  char str[20]; 
};

union val nums[10];
int k;

for (k = 0; k < 10; k++) {
  nums[k].int_num = k;
}

for (k = 0; k < 10; k++) {
  printf("%d  ", nums[k].int_num);
} 

数组是一种数据结构,用于存储所有相同类型的集合值。

共同体数组允许存储不同类型的值。

例如:

union type {
  int i_val;
  float f_val;
  char ch_val;
};
union type arr[3];
arr[0].i_val = 42;
arr[1].f_val = 3.14;
arr[2].ch_val = 'x'; 



内存管理

内存的组织方式

开发人员将程序编写完成后,程序要先转载到计算机的内核或者半导体内存中,再运行程序。

程序被组织成一下4个逻辑段:

可执行代码

静态数据。可执行代码和静态数据存储的内存位置。

动态数据(堆)。程序请求动态分配的内存来自内存池。

栈。局部数据对象、函数的参数以及调用函数和被调用函数的联系放在称之为栈的内存池中。

当使用基本数据类型声明变量时,C会在称为堆栈的内存区域中自动为变量分配空间。

例如,声明一个int变量通常会分配4个字节。我们可以通过使用sizeof运算符知道这一点:

int x;
printf("%d", sizeof(x)); /* output: 4 */

再举一个例子,给具有指定大小的数组分配连续的内存块,每个块的大小为一个元素的大小:

int arr[10];
printf("%d", sizeof(arr)); /* output: 40 */

只要你的程序显式声明基本数据类型或数组大小,就会自动管理内存。

但是,如果你希望实现一个程序,在该程序中直到运行时才确定数组的大小。

**动态内存分配是根据需要分配和释放内存的过程。**现在,你可以在运行时提示输入数组元素的数量,然后创建具有多元素的数组。

动态内存是使用指针管理的,这些指针指向在称为堆的区域中新分配的内存块。

除了使用堆栈进行自动内存管理和使用堆进行动态内存分配外,主内存中还存在静态管理的数据,这些数据在程序的生命期内一直存在。

内存管理函数

stdlib.h库包含内存管理函数。

程序顶部的语句#include <stdlib.h>

使你可以调用以下函数:

malloc(bytes) 返回一个指向连续内存块的指针,该内存块的大小为bytes。

calloc(num_items, item_size) 返回一个指向具有 num_items 项的连续内存块的指针,每个项的大小为item_size字节。

通常用于数组,结构和其他派生数据类型。分配的内存被初始化为0。

realloc(ptr, bytes) 将ptr指向的内存大小调整为字节大小。新分配的内存未初始化。

free(ptr) 释放ptr指向的内存块

当你不再需要分配的内存块时,请使用函数 free() 释放使该块内存可再次分配。

malloc函数

malloc() 函数在内存中分配指定数量的连续字节。

  • 分配的内存是连续的,可以视为数组。代替使用方括号[]来引用元素,而是使用指针算术遍历数组。
  • 建议使用+来引用数组元素。使用++或+=更改指针存储的地址。
  • 如果分配不成功,则返回NULL。因此,你应该包含用于检查NULL指针的代码。

一个简单的二维数组需要 (rows*columns)*sizeof(datatype) 字节的内存。

例如:

#include <stdlib.h>

int *ptr;
/* a block of 10 ints */
ptr = malloc(10 * sizeof(*ptr));

if (ptr != NULL) {
  *(ptr + 2) = 50;  /* 将 50 赋值给第3个int */
}

malloc 返回指向已分配内存的指针。

注意,将 sizeof 应用于 *ptr 而不是 int,如果稍后将 *ptr 声明更改为其他数据类型,则使代码更加健壮。

free函数

free() 函数是一个内存管理函数,被称为释放内存。通过释放内存,你可以在以后的程序中使用更多内存。

例如:

int* ptr = malloc(10 * sizeof(*ptr));
if (ptr != NULL)
  *(ptr + 2) = 50;  /* assign 50 to third int */
printf("%d\n", *(ptr + 2));

free(ptr);

calloc函数

calloc() 函数根据特定项(例如结构)的大小分配内存。

下面的程序使用calloc为结构分配内存,并使用malloc为结构中的字符串分配内存:

typedef struct {
  int num;
  char *info;
} record;

record *recs;
int num_recs = 2;
int k;
char str[ ] = "This is information";

recs = calloc(num_recs, sizeof(record));
if (recs != NULL) {
  for (k = 0; k < num_recs; k++) {
    (recs+k)->num = k;
    (recs+k)->info = malloc(sizeof(str));
    strcpy((recs+k)->info, str);
  }
} 

**calloc 在连续的内存块内为结构元素数组分配内存。**你可以使用指针算法从一种结构导航到另一种结构。

在为结构分配空间之后,必须为结构内的字符串分配内存。为信息成员使用指针允许存储任何长度的字符串。

动态分配的结构是链接列表和二叉树以及其他数据结构的基础。

realloc 函数

*realloc(void ptr, size_t size):更改已经配置的内存空间,即更改由malloc()函数分配的内存空间的大小。

如果将分配的内存减少,realloc仅仅是改变索引的信息。

如果是将分配的内存扩大,则有以下情况:

  1. 如果当前内存段后面有需要的内存空间,则直接扩展这段内存空间,realloc()将返回原指针。
  2. 如果当前内存段后面的空闲字节不够,那么就使用堆中的第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,并将原来的数据块释放掉,返回新的内存块位置。
  3. 如果申请失败,将返回NULL,此时,原来的指针仍然有效。

例如:

int *ptr;
ptr = malloc(10 * sizeof(*ptr));  
if (ptr != NULL) {
  *(ptr + 2) = 50;  /* assign 50 to third int */
}
ptr = realloc(ptr, 100 * sizeof(*ptr)); 
*(ptr + 30) = 75; 

realloc将原始内容保留在内存中,并扩展该块以允许更多存储。

#include <stdio.h>
#include <stdlib.h>
int main() {
	int* arr = malloc(sizeof(int));
	*arr = 13;
	arr = realloc(arr, 2 * sizeof(int));
	*(arr + 1) = *arr;
	printf("%d", *(arr + 1));
}

结果:

13

为字符串分配内存

在为字符串指针分配内存时,你可能需要使用字符串长度而不是sizeof运算符来计算字节。

例如:

char str20[20];
char *str = NULL;

strcpy(str20, "12345");
str = malloc(strlen(str20) + 1); 
strcpy(str, str20);
printf("%s", str); 

这种方法可以更好地管理内存,因为你分配的空间不会超出指针所需的空间。

当使用strlen确定字符串所需的字节数时,请确保为NULL字符'\0'添加一个额外的字节。

char 始终是一个字节,因此无需将内存需求乘以 sizeof(char)。

动态数组

许多算法实现了动态数组,因为这允许元素的数量根据需要增加。

由于不会一次分配所有元素,因此动态数组通常使用一种结构来跟踪当前数组的大小,当前容量以及指向元素的指针,如以下程序所示。

typedef struct {
  int *elements;
  int size;
  int cap;
} dyn_array;

dyn_array arr;

/* initialize array */
arr.size = 0;
arr.elements = calloc(1, sizeof(*arr.elements) );
arr.cap = 1;  /* room for 1 element */ 

要扩展更多元素:

arr.elements = realloc(arr.elements, (5 + arr.cap) * sizeof(*arr.elements));
if (arr.elements != NULL)
  arr.cap += 5; /* increase capacity */ 

向数组添加元素会增加其大小:

if (arr.size < arr.cap) {
  arr.elements[arr.size] = 50;
  arr.size++;
} else {
  printf("Need to expand the array.");
}

以上程序做了简化。

为了正确实现动态数组,应该将子任务分解为 init_array(),increment_array(),add_element() 和 display_array()之类的函数。

错误检查也被跳过,使演示简短。

访问文件

在C语言中,操作文件之前必须先打开文件;所谓“打开文件”,就是让程序和文件建立连接的过程。

打开文件之后,程序可以得到文件的相关信息,例如大小、类型、权限、创建者、更新时间等。在后续读写文件的过程中,程序还可以记录当前读写到了哪个位置,下次可以在此基础上继续操作。

标准输入文件 stdin(表示键盘)、标准输出文件 stdout(表示显示器)、标准错误文件 stderr(表示显示器)是由系统打开的,可直接使用。

使用 <stdio.h> 头文件中的 fopen() 函数即可打开文件,它的用法为:

FILE *fopen(char *filename, char *mode);

filename为文件名(包括文件路径),mode为打开方式,它们都是字符串。

fopen() 函数的返回值

fopen() 会获取文件信息,包括文件名、文件状态、当前读写位置等,并将这些信息保存到一个 FILE 类型的结构体变量中,然后将该变量的地址返回。

FILE 是 <stdio.h> 头文件中的一个结构体,它专门用来保存文件信息。我们不用关心 FILE 的具体结构,只需要知道它的用法就行。

如果希望接收 fopen() 的返回值,就需要定义一个 FILE 类型的指针。例如:

FILE *fp = fopen("demo.txt", "r");

表示以“只读”方式打开当前目录下的 demo.txt 文件,并使 fp 指向该文件,这样就可以通过 fp 来操作 demo.txt 了。fp 通常被称为文件指针。

再来看一个例子:

FILE *fp = fopen("D:\\demo.txt","rb+");

表示以二进制方式打开 D 盘下的 demo.txt 文件,允许读和写。

判断文件是否打开成功

**打开文件出错时,fopen() 将返回一个空指针,也就是 NULL,**我们可以利用这一点来判断文件是否打开成功,请看下面的代码:

FILE *fp;
if( (fp=fopen("D:\\demo.txt","rb") == NULL ){
    printf("Fail to open file!\n");
    exit(0);  //退出程序(结束程序)
}

我们通过判断 fopen() 的返回值是否和 NULL 相等来判断是否打开失败:如果 fopen() 的返回值为 NULL,那么 fp 的值也为 NULL,此时 if 的判断条件成立,表示文件打开失败。

以上代码是文件操作的规范写法,读者在打开文件时一定要判断文件是否打开成功,因为一旦打开失败,后续操作就都没法进行了,往往以“结束程序”告终。

fopen() 函数的打开方式

不同的操作需要不同的文件权限。例如,只想读取文件中的数据的话,“只读”权限就够了;既想读取又想写入数据的话,“读写”权限就是必须的了。另外,文件也有不同的类型,按照数据的存储方式可以分为二进制文件和文本文件,它们的操作细节是不同的。在调用 fopen() 函数时,这些信息都必须提供,称为“文件打开方式”。最基本的文件打开方式有以下几种:

img

例子:

#include <stdio.h>

int main() {  
  FILE *fptr;
  
  fptr = fopen("myfile.txt", "w");
  if (fptr == NULL) {
    printf("Error opening file.");
    return -1;
  }
  fclose(fptr);
  return 0;
} 

当使用字符串文字来指定文件名时,转义序列\表示单个反斜杠。

在此程序中,如果打开文件时出错,则将 -1 错误代码返回到系统。

在此程序中,如果打开文件时出错,则将-1错误代码返回到系统。错误处理将在以后介绍。

从文件读取

stdio.h库还包括用于从打开的文件读取的函数。

一个文件可以一次读取一个字符,也可以将整个字符串读入字符缓冲区,该缓冲区通常是用于临时存储的char数组。

  • getc(fp) 返回fp指向的文件中的下一个字符。如果已到达文件末尾,则返回EOF。

  • fgets(buff, n, fp) fgets函数功能为从指定的流中读取数据,每次读取一行。

其原型为:char *fgets(char *str, int n, FILE *stream);从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。

当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。

  • fscanf(fp, conversion_specifiers, vars) 从fp指向的文件中读取字符,并使用conversion_specifiers将输入分配给变量指针vars列表。与scanf一样,遇到空格或换行符时,fscanf会停止读取字符串。

例如:

#include <stdio.h>

int main() {  
  FILE *fptr;
  int c, stock;
  char buffer[200], item[10];
  float price;

  /* myfile.txt: Inventory\n100 Widget 0.29\nEnd of List */

  fptr = fopen("myfile.txt", "r");

  fgets(buffer, 20, fptr);    /* read a line */
  printf("%s\n", buffer);

  fscanf(fptr, "%d%s%f", &stock, item, &price); /* read data */
  printf("%d  %s  %4.2f\n", stock, item, price);

  while ((c = getc(fptr)) != EOF) /* read the rest of the file */
    printf("%c", c);

  fclose(fptr);
  return 0;
} 

gets() 函数读取直到换行符。 fscanf() 根据转换说明符读取数据。

然后 while 循环一次读取一个字符,直到文件结束。

写入文件

stdio.h库还包括用于写入文件的函数。写入文件时,必须显式添加换行符'\ n'。

  • fputc(char, fp) 将字符char写入fp指向的文件。

  • fputs(str, fp) 将字符串str写入fp指向的文件。

  • fprintf(fp, str, vars) 将字符串str打印到fp指向的文件。 str可以选择包括格式说明符和变量vars列表。

例如:

FILE *fptr;
char filename[50];
printf("Enter the filename of the file to create: ");
gets(filename);
fptr = fopen(filename, "w");

/* 写文件 */
fprintf(fptr, "Inventory\n");
fprintf(fptr, "%d %s %f\n", 100, "Widget", 0.29);
fputs("End of List", fptr); 

“ w”参数为fopen函数定义“写入模式”。

二进制文件I/O_1

当你具有数组或结构时,仅将字符和字符串写入文件可能会变得乏味。

要将整个内存块写入文件,有以下二进制函数:

img

fwrite(ptr, item_size, num_items, fp) 将num_items个item_size大小的项目从指针ptr写入文件指针fp所指向的文件。

fread(ptr, item_size, num_items, fp) 从文件指针fp所指向的文件中读取item_size大小的num_items项到ptr所指向的内存中。

fclose(fp) 关闭以文件fp打开的文件,如果关闭成功,则返回0。如果关闭错误,则返回EOF。

feof(fp) 当到达文件流的末尾时返回0。

二进制文件 I/O_2

该程序将整数数组写入文件,但是将结构数组写入文件同样容易。

以下程序演示了如何读写二进制文件:

FILE *fptr;
int arr[10];
int x[10];
int k;

/* 初始化数组的值 */
for (k = 0; k < 10; k++)
  arr[k] = k;

/* 将数组数据写入文件 */
fptr = fopen("datafile.bin", "wb");
fwrite(arr, sizeof(arr[0]), sizeof(arr)/sizeof(arr[0]), fptr);
fclose(fptr);

/* 从文件读取数组数据 */
fptr = fopen("datafile.bin", "rb");
fread(x, sizeof(arr[0]), sizeof(arr)/sizeof(arr[0]), fptr);
fclose(fptr);

/* 打印数组 */
for (k = 0; k < 10; k++)
  printf("%d", x[k]); 

该程序将整数数组写入文件,将结构数组写入文件同样容易。

注意,项目大小和项目数是通过使用元素的大小和整个变量的大小来确定的。

单独的文件扩展名并不能确定文件中数据的格式,但是它们对于指示期望的数据类型很有用。

例如,.txt扩展名表示文本文件,.bin表示二进制数据,.csv表示逗号分隔的值,.dat表示数据文件。

控制文件指针

stdio.h中有一些函数可以控制二进制文件中文件指针的位置:

ftell(fp) 返回一个long int值,该值对应于fp文件指针位置(从文件开头开始的字节数)。

fseek(fp, num_bytes, from_pos) 将 fp 文件指针的位置相对于from_pos位置移动num_bytes个字节,

该位置可以是以下常量之一:

-SEEK_SET 文件的开始

-SEEK_CUR 当前位置

-SEEK_END 文件结尾

以下程序从结构文件中读取一条记录:

typedef struct {
  int id;
  char name[20];
} item;

int main() { 
  FILE *fptr;
  item first, second, secondf;

  /* 创建记录 */
  first.id = 10276;
  strcpy(first.name, "Widget");
  second.id = 11786;
  strcpy(second.name, "Gadget");
  
  /* 将记录写入文件 */
  fptr = fopen("info.dat", "wb");
  fwrite(&first, 1, sizeof(first), fptr);
  fwrite(&second, 1, sizeof(second), fptr);
  fclose(fptr); 

  /* 文件包含2条类型的记录 */
  fptr = fopen("info.dat", "rb");

  /* 查找第二纪录 */
  fseek(fptr, 1*sizeof(item), SEEK_SET);
  fread(&secondf, 1, sizeof(item), fptr);
  printf("%d  %s\n", secondf.id, secondf.name);
  fclose(fptr);
  return 0;
} 

该程序将两个项目记录写入文件。为了只读取第二条记录,fseek() 将文件指针从文件开头移到 1 * sizeof(item) 个字节。

例如,如果你想将指针移动到第四条记录,则从文件开头(SEEK_SET)查找 3 * sizeof(item)。

异常处理

良好编程习惯的核心是使用错误处理技术。如果你忘记异常处理,即使是最扎实的编码技巧也可能无法阻止程序崩溃。

任何导致你的程序停止正常执行的情况都是异常。

异常处理(也称为错误处理)是一种处理运行时错误的方法。

C不明确支持异常处理,但是有一些方法可以管理错误:

-首先编写代码以防止错误。你无法控制用户输入,但是可以检查以确保用户输入了有效输入。

执行除法时,请采取额外的步骤以确保不会发生被0除的情况。

-使用exit语句正常结束程序执行。你可能无法控制文件是否可读取,但是不必让问题使程序崩溃。

使用 errno,perror() 和 strerror() 通过错误代码识别错误。

退出命令

exit命令立即停止程序的执行,并将退出代码发送回调用过程。

例如,如果一个程序被另一个程序调用,则调用程序可能需要知道退出状态。

使用exit避免程序崩溃是一个好习惯,因为它会关闭所有打开的文件连接和进程。

你可以通过exit语句返回任何值,但是0表示成功,-1表示失败。

预定义的 stdlib.h 宏 EXIT_SUCCESS 和 EXIT_FAILURE 也很常用。

例如:

int x = 10;
int y = 0;

if (y != 0)
  printf("x / y = %d", x/y);
else {
  printf("Divisor is 0. Program exiting.");
  exit(EXIT_FAILURE);
} 

使用errno

一些库函数(例如 fopen())在未按预期执行时会设置错误代码。

错误代码在名为errno的全局变量中设置,该变量在errno.h头文件中定义。

使用errno时,应在调用库函数之前将其设置为0。

要输出存储在errno中的错误代码,可以使用fprintf打印到strerr文件流,将标准错误输出到屏幕。

使用strerr是一个惯例问题,也是一种好的编程习惯。

你可以通过其他方式输出errno,但如果仅将strerr用于错误消息,则可以更轻松地跟踪异常处理。

例如:

#include <stdio.h>
#include <stdlib.h>
// #include <errno.h>

extern int errno;

int main() {
  FILE *fptr;
  int c;

  errno = 0;
  fptr = fopen("c:\\nonexistantfile.txt", "r");
  if (fptr == NULL) {
    fprintf(stderr, "Error opening file. Error code: %d\n", errno);
    exit(EXIT_FAILURE);
  }

  fclose(fptr);
  return 0;
} 

预处理指令

预定义的宏定义

除了定义自己的宏之外,还有一些标准的预定义宏在C程序中始终可用,而无需使用#define指令:

_DATE_ 当前日期,以字符串形式,格式为 Mm dd yyyy

_TIME_ 当前时间,以字符串形式,格式为 hh:mm:ss

_FILE_ 当前文件名作为字符串

_LINE_ 当前行号 为int值

_STDC_ 1

例如:

char curr_time[10];
char curr_date[12];
int std_c;

strcpy(curr_time, __TIME__);
strcpy(curr_date, __DATE__);
printf("%s %s\n", curr_time, curr_date);
printf("This is line %d\n", __LINE__);    
std_c = __STDC__;
printf("STDC is %d", std_c);