问题

C++ 中,std::atomic 是真正的「原子」吗?

回答
在 C++ 中讨论 `std::atomic` 是否是“真正的原子”时,我们需要拨开表面的术语,深入理解其底层含义和实际应用。答案并非一个简单的“是”或“否”,而是取决于你对“原子”的理解以及在什么上下文中去考量。

首先,让我们明确一下在并发编程领域,“原子性”(Atomicity)通常指的是一个操作是不可分割的,它要么完全执行,要么根本不执行,不存在中间状态被其他线程观察到。想象一下一个单次指令,CPU执行它的时候,其他进程或线程无法中断它并看到一个“部分完成”的状态。

那么,`std::atomic` 在 C++ 中扮演了什么角色?

`std::atomic` 是 C++ 标准库提供的一种类型,它为变量的操作提供了原子操作的保证。换句话说,当你使用 `std::atomic`(其中 `T` 是某种基本类型,比如 `int`, `bool`, `long long` 等)时,对该变量的某些特定操作(如读、写、增减、比较并交换等)会被编译器和底层硬件实现为原子操作。

“真正的原子”:硬件的基石

从最底层的角度来看,“真正的原子”性实际上是由 CPU 的指令集提供的硬件支持。现代 CPU 有一些特殊的指令,它们被设计成可以在一个指令周期内完成,并且不会被其他核心或进程中断。例如:

`LOCK` 前缀的 x86 指令: 在 x86 架构上,许多对内存的操作,如果前面加上 `LOCK` 前缀,CPU 会确保该操作的独占性。这意味着在执行 `LOCK` 指令期间,总线会被锁定,其他 CPU 核心无法访问该内存位置,直到操作完成。这为实现原子操作提供了硬件保障。
其他架构的类似机制: ARM、MIPS 等架构也有各自的机制来实现原子操作,通常是通过特殊的加载/存储指令,或者通过缓存一致性协议来确保操作的不可分割性。

`std::atomic` 如何利用硬件支持?

C++ 标准规定了 `std::atomic` 的行为,但具体的实现方式是由编译器和底层平台决定的。编译器会根据目标平台的指令集来选择最适合的底层实现:

1. 使用原子指令: 如果目标平台直接支持原子操作的 CPU 指令(如上述的 `LOCK` 前缀指令),编译器会生成这些指令来执行 `std::atomic` 的操作。这是最理想、最高效的情况,因为这些操作在硬件层面是真正的不可分割的。
2. 使用锁(Lockbased)作为回退: 如果目标平台没有直接支持特定原子操作的 CPU 指令,编译器可能会退而求其次,使用内部的锁机制来实现原子性。这意味着,当一个线程访问 `std::atomic` 变量时,它会尝试获取一个内部锁,执行操作后释放锁。这种方式虽然也能保证操作的原子性(其他线程在锁释放前无法访问),但效率会比直接的硬件原子指令低得多,因为它引入了锁争用和线程阻塞的可能性。

那么,`std::atomic` 总是“真正的原子”吗?

基于上述的实现机制,我们可以得出以下结论:

对于支持的简单操作(如读取、写入、增减、比较并交换): 当 `std::atomic` 变量的操作能够被直接映射到 CPU 的原生原子指令时,它们就可以被认为是“真正的原子”的。这意味着这些操作在单个指令周期内完成,且不会被其他线程观察到中间状态。这是 `std::atomic` 在大多数情况下想要提供的核心保证。
复杂操作和非标准操作: 需要注意的是,并非所有对 `std::atomic` 变量的操作都是原子性的。例如,如果你尝试对一个 `std::atomic` 执行一个涉及多个步骤的操作(比如先读取值,然后根据值做计算,最后再写入),那么这个复合操作本身不是原子性的,它可能在读取和写入之间被其他线程修改。`std::atomic` 提供的原子性仅限于其成员函数所定义的那些操作(如 `load()`, `store()`, `fetch_add()`, `compare_exchange_weak()`, `compare_exchange_strong()`)。
平台依赖性: “真正的原子”性在很大程度上是平台相关的。在 x8664 上,常见的原子操作通常能获得很好的硬件支持。但在某些更老的或更特殊的架构上,或者对于某些更复杂的 `std::atomic` 特性,实现可能更依赖于软件层面的同步原语,其“原子性”的代价和性能特征可能会有所不同。

关于“内存顺序”(Memory Order)

