C语言进阶

目录

一. 数据的存储

1.0 运算符优先级 

1.1数据类型介绍

1.2 整形在内存中的存储

1.3 整形提升和算术转换

1.3.1 正数的整形提升,高位补充符号位,即为0

1.3.2 负数的整形提升,高位补充符号位,即为1

1.3.3 算术转换:

1.3.4 sizeof里是不参与运算的

1.4 大小端判断

1.5 浮点数的存储规则

二. 指针的进阶

2.1 字符指针

2.2 指针数组

数组名表示的是数组首元素的地址,除了两种例外:

2.3 数组指针

2.3.1 数组指针的使用:二维数组传参

2.4 数组传参、指针参数

一维数组传参:

二维数组传参;

一级指针传参: (一级指针变量其实就是个地址,即整形整数,传参时用一级指针接收)

 二级指针传参:

2.5 函数指针

2.6 函数指针数组

2.7 指向函数指针数组的指针

2.8 回调函数(call-back)

2.8.1 回调函数介绍

 2.8.2 回调函数的应用

2.8.3 一个实用的框架

2.9 指针与 const 的关系

2.9.1  const 在 * 后面

 2.9.2  const 在 * 前面

2.9.3  const int* const p4 

 2.9.4  函数参数加 const

三. 字符串函数 + 内存函数

3.1 字符串函数

3.1.1 strlen  (计算字符串字节大小)

3.1.2 strcpy  (字符串拷贝)

3.1.3 strcat  (字符串追加/连接,在字符串后面追加上另一个字符串)

3.1.4 strcmp  (字符串比较)

3.1.5 strncpy

3.1.6 strncat

3.1.7 strncmp

3.1.8 strstr  (字符串中查找某段字符串)

3.1.9 strtok  (字符串分割)

3.1.10 strerror

3.2 内存函数

 3.2.1 memcpy

3.2.2 memmove

3.2.3 memcmp

3.2.4 memset

四. 自定义类型

 4.1 结构体

4.2 枚举

4.3 联合体

五. 动态内存的管理

5.1 malloc 和 free

5.2 calloc

5.3 realloc

5.4 常见的动态内存错误

5.5 柔性数组

六. 文件操作

七. 程序的编译

八. 补充

8.1 字符串数组、传参


一. 数据的存储

1.0 运算符优先级 

从左到右依次降低 

1.1数据类型介绍

1.2 整形在内存中的存储

对于整形来说:数据存放内存中其实存放的是补码。
正数的原、反、补码都相同;负整数的三种表示方法各不相同。
原码
直接将二进制按照正负数的形式翻译成二进制就可以。
反码
将原码的符号位不变,其他位依次按位取反就可以得到了。
补码
反码 +1 就得到补码。
三种表示方法均有 符号位 数值位 两部分,符号位用 0 表示 ,用 1 表示 负。
原码反码补码
int 20;0001 01000001 01000001 0100
int = -10;1000 10101111 01011111 0110

1.3 整形提升和算术转换

整形提升:当小于int的类型 (char、short) 在进行不同类型的计算时会发生整形提升。

C的整型算术运算总是至少以缺省整型类型的精度来进行的。当两个小于int整形的的操作数在计算机中进行计算时,计算机为了获得精度会先将两个类型在计算前先转换为int的整形。

整型提升是按照变量的数据类型(自身的数据类型)的符号位来提升的

1.3.1 正数的整形提升,高位补充符号位,即为0

    char a = 126;
    char b = 5;
    char c = a + b;

分析:计算机在运算时使用的是补码进行计算,当操作数进行运算时,使用的是补码,正数的原码和补码相同,但计算出来的还是补码,补码在需要打印时会先转换成原码的形式在进行打印。因为char小于int的4个字节,所以在计算时会发生整形提升,将char由8bit提升为32bit,然后相加得到一个补码。再将补码转换成原码以整形(32bit)的形式打印出来。
char a : 0111 1110, 整形提升为 00000000 00000000 00000000 0111 1110

char b :0000 0101,整形提升为 00000000 00000000 00000000 0000 0101

相加后的补码为 00000000 00000000 00000000 1000 0011, 再赋值给char c,即c在内存中以补码形式存放的二进制为1000 0011。

若c以char的形式去读取: 反码=补码-1,1000 0010; 原码:1111 1101,即-125;

若c 为unsigned char的形式去读取,反码=补码=原码,1000 0011,即为131。

1.3.2 负数的整形提升,高位补充符号位,即为1

	char a = -10;
	char b = 20;
	int c = a + b;

char a : 1000 1010, 整形提升为 11111111   11111111   11111111  1000 1010

