C 初级学习笔记(基础)

目录

1.预处理器指令

预定义宏

预处理器运算符 ()

参数化的宏

头文件 .h

引用头文件操作

2.函数(标识符&关键字&运算符)存储类

函数参数

a. 标识符&关键字

b. 运算符(算术、关系、逻辑、位、赋值、杂项/其它)

算术运算符

关系运算符

逻辑运算符

位运算符

赋值运算符

 杂项/其它运算符(sizeof & 三元)

扩展:运算符优先级

c. 存储类

1、auto 存储类(所有局部变量默认的存储类)

2、register 存储类(频繁访问)

3、static 存储类(一次初始化多次使用)

4、extern 存储类(全局引用声明)

d. 输入 & 输出

1.打开文件

2.关闭文件

3.写入文件

4.读取文件

e. 递归(函数的 if 套娃)

3.变量&常量(数据的缓冲区)

强制类型转换

变量定义和声明

变量初始化(为了避免不确定的行为/错误)

C 中的左值(Lvalues)和右值(Rvalues)

常量(程序运行期不变的值)

数组

声明数组

访问数组元素

enum(枚举)

指针*(通常说的是指向整型、字符型及数组等变量)

null 指针

函数指针(指向函数的指针变量)

字符串(使用空字符 结尾的一维字符数组)

结构体

访问结构成员

指向结构的指针

结构体大小的计算

共用体

访问共用体成员

位域(特殊的结构体成员)

为类型取个新名字 —— typedef

可变参数

4.语句&表达式

A.判断

判断语句

三元运算符

B.循环

循环类型

循环控制语句

无限循环

C.错误处理

errno、perror() 和 strerror()

程序退出状态

5.注释&空格及补充(都会被编译器给忽略掉)

作用域规则

局部变量

全局变量

形式参数

初始化局部变量和全局变量

内存管理

动态分配内存

重新调整内存大小和释放内存

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

命令行参数


C 程序结构,主要分为以下部分:

1.预处理器指令

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

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

#include 用来引入头文件

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

#define 预先定义

#define PI 3.14

常看下表:

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

预定义宏

描述
__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")

参数化的宏

CPP 一个强大的功能是可以使用参数化的宏来模拟函数

int square(int x) {
   return x * x;
}
//下面用宏重写了上面的代码
#define square(x) ((x) * (x))

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

头文件 .h

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

1.引用用户多用
#include <file>
2.引用系统多用
#include "file"

引用头文件操作

#include 指令会指示 C 预处理器浏览指定的文件作为输入。预处理器的输出包含了已经生成的输出,被引用文件生成的输出以及 #include 指令之后的文本输出

(注意!只引用一次头文件)

#ifndef HEADER_FILE//包装器已经定义,之后编译器会忽略它的再次定义 等于跳过
#define HEADER_FILE//再次定义不会报错

the entire header file file

#endif//跳过!

有条件引用实例:

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

 #define SYSTEM_H "system_1.h"
 #define SYSTEM_H "system_2.h"
 ...
 #include SYSTEM_H

2.函数(标识符&关键字&运算符)存储类

每个 C 语言程序都需要包含 main() 函数,main() 函数是程序运行的入口。

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

函数参数

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

a. 标识符&关键字

标识符:

C 标识符是用来标识变量、函数,或任何其他用户自定义项目的名称。

一个标识符可由字母或下划线 _ 及数字组成(但不能以数字开头),而且是大小写敏感的,

C 标识符内不允许出现标点字符,比如 @、$ 和 % 等等

长图警告!

关键字:

关键字 说明
auto 声明自动变量
break 跳出当前循环
case 开关语句分支
char 声明字符型变量或函数返回值类型
const 定义常量,如果一个变量被 const 修饰,那么它的值就不能再被改变
continue 结束当前循环,开始下一轮循环
default 开关语句中的"其它"分支
do 循环语句的循环体
double 声明双精度浮点型变量或函数返回值类型
else 条件语句否定分支(与 if 连用)
enum 声明枚举类型
extern 声明变量或函数是在其它文件或本文件的其他位置定义
float 声明浮点型变量或函数返回值类型
for 一种循环语句
goto 无条件跳转语句
if 条件语句
int 声明整型变量或函数
long 声明长整型变量或函数返回值类型
register 声明寄存器变量
return 子程序返回语句(可以带参数,也可不带参数)
short 声明短整型变量或函数
signed 声明有符号类型变量或函数
sizeof 计算数据类型或变量长度(即所占字节数)
static 声明静态变量
struct 声明结构体类型
switch 用于开关语句
typedef 用以给数据类型取别名
unsigned 声明无符号类型变量或函数
union 声明共用体类型
void 声明函数无返回值或无参数,声明无类型指针
volatile 说明变量在程序执行中可被隐含地改变
while 循环语句的循环条件

b. 运算符(算术、关系、逻辑、位、赋值、杂项/其它)

运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C 语言内置了丰富的运算符

算术运算符

运算符 描述 实例
+ 把两个操作数相加 A + B 将得到 30
- 从第一个操作数中减去第二个操作数 A - B 将得到 -10
* 把两个操作数相乘 A * B 将得到 200
/ 分子除以分母 B / A 将得到 2
% 取模运算符,整除后的余数 B % A 将得到 0
++ 自增运算符,整数值增加 1 A++ 将得到 11
-- 自减运算符,整数值减少 1 A-- 将得到 9

关系运算符

运算符 描述 实例
== 检查两个操作数的值是否相等,如果相等则条件为真。 (A == B) 为假。
!= 检查两个操作数的值是否相等,如果不相等则条件为真。 (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

赋值运算符

运算符 描述 实例
= 简单的赋值运算符,把右边操作数的值赋给左边操作数 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
|= 按位或且赋值运算符 C |= 2 等同于 C = C | 2

 杂项/其它运算符(sizeof & 三元)

运算符 描述 实例
sizeof() 返回变量的大小。 sizeof(a) 将返回 4,其中 a 是整数。
& 返回变量的地址。 &a; 将给出变量的实际地址。
* 指向一个变量。 *a; 将指向一个变量。
? : 条件表达式 如果条件为真 ? 则值为 X : 否则值为 Y

扩展:运算符优先级

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

c. 存储类

1、auto 存储类(所有局部变量默认的存储类)

auto 只能用在函数内,即 auto 只能修饰局部变量

2、register 存储类(频繁访问)

register 用于存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个字),且不能对它应用一元的 '&' 运算符(因为它没有内存位置)。(因为存储在寄存器中,所以访问速度更快。但由于存储在 RAM 中,故不能直接取地址)

3、static 存储类(一次初始化多次使用)

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

4、extern 存储类(全局引用声明)

extern 存储类用于定义在其他文件中声明的全局变量或函数。当使用 extern 关键字时,不会为变量分配任何存储空间,而只是指示编译器该变量在其他文件中定义。

extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当您使用 extern 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。

当您有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 extern 来得到已定义的变量或函数的引用。可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。

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

d. 输入 & 输出

标准文件:C 语言把所有的设备都当作文件,以下三个文件会在程序执行时自动打开,以便访问键盘和屏幕

标准文件 文件指针 设备
标准输入 stdin 键盘
标准输出 stdout 屏幕
标准错误 stderr 您的屏幕

(而常用的 I/O (输入/输出) 通常使用 printf() 和 scanf() 两个函数)

常用 IO 函数
读取/输入 I 写出/输出 O

int getchar(void)

函数从屏幕读取下一个可用的字符,并把它返回为一个整数

int putchar(int c)

函数把字符输出到屏幕上,并返回相同的字符

char *gets(char *s)

函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF

int puts(const char *s)

函数把字符串 s 和一个尾随的换行符写入到 stdout

int scanf(const char *format, ...)

函数从标准输入流 stdin 读取输入,并根据提供的 format 来浏览输入

(另外,在读取字符串时,只要遇到一个空格,scanf() 就会停止读取,所以 "this is test" 对 scanf() 来说是三个字符串)

int printf(const char *format, ...)

函数把输出写入到标准输出流 stdout ,并根据提供的格式产生输出

1.打开文件

