C语言学习笔记

目录

/**********01语言的发展**********************/

/***********02第一个C语言程序****************/

//***********03数据类型***************************/

//********04变量和常量*****************//

//*******08字符串+转义字符+注释*************//

//******11数组和操作符******************//

//*********21常见关键字和#define*************//

//****************22指针******************//

//******************28结构体********************//

//*****************29分支与循环********************//

//*********41 联系及作业题讲解*********//

//******************45 猜数字游戏******************//

//************48 函数**************//

//*******************60 数组***************//

//***********70 扫雷游戏**************//

//**************75 操作符******************//

//*********************84 初始指针********************//

//*****************90 作业讲解****************//

//*****************98 结构体************//

//***********101 调试***********************//

//************108 数据的存储****************//

//****************118 指针详解******************//

//***************133 指针笔试面试讲解***************//

//*************146 作业讲解***********************//

//******************161 字符函数和字符串函数***********//

//***************170 内存函数***********//

//*********174 内存函数讲解+结构体****************//

//*************179 位段********************//

//*************181 枚举和共同体**********//

//*******************189 动态分配内存***************//

//********************205 文件操作******************//

//******216 C语言预处理*******************//


/**********01语言的发展**********************/

(1)二进制语言
硬件-正电/负电-1/0

100010101000101010000000011111-手册
科学家

10001010-ADD-助记符-汇编语言----------低级语言

(2)B语言

(3)C语言,C++,--高级语言-------------------高级语言

国际标准
ANSI C-C89/90
C99/C11并不流行---不少的编译器不支持

/***********02第一个C语言程序****************/


//包含名为stdio的头文件,std-standard标准,i-input输入,o-output输出
#inlcude <stdio.h>
int main()
{
//print-打印,function函数,print-function
    printf("hello,world!\n");
    return 0;
}


//***********03数据类型***************************/


(1)常见数据类型
    1(字节)    char    //字符数据类型    %c
    2    short    //短整型
    4    int    //整型        %d
    4/8    long    //长整型        C语言标准规定:sizeof(long)>=sizeof(int))
    8    long long    //更长整型
    4    float    //单精度浮点数    %f
    8    double    //双精度浮点数    %lf
                    %x-打印16进制数字
                    %s-打印字符串
                    %p-打印地址-&a-取地址符
(2)字节                
    计算机中的单位    计算机是以硬件-正电/负电
    bit-比特位                1/0
    byte-字节        一个字节==8个比特位
    kb        1kb==1024字节byte
    mb        1mb==1024kb
    gb        1gb==1024mb
    tb        1tb==1024gb
    pb        1pb==1024tb


//********04变量和常量*****************//


(1)变量
    全局变量:定义在代码块({})之后的变量
    局部变量:定义在代码块({})内部的变量
注意:1、局部变量和全局变量名字建议不要相同,容易产生bug
          2、局部变量优先于全局变量
(2)scanf与取地址符&
    输入数据函数,scanf函数:scanf("%d%D",&a,&b);
    &取地址符
注意:C语言规定,变量要声明在代码块的最前面!!!!!
(3)变量的作用域
1、作用域(scope):程序设计概念,一段程序代码中所用的名字并不总是有效的可用的
    而限定这个名字的可用的代码范围就是这个名字的作用域
2、局部变量的作用域是变量所在的局部范围({代码块})
3、全局变量的作用域是整个工程
(4)变量的生命周期
    变量的生命周期是指变量的创建到变量的销毁之间的一个时间段
    1、局部变量的生命周期:进入作用域({})生命周期开始,出作用域生命周期结束
    2、全局变量的生命周期:整个程序的生命周期
注意:scanf是C语言提供的,scanf_c是VS编译器提供的(不建议使用),跨平台性和可移植性
(5)常量:
    字面常量;const常属性变量,define定义常量,枚举常量
(6)枚举常量
//枚举---列举
//性别:男、女、保密
//三原色:红,黄,蓝
//星期:1、2、3、4、5、6、7
//枚举关键字:enum
    enum Sex
    {
        MALE,
        FEMALE,
        SECRET
    };
    int main()
    {
         enum Sex s = FEMALE;
        return 0;
    }    


//*******08字符串+转义字符+注释*************//


(1)字符串
    “hello,world!\n”
    “”空字符串
    这种有双引号(Double Quote)引起来的一串字符称为字符串字面值(String Literal),简称字符串
注意:字符串的结束标志是一个‘\0’转义字符,在计算字符串长度的时候\0是结束标志,不算作字符串内容
(2)字符串长度    
    strlen()函数需要包含<string.h>头文件
    strlen();-计算字符串长度,string length
    printf("%d\n",strlen(array1));
(3)转义字符——转变原来的意思
    n->\n,由字母n变为换行符
    \t--水平制表符;
    \\用于表示一个反斜杠,防止它被解释为一个转义序列符
    \也可以用来分割,进行转义
注意:\ddd__ddd表示1-3个八进制数字,如:\130
           \xdd__dd表示2个十六进制数字,如:\x30 0
例子:\32---32是2个八进制数字,\32是一个转义字符,作为一个字符
//32作为8进制代表的那个十进制数字,作为ASCII码值,作为ASCII码值,对应的字符
//32-->10进制 26-->作为ASCII码值代表的字符
\x89算是一个字符,8和9都是16进制数
(4)注释
1、C语言风格的注释/*XXXXXXXX*/
    缺陷:不能嵌套注释
2、C++风格的注释//XXXXXXXXXXXXXXXX
    可以注释一行也可以注释多行
3、代码中有不需要的代码可以直接删除,也可以注释掉
真正的注释:-->代码中有些代码比较难懂,可以加一下注释文字
(5)‘\0’和0和‘0’
    1、‘\0’是转义字符,是一个字符,其ASCII值是0
    2、0是数字0,与‘\0’等价
    3、'0'是字符0,其ASCII值是48


//******11数组和操作符******************//


(1)数组
C语言中给出了数组的定义:一组相同类型元素的集合
数组定义:
    int arr[10]={1,2,3,4,5,6,7,8,9};//定义一个整型数组,最多存放10个元素
数组的使用:
    int arr[10];//定义一个存放10个整形数字的数组,数组的长度必须由常量确定
    array[NUM]代表这个数组,
    数组的下标从0开始,到NUM-1,用下表访问数组元素,array[0]是这个数组的第一个元素
(2)操作符
    1、算术操作符 + - * /(商) %(取余数)
    2、移位操作符 >> <<
    3、位操作符 &(按位与) ^(按位异或) | (按位或)
    4、赋值操作符 = += -= *= &= ^= 
    5、单目操作符(只有一个操作数) 
        !(逻辑反操作)-(负值)+(正值)&(取地址)
        sizeof(操作数的类型长度(以字节为单位))---sizeof计算的是变量或类型所占空间的大小
        ~(对一个数的二进制按位取反)--(前置,后置--)++(前置,后置++)
        *(间接访问操作符(解引用操作符))(类型)(强制类型转换)
    6、逻辑操作符 &&(逻辑与)||(逻辑或)---注意:C语言中表示真假,0为假,非0为真
        逻辑结果就是1或者0,只管两边的逻辑
    7、条件操作符(三目操作符)--3个操作数
        exp1 ? exp2 : expe3
        a>b ? a : b
    8、逗号表达式(用逗号隔开表达式),exp是表达式
        exp1,exp2,exp3,……,expN

//*********21常见关键字和#define*************//



(1)常见关键字(不能与变量名重合):
    auto,break,case,char,const,continue,default,do,double,else,enum,
    extern,float,for,goto,if,int,long,register,return,short,signed,
    sizeof,static,struct,switch,typedef,union,unsigned,void,volatile,while
1、auto-局部变量,自动变量,(一般都省略了)
2、register-寄存器关键字-register int a(建议把a定义为寄存器变量)
    //计算机存储数据,访问速度由高到低分别是:寄存器-高速缓存-内存-硬盘
3、int其实是signed int,只是把signed省略了。
4、union-联合体/共用体
5、typedef-类型定义-类型重定义
6、static-用来修饰变量和函数的
    a、修饰局部变量-静态局部变量,生命周期变长
    b、修饰全局变量-静态全局变量,改变了作用域,让静态全局变量只能在自己所在的源文件使用
    c、修饰函数-静态函数,改变了函数的连接属性,(外部链接属性变为内部链接属性)与全局变量相似
7、extern -声明外部变量
(2)#define
    1、利用#define定义标识符常量----#define MAX 100
    2、利用#define定义宏-带参数----#define  MAX(X,Y)  (X>Y?X:Y)

