c++ - 为什么要使用指针而非对象本身?

  显示原文与译文双语对照的内容

我来自一个Java背景,已经开始使用 C++ 中的对象。 但是,我所发生的一件事情是人们经常使用指针到对象而不是对象本身,例如:


Object *myObject = new Object;

而不是:

 
Object myObject;

 

或者,不使用函数,比如说 testFunc(),如下所示:

 
myObject.testFunc();

 

我们必须写:


myObject->testFunc();

但我不知道为什么我们要这样做。 我假设它与效率和速度有关,因为我们直接访问内存地址。 我说的对吧?

时间:

很遗憾,你经常看到动态分配。 这只是显示了有多少错误的C++ 程序员。

你在某种意义上有两个问题被捆绑在一起。 第一个是什么时候应该使用动态分配( 使用 new )? 第二个问题是我们应该使用指针?

重要take-home消息你应该总是使用适当的工具进行工作。 在几乎所有情况下,有比手工动态分配和/或者使用原始指针更合适和更安全的方法。

动态分配

在你的问题中,你演示了两种创建对象的方法。 主要差异是对象的存储持续时间。 Object myObject; 做在一块时,对象创建和自动存储时间,这意味着它将自动被摧毁时超出范围。 当你 new Object(), 对象动态存储时间,这意味着它保持活着直到你明确 delete 它。 当你需要时,你应该只使用动态存储持续时间。 即你总是应该喜欢创建对象和自动存储时间当你可以

你可能需要动态分配的主要两种情况:

  1. 需要比当前的对象范围——具体对象具体的内存位置,不是它的一个副本。 如果你可以复制/移动对象( 大多数时候你应该是),你应该更喜欢自动对象。
  2. 需要分配大量的内存,这可能很容易填满堆栈。 就好了,如果我们不需要关心这个( 大多数时候你不应该非得), 真的C++ 管辖范围之外的,但不幸的是我们必须处理我们正在开发的系统的现实。

当你绝对需要动态分配,你应该封装在一个智能指针或其他类型执行 RAII ( 和标准容器一样) 。 智能指针提供动态分配对象的所有权语义。 看一下 std::unique_ptrstd::shared_ptr,例如。 如果适当地使用它们,几乎可以完全避免执行自己的内存管理( 参见的零规则) 。

指针

然而,还有其他的更一般的用途原始指针除了动态分配,但大多数选择,你应该喜欢。 和之前一样,总是喜欢选择,除非你真的需要指针

  1. 磅你需要引用语义 。 有时候你想通过一个对象使用一个指针( 不管它是如何分配的) 因为你想要你的函数通过它访问( 不是它的一份副本) 特定对象。 然而,在大多数情况下,你应该更喜欢引用类型指针,因为这是专门为他们设计的。 注意这并不一定要扩展对象的生命周期超出当前范围,如上面的场景 1所示。 和以前一样,如果你可以传递对象的副本,则不需要引用语义。

  2. 你需要 。 你只能通过指针或者对对象的引用来调用函数 polymorphically ( 也就是说,根据对象的动态类型) 。 如果这是你需要的行为,那么你需要使用指针或者引用。 同样,引用应该是首选的。

  3. 你想代表一个对象是可选通过允许一个 nullptr 传递对象时被忽略。 如果是一个参数,你应该使用默认参数或者函数重载。 否则,你应该使用封装这种行为的类型,比如 boost::optional ( 也许很快, std::optional ——编辑std::optional 支持当前 C++14 n3797草案) 。

  4. 希望分离编译单元以提高编译时间的磅。 指针的有用属性是,你只需要pointed-to类型( 要实际使用该对象,你需要一个定义)的前向声明。 这允许你分离编译过程的一部分,这可能会显著提高编译时间。 参见 Pimpl习惯用法

  5. 需要接口与c库或C-style图书馆。 此时,你必须使用原始指针。 你最好做的是确保你的原始指针在最后可能的时刻松开。 你可以从智能指针获取原始指针,例如使用它的get 成员函数。 如果一个库为你执行一些配置,预计你释放通过处理,通常可以将处理与自定义在一个智能指针deleter将适当地释放对象。

指针有很多用例。

多态行为 。对于多态类型,指针( 或者引用) 用于避免切片:


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

引用语义,避免复制 。 对于non-polymorphic类型,指针( 或者引用) 将避免复制可能昂贵的对象


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 操作符是一个 anti-pattern现代 C++ 使用一个特殊的资源类( 标准容器之一) 或者智能指针 ( std::unique_ptr<> 或者 std::shared_ptr<> ) 。 请考虑:


