# 字符型数据


类型 字节数 取值范围
signed char 1 -128 ~ 127
unsigned char 1 0 ~ 255

注意:

  • c99 把字符型数据作为整型类型的一种。
  • 在使用有符号字符型变量时,允许储存的值为负数,但是字符的代码不可能是负值,所以在存储字符时只用到了 0-127 这一部分。
#include <stdio.h>
#include <iostream>
int main()
{
    signed char sc;
    unsigned char uc;
    //在 vc++ 中,char类型的变量是 signed char 类型的,在其他的编译器中 char 类型的变量可能是 unsigned char 类型的
    char ch;
    printf("%d, %d, %d\n", sizeof(sc), sizeof(uc), sizeof(ch));

    uc = -1;
    ch = -1;
    printf("%d, %d\n", ch, uc); // -1, 255
    /*
        由于 %d 是 int 类型,相当于
        int w;
        printf("%d, %d\n", w = ch, w = uc);
    */
    system("pause");
    return 0;
}

由于 1-1int 类型的数据,占 44 个字节,3232bitbit

  • 1-1 对应的补码:11124118\underbrace{11\cdots1}_{24}\,\underbrace{1\cdots1}_{8}

对于 unsigned char uc = -1; int w = uc

  • 1-1 赋值给 ucuc 时候,由于 char 只占一个字节,且 1-1int 类型

    ucuc 对应的补码:118\underbrace{1\cdots1}_{8},舍弃其余高的 2424

  • ucuc 赋值给 ww 时候,将 unsigned char 转化为 int 类型的数据

    首先 ucuc8bit8\,bit 全部复制到 ww 的低 8bit8\,bit 上。

    由于 ucuc 是无有符号的 char 类型数据,那么 ww 的高 2424 位的二进制上全部填写 00

    w:0024118w:\underbrace{0\cdots0}_{24}\,\underbrace{1\cdots1}_{8}

    由于采用补码存储,最终转换为原码输出:w=255w = 255

对于 char ch = -1; int w = ch

  • 1-1 赋值给 chch 时候,由于 char 只占一个字节,且 1-1int 类型

    ucuc 对应的补码:118\underbrace{1\cdots1}_{8},舍弃其余高的 2424

  • chch 赋值给 ww 时候,将 char 转化为 int 类型的数据

    首先 chch8bit8\,bit 全部复制到 ww 的低 8bit8\,bit 上。

    由于 chch 是有符号的 charchar 类型数据,并且最高位为 11,那么 ww 的高 2424 位的二进制上全部填写 11

    w:11124118w:\underbrace{11\cdots1}_{24}\,\underbrace{1\cdots1}_{8}

    由于采用补码存储,最终转换为原码输出:w=1w = -1

# 数组

# 数组作为函数参数

#include <stdio.h>
#include <iostream>
using namespace std;
//1. 形参是数组的形式,传入的是首元素的地址
//  - 地址是应该使用指针来接收
//    所以 arr[] 这里看似是数组,本质是指针变量
//	  其实z arr[] = *p
void bubbleSort(int arr[])
{
    //这里 arr 是指针变量,在 64 位中大小为 8 bit
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n - i; j++) {
            if (arr[j] < arr[j - 1]) {
                int t = arr[j];
                arr[j] = arr[j - 1];
                arr[j - 1] = t;
            }
        }
    }
}
int main()
{
    int arr[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    //数组名本质是:数组首元素的地址
    bubbleSort(arr);
    for (int i = 0; i < n; i++) {
        cout << arr[i] << " ";
    }
    system("pause");
    return 0;
}

上述的解决方式

#include <stdio.h>
#include <iostream>
using namespace std;
//1. 形参是数组的形式,传入的是首元素的地址
//  - 地址是应该使用指针来接收
//    所以 arr 这里看似是数组,本质是指针变量
void bubbleSort(int arr[], int n)
{
    // 这里 arr 是指针变量,在 64 位中大小为 8 字节
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n - i; j++) {
            if (arr[j] < arr[j - 1]) {
                int t = arr[j];
                arr[j] = arr[j - 1];
                arr[j - 1] = t;
            }
        }
    }
}
int main()
{
    int arr[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    // 数组名本质是:数组首元素的地址
    int n = sizeof(arr) / sizeof(arr[0]);
    bubbleSort(arr, n);
    for (int i = 0; i < n; i++) {
        cout << arr[i] << " ";
    }
    system("pause");
    return 0;
}

# 数组名

# 一维数组的数组名

#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
    int arr[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    printf("%p\n", arr);     // 000000000061FDF0
    printf("%p\n", &arr[0]); // 000000000061FDF0
    int n = sizeof(arr);
    printf("%d\n", n); // 40
    // 既然说 arr 为数组首元素地址
    //  为什么 sizeof (arr) 为 40? 而不是 4 或者 8
    system("pause");
    return 0;
}

数组名确实能表示首元素地址,但是有 22 个例外

  1. sizeof(数组名) ,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节

  2. &数组名 ,这里的数组名表示整个数组,取出的是整个数组的地址

    #include <stdio.h>
    #include <iostream>
    using namespace std;
    
    int main()
    {
       int arr[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
       printf("%p\n", arr);         // 00D5F940
       printf("%p\n", arr + 1);     // 00D5F944
    
       printf("%p\n", &arr[0]);     // 00D5F940
       printf("%p\n", &arr[0] + 1); // 00D5F944
    
       printf("%p\n", &arr);        // 00D5F940
       printf("%p\n", &arr + 1);    // 00D5F968
    
       system("pause");
       return 0;
    }
    

    可以发现, &数组名 + 1 为数组增长了 0x2816=2×161+8×160=400x28_{16} = 2\times 16^1 + 8\times 16^0 = 40

    即为数组的大小

#include <stdio.h>
#include <iostream>
using namespace std;
// 1. 形参是指针的形式,传入的是首元素的地址
//   - 地址是应该使用指针来接收
void bubbleSort(int *arr, int n)
{
    // 这里 arr 是指针变量,在 64 位中大小为 8 字节
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n - i; j++)
        {
            if (arr[j] < arr[j - 1])
            {
                int t = arr[j];
                arr[j] = arr[j - 1];
                arr[j - 1] = t;
            }
        }
    }
}
int main()
{
    int arr[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    // 数组名本质是:数组首元素的地址
    int n = sizeof(arr) / sizeof(arr[0]);
    bubbleSort(arr, n);
    for (int i = 0; i < n; i++)
    {
        cout << arr[i] << " ";
    }
    system("pause");
    return 0;
}

# 二维数组的数组名

#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
    int arr[3][4];
    int n = sizeof(arr);
    printf("%d\n", n); // 48
    arr; // 二维数组的数组名也表示数组首元素的地址
    //arr 表示是的第一行的地址(一维数组的地址)
    //arr + 1 表示是的第二行的地址(一维数组的地址)
    printf("%p\n", arr);         // 000000000061FDE0
    printf("%p\n", &arr[0] + 1); // 000000000061FDF0,比上述增长了 16 个字节
    // 行数 = 总数组大小 / 第一行数组大小
    int rs = sizeof(arr) / sizeof(arr[0]); // 48 / 16;
    printf("行数:%d\n", rs);
    // 列数 = 第一行数组大小 / 第一个数组元素大小
    int cs = sizeof(arr[0]) / sizeof(arr[0][0]); // 16 / 4;
    printf("列数:%d\n", cs);
    system("pause");
    return 0;
}

