跳至主要內容
C 语言

C 语言

丈墨...大约 16 分钟c/cppC语言

C 语言

C 语言学习时的笔记。

注意

由于在此文章之前,已经再学校的课堂上学习过 C 语言的相关知识了,所以在创作此文章时会比较的简略。同样是在菜鸟教程open in new window上进行的学习,以查漏补缺、复习知识和罗列重点知识为主,而重点会放在 cpp 的学习上,详见下一篇文章。

C 的令牌

C 程序由各种令牌(Token)组成,令牌可以是关键字、标识符、常量、字符串值,或者是一个符号。如,下面的 C 语句包括五个令牌:

printf("Hello, World! \n");

这五个令牌分别是:

printf
(
"Hello, World! \n"
)
;

数据类型

  • 枚举类型:它们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值的变量。
  • 派生类型:包括数组类型、指针类型和结构体类型。

区别

声明变量和定义变量的区别,前者不一定占据存储空间,可以使用 extern 关键字来声明,表示变量可能在其他文件中被定义;后者占据存储空间。

左值和右值

左值(lvalue):指向内存位置的表达式被称为左值(lvalue)表达式。左值可以出现在赋值号的左边或右边。
右值(rvalue):术语右值(rvalue)指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边(将亡对象)。

常量

前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。
整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。后缀可以是大写,也可以是小写,U 和 L 的顺序任意。

注意

C 语言中是不能定义二进制数变量的。

字符常量使用单引号,字符串常量使用双引号。

定义常量:

  • 使用 #define 预处理器: #define 可以在程序中定义一个常量,它在编译时会被替换为其对应的值。
  • 使用 const 关键字:const 关键字用于声明一个只读变量,即该变量的值不能在程序运行时修改。
    PS:通常将常量定义为大写字母。

#define 预处理指令和 const 关键字在定义常量时有一些区别:

  • 替换机制:#define 是进行简单的文本替换,而 const 是声明一个具有类型的常量。#define 定义的常量在编译时会被直接替换为其对应的值,而 const 定义的常量在程序运行时会分配内存,并且具有类型信息。

  • 类型检查:#define 不进行类型检查,因为它只是进行简单的文本替换。而 const 定义的常量具有类型信息,编译器可以对其进行类型检查。这可以帮助捕获一些潜在的类型错误。

  • 作用域:#define 定义的常量没有作用域限制,它在定义之后的整个代码中都有效。而 const 定义的常量具有块级作用域,只在其定义所在的作用域内有效。

  • 调试和符号表:使用 #define 定义的常量在符号表中不会有相应的条目,因为它只是进行文本替换。而使用 const 定义的常量会在符号表中有相应的条目,有助于调试和可读性。

存储类

  • auto 存储类是所有局部变量默认的存储类。定义在函数中的变量默认为 auto 存储类,这意味着它们在函数开始时被创建,在函数结束时被销毁。
  • register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个字),且不能对它应用一元的 '&' 运算符(因为它没有内存位置)
  • static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。
  • extern 存储类用于定义在其他文件中声明的全局变量或函数。当使用 extern 关键字时,不会为变量分配任何存储空间,而只是指示编译器该变量在其他文件中定义。extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。

运算符

位运算符:&(与)、|(或)、^(异或)、~(取反,得到的是目标值的补码)、<<(左移,补 0)、>>(右移,正数补 0,负数补 1)

函数

两种向函数传递参数的方式:

  • 传值调用:该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。
  • 引用调用:通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。

作用域规则

在程序中,局部变量和全局变量的名称可以相同,但是在函数内,如果两个名字相同,会使用局部变量值,全局变量不会被使用。
形式参数:函数的参数,被当作该函数内的局部变量,如果与全局变量同名形式参数会优先使用

全局变量与局部变量在内存中的区别:

  • 全局变量保存在内存的全局存储区中,占用静态的存储单元;
  • 局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。

PS:注意形参和实参的区别。

枚举

枚举(enum)类型通常用于为程序中的一组相关的常量取名字,以便于程序的可读性和维护性。
第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。

函数指针

函数指针是指向函数的指针变量。

声明于实例
// 声明:
typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型
// 实例:
#include <stdio.h>