//****************22指针******************//


(1)内存
    内存是电脑上特别重要的存储器,计算机中所有的程序的运行都是在内存中进行,
    所以为了有效使用内存,把内存划分为一个个小的内存单元,每个内存单元的大小
    都是1个字节,为了能够访问到内存的每个单元,就给内存单元编号,
    这些编号称为内存单元的地址。
1、一个内存单元为一个字节,一个btye=8bit
2、有一种变量是用来存放地址的,-指针变量-p=&a
3、int* p--int*是p的类型,*说明p是一个指针变量,int说明p这个指针变量所指向的变量类型为int
4、*p-//*是解引用操作符/间接引用操作符,*p是p变量代表的地址所指向的变量。
5、指针是一个变量,用来存在地址
6、指针变量大小=地址大小,不管所指向变量大小,同一平台指针变量大小相同,指针大小在32平台是4个字节,64位平台是8个字节

//******************28结构体********************//


1、描述复杂对象
    如人:身高,名字,年龄,身份,电话号码;书:书名,定价,作者,出版社,书号
2、复杂对象-结构体-我们自己创造的一种类型
    struct Book
    {
        char name [20];//C语言程序设计
        short price;//55
    };
    int main()
    {
        struct Book* pb=&b1;
    //利用结构体变量创建一个该类型的结构体变量
        struct Book b1={"C语言程序设计",55};
        printf("书名:%s\n",b1.name);
        printf("书名:%s\n",(*pb).name);
        printf("价格:%d\n",pb->price);
        return 0;
    }
注意:. 结构体变量.成员    -> 结构体指针->成员

//*****************29分支与循环********************//


//C语言是一门结构化的程序设计语言
        顺序结构;选择结构;循环结构
    分支(选择)语句:
        if / switch
    循环语句
        while / for / do while
(1)语句
    C语言中由一个分号;隔开的就是一条语句。
(2)If语句
1、if语句-语法结构
//
    if(表达式)    
        语句;
//
    if(表达式1)
        语句1;
    else(表达式2)
        语句2;
//多分支
    if(表达式1)
        语句1;
    else if(表达式2)
        语句2;
    else (表达式3)
        语句3;
2、!!else和最近的未匹配的if匹配!!
    当条件表达式是常量与变量相比,把常量写在左边,不容易出bug,是好的代码风格
    int main()
    {
        int num = 4;
        if ( 5==num ) 
        {
            printf("hehe\n");
        }
    }
(3)switch语句
1、switch语句也是一种分支语句,常常用于多分支情况
2、语法结构
    switch(a)
    {
    case 1 :
        语句1;break;
    case 2 :
        语句2;break;
    default:
        语句n;break;
    }
3、!!switch()必须是整型表达式!!case后面必须使整形常量表达式!!
注意:没有break时,就会一直往下执行!!
(3)while循环
1、while语法结构
    while(表达式)
    {
        break;(跳出循环)
        continue;(仅仅跳出单层循环)
        循环语句;
    }
注意:循环在break突然停止,之后的所有语句均不再执行,直接跳出循环体!!
    continue适用于终止本次循环的,也就是本次循环中continue后边的代码不会再执行,
    而是直接跳转到while语句的判断部分,进行下一次循环的入口判断。
2、EOF-end of file 文件结束标志
    int main()
    {
        int ch=0;
        while((ch=getchar())!=EOF)
        {
            if(ch<'0'|ch>'9')
                continue;
                putchar(ch);
        }
        return 0;
    }
(4)for循环(可以解决所有循环问题)
1、语法结构
    for(表达式1;表达式2;表达式3)
        循环语句;
    表达式1初始化,用于初始化循环变量;
    表达式2条件判断,用于判断循环终止;
    表达式3调整,用于循环条件的调整。
    顺序是:初始化->判断->循环->调整->判断->调整->判断->出去
2、while循环的初始化,判断,循环条件调整分的比较开,程序变长后,将变得不方便
3、for循环的一些建议:
    a.不可在for循环体内修改循环变量,防止for循环失去控制
    b.建议for欲绝的循环控制变量的取值采用“前闭后开区间”写法
4、for循环的初始化,调整,判断都可以省略;
    for循环的判断如果被省略,那判断条件:恒为正
    建议:不要随便省略代码
(5)do while语句的特点,循环至少执行一次,使用的场景有限,不是经常使用
(6)二分法
#include <stdio.h>

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

   int sz=sizeof(arr)/sizeof(arr[0]);//计算元素个数
   int left=0;//左下标
   int right=sz-1;//右下标

   while(left<=right)//寻找的基本条件
   {
       int mid=(left+right)/2;
       if(arr[mid]>k)
       {
           right=mid-1;
       }
       else if(arr[mid]<k)
       {
           left=mid+1;
       }
       else
       {
           printf("找到了,下标是:%d",mid);
           break
       }
   }
   if(left>right)
   {
       printf("找不到!\n");
   }

    return 0;
}

//*********41 联系及作业题讲解*********//


1、define不是关键词,是预处理指令
2、指针,是一个变量,用来存放地址,大小根据所指向变量不同而不同
3、static可以修饰局部变量,全局变量,函数,static所修饰的变量可以改变
4、if语句中,0表示假,非0为真
5、switch语句中的case后的表达式只能是整型表达式。case表达式不要求顺序,default可以任意位置
6、for循环,区间一般是左闭右开,但是根据情况适当变化,不要古怪

//******************45 猜数字游戏******************//


1、时间戳
    当前计算机的时间-计算机的起始时间(1970.1.1. 0:0:0)=差值(秒)
2、使用时间戳设置随机数的生成起点
    srand((unsigned int)time(NULL));
NULL为一个空指针
3、goto语句
(1)C语言中提供了可以虽易滥用的goto语句和标记跳转的标号,可以随意乱跳!!
容易写出BUG!!容易造成破坏!
(2)从理论上goto语句是没有必要的,实践中没有goto语句也可以很容易写出代码
(3)但是某些场合下goto语句还是用得着的,最常见的用法是终止程序在某些深度嵌套的结构
的处理过程,例如一次跳出两层或多层循坏
(4)例如:程序陷入死循环
int main()
{
again:
    printf("hello world!\n");
    goto again;
    renturn 0;
}
goto语句的真正适合的场景:
for()
    for()
    {
        for()
        {
            if(disaster)
                goto error;
        }
    }
error:
    if(disaster)
    //处理错误情况

//************48 函数**************//


1、函数的定义(维基百科)
    在计算机科学中,子程序(英语:subroutine procedure,function,
routine,method,subprogram,callable unit),是一个大型程序中的某部分代码,
由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,
具备相对的独立性。
    一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。
这些代码通常被集成为软件库。
2、库函数
    我们在开发的过程中每个程序员都可能用到,为了支持可移植性和提高
程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
3、常用的库函数
IO函数;字符串函数;字符操作函数;内存操作函数;时间/日期函数;数学函数;其它库函数
4、strcpy字符串复制函数,/0也会被复制,
5、printf打印%s时,遇到/0就会停止,不再继续打印余下的字符

6、自定义函数
    自定义函数有函数名,返回值类型和函数参数。但是都是自己设定的
ret_type fun_name(paral,*)
{
    statement;//语句项
}
    ret_type return-type 返回值类型
    fun_name 函数名
    paral 函数参数
7、函数参数
(1)实际参数(实参)
    真实传给函数的参数,叫实参。实参可以是:常量,变量,表达式,函数等,
无论实参是何种类型的量,在进行函数调用时,它们必须有确定的值,以便把这些值传给形参。
(2)形式参数(形参)
    形式参数是指函数名括号里的变量,因为形式参数只有在函数被调用的过程才实例化,(分配内存单元)
,所以叫形式参数,形式参数当函数调用完成后就自动销毁了,因此形式参数只有在函数中有效。
注意:形参实例化之后相当于实参的一份临时拷贝
8、函数的调用
(1)传值调用
    函数的形参和实参分别占有不同的内存块,对形参的修改不会影响实参
(2)传址调用
    传址调用是吧函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式
    这种传参方式可以让函数和函数外边变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
9、子函数的功能设计要干净,独立,可移植性更高
10、写函数先写怎么用,在具体实现函数,TDD-测试驱动开发
11、函数的嵌套调用

    函数与函数之间可以有机的组合
(1)嵌套调用
(2)链式访问
    把一个函数的返回值作为另外一个函数的参数
int main()
{
    printf("%d",printf("%d",printf("%d",43)));
    return 0;
}
    上述程序结果为4321,printf函数的返回值为打印的字符数