char b : 0001 0100, 整形提升为 00000000 00000000 00000000 0001 0100

相加后的补码为 11111111 11111111 11111111 1001 1110, 再赋值给int c。即为c在内存中以补码形式存放的二进制。

c以 int 的形式去读取,反码=补码-1,11111111 11111111 11111111 1001 1101

原码:10000000 00000000 00000000 0110 0010,即-98。

1.3.3 算术转换

当大于或等于int的不同类型要进行运算发生的就叫做算术转换:unsigned int、long、float、double、long long、long double

当一个int 与unsigned int进行计算时,会先将int 通过算术转换为unsigned int,再与unsigned int计算,如果是一个整形在与浮点型进行计算会将整形先转换成浮点型再进行计算。同类型的计算不会发生算术转换(不包括char和short)。

void foo(void)
{
    unsigned int a = 6;
    int b = -20;
    (a+b > 6) ? puts("> 6") : puts("<= 6");
}

答案是输出是 ">6"。原因是当表达式中存在 有符号类型 和 无符号类型 时所有的操作数都 自动转换为无符号类型。因此-20 变成了一个非常大的正整数,所以该表达式计算出的结果大于 6。

1.3.4 sizeof里是不参与运算的

表达式都有一个值属性和一个类型属性。一个double类型和int类型的进行计算,计算后的结果就是值属性,两个操作数一个是int类型,一个是double类型,就是类型属性。而sizeof会根据表达式来判断是否存在整形提升或算术转换,然后只需要推断下最后得到的类型是什么,最后计算字节大小就可以了,不会进行值计算。

#include<stdio.h>
int main()
{
    int a = 4;
    double b = 4.0f;
    printf("%d\n",sizeof(a+b)); //8,但a+b不会进行运算
 
    return 0;
}

1.4 大小端判断

小端模式:数据的低位放在低地址空间 (起始地址),数据的高位放在高地址空间

大端模式:数据的高位放在低地址空间 (起始地址),数据的低位放在高地址空间

数据读取时,是由 低地址-->高地址 顺序读取。如32位int型数0x12345678的存储:

//方法一
void check_sys()                //内存中数据的存储:由低地址到高地址
{
    int num = 1;                //小端: 00 00 00 01 大端: 01 00 00 00 
    char *p = (char *)&num;

    if( *p == 1 )               //读取低地址的字节
        printf("小端存储");    
}

//方法二
union UN
{
   char c;         //数据都是从低地址开始读起
   int i;          //数据都是从低地址开始存放的
};

int main()
{
   union UN a;
   a.i = 1;
   if( a.c == 1 )            //读取c,若对应i的低字节,即低地址--低字节,为小端
      printf("小端存储"); 
}

1.5 浮点数的存储规则

根据国际标准 IEEE( 电气和电子工程协会) 754 ,任意一个二进制浮点数 V 可以表示成下面的形式:
(-1)^S * M * 2^E
(-1)^s 表示符号位,当 s=0 V 为正数;当 s=1 V 为负数。
M 表示有效数字,大于等于 1 ,小于 2
2^E 表示指数位。
举例来说:
十进制的 5.0 ,写成二进制是 101.0 ,相当于 1.01×2^2
那么,按照上面 V 的格式,可以得出 s=0 M=1.01 E=2
十进制的 -5.0 ,写成二进制是 - 101.0 ,相当于 - 1.01×2^2 。那么, s=1 M=1.01 E=2

二. 指针的进阶

* 号的优先级

a. 优先级低于数组,如数组指针应该写 int  (*par) [10]

b. 优先级低于取结构体成员,如Struct结构体成员名name,p指向结构体,*p.name中,p优先

     与.name结合,再与*结合

c. 优先级低于( ),如函数指针int (*p) (int, int),*跟p须( )起来,否则优先跟后面的(int, int )结合

2.1 字符指针

指向字符的指针

int main()
{
    char ch = 'w';
    char *pc = &ch;
    *pc = 'w';
    return 0;
}
int main()
{
    const char* pstr = "hello bit.";    //pstr指针变量指向了常量字符串
    printf("%s\n", pstr);
    return 0;
}

2.2 指针数组

是一个数组,数组的里放的是指针

int* arr1[10];  //整形指针的数组
char *arr2[4];  //一级字符指针的数组
char **arr3[5]; //二级字符指针的数组

数组名表示的是数组首元素的地址,除了两种例外:

 sizeof (数组名) 。表示整个数组的大小,而不是首元素的大小;

&数组名。取出的是数组全部元素的地址,指针若+ - 整数时,将会移动整个数组大小的位移。

2.3 数组指针

是一个指针,指针指向一个数组。

注意:[ ]的优先级要高于*号的,所以必须加上()来保证 和 * 先结合。

