第五弹:C语言基础——函数详解

目录

 函数的概述

1 函数的概念

2 函数的意义

 函数的定义和使用

1 函数的定义

2 函数的调用

2.1 在同一文件中函数定义后函数调用

2.2 在同一文件中函数定义前函数调用

2.3 调用其它文件中定义的函数

2.3.1 在函数调用文件中进行声明

2.3.2 在头文件中进行函数的声明

 函数的参数

1 函数传参

1.1 值传参

1.2 地址传参

1.3 全局变量传参

2 数组作为函数参数

3 主函数的参数

3.1 main函数的返回值

3.2 main函数的参数

 函数和指针

1 指针函数

2 函数指针

3 函数指针数组

4 函数指针作为函数的参数

 字符串处理函数

1 字符串

2 字符串处理函数

2.1 字符串长度计算——strlen

2.2 字符串拷贝——strcpy/strncpy

2.3 字符串的连接——strcat/strncat

2.4 字符串比较——strcmp/strncmp

2.5 字符串查找——strstr

3 字符串数据和数据类型数据转换处理函数

3.1 数字字符串数据转换为整型数据

3.2 基本数据的格式输出到字符数组空间

3.3 字符数组数据格式化到数据变量

4 字符串切割函数

知识点5 递归函数

 内存管理

1 内存管理的方式

1.1 静态内存管理

1.2 动态内存管理

2 动态内存管理实现

2.1 动态内存开辟和释放

2.2 野指针和内存泄漏

3 内存分别

 程序的编译过程


 函数的概述

1 函数的概念

所谓的函数,指的是具有特定功能的有序指令语句的集合,可以提供使用者调用以完成功能语句的执行。此时的功能语句块由设计者实现模块化编程设计,使用者通过函数的调用实现代码重用的效果。

函数的实质:是实现代码的模块化编程和代码重用的设计思想。

2 函数的意义

1) 函数的定义可以实现功能语句的模块化编程设计和代码重用;

2) 对于共用性的功能型语句通过函数的调用,简化程序设计的过程;

3) 简化项目的设计的框架或者逻辑;

4) 通过函数的模块化设计,以实现项目的分工协作。

 函数的定义和使用

1 函数的定义

存储类型 数据类型 函数名(形参列表)/* 称为函数头,花括号包含的部分称为函数体 */

{

函数体中的功能性有序指令集合;

return语句 ;

}

注意:

1. 存储类型:修饰整个函数的属性,只能使用关键字static

        1) 没有使用static关键字修饰,此时函数的作用域(或者链接属性)为整个程序域,也就是可以在当前程序中的任意位置访问;

        2) 有使用static关键字修饰,此时函数的作用域为文件域,也就是只能在当前定义函数的文件中访问;

2. 数据类型:修饰函数返回结果数据值的数据类型

        1) 如果函数不需要返回数据值,此时使用void数据类型表示;此时的return语句可以省略,也可以不省略但是只能使用(return ;)表示,不能跟返回结果;

        2) 如果函数需要返回数据值,使用返回的数据值的数据类型表示

                i. 返回实际数据值;

                ii. 返回值函数调用的状态;

                iii 返回值同时是数据和状态;

3. 函数名:提供给使用者访问有序执行指令集合的接口名称。

4. 形参列表:表示函数数据的输入和输出的参数

        1) 无参数,函数功能语句的执行不需要数据的输入和输出,此时使用void表示。

        2) 有参数:

                可以有一个或者多个形参构成

                每一个形参的组成:数据类型 形参变量名,需要注意形参变量为局部变量,在函数模块内有效;

                如果是多个形参,形参之间使用逗号分隔;

5. 函数体的部分:使用花括号{}所包含的有序指令语句的集合。

/* 定义无参无返回值的函数 */
void test1(void)
{
        return;        /* 此处的return语句可以省略;同时如果不省略的时候,不能接返回数据值 */
}

/* 定义有参有返回值的函数 */
int add(int a, int b)    /* 形参变量作用域为函数域 */
{
        return a+b;
}

2 函数的调用

所谓函数的调用,指的是使用者根据函数名访问函数,在调用的同时会根据函数形参列表传递实际参数的过程。

2.1 在同一文件中函数定义后函数调用

函数定义和函数调用在同一个文件中,并且函数调用在函数定义之后,此时可以直接函数调用。

#include <stdio.h>
/* 函数的定义,在定义的同时也完成函数的声明过程 */
int add(int a, int b)
{
        return a+b;
}

int main()
{
        int x = 3;
        int y = 4;
        int z;

        z = add(x, y);     /* 在同一个文件中,函数定义后实现函数的调用 */
    
        printf("z = %d\n", z); 
}

2.2 在同一文件中函数定义前函数调用

函数定义和函数调用在同一个文件中,函数的调用在函数定义之前,需要对函数进行声明之后在调用。

函数的声明:

1. 函数声明的目的

是告诉编译器,函数定义在其它地方已经完成,在当前文件中可以直接调用;

2. 函数声明的语法:

extern 数据类型 函数名(形参列表);

        1) extern 是对函数进行外部声明引用,可以省略;

        2) 形参列表中的形参变量名可以省略;

int sub(int a, int b);        /* 函数声明,可以等价于以下几种情况 */
#if 0
extern int sub(int a, int b);
extern int sub(int, int);
int sub(int, int);
#endif

int main()
{
        int x = 3;
        int y = 4;
        int z;

        z = sub(x, y);    /* 函数的调用 */
    
        printf("z = %d\n", z); 
}
/* 函数定义 */
int sub(int a, int b)
{
        return a-b;
}

2.3 调用其它文件中定义的函数

函数调用之前,需要对其它文件中所定义的函数进行函数声明

2.3.1 在函数调用文件中进行声明

根据所需要调用的函数,选择函数进行声明

#include <stdio.h>
/* 定义无参无返回值函数 */
void test1(void)
{
        printf("test1\n");
}

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

int sub(int a, int b)
{
        return a-b;
}

2. 函数调用的源文件:app.c

#include <stdio.h>
/* 函数声明:对需要调用的函数在函数调用之前进行函数声明 */
void test1(void);
extern int add(int a, int b); 

int main()
{
        test1();

        printf("%d\n", add(5,2));
}

注意:程序的编译需要对函数定义和函数调用的源文件进行编译

gcc app.c func.c/* 编译成功默认生成的可执行程序文件为a.out */

2.3.2 在头文件中进行函数的声明

所定义的函数很多,且函数调用频率很高,设计者使用头文件进行函数的声明;对于使用者只需要包含头文件后直接进行函数的调用。

1. 函数定义的源文件:func.c

#include <stdio.h>

void test1(void)
{
        printf("test1\n");
}

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

int sub(int a, int b)
{
        return a-b;
}

2. 函数声明的头文件:func.h

#ifndef _FUNC_H_
#define _FUNC_H_
/* 函数声明 */
void test1(void);
extern int add(int a, int b); 
extern int sub(int a, int b); 

#endif

头文件中注意:

         1) 防止头文件被重复展开标识符

#ifndef _FUNC_H_
#define _FUNC_H_
    /* 数据类型的定义,或者函数和变量的外部声明应用*/
#endif

        2) 在头文件中不能做数据类型变量的定义或者函数的定义,否则可能出现重复定义。

3. 函数调用的源文件:app.c

#include <stdio.h>    /* 头文件的包含:尖括号<>所包含的头文件是在系统环境变量路径中所能找到的头文件 */

#include "func.h"     /* 头文件的包含:双引号""所包含的头文件,在系统环境变量路径下不能找到,一般为自定义的头文件 */

int main()
{
        test1();

        printf("%d\n", add(5,2));
}

注意:程序的编译需要对函数定义和函数调用的源文件进行编译,不需要对函数声明的头文件进行编译

gcc app.c func.c/* 编译成功默认生成的可执行程序文件为a.out */

 函数的参数

1 函数传参

在函数定义的时候,可以给所定义的函数设置形式参数,在函数调用的时候,需要将相同数据类型的实际参数传递给形式参数,需要完成函数传参的问题。在C语言中函数的传参注意包含三种形式,分别为值传参、地址传参和全局变量传参。

1.1 值传参

所谓的值传参,指的是形参为数据类型变量,在函数调用的时候,将相同数据类型实参值传递给形参的过程。其实质将实参值赋值给形参变量值。

特定:

1) 值传参只能实现数据值的输入,不能实现数据值的输出;

2) 对于形参变量值的修改,不会影响实参数据值。

3) 对于函数的返回值,也是采用值传参实现。

4) 形参变量存储空间和实参变量存储空间之间无任何关联。

#include <stdio.h>


void setVal(int val)
{
        printf("val = %d\n", val);
        val = 100;    /* 对形参变量val值的修改不会影响实参变量a的数据值 */
        printf("val -> %d\n", val);
}

int main()
{
        int a = 1;

        setVal(a);    /* 函数的调用,将实参变量a的值赋值给形参变量val, 此时val和a存储空间无任务管理 */
        printf("a = %d\n", a); 
}

1.2 地址传参

所谓地址传参,指的是在形参为指针变量的时候,将实参数据的地址赋值给形参变量;此时形参指针变量便指向的空间为实参数据空间。

特定:

        1) 地址传参,可以实现数据的输入和输出。

        2) 通过形参地址的解引用访问的是实参,通过形参修改实参数据值;

        3) 形参为指针变量所指向空间为实参空间。

#include <stdio.h>
void Swap(int *p, int *q) 
{
        int temp;
        temp = *p;
        *p = *q;
        *q = temp;
}

int main()
{
        int x = 3;  
        int y = 5;


        printf("%d: x = %d, y = %d\n", __LINE__, x, y);    /* x = 3, y = 5 */
        Swap(&x, &y);
        printf("%d: x = %d, y = %d\n", __LINE__, x, y);     /* x = 5, y = 3 */
}

注意:

地址传参的使用,在需要在函数内部去修改实参数据值的时候使用。

