题目如下: 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。 分析:
二叉树的前序遍历顺序是:先访问根节点,然后前序遍历左子树,再前序遍历右子树。
中序遍历顺序是:中序遍历根节点的左子树,然后是访问根节点,最后中序遍历右子树。
1、二叉树的前序遍历序列一定是该树的根节点
2、中序遍历序列中根节点前面一定是该树的左子树,后面是该树的右子树
从上面可知,题目中前序遍历的第一个节点{1}一定是这棵二叉树的根节点,根据中序遍历序列,可以发现中序遍历序列中节点{1}之前的{4,7,2}是这棵二叉树的左子树,{5,3,8,6}是这棵二叉树的右子树。然后,对于左子树,递归地把前序子序列{2,4,7}和中序子序列{4,7,2}看成新的前序遍历和中序遍历序列。此时,对于这两个序列,该子树的根节点是{2},该子树的左子树为{4,7}、右子树为空,如此递归下去(即把当前子树当做树,又根据上述步骤分析)。{5,3,8,6}这棵右子树的分析也是这样。
代码如下:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public class TestRecoverBinaryTree {
public TreeNode reConstructBinaryTree(int [] preOrder,int [] inOrder)
{
int pLen = preOrder.length;
int iLen = inOrder.length;
if(pLen==0 && iLen==0)
{
return null;
}
return btConstruct( preOrder, inOrder, 0, pLen-1,0, iLen-1);
}
//构建方法,pStart和pEnd分别是前序遍历序列数组的第一个元素和最后一个元素;
//iStart和iEnd分别是中序遍历序列数组的第一个元素和最后一个元素。
public TreeNode btConstruct(int[] preOrder, int[] inOrder, int pStart, int pEnd,int iStart,int iEnd)
{
//建立根节点
TreeNode tree = new TreeNode(preOrder[pStart]);
tree.left = null;
tree.right = null;
if(pStart == pEnd && iStart == iEnd)
{
return tree;
}
int root = 0;
//找中序遍历中的根节点
for(root=iStart; root<iEnd; root++)
{
if(preOrder[pStart] == inOrder[root])
{
break;
}
}
//划分左右子树
int leftLength = root - iStart;//左子树
int rightLength = iEnd - root;//右子树
//遍历左子树
if(leftLength>0)
{
tree.left = btConstruct(preOrder, inOrder, pStart+1, pStart+leftLength, iStart, root-1);
}
//遍历右子树
if(rightLength>0)
{
tree.right = btConstruct(preOrder, inOrder, pStart+leftLength+1, pEnd, root+1, iEnd);
}
return tree;
}
}
注意: 已知前序和中序遍历,可以确定一棵二叉树。已知中序和后序遍历,可以确定一棵二叉树。但是,已知前序和后序遍历,不能确定一棵二叉树。
C++的标准库关联容器map是不允许有key相同的键值对存在的。那么当key已经存在的情况下,我们再次插入相同的key,那么key的value会被覆盖吗?
测试代码:
#include <map>
#include <string>
#include <iostream>
using std::map;
using std::string;
using std::cout;
using std::endl;
using std::make_pair;
/**
* 测试map的插入覆盖特性
* 注意众多的using声明
*/
int main()
{
map<string, string> testMap;
testMap.insert(make_pair("bkey","bval"));
cout << "before convert: " << testMap["bkey"] << endl;
//insert方式,重复的key会直接被放弃,而不是进行覆盖(这一点与Java不同)
testMap.insert(make_pair("bkey","cval"));
cout <<"insert convert test: " << testMap["bkey"] << endl;
//[]方式是可以覆盖的
testMap["bkey"] = "dval";
cout <<"[] convert test:" << testMap["bkey"] << endl;
return 0;
}
测试结果:
/* 测试结果:
before convert: bval
insert convert test: bval
[] convert test:dval
*/
从测试结果我们可以得出结论
从测试结果我们可以看出,使用insert()插入元素的方式并不能覆盖掉相同key的值;而使用[]方式则可以覆盖掉之前的值。为什么会出现这样的结果呢?
我们可以通过源码来找原因,在map的源码中,insert方法是这样定义的:
pair<iterator,bool> insert(const value_type& __x)
{ return _M_t.insert_unique(__x); }
他调用_M_t.insert_unique(_x)方法,该方法会首先遍历整个集合,判断是否存在相同的key,如果存在则直接返回,放弃插入操作。如果不存在才进行插入。 而[]方式是通过重载[]操作符来实现的,它直接进行插入或覆盖。
sample1.cc
// Copyright 2005, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// A sample program demonstrating using Google C++ testing framework.
#include "sample1.h"
// Returns n! (the factorial of n). For negative n, n! is defined to be 1.
int Factorial(int n) {
int result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
}
// Returns true iff n is a prime number.
bool IsPrime(int n) {
// Trivial case 1: small numbers
if (n <= 1) return false;
// Trivial case 2: even numbers
if (n % 2 == 0) return n == 2;
// Now, we have that n is odd and n >= 3.
// Try to divide n by every odd number i, starting from 3
for (int i = 3; ; i += 2) {
// We only have to try i up to the square root of n
if (i > n/i) break;
// Now, we have i <= n/i < n.
// If n is divisible by i, n is not prime.
if (n % i == 0) return false;
}
// n has no integer factor in the range (1, n), and thus is prime.
return true;
}
sample1_unittest.cc
// Copyright 2005, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// A sample program demonstrating using Google C++ testing framework.
// This sample shows how to write a simple unit test for a function,
// using Google C++ testing framework.
//
// Writing a unit test using Google C++ testing framework is easy as 1-2-3:
// Step 1. Include necessary header files such that the stuff your
// test logic needs is declared.
//
// Don't forget gtest.h, which declares the testing framework.
#include <limits.h>
#include "sample1.h"
#include "gtest/gtest.h"
namespace {
// Step 2. Use the TEST macro to define your tests.
//
// TEST has two parameters: the test case name and the test name.
// After using the macro, you should define your test logic between a
// pair of braces. You can use a bunch of macros to indicate the
// success or failure of a test. EXPECT_TRUE and EXPECT_EQ are
// examples of such macros. For a complete list, see gtest.h.
//
// <TechnicalDetails>
//
// In Google Test, tests are grouped into test cases. This is how we
// keep test code organized. You should put logically related tests
// into the same test case.
//
// The test case name and the test name should both be valid C++
// identifiers. And you should not use underscore (_) in the names.
//
// Google Test guarantees that each test you define is run exactly
// once, but it makes no guarantee on the order the tests are
// executed. Therefore, you should write your tests in such a way
// that their results don't depend on their order.
//
// </TechnicalDetails>
// Tests Factorial().
// Tests factorial of negative numbers.
TEST(FactorialTest, Negative) {
// This test is named "Negative", and belongs to the "FactorialTest"
// test case.
EXPECT_EQ(1, Factorial(-5));
EXPECT_EQ(1, Factorial(-1));
EXPECT_GT(Factorial(-10), 0);
// <TechnicalDetails>
//
// EXPECT_EQ(expected, actual) is the same as
//
// EXPECT_TRUE((expected) == (actual))
//
// except that it will print both the expected value and the actual
// value when the assertion fails. This is very helpful for
// debugging. Therefore in this case EXPECT_EQ is preferred.
//
// On the other hand, EXPECT_TRUE accepts any Boolean expression,
// and is thus more general.
//
// </TechnicalDetails>
}
// Tests factorial of 0.
TEST(FactorialTest, Zero) {
EXPECT_EQ(1, Factorial(0));
}
// Tests factorial of positive numbers.
TEST(FactorialTest, Positive) {
EXPECT_EQ(1, Factorial(1));
EXPECT_EQ(2, Factorial(2));
EXPECT_EQ(6, Factorial(3));
EXPECT_EQ(40320, Factorial(8));
}
// Tests IsPrime()
// Tests negative input.
TEST(IsPrimeTest, Negative) {
// This test belongs to the IsPrimeTest test case.
EXPECT_FALSE(IsPrime(-1));
EXPECT_FALSE(IsPrime(-2));
EXPECT_FALSE(IsPrime(INT_MIN));
}
// Tests some trivial cases.
TEST(IsPrimeTest, Trivial) {
EXPECT_FALSE(IsPrime(0));
EXPECT_FALSE(IsPrime(1));
EXPECT_TRUE(IsPrime(2));
EXPECT_TRUE(IsPrime(3));
}
// Tests positive input.
TEST(IsPrimeTest, Positive) {
EXPECT_FALSE(IsPrime(4));
EXPECT_TRUE(IsPrime(5));
EXPECT_FALSE(IsPrime(6));
EXPECT_TRUE(IsPrime(23));
}
} // namespace
// Step 3. Call RUN_ALL_TESTS() in main().
//
// We do this by linking in src/gtest_main.cc file, which consists of
// a main() function which calls RUN_ALL_TESTS() for us.
//
// This runs all the tests you've defined, prints the result, and
// returns 0 if successful, or 1 otherwise.
//
// Did you notice that we didn't register the tests? The
// RUN_ALL_TESTS() macro magically knows about all the tests we
// defined. Isn't this convenient?
sample1_unittest.cc
测试sample1.cc
中的一下函数:
#ifndef GTEST_SAMPLES_SAMPLE1_H_
#define GTEST_SAMPLES_SAMPLE1_H_
// Returns n! (the factorial of n). For negative n, n! is defined to be 1.
int Factorial(int n);
// Returns true iff n is a prime number.
bool IsPrime(int n);
#endif // GTEST_SAMPLES_SAMPLE1_H_
思考:
平时我们写测试用例都是把一个函数的功能测试写到一个测试case里面的,那样往往会造成测试case很长、很复杂,
其它人理解起来很费力,像gtest中的sample,把一个函数的功能拆分开,把一类拆分到一个测试case中,还有就是
需要加上注释,虽然说是测试代码,但是也要像业务代码那样严格要求。
智能指针是C++
标准库中提供的一系列用于内存自动化管理的工具。智能指针就是利用RAII
(Resource Acquisition Is Initialization)来进行
内存管理的一个典型例子。
从形式上来看,智能指针是用对象封装了指向某个特定类型的对象指针,其本质上是一个对象,但是通过一些操作符重载使得其使用行为又类似一个原生的指针。 智能指针对象在生命周期结束时(离开作用域),自动的删除对象。智能指针的作用很明显,起到自动化的内存管理,防止开发者忘记了内存释放。 智能指针还是“异常安全的”,也就是说虽然你没忘记写delete对象的操作,但是也可能由于中间过程中抛出的异常而使程序在未执行到delete对象 之前就跳转到异常处理中了,从而导致内存泄露的发生。作为一个栈上的对象,即便是在有异常抛出的场景下,编译器也能保证销毁该对象,进而释放相应的内存。
int test_func() {
try {
...
Object* obj = new Object();
...
delete obj;
return 0;
} catch(...) {
...
return 1;
}
}
从语言的设计上来看,C++中的对象是具有“值语义”(Value Semantic)的,而不是Reference Semantic,这个是和Java有重大区别的。比如:Java中的一段代码:
Object obj1 = new Object();
Object obj2 = obj1;
这里仅仅只有一个对象创建出来,而obj1和obj2仅仅是“对象引用”而已。也就是说obj1和obj2实际关联到的是同一个对象,同一块内存区域。 但是,在C++中的对象却是“值语义”的,如下面这段代码:
Object obj1;
Object obj2 = obj1;
这里obj1和obj2是两个不同的对象,各自有自己的内存区域。C++对象的“值语义”在实际的使用中也带来的一些问题:
1)一些类型的对象的拷贝是毫无意义的,比如:socket对象、thread对象、抽象的TCP连接对象等,如果使用值传递, 那必然有一个对象的复制动作。而这类对象通常都是禁止复制的(类定义时通常将copy-constructor和operator =声明为private, 或者继承boost::noncopyable),因此没法进行值传递;
2)有些对象“很大”,其构造过程以及对象的复制过程很复杂,而“值语义”带来的对象复制和创建过程带来的开销是非常可观的, 这个会带来效率上的下降。(现在的编译器基本上都有返回值优化RVO/NVRO,可以一定程度上的缓解这类问题; 另外,C++11中引入的右值引用也是为了解决这个效率问题)
作为C开发人员,肯定第一时间就能想到用指针来解决这个问题。但是,指针是内存泄漏的万恶源头,而且使用指针来传递对象信息就更难管理了, 尤其是在大规模的软件开发中,什么时候释放指针?有哪些地方用到了这个指针,释放后别的地方要再使用了怎么办?在多线程环境下就更麻烦了, 没法掌控。智能指针是了解决这些问题而产生的,但是不同类型的智能指针的实现以及使用上也存在细节上的差异。
C++标准库中提供的智能指针有下面几种:
1)std::auto_ptr
: 从C++98开始出现,auto_ptr由于存在所有权转移以及无法和STL容器配合等原因很少被用到,在C++11中已经被废弃了,
这里也就不再介绍了,”More Effective C++”中有对auto_ptr的详细介绍;
2)std::unique_ptr
:C++11标准中引入,从名字可以看出,unique_ptr是不支持共享的,也就是不允许多个unique_ptr对象关联到同一个具体的对象指针;
3)std::shared_ptr
:C++11标准中引入,伴随shared_ptr的还有std::weak_ptr和std::enable_shared_from_this等一些辅助类;
shared_ptr通过“引用计数”的方法实现了多个shared_ptr共享一个对象指针的处理。shared_ptr的引用计数的处理是线程安全的,
但是并不是说shared_ptr就是线程安全的,在讲到shared_ptr的时候再展开吧。
除了C++标准库中的这几种外,还有准标准库boost中的boost::scoped_ptr。boost中也有boost::shared_ptr, C++标准库中的std::shared_ptr就是从boost::shared_ptr发展来的。本文先介绍一下std::unique_ptr,下一次再介绍复杂一点的std::shared_ptr。
std::unique_ptr
有两种形式,分别对应到指针和数组类型的管理。后者对于数组的管理使用了模板特化语法(更准确的说是部分特化或者是偏特化),
这里只要先能看懂就行了。
template<
class T,
class Deleter = std::default_delete > class unique_ptr;
template <
class T,
class Deleter
> class unique_ptr;
std::unique_ptr
最简单的使用方式如下。在该例子中,unique_ptr
仅用到了自动内存管理的特性,避免在分支处理中漏写了delete obj
的操作。
int test_func1() {
std::unique_ptr<Object> obj(new Object());
....
/// 在离开函数作用域时, obj的析构函数会自动释放内存
return 0;
int test_func1() {
std::unique_ptr<Object> obj = new Object(); /// Compiler Error
return 0;
}
int test_func1() {
std::unique_ptr<Object> obj;
obj = new Object(); /// Compiler Error
return 0;
}
第一个错误用法是试图使用copy-initialization进行对象的创建。以前的文章中介绍过,因为new Object()创建的Object对象指针和unique_ptr
explicit unique_ptr(pointer __p) noexcept
: _M_t(__p, deleter_type()) {
static_assert(!is_pointer<deleter_type>::value,
"constructed with null function pointer deleter");
}
第二个错误用法是试图通过使用opeator = 来进行赋值。而事实上unique_ptr并未提供这种类型的赋值操作重载,unique_ptr只支持下面几种方式赋值操作。
unique_ptr& operator=(unique_ptr&& __u) noexcept; /// 第一种方式
template<typename _Up, typename _Ep>
typename enable_if<__safe_conversion<_Up, _Ep>::value, unique_ptr&>::type
operator=(unique_ptr<_Up, _Ep>&& __u) noexcept; /// 第一种方式的模板形式
unique_ptr& operator=(nullptr_t) noexcept; /// 第二种方式
/// unique_ptr禁止了左值的拷贝构造函数和赋值操作
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
unique_ptr禁止了左值的拷贝构造函数和赋值操作,而auto_ptr是允许的,这也是两者之间的本质区别。因此,你无法实施对象的拷贝和赋值操作, 这些操作都会导致编译失败。unique_ptr只支持“右值引用”的赋值操作,这个是C++11标准中提出的“移动语义”(Move Semantic), 必须通过移动语义来进行对象的所有权转移。unique_ptr的意图就是保持对对象指针的唯一引用,而“拷贝语义”破坏了这一点, 而“移动语义”则更适用于unique_ptr。unique_ptr还支持空指针的赋值操作, 通过空指针赋值来释放对象的所有权。
std::unique_ptr<Object> obj1(new Object());
std::unique_ptr<Object> obj2;
obj2 = std::move(obj1); /// 通过move的方式进行对象指针的所有权转移;所有权转移后,obj1不再持有对象的指针
obj2 = nullptr; /// 将空指针赋值给obj2,obj2释放对象所有权
注:nullptr是C++11新引入的关键字,用来表示空指针;std::nullptr_t用来表示空指针类型。用来解决以前使用NULL(0)来表示空指针在函数重载上引入的二义性。
unique_ptr除了可以管理指针,还可以管理数组,auto_ptr则不能数组的管理。
std::unique_ptr<Object[]> obj_array(new Object[10]);
for (unsigned int i = 0; i < 10; i++) {
Object obj = obj_array[i];
......
}
数组类型版本unique_ptr重载了[]操作符,使用起来就像普通的数组一样。在离开作用域后,obj_array会自动调用delete []来进行数组的删除。
前面的文章中介绍过C++的多种new的形式,如果unique_ptr管理的对象指针是通过placement new创建的或者其他的自定义new操作, 那么需要将对应形式的delete操作传递给unique_ptr,以便在消除对象的时候能够正确的删除。unique_ptr接收两个模板参数, 第一个是对象类型,第二个是进行对象删除操作的类型,也叫“函数对象”(Function Object或者Functor),该类需要重载“()”操作。 第二个模板参数是缺省值,如果用户不指定,就用默认的deleter类型,该类型的对象会使用全局的::operator delete操作来销毁对象。
下面这个例子示例了如何在unique_ptr中使用自定义的deleter。这个例子中,创建的对象是通过placement new操作申请的内存, 在释放时,使用传入的MyDeleter函数对象进行对象的删除。如果仍然使用默认的deleter进行对象删除则会触发程序崩溃。
#include <stdlib.h>
#include <iostream>
#include <memory>
class MyDeleter {
public:
template<class T>
void operator() (T* ptr) {
std::cout << "MyDeleter" << std::endl;
free(ptr);
}
};
class Object {};
int main() {
void* buf = malloc(sizeof(Object));
std::unique_ptr<Object, MyDeleter> obj(new (buf) Object());
return 0;
}
如果在容器中管理原生的对象指针,那么需要在容器销毁时把容器中保存的对象指针也释放,否则就容易发生内存泄漏。 如果在容器中保存智能指针对象不就可以解决这个问题了吗,容器销毁时会调用每个容器中对象的析构函数,进而删除对象指针。 悲剧的是,在C++98标准中,auto_ptr是无法在STL容器中使用的。C++98标准规定了容器中的对象必须满足”CopyConstructible”和”CopyAssignable”两个约束, 也就说STL容器假定了存储的对象是可拷贝和赋值的,并且经过拷贝和赋值后形成了两个独立的对象。但是auto_ptr的拷贝构造函数和赋值操作导致了指针的所有权交换, 经过拷贝和赋值操作后,容器内的auto_ptr把持的对象指针可能变成NULL了!
伴随着“右值引用”的提出,在C++11中,STL容器也都提供了容器对象的”MoveConstructible”和”MoveAssignable”支持,当然也保留了原来的“拷贝语义”。
#include <vector>
#include <iostream>
#include <memory>
class Object {
public:
Object(int i = 0) : i_(i) {}
void dump() { std::cout << "Dump Object " << i_ << std::endl; }
~Object() { std::cout << "Delete Object " << i_ << std::endl; }
private:
int i_;
};
int main() {
std::vector<std::unique_ptr<Object> > v;
for (unsigned int i = 0; i < 10; i++) {
v.push_back(std::unique_ptr<Object>(new Object(i))); ///(1)
}
std::unique_ptr<Object> obj(new Object(20));
v.push_back(std::move(obj)); ///(2)
std::unique_ptr<Object> obj2 = std::move(v[3]); ///(3)
obj2->dump();
return 0;
}
这段代码演示了在STL容器中使用unique_ptr来管理对象指针简单用法,这里面有三个地方需要注意:(1)这里调用的vector的push_back成员函数是下面的这个重载函数,参数是“右值引用”而不是以前的“左值应用”,使用“移动语义”避免了一次临时对象拷贝带来的开销。
#if __cplusplus >= 201103L
void push_back(value_type&& __x) {
emplace_back(std::move(__x));
}
#endif
(2)将一个unique_ptr对象放入容器中时,需要使用“移动语义”,而不能使用“拷贝语义”。如果写成下面的这样,也是会编译失败的。v.push_back(obj); 原因前面也提到:unique_ptr不支持拷贝构造和赋值,必须使用“移动语义”。(3)将容器中的对象转移到其他对象,此时, 容器中的对象已经不在拥有对象指针的控制权,控制权已经在obj2中了,在obj2的生命周期结束后会释放对象。
std::unique_ptr的最大特点是维持其在对象指针所有权的唯一性,即std::unique_ptr不允许通过copy-initialization或者operator =来进行对象指针 所有权的复制,只能通过右值引用或者std::move来转移所有权到另外一个std::unique_ptr对象,如下面的代码所示。用C++ Concept的术语来说就是std::unique_ptr满足Move-Constructible和Move-Assignable,但是不满足Copy-Constructible和Copy-Assignable。
std::unique_ptr<Object> obj1 = new Object();
std::unique_ptr<Object> obj2;
obj2 = std::move(obj1); /// 通过move的方式进行对象指针的所有权转移;所有权转移后,obj1不再持有对象的指针
obj2 = nullptr; /// 将空指针赋值给obj2,obj2释放对象所有权
但是有时候我们需要std::unique_ptr具有Copy-Constructible和Copy-Assignable的属性,例如:作为参数传递给其他的函数、在多个实体间共享对象指针等等, 看下面一个例子:
/// 注意:这里是传值,不是左值引用或者右值引用
unsigned int function(std::unique_ptr<Object> obj) {
......
}
int main() {
std::unique_obj<Object> obj(new Object());
......
unsigned int result = function(obj); /// Compiler Error!!
......
}
这里只能通过引用的方式来进行参数传递,可以是左值引用也可以是右值引用。下面的例子是一个使用右值引用传递参数的正确写法:
unsigned int function(std::unique_ptr<Object>&& obj) {
......
}
int main() {
std::unique_obj<Object> obj(new Object());
......
unsigned int result = function(std::move(obj)); /// OK
......
}
std::shared_ptr则是实现了所有权共享的智能指针对象,持有shared_ptr对象的代码片段都具有对其包装的对象指针的部分所有权,只有所有的持有者都释放了, 其包装的对象指针才可以释放。std::shared_ptr内部是通过“引用计数”的方式来实现对于所有权的管理。使用std::shared_ptr,则上面的例子就可以修改为下面:
unsigned int function(std::shared_ptr<Object> obj) {
......
}
int main() {
std::shared_ptr<Object> obj(new Object()); /// 创建一个shared_ptr对象,持有Object对象指针,引用计数为1
......
/// 由于参数是通过传值方式,这里会产生一个shared_ptr的临时对象拷贝,
/// 在进入到function函数内部,shared_ptr对象对于Object对象指针的引用计数变为2
unsigned int result = function(obj);
......
/// 离开function作用域后,obj的对象指针引用计数减为1
/// 离开main函数作用域后, obj的对象指针引用计数减为0,此时删除Object对象
return 0;
}
shared_ptr也有很多类型的构造函数形式,正确使用shared_ptr必须要了解这些构造函数的形式以及意图。shared_ptr的构造函数、拷贝构造函数以及operator = 的形式大约有20多种,这里只介绍几个常用的,剩余的大家可以自己看看。
(1) template
shared_ptr<T>::shared_ptr() noexcept
缺省的构造函数,如果创建shared_ptr对象时不指定参数,则创建一个指向空指针对象的shared_ptr对象。
例如:shared_ptr
因为shared_ptr重载了opeator bool,因此你可以使用下面的方式来判断一个shared_ptr是否是有效的:
if (obj) { /// 即 obj.operator bool();
……
}
(2) template
template<typename _Tp1>
explicit shared_ptr<T>::shared_ptr(_Tp1* __p)
接受一个对象指针来构造shared_ptr对象。注意,这里的构造函数是一个成员函数模板,表示构造函数参数的指针类型和内部持有的对象指针的类型可能是不同类型。这个使我们平时使用的最普遍的构造函数,有下面两种不同的用法:
例1:std::shared_ptr
使用创建出来的Object对象指针来构造shared_ptr对象,shared_ptr对象管理Object对象的生命周期。
例2:class Document : public Object {};
std::shared_ptr<Object> obj(new Document);
使用创建出来的Document对象指针来构造shared_ptr对象。因为Document派生自Object,即相当在shared_ptr内部持有的Object对象指针实际指向的是Document对象,也就是C++的多态的基本用法。
该成员模板的第二个类型_TP1需要满足下面的约束,true == std::is_convertible<_TP1, T>::value; 才能使得构造函数成功,否则会有一个编译告警。这种约束在C++新标准中称为:Concept,以后再介绍。
为了实现在编译期检查这类错误,实现上使用了一些模板的静态推导的技巧(std::enable_if),有兴趣的同学可以下面的一些参考文献。
另外,注意该构造函数是声明为explicit,即不允许进行隐式的copy-initialization,只能通过direct-initialization来进行构造。关于copy-initialization和direct-initialization的差别.
shared_ptr
(3) template
template<typename _Tp1, typename _Deleter>
shared_ptr<T>::shared_ptr(_Tp1* __p, _Deleter __d)
和上面的构造函数类似,只是运行用户指定自己的对象释放操作,这个在unique_ptr中也介绍过,如下面的例子中的示例代码:
class MyDeleter {
public:
template<class T>
void operator() (T* ptr) {
std::cout << "MyDeleter" << std::endl;
free(ptr);
}
};
/// 这里通过placement new创建一个对象, 对应的对象删除操作需要将该内存释放
void* buf = malloc(sizeof(Object));
MyDeleter dtor;
std::shared_ptr<Object> obj(new (buf) Object(), dtor);
(4) template
shared_ptr<T>::shared_ptr(const shared_ptr&) noexcept = default;
缺省的拷贝构造函数。default关键字是C++11中引入的,表示由编译器来为我们自动生成一个默认拷贝构造函数。用法如下面的例子:
std::shared_ptr<Object> obj(new Object);
std::shared_ptr<Object> obj2(obj);
assert(obj2.use_count() == 2);
(5) template
template<typename _Tp1>
shared_ptr<T>::shared_ptr(const shared_ptr<_Tp1>& __r) noexcept
持有不同类型对象指针的shared_ptr的拷贝构造函数。和(2)类似,这个成员函数模板的类型_TP1同样需要满足下面的约束,true == std::is_convertible<_TP1, T>::value。例如:_TP1是T的派生类。
class Document : public Object {};
std::shared_ptr<Document> obj(new Document);
std::shared_ptr<Object> obj2(obj);
assert(obj2.use_count() == 2);
(6) template
template<typename _Tp1, typename _Del>
shared_ptr<T>::shared_ptr(std::unique_ptr<_Tp1, _Del>&& __r)
使用unique_ptr来构造shared_ptr对象。这里特别要注意的是unique_ptr是不具有共享对象所有权的,所以构造函数的参数是右值引用,而不是值传递。使用方式如下:
class Document : public Object {};
std::unique_ptr<Document> obj(new Document);
std::shared_ptr<Object> obj2(std::move(obj)); /// 使用右值引用
/// 经过std::move后,所有权转移到shared_ptr对象中,std::unique_ptr<Document> obj不再持有对象所有权
assert(obj2.use_count() == 1);
shared_Ptr对象满足”CopyConstructible”和”CopyAssignable”的要求,在STL容器中使用基本上和普通的指针一样使用,好处是不用开发人员再对容器中的指针逐个释放对象。
class ObjectManager {
public:
~ObjectManager() {
/// 这里vector会释放shared_ptr对象,进而释放掉Object对象。
/// 如果是std::vector<Object*>这种方式,开发人员需要在析构函数中手工遍历容器,然后逐个删除对象
}
void addObject(unsigned int id) {
objects_.push_back(std::shared_ptr<Object>(new Object(id)));
}
private:
std::vector<std::shared_ptr<Object> > objects_;
};
该例子中ObjectManager::addObject函数创建一个对象,并作为shared_ptr对象存储在vector容器中。这个写法会有一次额外的shared_ptr临时对象的创建和拷贝,对效率有影响。可以用下面两种方式来优化(都需要在C++11新标准下):
1) 使用移动语义,避免临时对象拷贝
objects_.push_back(std::move(std::shared_ptr
2) 使用vector的新的成员函数emplace_back,直接在vector对象尾部的空间上构建一个对象,避免临时对象的拷贝
objects_.emplace_back(new Object(id));
shared_ptr带来的自动内存管理并不是万能良药,使用不当还是会导致内存泄露的。一个典型的错误使用方式就是循环依赖,如下面的例子:
class A;
class B {
private:
std::shared_ptr<A> refA_;
};
class A {
private:
std::shared_ptr<B> refB_;
};
该例子中,A对象和B对象的类成员中各自持有了一个包含对方对象指针的shared_ptr对象,在A对象和B对象进行析构的时候,发现引用计数的数值都不为0, 导致无法正确的释放内存。
大家不要觉得这个例子很牵强,在实际工程中出现这个的概率还蛮大的。典型的如观察者模式,实现中通常观察者都需要维护着观察对象的指针, 而被观察对象则保存了所有来订阅状态变化的观察者的指针,以便在状态变化时,通过这些指针来触发观察者的动作。如果按照上面的实现, 显然就容易犯这个循环依赖的错误。
这也是为什么会有weak_ptr的引入,利用weak_ptr可以降上面的例子修改为下面的正确方式。
class A;
class B {
public:
void action();
private:
std::shared_ptr<A> refA_;
};
class A {
public:
void do_something();
private:
std::weak_ptr<B> refB_;
};
修改后,A对象的类成员中仅仅是持有了B对象指针的一个“弱引用”。你可以这么理解weak_ptr,weak_ptr不会增加对象指针所有权的引用计数, 只是表明一种监视关系。通过weak_ptr,你可以观察到当前的对象是否还可用。
使用时,先要做一次“权限提升”,然后才能操作其持有的对象。接上面的列子,使用方式如下:
void A::do_something() {
/// 检查其监视的这个对象是否还可用,即refB_持有的B对象引用计数大于0.
if (!refB_.expired()) {
/// 通过lock临时提升权限,真正持有该对象,然后对对象进行操作
std::shared_ptr<B> refB = refB_.lock();
refB->action();
}
}
通过这种方法,在A、B对象析构时,由于A::refB_并不真正持有B对象的所有权,因此能够解除循环依赖,使得析构函数能够被正确的执行。
先看下面一个例子:
void callback(std::shared_ptr<Object> obj) {
.......
}
class Object {
public:
void f() {
/// 希望在这里调用call函数,但是改怎么将自身作为shared_ptr对象传递给callback函数呢?
}
};
下面这种写法,直接通过this指针来构造一个shared_ptr,再将其自身作为参数传递给callback函数。
void Object::f() {
callback(std::shared_ptr
}
这个写法是错误的。Constructing a std::shared_ptr for an object that is already managed by another std::shared_ptr will not consult the internally stored weak reference and thus will lead to undefined behavior.
这里需要使用enable_shared_from_this用法。
/// 这是一个很有名的模式,(CRTP)Curiously Recurring Template Pattern. Object从enable_shared_from_this派生,而enable_shared_from_this又是使用Object来实例化的。
class Object : public std::enable_shared_from_this<Object> {
public:
void f() {
callback(shared_from_this());
}
};
enable_shared_from_this通常的实现都是使用weak_ptr来保存一个对象指针的弱引用,而通过shared_from_this则利用weak_ptr来构造一个shared_ptr,通过传值方式返回。
shared_ptr保证了对于引用计数的管理是线程安全的,实现上要么使用mutex做临界区保护要么通过lock-free的方式(原子指令+内存屏障)来保证引用计数的线程安全。
但是对其持有对象的任何操作都不是线程安全的,这写线程安全是需要开发者自己保证的。
shared_ptr智能指针背后的设计原理、实现以及使用涉及很多内容,本文只能算是一个简要介绍,对这个感兴趣的同学可以深入的读下面的一些文章。
文章列表:
http://herbsutter.com/2013/05/29/gotw-89-solution-smart-pointers/
http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/
http://www.boost.org/doc/libs/1_55_0/libs/smart_ptr/shared_ptr.htm
对于特定类类型的全体对象而言,访问一个 全局对象
有时是必要的。也许,
在程序的任意点需要统计已创建的特定类类型对象的数量;或者,全局对象可能
是指向类的错误处理例程的一个指针;或者,它是指向类类型对象的内在自由存
储区的一个指针。
然而,全局对象会破坏封装
:对象需要支持特定类抽象的实现。如果对象是
全局的,一般的用户代码就可以修改这个值。类可以定义类 静态成员,而不是
定义一个可普遍访问的全局对象。
通常,非 static 数据成员存在于类类型的每个对象中。不像普通的数据成
员,static 数据成员独立于该类的任意对象而存在; 每个 static 数据成员是
与类关联的对象,并不与该类的对象相关联。
正如类可以定义共享的 static 数据成员一样,类也可以定义 static 成员
函数。static 成员函数没有 this 形参, 它可以直接访问所属类的 static 成
员,但不能直接使用非 static 成员。
使用 static 成员而不是全局对象有三个优点。
在成员声明前加上关键字 static 将成员设为 static。static 成员遵循正 常的公有/私有访问规则。 例如,考虑一个简单的表示银行账户的类。每个账户具有余额和拥有者,并 且按月
class Account {
public:
// interface functions here
void applyint() { amount += amount * interestRate; }
static double rate() { return interestRate; }
static void rate(double); // sets a new rate
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
这个类的每个对象具有两个数据成员:owner 和 amount。对象没有与 static 数据成员对应的数据成员,但是,存在一个单独的 interestRate 对象, 由 Account 类型的全体对象共享。
可以通过作用域操作符从类直接调用 static 成员,或者通过对象、引用或 指向该类类型对象的指针间接调用。
Account ac1;
Account *ac2 = &ac1;
// equivalent ways to call the static member rate function
double rate;
rate = ac1.rate(); // through an Account object or reference
rate = ac2->rate(); // through a pointer to an Account object
rate = Account::rate(); // directly from the class using the scope
operator
像使用其他成员一样,类成员函数可以不用作用域操作符来引用类的
static 成员:
class Account {
public:
// interface functions here
void applyint() { amount += amount * interestRate; }
};
注:此出处为C++Primer中文版第四版