本来是想搞JNI的,但是需要一定的C/C++基础,索性就重温一下,此篇文章是结合菜鸟教程加上网上资料和一些自己的理解整合而成,以便后续查阅,篇幅巨长,做好心理准备 :)

1. 程序结构

#include <stdio.h>
/**
 * C 语言入口程序
 * @return
 */
int main() {//主函数,程序从这里开始执行
    printf("C 语言入门第一行代码 Hello World! \n");
    return 0;
}

#include <stdio.h> 相当于导包,std: 标准 ,io:输入输出
printf() 打印函数
return 0 终止main函数

2. 基本语法

关键字说明
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循环语句的循环条件

关键字大多与Java相似,只需查看背景色标注的特殊关键字即可!

3.数据类型

3.1 基本类型与Java比较

booleanbytecharshortintlongdoublefloatvoidsignedunsigned
Java11224884
C124484有符号无符号

C语言类型种类:

类型说明
基本类型算术类型,包括两种类型:整数类型和浮点类型。
枚举类型算术类型,被用来定义在程序中只能赋予其一定的离散整数值得变量。
void 类型类型说明符 void 表名没有可用的值
派生类型它们包括:指针类型、数组类型、结构类型、共用体类型和函数类型。

3.1整数类型

类型32 位64 位值范围
char11-128 到 127 或 0 到 255
unsigned char110 到 255
int44-32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647
unsigned int440 到 65,535 或 0 到 4,294,967,295
short22-32,768 到 32,767
unsigned short220 到 65,535
long48-2,147,483,648 到 2,147,483,647
unsigned long480 到 4,294,967,295

注意: 各种类型的存储大小与系统位数有关,但目前通用的以 64 为系统为主。

3.2浮点类型

类型比特(位)数有效数字取值范围
float46~71.2E-38 到 3.4E+38
double815~162.3E-308 到 1.7E+308
long double1618~193.4E-4932 到 1.1E+4932

他们的字节,精度,取值范围都可以通过代码打印实现,如下:

//char 1 字节
    printf("char 存储大小: %lu \n", sizeof(char));
    printf("unsinged char 存储大小: %lu \n", sizeof(unsigned char));

控制符

格式控制符说明
%c读取一个单一的字符
%hd、%d、%ld读取一个十进制整数,并分别赋值给 short、int、long 类型
%ho、%o、%lo读取一个八进制整数(可带前缀也可不带),并分别赋值给 short、int、long 类型
%hx、%x、%lx读取一个十六进制整数(可带前缀也可不带),并分别赋值给 short、int、long 类型
%hu、%u、%lu读取一个无符号整数,并分别赋值给 unsigned short、unsigned int、unsigned long 类型
%f、%lf读取一个十进制形式的小数,并分别赋值给 float、double 类型
%e、%le读取一个指数形式的小数,并分别赋值给 float、double 类型
%g、%lg既可以读取一个十进制形式的小数,也可以读取一个指数形式的小数,并分别赋值给 float、double 类型
%s读取一个字符串(以空白符为结束)
  • %c - char
  • %hd - 短整型,h - half
  • %d - int
  • %ld - long int
  • %f - float,黑t认输出6位于数,可通过%.3f这样的方式指定要输入的小数为3位,则第4位四舍五入
  • %lf - double,t认输出6位小数,可通过%.3f这样的方式指定要输入的小数为3位,则第4位四舍五入
  • %x - 十六进制输出int或者long int或者short int
  • %#x - 十六进制输出,#的作用就是在16进制数的前面加上“0x”
  • %o - 八进制输出
  • %#o - 八进制输出,#的作用就是在8进制数的前面加上“0”
  • %s - 字符串

4.变量

变量的名称可以由字母、数字和下划线字符组成。它必须以字母或下划线开头。大写字母和小写字母是不同的,因为 C 是大小写敏感的。

示例:

int age;//整型变量
float salary;//浮点型变量
char grade;//字符型变量
int *ptr;//指针变量
int i, j, k;//定义多个变量

与Java声明方式差不多,多了个指针变量

变量初始化 和 声明出来后续初始化 和Java类似,不再写了,主要记下 外部变量

//声明外部变量
extern int d;
extern int f;

在C语言中,使用 extern 关键字可以声明外部变量。外部变量是指在一个源文件中定义(即分配存储空间),但在另一个源文件中引用(即使用)的变量。通常情况下,如果一个变量在一个源文件中被定义,而在另一个源文件中需要使用它,就需要用到 extern 来声明该变量,告诉编译器该变量实际上是在其他地方定义的。

一般情况下,外部变量的定义通常会出现在程序的另一个源文件中,例如:

/* 文件 file1.c */
int d; // 定义外部变量d

/* 文件 file2.c */
extern int d; // 声明外部变量d,表示d是在其他地方定义的

int main() {
    // 使用外部变量d
    d = 10;
    return 0;
}

4.1 变量不初始化

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

void foo() {
    int x;  // 未初始化的局部变量
    // x 的初始值是未定义的,可能是任意值
    printf("%d\n", x);  // 可能输出任意值
}

全局变量静态变量

// 全局变量,默认初始化为0
int global_var;

void foo() {
    // 静态变量,默认初始化为0
    static int static_var;
    printf("%d\n", global_var);    // 输出0
    printf("%d\n", static_var);    // 输出0
}

注意:字符型变量(char):默认值为’\0’,即空字符。

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

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

int x = 10;       // 右边的 `10` 是一个右值
int sum = x + 5;  // `x + 5` 是一个右值

和Java一样


5.常量

5.1 整数常量

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

212         /* 合法的 */
215u        /* 合法的 */
0xFeeL      /* 合法的 */
078         /* 非法的:8 不是八进制的数字 */
032UU       /* 非法的:不能重复后缀 */

85         /* 十进制 */
0213       /* 八进制 */
0x4b       /* 十六进制 */
30         /* 整数 */
30u        /* 无符号整数 */
30l        /* 长整数 */
30ul       /* 无符号长整数 */

5.2 浮点常量

3.14159       /* 合法的 */
314159E-5L    /* 合法的 */
510E          /* 非法的:不完整的指数 */
210f          /* 非法的:没有小数或指数 */
.e55          /* 非法的:缺少整数或分数 */

5.3 字符常量

字符常量可以是一个普通的字符(例如 ‘x’)、一个转义序列(例如 ‘\t’),或一个通用的字符(例如 ‘\u02C0’)。
转义序列码:

转义序列含义
\\ 字符
’ 字符
"" 字符
?? 字符
\a警报铃声
\b退格键
\f换页符
\n换行符
\r回车
\t水平制表符
\v垂直制表符
\ooo一到三位的八进制数
\xhh . . .一个或多个数字的十六进制数

5.4 字符串常量

下面这三种形式所显示的字符串是相同的。

"hello, dear"

"hello, \

dear"

"hello, " "d" "ear"

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

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

5.5 定义常量

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

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

#define 预处理器

#define 常量名 常量值
#define PI 3.14159

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

示例:

#include <stdio.h>
 
#define LENGTH 10   
#define WIDTH  5
#define NEWLINE '\n'
 
int main()
{
 
   int area;  
  
   area = LENGTH * WIDTH;
   printf("value of area : %d", area);
   printf("%c", NEWLINE);
 
   return 0;
}

const 关键字

const 数据类型 常量名 = 常量值;
const int MAX_VALUE = 100;

相对于Java 就算前面加了个 const ,和Kotlin类似
示例:

#include <stdio.h>
 
int main()
{
   const int  LENGTH = 10;
   const int  WIDTH  = 5;
   const char NEWLINE = '\n';
   int area;  
   
   area = LENGTH * WIDTH;
   printf("value of area : %d", area);
   printf("%c", NEWLINE);
 
   return 0;
}