C++ 的 `std::atomic` 还有一个至关重要的概念叫做内存顺序(Memory Order)。它控制着原子操作与其他内存操作(包括其他原子操作和普通内存操作)之间的可见性关系。不同的内存顺序(如 `memory_order_relaxed`, `memory_order_acquire`, `memory_order_release`, `memory_order_acq_rel`, `memory_order_seq_cst`)提供了不同级别的同步保证。

`memory_order_relaxed`: 提供最弱的同步,只保证该操作本身的原子性,不保证与其他内存操作的顺序。
`memory_order_acquire` / `memory_order_release`: 提供了一定的可见性同步,确保在 release 操作之后的写入对 acquire 操作之后的读取是可见的。
`memory_order_seq_cst` (Sequentially Consistent): 提供最强的同步保证,是默认的内存顺序。它确保所有线程观察到的原子操作都遵循一个单一的、全局的顺序。这种顺序是通过在可能的情况下使用硬件提供的最强的原子指令(如带 `LOCK` 前缀的指令)并辅以相应的内存屏障来实现的。

当你说“真正的原子”时,可能也在隐含地问:“它是否能像一个单一的、不可分割的事件一样,在全局都被正确地观察到?” 在这个意义上,`std::atomic` 配合 `memory_order_seq_cst` 最接近这个概念。

总结一下:

`std::atomic` 的目标是提供原子操作的语义,即操作要么完全成功,要么完全不成功,没有中间状态。
其实现依赖于底层硬件指令集。当硬件提供原生原子指令时,`std::atomic` 的操作就是“真正的原子”的,高效且不可分割。
并非所有操作都是原子性的,只有 `std::atomic` 成员函数定义的那些操作(如 `load`, `store`, `fetch_add` 等)是原子性的。复合操作需要额外的同步手段。
内存顺序 (`memory_order`) 是理解 `std::atomic` 行为的关键。`memory_order_seq_cst` 提供了最强的全局可见性保证,但可能性能稍逊于更弱的内存顺序。

所以,与其问“是否是真正的原子”,不如更准确地说,`std::atomic` 是 C++ 中提供原子操作语义的抽象机制,它会尽可能地利用硬件的原生原子指令来满足这些语义。在大多数现代平台上,对于常见的原子操作,你可以将其视为“真正的原子”来理解和使用。但始终要记住其局限性(只对特定操作原子)以及内存顺序的重要性。

网友意见

user avatar

感觉现有的回答都不全面。

cpp原子库有些实现是调用操作系统提供的接口,所以这个实现可能跟操作系统有关。同时,又因为操作系统的实现是依赖于硬件的,所以具体的锁的实现要取决于硬件的支持情况。

题主有一个误区,因为原子操作一定需要关调度,关中断,这个理解是错的。锁的本质是同步数据,理论上说,只要保证特定的数据不被修改即可。在硬件层面上看,就是特定的物理内存,特定的cache line,不被修改,所以,并不是所有原子操作,都一定要关调度,关中断。

大多数主流的CPU,都会提供硬件指令,原子修改某个内存,对于Intel的CPU,手册system programming guide的8.1.2.2 Software Controlled Bus Locking里有详细的描述:

The bit test and modify instructions (BTS, BTR, and BTC).
The exchange instructions (XADD, CMPXCHG, and CMPXCHG8B).
The LOCK prefix is automatically assumed for XCHG instruction.
The following single-operand arithmetic and logical instructions: INC, DEC, NOT, and NEG.
The following two-operand arithmetic and logical instructions: ADD, ADC, SUB, SBB, AND, OR, and XOR.

如果操作系统使用的是硬件指令实现原子操作,那么用这些指令就可以了,这种实现不需要锁,不需要关中断或者调度。

对于ARM/PPC/RISCV也有类似的指令。

但是,也有例外情况。

比如ARM32/PPC32/RISCV32上不支持对8B数据的原子操作,只有Intel在32位环境中提供了CMPXCHG8B的指令(甚至于,Intel还提供了CMPXCHG16B的指令),这种情况就比较麻烦了。

atomic库里是提供了各种长度的数据的,如果硬件本身不支持,那么就需要通过软件实现。atomic库里有std::atomic_is_lock_free来告诉应用程序,这个操作是不是lock free的,如果不是,那么底层就是用锁来实现的。具体可以参考这个:

如果不是lock free的,那么其实它的实现跟用户自己用锁来实现是差不多的,甚至性能还不如用户自己实现。通过软件实现的原子操作是需要关中断,关调度,使用mem fence等动作保证数据不被改变,如果是用户自己的代码,确认中断不会更改关键数据的话,那么可以不用关中断。

