C语言入门笔记 | 豆包MarsCode AI刷题

191 阅读53分钟

C _Base Grammars

基础语法

标准代码架构

#include <stdio.h>  // 标准输入输出头文件,用于使用 printf 和 scanf 函数// 定义一个常量宏
#define PI 3.14159// 声明一个结构体
struct Person {
    char name[50];  // 字符数组,用于存储名字
    int age;        // 整数类型,用于存储年龄
};
​
// 函数声明
void greet(struct Person p);
int add(int a, int b);          // 两个整数相加的函数
void modifyValue(int *ptr);     // 使用指针修改变量值的函数// 主函数,程序的入口
int main() {
    // 变量声明与初始化
    int x = 10;  // 声明一个整型变量并赋值
    float radius = 5.5;  // 声明一个浮点型变量并赋值
    double area;  // 声明一个双精度浮点型变量
    
    // 条件判断语句 if-else
    if (x > 5) {
        printf("x is greater than 5.\n");
    } else {
        printf("x is less than or equal to 5.\n");
    }
​
    // 数组的声明与初始化
    int numbers[5] = {1, 2, 3, 4, 5};  // 声明一个整型数组
    char message[] = "Hello, World!";  // 声明并初始化字符数组(字符串)
​
    // 使用循环遍历数组
    printf("Array elements: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");
​
    // 计算圆的面积
    area = PI * radius * radius;
    printf("The area of the circle with radius %.2f is %.2f\n", radius, area);
​
    // 调用函数
    int sum = add(5, 7);  // 调用加法函数
    printf("The sum of 5 and 7 is %d\n", sum);
​
    // 指针的使用
    int value = 20;
    printf("Original value: %d\n", value);
    modifyValue(&value);  // 使用指针修改变量的值
    printf("Modified value: %d\n", value);
​
    // 使用结构体
    struct Person person1;  // 声明一个结构体变量
    person1.age = 30;       // 赋值
    strcpy(person1.name, "Alice");  // 使用 strcpy 函数复制字符串
    greet(person1);  // 调用 greet 函数,传递结构体变量
​
    // 指针与数组
    int *ptr = numbers;  // 指向数组的指针
    printf("First element using pointer: %d\n", *ptr);  // 通过指针访问数组第一个元素
​
    return 0;  // 主函数返回 0 表示程序正常结束
}
​
// 函数定义:用于打印欢迎信息
void greet(struct Person p) {
    printf("Hello, %s! You are %d years old.\n", p.name, p.age);
}
​
// 函数定义:两个整数相加并返回结果
int add(int a, int b) {
    return a + b;
}
​
// 函数定义:使用指针修改值
void modifyValue(int *ptr) {
    *ptr = 50;  // 通过指针修改传入的变量值
}
​

常量

常量在设置后是不能更改的

C语言中的常量可以根据其类型分为以下几类:

常量类型描述示例
整型常量表示整数的常量,可以是十进制、八进制、十六进制表示。100, 0123(八进制), 0x1A(十六进制)
浮点常量表示小数的常量,包括科学计数法。3.14, 1.0e-2
字符常量表示单个字符的常量,必须用单引号包围。'A', '9', '\n'
字符串常量表示一串字符的常量,必须用双引号包围。"Hello", "C语言"
符号常量#defineconst关键字定义的具有固定值的标识符常量。#define PI 3.14
常量宏

常量宏通常使用#define指令来定义,在预处理阶段会将宏的名称替换为其定义的值,其本质只是简单的文本替换,因此,宏不会进行类型检查、作用域控制等,容易导致一些难以发现的错误

故此,目前阶段仅将其作为文本替换器使用

//全局常量//字符型常量
#define str_a = 'A'
#define str_a = '\n'  //'\n'为转义字符,用于字符串换行  ;  '\b',退格  ;  '\'反斜杠即'/'//常量宏
#define Π 3.141  //#define 常量宏名 常量值//符号常量
#define PI (3+2)  //#define 常量名 含有符号的数字组合
int main()  //函数需定义为int类型,才能在函数中定义并初始化int变量
{
    int a = PI*2;  //这里实际的运算状况是 3+2*7 相当于直接将PI移过来了
    printf("i = %d\n",a);  //输出7,
    
    return 0;
}

变量

变量类型描述范围(根据实现)示例
int整型变量,用于存储整数。-32,768 到 32,767 (16位系统),较常见是32位系统int x = 5;
float单精度浮点数,用于存储小数。约为 ±3.4e–38 到 ±3.4e+38float y = 3.14;
double双精度浮点数,表示更大范围和更精确的小数。±1.7e–308 到 ±1.7e+308double z = 2.71828;
char字符变量,存储单个字符(ASCII码)。-128 到 127char c = 'A';
long长整型,存储更大的整数。-2^31 到 2^31-1 (32位系统)long n = 1000000;
short短整型,存储较小的整数。-32,768 到 32,767short s = 32767;
unsigned无符号整型,用于存储非负数。0 到 65535 (16位系统)unsigned int u = 5;
//变量
int x = 5;  //数据类型 变量名 = 值//混合运算中的变量强制转换
int main(){
    int i = 5;
    float f = i/2; //输出结果为2.000  
    //因为左右操作数均为整形变量,i/2的整形运算结果会省去小数变为2
    
    float f = (float)i/2; //(数据类型)变量名  
    //此时结果为2.5,由此也可推断混合运算的运算方法以左操作数为准
}

注意,在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( )
#include <stdio.h>int main() {
    int a = 1234;
    char c = 'A';
    float e = 1.3;
         
    printf("Integer: %d\n Character: %c\n", a, c); //printf("字符串%格式化类型" ,数据)    
    //注意 当改行代码有多个变量需要被打印时C语言是按照从左向右的顺序来识别参数的,故参数顺序需一一对应
   
    //修饰符混合使用
    printf("Integer: %7d\nFloat: %-7.3f\n", a, e);  
    // %-7.3f 意思为 左对齐;字符最小宽度为7;浮点精度为小数点后3位;浮点数据类型
        
    
}   
puts( )

输出一个字符串到标准输出(通常是显示器)。puts() 会自动在输出的字符串末尾加上一个换行符

int puts(const char *str);

  • 参数 str 是要输出的字符串。
  • 返回值为非负整数,表示写入的字符数量。如果发生错误,返回 EOF(即 -1)。
#include <stdio.h>
int main() {
    char str[] = "Hello, World!";
    puts(str);  // 输出字符串并自动换行
    return 0;
}
常用格式说明符
格式说明符描述示例
%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_sscanf 的安全版本,要求为字符串输入提供额外的参数,指定缓冲区的大小,以防止缓冲区溢出。在传入数组时缓冲区大小需要和数组长度相等,否者会出现致命bug,(unsigned)sizeof(a) 是常用的自主获取组长度的方法,传入单个数字时则不需要额外参数

