• C 语言基础


    文章目录

    C 语言语法

    一、 基础

    1、 第一个程序

    // 导入一个文件,std是一个标准库,io是输入输出
    /*
    <> 表示导入系统文件
    "" 表示导入自定义的文件
    */
    #include <stdio.h>
    #include <stdlib.h>  // 里面包含了system函数
    
    int main() {
        printf("Hello, World!\n");  // 打印输出hello world,行注释
        system("pause");  // 按任意键继续
        /* 块注释 */
        return 0;  // 表示函数的返回值
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    system:使用系统命令是,成功返回0

    2、 程序编译步骤

    C 代码编译成可执行程序经过4步:

    (1)预处理:宏定义展开、头文件展开、条件编译等,同时将代码中的注释删除,这里并不会检查语法

    (2)编译:检查语法,将预处理后文件编译生成汇编文件

    (3)汇编:将汇编文件生成目标文件(二进制文件)

    (4)链接:C语言写的程序是需要依赖各种库的,所以编译之后还需要把库链接到最终的可执行程序中去

    使用gcc查看编译过程

    gcc -E hello.c -o hello.i    # 预处理
    gcc -S hello.i -o hello.s    # 编译
    gcc -c hello.s -o hello.o    # 汇编
    gcc    hello.o -o hello.exe  # 链接
    :<<!
    # 一步编译的代码:
    !
    gcc -o hello.exe h1.c h2.c ...  # 后面可以编译多个代码,同时生成一个hello.exe 文件
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    -o:表示输出文件的地址

    3、 汇编语言

    mov 移动
    add 添加
    push 入栈
    pop 出栈
    call 调用
        
    eax 32位寄存器
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    二、 数据类型

    1、 常量与变量

    • 关键字:C语言里面有32个关键字

    • 数据类型

    • 常量

      • 在程序运行过程中,其值不能被改变的量
      • 常量一般出现在表达式或赋值语句中

      定义方式

      #define MAX 20
      const int max_ = 23;  // 不安全
      
      • 1
      • 2
    • 变量

      • 在程序运行过程中,其值可以发生改变的量
      • 变量在使用时必须先定义,定义变量前必须有相应的数据类型

      命名规则

      • 标识符不能是关键字
      • 标识符只能由字母、数字、下划线组成
      • 第一个字符必须为字母或下划线
      • 标识符中字符区分大小写

      变量特点

      • 变量在编译时为其分配相应的内存空间
      • 可以通过其名字和地址访问相应的内存

      定义方式

      int max_ = 23;  // 其是可以改变的
      
      • 1

    2、 整型

    2.1 格式化输出

    占位符含义
    %d输出一个有符号的十进制整型数据
    %o输出八进制的整型数据
    %x输出十六进制的整型数据,字母以小写输出
    %X输出十六进制的整型数据,字母以大写输出
    %u输出一个十进制的无符号数

    2.2 定义

    // 无符号 unsigned;有符号 signed
    int a = -10;  // 有符号代表有正负,默认为有符号
    unsigned int b = 10;  // 无符号数,只能为正数,计算结果也要为正数
    
    int c = 0123;  // 定义八进制数据,以 0 开头
    int d = 0x234ba;  // 定义十六进制数据,以 0x 开头
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    C 不能直接书写二进制数据的形式

    例如:

    #include <stdio.h>
    
    int main() {
        int a;
        scanf_s("%d", &a);  // 通过键盘输入赋值
        printf_s("%d \n", a);  // 输出值
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    使用scanf会出现安全问题,使用scanf_s,安全输入

    使用不同的关键字定义整型,其开辟的空间是不一样的,所占字节数与所选择的操作系统有关,可以使用sizeof(int)来查看所占空间(BYTE

    3、 字符型

    3.1 定义

    字符型变量用于存储一个单一字符,在C 语言中用char表示,其中每个字符变量都会占用1个字节。在给字符型变量赋值时,需要使用单引号

    字符型变量实际上并不是把该字符本身放到变量的内存单元中去,而是将该字符对应的acsii编码放到变量的存储单元中。char的本质就是一个1字节大小的整型

    特殊字符:

    • 转义字符
    char a = 'a';
    
    • 1

    3.2 输入

    #include <stdio.h>
    
    int main() {
        char a;
        scanf_s("%c", &a);  // 安全输入
        printf_s("%c \n", a);  // 安全输出
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4、 浮点型

    浮点型变量也可以称为实型变量,浮点型变量是用来存储小数值的。在 C 语言中,浮点型变量分为两种:单精度浮点数(float)、双精度浮点数(double),但是double型变量所表示的浮点数比float型变量更准确

    由于浮点型变量是由有限的存储单元组成的,因此只能提供有限的有效数字。在有效位以外的数字将被舍去,这样可能会产生一些误差。

    不以f结尾的常量是double类型,以f结尾的常量(如:3.14f)是float类型

    5、 类型限定符

    限定符含义
    extern声明一个变量,extern声明的变量没有建立存储空间
    const定义一个常量,常量的值不能修改
    Volatile防止编译器优化代码
    register定义寄存器变量,提高效率

    6、 字符串

    6.1 字符串常量

    • 字符串是内存中一段连续的char空间,以'\0'结尾
    • 字符串常量是有双引号括起来的字符序列

    字符串常量和字符常量不同

    • 每个字符串的结尾,编译器会自动添加一个结束标志位'\0'

    定义

    char* a = "hello";  // 使用指针来定义
    char b[] = "hello";  // 使用字符数组来定义
    printf("%s\n", a);  // 输出字符串
    
    • 1
    • 2
    • 3

    6.3 printf 和 putchar

    printf是输出一个字符串,putchar输出一个字符

    6.3.1 printf

    占位符:

    格式含义
    %a,%A浮点数、十六进制数字和p-计数法
    %c一个字符
    %C一个ISO宽字符
    %d有符号十进制整数(int)(%ld ,%Ld为:长整型数据,%hd:短整型数)
    %e,%E浮点数,e-计数法,E-计数法
    %f单精度浮点数
    %g,%G根据数值不同自动选择%f或%e
    %i有符号十进制数(与%d相同)
    %o无符号八进制整数
    %p指针
    %s对应字符串char*(%s = %hs = %hS 输出 窄字符)
    %S对应宽字符串WCAHR*(%ws = %S 输出宽字符串)
    %u无符号十进制整数(unsigned int)
    %x,%X使用十六进制数字0xf的无符号十六进制整
    %%打印一个%号
    %I64d用于int64 或者 long long
    %I64u用于uint64 或者unsigned long long
    %I64x用于64 位16进制数字

    附加格式:

    符号含义
    -左对齐
    +右对齐
    .n对于小数点,保留n位小数
    #对c,s,d,u无影响,对o类输出前加缀为o,对x类,在输出前缀加0x,对e,g,f当结果有小数时给出小数点
    m代表数据的最小宽度
    6.3.2 putchar
    char ch = '0';
    putchar(ch);  // 输出一个字符
    putchar('\n');
    putchar(97);
    
    • 1
    • 2
    • 3
    • 4

    输出字符可以是变量、字符、数字或者转义字符

    6.4 scanf_s 和 getchar

    getchar是从标准输入设备读取一个字符;scanf_s通过%转义的方式可以得到用户通过标准输入设备输入的数据

    6.4.1 scanf_s
    int a, b;
    scanf_s("%d %d", &a, &b);  // 输入两个整型数据,使用空格(分隔符)或换行
    
    • 1
    • 2
    6.4.2 getchar
    char ch;
    ch = getchar();  // 只会接收一个字符,如果有其他的,可以使用while循环遍历,也可以用来作为暂时停留界面
    putchar(ch);  
    
    • 1
    • 2
    • 3

    7、 类型转换

    数据有不同的类型,不同类型数据之间进行混合运算时必然涉及到类型的转换问题

    7.1 隐式转换

    自动转换:遵循一定的规则,由编译系统自动完成

    char a = 2;
    int c = a;  // a 变成了int类型,有符号数会转换成无符号数
    
    • 1
    • 2

    小范围的类型会自动转换成大范围的类型运算

    7.2 显式转换

    把表达式的运算结果强制转换成所需的数据类型

    数据类型2 变量2 = (数据类型2)变量1  // 其不会四舍五入,直接丢失后面的数据
    
    • 1

    其会造成精度的丢失

    三、 运算符

    1、 算术运算符

    用于处理四则运算

    +:加 -:减 *:乘 /:除 %:取余 ++:自增 --:自减
    // 前自增先赋值,后运算;后自增相反
    
    • 1
    • 2

    2、 赋值运算符

    用于将表达式的值赋给变量

    += -= /= *= = 
    
    • 1

    3、 比较运算符

    用于表达式的比较,并返回一个真值(true)或假值(false)

    == != > < >= <= 
    
    • 1

    4、 逻辑运算符

    用于根据表达式的值返回真值或假值

    !:非 &&:与 ||:或
    
    • 1

    所有非零的值都是真值,非真即假

    四、 流程结构

    C/C++支持最基本的三种程序运行结构:顺序结构、选择结构、循环结构

    • 顺序结构:程序按顺序执行,不发生跳转
    • 选择结构:依据条件是否满足,有选择的执行相应功能
    • 循环结构:依据条件是否满足,循环多次执行某段代码

    1、选择结构

    1.1 if语句

    作用:执行满足条件的语句

    if语句的三种形式

    • 单行格式if语句
    • 多行格式if语句
    • 多条件的if语句
    1.1.2 单行格式
    #include <stdio.h>
    int main() {
        // 选择结构 单行if语句
    
        // 用户输入一个数字
        int num = 0;
        printf_s("请输入一个数字:");
        scanf_s("%d", &num);
    
        // 判断数字是否大于100,是则输出原数
        if (num >= 100){  // 注意if语句后面不要加分号,否则,if将不会进行判断
            printf_s("%d\n", num);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1.1.3 多行if语句
    #include <stdio.h>
    int main() {
        // 选择结构 单行if语句
    
        // 用户输入一个数字
        int num = 1;
        printf_s("请输入一个数字:");
        scanf_s("%d", num);
    
        // 判断数字是否大于100,是则输出原数;否则,输出0
        if (num >= 100) {  // 注意if语句后面不要加分号,否则,if将不会进行判断
            printf_s("%d", num);
        }
        else {
            printf_s("%d", 0);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    1.1.4 多条件if语句
    #include <stdio.h>
    int main() {
        // 选择结构 单行if语句
    
        // 用户输入一个数字
        int num = 1;
        printf_s("请输入一个数字:");
        scanf_s("%d", num);
    
        // 判断数字是否大于100,是则输出原数;否则,输出0
        if (num >= 600) {  // 注意if语句后面不要加分号,否则,if将不会进行判断
            printf_s("%d", 600);
        }
        else if (num >= 100) {  // 注意if语句后面不要加分号,否则,if将不会进行判断
            printf_s("%d", num);
        }
        else {
            printf_s("%d", 0);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    if ( 条件1 ) { 条件1满足执行语句} else if ( 条件2 ) { 条件2满足,同时条件1不满足,执行的语句 }··· else { 都不满足执行的语句 }

    1.1.5 嵌套if语句

    在if语句中,可以嵌套使用if语句,达到更加精确的条件判断

    案例:输入3个数字,判断出最大的数字

    #include <stdio.h>
    int main() {
        int num = 1;
        int num1 = 1;
        int num2 = 1;
        printf_s("请输入三个数字:");
        scanf_s("%d,%d,%d", &num, &num1, &num2);
    
        if ( num > num1 ) {  // 判断 num 和 num1
            if ( num > num2 ) {  // 判断 num 和 num2
                printf_s("%d最大\n", num);
            }
            else {
                printf_s("%d最大\n", num2);
            }
        }
        else {
            if (num1 > num2) {
                printf_s("%d最大\n", num1);
            }
            else {
                printf_s("%d最大", num2);
            }
            printf_s("判断完成");
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    1.2 三目运算符

    作用:通过三目运算实现简单的判断

    语法:表达式1 ? 表达式2 : 表达式3

    #include <stdio.h>
    int main() {
    	
    	// 三目运算
    	
    	// 将 a 和 b 做比较,将大的值赋值给c
    	int a = 10;
    	int b = 20;
    	int c = 0;
    
    	c = a > b ? a : b;  // 如果 a 比 b 大,则将a赋值给c
        
        /*
        if ( a > b ) {
        	c = a;
        }
        else {
        	c = b
        }
        */
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在C++中,三目运算符返回的是变量,可以继续赋值

    1.3 switch语句

    作用:执行多条件分支语句

    语法:

    switch ( 表达式 ) {
        case 结果1: 执行语句; break;  // switch里面不一定每个case都要对应break,break的作用的向外跳出一层
        ······
        default: 执行语句; break;  // 不一定需要default判断
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    示例

    #include <stdio.h>
    int main() {
    
        // switch 语句
    
        // 电影打分
        int score = 0;
        printf_s("请输入分数:");
        scanf_s("%d", &score);
        printf_s("您打的分数为:%d\n", score);
    
        switch (score) {
            case 10:
                printf_s("是经典电影\n");
                break;  // 退出当前分支,如果没有break,则会继续向下运行
            default:  // 当所有条件不满足时,执行该语句
                printf_s("普通\n");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    缺点:判断的时候,只能是整型或者字符型,不可以是一个区间

    优点:结构清晰,执行效率高

    注意

    • switch语句中表达式类型只能是整型或者字符型
    • case里如果没有break,那么程序会一直向下执行

    2、 循环结构

    2.1 while循环语句

    作用:满足循环条件,执行循环语句

    语法:while ( 循环条件 ) { 循环语句 }

    解释:只要循环条件的结果为真,就执行循环语句

    #include <stdio.h>
    int main() {
    
    	int nu = 0;
    		// 在屏幕中打印0到9的数字
    	while ( nu < 10 )
    	{
    		printf_s("%d\n",nu);
    		nu++;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.2 do···while循环语句

    作用:满足循环条件,执行循环语句

    语法:do {循环语句} while (循环条件);

    注意:与while的区别在于do…while会先执行一次循环时间,在判断循环条件

    #include <stdio.h>
    
    int main() {
    
    	// do...while语句
    	// 在屏幕中输出 0 到 9 这10个数字
    	int num = 0;
    
    	do {
    		printf_s("%d\n", num);
    		num++;
    	} 
    	while (num <= 9);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2.3 for循环语句

    作用:满足循环条件,执行循环语句

    语法:for (起始表达式;条件表达式;末尾循环体) { 循环语句; }

    #include <stdio.h>
    
    int main() {
    
    	// for循环
    	// 从数字0打印到9
    
    	for (int i = 0; i < 10; i++ ) {
    		printf_s("%d\n", i);
    	}
    
    	// 也可以
    	int i = 0;
    	for (;;) {
    		if (i >= 10) {
    			break;
    		}
    		printf_s("%d\n", i++);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    2.4 嵌套循环

    作用:在循环中在嵌套一层循环,解决一些实际问题

    #include <stdio.h>
    
    int main() {
    
    	// 嵌套循环
    	for (int i = 0; i < 5; i++) {
    		for (int j = 0; j < 5; j++) {
    			printf_s(" * ");
    		}
    		printf_s("\n");
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3、 跳转语句

    3.1 break语句

    作用:用于跳出选择结构或者循环结构

    break使用的时机:

    • 出现在switch语句中,作用是终止case并跳出switch
    • 出现在循环语句中,作用是跳出当前的循环语句
    • 出现在嵌套循环中,跳出最近的内层循环语句

    3.2 continue语句

    作用:在循环语句中,跳过本次循环中余下尚未执行的语句,继续执行下一次循环

    #include <stdio.h>
    
    int main() {
    	
    	for (int i = 0; i <= 10; i++) {
    		if (i % 2 == 0) {
    			continue;
    		}
    		else {
    			ptintf_s("%d\t", i);
    		}
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    3.3 goto语句

    作用:可以无条件跳转语句

    语法:goto 标记;

    解释:如果标记的名称存在,执行到goto语句时,会跳转到标记的位置

    #include <stdio.h>
    
    int main() {
    	
    	// goto
    	printf_s("hello\n");
    	printf_s("hello\n");
    	printf_s("hello\n");
    	goto flag;
    	printf_s("hello\n");
    	printf_s("hello\n");
    flag:
    	printf_s("world\n");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    五、 数组和字符串

    1、 概述

    在程序设计中,为了方便处理数据把具有相同类型的若干变量按有序形式组织起来——称为数组

    数组就是在内存中连续的相同类型的变量空间。同一个数组所有的成员都是相同的数据类型,同时所有的成员在内存中的地址是连续的

    数组属于构造数据类型

    数组名是地址常量

    2、 定义数组

    int arr[] = { 1, 2, 4, 5, 6, 7 };
    int arr1[6] = { 1, 2, 3, 4, 5, 6 };
    int len = sizeof(arr) / sizeof(arr[0]);  // 获取数组的长度
    
    • 1
    • 2
    • 3

    整型数组默认初始化的值为0,

    3、 冒泡排序

    作用:最常用的排序算法,对数组内的元素进行排序

    1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个
    2. 对每一对相邻元素做同样的工作,执行完毕后,找到第一个最大值
    3. 重复以上的步骤,每次比较次数-1,直到不需要比较
    #include <stdio.h>
    
    int main() {
        // 利用冒泡排序,实现升序排序
        int arr[ ] = {4, 2, 8, 0, 5, 7, 1, 3, 9};
        printf_s("排序前:");
        for (int i = 0; i < (sizeof(arr) / sizeof(arr[0])); i++) {
            printf_s("%d\t", arr[i]);
        }
        for (int i = 0; i < (sizeof(arr) / sizeof(arr[0]) - 1); i++) {  // 排序次数为元素个数减一
            for (int j = 0; j < (sizeof(arr) / sizeof(arr[0]) - 1 - i); j++) {  // 内层循环对比次数元素个数 - 排序次数 - 1
                if (arr[j] > arr[j + 1]) {  // 如果前一个数字比第二个数字大,交换顺序
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
        printf_s("\n排序后:");
        for (int i = 0; i < (sizeof(arr) / sizeof(arr[0])); i++) {
            printf_s("%d\t", arr[i]);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    4、 二维数组

    二维数组定义的一般形式是:

    类型说明符 数组名[常量表达式1][常量表达式2];
    
    • 1

    其中常量表达式1表示行数,常量表达式2表示列数

    5、 字符串

    char arr[5] = { 'h', 'e', 'l', 'l', 'o' };  // 字符数组
    char* a = "world";  // 字符串
    
    • 1
    • 2

    如果字符数组有'\0'的标志,则可以认为其是一个字符串,字符串是字符数组的特例

    字符数组和字符串的区别:

    • C 语言中没有字符串这种数据类型,可以通过char的数组来替代
    • 字符串一定是一个char的数组,但char的数组未必是字符串
    • 数字0(和字符'\0'等价)结尾的char数组就是一个字符串,但如果char数组没有以数字0结尾,那么就不是一个字符串,只是普通字符数组,所以字符串是一种特殊的char数组

    5.1 字符串的输入

    char str[100];
    scanf_s("%s", str);  // 默认使用空格分隔
    
    // gets_s()
    char* gets_s(char* s);
    // 功能:从标准输入读取字符,并保存到指定的内存空间,直到出现换行或读到文件结尾为止,成功返回字符串,失败返回空
    
    // fgets_s()
    char* fgets(char* s, int size, FILE* stream);
    // 功能:从stream指定的文件中读入字符,保存到所指定的内存空间,直到出现换行字符、读到文件结尾或是已读了size-1个字符为止,最后会自动加上'\0',作为字符串结束,成功返回字符串,失败返回空。其通过键盘输入时,包括了最后的'\n'换行
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    scanf_sgets_s的区别:

    • scanf_s不允许输入有空格;gets_s允许输入有空格

    fgets里面的stream是文件操作指针

    例如,获取键盘输入的字符串:

    char ch[101];
    sacnf_s("%s", ch);
    
    gets_s(ch);
    
    fgets(ch, sizeof(ch), stdin);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面的代码都是获取从键盘输入的字符串

    5.2 字符串的输出

    printf_s("%s\n", str);
    
    int puts(const char* s);
    // 功能:标准设备输出字符串,在输出完成后自动输出一个'\n'
    
    int fputs(const char* s, FILE* stream);
    // 功能:将s所指定的字符串写入到stream指定的文件中,字符串结束符'\0'不写入文件,同时不会自动换行
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    例如:

    char c[] = "hello world";
    puts(c);
    fputs(c, stdout);
    
    • 1
    • 2
    • 3

    5.3 字符串长度

    #include <string.h>
    int strlen(const char* s);
    // 功能:计算指定字符串的长度,不包括'\0'
    
    • 1
    • 2
    • 3

    六、 函数

    1、 概述

    1.1 函数的分类

    C 程序是由函数构成的,我们写的代码都是由主函数 main() 开始执行的。函数是 C 程序的基本模块,是用于完成特定任务的程序代码单元。

    从函数定义的角度看,函数可分为系统函数和用户定义函数两种:

    • 系统函数,即库函数:这是编译系统提供的,用户不必自己定义这些函数,可以直接使用它们
    • 用户定义的函数:用以解决用户的专门需要

    从函数执行结果的角度来看, 函数可分为有返回值函数和无返回值函数两种

    • 有返回值函数:此类函数被调用执行完后将向调用者返回一个执行结果,称为函数返回值。(必须指定返回值类型和使用return关键字返回对应数据)
    • 无返回值函数:此类函数用于完成某项特定的处理任务,执行完成后不向调用者返回函数值。(返回值类型为void, 不用使用return关键字返回对应数据)

    从主调函数和被调函数之间数据传送的角度看,又可分为无参函数和有参函数两种

    • 无参函数:在函数定义及函数说明及函数调用中均不带参数。主调函数和被调函数之间不进行参数传送
    • 有参函数:在函数定义及函数说明时都有参数,称为形式参数(简称为形参)。在函数调用时也必须给出参数,称为实际参数(简称为实参)

    1.2 函数的作用

    1. 函数的使用可以省去重复代码的编写,降低代码重复率
    2. 函数可以让程序更加模块化,从而有利于程序的阅读,修改和完善

    1.3 函数的调用:获得随机数

    当调用函数时,需要关心5要素:

    • 头文件:包含指定的头文件
    • 函数名字:函数名字必须和头文件声明的名字一样
    • 功能:需要知道此函数能干嘛后才调用
    • 参数:参数类型要匹配
    • 返回值:根据需要接收返回值
    #include <time.h>
    #include <stdlib.h>
    #include <stdio.h>
    
    int getRandNum() {
        srand((unsigned int)time(NULL));  // 使用随机种子
        // srand((size_t)time(NULL));
        int n = rand() % 10;  // 产生0到9的随机数
        // rand() % (max - min + 1) + min  取得 min ~ max 的随机数
        return n;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2、 函数定义和使用

    2.1 函数的定义

    • 定义函数的目的

      • 将一个常用的功能封装起来,方便以后调用
    • 自定义函数的书写形式

      返回值类型 函数名(参数类型 形参1, 参数类型 形参2, ...) {
          函数体;
          返回值;
      }
      int get_num(int a, int b) {
          int sum = a + b;
          return sum;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

      在函数调用过程中传递的参数称为实参,有具体的值

      在函数的定义中参数称为形参,形式参数

      在函数调用过程中实参传递给形参

      在函数调用结束后,函数会在内存中销毁

    2.2 具体介绍

    2.2.1 函数名

    理论上,函数名是可以随意起名字,见名知义,应该让用户看到这个函数名字就知道这个函数的功能。注意,函数名的后面要加括号,代表这个是函数,而不是普通的变量

    2.2.2 形参列表

    在定义函数时,指定形参,在未出现函数调用时,它们并不占内存中的存储单元。因此,称它们是形式参数或虚拟参数,简称形参,表示它们并不是实际存在的数据,所以,形参里的变量不能赋值

    int sum(int a, int b = 20);  // 这会报错,形参在C语言里面不能赋值
    
    • 1

    在定义函数时指定的形参,必须是类型+变量的形式

    int sum(int a, int b); // right
    
    • 1
    2.2.3 函数体

    花括号里面的内容即为函数体的内容,这里为函数功能实现的过程,这和以前的写代码没太大区别,以前我们把代码写在main()函数里,现在只是把这个写到别的函数里面

    2.2.4 返回值

    函数的返回值是通过函数中的return语句获得的,return后面的值也可以是一个表达式

    1. 尽量保证return语句中表达式的值和函数返回类型是同一类型
    2. 如果函数返回的类型和return语句中表达式的值不一致,则以函数返回类型为准,即函数返回类型决定返回值类型。对数值型数据,可以自动进行类型转换。

    2.3 函数调用

    定义函数后,我们需要调用此函数才能执行到这个函数里面的代码段。这和main()函数不一样,main()为编译器设定好自动调用的主函数,无需人为调用,我们都是在main()函数里调用别的函数,一个C程序里有且只有一个main()函数

    2.3.1 函数执行流程
    1. 进入main函数
    2. 调用test函数
      • 它会在main()函数的前面寻找有没有一个名字叫做test的函数定义
      • 如果找到,接着检查函数的参数,这里调用函数是没有传参,函数定义也没有形参,参数类型匹配
      • 开始执行test()函数,这时候,main()函数李曼的执行会阻停在test()这一行代码,等待函数执行完成
    3. 函数执行完成后,main()函数继续执行
    2.3.2 形参和实参
    • 形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用
    • 实参出现在主调用函数中,进入被调函数后,实参也不能使用
    • 实参变量对形参变量的数据传递是"值传递",即单向传递,只由实参传给形参,而不能由形参传回来给实参
    • 在调用函数时,编译系统临时给形参分配存储单元。调用结束后,形参单元被释放
    • 实参单元与形参单元是不同的单元。调用结束后,形参单元被释放,函数调用结束返回主函数后则不能再使用该形参变量。实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数中实参的值

    2.4 函数声明

    #include <stdlib.h>
    #include <stdio.h>
    
    int getSum(int, int);  // extern int getSum(int, int);
    
    int main() {
        int sum = getSum(1, 2);
        printf_s("%d\n", sum);
        system("pause");
        return 0;
    }
    
    int getSum(int a, int b) {
        return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    从广义的角度来讲,声明中包含着定义,即定义是声明的一个特例,所以并非所有的声明都是定义:

    • int b它既是声明,同时又是定义
    • 对于extern b来讲,它只是声明,不是定义

    一般的情况下,把建立存储空间的声明称之为“定义”,而把不需要建立存储空间的声明称之为“声明”

    4、 多文件编程

    一个文件声明函数

    如,创建一个hello.h

    #ifndef NEW_HELLO_H  // 如果没有这个文件,可以防止头文件包含
    // 也可以使用 #pragma once  来防止头文件包含只能在Windows中使用
    #define NEW_HELLO_H
    
    int getSum(int, int);  // 可以把extern省略
    
    #endif //NEW_HELLO_H
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    一个文件实现函数hello.c

    #include "hello.h"
    
    int getSum(int a, int b) {
        return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    一个文件调用函数main.c

    #include <stdlib.h>
    #include <stdio.h>
    #include "hello.h"
    
    
    int main() {
        int sum = getSum(1, 2);
        printf_s("%d\n", sum);
        system("pause");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    编译代码

    gcc -o hello.exe main.c hello.c hello.h
    
    • 1

    5、 main函数

    int main(int argc, const char * argv[]) {
       	printf_s("命令行传入参数的个数:%d,第一个参数为:%s\n", argc, argv[0]);
        system("pause");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    main的含义:

    • main是函数的名称,和我们自定义的函数名称一样,也是一个标识符
    • 只不过main这个名称比较特殊,程序一启动就会自动调用它

    return 0的含义:

    • 告诉系统main函数是否正确的被执行了
    • 如果main函数的执行正常,那么就返回0
    • 如果main函数的执行不正常,那么就返回一个非0的函数

    返回值类型

    • 一个函数return 后面写的是什么类型,函数的返回值类型就必须是什么类型,所以写int

    形参列表的含义:

    1. int argc
      • 系统在启动程序是调用main函数时传递给argv的值的个数
    2. const char* argv[]
      • 系统在启动程序时传入的值,默认情况下系统会传入一个值,这个值就是main函数执行文件的路径
      • 也可以通过命令行或项目设置传入其它参数

    6、 递归函数

    什么是递归函数?

    • 一个函数在它的函数体内调用它自身称为递归调用

    递归函数构成条件

    • 自己调用自己
    • 存在一个条件能够让递归结束
    • 问题的规模能够缩小

    实例:求一个数的累加和

    #include <stdlib.h>
    #include <stdio.h>
    int getSum(int n) {
        return n == 1 ? 1 : getSum(n - 1) + n;
    }
    int main() {
        int n;
        scanf_s("%d", &n);
        printf_s("累加和为:%d\n", getSum(n));
        system("pause");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    递归和循环区别

    • 能用循环实现的功能,用递归都可以实现
    • 递归常用于"回溯", “树的遍历”,"图的搜索"等问题
    • 但代码理解难度大,内存消耗大(易导致栈溢出), 所以考虑到代码理解难度和内存消耗问题, 在企业开发中一般能用循环都不会使用递归

    七、 指针

    1、 概述

    1.1 内存

    内存含义:

    • 存储器:计算机的组成,用来存储程序和数据,辅助CPU进行运算处理的重要部分
    • 内存:内部存储器,暂存程序/数据——掉电丢失
    • 外存:外部存储器,长时间保存程序/数据——掉电不丢失

    内存是沟通CPU与硬盘的桥梁:

    • 暂存放CPU中的运算数据
    • 暂存与硬盘等外部存储器交换的数据

    1.2 物理存储器和存储地址空间

    有关内存的两个概念:物理存储器和存储地址空间

    物理存储器:实际存在的具体存储芯片

    • 主板上装插的内存条
    • 显示卡上的显示RAM芯片
    • 各种适配卡上的RAM芯片和ROM芯片

    存储地址空间:对存储器编码的范围。我们在软件上常说的内存是指这一层含义

    • 编码:对每个物理存储单元分配一个号码
    • 寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写

    1.3 内存地址

    • 将内层抽象成一个很大的一维字符数组
    • 编码就是对内存的每一个字节分配成一个32位或64位的编号
    • 这个内存编号我们称之为内存地址

    内存中的每一个数据都会分配相应的地址:

    • char:占一个字节分配一个地址
    • int:占四个字节分配四个地址

    1.4 指针和指针变量

    C 语言中把地址形象地称作指针

    获取地址的方法:

    int a = 10;
    printf("%x\n", &a);  // & 为取址运算符
    
    • 1
    • 2

    可以保存地址值(指针)的变量称为指针变量,因为指针变量中保存的是地址值,故可以把指针变量形象地比喻成地址箱

    2、 指针基础知识

    2.1 指针变量的定义和使用

    • 指针也是一种数据类型,指针变量也是一种变量
    • 指针变量指向谁,就把谁的地址赋值给指针变量
    // 定义一个指针变量
    int* p;  
    int a = 10;
    // 给指针变量赋值
    p = &a;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.2 指针变量间接修改变量的值

    使用取值运算符来修改指针变量所对应的值

    printf("修改前:%d", a);
    *p = 100;
    printf("修改后:%d", a);
    
    • 1
    • 2
    • 3

    2.3 指针大小

    • 使用sizeof()测量指针的大小,得到的总是:4或8
    • sizeof()测的是指针变量指向内存地址的大小
    • 在32位平台,所有指针地址都是32位(4bit)
    • 在64位平台,所有指针地址都是64位(8bit)
    printf("%d\n", sizeof(int*));
    
    • 1

    2.4 野指针和空指针

    指针变量也是变量,是变量就可以任意赋值,不要越界即可,但是任何数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知的(操作系统不允许此指针指向内存区域)。所以,也指针不会直接引发错误,操作指针指向的内存区域才会出问题。

    int* p = 100;  // 野指针 -> 指针变量指未知的空间
    
    // 操作系统将0~255作为系统占用空间,不允许访问操作
    // 操作野指针对应的空间可能报错
    printf("%d\n", *p);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    但是,野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量。C语言中,可以把NULL赋值给此指针,这样就标志此指针为空指针,没有任何指针。

    int* p = NULL;  // 空指针是指内存地址编号为0的内存空间
    
    • 1

    2.5 万能指针

    void*指针可以指向任意变量的内存空间:

    int a = 10;
    // 万能指针可以接收任意类型变量的内存地址
    void* p = &a;
    
    // *p = 100;  // 报错了,非法的间接寻址
    // 在通过万能指针修改变量的值时,需要找到变量对应的指针类型
    *(int*)p = 100;
    
    printf_s("%d\n", a);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.6 const修饰指针变量

    int a = 100;
    int b = 200;
    // 常量指针
    // 修饰 *,指针指向内存区域不能修改,指针指向可以改变
    const int* p1 = &a;
    // *p1 = 2;
    p1 = &b;
    // 指针常量
    //  修饰 p2,指针指向不能改变,指针指向的内存可以修改
    int* const p2 = &a;
    // p2 = &b;
    *p2 = 2;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3、 指针和数组

    3.1 数组指针

    指针操作数组

    数组名字是数组的首元素地址,但它是一个常量

    int arr[] = {1, 2, 3};
    // arr = 10;  // 其报错,因为a相当于一个指针常量,其指向不能改变
    // 数组名是数组第一个元素的首地址
    int* p = arr;  // p 和 arr 等价,指向数组的指针,数组指针
    for (int i = 0; i < 3; ++i) {
        printf_s("%d\n", *p++);  // 使用指针偏移
        // printf_s("%d\n", *(p+i));  // 相当于索引取值
        // printf_s("%d\n", p[i]);  // 索引取值
    }
    int step = p - arr;  // 两指针相减,得到的是指针偏移的步长
    printf_s("%d\", step);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    数组作为函数参数会退化为指针,丢失了数组的精度

    指针操作数组时,下标允许是负数

    3.2 指针加减运算

    • 指针计算不是简单的整数相加
    • 如果是一个int*,+1的结果是增加一个int的大小
    • 如果是一个char*,+1的结果是增加一个char的大小
    #include <stdio.h>
    
    void my_strcpy1(char* dest, char* src) {
        int i = 0;
        while (*(src+i)) {
            *(dest+i) = *(src+i);
            i++;
        }
        *(dest+i) = 0;
    }
    void my_strcpy2(char* dest, char* src) {
        // 纯指针偏移
        while (*src) {
            *dest++ = *src++;
        }
        *dest = 0;
    }
    void my_strcpy3(char* dest, char* src) {
        // 纯指针偏移
        while (*dest++ = *src++);
    }
    
    int main() {
        char str_[] = "hello world";
        char dest[100];
        my_strcpy3(dest, str_);
        printf_s("%s\n", dest);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    两个指针进行运算会变成野指针,其为没有意义的操作(两数组指针相减,其为偏移量);但是可以进行比较运算

    3.3 指针数组

    指针数组,它是数组,数组的每个元素都是指针类型

    int a = 1;
    int b = 2;
    int c = 3;
    int* d = &a;
    int* e = &b;
    int* f = &c;
    int* arr[] = {d, e, f};  // 指针数组,存储指针的数组
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    指针数组里面也可以存储数组,指针数组是一个特殊的二维数组模型

    #include <stdio.h>
    
    int main() {
        int a[] = {1, 2, 3};
        int b[] = {4, 5, 6};
        int c[] = {7, 8, 9};
        int* d[] = {&a, &b, &c};  // int** d = int* d[]; 
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                // 访问指针数组里面的内容
                // printf_s("%d\t", d[i][j]);
                // printf_s("%d\t", *(d[i] + j));
                // printf_s("%d\t", *(*(d+i) + j));
            }
            puts("");
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    4、 二级指针

    二级指针相当于指针数组

    二级指针加偏移量相当于跳过了一维数组的大小

    #include <stdio.h>
    
    int main() {
        int a[] = {1, 2, 3};
        int b[] = {4, 5, 6};
        int c[] = {7, 8, 9};
        int* d[] = {&a, &b, &c};
        int** p = d;
        // 二级指针加偏移量,相当于跳过了一个一维数组
        printf_s("%d", **(p + 1));
        // 一级指针加偏移量,相当于跳过了一个元素
        printf_s("%d", *(*p + 1));
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    5、 指针和函数

    5.1 地址传递

    #include <stdio.h>
    void test(int* a) {
        *a = 20;
    }
    
    
    int main() {
       int a = 10;
       printf_s("调用函数前:%d\n", a);
       test(&a);
       printf_s("调用函数后:%d\n", a);
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    5.2 数组名做函数参数

    数组名做函数参数,函数的形参会退化成指针

    #include <stdio.h>
    #ifndef bool
    typedef int bool;
    #define true 1
    #define false 0
    #endif
    void bubble(int* arr, int len){
        // 升序排序
        for (int i = 0; i < len; ++i) {
            for (int j = 0; j < len - i - 1; ++j) {
                if (arr[j] > arr[j+1]) {
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
    }
    
    
    int main() {
        int a[] = {4, 1, 5, 9, 6, 8, 7};
        bubble(a, 7);
        for (int i = 0; i < 7; ++i) {
            printf_s("%d\t", *(a + i));
        }
        puts("");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    5.3 指针函数

    指针作为函数返回值

    #include <stdio.h>
    #ifndef bool
    typedef int bool;
    #define true 1
    #define false 0
    #endif
    
    char* my_strchr(char* str_, char ch) {
        while (*str_) {
            if (*str_ == ch) {
                return str_;  // 返回切片后的字符数组
            }
            *str_++;
        }
        return NULL;  // 返回没有找到要开始切片的元素
    }
    
    
    int main() {
        char* str_ = "hello world";
        char* b = my_strchr(str_, 'l');
        printf_s("%s\n", b);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    5.4 函数指针

    如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。

    函数指针不可以进行运算

    #include <stdio.h>
    #ifndef bool
    typedef int bool;
    #define true 1
    #define false 0
    #endif
    
    void (*pointer) ();  // 定义一个函数指针
    void func() {
        printf_s("hello");
    }
    void print_hello(void (*p)()) {
        p();
    }
    
    int main() {
        pointer = func;  // 将函数的地址赋值给函数指针
        pointer();  // 调用函数
        
        
        // 也可以将函数指针作为参数传递给函数
        print_hello(func);  
        /*
         等价于
         void (*p)();  // 定义函数指针
         p = func;  // 给函数指针赋值
         p();  // 调用函数指针
         */
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    函数指针一般用于传递回调函数上面

    6、 指针和字符串

    6.1 字符串出现的次数

    使用strstr方法,来统计出现的次数

    #include <stdio.h>
    #include <string.h>
    
    int main() {
        char ch[] = "hello world";
        char a[] = "l";
        char* p = strstr(ch, a);  // 返回第一次出现字符串的地址
        int count = 0;
        while (p) {  // 如果字符串不为空
            count++;
            p += strlen(a);  // 跳过要查找的字符串
            p = strstr(p, a);  // 再次截取字符串
        }
        printf_s("%d\n", count);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    6.2 常用函数

    1. char* strcpy(char* dest, char* src);
      • 功能:将源字符串,拷贝给目标字符串
      • 参数:
        • dest:目标字符串
        • src:源字符串
      • 返回值:
        • 成功返回目标字符串
        • 失败返回NULL
    2. char* strncpy(char* dest, char* src, size_t n);
      • 功能:将源字符串的前n个字符拷贝给目标字符串
    3. int strcmp(char* s1, char* s2);
      • 功能:查看两个字符串是否一样
      • 参数:
        • s1:字符串1
        • s2:字符串2
      • 返回值:
        • s1 = s2:返回值等于0
        • s1 > s2:返回值大于0
        • s1 < s2:返回值小于0
    4. int strncmp(char* s1, char* s2, size_t n);
      • 功能:查看两个字符串的前n个字符是否相同
    5. char* strcat(char* dest, char* src);
      • 功能:将src字符串连接到dest的尾部,\0也会追加过去
      • 参数:
        • dest:目标字符串的首地址
        • src:源字符串的首地址
      • 返回值:
        • 成功返回目标字符串
        • 失败返回NULL
    6. char* strncat(char* dest, char* src, size_t n);
      • 功能:将src字符串前n个字符连接到dest的尾部,\0也会追加过去
    7. int sprintf(char* str, const* format, ...);
      • 功能:根据参数format字符串来转换格式化数据,然后将结果输出到str指定的空间中,直到出现字符串结束符\0为止
      • 参数:
        • str:字符串首地址
        • format:字符串格式化,用法和printf()一样
      • 返回值:
        • 成功返回实际格式化的字符个数
        • 失败返回NULL
    8. char* strchr(const char* s, char c);
      • 功能:在字符串s中查找字符c出现的位置
      • 参数:
        • s:字符串首地址
        • c:字符
      • 返回值:
        • 成功返回第一次出现c的地址
        • 失败返回NULL
    9. char* strstr(const char* dest, const char* src);
      • 功能:在字符串dest中查找字符串src第一次出现的位置
      • 参数:
        • dest:原字符串的首地址
        • src:匹配字符串的首地址
      • 返回值:
        • 成功返回第一次出现字符串的地址
        • 失败返回NULL
    10. char* strtok_s(char* str, const char* delim, char** context);
      • 功能:将字符串以字符串delim分割,返回分割第一个字符串,其余字符串放入context中
      • 参数:
        • str:要分割的字符串
        • delim:分割符
        • context:一个字符串数组的第一个元素的首地址
      • 返回值:
        • 成功返回分割后的字符串
        • 失败返回NULL
    11. int atoi(const char* nptr);
      • 功能:atoi()会扫描nptr,跳过前面的空格字符,知道遇到数字或正负号才开始转换,而遇到非数字或字符串结束符,才会结束转换,并将结果返回
      • 参数:
        • nptr:待转换的字符串
      • 返回自:
        • 成功转换后的整数
      • 类似的还有:
        • atof()/atol()
      • 注意:其在#include <stdlib.h>里面

    八、 内存管理

    1、 作用域

    C语言变量的作用域分别为:

    • 代码块作用域
    • 函数作用域
    • 文件作用域

    1.1 局部变量

    局部变量也叫auto自动变量,auto可不写,一般情况下代码块内部定义的变量都是自动变量,它有如下特点:

    • 在一个函数内定义,只在函数范围内有效
    • 在复合语句中定义,只在复合语句中有效
    • 随着函数调用的结束或复合语句的结束局部变量的声明,声明周期也结束
    • 如果没有赋初值,内容为随机
    • 存储在栈区

    1.2 全局变量

    • 在函数外定义,可被本文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,须用extern声明,extern int a;这里是声明,而不是定义
    • 全局变量的生命周期和程序运行周期一样
    • 不同文件的全局变量不可重名
    • 生命周期,从程序创建到程序销毁
    • 存储在数据区

    1.3 静态变量

    静态局部变量

    • static局部变量的作用域也是在定义的函数内有效
    • static局部变量的生命周期和程序运行周期一样,同时static局部变量的值只初始化一次,但可以多次赋值
    • static局部变量若未赋以初值,则由系统自动赋值,数值型变量自动赋初值0,字符串变量赋空字符
    • 存储在数据区

    静态全局变量

    • 作用域:定义所在的文件中
    • 生命周期:从程序创建到程序销毁
    • 存储位置:存储在数据区

    1.4 全局函数和静态函数

    在C语言中,函数默认都是全局的,使用关键字static可以将函数声明为静态,函数定义为static就意味着这个函数只能在定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数也没法使用

    对于不同文件中的static函数名字可以相同

    注意:

    • 允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的单元,互不干扰
    • 同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用
    • 所有的函数默认都是全局的,意味着所有的函数都不能重名,但如果是static函数,那么作用域是文件级的,所以不同的文件static函数名是可以相同的

    2、 内存布局

    2.1 内存分区

    C 代码经过预处理、编译、汇编、链接,4步后生成一个可执行程序

    代码区:

    • 程序执行二进制码(程序指令)
    • 共享、只读

    数据区:

    • 初始化数据区(data)
    • 未初始化数据区(bss)
    • 常量区

    栈区:

    • 系统为每一个程序分配一个临时的空间
    • 存储局部变量、函数信息、函数参数、数组
    • 栈区大小为:1M;在Windows中可以扩展到10M;在Linux中可以扩展到16M

    堆区:

    • 存储大数据、图片、音乐、视频
    • 手动开辟:malloc colloc realloc
    • 手动释放:free()

    程序在加载到内存前,代码区和全局区的大小就是固定的,程序运行期间不能改变。然后,执行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区、数据区和未初始化数据区之外,还额外增加了栈区、堆区

    2.2 堆空间开辟和释放

    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        // 开辟堆空间存储数据
        int* p = (int *)malloc(sizeof(int)*1000);  // 开辟1000个整型数据的空间
        // 使用堆空间
        *p = 123;
        printf_s("%d", *p);
        // 释放堆空间
        free(p);
        // 将指针置空,防止出现野指针
        p = NULL;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    注意:

    • 释放指针要释放同一个指针

      #include <stdlib.h>
      
      int main() {
          int* p = (int *)malloc(sizeof(int)*1000);
          p = 123;  // 改变了指针的地址,同时指针偏移也会改变指针的地址,其为无主指针
          free(p);  // 释放指针会报错
          return 0;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

    2.3 内存操作函数

    #include <string.h>

    1. void* memset(void* s, int c, size_t n);

      • 功能:将s的内存区域的前n个字节以参数c填入
      • 参数:
        • s:需要操作内存s的首地址
        • c:填充的字符,unsigned char—— 0 ~ 255
        • n:指定需要设置的大小,单位是字节
      • 返回值:s的首地址
    2. void* memcpy(void* dest, void* src, size_t n);

      • 功能:拷贝src所指的内存内容的前n个字节到dest所指的内存地址上
      • 参数:
        • dest:目标内存首地址
        • src:原内存首地址,注意:首地址不可以重叠
        • n:需要拷贝的字节数
      • 返回值:dest的首地址

      memmove()功能用法和memcpy()一样,区别在于:当内存空间重叠时,memmove()仍然能处理,不过执行效率更低

    3. void* memcmp(const void* s1, const void* s2, size_t n);

      • 功能:比较s1和s2所指向内存区域的前n个字节
      • 参数:
        • s1:内存首地址s1
        • s2:内存首地址s2
        • n:需比较的前n个字节
      • 返回值:
        • s1 = s2:返回值等于0
        • s1 > s2:返回值大于0
        • s1 < s2:返回值小于0
  • 相关阅读:
    ElasticSearch基本用法
    Windows程序意外挂掉,但显存依然被占用
    Xshell 常用命令大全手册
    【Vue】数据监视&输入绑定
    【sdk】- 对接阿里云抠图
    【Java 基础篇】Java Set 集合详解:轻松管理不重复元素
    2024年第三届数据统计与分析竞赛(A题)数学建模完整思路+完整代码全解全析
    为何学linux及用处
    ESP32烧写Arduino编译的固件
    时间与日期
  • 原文地址:https://blog.csdn.net/qq_62789540/article/details/125502457