FILE *fopen( const char *filename, const char *mode );
在这里,filename 是字符串,用来命名文件,访问模式 mode 的值可以是下列值中的一个:
模式 描述
r 打开一个已有的文本文件,允许读取文件。
w 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。
a 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。
r+ 打开一个文本文件,允许读写文件。
w+ 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。
a+ 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。

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

"rb","wb","ab","rb+","r+b","wb+","w+b","ab+","a+b"

2.关闭文件

 int fclose( FILE *fp );

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

C 标准库提供了各种函数来按字符或者以固定长度字符串的形式读写文件。

3.写入文件

int fputc( int c, FILE *fp );

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

int fputs( const char *s, FILE *fp );//把以 null 结尾的字符串写入到流中

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

#include <stdio.h>
 
int main()
{
   FILE *fp = NULL;//创建文件指针变量
 
   fp = fopen("/tmp/test.txt", "w+");//设置文件路径和格式(请确保文件已经建立)
   fprintf(fp, "This is testing for fprintf...n");//打印入文件中
   fputs("This is testing for fputs...n", fp);//写入文件中
   fclose(fp);//关闭文件
}

4.读取文件

从文件读取单个字符的最简单的函数

int fgetc( FILE * fp );

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

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

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

如果这个函数在读取最后一个字符之前就遇到一个换行符 'n' 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。您也可以使用 int fscanf(FILE *fp, const char *format, ...) 函数来从文件中读取字符串,但是在遇到第一个空格和换行符时,它会停止读取。

fgetc() fgets()
读取一个字符,遇到空格及换行符或文件末尾 EOF 为止 读取一行字符

二进制 IO 函数

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);

e. 递归(函数的 if 套娃)

C 语言支持递归,即一个函数可以调用其自身。但在使用递归时,程序员需要注意定义一个从函数退出的条件,否则会进入死循环。

递归函数在解决许多数学问题上起了至关重要的作用,比如计算一个数的阶乘、生成斐波那契数列,等等。

3.变量&常量(数据的缓冲区)

序号 类型与描述
1

基本数据类型
它们是算术类型,包括整型(int)、字符型(char)、浮点型(float)和双精度浮点型(double)

2

枚举类型
它们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值的变量

3

void 类型
类型说明符 void 表示没有值的数据类型,通常用于函数返回值

4 派生类型:
包括数组类型、指针类型和结构体类型

sizeof 运算符可以获取某对象或类型的存储字节大小

printf() 输出格式
格式字符 意义
a, A

以十六进制形式输出浮点数(C99 新增)。

实例 printf("pi=%an", 3.14);         输出 pi=0x1.91eb86p+1

d 以十进制形式输出带符号整数(正数不输出符号)
zu 输出 size_t 型(不能输出负数)
o 以八进制形式输出无符号整数(不输出前缀0)
x, X 以十六进制形式输出无符号整数(不输出前缀Ox)
u 以十进制形式输出无符号整数
f 以小数形式输出单、双精度实数
e, E 以指数形式输出单、双精度实数
g, G 以%f或%e中较短的输出宽度输出单、双精度实数
c 输出单个字符
s 输出字符串
p 输出指针地址
lu 32位无符号整数
llu 64位无符号整数

强制类型转换

强制类型转换是把变量从一种类型转换为另一种数据类型

类型转换:

  • 隐式类型转换:隐式类型转换是在表达式中自动发生的,无需进行任何明确的指令或函数调用。它通常是将一种较小的类型自动转换为较大的类型,例如,将int类型转换为long类型或float类型转换为double类型。隐式类型转换也可能会导致数据精度丢失或数据截断。(整数/浮点数提升)

  • 显式类型转换:显式类型转换需要使用强制类型转换运算符(type casting operator),它可以将一个数据类型的值强制转换为另一种数据类型的值。强制类型转换可以使程序员在必要时对数据类型进行更精确的控制,但也可能会导致数据丢失或截断。

(要注意的是强制类型转换运算符的优先级大于除法)

基本数据类型
类型 描述
char 通常是一个字节(八位), 这是一个整数类型
int 整型,4 个字节,取值范围 -2147483648 到 2147483647
size_t 无符号整型,即在库中定义为 unsigned int
float

单精度浮点值。单精度是这样的格式,1位符号,8位指数,23位小数

double