scanf_s 向参数传入数据时实际是向该数据的内存地址传值,因此需写为&变量名

#include <stdio.h>int main()
{
    //传入数字
    int num;
    printf("enter a number:");
    scanf_s("%d",&num); //传入数据时实际是向该数据的内存地址传值,因此需写为&变量名
    printf("you enter number is %d\n", num);
    
    //传入字符串
    char a[10];
    printf("enter a string(max length is 9):");
    //scanf_s("%s",a,10);
    scanf_s("%s", a, (unsigned)sizeof(a));  // 传入数组a以及它的大小
    printf("you enter string is %s\n",a);
​
    //混合传入
    char c[10];
    float f[10];
    printf("enter a string and float (max length is 9):\n");
    //scanf_s("%s",a,10);
    scanf_s("%s",c, (unsigned)sizeof(c)); 
    scanf_s("%f",&f[0]); //数组只以单个索引的形式传入值
    printf("you enter string is %s\nyou enter float is%f\n",c,f[0]);
    
​
    return 0;
}
​
//scanf已被启用,采用更安全感的scanf_s

(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() 会在读取到换行符或者到达指定字符数时停止

//gets&fgets
#include <stdio.h>int main()
{
    char f[10];
    printf("enter a string and float (max length is 9):\n");
    //gets(f);
    fgets(&f,sizeof(f), stdin); //fgets(字符数组变量名,数组长度,sthdin)
    printf("you enter int is %s\n", f);
    
    return 0;
}

sp : get( )在获取字符串并传给数组时会自动在末尾加上\0

格式说明符

scanf 函数根据 格式说明符 将输入的字符转换为相应的数据类型。以下是常用的格式说明符:

数据类型格式说明符示例代码示例输入说明
int%dscanf_s("%d", &num);123读取一个十进制整数。
float%fscanf_s("%f", &flt);3.14读取一个浮点数。
double%lf3.1415读取一个双精度浮点数。
char%cscanf_s(" %c", &ch);A读取一个字符(包括空白字符)。
字符串%sscanf_s("%s", str,(unsigned)sizeof(str));hello读取字符串,遇到空白符停止。
无符号整数%u123读取一个无符号整数。
八进制整数%o017读取一个八进制整数。
十六进制整数%x0x1F读取一个十六进制整数。
长整数%ld123456读取一个长整型变量。
长长整数%lld12345678读取一个长长整型变量。
指针%p0x7ffee读取一个指针类型的地址。

运算符

1. 算术运算符

算术运算符用于执行基本的数学运算,如加法、减法、乘法、除法等。

运算符描述示例结果
+加法a + bab 相加
-减法a - ba 减去 b
*乘法a * ba 乘以 b
/除法a / ba 除以 b
%取模(余数)a % ba 除以 b 的余数
++自增++aa++a 递增 1
--自减--aa--a 递减 1
  • ++aa++:前者是前置自增(先加再用),后者是后置自增(先用再加)。
  • --aa--:类似地,分别为前置和后置自减。
2. 关系运算符
#include <stdio.h>
int main() 
{
      int a = 123;
error if (3 < a < 10)}  //比较运算符无法进行中间值判断
    //由于比较运算符返回的是0和1值,因此 3 < a < 10 实际执行的是result=3<a和result<10,因为result必定为0或1,故此判断式无效
      if (3 < a && a < 10) //正确写法

关系运算符用于比较两个操作数,结果返回布尔值 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. 逻辑运算符
#include <stdio.h>

int main() 
{
    int i = 1; 
    i && printf("y ct see me \n");  //此时i被当作一个判断值使用
    //逻辑与短路运算,当i为0时不执行,i为1时执行逻辑语后的表达式
    //等价于下列if语句
    if (i)
    {
        printf("y ct see me \n");
    }
    
    
    
    int a = 1; 
    a || printf("y ct see me \n");  //此时i被当作一个判断值使用
    //逻辑或短路运算,当a为1时不执行,i为0时执行逻辑语后的表达式
    //等价于下列if语句
    if (!a)
    {
        printf("y ct see me \n");
    }
    
    return 0;
}

逻辑运算符用于布尔运算,常用于条件语句中的复杂判断。

运算符描述示例结果
&&逻辑与a && b如果 ab 都为真,返回 1;否则返回 0
逻辑或ab如果 ab 任一个为真,返回 1;否则返回 0
!逻辑非!a如果 a 为假,返回 1;否则返回 0
4. 位运算符

位运算符用于位级操作,操作数被视为位模式而非数值。

运算符描述示例结果
&按位与a & bab 按位与
按位或abab 按位或
^按位异或a ^ bab 按位异或
~按位取反~aa 的按位取反
<<左移a << 2a 左移 2 位
>>右移a >> 2a 右移 2 位
5. 赋值运算符

赋值运算符用于为变量赋值,通常可以结合算术运算符进行简化操作。

运算符描述示例结果
=赋值a = bb 的值赋给 a
+=加后赋值a += ba = a + b
-=减后赋值a -= ba = a - b
*=乘后赋值a *= ba = a * b
/=除后赋值a /= ba = a / b
%=取模后赋值a %= ba = a % b
<<=左移后赋值a <<= 2a = a << 2
>>=右移后赋值a >>= 2a = a >> 2
&=按位与后赋值a &= ba = a & b
^=按位异或后赋值a ^= ba = a ^ b
=按位或后赋值a= ba = ab
6. 条件运算符(三元运算符)

条件运算符用于根据条件的真假执行不同的表达式。

运算符描述示例结果
? :条件表达式(类似 if-else)a ? b : c如果 a 为真,执行 b,否则执行 c
7. 其他运算符
#include <stdio.h>
int main()
{
    int a = 0;
    int c = sizeof(a); //计算数据的字节大小
    printf("num size is %d\n", c); 
    return 0;
}

除了上述常用的运算符,还有一些其他的运算符,包括取地址、取值、大小、逗号等。

运算符描述示例结果
&取地址&a返回变量 a 的地址
*指针解引用*p返回指针 p 指向的值
sizeof计算数据类型大小sizeof(a)返回 a 的字节大小
,逗号表达式a = (x++, y++)先执行 x++,再执行 y++
->结构体指针成员访问p->member访问指针 p 指向的结构体的成员
.结构体成员访问s.member访问结构体 s 的成员
7.运算符优先级

{% folding blue::算数 > 关系 > 逻辑 :表格如下 %}

C语言运算符优先级表

优先级最高符号名称或含义使用形式结合方向说明
1()括号表达式左结合
[]数组下标表达式左结合
->取结构体成员(指针)表达式左结合
.取结构体成员(非指针)表达式左结合
sizeof返回数据类型大小表达式左结合
2!逻辑非表达式右结合
~位取反表达式右结合
++自增表达式右结合
--自减表达式右结合
-负号表达式右结合
*指针表达式右结合
&取地址表达式右结合
3*表达式左结合
/表达式左结合
%取余表达式左结合
4+表达式左结合
-表达式左结合
5<<左移表达式左结合
>>右移表达式左结合
6<小于表达式左结合
<=小于等于表达式左结合
>大于表达式左结合
>=大于等于表达式左结合
7==等于表达式左结合
!=不等表达式左结合
8&按位与表达式左结合
9^按位异或表达式左结合
10按位或表达式左结合
11&&逻辑与表达式左结合
12逻辑或表达式左结合
13? :条件运算符表达式右结合
14=赋值表达式右结合
+=加赋值表达式右结合
-=减赋值表达式右结合
*=乘赋值表达式右结合
/=除赋值表达式右结合
%=取余赋值表达式右结合
<<=左移赋值表达式右结合
>>=右移赋值表达式右结合
&=按位与赋值表达式右结合
^=按位异或赋值表达式右结合
=按位或赋值表达式右结合
15,逗号表达式左结合

考研复试可能会用

{% endfolding %}

逻辑语句

逻辑语句概述

注意 逻辑语句后一般不加分号(;)

#include <stdio.h>

int main() {
    int a = 5, b = 10;

    // if...else 语句
    if (a > b) {
        printf("a is greater than b\n");
    } 
    else if(a = b){
        printf("a is equcal than b\n");
    }
    else {
        printf("a is not greater than b\n");
    }

    // 逻辑与、逻辑或
    if (a < b && b > 0) {
        printf("Both conditions are true\n");
    }

    // 三元运算符
    int max = (a > b) ? a : b;
    printf("The maximum is %d\n", max);

    // switch 语句
    switch (a) {
        case 5:
            printf("a is 5\n");
            break;
        case 10:
            printf("a is 10\n");
            break;
        default:
            printf("a is not 5 or 10\n");
    }
    
    //for语句
    int length = 10;
    for (size_t t = 2; t < length; t++)  //for(初始化变量;判断条件;变量变化表达式)
        //for语句第一次循环 用初始量去判断条件,执行语句,执行变量变量变化表达式
        //二次后的循环则是 判断条件,执行语句,执行变量变量变化表达式
    {
        printf("cool %d\n",t);
    }

    return 0;
}

C 语言中的逻辑语句主要包括逻辑运算符条件语句。逻辑语句用于控制程序的执行流程,常用的逻辑语句有 ifelse ifelseswitchwhilefordo...while 等。

1. 逻辑运算符

逻辑运算符用于连接条件表达式或进行条件判断。C 语言中的逻辑运算符如下表所示:

运算符名称用法结果
&&逻辑与(AND)expr1 && expr2expr1expr2 均为真,结果为真,否则为假
逻辑或(OR)expr1 逻辑或 expr2expr1expr2 任一真,结果为真
!逻辑非(NOT)!exprexpr 为真,结果为假,反之亦然
2. 条件语句

C 语言中的条件语句用于根据不同的条件执行不同的代码块。以下是常见的条件语句:

语句用法解释
ifif (condition) { /* code */ }condition 为真时,执行 { /* code */ }
if...elseif (condition) { /* code1 */ } else { /* code2 */ }如果 condition 为真,执行 code1,否则执行 code2
else ifif (condition1) { /* code1 */ } else if (condition2) { /* code2 */ } else { /* code3 */ }多条件判断,第一个为真的条件执行相应的代码块
switchswitch (expression) { case val1: /* code */ break; case val2: /* code */ break; default: /* code */ }根据 expression 的值选择执行相应的 case 代码块
3. 循环语句

C 语言中的循环语句用于重复执行某一代码块,直到满足指定的条件。

语句用法解释
whilewhile (condition) { /* code */ }condition 为真时,重复执行 { /* code */ }
do...whiledo { /* code */ } while (condition);先执行一次代码块,再根据 condition 判断是否继续
forfor (init; condition; increment) { /* code */ }常用循环,按照初始化、条件判断、增量控制的顺序执行
4. 三元运算符

三元运算符是简化 if...else 语句的方式,语法如下:

运算符用法解释
?:condition ? expr1 : expr2condition 为真时,返回 expr1,否则返回 expr2
5.continue和break
语句用法解释
continuecontinue;当循环语句中出现 continue 时,跳过本次循环,执行下一次循环
breakbreak;当循环语句中出现 break 时,打断循环,执行后续语句

数组

数组类型用法示例访问方式特点
一维数组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]传递指针,需要传递数组大小。

