为了备战虚幻的学习和精进开发能力,最近一段时间都开始了C++和一些游戏开发模式的实体书的阅读
结果看实体书后,习惯在书上涂涂画画,居然好一段时间都忘记更新博客了
现在将逐步将笔记搬回博客上二次总结记录收纳

本笔记基于我的C#基础,只重点指出C++和C#的不同之处,其他内容默认当成C#处理即可
其实在基础语法来看,两者是极其相似的

命名空间作用域

C++用双冒号来::引用一个空间作用域,C#这方面统一只用一个点.

bool值

C++的bool值本质就是个数,所有的非零变量都是true,0变量为false,这意味着可以将数字直接运用到条件语句的比较里。

而C#的bool是单独的类型存在。

条件语句

C++的switch仅支持整数,甚至不支持字符串和浮点数

多少有点太弱了.....

形参和实参

C++里,外界调用函数时传入的参数被称为实参,函数内部的参数对象被称为形参。(在C#里,这部分通常会重点讨论值对象和引用对象传入的区别)

形参本质是实参的拷贝,所以对形参的修改不会动到外界实参,但是可以用指针传入来满足这个需求。

函数的定义和声明

C++通常会把函数的定义(类似接口或者抽象方法)写在头文件(.h),声明(实现)写在源文件(.cpp)。

也可以选择都在源文件里面实现,在源文件实现的时候,可以像一个变量一样,先定义一个函数(无函数体),后面再进行实现(写具体函数体)。如果不先定义,函数将无法调用,而实现可以写在调用后面。

C++也允许默认参数,但是如果同时存在定义和实现,必须写在定义的参数里面,否则会报错

内联函数

C++独有,使用Inline关键字定义的函数,调用的时候会被编译器解读为内部的函数代码直接插入到调用点上,可以提高调用效率。

缺点:不支持递归函数,会增加最终打包代码量体积,大内容函数支持不佳。总体建议给调用频繁且短小的函数使用来提高性能。

变量的存储类型

Auto:

类似C#的Var

Static

C++支持局部静态,局部静态只有第一次调用的时候会初始化赋值,而不支持外部文件获取调用,并且只允许在函数作用域内被调用。

但是!和C#不同,C++的全局静态一旦使用,会被强制限制为只有此文件可用!

Register:

令变量变成寄存器变量(寄存器位于CPU),比存储在内存里快得多。缺点:现在C++已经更新迭代支持自动分配寄存器设置,用处已经不大,最新版本本语法已经被废弃,而且寄存器是没有内存的,导致这变量无法使用指针。

Extern:

声明变量的时候使用,可以在一个文件里面引用其他文件已经定义好的变量和函数。(需要自己注意名字和参数是否一一对应)

不用担心重名问题,因为C++干脆强制要求所有文件的全局变量名都不可以重名

数组

C++里面的数组和C#开始差别较大了,比较大的差别有:

C++并没有不初始化的空间设置为null的概念,而是会直接开辟后塞入预设值(通常是一个巨大的负数),所以你会发现只需要声明就可以为每个空间直接赋值,但是如果你只要声明了一个空间,其他空间都会被赋值默认值

C++数组不提供length这种获取长度的api,你需要使用sizeof去将数组总长度除数组类型的长度(真够底层的)

C++字符数组可以直接整体输出,但是停止位置需要插入\0符号,否则会导致一直输出知道某一块无关内存正好存在\0符号才会停止输出

接下来直接贴完整笔记

一维数组


    #pragma region 知识点一 基本概念
    //数组是存储一组相同类型数据的集合
    //数组分为 一维、多维
    //一般情况 一维数组 就简称为 数组

    //数组的存储序列是线性的
    //意思就是数组中的数据之间 在内存中是紧挨着的
    #pragma endregion

    #pragma region 知识点二 数组的声明
    // 1.变量类型 数组名[容量];
    //   声明一个指定容量的数组,但是每个房间的值并不确定
    //   变量类型 可以是我们学过的 或者 没学过的所有变量类型
    int array[6]; //通过这种方式声明一个数组 相当于开了6个小房间 但是房间里面的int值是不确定

    // 2.变量类型 数组名[容量] = {值,值,值.....};
    //   声明一个指定容量的数组,每个房间的值在声明时进行初始化
    int array2[6] = {1,2,3,4,5,6};

    // 3.变量类型 数组名[] = {值,值,值.....};
    //   声明一个不指定容量的数组,具体容量编译器根据初始化值的数量自动推导数组大小
    int array3[] = { 1,2,3 };

    //注意,在声明数组时不能用变量作为容量
    //只有之后学了堆上分配的数组 才可以用变量表示容量
    /*int size = 6;
    int array4[size];*/

    #pragma endregion

    #pragma region 知识点三 数组的使用
    //1.数组的长度(容量)
    //  利用sizeof进行计算
    cout << sizeof(int) << endl;
    int arr[8] = { 1,2,3,4,5,6,7,8 };
    //sizeof(arr)通过这种方式可以得到数组占多少内存空间 (字节)
    //sizeof(int)通过这种方式又可以得到 数组类型 所占内存空间(字节)
    int length = sizeof(arr) / sizeof(int);
    cout << length << endl;

    //2.获取数组中的元素
    //数组中的下标和索引 他们是从0开始的
    //通过 索引下标去 获得数组中某一个元素的值时
    //一定注意!!!!!!!!
    //不能越界  数组的房间号 范围 是 0 ~ Length-1
    cout << arr[0] << endl;
    cout << arr[7] << endl;
    cout << arr[3] << endl;
    //如果越界 不会报错 但是会出现获取到不确定的数值!!!
    /*cout << arr[-1] << endl;
    cout << arr[8] << endl;*/

    //3.初始化所有坑位
    //  至少初始化一个值,后面的坑位会用0进行初始化
    int arr2[99] = {0};
    cout << arr2[0] << endl;
    cout << arr2[1] << endl;
    cout << arr2[2] << endl;
    cout << arr2[98] << endl;

    //4.修改数组中的元素
    arr2[0] = 99;
    cout << arr2[0] << endl;
    arr2[1] = 5;
    cout << arr2[1] << endl;

    //5.遍历数组 通过循环 快速获取数组中的每一个元素
    cout << "********************" << endl;
    int arr3[6] = { 156,223,3123,4435,5123,6123 };
    int l = sizeof(arr3) / sizeof(arr3[0]);
    for (int i = 0; i < l; i++)
    {
        //访问是可以通过变量作为索引去访问的
        //但是声明不能够通过变量去声明
        cout << arr3[i] << endl;
    }

    //6.增加数组的元素
    //  数组初始化以后 是不能够 直接添加新的元素的
    //搬家
    //想要把原本存储了n个元素的数组 变为存储n + m个元素
    cout << "********************" << endl;
    int arr4[7];
    for (int i = 0; i < l; i++)
    {
        arr4[i] = arr3[i];
    }
    //增加数组的元素了
    arr4[6] = 9999;
    for (int i = 0; i < 7; i++)
    {
        cout << arr4[i] << endl;
    }

    //7.删除数组的元素
    //  数组初始化以后 是不能够 直接删除元素的
    //搬家

    //8.查找数组中的元素
    int arr5[] = { 99,2,3,5,3 };
    //想要查找 3这个元素在数组中哪个位置
    //只有通过遍历才能确定 数组中 是否存储了一个目标元素
    int target = 3;

    for (int i = 0; i < sizeof(arr5)/sizeof(int); i++)
    {
        if (target == arr5[i])
        {
            cout << "和目标元素值相等的元素在第" << i << "索引位置" << endl;
            break;
        }
    }

    #pragma endregion

    #pragma region 知识点四 静态数组的限制
    //我们目前学习的数组相关知识,属于静态数组相关知识
    //声明一个静态数组时,数组的大小是固定的,并且必须在编译时确定
    //声明后,无法改变数组的大小或将其指向另一个数组
    //静态数组的大小和类型在声明时固定,不能在运行时修改

    //与之对应的动态数组
    //我们将学习了堆内存分配后再讲解
    #pragma endregion

    //总结:
    //1.概念:同一变量类型连续存储的数据集合
    //2.一定要掌握的数组相关知识:声明、遍历、增删查改
    //3.所有的变量类型都可以声明为 数组
    //4.它是用来批量存储游戏中的同一类型的对象的 容器  比如 所有的怪物 所有玩家
    //5.目前学习的声明和使用方式时静态数组的 ,以后学习了堆内存分配相关的知识 就知道动态数组是什么了

二维数组

    #pragma region 知识点一 基本概念
    //二维数组 是使用两个下标(索引)来确定元素的数组
    //两个下标可以理解成 行标  和 列标
    //比如矩阵
    // 1 2 3
    // 4 5 6 
    // 可以用二维数组 变量名[2][3] 表示 
    // 好比 两行 三列的数据集合
    #pragma endregion

    #pragma region 知识点二 二维数组的声明
    // 1.变量类型 数组名[行][列];
    //   声明一个指定行列容量的二维数组,但是每个房间的值并不确定
    //   变量类型 可以是我们学过的 或者 没学过的所有变量类型
    int arr[3][4]; //相当于就是分配了3*4=12个内存小房间用于存储int值 目前没有初始化
                   //因此其中每个房间中的值 是不确定的


    // 2.变量类型 数组名[行][列] = {{值,值,值.....}, {值,值,值.....}, {值,值,值.....} ....};
    //   声明一个指定行列容量的二维数组,每个房间的值在声明时进行初始化
    int arr2[3][4] = { {1,2,3,4},
                       {5,6,7,8},
                       {9,10,11,12} };


    // 3.变量类型 数组名[行][列] = {值,值,值,.....};
    //    上面是按行去初始化,我们也可以按“房间”初始化
    int arr3[3][4] = { 1,2,3,4,
                       5,6,7,8,
                       9,10,11,12 };

    #pragma endregion

    #pragma region 知识点三 二维数组的使用
    int array[3][4] = {1,2,3,4,
                       5,6,7,8,
                       9,10,11,12 };

    //1.数组的长度
    //  利用sizeof进行计算
    //获取行数
    //         4 * 12 = 48          /      4 * 4 = 16            =   3
    int rows = sizeof(array) / sizeof(array[0]);
    cout << rows << endl;
    //获取列数
    //         4 * 4 = 16       /     4         =   4
    int cols = sizeof(array[0]) / sizeof(int);
    cout << cols << endl;

    //2.获取数组中的元素
    //  注意:第一个元素的索引是0 最后一个元素的索引 肯定是长度 - 1
    cout << array[0][0] << endl;
    cout << array[0][1] << endl;
    cout << array[2][2] << endl;

    //3.初始化所有坑位
    //  至少初始化一个值,后面的坑位会用0进行初始化
    int array2[3][3] = { 0 };
    cout << array2[0][0] << endl;
    cout << array2[2][2] << endl;

    //4.修改数组中的元素
    array2[0][0] = 99;
    cout << array2[0][0] << endl;

    cout << "**********************" << endl;
    //5.遍历二维数组
    // 套路:用两个for循环进行嵌套
    //行
    for (int i = 0; i < rows; i++)
    {
        //列
        for (int j = 0; j < cols; j++)
        {
            //行 i  0 1 2
            //列 j  0 1 2 3
            // i j
            // 0 0 = 1
            // 0 1 = 2
            // 0 2 = 3
            // 0 3 = 4
            // 1 0 = 5
            // 1 1 = 5
            // 1 3 = 5
            //.......
            cout << array[i][j] << endl;
        }
    }

    //6.增加数组的元素
    //  数组初始化以后 是不能够 直接添加新的元素的
    //搬家
    // 在搬家过程中 要以这个容量更小的二维数组容器为主 否则会越界
    int array3[4][5];
    for (int i = 0; i < rows; i++)
    {
        for (int j = 0; j < cols; j++)
        {
            array3[i][j] = array[i][j];
        }
    }

    //7.删除数组的元素
    //  留给大家思考 自己去做一次
    //搬家
    // 在搬家过程中 要以这个容量更小的二维数组容器为主 否则会越界

    //8.查找数组中的元素
    // 如果要在数组中查找一个元素是否等于某个值

    #pragma endregion

    #pragma region 知识点四 在函数中传递二维数组
    //知识回顾
    //在函数中传递一维数组时
    //传入的本质上是一个指向首地址的指针
    //因此在内部我们无法准确获取数组的容量信息
    //需要从外部传入容量信息
    // 固定写法:
    // 返回值 函数名( 数组类型 数组名[], int 容量 )

    //二维数组
    //在函数中传递二维数组是一种特殊的写法
    //你必须告诉编译器二维数组有多少列
    // 固定写法:
    // 返回值 函数名( 数组类型 数组名[][列数], int 行数 )
    cout << "*************" << endl;
    test(array, rows);
    #pragma endregion

}