双精度浮点值。双精度是1位符号,11位指数,52位小数。

void 表示类型的缺失

变量定义和声明

变量定义和声明的区别:

  • 前者是需要建立存储空间的。例如:int a 在声明的时候就已经建立了存储空间。
  • 后者是不需要建立存储空间的,通过使用 extern 关键字声明变量名却不定义它。 例如:extern int a 其中变量 a 可以在别的文件中定义的,相当于外部变量(全局变量)

变量初始化(为了避免不确定的行为/错误)

  • 显式初始化:用 = 来赋值
  • 隐式初始化:系统默认为数据赋值

注意!非静态的局部变量是不会隐式初始化的!

总结:C 语言中变量的默认值取决于其类型和作用域。全局变量和静态变量的默认值为 0,字符型变量的默认值为 ,指针变量的默认值为 NULL,而局部变量没有默认值,其初始值是未定义的。

C 中的左值(Lvalues)和右值(Rvalues)

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

常量(程序运行期不变的值)

定义常量:

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

数组

C 语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。

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

C 语言还允许我们使用指针来处理数组,这使得对数组的操作更加灵活和高效。

声明数组

type arrayName [ arraySize ];

访问数组元素

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

double salary = balance[9];

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

int numbers[] = {1, 2, 3, 4, 5};
int length = sizeof(numbers) / sizeof(numbers[0]);

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

需要注意的是,虽然数组名表示数组的地址,但在大多数情况下,数组名会自动转换为指向数组首元素的指针。这意味着我们可以直接将数组名用于指针运算,例如在函数传递参数或遍历数组时。

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

enum(枚举)

枚举是 C 语言中的一种基本数据类型,用于定义一组具有离散值的常量。,它可以让数据更简洁,更易读。

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

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

枚举语法定义格式为:

enum 枚举名 {枚举元素1,枚举元素2,……};

在C 语言中,枚举类型是被当做 int 或者 unsigned int 类型来处理的,所以按照 C 语言规范是没有办法遍历枚举类型的。

不过在一些特殊的情况下,枚举类型必须连续是可以实现有条件的遍历。

以下实例使用 for 来遍历枚举的元素:

 enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
int main()
{
    // 遍历枚举元素
    for (day = MON; day <= SUN; day++) {
        printf("枚举元素:%d n", day);
    }
}

//以下枚举类型不连续,则无法遍历
enum
{
    ENUM_0,//默认从0开始
    ENUM_10 = 10,
    ENUM_11
};

指针*(通常说的是指向整型、字符型及数组等变量)

每一个变量都有一个内存位置,每一个内存位置都定义了可使用 & 运算符访问的地址,它表示了在内存中的一个地址。

type *var_name;
//例子如下:
int    *ip;    /* 一个整型的指针 */
double *dp;    /* 一个 double 型的指针 */
float  *fp;    /* 一个浮点型的指针 */
char   *ch;    /* 一个字符型的指针 */

null 指针

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

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

函数指针(指向函数的指针变量)

typedef int (*fun_ptr)(int,int); 
//声明一个指向同样参数、返回值的函数指针类型

函数指针变量是可以作为一个函数的参数,把通过函数指针来调用函数叫做回调函数。

字符串(使用空字符 结尾的一维字符数组)

so 是用于标记字符串的结束符,空字符(Null character)又称结束符,缩写 NUL,是一个数值为 0 的控制字符, 是转义字符,意思是告诉编译器,这不是字符 0,而是空字符哦!

char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', ''};
//等同于下面这个
char site[] = "RUNOOB";

其实我们不需要把 null 字符放在字符串常量的末尾。C 编译器会在初始化数组时,自动把 放在字符串的末尾以表示此字符串已经结束了。

操作字符串的函数
序号 函数 & 目的
1 strcpy(s1, s2);
复制字符串 s2 到字符串 s1
2 strcat(s1, s2);
连接字符串 s2 到字符串 s1 的末尾
3 strlen(s1);
返回字符串 s1 的长度
4 strcmp(s1, s2);
如果 s1 和 s2 是相同的,则返回 0;如果 s1<s2 则返回小于 0;如果 s1>s2 则返回大于 0
5 strchr(s1, ch);
返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置
6 strstr(s1, s2);
返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置