12、函数的声明与定义
    函数的声明一般在头文件中,包含自己的头文件时,用“”“”,用库函数时《》
(1)函数的声明
    a.告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体存不存在,无关紧要
    b.函数的声明一般出现在函数的使用之前,要满足先声明后使用
    c.函数的声明一般要放在头文件中
(2)函数的定义
    是指函数的具体实现,交代函数的功能实现
(3)头文件的格式
    test.h的内容:放置函数的声明
避免同一个头文件被多次引用,浪费资源
    #ifndef __TEST_H__
    #define__TEST_H__
    //函数的声明
    int Add(int x,int y);
    #endif //__TEST_H__
13、函数的递归
(1)递归的定义
    程序调用自身的编程技巧称为递归(recursion)。递归作为一种算法在程序设计语言中广泛应用。
一个过程或函数在其定义或说明中直接或间接调用自身的一种方法。他通常把一个大型复杂问题层层转化为一个
与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可以描述出解题过程所需要的多次重复计算,
大大减少了程序的代码量。
    递归的主要思考方式在于:把大事化小
(2)递归的两个必要条件
    a.存在限制条件,当满足限制条件的时候,递归不再继续
    b.每次递归调用之后越来越接近这个限制条件
(3)递归的常见错误:栈溢出
    Stack overflow
注意补充:内存分为3个区,分别为栈区;堆区;静态区
    栈区:局部变量,函数形参
    堆区:动态开辟的内存,malloc,calloc
    静态区:全局变量,static修饰的变量
注意:http://stackoverflow.com/--- >程序员的知乎
(4)调用自己要有条件,满足条件才继续递归,且每次递归越来越接近条件
(5)数组传参,穿过去的不是整个数组,而是数组首元素的地址!!!
(6)菲波那契数列:1 1 2 3 5 8 13 21 34 55 ...
思考:青蛙跳台阶《剑指offer》--67道笔试题
n个台阶,一次可以跳1个台阶,一次也可以跳2个台阶,这只青蛙跳到第n个台阶,有多少种跳法?

//*******************60 数组***************//


1、一维数组的创建和初始化
(1)数组的创建
    数组是一组相同类型元素的集合。数组的创建方式:
    type_t arr_name [const_n];
    //type_t 是指数组的元素类型
    //const_n 是一个常量表达式,用来指定数组的大小
注意:数组创建时,【】一定要给常量,不能使用变量
(2)数组的初始化
strlen和sizeof没有什么关联:
    sizeof是计算数组所占空间的大小,计算变量、数组、类型的大小-单位是字节-操作符
    strlen计算字符串的长度,不包括'\0',只针对字符串求长度-库函数,strlen就是找‘\0’之前的字符个数
2、一维数组的使用
3、数组在内存中的存储----连续开辟空间,连续存储---二维数组也是连续存储的!!!!!!!
4、二维数组的创建

    char arr[3][4];
5、二维数组的初始化
    char [4][5]={{1,2},{1,2,3,4}};
注意:列是不能省略的!!!!!
6、数组作为函数参数传递
    传过去的只是数组首元素的地址,&arr[0]
注意:数组名就是数组首元素地址!!!!!arr=&arr[0],arr是指针变量,其值为首元素地址
注意:以上情况有两个例外:
(1)int size=sizeof(arr)/sizeof(arr[0]);//sizeof单独放一个数组名,数组名代表整个数组,sizneof(arr)计算的是整个数组的大小,单位是字节
(2)&arr,&数组名,数组名代表整个数组,&数组名,取出的是整个数组的起始地址


//***********70 扫雷游戏**************//


1、编程时,不断测试逻辑,一层一层写,一层一层测试,不要吭哧吭哧一直写!!!!

//**************75 操作符******************//


1、操作符分类:
    算术操作符;移位操作符;位操作符;赋值操作符;单目操作符;关系操作符;
    逻辑操作符;条件操作符;逗号表达式;下标引用、函数调用和结构成员
2、算术操作符(双目操作符,两个操作数)
+ - * / %
(1)/ 整数/整数结果还为整数,得出的是整数商和余数,其中一个浮点数,结果则为浮点数
(2)%取余数,取模%左右操作符必须都是整数!!!!!
3、移位操作符
>>右移操作符。二进制移动。右移有除2的效果!!
    算术右移:右边丢弃,左边补符号位;通常为算术右移!!
    逻辑右移:右边丢弃,左边补零
注意:整数的二进制表示由:原码,反码;补码;!!存储到内存中的是补码!!
<<左移操作符,左边丢弃,右边补零
警告:对于移位操作符,不要移动负数位,这个是标准未定义的,
4、位操作符(只能作用于整数,浮点数与整数存储完全不一样)(补码运算)
    & //按位与 ;|  //按位或;^ //按位异或
5、赋值操作符
    复合赋值操作符:+=;-=;*=;/=;%=;>>=;<<=;&=;|=;^=
6、单目操作符----只有一个操作数
    !--逻辑反操作;-负值;+正值;&取地址;sizeof操作数的类型长度(以字节为单位);
~对一个数的二进制按位取反;--前置、后置--;++前置,后置++;*间接访问操作符(解引用操作符)
(类型)强制类型转换
(1)在逻辑上:0为假,非0为真
(2)sizeof(类型),必须加括号,如果是类型
(3)sizeof(内部表达式),内部表达式不参与实际运算,不实际赋值
(4)~按位(二进制)取反,所有位一起取反
(5)地址存储4/8字节,只由系统配置32位或64位决定!!
7、关系操作符
    >;<;<=;>=;!=;==
 8、逻辑操作符
    && 逻辑与;|| 逻辑或
注意:区分按位与和按位或,这里逻辑操作符关注操作数本身逻辑为真或假
注意:逻辑与&&左边只要出现0,右边直接不计算!!!
注意:逻辑或||左边只要出现真,右边直接不计算!!!
9、条件操作符(三目操作符)
    exp1?exp2:exp3    
判断exp1为真,计算exp2且为整个表达式结果;否则是exp3
    b=(a>5?3:-3);
10、逗号表达式
    逗号表达式,其实是用逗号隔开的多个表达式,
    逗号表达式,从左到右依次执行,整个表达式结果是最后一个表达式的结果
11、下标引用、函数调用和结构成员
(1)[]下标引用操作
    操作数:一个数组名+1个索引值;有两个操作数
(2)函数调用操作符 接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数
(3).结构体.成员------ ->结构体指针->成员名
//学生
//创建的一个结构体类型-struct Stu
struct Stu
{
    //成员变量,结构体变量,成员名
    char name[20];
    int age;
    char id[20];
};
int main()
{
    int a=20;
    //使用struct Stu这个类型创建了一个学生对象s1,并初始化
    struct Stu s1={“张三”,20,“20191212”};
    //指针变量使用
    struct Stu* ps=&s1;
    printf("%s\n",s1.name);//(*ps).name//ps->name
    printf("%s\n",s1.age;
    printf("%s\n",s1.id);
    return 0;
}
12、表达式求值
    表达式求值的顺序一部分是由操作符的优先级和结合性决定的
    同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型
(1)隐式类型转换
    C的整型算术运算总是至少以缺省整形类型的精度来进行的。
    为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换为整型提升
a.整型提升的意义:
    表达式的整型运算要在CPU的相应运算期间内执行,CPU内整型运算器(ALU)的操作数的字节长度
一般就是Int的字节长度,同时也是CPU的通用寄存器的长度
    因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度
    通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能
有这种字节相加指令)。所以,表达式中各种长度可能的小于Int长度的整型值,都必须先转换为int或unsigned int,
然后才能送到CPU去执行运算。
b.例子:
    char a,b,c;
    ...
    a=b+c;
    b和c的值被提升为普通整型
c.整型提升
    整型提升是按照变量的数据类型的符号位来提升的
    1.无符号位,补0;2.有符号位,按符号位,负数补1,正数补0
d.算数转换
    如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,
否则操作就无法进行,下面的层次体系称为寻常算术转换。
        long double
        double
        float
        unsigned long int 
        long int
        unsigned int
        int    
    如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另一个操作数类型后执行运算    

//*********************84 初始指针********************//