//函数中传递二维数组的固定写法
//返回值 函数名( 数组类型 数组名[][列数], int 行数 )
void test(int array[][4], int rows)
{
    for (int i = 0; i < rows; i++)
    {
        for (int j = 0; j < 4; j++)
        {
            cout << array[i][j] << endl;
        }
    }
}

//总结:
//1.概念:同一变量类型的 行列数据集合
//2.一定要掌握的内容:声明、遍历、增删查改
//3.所有的变量类型都可以声明为二维数组
//4.我们目前学习的声明方式 静态数组,以后学习堆中分配的动态数组
//5.二维数组在函数中应该如何传递 返回值 函数名( 数组类型 数组名[][列数], int 行数 )
//6.游戏中一般会用二维数组 来存储 矩阵,在控制台小游戏中我们可以用二维数组来表示 地图的格子

字符数组

知识点一 字符数组的声明和初始化
    //字符数组就是类型为字符char的一维数组

    //常用方式:
    //方式一:聚合初始化
    char charArr[5] = { 'H', 'E', 'L', 'L', 'O' };
    char charArr2[] = { 'H', 'E', 'L', 'L', 'O' };

    //方式二:单独初始化
    char charArr3[5];
    charArr3[0] = 'H';
    charArr3[1] = 'E';

    //这几种声明初始化的方式,如果直接将字符数组拿来打印 会出现问题
    cout << charArr << endl;
    cout << charArr2 << endl;
    cout << charArr3 << endl;

    #pragma endregion

    #pragma region 知识点二 使用字符串初始化字符数组
    //可以直接使用字符串对字符数组进行赋值
    char charArr4[] = "Hellow World"; //string
    cout << charArr4 << endl;
    //原本看起来只有12个字符的字符串 但是长度却有13个
    int length = sizeof(charArr4) / sizeof(char);
    cout << length << endl;
    //通过观察长度,我们发现了一个潜在规则
    //对字符数组用字符串去赋值时,会默认在尾部加上一个\0的转移字符
    //代表字符串的结束
    #pragma endregion

    #pragma region 知识点三 利用结束符号决定字符数组表示的字符串何时结束
    char charArr5[5];
    charArr5[0] = 'A';
    charArr5[1] = 'B';
    //charArr5[2] = 'X';
    //charArr5[3] = 'C';
    //charArr5[4] = '\0';
    //如果不对字符数组后面的内容进行初始化 并且其中没有\0
    //在打印时就不知道何时结束,就会不停的往后读取
    //因为没有明确的终止符,程序会根据内存中是否存在 '\0' 停止打印
    //可能打印出多余字符,甚至是乱码
    //也就是说可能出现访问到字符数组范围之外的内存区域,产生未定义的行为
    cout << charArr5[2] << endl;
    cout << charArr5 << endl;
    //关于“烫”字符
    //在内存中,汉字字符使用多字节编码(比如 UTF-8 或 GBK)
    //当程序读取到这些内存内容时,会尝试解释它们。
    //由于编码规则,有可能形成合法的汉字编码,导致控制台上显示出汉字或其他非预期字符

指针

这位更是重量级,所以这里每一章节的笔记都进行记录。

指针的基本概念

可以把指针认为是C#里面某个内存地址的门牌号