结构体

C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。类似 Java 的集合

struct tag { 
    member-list
    member-list 
    member-list  
    ...
} variable-list ;
//一般情况下 tag、member-list、variable-list 这三部分至少要写出两个

访问结构成员

为了访问结构的成员,我们使用成员访问运算符(.)。成员访问运算符是结构变量名称和我们要访问的结构成员之间的一个句号“.”

指向结构的指针

struct Books *struct_pointer;
struct Books Book1;
struct_pointer = &Book1;

结构指针要访问结构成员必须使用 -> 运算符,如下:

struct_pointer -> title;

结构体大小的计算

C 语言中,我们可以使用 sizeof 运算符来计算结构体的大小,sizeof 返回的是给定类型或变量的字节大小。

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

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

共用体

可以使用 union 关键字来定义共用体类型的变量

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

(共用体占用的内存应足够存储共用体中最大的成员)

访问共用体成员

成员访问运算符是共用体变量名称和我们要访问的共用体成员之间的一个“.”

同一时刻只能使用一个数据成员

位域(特殊的结构体成员)

位域允许我们按位对成员进行定义,即指定其占用的位数。例子如下:

struct
{
  unsigned int widthValidated : 1;
  unsigned int heightValidated : 1;
} status;

(位域的访问是通过点运算符(.)来实现的,与普通的结构体成员访问方式相同。八位一字节)

分解到小为 char 类型

结构体以 4 bytes 为位域存储单位

位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:

struct k{
    int a:1;
    int  :2;    /* 该 2 位不能使用 */
    int b:3;
    int c:2;
};

位域的使用和结构成员的使用相同,其一般形式为:

  • 位域变量名.位域名
  • 位域变量名->位域名

(位域允许用各种格式输出。)

为类型取个新名字 —— typedef

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

可变参数

希望函数带有可变数量的参数,而不是预定义数量的参数。

C 语言为这种情况提供了一个解决方案,它允许您定义一个函数,能根据具体的需求接受可变数量的参数。

int func_name(int arg1, ...);

请注意,函数 func() 最后一个参数写成省略号,即三个点号(...),省略号之前的那个参数是 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

4.语句&表达式

程序运行逻辑

; 是语句结束符,它表明一个逻辑实体的结束

A.判断

判断结构要求程序员指定一个或多个要评估或测试的条件,以及条件为真时要执行的语句(必需的)和条件为假时要执行的语句(可选的)。

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

(上面是大多数编程语言中典型的判断结构的一般形式)

判断语句

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

三元运算符

条件运算符 ? :,可以用来替代 if...else 语句。它的一般形式如下:

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

B.循环

我们可能需要多次执行同一块代码。一般情况下,语句是按顺序执行的:函数中的第一个语句先执行,接着是第二个语句,依此类推。循环语句允许我们多次执行一个语句或语句组,下面是大多数编程语言中循环语句的流程图:

循环类型

循环类型 描述
while 循环 当给定条件为真时,重复语句或语句组。它会在执行循环主体之前测试条件。
for 循环 多次执行一个语句序列,简化管理循环变量的代码。
do...while 循环 除了它是在循环主体结尾测试条件外,其他与 while 语句类似。
嵌套循环 您可以在 while、for 或 do..while 循环内使用一个或多个循环。

更多内容:C while 和 do while 区别

循环控制语句

循环控制语句改变你代码的执行顺序。通过它你可以实现代码的跳转。

C 提供了下列的循环控制语句。点击链接查看每个语句的细节。

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

无限循环

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

(注意:您可以按 Ctrl + C 键终止一个无限循环)

C.错误处理

C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据

(注意!在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误状态代码。)

errno、perror() 和 strerror()

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

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

被 0 除的错误(在进行除法运算时,如果不检查除数是否为零,则会导致一个运行时错误)

程序退出状态

通常情况下,程序成功执行完一个操作正常退出的时候会带有值 EXIT_SUCCESS。在这里,EXIT_SUCCESS 是宏,它被定义为 0。

如果程序中存在一种错误情况,当您退出程序时,会带有状态值 EXIT_FAILURE,被定义为 -1。