int *p1[10];    //指针数组
int (*p2)[10];  //数组指针

2.3.1 数组指针的使用:二维数组传参

#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)    //传统方式
{
    int i = 0;
    for(i=0; i<row; i++)
   {
        for(j=0; j<col; j++)
       {
            printf("%d ", arr[i][j]);
       }
        printf("\n");
   }
}

void print_arr2(int (*arr)[5], int row, int col)    //数组指针方式
{
    int i = 0;
    for(i=0; i<row; i++)
   {
        for(j=0; j<col; j++)
       {
            printf("%d ", arr[i][j]);
       }
        printf("\n");
   }
}
int main()
{
    int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
    print_arr1(arr, 3, 5);
            //数组名arr,表示首元素的地址
            //但是二维数组的首元素是二维数组的第一行
            //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
            //可以数组指针来接收
    print_arr2(arr, 3, 5);
    return 0;
}

练习:下列是什么

int arr[5];            //整形数组
int *parr1[10];        //指针数组,其指针指向一个整形
int (*parr2)[10];      //数组指针,指向整形数组的指针
int (*parr3[10])[5];  //数组,数组存放的变量类型是数组指针
------------------------------------------------------------------
parr1先跟[10]结合,是个数组,数组里存放的是指针int *
parr2先跟*结合,是个指针,指针指向的是数组int [10]
parr3先跟[10]结合,是个数组。数组的类型是int (* )[5],即指向整形数组的指针

技巧:parr3[10]已经确定是个数组了,把数组拿掉,剩下的int (*  ) [5]就是数组的类型了。

2.4 数组传参、指针参数

一维数组传参:

#include <stdio.h>
void test(int arr[])    //形参为数组
{}
void test(int arr[10])  //形参为数组,10可不写,因为本质传的是地址不是数组 
{}
void test(int *arr)     //形参为指针
{}

void test2(int **arr)  //*arr来接收数组,数组的类型是int *
{}

int main()
{
 int arr[10] = {0};      //数组
 int *arr2[20] = {0};    //指针数组

 test(arr);
 test2(arr2);
}

二维数组传参;

void test(int arr[3][5])   //形参为数组
{}
void test(int arr[][5])    //二维数组的行可省略不写,列不可省略
{}

void test6(int(*arr)[5])   //形参为指针。用指针接收,其指针指向一个一维数组
{}                                                 ^
                                                   | 
int main()                                         |    
{                                                  |
  int arr[3][5] = {0};                             v  
  test(arr);               //数组名为首元素地址,即一个一维数组
}

一级指针传参: (一级指针变量其实就是个地址,即整形整数,传参时用一级指针接收)

#include <stdio.h>
void print(int *p, int sz)     //一级指针变量接收,变量类型是int *
{
  int i = 0;
  for(i=0; i<sz; i++)
  {
     printf("%d\n", *(p+i));
  }
}
int main()
{
  int arr[10] = {1,2,3,4,5,6,7,8,9};
  int *p = arr;
  int sz = sizeof(arr)/sizeof(arr[0]);
  
  print(p, sz);         //一级指针变量p作为参数,传给函数
  return 0;
}

当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

 
void test(int* p)  //一级指针接收
{ }
int main()
{
	int a = 0;  
	test(&a);          //方法1:一个数
 
	int a = 0;
	int* ptr = &a;
	test(ptr);         //方法2:一级指针 (值传递)
 
	int arr[10] = { 0 };
	test(arr);             //方法3:一维数组
 
}

 二级指针传参:

#include <stdio.h>
void test(int** ptr)
{
	printf("num=%d \n", **ptr);
}
int main()
{
	int n = 10;
	int* p = &n;
	int** pp = &p;
	test(pp);
	test(&p);   //一级指针的地址
	return 0;
}

当一个函数的参数部分为二级指针的时候,函数能接收什么参数?

void test(int** p)
{ }
int main()
{
	int** ptr;
	test(ptr);     //方法1:二级指针
 
	int* p2;
	test(&p2);     //方法2:一级指针 (地址传递)
 
	int* arr[10];
	test(arr);     //方法3:指针数组
}

2.5 函数指针

函数指针解引用: c = ( *pf2 ) ( 1,2 )         // pf2为函数指针

  •  函数名可直接表示函数的地址,类似于数组名。
  • * 号优先级低于( )。函数指针 int (*p) (int, int), *p要( )起来,否则p优先跟(int,int )结合
  •   c = ( *pf2 ) ( 1,2 ) 可以改成  c = pf2 ( 1,2 ),   *号可省略,很重要

函数指针的形式函数的返回值类型(*指针名)(函数的参数列表类型)