一维数组

#include <stdio.h>
int main()
{
    // 声明并初始化
    int a[10] = { 1,2,3,4,5,6,7,8,9,10 }; //数组类型 数组名[数组大小] = { }
    int b[] = {1,2,3,4,6,8,90,34,3,312} //无数组大小的初始化方式,编辑器会自动匹配数组大小
    int length_b = sizeof(a) / sizeof(int); //获取数组长度,方便后续操作
    
    int arr[5];  // 声明一个长度为5的整型空数组  此时数组里的值均为0
    
    
    //访问数组
    // 给数组的第3个元素赋值
    arr[2] = 10;  //数组名[索引号] = 值
    
    // 获取第1个元素的值
    int x = arr[0];  // 变量名 = 数组名[索引号]
    
    //遍历数组
    for(int i = 0; i < 5; i++) {
    printf("%d ", arr[i]);
    }

    return 0;
}

主要 数组索引从0开始,因此大小为10的数组实际索引为0到9

关于数组初始化 :

通常可在数组后添加一个新的变量来动态的记录数组长度,常用的以为数组长度算法为int length_a = sizeof(a) / sizeof(int);

关于数组越界的问题 :

在访问数组时,如果使用的索引超出了数组的有效范围,可能会导致不可预知的行为或程序崩溃。从内存层面上,溢出的数组值会和该数组相邻的变量产生冲突,使得该变量获得错误的值,Visual Studio 2022会自动检测数组下标溢出问题,并阻止编译,但其他编译器仍可能出现问题

多维数组

#include <stdio.h>
int main()
{
    int matrix[3][4];  // 3行4列的二维数组
    
    int matrix[2][3] = {  //初始化
        {1, 2, 3},
        {4, 5, 6}
    };    
    
    matrix[1][2] = 10;  // 设置第2行第3列的值为10

    return 0;
}
多维数组的内存结构

二维数组在内存中是按行连续存储的。例如,matrix[2][3] 在内存中的存储顺序为:matrix[0][0], matrix[0][1], matrix[0][2], matrix[1][0], matrix[1][1], matrix[1][2]

数组传递

#include <stdio.h>

// 函数原型声明,确保在 main 函数之前声明 print_a 函数
void print_a(int a[], int length);

int main()
{
    int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int length_a = sizeof(a) / sizeof(int);
    
    print_a(a, length_a); // 调用 print_a 函数
    printf("length_a = %d\n", length_a); // 输出数组长度,输出为10
    
    return 0;
}