某些开源库并没有考虑到不同硬件上的原子操作差异,所以在某些平台上,使用了原子操作的开源库实际上是会有风险的(比如openmp的kmp库在PPC/RISCV上)。

所以,原子操作可能是虚假的(软件模拟),也可能是真实的(硬件指令),在X86平台上,基本上都是真实的,在非X86平台,有可能是虚假的。

user avatar

这个问题要仔细说明白的话,不简单。但广泛给个四海而皆准的回答就是:std::atomic确实是都用“锁”的——区别无非是哪种“锁”而已。


扩展一点说:

首先,std::atomic可以特化为不同类型,平台上如果直接提供了原子指令的类型,会使用源自指令来实现(这在很多人眼中,这就是“无锁”了)。没有直接提供的类型,会直接用锁来实现。

然后,问题就退化为:原子指令到底是不是真的“无锁”。实际上在逻辑上,原子指令本身在逻辑上,也是一种“锁”的实现。

接着,简单解释一下原子指令里的“锁”:

早期的双U/双核年代,原子指令是需要在多CPU之间额外支持的。它的基本原理是当进入原子指令时,会锁住整个总线,使之进入“原子状态”(没记错应该还会关掉硬中断)。

现在核心多了,再动辄锁总线就性能损耗太大了,于是就变为锁cache。对应内存的cache块的标记为独占(加锁),等到操作结束才会解除(解锁)。而这时候,抢锁的操作实际上是由电路实现的(类似于抢答器之类的电路)。

最后,原子指令在理论上已经达到了最优解了,虽然肯定还是无法避免性能损失(抢不到锁的脏页要放弃后重新加载),但肯定比在软件层做得要更好了。

user avatar

是真正的“原子”。只不过,你严重误解了原子这个词。


一个变量是原子的,意思并不是整个程序对这个变量的八万次读写是一个整体。没这回事,不要和数据库事务的原子性混淆。


变量是原子的,意思是每次操作这个变量,包括读取其值、更改其值,这次操作中执行的唯一一条指令是原子的(对于面向对象语言,也可以是一次接口调用是原子的,这个后面会讲到)。

正因此,很多CPU才不得不专门提供一条Test and Exchange指令,把“检测内容,等于xx则修改其值”这样必须两三条指令才能完成的操作整合进一条指令,且在执行时锁定总线。

如果只靠总线锁定前缀的话,这个test and exchange操作先后执行的几条指令,虽然每一条都是原子的,但加起来就不是了。也就是这里必须提供一个方法,用来把几条指令绑起来(的确有CPU允许你在连续执行多条指令期间锁定总线,从而灵活组合一堆指令、让它们加起来是原子的。但这类操作往往需要很高特权级,不是用户态应用可以用的)。


注意这里有个思想:锁操作代价高昂,因此锁的粒度一定要小。最好小到只有一条指令,那么锁操作的代价就完全可控了。


注意,一条指令也是需要取指、译码、执行、写回结果等许多步骤的。

普通指令,这些步骤可以打断,可以相互穿插。比如,字节不对齐时,一条指令可能需要两次以上访存才能把待操作的数字载入寄存器,如果两次访存之间被打断、且另一条指令改写了被操作数字时,这里就会出现脏读(脏写也类似)。

CPU可以智能的分析指令执行涉及到的东西,从而避免几条指令穿插影响。但这个智能并不太高。比如,操纵和CPU字长相等的、字节对齐的int时,很多CPU可以保证原子性。

没错,不用锁总线,天然支持,只要你保证它的地址对齐满足CPU要求。但你要自己查阅资料来确认这一点,同时还要确认你的编译器生成的代码能够保证地址对齐。

那么这时候,cpp的atomic实际上等于什么都没做。


当然了,经常的,仅仅一条甚至七八条指令的原子性也是不够用的。比如,电商开发中经常遇到的,用户购买流程,需要先锁库存,下订单,等待付款,付款完成再把商品标记为出库:这一整套成千上万条指令都必须是原子的。


CPP的atomic不能也不该来保证这一点。

还记得吗?锁的粒度要尽量小。所以CPU努力把它做到了一条指令。

但是,电商这种情况该怎么办?

很简单,用一个atomic变量来保护数据。

注意了,CPU只能保护这个atomic变量不脏读脏写,它可保护不了你要保护的一大片数据在千百条指令执行过程中的原子性。