int (*ptf)(int,int)                       // 这样是定义一个函数指针 变量                                    

typedef   int ( *pFUN)(int,int)  // 这样是定义了一个函数指针 类型

int Add(int x, int y) {
	   return x+y;
}

int main()
{
    int (*pf)(int, int) = &Add  //直接定义函数指针pf
    
    pFUN pf2;                   //通过指针类型定义变量
    int c = (*pf2)(1,2);        //函数指针的应用
}

函数作为参数传参时,参数类型为 指向该函数类型的指针 

Typedef  int (*pFUN) (int, int);        //定义函数指针
int fun( int x, int y )                  //函数fun
{
    return x+y;
}

int function( pFUN fun, int a, int b)    //函数fun作为参数传参的类型,为函数指针
{
    return fun(a,b);
}

2.6 函数指针数组

是一个数组,数组里存放函数指针,即存放函数的地址。

变量定义举例:首先是个数组 arr[10],数组存放的类型是函数指针 (void) (* ) ( int, int ), 两个加起来就是函数指针数组:(void) (* arr [10] ) ( int, int ) = { 0 };

函数指针数组的用途: 转移表
#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;}
int div(int a, int b){
           return a / b;}

int main()
{
     int x, y;
     int input = 1;
     int ret = 0;                   //函数名表示函数地址
     int(*p[5])(int x, int y) = { 0, add, sub, mul, div };        //转移表
     while (input)
     {
          printf( "*************************\n" );
          printf( " 1:add           2:sub \n" );
          printf( " 3:mul           4:div \n" );
          printf( "*************************\n" );
          printf( "请选择:" );
      scanf( "%d", &input);
          if ((input <= 4 && input >= 1))
         {
          printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = (*p[input])(x, y);     //*p解引用,得到对应的函数,再传参x,y
         }
          else
               printf( "输入有误\n" );
          printf( "ret = %d\n", ret);
     }
      return 0;
}

2.7 指向函数指针数组的指针

是一个指针,指针指向函数指针数组。

变量定义举例:首先是个指针:*p,指针指向数组:(*p) [10] ,数组存放的类型是函数指针

(void) (* ) ( int, int );加起来就是:(void) (*  (*p) [10] ) ( int, int ) 。

void test(const char* str)
{
 printf("%s\n", str);
}

int main()
{
 void (*pfun)(const char*) = test;        //函数指针pfun
 
 void (*pfunArr[5])(const char* str);     //存放函数指针的数组pfunArr
 pfunArr[0] = test;
                                     //指向函数指针数组pfunArr的指针ppfunArr
 void (*(*ppfunArr)[5])(const char*) = &pfunArr;   
 return 0;
}

2.8 回调函数(call-back)

2.8.1 回调函数介绍

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

讲人话就是

把一个函数Call-Back(地址)作为参数传递传给其他函数P,函数P的形参用函数指针接收。而这个函数Call-Back会在某个时刻被调用执行,函数Call-Back就叫回调函数

如果代码立即被执行就称为同步回调,如果过后再执行,则称之为异步回调

举例:假设我们要使用一个排序函数来对数组进行排序,那么在主程序(Main program)中,我们先通过库,选择一个库排序函数(Library function)。但排序算法有很多,有冒泡排序,选择排序,快速排序,归并排序。同时,我们也可能需要对特殊的对象进行排序,比如特定的结构体等。库函数会根据我们的需要选择一种排序算法,然后调用实现该算法的函数来完成排序工作。这个被调用的排序函数就是回调函数(Callback function)。

img

int Callback_1(int a)   ///< 回调函数1
{
    printf("Hello, this is Callback_1: a = %d ", a);
    return 0;
}

int Callback_2(int b)  ///< 回调函数2
{
    printf("Hello, this is Callback_2: b = %d ", b);
    return 0;
}

int Callback_3(int c)   ///< 回调函数3
{
    printf("Hello, this is Callback_3: c = %d ", c);
    return 0;
}

int Handle(int x, int (*Callback)(int))   ///< 这里形参用函数指针接收函数
{
    (*Callback)(x);
}

int main()
{
    Handle(4, Callback_1);
    Handle(5, Callback_2);
    Handle(6, Callback_3);
    return 0;
}

 2.8.2 回调函数的应用

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

/**************** 函数指针结构体 **************/

typedef struct _OP {
    float (*p_add)(float, float); 
    float (*p_sub)(float, float); 
    float (*p_mul)(float, float); 
    float (*p_div)(float, float); 
} OP; 

/***************** 加减乘除函数 ****************/

float ADD(float a, float b) 
        {   return a + b;   }

float SUB(float a, float b) 
        {   return a - b;   }