需要强调的是,C++里面堆内存和栈内存都可以设置指针,甚至任何变量都可以自定义用堆内存或栈内存存储。

    #pragma region 知识点一 数据在内存中如何被存储
    //位(bit)是计算机中最小的内存单位
    //字节(byte)是计算机中最小的存储单位
    // 1byte = 8bit

    //计算机内存中 每个字节都有一个唯一的内存地址
    //无论是32位还是64位的计算机系统,内存地址都是按字节编号的
    //这意味着,计算机的内存地址是以字节为单位的

    //什么是内存地址?
    //系统会按照 字节 对每个 内存单元 进行编号
    //所谓的内存单元,你可以理解为一个个带有门牌号的小房间
    //想要使用内存,就需要知道房间的门牌号码
    //比如
    //声明一个整型变量 
    // int i = 2;
    // int 占4个字节,系统就会为该变量分配4个小房间
    // 4个小房间的门牌号码为 5001~5004
    // 
    //接着我们再声明一个整型变量
    // int j = 10;
    // j同样占4个字节,系统会在内存中接着为j分配小房间
    // 5005~5008

    //也就说当我们要访问i和j具体存储的信息时
    //首先要找到存储i和j对应的房间门牌号码(内存单元们)
    // 读取i时,找到i的起始地址5001,因为整型占4个字节
    // 因此系统会从5001开始读取4个字节出来作为i的值(5001~5004内存单元)
    // 
    // 同样读取j时,也是一样,j的起始地址为5005,也占4个字节
    // 系统会从5005开始读取4个字节作为j的值(5005~5008内存单元)

    //而内存单元的门牌号码,我们在程序语言中称之为内存地址(简称地址)
    //通过地址(门牌号码)我们就可以访问到指定的内存单元
    //所以可以形象的认为 地址 是 指向 内存单元的
    #pragma endregion

    #pragma region 知识点二 指针是什么?
    //为了方便记忆,前辈们制定了一个规则
    //我们将指向变量地址的对象 形象的称为该变量的 “指针”
    //既然 指针 是指向 内存地址 的,那么我们便可以通过指针找到对应的内存单元

    //而在程序中
    //指针是一个变量,其值为另一个变量的地址
    //我们可以通过指针间接的访问和修改该变量的值
    // 
    //因为我们可以通过指针得到它指向的地址(内存地址)
    //从而得到对应的内存单元,而内存单元中存储的就是变量的具体值
    //所以我们可以通过指针修改变量的值

    //说人话:
    //指针变量就是 存储其他变量地址 的变量
    //我们不仅可以通过指针得到其他变量的地址
    //还可以根据该地址得到其他变量具体的值
    #pragma endregion
}

指针的基础使用

注意区别*和&:

(*) :解引用,即得到指针的所指向内存地址的指,左值是指对内存地址的指进行赋值,右指就只是单纯得到内存地址的指。(C++中的左值和右值_c++左值和右值-CSDN博客)

(&):得到某个变量的内存地址,通常用来赋值给指针。

由于C++是没有类似C#和java的自动内存管理机制的,所以得时刻注意空指针和野指针问题,否则即使你对内存空间的任意修改甚至都不会报错,会导致诡异的bug发生。

    #pragma region 知识点一 指针的声明
    //声明写法:
    //变量类型* 指针变量名;
    //或
    //变量类型 *指针变量名;
    //注意:
    //1.任何变量类型都可以声明为指针变量类型
    //  就是在变量类型后面加一个*
    //2.指针变量的常用命名规范
    //  前缀法:p或ptr作为前缀
    //  后缀法:_p或_ptr作为后缀 
    //  指针类型变量名:ptr\p+变量类型+变量名 => ptrIntValue、pIntValue
    //  等等

    int* p_i;
    char *ptr_c;
    float* f_ptr;
    unsigned* u_p;
    string* str_ptr;
    int* ptrIntValue;
    #pragma endregion

    #pragma region 知识点二 指针的初始化
    //指针必须赋值为内存中的有效地址(已经分配给变量的地址)
    //也就是说,根据我们目前学习的知识,我们在使用指针变量时
    //必须让他指向一个已经声明了的变量,并且该变量的类型需要和指针变量的变量类型一致
    int i = 10;
    //如果我们想声明一个指针指向i的内存地址,我们需要获取到i的地址
    //我们可以利用 地址符号 &i 来获取
    int* p_i2; //= &i;
    p_i2 = &i;
    cout << p_i2 << endl;
    
    //为什么要用相同的变量类型的指针去存储地址
    //主要是因为 一个变量它所占用的内存单元可能是多个 而指针变量存储的是它第一个内存单元的门牌号
    //如果指针变量类型和存储的变量类型不一致,那么之后就无法准确的读取对应个数个内存单元
    //来获取其中的内容
    float f = 5.5f;
    float* f_ptr2 = &f;
    cout << f_ptr2 << endl;
    #pragma endregion

    #pragma region 知识点三 指针的解引用操作符
    //所谓的 指针解引用操作符 其实就是 *
    //如果我们想要获取到指针所指向的内存地址中存储的值
    //我们只需要在指针变量前加一个解引用操作符,便可以得到其中存储的值
    cout << *p_i2 << endl;
    cout << *f_ptr2 << endl;
    //并且我们还可以利用它改变原有的值
    *p_i2 = 99;
    cout << *p_i2 << endl;
    cout << i << endl;
    #pragma endregion

    #pragma region 知识点四 空指针和野指针
    //空指针是
    //不指向任何有效内存的指针
    //给指针赋值为 nullptr 或 NULL(老版本) 表示该指针为空指针
    int* ptr_i5;// = nullptr;
    ptr_i5 = nullptr;

    //野指针是
    //指向无效或不可预知内存地址的指针
    //野指针会导致程序出现各种不可预料的错误
    //比如:程序崩溃、数据损坏、访问非法内存等等
    //主要出现的原因:
    //1.未初始化的指针(目前主要遇到的为该种情况)
    //2.指向内存被释放后仍使用的指针
    //3.指向内存位置超出作用域的指针
    //4.指向未定义为分配的内存的指针
    //等等

    //注意:
    //为了避免出问题
    //在声明指针时如果不马上使用
    //建议将指针声明为空指针
    #pragma endregion

    //总结
    //1.指针就是存储变量内存地址的变量
    //2.想要获取某一个变量地址 使用 地址符号 &来获取
    //3.想要获取指针指向内存地址中存储的值 使用解引用操作符(取值操作符)*
    //4.在声明指针时,如果不马上使用该指针,建议大家将其声明为空指针 nullptr
    //5.野指针会对程序带来不可预估的问题,需要引起重视

    //对于*和&的记忆口诀
    //看见&想地址
    //看见*想内容

指针的更多使用

指针的字节大小是固定的,64位电脑里面,指针通常8个字节,32位则是4个。

指针的大小固定是因为指针只记录变量首个地址的门牌号

但是指针的类型会影响指针解读内存的位数,比如对int指针解引用,就是从int的首地址往下连续解读int的四个字节。

   #pragma region 知识点一 指针变量所占内存空间
   //不管何种变量类型的指针变量
   //所占的内存空间都是一样的
   //因为它是用来存储变量的内存地址的(存储内存单元的门牌号码)
   //门牌号码是有范围限制的
   //不需要因为变量类型的不同而改变大小
   //在不同位数的操作系统 和 不同编译器设置上
   //指针所占的字节数有所不同
   //32位:4字节
   //64位:8字节(常见)
   int a = 1;
   int* p = &a;
   cout << sizeof(p) << endl;

   char c = 'A';
   char* p_c = &c;
   cout << sizeof(p_c) << endl;

   bool b = true;
   bool* p_b = &b;
   cout << sizeof(p_b) << endl;
   #pragma endregion

   #pragma region 知识点二 &(地址符号) 和 *(解引用操作符) 的混合使用
   //回顾
   //& 地址符号 主要用于获取变量地址 用于指针赋值
   int* p2 = &a;
   cout << p2 << endl;
   cout << &p2 << endl;
   //* 解引用操作符 主要用于获取地址中存储的内容
   cout << *p2 << endl;


   //这两个符号是可以混合使用的
   //主要用法有两种
   //1.&*
   //  使用时从右向左开始结合
   //  表示先获取指针指向地址的值,再获取该值的地址
   //  相当于获取指针指向的内存地址
   int* pp = &*p2;
   cout << p2 << endl;
   cout << pp << endl;

   cout << &p2 << endl;
   cout << &pp << endl;

   //&* 只能和指针变量配合使用
   //&*a
  
   //2.*&
   //  使用时从右向左开始结合
   //  表示先获取变量的地址,再获取该地址中存储的值
   //  相当于是获取一个变量的值
   cout << *&a << endl;

   //*&是可以和指针变量配合使用的
   //相当于先得到指针自己的地址,然后再通过这个地址得到其中存储的内容
   //指针存储的地址值
   cout << &a << endl;
   cout << *&p << endl;
   cout << p << endl;

   //注意:
   //只要记住了*和&两个符号的作用
   //那么在处理混合使用时,就分开看就可以了,从右往左看即可
   //*获取地址中存储的变量的值
   //&获取变量的地址的
  
   #pragma endregion

   #pragma region 知识点三 指针的自增减运算
   //指针是可以做自增减运算的
   //由于指针存储的是变量的地址
   //当我们做指针自增减运算时
   //相当于是对地址进行运算
   //并且地址的变化是遵循以下规律的
   // 自增:将指针向前移动 一个元素的大小
   // 自减:将指针向后移动 一个元素的大小
   // 元素大小:指针变量的类型决定,sizeof(指针变量类型)得到的就是元素大小
   //
   // 举例说明:
   cout << "***************" << endl;
   int bb = 10;
   int* ptrbb = &bb;
   cout << ptrbb << endl;
   ptrbb++;
   cout << ptrbb << endl;
   ++ptrbb;
   cout << ptrbb << endl;
   ptrbb = ptrbb + 1;
   cout << ptrbb << endl;

   ptrbb = ptrbb - 3;
   cout << ptrbb << endl;


   #pragma endregion

   #pragma region 知识点四 空类型指针
   //空类型指针是一种特殊的指针
   //它表示不特定类型的指针
   //指针变量可以指向任意的变量类型
   //具体存放何种类型,由后续赋值决定
   //赋值后,如果需要使用需要强转为对应的数据类型
   cout << "***************" << endl;
   void* pVoid;
   short s = 55;
   pVoid = &s;
   cout << pVoid << endl;
   short s2 = *(short*)pVoid;
   cout << s2 << endl;

   #pragma endregion

