从Python语言的角度看C++的指针

技术背景

从一个Python Coder的角度来说,其实很羡慕C++里面指针类型的用法,即时指针这种用法有可能会给程序带来众多的不稳定因素(据C++老Coder所说)。本文主要站在一个C++初学者的角度来学习一下指针的用法,当然,最好是带着一定的Python基础再去学习C++的逻辑,会更容易一些。

内存地址赋值

一般认为,指针就是一个内存地址。其实Python中也可以获取内存地址,但如果直接使用Python的内存地址作为指针,那会是一个非常hacky的操作。使用内存地址有一个非常重要的好处,就是可以在不改动指针的情况下,直接在其他函数内修改指针对应的数据,直接避免了非必要的传参。比如下面这个示例:

// g++ main.cpp -o main && ./main
#include <iostream>
class Check{
    public:
        int* p;
    void func_1(int* p);
};

void Check::func_1(int* p){
    printf("%d\n", *p);
}

int main(){
    int var = 1;
    Check new_check;
    new_check.p = &var;
    new_check.func_1(new_check.p);
    var++;
    new_check.func_1(new_check.p);
}

在这个示例中,我们把var这个变量的内存地址作为new_check的一个属性值,然后在不改变new_check对象本身的情况下,我们在外部修改了var的值。那么在修改var前后,同样使用new_check的一个打印函数去打印指针所指向的内容,我们发现指针指向的内容已经被改变了:

$ g++ main.cpp -o main && ./main
1
2

在Python的常规编程中,如果不直接对new_check.p进行修改或者重新复制,我们是没办法改变new_check.p的值的,这是使用C++指针的好处之一。

多重索引

多重的指针,有点类似于一个链表的数据结构,在Python中必须要实现一个链表或者使用多层赋值的NamedTuple,但是在C++里面用起来就非常的自然:

// g++ main.cpp -o main && ./main
#include <iostream>
class Check{
    public:
        int** p;
    void func_1(int** p);
};

void Check::func_1(int** p){
    printf("%d\n", **p);
}

int main(){
    int var = 1;
    int num = 2;
    int* p_out = &var;
    Check new_check;
    new_check.p = &p_out;
    new_check.func_1(new_check.p);
    p_out = &num;
    new_check.func_1(new_check.p);
}

这里我们修改的是第二重指针指向的变量,从原来的指向var,变成了指向num的一个指针。由于我们把这个第二重的指针赋值给了第一重指针的索引,所以这里我们改变第二重指针指向的变量之后,第一重指针指向的最终变量也会发生变化:

$ g++ main.cpp -o main && ./main
1
2

数组指针

C++中可以用一个指针ptr指向数组的第一个元素,然后通过迭代指针的方法,例如使用ptr++来指向数组的下一个元素。

// g++ main.cpp -o main && ./main
#include <iostream>
#include <vector>
using namespace std;

int main(){
    int var[] = {1, 2, 3, 4, 5};
    vector<int> g{-1}, l{-1};
    int *t = nullptr;
    int len = sizeof(var) / sizeof(var[0]);
    for (int i=0; i<len; i++){
        if (var[i] <= 2){
            l.push_back(var[i]);
        }
        else{
            g.push_back(var[i]);
        }
    }
    g.push_back(6);
    t = &g[0];
    for (int i=0; i<g.size()-1; i++){
        t++;
        printf("%d\n", *t);
    }
}

输出的结果为:

$ g++ main.cpp -o main && ./main
3
4
5
6

这里需要注意的一点是,在这个代码中把数组的第一个元素赋值给指针是在数组完成更新之后操作的。如果在这之前操作,会因为push_back的操作导致指针移位,使得定义好的指针不再有意义,输出的结果也会是不可预测的随机值。只有这种原位替换,才不会影响到指针的指向:

// g++ main.cpp -o main && ./main
#include <iostream>
#include <vector>
using namespace std;

int main(){
    int var[] = {1, 2, 3, 4, 5};
    int *t = nullptr;
    int len = sizeof(var) / sizeof(var[0]);
    t = &var[0];
    var[1] *= -1;
    for (int i=0; i<len; i++){
        printf("%d\n", *t);
        t++;
    }
}

这个案例中我们在定义了数组指针之后,又修改了数组var的第二个元素,输出结果如下:

1
-2
3
4
5

这里我们就可以看到,第二个元素被成功修改,但通过指针还是可以正常的索引到这个数组。

指针应用

这里我们用指针来完成一个“打格点的任务”。简单描述就是,将三维空间划分成若干个网格,然后将处于同一个网格的原子序号输出出来。这里使用的空间坐标,我们用c++的随机数生成器产生一个均匀分布的随机二维矩阵:

#include <vector>
#include <random>
using namespace std;

vector<vector<float>> random_crd(int random_seed, int n_atoms, int dimensions){
    std::default_random_engine e;
    // 产生[-0.5, 0.5]之间的均匀分布随机数
    std::uniform_real_distribution<double> u(-0.5, 0.5);
    e.seed(random_seed);
    // 初始化一个shape为(n_atoms, dimensions)的矩阵,所有的元素初始化为0
    vector<vector<float>> crd(n_atoms, vector<float>(dimensions, 0));
    for (int i=0; i<n_atoms; i++){
        for (int j=0; j<dimensions; j++){
            crd[i][j] = u(e);
        }
    }
    return crd;
}

这个产生的向量的形式大致是这样的:

// g++ main.cpp -o main && ./main
#include <iostream>
#include <vector>
#include <random>
using namespace std;