#defineconst 区别:

  1. 预处理 vs 编译时声明:
    • #define 是一个预处理指令,它在程序编译之前由预处理器处理。它简单地将符号常量替换为其定义的文本内容。
    • const 是一个关键字,用于声明一个常量变量。它在编译时会被编译器处理,分配相应的内存空间,并进行类型检查。
  2. 类型检查:
    • #define 不会进行任何类型检查,因为它只是简单的文本替换。这意味着,可以将任何类型的值赋给一个使用 #define 定义的符号常量。
    • const 声明的常量具有特定的数据类型,编译器会对其进行类型检查。这样可以提供更好的类型安全性,并避免意外的数据类型错误。
  3. 作用域和命名空间:
    • #define 定义的常量是全局的,它们在整个程序中都有效。因此,可能会引起命名冲突或者意外的文本替换。
    • const 声明的常量遵循普通变量的作用域规则,可以限定在某个函数内或者某个代码块内,不会污染全局命名空间。
  4. 内存分配:
    • #define 并不会在内存中分配存储空间,它只是简单的文本替换,因此不占用额外的内存。
    • const 声明的常量会在内存中分配存储空间,因为它是一个实际的变量。

总结来说, #define 适用于简单的符号常量定义和条件编译,而 const 更适合于类型安全和需要分配内存的常量定义。在实际编程中,推荐优先使用 const 来定义常量,除非需要使用 #define 进行文本替换或者条件编译。


6. 存储类

C语言中的存储类别(storage class)指定了变量或函数的存储方式、作用域和生命周期。主要的存储类别包括 autoregisterstaticextern

6.1 auto 存储类

  • auto 存储类是默认的存储类别,当没有显式地指定其他存储类别时,变量通常被视为 auto 类型。
  • auto 声明的变量会在定义所在的代码块内(即局部作用域)生效,在代码块结束时自动销毁。
void example_function() {
    auto int x = 10;  // auto 可以省略不写,默认就是 auto 类型
    // x 在此函数内有效,函数结束时销毁
}

相当于Java 中局部变量

6.2 register寄存器

  • register 存储类用于请求将变量存储在CPU的寄存器中,以便快速访问。
  • 注册变量仅能定义在函数内,并且不能使用 & 操作符获取其地址,因为寄存器变量并非一定会分配内存地址。
void example_function() {
    register int counter;  // 请求编译器将 counter 存储在寄存器中
    // 使用 counter 变量
}

寄存器只用于需要快速访问的变量,比如计数器。还应注意的是,定义 ‘register’ 并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。
注意事项:编译器有权忽略 register 关键字的请求,特别是在寄存器数量有限或者寄存器不足的情况下。

6.3 static 存储类

  • static 存储类可以用于变量和函数。
  • 对于变量, static 使变量在程序的整个生命周期内都存在,而不是在作用域结束时被销毁。
  • 对于函数, static 限制了函数的作用域只在当前文件内有效。
void example_function() {
    static int count = 0;  // static 变量只初始化一次,不会在每次函数调用时重新初始化
    count++;
}

static void helper_function() {
    // helper_function 只能在当前文件内部调用
}

注意事项

  • 如果 static 用于全局变量,那么该变量只能在当前文件内访问。
  • 在函数内部使用 static 变量时,该变量会在第一次调用时初始化,然后在函数调用之间保持其值。

6.4 extern 存储类

  • extern 存储类用于提供一个全局变量或函数的引用,声明它的存在而不进行定义。
  • 当使用 extern 关键字时,不会为变量分配任何存储空间,而只是指示编译器该变量在其他文件中定义。
  • 在多个文件共享变量或函数时非常有用,允许一个文件访问另一个文件中定义的全局变量或函数。

第一个文件:main.c

#include <stdio.h>
 
int count ;
extern void write_extern();
 
int main()
{
   count = 5;
   write_extern();
}

第二个文件:support.c

#include <stdio.h>
 
extern int count;
 
void write_extern(void)
{
   printf("count is %d\n", count);
}

执行打印

count is 5

7.运算符

包括算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符和其他运算符。
与Java基本相同,可速通,重点了解 **杂项运算符 **即可

7.1 算术运算符

与Java功能和优先级基本相似,但还是有细微区别的,这里不做解释,知道就行
假设变量 A 的值为 10,变量 B 的值为 20,则:

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

注意:在Java中,运算符的类型会根据操作数的类型进行自动推断,而C语言中则需要显式地进行类型转换以避免意外的结果或错误。

7.2 关系运算符

与Java优先级和用法上基本相同,在涉及浮点数比较和整数溢出时可能会有些微差异。
假设变量 A 的值为 10,变量 B 的值为 20,则:

运算符描述实例
==检查两个操作数的值是否相等,如果相等则条件为真。(A == B) 为假。
!=检查两个操作数的值是否相等,如果不相等则条件为真。(A != B) 为真。
>检查左操作数的值是否大于右操作数的值,如果是则条件为真。(A > B) 为假。
<检查左操作数的值是否小于右操作数的值,如果是则条件为真。(A < B) 为真。
>=检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。(A >= B) 为假。
<=检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。(A <= B) 为真。

7.3 逻辑运算符

与Java优先级和用法上基本相同,但Java具有短路求值(short-circuit evaluation)的特性,即如果第一个操作数已经决定了整个表达式的结果,就不会再对第二个操作数求值。这种行为在C语言中并不总是保证的,具体取决于编译器的实现和优化。
假设变量 A 的值为 1,变量 B 的值为 0,则:

运算符描述实例
&&称为逻辑运算符。如果两个操作数都非零,则条件为真。(A && B) 为假。
||称为逻辑运算符。如果两个操作数中有任意一个非零,则条件为真。(A || B) 为真。
!称为逻辑运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。!(A && B) 为真。

7.4 赋值运算符

与Java 相比基本一致,细微之处这里不做深究

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

7.5 杂项运算符 ↦ sizeof & 三元

!!!重点!!!

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

sizeof() 运算符

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

#include <stdio.h>

int main() {
    printf("Size of int: %zu bytes\n", sizeof(int));
    printf("Size of double: %zu bytes\n", sizeof(double));
    
    int arr[] = {1, 2, 3, 4, 5};
    printf("Size of arr[]: %zu bytes\n", sizeof(arr));
    
    return 0;
}

//输出
Size of int: 4 bytes
Size of double: 8 bytes
Size of arr[]: 20 bytes

& 和 * 运算符

  • & 运算符用于获取变量的地址。
  • \* 运算符用于通过指针访问地址中存储的值(解引用)。
#include <stdio.h>

int main() {
    int num = 10;
    int *ptr = &num; // 指针 ptr 存储了变量 num 的地址

    printf("Address of num: %p\n", &num);
    printf("Value of ptr: %p\n", ptr); // 打印指针 ptr 中存储的地址
    printf("Value at ptr: %d\n", *ptr); // 打印 ptr 指向的地址中的值

    return 0;
}

//输出
Address of num: 0x7ffee1ed4b3c
Value of ptr: 0x7ffee1ed4b3c
Value at ptr: 10

看不懂没关系,能看懂更好,还没到指针呢,有个概念就行

?: 运算符(条件运算符)

?: 是 C 语言中的条件运算符,也称为三元运算符,和Java一致,可跳过

#include <stdio.h>

int main() {
    int num = 10;
    int result;

    // 三元运算符示例
    result = (num > 5) ? 100 : 200;
    printf("Result 1: %d\n", result); // 因为 num 大于 5,所以 result 等于 100

    num = 3;
    result = (num > 5) ? 100 : 200;
    printf("Result 2: %d\n", result); // 因为 num 小于等于 5,所以 result 等于 200

    return 0;
}

//输出
Result 1: 100
Result 2: 200

7.6 C 中的运算符优先级

与Java基本一致,可略过
C 语言中的运算符优先级从高到低排列如下(同一级别内的运算符是从左到右计算的):

类别运算符结合性
一元运算符++, —(后缀)+, -(一元正负)!, ~(逻辑非、按位取反)*(指针解引用)&(取地址)从右向左结合
乘法和除法*, /, %左结合
加法和减法+, -左结合
移位运算<<, >>左结合
关系运算符<, <=, >, >=左结合
相等性运算符==, !=左结合
按位与运算AND&左结合
按位异或运算XOR^左结合
按位或运算^左结合
逻辑与运算&&左结合
逻辑或运算||左结合
条件运算符? :右结合
赋值运算符=, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=右结合
逗号运算符,左结合