如果需要修改的是数据变量,则使用数据变量相同数据类型的一级指针作为形参;

如果需要修改数据是指针变量,则使用指针变量数据类型的指针作为形参。

1.3 全局变量传参

所谓的全局变量传参,不是真正意义上利用参数实现数据的传递,实质是利用全局变量的属性实现数据值的传递过程。由于全局变量的作用域为整个程序域,所以可以在不同的函数模块内访问到全局变量的数据值。从而实现不同模块之间数据传递的过程。

特定:

        1) 全局变量传参,实现简单。

        2) 如果在函数模块内访问全局变量,则此时的模块依赖于全局变量的存在,模块的兼容性和可移植性会更低。

        3) 在一般情况下,不建议使用。

#include <stdio.h>
int m = 4;
int n = 5;

void SWAP()
{
        int temp;
        temp = m;
        m = n;
        n = temp;
}

int main()
{
        printf("%d: m = %d, n = %d\n", __LINE__, m, n);
        SWAP();
        printf("%d: m = %d, n = %d\n", __LINE__, m, n);
}

2 数组作为函数参数

数组的访问不能整体访问,只能通过数组名和元素序号逐一元素访问;

数组名[下标];下标范围:[0, 数组元素个数)

对于数组参数的传递,也不能整体传参;

#include <stdio.h>

extern void showArr(int a[10]);

int main()
{
        int arr[10] = {1,2,3,4,5,6,7,8,9,10};
        showArr(arr);
}

void showArr(int a[10])
{
        int i;
        for (i = 0; i < sizeof(a)/sizeof(a[0]); i++)
                printf("a[%d] = %d\n", i, a[i]);
}

注意:

程序的执行结果,不能将整个数组中的所有元素顺序输出,实际输出元素的个数:

        1) 在64位操作系统中输出为:arr[0] 和 arr[1] 的结果。

        2) 在32位操作系统中输出为:arr[0]的结果。

原因:

数组作为形参的时候,不在表示为数组;实质表示的数组元素类型的指针变量,所接受的为所传递数组首元素地址。

在数组传参的时候,需要传递数组首元素地址和数组元素的个数

#include <stdio.h>


extern void show_arr(int p[], int nmember);    /* 数组作为形参表示数组元素类型的指针,等价于void show_arr(int *p, int nmember); */


int main()
{
        int arr[10] = {1,2,3,4,5,6,7,8,9,10};                 
        show_arr(arr, sizeof(arr)/sizeof(arr[0]));
}

void showArr(int a[10])
{
        int i;
        for (i = 0; i < sizeof(a)/sizeof(a[0]); i++)
                printf("a[%d] = %d\n", i, a[i]);
}

void show_arr(int p[], int nmember)
{
        int i;

        for (i = 0; i < nmember; i++)
                printf("p[%d] = %d\n", i, p[i]);
}

3 主函数的参数

在应用程序设计的时候,C语言程序的入口函数为main函数,其参数和返回值可以有多种表示形式;

3.1 main函数的返回值

1. 主函数无返回值
    main函数返回值数据类型使用void标识
void main()
{
}
2. main函数有返回值
    main函数的返回值数据类型可以是任意数据类型,还可以是指针。需要根据需求设计

3.2 main函数的参数

1. main函数无参数情况

此时main函数的参数列表需要使用void表示

int main(void)
{
}

2. main函数有参数,在函数内不能访问

int main()    /* 此时的main函数的参数列表为可变参数列表,可以传递任意数据类型的任意个参数 */
{
}

3. main函数可以通过shell终端传递任意个字符串数据

int main(int argc, char *argv[])
{
        int i;

        printf("argc = %d\n", argc);

        for (i = 0; i < argc; i++)
                printf("argv[%d] : %s\n", i, argv[i]);
        return 0;     
}

 函数和指针

1 指针函数

所谓的指针函数,实质是函数;函数返回值数据类型为指针的函数称为函数指针。

一般用于字符串操作函数和内存管理的函数,以便于实现内存的链式存储。

定义语法:

存储类型 数据类型 * 函数名(形参列表) 
{
}

和普通函数的区别:

唯一的区别在于指针函数返回值数据类型为指针,其余没有任何区别。

#include <stdio.h>

char * my_strcpy(char *dest, const char *src)
{
        char *p = dest;
        const char *q = src;
        while(*q != '\0') {
                *p = *q; 
                p++;
                q++;
        }
    
        return dest;
}


int main()
{
        char *str = "hello";
        char buf[16];
#if 0
        my_strcpy(buf, str);
        printf("buf : %s\n", buf);
#else
        printf("%s\n", my_strcpy(buf, str));    /* 指针函数的返回值作为其它函数调用的参数,以实现数据的链式存储表示 */
#endif
}

2 函数指针

所谓的函数指针,实质是指针,表示为指向函数的指针称为函数指针。

所定义的任意的函数,都需要在内存中存储,而存储空间的起始地址可以定义函数指针存储。

函数指针定义语法:

数据类型 (*函数指针变量名)(形参列表);

函数指针初始值设置:

函数指针在设置值的时候,只能将函数的形参和返回值数据类型均一致的函数地址进行赋值。

#include <stdio.h>
/* 函数定义:
 * 此时函数名add有两层含义:
 * 1) 表示函数中有序指令集合名称,也就是所谓的函数名称
 * 2) 表示函数有序指令集合存储地址,也就是所谓的函数指针;
 */ 
int add(int a, int b)
{
        return a+b;
}

int sub(int a, int b)
 {
     return a-b; 
 }
   
int mul(int a, int b)
{
    return a*b;
}
int main()
{
        int x = 5;
        int y = 3;
        int z;

        int (*p)(int, int);    /* 定义函数指针 */

        z = add(x, y); 
        printf("z = %d\n", z); 

        p = &add;                        /* 设置函数指针变量p的值,值为add函数的地址 */
        printf("%d\n", (*p)(x, y));      
        
        p = sub;                         /* 函数名也是函数地址,将sub函数的地址sub赋值给函数指针变量p */
        printf("%d\n", p(x, y));          
        p = mul;                         /* 函数名也是函数地址,将mul函数的地址mul赋值给函数指针变量p */
        printf("%d\n", p(x, y));         /* 相同语句会根据指针指向不同,实现不同的功能。指向是在程序执行过程中决定 */
}

注意:

函数之间访问和函数指针访问函数的区别:

1. 相同点:都可以实现函数的访问

2. 不同点:

        a) 函数直接访问,在程序编译过程中根据函数名决定所访问的函数;

        b) 函数指针访问,在程序指向过程中根据函数指针所指向的函数决定所访问函数功能;

3 函数指针数组

所谓的函数指针数组,实质是数组,数组中的每一个元素为函数指针。

定义的语法:

        数据类型 (* 函数指针数组名[常量表达式])(形参列表);

        常量表达式:表示数组元素的个数;

        函数指针数组名:表示数组集合变量名

        形参列表:表示数组元素所表示函数指针所指向函数的形参列表;

        数据类型:表示数组元素所表示函数指针所指向函数返回值的数据类型;

#include <stdio.h>

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

int sub(int a, int b)
{
        return a-b;
}

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

typedef int (*FUNC_P) (int,int);        /* 返回值为int,参数为(int, int)的函数指针取别名为FUNC_PFUNC_P */
   




int main()
{
        int x = 5;
        int y = 3;
        int i;
#if 0
        /* 定义函数指针数组: */
        int (*arr[3])(int, int) = {add, sub, mul};
#else
        FUNC_P arr[3] = {add, sub, mul};
#endif
        for (i = 0; i < sizeof(arr)/sizeof(arr[0]); i++) {
                printf("%d\n", arr[i](x, y));
        }
}

4 函数指针作为函数的参数

        在通常情况,函数调用过程是:使用者调用设计者所设计的函数完成某功能,在实际应用过程中可能存在设计者不确定功能实现的具体方法,需要由设计者实现,但是设计者不具备该功能实现的机制。

        此时设计者在设计函数模块的时候,将函数的参数设置为函数指针,由设计者定义回调函数提供给设计者进行调用。

求创建回调函数,并将函数地址提供给设计者,设计者内部通过回调函数地址访问回调函数,以实现具体功能 */

#include <stdio.h>


/* 设计者设计运算器函数接口:可以实现两个整型数据的运算功能,但是具体运算实现不确定 */
int test(int(*p)(int, int), int a, int b)
{
        return p(a, b); 
}

/* 使用根据自己需求创建回调函数,并将函数地址提供给设计者,设计者内部通过回调函数地址访问回调函数,以实现具体功能 */
int add(int a, int b)
{
        return a+b;
}

int sub(int a, int b)
{
        return a-b;
}

int main()
{
        int x = 5;
        int y = 3;    

        printf("%d\n", test(add, x, y));    /* 两个整型数据的加法运算 */
        printf("%d\n", test(sub, x, y));    /* 两个整型数据的减法运算 */
}

应用实例:

#include <stdlib.h>
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
功能:实现任意数据类型数组元素的排序
参数:
    参数1:base 表示数组首元素基地址;
    参数2:nmemb 表示数组元素的个数;
    参数3:size 表示数组中元素所占空间大小
    参数4:comper是函数指针,指向的是由使用者根据自定义的需要排序数组元素的数据类型所定义的回调函数,用于数组元素之间大小的比较

 字符串处理函数

1 字符串

所谓的字符串,指的是以字符'\0'结尾的连续多个字符所构成的集合称为字符串;包含字符串常量和字符串变量。

1. 字符串常量

所谓的字符串常量,指的是不能被修改的字符串数据,称为字符串常量;

        a) 直接表示字符串常量

                "char"、"hello"

        b) 指针表示:

                char p = "long";/ 开辟两段存储空间,分别为:

                1) 字符串常量"long"的存储空间,

                2)指针变量p的存储空间(可以修改);p所指向的存储空间的内容为字符串常量(不能修改) */