1、指针
    在计算机科学中,指针(pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中的另一个地方的值。
由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元,因此,将地址形象化为“指针”,意思是通过它找到以它为地址的内存单元。
总结:指针就是变量,用来存放地址的变量,(存放在指针中的值都被当做地址处理)
    32位机器,每个地址指向一个字节,2的32次方,每个地址标识一个字节,我们可以给(2^32Byte==2^32/1024KB==2^32/1024/1024MB
==2^32/1024/1024/1024GB==4GB)4GB的空闲进行编址。
    在32位机器,地址是32个0或者1组成二进制序列,那地址就的用4个字节的空间存储,所以一个指针变量的大小就应该是4个字节。
    在64位机器,地址线有64个,那一个指针变量大小为8个字节,才能存放一个地址
总结:指针是用来存放地址的,地址是唯一标识一块地址空间的
    指针的大小在32位平台上是4个字节,在64位平台上是8个字节
2、指针类型
(1)指针类型决定了指针进行解引用操作的时候,能够访问空间的大小
    int* p; *p能够访问4个字节;
    char* p; *p能够访问1个字节;;
    double* p; *p能够访问8个字节;
(2)指针类型决定了指针走一步走多远(指针的步长)
(3)局部变量不初始化,默认是随机值
(5)可以初始化指针变量为NULL,指针不要越界!!!
(6)如何避免野指针?1、指针初始化;2、小心指针越界;3、指针指向空间释放即置NULL;4、指针使用之前检查有效性
3、指针运算

(1)指针+-整数
(2)指针-指针
    !!地址-地址是数组中的元素个数!!
(3)指针的关系运算(比较大小)
注意:标准规定->允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许
与指向第一个元素之前的的那个内存位置的指针进行比较。
(4)指针与数组
    数组名arr是数组首元素的地址;
有两个例外:a.&arr,-&数组名-数组名不是首元素的地址-数组明表示整个数组-&数组名 取出的是整个数组的地址
    b.sizeof(arr)-sizeof(数组名)-数组名表示整个数组-sizeof(数组名)计算的是整个数组的大小
(5)二级指针
    int a=10;
    int* pa=&a;//pa就是一级指针
    int** ppa=&pa;//ppa就是二级指针
(6)指针数组
    int* arr[]={&a,&b,&c};
    int i=0;
    for(i=0;i<3;i++)
    {
        printf("%d ",*(arr[i]));
    }

//*****************90 作业讲解****************//


1、能把函数处理结果的两个数据返回给主调函数,不正确的是:return这两个数,return只能一个数
可以采用形参用数组;形参用指针;用两个全局变量;
2、函数可以嵌套调用,不可以嵌套定义
3、函数调用excel((v1,v2),(v3,v4),v5,v6)中,实参的个数是4个,!!()是逗号表达式,用逗号隔开的是逗号表达式
4、函数设计应该高内聚低耦合!!尽量少使用全局变量!!
5、随机变量不初始化为随机值,全局变量不初始化默认为0
6、sizeof是计算变量类型所占内存的大小,恒大于等于0,无符号数为大于等于0,sizeog规定返回无符号数,
当整数与无符号数进行运算,自动进行转化为无符号数再处理运算
7、system("pause");//system库函数-执行系统命令-pause(暂停),加上头文件<stdlib.h>


//*****************98 结构体************//


1、结构体类型的声明
(1)结构
    结构是一些值的集合,这些纸称为成员变量。结构的每个成员可以是不同类型的变量
(2)结构体创建
//学生
//创建的一个结构体类型-struct Stu;struct-结构体关键字;Stu-结构体标签
struct Stu
{
    //成员变量,结构体变量,成员名
    char name[20];
    int age;
    char id[20];
}s1,s2,s3;//s1,s2,s3是三个全局的结构体变量,但是尽量不用,全局变量
typedef struct Stu
{
    //成员变量,结构体变量,成员名
    char name[20];
    int age;
    char id[20];
}Stu;
//给创建的结构体变量重新命名为Stu,其中struct Stu=Stu
注意:创建结构体类型的时候,不开辟内存空间
int main()
{
    struct Stu s1;//创建局部的结构体变量
    Stu s2;
    return 0;
}
(3)结构体可以嵌套初始化
    结构体传参的时候,要传结构体的地址:
    函数传参的时候,参数是需要压栈的,如果传递一个结构体对象的时候,
结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。


//***********101 调试***********************//


1、bug调试的基本步骤
    (1)发现程序错误的存在;
    (2)以隔离、消除等方式对错误进行定位;
    (3)确定错误产生的原因
    (4)提出纠正错误的解决办法;
    (5)对程序错误予以改正,重新测试
2、Debug和Relaese的介绍
(1)Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序
(2)Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好的使用
3、调试快捷键
4、设置断点---右击鼠标可以设置断点条件!!!

(1)F5 启动调试,必须与断点联合使用,用来直接调到下一个断点处
(2)F10 逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句
(3)F11 逐语句,每次都执行一条语句,但是这个快捷间可以使我们的执行逻辑进入函数内部(最常用的)
(4)ctrl+F5 不需要调试直接运行时使用
5、调试的时候查看程序当前信息
----查看临时变量的值
    调试->窗口->自动窗口(自动上下文相关变量)但是会自动消失
    调试->窗口->监视,自己输入变量,不会消失
    调试->窗口->内存,内存存储的变化
    调试->窗口->反汇编(汇编语言)
    调试->窗口->寄存器
    调试->窗口->调用堆栈(以栈的形式展示函数调用逻辑)
6、栈区的默认使用:
    先使用高地址处的空间,再使用低地址处的空间
7、数组随着下标的增长,地址是由低到高变化的
8、怎么写出容易调试的代码?

(1)代码运行正常;(2)bug很少;(3)效率高;(4)可读性高;
(5)可维护性高;(6)注释清晰;(7)文档齐全
9、使用指针一定要判断指针的有效性,使用assert()断言判断空指针
10、给指针+const可以保护const变量不被修改,

const int* p=&num;
    const放在指针变量*左边,修饰的是*p,也就是说:不能通过p来改变*p(num)的值
int * const p=&num;
    const放在指针变量*右边,修饰的是p本身,也就是说,不能改变指针变量p
11、常见错误:编译型错误;链接型错误;运行时错误

//************108 数据的存储****************//


1、数据类型介绍(内置类型+自定义类型/构造类型)
(1)内置类型
    char---字符型数据类型
    short----短整型
    int-------整形
    long-----长整型
    long long----更长的整形
    float-----单精度浮点数
    double-----双精度浮点数
(2)构造类型
    数组类型
    结构体类型----struct
    枚举类型---enum
    联合类型---union
(3)空类型 void
    void表示空类型(无类型),通常应用于函数的返回类型、函数的参数、指针类型
2、整形在内存中的存储
    正数的原、反、补码都相同
    对于整形来说:数据存放内存中其实存放的是补码
注意:在计算机系统中,数值一律用补码来表示和存储,原因在于,使用补码可以将符号位和
数值域统一处理,同时,加法和减法也可以统一处理(CPU只有加法器),此外,补码与原码相互
转换,其运算过程相同,不要格外的硬件电路
3、整数分为两种
(1)有符号数:正数:原码、反码、补码相同
    负数:原码、反码、补码不同,要进行计算
(2)无符号数:原码、反码、补码相同
4、大端小端-----!!字节为顺序,最小单位!!
(1)大端(存储)模式:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中
(2)小端(存储)模式:是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中
注意:大端字节序存储模式---小端字节序存储模式
5、%d---打印十进制的有符号数字
    %u---打印十进制的无符号数字
6、signed char可以表示的范围为-128->-1------|---->0->127(10000000定义为-128)
    循环,一旦用char表示128就循环到-128
    unsigned char可以表示的范围0->256
7、浮点型在内存中的存储
(1)常见的浮点数:float,double,long double类型
(2)浮点数的表示范围:float.h

//****************118 指针详解******************//


1、指针的概念
(1)指针就是个变量,用来存放地址,地址唯一的标识一块内存空间
(2)指针的大小是固定的4/8个字节(32位平台/64位平台)
(3)指针是有类型的,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限
(4)指针的运算
2、字符指针
const char*p="abcdef",这个是一个常量字符串
3、指针数组------是数组,用来存放指针
4、数组指针------是指针,指向数组的指针,存放数组的地址
(1)arr----首元素地址
(2)&arr[0]------首元素的地址
(3)&arr--------数组的地址
注意:int *p1[10]是指针数组,一个数组,元素是10个指针,[]有优先结合性
    int (*p1)[10]是数组指针,一个指针,指向一个数组,数组元素是10个整型
例子:char* arr[5];
    char*(*pa)[5]=&arr;
数组指针等价于数组名,有时使用可以参考arr[i][j]
(4)练习
    int arr[5]---------------------整形数组,数组元素为5个整型
    int *parr1[10]----------------指针数组,数组元素为10个指向整形元素的指针
    int (*parr2)[10]--------------数组指针,指针指向有10个整型元素的数组
    int (*parr3[10])[5]-----------parr3是一个数组,该数组有10元素,每个元素是一个数组指针,该数组指针指向的数组有5个元素,每个元素是int
5、数组参数、指针参数
(1)一维数组传参
#include <stdio.h>
 void test(int arr[])
 {
 }
void test(int arr[10])
{
}
void test(int *arr)
{
}
void test2(int *arr[20])
{
}
void test2(int **arr)
{
}
int main()
{
    int arr[10]={0};
    int *arr2[20]={0};
    test(arr);
    test2(arr2);
    return 0;
}
(2)二维数组传参//列数不可以省略//接收的必须是指向数组第一行的数组指针才可以
void test(int *arr)//err
{
}
void test(int* arr[5])//err
{
}
void test(int (*arr)[5])//err
{
}
void test(int **arr)//err
{
}
int main()
{
    int arr[3][5]={0};
    test(arr);
}
(3)指针传参
void test(char **p)//二级指针形参
{
}
int main()
{
    char c='b';
    char*pc=&c;
    char**ppc=&pc;
    char* arr[10];
    test(&pc);//可以传一维指针的地址
    test(ppc);//一维指针的指针
    test(arr);//一维指针数组的数组名
    return 0;
}
6、函数指针
    注意:&函数名  和 函数名 都是函数的地址
int (*pa)(int,int)=Add;//指向函数的指针,函数名是Add
printf("%d\n,(*pa)(2,3)");
例子:
void(* signal(int,void(*)(int)))(int);

typedef void(*pfun_t)(int);
pfun_t signal(int,pfun_t);

typedef unsigned int uint;
函数指针*可有可无,解引用都可以
7、函数指针数组
(1)char* my_strcpy(char* dest,const char*src);
    1、写一个函数指针pf,能够指向my_strcpy
    2、写一个函数指针数组,能够存放4个my_strcpy函数的地址
    char*(*pf)(char*,const char*)=mt_strcpy;
    char* (*pfArr[4])(char*,const char*)={my_strcpy};
(2)函数指针数组的用途是“转移表”
8、回调函数(使用函数指着调用函数)
    回调函数就是一个通过函数指针调用的函数,如果你把函数的指针(地址)作为参数传递给另一个函数,
当这个指针被用来调用其所指向的函数,我们就说这是回调函数。回调函数不是有该函数的实现方直接调用,而是在特定的
事件或条件发生时由另外的一方调用的,用于该事件或条件进行响应。
9、指向函数指针数组的指针
定义:指向函数指针数组的指针是一个指针指向一个数组,数组的元素都是函数指针
    int arr[10]={10};
    int (*p)[10]=&arr;//取出数组的指针
    int (*pfArr[4])(int,int);//pfArr是一个数组-函数指针的数组
    int(*(*ppfArr)[4])(int,int)=&pfArr;
10、qsort函数
(1)void* 类型的指针 可以接收任意类型的地址
    void*类型的指针,不可以进行解引用操作,不确定访问几个字节,也不可以进行加减整数的操作


//***************133 指针笔试面试讲解***************//


1、一维数组
//数组名是首元素的地址
//1、sizeof(数组名)-数组名表示整个数组
//2、&数组-数组名表示整个数组
//一维数组
int main()
{
    int a[]={1,2,3,4};
    printf("%d\n",sizeof(a));//16,sizeof(数组名)-计算的是整个数组的总大小,sizeof的单位都是字节
    printf("%d\n",sizeof(a+0));//4/8,数组名这里表示首元素的地址,a+0还是首元素地址,地址的大小就是4/8字节
    printf("%d\n",sizeof(*a));//4,a表示首元素地址,*a解引用表示首元素,首元素大小是4字节
    printf("%d\n",sizeof(a+1));//4/8,a+1表示第2个元素的地址,为4/8
    printf("%d\n",sizeof(a[1]));//4,表示第2个元素,大小为4字节
    printf("%d\n",sizeof(&a));//4/8,&a表示整个数组地址,但也是地址,所以是4/8
    printf("%d\n",sizeof(*&a));//16,对整个数组地址解引用,即为整个数组,整个数组大小为16字节
    printf("%d\n",sizeof(&a+1));//4/8,&a是数组的地址,&a+1虽然地址跳过整个地址,但是还是地址,大小为4/8字节
    printf("%d\n",sizeof(&a[0]));//4/8,对首元素取地址,&a[0]是首元素地址
    printf("%d\n",sizeof(&a[0]+1));//4/8,&a[0]+1是第2个元素
    return 0;
}
注意:strlen();需要地址,地址开始寻找‘\0’
//字符数组
//strlen计算字符串长度,给地址,到\0为止
int main()
{
    char arr[]={'a','b','c','d','e','f'};
    printf("%d\n",strlen(arr));//随机值
    printf("%d\n",strlen(arr+0));//随机值
    printf("%d\n",strlen(*arr));//不合法
    printf("%d\n",strlen(arr[1]));//不合法
    printf("%d\n",strlen(&arr));//数组地址,随机值
    printf("%d\n",strlen(&arr+1));//随机值
    printf("%d\n",strlen(&arr[0]+1));//随机值

    return 0;
}

//字符串数组
int main()
{
    char arr[]="abcedf";

    printf("%s\n,sizeof(arr)");//7,sizeof(arr)计算的是数组的大小,单位是字节
    printf("%s\n,sizeof(arr+0)");//4/8,代表的是首元素地址,地址大小是4/8
    printf("%s\n,sizeof(*arr)");//1,计算的是首元素的大小,1字节
    printf("%s\n,sizeof(arr[1])");//1,首元素大小,1字节
    printf("%s\n,sizeof(&arr)");//4/8,取地址是整个数组的地址,但还是地址,地址大小为4/8字节
    printf("%s\n,sizeof(&arr+1)");//4/8,&arr+1是跳过整个数组的地址,还是地址,大小是4/8字节
    printf("%s\n,sizeof(&arr[0]+1)");//4/8,第二个元素的地址,地址大小为4/8字节

    return 0;
}
2、二维数组
int main()
{
    int a[3][4]={0};
    printf("%d\n",sizeof(a));//48,sizeof(数组名)计算的是整个数组的大小,
    printf("%d\n",sizeof(a[0][0]));//4,计算首元素大小,单位字节
    printf("%d\n",sizeof(a[0]));//16,a[0]代表第一行作为一维数组的数组名
    //sizeof(arr[0])把数组名单独放在sizeof()内,计算的是第一行的大小
    printf("%d\n",sizeof(a[0]+1));//4/8,没有单独方a[0],此时a[0]代表的是一维数组的首元素地址,
    //把二维数组看做一维数组,首元素就是第一行数组,a[0]+1就是第2行数组地址,地址大小为4/8
    printf("%d\n",sizeof(*(a[0]+1)));//4,a[0]+1解引用是第一行第二个元素,大小为4个字节
    printf("%d\n",sizeof(a+1));//4/8,没有单独,所以是首元素地址,二维数组的首元素就是第一行数组,a+1就是第2行数组地址,地址大小为4/8
    printf("%d\n",sizeof(*(a+1)));//16,对第2行的数组地址解引用,计算的是数组的大小
    printf("%d\n",sizeof(&a[0]+1));//4/8,地址,第2行的地址
    printf("%d\n",sizeof(*(&a[0]+1)));//16,计算第2行数组长度
    printf("%d\n",sizeof(*a));//16,a是首元素地址,第一行地址,*a就是第一行,sizeof(*a)就是计算第一行的大小
    printf("%d\n",sizeof(a[3]));//16,不会真的访问第4行,只知道是一个一维4列数组

    return 0;
}
3、总结
数组名的意义:
(1)sizeof(数组名),这里的数组名表示整个数组,计算的整个数组的大小
(2)&数组名,这里的数组名表示整个数组,取出的是整个数组的地址
(3)除此之外所有的数组名都表示首元素的地址
4、
    int a[5]={1,2,3,4,5};
    int *ptr=(int *)(&a+1);
注意:p应该是数组指针,int (*p)[5]=(&a+1);这里强制类型转化
5、
    char* a[]={"work","at","alibaba"};
注意:这里是将字符串的首字符地址传给字符指针

//*************146 作业讲解***********************//


(1)指针-指针得到的是指针之间的元素个数
(2)struct---结构体类型的关键字
    struct student
{
    int num;
    char name[32];
    float score;
}stu;
    struct student是用户定义的结构体类型
    num,score都是结构体成员名;
    stu是用户定义的结构体类型的变量名
(3)
char str1[]="hello bit.";
char str2[]="hello bit";
char *str3="hello bit";
char *str4="hello bit";
    str1!=str2        str3==str4
两个数组,放在不同的栈空间;常量字符串不能只读不写,常量字符串不能被修改,所以内存中只有一个
(4)
    定义一个函数指针,指向的函数有两个in形参并且返回一个函数指针,返回指针指向一个有一个Int形参且返回int的函数,下面哪一个是正确的?
A,int(*(*F)(int,int) )(int)----T
B,int(*F)(int,int)-------F
(5)
    int arr[3][5]={1,2,3,4,5,6,7,8,9,10};
    print_arr(arr,3,5);
下面参数设计正确是:void print_arr(int(*arr)[5],int row,int col);
(6)
    !!!二维数组的首元素地址是整个第一行数组地址!!!!
(7)
    strcat(str1,str2);//是根据找str2的/0停止的
    strncat(str1,str2,n);//是根据n来停止的,与\0无关
    strstr(str1,str2);//判断str2字符串是否是str1指向的字符串的字串

//******************161 字符函数和字符串函数***********//


1、库函数介绍
(1)求字符串函数
    strlen
(2)长度不受限制的字符串函数
    strcpy---strcat---strcmp
(3)长度受限制的字符串函数介绍
    strncpy---strncat---strncmp
(4)字符串查找
    strstr---strtok
(5)错误信息报告
    strerror
(6)字符操作
(7)内存操作函数
    memcpy---memmove---memset---memcmp
2、特别注意!!
    C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在常量字符串中
或者字符数组中,字符串常用适用于那些对它不做修改的字符串函数
3、函数介绍
(1)strlen---求字符串长度
    size_t strlen ( const char* str );
    (1)字符串已经‘\0’作为结束标志,strlen函数返回的是在字符串中‘\0’前面出现的字符个数(不包含‘\0’)
    (2)参数指向的字符串必须要以‘\0’结束
    (3)注意函数的返回值size_t,是无符号的(易错)!!!!!size_t就是unsigned int无符号整形
    (4)模拟实现strlen函数--计数器的方法--递归---指针-指针!!递归很重要可以不创建临时变量!!
(2)strcpy---拷贝,复制
    char* strcpy( char* destination,const char* source );
    (1)源字符串必须以‘\0’结束
    (2)会将源字符串中的‘\0’拷贝到目标空间
    (3)目标空间足够大,以确保能存放源字符串
    (4)目标空间必须可变
    (5)模拟实现
        while(*dest++ = *src++)
        {
            ;
        }
(3)strcat----追加字符串
    char* strcat( char* destination,const char* source)
    (1)源字符串必须以‘\0’结束
    (2)目标空间必须由足够大,能容纳下源字符串的内容
    (3)目标空间必须可修改
    (4)字符串自己给自己追加需要用strncat
(4)strcmp---比较两个字符串大小---对应字符比较ASCII值
    char* strcmp( char* destination,const char* source )
(5)strncpy---拷贝num个字符从源字符串到目标空间
    char* strncpy( char* destination,const char* source,size_t num)
    如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个
(6)strstr---寻找子串
    char* strstr( const char*string,const char*strCharSet )
注意:读文档时,NULL---空指针;NUL/Null---‘\0’
    char* p1="abcdef";
    char* p2="def";
    char* ret= strstr(p1,p2);
(7)strtork
    char* strtock( char* str,const char* sep);
    (1)sep参数是个字符串,定义了用作分隔符的字符集合
    (2)第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记
    (3)strtork函数找到str中的下一个标记,并将其用\0结尾,返回一个指向这个标记的指针。(注:strtork函数会
改变被操作的字符串,所以在使用strtork函数切分得字符串一般都是临时拷贝的内容并且可以修改)
    (4)strtork函数的第一个参数不为NULL,函数将找到str中的第一个标记,strtork函数将保存它在字符串中的位置
    (5)strtork函数的第一个参数为NULL,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
    (6)如果字符串中不存在更多的标记,则返回NULL指针
(8)strerror
    char* strerror (int errnum);
    返回错误码,所对应的错误信息---头文件为#include <errno.h>
//errno是一个全局变量,错误码的全局变量
//当C语言的库函数在执行过程中,发生了错误,就会把对应的错误码,赋值给errno中
    char* str=strerror( errno );
    printf("%s\n",strerror(errno));
(9)字符转换函数
    int tolower( int c );
    int toupper( int c );

//***************170 内存函数***********//


前言:
    之前的函数操作对象是字符串,需要‘\0’
    而对于整形数组,浮点型数组,结构体数组就不适用
(1)memcpy
    void* memcpy( void* destination,const void* source,size_t num )//num的单位是字节
    (1)函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置
    (2)这个函数在遇到'\0'的时候并不会停下来
    (3)如果source和destination有任何的重叠,复制的结果都是未定义的
    (4)模拟实现
(2)memmove---处理内存重叠的拷贝情况
    void* memmove(void* destinaiton,const void* sourece,size_t num)
    (1)和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的
    (2)如果源空间和目标空间出现重叠,就得使用memmove函数处理
    (3)模拟实现---根据src与dest的地址前后关系,来确定从前向后拷贝还是从后向前拷贝
(3)memcmp---比较ptr1和ptr2指针开始的num个字节
    int memcmp(const* ptr1,const void*ptr2,size_num)
(4)memset---内存设置函数,单位为字节
    void* memset(void* dest,int c,size_t count)    

//*********174 内存函数讲解+结构体****************//


1、C语言中的内置类型&自定义类型
    (1)内置类型:char\short\int\long\float\double
    (2)自定义类型:结构体\枚举\联合体
2、结构体
(1)基础知识
    a.结构是做一些值的集合,这些值称为成员变量,结构的每个成员可以使不同类型的变量
    注意:数组是相同类型元素的集合
(2)结构的声明
    struct tag//结构体关键字 struct//tag是结构体标签//struct tag是结构体类型名称
    {
        member-list;//结构体的成员变量
    }variable-list;//创建一个结构体变量,此处为全局变量,所以一般不在此处创建
(3)数据结构
    数据在内存中的存储结构
(4)结构体定义内部不可以定义自己类型的成员----结构体的自引用---使用指针---数据域+指针域
    //可以包含其他的结构体类型
    struct Node
    {
        int data;
        struct Node* next;
    };//此处仅为自定义结构体类型的声明,没有定义变量
(5)typedef struct Node
    {
        int data;
        struct Node* next;
    }Node;//此处对于结构体类型重命名,struct Node==Node
    struct Node n1;
    Node n2;
(6)结构体变量的初始化
(7)结构体内存对齐---结构体的对齐规则
    1、第一个成员在于结构体变量偏移量为0的地址处
    2、其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
        对齐数=编译器默认的一个对齐数与该成员大小的较小值
        VS中默认的值为8
    3、结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
    4、如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,
    结构体的整数大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
(8)结构体内存对齐---原因
    1、平台原因(移植原因):不是所有硬件平台都能访问任意地址上的任意数据;某些硬件平台只能
                在某些地址处去某些特定类型的数据,否则抛出硬件异常
    2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐,原因在于,为了访问未对齐的内存,
            内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问
    总体来说:结构体的内存对齐--->空间换取时间
    注意:从节约内存的角度来说,让占用空间小的成员尽量集中在一起
(9)修改默认对齐数
    #pragram pack(4)//设置默认对齐数位4
    #pragrma pack()//取消设置的默认对齐数
(10)求解结构体成员变量内存地址的偏移量---offsetof是宏
    size_t offsetof(structName,memberName)
举例:offsetof(struct Node,data)
注意:头文件是#include <stddef.h>
(11)结构体传参---结构体指针
结论:结构体传参的时候,要传结构体的地址
原因:函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销,
    如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降

//*************179 位段********************//


1、位段
    位段的声明和结构是类似的,有两个不同:
    (1)位段的成员必须是int,unsigned int 或signed int
    (2)位段的成员名后边有一个冒号和一个数字
struct S
{
    int a : 2;//给a分配内存2个bit
    int b : 5;//给b分配内存5个bit
    int c : 10;//给c分配内存10个bit
    int d : 30;//给d分配内存30个bit
};
2、位段的内存分配
    (1)位段的成员可以是int, unsigned int, signed int或者是char(属于整形家族)类型
    (2)位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的
    (3)位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
3、位段的跨平台问题
    (1)int 位段被当成有符号数还是无符号数是不确定的
    (2)位段中最大位的数目不能确定
    (3)位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
    (4)当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
总结:跟结构体相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在

//*************181 枚举和共同体**********//


1、枚举----列举
    enum Day//星期----枚举类型
    {
        Mon,//0------枚举的可能取值
        Tues,//1---默认初始值是从0开始,一次递增
        Wed,//2----//也可以自己赋初始值----//Wed=9;
        Thur,//3
        Fri,//4
        Sat,//5
        Sun//6
    };
    int main()
    {
        enum Day day=Mon;
        return 0;
    }
2、枚举的优点
    我们可以使用#define定义常量,为什么使用枚举,枚举的优点:
    (1)增加代码的可读性和可维护性
    (2)和#deifne定义的标识符比较枚举有类型检查,更加严谨
    (3)防止命名污染(封装)
    (4)便于调试
    (5)使用方便,一次可以定义多个常量
3、联合-联合体-共用体定义
    联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员共用
同一块空间(所以联合也叫共用体),比如:
//联合类型的声明
    union Un
    {
        char c;
        int i;
    };
4、联合的特点
    联合的成员是共用同一块内存空间,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少要有能力保存最大的那个成员)
