C语言快速入门笔记

C语言

最近写C++程序的时候温习了一下C语言的设计,想起来了这一份笔记,已经对着C++进行了一些更正,希望能帮助到大家,个人感觉看完这些对C语言有一个大体的概念是没有问题的,参考的图里面有很多都是知乎和菜鸟编程的内容,其中还有部分是我学习计算机系统基础课程的笔记(这一部分更深了),但是出于一个难度的考虑,我就没有太过深入,但是改写的都写了,如果有什么问题欢迎大家在评论区中指正。大学课程中不常考的函数指针这些东西都没有提及,可能以后会单独出一点小的文章来解释一下吧(尤其是有关内存的地方)

背景知识

1972 年,为了移植与开发 UNIX 操作系统,丹尼斯·里奇在贝尔电话实验室设计开发了 C 语言。在 1978 年,布莱恩·柯林汉(Brian Kernighan)和丹尼斯·里奇(Dennis Ritchie)制作了 C 的第一个公开可用的描述,现在被称为 K&R 标准。UNIX 操作系统,C编译器,和几乎所有的 UNIX 应用程序都是用 C 语言编写的。由于各种原因,C 语言现在已经成为一种广泛使用的专业语言。

C 语言是以 B 语言为基础的,B 语言大概是在 1970 年被引进的。C 语言标准是于 1988 年由美国国家标准协会(ANSI,全称 American National Standard Institute)制定的。

目前,C 语言是最广泛使用的系统程序设计语言。大多数先进的软件都是使用 C 语言实现的。当今最流行的 Linux 操作系统和 RDBMS(Relational Database Management System:关系数据库管理系统) MySQL 都是使用 C 语言编写的。

C 语言最初是用于系统开发工作,特别是组成操作系统的程序。由于 C 语言所产生的代码运行速度与汇编语言编写的代码运行速度几乎一样,所以采用 C 语言作为系统开发语言。

标准版本年代主要改进
C89/C901989/90第一个标准化版本,只支持基本语法
C991999单行注释、变长数组、定长整型、inline
C112011多线程、原子操作、Unicode支持、对齐
C172017无新特性,修复 C11 bug
C232023更现代语法,UTF-8 友好,nullptr,增强兼容性