void print_a(int a[], int length)  //数组a传递进print_a时传递的不是数组本身,而是指针
{
    int length_a_p = sizeof(a) / sizeof(int); //此时sizeof(a)的值为它指针的大小,即8byte,故length_a_p此时的值并不能反映数组a的大小
    printf("length_a_p = %d\n", length_a_p); //输出为2
    
    int i;
    for (i = 0; i < length; i++)
    {
        printf("%d\n", a[i]); 
    }
}

数组传递是按指针传递的,你传递的是数组首元素的地址,而不是整个数组。数组名退化为指针

数组的长度信息不会自动传递,数组长度信息丢失,需要手动传递长度。上述代码提供了一个可行的长度传递逻辑

指针传递使得函数可以修改原数组的值,因为指针指向的是相同的内存地址。

字符数组

#include <stdio.h>

int main()
{
    char str[6] = { 'H', 'e', 'l', 'l', 'o', '\0' };  // 定义并初始化一个字符串。
    //此方法有诸多弊端,例如结尾符忘写会输出乱码
    char str2[] = "Hello";  // 字符串的简化初始化形式。通常使用这种方法初始化


    int i;
    for (i = 0; i < 6; i++)
    {
        printf("str = %c\n", str2[i]);  //由于数组字符初始化时最后以为必定为\0,故长度为6的字符数组实际有效值只有5个,有效索引为0到4
    }

    printf("str = %s\n", str); //字符数组可通过%s格式化来整体输出

    return 0;
}
关于字符数组初始化

字符数组是用于存储字符的数组,通常用于存储和处理字符串。在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):计算变量所占的内存大小
// 对于数组大小的计算
#include <stdio.h>
int main() {
    int arr[10];
    printf("Size of array: %zu bytes\n", sizeof(arr)); //计算整个数组的大小
    printf("Size of one element: %zu bytes\n", sizeof(arr[0])); //计算单个数据的大小
    printf("Number of elements in the array: %zu\n", sizeof(arr) / sizeof(arr[0])); //真个数组大小/单个数据大小=数组长度  ***常用***
    return 0;
}
//对于构造体大小的计算
#include <stdio.h>
struct myStruct {
    int a;
    char b;
    float c;
};

int main() {
    struct myStruct s;
    printf("Size of struct: %zu bytes\n", sizeof(s)); //输出为12
    return 0;
}

尽管 int 是 4 字节,char 是 1 字节,float 是 4 字节,但结构体可能因为内存对齐而占用更多的字节数,详见构造体对齐章节

字符串操作函数str系列

该系列操作函数需调用<string.h>

在 C 语言中,string.h 头文件中提供了一些常用的字符串操作函数,包括 strlenstrcpystrcatstrcmp。这些函数专门用于处理以 '\0' 结尾的字符串

其中strcpystrcat由于其安全性的不足而被弃用,现用更安全的strcpy_sstrcat_s ,他们要求提供dest_size: 目标字符串的大小(总字节数,必须包含足够的空间来容纳源字符串和终止符 \0)来防止字符串溢出组中

dest_size 必须包含现有字符串、源字符串以及终止符 \0

如果目标缓冲区不够大,函数将不会追加字符串并返回错误。

如果目标或源字符串指针为 NULL,函数也会返回错误。

#include <stdio.h>
#include <string.h>
int main() {
    char str_x[] = "test";
    char str_b[30] = "Hello,World!";
    char str_a[6] = { 'H', 'e', 'l', 'l', 'o', '\0' };
    char str_c[20];


    int lenth_a;
    lenth_a = strlen(str_a); //字符串计数器不包含组结尾的\0,因此返回值比实际组长度小1
    printf("lenth of str_a is %d\n", lenth_a);
    printf("size of str_a is %d\n", 6);
    
    //字符串复制到另一字符串
    strcpy_s(str_c,sizeof(str_c), str_b);  //strcpy_s(目标组,目标组大小,源组)
    puts(str_c);


    //字符串尾追加字符串
    strcat_s(str_b,sizeof(str_b),str_x);  //strcat_s(目标组,目标组大小,源组)
    puts(str_b);
    

    //ASK码比较字符串大小
    int j = strcmp(str_x, str_b);
    printf("str_x campare with str_b %d\n",j);
    //strcmp依照阿斯克码表进行比较,比较的大小结果并非实际字符长度大小,实际用途不明

    return 0;
} 
函数功能返回值用法示例注意事项
strlen计算字符串长度返回字符串长度,不含 \0size_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 所指向的变量的值。
#include <stdio.h>

int main() {
    int a = 10;    // 普通变量
    int *p = &a;   // p是指向a的指针  // 数据类型 *指针变量名 = &变量名
    
    printf("a的值: %d\n", a);      // 输出a的值
    printf("a的地址: %p\n", &a);   // 输出a的地址
    printf("p的值: %p\n", p);      // 输出p的值(即a的地址)
    printf("p指向的值: %d\n", *p); // 输出p指向的值,即a的值

    return 0;
}

注意!指针的类型决定了指针指向的数据类型,比如:

  • int *p; 指向 int 类型的指针
  • char *p; 指向 char 类型的指针
  • float *p; 指向 float 类型的指针 指针类型决定了在解引用时如何解释内存中的数据。
指针变量和普通变量的区别:
特性普通变量指针变量
存储内容变量的值另一个变量的地址
访问方式直接访问变量值通过解引用访问指向的变量值
取地址符使用不需要需要使用*进行解引用
使用场景存储基本数据存储内存地址,操作复杂数据结构

指针传递

指针传递指的是通过函数参数传递指针(即变量的地址),从而使函数能够直接操作原始变量的值。这是C语言实现“传引用”功能的方式,因为C语言默认的参数传递是“传值”,即传递的是变量的副本,而不是变量本身。

#include <stdio.h>

int change_value(int *p) {
    printf("p的地址: %p\n", p);
    *p = 99; //通过解引用符*直接访问a所在的内存地址,实现对a的修改

}

int main() {
    int a = 10;    // 普通变量
    printf("a的值: %d\n", a);
    printf("a的地址: %p\n", &a);

    change_value(&a); //将a的地址传给形参p
    printf("a的值: %d\n", a);
  
    return 0;
}
  • change_value(int *p):这个函数接受一个int类型的指针p,即指向一个int类型变量的地址。
  • *p = 99;:通过解引用指针p,直接修改了指针指向的变量(即a)的值。

指针偏移

指针偏移指的是通过对指针进行算术运算来访问相邻的内存单元。它与数组访问紧密相关。指针偏移是通过修改指针的值,使其指向内存中的不同位置,从而访问相邻的元素

#include <stdio.h>