巧妙利用联合体来判断大端还是小端字节序
    int check_sys()
    {
        union Un
        {
            char c;
            int i;
        }u;
        u.i=1;
        //返回1,表示小端
        //返回0,表示大端
        ruturn u.c;
    }    
5、联合大小的计算
    (1)联合的大小至少是最大成员的大小
    (2)当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
union Un
{
    int a;//对齐数,8/4-4
    char arr[5];//对齐数,8/1-1,使用数组元素的大小
}

//*******************189 动态分配内存***************//


1、内存分配
(1)栈区
    局部变量;函数的形式参数
(2)堆区
    动态内存分配
(3)静态区
    全局变量;静态变量;static int a=10;
2、内存的使用方式
(1)创建一个变量
    int a=10;//局部变量-栈区----//全局变量-静态区
(2)创建一个数组
    int arr[10];//局部变量-栈区----//全局变量-静态区
注意:上述开辟空间的方式有两个特点
    (1)空间开辟大小是固定的
    (2)数组在申明的时候,必须指定数组的长度,它所需要的内训在编译时分配
3、动态内存的意义    
    对于空间的需求,不仅仅是上述的情况,有时候我们需要空间大小在程序运行的时候才能确定,那数组编译时开辟空间的方式不能满足,使用动态存开辟