指针和常量

const的放置顺序会得到两种不同功能的指针,一个不能改变其中的值可以改变地址,一个不能改变地址可以改变其中的值。

如果加上两个const,那就都不可以改变。

    #pragma region 常量知识回顾
    //常量是指在程序执行过程中其值不能被改变的变量,大部分的常量在声明时都必须被初始化
    //关键点:
    //1.必须初始化
    //2.不能被修改
    const int i = 10;
    int const i2 = 20;
    #pragma endregion

    #pragma region 知识点一 指向常量的指针
    //注意:
    //指向常量的指针,并不是只能指向常量,也可以指向普通变量
    //这样命名的主要原因是,这种指针不能改变值,类似常量的特点

    //指向常量的指针,在*前面加一个const
    //特点:
    //可以改变指针指向的地址,但是不能改变值(不能直接改变指针指向地址的值)
    int j = 1;
    const int* p = &j;
    //在星号前加上const关键词的指针 称为 指向常量的指针
    //不能够改变其中存储的值的
    //*p = 10;
    //但是可以改变该指针的指向
    p = &i;
    int const* p2 = &i;
    //不能够改变其中存储的值的
    //*p2 = 11;
    //但是可以改变该指针的指向
    p2 = &j;
    #pragma endregion

    #pragma region 知识点二 指针常量
    //注意:
    //指针常量,不能指向常量,可以指向普通变量
    //这样命名的主要原因是,这种指针不能改变指向,但是可以改变指向地址中存储的值

    //指针常量,在*后加上const
    //特点:
    //不能改变指针指向的地址,但是可以改变值(指针指向地址中存储的值)
    int j2 = 10;
    //指针常量
    int* const ptr = &j2;
    //可以改变指针中指向地址中存储的值
    *ptr = 20;
    cout << j2 << endl;
    cout << *ptr << endl;
    //但是不能够再改变该指针的指向了
    //ptr = &j;

    //不能指向常量 因为 它的特点和常量的特点冲突了
    /*int const jj = 99;
    int* const ptr2 = &jj;*/
    #pragma endregion

    #pragma region 知识点三 指向常量的指针常量
    //指向常量的指针:可以改变地址指向,不能改变地址中存储的值
    //指针常量:不能改变指针指向的地址,可以改变地址中存储的值
    //那么,如果想要地址和值都不能改变,那么就可以构建一个指向常量的指针常量
    //即*前后都加上const
    const int jjj = 10;
    int const jjj2 = 20;
    //指向常量的指针常量
    const int* const ptr3 = &jjj;
    //或者这样写
    int const* const ptr4 = &jjj2;
    //不能改变指向了
    //ptr3 = &jjj2;
    //ptr4 = &jjj;
    //不能改变指向地址中存储的值
    /**ptr3 = 10;
    *ptr4 = 10;*/
   
    #pragma endregion

    //总结
    //指向常量的指针: const在 *前面去添加即可,值不能改,指向可以改
    //指针常量:const在 *后面去添加即可,值可以改,指向不能改
    //指向常量的指针常量:const在 *前后都加,值和指向都不能改了

指针和一维数组

C++的一维数组名本身就默认代表为第一个元素的指针变量。

而数组[index]则代表了对对应序号元素指针的解引用,也可以写为*(arr+index)。

数组在内存里面是连续存储的。

C++的数组和C#差别极大,建议详细看笔记。


    #pragma region 知识点一 指针和一维数组的关系
    //数组在内存中是连续存储的
    //因此我们只要利用指针获取到数组一开始的位置
    //就可以通过指针的自增减获取到数组中的所有信息
    #pragma endregion

    #pragma region 知识点二 建立关系
    //关键点:
    //1.指针类型需要和数组类型一致
    int array[4] = { 1,2,3,4 };
    int* ptr = nullptr;

    //2.获取数组一开始的位置
    //方式一:数组变量名即可表示数组的首地址
    ptr = array;
    cout << ptr << endl;

    //方式二:获取数组第一个元素的地址
    ptr = &array[0];
    cout << ptr << endl;

    #pragma endregion

    #pragma region 知识点三 利用指针获取数组元素
    //首地址便是第一个元素
    cout << *ptr << endl;
    
    //指针自增减可以移动 元素类型占用内存空间 个单位
    cout << *++ptr << endl;
    cout << *++ptr << endl;
    cout << *++ptr << endl;

    cout << *--ptr << endl;
    //还可以直接 + - 但是如果不 += -=的话 自己位置是不会改变的
    cout << *(ptr - 1) << endl;
    cout << *ptr << endl;
    
    //也可以直接减n个单位
    cout << *(ptr -= 2) << endl;
    cout << *(ptr += 3) << endl;

    //一定注意 不能越界 
    cout << *(ptr + 1) << endl;
    #pragma endregion

    #pragma region 知识点四 数组名当做指针变量使用
    //数组名不能像刚才指针一样 去改变它指向的内存地址
    //我们只能利用它的地址来进行计算 不能改变
    cout << *array << endl;
    cout << *(array + 1) << endl;
    cout << *(array + 2) << endl;
    cout << *(array + 3) << endl;
    #pragma endregion

    #pragma region 知识点五 利用指针遍历数组
    //注意:我们无法直接通过指针获取到数组的容量
    //因为指针其实只是记录了一个地址而已,通过地址并不能知道数组容量
    int* p = array;
    int* p2 = array;
    for (int i = 0; i < sizeof(array) / sizeof(int); i++)
    {
        //第一种 去偏移计算
        cout << *(p + i) << endl;
        //第二种 去自增
        cout << *p2++ << endl;
        //第三种 把数组名当成指针用 但是不能改变数组名的指向
        cout << *(array + i) << endl;
        //传统方式
        cout << array[i] << endl;
    }
    #pragma endregion

    //总结:
    //1.指针变量类型需要和数组保持一致
    //2.数组名字和数组首元素的地址 都可以获取到数组开始的位置(首地址)

指针和二维数组

C++的二维数组有一类专门适配的指针类型,叫数组指针