C89/C90

  • 函数声明必须写在代码最前面(变量定义也一样)
  • 只支持基本的数据类型(int, float, char
  • 不支持 // 单行注释(只能用 /* ... */
  • 不支持变量在中间定义

C99

  • ✅ 支持 // 单行注释
  • ✅ 变量可以在代码块中任意位置定义
  • ✅ 新数据类型:long long int_Bool_Complex
  • ✅ 支持 变长数组(VLA)
  • ✅ 增加了 inline 内联函数
  • ✅ 添加了头文件 <stdint.h>(提供如 int32_t 等定长整型)

C11

  • ✅ 引入 多线程支持(头文件 <threads.h>
  • ✅ 增加 原子操作 支持(如 atomic_int
  • ✅ 添加 _Static_assert 编译期断言
  • ✅ 增加 alignasalignof 支持内存对齐
  • ✅ 更好的 Unicode 字符支持(char16_t, char32_t
  • ✅ 增加可选的“安全函数”(如 strncpy_s

C17/C18

  • 📌 修复 bug
  • 📌 对 C11 的小幅修订
  • 📌 为 C 的未来标准做准备

C23

  • ✅ 新关键字:typeofnullptr(类似 C++ 的)
  • ✅ 改进的格式化输出函数(如 printf 对 UTF-8 更友好)
  • ✅ 改进宏定义和预处理指令
  • ✅ 更丰富的标准库工具函数
  • ✅ 更好地与 C++ 接轨(兼容性增强)

C 程序主要包括以下部分:

  • 预处理器指令
  • 函数
  • 变量
  • 语句 & 表达式
  • 注释
#include <stdio.h>
int main(){
	 /* 我的第一个 C 程序 */
   printf("Hello, World! \n");
   return 0;
}
  1. 程序的第一行 #include <stdio.h> 是预处理器指令,告诉 C 编译器在实际编译之前要包含 stdio.h 文件。
  2. 下一行 int main() 是主函数,程序从这里开始执行。
  3. 下一行 // 将会被编译器忽略,这里放置程序的注释内容。它们被称为程序的注释。
  4. 下一行 printf(…) 是 C 中另一个可用的函数,会在屏幕上显示消息 “Hello, World!”。
  5. 下一行 return 0; 终止 main() 函数,并返回值 0。

C程序的编译和运行

如果您使用的是 Linux 或 UNIX,请在命令行使用下面的命令来检查您的系统上是否安装了 GCC:

$ gcc -v

如果未安装 GCC,那么请按照 http://gcc.gnu.org/install/ 上的详细说明安装 GCC。

为了在 Windows 上安装 GCC,您需要安装 MinGW-w64。

MinGW-w64 是一个开源项目,它为 Windows 系统提供了一个完整的 GCC 工具链,支持编译生成 32 位和 64 位的 Windows 应用程序。

访问 MinGW-w64 的主页 mingw-w64.org,进入 MinGW 下载页面 https://www.mingw-w64.org/downloads/,下载最新版本的 MinGW-w64 安装程序。

MinGW-w64 的下载详情页面包含了很多 MinGW-w64 及特定工具的整合包:

我们只安装 MinGW-w64 ,所以只需下载 MinGW-w64 即可,点击红框中的"SourceForge"超链接,就会进入 SourceForge 中的 MinGW-w64 下载页面。

在 SourceForge 上下载 MinGW-w64 :https://sourceforge.net/projects/mingw-w64/files/

Github 上也有编译好的二进制包:https://github.com/niXman/mingw-builds-binaries/releases

当安装 MinGW 时,您至少要安装 gcc-core、gcc-g++、binutils 和 MinGW runtime,但是一般情况下都会安装更多其他的项。

添加您安装的 MinGW 的 bin 子目录到您的 PATH 环境变量中,这样您就可以在命令行中通过简单的名称来指定这些工具。

当完成安装时,您可以从 Windows 命令行上运行 gcc、g++、ar、ranlib、dlltool 和其他一些 GNU 工具。


数据类型

变量

序号类型与描述
1基本数据类型它们是算术类型,包括整型(int)、字符型(char)、浮点型(float)和双精度浮点型(double)。
2**枚举类型:**它们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值的变量。
3**void 类型:**类型说明符 void 表示没有值的数据类型,通常用于函数返回值。
4**派生类型:**包括数组类型、指针类型和结构体类型。

整数类型,其中int的大小是和系统位数有关,由于现在基本都是64位机,所有int默认都是4字节的

类型存储大小值范围
char1 字节-128 到 127 或 0 到 255
unsigned char1 字节0 到 255
signed char1 字节-128 到 127
int2 或 4 字节-32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647
unsigned int2 或 4 字节0 到 65,535 或 0 到 4,294,967,295
short2 字节-32,768 到 32,767
unsigned short2 字节0 到 65,535
long4 字节-2,147,483,648 到 2,147,483,647
unsigned long4 字节0 到 4,294,967,295

所以为什么在例如单片机的领域我们都是使用相关的size_t等类型而不是直接用int等,就是为了具有平台可移植性

浮点类型,因为是统一标准的所以不存在这类问题

类型存储大小值范围精度
float4 字节1.2E-38 到 3.4E+386 位有效位
double8 字节2.3E-308 到 1.7E+30815 位有效位
long double16 字节3.4E-4932 到 1.1E+493219 位有效位

在这里插入图片描述

void 类型指定没有可用的值。它通常用于以下三种情况下:

序号类型与描述
1函数返回为空C 中有各种函数都不返回值,或者您可以说它们返回空。不返回值的函数的返回类型为空。例如 void exit (int status);
2函数参数为空C 中有各种函数不接受任何参数。不带参数的函数可以接受一个 void。例如 int rand(void);
3指针指向 void类型为 void * 的指针代表对象的地址,而不是类型。例如,内存分配函数 *void malloc( size_t size ); 返回指向 void 的指针,可以转换为任何数据类型。

这个void类型是C语言实现像其他语言那样的范式等特色的关键!


变量定义就是告诉编译器在何处创建变量的存储,以及如何创建变量的存储。变量定义指定一个数据类型,并包含了该类型的一个或多个变量的列表,如下所示:

type variable_list;

type 表示变量的数据类型,可以是整型、浮点型、字符型、指针等,也可以是用户自定义的对象。

variable_list 可以由一个或多个变量的名称组成,多个变量之间用逗号,分隔,变量由字母、数字和下划线组成,且以字母或下划线开头(命名规则和Java、Python等一样的)。


变量的初始化可以在定义时进行,也可以在后续的代码中进行。

初始化器由一个等号,后跟一个常量表达式组成,如下所示:

type variable_name = value;
//和下面的方式是等价的
type variable_name;    // 变量定义
variable_name = new_value;    // 变量初始化

其中,type 表示变量的数据类型,variable_name 是变量的名称,value 是变量的初始值。


变量在使用之前应该被初始化。未初始化的变量的值是未定义的,可能包含任意的垃圾值。因此,为了避免不确定的行为和错误,建议在使用变量之前进行初始化。

在 C 语言中,如果变量没有显式初始化,那么它的默认值将取决于该变量的类型和其所在的作用域。

对于全局变量和静态变量(在函数内部定义的静态变量和在函数外部定义的全局变量),它们的默认初始值为零。

以下是不同类型的变量在没有显式初始化时的默认值:

  • 整型变量(int、short、long等):默认值为0。
  • 浮点型变量(float、double等):默认值为0.0。
  • 字符型变量(char):默认值为’\0’,即空字符。
  • 指针变量:默认值为NULL,表示指针不指向任何有效的内存地址。
  • 数组、结构体、联合等复合类型的变量:它们的元素或成员将按照相应的规则进行默认初始化,这可能包括对元素递归应用默认规则。

需要注意的是,局部变量(在函数内部定义的非静态变量)不会自动初始化为默认值,它们的初始值是未定义的(包含垃圾值)。因此,在使用局部变量之前,应该显式地为其赋予一个初始值。

总结起来,C 语言中变量的默认值取决于其类型和作用域。全局变量和静态变量的默认值为 0,字符型变量的默认值为 \0,指针变量的默认值为 NULL,而局部变量没有默认值,其初始值是未定义的,往往都是垃圾值会影响得到的结果的。

变量的声明有两种情况:

  • 1、一种是需要建立存储空间的。例如:int a 在声明的时候就已经建立了存储空间。
  • 2、另一种是不需要建立存储空间的,通过使用extern关键字声明变量名而不定义它。 例如:extern int a 其中变量 a 可以在别的文件中定义的。

除非有 extern 关键字,否则都是变量的定义。

extern int i; //声明,不是定义
int i; //声明,也是定义

在 C 语言中,使用 extern 关键字声明的变量是外部变量,表示该变量在其他文件中定义。

extern 关键字在 C 语言中用于声明外部变量或函数,使得它们可以在多个文件中共享和使用。

extern 主要用于实现模块化编程和代码的分离。

extern 变量的声明和定义通常放在不同的文件中。

为了使用 extern 变量和函数,必须将所有相关文件一起编译。


常量

常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量。常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量。常量就像是常规的变量,只不过常量的值在定义后不能进行修改。常量可以直接在代码中使用,也可以通过定义常量来使用。

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

浮点常量由整数部分、小数点、小数部分和指数部分组成。您可以使用小数形式或者指数形式来表示浮点常量。当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。当使用指数形式表示时, 必须包含小数点、指数,或同时包含两者。带符号的指数是用 e 或 E 引入的。

3.14159       /* 合法的 */
314159E-5L    /* 合法的 */
//别忘了这里的E表示的是10的-5次方而不是自然数e
510E          /* 非法的:不完整的指数 */
210f          /* 非法的:没有小数或指数 */
.e55          /* 非法的:缺少整数或分数 */

**字符常量是括在单引号中,例如,‘x’ 可以存储在 char 类型的简单变量中。**这个很关键是很容易错的,尤其是这个单引号。字符常量可以是一个普通的字符(例如 ‘x’)、一个转义序列(例如 ‘\t’),或一个通用的字符(例如 ‘\u02C0’)。

转义序列含义
\\ 字符
’ 字符
"" 字符
?? 字符
\a警报铃声
\b退格键
\f换页符
\n换行符
\r回车
\t水平制表符
\v垂直制表符

字符串字面值或常量是括在双引号 " " 中的。一个字符串包含类似于字符常量的字符:普通的字符、转义序列和通用的字符。您可以使用空格做分隔符,把一个很长的字符串常量进行分行。

"hello, dear"

"hello, \

dear"

"hello, " "d" "ear"
//这三个是等价的,第二个因为\是不识别的不会被打印出来(转移字符构成不完整),所以可以直接的连接到一起,最后一个则是字符串具有自动合并的功能

字符串常量在内存中以 null 终止符 \0 结尾。例如:

char myString[] = "Hello, world!"; //系统对字符串常量自动加一个 '\0'

请注意,把常量定义为大写字母形式,是一个很好的编程习惯。

在 C 中,有两种简单的定义常量的方式:

  1. 使用 #define 预处理器: #define 可以在程序中定义一个常量,它在编译时会被替换为其对应的值。

    下面是使用 #define 预处理器定义常量的形式:

    #define 常量名 常量值
    

    下面的代码定义了一个名为 PI 的常量:

    #define PI 3.14159
    

    在程序中使用该常量时,编译器会将所有的 PI 替换为 3.14159。

  2. 使用 const 关键字:const 关键字用于声明一个只读变量,即该变量的值不能在程序运行时修改。

    您可以使用 const 前缀声明指定类型的常量,如下所示:

    const 数据类型 常量名 = 常量值;
    

    下面的代码定义了一个名为MAX_VALUE的常量:

    const int MAX_VALUE = 100;
    

    在这里插入图片描述

    这个需要进行解释:

    1. 第一个由于没有值,所以不是常量不合理
    2. 第二个由于声明了var是一个const不可改变的但是在后面的语句中var=5尝试进行修改,所以也是错误的
    3. 仅仅只有最后一种是对的

    在这里插入图片描述

#define 与 const 这两种方式都可以用来定义常量,选择哪种方式取决于具体的需求和编程习惯。通常情况下,建议使用 const 关键字来定义常量,因为它具有类型检查和作用域的优势,而 #define 仅进行简单的文本替换,可能会导致一些意外的问题。

#define 不进行类型检查,因为它只是进行简单的文本替换。而 const 定义的常量具有类型信息,编译器可以对其进行类型检查。这可以帮助捕获一些潜在的类型错误。**所以我们有的坏人就是用这种方式来声明一些不合法但是可以成功的需求。#define 定义的常量没有作用域限制,它在定义之后的整个代码中都有效。(这个可能导致工程文件建立出现一系列的问题)**而 const 定义的常量具有块级作用域,只在其定义所在的作用域内有效。

储存类

存储类定义 C 程序中变量/函数的存储位置、生命周期和作用域。这些说明符放置在它们所修饰的类型之前。

auto,register,static,extern

  • auto

    auto 存储类是所有局部变量默认的存储类。

    定义在函数中的变量默认为 auto 存储类,这意味着它们在函数开始时被创建,在函数结束时被销毁。

    {
       int mount;
       auto int month;
    }
    

    上面的实例定义了两个带有相同存储类的变量,auto 只能用在函数内,即 auto 只能修饰局部变量。

  • register

    register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个字),且不能对它应用一元的 ‘&’ 运算符(因为它没有内存位置)。

    register 存储类定义存储在寄存器,所以变量的访问速度更快,但是它不能直接取地址,因为它不是存储在 RAM 中的。需要频繁访问的变量上使用 register 存储类可以提高程序的运行速度。

    {
       register int  miles;
    }
    

    寄存器只用于需要快速访问的变量,比如计数器。还应注意的是,定义 ‘register’ 并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制

  • static

    static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。全局声明的一个 static 变量或方法可以被任何函数或方法调用,只要这些方法出现在跟 static 变量或方法同一个文件中。

    静态变量在程序中只被初始化一次,即使函数被调用多次,该变量的值也不会重置。

    #include <stdio.h>
     
    /* 函数声明 */
    void func1(void);
     
    static int count=10;        /* 全局变量 - static 是默认的 */
     
    int main()
    {
      while (count--) {
          func1();
      }
      return 0;
    }
     
    void func1(void)
    {
    /* 'thingy' 是 'func1' 的局部变量 - 只初始化一次
     * 每次调用函数 'func1' 'thingy' 值不会被重置。
     */                
      static int thingy=5;
      thingy++;
      printf(" thingy 为 %d , count 为 %d\n", thingy, count);
    }
    
     thingy 为 6 , count 为 9
     thingy 为 7 , count 为 8
     thingy 为 8 , count 为 7
     thingy 为 9 , count 为 6
     thingy 为 10 , count 为 5
     thingy 为 11 , count 为 4
     thingy 为 12 , count 为 3
     thingy 为 13 , count 为 2
     thingy 为 14 , count 为 1
     thingy 为 15 , count 为 0
    

    我们可以发现每次调用都会修改thingy和count的值,全局都会共用的,所以一般在记录某个函数被调用多少次之类的应用中会使用这个

  • extern

    extern 存储类用于定义在其他文件中声明的全局变量或函数。当使用 extern 关键字时,不会为变量分配任何存储空间,而只是指示编译器该变量在其他文件中定义。extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当您使用 extern 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。当您有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 extern 来得到已定义的变量或函数的引用。可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。

    extern 修饰符通常用于当有两个或多个文件共享相同的全局变量或函数的时候

作用域

任何一种编程中,作用域是程序中定义的变量所存在的区域,超过该区域变量就不能被访问。

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

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

当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动对其初始化,如下所示:

数据类型初始化默认值
int0
char‘\0’
float0
double0
pointerNULL

正确地初始化变量是一个良好的编程习惯,否则有时候程序可能会产生意想不到的结果,因为未初始化的变量会导致一些在内存位置中已经可用的垃圾值。

C 语言中有三个地方可以声明变量:

  1. 在函数或块内部的局部变量

    1. 在某个函数或块的内部声明的变量称为局部变量。它们只能被该函数或该代码块内部的语句使用。局部变量在函数外部是不可知的。下面是使用局部变量的实例。在这里,所有的变量 a、b 和 c 是 main() 函数的局部变量。
  2. 在所有函数外部的全局变量

    1. 全局变量是定义在函数外部,通常是在程序的顶部。全局变量在整个程序生命周期内都是有效的,在任意的函数内部能访问全局变量。全局变量可以被任何函数访问。也就是说,全局变量在声明后整个程序中都是可用的。
    #include <stdio.h>
     
    /* 全局变量声明 */
    int g;
     
    int main ()
    {
      /* 局部变量声明 */
      int a, b;
     
      /* 实际初始化 */
      a = 10;
      b = 20;
      g = a + b;
     
      printf ("value of a = %d, b = %d and g = %d\n", a, b, g);
     
      return 0;
    }
    

    在程序中,局部变量和全局变量的名称可以相同,但是在函数内,如果两个名字相同,会使用局部变量值,全局变量不会被使用。局部优先原理!!!

    #include <stdio.h>
     
    /* 全局变量声明 */
    int g = 20;
     
    int main ()
    {
      /* 局部变量声明 */
      int g = 10;
     
      printf ("value of g = %d\n",  g);
     
      return 0;
    }
    //输出:
    value of g = 10
    
  3. 形式参数的函数参数定义中

    1. 函数的参数,形式参数,被当作该函数内的局部变量,如果与全局变量同名它们会优先使用。

强制类型转换

强制类型转换是把变量从一种类型转换为另一种数据类型。例如,如果您想存储一个 long 类型的值到一个简单的整型中,您需要把 long 类型强制转换为 int 类型。您可以使用强制类型转换运算符来把值显式地从一种类型转换为另一种类型,如下所示:

(type_name) expression

这里要注意的是强制类型转换运算符的优先级大于除法,类型转换可以是隐式的,由编译器自动执行,也可以是显式的,通过使用强制类型转换运算符来指定。在编程时,有需要类型转换的时候都用上强制类型转换运算符,是一种良好的编程习惯。

在这里插入图片描述

常用的算术转换是隐式地把值强制转换为相同的类型。编译器首先执行整数提升,如果操作数类型不同,则它们会被转换为左边层次中出现的最高层次的类型

#include <stdio.h>
 
int main()
{
   int  i = 17;
   char c = 'c'; /* ascii 值是 99 */
   float sum;
 
   sum = i + c;
   printf("Value of sum : %f\n", sum );
 
}

在这里,c 首先被转换为整数,但是由于最后的值是 float 型的,所以会应用常用的算术转换,编译器会把 i 和 c 转换为浮点型,并把它们相加得到一个浮点数。

所以在出现运算异常结果的时候可以使用反汇编程序来进行debug观察类型转换的事情

运算与运算符

运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C 语言内置了丰富的运算符,并提供了以下类型的运算符:

  • 算术运算符

  • 关系运算符

  • 逻辑运算符

  • 位运算符

  • 赋值运算符

  • 杂项运算符

  • 算术运算符

运算符描述实例
+把两个操作数相加A + B 将得到 30
-从第一个操作数中减去第二个操作数A - B 将得到 -10
*把两个操作数相乘A * B 将得到 200
/分子除以分母B / A 将得到 2
%取模运算符,整除后的余数B % A 将得到 0
++自增运算符,整数值增加 1A++ 将得到 11
自减运算符,整数值减少 1A-- 将得到 9
  • 关系运算符
运算符描述实例
==检查两个操作数的值是否相等,如果相等则条件为真。(A == B) 为假。
!=检查两个操作数的值是否相等,如果不相等则条件为真。(A != B) 为真。
>检查左操作数的值是否大于右操作数的值,如果是则条件为真。(A > B) 为假。
<检查左操作数的值是否小于右操作数的值,如果是则条件为真。(A < B) 为真。
>=检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。(A >= B) 为假。
<=检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。(A <= B) 为真。
  • 逻辑运算符
运算符描述实例
&&称为逻辑与运算符。如果两个操作数都非零,则条件为真。(A && B) 为假。
!称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。!(A && B) 为真。
  • 位运算符

位运算符作用于位,并逐位执行操作。&、 | 和 ^ 的真值表如下所示:

| p | q | p & q 与 | p | q 或 | p ^ q 异或 |
| — | — | — | — | — |
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 1 | 1 |
| 1 | 1 | 1 | 1 | 0 |
| 1 | 0 | 0 | 1 | 1 |

运算符描述实例
&对两个操作数的每一位执行逻辑与操作,如果两个相应的位都为 1,则结果为 1,否则为 0。
按位与操作,按二进制位进行"与"运算。运算规则:(A & B) 将得到 12,即为 0000 1100
对两个操作数的每一位执行逻辑或操作,如果两个相应的位都为 0,则结果为 0,否则为 1。
按位或运算符,按二进制位进行"或"运算。运算规则:(AB) 将得到 61,即为 0011 1101
^对两个操作数的每一位执行逻辑异或操作,如果两个相应的位值相同,则结果为 0,否则为 1。
异或运算符,按二进制位进行"异或"运算。运算规则:(A ^ B) 将得到 49,即为 0011 0001
~对操作数的每一位执行逻辑取反操作,即将每一位的 0 变为 1,1 变为 0。
取反运算符,按二进制位进行"取反"运算。运算规则:
`~1=-2;
~0=-1;`(~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。
<<将操作数的所有位向左移动指定的位数。左移 n 位相当于乘以 2 的 n 次方。
二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。A << 2 将得到 240,即为 1111 0000
>>将操作数的所有位向右移动指定的位数。右移n位相当于除以 2 的 n 次方。
二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补 0,负数左补 1,右边丢弃。A >> 2 将得到 15,即为 0000 1111
  • 赋值运算符
运算符描述实例
=简单的赋值运算符,把右边操作数的值赋给左边操作数C = A + B 将把 A + B 的值赋给 C
+=加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数C += A 相当于 C = C + A
-=减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数C -= A 相当于 C = C - A
*=乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数C *= A 相当于 C = C * A
/=除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数C /= A 相当于 C = C / A
%=求模且赋值运算符,求两个操作数的模赋值给左边操作数C %= A 相当于 C = C % A
<<=左移且赋值运算符C <<= 2 等同于 C = C << 2
>>=右移且赋值运算符C >>= 2 等同于 C = C >> 2
&=按位与且赋值运算符C &= 2 等同于 C = C & 2
^=按位异或且赋值运算符C ^= 2 等同于 C = C ^ 2
=按位或且赋值运算符
  • 杂项运算符
运算符描述实例
sizeof()返回变量的大小。sizeof(a) 将返回 4,其中 a 是整数。
&返回变量的地址。&a; 将给出变量的实际地址。
*指向一个变量。*a; 将指向一个变量。
? :条件表达式如果条件为真 ? 则值为 X : 否则值为 Y

这里需要注意两个运算符:/,++

  • 对于除法,C语言采用的是整数除法,所以int/int所得的不会出现一个分数往往得到的值都是我们不想要的,所以在需要取整和不需要取整两种情况会有很大的区别
3/5=0
//这里是两个int相除得到的结果一定是一个int,所以会进行舍入等情况
3.0/5 = 0.6 
3/0.5=0.6
//这里两个int只要有一个是double形式其他的都会进行自动类型转换成两个浮点类型的除法
int a = 3;
int b = 5;
a/b = 0 (double)a/b=0.6 

另一个就是++,当然—也是这样的

//先赋值后运算
	int c;
  int a = 10;
  c = a++;
  printf("%d",c);
//这里面的c等于10,但是后面打印的时候就是等于11了
//先运算后赋值:
  c = ++a; 
  printf("%d",c);
//这里的c等于11,先会执行a=a+1的操作,然后再执行c=a的操作,后面打印也是等于11

这个逻辑运算符主要都是在条件结构中的关系句多次判断中使用

位运算主要是在清洗数据,控制某些寄存器的值(比如单片机中就很常见这些运算符号)

这里取反是~不是!,这一点一定要分清楚,!是将布尔值进行反转的,但是这个是将一系列的数据进行反转的!!

#include <stdio.h>
 
int main()
{
   int a = 4;
   short b;
   double c;
   int* ptr;
 
   /* sizeof 运算符实例 */
   printf("Line 1 - 变量 a 的大小 = %lu\n", sizeof(a) );
   printf("Line 2 - 变量 b 的大小 = %lu\n", sizeof(b) );
   printf("Line 3 - 变量 c 的大小 = %lu\n", sizeof(c) );
 
   /* & 和 * 运算符实例 */
   ptr = &a;    /* 'ptr' 现在包含 'a' 的地址 */
   printf("a 的值是 %d\n", a);
   printf("*ptr 是 %d\n", *ptr);
 
   /* 三元运算符实例 */
   a = 10;
   b = (a == 1) ? 20: 30;
   //这个主要是为了看懂代码实际上自己写要尽量避免
   printf( "b 的值是 %d\n", b );
 
   b = (a == 10) ? 20: 30;
   printf( "b 的值是 %d\n", b );
}
Line 1 - 变量 a 的大小 = 4
Line 2 - 变量 b 的大小 = 2
Line 3 - 变量 c 的大小 = 8
a 的值是 4
*ptr 是 4
b 的值是 30
b 的值是 20

运算优先级:

类别运算符结合性
后缀() [] -> . ++ - -从左到右
一元+ - ! ~ ++ - - (type)* & sizeof从右到左
乘除* / %从左到右
加减+ -从左到右
移位<< >>从左到右
关系< <= > >=从左到右
相等== !=从左到右
位与 AND&从左到右
位异或 XOR^从左到右
位或 OR
逻辑与 AND&&从左到右
逻辑或 OR
条件?:从右到左
赋值= += -= *= /= %=>>= <<= &= ^==
逗号,从左到右

跳转语句

C 语言把任何非零非空的值假定为 true,把null 假定为 false

语句描述
if 语句一个 if 语句 由一个布尔表达式后跟一个或多个语句组成。
if…else 语句一个 if 语句 后可跟一个可选的 else 语句,else 语句在布尔表达式为假时执行。
嵌套 if 语句您可以在一个 ifelse if 语句内使用另一个 ifelse if 语句。
switch 语句一个 switch 语句允许测试一个变量等于多个值时的情况。
嵌套 switch 语句您可以在一个 switch 语句内使用另一个 switch 语句。

if-else语句

if(statements ){

}else if(statements){

}else{

}
//这个else后面不需要任何条件语句 

当使用 if…else if…else 语句时,以下几点需要注意:

  • 一个 if 后可跟零个或一个 else,else 必须在所有 else if 之后。
  • 一个 if 后可跟零个或多个 else if,else if 必须在 else 之前。
  • 一旦某个 else if 匹配成功,其他的 else if 或 else 将不会被测试。

三元运算符

可以用来替代 if…else 语句。它的一般形式如下:

Exp1 ? Exp2 : Exp3;

其中,Exp1、Exp2 和 Exp3 是表达式。请注意,冒号的使用和位置。? 表达式的值是由 Exp1 决定的。如果 Exp1 为真,则计算 Exp2 的值,结果即为整个表达式的值。如果 Exp1 为假,则计算 Exp3 的值,结果即为整个表达式的值。

这个往往在赋值表达式中很常见

switch语句

switch(expression){
    case constant-expression  :
       statement(s);
       break; /* 可选的 */
    case constant-expression  :
       statement(s);
       break; /* 可选的 */
  
    /* 您可以有任意数量的 case 语句 */
    default : /* 可选的 */
       statement(s);
}
  • switch 后面的表达式的值将会与每个 case 后面的常量值进行比较,直到找到匹配的值或者执行到 default(如果存在)。
  • 如果找到匹配的值,将执行相应 case 后面的代码块,然后跳出 switch 语句。
  • 如果没有匹配的值,并且有 default,则执行 default 后面的代码块。
  • 如果没有匹配的值,并且没有 default,则跳过整个 switch 语句直到结束。

switch 语句必须遵循下面的规则:

  • switch 表达式的类型: switch 语句中的表达式必须是整数类型(char、short、int或枚举),或者是能够隐式转换为整数类型的表达式。
  • case 标签的唯一性: 在 switch 语句中,每个 case 标签必须是唯一的,不能有重复的值。
  • 默认情况的可选性: switch 语句中的 default 标签是可选的。如果没有匹配的 case 标签,则会执行 default 标签下的代码块(如果存在)。
  • case 标签中的常量值: case 标签后面的值必须是一个常量表达式,这意味着它的值在编译时就能确定。(因为这个表是被存储在了静态区)
  • case 标签的顺序: switch 语句中的 case 标签的顺序并不重要,它们可以按照任意顺序编写。程序会按照 case 标签出现的顺序依次匹配。
  • break 语句的使用: 在每个 case 标签的代码块结束处通常需要使用 break 语句来终止 switch 语句的执行。如果没有 break 语句,程序将会继续执行下一个 case 标签中的代码,直到遇到 break 语句或 switch 语句结束。
  • switch 语句的嵌套: switch 语句可以嵌套在其他 switch 语句中,但是需要注意代码的可读性和复杂性。
  • case 标签和表达式的范围: switch 语句的 case 标签可以是整数常量表达式,但不能是浮点数或字符串。

循环结构

有的时候,我们可能需要多次执行同一块代码。一般情况下,语句是按顺序执行的:函数中的第一个语句先执行,接着是第二个语句,依此类推。编程语言提供了更为复杂执行路径的多种控制结构。循环语句允许我们多次执行一个语句或语句组

while语句

while(condition)
{
   statement(s);
}

for语句

for ( init; condition; increment )
{
   statement(s);
}

下面是 for 循环的控制流:

  1. init 会首先被执行,且只会执行一次。这一步允许您声明并初始化任何循环控制变量。您也可以不在这里写任何语句,只要有一个分号出现即可。
  2. 接下来,会判断 condition。如果为真,则执行循环主体。如果为假,则不执行循环主体,且控制流会跳转到紧接着 for 循环的下一条语句。
  3. 在执行完 for 循环主体后,控制流会跳回上面的 increment 语句。该语句允许您更新循环控制变量。该语句可以留空,只要在条件后有一个分号出现即可。
  4. 条件再次被判断。如果为真,则执行循环,这个过程会不断重复(循环主体,然后增加步值,再然后重新判断条件)。在条件变为假时,for 循环终止。

do…while语句

do
{
   statement(s);

}while( condition );

在 C 语言中,do…while 循环是在循环的尾部检查它的条件。do…while 循环与 while 循环类似,但是 do…while 循环会确保至少执行一次循环(先do一次再while循环)。

条件表达式出现在循环的尾部,所以循环中的 statement(s) 会在条件被测试之前至少执行一次。

如果条件为真,控制流会跳转回上面的 do,然后重新执行循环中的 statement(s)。这个过程会不断重复,直到给定条件变为假为止。

在这里插入图片描述

在这里插入图片描述在这里插入图片描述

跳转控制语句——break,continue,goto

控制语句描述
break 语句终止循环switch 语句,程序流将继续执行紧接着循环或 switch 的下一条语句。
continue 语句告诉一个循环体立刻停止本次循环迭代,重新开始下次循环迭代。
goto 语句将控制转移到被标记的语句。但是不建议在程序中使用 goto 语句。

C 提供了下列的循环控制语句。

break语句

C 语言中 break 语句有以下两种用法:

  1. break 语句出现在一个循环内时,循环会立即终止,且程序流将继续执行紧接着循环的下一条语句。
  2. 它可用于终止 switch 语句中的一个 case。

如果您使用的是嵌套循环(即一个循环内嵌套另一个循环),break 语句会停止执行最内层的循环,然后开始执行该块之后的下一行代码。

continue语句

C 语言中的 continue 语句有点像 break 语句。但它不是强制终止,continue 会跳过当前循环中的代码,强迫开始下一次循环。

对于 for 循环,continue 语句执行后自增语句仍然会执行。对于 whiledo…while 循环,continue 语句重新执行条件判断语句。

goto语句

C 语言中的 goto 语句允许把控制无条件转移到同一函数内的被标记的语句。

***注意:*在任何编程语言中,都不建议使用 goto 语句。因为它使得程序的控制流难以跟踪,使程序难以理解和难以修改。任何使用 goto 语句的程序可以改写成不需要使用 goto 语句的写法。

goto label;
..
.
label: statement;

在这里,label 可以是任何除 C 关键字以外的纯文本,它可以设置在 C 程序中 goto 语句的前面或者后面。

在这里插入图片描述

死循环

如果条件永远不为假,则循环将变成无限循环。for 循环在传统意义上可用于实现无限循环。由于构成循环的三个表达式中任何一个都不是必需的,您可以将某些条件表达式留空来构成一个无限循环。

#include <stdio.h>
 
int main ()
{
   for( ; ; )
   {
      printf("该循环会永远执行下去!\n");
   }
   return 0;
}
//但是我们往往都是使用while(True)这种来实现死循环的,主打一个少些代码

当条件表达式不存在时,它被假设为真。您也可以设置一个初始值和增量表达式,但是一般情况下,C 程序员偏向于使用 for(;😉 结构来表示一个无限循环。

**注意:**您可以按 Ctrl + C 键终止一个无限循环,这是用停止程序的方法从而来关闭这个死循环

函数

函数声明

函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数,即主函数 main() ,所有简单的程序都可以定义其他额外的函数。您可以把代码划分到不同的函数中。如何划分代码到不同的函数中是由您来决定的,但在逻辑上,划分通常是根据每个函数执行一个特定的任务来进行的。函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的实际主体。

C 语言中的函数定义的一般形式如下:

return_type function_name( parameter list )
{
   body of the function
}

在 C 语言中,函数由一个函数头和一个函数主体组成。下面列出一个函数的所有组成部分:

  • **返回类型:**一个函数可以返回一个值。return_type 是函数返回的值的数据类型。有些函数执行所需的操作而不返回值,在这种情况下,return_type 是关键字 void
  • **函数名称:**这是函数的实际名称。函数名和参数列表一起构成了函数签名。
  • **参数:**参数就像是占位符。当函数被调用时,您向参数传递一个值,这个值被称为实际参数。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。
  • **函数主体:**函数主体包含一组定义函数执行任务的语句。

函数就是C的一种封装思想,是高级语言的关键特征

创建 C 函数时,会定义函数做什么,然后通过调用函数来完成已定义的任务。

当程序调用函数时,程序控制权会转移给被调用的函数。被调用的函数执行已定义的任务,当函数的返回语句被执行时,或到达函数的结束括号时,会把程序控制权交还给主程序。调用函数时,传递所需参数,如果函数返回一个值,则可以存储返回值。

函数传参调用

这是函数最最关键的部分,C语言遵循的是按值来进行传递,就是说在不使用引用传递的情况下,一个函数就是一个独立的整体,里面的任何改变都不会影响外面的原来的值

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

如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。

传值调用

向函数传递参数的传值调用方法,把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。

默认情况下,C 语言使用传值调用方法来传递参数。一般来说,这意味着函数内的代码不会改变用于调用函数的实际参数。

/* 函数定义 */
void swap(int x, int y)
{
   int temp;

   temp = x; /* 保存 x 的值 */
   x = y;    /* 把 y 赋值给 x */
   y = temp; /* 把 temp 赋值给 y */
  
   return;
}
#include <stdio.h>
 
/* 函数声明 */
void swap(int x, int y);
 
int main ()
{
   /* 局部变量定义 */
   int a = 100;
   int b = 200;
 
   printf("交换前,a 的值: %d\n", a );
   printf("交换前,b 的值: %d\n", b );
 
   /* 调用函数来交换值 */
   swap(a, b);
 
   printf("交换后,a 的值: %d\n", a );
   printf("交换后,b 的值: %d\n", b );
 
   return 0;
}
//输出:
交换前,a 的值: 100
交换前,b 的值: 200
交换后,a 的值: 100
交换后,b 的值: 200

引用调用

通过引用传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。

传递指针可以让多个函数访问指针所引用的对象,而不用把对象声明为全局可访问。能接受指针作为参数的函数,也能接受数组作为参数,这两者在本质上是一致的

/* 函数定义 */
void swap(int *x, int *y)
{
   int temp;
   temp = *x;    /* 保存地址 x 的值 */
   *x = *y;      /* 把 y 赋值给 x */
   *y = temp;    /* 把 temp 赋值给 y */
  
   return;
}
#include <stdio.h>
 
/* 函数声明 */
void swap(int *x, int *y);
 
int main ()
{
   /* 局部变量定义 */
   int a = 100;
   int b = 200;
 
   printf("交换前,a 的值: %d\n", a );
   printf("交换前,b 的值: %d\n", b );
 
   /* 调用函数来交换值
    * &a 表示指向 a 的指针,即变量 a 的地址 
    * &b 表示指向 b 的指针,即变量 b 的地址 
   */
   swap(&a, &b);
 
   printf("交换后,a 的值: %d\n", a );
   printf("交换后,b 的值: %d\n", b );
 
   return 0;
}
//输出:
交换前,a 的值: 100
交换前,b 的值: 200
交换后,a 的值: 200
交换后,b 的值: 100

函数的返回

首先我们知道C的函数声明和返回值必须是匹配的,但是关于指针部分的返回内容我们还是没有讲解的

C 允许您从函数返回指针。为了做到这点,您必须声明一个返回指针的函数,如下所示:

int * myFunction(){
}

很明显的声明需要声明类型中必须要有一个指针,另外,C 语言不支持在调用函数时返回局部变量的地址,除非定义局部变量为 static 变量(因为这种函数的局部变量没有static是会被自动进行销毁的,那么返回的地址就是没有任何东西存的垃圾地址,所以不支持)。

指针与数组

数组

C 语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。但是他不是连续的声明一系列的变量,而是有一种特殊的索引来进行单个变量的值修改

所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。

在 C 中要声明一个数组,需要指定元素的类型和元素的数量,如下所示:

type arrayName [ arraySize ];

这叫做一维数组。arraySize 必须是一个大于零的整数常量,type 可以是任意有效的 C 数据类型。


在 C 中,您可以逐个初始化数组,也可以使用一个初始化语句,如下所示:

double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};

大括号 { } 之间的值的数目不能大于我们在数组声明时在方括号 [ ] 中指定的元素数目。

如果您省略掉了数组的大小,数组的大小则为初始化时元素的个数。因此,如果:

double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};

**所有的数组都是以 0 作为它们第一个元素的索引,也被称为基索引,数组的最后一个索引是数组的总大小减去 1。**以下是上面所讨论的数组的的图形表示:

数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。例如:

double salary = balance[9];

数组长度可以使用 sizeof 运算符来获取数组的长度

#include <stdio.h>

int main() {
    int array[] = {1, 2, 3, 4, 5};
    int length = sizeof(array) / sizeof(array[0]);
		//sizeof+数组名计算的是全部的元素字节大小
    printf("数组长度为: %d\n", length);

    return 0;
}

在 C 语言中,数组名表示数组的地址,即数组首元素的地址。当我们在声明和定义一个数组时,该数组名就代表着该数组的地址。

int myArray[5] = {10, 20, 30, 40, 50};

在这里,myArray 是数组名,它表示整数类型的数组,包含 5 个元素。myArray 也代表着数组的地址,即第一个元素的地址。

数组名本身是一个常量指针,意味着它的值是不能被改变的,一旦确定,就不能再指向其他地方。

虽然数组名表示数组的地址,但在大多数情况下,数组名会自动转换为指向数组首元素的指针。这意味着我们可以直接将数组名用于指针运算


概念描述
多维数组C 支持多维数组。多维数组最简单的形式是二维数组。
传递数组给函数您可以通过指定不带索引的数组名称来给函数传递一个指向数组的指针。
从函数返回数组C 允许从函数返回数组。
指向数组的指针您可以通过指定不带索引的数组名称来生成一个指向数组中第一个元素的指针。
静态数组与动态数组静态数组在编译时分配内存,大小固定,而动态数组在运行时手动分配内存,大小可变。

静态数组与动态数组

在 C 语言中,有两种类型的数组:

  • 静态数组:编译时分配内存,大小固定。
  • 动态数组:运行时手动分配内存,大小可变。

静态数组的生命周期与作用域相关,而动态数组的生命周期由程序员控制。在使用动态数组时,需要注意合理地分配和释放内存,以避免内存泄漏和访问无效内存的问题。

静态数组是指在编译时确定大小的数组,其大小在程序运行期间不能改变。在 C 语言中,静态数组的内存分配在栈区,通常使用方括号 [] 来定义。

静态数组的特点包括:

  • 内存分配:静态数组的内存通常分配在栈上,随着函数的调用和返回而自动管理。
  • 大小固定:在定义时指定大小,且在程序运行过程中不能更改。
  • 效率:由于在栈上分配内存,访问速度较快。
  • 生命周期:静态数组的生命周期始于其定义时。如果在函数内部定义,生命周期与函数的调用相同;如果在全局范围定义,生命周期贯穿整个程序运行。

动态数组是在运行时通过动态内存分配函数(如 malloc 和 calloc)手动分配内存的数组,其存在的地址是堆

动态数组特点如下:

  • 内存分配:动态数组的内存空间**在运行时通过动态内存分配函数手动分配,并存储在堆上。**需要使用 malloccalloc 等函数来申请内存,并使用 free 函数来释放内存。
  • 大小可变:动态数组的大小在运行时可以根据需要进行调整。可以使用 realloc 函数来重新分配内存,并改变数组的大小。
  • 生命周期:动态数组的生命周期由程序员控制。需要在使用完数组后手动释放内存,以避免内存泄漏。(只要你没结束程序没有free就一直存在,所以我们有些要保存和共享使用的数据往往就用动态数组来申请堆内存进行保存)

具体操作实现看内存管理部分!!!这里仅仅写一个简单的例子

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

int main() {
    int size = 5;
    int *dynamicArray = (int *)malloc(size * sizeof(int)); // 动态数组内存分配

    if (dynamicArray == NULL) {
        printf("Memory allocation failed.\n");
        return 1;
    }

    printf("Enter %d elements: ", size);
    for (int i = 0; i < size; i++) {
        scanf("%d", &dynamicArray[i]);
    }

    printf("Dynamic Array: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", dynamicArray[i]);
    }
    printf("\n");

    free(dynamicArray); // 动态数组内存释放

    return 0;
}

指针

学习 C 语言的指针既简单又有趣。通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。所以,想要成为一名优秀的 C 程序员,学习指针是很有必要的。正如您所知道的,每一个变量都有一个内存位置,每一个内存位置都定义了可使用 & 运算符访问的地址,它表示了在内存中的一个地址。

在这里插入图片描述

概念描述
指针的算术运算可以对指针进行四种算术运算:++、–、+、-
指针数组可以定义用来存储指针的数组。
指向指针的指针C 允许指向指针的指针。
传递指针给函数通过引用或地址传递参数,使传递的参数在调用函数中被改变。
从函数返回指针C 允许函数返回指针到局部变量、静态变量和动态内存分配。

指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:

type *var_name;

在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var_name 是指针变量的名称。用来声明指针的星号 ***** 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。

所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。我们声明的时候之所以必须要这样就是为了取地址等操作的时候知道用什么类型的方式去解释这个地址指向的数据的含义

定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符 * 来返回位于操作数所指定地址的变量的值。

*a表示的是利用a这个变量中存的值P,去P这个地址上找到数据,

&表示取到变量对应的地址,被我们称作取地址符

#include <stdio.h>
 
int main ()
{
   int  var = 20;   /* 实际变量的声明 */
   int  *ip;        /* 指针变量的声明 */
 
   ip = &var;  /* 在指针变量中存储 var 的地址 */
	 
   printf("var 变量的地址: %p\n", &var  );
 
   /* 在指针变量中存储的地址 */
   printf("ip 变量存储的地址: %p\n", ip );
 
   /* 使用指针访问值 */
   printf("*ip 变量的值: %d\n", *ip );
 
   return 0;
}

在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为指针。NULL 指针是一个定义在标准库中的值为零的常量。

#include <stdio.h>
 
int main ()
{
   int  *ptr = NULL;
 
   printf("ptr 的地址是 %p\n", ptr  );
 
   return 0;
}
//这里是真的值为0,但是0这个地址往往都是在内核区中的被操作系统保护了的
ptr 的地址是 0x0

在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。


指针的运算:

C 指针是一个用数值表示的地址。因此,您可以对指针执行算术运算。可以对指针进行四种算术运算:++、–、+、-。没有什么乘除法的!!!

*假设 ptr 是一个指向地址 1000 的整型指针,是一个 32 位的整数,让我们对该指针执行下列的算术运算:*ptr++ 执行完上述的运算之后,ptr 将指向位置 1004,因为 ptr 每增加一次,它都将指向下一个整数位置,即当前位置往后移 4 字节。这个运算会在不影响内存位置中实际值的情况下,移动指针到下一个内存位置。如果 ptr 指向一个地址为 1000 的字符,上面的运算会导致指针指向位置 1001,因为下一个字符位置是在 1001。

所以我们指针++的操作,实际上是指向下一个元素的存储单元,指针的每一次递减,它都会指向前一个元素的存储单元。指针在递增和递减时跳跃的字节数取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节。这也是为什么我们声明的时候必须指定指向的内容的原因。

递增一个指针意味着让指针指向下一个内存位置。指针的递增操作会根据指针所指向的数据类型进行适当的内存偏移。我们喜欢在程序中使用指针代替数组,因为变量指针可以递增,而数组不能递增,数组可以看成一个指针常量。

#include <stdio.h>
 
const int MAX = 3;
 
int main ()
{
   // 定义一个整数数组
   int var[] = {10, 100, 200};
   // 定义一个整数变量 i 和一个整数指针 ptr
   int i, *ptr;
 
   // 将指针 ptr 指向数组 var 的起始地址
   ptr = var;
   // 循环遍历数组
   for ( i = 0; i < MAX; i++)
   {
      // 打印当前指针 ptr 所指向的地址
      printf("存储地址:var[%d] = %p\n", i, ptr );
      // 打印当前指针 ptr 所指向地址的值
      printf("存储值:var[%d] = %d\n", i, *ptr );
 
      // 将指针 ptr 移动到下一个数组元素的位置
      ptr++;
   }
   return 0;
}

指针的比较:

可以比较指针来确定它们的关系。指针比较主要用于确定两个指针是否指向相同的内存位置或确定一个指针是否位于另一个指针之前或之后。指针可以用关系运算符进行比较,如==!=<><=>=。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。(必须要有相关性啊,这个比较不是简单意义上的把地址都取出来然后比地址大小的!!!)

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *ptr1 = &arr[1];  // 指向 arr[1],值为 20
    int *ptr2 = &arr[3];  // 指向 arr[3],值为 40

    if (ptr1 < ptr2) {
        printf("ptr1 在 ptr2 之前\n");  // 这行会被输出
    } else {
        printf("ptr1 在 ptr2 之后或相同位置\n");
    }

    if (ptr1 > ptr2) {
        printf("ptr1 在 ptr2 之后\n");
    } else {
        printf("ptr1 在 ptr2 之前或相同位置\n");  // 这行会被输出
    }

    return 0;
}
//输出:
ptr1 在 ptr2 之前
ptr1 在 ptr2 之前或相同位置
  • 相等比较 (==!=): 用于判断两个指针是否指向相同的内存位置。
  • 大小比较 (<, >, <=, >=): 通常用于指针遍历数组或内存块时,判断一个指针是否在另一个指针之前或之后。

需要注意的是,指针比较只有在指向同一个数组或同一内存块时才有意义,否则行为未定义。


指针数组:

C 指针数组是一个数组,其中的每个元素都是指向某种数据类型的指针。指针数组存储了一组指针,每个指针可以指向不同的数据对象。指针数组通常用于处理多个数据对象,例如字符串数组或其他复杂数据结构的数组。

指针数组,就是数组里面存的全是指针,这个在存很多字符串中很常见,比如下面的例子

用一个指向字符的指针数组来存储一个字符串列表,如下:

#include <stdio.h>
 
const int MAX = 4;
 
int main ()
{
   const char *names[] = {
                   "Zara Ali",
                   "Hina Ali",
                   "Nuha Ali",
                   "Sara Ali",
   };
   int i = 0;
 
   for ( i = 0; i < MAX; i++)
   {
      printf("Value of names[%d] = %s\n", i, names[i] );
   }
   return 0;
}
//
Value of names[0] = Zara Ali
Value of names[1] = Hina Ali
Value of names[2] = Nuha Ali
Value of names[3] = Sara Ali

多维数组/多重指针

多维数组:

多维数组最简单的形式是二维数组。一个二维数组,在本质上,是一个一维数组的列表。声明一个 x 行 y 列的二维整型数组,形式如下:

type arrayName [ x ][ y ];

其中,type 可以是任意有效的 C 数据类型,arrayName 是一个有效的 C 标识符。一个二维数组可以被认为是一个带有 x 行和 y 列的表格。下面是一个二维数组,包含 3 行和 4 列:

int x[3][4];

因此,数组中的每个元素是使用形式为 a[ i , j ] 的元素名称来标识的,其中 a 是数组名称,i 和 j 是唯一标识 a 中每个元素的下标。

和我们平常线性代数是一样的,所以行在前面列在后面,但是实际上我们要理解原因这个a[i][j]等价于(*(a + i) + j)【前面的是大单位,后面的是小单位】*


多次指向的指针(多重指针):

指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。

在这里插入图片描述

画图分析这种指针链是很有必要的,这种指针的使用是很多高级算法还有高级数据结构的基础

#include <stdio.h>
 
int main ()
{  int  V;
   int  *Pt1;
   int  **Pt2;
   V = 100;
   /* 获取 V 的地址 */
   Pt1 = &V;
   /* 使用运算符 & 获取 Pt1 的地址 */
   Pt2 = &Pt1;
   /* 使用 pptr 获取值 */
   printf("var = %d\n", V );
   printf("Pt1 = %p\n", Pt1 );
   printf("*Pt1 = %d\n", *Pt1 );
   printf("Pt2 = %p\n", Pt2 );
   printf("**Pt2 = %d\n", **Pt2);
   return 0;
}

函数指针

枚举类型

枚举是 C 语言中的一种基本数据类型,用于定义一组具有离散值的常量,它可以让数据更简洁,更易读(维护易读性是关键,由于如果使用大量的宏可能导致工程出现各种各样的不兼容问题)。

枚举类型通常用于为程序中的一组相关的常量取名字,以便于程序的可读性和维护性。

定义一个枚举类型,需要使用 enum 关键字,后面跟着枚举类型的名称,以及用大括号 {} 括起来的一组枚举常量。每个枚举常量可以用一个标识符来表示,也可以为它们指定一个整数值,如果没有指定,那么默认从 0 开始递增。

enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};

第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。

enum season {spring, summer=3, autumn, winter};
//没有指定值的枚举元素,其值为前一元素加 1。也就说 spring 的值为 0,summer 的值为 3,autumn 的值为 4,winter 的值为 5
//我们可以通过以下三种方式来定义枚举变量
//1、先定义枚举类型,再定义枚举变量
				enum DAY
				{
				      MON=1, TUE, WED, THU, FRI, SAT, SUN
				};
				enum DAY day;
//2、定义枚举类型的同时定义枚举变量
				enum DAY
				{
				      MON=1, TUE, WED, THU, FRI, SAT, SUN
				} day;
//3、省略枚举名称,直接定义枚举变量
				enum
				{
				      MON=1, TUE, WED, THU, FRI, SAT, SUN
				} day;

在C 语言中,枚举类型是被当做 int 或者 unsigned int 类型来处理的,所以按照 C 语言规范是没有办法遍历枚举类型的。不过在一些特殊的情况下,枚举类型必须连续是可以实现有条件的遍历。

#include <stdio.h>
 
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
int main()
{
    // 遍历枚举元素
    for (day = MON; day <= SUN; day++) {
        printf("枚举元素:%d \n", day);
    }
}
#include <stdio.h>
#include <stdlib.h>
int main()
{
 
    enum color { red=1, green, blue };
 
    enum  color favorite_color;
 
    /* 用户输入数字来选择颜色 */
    printf("请输入你喜欢的颜色: (1. red, 2. green, 3. blue): ");
    scanf("%u", &favorite_color);
 
    /* 输出结果 */
    switch (favorite_color)
    {
    case red:
        printf("你喜欢的颜色是红色");
        break;
    case green:
        printf("你喜欢的颜色是绿色");
        break;
    case blue:
        printf("你喜欢的颜色是蓝色");
        break;
    default:
        printf("你没有选择你喜欢的颜色");
    }
 
    return 0;
}

结构体和联合体

结构体

C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。结构体中的数据成员可以是基本数据类型(如 int、float、char 等),也可以是其他结构体类型、指针类型等。

结构体定义由关键字 struct 和结构体名组成,结构体名可以根据需要自行定义。

struct 语句定义了一个包含多个成员的新的数据类型,struct 语句的格式如下:

struct tag { 
    member-list
    member-list 
    member-list  
    ...
} variable-list ;

tag 是结构体标签。member-list 是标准的变量定义,比如 int i; 或者 float f;,或者其他有效的变量定义。variable-list 结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量。

那么我们一旦声明了一个结构体在内存中这些都是成块的,但不是连续的bit的,就是说会有一些空间浪费,是因为数据在内存中按特定边界对齐,提高访问效率

而且win和linux平台的对齐规则是不一样的

//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//同时又声明了结构体变量s1
//这个结构体并没有标明其标签
struct 
{
    int a;
    char b;
    double c;
} s1;

//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//结构体的标签被命名为SIMPLE,没有声明变量
struct SIMPLE
{
    int a;
    char b;
    double c;
};
//用SIMPLE标签的结构体,另外声明了变量t1、t2、t3
struct SIMPLE t1, t2[20], *t3;

//也可以用typedef创建新类型
typedef struct
{
    int a;
    char b;
    double c; 
} Simple2;
//现在可以用Simple2作为类型声明新的结构体变量
Simple2 u1, u2[20], *u3;

在这里插入图片描述

在左边的声明中,第一个和第二声明被编译器当作两个完全不同的类型,即使他们的成员列表是一样的,如果令 t3=&s1,则是非法的。

结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等。

和其它类型变量一样,对结构体变量可以在定义时指定初始值。

#include <stdio.h>

struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
} book = {"C 语言", "RUNOOB", "编程语言", 123456};