类似的,若 arr 是二维数组,那么 &arr 就是整个二维数组的地址

# 指针

# 指针

指针就是内存中一个最小单元的编号,也就是地址

通常指的是指针变量,用来存放内存地址的变量

即:指针就是地址,口语中说的指针通常指的是指针变量

指针变量:存放地址

那我们就可以这样理解:

把内存单元的编号就称为地址(指针)

指针其实就是地址,地址就是编号

指针就是内存单元的编号

img

指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。

那这里的问题是:

  • 一个小的单元到底是多大?(1 个字节)
  • 如何编址?

在 32 位的机器上,地址是 32 个 0 或者 1 组成二进制序列,那地址就得用 4 个字节的空间来存储,所以 一个指针变量的大小就应该是 4 个字节。

那如果在 64 位机器上,如果有 64 个地址线,那一个指针变量的大小是 8 个字节,才能存放一个地 址。

总结:

  • 指针变量是用来存放地址的,地址是唯一标示一块地址空间的。

  • 指针的大小在 32 位平台是 4 个字节,在 64 位平台是 8 个字节


#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
    char *pc = NULL;
    short *ps = NULL;
    int *pi = NULL;
    double *pd = NULL;
    //ptr_t pt = NULL; 不采用统一的类型
    //sizeof 返回的值的类型是无符号整型
    printf("%zu\n", sizeof(pc)); // 给 sizeof () 的返回值打印
    printf("%zu\n", sizeof(ps));
    printf("%zu\n", sizeof(pi));
    printf("%zu\n", sizeof(pd));
    system("pause");
    return 0;
}

int *pa;pa 是指针, pa 指向的对象是 int 类型

# 指针类型

# 意义一

#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
    // 0001 0001 0010 0010 ...
    // 第一个字节:44
    // 第二个字节:33
    // 第三个字节:22
    // 第四个字节:11
    int a = 0x11223344;
    int *pa = &a;
    // 找到 a 的地址,将 a 的值改为 0
    // 第一个字节:00
    // 第二个字节:00
    // 第三个字节:00
    // 第四个字节:00u
    *pa = 0;

    system("pause");
    return 0;
}

img

#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
    int a = 0x11223344;
    // int *pa = &a;
    // *pa = 0;
    // 由于 &a 取出的是 a 的地址,指针变量照样存储
    char *pc = (char *)&a; // int *
    *pc = 0;
    system("pause");
    return 0;
}

img

指针类型决定了指针在被解引用操作访问几个字节

  • int * 类型解引用访问 44 个字节
  • char * 类型解引用访问 11 个字节
  • ......

相同长度的类型也不能混用

# 意义二

#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
    int a = 0x11223344;
    int *pa = &a;
    char *pc = (char *)&a;

    printf("pa = %p\n", pa); // 000000000061FE0C
    printf("pa + 1 = %p\n", pa + 1); // 000000000061FE10

    printf("pc = %p\n", pc); // 000000000061FE0C
    printf("pc + 1 = %p\n", pc + 1); // 000000000061FE0D
    // 十进制 100 = 十六进制 0x64

    system("pause");
    return 0;
}

img

发现 pa 增加了 44 个字节, pc 增加了一个字节


** 结论:** 指针的类型决定了 +- 1 的操作,跳过了几个字节

决定了指针的步长

img

# 类型不能通用

#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
    int a = 0;
    int *pi = &a;            // pi 解引用访问 4 个字节, pi + 1 也是跳过 4 个字节
    float *pf = (float *)&a; // pf 解引用访问 4 个字节, pf + 1 也是跳过 4 个字节
    // int * 和 float * 是不是可以通用?
    //   不能!
    *pi = 100;

    system("pause");
    return 0;
}

img

#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
    int a = 0;
    int *pi = &a;            // pi 解引用访问 4 个字节, pi + 1 也是跳过 4 个字节
    float *pf = (float *)&a; // pf 解引用访问 4 个字节, pf + 1 也是跳过 4 个字节
    // int * 和 float * 是不是可以通用?
    //   不能!
    *pf = 100.0;//浮点数是分数,存放的方式与整数不同

    system("pause");
    return 0;
}

img

# 野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

# 指针为初始化

#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
    int *p; // p 没有初始化,就意味着没有明确的指向
    // 一个局部变量不初始化的化,默认是随机值: 0xcccccccc
    //  - 一个非法的地址
    *p = 10; // 非法访问内存,这里的 p 就是野指针

    system("pause");
    return 0;
}

# 指针越界

#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
    int arr[10] = {0};
    int *p = arr; // arr:首元素地址, &arr[0]
    for (int i = 0; i <= 10; i++)
    {
        // 当指针指向的范围超出数组 arr 的范围时,p 就是野指针
        *p = i;
        p++;
    }
    system("pause");
    return 0;
}

# 指针指向的空间释放

#include <stdio.h>
#include <iostream>
using namespace std;
int *test()
{
    // 由于 a 是局部变量,调用完 test () 函数后,a 会销毁
    //   - a 的空间释放还给操作系统,不属于当前程序了
    int a = 10;
    // 返回的是 a 的地址
    return &a;
}
int main()
{
    // 此时指针变量 p 获取的是 a 释放后的地址
    //	此时 p 属于野指针
    int *p = test();
    //a 释放后的该地址没有被覆盖
    printf("%d\n", *p);// 10
    
    // 原先 a 释放后的地址可能会被其他数据覆盖
    printf("ffff");//a 释放后的该地址可能被覆盖
    printf("%d\n", *p);// 5, 6, 7 等等
	
    system("pause");
    return 0;
}

类似于,女朋友换电话了,你打她之前的电话,发现是别人

# 避免野指针

  1. 指针初始化
  2. 小心指针越界
  3. 指针指向空间释放及时置 NULL
  4. 避免返回局部变量的地址
  5. 指针使用之前检查有效性