5.注释&空格及补充(都会被编译器给忽略掉)

  • //我是单行注释
  • /*我是双行注释*/

作用域规则

任何一种编程中,作用域是程序中定义的变量所存在的区域,超过该区域变量就不能被访问。C 语言中有三个地方可以声明变量:

  1. 在函数或块内部的局部变量
  2. 在所有函数外部的全局变量
  3. 形式参数的函数参数定义中

局部变量

在某个函数或块的内部声明的变量称为局部变量。它们只能被该函数或该代码块内部的语句使用。局部变量在函数外部是不可知的

全局变量

全局变量是定义在函数外部,通常是在程序的顶部。全局变量在整个程序生命周期内都是有效的,在任意的函数内部能访问全局变量。

全局变量可以被任何函数访问。也就是说,全局变量在声明后整个程序中都是可用的

形式参数

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

初始化局部变量和全局变量

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

内存管理

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

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

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

动态分配内存

#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) );
   if( description == NULL )
   {
      fprintf(stderr, "Error - unable to allocate required memoryn");
   }
   else
   {
      strcpy( description, "Zara ali a DPS student in class 10th");
   }
   printf("Name = %sn", name );
   printf("Description: %sn", description );
}

重新调整内存大小和释放内存

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

或者,您可以通过调用函数 realloc() 来增加或减少已分配的内存块的大小。让我们使用 realloc() 和 free() 函数,再次查看上面的实例:

#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 memoryn");
   }
   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 memoryn");
   }
   else
   {
      strcat( description, "She is in class 10th");
   }
   
   printf("Name = %sn", name );
   printf("Description: %sn", description );
 
   /* 使用 free() 函数释放内存 */
   free(description);
}

(您可以尝试一下不重新分配额外的内存,strcat() 函数会生成一个错误,因为存储 description 时可用的内存不足。)

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

  • malloc() 函数:用于动态分配内存。它接受一个参数,即需要分配的内存大小(以字节为单位),并返回一个指向分配内存的指针

  • free() 函数:用于释放先前分配的内存。它接受一个指向要释放内存的指针作为参数,并将该内存标记为未使用状态

  • calloc() 函数:用于动态分配内存,并将其初始化为零。它接受两个参数,即需要分配的内存块数和每个内存块的大小(以字节为单位),并返回一个指向分配内存的指针

  • realloc() 函数:用于重新分配内存。它接受两个参数,即一个先前分配的指针和一个新的内存大小,然后尝试重新调整先前分配的内存块的大小。如果调整成功,它将返回一个指向重新分配内存的指针,否则返回一个空指针

  • sizeof 运算符:用于获取数据类型或变量的大小(以字节为单位)

  • 指针运算符:用于获取指针所指向的内存地址或变量的值

  • & 运算符:用于获取变量的内存地址

  • * 运算符:用于获取指针所指向的变量的值

  • -> 运算符:用于指针访问结构体成员,语法为 pointer->member,等价于 (*pointer).member

  • memcpy() 函数:用于从源内存区域复制数据到目标内存区域。它接受三个参数,即目标内存区域的指针、源内存区域的指针和要复制的数据大小(以字节为单位)

  • memmove() 函数:类似于 memcpy() 函数,但它可以处理重叠的内存区域。它接受三个参数,即目标内存区域的指针、源内存区域的指针和要复制的数据大小(以字节为单位)

命令行参数

执行程序时,可以从命令行传值给 C 程序。这些值被称为命令行参数,它们对程序很重要,特别是当您想从外部控制程序,而不是在代码内对这些值进行硬编码时,就显得尤为重要了。

命令行参数是使用 main() 函数参数来处理的,其中,argc 是指传入参数的个数,argv[] 是一个指针数组,指向传递给程序的每个参数。下面是一个简单的实例,检查命令行是否有提供参数,并根据参数执行相应的动作:

#include <stdio.h>

int main( int argc, char *argv[] )  
{
   if( argc == 2 )
   {
      printf("The argument supplied is %sn", argv[1]);
   }
   else if( argc > 2 )
   {
      printf("Too many arguments supplied.n");
   }
   else
   {
      printf("One argument expected.n");
   }
}