int max(int x, int y)
{
    return x > y ? x : y;
}

int main(void)
{
    /* p 是函数指针 */
    int (* p)(int, int) = & max; // &可以省略
    int a, b, c, d;

    printf("请输入三个数字:");
    scanf("%d %d %d", & a, & b, & c);

    /* 与直接调用函数等价,d = max(max(a, b), c) */
    d = p(p(a, b), c);

    printf("最大的数字是: %d\n", d);

    return 0;
}

回调函数

函数指针作为某个函数的参数。简单讲:回调函数是由别人的函数执行时调用你实现的函数。

字符串

当字符串数组中有'\0'时,'\0'也要算在长度里面。

结构体

如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明,如下所示:

代码
struct B;    //对结构体B进行不完整声明

//结构体A中包含指向结构体B的指针
struct A
{
    struct B *partner;
    //other members;
};

//结构体B中包含指向结构体A的指针,在A声明完后,B也随之进行声明
struct B
{
    struct A *partner;
    //other members;
};

为了访问结构的成员,使用成员访问运算符(.)。

结构作为参数时,传参方式与其他类型的变量或指针类似。
为了使用指向结构的指针访问结构的成员,必须使用 -> 运算符

对于结构体,sizeof 将返回结构体的总字节数,包括所有成员变量的大小以及可能的填充字节(即最大可能的字节数)。

注意

结构体的大小可能会受到编译器的优化和对齐规则的影响,编译器可能会在结构体中插入一些额外的填充字节以对齐结构体的成员变量,以提高内存访问效率。因此,结构体的实际大小可能会大于成员变量大小的总和,如果你需要确切地了解结构体的内存布局和对齐方式,可以使用 offsetof 宏和 attribute((packed)) 属性等进一步控制和查询结构体的大小和对齐方式。

联合

一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。
为了定义共用体,使用 union 语句,方式与定义结构类似。

位域

位域(bit-field)是一种特殊的结构体成员,允许我们按位对成员进行定义,指定其占用的位数。、
所谓"位域"是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

位域的特点和使用方法如下:

  • 定义位域时,可以指定成员的位域宽度,即成员所占用的位数。
  • 位域的宽度不能超过其数据类型的大小,因为位域必须适应所使用的整数类型。
  • 位域的数据类型可以是 int、unsigned int、signed int 等整数类型,也可以是枚举类型。
  • 位域可以单独使用,也可以与其他成员一起组成结构体。
  • 位域的访问是通过点运算符(.)来实现的,与普通的结构体成员访问方式相同。

声明方式:

struct 位域结构名
{
type [member_name] : width ;
...
} [结构体变量名];

// 注意:type只能为 int(整型),unsigned int(无符号整型),signed int(有符号整型) 三种类型。

几点说明:

  • 一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
    struct bs{
      unsigned a:4;
      unsigned  :4;    /* 空域 */
      unsigned b:4;    /* 从下一单元开始存放 */
      unsigned c:4
    }
    
    其中第二个是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。
  • 位域的宽度不能超过它所依附的数据类型的长度,成员变量都是有类型的,这个类型限制了成员变量的最大长度,: 后面的数字不能超过这个长度,成员变量的大小也不能超过:后面的数字。

文件读写

打开文件:

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

关闭文件:

 int fclose( FILE *fp );
//  成功返回0,出错返回EOF。

写入文件:

int fputc( int c, FILE *fp );
// 成功返回写入的字符,出错返回EOF。
int fputs( const char *s, FILE *fp );
//  成功返回一个非负值,失败返回EOF。

// 似乎有fprintf和fputs的方法来写入文件

读取文件:

int fgetc( FILE * fp );
// 读取一个字符。返回值是读取的字符,如果发生错误则返回 EOF。
char *fgets( char *buf, int n, FILE *fp );
// 读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串。如果这个函数在读取最后一个字符之前就遇到一个换行符 '\n' 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。
int fscanf(FILE *fp, const char *format, 与format对应类型的变量)
// 函数来从文件中读取字符串,但是在遇到第一个空格和换行符时,它会停止读取。

二进制输入输出:

size_t fread(void *ptr, size_t size_of_elements,
             size_t number_of_elements, FILE *a_file);