4、动态分配内存函数malloc和free
//头文件为#include <stdlib.h>
    void* malloc (size_t size);//size_t代表unsigned int
    这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针
(1)如果开辟成功,则返回一个指向开辟好空间的指针
(2)如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查
(3)返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定
(4)如果参数size为0,malloc的行为是标准是未定义的,取决于编译器
(5)int* p=(int*)malloc(10*sizeof(int))    //向内存申请10个整型内存空间
    if (p==NULL)
    {    //头文件包含#include <errno.h>
        printf("%s\n",strerror(errno));//打印错误码对应的错误信息
    }
    else
    {
        //正常使用空间
        int i=0;
        for(i=0;i<10,i++)
        {
            *(p+i)=i;
        }
    }
(6)C语言提供了函数free,专门用来做动态内存的释放和回收的,函数原型如下:
    void* free(void* ptr);
    例子:free(p);p=NULL;
    虽然内存空间被释放,但是p存储的值依然为那个地址,所以p赋值空指针NULL
(7)free函数用来释放动态开辟的内存
    如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的
    如果参数Ptr是NULL指针,则函数什么事都不做
5、动态分配内存函数calloc
    calloc也用来动态内存分配,原型如下:
    void* calloc(size_t num,size_t size);
(1)函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0
(2)与函数malloc的区别只在于calloc会在返回地址在之前把申请的空间的每个字节初始化为0
6、动态分配内存函数realloc---可以对动态开辟内存大小继续调整
    realloc函数的出现让动态内存管理更加灵活,为了对内存的大小做灵活的调整,
    void* realloc(void* ptr,size_t size);
