Refptr and PassRefPtr basic

本文介绍Webkit中管理引用计数对象的智能指针RefPtr和PassRefPtr,详细解释了这两种指针如何工作,以及如何正确使用它们来避免内存泄漏和性能问题。

http://www.webkit.org/coding/RefPtr.html

Refptr and PassRefPtr basic

Webkit中的很多对象都是引用计数的,所使用的方法是在类中加入ref和deref成员函数用于增加和减小引用计数。调用ref的次数一定要和调用deref的次数相同,当引用计数deref到1的时候,该对象会被释放。Webkit中的很多类通过继承RefCounted类模板来实现这种方法。

2005年,我们发现有很多的内存泄露,特别是在HTML editing code中误用了ref和deref调用的时候。

我们想用智能指针来解决这个问题。但是,一些验证性试验表明智能指针会使用额外的引用计数操作,会导致性能降低。例如,如果一个函数使用了智能指针作为参数,并且返回同样的智能指针作为返回值,单就是作为参数进行传递和返回该值,由于对象在智能指针之间传递,就会导致增加或者减少引用计数两到四次。所以我们在寻找一种理想的方式,能让我们使用智能指针,并且避免大量使用引用计数。

一个解决方法的灵感来自于C++标准类模板auto_ptr。使用auto_ptr的对象之间在赋值的时候,它的所有者也发生了改变,赋值者的引用数会变为0,对象归被赋值者所有。

Maciej Stachowiak设计了一对类模板,RefPtr和PassRefPtr,实现了这种方案,来解决WebCore的引用计数问题。

原始指针(Rawpointer)

当讨论类似RefPtr类模板这样的智能指针的时候,我们使用了原始指针这个术语来表示C++语言的内建指针类型。这里给出了使用原始指针实现的规范的setter函数:

// example, not preferred style

 

class Document {

   ...

   Title* m_title;

}

 

Document::Document()

    :m_title(0)

{

}

 

Document::~Document()

{

   if (m_title)

       m_title->deref();

}

 

void Document::setTitle(Title* title)

{

   if (title)

       title->ref();

   if (m_title)

       m_title->deref();

   m_title = title;

}

RefPtr

RefPtr是一个简单的智能指针类,它调用ref来增加引用,调用deref来减小引用。RefPtr在任何有ref和deref成员函数的对象上都会起作用,下面是用RefPtr写的setter例子

// example, not preferred style

 

class Document {

   ...

   RefPtr<Title> m_title;

}

 

void Document::setTitle(Title* title)

{

   m_title = title;

}

如果单独使用RefPtr也会导致引用计数的泛滥

RefPtr<Node> createSpecialNode()

{

   RefPtr<Node> a = new Node;

   a->setSpecial(true);

   return a;

}

 

RefPtr<Node> b = createSpecialNode();

我们假设node对象的引用计数从0开始,当它赋值给a的时候,引用计数增加了1(引用operator=被RefPtr重载了),从函数返回的时候,因为a实际上只是一个临时变量,编译器会把它复制给一个临时变量作为返回值,此时发生了赋值操作,导致引用计数变为2,同时由于a是临时变量,所以在a被销毁的时候,析构函数被调用,导致引用计数变为1。作为返回值的临时变量把值赋值给变量b,此时引用计数又会增加为2,在临时变量销毁的时候,引用计数又变为1。

(如果编译器进行了返回值优化的话,会减少一次引用计数的增加减少过程)

如果函数的参数和返回值都涉及了智能指针的使用,那么引用计数导致的overhead会更多。

解决方案就是PassRefPtr

PassRefPtr

PassRefPtr与RefPtr类似,但是有一点不同,当你复制一个PassRefPtr或者把一个PassRefPtr赋值给一个RefPtr或者另外一个PassRefPtr的时候,PassRefPtr中的原始指针会被设置为0;该操作不会导致引用计数有任何的变化。下面是用PassRefPtr的例子

PassRefPtr<Node> createSpecialNode()

{

   PassRefPtr<Node> a = new Node;

   a->setSpecial(true);

   return a;

}

 

RefPtr<Node> b = createSpecialNode();

假设Node对象起始引用计数为0,当它赋值给a的时候,引用计数增加为1。在函数返回的时候,变量a赋值给返回结果临时变量的时候,a中的引用被设置为0,在返回结果临时变量赋值给变量b的时候,b中的引用被设置为0。

