问题

C++ 中 new 和定位 new 的返回值都是 void*,却为什么可以被赋值给不同类型的指针?

回答
在 C++ 中,`new` 和定位 `new` 操作符的返回值都是 `void`,这确实是它们能够被赋值给不同类型指针的关键。要理解这一点,我们需要深入 C++ 的内存管理和类型转换机制。

`new` 操作符:动态内存分配的通用接口

首先,我们来看看 `new` 操作符。它的核心作用是在堆(heap)上分配一块内存,用于存放我们指定类型的数据。

1. 内存分配: 当你写 `new T`(其中 `T` 是任何类型),底层的 C++ 运行时库会做两件事:
请求内存: 它会向操作系统请求一块足够大的内存来存储一个 `T` 类型对象。这个请求通常是通过底层 C 函数 `malloc` 来实现的。`malloc` 的原型是 `void malloc(size_t size);`,它接受一个 `size_t` 类型的参数,表示需要分配的字节数,并返回一个 `void` 指针,指向这块分配好的内存的起始地址。
返回 `void`: `new` 操作符直接接收 `malloc` 返回的 `void`。`void` 是一个“通用指针”类型,它不指向任何特定的数据类型,因此可以指向任何类型的数据。你可以把它想象成一个“原始的内存地址”,它知道内存的位置,但不知道这块内存里存放的是什么类型的数据。

2. 类型构造: 在分配完内存之后(或者说,在 `malloc` 返回 `void` 之后),`new` 操作符还会调用 `T` 类型的构造函数,在分配好的内存上构造一个 `T` 类型的对象。这是 `new` 与 `malloc` 的主要区别之一:`malloc` 只负责分配内存,不负责初始化;而 `new` 则在分配内存的基础上,完成了对象的构造。

3. 返回值: 最终,`new` 操作符返回一个指向新分配并构造好的 `T` 类型对象的指针,这个指针的类型就是 `T`。

那为什么 `new` 操作符的“底层”会返回 `void`,然后我们却可以直接得到 `T` 呢?

这涉及到 C++ 的类型推断和隐式转换(或者更准确地说,是 `new` 操作符重载的返回类型)。

虽然 `malloc` 返回 `void`,但 C++ 标准规定了 `new` 操作符的返回值类型。当你写 `T ptr = new T;` 时,编译器知道你期望得到一个 `T` 类型的指针。

`new T` 的表达式在执行时,会先进行内存分配(得到 `void`),然后进行构造。
C++ 的 `new` 操作符本身被重载了,它会根据你使用的类型 `T` 来返回相应类型的指针。
编译器在处理 `new T` 这个表达式时,会“知道”它要分配 `sizeof(T)` 字节的内存,并且最终返回的是 `T` 类型的指针。
因此,`new T` 的结果在表达式的层面上,直接就是一个 `T` 类型的指针,并不会暴露底层的 `void`。

总结 `new` 的过程:

`T ptr = new T;`

1. 编译器计算 `sizeof(T)`。
2. 调用 `operator new(sizeof(T))`(这是全局或类的 `new` 操作符)。
3. `operator new` 最终会调用 `malloc` 来分配内存,返回 `void`。
4. `new T` 的核心部分(placement new 的变体)负责在 `void` 指向的内存上调用 `T` 的构造函数。
5. 整个 `new T` 表达式的结果被类型化为 `T`。

所以,你在 `new T` 后面直接得到的是 `T`,而不是 `void`。你写的 `new` 是一个表达式,它的结果是一个具有正确类型的指针。

定位 `new` (Placement New):在你指定的内存上构造对象

定位 `new` 是一种特殊的 `new` 语法,它允许你在已经分配好的一块内存上构造一个对象。它的语法是 `new (placementexpression) T` 或 `new (placementexpression) T(initializerlist)`。

`placementexpression` 是一个表达式,它计算出一个 `void` 指针,指向你希望放置对象的那块内存。

定位 `new` 的返回值:

定位 `new` 的表达式 `new (ptr) T(...)` 也会返回一个 `T` 类型的指针,指向在 `ptr` 所指向的内存上新构造的 `T` 对象。

为什么定位 `new` 看起来更直接地与 `void` 相关?

定位 `new` 的操作,顾名思义,就是“放置”一个对象在某个特定的内存位置。这个位置是由你提供的 `void` 指针来指定的。

1. 你提供 `void`: 你需要自己先准备好一块内存,并用一个 `void` 指针指向它。这块内存可以是通过 `malloc` 分配的,也可以是堆栈上的数组,或者是某个对象成员的地址。
2. 调用 `operator new(size_t, void)`: 当你写 `new (ptr) T(...)` 时,实际上是在调用一个特殊的 `operator new` 函数。这个函数被重载了,它接受一个 `void` 参数(即 `ptr`)和可能还有构造函数需要的参数。
3. 构造对象: 这个特殊的 `operator new` 函数不负责分配内存(因为内存已经由你提供了)。它的主要工作是在 `ptr` 指向的内存地址上,调用 `T` 的构造函数(以及 `initializerlist` 中的初始化)。
4. 返回 `T`: 就像普通的 `new` 一样,定位 `new` 的表达式 `new (ptr) T(...)` 的最终结果是一个 `T` 类型的指针,指向在 `ptr` 指向的内存上成功构造的 `T` 对象。