左结合和右结合释义:
右结合
指的是运算符从右向左进行结合。这意味着如果一个表达式中有多个相同优先级的右结合运算符,先处理靠近表达式末尾的运算符。赋值运算符 ( =) 就是一个经典的右结合运算符,因为它会先计算右边的表达式,然后将结果赋值给左边的变量。

int a, b, c;
a = b = c = 10;

c = 10 先执行,然后 b = c,最后 a = b。因此, abc 都被赋值为 10
左结合
指的是运算符从左向右进行结合。大多数运算符都是左结合的,这意味着如果一个表达式中有多个相同优先级的左结合运算符,先处理靠近表达式开头的运算符。

int result = 10 + 20 - 5;

10 + 20 先计算,然后再减去 5。因此, result 的值为 25

8.判断

8.1 判断语句

同Java

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

8.2 ? : 运算符(三元运算符)

同Java

Exp1 ? Exp2 : Exp3;

9. 循环

9.1 循环类型

同Java

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

9.2 循环控制语句

C语言中的 goto 类似于Java中 break 标签; 很少用!

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

goto 语句使用示例:

#include <stdio.h>

int main() {
    int i = 0;

loop:  // 定义标签
    printf("%d ", i);
    i++;

    if (i < 10)
        goto loop;  // 转到标签处

    return 0;
}

//输出
定义了一个名为loop的标签,然后在循环体中使用了 goto 语句来无条件地跳转到这个标签处。程序输出将是:0 1 2 3 4 5 6 7 8 9。

9.3 无限循环

同Java

#include <stdio.h>
 
int main ()
{
   for( ; ; )
   {
      printf("该循环会永远执行下去!\n");
   }
   return 0;
}

10. 函数

10.1 定义函数

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

一个标准的C语言函数定义包含以下几个部分:

  1. 返回类型 (Return Type): 函数可以返回一个值,返回类型指定了这个返回值的数据类型。如果函数不返回任何值,则返回类型为 void
  2. 函数名 (Function Name): 函数名是程序中调用函数的唯一标识符,它应该符合C语言的命名规范。
  3. 参数列表 (Parameter List): 参数列表包含函数需要的输入参数,每个参数由其数据类型和参数名组成。参数列表可以为空。
  4. 函数体 (Function Body): 函数体包含了函数的具体实现,即函数执行的代码块。
// 函数定义
int add(int a, int b) {
    int sum = a + b;
    return sum;
}

10.2 函数声明

return_type function_name( parameter list );
// 函数声明
int add(int a, int b);

//在函数声明中,参数的名称并不重要,只有参数的类型是必需的,因此下面也是有效的声明:

int add(int, int);

int add(int a, int b); 是函数 add 的声明,它告诉编译器有一个名为 add 的函数,它接受两个整数参数 ab,返回一个整数。
注意事项

  • 函数必须先声明后定义,或者直接定义。声明告诉编译器有这个函数的存在,定义提供了函数的具体实现。
  • 函数定义的顺序通常是在 main 函数之前,因为 main 函数是程序的入口。
  • 在C语言中,函数的返回值和参数类型都必须在函数声明和定义中明确指定,C语言不支持函数重载。

10.3 调用函数

int result = add(3, 5);

既然函数声明不是必须的,那么它的应用场景有哪些呢?
1.函数定义在调用之后: 如果函数定义在主函数 main() 之后,而主函数或其他函数需要调用该函数,则需要在调用函数之前进行函数声明。这样编译器在编译过程中就能够知道函数的存在和其特征(返回类型及参数类型),以便正确生成调用代码。

// 函数声明
int add(int a, int b);

int main() {
    int result = add(3, 5);
    printf("Result: %d\n", result);
    return 0;
}

// 函数定义
int add(int a, int b) {
    return a + b;
}

2.函数需要被多个文件使用: 当函数定义在一个源文件中,而其他源文件也需要调用该函数时,可以将函数声明放在头文件(.h 文件)中,然后在各个源文件中包含这个头文件。这样可以确保所有源文件都能访问到函数声明,从而进行正确的函数调用。
add.h:

// 头文件中的函数声明
int add(int a, int b);

main.c:

#include "add.h"

int main() {
    int result = add(3, 5);
    printf("Result: %d\n", result);
    return 0;
}

add.c:

#include "add.h"

// 函数定义
int add(int a, int b) {
    return a + b;
}

3.函数实现分离: 在大型程序中,为了提高代码的模块化和可维护性,经常会将函数的声明和实现分开。这样做可以让每个模块(文件)只关注自己需要使用的函数接口,而不必关心函数的具体实现细节。
4.函数重载: 虽然C语言本身不支持函数重载,但通过函数声明可以实现类似的效果,即在不同的文件或不同的作用域中定义具有相同名称但不同参数列表的函数,以实现多态的效果。

10.4 函数参数

这点和Java类似

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

请看 VCR:

传值调用

void changeValue(int x) {//已在内存中给x开辟了新的空间
    x = 100; // 修改形参 x 的值
}

int main() {
    int num = 50;
    changeValue(num);
    printf("Value after function call: %d\n", num); // 输出:50,因为传值调用不会改变原始值
    return 0;
}

引用调用

void changeValue(int *ptr) {
    *ptr = 100; // 通过指针修改实参的值
}

int main() {
    int num = 50;
    changeValue(&num); // 传递 num 的地址
    printf("Value after function call: %d\n", num); // 输出:100,因为引用调用修改了原始值
    return 0;
}

看见 & 变量就是获取地址的,\* 就是一级指针 ,两颗星就是二级指针,以此类推,一般不超过三级,这里先有个概念


11. 作用域规则

11.1 局部变量

在某个函数或块的内部声明的变量称为局部变量。

#include <stdio.h>
 
int main ()
{
  /* 局部变量声明 */
  int a, b;
  int c;
 
  /* 实际初始化 */
  a = 10;
  b = 20;
  c = a + b;
 
  printf ("value of a = %d, b = %d and c = %d\n", a, b, c);
 
  return 0;
}

11.2 全局变量

全局变量是定义在所有函数外部的变量,它们的作用域从定义处开始,直到文件结束。

#include <stdio.h>

int globalVar = 100; // 全局变量

void func() {
    printf("Global variable globalVar: %d\n", globalVar); // 可以访问全局变量
}

int main() {
    printf("Global variable globalVar: %d\n", globalVar); // 可以访问全局变量
    func();
    return 0;
}

11.3 形式参数

形式参数是函数定义中声明的参数,它们的作用域仅限于函数内部。

void func(int param) { // param 是形式参数
    printf("Formal parameter param: %d\n", param);
}

int main() {
    int num = 50;
    func(num); // 将 num 作为实际参数传递给 func() 函数
    return 0;
}

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

在C语言中,局部变量和全局变量可以在声明时进行初始化。未初始化的全局变量和静态存储期的局部变量会被编译器初始化为0,当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。

#include <stdio.h>

int globalVar1 = 100; // 全局变量的初始化
int globalVar2; // 未显式初始化的全局变量会被初始化为0

void func() {
    int localVar = 50; // 局部变量的初始化
    printf("Local variable localVar: %d\n", localVar);
}

int main() {
    printf("Global variables: %d and %d\n", globalVar1, globalVar2);
    func();
    return 0;
}
数据类型初始化默认值
int0
char‘\0’
float0
double0
pointerNULL

12. 数组

C 语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。
所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。

12.1 声明数组

type arrayName [ arraySize ];

double balance[10];

注意:[] 要写在 变量名称后面!

12.2 初始化数组

逐个初始化数组

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

12.3 访问数组元素

double salary = balance[9];

12.4 获取数组长度

在C语言中,数组一旦被声明,其长度在编译时就已经确定,因此没有内建的方法可以直接获取数组的长度。

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

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