size_t fwrite(const void *ptr, size_t size_of_elements,
             size_t number_of_elements, FILE *a_file);

预处理器

C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。把 C 预处理器(C Preprocessor)简写为 CPP。

知识点open in new window:预处理器指令、预定于宏、预处理器运算符、参数化的宏

头文件

头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。

系统头文件: #include <file>
用户头文件: #include "file"

只引用一次头文件:如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。解决方法是使用包装器 #ifndef。

#ifndef HEADER_FILE
#define HEADER_FILE

the entire header file file

#endif

有条件引用:
有时需要从多个不同的头文件中选择一个引用到程序中。如果头文件比较多的时候,为了稳妥,预处理器使用宏来定义头文件的名称。

#define SYSTEM_H "system_1.h"
#if SYSTEM_1
   # include SYSTEM_H
#elif SYSTEM_2
   # include "system_2.h"
#elif SYSTEM_3
   ...
#endif

不是用头文件的名称作为 #include 的直接参数,只需要使用宏名称代替。SYSTEM_H 会扩展,预处理器会查找 system_1.h,就像 不使用宏名称代替一样。SYSTEM_H 可通过 -D 选项被您的 Makefile 定义。

错误处理

C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。

C 语言提供了 perror() 和 strerror() 函数来显示与 errno 相关的文本消息。

  • perror() 函数显示传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。
  • strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。

可变参数

有时,您可能会碰到这样的情况,您希望函数带有可变数量的参数,而不是预定义数量的参数。C 语言为这种情况提供了一个解决方案,它允许您定义一个函数,能根据具体的需求接受可变数量的参数。

声明:int func_name(int arg1, ...);
int 代表了要传递的可变参数的总数,省略号表示可变参数列表,需要使用 stdarg.h 头文件,该文件提供了实现可变参数功能的函数和宏。

具体步骤:

  • 定义一个函数,最后一个参数为省略号,省略号前面可以设置自定义参数。
  • 在函数定义中创建一个 va_list 类型变量,该类型是在 stdarg.h 头文件中定义的。
  • 使用 int 参数和 va_start() 宏来初始化 va_list 变量为一个参数列表。宏 va_start() 是在 stdarg.h 头文件中定义的。
  • 使用 va_arg() 宏和 va_list 变量来访问参数列表中的每个项。
  • 使用宏 va_end() 来清理赋予 va_list 变量的内存。

常用的宏有:

  • va_start(ap, last_arg):初始化可变参数列表。ap 是一个 va_list 类型的变量,last_arg 是最后一个固定参数的名称(也就是可变参数列表之前的参数)。该宏将 ap 指向可变参数列表中的第一个参数。
  • va_arg(ap, type):获取可变参数列表中的下一个参数。ap 是一个 va_list 类型的变量,type 是下一个参数的类型。该宏返回类型为 type 的值,并将 ap 指向下一个参数。
  • va_end(ap):结束可变参数列表的访问。ap 是一个 va_list 类型的变量。该宏将 ap 置为 NULL。

内存管理

在 <stdlib.h> 头文件中,C 语言为内存的分配和管理提供了几个函数:

  • void *calloc(int num, int size);
  • void free(void *address);
  • void *malloc(int num);
  • void *realloc(void *address, int newsize);

动态内存分配完成后,在程序推出之前要释放(free)分配的内存。

命令行参数

命令行参数是使用 main() 函数参数来处理的,其中,argc 是指传入参数的个数,argv[] 是一个指针数组,指向传递给程序的每个参数。应当指出的是,argv[0] 存储程序的名称,argv[1] 是一个指向第一个命令行参数的指针,*argv[n] 是最后一个参数。如果没有提供任何参数,argc 将为 1,否则,如果传递了一个参数,argc 将被设置为 2。
多个命令行参数之间用空格分隔,但是如果参数本身带有空格,那么传递参数的时候应把参数放置在双引号 "" 或单引号 '' 内部。

排序算法

冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序(具体描述和实现代码并未给出)

结束

虽然在开头说要简单的复习一下,笔记估计不会太多,但是搞完下来,感觉还是不少 😂。也好,也是对 C 语言的一次复习了,接下来就是进军 cpp。

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.5