vector<vector<float>> random_crd(int random_seed, int n_atoms, int dimensions){
    std::default_random_engine e;
    std::uniform_real_distribution<double> u(-0.5, 0.5);
    e.seed(random_seed);
    vector<vector<float>> crd(n_atoms, vector<float>(dimensions, 0));
    for (int i=0; i<n_atoms; i++){
        for (int j=0; j<dimensions; j++){
            crd[i][j] = u(e);
        }
    }
    return crd;
}

int main(){
    int N = 10;
    int D = 3;
    vector<vector<float>> crd = random_crd(0, N, D);
    for (int i=0; i<N; i++){
        for (int j=0; j<D; j++){
            printf("%f,", crd[i][j]);
        }
        printf("\n");
    }
}

打印输出结果为:

-0.368462,-0.041350,-0.281041,
0.178865,0.434693,0.019416,
-0.465428,0.029700,-0.492302,
-0.433158,0.186773,0.430436,
0.026929,0.153919,0.201191,
0.262198,-0.452535,-0.171766,
0.256410,-0.134661,0.482550,
0.253356,-0.427314,0.384707,
-0.063589,-0.022268,-0.225093,
-0.333493,0.397656,-0.439436,

我们可以先简单的假设把这个-0.5到0.5的范围切成8个正方体,或者叫8个Grid。总粒子数为N,然后可以假设每个Grid中的粒子数有一个上限M。这样一来我们可以构造一个(8,M)的矩阵用于存储每一个Grid中的原子序号,然后用一个大小为N的指针数组来定位每一个Grid中当前的索引记录。

// g++ main.cpp -o main && ./main
#include <iostream>
#include <vector>
#include <random>
using namespace std;
// 产生一个随机初始化的空间坐标
vector<vector<float>> random_crd(int random_seed, int n_atoms, int dimensions){
    std::default_random_engine e;
    std::uniform_real_distribution<double> u(-0.5, 0.5);
    e.seed(random_seed);
    vector<vector<float>> crd(n_atoms, vector<float>(dimensions, 0));
    for (int i=0; i<n_atoms; i++){
        for (int j=0; j<dimensions; j++){
            crd[i][j] = u(e);
        }
    }
    return crd;
}
// 将空间格点化,输出位于每一个格点中的原子序号
vector<vector<int>> grids(vector<vector<float>> crd, int *grid_ptr[8], int max_atoms){
    int grid_index = 0;
    vector<vector<int>> grid_atoms(8, vector<int>(max_atoms, -1));
    for (int i=0; i<crd.size(); i++){
        // 计算当前原子的坐标所处的格点序号
        grid_index += 4 * static_cast<int>(crd[i][0] > 0);
        grid_index += 2 * static_cast<int>(crd[i][1] > 0);
        grid_index += 1 * static_cast<int>(crd[i][2] > 0);
        // 向对应格点矩阵中添加原子序号
        if (grid_ptr[grid_index] == nullptr){
            grid_atoms[grid_index][0] = i;
            grid_ptr[grid_index] = &grid_atoms[grid_index][0];
        }
        else{
            // 指针移位
            grid_ptr[grid_index]++;
            *(grid_ptr[grid_index]) = i;
        }
        grid_index = 0;
    }
    return grid_atoms;
}

int main(){
    int N = 10;
    int D = 3;
    int M = 4;
    vector<vector<float>> crd = random_crd(0, N, D);
    // 初始化一个nullptr空指针
    int *grid_ptr[8];
    for (int i=0; i<8; i++){
        grid_ptr[i] = nullptr;
    }
    // 计算格点化
    vector<vector<int>> grid_atoms = grids(crd, grid_ptr, M);
    // 打印输出
    for (int i=0; i<8; i++){
        for (int j=0; j<M; j++){
            printf("%d,", grid_atoms[i][j]);
        }
        printf("\n");
    }
    return 0;
}

上述代码的运行结果为:

$ g++ main.cpp -o main && ./main
0,8,-1,-1,
-1,-1,-1,-1,
2,9,-1,-1,
3,-1,-1,-1,
5,-1,-1,-1,
6,7,-1,-1,
-1,-1,-1,-1,
1,4,-1,-1,

如果把参数改为:20个原子、单格点最大原子数为5,得到的输出结果为:

0,8,11,-1,-1,
-1,-1,-1,-1,-1,
2,9,17,-1,-1,
3,15,18,-1,-1,
5,10,12,16,19,
6,7,13,-1,-1,
-1,-1,-1,-1,-1,
1,4,14,-1,-1,

整体来说这个实现方法用起来还是比较灵活的。

总结概要

本文主要是站在一个有一定的Python经验的C++新手的角度,学习一下C++中的指针使用方法。指针其实就是一个内存地址的标记,同时在用法上也跟Python中的迭代器很相似,可以通过指针移位来标记下一个需要读取或者更新的位置。通过这一篇文章,可以掌握指针对象的赋值、多重指针的使用和数组指针的使用,以及最后我们介绍了一个基于指针数组来实现的空间格点划分算法。

版权声明

本文首发链接为:https://www.cnblogs.com/dechinphy/p/pointer.html

作者ID:DechinPhy

更多原著文章:https://www.cnblogs.com/dechinphy/

请博主喝咖啡:https://www.cnblogs.com/dechinphy/gallery/image/379634.html

posted @ 2024-03-05 10:54  DECHIN  阅读(358)  评论(0编辑  收藏  举报