int main() {
    int arr[9] = {1,2,3,4,5,6,7,8,9};    // 普通变量
    int *p = arr; //数组名储存的是起始地址

    //*p = 99; //通过解引用符*直接访问a所在的内存地址,实现对a的修改

    //printf("%d", arr[0]);
    for (int i = 0; i < sizeof(arr)/sizeof(int); i++)
    {
        //正序
        printf("i=%d arr[i]=%d\n",i,*(p+i)); 
        //int的大小为4byte,指针+1相当于内存地址向后移动4位

        //倒序
        printf("i=%d arr[i]=%d\n", i, *(p + sizeof(arr) / sizeof(int)-1-i));
        //通过起始地址和数组大小定位到结束地址,反向递归
    }
  
    return 0;
}
  • int *p = arr;:指针p指向数组arr的第一个元素。
  • *(p + i):通过指针偏移,访问数组的第i个元素。这里的p + i表示指针p向后偏移i个位置,*(p + i)解引用偏移后的地址,得到对应的元素值。

动态内存分配

在C语言中,内存可以通过动态内存分配的方式进行管理。静态内存分配是在程序编译时确定的,如局部变量和全局变量,它们在程序运行时占用固定的内存。相比之下,动态内存分配是在程序运行时,通过显式调用特定的函数来申请或释放内存,内存的大小可以根据需要动态变化,且程序员需要手动释放不再使用的内存

动态内存的申请可以使用标准库中的函数malloc()calloc()realloc()等。最常用的函数是malloc(),它用于申请指定字节数的内存

1.malloc() 函数

使用malloc()前应先引入<stdlib.h>

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

int main() {
    //申请空间填入数组
    int* arr;
    int n = 5;

    // 动态分配内存以存储 5 个整数
    arr = (int*)malloc(n * sizeof(int));  //指针名=(指针类型)malloc(申请空间大小)

    // 检查内存是否分配成功
    if (arr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }

    // 使用数组
    for (int i = 0; i < n; i++) {
        arr[i] = i + 1;
    }

    // 打印数组元素
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    
    // 释放分配的内存
    free(arr); 
//-------------------------------------------------------------------------------//
    //申请空间填入字符串
    char* arr_str;
    char c_arr[30];
    int size_arr = sizeof(c_arr) / sizeof(char);
    printf("enter c_arr's value\n");
    scanf_s("%s", c_arr, (unsigned)sizeof(c_arr));
    //puts(c_arr);

    arr_str = (char*)malloc(size_arr);  //申请内存空间

    for (int i = 0; i < size_arr; i++)
    {
        arr_str[i] = c_arr[i];
    }

    for (int i = 0; i < size_arr; i++)
    {
        printf("arr[%d] is %c\n",i, arr_str[i]);
    }

    free(arr_str);
    return 0;
}
  • 内存申请:我们使用malloc()分配了n个整数大小的内存,并将其返回的通用指针转换为int*类型。
  • 内存使用:内存分配后可以像普通数组一样使用指针来访问。
  • 内存释放:使用完内存后,必须调用free()函数来释放之前动态分配的内存,以避免内存泄漏
2. calloc() 函数

calloc()函数用于动态分配内存,并将分配的内存初始化为0

// 分配 5 个整型,并初始化为 0
int* arr = (int*)calloc(5, sizeof(int));  

calloc()在初始化数组时非常有用,它会将所有分配的内存块初始化为0,而malloc()不会进行初始化。

3. realloc() 函数

realloc()函数用于重新调整已经动态分配的内存块的大小。它的语法如下:

 // 将原内存大小调整为可以存储 10 个整型
arr = (int*)realloc(arr, 10 * sizeof(int)); 

realloc()可以扩展或缩小之前分配的内存块。如果扩展,新的内存区域的内容是未初始化的

4. free() 函数

free()函数用于释放malloc()calloc()、或realloc()动态分配的内存。它的语法非常简单:

free(arr);

注意:释放后不能再访问释放的内存区域,否则会导致未定义的行为

栈与堆的差异

1. 栈(Stack)

栈是内存中的一块区域,用于存储局部变量、函数调用相关的信息(如返回地址、参数等)。它遵循后进先出(LIFO, Last In First Out)的原则

  • 自动管理:栈的内存是由编译器自动分配和释放的,程序不需要手动管理。
  • 存储局部变量和函数调用信息:栈用于存储局部变量(如函数内部定义的变量)以及函数调用的参数、返回地址等信息。
  • 内存空间有限:栈的大小通常是有限的,因为它的内存是为单个线程分配的固定大小。栈溢出(Stack Overflow)可能发生在递归深度过大或者分配的局部变量过多时。
  • 快速分配与释放:由于栈是自动管理的,其内存分配和释放速度非常快,只需调整栈指针即可。
  • 存储方式:内存按照严格的顺序(LIFO)进行分配。每次调用函数时,都会在栈上为其分配一块空间,函数结束后,这块空间会被立即释放,如果采用取地址的方式对栈进行访问,会等到不同于第一次访问得到的乱码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* stack_print()
{
    char c[20] = "i am so tired\n";//字符串数组c被存在栈中
    char* p;//指针p被存在栈中
    p = c; //将指针c赋值给p
    puts(p); //通过p的值锁定字符串c的起始地址,并开始打印字符串c中的值
    return p;
    //此步是自动在栈中执行的,执行并return完毕后字符串c和指针p的值都会被自动清除,后续无法再通过指针p来找到字符串c,
}

int main() {
    char* p;
    p = stack_print();
    printf("print again\n");

    puts(p); //即使传回存放着c地址的指针p,由于该地址中c的值也早已消失,故此步编译不通

    return 0;
}

cp会存储在栈中,函数返回时,这些变量会被自动释放,无法通过指针再次访问

2. 堆(Heap)

堆是内存中用于动态分配的区域,程序员可以通过函数(如malloc()calloc()realloc()等)手动管理堆内存。

  • 手动管理:堆中的内存是由程序员通过函数手动分配和释放的。分配的内存不会自动释放,程序员需要显式地调用free()函数释放内存。
  • 适合动态内存分配:堆适合用于动态内存分配,可以根据程序的需要分配任意大小的内存,这在程序需要灵活的内存管理时非常有用。
  • 内存空间较大:堆的内存通常比栈大得多,但堆的内存分配速度通常比栈慢,因为它需要找到合适大小的空闲内存块,并进行更多的管理操作。
  • 内存碎片问题:由于堆中的内存分配和释放是动态的,频繁的分配和释放操作会导致内存碎片问题,即大量的小块未使用的内存分散在堆中,影响内存利用率。
  • 访问较慢:由于堆中的内存分配不如栈的内存分配有序,因此访问堆中的内存通常比栈中的内存要慢。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* malloc_print() 
{
    char* p = (char*)malloc(20);
    char c[20] = "i am so tired\n";//字符串数组c被存在栈中
    strcpy_s(p,20,c);  //需要将c复制给p才能在方法结束时保留其中数据
    puts(p);
    return p;    
}