#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    int a = 10;
    int *p = &a;

    // NULL -> 0
    int *p2 = NULL;
    /*
    	// 写入访问权限冲突,0 地址空间不能访问
	    *p2 = 100;
    */
    if (p2 != NULL) { // if (p3)
        *p2 = 100;// ok
    }
    cout << p2;


    system("pause");
    return 0;
}

# 指针运算

# 指针 + 正数(访问数组)

arr[i] = *(arr + i) 等价

#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
#define N 5
    float arr[N];
    float *p;
    // 虽然 &arr[N] 不属于我,但是可以看他
    for (p = &arr[0]; p < &arr[N];)
    {
        *p++ = 0;
        //*p = 0;
        // p++;
        cout << *(p - 1) << " "; // 0 0 0 0 0
    }
    system("pause");
    return 0;
}

img

注意:

*p++ -> *p; p++

(*p)++


#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    // -128 ~ 127
    char arr[5];
    int n = sizeof(arr) / sizeof(arr[0]);
    /*
        char *p = arr;
        //用指针访问
        for (int i = 0; i < n; i++, p++) {
            *p = 1;
            printf("%d ", arr[i]); // 1 1 1 1 1
        }
    */
    char *p = arr;
    // 用指针访问
    for (int i = 0; i < n; i++)
    {
        *(p + i) = 1;
        printf("%d ", arr[i]); // 1 1 1 1 1
    }

    system("pause");
    return 0;
}

# 指针 - 指针

#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    int arr[10];
    printf("%d\n", &arr[9] - &arr[0]); // 9
    printf("%d\n", &arr[0] - &arr[9]); // -9

    system("pause");
    return 0;
}

img

| 指针 - 指针 |:得到的指针和指针之间元素的个数

注意:不是所有的指针都能相减

  • 指向同一块空间的 2 个指针才能相减,才有意义

# 统计字符串长度

#include <stdio.h>
#include <string.h>
#include <iostream>
using namespace std;

int myStrlen(char *str);

int main()
{
    int len = strlen("abcdef");
    printf("%d\n", len); // 6

    printf("%d\n", myStrlen("abcdef")); // 6

    system("pause");
    return 0;
}

/*
    str
    ⬇
    a b c d e f \0

    "abcdef" 传入 a 的地址
    str 遇到 \0 不统计
*/
int myStrlen(char *str)
{
    int c = 0;
    while (*str != '\0')
    {
        c++;
        str++;
    }
    return c;
}

方式二:采用指针 - 指针

获取首尾地址。最后用地址相减

/*
    str
    ⬇
    a b c d e f \0

    "abcdef" 传入 a 的地址
    str 遇到 \0 不统计
*/
int myStrlen(char *str)
{
    char *start = str;
    while (*str != '\0')
    {
        str++;
    }
    return str - start;
}

指针 + 指针?地址 + 地址?无意义

# 指针的关系运算

for (int *p = arr[10]; p >= &arr[0];)
{
    *--p = 0;
    // p--;
    //*p = 0;
}

代码简化

for (int *p = arr[10]; p >= &arr[0]; p--)
{
    *p = 0;
}

实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

即允许 p1p2 比,但是不允许 p2p3

img

# 指针与数组

数组:一组相同类型元素的集合

指针变量:是一个变量,存放的是地址

#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    int arr[10];
    // arr 是首元素的地址
    // &arr[0]

    int *p = arr;
    // 通过指针访问数组
    int n = sizeof(arr) / sizeof(&arr[0]);
    for (int i = 0; i < n; i++)
    {
        printf("%p ----- %p ----- %p\n", &arr[i], p + i, arr + i);
    }
    system("pause");
    return 0;
}

sdtout

  • 000000000061FDE0 ----- 000000000061FDE0 ----- 000000000061FDE0
    000000000061FDE4 ----- 000000000061FDE4 ----- 000000000061FDE4
    000000000061FDE8 ----- 000000000061FDE8 ----- 000000000061FDE8
    000000000061FDEC ----- 000000000061FDEC ----- 000000000061FDEC
    000000000061FDF0 ----- 000000000061FDF0 ----- 000000000061FDF0
    

img

# 二级指针

一级指针

#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    int a = 10;
    int *pa = &a; // pa 是一个「指针变量」,一级指针变量
    *pa = 20;
    printf("%d\n", a); // 20

    system("pause");
    return 0;
}

二级指针

#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    int a = 10;
    int *pa = &a; // pa 是一个「指针变量」,一级指针变量

    // pa 指向 a 的地址 0x0012ff40
    // ppa 指向 pa 的地址 0x0018ff32
    int **ppa = &pa; // ppa 是一个「二级指针变量」
    **ppa = 20;
    
    printf("%d\n", a); // 20

    system("pause");
    return 0;
}

img

二级指针是用来存放一级指针变量的地址

# 指针数组

存放指针的数组就是指针数组

#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    int a = 10;
    int b = 20;
    int c = 30;

    int arr[10];

    int *pa = &a;
    int *pb = &b;
    int *pc = &c;

    // 多个会比较麻烦,指针数组
    int *parr[10] = {&a, &b, &c}; // 每个元素都是指针。数组指针,每个对象类型为 int 类型

    system("pause");
    return 0;
}

#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    int a = 10;
    int b = 20;
    int c = 30;
    // 指针数组
    int *parr[10] = {&a, &b, &c}; // 每个元素都是指针。数组指针,每个对象类型为 int 类型
    int n = sizeof(parr) / sizeof(parr[0]);
    for (int i = 0; i < 3; i++)
    {
        printf("%d ", *(parr[i]));
    }

    system("pause");
    return 0;
}

image-20230708204850658


image-20230708210128604

指针打印二维数组

arr[i] = *(arr + i) 等价

#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    // int arr[3][4] = {1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6};
    // 1 2 3 4
    // 2 3 4 5
    // 3 4 5 6

    int arr1[4] = {1, 2, 3, 4};
    int arr2[4] = {2, 3, 4, 5};
    int arr3[4] = {3, 4, 5, 6};

    int *parr[3] = {arr1, arr2, arr3}; // arr1 数组名代表数组首地址

    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 4; j++)
        {
            // *(p + i) --> p[i]
            printf("%d ", parr[i][j]);
            //printf("%d ", *(parr[i] + j));
            //printf("%d ", *(*(parr + i) + j));
        }
        printf("\n");
    }

    system("pause");
    return 0;
}

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

# 字符指针(const)