p[0] = 'a';/* 运行时出现错误,p所指向的是字符串常量数据,不能修改

                在实际应用过程中常定义const指针变量存储字符串常量的地址;

                const char *p = "long" */

字符串常量:存储在内存的常量区。

2. 字符串变量

所谓的字符串变量,指的是可以被修改的字符串数据,称为字符串变量;

        a) 字符数组存储字符串数据:

                一般只会使用到字符数组的部分空间实现字符串变量数据的存储;只要遇到'\0'字符串结束。

                避免字符串访问过程中对于数组空间的越界。

                char buf[] = "long";/* 开辟两段存储空间,分别为:

                1) 字符串常量"long"的存储空间,只有在初始化赋值的时候能够访问,后续不能再次访问到;

                2) 字符数组的存储空间(5字节);里面的内容可以修改*/

b) 动态管理的字符指针指向空间存储字符串数据。

        字符串变量:存储在栈区、堆区、和静态存储区的字符串数据

2 字符串处理函数

2.1 字符串长度计算——strlen

/* 函数原型 */
#include <string.h>
char *strcpy(char *dest, const char *src);
功能:将指针src所指向的字符串数据(包含'\0')拷贝到指针dest所指向的目标存储空间中
参数:
    参数1:src表示源字符串数据的起始地址;
    参数2:dest表示目标存储空间起始地址;
返回值:
    返回的是目标存储空间起始地址,实现数据的链式存储
/* 自定义实现 */  
char *my_strcpy(char *dest, const char *src)
{
#if 0
        int i;
        for (i = 0; src[i] != '\0'; i++) {
                dest[i] = src[i];
        }
        dest[i] = src[i];
#else
        char *p = dest;
        const char *q = src;
        while(*p++ = *q++);
#endif
        return dest;
}      
                                 
char *strncpy(char *dest, const char *src, size_t n);
功能:将指针src所指向的字符串数据(包含'\0')的前n个字符拷贝到指针dest所指向的目标存储空间中;
参数:
    参数1:src表示源字符串数据的起始地址;
    参数2:dest表示目标存储空间起始地址;
    参数3:拷贝的字符个数
        n >= 指针src所指向字符串数据的长度的时候,按照src实际字符个数拷贝(包含'\0'),等价于strcpy
        n < 指针src所指向字符串数据长度的时候,拷贝前n个字符
返回值:
    返回的是目标存储空间起始地址,实现数据的链式存储。

在字符数组中,sizeof和strlen的区别:

        1) sizeof是运算符关键字,计算整个数组所占内存空间的大小;

        2) strlen是函数,计算字符串中字符的个数,遇到'\0'结束;

2.2 字符串拷贝——strcpy/strncpy

/* 函数原型 */
#include <string.h>
char *strcpy(char *dest, const char *src);
功能:将指针src所指向的字符串数据(包含'\0')拷贝到指针dest所指向的目标存储空间中
参数:
    参数1:src表示源字符串数据的起始地址;
    参数2:dest表示目标存储空间起始地址;
返回值:
    返回的是目标存储空间起始地址,实现数据的链式存储
/* 自定义实现 */  
char *my_strcpy(char *dest, const char *src)
{
#if 0
        int i;
        for (i = 0; src[i] != '\0'; i++) {
                dest[i] = src[i];
        }
        dest[i] = src[i];
#else
        char *p = dest;
        const char *q = src;
        while(*p++ = *q++);
#endif
        return dest;
}      
                                 
char *strncpy(char *dest, const char *src, size_t n);
功能:将指针src所指向的字符串数据(包含'\0')的前n个字符拷贝到指针dest所指向的目标存储空间中;
参数:
    参数1:src表示源字符串数据的起始地址;
    参数2:dest表示目标存储空间起始地址;
    参数3:拷贝的字符个数
        n >= 指针src所指向字符串数据的长度的时候,按照src实际字符个数拷贝(包含'\0'),等价于strcpy
        n < 指针src所指向字符串数据长度的时候,拷贝前n个字符
返回值:
    返回的是目标存储空间起始地址,实现数据的链式存储。

2.3 字符串的连接——strcat/strncat

/* 函数原型 */
#include <string.h>
char *strcat(char *dest, const char *src);
功能:将指针src所指向的字符串数据(包含'\0')拷贝到指针dest所指向字符串数据的末尾('\0'地址位置开始);
参数:
    参数1:src表示源字符串数据的起始地址;
    参数2:dest表示目标存储空间起始地址;
返回值:
    返回的是目标存储空间起始地址,实现数据的链式存储。
    
/* 自定义函数实现*/
char *my_strcat(char *dest, const char *src)
{
        int i;
        int j;

        for (i = 0; dest[i] != '\0'; i++);

        for (j = 0; src[j] != '\0'; j++)
                dest[i+j] = src[j];
        dest[i+j] = src[j];

        return dest;
}

