• 《Beginning C++20 From Novice to Professional》第六章 Pointers and References


    指针和引用都是间接寻址在高级语言中的表现,一是提供效率上的保证,二是给动态内存的操作带来很多方便

    这一章的学习目标:

    What is a pointer?

    这幅图表明了指针其实是一种独立的变量类型,存的是地址这种数据

    You are not obliged to initialize a pointer when you define it, but it’s reckless not to. Uninitialized pointers are more dangerous than ordinary variables that aren’t initialized.

    未初始化的指针比变量更危险

    每种操作系统下程序的指针大小都是确定的,对于64位系统,指针通常都是8字节

    The Address-Of Operator 取地址操作符

    &获取对象的地址,即指针的数据类型

    当然有一个问题是,对象的地址一般都是若干个字节,取地址符的结果是连续空间的第一个字节

    The Indirection Operator 解引用操作符

    又叫dereference operator

    1. # include
    2. # include
    3. using namespace std;
    4. int main() {
    5. int unit_price{295}; // Item unit price in cents
    6. int count{}; // Number of items ordered
    7. int discount_threshold{25}; // Quantity threshold for discount
    8. double discount{0.07}; // Discount for quantities over discount_threshold
    9. int* pcount{&count}; // Pointer to count
    10. std::cout << "Enter the number of items you want: ";
    11. std::cin >> *pcount;
    12. std::cout << std::format("The unit price is ${:.2f}\n", unit_price / 100.0);
    13. // Calculate gross price
    14. int* punit_price{&unit_price}; // Pointer to unit_price
    15. int price{*pcount * *punit_price}; // Gross price via pointers
    16. auto* pprice{&price}; // Pointer to gross price
    17. // Calculate net price in US$
    18. double net_price{};
    19. double* pnet_price{nullptr};
    20. pnet_price = &net_price;
    21. if (*pcount > discount_threshold) {
    22. std::cout <<
    23. std::format("You qualify for a discount of {:.0f} percent.\n", discount * 100);
    24. *pnet_price = price * (1 - discount) / 100;
    25. }
    26. else { net_price = *pprice / 100; }
    27. std::cout << std::format("The net price for {} items is ${:.2f}\n", *pcount, net_price);
    28. }

    这个例子就是说明,解引用可以获取指针指向的对象的值,至于要不要这么写我们可以看一下指针的设计思路

    Why Use Pointers? 为什么要用指针

    1. 动态内存以内存为标识,需要指针管理动态内存
    2. 操作数组有其优势
    3. 指针使得函数能够访问大片内存
    4. 指针对多态的实现很重要

    后两条对于引用也是一样的,但是书上说到的是他的应用,我没有理解为什么会出现专门存变量的地址的变量,似乎是加码

    但是思考过之前的汇编就会发现,间接寻址在编程语言中是基础部分,是必不可少的组成,在高级语言中通常都隐藏了地址这个概念,但不代表它们实现功能就不需要地址了

    地址这个概念比指针要先出现,任何编程都依赖地址,而指针就是间接寻址的中级表现,低级表现就是汇编,高级表现就是Java中的引用,以及Python中的无指针,当然这样做也会带来效率问题

    Pointers and Arrays

    数组名在很多情况下可以当作指针使用,这也是指针和数组联系紧密的地方

    Pointer Arithmetic 指针运算

    指针的运算基于其所指的数据类型,当我们规定指针+1的时候,我们考虑的是实用性而不是地址的加一减一,因为对于不同类型来说,地址加减没有意义,加一减一也不能代表不同的对象

    尤其是在数组中,指针算术运算代表前后移动,也就是指向的元素在左右平移

    The Difference Between Pointers 指针作差

    Comparing Pointers 指针比较

    有了指针的差的定义,那么比大小就很符合直觉了,对于数组中的元素来说,索引越大的对应的指针越大,比较的基础是作差,当x-y<0时,x

    Dynamic Memory Allocation 动态内存分配

    之前我们所有的对象都在编译期间分配内存,除了第五章中使用vector时用while循环输入元素,那个地方是动态分配的内存,其他的对象都在程序运行之前就已经确定内存,进入程序执行过程后无论我们使不使用,这些内存都已经分配了

    不过总有一些时候我们无法预知程序需要多少变量和对象,而这些对象也无法命名,之前说过指针就是用来干这个的,简称为匿名对象,通过地址我们可以手动访问和管理这些无名对象

    第三章我们说过对象的生存期有三种:自动、静态、动态,前两种已经看过使用方式,这一章将介绍动态生存期对象的特点

    The Stack and the Free Store 栈&自由存储区

    书里有讲这一点很好,作为C++使用者有必要了解一点内存和操作系统的相关知识,虽然C++正逐渐现代化到不需要我们考虑底层的东西,但是不代表底层的C部分被完全覆盖,保证效率和灵活性的前提下,了解一些这方面的知识有助于我们写出更好的代码来

    The space for an automatic variable is allocated in a memory area called the stack.

    对于自动生存期的对象来说,其生存期由初始化起到块结束,存储这些变量的地方叫做栈,栈的大小由编译器决定,编译选项可以设置栈大小,不过通常不需要手动设置

    当我们使用函数的时候,caller的地址以及函数的参数parameter都会被保存到临时栈里,函数运行结束后参数被销毁,然后返回caller继续执行调用者代码

    Memory that is not occupied by the operating system or other programs that are currently loaded is called the free store.

    程序或操作系统没有使用的内存叫做自由存储区,在C++中这些内存的主动使用由new和delete关键字来处理,我们申请一块空间存储动态对象,然后返回它的地址,使用指针管理地址,使用完毕之后释放这些空间以供其他程序或逻辑再次使用

    动态的特点就是需要我们手动申请和释放,但是申请和释放不要求在同一块代码同时完成,可以函数A申请,函数B释放,只是我们不要忘记释放或者重复释放,管理好内存就不会产生错误;当然如果忘记释放了,当程序结束后,所有内存都会被释放,但是这段程序里的其他申请会受影响

    自由存储区还有一种说法是堆,实际上堆比自由存储区更常用,但是标准里是这么写的书里也这么说,大家说堆的时候我们要知道在C++世界里heap=the free store

    Using the new and delete Operators

    (不知道为什么clion这里会说pvalue内存泄漏了)

    当然这里申请的是一个double的空间,如果我们的对象(像后面要学习的类或者STL)特别大,我们就需要考虑堆里空间是否够使用的问题,当堆空间不足以使用的时候,new操作会抛出异常,我们不处理的话程序就会终止,这个问题在后面16章会再次讨论

    我们也可以申请空间的同时给这片地址初始化一个值,不过对指针进行空初始化的话会给一个nullptr初值

    使用完之后我们要进行配套操作delete,甚至nullptr我们也可以释放,所以在重复使用指针的时候先记得删除

    Dynamic Allocation of Arrays

    申请数组没什么好说的,加一个[]即可,delete需要配套使用

    Member Selection Through a Pointer

    通过指针访问成员有一个专门的运算符,否则一直解引用有点复杂

    显然一个箭头比括号+解引用+点要更简约

    Hazards of Dynamic Memory Allocation 动态内存的危险

    Dangling Pointers and Multiple Deallocations

    指的是悬垂指针和重复释放的问题

    悬垂指针说的是那些指向被释放内存的指针:假设两个指针指向同一处内存,你只释放了一个指针,那片内存已经被回收,但是另外一个指针不需要你释放也已经指向了被回收的内存,此时这片内存可能被其他代码使用,这个时候我们再访问这个未释放的指针,会导致致命的问题

    重复释放指的是一个指针释放两次的情况

    这种写法在GCC中可以编译通过但是程序会返回异常

    Allocation/Deallocation Mismatch

    不匹配说的是delete和new不匹配,如果申请了一个数组但是不用delete[]释放,相反用delete释放数组都会导致错误

    任何不匹配的情况不是UB就是内存泄漏

    Memory Leaks

    If you lose the address of free store memory you have allocated, by overwriting the address in the pointer you were using to access it, for instance, you have a memory leak.

    简言之,我们申请的要用的内存访问不到了,用来访问的指针丢了、被释放了等等都会导致这种情况

    Fragmentation of the Free Store

    一般叫堆碎片,因为我们的new通常是申请一段连续的若干字节,当我们频繁申请释放之后,堆空间会变得碎片化,可用空间不再呈现大段空白而是穿插在被申请的内存之间,这也使得我们的程序难以再次申请大空间

    不过现在机器一般都有虚拟内存,而且new、delete的实现也很大程度上避免了这种情况,一般不需要担心内存碎片造成的程序性能下降的问题,这里提到这个主要是为了问题说明的完整性,因为这也属于动态内存的问题之一

    Golden Rule of Dynamic Memory Allocation

    ever use the operators new, new[], delete, and delete[] directly in day-to-day coding. These
    operators have no place in modern C++ code. Always use either the std::vector<> container (to replace dynamic arrays) or a smart pointer (to dynamically allocate individual objects and manage their lifetimes).

    hhh最简单的避免内存问题的方法就是不要用裸指针,即避免使用new、delete这种底层原语

    书里讲这些的目的不是让你使用它,而是让你碰到这种代码甚至需要解决他们的bug的时候知道怎么重构

    Raw Pointers and Smart Pointers 裸指针和智能指针

    里定义了三类智能指针,最大的好处在于你不用手动释放内存,智能指针在生存期结束后会自动释放内存

    12章讲类的时候我们会更能体会智能指针的好处,这里只是简单介绍一下

     智能指针(现代 C++) | Microsoft Learn

    CppGuide/articles/C++必知必会的知识点/详解C++11中的智能指针.md at master · balloonwj/CppGuide · GitHub

    很多文章都详细讲了三种类型,最常使用的就是前两种,一种资源独享,一种共享

    Using unique_ptr Pointers

    unique_ptr独占资源,指针析构时对应的地址也被释放,常用的场景是实现多态,这一点在我们讲到类的时候会更详细介绍

    我们可以使用get成员函数获取指针指向的地址(成员函数就是类拥有的一些操作,讲到类的时候会说)

    下面来看使用智能指针指向数组的例子:

    This initialization is not always necessary, and may, at times, impact performance (zeroing out memory takes time).

    这里说申请空间时对这些内存进行值初始化其实不总是我们希望的操作,初始化为0还不如不初始化,C++20有个新函数允许我们对申请的空间进行默认初始化(什么也不做)

    std::make_unique, std::make_unique_for_overwrite - cppreference.com

    至于书上说的这个名字,是提案中出现的命名,实际写作make_unique_for_overwrite


    下面学习一下reset()和release()的用法

    Using shared_ptr Pointers

    std::shared_ptr - cppreference.com

     而unique_ptr就不允许复制;同样有make_shared_for_overwrite创建一个默认初始化的shared_ptr

    下面是一个综合例子:

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. int main() {
    7. std::vectordouble> > > records;// Temperature records by days
    8. size_t day{1}; // Day number
    9. while (true) // Collect temperatures by day
    10. {
    11. // Vector to store current day's temperatures created in the free store
    12. auto day_records{std::make_shareddouble> >()};
    13. records.push_back(day_records);// Save pointer in records vector
    14. std::cout << "Enter the temperatures for day " << day++
    15. << " separated by spaces. Enter 1000 to end:\n";
    16. while (true) {
    17. // Get temperatures for current day
    18. double t{};// A temperature
    19. std::cin >> t;
    20. if (t == 1000.0) break;
    21. day_records->push_back(t);
    22. }
    23. std::cout << "Enter another day's temperatures (Y or N)? ";
    24. char answer{};
    25. std::cin >> answer;
    26. if (std::toupper(answer) != 'Y') break;
    27. }
    28. day = 1;
    29. for (auto record: records) {
    30. double total{};
    31. size_t count{};
    32. std::cout << std::format("\nTemperatures for day {}:\n", day++);
    33. for (auto temp: *record) {
    34. total += temp;
    35. std::cout << std::format("{:6.2f}", temp);
    36. if (++count % 5 == 0) std::cout << std::endl;
    37. }
    38. std::cout << std::format("\nAverage temperature: {:.2f}", total / count) << std::endl;
    39. }
    40. }

    一般容器套容器我们都是用指针的,容器对象太大的话会导致空间紧张,容器存指针就还能接受,而且指针操作也不麻烦

    Understanding References 

    这里讲引用其实不如在函数里讲,不涉及对象复制的话其实引用用处不大

    Defining References

    rdata在这里相当于一个别名,我们对引用的所有操作相当于对被引用对象的直接操作

    这里再说一下常量引用,这种引用是不允许我们通过引用修改被引用对象的写法,和常量指针类似

    还有一种const是指针常量,这种是const写在*后面的指针,不允许更改指针存储的地址,指针永远指向那个地址,用的比较少

    Using a Reference Variable in a Range-Based for Loop

    range-for中使用引用主要也是range-for自身的特点导致,range-for使用的是迭代器/指针去遍历一个范围,如果不使用引用,我们将不会对range产生任何实质性更改,处理的永远都是复制来的元素副本

    涉及元素复制的开销都要想到引用,range-for

    range-for用指针处理序列,使用引用的时候内部的临时变量也是引用

  • 相关阅读:
    【Python-编程模式】
    html 菜单点击切换样式,菜单<a> 控制iframe
    腾讯云饥荒服务器配置选择和费用价格表
    MMDet3D——报错解决:KeyError: ‘XXX is not in the models registry‘
    如何用Stable Diffusion模型生成个人专属创意名片?
    研发管理工具选型要考虑哪些内容?
    [HDLBits] Fsm serialdp
    【docker系列】逐行解析Nginx镜像Dockerfile(学习经典)
    PyQT6 pip install (三) 百篇文章学PyQT
    python链表_递归求和_递归求最大小值
  • 原文地址:https://blog.csdn.net/asdfghjkl12211/article/details/138138463