(1)ptr是需要进行  调整的之前开辟的动态内存地址
(2)size调整之后的内存大小
(3)返回值为调整之后的内存起始位置
(4)这个函数调整原内存空间大小的基础上,还是会将原来内存中的数据移动到新的空间
注意:realloc函数使用注意事项:
(1)如果p指向的空间之后有足够的内存空间可以追加,则直接追加,后返回P
(2)如果p指向的空间之后没有足够的内存空间可以追加,则realloc函数会重新找一个新的内存区域,
    开辟一块满足需求的空间,并且把原来内存中的数据拷贝回来,释放旧的内存空间,最后
    返回新开辟的内存空间地址
(3)给一个新指针来接收realloc的返回指针,不然如果开辟失败会破坏掉原地址
7、常见的动态内存错误
(1)对NULL指针的解引用操作
    应该先判断返回值是否为NULL再进行解引用操作
(2)对动态开辟空间的越界访问
(3)对非动态开辟内存的free
(4)使用free释放动态开辟内存的一部分---*p++=i;使用*(p+i)=i;p不可以改变
(5)对同一块动态内存的多次释放
    防止:free(p);p=NULL;可以有效避免多次释放,置为空指针
(6)动态开辟内存忘记释放(内存泄漏)
void GetMemory(char* p)
{
    p=(char*)malloc(100);
}
void Test(void)
{
    char *str=NULL;
    GetMemory(str);
    strcpy(str,"hello world");
    printf(str);//没有错误,同printf("abcdef");
}
int main()
{
    Test();
    return 0;
}
//1、运行代码程序会出现崩溃现象
//2、程序存在内存泄漏的问题
//str以值传递的形式给p
//p是GetMemory函数的形参,只能函数内部有效,等GetNemory函数返回之后,动态开辟内存尚未释放并且
//无法找到,所以会造成内存泄露    
8、C/C++程序内存分配的几个区域
(1)栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些
    存储单元自动被释放。占内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
    栈区主要存放运行函数而分配的局部变量,函数参数,返回数据,返回地址等。
(2)堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表。
(3)代码段:存放函数体(类成员函数和全局函数)的二进制代码
注意:
    实际上普通的局部变量是在栈区分配空间,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,知道程序结束才销毁,所以生命周期变长。
9、柔性数组(flexible array)
    C99中,结构中的最后一个元素允许时未知大小的数组,这就叫做【柔性数组】成员。
例如:
typedef struct st_type
{
    int i;
    int a[0];//柔性数组成员//或者写法:int arr[];//未知大小
//数组的大小可以调整---malloc动态开辟内存空间
}
10、寄存器
    cache-高速缓存
        内存
            硬盘
11、柔型数组的特点
(1)结构中的柔性数组成员前面必须至少一个其他成员
(2)sizeof返回的这种结构大小不包括柔性数组的内存
(3)包含柔型数组成员的结构用,malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小

//********************205 文件操作******************//


1、文件---程序设计中通常有:程序文件、数据文件
(1)程序文件
    包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)
(2)数据文件
    文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件
2、文件名
    一个文件要有一个唯一的文件标识,以便用户识别和引用
    文件名包含3部分:文件路径+文件名主干+文件后缀
3、文件类型
(1)根据数据的组织形式,数据文件被称为“文本文件”或者“二进制文件”
(2)数据在内存中以二进制形式存储,如果不仅转换的输出到外存,就是“二进制文件”
(3)如果要求在外存上以ASCII码的形式存储,则需要在存储前转换,以ASCII字符的形式存储的文件就是“文本文件”
4、文件缓冲区
    ANSIC标准采用“缓冲文件系统”处理的数据文件,所谓缓冲文件系统是指系统自动的在内存中为程序中每一个正在使用的
文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区才一起送到磁盘上。如果从磁盘向计算机
读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个的将数据送到程序数据区(程序变量等),
缓冲区的大小根据C编译系统决定的。
5、文件指针
(1)缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”
(2)每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。
    这些信息是保存在一个结构体变量中,该结构体类型是有系统声明的,取名FILE.
(3)FILE* pf;//文件指针变量
    定义pf是一个指向FILE类型数据的指针变量,可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该
文件。也就是说,通过文件指针变量能够找到与它关联的文件
6、文件的打开与关闭
(1)在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系
    ANSIC规定使用fopen函数来打开文件,fclose来关闭文件
    FILE* fopen(const char* filename,const char* mode);
    int fclose(FILE* stream);