int main()
{
    printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", 
    book.title, book.author, book.subject, book.book_id);
}

当然更普遍的方法是使用.的成员访问符:为了访问结构的成员,我们使用成员访问运算符(.)。成员访问运算符是结构变量名称和我们要访问的结构成员之间的一个句号。

C 语言中,我们可以使用 sizeof 运算符来计算结构体的大小,sizeof 返回的是给定类型或变量的字节大小。对于结构体,sizeof 将返回结构体的总字节数,包括所有成员变量的大小以及可能的填充字节。

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

#include <stdio.h>
#include <string.h>
 
struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
};
 
int main( )
{
   struct Books Book1;        /* 声明 Book1,类型为 Books */
   struct Books Book2;        /* 声明 Book2,类型为 Books */
 
   /* Book1 详述 */
   strcpy( Book1.title, "C Programming");
   strcpy( Book1.author, "Nuha Ali"); 
   strcpy( Book1.subject, "C Programming Tutorial");
   Book1.book_id = 6495407;

   /* Book2 详述 */
   strcpy( Book2.title, "Telecom Billing");
   strcpy( Book2.author, "Zara Ali");
   strcpy( Book2.subject, "Telecom Billing Tutorial");
   Book2.book_id = 6495700;
 
   /* 输出 Book1 信息 */
   printf( "Book 1 title : %s\n", Book1.title);
   printf( "Book 1 author : %s\n", Book1.author);
   printf( "Book 1 subject : %s\n", Book1.subject);
   printf( "Book 1 book_id : %d\n", Book1.book_id);

   /* 输出 Book2 信息 */
   printf( "Book 2 title : %s\n", Book2.title);
   printf( "Book 2 author : %s\n", Book2.author);
   printf( "Book 2 subject : %s\n", Book2.subject);
   printf( "Book 2 book_id : %d\n", Book2.book_id);

   return 0;
}