#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    char ch = 'a';
    char *pc = &ch;
    *pc = 'b';
    printf("%c\n", ch);


    system("pause");
    return 0;
}

#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{

    // 赋值给 p 的是字符串的首字符的地址
    //         ↓
    //         abcdef\0
    char *p = "abcdef"; // 把字符串首字符 a 的地址赋值给了 p
    //char arr[] = "abcdef";
    
    // %s 将地址及后面的地址所在字符全部打印出来,遇到 '\0' 截止
    //	- printf 的功能
    printf("%s\n", p); // abcdef

    system("pause");
    return 0;
}

"abcdef"常量字符串

char *p = "abcdef"; // 把字符串首字符 a 的地址赋值给了 p
*p = 'w'; // 报错,写入访问权限冲突

需要加入 const

const char *p = "abcdef"; // 有效的保护字符串

#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{

    const char *p1 = "abcdef";
    const char *p2 = "abcdef";

    // arr 数组独立空间
    char arr1[] = "abcdef";
    char arr2[] = "abcdef";

    if (p1 == p2)
        printf("p1 == p2\n"); // this
    else
        printf("p1 != p2\n");

    if (arr1 == arr2)
        printf("arr1 == arr2\n");
    else
        printf("arr1 != arr2\n"); // this

    system("pause");
    return 0;
}

image-20230710221226758


# 数组指针

指针数组:是用来存放指针的数组

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

// p1 是指针数组, p1 先更 [] 结合, [] 的优先级高于 *
// 	[int *, int *, int *, ...]
int *p1[10]; 

// p2 是数组指针, p2 先更 * 结合,p2 可以指向一个数组, 其中每个元素是 int 类型
//	[int, int, int, ...]
int (*p2)[10]; 

# 数组名

数组名确实能表示首元素地址,但是有 22 个例外

  1. sizeof(数组名) ,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
  2. &数组名 ,这里的数组名表示整个数组,取出的是整个数组的地址
#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    int arr[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    printf("%p\n", arr);         // 00D5F940
    printf("%p\n", arr + 1);     // 00D5F944

    printf("%p\n", &arr[0]);     // 00D5F940
    printf("%p\n", &arr[0] + 1); // 00D5F944

    printf("%p\n", &arr);        // 00D5F940
    printf("%p\n", &arr + 1);    // 00D5F968

    system("pause");
    return 0;
}

# 数组指针

#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    int arr[10] = {};
    //int *p = &arr; //有警告的
    
    // 整型指针用来存放整型的指针
    // 字符指针用来存放字符的指针
    // 数组指针用来存放数组的指针
    int (*p2)[10] = &arr;// p2 是数组指针, p2 先更 * 结合,p2 指向数组, 其中每个元素是 int 类型
    
    system("pause");
    return 0;
}

数组指针用来存放数组的指针

  • int (*p2)[10] = &arr

p2 的类型: int (*)[10]

数组指针的类型。

这个指针指向数组,10 个元素,每个元素是 int 类型


#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    char *arr[5] = {};
    char *(*pc)[5] = &arr;

    system("pause");
    return 0;
}

注意:若 int (*p)[] = &arr ,那么默认为 0 个元素: int (*p)[0]


# 数组指针遍历数组

#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int(*p)[10] = &arr; // p2 是数组指针, p2 先更 * 结合,p2 指向数组, 其中每个元素是 int 类型

    int n = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i < n; i++)
    {
        // p 是指向数组,存放数组的地址
        // *p:对 p 解引用,即找到整个数组。
        //  - 相当于数组名 arr,数组名又是数组首元素的地址
        //  - 就是:*p == arr
        //  - *p 本质上是数组首元素的地址

        // *(*p + i):获取每个元素的地址,然后解引用
        printf("%d ", *(*p + i));
    }

    printf("%d\n", *p == arr); // 1

    system("pause");
    return 0;
}

上面的写法别扭,最好用其余常用的方法。

#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int *p = arr;
    int n = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i < n; i++)
    {
        printf("%d ", *p + i);
    }


    system("pause");
    return 0;
}

# 数组指针的常见用法(二维数组)

/*
    数组名 arr 首元素地址
    二维数组的首元素是第一行的地址:一维数组的地址
    p 数组指针,存放数组的地址
    *p:对 p 解引用,即找到整个数组。
    - 相当于数组名 arr,数组名又是数组首元素的地址
    - 就是:*p == arr
    - *p 本质上是数组首元素的地址
    访问每一行 p + 1
*/
void print2(int (*p)[5], int r, int c)
{
    for (int i = 0; i < r; i++)
    {
        // *(p + i) 相当于获取第 i 行的「数组名」
        printf("%d\n", sizeof(*(p + i))); // 20,这是特例
        int *p1 = *(p + i);
        printf("%d\n", sizeof(p1)); // 8(指针大小)
    }
}

image-20230711225509361

#include <stdio.h>
#include <iostream>
using namespace std;