用法非常繁多,建议看详细笔记。

   #pragma region 知识点一 指针和二维数组的关系
   //二维数组在内存中也是连续存储的
   //因此我们只要利用指针获取到二维数组一开始的位置
   //就可以通过指针的 增减 获取到二维数组中的所有信息
   #pragma endregion

   #pragma region 知识点二 建立关系
   //关键点:
   //1.指针类型需要和二维数组类型一致
   int arr[2][4] = { 1,2,3,4,
                     5,6,7,8 };

   //2.按元素建立关系
   int* p = &arr[0][0];//得到的是第一行第一列的元素地址
   int* ptr = arr[0];//得到的是第一行的地址 = 第一行第一列的元素地址

   //3.按行数组建立关系
   //利用数组指针,获取二维数组中一行的一维数组起始地址
   //数组指针是一个指向整个数组的指针
   //本质上是一个指针,指向一个包含n个元素的数组
   //数组指针定义规则:变量类型 (*指针名)[元素个数]
   //          
   //int (*p2)[4] 表示指向包含4个int元素的一维数组的指针
   //int:数组中元素的类型
   //*p:指针变量
   //[4]:表示这个指针指向的数组包含4个int元素
   //指向二维数组中第一个一维数组的指针
   int (*p2)[4] = arr;


   #pragma endregion

   #pragma region 知识点三 利用指针获取二维数组元素
   //由于二维数组的存储序列也是连续的
   //因此利用指针增减也可以获取到所有元素
   //1.按元素获取的指针
   cout << "按元素获取内容" << endl;
   cout << *p << endl;//1
   cout << *(p + 1) << endl;//2
   cout << *++p << endl;//2
   cout << *--p << endl;//1
   cout << *(p += 3) << endl;//4
   cout << *++p << endl;//5
   cout << *(p + 3) << endl;//8
   //注意:
   //要在范围内访问0<= i < 行*列 之间
   //不要越界,不要越界,不要越界

   //2.用数组指针获取元素
   // p2是什么?
   // 类型是:数组指针 int (*)[4]
   // 指向的内容:指向二维数组中的第一个一维数组
   // 每次增减 偏移位置为:元素字节数 * 容量
   //第一行数组首地址
   cout << "**************" << endl;
   cout << p2 << endl;
   //取出数组地址
   cout << *p2 << endl;

   //第二行数组首地址
   cout << p2 + 1 << endl;//偏移元素*容量 16个字节
   //取出数组地址
   //int*
   cout << *(p2 + 1) << endl;
   cout << **(p2 + 1) << endl;
   //先偏移行地址,再在这一行中偏移元素地址
   cout << *(*(p2 + 1) + 1) << endl;
   cout << *(*(p2 + 1) + 2) << endl;
   cout << *(*(p2 + 1) + 3) << endl;
   cout << "**************" << endl;
   // *p2是什么?
   // 类型是:指向一维数组指针 int*
   // 指向的内容:指向二维数组中的第一个一维数组中的第一个元素
   // 每次增减 偏移位置为:元素字节数
   cout << *p2 << endl;
   cout << *p2 + 1 << endl;//偏移元素的 4个字节
   //一维数组第一个元素地址中存储的值
   cout << **p2 << endl;
   cout << *(*p2 + 1) << endl;//往后移动元素n个字节数
   cout << *(*p2 + 2) << endl;
   cout << *(*p2 + 3) << endl;
   cout << *(*p2 + 4) << endl;
   cout << *(*p2 + 5) << endl;
   cout << *(*p2 + 6) << endl;
   cout << *(*p2 + 7) << endl;

   cout << "结合数组指针 利用数组思维去获取元素内容" << endl;
   // p2是什么?
   // 类型是:数组指针 int (*)[4]
   // 指向的内容:指向二维数组中的第一个一维数组
   // 每次增减 偏移位置为:元素字节数 * 容量
   // *p2是什么?
   // 类型是:指向一维数组指针 int*
   // 指向的内容:指向二维数组中的第一个一维数组中的第一个元素
   // 每次增减 偏移位置为:元素字节数
   cout << (*p2)[0] << endl;
   cout << (*p2)[1] << endl;
   cout << (*p2)[2] << endl;
   cout << (*p2)[3] << endl;
   cout << (*(p2 + 1))[0] << endl;
   cout << (*(p2 + 1))[1] << endl;
   cout << (*(p2 + 1))[2] << endl;
   cout << (*(p2 + 1))[3] << endl;
   #pragma endregion

   #pragma region 知识点四 数组当做指针变量使用
   cout << "*************" << endl;
   cout << arr[0][0] << endl;
   cout << arr[0] << endl;

   //1.利用行地址来获取元素
   //  先得到行首地址,在通过加减来获取某行某元素
   cout << *arr[0] << endl;
   cout << *p2[0] << endl;
   cout << *(arr[0] + 1) << endl;
   cout << *(p2[0] + 1) << endl;
   cout << *(arr[0] + 2) << endl;
   cout << *(arr[0] + 3) << endl;
   //cout << *(arr[0] + 4) << endl;
   cout << *arr[1] << endl;
   cout << *(arr[1] + 1) << endl;
   cout << *(arr[1] + 2) << endl;
   cout << *(arr[1] + 3) << endl;

   //2.利用二维数组名来获取元素
   //关键知识:
   // 二维数组名 可以理解为一个 int (*)[4] 类型的指针
   //arr和*arr的区别
   //就像前面讲解的 p2 和 *p2 的区别
   cout << arr << endl;
   cout << *arr << endl;

   //对于我们来说,需要知道的是
   //arr + 1 会按行偏移
   //偏移后得到的地址是当前行的首地址
   //arr的使用规则,和数组指针p2使用规则是一样的
   //数组指针
   cout << arr + 1 << endl;
   
   //取出一维数组的地址
   //int* 指针
   cout << *(arr + 1) << endl;
   cout << **(arr + 1) << endl;

   cout << *(*(arr + 1) + 1) << endl;
   cout << *(*(arr + 1) + 2) << endl;
   cout << *(*(arr + 1) + 3) << endl;

   //二维数组名和数组指针有什么区别?
   //他们通过sizeof获取容量时
   //二维数组名得到的是实实在在的这个二维数组占用了多少个字节
   //数组指针(只要是指针)占用的内存空间是8个字节
   cout << sizeof(p2) << endl;
   cout << sizeof(arr) << endl;
   #pragma endregion

   #pragma region 知识点五 利用指针遍历数组
   cout << "遍历二维数组" << endl;
   int array[2][4] = { 1,2,3,4,
                       5,6,7,8 };
   //按元素 int指针
   int* p3 = &array[0][0];//array[0];
   //按行 int数组指针
   int (*ptr3)[4] = array;
   //行
   for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
   {
       //列
       for (int j = 0; j < sizeof(array[0])/sizeof(int); j++)
       {
           cout << array[i][j] << endl;
           cout << *p3++ << endl;
           cout << *(*(ptr3 + i) + j) << endl;
           cout << *(*(array + i) + j) << endl;
           cout << *(array[i] + j) << endl;
           cout << *(ptr3[i] + j) << endl;
       }
   }
   #pragma endregion

指针和字符数组

字符数组主要要注意的是获取长度不太容易,其他没什么差别

    #pragma region 知识回顾1 字符数组
    //1.字符数组就是类型为字符char的一维数组
    
    //2.字符数组可以用字符串初始化(默认会在结尾加上\0结束符号)
    char charArr[] = "Hellow World"; 
    
    //3.字符数组可以直接用于打印,但是需要用\0表示结束
    char charArr2[] = { 'A', 'B', 'C' };
    //会越界,知道遇到\0才会结束,会导致出现乱码
    cout << charArr2 << endl;

    //遇到\0就会停止
    char charArr3[] = { 'A', 'B', 'C', '\0'};
    cout << charArr3 << endl;
    #pragma endregion

    #pragma region 知识回顾2 指针和常量
    //指向常量的指针: const在 *前面,值不能改,指向可以改
    //指针常量:const在 *后面,值可以改,指向不能改
    //指向常量的指针常量:const在 *前后都加,值和指向都不能改了
    #pragma endregion

    #pragma region 知识回顾3 指针和一维数组建立联系
    //1.数组变量名
    char* ptrChar = charArr;
    //2.第一个元素的地址
    ptrChar = &charArr[0];
    #pragma endregion

    #pragma region 知识点一 指向常量的字符指针可以直接用字符串赋值
    //注意:老版本的C++中甚至是可以不用加const
    //指向能变、值不能变
    const char* ptrChar2 = "测试用字符串";
    #pragma endregion

    #pragma region 知识点二 字符指针可以直接用于打印
    //注意:指针指向的字符数组一定要有\0 否则一样会越界
    cout << ptrChar2 << endl;
    /*ptrChar2 = charArr2;
    cout << ptrChar2 << endl;*/
    #pragma endregion

    #pragma region 知识点三 利用字符指针遍历字符串
    //字符指针 用for循环遍历 就不太方便了 因为它有可能指向的是一个字符串字面量 不太好去获取容量
    //这种方式一定要保证 有\0 否则会越界
    /*while (*ptrChar2 != '\0')
    {
        cout << *ptrChar2;
        ptrChar2++;
    }*/
    //为了让字符指针不要自增减去改变指向的位置
    //我们可以加一个偏移量 来保证不动
   /* int index = 0;
    while (*(ptrChar2 + index) != '\0')
    {
        cout << *(ptrChar2 + index);
        ++index;
    }*/
    printPtrChar(ptrChar2);
    printPtrChar(ptrChar2);
    printPtrChar(ptrChar2);
    #pragma endregion
}