(2)打开方式如下:(文件使用方式)---含义---如果指定文件不存在
    “r”只读---为了输入数据,打开一个已经存在的文本文件---出错
    “w”只写---为了输出数据,打开一个文本文件---建立一个新的文件
    “a”追加---想文本文件尾添加数据---出错
    “rb”只读---为了输入数据,打开一个二进制文件---出错
    “wb”只写---为了输出数据,打开一个二进制文件---建立一个新的文件    
    “ab”追加---想一个二进制文件尾添加数据---出错
    “r+”读写---为了读和写,打开一个文本文件---出错
    “w+”读写---为了读和写,建议一个新的文件---建立一个新的文件
    “a+”读写---打开一个文件,在文件尾进行读写---建立一个新的文件
    “rb+”读写---为了读和写打开一个二进制文件---出错
    “wb+”读写---为了读和写,新建一个新的二进制文件---建立一个新的文件
    “ab+”读写---打开一个二进制文件,在文件尾进行读和写---建立一个新的文件
7、文件的顺序读写
(1)字符输入函数---fgetc---所有输入流
(2)字符输出函数---fputc----所有输出流
(3)文本行输入函数---fgets---所有输入流
(4)文本行输出函数---fputs---所有输出流
(5)格式化输入函数---fscanf---所有输入流
(6)格式化输出函数---fprintf---所有输出流
(7)二进制输入---fread---文件
(8)二进制输出---fwrite---文件
8、对比一组函数
    scanf/fscanf/sscanf
    printf/fprintf/sprintf
(1)scanf/printf 是针对标准输入流/标准输出流的格式化输入/输出语句
(2)fscanf/fprintf 是针对所有输入流/所有输出流的格式化输入/输出语句
(3)sscanf 是从字符串中读取格式化的数据;/sprintf 是把格式化的数据输出成(存储到)字符串
9、文件的随机读写
(1)fseek---根据文件指针的位置和偏移量来定位文件指针
    int fseek(FILE* stream,long int offset,int origin);//offset只偏移量,origin文件指针的当前位置
origin选项:SEEK_CUR,SEEK_END,SEEK_SET
(2)ftell---返回文件指针相对于起始位置的偏移量
    long int ftell(FILE* stream);
(3)rewind
    让文件指针的位置回到文件的起始位置
    void rewind(FILE* stream);    
10、文件结束判定---EOF,end of file
    被错误使用的feof,牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件是否结束
而是应用于当文件读取结束时,判断是读取失败结束,还是遇到文件尾结束
(1)文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)
    例如:fgetc判断是否为EOF;fgets判断返回值是否为NULL
(2)二进制文件的读取结束判断,判断返回值是否小于实际要读的个数
    例如:fread判断返回值是否小于实际要读的个数

//******216 C语言预处理*******************//


1、程序的翻译环境和执行环境
    (1)在ANSI C的任何一种实现中,存在两种不同的环境
    (2)第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。第二种是执行环境,它用于实际执行代码
    (3)exe文件为二进制文件
    (4)每个.c源文件都会经过分别的编译器分别生成目标文件,再统一经过链接器形成可执行程序
注:组成一个程序的每个源文件通过编译过程分别转换为目标代码(object code);每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
    (5)翻译分为编译+链接;编译分为预编译+编译+汇编;
2、编译:预编译+编译+汇编
(1)预编译(文本操作)test.c->test.i:
    //#include---头文件的包含*//*注释删除---使用空格替换注释*//*#define---预处理指令
(2)编译(C->汇编)test.i->test.s
    //语法分析//词法分析//语义分析//符号汇总
(3)汇编test.c->test.o
    //形成符号表
3、链接
    合并段表*//*符号表的合并和重定位
4、运行环境
(1)程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码只如只读内存来完成
(2)程序的执行开始。接着便调用main函数
(3)开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态内存(static),存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
(4)终止程序。正常终止main函数,也有可能是意外终止。
//推荐书籍《程序员的自我修养》
5、预编译详解(预处理)
(1)预定义符号---(语言中内置,本来就有的)
    __FILE__//进行编译的源文件%s
    __LINE__//文件当前的行号%d
    __DATE__//文件被编译的日期%s
    __TIME__//文件被编译的时间%s
    __STDC__//如果编译器遵循ANSI C,其值为1,否则未定义
printf("file:%s line:%d\n",__FILE__,__LINE__);//文件绝对路径+文件名
(2)#define定义标识符---预处理指令
注:#开头的指令均为预处理指令,#define,#inlcude,#pragma,#if,#endif,#ifdef,#line
    #define后不加;为好
    #define定义宏,包括了一个规定,允许把参数替换到文本中,称为宏(macro)或定义宏(define macro)
下面是宏的申明方式
#define name( parament-list)stuff其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中
注意:参数列表的左括号必须与name相邻,如果两者之间有任何的空白存在,参数列表就会被解释为stuff的一部分
如:#define SQUARE(x) ((x)*(x))---------SQUARE(5)
//多加括号,不容易出错!!!!避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用
(3)#define替换规则
    //在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果是,首先被替换
    //替换文本随后被插入到称重原来文本的位置上,对于宏,参数名被他们的值替换
    //最后,再次对结果文件进行扫描,看看它是否包含#define定义的符号,如果是,就重复上述过程
注意:
    //宏参数和#define定义中可以出现其他#define定义的变量,但是对于宏,不能出现递归
    //当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
(4)#和##
#define PRINT(X) printf("the value of "#X" is %d\n",X)----实现了把参数插入字符串中
    #X---->"X",相邻字符串会自动合并
##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符
#define ADD_TO_SUM(num,value) sum##sum += value;
    ADD_TO_SUM(5,10);//作用是:给sum5增加10
注:这样的连接必须产生一个合法的标识符,否则其结果就是未定义的
(5)带副作用的宏参数
    当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果,
副作用就是表达式求值的时候出现的永久性效果,例如:
    x+1;//不带副作用
    x++;//带有副作用
例如:#define MAX(X,Y)   ((X)>(Y)?(X):(Y))
    max = MAX(a++,b++);----max = ((a++)>(b++)?(a++):(b++));
(6)宏和函数对比
    宏通常被应用于执行简单的运算,比如在上例子中在两个数中找出较大的一个
宏的优点-不用函数的原因:
    //用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作的所需要时间更多,所以宏比函数在程序的规模和速度方面更胜一筹;
    //更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用,反之宏可以适用于整型、长整型、浮点型等可以用于>来比较的类型,宏是类型无关的
宏的缺点:
    //每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度,
    //宏是无法调试的--//---宏由于类型无关,不够严谨
    //宏可能会带来运算符优先级的问题,导致程序容易出错
(7)#undef---用于移除一个宏定义
      #undef NAME //如果一现存的名字需要被重新定义,那么它的旧名字首先要被移除
(8)命令行定义
    许多C的编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程,例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,可以用到
(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写)
6、条件编译
    在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便,使用条件编译指令
//#define __DEBUG__    1
    #ifdef  __DEBUG__
    printf("%d\n",arr[i]);//为了观察数组是否赋值成功
    #endif  //__DEBUG__
(1)常见的条件编译指令1
    #define 常量表达式  1
    #if 常量表达式
        //******
    #endif
    //常量表达式有预处理器求值
(2)多个分支的条件编译
    #if 常量表达式
        //***
    #elif 常量表达式
        //***
    #else
        //***
    #endif
(3)判断是否被定义
    # ifdef(symbol)== #if defined(symbol)
    # ifndef(symbol)== #if !defined(symbol)
(4)嵌套指令
#if defined(OS_UNIX)
    #ifdef OPTION1
        unix_version_option1();
    #endif
    #ifdef OPTION2
        unix_version_option2();
    #endif
#elif defined(OS_MSDOS)
    #ifdef OPTION2
        msdod_version_option2();
    #endif
#endif
7、文件包含
    头文件被包含的方式:
(1)本地文件包含
    #include "filename.h"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就想查找库函数头文件一样在标准位置查找头文件。
如果找不到就提示编译错误,linux环境的标准头文件的路径:
    /usr/include
    VS环境的标准头文件的路径:
    C:\Program Files(x86)Microsoft Visual Studio 9.0\VC\include
(2)库文件包含
    #include <filename.h>
查找策略:直接去标准路径下去查找,如果找不到就提示编译错误
(3)避免头文件重复包含
#ifndef ___TEST_H__
#define __TEST_H__
    //*******
#endif  //__TEST_H_
或者
#pragma once
//推荐书籍《C语言深度解剖》


 

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值