void print1(int arr[3][5], int r, int c)
{
    for (int i = 0; i < r; i++)
    {
        for (int j = 0; j < c; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

// 数组名 arr 首元素地址
// 二维数组的首元素是第一行的地址:一维数组的地址
// p 为数组指针。访问每一行 p + 1
void print2(int (*p)[5], int r, int c)
{
    for (int i = 0; i < r; i++)
    {
        for (int j = 0; j < c; j++)
        {
            // *(p + i):获取每一行的地址,相当于数组名
            // *(p + i) + j:获取 i 行第 j 列的地址
            printf("%d ", *(*(p + i) + j));
        }
        printf("\n");
    }
}

int main()
{

    int arr[3][5] = {1, 2, 3, 4, 5, 2, 3, 4, 5, 6, 3, 4, 5, 6, 7};
    int n = sizeof(arr) / sizeof(arr[0]);
    int m = sizeof(arr[0], arr[0][0]);
    // arr 首元素地址
    print2(arr, n, m);

    system("pause");
    return 0;
}

p + 1 为什么跳过一行?

  • int (*p)[5];

    p 的类型: int (*)[10]

    p 指向一个整型数组,5 个元素,每个元素是 int 类型

    p + 1 :跳过一个 5 个 int 元素的数组

# 数组指针数组

int arr[5];      //arr 是整型数组
    int *parr1[10];  //parr1 是数组,每一个元素是 int * 类型,整型指针数组
    int (*parr2)[10]; //parr2 是数组指针
    /*
        parr3 先更 [10] 结合,是一个数组,10 个元素,其中每个元素是 int (*)[5] 类型
        
        首先移除 parr3 [10], 剩余 int (*)[5] 是数组指针类型
        而 parr3 [10] 又是数组,
        那么整体: parr3 是存放数组指针的数组
            - 数组 10 个元素,每个元素是数组指针
    */
    int (*parr3[10])[5];

image-20230711233632878

# 数组参数、指针参数

# 一维数组传参

#include <stdio.h>
void test(int arr[])//ok
{}
void test(int arr[10])//ok
{}
void test(int *arr)//ok
{}
void test2(int *arr[])//ok
{}
void test2(int *arr[20])//ok
{}
// 二级指针是指向一级指针的地址
void test2(int **arr)//ok
{}
int main()
{
    int arr[10] = {0};
    int *arr2[20] = {0};
    test(arr);
    test2(arr2);
}

# 二维数组传参

void test(int arr[3][5])//ok
{}
void test(int arr[][])//err
{}
void test(int arr[][5])//ok
{}
// 总结:二维数组传参,函数形参的设计只能省略第一个 [] 的数字。
// 因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
// 这样才方便运算。
//arr 是指针,指向的是 int 类型
void test(int *arr)//err:一维数组的地址不能放入 int 类型
{}
void test(int* arr[5])//err:arr 先更 [5] 结合,是数组,每个元素是 int * 类型
{}
//p2 是数组指针,p2 先更 * 结合,p2 指向数组,其中每个元素是 int 类型
void test(int (*arr)[5])//ok
{}
// 二级指针是指向一级指针的地址
void test(int **arr)//err
{}
int main()
{
    int arr[3][5] = {0};
    // 二维数组的数组名,表示首元素的地址,其实是第一行的地址
    //	- 第一行是一个一维数组
    test(arr);
}

# 一级指针传参

#include <stdio.h>
void print(int *p, int sz)
{
    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]);
    // 一级指针 p,传给函数
    print(p, sz);
    return 0;
}

反过来思考:

void test1(int *p)
{}
//test1 函数能接收什么参数?
/*
	int a = 10;
	int *ptr = &a;
	test1(&a);
	test1(ptr);
	
	int arr[10];
	test1(arr);
*/

# 二级指针传参

#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(char **p)
{}
int main()
{
    int *p1;
    test(&p1);
    
    int **p2;
    test(p2);
    
    // 指针数组
    int *arr[10];
    test(arr);
}

# 函数指针

# 概念

数组指针:指向数组的指针就是数组指针

函数指针:指向函数的指针就是函数指针

#include <stdio.h>
#include <iostream>
using namespace std;

int add(int x, int y)
{
    return x + y;
}
int main()
{
    int arr[5] = {};
    //&arr - 取出的数组的地址
    int (*p)[5] = &arr; // 数组指针

    //&函数名 - 取出的就是函数的地址呢?
    printf("%p\n", &add); // 0000000000401550
    printf("%p\n", add);  // 0000000000401550
    //对于函数来说,&函数名和函数名都是函数的地址,没有什么特殊的

    system("pause");
    return 0;
}

#include <stdio.h>
#include <iostream>
using namespace std;

int add(int x, int y)
{
    return x + y;
}
int main()
{
    int arr[5] = {};
    //&arr - 取出的数组的地址
    int(*p)[5] = &arr; // 数组指针

    // pf 是指针,指向的是函数 (),参数是 int, int,函数的返回类型是 int
    //  - 发现更数组指针写法一样
    // pf 的类型是 int (*)(int, int)
    int (*pf)(int, int) = &add;

    system("pause");
    return 0;
}

# 调用函数

#include <stdio.h>
#include <iostream>
using namespace std;

int add(int x, int y)
{
    return x + y;
}
int main()
{
    /*
        int a = 10;
        int *pa = &a;
        *pa = 20;
        printf("%d\n", pa);
    */
    //int (*pf)(int, int) = &add;
    int (*pf)(int, int) = add;
    
    // 先解引用找到函数 (*pf),然后传参并调用函数
    int ans = (*pf)(2, 3);
    // 可以直接用,此时 * 是个摆设
    // 由于 add() 函数的地址 add 给了 pf,由于 add(2, 3) 可以,那么 pf(2, 3) 也应该可以
    // 此时 pf 与 add 等价
    int ans1 = pf(2, 3); // 5
    
    printf("%d\n", ans); // 5

    system("pause");
    return 0;
}

# 形参传入函数地址

#include <stdio.h>
#include <iostream>
using namespace std;

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

// pf 获取到函数地址
void calc(int (*pf)(int, int))
{
    int a = 3;
    int b = 5;
    int ans = pf(a, b);
    printf("%d", ans); // 8
}

int main()
{
    // 将函数地址传入 calc 函数
    calc(add);

    system("pause");
    return 0;
}

# 二者结合

#include <stdio.h>
#include <iostream>
using namespace std;

int test(const char *s)
{
    return 0;
}

// pf 获取到函数地址
void test2(int (*pf)(int, int))
{
    int a = 3;
    int b = 5;
    int ans = pf(a, b);
    printf("%d\n", ans); // 8
}

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

int main()
{
    int (*pf)(const char *) = test;

    void (*pf2)(int (*)(int, int)) = test2;

    //调用 test2 函数
    // (*pf2)(add);
    pf2(add);

    system("pause");
    return 0;
}

# 练习

( *( void (*)() )0 )();
/*
	void (*p)();
		p 是函数指针
	t = void (*)() 是函数指针类型
	(t) 0 强制类型转换:0 强制转换为函数指针类型
	说明 0 是一个函数的地址 a
	*a 解引用,找到函数 f,然后 f () 调用函数
	-----------------------------------
	上述为一次函数调用,调用的是 0 作为地址处的函数。
	1. 把 0 强制类型转化为:无参,返回类型是 void 的函数的地址
	2. 调用 0 地址处的函数
*/

void ( *signal( int, void (*)(int) ) )(int);
/*
	signal 先与 (...) 结合,所以是函数名
	signal ( int, void (*)(int) ) 是一次函数的声明
		- int add (int x, int y){}
		  int add (int, int); // 函数声明
	剩余 void (* )(int); 为函数指针类型
	------------
	声明的 signal 函数的第一个参数的类型是 int, 第二个参数的类型是函数指针(该函数指针指向的函数参数是 int,返回类型是 void),signal 函数的 == 返回类型也是一个函数指针 ==。整体也是一次函数声明
*/
---------------------
    
typedef void (*pf_t)(int); // 把 void (*)(int) 类型重命名为 pf_t
int main() {
    // 简化 void (*signal ( int, void (*)(int) ) )(int);
    pf_t signal(int, pf_t);
}

# 函数指针的用途(回调函数)(根据不同的需求调用不同函数)

#include <stdio.h>
#include <iostream>
using namespace std;

// 写一个计算器
// 加法、减法、乘法、除法
void menu()
{
    printf("*********************************\n");
    printf("******* 1. add    2. sub ********\n");
    printf("******* 3. mul    2. div ********\n");
    printf("******* 0. exit          ********\n");
    printf("*********************************\n");
}

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

int sub(int x, int y)
{
    return x - y;
}

int mul(int x, int y)
{
    return x * y;
}

int div1(int x, int y)
{
    return x / y;
}

int main()
{

    int input = 1;
    int x = 0;
    int y = 0;
    int ans = 0;
    do
    {
        menu();
        printf("请选择:>");
        scanf("%d", &input);

        switch (input)
        {
        case 1:
            printf("\n请输入操作数");
            scanf("%d %d", &x, &y);
            ans = add(x, y);
            printf("%d\n", ans);
            break;
        case 2:
            printf("\n请输入操作数");
            scanf("%d %d", &x, &y);
            ans = sub(x, y);
            printf("%d\n", ans);
            break;
        case 3:
            printf("\n请输入操作数");
            scanf("%d %d", &x, &y);
            ans = mul(x, y);
            printf("%d\n", ans);
            break;
        case 4:
            printf("\n请输入操作数");
            scanf("%d %d", &x, &y);
            ans = div1(x, y);
            printf("%d\n", ans);
            break;
        case 0:
            printf("退出计算机\n");
            break;
        default:
            printf("选择错误\n");
            break;
        }

    } while (input);

    system("pause");
    return 0;
}

发现代码冗余,将 case 中函数封装

...

// 回调函数
void calc(int (*pf)(int, int))
{
    int x = 0;
    int y = 0;
    int ans = 0;
    printf("\n请输入操作数: ");
    scanf("%d %d", &x, &y);
    ans = pf(x, y);
    printf("%d\n", ans);
}

int main()
{

    int input = 1;
    do
    {
        menu();
        printf("请选择:>");
        scanf("%d", &input);

        switch (input)
        {
        case 1:
            // 将目的函数地址作为形参传入 calc 函数
            calc(add);
            break;
        case 2:
            calc(sub);
            break;
        case 3:
            calc(mul);
            break;
        case 4:
            calc(div1);
            break;
        case 0:
            printf("退出计算机\n");
            break;
        default:
            printf("选择错误\n");
            break;
        }

    } while (input);

    system("pause");
    return 0;
}

# 函数指针数组(转移表)

指针数组: int *arr1[5]; char *arr2[5];

函数指针也是指针

把函数和指针放在数组中,就是函数指针数组

#include <stdio.h>
#include <iostream>
using namespace std;

...
int main()
{

    // pf 是函数指针
    int (*pf)(int, int) = add;

    // arr 先更 [4] 结合,是一个数组,4 个元素,其中每个元素是函数指针类型:int (*)(int, int)
    int (*arr[4])(int, int){add, sub, mul, div1};

    for (int i = 0; i < 4; i++)
    {
        // (* arr[i])(4, 8);
        int ans = arr[i](8, 4);
        
        printf("%d ", ans); // 12 4 32 2
    }

    system("pause");
    return 0;
}

# 函数指针数组的用途(根据不同的需求调用不同函数)

#include <stdio.h>
#include <iostream>
using namespace std;

// 写一个计算器
// 加法、减法、乘法、除法
void menu()
{
    printf("*********************************\n");
    printf("******* 1. add    2. sub ********\n");
    printf("******* 3. mul    2. div ********\n");
    printf("******* 0. exit          ********\n");
    printf("*********************************\n");
}

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

int sub(int x, int y)
{
    return x - y;
}

int mul(int x, int y)
{
    return x * y;
}

int div1(int x, int y)
{
    return x / y;
}

// 回调函数
void calc(int (*pf)(int, int))
{
    int x = 0;
    int y = 0;
    int ans = 0;
    printf("\n请输入操作数: ");
    scanf("%d %d", &x, &y);
    ans = pf(x, y);
    printf("%d\n", ans);
}

int main()
{

    int input = 1;
    // 函数指针数组
    // 转移表
    int (*arr[])(int, int) = {0, add, sub, mul, div1};
    cout << arr[0];
    do
    {
        menu();
        printf("请选择:>");
        scanf("%d", &input);
        if (input >= 1 && input <= 4)
        {
            // 函数指针,获取函数的地址
            int (*pf)(int, int) = arr[input];
            calc(pf);
        }

    } while (input);

    system("pause");
    return 0;
}

# 指向函数指针数组的指针

int main()
{

    int input = 1;
    // 函数指针数组
    int (*pfarr[])(int, int) = {0, add, sub, mul, div1};

    // 指向「函数指针数组」的指针
    //  ppfarr 首先更 * 结合是一个指针,再更 [5] 结合,指向一个数组,5 个元素
    //      - 还剩余 int (*)(int, int)。
    //  数组中每个元素类型为函数指针类型:int (*)(int, int)
    
    int (*(*ppfarr)[5])(int, int) = &pfarr;

    system("pause");
    return 0;

# 回调函数(void *)

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

// 回调函数
void calc(int (*pf)(int, int))
{
    int x = 0;
    int y = 0;
    int ans = 0;
    printf("\n请输入操作数: ");
    scanf("%d %d", &x, &y);
    ans = pf(x, y);
    printf("%d\n", ans);
}

#include <stdio.h>
#include <iostream>
using namespace std;

// size_t 类型表示C中任何对象所能达到的最大长度,它是无符号整数。
// void 指针可以指向任意类型的数据,就是说可以用任意类型的指针对 void 指针对 void 指针赋值
void qsort(void *base,                                // 你要比较的数据的起始地址
           size_t num,                                // 待排序的数据元素个数
           size_t width,                              // 待排序的数据元素的大小(单位是字节)
           int (*cmp)(const void *e1, const void *e2) // 函数指针-比较函数
);

// 比较 2 个整型元素
//  e1 指向一个整数
//  e2 指向一个整数
int cmp_int(const void *e1, const void *e2)
{
    return *(int *)e1 - *(int *)e2;
}

int main()
{

    int arr[] = {9, 8, 7, 6, 5, 4, 3, 2, 1};
    int n = sizeof(arr) / sizeof(arr[0]);
    // qsort 排序
    qsort(arr, n, sizeof(arr[0]), cmp_int);
    for (int i = 0; i < n; i++)
    {
        cout << arr[i] << " ";
    }
    cout << endl;

    system("pause");
    return 0;
}

# 结构体

# 结构体的定义

结构是一些值的集合,这些值称为成员变量。

结构的每个成员可以是不同类型的变量

// 声明的结构体类型 struct Person
struct Person
{
    char name[20];
    char tel[12];
    char gender[5];
    int high;
};

// 声明的结构体类型 struct Person
struct Person
{
    char name[20];
    char tel[12];
    char gender[5];
    int high;
} p1, p2; //p1, p2 是使用 struct Person 结构体类型创建的 2 个变量
//p1, p2 是两个全局的结构体变量

#include <stdio.h>
#include <iostream>
using namespace std;

// 声明的结构体类型 struct Person
struct Person
{
    char name[20];
    char tel[12];
    char gender[5];
    int high;
};

int main()
{
    struct Person p1; // 结构体变量的创建

    system("pause");
    return 0;
}

struct Person p1p1 是结构体变量


// 声明的结构体类型 struct Person
struct Person
{
    char name[20];
    char tel[12];
    char gender[5];
    int high;
};
struct Stu
{
    struct Person p;
    int num;
    float f;
};

# 匿名结构体类型

#include <stdio.h>
#include <iostream>
using namespace std;
// 匿名结构体类型只能使用一次
struct
{
    int a;
    char b;
    float c;
} x;
// 匿名结构体类型只能使用一次
struct
{
    int a;
    char b;
    float c;
} a[20], *p; //p 匿名结构体的指针
int main()
{
	// 警告:编译器会把上面的两个声明当成完全不同的两个类型。所以是非法的。
    p = *x;
    system("pause");
    return 0;
}

# 结构的自引用

struct Node
{
    int data; // 4
    struct Node next;
};
int main()
{
    sizeof(struct Node);
    system("pause");
    return 0;
}

上述为错误写法,因为编译器认为这是同一个类型的结构体,无线套娃下去。即节点包节点......


正确写法:

struct Node
{
    int data; // 数据域
    struct Node *next; // 指针域
};
int main()
{
    printf("%d", sizeof(struct Node)); // 16
    system("pause");
    return 0;
}

因为结构体指针大小是固定的,存放地址的。


typedef struct
{
    int data;
    Node *next;
}Node;

错误。匿名结构体需要结构体存在,而结构体存在又需要匿名。即:鸡生蛋问题。


# 结构体的重命名

typedef struct Node
{
    int data;
    Node *next;
}* LinkList;

等价于

typedef struct Node
{
    int data;
    Node *next;
};
typedef struct Node * LinkList; // ListList 是指针类型

# 结构体的初始化

// 声明的结构体类型 struct Person
struct Person
{
    char name[20];
    char tel[12];
    char gender[5];
    int high;
};
int main()
{
    struct Person p1 = {
        "张三",
        "15596668862",
        "男",
        172}; // 结构体变量的创建以及赋值,初始化
    
    return 0;
}

嵌套结构体

#include <stdio.h>
#include <iostream>
using namespace std;

// 声明的结构体类型 struct Person
struct Person
{
    char name[20];
    char tel[12];
    char gender[5];
    int high;
};

struct Stu
{
    struct Person p;
    int num;
    float f;
};
int main()
{
    struct Person p1 = {
        "张三",
        "15596668862",
        "男",
        172}; // 结构体变量的创建以及赋值,初始化

    struct Stu s = {
        {"李四",
         "15596668863",
         "男",
         170},
        100,
        3.14};
    
    system("pause");
    return 0;
}

image-20230708214355838

发现 f = 3.1400001 ,浮点数在内存中不能精确保存


struct Node
{
 int data;
 struct Point p;
 struct Node* next; 
}n1 = {10, {4,5}, NULL}; // 结构体嵌套初始化

# 结构体成员的访问

int main()
{
    //...
    printf("%s,%s,%s,%d\n", p1.name, p1.tel, p1.gender, p1.high);
    printf("%s,%s,%s,%d\n", s.p.name, s.p.tel, s.p.gender, s.p.high);
    return 0;
}

# 结构体指针与结构体变量

void print2(struct Person *p)
{
    printf("%s %s %s %d\n", p->name, p->tel, p->gender, p->high);// 结构体指针 -> 成员变量
}
void print1(struct Person p)
{
    printf("%s %s %s %d\n", p.name, p.tel, p.gender, p.high);// 结构体变量。成员变量
}
int main()
{
    struct Person p1 = {
        "张三",
        "15596668862",
        "男",
        172}; // 结构体变量的创建以及赋值,初始化
    struct Stu s = {
        /*
        {"李四",
         "15596668863",
         "男",
         */
        
         170},
        100,
        3.14};
    printf("%s,%s,%s,%d\n", p1.name, p1.tel, p1.gender, p1.high);     // 张三,15596668862, 男,172
    printf("%s,%s,%s,%d\n", s.p.name, s.p.tel, s.p.gender, s.p.high); // 张三,15596668862, 男,172
    print1(p1);  // 张三,15596668862, 男,172
    print2(&p1); // 张三,15596668862, 男,172
    return 0;
}

上面的 print1print2 函数哪个好些?

答案是:首选 print2 函数。

函数传参的时候,参数是需要压栈的。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

结论: 结构体传参的时候,要传结构体的地址。

# 动态内存管理

int main()
{
    // 内存空间大小不能更改
    int a = 10;  // 4 个字节
    int arr[10]; // 40 个字节
    system("pause");
    return 0;
}

# malloc 和 free

C 语言提供了一个动态内存开辟的函数:

void* malloc (size_t size);

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

  • 如果开辟成功,则返回一个指向开辟好空间的指针。

  • 如果开辟失败,则返回一个 NULL 指针,因此 malloc 的返回值一定要做检查。

  • 返回值的类型是 void* ,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己 来决定。

  • 如果参数 size 为 0, malloc 的行为是标准是未定义的,取决于编译器。

#include <errno.h>
#include <string.h>
#include <stdlib.h> //malloc 函数的头文件
int main()
{
    int arr[10] = {};
    // 动态内存开辟
    int *p = (int *)malloc(40);
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));
        return 1;
    }
    // 使用内存
    for (size_t i = 0; i < 10; i++)
    {
        *(p + i) = i;
        printf("%d ", *(p + i)); // 0 1 2 3 4 5 6 7 8 9
    }
    printf("\n");
    
    // 由于 malloc... 申请的空间在堆区,那么需要自己释放空间
    // 没有 free 并不是说内存空间就不会回收了,当程序推出的时候,系统会自动回收内存空间
    free(p);
	p = NULL; //f p 是野指针。
    return 0;
}