char *strncat(char *dest, const char *src, size_t n);
功能:将指针src所指向的字符串数据(包含'\0')的前n个字符拷贝到指针dest所指向字符串数据的末尾('\0'地址位置开始);
参数:
    参数1:src表示源字符串数据的起始地址;
    参数2:dest表示目标存储空间起始地址;
    参数3:拷贝的字符个数
        n >= 指针src所指向字符串数据的长度的时候,按照src实际字符个数拷贝(包含'\0'),等价于strcat
        n < 指针src所指向字符串数据长度的时候,拷贝前n个字符
返回值:
    返回的是目标存储空间起始地址,实现数据的链式存储。

2.4 字符串比较——strcmp/strncmp

/* 函数原型 */
#include <string.h>
int strcmp(const char *s1, const char *s2);
功能:比较s1和s2所指向字符串数据,对字符串中对应序号的字符ASCII编码值的比较
返回值:
    s1 > s2 返回1,s1 == s2 返回0,s1 < s2 返回-1
/* 自定义实现 */    
int my_strcmp(const char *s1, const char *s2)
{
        int i;

        for (i = 0; s1[i] != '\0' && s2[i] != '\0'; i++) {
                if (s1[i] > s2[i])
                        return 1;
                if (s1[i] < s2[i])
                        return -1;
        }

        if (s1[i] == '\0' && s2[i] == '\0')
                return 0;
        if (s1[i] == '\0')
                return -1;
        if (s2[i] == '\0')
                return 1;
}                

int strncmp(const char *s1, const char *s2, size_t n);
功能:比较s1和s2所指向的字符数据中的前n个字符
参数:
    n >= s1字符串中字符个数 || n >= s2字符串中字符个数,等价于strcmp
    n < s1 字符串中字符个数 && n < s2字符串中字符个数;只会对前n个字符进行比较;

2.5 字符串查找——strstr

/* 函数原型 */
#include <string.h>
char *strstr(const char *haystack, const char *needle);
功能:在haystack指针指向的父串中查找needle指针指向的子串是否存在
参数:
    参数1:haystack表示父串的起始地址;
    参数2:needle表示子串的起始地址
返回值:
    如果存在,返回子串在父串中第一次出现的起始地址;
    如果不存在,返回NULL;
/* 自定义实现 */
char *my_strstr(const char *haystack, const char *needle)
{
        const char *p = haystack;
        const char *q = needle;
        const char *r; 

        while(*p != '\0') {
                if (*p != *q) {
                        p++;
                        continue;
                }
    
                r = p;
                q = needle;
                while(*q != '\0') {
                        if (*q == *r) {
                                q++;
                                r++;
                        } else {
                                break;
                        }
                }
                if (*q == '\0')
                        return p;
        }
        return NULL;
}

3 字符串数据和数据类型数据转换处理函数

3.1 数字字符串数据转换为整型数据

/* 函数原型 */
#include <stdlib.h>

int atoi(const char *nptr);            /* int 类型的整型数据 */
long atol(const char *nptr);           /* long 类型的整型数据 */
long long atoll(const char *nptr);     /* long long 类型的整型数据 */
功能:将数字字符串数据转换为整型数据
    注意:转换只针对于字符'0' ~ '9' 的数字字符,其它字符不能转换会自动结束;
/* 自定义函数 */
int my_atoi(const char *nptr)
{
        const char *p = nptr;
 
        int num = 0;

        while(*p >= '0' && *p <= '9') {
                num = 10*num + (*p - '0');
                p++;
        }
        return num;
}

3.2 基本数据的格式输出到字符数组空间

int sprintf(char *str, const char *format, ...);
参数:
    str:目标存储空间起始地址
    format:格式化信息,和printf一致;
返回值:
    返回格式成功的字节数;
                
int snprintf(char *str, size_t size, const char *format, ...);
功能等价于sprintf,比sprintf更安全:
    传递了存储空间的大小,避免字符数组存储空间的越界访问。

3.3 字符数组数据格式化到数据变量

int sscanf(const char *str, const char *format, ...);
参数:
    str:表示字符数组存储空间起始地址;
    format:格式化信息,和scanf一致。接收数据使用数据的存储空间地址表示

4 字符串切割函数

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


int main()
{
        char buf[] = "asdffj;jsksaj;kfjkawe;kjgkes;jkgesrk;jgjserkj";
        char *arr_p[8];
        int i = 0;
        char *p;

        p = strtok(buf, ";");
        while(p != NULL) {
                arr_p[i] = p;
                i++;
                p = strtok(NULL, ";");
        }

        while(i--) {
                printf("%s\n", arr_p[i]);
        }   
}

知识点5 递归函数

所得递归函数,函数在执行过程中,满足函数自己调用自己或者间接调用自己的时候,此时的函数称为递归函数。

递归函数算法思路满足递推和回归两个阶段:

递归函数的原则:

1) 当算法满足,当前数据运算,和去掉当前数据运算算法一致的时候,此时可以采用递归函数实现

2) 递归具体实现:

        单个函数的递归:函数自己调用自己本身;

        多个函数的递归:函数之间循环调用

        如果两个函数:A->B,B->A

3) 递归函数,必须存在结束条件。需要避免递归调用过程中出现死循环过程。