12.5 数组名

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

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

在这里,myArray 是数组名,它表示整数类型的数组,包含 5 个元素。myArray 也代表着数组的地址,即第一个元素的地址。
数组名本身是一个常量指针,意味着它的值是不能被改变的,一旦确定,就不能再指向其他地方。

使用&运算符来获取数组的地址

int myArray[5] = {10, 20, 30, 40, 50};
int *ptr = &myArray[0]; // 或者直接写作 int *ptr = myArray;

12.6 C 中数组详解

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

12.7 C语言数组与Java数组的比较

特征C语言数组Java数组
声明和定义int numbers[10];int[] numbers = new int[10];
长度获取没有内建方法获取数组长度,通常使用sizeof计算。使用 array.length 属性获取数组长度。
动态分配不支持动态数组,长度在编译时确定。支持动态数组,可以使用ArrayList等类实现动态长度。
多维数组int matrix[3][3];int[][] matrix = new int[3][3];
数组名的特性数组名即指向数组首元素的指针,可以作为函数参数传递。数组是对象,数组变量持有数组对象的引用。
元素访问使用下标访问,下标从0开始。使用下标访问,下标从0开始。
越界访问可能导致未定义行为或者崩溃。抛出ArrayIndexOutOfBoundsException。
初始化int numbers[] = {1, 2, 3, 4, 5};int[] numbers = {1, 2, 3, 4, 5};
参数传递数组作为指针传递给函数,不会传递数组长度。数组作为对象引用传递给函数,可以使用 array.length 获取长度。
动态内存管理使用malloc()和free()实现动态数组。自动垃圾回收器处理内存管理,无需手动分配或释放。

13. enum(枚举)

每个枚举常量可以用一个标识符来表示,也可以为它们指定一个整数值,如果没有指定,那么默认从 0 开始递增。
定义格式为:

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

13.1 枚举变量的定义

三种方式来定义枚举变量
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;

示例:

#include <stdio.h>
 
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};
 
int main()
{
    enum DAY day;
    day = WED;
    printf("%d",day);
    return 0;
}

//输出
3

使用 for 来遍历枚举的元素:

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

//输出
枚举元素:1 
枚举元素:2 
枚举元素:3 
枚举元素:4 
枚举元素:5 
枚举元素:6 
枚举元素:7

以下枚举类型不连续,这种枚举无法遍历。

enum
{
    ENUM_0,
    ENUM_10 = 10,
    ENUM_11
};

枚举在 switch 中的使用:

#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;
}

//输出
请输入你喜欢的颜色: (1. red, 2. green, 3. blue): 1
你喜欢的颜色是红色

13.2 将整数转换为枚举

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
 
    enum day
    {
        saturday,
        sunday,
        monday,
        tuesday,
        wednesday,
        thursday,
        friday
    } workday;
 
    int a = 1;
    enum day weekend;
    weekend = ( enum day ) a;  //类型转换
    //weekend = a; //错误
    printf("weekend:%d",weekend);
    return 0;
}

//输出
weekend:1

14. 指针

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

#include <stdio.h>
 
int main ()
{
    int var_runoob = 10;
    int *p;              // 定义指针变量
    p = &var_runoob;
 
   printf("var_runoob 变量的地址: %p\n", p);
   return 0;
}

//输出
var_runoob 变量的地址: 0x7ffeeaae08d8

注意:int \*p; 代表 int 类型指针,因为 var\_runoob是int类型,而不是指针的值只有四个字节(int 四个字节)
在这里插入图片描述

14.1 什么是指针?

type *var_name;

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

int    *ip;    /* 一个整型的指针 */
double *dp;    /* 一个 double 型的指针 */
float  *fp;    /* 一个浮点型的指针 */
char   *ch;    /* 一个字符型的指针 */

不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。

14.2 如何使用指针?

使用指针时会频繁进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符\*来返回位于操作数所指定地址的变量的值。

#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;
}

//输出
var 变量的地址: 0x7ffeeef168d8
ip 变量存储的地址: 0x7ffeeef168d8
*ip 变量的值: 20

14.3 C 中的 NULL 指针

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

#include <stdio.h>
 
int main ()
{
   int  *ptr = NULL;
 
   printf("ptr 的地址是 %p\n", ptr  );
 
   return 0;
}

//输出
ptr 的地址是 0x0

如需检查一个空指针,您可以使用 if 语句,如下所示:

if(ptr)     /* 如果 p 非空,则完成 */
if(!ptr)    /* 如果 p 为空,则完成 */

注意:当指针被定义出来,而不进行赋值时,此时它是一个野指针,可能指向任意一块地址,如果直接使用 程序会报错!

14.4 C 指针详解

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

简记:指针代表某个变量或称为一块内存的首地址,通过 地址+1 等算数运算可以对其连续内存进行访问。


15. 函数指针与回调函数

15.1 函数指针

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

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

其中 第一个int是返回类型,(\*fun\_ptr) 是函数指针,(int,int) 是函数的参数

  1. 定义函数指针:函数指针的定义类似于函数声明,但加上了 \*。例如, int (\*func\_ptr)(int, int); 定义了一个指向返回 int 类型并接受两个 int 参数的函数的指针。
  2. 初始化:可以将函数指针初始化为函数名。比如, func\_ptr = my\_function;,这里 my\_function 是一个函数的名字。
  3. 调用函数:可以通过函数指针调用函数,如 result = (\*func\_ptr)(arg1, arg2);result = func\_ptr(arg1, arg2);

实例

以下实例声明了函数指针变量 p,指向函数 max:

#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;
}

//输出
请输入三个数字:1 2 3
最大的数字是: 3

15.2 回调函数

以下是来自知乎作者常溪玲的解说:
  
  你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。

示例

定义回调函数

#include <stdio.h>

// 回调函数的签名
typedef int (*CallbackFunction)(int, int);

// 回调函数的实现
int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}

传递回调函数

// 接受函数指针的函数
int compute(int x, int y, CallbackFunction callback) {
    return callback(x, y);
}

int main() {
    int result;

    // 使用 add 作为回调函数
    result = compute(3, 4, add);
    printf("Addition result: %d\n", result);

    // 使用 multiply 作为回调函数
    result = compute(3, 4, multiply);
    printf("Multiplication result: %d\n", result);

    return 0;
}

有点像Kotlin的函数类型参数,不能这样说 毕竟C先出来的,Kotlin也算是集百家之长,互相类比可以更好理解语言特性。


16. 字符串

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

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

等同于

char site[] = "RUNOOB";

在这里插入图片描述

操作字符串的函数:

序号函数 & 目的
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 的第一次出现的位置。

17. 结构体

Java 中的类(类似于 C 语言的结构体)
结构体是C语言中的一种用户定义的数据类型,用于将不同类型的数据组合成一个单一的单元。结构体的定义使用 struct 关键字
结构体中的数据成员可以是基本数据类型(如 intfloatchar 等),也可以是其他结构体类型、指针类型等。

17.1 定义结构

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

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

struct Person {
    char name[50];
    int age;
    float height;
};

17.2 声明结构体变量

struct Person person1; // 声明 person1 变量
struct Person person2 = {"Alice", 30, 5.5}; // 声明并初始化 person2 变量

17.3 访问结构体成员

使用点操作符( .)来访问结构体成员:

struct Student s1; // s1 是结构体Student的别名
s1.age = 20;
strcpy(s1.name, "Alice");//将 Alice 赋值给s1.name
s1.gpa = 3.5;

17.4 使用指针访问结构体成员

如果有一个指向结构体的指针,可以使用箭头操作符( ->)来访问成员:

struct Person *ptr = &person1;
ptr->age = 26;
printf("Updated Age: %d\n", ptr->age);

在后续的JNI开发中,会经常看到 ->操作符

17.5 在函数中使用结构体

结构体可以作为函数参数传递。传递结构体时有两种方式:按值传递和按引用传递(即传递指针)。

// 按值传递
void printPerson(struct Person p) {
    printf("Name: %s, Age: %d, Height: %.1f\n", p.name, p.age, p.height);
}

