指针和引用都是间接寻址在高级语言中的表现,一是提供效率上的保证,二是给动态内存的操作带来很多方便
这一章的学习目标:
这幅图表明了指针其实是一种独立的变量类型,存的是地址这种数据
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字节
&获取对象的地址,即指针的数据类型
当然有一个问题是,对象的地址一般都是若干个字节,取地址符的结果是连续空间的第一个字节
又叫dereference operator
- # include
- # include
-
- using namespace std;
-
- int main() {
- int unit_price{295}; // Item unit price in cents
- int count{}; // Number of items ordered
- int discount_threshold{25}; // Quantity threshold for discount
- double discount{0.07}; // Discount for quantities over discount_threshold
- int* pcount{&count}; // Pointer to count
- std::cout << "Enter the number of items you want: ";
- std::cin >> *pcount;
- std::cout << std::format("The unit price is ${:.2f}\n", unit_price / 100.0);
- // Calculate gross price
- int* punit_price{&unit_price}; // Pointer to unit_price
- int price{*pcount * *punit_price}; // Gross price via pointers
- auto* pprice{&price}; // Pointer to gross price
- // Calculate net price in US$
- double net_price{};
- double* pnet_price{nullptr};
- pnet_price = &net_price;
- if (*pcount > discount_threshold) {
- std::cout <<
- std::format("You qualify for a discount of {:.0f} percent.\n", discount * 100);
- *pnet_price = price * (1 - discount) / 100;
- }
- else { net_price = *pprice / 100; }
- std::cout << std::format("The net price for {} items is ${:.2f}\n", *pcount, net_price);
- }
这个例子就是说明,解引用可以获取指针指向的对象的值,至于要不要这么写我们可以看一下指针的设计思路
后两条对于引用也是一样的,但是书上说到的是他的应用,我没有理解为什么会出现专门存变量的地址的变量,似乎是加码
但是思考过之前的汇编就会发现,间接寻址在编程语言中是基础部分,是必不可少的组成,在高级语言中通常都隐藏了地址这个概念,但不代表它们实现功能就不需要地址了
地址这个概念比指针要先出现,任何编程都依赖地址,而指针就是间接寻址的中级表现,低级表现就是汇编,高级表现就是Java中的引用,以及Python中的无指针,当然这样做也会带来效率问题
数组名在很多情况下可以当作指针使用,这也是指针和数组联系紧密的地方
指针的运算基于其所指的数据类型,当我们规定指针+1的时候,我们考虑的是实用性而不是地址的加一减一,因为对于不同类型来说,地址加减没有意义,加一减一也不能代表不同的对象
尤其是在数组中,指针算术运算代表前后移动,也就是指向的元素在左右平移
有了指针的差的定义,那么比大小就很符合直觉了,对于数组中的元素来说,索引越大的对应的指针越大,比较的基础是作差,当x-y<0时,x 之前我们所有的对象都在编译期间分配内存,除了第五章中使用vector时用while循环输入元素,那个地方是动态分配的内存,其他的对象都在程序运行之前就已经确定内存,进入程序执行过程后无论我们使不使用,这些内存都已经分配了 不过总有一些时候我们无法预知程序需要多少变量和对象,而这些对象也无法命名,之前说过指针就是用来干这个的,简称为匿名对象,通过地址我们可以手动访问和管理这些无名对象 第三章我们说过对象的生存期有三种:自动、静态、动态,前两种已经看过使用方式,这一章将介绍动态生存期对象的特点 书里有讲这一点很好,作为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 (不知道为什么clion这里会说pvalue内存泄漏了) 当然这里申请的是一个double的空间,如果我们的对象(像后面要学习的类或者STL)特别大,我们就需要考虑堆里空间是否够使用的问题,当堆空间不足以使用的时候,new操作会抛出异常,我们不处理的话程序就会终止,这个问题在后面16章会再次讨论 我们也可以申请空间的同时给这片地址初始化一个值,不过对指针进行空初始化的话会给一个nullptr初值 使用完之后我们要进行配套操作delete,甚至nullptr我们也可以释放,所以在重复使用指针的时候先记得删除 申请数组没什么好说的,加一个[]即可,delete需要配套使用 通过指针访问成员有一个专门的运算符,否则一直解引用有点复杂 显然一个箭头比括号+解引用+点要更简约 指的是悬垂指针和重复释放的问题 悬垂指针说的是那些指向被释放内存的指针:假设两个指针指向同一处内存,你只释放了一个指针,那片内存已经被回收,但是另外一个指针不需要你释放也已经指向了被回收的内存,此时这片内存可能被其他代码使用,这个时候我们再访问这个未释放的指针,会导致致命的问题 重复释放指的是一个指针释放两次的情况 这种写法在GCC中可以编译通过但是程序会返回异常 不匹配说的是delete和new不匹配,如果申请了一个数组但是不用delete[]释放,相反用delete释放数组都会导致错误 任何不匹配的情况不是UB就是内存泄漏 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. 简言之,我们申请的要用的内存访问不到了,用来访问的指针丢了、被释放了等等都会导致这种情况 一般叫堆碎片,因为我们的new通常是申请一段连续的若干字节,当我们频繁申请释放之后,堆空间会变得碎片化,可用空间不再呈现大段空白而是穿插在被申请的内存之间,这也使得我们的程序难以再次申请大空间 不过现在机器一般都有虚拟内存,而且new、delete的实现也很大程度上避免了这种情况,一般不需要担心内存碎片造成的程序性能下降的问题,这里提到这个主要是为了问题说明的完整性,因为这也属于动态内存的问题之一 ever use the operators new, new[], delete, and delete[] directly in day-to-day coding. These hhh最简单的避免内存问题的方法就是不要用裸指针,即避免使用new、delete这种底层原语 书里讲这些的目的不是让你使用它,而是让你碰到这种代码甚至需要解决他们的bug的时候知道怎么重构 12章讲类的时候我们会更能体会智能指针的好处,这里只是简单介绍一下 智能指针(现代 C++) | Microsoft Learn CppGuide/articles/C++必知必会的知识点/详解C++11中的智能指针.md at master · balloonwj/CppGuide · GitHub 很多文章都详细讲了三种类型,最常使用的就是前两种,一种资源独享,一种共享 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()的用法 std::shared_ptr - cppreference.com 而unique_ptr就不允许复制;同样有make_shared_for_overwrite创建一个默认初始化的shared_ptr 下面是一个综合例子: 一般容器套容器我们都是用指针的,容器对象太大的话会导致空间紧张,容器存指针就还能接受,而且指针操作也不麻烦 这里讲引用其实不如在函数里讲,不涉及对象复制的话其实引用用处不大 rdata在这里相当于一个别名,我们对引用的所有操作相当于对被引用对象的直接操作 这里再说一下常量引用,这种引用是不允许我们通过引用修改被引用对象的写法,和常量指针类似 还有一种const是指针常量,这种是const写在*后面的指针,不允许更改指针存储的地址,指针永远指向那个地址,用的比较少 range-for中使用引用主要也是range-for自身的特点导致,range-for使用的是迭代器/指针去遍历一个范围,如果不使用引用,我们将不会对range产生任何实质性更改,处理的永远都是复制来的元素副本 涉及元素复制的开销都要想到引用,range-for range-for用指针处理序列,使用引用的时候内部的临时变量也是引用Dynamic Memory Allocation 动态内存分配
The Stack and the Free Store 栈&自由存储区
Using the new and delete Operators
Dynamic Allocation of Arrays
Member Selection Through a Pointer
Hazards of Dynamic Memory Allocation 动态内存的危险
Dangling Pointers and Multiple Deallocations
Allocation/Deallocation Mismatch
Memory Leaks
Fragmentation of the Free Store
Golden Rule of Dynamic Memory Allocation
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).Raw Pointers and Smart Pointers 裸指针和智能指针
Using unique_ptr
Using shared_ptr
Understanding References
Defining References
Using a Reference Variable in a Range-Based for Loop