int main() {
    char* p;
    p = malloc_print();
    //printf("print finish");
    puts(p);
    //由于堆会在进程结束之前始终存在,故其中数据可重复读取
    free(p); //手动释放内存

    return 0;
}
3. 栈与堆的区别
属性栈(Stack)堆(Heap)
内存分配自动由编译器管理手动管理(通过malloc()free()等函数)
内存大小较小(通常为几MB,因平台而异)较大(受限于系统可用内存)
存储内容局部变量、函数参数、函数返回地址等动态分配的内存(如动态数组、链表节点等)
分配速度快速(由编译器完成)较慢(需要手动分配,查找合适的内存块)
管理方式后进先出(LIFO)无特定的管理方式,基于内存池和自由链表等技术
释放内存自动(函数返回时释放)手动(必须调用free()释放)
内存碎片问题不会产生碎片频繁分配和释放会产生内存碎片
访问速度较快(顺序访问)较慢(随机访问,查找耗时)
常见问题栈溢出(Stack Overflow)内存泄漏、碎片问题

函数

函数是一个独立的代码片段,完成某个特定的任务。它可以接收输入(参数),并返回结果,通常用于实现一些经常使用的功能,可以减少代码的重复,提高代码的可读性和可维护性。

标准函数

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

int fuc_print(int f);  // 函数声明

int main_1() {  //主函数
    int j = 2;

    j = fuc_print(j); // 调用fuc_print函数,函数()中填入实参,实现参数传递,当函数有返回值时,该值将被赋值给j

    printf("j is %d\n", j);

    return 0;
}

int fuc_print(int f) {   //函数类型 函数名字(形参){ 函数体 }
    printf("function is running\n");
    f = 8888;
    return f;  //函数返回值
}
函数定义的组成:
  • 返回类型:函数返回的值的类型。例如,int 表示函数返回一个整数,void 表示函数不返回任何值。

    函数的类型即他的返回值类型,具体种类参考数据类型列表

  • 函数名:标识函数的名称,程序通过名称调用函数。

  • 参数列表:传递给函数的输入数据,可以是多个参数,每个参数都有其类型。参数列表位于括号内,如果没有参数,括号为空。

  • 函数体:函数执行的具体代码块。

  • 返回值:通过 return 语句返回给调用者的结果,如果函数的返回类型是 void,则不需要 return 值。

库函数

通过导入库来调用的函数,称之为库函数,例如

#include <string.h>包含 :

printf:用于格式化输出。

scanf:用于从标准输入读取数据。

strlen:用于计算字符串的长度。

strcpy:用于复制字符串。

#include <stdlib.h>包含 :

malloc:用于动态内存分配。

内联函数

内联函数建议编译器在函数调用处展开函数代码,而不是进行真正的函数调用,避免了函数调用的开销,从而提高性能,即在return处写计算代码,省略了函数体

inline int add(int a, int b) { //形参中定义变量
    return a + b;  //return中进行计算
}

函数指针

函数在 C 语言中可以通过指针来引用,这使得可以动态选择要调用的函数

int add(int a, int b) {
    return a + b;
}

int main() {
    int (*funcPtr)(int, int);  // 定义一个函数指针
    funcPtr = &add;            // 将add函数地址赋给指针
    int result = funcPtr(2, 3);  // 通过指针调用函数
    
    printf("%d", result);  // 输出 5
    return 0;
}

递归函数

C 语言支持递归,即一个函数可以在其定义中调用自身。递归用于解决分解为较小子问题的复杂问题,递归函数题目的关键是找到公式

考试爱考

//阶乘问题
#include "C_L_Header.h"

int fuc_factorial(int f);  // 函数声明

int main() {
    int a = 0;
    printf("input int number\n");
    scanf_s("%d", &a);
    a = fuc_factorial(a);

    printf("number be factorialad is %d\n", a);
    return 0;
}

//递归函数
int fuc_factorial(int f) {
    if (f == 1)
    {
        return 1;
    }

    return f * fuc_factorial(f-1);  //函数计算中包含函数本体
}

递归问题对数学思维能力有极高的要求,作为一个普通人,我选择多看多记

{% btn regular::常用库速查::mikumikudaifans.github.io/Displace.gi… fa-play-circle %}

头文件

在 C 语言中,头文件.h 文件)是一个包含常量、函数原型、数据类型定义、宏定义等的文件。头文件的主要作用是让多个 .c 文件共享声明,以实现代码的重用性模块化,并且头文件能够避免重复定义,提升代码的可读性和维护性

在复杂的项目中,可能会有多个文件包含同一个头文件,这样会造成重复包含,导致编译错误。为了防止这种情况,通常会在头文件中添加包含保护 : #pragma once用于使头文件仅被调用一次,

ifndef#endif 的使用

通过 #ifndef#define 的组合,确保头文件的内容只会被处理一次

  • #ifndef 是 "if not defined" 的缩写,意思是如果宏 EXAMPLE_H 没有定义,则继续执行后面的代码。而 #define EXAMPLE_H 则是在第一次进入时定义这个宏。这样做的目的是确保头文件不会被多次包含
  • #endif 是用于结束这个条件编译块,它标志着 #ifndef 块的结束
// example.h
#ifndef EXAMPLE_H // 如果没有定义EXAMPLE_H
#define EXAMPLE_H // 定义EXAMPLE_H

// 宏定义
#define MAX_VALUE 100

// 函数声明
void printMessage();
int addNumbers(int a, int b);

// 结构体定义
typedef struct {
    char name[50];
    int id;
} Student;

#endif

其中的fuc_print(int f)是其他.c文件中定义的函数,将其在头文件中声明可被其它.c文件的调用

用法

要在一个 C 文件中使用头文件,通常需要使用 #include 预处理指令。它的作用是将头文件的内容复制到包含它的源文件中。包含头文件有两种方式:

  • 尖括号方式#include <file.h> 用于包含系统库文件或标准头文件。编译器会在系统预定义的目录中查找这些文件。
  • 双引号方式#include "file.h" 用于包含用户定义的头文件。编译器会首先在当前目录查找头文件,如果找不到,再到系统目录查找,通常使用此种方法
/// C_L_Header.h
#pragma once  //能防止文件被多次包含
#include <stdio.h>  //库声明
#include <stdlib.h>
#include <string.h>

#define  head_b  10;  //常量宏声明
int fuc_print(int f);  // 函数声明
///C_Language_Learning_TEST.c
#include "C_L_Header.h"  //导入头文件

int fuc_print(int f);  // 函数声明

int main_1() {
    int j = 2;

    j = fuc_print(j); // 正确调用fuc_print函数

    printf("j is %d\n", j);

    return 0;
}

int fuc_print(int f) {
    printf("function is running\n");
    f = 8888;
    return f;
}

此案例使用了头文件来省略库文件的导入

// test
#include "C_L_Header.h"