// 按引用传递(通过指针)
void updatePerson(struct Person *p) {
    p->age += 1; // 增加年龄
}

int main() {
    struct Person person = {"Charlie", 28, 5.9};
    printPerson(person); // 按值传递
    updatePerson(&person); // 按引用传递
    printPerson(person); // 查看更新后的结果
    return 0;
}

17.6 嵌套结构体

结构体可以包含其他结构体,称为嵌套结构体:

struct Address {
    char street[100];
    char city[50];
    int zipcode;
};

struct Employee {
    char name[50];
    int id;
    struct Address address; // 嵌套 Address 结构体
};

int main() {
    struct Employee emp = {"David", 1001, {"123 Main St", "Metropolis", 12345}};
    printf("Employee Name: %s\n", emp.name);
    printf("Employee Address: %s, %s, %d\n", emp.address.street, emp.address.city, emp.address.zipcode);
    return 0;
}

17.8 结构体数组

可以定义结构体数组来存储多个结构体实例:

struct Person people[3] = {
    {"Emma", 22, 5.5},
    {"Liam", 30, 6.1},
    {"Olivia", 28, 5.7}
};

for (int i = 0; i < 3; ++i) {
    printf("Name: %s, Age: %d, Height: %.1f\n", people[i].name, people[i].age, people[i].height);
}

17.9 匿名结构体

在某些情况下,可以使用匿名结构体:

struct {
    char name[50];
    int age;
    float height;
} person3 = {"Sophia", 24, 5.8};

printf("Name: %s, Age: %d, Height: %.1f\n", person3.name, person3.age, person3.height);

17.10 结构体与内存对齐

由于内存对齐的原因,结构体的大小可能比所有成员大小之和要大。编译器会在需要时插入填充字节(padding)来确保结构体成员在内存中的对齐。

struct Example {
    char a;
    int b;
    char c;
};

printf("Size of Example: %lu\n", sizeof(struct Example)); // 可能输出 12 而不是 6

内存对齐要求:为了提高访问效率,许多系统要求数据按特定边界对齐。比如, int 通常需要按 4 字节对齐。这意味着结构体中的 int 成员可能需要在内存中从一个 4 字节对齐的地址开始,这会导致内存中的填充字节(padding)。
想要深入了解的,可以单独查阅相关资料,此处你知道就行了,也可能是一个面试点哦!


18. 共用体

共用体(Union)是C语言中一种特殊的数据结构,它允许在相同的内存位置存储不同类型的数据。共用体的所有成员共享同一个内存空间,因此在同一时刻只能存储一个成员的值。共用体的大小等于其最长成员的大小。

18.1 定义共用体

共用体的定义与结构体类似,使用关键字 union

#include <stdio.h>

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

18.2 声明和初始化共用体变量

int main() {
    union Data data;

    data.i = 10;
    printf("data.i: %d\n", data.i);

    data.f = 220.5;
    printf("data.f: %f\n", data.f);

    strcpy(data.str, "C Programming");
    printf("data.str: %s\n", data.str);

    return 0;
}

需要注意的是,由于共享同一块内存,赋值给一个成员会影响到其他成员。例如,在给 data.f 赋值后, data.i 将不再有效。

18.3 共用体的大小

共用体的大小等于其最长成员的大小。可以使用 sizeof 运算符来确定共用体的大小:

int main() {
    union Data data;
    printf("Size of union: %lu\n", sizeof(data));
    return 0;
}

使用共用体实现多态行为

#include <stdio.h>

enum DataType {
    INT,
    FLOAT,
    STRING
};

struct Container {
    enum DataType type;
    union {
        int i;
        float f;
        char str[20];
    } data;
};

void printData(struct Container container) {
    switch (container.type) {
        case INT:
            printf("Integer: %d\n", container.data.i);
            break;
        case FLOAT:
            printf("Float: %f\n", container.data.f);
            break;
        case STRING:
            printf("String: %s\n", container.data.str);
            break;
    }
}

int main() {
    struct Container container;

    container.type = INT;
    container.data.i = 10;
    printData(container);

    container.type = FLOAT;
    container.data.f = 220.5;
    printData(container);

    container.type = STRING;
    strcpy(container.data.str, "C Programming");
    printData(container);

    return 0;
}

19. 位域

在C语言中,位域(bit fields)是一种用于在结构体(struct)中定义和操作特定位数的成员的方式。位域主要用于节省内存空间,并且在需要直接操控硬件寄存器或者进行网络协议数据处理时非常有用。

19.1 定义与语法

struct {
    unsigned int field1 : N;
    unsigned int field2 : M;
    ...
};
  • unsigned int:通常使用 unsigned int 来定义位域的类型,但也可以使用 signed int 或者其他整型类型。
  • field1:位域成员的名称。
  • Nfield1 占用的位数。必须为非负整数。

19.2 示例

#include <stdio.h>

struct {
    unsigned int a : 3;  // 占用3个位
    unsigned int b : 5;  // 占用5个位
    unsigned int c : 6;  // 占用6个位
} bitFields;

int main() {
    bitFields.a = 5;  // 5的二进制表示为101,占3位
    bitFields.b = 25; // 25的二进制表示为11001,占5位
    bitFields.c = 45; // 45的二进制表示为101101,占6位

    printf("a: %u\n", bitFields.a);
    printf("b: %u\n", bitFields.b);
    printf("c: %u\n", bitFields.c);

    return 0;
}

位域的特点和使用方法如下:
内存对齐
位域的成员是按照其声明顺序存储的,可能会涉及到内存对齐。位域的存储顺序和位的排列可能会因不同的编译器和体系结构而有所不同。通常情况下,位域成员按照它们在结构体中的声明顺序紧凑地存储在一起,但具体的存储细节可能会有所不同。
位域的类型
位域成员通常使用 unsigned int 类型,尽管也可以使用其他整数类型(如 signed int),但不同的编译器可能对带符号位域有不同的处理方式。

cstruct {
    signed int flag1 : 1;  // 1 bit (signed)
    unsigned int flag2 : 3;  // 3 bits (unsigned)
} bitfield;

位域的大小限制
位域成员的位数不能超过其基础类型的位数。对于 unsigned int 类型,通常有 32 位,位域成员的位数必须小于或等于 32。


20. typedef

typedef 是 C 语言中的一个关键字,用于创建数据类型的别名,使代码更易读和维护。

typedef unsigned long ulong;
ulong a, b;

其中,ulongunsigned long 的别名

#define 是预处理指令,用于定义宏,可以是常量、表达式或代码片段。

#define PI 3.14
#define MAX(a, b) ((a) > (b) ? (a) : (b))

#define 在编译前会将所有的 PI 替换成 3.14MAX(a, b) 替换成 (a) > (b) ? (a) : (b)

区别在于:

  1. typedef 定义的是新的类型别名,而 #define 定义的是简单的文本替换或宏。
  2. typedef 在编译阶段处理, #define 在预处理阶段处理。
  3. typedef 保持类型安全和作用域,而 #define 可能导致宏展开的问题,比如缺少括号导致的优先级错误。

21. 输入 & 输出 (了解)

在 C 语言中,输入和输出(I/O)操作是程序与用户或其他程序进行交互的主要方式。C 语言提供了一组标准库函数来处理这些操作,这些函数定义在 <stdio.h> 头文件中

21.1 标准文件

C 语言把所有的设备都当作文件。所以设备(比如显示器)被处理的方式与文件相同。以下三个文件会在程序执行时自动打开,以便访问键盘和屏幕。

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

21.2 getchar() 和 putchar()

**getchar()**:

  • 功能:从标准输入(通常是键盘)读取下一个字符。
  • 返回值:返回读取的字符(类型为 int),如果遇到文件结束符(EOF)则返回 EOF
  • 用法:常用于从输入中逐个字符读取。

**putchar()**:

  • 功能:将一个字符写入到标准输出(通常是屏幕)。
  • 参数:要写入的字符(类型为 int)。
  • 返回值:成功则返回写入的字符,失败则返回 EOF
  • 用法:常用于逐个字符输出。