关键点:`new (ptr) T` 表达式的返回值是 `T`,而不是 `void`。

你可能会觉得困惑,因为我们传入的是 `void`。但是,`new` 表达式的返回值类型是根据 `T` 来确定的。编译器知道你正在使用 `T` 类型,因此 `new (ptr) T` 这个表达式的结果被类型化为 `T`。

举个例子:

```c++
include
include // For placement new

class MyClass {
public:
int value;
MyClass(int v) : value(v) {
std::cout << "MyClass constructor called with value: " << value << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called for value: " << value << std::endl;
}
};

int main() {
// 1. Standard new
MyClass obj1 = new MyClass(10); // Returns MyClass
std::cout << "obj1 value: " << obj1>value << std::endl;
delete obj1; // Calls destructor and frees memory

std::cout << "" << std::endl;

// 2. Placement new
char buffer[sizeof(MyClass)]; // Allocate raw memory

// The expression new (buffer) MyClass(20) returns MyClass
MyClass obj2 = new (buffer) MyClass(20);

std::cout << "obj2 value: " << obj2>value << std::endl;

// For placement new, we MUST manually call the destructor
// and we CANNOT use delete on the pointer.
obj2>~MyClass(); // Manually call destructor

std::cout << "" << std::endl;

// 3. A more common scenario: using placement new with malloc
void raw_memory = malloc(sizeof(MyClass));
if (!raw_memory) {
// Handle allocation failure
return 1;
}

// new (raw_memory) MyClass(30) returns MyClass
MyClass obj3 = new (raw_memory) MyClass(30);
std::cout << "obj3 value: " << obj3>value << std::endl;

// Again, manually call destructor
obj3>~MyClass();

// Free the memory allocated by malloc
free(raw_memory);

return 0;
}
```

输出:

```
MyClass constructor called with value: 10
obj1 value: 10
MyClass destructor called for value: 10

MyClass constructor called with value: 20
obj2 value: 20
MyClass destructor called for value: 20

MyClass constructor called with value: 30
obj3 value: 30
MyClass destructor called for value: 30
```

从输出可以看到,无论是 `new MyClass(10)` 还是 `new (buffer) MyClass(20)` 或 `new (raw_memory) MyClass(30)`,它们返回的都是 `MyClass`。

为什么我们可以赋值给不同类型的指针?

现在回到你的核心问题:为什么 `new` 和定位 `new` 返回的 `void`(或者说,它们实际返回的 `T`,虽然我们讨论了 `T`,但底层分配是 `void`)可以被赋值给不同类型的指针?

这里可能存在一些混淆。`new` 和定位 `new` 的表达式的返回值,并非直接是 `void`,而是对应于你声明的对象类型 `T` 的指针,即 `T`。

但是,如果你显式地声明一个 `void` 变量来接收 `new` 或定位 `new` 的结果,那么它们确实都可以被赋值:

```c++
void generic_ptr;

// Standard new
MyClass obj_ptr = new MyClass(1);
generic_ptr = obj_ptr; // void can hold any object pointer. This is fine.

// Placement new
char buffer[sizeof(MyClass)];
MyClass obj_ptr2 = new (buffer) MyClass(2);
generic_ptr = obj_ptr2; // void can hold any object pointer. This is fine.
```

原因解释:

1. `void` 的通用性: `void` 是 C++ 中设计用来表示“指向任何类型数据的指针”的。它不关心它指向的内存块是什么类型的数据,只关心地址。因此,任何类型的对象指针(如 `int`, `char`, `MyClass`)都可以被安全地赋值给一个 `void` 变量。这种赋值不会丢失信息,因为 `void` 只是一个“地址容器”,它本身不包含类型信息。

2. 类型转换(隐式与显式):
从 `T` 到 `void`: 当你写 `void p = new T;` 时,`new T` 的结果是 `T`。C++ 允许从任何对象指针类型(`T`)隐式转换为 `void`。这是因为 `void` 是一个“未类型化的指针”,它可以指向任何对象的起始地址。
从 `void` 到 `T`: 如果你有一个 `void` 指针,并想把它转换回一个特定类型的指针(比如 `T`),你就必须进行显式类型转换(`static_cast(void_ptr)`)。这是因为编译器不知道 `void` 指向的是什么类型,需要你明确告诉它。如果类型不匹配,就会导致未定义行为。

定位 `new` 的 `void` 参数的角色:

在定位 `new` 的语法 `new (ptr) T(...)` 中,`ptr` 必须是一个 `void`(或者可以隐式转换为 `void` 的指针类型)。这是因为:

内存地址的通用性: `new` 操作符的第一个参数(placementexpression)代表了你想要将对象放置的内存的起始地址。内存地址本身是通用的,不依赖于它上面要构造的对象的类型。所以,使用 `void` 作为地址参数是非常自然的。
`operator new(size_t, void)` 的签名: C++ 标准定义了重载的 `operator new` 函数,其中一个签名就是 `void operator new(size_t size, void ptr);`。当我们使用定位 `new` 时,编译器就是在寻找并调用这个(或类似的,例如带构造函数参数的)`operator new`。

总结来说:

`new` 和定位 `new` 的表达式的返回值是 `T`,即你声明要创建的对象的类型指针。
`void` 是一种通用的指针类型,可以持有任何类型的对象指针。
从 `T` 到 `void` 的赋值是隐式进行的,并且是安全的,因为它只存储地址。
定位 `new` 的“placementexpression”参数是一个 `void`,因为它代表的是一个原始的内存地址,该地址的用途(即上面要构造什么类型的对象)是在 `new` 表达式的后面指定的。

所以,当你看到 `new` 和定位 `new` 的结果被赋值给不同类型的指针时,通常是指将它们的 `T` 返回值赋值给了 `void`(这是允许的),或者更根本地说,是 `void` 本身作为一种通用地址容器,能够持有任何对象的地址。它们返回的并非是“一个可以直接被随意解释为任何类型的指针”,而是“一个指向了特定类型 `T` 对象的指针,但其地址本身可以被 `void` 存储”。

网友意见

user avatar

这里有两个不同的东西,new expression(也叫 new operator)和 operator new()。

new expression 是语言内建机制,不能重载,我们常写的 new T、new T(param) 等就是这个,它返回有类型的指针 T*。new T 做两件固定的事,第一步调用 operator new(),第二步调用 T 的构造函数,这两步都由编译器自动产生。

operator new() 可以重载,它返回 void*,是一块未初始化的原始内存,随后控制返回到内建的 new expression 中。

placement new 的 operator new() 默认实现如下。它的第一个 size_t 是个必须有,但虚置无用的参数。它的功能就是返回已由用户提供的原始内存地址。

       void* operator new(size_t, void* loc) {     return loc; }      

常规的 new expression 执行过程如下:

       #include <new>  class Widget { public:     Widget(Param p) { /*...*/ }     ~Widget() { /*...*/ }     //... };  // 常规的 new expression Widget* pw = new Widget(param);  // 大致等价于以下过程: // [1] 直接调用 operator new() 函数分配原始内存 void* raw = operator new(sizeof(Widget)); // 上面作用类似下面这句 void* raw = malloc(sizeof(Widget));  // [2] 利用 placement new 调用 Widget 的构造函数 // 这里也是本题的关键: new expression 会将 void* 转换为有类型的指针 Widget* Widget* pw = new(raw) Widget(param);      

相应地,常规的 delete expression 执行过程如下:

       // 常规的 delete expression delete pw;  // 大致等价于以下过程: // [1] 手工调用 Widget 的析构函数 pw->~Widget(); // [2] 直接调用 operator delete() 函数释放原始内存 operator delete(raw); // 上面作用类似下面这句 free(raw);      

以上模拟的是 Widget 构造函数中没有抛出异常时的 new-delete 过程。

没有 placement delete expression 语法,只有 placement operator delete() 函数。在用 placement new 时,只要考虑手工调用 Widget 的析构函数即可,而 placement operator delete() 是 Widget 的构造函数中抛出异常时,由编译器自动调用的。

类似的话题

  • 回答
    在 C++ 中,`new` 和定位 `new` 操作符的返回值都是 `void`,这确实是它们能够被赋值给不同类型指针的关键。要理解这一点,我们需要深入 C++ 的内存管理和类型转换机制。 `new` 操作符:动态内存分配的通用接口首先,我们来看看 `new` 操作符。它的核心作用是在堆(heap).............
  • 回答
    在 C++ 中,当你有一个指针,然后让这个指针指向了新的内存地址,而它原来指向的内存地址是通过 `new` 分配出来的,那么原来被指向的那个对象的内存并不会“立刻”被释放。C++ 的内存管理机制需要你主动去处理。让我为你细致地讲讲这个过程,尽量去除那些生硬的、像 AI 才会用的表述。想象一下,你有一.............
  • 回答
    在 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++ 中讨论 `std::atomic` 是否是“真正的原子”时,我们需要拨开表面的术语,深入理解其底层含义和实际应用。答案并非一个简单的“是”或“否”,而是取决于你对“原子”的理解以及在什么上下文中去考量。首先,让我们明确一下在并发编程领域,“原子性”(Atomicity)通常指的是一个操作.............
  • 回答
    在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++ 编程中,指针和引用都是用来间接访问内存中数据的强大工具,但它们扮演的角色以及使用方式却各有侧重。很多人会疑惑,既然有了引用,为什么还需要指针呢?我们来深入聊聊这个问题。 指针:内存地址的直接操纵者简单来说,指针是一个变量,它存储的是另一个变量的内存地址。你可以想象一个房间的门牌号,这个门.............

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

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