free 函数用来释放动态开辟的内存。

  • 如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的。

  • 如果参数 ptr 是 NULL 指针,则函数什么事都不做。


# calloc

函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为 0。

与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全 0。

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h> // malloc 函数的头文件

// 开辟 10 个整型的空间
int main()
{
    int *p = (int *)calloc(10, sizeof(int));
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));
    }
    // 打印
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", *(p + i));
    }
    // 释放
    free(p);
    p = NULL;

    system("pause");
    return 0;
}

# realloc

  • realloc 函数的出现让动态内存管理更加灵活。

  • 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小 的调整。

void* realloc (void*  ptr, size_t size);
  • ptr 是要调整的内存地址

  • size 调整之后新大小

  • 返回值为调整之后的内存起始位置。

  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。

  • realloc 在调整内存空间的是存在两种情况:

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

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

image-20230713210653883

情况一

当是情况 1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化

image-20230713210627230

情况二

当是情况 2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小 的连续空间来使用。这样函数返回的是一个新的内存地址。

image-20230713210642324

由于上述的两种情况, realloc 函数的使用就要注意一些。

# 常见错误

# 1. 对 NULL 指针解引用

代码如下(示例):

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p = (int*)malloc(20);
	*p = 0;// 对 p 这个地址解引用并赋值为 0
	free(p);
	return 0;
}