typedef关键字

C 语言提供了 typedef 关键字,您可以使用它来为类型取一个新的名字。下面的实例为单字节数字定义了一个术语 BYTE

typedef unsigned char BYTE;

在这个类型定义之后,标识符 BYTE 可作为类型 unsigned char 的缩写,例如:

BYTE  b1, b2;

按照惯例,定义时会大写字母,以便提醒用户类型名称是一个象征性的缩写,但您也可以使用小写字母,如下:

typedef unsigned char byte;

您也可以使用 typedef 来为用户自定义的数据类型取一个新的名字。

#define 是 C 指令,用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:

  • typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
  • typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。

联合体

共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。虽然我们第一眼看起来这个并不是很好用,甚至感觉他没有什么用,但是实际上,在比如控制一个寄存器的时候我们用一个数据int来表示里面所有位的数据,但是我们实际访问的时候仅仅需要几个bit的数据,那么我们可以用这个将其拆分成四个char来进行处理(在一些通信等等底层构建往往很可能就是用了这种技术的,比如通信穿6个字节,但是实际上会传两个int类型,那么这两个int肯定需要进行拆分和拼接从而还原成原来的样子,所以就利用这个技术)你在单片机等技术的学习中肯定会碰到这方面的应用的

为了定义共用体,您必须使用 union 语句,方式与定义结构类似。union 语句定义了一个新的数据类型,带有多个成员。