注意:所有的递归函数,都可以使用循环控制逻辑实现,递归简化代码。

#include <stdio.h>

int func(int x)
{
        int i;
        int num = 1;

        for (i = 1; i <= x; i++)
                num *= i;
        return num;
}

int test(int x)
{
        if (x == 0)
                return 1;
        return x * test(x-1);
}

int main()
{
        int i = 0;

        for (i = 0; i < 10; i ++) {
                printf("%d! = %d(%d)\n", i, func(i), test(i));
        }
}

练习:棋盘上放麦粒的问题

1) 现有棋盘,一共有64格,格子编号为0-63;

2) 在格子中放麦粒:

        编号为0的格子放1粒,后续格子麦粒数是前一个格子麦粒的2被;

        格子的编号:0 1 2 3

        格子麦粒数:1 2 4 8

        编号为n的格子的麦粒数 = 2 * 编号为n-1的格子麦粒数

问:编号为i的格子麦粒数据,以及编号为i的格子的前面所有格子麦粒数。

 内存管理

在Linux系统中,程序的运行会创建进程,进程是资源管理的最小单位。

1) 在32位操作系统中

        程序运行的进程自动构造4G的虚拟内存空间,其地址编号为:0x0 ~ 0xffff ffff。程序中数据(程序执行指令、常量、变量)都会在虚拟内存中存储,所有的虚拟内存是被完全访问

2) 在64位操作系统中,虚拟内存的最大数是140737488224256,换算成16进制是0x7FFFFFFE0000。虚拟内存空间未被完全访问,其中未被访问的空间未保留。

1 内存管理的方式

根据数据存储空间在虚拟内存中的开辟方式分为:静态内存管理和动态内存管理

1.1 静态内存管理

所谓的静态内存管理,指的是数据存储空间会随着 程序运行过程中自动开辟和释放

在程序编译的时候,决定数据存储空间的大小和生命周期,在程序执行的时候按照预先所设定进行空间内存管理。

主要包含全局变量和局部变量存储空间

1. 全局变量:

        生命周期为程序执行开始到程序执行结束;

        存储位置为静态存储区

        作用域(链接属性):默认为全局域,使用static关键修饰作用域为当前文件域。

2. 局部变量:

        a) 默认的auto修饰的局部变量

                生命周期为程序语句执行到模块结束;

                存储位置为栈区

                作用域为模块域

        b) static修饰的局部变量

                生命周期为程序语句执行到程序结束

                存储位置为静态存储区

                作用域为模块域

1.2 动态内存管理

所谓的动态内存管理,指的是由使用者根据数据存储空间的大小和生命周期管理内存;

生命周期:空间的使用者手动开辟到空间的使用者手动释放

存储位置:堆区;

作用域:是由空间起始地址的存储变量的属性表示。

2 动态内存管理实现

在标准C库中,提供了动态内存管理函数:malloc、calloc、realloc、free

2.1 动态内存开辟和释放

#include <stdlib.h>
void *malloc(size_t size);
功能:malloc动态开辟指定size字节大小的连续内存空间
    实际开辟内存空间的大小略大于size字节,多出部分用于存储空间的相关信息。
参数:
    size > 0,按照指定size字节数开辟存储空间,成功返回空间的起始地址,开辟成功的空间数据未初始化;失败返回NULL
    size == 0,可能返回NULL;也可能返回开辟成功内存空间的起始地址,所开辟成功内存空间不能进行读写访问,否则导致内存的越界访问。
返回值:
    返回值为void类型指针,可以用于存储任意数据类型的地址,在动态内存管理过程中可以根据使用者的实际需求进行指针类型的转换,以实现动态内存存储任意数据类型的数据            
void free(void *ptr);
功能:释放指针变量ptr所指向的动态开辟内存空间;
注意:
    1) 动态内存空间的释放,只需要传递空间的起始地址;
    2) 在动态内存释放之后,需要将指针变量设置为NULL,表示指针变量没有指向。否则出现野指针,此时访问出现未知异常。
    3) 对于动态开辟的内存空间,在不使用的时候,需要及时动态释放,否则一直占用内存,导致程序执行效率降低。
    
void *calloc(size_t nmemb, size_t size);
功能:calloc按照数据元素个数nmemb和元素空间字节数size开辟连续的nmemb*size字节内存空间
    等价于:malloc(nmemb*size);
    
void *realloc(void *ptr, size_t size);
功能:
    对ptr指针指向的已开辟的原空间进行重新开辟指定大小的size字节大小空间,用于原空间的减小和放大处理
参数:    
    参数1:ptr表示原开辟成功内存空间的起始地址;
    参数2:size表示新开辟内存空间的字节数
        size <= 原开辟成功内存空间大小,表示内存空间截短。保留原内存空间的前size字节空间,返回值为原ptr地址值;
        size > 原开辟成功内存空间大小:
            size <= 原开辟内存空间大小 + 向后未使用连续内存空间大小
                直接在原空间后追加开辟空间,并返回原空间地址ptr的值;
            size > 原开辟内存空间大小 + 向后未使用连续内存空间大小
                重新找到足够连续未使用内存空间开辟,并将原内存空间数据拷贝到新开辟内存空间,在释放原内存空间;

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