malloc 等等函数在开辟空间时都是有可能开辟失败的,万一失败,就是返回空指针,你直接对空指针解引用并赋值肯定是有问题的

所以我们这里还是要进行指针检验
代码如下(示例):

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p = (int*)malloc(20);
	if(p==NULL)
	{
	return -1;
	// 如果返回,开辟失败结束程序,如果没有返回则可进行下面的操作
	}
	*p = 0;// 对 p 这个地址解引用并赋值为 0
	free(p);
	return 0;
}

# 2. 对动态开辟空间的越界访问

代码如下(示例):

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p = (int*)malloc(200);//200 个字节也就是 50 个 int 型
	if (p == NULL)
	{
		return -1;
	}
	int i = 0;
	for (i = 0;i < 60;i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}

这个代码乍一看上去没有问题,但是仔细看的话就会发现端倪,malloc 开辟 200 字节空间也就是 50 个 int 型,你 for 循环赋值最多循环次数也只能是 50 次啊,你循环 60 次肯定是越界访问了,这里也是妥妥的会报错。

# 3. 对非动态开辟使用 free 函数

代码如下(示例):

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int a = 10;
	int*p = &a;
	free(p);
	p = NULL;
}

我们这里 int 创建了 a,然后把 a 的地址赋给了 int * 类型的 p,再然后 free 掉 p。这种操作也是铁定会报错的,p 这个局部变量是在栈上的,而 free 函数针对的是堆区