union [union tag]
{
   member definition;
   member definition;
   ...
   member definition;
} [one or more union variables];

当然这里的定义式是很有问题的,我们还是举例子来讲清楚吧

union Data
{
   int i;
   float f;
   char  str[20];
} data;

Data 类型的变量可以存储一个整数、一个浮点数,或者一个字符串。这意味着一个变量(相同的内存位置)可以存储多个多种类型的数据。您可以根据需要在一个共用体内使用任何内置的或者用户自定义的数据类型。共用体占用的内存应足够存储共用体中最大的成员。例如,在上面的实例中,Data 将占用 20 个字节的内存空间,因为在各个成员中,字符串所占用的空间是最大的。所以如果你堆上面这个data来进行sizeof的话返回的值应该是20(对应最大成员str数组的大小)

为了访问共用体的成员,我们使用成员访问运算符(.)。成员访问运算符是共用体变量名称和我们要访问的共用体成员之间的一个句号。访问方法和联合体是一样的

#include <stdio.h>
#include <string.h>
 
union Data
{
   int i;
   float f;
   char  str[20];
};
 
int main( )
{
   union Data data;        
 
   data.i = 10;
   data.f = 220.5;
   strcpy( data.str, "C Programming");
 
   printf( "data.i : %d\n", data.i);
   printf( "data.f : %f\n", data.f);
   printf( "data.str : %s\n", data.str);
 
   return 0;
}
//输出:
data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programming