{
 auto b = new Base;
. . .//oops, if an exception is thrown, destructor not called!
 delete b;
}

vs 。


{
 auto b = std::make_unique<Base>();
. . .//OK, now exception safe
}

原始指针只能用作"视图",而不应以任何方式参与所有权,它通过直接创建或者通过返回值隐式。 参见这q&从 C++ FAQ

更多 fine-grained life-time控制每次一个共享的指针被复制(e。g。 作为函数参数,它指向的资源正在保持活动状态。 常规对象( 不是由 new 创建,直接由你或者在资源类中创建) 在超出范围时被销毁。

使用指针的另一个好理由是前向声明 。 在一个足够大的项目中,他们可以真正加快编译时间。

有许多优秀的回答这个问题,但我觉得"灵魂"的一部分,你的问题不回答。

让我们检查这两个语句:

Java:


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", and 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 we get a"memory leak",
//i.e. a piece of claimed memory that the app cannot use and we cannot reclaim it.

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.
//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"基元"类型,则它们是复制的实际值,而不是指针。 那么,

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#的问题) 语义的。

前言

C++ 相反,Java与不一样。 Java宣传机器希望你相信,因为Java具有类似语法的C++,所以语言是相似的。 没有比这更远离真相的了。 这些错误是Java程序员进入 C++ 并使用Java-like语法而不理解它的代码的含义的一部分。

开始吧

但我不知道为什么我们要这样做。 我假设它与效率和速度有关,因为我们直接访问内存地址。 我说的对吧?

相反,实际上,堆比堆栈慢得多,因为堆栈与堆比较简单。 自动存储变量( 又名堆栈变量)的析构函数一旦超出范围就会被调用。 例如:


{
 std::string s;
}
//s is destroyed here

另一方面,如果使用动态分配的指针,则必须手动调用它的析构函数。 delete 为你调用这里析构函数。


{
 std::string* s = new std::string;
}
delete s;//destructor called

这无关 new 在 C# 和java语法普遍。 它们用于完全不同的目的。

动态分配的好处

1.你不需要事先知道数组的大小

许多 C++ 程序员遇到的第一个问题是当他们接受来自用户的任意输入时,你只能为堆栈变量分配一个固定的大小。 不能更改数组的大小。 例如:


char buffer[100];
std::cin>> buffer;
//bad input = buffer overflow

当然,如果你使用 std::stringstd::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.数组不是指针

这怎么会让你觉得? 一旦你理解数组和指针背后的混淆/神话,答案就会变得清晰。 通常假设它们是相同的,但它们不是。 这个谣传来自于一个事实,指针可以像数组一样是 subscripted,因为数组在函数声明中衰减到顶端的指针。 但是,一旦数组衰变为指针,指针就会失去它的sizeof 信息。 所以 sizeof(pointer) 将字节的指针的大小,通常 8字节 64位 系统上。

不能给数组赋值,只能初始化它们。 例如:


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

另一方面,你可以用指针做任何事。 不幸的是,由于指针和数组之间的区别是hand-waved和 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();
}

所以假设只有三角形有旋转函数,如果你试图在类的所有对象上调用它,它是一个编译器错误。 使用 dynamic_cast,你可以模拟 as 关键字。 如果转换失败,则返回无效指针。 所以 test 本质上是一个 shorthand 检查如果 test 是null或无效的指针,这意味着失败。!

自动变量的优点

在看到了动态分配可以做的所有事情之后,你可能想知道为什么没有人总是使用动态分配? 我已经告诉你一个原因,堆是缓慢的。 如果你不需要所有的内存,你就不应该滥用它。 以下是没有特定顺序的一些缺点:

  • 它很容易出错。手动内存分配很危险,而且很容易泄漏。 如果你不擅长使用调试器或者 valgrind ( 内存泄漏工具),你可以把你的头发从你的头发中拔出。 幸运的是,自动化的习惯用法和智能指针缓解了这一点,但是你必须熟悉一些惯例,比如三个规则和五个规则。 里面有很多信息,如果你不知道或者不关心的初学者会落入这个陷阱。

  • 这不是必要的。与Java和 C# 不同的是,在到处使用 new 关键字时,在 C++ 中,如果需要,只使用它。 常见的说法是,如果你有锤子,一切看起来都像钉子。 而初学者开始 C++ 是谁害怕和学习使用堆栈指针变量的习惯,java和 C# 程序员开始通过使用指针没有理解它! 这就是stepping的脚步。 你必须放弃你所知道的一切,因为语法是一回事,学习语言是另一回事。

