C_Note
C _Base Grammars
基础语法
C_Language ? Cat_Language!!!
标准代码架构
1 |
|
常量
常量在设置后是不能更改的
C语言中的常量可以根据其类型分为以下几类:
常量类型 | 描述 | 示例 |
---|---|---|
整型常量 | 表示整数的常量,可以是十进制、八进制、十六进制表示。 | 100 , 0123 (八进制), 0x1A (十六进制) |
浮点常量 | 表示小数的常量,包括科学计数法。 | 3.14 , 1.0e-2 |
字符常量 | 表示单个字符的常量,必须用单引号包围。 | 'A' , '9' , '\n' |
字符串常量 | 表示一串字符的常量,必须用双引号包围。 | "Hello" , "C语言" |
符号常量 | 用#define 或const 关键字定义的具有固定值的标识符常量。 |
#define PI 3.14 |
常量宏
常量宏通常使用#define
指令来定义,在预处理阶段会将宏的名称替换为其定义的值,其本质只是简单的文本替换,因此,宏不会进行类型检查、作用域控制等,容易导致一些难以发现的错误
故此,目前阶段仅将其作为文本替换器使用
1 | //全局常量 |
变量
变量类型 | 描述 | 范围(根据实现) | 示例 |
---|---|---|---|
int |
整型变量,用于存储整数。 | -32,768 到 32,767 (16位系统),较常见是32位系统 | int x = 5; |
float |
单精度浮点数,用于存储小数。 | 约为 ±3.4e–38 到 ±3.4e+38 | float y = 3.14; |
double |
双精度浮点数,表示更大范围和更精确的小数。 | ±1.7e–308 到 ±1.7e+308 | double z = 2.71828; |
char |
字符变量,存储单个字符(ASCII码)。 | -128 到 127 | char c = 'A'; |
long |
长整型,存储更大的整数。 | -2^31 到 2^31-1 (32位系统) | long n = 1000000; |
short |
短整型,存储较小的整数。 | -32,768 到 32,767 | short s = 32767; |
unsigned |
无符号整型,用于存储非负数。 | 0 到 65535 (16位系统) | unsigned int u = 5; |
1 | //变量 |
注意,在C语言中当变量被定义后它的类型就无法改变了,上文的(float)i
应被视为一种将i
转换为float形式的新的临时变量的表达式
C语言中的混合运算以float f = i/2;
为例,他的结果数据类型和运算数据类型是分开的,流程上来讲是先判断操作数的数据类型,当左右操作数均为整形时执行整形计算,其他情况执行浮点运算,此时得出的结果为int类型数据,随后将结果返回变量f
时被转换为float类型
下面是 C 语言中常用的变量类型的表格展示,包括每种类型的描述、占用的内存大小以及表示的范围
数据类型 | 描述 | 大小(通常) | 范围 |
---|---|---|---|
int |
整数类型,表示带符号的整型数据 | 4 字节(考试可能会问) | -2,147,483,648 到 2,147,483,647 |
unsigned int |
无符号整数类型 | 4 字节 | 0 到 4,294,967,295 |
short |
短整型,带符号 | 2 字节 | -32,768 到 32,767 |
unsigned short |
无符号短整型 | 2 字节 | 0 到 65,535 |
long |
长整型,带符号 | 8 字节 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
unsigned long |
无符号长整型 | 8 字节 | 0 到 18,446,744,073,709,551,615 |
float |
单精度浮点型,用于表示小数 | 4 字节 | 3.4E-38 到 3.4E+38(6 位有效数字) |
double |
双精度浮点型,用于表示高精度小数 | 8 字节 | 1.7E-308 到 1.7E+308(15 位有效数字) |
char |
字符类型,用于表示单个字符 | 1 字节 | -128 到 127(或 0 到 255,取决于系统) |
unsigned char |
无符号字符类型 | 1 字节 | 0 到 255 |
long double |
扩展精度浮点型 | 16 字节 | 3.4E-4932 到 1.1E+4932(18-19 位有效数字) |
_Bool |
布尔类型(C99 引入),表示真或假 | 1 字节 | 0(假)或 1(真) |
void |
无类型,通常用于函数返回类型和指针类型 | 无 | 无法表示数据 |
标准输出函数
prtinf( )
1 |
|
puts( )
输出一个字符串到标准输出(通常是显示器)。puts()
会自动在输出的字符串末尾加上一个换行符
int puts(const char *str);
- 参数
str
是要输出的字符串。 - 返回值为非负整数,表示写入的字符数量。如果发生错误,返回
EOF
(即 -1)。
1 |
|
常用格式说明符
格式说明符 | 描述 | 示例 |
---|---|---|
%d |
以 十进制形式输出带符号整数 | printf("%d", 123); => 123 |
%i |
以 十进制形式输出带符号整数(与 %d 相同) |
printf("%i", 123); => 123 |
%u |
以 十进制形式输出无符号整数 | printf("%u", 123); => 123 |
%f |
以 浮点数形式输出 | printf("%f", 3.14); => 3.140000 |
%e |
以 科学计数法形式输出浮点数 | printf("%e", 123.45); => 1.234500e+02 |
%g |
自动选择使用 %e 或 %f 格式 |
printf("%g", 123.45); => 123.45 |
%c |
输出单个字符 | printf("%c", 'A'); => A |
%s |
输出字符串 | printf("%s", "Hello"); => Hello |
%x |
以 小写十六进制形式输出无符号整数 | printf("%x", 255); => ff |
%X |
以 大写十六进制形式输出无符号整数 | printf("%X", 255); => FF |
%o |
以 八进制形式输出无符号整数 | printf("%o", 255); => 377 |
%p |
输出指针的值(地址) | printf("%p", &a); => 0x7ff... |
%% |
输出百分号 % 本身 |
printf("%%"); => % |
其他格式化修饰符
修饰符 | 描述 | 示例 |
---|---|---|
- |
左对齐(默认右对齐) | printf("%-10d", 123); => 123 |
+ |
强制输出数值符号(正数显示 + 号) |
printf("%+d", 123); => +123 |
|
正数前输出空格,负数前输出 - 号 |
printf("% d", 123); => 123 |
0 |
用零填充(通常用于数字) | printf("%04d", 5); => 0005 |
# |
对于 %o 、%x 或 %X ,显示进制前缀 |
printf("%#x", 255); => 0xff |
数字 | 最小字段宽度 | printf("%5d", 12); => 12 |
.数字 |
精度控制,用于浮点数 | printf("%.2f", 3.14159); => 3.14 |
标准读取函数
1.scanf( )
scanf
是 C 语言中用于从标准输入(通常是键盘)读取数据的函数。它可以根据指定的格式字符串,将输入的内容转换为对应的变量值,但他在Visual Studio 2022已被弃用,改用更安全的scanf_s
scanf_s
是 scanf
的安全版本,要求为字符串输入提供额外的参数,指定缓冲区的大小,以防止缓冲区溢出。在传入数组时缓冲区大小需要和数组长度相等,否者会出现致命bug, (unsigned)sizeof(a)
是常用的自主获取组长度的方法,传入单个数字时则不需要额外参数
scanf_s
向参数传入数据时实际是向该数据的内存地址传值,因此需写为&变量名
1 |
|
(unsigned)
是一个类型转换运算符,它将 sizeof(a)
的结果强制转换为无符号整数类型 unsigned int
这是为了确保传递给 scanf_s
的第三个参数是 unsigned int
类型,而不是 size_t
,以避免类型不匹配的问题………..规范化保守策略总是好的
关于混合传值
scanf通常读数读到空格就会中断,因此一次向多个变量传值使用空格来中断第一个传值过程,再次输入则向下一个变量传值
2.gets( )
用于获取一行的输入,遇到\n时中断,但gets()
函数不安全,因为它没有检查缓冲区的大小。输入的字符串如果超过数组大小,可能会导致缓冲区溢出,带来严重的安全漏洞。因此,gets()
已经在 C11 标准中被废弃,不推荐使用。
由于 gets()
存在安全性问题,通常建议使用更安全的替代函数 fgets()
:
fgets(str, sizeof(str), stdin)
:str
是字符数组,sizeof(str)
表示最多读取的字符数,stdin
是标准输入流。fgets()
会在读取到换行符或者到达指定字符数时停止
1 | //gets&fgets |
sp : get( )在获取字符串并传给数组时会自动在末尾加上\0
格式说明符
scanf
函数根据 格式说明符 将输入的字符转换为相应的数据类型。以下是常用的格式说明符:
数据类型 | 格式说明符 | 示例代码 | 示例输入 | 说明 |
---|---|---|---|---|
int |
%d |
scanf_s("%d", &num); |
123 | 读取一个十进制整数。 |
float |
%f |
scanf_s("%f", &flt); |
3.14 | 读取一个浮点数。 |
double |
%lf |
3.1415 | 读取一个双精度浮点数。 | |
char |
%c |
scanf_s(" %c", &ch); |
A | 读取一个字符(包括空白字符)。 |
字符串 | %s |
scanf_s("%s", str,(unsigned)sizeof(str)); |
hello | 读取字符串,遇到空白符停止。 |
无符号整数 | %u |
123 | 读取一个无符号整数。 | |
八进制整数 | %o |
017 | 读取一个八进制整数。 | |
十六进制整数 | %x |
0x1F | 读取一个十六进制整数。 | |
长整数 | %ld |
123456 | 读取一个长整型变量。 | |
长长整数 | %lld |
12345678 | 读取一个长长整型变量。 | |
指针 | %p |
0x7ffee | 读取一个指针类型的地址。 |
运算符
1. 算术运算符
算术运算符用于执行基本的数学运算,如加法、减法、乘法、除法等。
运算符 | 描述 | 示例 | 结果 |
---|---|---|---|
+ |
加法 | a + b |
a 与 b 相加 |
- |
减法 | a - b |
a 减去 b |
* |
乘法 | a * b |
a 乘以 b |
/ |
除法 | a / b |
a 除以 b |
% |
取模(余数) | a % b |
a 除以 b 的余数 |
++ |
自增 | ++a 或 a++ |
a 递增 1 |
-- |
自减 | --a 或 a-- |
a 递减 1 |
++a
和a++
:前者是前置自增(先加再用),后者是后置自增(先用再加)。--a
和a--
:类似地,分别为前置和后置自减。
2. 关系运算符
1 |
|
关系运算符用于比较两个操作数,结果返回布尔值 1
(真)或 0
(假)。
运算符 | 描述 | 示例 | 结果 |
---|---|---|---|
== |
等于 | a == b |
如果 a 等于 b ,返回 1 ;否则返回 0 |
!= |
不等于 | a != b |
如果 a 不等于 b ,返回 1 ;否则返回 0 |
> |
大于 | a > b |
如果 a 大于 b ,返回 1 ;否则返回 0 |
< |
小于 | a < b |
如果 a 小于 b ,返回 1 ;否则返回 0 |
>= |
大于等于 | a >= b |
如果 a 大于等于 b ,返回 1 ;否则返回 0 |
<= |
小于等于 | a <= b |
如果 a 小于等于 b ,返回 1 ;否则返回 0 |
3. 逻辑运算符
1 |
|
逻辑运算符用于布尔运算,常用于条件语句中的复杂判断。
运算符 | 描述 | 示例 | 结果 |
---|---|---|---|
&& |
逻辑与 | a && b |
如果 a 和 b 都为真,返回 1 ;否则返回 0 |
|| | 逻辑或 | a || b | 如果 a 或 b 任一个为真,返回 1 ;否则返回 0 |
! |
逻辑非 | !a |
如果 a 为假,返回 1 ;否则返回 0 |
4. 位运算符
位运算符用于位级操作,操作数被视为位模式而非数值。
运算符 | 描述 | 示例 | 结果 |
---|---|---|---|
& |
按位与 | a & b |
a 和 b 按位与 |
| | 按位或 | a | b | a 和 b 按位或 |
^ |
按位异或 | a ^ b |
a 和 b 按位异或 |
~ |
按位取反 | ~a |
a 的按位取反 |
<< |
左移 | a << 2 |
a 左移 2 位 |
>> |
右移 | a >> 2 |
a 右移 2 位 |
5. 赋值运算符
赋值运算符用于为变量赋值,通常可以结合算术运算符进行简化操作。
运算符 | 描述 | 示例 | 结果 |
---|---|---|---|
= |
赋值 | a = b |
将 b 的值赋给 a |
+= |
加后赋值 | a += b |
a = a + b |
-= |
减后赋值 | a -= b |
a = a - b |
*= |
乘后赋值 | a *= b |
a = a * b |
/= |
除后赋值 | a /= b |
a = a / b |
%= |
取模后赋值 | a %= b |
a = a % b |
<<= |
左移后赋值 | a <<= 2 |
a = a << 2 |
>>= |
右移后赋值 | a >>= 2 |
a = a >> 2 |
&= |
按位与后赋值 | a &= b |
a = a & b |
^= |
按位异或后赋值 | a ^= b |
a = a ^ b |
|= | 按位或后赋值 | a |= b | a = a | b |
6. 条件运算符(三元运算符)
条件运算符用于根据条件的真假执行不同的表达式。
运算符 | 描述 | 示例 | 结果 |
---|---|---|---|
? : |
条件表达式(类似 if-else) | a ? b : c |
如果 a 为真,执行 b ,否则执行 c |
7. 其他运算符
1 |
|
除了上述常用的运算符,还有一些其他的运算符,包括取地址、取值、大小、逗号等。
运算符 | 描述 | 示例 | 结果 |
---|---|---|---|
& |
取地址 | &a |
返回变量 a 的地址 |
* |
指针解引用 | *p |
返回指针 p 指向的值 |
sizeof |
计算数据类型大小 | sizeof(a) |
返回 a 的字节大小 |
, |
逗号表达式 | a = (x++, y++) |
先执行 x++ ,再执行 y++ |
-> |
结构体指针成员访问 | p->member |
访问指针 p 指向的结构体的成员 |
. |
结构体成员访问 | s.member |
访问结构体 s 的成员 |
7.运算符优先级
算数 > 关系 > 逻辑 :表格如下
C语言运算符优先级表
优先级最高符号 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
---|---|---|---|---|
1 | () | 括号 | 表达式 | 左结合 |
[] | 数组下标 | 表达式 | 左结合 | |
-> | 取结构体成员(指针) | 表达式 | 左结合 | |
. | 取结构体成员(非指针) | 表达式 | 左结合 | |
sizeof | 返回数据类型大小 | 表达式 | 左结合 | |
2 | ! | 逻辑非 | 表达式 | 右结合 |
~ | 位取反 | 表达式 | 右结合 | |
++ | 自增 | 表达式 | 右结合 | |
-- | 自减 | 表达式 | 右结合 | |
- | 负号 | 表达式 | 右结合 | |
* | 指针 | 表达式 | 右结合 | |
& | 取地址 | 表达式 | 右结合 | |
3 | * | 乘 | 表达式 | 左结合 |
/ | 除 | 表达式 | 左结合 | |
% | 取余 | 表达式 | 左结合 | |
4 | + | 加 | 表达式 | 左结合 |
- | 减 | 表达式 | 左结合 | |
5 | << | 左移 | 表达式 | 左结合 |
>> | 右移 | 表达式 | 左结合 | |
6 | < | 小于 | 表达式 | 左结合 |
<= | 小于等于 | 表达式 | 左结合 | |
> | 大于 | 表达式 | 左结合 | |
>= | 大于等于 | 表达式 | 左结合 | |
7 | == | 等于 | 表达式 | 左结合 |
!= | 不等 | 表达式 | 左结合 | |
8 | & | 按位与 | 表达式 | 左结合 |
9 | ^ | 按位异或 | 表达式 | 左结合 |
10 | | | 按位或 | 表达式 | 左结合 |
11 | && | 逻辑与 | 表达式 | 左结合 |
12 | || | 逻辑或 | 表达式 | 左结合 |
13 | ? : | 条件运算符 | 表达式 | 右结合 |
14 | = | 赋值 | 表达式 | 右结合 |
+= | 加赋值 | 表达式 | 右结合 | |
-= | 减赋值 | 表达式 | 右结合 | |
*= | 乘赋值 | 表达式 | 右结合 | |
/= | 除赋值 | 表达式 | 右结合 | |
%= | 取余赋值 | 表达式 | 右结合 | |
<<= | 左移赋值 | 表达式 | 右结合 | |
>>= | 右移赋值 | 表达式 | 右结合 | |
&= | 按位与赋值 | 表达式 | 右结合 | |
^= | 按位异或赋值 | 表达式 | 右结合 | |
|= | 按位或赋值 | 表达式 | 右结合 | |
15 | , | 逗号 | 表达式 | 左结合 |
考研复试可能会用
逻辑语句
逻辑语句概述
注意 逻辑语句后一般不加分号(;)
1 |
|
C 语言中的逻辑语句主要包括逻辑运算符和条件语句。逻辑语句用于控制程序的执行流程,常用的逻辑语句有 if
、else if
、else
、switch
、while
、for
、do...while
等。
1. 逻辑运算符
逻辑运算符用于连接条件表达式或进行条件判断。C 语言中的逻辑运算符如下表所示:
运算符 | 名称 | 用法 | 结果 |
---|---|---|---|
&& |
逻辑与(AND) | expr1 && expr2 |
若 expr1 和 expr2 均为真,结果为真,否则为假 |
| | 逻辑或(OR) | expr1 逻辑或 expr2 |
若 expr1 和 expr2 任一真,结果为真 |
! |
逻辑非(NOT) | !expr |
若 expr 为真,结果为假,反之亦然 |
2. 条件语句
C 语言中的条件语句用于根据不同的条件执行不同的代码块。以下是常见的条件语句:
语句 | 用法 | 解释 |
---|---|---|
if |
if (condition) { /* code */ } |
当 condition 为真时,执行 { /* code */ } |
if...else |
if (condition) { /* code1 */ } else { /* code2 */ } |
如果 condition 为真,执行 code1 ,否则执行 code2 |
else if |
if (condition1) { /* code1 */ } else if (condition2) { /* code2 */ } else { /* code3 */ } |
多条件判断,第一个为真的条件执行相应的代码块 |
switch |
switch (expression) { case val1: /* code */ break; case val2: /* code */ break; default: /* code */ } |
根据 expression 的值选择执行相应的 case 代码块 |
3. 循环语句
C 语言中的循环语句用于重复执行某一代码块,直到满足指定的条件。
语句 | 用法 | 解释 |
---|---|---|
while |
while (condition) { /* code */ } |
当 condition 为真时,重复执行 { /* code */ } |
do...while |
do { /* code */ } while (condition); |
先执行一次代码块,再根据 condition 判断是否继续 |
for |
for (init; condition; increment) { /* code */ } |
常用循环,按照初始化、条件判断、增量控制的顺序执行 |
4. 三元运算符
三元运算符是简化 if...else
语句的方式,语法如下:
运算符 | 用法 | 解释 |
---|---|---|
?: |
condition ? expr1 : expr2 |
当 condition 为真时,返回 expr1 ,否则返回 expr2 |
5.continue和break
语句 | 用法 | 解释 |
---|---|---|
continue |
continue; |
当循环语句中出现 continue 时,跳过本次循环,执行下一次循环 |
break |
break; |
当循环语句中出现 break 时,打断循环,执行后续语句 |
SP.逻辑语句中执行的变量变化会传递到逻辑语句外
1 | int main() |
数组
数组类型 | 用法示例 | 访问方式 | 特点 |
---|---|---|---|
一维数组 | int arr[5]; |
arr[i] |
固定大小,元素在内存中连续存储。 |
二维数组 | int arr[3][4]; |
arr[i][j] |
类似矩阵,元素按行优先存储。 |
多维数组 | int arr[2][3][4]; |
arr[i][j][k] |
用于复杂的多维数据存储。 |
字符数组 | char str[] = "Hello"; |
str[i] |
以 '\0' 结尾,表示字符串。 |
动态数组 | int *arr = (int *)malloc(5 * sizeof(int)); |
arr[i] |
动态分配内存,大小可变。 |
参数传递 | void func(int arr[], int size) |
arr[i] |
传递指针,需要传递数组大小。 |
一维数组
1 |
|
主要 数组索引从0开始,因此大小为10的数组实际索引为0到9
关于数组初始化 :
通常可在数组后添加一个新的变量来动态的记录数组长度,常用的以为数组长度算法为int length_a = sizeof(a) / sizeof(int);
关于数组越界的问题 :
在访问数组时,如果使用的索引超出了数组的有效范围,可能会导致不可预知的行为或程序崩溃。从内存层面上,溢出的数组值会和该数组相邻的变量产生冲突,使得该变量获得错误的值,Visual Studio 2022会自动检测数组下标溢出问题,并阻止编译,但其他编译器仍可能出现问题
多维数组
1 |
|
多维数组的内存结构
二维数组在内存中是按行连续存储的。例如,matrix[2][3]
在内存中的存储顺序为:matrix[0][0], matrix[0][1], matrix[0][2], matrix[1][0], matrix[1][1], matrix[1][2]
。
数组传递
1 |
|
数组传递是按指针传递的,你传递的是数组首元素的地址,而不是整个数组。数组名退化为指针
数组的长度信息不会自动传递,数组长度信息丢失,需要手动传递长度。上述代码提供了一个可行的长度传递逻辑
指针传递使得函数可以修改原数组的值,因为指针指向的是相同的内存地址。
字符数组
1 |
|
关于字符数组初始化
字符数组是用于存储字符的数组,通常用于存储和处理字符串。在C语言中,字符串是以空字符 '\0'
结尾的字符数组,在对字符数组进行操作是要尤为注意索引问题
数据大小问题
数据类型 | 每个元素占用的内存大小 |
---|---|
int |
通常 4 字节 |
float |
通常 4 字节 |
double |
通常 8 字节 |
char |
通常 1 字节 |
*p 指针 |
32 位系统是 4 字节,64 位系统是 8 字节 |
这些大小可能会根据平台和编译器有所不同,sizeof
运算符是确定实际大小的最可靠方式,常用的算法为sizefo(arrname)/siezof(datat[0])
关于sizeof()
sizeof(type)
:计算指定类型的大小sizeof(variable)
:计算变量所占的内存大小
1 | // 对于数组大小的计算 |
1 | //对于构造体大小的计算 |
尽管 int
是 4 字节,char
是 1 字节,float
是 4 字节,但结构体可能因为内存对齐而占用更多的字节数,详见构造体对齐章节
字符串操作函数str系列
该系列操作函数需调用<string.h>
在 C 语言中,string.h
头文件中提供了一些常用的字符串操作函数,包括 strlen
、strcpy
、strcat
和 strcmp
。这些函数专门用于处理以 '\0'
结尾的字符串
其中strcpy
、strcat
由于其安全性的不足而被弃用,现用更安全的strcpy_s
和strcat_s
,他们要求提供dest_size
: 目标字符串的大小(总字节数,必须包含足够的空间来容纳源字符串和终止符 \0
)来防止字符串溢出组中
dest_size
必须包含现有字符串、源字符串以及终止符 \0
。
如果目标缓冲区不够大,函数将不会追加字符串并返回错误。
如果目标或源字符串指针为 NULL
,函数也会返回错误。
1 |
|
函数 | 功能 | 返回值 | 用法示例 | 注意事项 |
---|---|---|---|---|
strlen |
计算字符串长度 | 返回字符串长度,不含 \0 |
size_t len = strlen(str); |
只计算 \0 之前的字符,不包括 \0 ,且传入字符串必须以 \0 结尾 |
strcpy_s |
安全地将源字符串复制到目标字符串 | 返回目标字符串指针 | strcpy(dest,sizeof(dest), src); |
目标缓冲区必须足够大以容纳源字符串和 \0 ,否则失败 |
strcat_s |
安全地将源字符串追加到目标字符串后 | 返回目标字符串指针 | strcat(dest,sizeof(dest), src); |
目标缓冲区必须包含现有字符串、源字符串和 \0 ,否则失败 |
strcmp |
比较两个字符串 | 0:相等,正数:大于,负数:小于 | int cmp = strcmp(str1, str2); |
按字典顺序比较,区分大小写,比较到第一个不同的字符即停止 |
指针
指针变量
指针是一个存储地址的变量,而指针变量就是指针的具体实现。它存储的是另一个变量的内存地址,而不是直接存储数据值。指针是C语言的一个核心概念,允许你更高效和灵活地操作内存、数组、字符串以及函数等
取地址操作符 &
和解引用操作符 *
&
取地址符:用于获取变量的内存地址。例如&a
就是变量a
的内存地址。*
解引用符:用于访问指针指向的变量的值。例如*p
表示访问p
所指向的变量的值。
1 |
|
注意!指针的类型决定了指针指向的数据类型,比如:
int *p;
指向int
类型的指针char *p;
指向char
类型的指针float *p;
指向float
类型的指针 指针类型决定了在解引用时如何解释内存中的数据。
指针变量和普通变量的区别:
特性 | 普通变量 | 指针变量 |
---|---|---|
存储内容 | 变量的值 | 另一个变量的地址 |
访问方式 | 直接访问变量值 | 通过解引用访问指向的变量值 |
取地址符使用 | 不需要 | 需要使用* 进行解引用 |
使用场景 | 存储基本数据 | 存储内存地址,操作复杂数据结构 |
指针传递
指针传递指的是通过函数参数传递指针(即变量的地址),从而使函数能够直接操作原始变量的值。这是C语言实现“传引用”功能的方式,因为C语言默认的参数传递是“传值”,即传递的是变量的副本,而不是变量本身。
1 |
|
change_value(int *p)
:这个函数接受一个int
类型的指针p
,即指向一个int
类型变量的地址。*p = 99;
:通过解引用指针p
,直接修改了指针指向的变量(即a
)的值。
指针偏移
指针偏移指的是通过对指针进行算术运算来访问相邻的内存单元。它与数组访问紧密相关。指针偏移是通过修改指针的值,使其指向内存中的不同位置,从而访问相邻的元素
1 |
|
int *p = arr;
:指针p
指向数组arr
的第一个元素。*(p + i)
:通过指针偏移,访问数组的第i
个元素。这里的p + i
表示指针p
向后偏移i
个位置,*(p + i)
解引用偏移后的地址,得到对应的元素值。
动态内存分配
在C语言中,内存可以通过动态内存分配的方式进行管理。静态内存分配是在程序编译时确定的,如局部变量和全局变量,它们在程序运行时占用固定的内存。相比之下,动态内存分配是在程序运行时,通过显式调用特定的函数来申请或释放内存,内存的大小可以根据需要动态变化,且程序员需要手动释放不再使用的内存
动态内存的申请可以使用标准库中的函数malloc()
、calloc()
、realloc()
等。最常用的函数是malloc()
,它用于申请指定字节数的内存
1.malloc()
函数
使用malloc()
前应先引入<stdlib.h>
1 |
|
- 内存申请:我们使用
malloc()
分配了n
个整数大小的内存,并将其返回的通用指针转换为int*
类型。 - 内存使用:内存分配后可以像普通数组一样使用指针来访问。
- 内存释放:使用完内存后,必须调用
free()
函数来释放之前动态分配的内存,以避免内存泄漏。
2. calloc()
函数
calloc()
函数用于动态分配内存,并将分配的内存初始化为0
1 | // 分配 5 个整型,并初始化为 0 |
calloc()
在初始化数组时非常有用,它会将所有分配的内存块初始化为0,而malloc()
不会进行初始化。
3. realloc()
函数
realloc()
函数用于重新调整已经动态分配的内存块的大小。它的语法如下:
1 | // 将原内存大小调整为可以存储 10 个整型 |
realloc()
可以扩展或缩小之前分配的内存块。如果扩展,新的内存区域的内容是未初始化的
4. free()
函数
free()
函数用于释放malloc()
、calloc()
、或realloc()
动态分配的内存。它的语法非常简单:
1 | free(arr); |
注意:释放后不能再访问释放的内存区域,否则会导致未定义的行为
栈与堆的差异
1. 栈(Stack)
栈是内存中的一块区域,用于存储局部变量、函数调用相关的信息(如返回地址、参数等)。它遵循后进先出(LIFO, Last In First Out)的原则
自动管理:栈的内存是由编译器自动分配和释放的,程序不需要手动管理。
存储局部变量和函数调用信息:栈用于存储局部变量(如函数内部定义的变量)以及函数调用的参数、返回地址等信息。
内存空间有限:栈的大小通常是有限的,因为它的内存是为单个线程分配的固定大小。栈溢出(Stack Overflow)可能发生在递归深度过大或者分配的局部变量过多时。
快速分配与释放:由于栈是自动管理的,其内存分配和释放速度非常快,只需调整栈指针即可。
存储方式:内存按照严格的顺序(LIFO)进行分配。每次调用函数时,都会在栈上为其分配一块空间,函数结束后,这块空间会被立即释放,如果采用取地址的方式对栈进行访问,会等到不同于第一次访问得到的乱码
1 |
|
c
和p
会存储在栈中,函数返回时,这些变量会被自动释放,无法通过指针再次访问
2. 堆(Heap)
堆是内存中用于动态分配的区域,程序员可以通过函数(如malloc()
、calloc()
、realloc()
等)手动管理堆内存。
- 手动管理:堆中的内存是由程序员通过函数手动分配和释放的。分配的内存不会自动释放,程序员需要显式地调用
free()
函数释放内存。 - 适合动态内存分配:堆适合用于动态内存分配,可以根据程序的需要分配任意大小的内存,这在程序需要灵活的内存管理时非常有用。
- 内存空间较大:堆的内存通常比栈大得多,但堆的内存分配速度通常比栈慢,因为它需要找到合适大小的空闲内存块,并进行更多的管理操作。
- 内存碎片问题:由于堆中的内存分配和释放是动态的,频繁的分配和释放操作会导致内存碎片问题,即大量的小块未使用的内存分散在堆中,影响内存利用率。
- 访问较慢:由于堆中的内存分配不如栈的内存分配有序,因此访问堆中的内存通常比栈中的内存要慢。
1 |
|
3. 栈与堆的区别
属性 | 栈(Stack) | 堆(Heap) |
---|---|---|
内存分配 | 自动由编译器管理 | 手动管理(通过malloc() 、free() 等函数) |
内存大小 | 较小(通常为几MB,因平台而异) | 较大(受限于系统可用内存) |
存储内容 | 局部变量、函数参数、函数返回地址等 | 动态分配的内存(如动态数组、链表节点等) |
分配速度 | 快速(由编译器完成) | 较慢(需要手动分配,查找合适的内存块) |
管理方式 | 后进先出(LIFO) | 无特定的管理方式,基于内存池和自由链表等技术 |
释放内存 | 自动(函数返回时释放) | 手动(必须调用free() 释放) |
内存碎片问题 | 不会产生碎片 | 频繁分配和释放会产生内存碎片 |
访问速度 | 较快(顺序访问) | 较慢(随机访问,查找耗时) |
常见问题 | 栈溢出(Stack Overflow) | 内存泄漏、碎片问题 |
函数
函数是一个独立的代码片段,完成某个特定的任务。它可以接收输入(参数),并返回结果,通常用于实现一些经常使用的功能,可以减少代码的重复,提高代码的可读性和可维护性。
标准函数
1 |
|
函数定义的组成:
返回类型:函数返回的值的类型。例如,
int
表示函数返回一个整数,void
表示函数不返回任何值。函数的类型即他的返回值类型,具体种类参考数据类型列表
函数名:标识函数的名称,程序通过名称调用函数。函数名储存了函数的入口地址,其本质是个指针
参数列表:传递给函数的输入数据,可以是多个参数,每个参数都有其类型。参数列表位于括号内,如果没有参数,括号为空。
函数体:函数执行的具体代码块。
返回值:通过
return
语句返回给调用者的结果,如果函数的返回类型是void
,则不需要return
值。
库函数
通过导入库来调用的函数,称之为库函数,例如
#include <string.h>
包含 :
**printf
**:用于格式化输出。
**scanf
**:用于从标准输入读取数据。
**strlen
**:用于计算字符串的长度。
**strcpy
**:用于复制字符串。
#include <stdlib.h>
包含 :
**malloc
**:用于动态内存分配。
内联函数
内联函数建议编译器在函数调用处展开函数代码,而不是进行真正的函数调用,避免了函数调用的开销,从而提高性能,即在return
处写计算代码,省略了函数体
1 | inline int add(int a, int b) { //形参中定义变量 |
函数指针
函数在 C 语言中可以通过指针来引用,这使得可以动态选择要调用的函数
1 | int add(int a, int b) { |
递归函数
C 语言支持递归,即一个函数可以在其定义中调用自身。递归用于解决分解为较小子问题的复杂问题,递归函数题目的关键是找到公式
考试爱考
1 | //阶乘问题 |
递归问题对数学思维能力有极高的要求,作为一个普通人,我选择多看多记
递归问题头文件
在 C 语言中,头文件(.h
文件)是一个包含常量、函数原型、数据类型定义、宏定义等的文件。头文件的主要作用是让多个 .c
文件共享声明,以实现代码的重用性和模块化,并且头文件能够避免重复定义,提升代码的可读性和维护性
在复杂的项目中,可能会有多个文件包含同一个头文件,这样会造成重复包含,导致编译错误。为了防止这种情况,通常会在头文件中添加包含保护 : #pragma once
用于使头文件仅被调用一次,
ifndef
和 #endif
的使用
通过 #ifndef
和 #define
的组合,确保头文件的内容只会被处理一次
#ifndef
是 “if not defined” 的缩写,意思是如果宏EXAMPLE_H
没有定义,则继续执行后面的代码。而#define EXAMPLE_H
则是在第一次进入时定义这个宏。这样做的目的是确保头文件不会被多次包含#endif
是用于结束这个条件编译块,它标志着#ifndef
块的结束
1 | // example.h |
其中的fuc_print(int f)
是其他.c
文件中定义的函数,将其在头文件中声明可被其它.c
文件的调用
用法
要在一个 C 文件中使用头文件,通常需要使用 #include
预处理指令。它的作用是将头文件的内容复制到包含它的源文件中。包含头文件有两种方式:
- 尖括号方式:
#include <file.h>
用于包含系统库文件或标准头文件。编译器会在系统预定义的目录中查找这些文件。 - 双引号方式:
#include "file.h"
用于包含用户定义的头文件。编译器会首先在当前目录查找头文件,如果找不到,再到系统目录查找,通常使用此种方法
1 | /// C_L_Header.h |
1 | ///C_Language_Learning_TEST.c |
此案例使用了头文件来省略库文件的导入
1 | // test |
此案例使用头文件对C_Language_Learning_TEST.c
中的int fuc_print(int f)
实现调用
注意,一个项目中只能有一个main
函数,他是程序执行的入口,当出现复数main
时会导致程序无法找到入口,从而报错
局部变量与全局变量
全局变量(不要用! 不要用!! 不要用!!!)
全局变量是指在所有函数之外定义的变量,可以被程序中所有函数访问和修改,它的作用域是从变量定义开始,直到程序结束为止,在整个程序的生命周期内存在,
作用域:全局变量的作用域是整个程序,即可以在程序中的任何地方被访问(在同一源文件或通过
extern
声明的其他源文件)且全局变量常用于在多个函数之间共享数据,而不需要显式传递参数生命周期:全局变量从程序开始执行时创建,并且直到程序结束时才会被销毁。
内存位置:全局变量通常存储在静态数据区(静态存储区)中,而不是栈或堆中
1 |
|
注意事项:
- 命名冲突:全局变量如果和局部变量同名时,局部变量会覆盖全局变量的作用域。
- 全局变量修改容易影响其他函数:由于全局变量可以被任何函数修改,可能会造成意外的数据修改。因此,需要小心使用全局变量,避免在复杂程序中难以跟踪的错误,故此我们一般避免使用全局变量
局部变量
局部变量是指在函数或代码块内部定义的变量,它只能在该函数或代码块内部使用,在该范围之外是不可见的,局部变量通常用于函数的内部计算,不需要与其他函数共享数据,但可通过接口将局部变量传值给外部函数
作用域:局部变量的作用域仅限于定义它的函数或代码块。它在函数或块外是不可见的。
生命周期:局部变量的生命周期开始于函数或块的执行,结束于函数或块的结束。每次函数调用时,都会创建新的局部变量。
内存位置:局部变量通常存储在栈中,函数结束时会自动销毁
1 |
|
注意事项:
- 作用范围受限:局部变量只能在它定义的函数或代码块内使用,不能在其他函数中访问。
- 局部变量不保留值:每次进入函数时,局部变量都会重新创建,之前的值不会保留。
- 栈溢出:过多的局部变量会导致栈内存不足,导致程序栈溢出(特别是在递归调用中)
sp.静态局部变量
静态局部变量是局部变量的一种特殊形式,它的生命周期是程序的整个执行过程,但它的作用域仍然局限在定义它的函数中,静态局部变量在程序执行期间只被初始化一次,并且它的值在函数调用结束后仍然保持
- 在局部变量的定义前加上关键字
static
即可定义静态变量
1 |
|
适合用来当全局计数器
构造体
构造体(struct
)是C语言中非常重要的特性,它允许将不同类型的变量组合在一起,形成一种更复杂的数据类型
标准构造体
- 定义一个
struct
类型的变量时,需要使用struct
关键字,后跟结构体标签,例如struct name { }
进行构造体定义 - 在函数中使用时应先进行实例化
struct name instancename;
- 通过**
.
(点操作符)**来访问构造体成员,例如instance mane.parameter = value
进行指名修改值
1 |
|
sp.构造体初始化也可有struct ababa_s s={0};
,其含义为所有值均为0
构造体数组
利用构造体可以储存不同类型数值的特性,可以定义构造体数组,用于存储同类型的多个结构体变量,常用于构建信息列表
1 |
|
构造体指针
可以使用指针来指向构造体变量,并通过箭头操作符 ->
访问成员
1 |
|
值的注意的是指针p
每次加1,就会跳转到构造体数组下一个索引的起始位置
匿名构造体
在定义时可以省略标签,这种结构体被称为匿名结构体,适用于仅需要一次的结构体定义
1 | struct { |
使用typedef
另命名构造体
typedef
声明新的类型名来代替已有的类型名,方便后续调用
1 |
|
typedef int INTEGER;
会在项目需要修改一类数据的类型时起到方便修改的作用
构造体的嵌套
可实现多个构造体的统一调用,使用.
来进行层级穿透实现对子构造体实例的调用
1 |
|
构造体对齐
一般内存对齐
构造体中的成员会存储在内存中,但它们的存储位置通常会受对齐限制的影响。对齐是指编译器为提高CPU访问速度,对数据在内存中的存储方式进行调整。具体来说,编译器会根据对齐规则决定每个成员的存储位置,可能在成员之间插入一些“填充字节”(也叫做“空洞”)
- 基本对齐规则:构造体中每个成员的存储地址必须是该成员大小的整数倍。
- 整体对齐规则:构造体的总大小必须是最大成员大小的整数倍。
1 | struct Example { |
char a
:占用1个字节,但为了对齐,接下来3个字节会被填充(padding),使得b
的地址是4字节对齐。int b
:占用4个字节。char c
:占用1个字节,但为了对齐整个结构体的大小为最大成员(4字节int
)的整数倍,最后会再填充3个字节。
强制对齐
C语言允许使用**#pragma pack
**指令来改变默认的对齐方式,减少内存浪费,但可能会影响访问速度
1 |
|
此时,该结构体的大小变为6字节,没有填充字节
共用体
共用体(union
)是C语言中的一种数据结构,与结构体类似,但它们的内存分配方式不同。在共用体中,所有成员共享同一块内存空间,因此同一时间只能存储一个成员的值,这使得共用体的大小等于其最大成员的大小。共用体主要用于节省内存,特别适合在需要在不同时间存储不同类型的数据的场景下使用。
存储覆盖:由于共用体的所有成员共享内存空间,修改一个成员的值会覆盖其他成员的值。
大小:共用体的大小等于其最大成员的大小,而不是所有成员大小之和。
初始化:可以只对一个成员进行初始化,后续对其他成员赋值会覆盖之前的数据。
用途场景:适用于需要节省内存的场景,或需要在同一位置以不同方式解释数据的场景,比如网络数据包、硬件寄存器操作等。
共用体的定义与结构体类似,使用关键字 union
。语法格式如下:
1 |
|
在这段代码中,由于共用体 data
的成员共享同一块内存,赋值给 data.f
后会覆盖 data.i
的值。同理,赋值 data.str
后也会覆盖之前的成员值。
共用体的优点
- 节省内存:特别是在嵌入式系统或内存紧张的场景中,可以用共用体减少内存使用。
- 数据解析:共用体可以用来解析复杂的数据结构,例如解析协议报文,可以用不同成员表示同一数据的不同解释方式。
- 数据转换:在数据转换中,可以用共用体将数据视为不同类型处理,比如将
int
直接看作char
数组处理。
他或许可以作为一个可接收多数据类型但只保留最后一种输入类型的存储池,若是能有数据类型识别算法,应该可以实现无限制输入…..
共用体与结构体的区别
特性 | 共用体 (union ) |
结构体 (struct ) |
---|---|---|
内存分配 | 所有成员共享同一块内存空间,只分配最大成员的内存大小。 | 每个成员都有独立的内存空间,总大小是所有成员大小之和。 |
同时存储多个成员 | 不可以,只能同时存储一个成员的值。 | 可以,每个成员的值独立存在。 |
用途 | 节省内存,在不同时间存储不同类型的变量。 | 管理多个相关变量。 |
共用体与构造体的组合
将共用体作为数据传入构造体的中间商,使得构造体实例可以调用共用体中的数据
1 |
|
共用体Data
中的int
、float
和char
共享同一块内存。每次赋值时,前一个值会被覆盖
C++引用
C++是完全兼容C的,故此在正式学习数据结构前C++的个别语法可极大方便数据操作
C++&
引用
在函数参数列表中使用引用(例如 int &b
),告诉编译器不要创建参数的副本,而是直接使用调用函数时传入的变量,在这种情况下,b
成为传入变量的一个别名,可以直接操作这个变量的内容,而不需要通过指针或重新复制数据
1 |
|
C++bool
没啥好说的,是个人都认识
1 |
|
C BASE GRAMMER OVER
- Title: C_Note
- Author: Slience_Displace
- Created at: 2024-10-14 00:00:00
- Updated at: 2024-11-27 15:05:07
- Link: https://mikumikudaifans.github.io/Displace.github.io/2024/10/14/C_Note/
- License: This work is licensed under CC BY-NC-SA 4.0.