float MUL(float a, float b) 
        {   return a * b;   }

float DIV(float a, float b) 
        {   return a / b;   }

/**************** 初始化函数指针 **************/
void init_op(OP *op )
{
    op->p_add = ADD;
    op->p_sub = SUB;
    op->p_mul = &MUL;    //可加可不加&,函数名本身就是函数地址
    op->p_div = &DIV;
}

/**************** 库函数 *******************/     //回调函数的接收:函数指针
float add_sub_mul_div(float a, float b, float (*op_func)(float, float))
{
    return (*op_func)(a, b);
}

int main(int argc, char *argv[]) 
{
    OP *op = (OP *)malloc(sizeof(OP)); 
    init_op(op);
    
    /* 直接使用函数指针调用函数 */ 
    printf("ADD = %f, SUB = %f, MUL = %f, DIV = %f\n", 
            (op->p_add)(1.3, 2.2), (*op->p_sub)(1.3, 2.2), 
            (op->p_mul)(1.3, 2.2), (*op->p_div)(1.3, 2.2));
     
    /* 调用回调函数 */ 
    printf("ADD = %f, SUB = %f, MUL = %f, DIV = %f\n", 
            add_sub_mul_div(1.3, 2.2, ADD),     
            add_sub_mul_div(1.3, 2.2, SUB), 
            add_sub_mul_div(1.3, 2.2, MUL), 
            add_sub_mul_div(1.3, 2.2, DIV));    //回调函数的传参:函数地址

    return 0; 
}

2.8.3 一个实用的框架

一个GPRS模块联网的小项目,使用过的同学大概知道2G、4G、NB等模块要想实现无线联网功能都需要经历模块上电初始化、注册网络、查询网络信息质量、连接服务器等步骤,这里的的例子就是,利用一个状态机函数(根据不同状态依次调用不同实现方法的函数),通过回调函数的方式依次调用不同的函数,实现模块联网功能,如下:

/*********  工作状态处理  *********/
typedef struct
{
 uint8_t mStatus;
 uint8_t (* Funtion)(void); //函数指针的形式
} M26_WorkStatus_TypeDef;  //M26的工作状态集合调用函数


/**********************************************
** >M26工作状态集合函数
***********************************************/
M26_WorkStatus_TypeDef M26_WorkStatus_Tab[] =
{    
    {GPRS_NETWORK_CLOSE,  M26_PWRKEY_Off  }, //模块关机
    {GPRS_NETWORK_OPEN,  M26_PWRKEY_On  }, //模块开机
    {GPRS_NETWORK_Start,   M26_Work_Init  }, //管脚初始化
    {GPRS_NETWORK_CONF,  M26_NET_Config  }, /AT指令配置
    {GPRS_NETWORK_LINK_CTC,  M26_LINK_CTC  }, //连接调度中心  
    {GPRS_NETWORK_WAIT_CTC, M26_WAIT_CTC  },  //等待调度中心回复 
    {GPRS_NETWORK_LINK_FEM, M26_LINK_FEM  }, //连接前置机
    {GPRS_NETWORK_WAIT_FEM, M26_WAIT_FEM  }, //等待前置机回复
    {GPRS_NETWORK_COMM,  M26_COMM   }, //正常工作    
    {GPRS_NETWORK_WAIT_Sig,  M26_WAIT_Sig  },  //等待信号回复
    {GPRS_NETWORK_GetSignal,  M26_GetSignal  }, //获取信号值
    {GPRS_NETWORK_RESTART,  M26_RESET   }, //模块重启
}
/**********************************************
** >M26模块工作状态机,依次调用里面的12个函数   
***********************************************/
uint8_t M26_WorkStatus_Call(uint8_t Start)
{
    uint8_t i = 0;
    for(i = 0; i < 12; i++)
    {
        if(Start == M26_WorkStatus_Tab[i].mStatus)
        {          
            return M26_WorkStatus_Tab[i].Funtion();
        }
    }
    return 0;
}

2.9 指针与 const 的关系

链接:const* 和 *const

2.9.1  const 在 * 后面

int *const p1:表示常量指针,必须在定义时初始化,而且初始化后不能改变指向,但可以通过该指针修改内存里面的数据

1:必须在定义时初始化      (下列没初始化导致报错)                                                                       

2:不能改变指向                                                                                                                                    

3:可以通过该指针修改内存里面的数据                                                                                               

 2.9.2  const 在 * 前面

int const *p2 、 const int *p3:表示指向常量的指针,不是必须初始化,能改变指向,但是不能通过该指针修改内存里面的数据可以通过原来的变量修改内存的数据

 ​​​​​​1.不是必须初始化                                                                                                                                  