1.( N ) RVO - Aka,( 命名名) 返回值优化

一个许多编译器优化称为省略优化返回值。 这些东西可以避免不必要的复制,这些复制对于非常大的对象非常有用,比如包含许多元素的向量。 通常的惯例是使用指针所有权转移而不是复制移动他们的大对象。 这导致初始语义智能指针移动。

如果你使用的是指针,( N ) RVO不会发生 更有利和更少的error-prone利用( N ) RVO而不是返回或传递指针如果你担心优化。 如果函数的调用方负责 delete的动态分配的对象,则会发生错误。 如果指针像烫手的土豆一样传递,那么跟踪对象的所有权会很困难。 只使用堆栈变量,因为它更简单更好。

但我不知道为什么我们要这样使用它?

如果你使用的话,我将比较它在函数体内的工作方式:

 
Object myObject;

 

在函数中,一旦函数返回,你的myObject 将被破坏。 所以如果你不需要你的对象之外的对象,这很有用。 这里对象将放置在当前线程堆栈上。

如果你在函数体内写入:


 Object *myObject = new Object;

一旦函数结束,myObject 指向的对象类实例将不会被破坏,并且分配在堆上。

现在,如果你是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,它们将自动管理对象的生命周期。

C++ 提供了三种传递对象的方法: 按指针,按引用和按值。 使用后一个( 唯一的例外是诸如 int,布尔等基元类型) 限制你。 如果你想使用 C++ 而不仅仅是一个奇怪的玩具,你最好知道这三种方法之间的区别。

Java假设没有像'谁和什么时候应该销毁这个'这样的问题。 答案是:垃圾收集器,非常糟糕。 然而,它不能为内存泄漏提供 100%保护( 是的, java泄漏内存) 。 实际上,GC给你一个错误的安全感。 你的SUV越大,你通向evacuator的时间越长。

C++ 让你面对对象管理生命周期。 好的,有一些方法可以处理这个( 智能指针家族,Qt中的QObject等等),但是它们都不能像GC那样使用'and并忘记'方式: 你应该总是 记住内存处理。 不仅要关心一个对象,还必须避免多次销毁同一个对象。

不害怕确定:循环引用- 自己处理,人类。 记住:杀死每个对象正是一次,我们 C++ 运行时不喜欢那些惹尸体,别管死的。

所以,回到你的问题。

当你通过你的对象的值,而不是通过指针或引用,复制对象( 整个对象,不管是几个字节还是一个巨大的数据库转储- 你足够聪明来避免后者,不是你)'='每次你做。 要访问对象的成员,你可以使用'。'( 点) 。

当你通过指针传递对象时,只复制几个字节( 4在 32位 系统上,8在 64位 上),即- 这个对象的地址。 为了给每个人展示这个,你在访问成员时使用这个奇特的'->'操作符。 或者你可以使用'*'和'。'的组合。

当你使用引用时,你得到的指针是一个值。 这是一个指针,但是你通过'。'访问成员。

然后,再一次震撼你的头脑: 当声明几个用逗号分隔的变量时,( 注意手部):

  • 类型给每个人
  • Value/pointer/reference 修饰符是单独

例如:


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 上面 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

在 C++ 中,在堆栈( 在块中使用 Object object; 语句) 上分配的对象只能在它们声明的作用域内。 当代码块完成执行时,声明的对象将被销毁。 如果在堆上分配内存,则使用 Object* obj = new Object() 在你调用 delete obj 之前,它们一直处于堆中。

当我想在声明/分配它的代码块中使用对象时,我将在堆上创建一个对象。

从技术上说,这是内存分配问题,但是这里还有两个更实用的方面。 它有两个方面: 1 ) 作用域,当你定义一个不带指针的对象时,你将不能再访问它,而在定义一个带有"新建"指针的指针之前,你可以从任何地方访问它,直到你在同一个指针上调用"删除"。 2 ) 如果你想传递参数给你想要传递指针或者引用的函数以便更高效。 当你传递对象时,对象被复制,如果这是一个使用大量内存的对象,这可能是CPU消耗( 例如。 复制一个充满数据的向量。 当你传递一个指针时,你传递的是一个 int ( 取决于实现,但大多数是一个 int ) 。

除这里之外,你需要理解"新建"在堆上分配了需要在某些时候释放的内存。 当你不需要使用"新建"时,建议使用常规对象定义"在堆栈上"。

让我们说你有 class A 包含 class B 当你想要调用的函数 class Bclass A 外你会获得一个指向这个类,你可以做任何你想做的,也会改变你 class Aclass B的上下文

但是要小心动态对象

...