在这里,我们可以看到共用体的 if 成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因。

这里是因为我们最后进行输出的时候是对标str中的内容再去用int和float进行解释的,所以肯定是有所损坏的!

字符数组——字符串

C语言的字符串是最最特殊的东西,他不是一个单独的引用类型

在 C 语言中,字符串实际上是使用空字符 \0 结尾的一维字符数组。因此,\0 是用于标记字符串的结束。空字符(Null character)又称结束符,缩写 NUL,是一个数值为 0 的控制字符,\0 是转义字符,意思是告诉编译器,这不是字符 0,而是空字符。

注意我们单个字符的表示一定是单引号的!!!!这一点很重要,字符串表示是用””双引号的

char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};

依据数组初始化规则,您可以把上面的语句写成以下语句:

char site[] = "RUNOOB";

在这里插入图片描述

而这个字符数组也是像Java的String的底层实现的逻辑,很关键的一点字符串的比较一定是要借助函数的!!!!!strcmp函数,否则比较引用一定不相同的!!!!

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

相关的字符处理函数

C 中有大量操作字符串的函数:

序号函数 & 目的函数意义功能
1**strcpy(s1, s2);**复制字符串 s2 到字符串 s1。String Copy字符串复制与赋值
2**strcat(s1, s2);**连接字符串 s2 到字符串 s1 的末尾。String Concatenate字符串连接
3**strlen(s1);**返回字符串 s1 的长度。String Length字符串长度求解
4**strcmp(s1, s2);**如果 s1 和 s2 是相同的,则返回 0;如果 s1<s2 则返回小于 0;如果 s1>s2 则返回大于 0。String Compare字符串比较
5**strchr(s1, ch);**返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。string character在字符串中查找第一个出现的指定字符
6**strstr(s1, s2);**返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。string string在字符串中查找第一个出现的指定子字符串