#include <stdio.h>

int main() {
    char c;

    // 从标准输入读取一个字符
    c = getchar();
    
    // 将读取的字符输出到标准输出
    putchar(c);

    return 0;
}

21.3 gets() 和 puts()

**gets()** (注意:这个函数已经被弃用,不建议使用):

  • 功能:从标准输入读取一行字符串(包括空格),直到遇到换行符。
  • 返回值:返回读取的字符串指针,失败则返回 NULL
  • 注意gets() 不会检查缓冲区溢出风险,容易导致安全问题。

**puts()**:

  • 功能:将一个字符串输出到标准输出,并在末尾自动添加换行符。
  • 参数:要输出的字符串。
  • 返回值:成功则返回非负值,失败则返回 EOF
#include <stdio.h>

int main() {
    char str[100];

    // 从标准输入读取一行字符串
    fgets(str, sizeof(str), stdin);  // 使用 fgets 替代 gets,防止缓冲区溢出
    
    // 将读取的字符串输出到标准输出
    puts(str);

    return 0;
}

21.4 scanf() 和 printf()

**scanf()**:

  • 功能:从标准输入读取格式化数据。
  • 参数:格式化字符串和可选的变量地址。
  • 返回值:成功读取的项目数,遇到错误或到达文件末尾则返回 EOF

**printf()**:

  • 功能:将格式化数据输出到标准输出。
  • 参数:格式化字符串和可选的数据。
  • 返回值:成功输出的字符数,失败则返回负值。
#include <stdio.h>

int main() {
    int num;
    char str[100];

    // 从标准输入读取一个整数和一行字符串
    printf("Enter an integer: ");
    scanf("%d", &num);  // 读取整数

    printf("Enter a string: ");
    scanf("%s", str);   // 读取字符串(不包括空格)

    // 将读取的数据输出到标准输出
    printf("You entered integer: %d\n", num);
    printf("You entered string: %s\n", str);

    return 0;
}

22. 文件读写 (了解)

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

22.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"

**fopen()**

  • 功能:打开一个文件并返回一个文件指针。
  • 参数:文件名和模式(如 "r", "w", "a", "rb", "wb" 等)。
  • 返回值:成功时返回文件指针,失败时返回 NULL

22.2 关闭文件

使用 fclose 函数关闭文件。成功关闭文件时返回 0,否则返回 EOF。

int fclose(FILE *stream);
  • stream:需要关闭的文件指针。

22.3 写入文件

使用 fprintf 函数写入文本数据

int fprintf(FILE *stream, const char *format, ...);
  • stream:文件指针。
  • format:格式字符串,与 printf 类似。
FILE *fp = fopen("example.txt", "w");
fprintf(fp, "Hello, world!\n");
fclose(fp);

使用 fwrite 函数写入二进制数据

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
  • ptr:指向要写入数据的指针。
  • size:每个元素的大小(以字节为单位)。
  • count:要写入的元素个数。
  • stream:文件指针。
FILE *fp = fopen("example.bin", "wb");
int data[5] = {1, 2, 3, 4, 5};
fwrite(data, sizeof(int), 5, fp);
fclose(fp);

22.4 读取文件

使用 fscanf 函数读取文本数据

int fscanf(FILE *stream, const char *format, ...);
  • stream:文件指针。
  • format:格式字符串,与 scanf 类似。
FILE *fp = fopen("example.txt", "r");
char buffer[100];
fscanf(fp, "%s", buffer);
printf("%s\n", buffer);
fclose(fp);

使用 fread 函数读取二进制数据

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
  • ptr:指向存储读取数据的缓冲区。
  • size:每个元素的大小(以字节为单位)。
  • count:要读取的元素个数。
  • stream:文件指针。
FILE *fp = fopen("example.bin", "rb");
int data[5];
fread(data, sizeof(int), 5, fp);
for(int i = 0; i < 5; i++) {
    printf("%d ", data[i]);
}
fclose(fp);

22.5 二进制 I/O 函数

fseek 和 ftell 函数

用来在文件中设置和获取位置:

int fseek(FILE *stream, long int offset, int origin);
long int ftell(FILE *stream);
  • stream:文件指针。
  • offset:相对于 origin 的偏移量。
  • origin:文件开始位置( SEEK\_SET)、当前位置( SEEK\_CUR)或文件末尾位置( SEEK\_END)。
FILE *fp = fopen("example.bin", "rb");
fseek(fp, 0, SEEK_END);
long filesize = ftell(fp);
fseek(fp, 0, SEEK_SET); // 返回文件开头
printf("File size: %ld bytes\n", filesize);
fclose(fp);

fflush 函数

将缓冲区中的数据强制写入文件:

int fflush(FILE *stream);
  • stream:文件指针。传入 NULL 时会刷新所有打开的输出流。
FILE *fp = fopen("example.txt", "w");
fprintf(fp, "Hello, world!\n");
fflush(fp); // 保证数据立即写入文件
fclose(fp);

23. 预处理器(了解)

C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。
所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。