int main() {
    int l = 9;

    l = fuc_print(l);  // 正确调用fuc_print函数
    printf("fuc used successfully, l is %d\n", l);
    
    int b = 10 * head_b;  //对头文件中定义的常量宏head_b进行调用
    printf("%d", b);

    return 0;
}

此案例使用头文件对C_Language_Learning_TEST.c中的int fuc_print(int f)实现调用

注意,一个项目中只能有一个main函数,他是程序执行的入口,当出现复数main时会导致程序无法找到入口,从而报错

局部变量与全局变量

全局变量(不要用! 不要用!! 不要用!!!)

全局变量是指在所有函数之外定义的变量,可以被程序中所有函数访问和修改,它的作用域是从变量定义开始,直到程序结束为止,在整个程序的生命周期内存在,

  • 作用域:全局变量的作用域是整个程序,即可以在程序中的任何地方被访问(在同一源文件或通过extern声明的其他源文件)且全局变量常用于在多个函数之间共享数据,而不需要显式传递参数
  • 生命周期:全局变量从程序开始执行时创建,并且直到程序结束时才会被销毁。
  • 内存位置:全局变量通常存储在静态数据区(静态存储区)中,而不是栈或堆中
#include <stdio.h>
int global_var = 10; // 全局变量
void main() {
    printf("Function 1, global_var = %d\n", global_var);
    global_var++; // 修改全局变量
    printf("Function 1, global_var = %d\n", global_var);
}
注意事项:
  1. 命名冲突:全局变量如果和局部变量同名时,局部变量会覆盖全局变量的作用域。
  2. 全局变量修改容易影响其他函数:由于全局变量可以被任何函数修改,可能会造成意外的数据修改。因此,需要小心使用全局变量,避免在复杂程序中难以跟踪的错误,故此我们一般避免使用全局变量
局部变量

局部变量是指在函数或代码块内部定义的变量,它只能在该函数或代码块内部使用,在该范围之外是不可见的,局部变量通常用于函数的内部计算,不需要与其他函数共享数据,但可通过接口将局部变量传值给外部函数

  • 作用域:局部变量的作用域仅限于定义它的函数或代码块。它在函数或块外是不可见的。
  • 生命周期:局部变量的生命周期开始于函数或块的执行,结束于函数或块的结束。每次函数调用时,都会创建新的局部变量。
  • 内存位置:局部变量通常存储在中,函数结束时会自动销毁
#include "C_L_Header.h"

int fuc_factorial(int f)
{
    f = 22;
    printf("number is %d\n", f);
    return f;
}

int main() {
    int a = 0; //局部变量a的作用域仅在main()函数的{}中
    printf("number is %d\n",a);

    fuc_factorial(a); //局部变量a传值给fuc_factorial()函数

    return 0;
}
注意事项:
  1. 作用范围受限:局部变量只能在它定义的函数或代码块内使用,不能在其他函数中访问。
  2. 局部变量不保留值:每次进入函数时,局部变量都会重新创建,之前的值不会保留。
  3. 栈溢出:过多的局部变量会导致栈内存不足,导致程序栈溢出(特别是在递归调用中)
sp.静态局部变量

静态局部变量是局部变量的一种特殊形式,它的生命周期是程序的整个执行过程,但它的作用域仍然局限在定义它的函数中,静态局部变量在程序执行期间只被初始化一次,并且它的值在函数调用结束后仍然保持

  • 在局部变量的定义前加上关键字static即可定义静态变量
#include <stdio.h>

void function() {
    static int static_var = 0; // 静态局部变量 //static 变量类型 变量名 = 值;
    static_var++;
    printf("static_var = %d\n", static_var);
}

int main() {
    function();
    function();
    function(); // 连续调用函数,static_var的值会累加
    return 0;
}

适合用来当全局计数器

构造体

构造体(struct)是C语言中非常重要的特性,它允许将不同类型的变量组合在一起,形成一种更复杂的数据类型

标准构造体

  • 定义一个struct类型的变量时,需要使用struct关键字,后跟结构体标签,例如struct name { }进行构造体定义
  • 在函数中使用时应先进行实例化struct name instancename;
  • 通过 .(点操作符) 来访问构造体成员,例如instance mane.parameter = value进行指名修改值
#include <stdio.h>
struct ababa_s  //定义一般构造体
{
    int age;
    char ber;
    float num;
};
int main() {
    struct ababa_s s={ 19,'k',1.6}; //创建构造体实例,初始化并赋值
    s.age = 99; //指名赋值  //实例名.变量名 = 值
    printf("构造体 :  %d %c %f \n", s.age, s.ber, s.num);
}

sp.构造体初始化也可有struct ababa_s s={0};,其含义为所有值均为0

构造体数组

利用构造体可以储存不同类型数值的特性,可以定义构造体数组,用于存储同类型的多个结构体变量,常用于构建信息列表

#include <stdio.h>
struct ababa_s  //一般构造体
{
    int age;
    char ber;
    float num;
};
int main() 
{
 struct ababa_s s[3]; //创建构造体数组
    
 for (int i = 0; i < 3; i++)
 {
     printf("请输入第 %d 组数据 (格式: 整数 字符 浮点数): \n", i + 1);
     
     scanf_s("%d %c %f",&s[i].age, &s[i].ber,1, &s[i].num);  //复合类型输入时""中的空格决定了一个类型数据输入的中断

     printf("构造体 : %d %c %f \n", s[i].age, s[i].ber, s[i].num);
 }   
}

构造体指针

可以使用指针来指向构造体变量,并通过箭头操作符 -> 访问成员

#include <stdio.h>
struct ababa_s  //一般构造体
{
    char name[10];
    int age;   
    float tall;
};

int main() 
{
    struct ababa_s s = {"jojo",17,1.87 };
    struct ababa_s sarr[3] = { "jojo",17,1.87 ,"koko",14,1.56,"didi",23,1.77};

    struct ababa_s* p; //定义构造体指针
    p = &s;

    //使用解地址符实现对构造体的修改 不常用
    (*p).age = 22;
    printf("%s %d %.3fm\n", (*p).name, (*p).age, (*p).tall); //*的运算优先级低于.故此应加()才能保证编译正确


    //使用指针箭头实现对构造体的修改 常用
    p = sarr;
    for (size_t i = 0; i < 3; i++)
    {
        p->age = 18; //此时p指向sarr[0] 
        printf("%s %d %.3fm\n", p->name, p->age, p->tall);
        p++; //p+1后p指向sarr[1]
    }
    
}

值的注意的是指针p每次加1,就会跳转到构造体数组下一个索引的起始位置

匿名构造体

在定义时可以省略标签,这种结构体被称为匿名结构体,适用于仅需要一次的结构体定义

struct {
    int x;
    int y;
} point;

使用typedef另命名构造体

typedef声明新的类型名来代替已有的类型名,方便后续调用