注:PassRefPtr通过在赋值的时候,传递指针而不是增加减少引用计数的方式,降低了引用计数泛滥导致的性能降低问题。

但是,Safari team在实践中发现,PassRefPtr这种方式很容易出现问题。

static RefPtr<Ring>g_oneRingToRuleThemAll;

 

void finish(PassRefPtr<Ring> ring)

{

   g_oneRingToRuleThemAll = ring;

   ...

   ring->wear();

}

在ring->wear()的时候,ring中包含的指针已经被设置为0.为了避免发生这种情况,我们建议只在函数参数的返回值的时候,使用PassRefPtr,如果要使用其中的变量,要把它赋值给一个RefPtr本地变量

static RefPtr<Ring>g_oneRingToRuleThemAll;

 

void finish(PassRefPtr<Ring> prpRing)

{

   RefPtr<Ring> ring = prpRing;

   g_oneRingToRuleThemAll = ring;

    ...

   ring->wear();

}

混合使用RefPtr和PassRefPtr

我们建议,在作为参数进行传递和函数返回值的情况下使用PassRefPtr,其他情况下使用RefPtr,但是也有很多时候,你会希望像PassRefPtr那样,转移RefPtr的所有权。RefPtr有一个名为release的成员函数实现着这功能。它设置RefPtr的值为0,并且构建新的PassRefPtr而不改变引用计数。

PassRefPtr<Node> createSpecialNode()

{

   RefPtr<Node> a = new Node;

   a->setCreated(true);

   return a.release();//这里返回PassRefPtr,并且把a中的引用设置为0

}

 

RefPtr<Node> b = createSpecialNode();

这既保证了PassRefPtr的效率,又降低了技巧性的语义导致问题的机会。

 

与原始指针一起使用

如果一个函数的参数需要使用原始指针,那么可以使用RefPtr的get函数

printNode(stderr, a.get());

但是很多操作可以直接使用RefPtr或者PassRefPtr,而不用显式的调用get函数

RefPtr<Node> a = createSpecialNode();

Node* b = getOrdinaryNode();

 

// the * operator

*a = value;

 

// the -> operator

a->clear();

 

// null check in an if statement

if (a)

   log("not empty");

 

// the ! operator

if (!a)

   log("empty");

 

// the == and != operators, mixing with rawpointers

if (a == b)

   log("equal");

if (a != b)

   log("not equal");

 

// some type casts

RefPtr<DerivedNode> d =static_pointer_cast<DerivedNode>(a);

注:RefPtr对多种操作符都进行了重载

一般来说,RefPtr和PassRefPtr践行了一个简单的规则:他们总是平衡ref和deref调用,来保证程序员不会丢失对deref的调用。但是对于我们已经有了一个原始指针,并且有了引用计数,希望能够转移它的所有权的情况,应该使用adoptRef函数。

RefPtr<Node> node =adoptRef(rawNodePointer);

反过来,如果想把RefPtr传递给一个原始指针,而不改变引用计数,那么就要使用PassRefPtr的leakRef函数

RefPtr和new objects

在这里的例子中,我们谈论的对象都是引用计数为0,但是为了效率和简化,RefCounted类根本不使用引用计数0。对象在创建的时候就使用引用计数1.最好的编程实践是在new以后,马上把它们放进RefPtr,这样可以防止程序员在用完这些对象的时候忘记调用deref,让RefPtr帮助我们管理这些对象。也就是说,任何人调用new创建一个对象后,要马上调用adoptRef(该函数会返回一个PassRefPtr)。在WebCore中我们使用命名为create的static函数,而不是直接使用new

PassRefPtr<Node> Node::create()

{

   return adoptRef(new Node);

}

 

RefPtr<Node> e = Node::create();

从adoptRef和PassRefPtr的实现,我们可以看到它们是很高效的。对象的起始引用计数是1,而且在创建过程中没有任何对引用计数的操作

// preferred style

PassRefPtr<Node> createSpecialNode()

{

   RefPtr<Node> a = Node::create();

   a->setCreated(true);

   return a.release();

}

 

RefPtr<Node> b = createSpecialNode();

Node对象在Node::create中,被adoptRef放进PassRefPtr中,然后通过RefPtr的重载过的=操作符赋值给RefPtr对象a,最后在函数返回的时候,RefPtr的release函数返回PassRefPtr对象,然后又通过赋值操作赋值给RefPtr对象b,这期间没有对引用计数发生过任何操作。