int main()
{
        int i;
        char *p; 
        char *str;
        
        p = (char *)malloc(4);
        if (p == NULL) {
                perror("malloc");
                return -1; 
        }
        int *q = (int *)p;

        *q = 0x12345678;

        for (i = 0; i < 4; i++)
                printf("%x\n", p[i]);    
        free(p);
        p = NULL;
        q = NULL;

        p = calloc(16, 1);
        if (p == NULL) {
                perror("calloc");
                return -1;
        }
        strcpy(p, "abcdef");
        printf("%p:%s\n", p, p);
        str = realloc(p, 8);
        printf("%p:%s\n", p, p);
        free(p);
        free(str);
        p = realloc(p, 2048);
        printf("%p:%s\n", p, p);

        free(p);
        p = NULL;
}

2.2 野指针和内存泄漏

1. 野指针

        所谓的野指针,指的是指针指向的内存空间未初始化

        指针变量在定义的时候,未设置初始值而使用默认值;此时指向的空间未做开辟;

        指针变量在指向动态管理内存的时候,有将指针指向的内存空间做释放,此时指向的空间未做开辟;

        野指针的访问,可能会导致未知异常,需要谨慎:

        在指针变量定义的时候有明确指向空间和指向空间释放的时候,将指针变量值设置NULL。在访问指针的时候,对指针进行判断。

2. 内存泄漏

        所谓的内存泄漏,所开辟的内存空间,一直占用未被使用,同时也不能对该内存空间进行读写访问。也不能再次进行内存资源的分配管理。就会导致内存泄漏。

        内存泄漏的根本原因:已动态开辟的内存空间没有指针指向,无法实现内存的访问。之前指向该内存空间的指针变量被修改。

避免内存泄漏:

        1) 合理选择内存空间的开辟和释放;

        2) 在对内存空间指针访问的时候,不要随意修改指针的指向;如果要修改,在指向改变之前对原指向空间进行释放或者使用新的指针变量存储原空间地址。

        3) 实时检测内存资源情况,如果有检测到内存泄漏,需要立即做内存释放。

3 内存分别

在32位操作系统,其内存分布管理

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

extern void func();

void test(void)
{
    
}

int a;
static int c;
int d;

int main()
{
        static int b;
        static int e;
        char *p = malloc(1);    
        /* .text:代码段 */
        printf("test: %p\n", test);
        printf("main: %p\n", main);
        printf("func: %p\n", func);
        /* .rodata:常量数据 */
        printf("%p\n", "ancd");
        /* .data:已初始化静态数据变量 */
        printf("&d:%p\n", &d);
        printf("&e:%p\n", &e);
        /* .bss:未初始化静态数据变量 */
        printf("&a:%p\n", &a);
        printf("&b:%p\n", &b);
        printf("&c:%p\n", &c);
        /* 堆区 */
        printf("p = %p\n", p);
        /* stack:栈区 */
        printf("&p = %p\n", &p);

}

void func()
{

}

 程序的编译过程

在Linux系统中,提供gcc编译器,实现面向过程的C语言程序的编译,生成可执行程序。

1. 整体编译:

        gcc 源文件

        源文件:可以是一个或者多个.c文件,

        源文件.c文件的个数是由程序中的所有.c文件构成;

        在编译成功生成可执行程序,默认情况下,可执行程序位a.out;可以是-o选项指定所生成的可执行程序

        例如:gcc hello.c -o hello编译源文件hell.c,成功生成可执行程序hello

1. 分布编译:

        按照C语言程序的具体编译过程,实现整个程序的编译,具体的步骤:

        a) 预处理阶段:

        所谓的程序的预处理,就是实现预处理指令的执行,具体包含

                1) #include 预处理指令实现头文件的展开;

                2) #define 标识符(常量和宏函数)的替换;

                3) # if 条件预处理指令实现语句选择编译;

预处理实现:

        gcc -E hello.c -o hello.i /* -E 预处理选项,对源文件hello.c进行预处理生成hello.i文件 */

b) 编译阶段:

        所谓的编译阶段,实现语句的语法检测,将高级语言(C/C++)翻译为汇编代码

        gcc -S hello.i -o hello.s/* -S 编译选项,将预处理后的C代码编译位汇编代码 */

c) 汇编阶段

        汇编阶段,是将汇编指令翻译为机器指令(二进制代码指令)

        gcc -c hello.s -o hello.o/* -c汇编选项,将汇编代码翻译为二进制机器代码 */

d) 链接阶段

        链接阶段,是实现程序的链接,具体的链接包含:

                1) 将程序中的所有.o文件的.text段、.data段、.bss段等分别进行链接为整体;

                2) 链接所依赖的库程序,

                        静态库,直接将库代码链接到可执行程序中;

                        动态库,链接所依赖库的信息

                3) 链接启动代码

                链接main函数启动前的初始化代码

                gcc hello.o -o hello

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值