宏定义是一个非常有用的功能,最近Flutter的Dart语言 也开始支持了宏定义,大致看了一下,和C语言的也差不多,毕竟好用的东西总是抄来抄去。

  1. 宏替换:通过 #define 定义宏,预处理器在编译前替换源代码中的宏标识符。
  2. 条件编译:使用 #if#ifdef#ifndef#else#elif#endif 控制哪些代码片段被编译,常用于跨平台开发和调试。
  3. 文件包含:通过 #include 指令插入其他文件的内容,支持代码的模块化和重用。
  4. 宏定义:允许定义函数式宏(如 #define MAX(a, b) ((a) > (b) ? (a) : (b))),用于简化代码和提高可读性。
  5. 错误和警告:使用 #error#warning 生成编译时错误和警告信息,帮助调试和开发。
指令描述
#define定义宏
#include包含一个源代码文件
#undef取消已定义的宏
#ifdef如果宏已经定义,则返回真
#ifndef如果宏没有定义,则返回真
#if如果给定条件为真,则编译下面代码
#else#if 的替代方案
#elif如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif结束一个 #if……#else 条件编译块
#error当遇到标准错误时,输出错误消息
#pragma使用标准化方法,向编译器发布特殊的命令到编译器中

23.1 预处理器实例

#define MAX_ARRAY_LENGTH 20

通俗讲就是:定义 MAX\_ARRAY\_LENGTH 就是20

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

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

#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,您可以在编译期间随时开启或关闭调试。

23.2 预定义宏

C语言中的预定义宏是由编译器自动定义的宏,用于提供编译环境的关键信息。

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

这些宏在调试和日志记录中非常有用,可以帮助开发者了解代码的编译环境和位置。

23.3 预处理器运算符

C语言的预处理器运算符用于在预处理阶段处理宏和条件编译。

  1. **#**(字符串化运算符):将宏参数转换为字符串。例如, #define STRINGIFY(x) #xSTRINGIFY(Hello) 变为 "Hello"
  2. **##**(标记粘贴运算符):将两个宏参数拼接成一个标记。例如, #define CONCAT(a, b) a##bCONCAT(Hello, World) 变为 HelloWorld
  3. **#include**:用于包含其他文件的内容到当前文件中,可以是系统文件或自定义文件。
  4. **#define**:定义宏,可以是常量、表达式或函数式宏。
  5. **#ifdef****#ifndef**:条件编译指令,用于检查宏是否已定义( #ifdef)或未定义( #ifndef)。
  6. **#if****#elif****#else****#endif**:用于根据条件编译不同的代码块。

23.4 参数化的宏

C语言的参数化宏是指那些接受参数的宏定义,使得宏在使用时能够根据传入的不同参数生成不同的代码。定义参数化宏的基本语法是:

#define MACRO_NAME(parameters) replacement_code
#define SQUARE(x) ((x) * (x))

SQUARE 是一个宏, x 是参数, ((x) \* (x)) 是宏展开后的代码。使用这个宏时,编译器会将 SQUARE(5) 替换为 ((5) \* (5))


24. 头文件

在 C 语言中,头文件(header files)是一个重要的组成部分,通常以 .h 为扩展名。它们用于声明函数、定义常量、数据结构以及进行条件编译等。这种组织方式使得代码更具模块化和可维护性。

24.1 主要功能

  1. 函数声明:头文件中通常包含函数的原型,使得编译器能够识别函数的返回类型和参数类型。
  2. 宏定义:可以定义常量和宏,使用 #define 指令,便于在多个源文件中共享。
  3. 数据结构:可以定义结构体、联合体和枚举类型,方便在多个文件中使用。
  4. 条件编译:通过 #ifndef#define#endif 指令,防止同一头文件被多次包含,从而避免重复定义。

24.2 使用方法

  1. 包含头文件:通过 #include 指令将头文件包含到源文件中:
    • 标准库头文件:使用尖括号,例如 #include <stdio.h>
    • 自定义头文件:使用双引号,例如 #include "myheader.h"
  2. 标准库头文件:C 语言提供了许多标准库头文件,如:
    • <stdio.h>:输入输出函数。
    • <stdlib.h>:通用工具函数(如内存分配)。
    • <string.h>:字符串处理函数。

例子

// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H

#define PI 3.14

void printMessage();

#endif
// main.c
#include <stdio.h>
#include "myheader.h"

void printMessage() {
    printf("Hello, World!\n");
}

int main() {
    printMessage();
    printf("Value of PI: %f\n", PI);
    return 0;
}

24.3 标准库头文件

C 标准库头文件(Standard Library Header Files)是由 ANSI C(也称为 C89/C90)和 ISO C(C99 和 C11)标准定义的一组头文件,这些头文件提供了大量的函数、宏和类型定义,用于处理输入输出、字符串操作、数学计算、内存管理等常见的编程任务。

头文件功能简介
<stdio.h>标准输入输出库,包含 printfscanf 等函数
<stdlib.h>标准库函数,包含内存分配、程序控制等函数
<string.h>字符串操作函数,如 strlenstrcpy
<math.h>数学函数库,如 sincossqrt
<time.h>时间和日期函数,如 timestrftime
<ctype.h>字符处理函数,如 isalphaisdigit
<limits.h>定义各种类型的限制值,如 INT\_MAX
<float.h>定义浮点类型的限制值,如 FLT\_MAX
<assert.h>断言宏 assert,用于调试检查
<errno.h>定义错误码变量 errno 及相关宏
<stddef.h>定义通用类型和宏,如 size\_tNULL
<signal.h>处理信号的函数和宏,如 signal
<setjmp.h>提供非本地跳转功能的宏和函数
<locale.h>地域化相关的函数和宏,如 setlocale

25. 强制类型转换

强制类型转换(type casting)用于将一种数据类型显式转换为另一种类型。强制类型转换可以在需要时改变变量的类型,通常用于不同类型之间的运算或数据存储。语法为 (new\_type) expression

强制类型转换的语法

int a = 10;
double b = (double)a; // 将 int 转换为 double

整数提升

整数提升是指把小于 intunsigned int 的整数类型转换为 intunsigned int 的过程。

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

结果

Value of sum : 116

常用的算术转换

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

在这里插入图片描述


26. 错误处理

C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。

26.1 errno、perror() 和 strerror()

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

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

示例

#include <stdio.h>
#include <errno.h>
#include <string.h>
 
extern int errno ;
 
int main ()
{
   FILE * pf;
   int errnum;
   pf = fopen ("unexist.txt", "rb");
   if (pf == NULL)
   {
      errnum = errno;
      fprintf(stderr, "错误号: %d\n", errno);
      perror("通过 perror 输出错误");
      fprintf(stderr, "打开文件错误: %s\n", strerror( errnum ));
   }
   else
   {
      fclose (pf);
   }
   return 0;
}
错误号: 2
通过 perror 输出错误: No such file or directory
打开文件错误: No such file or directory

26.2 被零除的错误

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

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
   int dividend = 20;
   int divisor = 0;
   int quotient;
 
   if( divisor == 0){
      fprintf(stderr, "除数为 0 退出运行...\n");
      exit(-1);
   }
   quotient = dividend / divisor;
   fprintf(stderr, "quotient 变量的值为 : %d\n", quotient );
 
   exit(0);
}

输出

除数为 0 退出运行...

26.3 程序退出状态

通常情况下,程序成功执行完一个操作正常退出的时候会带有值 EXIT_SUCCESS。在这里,EXIT_SUCCESS 是宏,它被定义为 0。
如果程序中存在一种错误情况,当您退出程序时,会带有状态值 EXIT_FAILURE,被定义为 -1。所以,上面的程序可以写成:

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
   int dividend = 20;
   int divisor = 5;
   int quotient;
 
   if( divisor == 0){
      fprintf(stderr, "除数为 0 退出运行...\n");
      exit(EXIT_FAILURE);
   }
   quotient = dividend / divisor;
   fprintf(stderr, "quotient 变量的值为: %d\n", quotient );
 
   exit(EXIT_SUCCESS);
}

输出

quotient 变量的值为 : 4

26.4 assert() 宏

assert() 是一个宏,用于在调试期间检测条件。如果条件为假,它会输出错误信息并终止程序。

#include <assert.h>

int divide(int a, int b) {
    assert(b != 0); // 确保除数不为零
    return a / b;
}

27. 递归

同Java一样,如果你已经了解了,可以跳过本节,或者再重温一下。
递归是 C 语言中一种重要的编程技巧,它指的是一个函数调用自身以解决问题。递归通常用于解决可以被分解为更小子问题的任务,比如计算阶乘、斐波那契数列等。

递归的基本组成

  1. 基例(Base Case)
    • 每个递归函数必须有一个或多个基例,以防止无限递归。基例是函数停止递归的条件。
  2. 递归案例(Recursive Case)
    • 这是函数调用自身的部分。它将问题分解为更小的子问题。

以下是一个计算阶乘的递归函数示例:

#include <stdio.h>

int factorial(int n) {
    if (n == 0)  // 基例
        return 1;
    else         // 递归案例
        return n * factorial(n - 1);
}

int main() {
    int num = 5;
    printf("Factorial of %d is %d\n", num, factorial(num));
    return 0;
}

递归的优缺点

优点

  • 代码简洁,易于理解和维护,尤其对于解决复杂问题(如树的遍历)时。

缺点

  • 递归调用会占用栈空间,可能导致栈溢出。
  • 过多的递归调用可能导致性能问题,尤其在没有优化的情况下。

递归 vs. 迭代

  • 递归:更直观,适合解决自然递归的问题。
  • 迭代:通常性能更好,使用循环结构替代递归,节省栈空间。

递归深度

在 C 语言中,递归深度受限于栈的大小。可通过 ulimit -s 命令查看和设置栈大小。


28. 可变参数

C 语言中的可变参数允许函数接受不定数量的参数。这个特性主要通过标准库中的 <stdarg.h> 头文件来实现,通常用于需要灵活参数列表的场景,比如实现 printf 函数。

可变参数的基本步骤

  1. 包含头文件
#include <stdarg.h>

2. 定义函数

  • 在函数参数列表中,最后一个参数通常是固定的,之前的参数可以是可变的。

3. 使用宏来处理参数

  • 使用 va\_startva\_argva\_end 宏来遍历可变参数。

示例代码

#include <stdio.h>
#include <stdarg.h>

int sum(int count, ...) {
    va_list args;        // 声明一个可变参数列表
    int total = 0;

    va_start(args, count); // 初始化参数列表

    for (int i = 0; i < count; i++) {
        total += va_arg(args, int); // 获取下一个参数
    }

    va_end(args); // 清理工作
    return total;
}

int main() {
    printf("Sum of 1, 2, 3 is: %d\n", sum(3, 1, 2, 3));
    printf("Sum of 10, 20, 30, 40 is: %d\n", sum(4, 10, 20, 30, 40));
    return 0;
}

