V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
yiouejv
V2EX  ›  C++

我整理了这篇指针的知识点,想必对你有用

  •  
  •   yiouejv · 2021-02-24 23:46:32 +08:00 · 2193 次点击
    这是一个创建于 1366 天前的主题,其中的信息可能已经有所发展或是发生改变。

    指针和引用的区别

    1. 非空区别: 任何情况下都不能使用指向空值的引用,一个引用必须总是指向某些对象。 指针可以指向空。
    2. 合法性区别: 引用在使用之前不需要测试合法性,指针应该总是要被测试,防止其为空
    3. 可修改性区别: 引用在初始化时指定对象,以后不能修改。

    指针传递动态内存

    例 1: 程序测试后会有什么结果?

    #include<iostream>
    #include<cstring>
    using namespace std;
    
    void getMemory(char*p, int num)
    {
        p = (char*)malloc(sizeof(char) * num);
    }
    
    int main(int argc, const char* argv[])
    {
        char *str = NULL;
        getMemory(str, 100);
        strcpy(str, "hello");
        return 0;
    }
    

    问题出现在getMemory里,编译器总是为函数的每个参数制作一个临时副本。在本题中,p 为 str 的拷贝,p 申请了一个新的内存空间,但是并没有影响到 str,str 还是 NULL,再调用 strcpy(), 则会使代码崩溃,并且 p 申请的内存也一直没用释放,造成内存泄露。

    正确的方法是把往 getMemory 内传入 str 的地址。

    #include<iostream>
    #include<cstring>
    using namespace std;
    
    void getMemory(char**p, int num)
    {
        *p = (char*)malloc(sizeof(char) * num);
    }
    
    int main(int argc, const char* argv[])
    {
        char *str = NULL;
        getMemory(&str, 100);
        strcpy(str, "hello");
        cout << str << endl;
        return 0;
    }
    

    不过这样写有些麻烦,我们可以直接把申请好的内存返回。

    #include<iostream>
    #include<cstring>
    using namespace std;
    
    char* getMemory(int num)
    {
        return (char*)malloc(sizeof(char) * num);
    }
    
    int main(int argc, const char* argv[])
    {
        char *str = NULL;
        str = getMemory(100);
        strcpy(str, "hello");
        cout << str << endl;
        return 0;
    }
    

    例 2: 这个函数有什么问题?

    char* strA()
    {
        char str[] = "hello world";
        return str;
    }
    

    str 里存的地址是函数 strA 栈帧里"hello world"的首地址,函数调用完成,临时空间被重置。

    如果要获得正确的函数,可以这样写:

    char* strA()
    {
        char *str = "hello world";
        return str;
    }
    

    首先要搞清楚char str[], char* str,

    char str[] 分配的是一个局部数组,

    char* str 分配的是一个指针遍历,

    局部数组是局部变量,它对应的是内存中的栈。指针变量是全局变量,它对应的是内存中的全局区域。

    不过上述代码只能 c 语言这么写,c++不允许

    ISO C++ forbids converting a string constant to ‘char*’

    可以这么写:

    char* strA()
    {
        static char str[] = "hello world";
        return str;
    }
    

    例 3: 下面代码的输出结果是什么?

    #include<iostream>
    using namespace std;
    
    class A
    {
    public:
        A() { m_a = 1; m_b = 2; }
        ~A(){}
        void fun() { printf("%d%d", m_a, m_b);}
    private:
        int m_a;
        int m_b;
    };
    
    class B
    {
    public:
        B() {m_c = 3;}
        ~B();
        void fun() {printf("%d", m_c);}
    private:
        int m_c;
    };
    
    
    int main(int argc, const char* argv[])
    {
        A a;
        B *pb = (B*)(&a);
        pb->fun();
        return 0;
    }
    

    这道题的目的就是考察你对内存偏移的理解,

    B* pb = (B*)(&a);, 这是一个野蛮的转换,强制把 a 地址内容看成一个 B 类的对象,pb 指向的是 a 类的内存空间, 把 a 类空间按照 B 类的结构来解读。

    函数指针

    例 1: 找出下列程序的错误

    #include<iostream>
    using namespace std;
    
    int Max(int x, int y)
    {
    	return x > y ? x : y;
    }
    
    int main(int argc, char const *argv[])
    {
    	int *p = &Max;
    	cout << p(2, 1) << endl;
    	return 0;
    }
    

    这道程序提存在着函数指针的错误使用问题,正确的写法为: int (*p)(int, int) = &Max;

    int *p p 是 int 型的指针

    int *p(int, int), p 是一个函数,返回值为int*

    int (*p)(int, int), p 是一个指针,指向函数的地址,函数的返回值为 int

    例 2: 下面的数据声明都代表什么?

    float(**def)[10];
    double*(*gh)[10];
    double(*f[10])();
    int*((*b)[10]);
    long(*fun)(int)
    int(*(*F)(int, int))(int)
    

    答案如下:

    float(**def)[10];  // def 是二级指针,指向一级指针,一级指针指向数组,数组的大小为 10,数组元素类型为 float
    double*(*gh)[10];  // gh 是一级指针,指向一个数组,数组大小为 10,数组元素的类型为 double*
    double(*f[10])();  // f 是一个数组,数组大小为 10,数组的元素类型为指针,指针指向的类型为 double() 的函数
    int*((*b)[10]);  // b 是一个指针,指向一个数组,数组的大小为 10,数组元素的类型为 int*
    long(*fun)(int)  // fun 是一个函数指针,指向 long(int) 型的函数
    int(*(*F)(int, int))(int)  // F 是一个指针,指向一个函数,函数的参数为(int, int), 函数的返回值是一个指针,指向一个函数,函数的参数为(int), 函数的返回值为 int
    

    指针数组和数组指针

    例 1: 以下程序的输出是什么?

    #include<iostream>
    using namespace std;
    
    int main(int argc, char const *argv[])
    {
        int v[2][10] = {
            {1,2,3,4,5,6,7,8,9,10},
            {11,12,13,14,15,16,17,18,19,20},
        };
        int (*a)[10] = v;       // 数组指针是一个二级指针
        cout << a << endl;      // a 是一个指针,指向 {1,2,3,4,5,6,7,8,9,10}
        cout << *a << endl;     // *a 也是一个指针,指向 1 的地址
        cout << **a << endl;    // **a 取 1 的值
    
        cout << a + 1 << endl;  // 指针向后偏移一个位置,这个位置的长度为指针所指容量的大小,偏移后指向 {11,12,13,14,15,16,17,18,19,20}
        cout << *(a + 1) << endl;  // 和*a 的原理是一样的,指向 11 的地址
        cout << **(a + 1) << endl;  // 取 11 的值
    
        cout << *a + 1 << endl;  // *a 指向 1 的地址,*a + 1, 指针向后偏移一个位置,*a 指向的是 int 型的数据 m, 向后偏移 sizeof(int),指向 2
        cout << *(*a+1) << endl;  // 取 2
        return 0;
    }
    

    例 2: 用变量 a 给出下面的定义

    1. 一个整型数。
    2. 一个指向整型数的指针。
    3. 一个指向指针的指针,它指向的指针是一个指向一个整型数
    4. 一个有 10 个整型数的数组
    5. 一个有 10 个指针的数组,该指针是指向一个整型数的
    6. 一个指向有 10 个整型数数组的指针
    7. 一个指向函数的指针,该函数有一个整型参数并返回一个整型数
    8. 一个有 10 个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数

    答案:

    // 1
    int a;
    // 2
    int *a;
    // 3
    int **a;
    // 4
    int a[10];
    // 5
    int *a[10];
    // 6
    int (*a)[10];
    // 7
    int (*a)(int);
    // 8
    int (*a[10])(int);
    

    例 3: 写出如下程序片段的输出

    int a[] = {1,2,3,4,5};
    int *ptr = (int*)(&a + 1);
    printf("%d %d", *(a+1), *(ptr - 1));
    

    答案:

    #include<iostream>
    #include<cstdio>
    using namespace std;
    
    int main(int argc, char const *argv[])
    {
        int a[] = {1,2,3,4,5};
        int *ptr = (int*)(&a + 1);
    
        cout << a << endl;  // 数组名的一个指向数组元素的常量指针
        cout << &a << endl;  // &a 并不是一个指向常量指针的指针,而是一个指向整个数组的指针
    
        // 以下两行验证以上的观点
        cout << a + 1 << endl;
        cout << &a + 1 << endl;
    
        // 所以 a + 1 指向 2,*(a+1) 为 2
        // &a + 1 应该指向 5 的下一个元素,ptr - 1 指向 5
        printf("%d %d", *(a+1), *(ptr - 1));
        return 0;
    }
    

    迷途指针和野指针

    迷途指针: 指针指向一个内存,这个内存会回收了,但是没有对这个指针做处理,没有将指针设为空

    野指针: 声明了一个指针,没有将指针初始化。

    例 1: 下面的程序输出结果是什么?

    #include<iostream>
    using namespace std;
    
    int main(int argc, char const *argv[])
    {
        char s1[] = "hello";
        char s2[] = "the";
        char s3[] = "world";
        char* a[] = {s1, s2, s3};
        char **pa = a;
        pa++;
        cout << *pa << endl;
        return 0;
    }
    

    a 是一个常量指针,指向数组的首地址,pa++, 向后挪一个指针大小,指向 s2, 输出 "the"

    指针和句柄

    句柄是一个整数,是操作系统在内存中维护的一个对象,内存物理地址列表的整数索引,因为操作系统在内存管理时经常会将当前空闲对象的内存释放掉,当需要访问时再重新提交到物理内存,所以对象的物理地址是变化的,不允许程序直接通过物理地址来访问对象。 程序将想访问的对象的句柄传递给系统,系统根据句柄检索自己维护的对象列表就能知道程序想访问的对象及其物理地址了。

    句柄是一种指向指针的指针。 我们知道,所谓指针是一种内存地址,应用程序启动后,组成这个程序的各对象是驻留在内存的。如果简单的理解,似乎我们只要知道内存的首地址,那么就可以随时用这个地址访问对象。 但是,如果真的这样认为,那么就大错特错了,我们知道,操作系统是一个以虚拟内存为基础的,存在换页现象,对象呗移动意味着它的地址变化了,如果地址总是变化,我们该怎么寻找对象呢? 为了解决这个问题,操作系统为各应用程序腾出一些内存地址,用来专门登记各应用对象在内存中的地址变化,而登记的地址是不变的,操作系统移动对象后,将对象的地址告诉给句柄,通过句柄就能知道对象具体的位置了。

    句柄--> 登记对象地址的地址 --> 对象的地址 --> 对象

    程序每次重新启动,系统不保证分配给这个程序的句柄还是原来的句柄,就好比去电影院每次卖给我们的都不是同一个座位。

    this 指针

    关于 this 指针,有这样一段描述: 当你进入一个房子后,你可以看见桌子,椅子,等,但是你看不到全貌了

    对于一个类的实例来说,你可以看到成员函数,成员变量,但是实例本身呢? this 指针就是这样一个指针,时时刻刻指向实例本身

    1. this 指针本质是一个函数参数,只是编译器隐藏起来的,语法层面的参数,实际上,成员函数默认第一个参数为 T* const this
    2. this 在成员函数的开始前构造,结束后清除。

    a.func(10); 会被编译器编译成 A::func(&a, 10);, 看起来和静态函数没区别,不过,区别还是有的。 编译器通常会对 this 指针做一些优化,this 指针的传递效率比较高,如 VC 通常是通过 ecx 寄存器传递。

    1. this 指针并不占用对象空间,this 相当于非静态成员函数的一个隐含的参数,不占对象空间。
    2. this 指针存放在何处? this 指针会因编译器不同而有不同的位置,可能是堆,栈,也可能是寄存器。
    3. this 指针是如何传递给类中的函数的? 大多数编译器是通过 ecx 寄存器传递 this 指针,事实上,这也是一个潜规则,一半来说,不同编译器都会遵从一致的传参原则,否则不同的编译器产生的 obj 就无法匹配了
    4. 我们只有获得一个对象后,才能通过对象使用 this 指针,如果我们知道对象 this 的位置,可以直接使用吗?

    this 指针只有在成员函数中才有定义。 因此,你获得一个对象后,也不能通过对象使用 this 指针,只有在成员函数内才有 this

    4 条回复    2021-02-25 22:42:08 +08:00
    wutiantong
        1
    wutiantong  
       2021-02-25 00:40:53 +08:00   ❤️ 7
    所以中文编程技术社区一直存在的一个严重问题是,无论是在书本上还是论坛博客上,到处都可能充斥着似是而非,混淆概念,不够精确的表达。就以这篇文章来说:

    1. 指针可以指向空。
    2. 指针应该总是要被测试,防止其为空
    3. 编译器总是为函数的每个参数制作一个临时副本。
    4. p 申请了一个新的内存空间。。。并且 p 申请的内存。。。
    5. 我们可以直接把申请好的内存返回。
    6. str 里存的地址是函数 strA 栈帧里"hello world"的首地址
    7. (这里整段都是错误的,包括代码) char str[] 分配的是一个局部数组,char* str 分配的是一个指针遍历 。。。 指针变量是全局变量,它对应的是内存中的全局区域。
    8. (又在讲错误的东西)强制把 a 地址内容看成一个 B 类的对象,pb 指向的是 a 类的内存空间, 把 a 类空间按照 B 类的结构来解读。
    9. *a 也是一个指针,指向 1 的地址
    10. 数组名的一个指向数组元素的常量指针。。。&a 并不是一个指向常量指针的指针
    bfdh
        2
    bfdh  
       2021-02-25 10:55:47 +08:00
    指针真的是越解释越乱,指针就是一个地址而已,那些所谓的指针类型不过是解读指针指向内容的方法而已,都不影响指针的本质。
    jones2000
        3
    jones2000  
       2021-02-25 13:29:31 +08:00
    @bfdh "指针真的是越解释越乱" 这样才能有突出有难度,好忽悠, 1 句话就解释清楚了, 后面这么卖文章和课程呢.
    yiouejv
        4
    yiouejv  
    OP
       2021-02-25 22:42:08 +08:00
    我又不是培训机构,自己学习分享而已。不卖课哈
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1161 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 18:08 · PVG 02:08 · LAX 10:08 · JFK 13:08
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.