#include <stdio.h>
typedef struct ababa_s  
{
    char name[10];
    int age;
    float tall;
}ab, * pab; //定义构造体别名 和 指针别名

typedef int INTEGER; //定义数据类型int的别名

int main() 
{
    ab s = { 0 }; //实例化构造体并初始化
    pab p; //实例化指针
    INTEGER i = 888; //INTEGER等价于int

    p = &s;
    p->age = i;
    printf("age is %d", p->age);

}

typedef int INTEGER;会在项目需要修改一类数据的类型时起到方便修改的作用

构造体的嵌套

可实现多个构造体的统一调用,使用.来进行层级穿透实现对子构造体实例的调用

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

typedef struct address_s
{
    char addr[50];
}ad;

struct Info {
    char name[50];
    int age;
    ad a; //在构造体中实例化另一个构造体
};

int main() {

    struct Info nin = {"jojo",14};
    strcpy_s(nin.name, sizeof(nin.name), "dio");
    strcpy_s(nin.a.addr, sizeof(nin.a.addr), "Mo li o jo"); //通过nin向a写入数据

    printf("name %s \nage %d \naddress %s\n", nin.name, nin.age, nin.a.addr);
    
    return 0;
}

构造体对齐

一般内存对齐

构造体中的成员会存储在内存中,但它们的存储位置通常会受对齐限制的影响。对齐是指编译器为提高CPU访问速度,对数据在内存中的存储方式进行调整。具体来说,编译器会根据对齐规则决定每个成员的存储位置,可能在成员之间插入一些“填充字节”(也叫做“空洞”)

  • 基本对齐规则:构造体中每个成员的存储地址必须是该成员大小的整数倍。
  • 整体对齐规则:构造体的总大小必须是最大成员大小的整数倍。
struct Example {
    char a;   // 1字节
    int b;    // 4字节
    char c;   // 1字节
};
//Memory Layout: | a | - | - | - | b | b | b | b | c | - | - | - |
//该构造体的总大小应为 1+3 + 4 + 1+3 = 12个字节
  • char a:占用1个字节,但为了对齐,接下来3个字节会被填充(padding),使得b的地址是4字节对齐。
  • int b:占用4个字节。
  • char c:占用1个字节,但为了对齐整个结构体的大小为最大成员(4字节int)的整数倍,最后会再填充3个字节。
强制对齐

C语言允许使用 **#pragma pack**指令来改变默认的对齐方式,减少内存浪费,但可能会影响访问速度

#pragma pack(1)  // 强制1字节对齐
struct Example {
    char a;
    int b;
    char c;
};
#pragma pack()   // 恢复默认对齐

此时,该结构体的大小变为6字节,没有填充字节

共用体

共用体(union)是C语言中的一种数据结构,与结构体类似,但它们的内存分配方式不同。在共用体中,所有成员共享同一块内存空间,因此同一时间只能存储一个成员的值,这使得共用体的大小等于其最大成员的大小。共用体主要用于节省内存,特别适合在需要在不同时间存储不同类型的数据的场景下使用。

  • 存储覆盖:由于共用体的所有成员共享内存空间,修改一个成员的值会覆盖其他成员的值。
  • 大小:共用体的大小等于其最大成员的大小,而不是所有成员大小之和。
  • 初始化:可以只对一个成员进行初始化,后续对其他成员赋值会覆盖之前的数据。
  • 用途场景:适用于需要节省内存的场景,或需要在同一位置以不同方式解释数据的场景,比如网络数据包、硬件寄存器操作等。

共用体的定义与结构体类似,使用关键字 union。语法格式如下:

#include <stdio.h>

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

int main() {
    union Data data;

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

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

    // 注意:由于共用体成员共享同一内存空间,先前的 `i` 和 `f` 值会被覆盖
    snprintf(data.str, sizeof(data.str), "Hello");
    printf("data.str: %s\n", data.str);

    return 0;
}

在这段代码中,由于共用体 data 的成员共享同一块内存,赋值给 data.f 后会覆盖 data.i 的值。同理,赋值 data.str 后也会覆盖之前的成员值。

共用体的优点
  • 节省内存:特别是在嵌入式系统或内存紧张的场景中,可以用共用体减少内存使用。
  • 数据解析:共用体可以用来解析复杂的数据结构,例如解析协议报文,可以用不同成员表示同一数据的不同解释方式。
  • 数据转换:在数据转换中,可以用共用体将数据视为不同类型处理,比如将 int 直接看作 char 数组处理。

他或许可以作为一个可接收多数据类型但只保留最后一种输入类型的存储池,若是能有数据类型识别算法,应该可以实现无限制输入.....

共用体与结构体的区别
特性共用体 (union)结构体 (struct)
内存分配所有成员共享同一块内存空间,只分配最大成员的内存大小。每个成员都有独立的内存空间,总大小是所有成员大小之和。
同时存储多个成员不可以,只能同时存储一个成员的值。可以,每个成员的值独立存在。
用途节省内存,在不同时间存储不同类型的变量。管理多个相关变量。

共用体与构造体的组合

将共用体作为数据传入构造体的中间商,使得构造体实例可以调用共用体中的数据

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

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

struct Info {
    char name[50];
    union Data data;
};

int main() {
    struct Info info;

    // 使用 strcpy_s 给 info.name 赋值
    strcpy_s(info.name, sizeof(info.name), "John");

    info.data.i = 10;  // 使用整数
    printf("Name: %s, Data: %d\n", info.name, info.data.i);

    info.data.f = 3.14;  // 使用浮点数
    printf("Name: %s, Data: %.2f\n", info.name, info.data.f);

    // 使用 strcpy_s 给 info.data.str 赋值
    strcpy_s(info.data.str, sizeof(info.data.str), "Hello");
    printf("Name: %s, Data: %s\n", info.name, info.data.str);

    return 0;
}

共用体Data中的intfloatchar共享同一块内存。每次赋值时,前一个值会被覆盖

C++引用

C++是完全兼容C的,故此在正式学习数据结构前C++的个别语法可极大方便数据操作

C++&引用

在函数参数列表中使用引用(例如 int &b),告诉编译器不要创建参数的副本,而是直接使用调用函数时传入的变量,在这种情况下,b 成为传入变量的一个别名,可以直接操作这个变量的内容,而不需要通过指针或重新复制数据

#include <stdio.h>

void AAA(int& b)
{
	b++;
}

int main()
{
	int a = 1;
	AAA(a);  //通过&p操作,直接将AAA中计算的b值返回给了a
	printf("a=%d\n", a); //此时a的值为2
	return 0;
}

C++bool

没啥好说的,是个人都认识

#include <stdio.h>
int main()
{
	bool a = true;
	bool b = false;
	printf("a=%d,b=%d\n", a, b); //true为1 false为0
	return 0;
}

C BASE GRAMMER OVER