• C++-指针


    指针

    指针(Pointer)就是内存的地址,C语言允许用一个变量来存放指针,这种变量称为指针变量。指针变量可以存放基本类型数据的地址,也可以存放数组、函数以及其他指针变量的地址。

    程序在运行过程中需要的是数据和指令的地址,变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符
    在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址;程序被编译和链接后,这些名字都会消失,取而代之的是它们对应的地址。

    常见指针变量的定义

    int *p;	p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组。
    int **p;	p 为二级指针,指向 int * 类型的数据。
    int *p[n];	p 为指针数组,数组里存放的都是指针。[] 的优先级高于 *,所以应该理解为 int *(p[n]);
    int (*p)[n];	p 为二维数组指针,指向数组。
    int *p();	p 是一个函数,它的返回值类型为 int *int (*p)();	p 是一个函数指针,指向原型为 int func() 的函数。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    指针的特点:

    1. 指针变量可以进行加减运算,例如p++、p+i、p-=i。指针变量的加减运算并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关。

    2. 给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数,例如int *p = 1000;是没有意义的,使用过程中一般会导致程序崩溃。

    3. 使用指针变量之前一定要初始化,否则就不能确定指针指向哪里,如果它指向的内存没有使用权限,程序就崩溃了。对于暂时没有指向的指针,建议赋值NULL。

    4. 两个指针变量可以相减。如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间相差的元素个数。

    5. 数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和 sizeof、& 运算符一起使用时数组名才表示整个数组,表达式中的数组名会被转换为一个指向数组的指针。

    空指针NULL和void指针

    空指针:
    定义:指针变量指向内存中编号为0的空间

    用途:初始化指针变量

    注意:空指针指向的内存是不可以访问的

    //指针变量p指向内存地址编号为0的空间
    	int * p = NULL;
    	//访问空指针报错 
    	//内存编号0 ~255为系统占用内存,不允许用户访问
    	cout << *p << endl;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    void指针:
    void 用在函数定义中表示函数没有返回值,用在指针这里表示指针指向的数据的类型是未知的。

    也就是说,void *表示一个有效指针,它确实指向实实在在的数据,只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换。

    C语言动态内存分配函数 malloc() 的返回值就是void *类型,在使用时要进行强制类型转换,如下:

     //分配可以保存30个字符的内存,并把返回的指针转换为 char *
    char *str = (char *)malloc(sizeof(char) * 30);
    
    • 1
    • 2

    NULL和nullptr

    NULL到底是什么?
    NULL 并不是 C++ 的关键字,它是 C++ 为我们事先定义好的一个宏,并且它的值往往就是字面量 0(#define NULL 0)。

    C++ 中将 NULL 定义为字面常量 0,虽然能满足大部分场景的需要,但个别情况下,它会导致程序的运行和我们的预期不符。例如:

    void isnull(void *c){
        cout << "void*c" << endl;
    }
    void isnull(int n){
        cout << "int n" << endl;
    }
    int main() {
        isnull(0);
        isnull(NULL);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    程序执行结果为:

    int n
    int n
    
    • 1
    • 2

    对于 isnull(NULL),我们期望它实际调用的是参数为 void*c 的 isnull() 函数,但观察程序的执行结果不难看出,并不符合我们的预期。

    为了解决NULL的这一不足,在 C++11 标准中引入一个新关键字,即 nullptr

    nullptr 是一个关键字,专用于初始化空类型指针,所以开发中一般建议将空指针初始化为nullptr

    野指 针

    如果一个指针指向的内存没有访问权限,或者指向一块已经释放掉的内存,那么就无法对该指针进行操作,这样的指针称为野指针(Wild Pointer)。

    1、指向的内存没有访问权限

    #include <stdio.h>
    int main(){
        char *str;
        gets(str);
        puts(str);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    分析:
    str 是局部变量,它的值是不确定的,是随机的,不知道指向哪块内存。一般情况下,这块内存要么没有访问权限,要么还没有分配,当 gets() 函数试图将读取到的字符串写入这块内存时,必然会发生错误。

    2、

    int main(){
        char *str = (char*)malloc(20*sizeof(char));
        strcpy(str, "C语言123456");
        puts(str); //C语言123456
        free(str);
        //应该在这里设置 str=NULL,否则就会产生野指针
        if(str){
            puts(str);//乱码
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    分析:
    free() 只是释放掉了动态分配的内存,但并未改变 str 的值,str 的值不是 NULL,它仍然指向被释放掉的内存,所以会执行 if 语句里面的 puts() 函数。但由于此时的内存已经被释放掉了,原来的字符串已经不在了,所以输出的数据是未知的。

    要想规避野指针,就要养成良好的编程习惯:

    1. 指针变量如果暂时不需要赋值,一定要初始化为NULL
      因为任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。

    2. 当指针指向的内存被释放掉时,要将指针的值设置为 NULL
      因为 free() 只是释放掉了内存,并为改变指针的值。

    const修饰指针

    const修饰指针有三种情况

    1. const修饰指针 — 常量指针
    2. const修饰常量 — 指针常量
    3. const既修饰指针又修饰常量
    int main() {
    
    	int a = 10;
    	int b = 10;
    
    	//const修饰的是指针,指针指向可以改,指针指向的值不可以更改
    	const int * p1 = &a; 
    	p1 = &b; //正确
    	//*p1 = 100;  报错
    	
    
    	//const修饰的是常量,指针指向不可以改,指针指向的值可以更改
    	int * const p2 = &a;
    	//p2 = &b; //错误
    	*p2 = 100; //正确
    
        //const既修饰指针又修饰常量,都不能改
    	const int * const p3 = &a;
    	//p3 = &b; //错误
    	//*p3 = 100; //错误
    
    	system("pause");
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    const的使用场景:
    在C语言中,单独定义 const 变量没有明显的优势,完全可以使用#define命令代替。
    const 通常用在函数形参中,如果形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用 const 来限制。
    如下:

    size_t strlen ( const char * str );
    
    • 1

    指针和引用

    什么是引用?

    int main() {
        int a = 99;
        int &r = a;
        cout << a << ", " << r << endl; //99, 99
        cout << &a << ", " << &r << endl; //0x28ff44, 0x28ff44
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上面例子中,变量 r 就是变量 a 的引用,它们用来指代同一份数据;也可以说变量 r 是变量 a 的别名。

    指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的
    它可以被改变,包括两种改变,
    1、指针指向的改变
    2、指向地址存放的数据的改变。

    引用是一个别名,它在逻辑上不是独立的,它是依附于变量而存在的,所以其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。

    指针和引用的相同点和不同点:

    ★相同点:

    ●都是地址的概念;指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。

    ★不同点:

    ●指针是一个实体,而引用仅是个别名;

    ●引用“从一而终”,指针可以“见异思迁”;

    ●引用不能为空,指针可以为空;

    ●引用是类型安全的,而指针不是 ,因为引用比指针多了类型检查

    ●“sizeof (引用) "得到的是所指向的对象的大小,而“sizeof (指针)”得到的是指针本身的大小;

    智能指针

    智能指针是什么?
    在 C++ 开发中,我们经常会遇到诸如程序运行中突然崩溃、程序运行所用内存越来越多最终不得不重启等问题,这些问题往往都是内存资源管理不当造成的。比如:

    • 有些内存资源已经被释放,但指向它的指针并没有改变指向(成为了野指针),并且后续还在使用;
    • 有些内存资源已经被释放,又试图再释放一次(重复释放同一块内存会导致程序运行崩溃);
    • 没有及时释放不再使用的内存资源,造成内存泄漏,程序占用的内存资源越来越多。

    为了避免以上这些问题,自动垃圾回收变得十分重要,C++虽然并没有向java、python那样完全支持自动垃圾回收,但是在C++98/03 标准中,支持使用 auto_ptr 智能指针来实现堆内存的自动回收;C++11 新标准在废弃 auto_ptr 的同时,增添了 unique_ptr、shared_ptr 以及 weak_ptr 这 3 个智能指针来实现堆内存的自动回收,不再需要手动new/delete。

    智能指针的实现原理:
    利用代理模式,把裸指针包装起来,在构造函数里初始化,在析构函数里释放。这样当对象失效销毁时,C++ 就会自动调用析构函数,完成内存释放、资源回收等清理工作。

    unique_ptr

    unique_ptr 核心特点
    unique_ptr 指针指向的堆内存无法同其它 unique_ptr 共享,也就是说,每个 unique_ptr 指针都独自拥有对其所指堆内存空间的所有权。每个 unique_ptr 指针指向的堆内存空间的引用计数,都只能为 1,一旦该 unique_ptr 指针放弃对所指堆内存空间的所有权,则该空间会被立即释放回收。

    unique_ptr 本质是对象
    unique_ptr 虽然名字叫指针,用起来也很像,但它实际上并不是指针,而是一个对象。所以,不要企图对它调用 delete,它会自动管理初始化时的指针,在离开作用域时析构释放内存。

    unique_ptr 的所有权

    使用 unique_ptr 的时候还要特别注意指针的“所有权”问题。

    正如它的名字,表示指针的所有权是“唯一”的,不允许共享,任何时候只能有一个“人”持有它。

    为了实现这个目的,unique_ptr 应用了 C++ 的“转移”(move)语义,同时禁止了拷贝赋值,所以,在向另一个 unique_ptr 赋值的时候,要特别留意,必须用 std::move() 函数显式地声明所有权转移

    赋值操作之后,指针的所有权就被转走了,原来的 unique_ptr 变成了空指针,新的 unique_ptr 接替了管理权,保证所有权的唯一性:

    auto ptr1 = make_unique<int>(42);    // 工厂函数创建智能指针
    auto ptr2 = std::move(ptr1);         // 使用move()转移所有权
    
    • 1
    • 2

    记住:尽量不要对 unique_ptr 执行赋值操作,让它“自生自灭”,完全自动化管理。

    unique_ptr和栈上分配对象对比:

    • 智能指针的目标是“自动内存管理”,它本身是一个在栈上分配的对象,但里面的内容是在堆上,所以当你需要一个生命周期有限,又得“动态”在堆上创建的时候,就用unique_ptr。

    • 如果不是动态创建,与堆无关,那显然直接用栈对象更省事,没有了智能指针的成本。

    shared_ptr

    shared_ptr 与 unique_ptr 的最大不同点:它的所有权是可以被安全共享的,也就是说支持拷贝赋值,允许被多个“人”同时持有,就像原始指针一样。

    
    auto ptr1 = make_shared<int>(42);    // 工厂函数创建智能指针
    auto ptr2 = ptr1;                  // 直接拷贝赋值,不需要使用move()
    assert(ptr1 && ptr2);              // 此时两个智能指针均有效
    assert(ptr1 == ptr2);             // shared_ptr可以直接比较
    
    // 两个智能指针均不唯一,且引用计数为2
    assert(!ptr1.unique() && ptr1.use_count() == 2); 
    assert(!ptr2.unique() && ptr2.use_count() == 2); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    shared_ptr 支持安全共享的原理在于内部使用了“引用计数”。
    即:引用计数最开始的时候是 1,表示只有一个持有者。如果发生拷贝赋值,引用计数就增加,而发生析构销毁的时候,引用计数就减少。只有当引用计数减少到 0,即没有任何人使用这个指针的时候,它才会真正调用 delete 释放内存。

    因为 shared_ptr 可以拷贝赋值,所以它可以在任何场合替代原始指针,而不用再担心资源回收的问题,比如用于容器存储指针、用于函数安全返回动态创建的对象等等。

    shared_ptr有哪些线程安全隐患?
    1、引用计数的加减操作是否线程安全?
    安全,因为是原子操作
    2、shared_ptr指向的对象是否线程安全
    不安全
    当智能指针发生拷贝的时候,会先拷贝智能指针,再拷贝对象,这两个操作并不是原子的,所以不安全。
    比如在计数-1的时候,其内部的指向被其他线程修改了。引用计数的异常会导致某个管理的对象被提前析构,后续在使用到该数据的时候触发core dump。

    shared_ptr 的注意事项
    1、虽然 shared_ptr 非常“智能”,但天下没有免费的午餐,它也是有代价的,引用计数的存储和管理都是成本,这方面是 shared_ptr 不如 unique_ptr 的地方。

    2、如果不考虑应用场合,过度使用 shared_ptr 就会降低运行效率。不过,你也不需要太担心,shared_ptr 内部有很好的优化,在非极端情况下,它的开销都很小。

    3、另外一个要注意的地方是 shared_ptr 的销毁动作。
    因为我们把指针交给了 shared_ptr 去自动管理,但在运行阶段,引用计数的变动是很复杂的,很难知道它真正释放资源的时机,无法像 Java、Go 那样明确掌控、调整垃圾回收机制。
    要特别小心对象的析构函数,不要有非常复杂、严重阻塞的操作。一旦 shared_ptr 在某个不确定时间点析构释放资源,就会阻塞整个进程或者线程,“整个世界都会静止不动”( Go 也是)。排查起来费了很多功夫,真的是“血泪教训”。

    4、shared_ptr 的引用计数也导致了一个新的问题,就是“循环引用”,这在把 shared_ptr 作为类成员的时候最容易出现,典型的例子就是链表节点,看下面的例子:

    auto n1 = make_shared<Node>();   // 工厂函数创建智能指针
    auto n2 = make_shared<Node>();   // 工厂函数创建智能指针
    
    n1->next = n2;                 // 两个节点互指,形成了循环引用
    n2->next = n1;
    
    assert(n1.use_count() == 2);    // 引用计数为2
    assert(n2.use_count() == 2);    // 无法减到0,无法销毁,导致内存泄漏
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里,两个节点指针刚创建时,引用计数是 1,但指针互指(即拷贝赋值)之后,引用计数都变成了 2。

    这个时候,shared_ptr 就“犯傻”了,意识不到这是一个循环引用,多算了一次计数,后果就是引用计数无法减到 0,无法调用析构函数执行 delete,最终导致内存泄漏。

    想要从根本上杜绝循环引用,光靠 shared_ptr 是不行了,必须要用到它的“小帮手”:weak_ptr。

    weak_ptr

    weak_ptr 顾名思义,功能很“弱”。它专门为打破循环引用而设计,只观察指针,不会增加引用计数(弱引用),但在需要的时候,可以调用成员函数 lock(),获取 shared_ptr(强引用)。刚才的例子里,只要改用 weak_ptr,循环引用的烦恼就会消失、

    不严谨的weak_ptr 和shared_ptr的区别:
    shared_ptr是强引用,无论如何都需要持有共享对象的时候就用它。

    weak_ptr是弱引用,不一定要持有对象,只是“偶尔”想去看看对象在不在,不在也可以接受。

    weak_ptr怎么用呢?
    weak_ptr与shared_ptr配合着使用,用weak_ptr先确保shared_ptr是持有指针的,然后再放心使用

    总结

    智能指针是代理模式的具体应用,它使用 RAII 技术代理了裸指针,能够自动释放内存,无需程序员干预,所以被称为“智能指针”。

    如果指针是“独占”使用,就应该选择 unique_ptr,它为裸指针添加了很多限制,更加安全。

    如果指针是“共享”使用,就应该选择 shared_ptr,它的功能非常完善,用法几乎与原始指针一样。

    应当使用工厂函数 make_unique()、make_shared() 来创建智能指针,强制初始化,而且还能使用 auto 来简化声明。

    shared_ptr 有少量的管理成本,也会引发一些难以排查的错误,所以不要过度使用。

    彻底搞懂指针

  • 相关阅读:
    使用CSS变量实现主题定制真的很简单
    哈希表Map和Set【万字文】
    Java中的JDBC如何连接数据库并执行操作
    ​​​​​​​实验二 运算符和内置函数使用(Python程序设计实验报告)
    Camtasia2024免费版mac电脑录屏软件
    leetcode做题笔记198. 打家劫舍
    用于微小目标检测的上下文扩展和特征细化网络
    MyBatis中如何实现一个分页功能呢?
    华南理工大学控制工程考研经验分享
    Android Tv连接charles
  • 原文地址:https://blog.csdn.net/qq_40337086/article/details/122809205