那该怎么办?

简单,想象一条虚拟的总线,当函数a执行时,锁住这条虚拟总线;执行完了,再给这条虚拟总线解锁。

比如,我们可以把数据库连接看作这条总线,然后把一个atomic变量等于1看作锁定状态,等于0则是free状态:那么,只要每个人都确认这个锁的从0到1是自己做的(也就是持有这个锁),那么就可以去修改数据库了。注意改完了要把锁恢复到0哦。

类似的,只要通过接口或者别的什么(甚至是文档中的约定)确保一切访问之前都要举行一个“锁定虚拟总线”的仪式,假装自己真的是通过这条总线访问的,那么当然也能达到原子访问的目的。

这就是为什么很多库会告诉你“随便用!我保证线程安全”的原因:很简单,它把必要的仪式都写死在接口实现代码里了。

注意,你的确得到了原子性/线程安全,执行时但并没有真的锁物理总线(我在脑中锁虚拟总线,关你物理总线什么事)。

只除了那条至关重要的test and exchange执行时。


仔细阅读、提炼这段描述,你才会理解为什么test and Exchange是必要的、为什么没有它就没办法正确实现锁。


当然,锁数据库链接粒度实在太大,这等于把数据库变成串行的了。实践中,数据库内部替你维护了表(级)锁、行锁等更小粒度的锁,甚至还区分读写锁。。。


类似的,电商购物,你也不要真的从用户下订单到支付完成都锁定数据库表或者表中的一行。不合适的使用大粒度锁是专业水平欠佳的表现。

相反,你要审慎思考:占用库存究竟是”锁住数据库中的一行(从而独占的修改它)”呢,还是“正确的把数据库余额减一(这个操作需要原子性),然后记住这个临时扣除状态,在用户付款后把状态改为永久、或者在用户付款后把余额再加一”:前者带来极长时间的数据库锁定,而后者只需要修改余额这个动作本身是原子性的。


这就是锁的优化。

前些年很是流行过一阵“无锁编程”,其核心思想就是不用任何大粒度锁,而是把一切同步操作都压缩到一条CPU指令。

换句话说,无锁编程并不是真正的无锁,而是锁粒度的极致优化,优化到只剩一条test and exchange指令。

换句话说:一切复杂操作的原子性、最终都可以归结为“一个guard变量”的原子性。

再换句话说,每次访问之前,都先围绕着guard举行一套仪式,就可以保证另外一堆复杂数据的原子性。这套仪式的关键,最终都可以归结到一条test and exchange指令。


能否认识到这一点,是你能否理解你的问题的必要前提(也是能否看出那群“一提12306,就说锁库存很可怕”的人的成色的前提)。

换句话说,真的理解了这里提到的东西,你才会明白自己的问题错在哪里。

user avatar

似乎很少看到在讨论 atomic 的时候同时讨论 interrupts。我认为这两者虽然都直接与 CPU 有关,但是是可以分开讨论的,因为,虽然你每敲一下键盘、移动一下鼠标 CPU 都会被中断,但中断处理例程(Interrupt Service Routine,IRS)一般位于 driver 里[1],并且 CPU 从中断恢复之后,用户态程序的环境跟之前是一样的,而且用户态程序也无法直接操作 IF 来禁止被中断[2],只有 OS 核心才行。

关于是否会使用锁机制的问题,虽然我不是硬件方面的专家不过据我了解,比如 atomic 在 x86 上最终可能以类似 lock cmpxchg8b 这样的指令来实现。这个 lock 前缀意味着锁,目的是保证在其修饰的指令执行时对 shared memory 的独占,它可能会锁住 BUS,但如果这块内存在 CPU 里面已经 cache,那么只需要锁 cache 就可以了。

Beginning with the P6 family processors, when the LOCK prefix is prefixed to an instruction and the memory area being accessed is cached internally in the processor, the LOCK# signal is generally not asserted. Instead, only the processor’s cache is locked. Here, the processor’s cache coherency mechanism ensures that the operation is carried out atomically with regards to memory.

atomic 是 CPU 实现的,所以,是的,在不同的 CPU 上实现肯定有差别。在讨论 atomic 的时候,我们更多关注是内存对齐,内存一致性,内存读写顺序的问题。

参考

  1. ^ https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-interrupt-service-routines
  2. ^ https://en.wikipedia.org/wiki/Interrupt_flag
user avatar