关键宏解释

  1. va_list
    • 定义一个类型,用于存储可变参数列表的信息。
  2. va_start(va_list ap, last_param)
    • 初始化 va\_list 变量,准备访问可变参数。 last\_param 是在可变参数列表之前的最后一个参数。
  3. va_arg(va_list ap, type)
    • 获取下一个参数的值,并将 va\_list 变量移到下一个参数。 type 是你期望的参数类型。
  4. va_end(va_list ap)
    • 清理 va\_list 变量。

29. 内存管理

C 语言中的内存管理是程序设计中的一个重要方面,它涉及到如何分配、使用和释放内存。正确的内存管理可以有效提高程序的性能并避免内存泄漏等问题。
C 语言为内存的分配和管理提供了几个函数。这些函数可以在 <stdlib.h> 头文件中找到。

29.1 动态内存管理函数

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

**malloc**

  • 用于分配指定字节数的内存块,返回指向这块内存的指针。
  • 语法: void\* malloc(size\_t size);
  • 如果分配失败,返回 NULL
int *arr = (int*) malloc(10 * sizeof(int)); // 分配一个整型数组

**calloc**

  • 类似于 malloc,但同时会初始化分配的内存为零。
  • 语法: void\* calloc(size\_t num, size\_t size);
int *arr = (int*) calloc(10, sizeof(int)); // 分配并初始化为0

**realloc**

  • 用于重新调整已分配内存的大小,可以增大或缩小内存块。
  • 语法: void\* realloc(void\* ptr, size\_t new\_size);
arr = (int*) realloc(arr, 20 * sizeof(int)); // 重新分配更大的内存

**free**

  • 用于释放之前分配的内存,防止内存泄漏。
  • 语法: void free(void\* ptr);
free(arr); // 释放内存

29.2 内存管理的注意事项

  1. 内存泄漏
    • 如果分配的内存未被释放,程序运行后会消耗越来越多的内存,最终可能导致系统内存耗尽。
  2. 野指针
    • 指向已经释放的内存区域的指针,访问这块内存可能导致未定义行为。
  3. 越界访问
    • 访问未分配或已释放内存,可能导致程序崩溃或数据损坏。
  4. 多次释放
    • 同一内存块被多次释放会导致程序崩溃或不稳定。

29.3 示例代码

使用 mallocfree

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

int main() {
    int *arr;
    int n = 5;

    // 动态分配内存
    arr = (int*) malloc(n * sizeof(int));
    if (arr == NULL) {
        printf("Memory allocation failed!\n");
        return 1; // 分配失败
    }

    // 初始化并使用数组
    for (int i = 0; i < n; i++) {
        arr[i] = i + 1;
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 释放内存
    free(arr);
    arr = NULL; // 避免野指针

    return 0;
}

29.4 常用的内存管理函数和运算符

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

30. 未定义行为(Undefined behavior)

未定义行为指的是 C 语言标准中未定义的操作。这意味着程序员在使用这些操作时,无法预测程序的运行结果。不同的编译器、平台或优化设置可能会导致不同的结果,甚至可能在某些情况下导致程序崩溃。

30.1 常见的未定义行为示例

数组越界

当我们尝试访问数组的越界元素时,即访问数组的第0个元素之前或数组长度之后的元素时,编译器无法确定访问到的内存空间中存储的是什么内容,因此会导致未定义行为。例如:

int arr[3] = {1, 2, 3};
printf("%d\n", arr[5]); // 越界访问,结果未定义

解引用空指针

当我们尝试对空指针进行解引用操作时,编译器无法确定要访问的内存空间中存储的内容,因此会导致未定义行为。例如:

int *ptr = NULL;
printf("%d\n", *ptr); // 解引用空指针,结果未定义

未初始化的局部变量

当我们使用未初始化的局部变量时,其值是未定义的,因此会导致未定义行为。例如:

int x;
printf("%d\n", x); // x 未初始化,结果未定义

浮点数除以零

当我们尝试对浮点数进行除以零的操作时,结果是未定义的。例如:

float x = 1.0;
float y = x / 0.0; // 浮点数除以零,结果未定义

整数除以零

当我们尝试对整数进行除以零的操作时,结果是未定义的。例如:

int x = 10;
int y = x / 0; // 整数除以零,结果未定义

符号溢出

当整数运算导致结果超出了整数类型能表示的范围时,结果是未定义的。例如:

signed char x = 127;
x = x + 1; // signed char 溢出,结果未定义

位移操作数太大

当执行位移操作时,位移的位数大于或等于操作数的位数时,结果是未定义的。例如:

int x = 1;
int y = x << 32; // 位移操作数太大,结果未定义

错误的类型转换

当我们进行不安全的类型转换时,结果是未定义的。例如:

int *ptr = (int *)malloc(sizeof(int));
float *fptr = (float *)ptr; // 错误的类型转换,结果未定义

内存越界

当我们向已经释放或未分配的内存写入数据时,结果是未定义的。例如:

int *ptr = (int *)malloc(sizeof(int));
free(ptr);
*ptr = 10; // 内存越界,结果未定义

未定义的浮点数行为

比如比较两个 NaN(非数字)值是否相等,这是未定义的行为。例如:

float x = sqrt(-1);
float y = sqrt(-1);
if (x == y) {
    printf("NaN values are equal\n");
}

30.2 如何规避

  • 仔细阅读和遵守 C 语言标准:了解哪些操作可能导致未定义行为,并避免这些操作。
  • 使用静态分析工具:这些工具可以帮助检测潜在的未定义行为。
  • 进行彻底的测试:测试程序的不同执行路径,以确保程序在各种情况下都能正确运行。
  • 避免依赖未定义行为:不要假设未定义行为会产生特定的结果。
  • 使用安全的函数和库:使用标准库提供的、定义良好的函数,避免使用可能导致未定义行为的非标准或不安全的函数。

31. 命令行参数

命令行参数是指在程序执行时通过命令行传递给程序的输入。这种机制允许用户在启动程序时提供数据,使得程序更加灵活和可配置。命令行参数通常用于传递文件名、选项或其他配置参数。

31.1 如何使用命令行参数

命令行参数通过 main 函数的两个参数传递: argcargv

  • argc(argument count)是一个整数,表示传递给程序的参数数量,包括程序本身的名称。
  • argv(argument vector)是一个字符串数组,其中每个元素是一个命令行参数。 argv[0] 通常是程序的名称, argv[1] 是第一个实际参数,以此类推。

31.2 示例代码

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("参数个数: %d\n", argc);
    
    for (int i = 0; i < argc; i++) {
        printf("参数 %d: %s\n", i, argv[i]);
    }

    return 0;
}

编译和运行
将上述代码保存为 example.c,然后在终端中编译和运行:

gcc example.c -o example
./example arg1 arg2 arg3

输出

参数个数: 4
参数 0: ./example
参数 1: arg1
参数 2: arg2
参数 3: arg3

31.3 参数解析

在实际应用中,命令行参数通常需要解析。可以手动解析,也可以使用库(如 getopt)来处理选项和参数。
手动解析

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

int main(int argc, char *argv[]) {
    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "--help") == 0) {
            printf("用法: ./example [options]\n");
            return 0;
        } else if (strcmp(argv[i], "--version") == 0) {
            printf("版本: 1.0\n");
            return 0;
        } else {
            printf("未知参数: %s\n", argv[i]);
        }
    }

    return 0;
}

常用库
对于更复杂的命令行参数解析,可以使用标准库中的 getopt 函数。

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    int opt;
    
    while ((opt = getopt(argc, argv, "hv")) != -1) {
        switch (opt) {
            case 'h':
                printf("帮助信息\n");
                return 0;
            case 'v':
                printf("版本信息\n");
                return 0;
            default:
                fprintf(stderr, "用法: %s [-h] [-v]\n", argv[0]);
                return 1;
        }
    }

    return 0;
}

最后

回头一看,能写这么多我是真NB,你能看到这里更NB,不能再写了,编辑器都开始卡了,就到这里吧,兄弟们,学起来!

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