4. 使用 free 释放一块动态内存开辟内存的一部分
代码如下(示例):

// 使用 free 释放一块动态内存开辟内存的一部分
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return -1;
	}
	// 使用
	int i = 0;
	for (i = 0;i < 5;i++)
	{
		*p++ = i;
	}
	// 释放
	free(p);
	p = NULL;
}

这里的代码有什么问题呢?我们画一个图就一目了然了

在这里插入图片描述

一开始 p 在上图位置,然而随着 for 循环,p++ 这个操作,p 指向的位置不断往后,一直到下图位置

在这里插入图片描述

这时 p 已经不指向原先开辟空间的位置了,你这时候去用 free 释放掉显然是不合适的

5.对同一块空间多次释放
我们先来看2段代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p = (int*)malloc(40);
	if (p == NULL)
	{
		return -1;
	}
	free(p);
	free(p);
}
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p = (int*)malloc(40);
	if (p == NULL)
	{
		return -1;
	}
	free(p);
	p = NULL;
	free(p);
}

两段代码都是对同一块空间多次释放,但第一段代码会报错,第二段不会。
解释如下:
第一段代码你已经释放掉 p 所指向的空间了,空间里什么也没有了,但 p 仍然指向那块空间,所以你再次释放不属于你的空间肯定会报错。
第二段代码你释放掉 p 所指向空间,然后用空指针给 p 赋值,再去释放空指针,我们知道,free 空指针是什么也不做,所以不会报错。

# 6. 动态开辟内存忘记释放

对于动态开辟内存忘记释放,在堆区上申请的空间有 2 种回收方式:
1. 你自己 free 掉
2. 程序退出时,系统自动回收
我们先来看一段代码

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p = (int*)malloc(40);
	if (p == NULL)
	{
		return -1;
	}
	getchar();
	return 0;
}

该代码我们没有自己使用 free 来释放内存,而中间又有 getchar 一直在等待接收字符,打个比方:假如你中途去上厕所或者干其他事情了,getchar 一直没有接收到字符,程序就一直没有结束,那我们用 p 开辟的空间在你上厕所期间就一直被占用,那块空间系统没办法去做别的有意义的事情。而上升到将来公司层面:我们写的程序可能一天 24h 都在跑,那遇到这种情况,你没有 free 掉内存,你不用又不回收,整体效率的影响是非常大的。

# c/c++ 的内存开辟

image-20230713212938387

C/C++ 程序内存分配的几个区域:

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

有了这幅图,我们就可以更好的理解在《C 语言初识》中讲的 static 关键字修饰局部变量的例子了。

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。

但是被 static 修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁

所以生命周期变长。

# 柔性数组

  • 结构中的柔性数组成员前面必须至少一个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用 malloc () 函数进行内存的动态分配,并且分配的内存应该大于结构的大
    小,以适应柔性数组的预期大小。
//code1
typedef struct st_type
{
    int i;
    int a[0];// 柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a);//4

# 使用

// 代码 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);

这样柔性数组成员 a,相当于获得了 100 个整型元素的连续空间

# 优势

上述的 type_a 结构也可以设计为:

// 代码 2
    typedef struct st_type
    {
        int i;
        int *p_a;
    }type_a;
    type_a *p = (type_a *)malloc(sizeof(type_a));
    p->i = 100;
    p->p_a = (int *)malloc(p->i*sizeof(int));
    // 业务处理
    for(i=0; i<100; i++)
    {
        p->p_a[i] = i;
    }
    // 释放空间
    free(p->p_a);
    p->p_a = NULL;
    free(p);
    p = NULL;

上述 代码1代码2 可以完成同样的功能,但是 方法 1 的实现有两个好处

第一个好处是:方便内存释放

如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给
用户。用户调用 free 可以释放结构体,但是用户并不知道这个结构体内的成员也需要 free,所以你
不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好
了,并返回给用户一个结构体指针,用户做一次 free 就可以把所有的内存也给释放掉。

第二个好处是:这样有利于访问速度.

连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正
你跑不了要用做偏移量的加法来寻址)