void printPtrChar( const char* p)
{
    int index = 0;
    while (*(p + index) != '\0')
    {
        cout << *(p + index);
        ++index;
    }
    cout << endl;
}

//总结
//字符指针在很多场景下可以当成字符串或者字符数组来使用

指针变量作为函数参数

由于形参和实参是拷贝的关系(不共享地址),所以传入实参后无法通过形参直接更改普通变量,这个时候我们需要传入指针来规避这个问题。(但是注意,这传入的指针和原指针地址也不同,只是记录的地址一样,所以解引用的地址一样)

#pragma region 知识回顾 按值传递
//当我们在使用函数时
//C++会把实参的副本传递给形参,这意味着函数内部的形参和外部的实参是两个独立的变量
//它们只是初始值相同,对形参的修改只会影响副本,不会改变原始的实参
//形参和实参本质上是两个不同的变量,只是变量中存储的值是相同的
void swap(int a, int b)
{
    int tmp = a;
    a = b;
    b = tmp;
    //形参交换结果
    cout << "形参交换结果" << endl;
    cout << "形参 a = " << a << endl;
    cout << "形参 b = " << b << endl;
}
#pragma endregion

#pragma region 知识点 指针变量作为函数参数
//当指针变量作为函数参数时
//实参传递给函数体后同样会生成指针变量副本
//但是!!!
//由于副本与原指针指向同一个门牌号
//因此如果修改指针变量指向房间中的内容
//原指针中指向房间中内容也会改变
//也就说,如果你想要在函数内部利用形参改变实参的值
//那么可以利用指针来传递参数,达到目的
void swap(int* aPtr, int* bPtr)
{
    int tmp = *aPtr;
    *aPtr = *bPtr;
    *bPtr = tmp;
    //形参交换结果
    cout << "形参交换结果" << endl;
    cout << "形参 *aPtr = " << *aPtr << endl;
    cout << "形参 *bPtr = " << *bPtr << endl;
}
#pragma endregion

指向函数的指针

类似C#里面的委托,可以把函数传来传去。

    //声明一个函数指针 指向一个结构一样的函数
    void (*func)() = nullptr;//test;
    func = test;

    test();
    func();

    cout << func << endl;
    cout << test << endl;

    (*func)();

    //声明一个函数指针 无返回值 有两个参数的
    void (*func2)(int, int) = test2;
    func2(11, 12);
    (*func2)(13, 14);

    //(*func)()和func() 有什么区别?
    //本质上没有任何区别,只是写法上不同而已
    //func()这样写 编译器在编译的时候其实也会将其转换为 (*func)()
    //因此我们平时其实会选择使用更简单的写法 直接函数指针名去调用指向的函数即可


#pragma region 知识点一 函数在内存中的存储
//我们之前学习了变量的存储
//变量是存储在内存中的小房间中的
//那么函数是否存储在内存中呢?
//答案是肯定的
//只不过它的存储区域有所不同!
// 
//当程序启动时,操作系统会将程序的各个部分(包括代码段、数据段、堆和栈)加载到内存中。
//在这个过程中,程序中定义的所有函数的机器码(即执行指令)会被加载到代码段中。
// 
//代码段:
//程序内存中的一部分,专门用于存储可执行代码。它是只读的,防止在执行过程中修改代码。
//而每个函数在代码段中都有一个入口地址
//这个地址指向该函数的第一条指令。
//函数名在编译时被映射为这个地址,使得我们可以通过函数名来调用函数!!
#pragma endregion

#pragma region 知识点二 指向函数的指针
//指向函数的指针(函数指针)在 C++ 中是一种强大的工具
//它允许你通过指针调用函数、实现回调函数、函数数组等功能
//声明语法:
//返回值类型 (*指针名)(参数类型列表)
//  int (*function_ptr)(int, int)
//  void (*function_ptr)()
//注意事项:
//1.用函数指针指向函数时,函数的返回类型和参数类型必须和函数指针结构上完全一致
//2.函数指针之所以需要用括号把*和指针名括起来,是用于区分 函数指针 和 返回指针的函数
//3.函数指针一般指 指向函数的指针
//  指针函数一般指 返回类型是指针的函数(之后会专门讲)

void test()
{
    cout << "test函数" << endl;
}

void test2(int a, int b)
{
    cout << a << endl;
    cout << b << endl;
}
#pragma endregion

回调函数

和C#类似的概念,即把函数指针传到别的函数里内部调用

 void doSomthing(void (*func_ptr)());
void laterDoSomthing();
void laterDoSomthing3();

void doSomthing2(int a, int b, void (*fun_ptr)(int));
void laterDoSomthing2(int i);
int main()
{
    std::cout << "回调函数\n";

    //传入的该laterDoSomthing函数,就可以称为回调函数 因为它会在另一个函数中被调用
    //具体调用时机 是由另一个函数中逻辑决定
    doSomthing(laterDoSomthing);
    doSomthing(laterDoSomthing3);

    doSomthing2(5, 7, laterDoSomthing2);
}

#pragma region 知识点一 什么是回调函数
//回调函数(Callback Function)
//是指通过函数指针将一个函数的地址传递给另一个函数
//并在合适的时机调用该函数,它通常用于延迟执行某些逻辑

//说人话:
//回调函数就是 一个函数作为参数传递给另一个函数,并在后者执行过程中调用前者 的一种用法
//先记录某一个函数,过一会儿再调用
//相当于 一会儿回头再调用 的函数
#pragma endregion

#pragma region 知识点二 如何使用回调函数
//将函数指针作为某一个函数A的参数,当调用函数A时,将函数作为参数进行传递
//那么这个被传递的函数指针就可以称为回调函数了

//有一个函数指针作为参数的函数,那么传入的这个函数指针 我们一般就称它为回调函数
void doSomthing(void (*func_ptr)())
{
    cout << "doSomthing要提前执行的逻辑" << endl;

    //执行传入的回调函数
    func_ptr();
}

void laterDoSomthing()
{
    cout << "晚点才会执行的逻辑" << endl;
}

void laterDoSomthing3()
{
    int a = 99;
    int b = 88;
    cout << a + b << endl;
}

void doSomthing2(int a, int b, void (*fun_ptr)(int))
{
    fun_ptr(a + b);
}

void laterDoSomthing2(int i)
{
    cout << i / 4.0f << endl;
}

#pragma endregion

#pragma region 知识点三 回调函数对于我们的意义
// 1.延迟执行函数逻辑
// 2.让回调执行的逻辑可以变化!
#pragma endregion

空类型指针调用函数

void*类型的指针可以接纳任何变量,然后调用的时候转化为对应的类型即可

void test();
int add(int a, int b);
int main()
{
    std::cout << "空类型指针调用函数\n";

    void* p = nullptr;
    int a = 10;
    p = &a;
    cout << *((int*)p) << endl;
    bool b = true;
    p = &b;

    void* func = test;
    (*((void (*)())func))();
    ((void (*)())func)();

    func = add;
    cout << ((int (*)(int, int))func)(10, 20) << endl;
    cout << (*((int (*)(int, int))func))(20, 30) << endl;
}

#pragma region 知识回顾 空类型指针
//空类型指针是一种特殊的指针
//它表示不特定类型的指针
//指针变量可以指向任意的变量类型
//具体存放何种类型,由后续赋值决定
//赋值后,如果需要使用需要强转为对应的数据类型
#pragma endregion