是真正的原子,直接对应到编译器的原子intrinsics,对应到CPU的相应指令。

都不是。你对原子的理解有偏差。

user avatar

atomic是否用锁:可以通过atomic::is_lock_free和is_always_lock_free来判断。一般对于一些基本类型来说都不会用互斥锁,你传一个比较大的自定义类型进去大概率就得用锁了

atomic的实现:对于不用的cpu体系结构来说确实是不同的,比如你说的原子自增操作,对x86来说一个lock前缀就搞定了,对于其他体系结构可能需要在循环中使用ll/sc指令对或者cas操作来做一些争抢。

无论是lock前缀还是循环式的争抢,都只是指令层面的动作,与操作系统无关,不会禁用操作系统对其他线程的调度,一般只是锁住总线来做一个原子的读取和更新(在不使用ring bus而使用其他互联网络的系统中会更复杂一些),其他核上的线程还是在正常运行,只是可能因为总线被占用而某条指令卡住一小会。在使用ring bus的处理器中,实际上即使你不用atomic,只是单纯做一个对齐的写操作,总线也会被你占住一段时间, 这种占住总线的行为正是缓存一致性的serialization特性的要求

类似的话题

  • 回答
    在 C++ 中讨论 `std::atomic` 是否是“真正的原子”时,我们需要拨开表面的术语,深入理解其底层含义和实际应用。答案并非一个简单的“是”或“否”,而是取决于你对“原子”的理解以及在什么上下文中去考量。首先,让我们明确一下在并发编程领域,“原子性”(Atomicity)通常指的是一个操作.............
  • 回答
    在 C++ 中,`std::string` 声明在循环内部还是外部,这并非一个简单的“总是这样做”的问题,而是涉及到效率、内存管理、以及代码意图的考量。这就像是在问,你是在路边买了个三明治边走边吃,还是回家坐下来慢慢享用。两者都有各自的场景和理由。让我们深入剖析一下这两种做法: 声明在循环外部当我们.............
  • 回答
    在 C++ 中,为基类添加 `virtual` 关键字到析构函数是一个非常重要且普遍的实践,尤其是在涉及多态(polymorphism)的场景下。这背后有着深刻的内存管理和对象生命周期管理的原理。核心问题:为什么需要虚析构函数?当你在 C++ 中使用指针指向一个派生类对象,而这个指针的类型是基类指针.............
  • 回答
    结构体变量的读写速度 并不比普通变量快。这是一个常见的误解。事实上,在很多情况下,访问结构体成员的开销会比直接访问普通变量稍微 大一些,而不是更小。要详细解释这一点,我们需要深入理解 C++ 中的变量、内存模型以及编译器的工作方式。 1. 普通变量的读写首先,我们来看看一个简单的普通变量,例如:``.............
  • 回答
    在C++中,表达式 `unsigned t = 2147483647 + 1 + 1;` 的求值过程,既不是UB(Undefined Behavior),也不是ID(ImplementationDefined Behavior),而是一个有明确定义的整数溢出(Integer Overflow)行为。.............
  • 回答
    关于C++自定义函数写在 `main` 函数之前还是之后的问题,这涉及到C++的编译和链接过程,以及我们编写代码时的可读性和维护性。理解这一点,对你写出更健壮、更易于理解的代码非常有帮助。总的来说, 将自定义函数写在 `main` 函数之前通常是更推荐的做法,尤其是对于项目中主要的、被 `main`.............
  • 回答
    在C++中,函数返回并不是一个简单地“跳出去”的操作,它涉及到多个步骤,并且与值的传递方式、调用栈以及编译器优化等因素紧密相关。我们来详细拆解一下这个过程,力求还原真实的执行场景。核心概念:调用栈 (Call Stack)要理解函数返回,就必须先理解调用栈。当你调用一个函数时,程序会在调用栈上为这个.............
  • 回答
    在 C++ 中,将 `std::string` 类型转换为 `int` 类型有几种常见且强大的方法。理解它们的原理和适用场景对于编写健壮的代码至关重要。下面我将详细介绍几种常用的方法,并分析它们的优缺点: 方法一:使用 `std::stoi` (C++11 及以后版本)这是 最推荐 的方法,因为它提.............
  • 回答
    vector 和 stack 在 C++ 中都有各自的用处,它们虽然都属于序列容器,但设计目标和侧重点不同。可以这么理解:vector 就像一个可以随意伸缩的储物空间,你可以按照任何顺序往里面放东西,也可以随时拿出任何一个东西。而 stack 就像一个堆叠的盘子,你只能在最上面放盘子,也只能从最上面.............
  • 回答
    在C++中,区分 `char` 和数值(如 `int`, `float`, `double` 等)是编程中的基本概念,但理解其背后的机制能帮助你写出更健壮的代码。首先,我们需要明确一点:在C++底层,`char` 类型本质上也是一种整数类型。它通常用来存储单个字符的ASCII码值或其他编码标准下的数.............
  • 回答
    在C++中,我们不能直接“判断”一个指针指向的是栈(stack)还是堆(heap)。这种判断本身在很多情况下是不明确的,而且C++标准并没有提供直接的运行时机制来做到这一点。不过,我们可以通过一些间接的思考和观察来理解这个问题,并解释为什么直接判断很困难,以及我们通常是如何“知道”一个指针指向哪里。.............
  • 回答
    在 C++ 中,对整数进行除以 2 和右移 1 看起来很相似,它们都能将数字“减半”。但实际上,它们在底层执行机制、对负数和浮点数的影响,以及一些细微之处存在显著差异。我们来深入剖析一下。 除以 2 (`/ 2`):标准的算术运算在 C++ 中,`a / 2` 是一个标准的算术除法运算。它遵循正常的.............
  • 回答
    在 C 中,`async` 和 `await` 关键字提供了一种优雅的方式来编写异步代码,但它们并非直接等同于多线程。理解这一点至关重要。异步并非强制多线程,但常常借助它首先,我们要明确一个核心概念:异步编程的本质是为了提高程序的响应性和吞吐量,而不是简单地将任务并行执行。 异步的目的是让程序在等待.............
  • 回答
    如果 C 真的引入了类似 F 那样的管道运算符 “|>”,这无疑会是一场不小的革新,尤其是在函数式编程风格日益受到重视的今天。那么,它会带来什么变化?我们的代码会变成什么样?首先,我们得理解 F 中的管道运算符 `|>` 是做什么的。简单来说,它就是将一个表达式的结果作为另一个函数调用的第一个参数传.............
  • 回答
    在C中确实不存在Java或C++那样的“友元类”(friend class)机制。这常常让习惯了这种特性的开发者感到不适应,甚至认为这种设计“不太合理”。但实际上,C的设计哲学侧重于封装和明确的接口,友元类这种打破封装的特性并非是其追求的目标。那么,这种设计真的“不合理”吗?或者说,我们是否可以找到.............
  • 回答
    在C++中,当你在一个对象的成员函数内部执行 `delete this;` 时,对象的析构函数会先被调用,然后 `delete` 操作才会完成,并将内存释放。让我们来详细拆解一下这个过程,避免任何可能引起误解的地方。 核心机制:`delete this;` 的工作原理`delete this;` 这.............
  • 回答
    在 C++ 中处理超出标准 `char`、`int` 等基本数据类型表示范围的整数,其实并不是一个“存储”的问题,而是一个选择更合适数据类型的问题。C++ 为我们提供了多种整数类型,每种类型都有其固定的存储大小和取值范围。当我们需要处理的数值超出了某个类型的默认范围时,我们就需要选用更大的类型来容纳.............
  • 回答
    在C++中,当你使用指针作为 `std::map` 或 `std::set` 的键时,是否能改变键指向的对象,这涉及到指针的拷贝语义和容器内部的工作机制。理解这一点,我们需要深入分析以下几个方面:1. C++ 中的拷贝语义与指针首先,需要明确C++中拷贝一个指针时发生了什么。当你将一个指针赋值给另一.............
  • 回答
    在 C++ 编程中,指针和引用都是用来间接访问内存中数据的强大工具,但它们扮演的角色以及使用方式却各有侧重。很多人会疑惑,既然有了引用,为什么还需要指针呢?我们来深入聊聊这个问题。 指针:内存地址的直接操纵者简单来说,指针是一个变量,它存储的是另一个变量的内存地址。你可以想象一个房间的门牌号,这个门.............
  • 回答
    在C语言中,`struct`(结构体)之所以能成为构建复杂数据结构的基石,在于它提供了将不同类型的数据成员组合成一个单一逻辑单元的能力。这就像我们在现实生活中将不同零散的物品(姓名、年龄、学号等)打包成一个“学生”的概念一样。让我们一层层剥开,看看`struct`是如何做到这一点的,以及它在数据结构.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有