2.能改变指向,但不能修改内存所指向的内存里面的数据                                                                       

3.可以通过原来的变量修改内存的数据                                                                                                   

2.9.3  const int* const p4 

第一种情况和第二种情况的结合。

 2.9.4  函数参数加 const

链接:const 与指针的组合

三. 字符串函数 + 内存函数

函数介绍求字符串长度strlen
长度不受限制的字符串函数strcpy
strcat
strcmp
长度受限制的字符串函数介绍strncpy
strncat
strncmp
字符串查找strstr
strtok
错误信息报告strerror
字符操作
内存操作函数memcpy
memmove
memset
memcmp

3.1 字符串函数

3.1.1 strlen  (计算字符串字节大小)

size_t   strlen  ( const char * str  );                // Return the length of string
  • 字符串以 '\0' 作为结束标志,strlen函数返回的是在字符串中 '\0' 前面出现的字符个数(不包 '\0' )
  • 参数指向的字符串必须要以 '\0' 结束
  • 注意函数的返回值为size_t,是无符号的( 易错

3.1.2 strcpy  (字符串拷贝)

char* strcpy  char * destination , const char * source );            // Return destination
  • 源字符串必须以 '\0' 结束 (src 和 dest 所指内存区域不可以重叠)
  • 会将源字符串中的 '\0' 拷贝到目标空间
  • 目标空间必须足够大,以确保能存放源字符串
  • 目标空间必须可变

3.1.3 strcat  (字符串追加/连接,在字符串后面追加上另一个字符串)

char * strcat ( char * destination, const char * source );

  • 源字符串必须以 '\0' 结束,会将源字符串中的 '\0' 拷贝到目标空间
  • 目标空间必须有足够的大,能容纳下源字符串的内容
  • 目标空间必须可修改
  • 字符串不可自己给自己追加,'\0'会被覆盖掉,陷入死循环

3.1.4 strcmp  (字符串比较)

int strcmp ( const char * str1 , const char * str2 );                //Return  '>0'  or  '0'  or  '<0'
  • 比较到 出现字符不一样 或者一个字符串结束 或者字符串全部比较完
  • 第一个字符串大于第二个字符串,则返回大于0的数字
  • 第一个字符串等于第二个字符串,则返回0
  • 第一个字符串小于第二个字符串,则返回小于0的数字
#include<stdio.h>
#include<string.h>
int main()
{
	char* p1 = "abcdef";
	char* p2 = "abcdef";
	char* p3 = "abcd";
	char* p4 = "bcde";

	printf("%d\n", strcmp(p1,p2 ));    //0
	printf("%d\n", strcmp(p1,p3 ));    //1, P3提前遇到"\0",且e > "\0"
	printf("%d\n", strcmp(p3,p4 ));    //-1,a < b
}

3.1.5 strncpy

char * strncpy ( char * destination , const char * source , size_t num );     //Return destination

  • 拷贝num个字符从源字符串到目标空间
  • 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个

3.1.6 strncat

char * strncpy ( char * destination , const char * source , size_t num );     //Return destination
  • 在字符串后面追加上另一个字符串的前n个字符

3.1.7 strncmp

int   strncmp ( const char * str1 , const char * str2 , size_t num );
  • 比较字符串的前n个字符

3.1.8 strstr  (字符串中查找某段字符串)

char * strstr ( const char * str1 , const char * str2 );
  • 在字符串str1中查找是否含有字符串str2

3.1.9 strtok  (字符串分割)

char * strtok ( char * str , const char * sep );
  • sep参数是个字符串,定义了用作分隔符的字符集合
  • 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标
  • strtok函数找到 str 中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。  ( 注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷    贝的内容 并且可修改。)
  • strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串 中的位置。
  • strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
  • 如果字符串中不存在更多的标记,则返回 NULL 指针。
/* 字符串分割 */
#include <stdio.h>
#include <string.h>
int main ()
{
  char str[] ="- This, a sample string.";
  char * pch;
  printf ("Splitting string \"%s\" into tokens:\n",str);
  pch = strtok (str," ,.-");
  while (pch != NULL)
 {
    printf ("%s\n",pch);
    pch = strtok (NULL, " ,.-");
 }
  return 0;
}
/* 数组分割 */
#include <stdio.h>
int main()
{
   char *p = "zhangpengwei@bitedu.tech";
 const char* sep = ".@";
 char arr[30];
 char *str = NULL;
 strcpy(arr, p);//将数据拷贝一份,处理arr数组的内容
 for(str=strtok(arr, sep); str != NULL; str=strtok(NULL, sep))
 {
 printf("%s\n", str);
 }
}

3.1.10 strerror