#pragma region 知识点一 空类型指针指向函数
//空类型指针可以指向任意类型的函数
void test()
{
    cout << "test" << endl;
}

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

#pragma region 知识点二 空类型函数指针的使用
//必须将空类型指针强转为 返回值类型,参数类型 符合规范的指针后使用
//注意:
//强转时省略函数指针名即可
#pragma endregion

函数返回值为指针

注意不要把局部指针变量传出来,因为局部变量会在执行后随时可能被销毁,导致指针变野指针。

解决方法是局部变量指向堆内存,但是这意味着你必须在外界即使对其delete防止内存泄漏

除了指针和&,其他返回普通类型和类对象的返回值基本都是安全的。C++会通过拷贝构造函数(或移动语义)生成副本,原始对象销毁不影响返回值

#pragma region 知识点一 返回值为指针的函数是什么
//顾名思义
//既然指针属于变量类型
//那么自然也可以作为函数的返回值
int* add(int a, int b)
{
    //局部变量
    int c = a + b;
    int* p = &c;
    return p;
}

int* add2(int a, int b)
{
    //静态变量
    static int c = a + b;
    return &c;
}

int* add3(int a, int b)
{
    result = a + b;
    return &result;
}

#pragma endregion

#pragma region 知识点二 返回指针变量指向地址的生命周期问题
//函数返回的指针返回值,需要分情况分析
//1.局部变量:不允许返回局部变量地址,因为局部变量在函数执行完毕后就会被释放
//           如果在外部使用可能不能得到正确结果,可能还会导致程序崩溃等问题
//           即使打印了正确的值,但也是偶然的,不可靠的,不可预知的
//           因此,不要在返回指针变量时,返回指向局部变量的指针
//2.全局变量:允许返回全局变量地址,因为全局变量生命周期从程序开始,一直持续到程序结束为止
//3.静态变量:允许返回静态变量地址,静态变量初始化后会一直占用内存空间,直到程序结束
#pragma endregion

//总结
//函数是指针返回值时,我们返回出去的地址
//不能是局部变量的地址,一定是全局变量或者静态变量的
//因为局部变量用完就会被释放,之后在外部使用可能会出问题
//而全局和静态变量不存在这样的问题,因为他们的生命周期是和程序“同生共死的”

指针数组

顾名思义就是用数组存指针,操作和普通数组差不多,就是解引用的时候记得解两次哦。

   #pragma region 知识回顾 数组指针
   //数组指针是我们在学习指针和数组中 二维数组相关知识时学习的
   //数组指针是一个指向整个数组的指针,本质上是一个指针,指向一个包含n个元素的数组

   //数组指针定义规则:变量类型 (*指针名)[元素个数]    
   //int (*p2)[4] 表示指向包含4个int元素的一维数组的指针
   int arr[3][4] = { 1,2,3,4,
                     5,6,7,8,
                     9,10,11,12 };
   int (*p)[4] = arr;
   cout << p << endl;
   cout << *p << endl;
   cout << (*p)[1] << endl;
   cout << *((*p) + 1) << endl;
   
   #pragma endregion

   #pragma region 知识点一 指针数组基本概念
   //指针数组是由n个指针组成的数组,每个指针可以指向一个单独的元素地址
   //定义规则:
   //变量类型* 数组名[数组长度];
   
   //重点:
   //指针数组中每个元素存储的是地址!!!!
   //想要获取地址中的值,需要加*

   //举例:
   int* p2[4];
   int a = 10;
   p2[0] = &a;
   //获取指定元素地址
   cout << p2[0] << endl;
   //获取指定元素值
   cout << *p2[0] << endl;
   
   #pragma endregion

   #pragma region 知识点二 指针数组的使用
   //声明
   int b = 11;
   int c = 12;
   int* p3[] = { &a, &b, &c };
   //查找地址
   cout << p3[0] << endl;
   cout << p3[1] << endl;
   cout << p3[2] << endl;
   //查找地址对应的值
   cout << *p3[0] << endl;
   cout << *p3[1] << endl;
   cout << *p3[2] << endl;
   //修改指向地址
   int d = 13;
   p3[0] = &d;
   cout << p3[0] << endl;
   cout << *p3[0] << endl;
   //修改值
   *p3[0] = 99;
   cout << p3[0] << endl;
   cout << *p3[0] << endl;
   cout << d << endl;

   //遍历
   //容量
   //得到指针数组一共占多少字节
   cout << "得到指针数组一共占多少字节:" << sizeof(p3) << endl;
   //得到指针中一个元素占多少字节
   cout << "得到指针中一个元素占多少字节:" << sizeof(p3[0]) << endl;
   //得到容量
   cout << "得到容量:" << sizeof(p3) / sizeof(p3[0]) << endl;

   for (int i = 0; i < sizeof(p3) / sizeof(p3[0]); i++)
   {
       cout << p3[i] << endl;
       cout << *p3[i] << endl;
   }

   //地址关系
   //数组名代表首地址(第一个元素的首地址)
   cout << p3 << endl;
   cout << &p3[0] << endl;
   //代表指针数组第一个元素中存储的地址
   cout << *p3 << endl;
   //代表指针数组第一个元素中存储的地址指向的值(根据类型来定具体值是什么)
   cout << **p3 << endl;

   //指针增减
   //第二个元素的首地址
   cout << p3 + 1 << endl;
   //第二个元素中存储的值(因为指针数组 所以是对应的一个地址)
   cout << *(p3 + 1) << endl;
   cout << **(p3 + 1) << endl;
   cout << "************" << endl;
   for (int i = 0; i < sizeof(p3) / sizeof(p3[0]); i++)
   {
       //指针数组中第i个元素的地址
       cout << p3 + i << endl;
       //指针数组中第i个元素中存储的值(因为是指针数组 所以取出来的也是地址)
       cout << *(p3 + i) << endl;
       //指针数组中第i个元素中存储的地址指向的值
       cout << **(p3 + i) << endl;
   }
   
   #pragma endregion

   //总结
   //对于指针数组来说
   //最重要的就是理解指针数组在内存中存储信息的关系
   //主要理解值和地址的关系
   //指针数组名和本身地址的关系即可

多级指针

顾名思义,套娃指针,项目里面不建议超过三次,对人脑有害哦

    #pragma region 知识点一 多级指针是什么
    //多级指针就是指向指针的指针
    //可以理解为一种指针的嵌套结构
    //是用来存储指针变量的地址的
    #pragma endregion

    #pragma region 知识点二 一级指针
    //我们目前常用的指针都是一级指针,主要用来存储普通变量的地址
    //举例
    int a = 99;
    int* p = &a;
    //变量的地址
    cout << p << endl;
    //变量的地址存储的值
    cout << *p << endl;
    #pragma endregion

    #pragma region 知识点三 二级指针
    //二级指针是用来指向 一级指针变量地址 的指针
    //举例
    int** pp = &p;
    //指针p的地址
    cout << pp << endl;
    //指针p的地址中存储的内容(由于指针p是指针,所以它存储的内容也是一个地址 - a的地址)
    cout << *pp << endl;
    //指针p的地址中存储的地址中对应的值
    cout << **pp << endl;
    #pragma endregion

    #pragma region 知识点四 三级指针
    //三级指针是用来指向 二级指针变量地址 的指针
    //举例
    int*** ppp = &pp;
    //得到的是2级指针变量对应的地址(2级指针地址) pp的地址
    cout << ppp << endl;
    //得到的是2级指针变量对应的地址当中存储的地址(1级指针地址) p的地址
    cout << *ppp << endl;
    //得到的是2级指针变量对应的地址当中存储的地址(1级指针地址)中存储的地址(变量的地址)a的地址
    cout << **ppp << endl;
    //a的地址中存储的值 a的值 99
    cout << ***ppp << endl;
    #pragma endregion

    #pragma region 知识点五 多级指针
    //多级指针的本质就是用来存储 指针变量的地址 的
    //多级指针可以有n级,第 n级 指针是用来存储 n-1 级指针的地址的
    //如果想要获取多级指针最底层的值,只需要再前面加n个*即可
    #pragma endregion

内存分配

内存存储区域

我们主要关注堆和栈内存