RefCounted类实现了一个运行期的检查,如果我们再创建一个对象之后,没有调用adoptRef,而调用ref或者deref将会发生assertion。

使用指南

在WebKit代码中使用RefPtr和PassRefPtr应遵守如下的使用指南:

本地变量

如果所有权和生命期可以保证是在局部的,那么本地变量可以使用原始指针

如果代码需要保持住所有权或者保证生命期,那么应该使用RefPtr本地变量

永远不要使用PassRefPtr作为本地变量

成员变量

如果能够保障成员变量的所有权和生命期,成员变量可以使用原始指针

如果类需要获得所有权或者保证生命期,那么成员变量应该使用RefPtr

成员变量永远不要使用PassRefPtr

函数参数

如果函数不用于获取对象的所有权,那么参数可以使用原始指针

如果函数需要获取对象的所有权,那么参数应该是PassRefPtr。大部分的setter函数都应该如此设计。除非参数非常简单,否则的话,参数应该在函数一开始就传递给RefPtr对象,通常函数参数名称可以以”prp”作为前缀

函数结果

如果函数结果是一个对象,但是该对象的所有权还是属于原来对象,那么这个返回对象应该是一个原始指针,大部分的getter函数都是如此定义的(注:即结果对象是包含在原始对象中的,通过getter只是返回其引用或者指针,但是该返回的对象仍然从属于原始对象)

如果函数结果是一个新生成的对象或者它的所有权因为某种原因需要转移,那么这个结果应该使用PassRefPtr(该对象可以转移所有权)。因为本地变量一般使用RefPtr,所以在函数返回的时候,应该调用RefPtr的release函数返回一个PassRefPtr对象

新对象

新创建的对象在创建后需要马上放入到RefPtr中,这样只能指针就是自动控制引用计数操作

对于RefCounted对象,应该使用adoptRef函数把它放入到PassRefPtr中。

注:WebCore中的例子

static PassRefPtr<DocumentLoader> DocumentLoader::create(constResourceRequest& request, const SubstituteData& data)

{

returnadoptRef(new DocumentLoader(request, data));

}

DocumentLoader继承自RefCounted<DocumentLoader>

 

最好的创建方式是,使用私有的构造函数,和共有的创建函数,该创建函数返回一个PassRefPtr,(注:采用私有的构造函数,就防止外界使用new来创建新的对象,必须使用类提供的静态创建函数,该函数保证了对象在被创建后马上就被放入到RefPtr中)

 

这篇文章是关于Webkit中一系列非常基础的类的说明,看明白这篇文档之前还需要了解如下信息:

RefCounted是所有智能指针的基类,它提供了ref和deref操作,用于增减引用计数,RefCounted还有它自己的基类,RefCountedBase。

为了解决引用计数在作为参数和返回值的时候,频繁增减的问题,引入RefPtr和PassRefPtr对象,和一系列使用这些对象的规则

首先需要说明的是RefPtr和PassRefPtr是模板,使用这些模板定义的对象同样也一个对象而不是指针,在这些对象中封装了对引用计数的ref和deref操作。

其次来看一些重要的方法实现,对于我们理解上述文档很有好处,

RefPtr::release(),该函数返回一个PassRefPtr对象,用于传递原始指针的所有权

PassRefPtr<T> RefPtr::release()

{

 PassRefPtr<T> tmp = adoptRef(m_ptr);m_ptr = 0; return tmp;

}

PassRefPtr<T>::leakRef(),该函数返回原始指针

template<typename T> inline T*PassRefPtr<T>::leakRef() const

    {

       T* ptr = m_ptr;

       m_ptr = 0;

       return ptr;

}

adoptRef这个函数非常重要,它返回一个PassRefPtr对象,经常在创建对象的函数中使用。

template<typename T> inlinePassRefPtr<T> adoptRef(T* p)

    {

       adopted(p);

       return PassRefPtr<T>(p, true);

}

同时值得说明的是,在RefPtr和PassRefPtr对象中对引用计数的操作,统一调用了

void refIfNotNull(T*ptr)和void derefIfNotNull(T* ptr),在PassRefPtr中对这两个函数的实现如下:

template<typename T> REF_DEREF_INLINEvoid refIfNotNull(T* ptr)

    {

       if (LIKELY(ptr != 0))

           ptr->ref();

    }

 

   template<typename T> REF_DEREF_INLINE void derefIfNotNull(T* ptr)

    {

       if (LIKELY(ptr != 0))

           ptr->deref();

}

在RefCountedBase中实现了ref和deref方法,从而实现了智能指针对其保存的引用计数的操作。

而RefPtr和PassRefPtr智能指针并不是只能用于RefCountedBase类对象的,只要重现实现void refIfNotNull(T* ptr)和void derefIfNotNull(T* ptr)函数,就能将智能指针用于其他有引用计数的对象或者结构,在WebCore中,RefPtrCairo.h中就定义了一系列这样的函数,这样在包含该头文件以后,我们就可以使用RefPtr和PassRefPtr管理Cairo的对象了

 

智能指针:

http://baike.baidu.com/view/1391603.htm

http://zh.wikipedia.org/wiki/%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88
<think> 我们正在解决的是C++编译链接错误,特别是关于标准库函数未定义的引用(如`std::cout`, `std::endl`等)以及链接器返回的错误`ld returned 1 exit status`。这些错误通常发生在链接阶段,表明编译器找到了函数声明,但链接器找不到这些函数的实现(定义)。 根据引用[1]和引用[2]中的错误信息,我们可以推断问题可能涉及以下几个方面: 1. **未正确链接C++标准库**:这是最常见的原因。C++程序需要链接标准库(如`libstdc++`或`libc++`)。在使用g++(GCC的C++编译器)时,通常会默认链接标准库。但如果使用gcc(C编译器)来编译C++代码,则不会自动链接C++标准库,从而出现这类错误。 2. **编译器与标准库不匹配**:例如,使用不同版本或不同厂商的编译器和标准库可能导致符号不匹配。 3. **代码中未包含必要的头文件**:虽然这通常会导致编译错误(而不是链接错误),但有时如果声明不正确也可能导致链接问题。 4. **多个库或目标文件链接顺序问题**:链接器在解析符号时,会按照从左到右的顺序搜索库。如果某个库在依赖它的目标文件之前出现,则可能无法解析符号。 针对引用[4]中提到的现象(在ubuntu下使用code::blocks时,虽然工程配置为gcc但实际上需要g++),我们也可以推断用户可能在使用gcc而不是g++来链接C++程序。 ### 解决方案步骤: #### 步骤1:确认使用正确的编译器 在VS Code中,确保你使用g++(而不是gcc)来编译链接C++程序。检查你的`tasks.json`配置文件(位于项目目录的`.vscode`子目录下),确认编译器路径和参数设置正确。 例如,一个典型的编译C++程序的`tasks.json`配置如下: ```json { "version": "2.0.0", "tasks": [ { "label": "build", "type": "shell", "command": "g++", "args": [ "-g", "-o", "${workspaceFolder}/output.exe", "${workspaceFolder}/*.cpp" ], "group": { "kind": "build", "isDefault": true } } ] } ``` 注意: - `"command": "g++"` 这里明确使用g++。 - 如果使用gcc,则需要手动添加链接C++标准库的参数(不推荐,不如直接用g++)。 #### 步骤2:检查是否包含必要的头文件 确保代码中包含了所有必要的标准库头文件。例如,使用`std::cout`和`std::endl`需要包含`<iostream>`。 示例代码: ```cpp #include <iostream> int main() { std::cout << "Hello, World!" << std::endl; return 0; } ``` #### 步骤3:确保正确的链接顺序 如果你的项目包含多个源文件或库,确保在链接命令中,库文件放在依赖于它们的源文件之后。 然而,对于标准库,g++会自动将其放在正确的位置。所以如果你使用g++,通常不需要手动指定。 #### 步骤4:检查编译器安装和环境变量 确保你的编译器(如MinGW-w64或Cygwin)已正确安装,并且其`bin`目录已添加到系统的PATH环境变量中。可以通过在终端中运行以下命令来验证: ```bash g++ --version ``` 如果没有输出正确的版本信息,则说明安装或PATH设置有误。 #### 步骤5:重新安装编译器 如果以上步骤都不能解决问题,考虑重新安装编译器。有时编译器安装不完整会导致标准库缺失。 对于Windows用户,推荐使用MinGW-w64。可以从 [MinGW-w64官网](https://www.mingw-w64.org/) 下载安装器,或通过MSYS2安装。 ### 示例解决方案(针对引用[1]中的错误) 假设用户自定义了头文件,但在编译时出现`undefined reference`错误,这可能是由于头文件中声明的函数没有在源文件中定义,或者定义了但没有被编译(比如忘记将实现文件加入编译命令)。 例如,用户可能有如下文件结构: ``` project/ |-- library/ |-- myheader.h |-- test.cpp ``` 在`myheader.h`中声明了一个函数: ```cpp #ifndef MYHEADER_H #define MYHEADER_H #include <string> void sssp(std::string s, char c); #endif ``` 在`test.cpp`中使用了这个函数: ```cpp #include "library/myheader.h" int main() { sssp("hello", 'e'); return 0; } ``` 但如果在编译命令中,只编译了`test.cpp`,而没有编译包含`sssp`函数实现的源文件(比如`myheader.cpp`),就会出现链接错误。 因此,在`tasks.json`中,需要将实现文件也加入编译: ```json "args": [ "-g", "-o", "${workspaceFolder}/output.exe", "${workspaceFolder}/test.cpp", "${workspaceFolder}/library/myheader.cpp" // 添加实现文件 ] ``` ### 总结错误原因: 1. 使用gcc而不是g++链接C++程序。 2. 缺少必要的源文件(特别是包含函数定义的源文件)在链接命令中。 3. 编译器安装问题导致标准库缺失。 ### 附加建议: 在VS Code中,可以通过安装扩展来简化C++开发环境配置: - 安装官方C++扩展:`ms-vscode.cpptools` - 使用CMake Tools扩展(如果你的项目使用CMake) 按照上述步骤,应该能解决链接错误。如果问题仍然存在,请提供更多关于项目结构和编译命令的信息。 §§相关问题§§ 1. 如何在VS Code中配置CMake项目? 2. 如何解决C++中“multiple definition”链接错误? 3. 如何在Windows上安装MinGW-w64并配置VS Code? </think> ### 解决VS Code编译C++时的`undefined reference`和`collect2.exe`错误 以下是针对链接器错误的完整解决方案,参考了您提供的错误信息[^1][^2][^4]: #### 1. **确认编译器配置** ```json // .vscode/tasks.json { "version": "2.0.0", "tasks": [{ "label": "build", "type": "shell", "command": "g++", "args": [ "-g", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}.exe", "-lstdc++" // 关键:明确链接C++标准库 ], "group": { "kind": "build", "isDefault": true } }] } ``` > 添加`-lstdc++`参数强制链接C++标准库[^1],这是解决`undefined reference to std::cout`的核心步骤 #### 2. **检查头文件包含** ```cpp // 确保包含必要的标准库头文件 #include <iostream> // std::cout, std::endl #include <string> // std::string using namespace std; // 或明确使用std:: ``` #### 3. **验证编译器选择** ```bash # 在VS Code终端执行 g++ --version # 应显示类似输出: # g++ (MinGW.org GCC-8.2.0-5) 8.2.0 ``` > 如果显示为`gcc`而非`g++`,需修改tasks.json中的`"command": "g++"`[^4] #### 4. **解决字符串转换问题** ```cpp // 错误示例(引用[3]) std::ofstream file(std::string("data.txt")); // 正确修改 std::ofstream file("data.txt"); // 直接使用const char* // 或显式转换 std::string filename = "data.txt"; std::ofstream file(filename.c_str()); ``` #### 5. **完整调试配置** ```json // .vscode/launch.json { "version": "0.2.0", "configurations": [{ "name": "Debug C++", "type": "cppdbg", "request": "launch", "program": "${fileDirname}/${fileBasenameNoExtension}.exe", "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": true, "MIMode": "gdb", "miDebuggerPath": "gdb.exe" }] } ``` ### 常见原因总结 | 错误类型 | 原因 | 解决方案 | |----------|------|----------| | `undefined reference to std::cout` | 未链接C++标准库 | 添加`-lstdc++`编译参数 | | `collect2: error: ld returned 1` | 链接器找不到实现 | 检查头文件包含和命名空间 | | 字符串转换错误 | C++与C字符串混用 | 使用`.c_str()`显式转换 | > **重要提示**:MinGW安装时需勾选`mingw32-libstdc++`组件,缺失会导致标准库链接失败[^1][^4]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值