char * strerror ( int errnum );
  • 获取指向错误消息字符串的指针
  • 返回值为char * 类型 。指向描述错误错误的错误字符串的指针

函数解释:C语言的库函数在执行失败时,都会有一个错误码(0 1 2 3 4 5 6 7 8 9 ........),strerror会把错误码转成错误信息,其中errno会将错误码的错误信息的首字符地址返回来。

用法示例

字符分类函数:(如果不是要判断的字符,返回0,是的话返回非0)
函数           如果它的参数符合下列条件就返回真
iscntrl        任何控制字符
isspace     空白字符:空格‘ ’,换页‘\f’,换行'\n',回车'\r',制表符'\t',或垂直制表符'\v'
isdigit        十进制数字0~9
isxdigit      十六进制数字,包括所有十进制数字,小写字母a~f,大写字母A~F
islower      小写字母a~z
isupper     大写字母A~Z
isalpha     字母a~z或A~Z
isalnum    字母或数字a~z,A~Z或0~9
ispunct     标点符号,任何不属于数字或字母的图像字符(可打印符号)
isgraph     任何图像字符

字符转换:
int tolower ( int c );  //大写字符转小写字符
int toupper ( int c ); //小写字符转大写字符
 

3.2 内存函数

需包含头文件include<string.h>

 3.2.1 memcpy

void * memcpy ( void * destination , const void * source , size_t num );
  • 函数memcpysource的位置开始向后复制num个字节的数据到destination的内存位置
  • 这个函数在遇到 '\0' 的时候并不会停下来
  • 如果sourcedestination有任何的重叠,复制的结果都是未定义的

3.2.2 memmove

void * memmove ( void * destination , const void * source ,   size_t num );
  • memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的
  • 如果源空间和目标空间出现重叠,就得使用memmove函数处理

3.2.3 memcmp

int memcmp (   const void * ptr1 ,   const void * ptr2 ,   size_t num );
  • 比较从ptr1ptr2指针开始的num个字节

3.2.4 memset

是一个初始化函数,作用是逐字节地刷内存,将某一块内存中的全部字节设置为指定的值  0或 -1

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

  • s指向要填充的内存块
  • c是要被设置的值,不能用它将int数组出初始化为0和-1之外的其他值
  • n是要被设置该值的字节
  • 返回类型是一个指向存储区s的指针

四. 自定义类型

 4.1 结构体

链接:C语言初阶

4.2 枚举

枚举顾名思义就是一 一列举, 把可能的取值一 一列举。
  • 第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1
  • 没有指定值的枚举元素,其值为前一元素加1
 typedef enum
{
    Yield_Mode,                  ///0x00,第一个元素不赋值,默认为0
    Write_Sequence,              ///0x01
    Read_Sequence, 
    Read_Temperature = 0x10,     ///0x10,直接赋值
    Read_Humidity,               ///0x11,上一个元素值+1
    Read_Flash,
    Network_Button_On,
    Network_Button_Off,
    Shut_Dowm                    ///0x15
}SMK_Command_t;

SMK_Command_t SMK_Command = Yield_Mode;    //定义变量
void Test_FUN( SMK_Command_t mode )         //枚举作为参数
{
    if( mode == Yield_Mode)    xxx;
}
Test_FUN( &SMK_Command );

枚举定义举例 

enum Color//颜色
{
 RED=1,
 GREEN=4,
 BLUE            //这里BLUE的值是GREEN+1,即5
};

typedef enum weekday//星期
{
 Mon,
 Tues,
 Wed,
}weekday_t;        //定义类型enum weekday


int main()
{
    enum Color clr = GREEN;  //只能拿枚举常量给枚举变量赋值,才不会出现类型的差异
    weekday_t weekday = Mon; //根据类型定义变量weekday
}
typedef enum                     //定义一些命令,命令是什么意思不重要↓
{
	READ_ID            = 0xEFC8, // command: read ID register
	SOFT_RESET         = 0x805D, // soft reset
	SLEEP              = 0xB098, // sleep
	WAKEUP             = 0x3517, // wakeup
}etCommands;

static result_t SHTC3_WriteCmd(etCommands cmd) //可以限定形参的类型
{
    //
}

枚举的优点
  • 增加代码的可读性和可维护性
  • #define定义的标识符比较枚举有类型检查,更加严谨。
  • 防止了命名污染(封装)
  • 便于调试
  • 使用方便,一次可以定义多个常量

4.3 联合体

这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以也叫共用体)。
union UN
{
   char c;
   int i;
};

int main()
{
   union UN a;
   a.i = 1;
   if( a.c == 1 )
      printf("小端存储"); 
}

 联合体大小:

1.联合体的大小至少是最大成员的大小

