我来自 Java 背景并开始使用 C++ 中的对象。但是我想到的一件事是人们经常使用指向对象的指针而不是对象本身,例如这个声明:
Object *myObject = new Object;
而不是:
Object myObject;
或者不使用函数,假设 testFunc()
,如下所示:
myObject.testFunc();
我们必须写:
myObject->testFunc();
但我不明白我们为什么要这样做。我认为这与效率和速度有关,因为我们可以直接访问内存地址。我对吗?
很遗憾,您经常看到动态分配。这只是表明有多少糟糕的 C++ 程序员。
从某种意义上说,你有两个问题捆绑在一起。首先是什么时候应该使用动态分配(使用 new
)?第二个是我们什么时候应该使用指针?
重要的带回家的信息是,您应该始终使用适当的工具来完成工作。在几乎所有情况下,都有比执行手动动态分配和/或使用原始指针更合适和更安全的方法。
动态分配
在您的问题中,您已经演示了两种创建对象的方法。主要区别在于对象的存储持续时间。在块内执行 Object myObject;
时,创建的对象具有自动存储持续时间,这意味着它会在超出范围时自动销毁。当您执行 new Object()
时,对象具有动态存储持续时间,这意味着它会一直保持活动状态,直到您明确地 delete
它。您应该只在需要时使用动态存储持续时间。也就是说,您应该始终在可能的情况下更喜欢创建具有自动存储持续时间的对象。
您可能需要动态分配的主要两种情况:
您需要该对象比当前范围更长 - 该特定内存位置的特定对象,而不是它的副本。如果您可以复制/移动对象(大多数情况下应该如此),您应该更喜欢自动对象。您需要分配大量内存,这可能很容易填满堆栈。如果我们不必关心这个就好了(大多数时候你不应该关心),因为它确实超出了 C++ 的范围,但不幸的是,我们必须处理系统的现实我们正在开发。
当您确实需要动态分配时,您应该将其封装在智能指针或其他执行 RAII 的类型(如标准容器)中。智能指针提供动态分配对象的所有权语义。例如,看一下 std::unique_ptr
和 std::shared_ptr
。如果您适当地使用它们,您几乎可以完全避免执行您自己的内存管理(请参阅 Rule of Zero)。
指针
但是,除了动态分配之外,原始指针还有其他更一般的用途,但大多数都有您应该喜欢的替代方案。和以前一样,除非你真的需要指针,否则总是更喜欢替代方案。
您需要参考语义。有时您想使用指针传递一个对象(不管它是如何分配的),因为您希望传递它的函数能够访问该特定对象(而不是它的副本)。但是,在大多数情况下,您应该更喜欢引用类型而不是指针,因为这正是它们的设计目的。请注意,这不一定是关于将对象的生命周期延长到当前范围之外,如上面的情况 1。和以前一样,如果您可以传递对象的副本,则不需要引用语义。你需要多态性。您只能通过指针或对对象的引用以多态方式(即根据对象的动态类型)调用函数。如果这是您需要的行为,那么您需要使用指针或引用。同样,参考文献应该是首选。您希望通过在省略对象时允许传递 nullptr 来表示对象是可选的。如果它是一个参数,您应该更喜欢使用默认参数或函数重载。否则,您最好使用封装此行为的类型,例如 std::optional(在 C++17 中引入 - 对于早期的 C++ 标准,使用 boost::optional)。您希望解耦编译单元以缩短编译时间。指针的有用属性是您只需要指向类型的前向声明(要实际使用该对象,您需要一个定义)。这允许您解耦编译过程的各个部分,这可能会显着缩短编译时间。请参阅 Pimpl 成语。您需要与 C 库或 C 样式库进行交互。此时,您被迫使用原始指针。你能做的最好的事情是确保你只在最后一刻松开你的原始指针。您可以从智能指针获取原始指针,例如,通过使用其 get 成员函数。如果一个库为您执行了一些分配,它希望您通过句柄解除分配,您通常可以使用自定义删除器将句柄包装在智能指针中,该删除器将适当地解除分配对象。
指针有很多用例。
多态行为。对于多态类型,使用指针(或引用)来避免切片:
class Base { ... };
class Derived : public Base { ... };
void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }
Derived d;
fun(d); // oops, all Derived parts silently "sliced" off
gun(&d); // OK, a Derived object IS-A Base object
hun(d); // also OK, reference also doesn't slice
引用语义并避免复制。对于非多态类型,指针(或引用)将避免复制潜在昂贵的对象
Base b;
fun(b); // copies b, potentially expensive
gun(&b); // takes a pointer to b, no copying
hun(b); // regular syntax, behaves as a pointer
请注意,C++11 具有移动语义,可以避免将昂贵对象的许多副本复制到函数参数中并作为返回值。但是使用指针肯定会避免这些,并允许在同一个对象上使用多个指针(而一个对象只能从一次移动)。
资源获取。使用 new
运算符创建指向资源的指针是现代 C++ 中的一种反模式。使用特殊资源类(标准容器之一)或智能指针(std::unique_ptr<>
或 std::shared_ptr<>
)。考虑:
{
auto b = new Base;
... // oops, if an exception is thrown, destructor not called!
delete b;
}
对比
{
auto b = std::make_unique<Base>();
... // OK, now exception safe
}
原始指针仅应用作“视图”,而不应以任何方式涉及所有权,无论是通过直接创建还是通过返回值隐含。另见this Q&A from the C++ FAQ。
更细粒度的生命周期控制 每次复制共享指针(例如作为函数参数)时,它指向的资源都会保持活动状态。常规对象(不是由 new
创建的,由您直接创建或在资源类中创建)在超出范围时会被销毁。
unique_ptr
/move 语义,不推荐使用创建,而且将原始指针作为参数或暗示所有权转移的返回值传递恕我直言
hun(b)
还需要了解签名,除非您可以在编译之前不知道您提供了错误的类型。虽然引用问题通常不会在编译时被捕获并且会花费更多精力进行调试,但如果您正在检查签名以确保参数正确,您还可以查看是否有任何参数是引用因此引用位成为一个非问题(尤其是在使用显示所选函数签名的 IDE 或文本编辑器时)。另外,const&
。
这个问题有很多很好的答案,包括前向声明、多态等的重要用例,但我觉得你问题的“灵魂”的一部分没有得到回答——即不同语法在 Java 和 C++ 中的含义。
让我们来看看比较两种语言的情况:
爪哇:
Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java
object1 = object2;
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other
与此最接近的是:
C++:
Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use
//and that we have no way to reclaim...
object1 = object2; //Same as Java, object1 points to object2.
让我们看看另一种 C++ 方式:
Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...
考虑它的最佳方式是——或多或少——Java(隐式)处理指向对象的指针,而 C++ 可能处理指向对象的指针或对象本身。这也有例外——例如,如果您声明 Java“原始”类型,它们是被复制的实际值,而不是指针。所以,
爪哇:
int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.
也就是说,使用指针不一定是正确或错误的处理方式;然而,其他答案已经令人满意地涵盖了这一点。但总体思路是,在 C++ 中,您可以更好地控制对象的生命周期以及它们的生存位置。
重要的是——Object * object = new Object()
构造实际上是最接近典型 Java(或 C#)语义的东西。
Object object1 = new Object(); Object object2 = new Object();
是非常糟糕的代码。第二个 new 或第二个 Object 构造函数可能会抛出,现在 object1 被泄露。如果您使用的是原始 new
,则应尽快将 new
对象包装在 RAII 包装器中。
前言
与炒作相反,Java 与 C++ 完全不同。 Java 炒作机器希望您相信,因为 Java 具有类似 C++ 的语法,所以这些语言是相似的。没有什么比事实更离谱了。这种错误信息是 Java 程序员在不了解其代码含义的情况下转向 C++ 并使用类似 Java 的语法的部分原因。
接下来我们去
但我不明白我们为什么要这样做。我认为这与效率和速度有关,因为我们可以直接访问内存地址。我对吗?
相反,实际上。 The heap is much slower 比栈,因为栈比堆很简单。自动存储变量(也称为堆栈变量)一旦超出范围就会调用其析构函数。例如:
{
std::string s;
}
// s is destroyed here
另一方面,如果使用动态分配的指针,则必须手动调用其析构函数。 delete
为您调用此析构函数。
{
std::string* s = new std::string;
}
delete s; // destructor called
这与 C# 和 Java 中流行的 new
语法无关。它们用于完全不同的目的。
动态分配的好处
1.不必提前知道数组的大小
许多 C++ 程序员遇到的第一个问题是,当他们接受来自用户的任意输入时,您只能为堆栈变量分配固定大小。您也不能更改数组的大小。例如:
char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow
当然,如果您改为使用 std::string
,则 std::string
会在内部调整自身大小,这样就不会有问题。但本质上解决这个问题的方法是动态分配。您可以根据用户的输入分配动态内存,例如:
int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];
旁注:许多初学者犯的一个错误是使用可变长度数组。这是一个 GNU 扩展,也是 Clang 中的一个,因为它们反映了 GCC 的许多扩展。因此,不应依赖以下 int arr[n] 。
因为堆比栈大得多,所以可以任意分配/重新分配他/她需要的内存,而栈有一个限制。
2.数组不是指针
你问这有什么好处?一旦你理解了数组和指针背后的困惑/神话,答案就会变得清晰。通常认为它们是相同的,但它们不是。这个神话来自这样一个事实,即指针可以像数组一样下标,并且由于数组衰减到函数声明中顶层的指针。但是,一旦数组衰减为指针,指针就会丢失其 sizeof
信息。所以 sizeof(pointer)
将以字节为单位给出指针的大小,在 64 位系统上通常为 8 个字节。
您不能分配给数组,只能初始化它们。例如:
int arr[5] = {1, 2, 3, 4, 5}; // initialization
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
// be given by the amount of members in the initializer
arr = { 1, 2, 3, 4, 5 }; // ERROR
另一方面,你可以用指针做任何你想做的事情。不幸的是,由于指针和数组之间的区别在 Java 和 C# 中是手牵手的,因此初学者并不了解其中的区别。
3. 多态性
Java 和 C# 具有允许您将对象视为另一个对象的工具,例如使用 as
关键字。因此,如果有人想将 Entity
对象视为 Player
对象,则可以这样做 Player player = Entity as Player;
如果您打算在应仅适用于特定类型的同类容器上调用函数,这将非常有用。该功能可以通过以下类似方式实现:
std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
if (!test) // not a triangle
e.GenericFunction();
else
e.TriangleOnlyMagic();
}
所以说如果只有 Triangles 有一个 Rotate 函数,如果你试图在类的所有对象上调用它,那将是一个编译器错误。使用 dynamic_cast
,您可以模拟 as
关键字。需要明确的是,如果强制转换失败,它会返回一个无效的指针。所以 !test
本质上是检查 test
是否为 NULL 或无效指针的简写,这意味着强制转换失败。
自动变量的好处
在看到动态分配可以做的所有伟大事情之后,您可能想知道为什么没有人不一直使用动态分配?我已经告诉过你一个原因,堆很慢。如果你不需要所有的记忆,你不应该滥用它。所以这里有一些不按特定顺序排列的缺点:
它容易出错。手动分配内存很危险,而且很容易发生泄漏。如果你不熟练使用调试器或 valgrind(一种内存泄漏工具),你可能会把头发从头上扯下来。幸运的是,RAII 习语和智能指针稍微缓解了这一点,但您必须熟悉诸如三规则和五规则之类的实践。需要吸收的信息很多,不知道或不在意的初学者会掉入这个陷阱。
没有必要。与 Java 和 C# 不同的是,在任何地方都习惯使用 new 关键字,在 C++ 中,您应该只在需要时使用它。俗话说,如果你有一把锤子,一切看起来都像钉子。 C++ 初学者害怕指针,习惯性地学习使用堆栈变量,而 Java 和 C# 程序员从不了解指针就开始使用指针!这实际上是走错了路。你必须放弃你所知道的一切,因为语法是一回事,学习语言是另一回事。
1. (N)RVO - 又名,(命名)返回值优化
许多编译器进行的一项优化称为省略和返回值优化。这些东西可以避免不必要的副本,这对于非常大的对象很有用,例如包含许多元素的向量。通常,通常的做法是使用指针来转移所有权,而不是复制大对象来移动它们。这导致了移动语义和智能指针的出现。
如果您使用指针,则 (N)RVO 不会出现 NOT。如果您担心优化,利用 (N)RVO 而不是返回或传递指针更有益且不易出错。如果函数的调用者负责delete
动态分配的对象等,则可能会发生错误泄漏。如果指针像烫手山芋一样被传递,那么跟踪对象的所有权可能会很困难。只需使用堆栈变量,因为它更简单更好。
{ std::string* s = new std::string; } delete s; // destructor called
....肯定这个 delete
不会工作,因为编译器将不再知道 s
是什么?
使用指针的另一个好理由是 forward declarations。在足够大的项目中,它们确实可以加快编译时间。
std::unique_ptr<T>
确实适用于 T
的前向声明。您只需要确保在调用 std::unique_ptr<T>
的析构函数时,T
是一个完整类型。这通常意味着包含 std::unique_ptr<T>
的类在头文件中声明其析构函数并在 cpp 文件中实现它(即使实现为空)。
在 C++ 中,分配在堆栈上的对象(在块中使用 Object object;
语句)将仅存在于声明它们的范围内。当代码块完成执行时,声明的对象将被销毁。然而,如果您使用 Object* obj = new Object()
在堆上分配内存,它们将继续存在于堆中,直到您调用 delete obj
。
当我不仅喜欢在声明/分配它的代码块中使用对象时,我会在堆上创建一个对象。
Object obj
并不总是在堆栈中 - 例如全局变量或成员变量。
C++ 提供了三种传递对象的方法:指针、引用和值。 Java 限制了你使用后者(唯一的例外是原始类型,如 int、boolean 等)。如果你想使用 C++ 而不仅仅是一个奇怪的玩具,那么你最好了解这三种方式之间的区别。
Java 假装不存在“谁应该在什么时候销毁这个?”这样的问题。答案是:垃圾收集器,伟大而可怕。然而,它不能提供 100% 的内存泄漏保护(是的,java can leak memory)。实际上,GC 给你一种错误的安全感。您的 SUV 越大,通往疏散器的路越长。
C++ 让您与对象的生命周期管理面对面。好吧,有办法解决这个问题(smart pointers 系列,Qt 中的 QObject 等等),但它们都不能像 GC 那样以“即发即弃”的方式使用:你应该始终请记住内存处理。您不仅应该关心破坏一个对象,还必须避免多次破坏同一个对象。
还不害怕?好的:循环引用 - 自己处理它们,人类。请记住:精确地杀死每个对象一次,我们 C++ 运行时不喜欢那些乱搞尸体的人,不理会死人。
所以,回到你的问题。
当您通过值而不是指针或引用传递对象时,您会复制对象(整个对象,无论是几个字节还是巨大的数据库转储 - 您足够聪明,可以避免后者,不是'你呢?)每次你做'='。要访问对象的成员,请使用“。” (点)。
当您通过指针传递对象时,您只复制了几个字节(32 位系统上为 4 个,64 位系统上为 8 个),即 - 该对象的地址。为了向所有人展示这一点,您在访问成员时使用了这个花哨的 '->' 运算符。或者您可以使用“*”和“.”的组合。
当你使用引用时,你会得到一个伪装成值的指针。它是一个指针,但您可以通过“.”访问成员。
而且,再一次让你大吃一惊:当你声明几个用逗号分隔的变量时,然后(注意手):
类型是给每个人的
值/指针/引用修饰符是单独的
例子:
struct MyStruct
{
int* someIntPointer, someInt; //here comes the surprise
MyStruct *somePointer;
MyStruct &someReference;
};
MyStruct s1; //we allocated an object on stack, not in heap
s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'
s1.someReference.someInt = 5; //now s1.someInt has value '5'
//although someReference is not value, it's members are accessed through '.'
MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.
//OK, assume we have '=' defined in MyStruct
s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one
std::auto_ptr
已弃用,请不要使用它。
但我想不通我们为什么要这样使用它?
如果您使用,我将比较它在函数体内的工作方式:
Object myObject;
在函数内部,一旦此函数返回,您的 myObject
将被销毁。因此,如果您不需要函数之外的对象,这很有用。该对象将被放入当前线程堆栈。
如果你在函数体内写:
Object *myObject = new Object;
那么 myObject
指向的 Object 类实例将不会在函数结束后被销毁,并且分配在堆上。
现在,如果您是 Java 程序员,那么第二个示例更接近 Java 下对象分配的工作方式。此行:Object *myObject = new Object;
等效于 java:Object myObject = new Object();
。不同之处在于,在 java 下 myObject 会被垃圾回收,而在 c++ 下它不会被释放,你必须在某个地方显式调用 `delete myObject;'否则你会引入内存泄漏。
从 c++11 开始,您可以使用安全的动态分配方式:new Object
,通过将值存储在 shared_ptr/unique_ptr 中。
std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");
// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared");
此外,对象通常存储在容器中,例如 map-s 或 vector-s,它们会自动管理对象的生命周期。
then myObject will not get destroyed once function ends
绝对会。
myObject
仍将被销毁,就像任何其他局部变量一样。不同之处在于它的值是指向对象的指针,而不是对象本身,并且哑指针的销毁不会影响其指针对象。所以 object 将在所述破坏中幸存下来。
从技术上讲,这是一个内存分配问题,但是这里有两个更实际的方面。它与两件事有关:1)范围,当您定义一个没有指针的对象时,您将无法在定义它的代码块之后再访问它,而如果您使用“new”定义一个指针,那么您可以从任何有指向此内存的指针的地方访问它,直到您在同一指针上调用“删除”。 2)如果你想将参数传递给一个函数,你想传递一个指针或引用以提高效率。当您传递一个对象时,该对象将被复制,如果这是一个使用大量内存的对象,这可能会消耗 CPU(例如,您复制一个充满数据的向量)。当你传递一个指针时,你传递的只是一个 int(取决于实现,但大多数都是一个 int)。
除此之外,您需要了解“新”在堆上分配需要在某个时候释放的内存。当您不必使用“新”时,我建议您使用“堆栈上”的常规对象定义。
那么主要问题是为什么我应该使用指针而不是对象本身?我的回答是,你应该(几乎)永远不要使用指针而不是对象,因为 C++ 有 references,它更安全then 指针并保证与指针相同的性能。
您在问题中提到的另一件事:
Object *myObject = new Object;
它是如何工作的?它创建 Object
类型的指针,分配内存以适应一个对象并调用默认构造函数,听起来不错,对吧?但实际上它并不是那么好,如果你动态分配内存(使用关键字 new
),你还必须手动释放内存,这意味着在代码中你应该有:
delete myObject;
这会调用析构函数并释放内存,看起来很容易,但是在大型项目中可能很难检测一个线程是否释放了内存,但为此您可以尝试shared pointers,这些会稍微降低性能,但工作起来要容易得多跟他们。
现在一些介绍已经结束,回到问题。
在函数之间传输数据时,您可以使用指针而不是对象来获得更好的性能。
看一看,你有 std::string
(它也是对象),它包含很多数据,例如大 XML,现在你需要解析它,但为此你有函数 void foo(...)
,它可以用不同的方式声明:
无效 foo(std::string xml);在这种情况下,您会将变量中的所有数据复制到函数堆栈,这需要一些时间,因此您的性能会很低。无效 foo(std::string* xml);在这种情况下,您将传递指向对象的指针,与传递 size_t 变量的速度相同,但是此声明容易出错,因为您可以传递 NULL 指针或无效指针。指针通常在 C 中使用,因为它没有引用。无效 foo(std::string& xml);这里传递引用,基本上和传递指针是一样的,但是编译器做了一些事情,你不能传递无效的引用(实际上可以创建无效引用的情况,但它欺骗了编译器)。 void foo(const std::string* xml);这里和第二个一样,只是指针值不能改变。 void foo(const std::string& xml);这里和第三个一样,但是对象值不能改变。
我还想提一下,无论您选择哪种分配方式(使用 new
或 regular),都可以使用这 5 种方式来传递数据。
另一件事要提一下,当您以 regular 方式创建对象时,您会在堆栈中分配内存,但是当您使用 new
创建它时,您会分配堆。分配堆栈要快得多,但对于非常大的数据数组来说它有点小,所以如果你需要大对象,你应该使用堆,因为你可能会出现堆栈溢出,但通常这个问题可以使用 STL containers 和记住 std::string
也是容器,有些人忘记了 :)
假设您有包含 class B
的 class A
当您想在 class A
之外调用 class B
的某些函数时,您只需获得指向此类的指针,您可以做任何您想做的事情,它也会改变上下文您的 class A
中的 class B
但要小心动态对象
使用指向对象的指针有很多好处 -
效率(正如您已经指出的那样)。将对象传递给函数意味着创建对象的新副本。使用来自第三方库的对象。如果您的对象属于第三方代码,并且作者打算仅通过指针(没有复制构造函数等)使用他们的对象,那么您可以绕过该对象的唯一方法是使用指针。按值传递可能会导致问题。 (深拷贝/浅拷贝问题)。如果对象拥有资源并且您希望所有权不与其他对象共享。
这已经详细讨论过了,但在 Java 中一切都是指针。它没有区分堆栈和堆分配(所有对象都在堆上分配),因此您没有意识到您正在使用指针。在 C++ 中,您可以根据您的内存要求将两者混合使用。性能和内存使用在 C++ 中更具确定性(duh)。
Object *myObject = new Object;
这样做将创建对必须显式删除以避免内存泄漏的对象(在堆上)的引用。
Object myObject;
这样做将创建一个自动类型的对象(myObject)(在堆栈上),当对象(myObject)超出范围时将自动删除该对象(myObject)。
指针直接引用对象的内存位置。 Java 没有这样的东西。 Java 具有通过哈希表引用对象位置的引用。你不能用这些引用在 Java 中做任何像指针算术这样的事情。
要回答您的问题,这只是您的偏好。我更喜欢使用类似 Java 的语法。
C++ 中对象指针的关键优势是允许多态数组和同一超类的指针映射。例如,它允许将鹦鹉、鸡、知更鸟、鸵鸟等放入 Bird 数组中。
此外,动态分配的对象更灵活,可以使用 HEAP 内存,而本地分配的对象将使用 STACK 内存,除非它是静态的。堆栈上有大对象,尤其是在使用递归时,无疑会导致堆栈溢出。
使用指针的原因之一是为了与 C 函数交互。另一个原因是节省内存;例如:与其将包含大量数据并具有处理器密集型复制构造函数的对象传递给函数,不如将指针传递给该对象,从而节省内存和速度,尤其是在循环中时,但是在这种情况下,reference 会更好,除非您使用的是 C 样式的数组。
在内存利用率非常高的领域,指针会派上用场。例如,考虑一个极小极大算法,其中将使用递归例程生成数千个节点,然后使用它们来评估游戏中的下一个最佳移动,释放或重置的能力(如智能指针)显着减少了内存消耗。而非指针变量继续占用空间,直到它的递归调用返回一个值。
我将介绍一个重要的指针用例。当您在基类中存储某些对象时,它可能是多态的。
Class Base1 {
};
Class Derived1 : public Base1 {
};
Class Base2 {
Base *bObj;
virtual void createMemerObects() = 0;
};
Class Derived2 {
virtual void createMemerObects() {
bObj = new Derived1();
}
};
所以在这种情况下,你不能将 bObj 声明为直接对象,你必须有指针。
tl; dr:不要“使用指针而不是对象本身”(通常)
你问为什么你应该更喜欢指针而不是对象本身。嗯,你不应该,作为一般规则。
现在,这条规则确实有多个例外,其他答案已经说明了它们。问题是,这些天来,许多这些例外不再有效!让我们考虑 accepted answer 中列出的例外情况:
您需要参考语义。
如果您需要引用语义,请使用 references,而不是指针;请参阅 @ST3's answer 的 answer。事实上,有人可能会争辩说,在 Java 中,您传递的通常是引用。
你需要多态性。
如果您知道要使用的类集,通常只需使用 std::variant<ClassA, ClassB, ClassC>
(参见说明 here)并使用访问者模式对它们进行操作。现在,诚然,C++ 的变体实现并不是最漂亮的。但我通常更喜欢它而不是用指针弄脏。
你想表示一个对象是可选的
绝对不要为此使用指针。您有 std::optional
,与 std::variant
不同,它非常方便。改用那个。 nullopt
是一个空(或“null”)可选。而且 - 它不是一个指针。
您希望解耦编译单元以缩短编译时间。
您也可以使用引用而不是指针来实现这一点。要在一段代码中使用Object&
,只需说class Object;
,即使用forward-declaration。
您需要与 C 库或 C 样式库进行交互。
是的,好吧,如果您使用已经使用指针的代码,那么-您必须自己使用指针,无法解决这个问题:-(并且C没有引用。
此外,有些人可能会告诉您使用指针来避免复制对象。好吧,由于 return-value and named-return-value optimizations (RVO and NRVO),这对于返回值来说并不是一个真正的问题。在其他情况下 - 引用避免复制就好了。
不过,底线规则仍然与公认的答案相同:仅当您有充分的理由需要指针时才使用指针。
PS - 如果您确实需要指针,您仍然应该avoid using new
and delete
directly。 smart pointer 可能会更好地为您服务 - 它会自动释放(不像 Java,但仍然如此)。
用指针,
可以直接和内存对话。
可以通过操作指针来防止程序的大量内存泄漏。
“需要是发明之母。”我想指出的最重要的区别是我自己编码经验的结果。有时您需要将对象传递给函数。在这种情况下,如果您的对象属于一个非常大的类,那么将其作为对象传递将复制其状态(您可能不想要 ..并且可以是大开销),从而导致复制对象的开销 .while 指针是固定的4 字节大小(假设为 32 位)。其他原因上面已经提到了……
std::string test;
一样通过常量引用传递 void func(const std::string &) {}
但除非函数需要更改输入,在这种情况下我建议使用指针(这样任何阅读代码的人都会注意到 &
,并理解函数可能会改变它的输入)
已经有很多很好的答案,但让我举一个例子:
我有一个简单的 Item 类:
class Item
{
public:
std::string name;
int weight;
int price;
};
我制作了一个向量来容纳一堆。
std::vector<Item> inventory;
我创建了一百万个 Item 对象,并将它们推回向量上。我按名称对向量进行排序,然后对特定项目名称进行简单的迭代二进制搜索。我对程序进行了测试,完成执行需要 8 多分钟。然后我像这样更改我的库存向量:
std::vector<Item *> inventory;
...并通过 new 创建我的百万个 Item 对象。我对代码所做的唯一更改是使用指向项目的指针,除了我在最后添加的用于内存清理的循环。该程序在 40 秒内运行,或者比 10 倍的速度提升要好。编辑:代码位于 http://pastebin.com/DK24SPeW 通过编译器优化,它在我刚刚测试它的机器上只显示了 3.4 倍的增长,这仍然是相当可观的。
push_back
。当然这个副本。您应该在创建对象时emplace
就地执行(除非您需要将它们缓存在其他地方)。
Object myObject(param1, etc...)