这里面必须强调一个函数就是这个strcpy,由于字符串是一个常量数组,我们进行其值的更改时必须借助函数来进行

预处理器与宏

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

***所有的预处理器命令都是以井号(#)开头。*它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。

常用的预处理指令

指令描述
#define定义宏
#include包含一个源代码文件
#undef取消已定义的宏
#ifdef如果宏已经定义,则返回真
#ifndef如果宏没有定义,则返回真
#if如果给定条件为真,则编译下面代码
#else#if 的替代方案
#elif如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif结束一个 #if……#else 条件编译块
#error当遇到标准错误时,输出错误消息
#pragma使用标准化方法,向编译器发布特殊的命令到编译器中

但是我们不能将所有的含有#得都应该被认为时预处理命令,但是我们要知道有很多库有一些扩展性的功能导致有变化的可能导致有一些变化,上面得仅仅是比较常见的命令但是不是所有的预处理命令!!!

#define MAX_ARRAY_LENGTH 20

这个指令告诉 CPP 把所有的 MAX_ARRAY_LENGTH 定义为 20。使用 #define 定义常量来增强可读性,这个是纯正的文本替换,但是有一些功能是比文本替换智能的

#include <stdio.h>
#include "myheader.h"

这些指令告诉 CPP 从系统库中获取 stdio.h,并添加文本到当前的源文件中。下一行告诉 CPP 从本地目录中获取 myheader.h,并添加内容到当前的源文件中。

#undef  FILE_SIZE
#define FILE_SIZE 42

这个指令告诉 CPP 取消已定义的 FILE_SIZE,并定义它为 42。

#ifndef MESSAGE
   #define MESSAGE "You wish!"
#endif

这个指令告诉 CPP 只有当 MESSAGE 未定义时,才定义 MESSAGE。

#ifdef DEBUG
   /* Your debugging statements here */
#endif

这个指令告诉 CPP 如果定义了 DEBUG,则执行处理语句。在编译时,如果您向 gcc 编译器传递了 -DDEBUG 开关量,这个指令就非常有用。它定义了 DEBUG,您可以在编译期间随时开启或关闭调试。


内置宏

ANSI C 定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。

描述
DATE当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。
TIME当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。
FILE这会包含当前文件名,一个字符串常量。
LINE这会包含当前行号,一个十进制常量。
STDC当编译器以 ANSI 标准编译时,则定义为 1。

宏运算符

宏延续运算符(\)

一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。例如:

#define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")

字符串常量化运算符(#)

在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。

#include <stdio.h>
 
#define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")
    //假如你这里定义的时候printf后面有;后面的main就不可以再写;分号了
 
int main(void)
{
   message_for(Carole, Debra);
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Carole and Debra: We love you!

标记粘贴运算符(##)

宏定义内的标记粘贴运算符(##)会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记。例如:

#include <stdio.h>
 
#define tokenpaster(n) printf ("token" #n " = %d", token##n)
//这里将token和n两个连到一起成立token34(这个34就是对应的n的位置)
 
int main(void)
{
   int token34 = 40;
   
   tokenpaster(34);
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

token34 = 40

这是怎么发生的,因为这个实例会从编译器产生下列的实际输出:

printf ("token34 = %d", token34);

这个实例演示了 token##n 会连接到 token34 中,在这里,我们使用了字符串常量化运算符(#)标记粘贴运算符(##)

defined() 运算符

预处理器 defined 运算符是用在常量表达式中的,**用来确定一个标识符是否已经使用 #define 定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。**下面的实例演示了 defined() 运算符的用法:

#include <stdio.h>
//我们可以发现这个关键是再#if和e#endif中进行配合使用的
#if !defined (MESSAGE)
   #define MESSAGE "You wish!"
#endif
 
int main(void)
{
   printf("Here is the message: %s\n", MESSAGE);  
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Here is the message: You wish!

函数化的宏

**CPP 一个强大的功能是可以使用参数化的宏来模拟函数。**例如,下面的代码是计算一个数的平方:

int square(int x) {
   return x * x;
}

我们可以使用宏重写上面的代码,如下:

#define square(x) ((x) * (x))

在使用带有参数的宏之前,必须使用 #define 指令定义。**参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格。**例如:

#include <stdio.h>
 
#define MAX(x,y) ((x) > (y) ? (x) : (y))
 
int main(void)
{
   printf("Max between 20 and 10 is %d\n", MAX(10, 20));  
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Max between 20 and 10 is 20

程序产生的全过程——编译,链接,加载,运行

文件管理

一个文件,无论它是文本文件还是二进制文件,都是代表了一系列的字节。C 语言不仅提供了访问顶层的函数,也提供了底层(OS)调用来处理存储设备上的文件。

这里的FILE是一个结构体里面村里很多有关文件的属性内容,下面就是在stdio.h头文件中的定义:

typedef struct  {  
        short           level;          / fill/empty level of buffer /  
        unsigned        flags;          / File status flags    /  
        char            fd;             / File descriptor      /  
        unsigned char   hold;           / Ungetc char if no buffer /  
        short           bsize;          / Buffer size          /  
        unsigned char   buffer;        / Data transfer buffer /  
        unsigned char   *curp;          / Current active pointer /  
        unsigned        istemp;         / Temporary file indicator /  
        short           token;          / Used for validity checking /  
}       FILE;                           / This is the FILE object */ 

EOF:EOF是end of file的缩写,表示"文字流"(stream)的结尾。这里的"文字流",可以是文件(file),也可以是标准输入(stdin)。该宏定义在stdio.h中。

注意啊,这里的EOF仅仅类似字符串中的\0,是仅仅操作系统来识别的,我们判断遍历是不是到达末尾不是主要用这个的呀!

打开文件

您可以使用 fopen( ) 函数来创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型 FILE 的一个对象,类型 FILE 包含了所有用来控制流的必要的信息。下面是这个函数调用的原型:

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

在这里,filename 是字符串,用来命名文件,访问模式 mode 的值可以是下列值中的一个:

模式描述
r打开一个已有的文本文件,允许读取文件。read
w打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,文件内容会被清空(即文件长度被截断为0)。write
a打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。add
r+打开一个文本文件,允许读写文件。
w+打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。
a+打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。

这里的加号表示允许读写,开启所有的IO权限。

如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:

"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"
//主要是加了b表示binary二进制文件的意思,我们可以发现这个b的位置是只要在上面的文本字母后面就可以了

关闭文件

函数的原型如下:

 int fclose( FILE *fp );

**如果成功关闭文件,fclose( ) 函数返回零,如果关闭文件时发生错误,函数返回 EOF。这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。**EOF 是一个定义在头文件 stdio.h 中的常量。

写入到文件

fputc:

int fputc( int c, FILE *fp );

函数 fputc() 把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回 EOF

这个函数是用来进行一个一个字符进行写入的,但是我们一般写入都是按照字符串来写的,所以用下面的fputs函数

fputs:

int fputs( const char *s, FILE *fp );

**函数 fputs() 把字符串 s 写入到 fp 所指向的输出流中。**如果写入成功,它会返回一个非负值,如果发生错误,则会返回 EOF

fprintf:

int fprintf(FILE *fp,const char *format, ...) 
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
  • format – 这是 C 字符串,包含了要被写入到流 stream 中的文本。**它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。(就是类似printf函数中的嵌入一些变量的逻辑)**format 标签属性是 %[flags][width][.precision][length]specifier
specifier(说明符)输出
c字符
d 或 i有符号十进制整数
e使用 e 字符的科学科学记数法(尾数和指数)
E使用 E 字符的科学科学记数法(尾数和指数)
f十进制浮点数
g自动选择 %e 或 %f 中合适的表示法
G自动选择 %E 或 %f 中合适的表示法
o有符号八进制
s字符的字符串
u无符号十进制整数
x无符号十六进制整数
X无符号十六进制整数(大写字母)
p指针地址
n无输出
%字符
flags(标识)描述
-在给定的字段宽度内左对齐,默认是右对齐(参见 width 子说明符)。
+强制在结果之前显示加号或减号(+ 或 -),即正数前面会显示 + 号。默认情况下,只有负数前面会显示一个 - 号。
(space)如果没有写入任何符号,则在该值前面插入一个空格。
#与 o、x 或 X 说明符一起使用时,非零值前面会分别显示 0、0x 或 0X。与 e、E 和 f 一起使用时,会强制输出包含一个小数点,即使后边没有数字时也会显示小数点。默认情况下,如果后边没有数字时候,不会显示显示小数点。与 g 或 G 一起使用时,结果与使用 e 或 E 时相同,但是尾部的零不会被移除。
0在指定填充 padding 的数字左边放置零(0),而不是空格(参见 width 子说明符)。
width(宽度)描述
(number)要输出的字符的最小数目。如果输出的值短于该数,结果会用空格填充。如果输出的值长于该数,结果不会被截断。
*宽度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。
.precision(精度)描述
.number对于整数说明符(d、i、o、u、x、X):precision 指定了要写入的数字的最小位数。如果写入的值短于该数,结果会用前导零来填充。如果写入的值长于该数,结果不会被截断。精度为 0 意味着不写入任何字符。对于 e、E 和 f 说明符:要在小数点后输出的小数位数。对于 g 和 G 说明符:要输出的最大有效位数。对于 s: 要输出的最大字符数。默认情况下,所有字符都会被输出,直到遇到末尾的空字符。对于 c 类型:没有任何影响。当未指定任何精度时,默认为 1。如果指定时不带有一个显式值,则假定为 0。
.*精度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。
length(长度)描述
h参数被解释为短整型或无符号短整型(仅适用于整数说明符:i、d、o、u、x 和 X)。
l参数被解释为长整型或无符号长整型,适用于整数说明符(i、d、o、u、x 和 X)及说明符 c(表示一个宽字符)和 s(表示宽字符字符串)。
L参数被解释为长双精度型(仅适用于浮点数说明符:e、E、f、g 和 G)。
  • 附加参数 – 根据不同的 format 字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。参数的个数应与 % 标签的个数相同。

读取文件内容

fgetc:

下面是从文件读取单个字符的最简单的函数:

int fgetc( FILE * fp );

fgetc() 函数从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回 EOF

fgets:

下面的函数允许您从流中读取一个字符串:

char *fgets( char *buf, int n, FILE *fp );

函数 fgets() 从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串。

fscanf:

int fscanf(FILE *stream, const char *format, ...)
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
  • format – 这是 C 字符串,包含了以下各项中的一个或多个:空格字符、非空格字符format 说明符。format 说明符形式为 [=%[*][width][modifiers]type=]
参数描述
*这是一个可选的星号,表示数据是从流 stream 中读取的,但是可以被忽视,即它不存储在对应的参数中。
width这指定了在当前读取操作中读取的最大字符数。
modifiers为对应的附加参数所指向的数据指定一个不同于整型(针对 d、i 和 n)、无符号整型(针对 o、u 和 x)或浮点型(针对 e、f 和 g)的大小: h :短整型(针对 d、i 和 n),或无符号短整型(针对 o、u 和 x) l :长整型(针对 d、i 和 n),或无符号长整型(针对 o、u 和 x),或双精度型(针对 e、f 和 g) L :长双精度型(针对 e、f 和 g)
type一个字符,指定了要被读取的数据类型以及数据读取方式。具体参见下一个表格。

fscanf 类型说明符:

类型合格的输入参数的类型
c单个字符:读取下一个字符。如果指定了一个不为 1 的宽度 width,函数会读取 width 个字符,并通过参数传递,把它们存储在数组中连续位置。在末尾不会追加空字符。char *
d十进制整数:数字前面的 + 或 - 号是可选的。int *
e,E,f,g,G浮点数:包含了一个小数点、一个可选的前置符号 + 或 -、一个可选的后置字符 e 或 E,以及一个十进制数字。两个有效的实例 -732.103 和 7.12e4float *
o八进制整数。int *
s字符串。这将读取连续字符,直到遇到一个空格字符(空格字符可以是空白、换行和制表符)。char *
u无符号的十进制整数。unsigned int *
x,X十六进制整数。int *
  • 附加参数 – 根据不同的 format 字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。参数的个数应与 % 标签的个数相同。
#include <stdio.h>

int main() {
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) {
        printf("Failed to open file\n");
        return 1;
    }

    int id;
    char name[50];
    float score;

    int result = fscanf(fp, "%d %s %f", &id, name, &score);

    if (result == 3) {
        printf("Read: ID=%d, Name=%s, Score=%.2f\n", id, name, score);
    } else if (result == EOF) {
        printf("Reached end of file or error occurred.\n");
    } else {
        printf("Failed to read all fields. Read %d out of 3.\n", result);
    }

    fclose(fp);
    return 0;
}
//用来处理123 John 78.5的数据的

文件遍历

#include <stdio.h>

int main() {
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) {
        printf("Failed to open file\n");
        return 1;
    }

    int num;
    while (fscanf(fp, "%d", &num) == 1) {
        printf("Read number: %d\n", num);
    }

    fclose(fp);
    return 0;
}
//这里利用了返回值来进行判断,因为读取文件的时候fscanf返回EOF就是文件截至

二进制文件读写

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
  • ptr – 这是指向带有最小尺寸 sizenmemb* 字节的内存块的指针。
  • size – 这是要读取的每个元素的大小,以字节为单位。
  • nmemb – 这是元素的个数,每个元素的大小为 size 字节。
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流。

成功读取的元素总数会以 size_t 对象返回,size_t 对象是一个整型数据类型。如果总数与 nmemb 参数不同,则可能发生了一个错误或者到达了文件末尾。

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
  • ptr – 这是指向要被写入的元素数组的指针。
  • size – 这是要被写入的每个元素的大小,以字节为单位。
  • nmemb – 这是元素的个数,每个元素的大小为 size 字节。
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。

如果成功,该函数返回一个 size_t 对象,表示元素的总数,该对象是一个整型数据类型。如果该数字与 nmemb 参数不同,则会显示一个错误。

内存管理

C 语言为内存的分配和管理提供了几个函数。这些函数可以在 <stdlib.h> 头文件中找到。在 C 语言中,内存是通过指针变量来管理的。指针是一个变量,它存储了一个内存地址,这个内存地址可以指向任何数据类型的变量,包括整数、浮点数、字符和数组等。C 语言提供了一些函数和运算符,使得程序员可以对内存进行操作,包括分配、释放、移动和复制等。

作用函数和描述
开内存同时进行初始化**void calloc(int num, int size);**在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 numsize 个字节长度的内存空间,并且每个字节的值都是 0。
释放指针块内的内容**void free(void *address);**该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。
堆中开内存**void *malloc(int num);**在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。
重新分配内存(先开辟空间再复制原内容)**void *realloc(void *address, int newsize);**该函数重新分配内存,把内存扩展到 newsize

***注意:*void * 类型表示未确定类型的指针。C、C++ 规定 void * 类型可以通过类型转换强制转换为任何其它类型的指针。

C 语言中常用的内存管理函数和运算符:

  • malloc() 函数:用于动态分配内存。它接受一个参数,即需要分配的内存大小(以字节为单位),并返回一个指向分配内存的指针。
  • free() 函数:用于释放先前分配的内存。它接受一个指向要释放内存的指针作为参数,并将该内存标记为未使用状态。
  • calloc() 函数:用于动态分配内存,并将其初始化为零。它接受两个参数,即需要分配的内存块数和每个内存块的大小(以字节为单位),并返回一个指向分配内存的指针。
  • realloc() 函数:用于重新分配内存。它接受两个参数,即一个先前分配的指针和一个新的内存大小,然后尝试重新调整先前分配的内存块的大小。如果调整成功,它将返回一个指向重新分配内存的指针,否则返回一个空指针。
  • sizeof 运算符:用于获取数据类型或变量的大小(以字节为单位)。
  • 指针运算符:用于获取指针所指向的内存地址或变量的值。
  • & 运算符:用于获取变量的内存地址。
  • 运算符:用于获取指针所指向的变量的值。
  • > 运算符:用于指针访问结构体成员,语法为 pointer->member,等价于 *(pointer).member
  • memcpy() 函数:用于从源内存区域复制数据到目标内存区域。它接受三个参数,即目标内存区域的指针、源内存区域的指针和要复制的数据大小(以字节为单位)。
  • memmove() 函数:类似于 memcpy() 函数,但它可以处理重叠的内存区域。它接受三个参数,即目标内存区域的指针、源内存区域的指针和要复制的数据大小(以字节为单位)。

最下面这两个很少使用(对于C语言基础的人来说)

开辟空间

如果您预先不知道需要存储的文本长度,例如您想存储有关一个主题的详细描述。在这里,我们需要定义一个指针,该指针指向未定义所需内存大小的字符,后续再根据需求来分配内存

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main()
{
   char name[100];
   char *description;
 
   strcpy(name, "Zara Ali");
 
   /* 动态分配内存 */
   description = (char *)malloc( 200 * sizeof(char) );
   //开辟的单位是字节,这里用sizeof来进行一定程度上的代码简化
   if( description == NULL )
   {
      fprintf(stderr, "Error - unable to allocate required memory\n");
   }
   else
   {
      strcpy( description, "Zara ali a DPS student in class 10th");
   }
   printf("Name = %s\n", name );
   printf("Description: %s\n", description );
}

上面的程序也可以使用 calloc() 来编写,只需要把 malloc 替换为 calloc 即可。当动态分配内存时,您有完全控制权,可以传递任何大小的值。而那些预先定义了大小的数组,一旦定义则无法改变大小。

调整空间大小与释放空间

您可以通过调用函数 realloc() 来增加或减少已分配的内存块的大小。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main()
{
   char name[100];
   char *description;
 
   strcpy(name, "Zara Ali");
 
   /* 动态分配内存 */
   description = (char *)malloc( 30 * sizeof(char) );
   if( description == NULL )
   {
      fprintf(stderr, "Error - unable to allocate required memory\n");
   }
   else
   {
      strcpy( description, "Zara ali a DPS student.");
   }
   /* 假设您想要存储更大的描述信息 */
   description = (char *) realloc( description, 100 * sizeof(char) );
   if( description == NULL )
   {
      fprintf(stderr, "Error - unable to allocate required memory\n");
   }
   else
   {
      strcat( description, "She is in class 10th");
   }
   
   printf("Name = %s\n", name );
   printf("Description: %s\n", description );
 
   /* 使用 free() 函数释放内存 */
   free(description);
}

当程序退出时,操作系统会自动释放所有分配给程序的内存,但是,建议您在不需要内存时,都应该调用函数 free() 来释放内存。

C语言常见的未定义行为

这个是bug和出现反常问题的主要来源——我们认为他会怎么做,但实际上C标准没有定义这种行为导致其不能按照我们想当然的去执行

在 C 语言中,**“undefined behavior”(未定义行为)**是指程序的行为在 C 语言标准中没有明确定义,因此可以表现为任何结果。这意味着当程序出现未定义行为时,它可能会产生不可预测的结果,包括程序崩溃、数据损坏、安全漏洞,甚至可能看起来正常运行。未定义行为是C语言中一个重要的概念,因为它涉及到程序的正确性和安全性。会导致出现很奇怪的现象(为什么C环境不同很容易结果大相径庭):

  • 同一段代码在不同编译器上可能表现不同;
  • 同一编译器不同版本也可能有不同行为;
  • 优化级别不同可能导致行为变化;
  • 最糟糕的是:你无法预测其后果

C 设计哲学强调性能和灵活性 ,为了不强制加上运行时检查(如边界检查、类型安全等),将很多责任交给了程序员。因此:

  • 编译器不做额外检查;
  • 允许进行底层操作(如指针运算);
  • 给编译器优化留下空间;
  • 导致某些非法操作不会立刻报错。

常见的未定义行为

下面有十条常见的UB情况

1. 越界访问数组

int arr[5] = {0};
arr[10] = 42;  // 未定义行为!

2. 解引用空指针或野指针

int *p = NULL;
int x = *p;  // 未定义行为!

3. 整数溢出(对于带符号整数)

int a = INT_MAX;
a++;  // 超过最大值 → 未定义行为

4. 使用未初始化的变量

int x;
printf("%d\\n", x);  // 未定义行为!

5. 多次释放同一块内存

int *p = malloc(10);
free(p);
free(p);  // 双重释放 → 未定义行为

6. 类型转换后调用函数指针

void (*fp)() = (void(*)())main;
fp();  // 调用非函数指针 → 未定义行为

7. 修改字符串字面量

char *str = "hello";
str[0] = 'H';  // 修改常量字符串 → 未定义行为

8. longjmp 到已返回的函数

jmp_buf env;
void f() {
    if (setjmp(env) == 0)
        longjmp(env, 1);  // 非法跳转到已退出函数 → 未定义行为
}

9. 违反严格别名规则(Strict Aliasing)

int x = 10;
float *p = (float*)&x;  // 不同类型指针访问相同内存 → UB

10. 移位操作超出位宽

unsigned int x = 1;
x << 32;  // 假设 int 是 32 位 → 移位超出范围 → UB

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值