#pragma region 知识点一 不同的内存存储区域
//在程序运行时,系统中会有不同的内存存储区域
// 
//我们目前学习过的知识,涉及的存储区域有:
//1.代码段(Code Segment,或称代码存储区):
//  存储 函数的机器码(执行指令)等信息的区域
//  通常是只读的,防止程序意外修改自己的代码
//  在程序加载时就被加载到内存中
//  生命周期较长,与程序同生共死
// 
//2.数据段(Data Segment,或称数据存储区):存储 全局变量、静态变量 等信息的区域
//  一般分成两个子区域
//  初始化数据段:存储已初始化的全局和静态变量
//  未初始化数据段(BSS Segment):存储未初始化的全局和静态变量,通常初始化为零
//  生命周期较长,与程序同生共死
// 
//3.常量段(Constant Segment,或称常量存储区):存储 字符串字面量和其他只读数据 信息的区域
//  通常是只读的,以防止程序修改常量的值
//  比如 cout << "Hello, World" << endl 中的 "Hello, World" 就存储在常量区
//  生命周期由程序控制
//  注意:我们声明的常量变量不存储在常量段,而是根据在哪里声明决定存储在哪里
// 
//4.栈(Stack):存储 局部变量、函数参数、返回值 等信息的区域
//  由编译器自动管理,随着函数的调用和返回而分配和释放
//  生命周期与函数调用相同,快速分配和释放
// 
//未学习的存储区域:
//5.堆(Heap):存储 动态分配的内存数据 等信息的区域
//  由程序员手动管理,需要主动分配,主动释放
//  生命周期不固定,由程序员决定生死
#pragma endregion

#pragma region 知识点二 不同存储区域对于我们的影响
//由于 全局变量、静态变量、函数等内容
//在生命周期的角度上一般不会出现什么问题(不会出现被销毁了找不到的情况)
// 
//而会因为生命周期问题给我们带来麻烦的主要是栈和堆上存储的内容
//因此在整个开发生涯中我们需要明确区分存储在栈和堆上的内容

//栈(Stack)
//由编译器自动管理,我们无需手动管理
//堆(Heap)
//由程序员手动管理,需要主动分配,主动释放
#pragma endregion

#pragma region 知识点三 感受栈
//函数中的局部变量体现
//我们之前学习指针函数(返回值为指针的函数)充分表现了这一点
int* add(int a, int b)
{
    int c = a + b;
    int* p = &c;
    return p;
}
#pragma endregion

堆上分配内存

C++很有意思的一点是,它的任何变量几乎都可以指定堆内存和栈内存存储,而不像C#和类型绑定。

但是栈内存有个限制是不能装太大的值,如果太大只能用堆内存手动管理。

但是堆内存一定要注意即使delete避免内存泄漏的问题

  #pragma region 知识回顾 堆是什么
  //存储 动态分配的内存数据 等信息的区域
  //由程序员手动管理,需要主动分配,主动释放
  //生命周期不固定,由程序员决定生死
  #pragma endregion

  #pragma region 知识点一 如何在堆上分配内存
  //使用new关键字配合指针使用 即可在堆上分配内存
  //1.单个对象分配
  //语法:
  //变量类型* 指针名 = new 变量类型(初始值(可选))
  //作用:
  //new 的作用就是“开房”(分配内存房间,门牌号,和具体的内存容量)
  //注意:
  //指针变量本质上还是在栈上,只是其中存储的地址是堆上的地址
  //指针变量本身还是由系统管理,我们需要手动管理的是他指向的地址
  int* p = new int(10);
  cout << p << endl;
  cout << *p << endl;
  *p = 99;
  cout << p << endl;
  cout << *p << endl;

  char* c = new char('A');
  bool* b = new bool(true);


  //2.分配数组
  //语法:
  //数组类型* 指针名 = new 数组类型[容量] {初始值(可选)};
  //作用:
  //new的作用同样是“开房”(分配了n个连续的内存房间,具体是否初始化房间里的值,自己定)
  //这时 指针arr相当于就是数组名
  int* arr = new int[4] {1,2,3,4};
  cout << arr[0] << endl;
  cout << arr[1] << endl;

  int (*arr2)[3] = new int[2][3]{ 1,2,3,4,5,6 };
  #pragma endregion

  #pragma region 知识点二 如何在堆上释放内存
  //使用delete关键字 即可在堆上释放内存
  //语法:
  //delete 指针变量 —— 配对 new
  //delete[] 数组变量 —— 配对 new[]
  //作用:
  //delete的作用就是释放通过new在堆上分配的动态内存
  //注意:
  //delete的作用并不是销毁指针变量本身,而是销毁指针变量指向的堆内存

  //1.释放单个对象
  delete p;
  p = nullptr;

  delete c;
  c = nullptr;

  delete b;
  b = nullptr;

  //2.释放动态分配的数组
  //注意:释放数组时一定要加上[],否则只会释放第一个元素,会造成内存泄漏或崩溃
  delete[] arr;
  arr = nullptr;

  delete[] arr2;
  arr2 = nullptr;
  #pragma endregion

内存安全

堆内存一定要注意即使delete避免内存泄漏的问题。

空指针和野指针和内存越界的情况一定要注意检查,C++甚至不会对其报错!

 #pragma region 知识点一 什么是内存安全
 //内存安全(Memory Safety)指的是程序在访问和操作内存时不会因为出现程序问题
 //导致程序报错、崩溃的情况。
 //想要保证内存安全,我们主要保证以下几点即可
 //1.了解内存销毁时机
 //2.防止内存泄漏
 //3.防止越界访问
 //4.空指针检查
 //5.避免使用野指针
 #pragma endregion

 #pragma region 知识点二 了解内存销毁时机
 //1.栈内存
 //  在函数调用结束时,局部变量的内存会自动销毁。
 //  栈内存是自动管理的,一旦程序退出某个作用域(例如函数或代码块)
 //  该作用域内的局部变量的内存会自动释放
 //  因此我们只需注意不要在作用域外使用作用域内的在栈上分配的内存
 // 
 //2.堆内存
 //  使用 new 分配的堆内存需要在不使用后通过 delete 或 delete[] 进行释放
 //  因此我们只需要注意控制堆内存对象的生命周期,不用时一定要删除

 //堆和栈的其他区别:
 //1.内存空间大小不同
 //  栈的大小通常是固定的(比如几兆),是由操作系统决定的。
 //  因此我们不能在其中分配大内存
 //  堆的大小取决于系统可用的内存,理论上可以分配很大的内存
 //  因此我们可以在其中分配大内存

 //2.速度不同
 //  栈的分配和释放速度快,因为是由系统直接管理
 //  堆的分配和释放速度较慢

 //3.线程安全
 //  栈是线程私有的,不同线程的栈彼此独立,是线程安全的
 //  堆是进程共享的,多线程访问时需要保证线程安全

 //如何应用
 //栈:适用于小规模,短生命周期的数据
 //堆:适用于动态分配的大规模数据,或生命周期跨越函数调用的数据
 #pragma endregion

 #pragma region 知识点三 防止内存泄漏
 //内存泄漏主要指不再被使用的内容一直占用内存资源
 //在使用堆内存时很容易造成这样的结果
 //1.堆内存忘记释放(一直占用堆内存)
 //2.丢失对已分配堆内存的记录(无法释放,一直占用堆内存)
 //出现以上类似的情况就会造成内存泄漏
 //当内存达到一定的瓶颈是就会崩溃或报错,程序无法正常运行
 //int* p = new int(10);
 //让原本指向堆内存的指针在没有释放之前指向了别的内容
 /*p = nullptr;
 p = new int(11);*/
 #pragma endregion

 #pragma region 知识点四 防止越界访问
 //我们需要限制数组或指针的访问范围
 //要避免访问超出分配范围的内存
 #pragma endregion

 #pragma region 知识点五 空指针检查
 //养成好习惯
 // 1.在声明指针没有初始化时对它赋值为nullptr
 // 2.对于指向堆内存的指针,释放后,同样对它赋值为nullptr
 //这样在使用指针之前判断它是否为nullptr空指针再决定如何使用它
 int* p = nullptr;
 p = new int(10);
 delete p;
 p = nullptr;

 if (p != nullptr)
 {

 }
 #pragma endregion

 #pragma region 知识点六 避免使用野指针
 //避免指针指向已释放的内存空间
 //确定指针指向的内存不能用时,一定置空
 #pragma endregion