2.联合体的大小,不一定总是最大成员的大小

3.当最大成员的大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍

#include <stdio.h>
 
union un
{
	char arr[5];  //5个字节
	int i;        //4个字节
};
 
int main()
{
	printf("%d\n", sizeof(union un));        //为8字节
	return 0;
}

五. 动态内存的管理

为什么存在动态内存分配

我们已经掌握的内存开辟方式有:
int val = 20;        //在栈空间上开辟四个字节
char arr[10] = {0};  //在栈空间上开辟10个字节的连续空间

 但是上述的开辟空间的方式有两个特点:

1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道, 那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了。

C/C++程序内存分配的几个区域:
1. 栈区( stack ):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
回地址等。
2. 堆区( heap ):一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收 。分
配方式类似于链表。
3. 数据段(静态区)( static )存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

5.1 malloc 和 free

void* malloc ( size_t size );     //向内存申请一块 连续可用 的空间,并返回指向这块空间的指针
头文件:#include <malloc.h> 或 #include <stdlib.h>。        size单位是字节
  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数 size 0malloc的行为是标准是未定义的,取决于编译器。
void free ( void* ptr );         // 用来做动态内存的释放和回收,ptr是指向动态开辟内存的指针
  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数 ptr NULL指针,则函数什么事都不做。
#include <stdio.h>    //需包含这个头文件
int main()
{
     //代码1
  int num = 0;
  scanf("%d", &num);
  int arr[num] = {0};
     //代码2
  int* ptr = NULL;
  ptr = (int*)malloc(num*sizeof(int));
  if(NULL != ptr)    //判断ptr指针是否为空
  {
     int i = 0;
     for(i=0; i<num; i++)
     {
        *(ptr+i) = 0;
     }
  }
  free(ptr);    //释放ptr所指向的动态内存
  ptr = NULL;   //是否有必要?
  return 0;
}

5.2 calloc

void* calloc ( size_t num , size_t size );
头文件:#include <malloc.h> 或 #include <stdlib.h>        num为元素个数, size为元素大小
  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0
#include <stdio.h>
#include <stdlib.h>
int main()
{
 int *p = (int*)calloc(10, sizeof(int));
 if(NULL != p)
 {
 //使用空间
 }
 free(p);
 p = NULL;
 return 0;
}

5.3 realloc

头文件:#include <malloc.h> 或 #include <stdlib.h> 

  • realloc函数的出现让动态内存管理更加灵活
  • 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小 的调整。
void* realloc ( void* ptr , size_t size );
头文件:#include <malloc.h> 或 #include <stdlib.h>          size为要重新开辟的总字节数
  • ptr 是将要改变的空间的起始地址
  • size 调整后的大小,单位byte
  • 返回值为调整后的内存的起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 的空间。
    • realloc在调整内存空间的是存在两种情况: 

                   情况1:原有空间之后有足够大的空间 

                   情况2:原有空间之后没有足够大的空间  

情况 1
当是情况 1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况 2
当是情况 2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
#include <stdio.h>
int main()
{
  int *ptr = (int*)malloc(100);      //先开辟一块空间使用
  if(ptr != NULL)
  {
     //业务处理
  }
  else
  {
     exit(EXIT_FAILURE);    
  }
                          //扩展容量
  //代码1
  ptr = (int*)realloc(ptr, 1000); //这样不行。如果(空间不够)扩容失败会返回空指针&&
                          //而直接将空指针赋给ptr,ptr会丢失之前开辟的那块空间地址
  //代码2
  int*p = NULL;
  p = realloc(ptr, 1000);
  if(p != NULL)
  {
     ptr = p;             //确保空间已经开辟好了再将ptr指向扩展的这块空间
  }
  //业务处理
  free(ptr);
  return 0;
}

5.4 常见的动态内存错误

  • 对NULL指针的解引用操作
  • 对动态开辟空间的越界访问
  • 对非动态开辟内存使用free释放
  • 使用free释放一块动态开辟内存的一部分
  • 对同一块动态内存多次释放
  • 动态开辟内存忘记释放(内存泄漏)

5.5 柔性数组

C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
typedef struct st_type
{
  int i;
  int a[];//柔性数组成员
}type_a;
柔性数组的特点:
  • 结构中的柔性数组成员前面必须至少一个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
柔性数组的使用:
//代码1
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
//业务处理
p->i = 100;
for(i=0; i<100; i++)
{
 p->a[i] = i;
}
free(p);

优势:

  • 方便内存释放
  • 这样有利于访问速度

六. 文件操作

七. 程序的编译


 

八. 补充

8.1 字符串数组、传参

传送门:C语言零碎知识点之字符串数组

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值