const关键字
如果const位于* 的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量; 如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。
指向常量的指针:
const 类型 *指针名 或者 类型 const *指针名 (约束针对所指对象访问)
表示常量:
const 类型 常量标识符= 常量表达式
指针常量:
指针常量说明指针的值只能在定义的时候初始化,定义后不能修改,不能改变指针的取向。但不影响所指向对象的访问特性。
类型 * const 指针名
指向常量的指针常量:
const 类型 * const指针名 或者 类型 const * const 指针名
常引用
冠以const定义的引用,将约束对象用别名的方式访问的时候为只读。
const 类型 & 引用名 = 对象名
函数指针
函数类型
函数的类型是指函数的接口,包括函数的参数定义和返回值类型。
例如,以下是类型相同的函数:
double p1(double,double);
double p2(double,double);
double p3(double,double);
c++中可以用typedef关键字定义函数类型名。函数类型名的一般形式为:
typedef 类型 函数类型名(形式参数表)
例如以上的三个函数的声明可以写成以下方式:
typedef double functionType(double,double);
functionType p1,p2,p3;
内联函数
可以用于定义一些功能比较简单、代码比较短的函数。编译的时候,系统把内联函数的函数体嵌入到每个函数调用的地方,节省程序运行时的调用开销。
定义内联函数的方法,在函数名第一次出现的时候,在函数名之前冠以关键字inline。通常在函数原型中指出。若已在函数原型中指定inline,则函数定义的时候不能重复给出。
特征:
- 相当于把内联函数里面的内容写在调用内联函数处;
- 相当于不用执行进入函数的步骤,直接执行函数体;
- 相当于宏,却比宏多了类型检查,真正具有函数特性;
- 不能包含循环、递归、switch 等复杂操作;
- 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。
声明:
incline 类型 函数名(形参列表);
编译器对 inline 函数的处理步骤:
- 将 inline 函数体复制到 inline 函数调用点处;
- 为所用 inline 函数中的局部变量分配内存空间;
- 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
- 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。
优点
- 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
- 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
- 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
- 内联函数在运行时可调试,而宏定义不可以。
缺点
- 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
- inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
- 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器
重载函数
c++编译器只根据函数参数列表(参数的类型和个数)进行重载版本的调用匹配,函数返回值的内容不起作用,例如:
int a(int, int);
double a(int,int);
这两个不是重载函数,c++会认为函数重复说明。
存储特性
c++有两类存储特性:自动存储和静态存储。
c++把变量默认为自动存储,所以关键字auto很少用。
关键字register说明把变量存放在寄存器中。如今的c++编译器已经可以识别经常使用的变量,决定是否存放在寄存器中,而不需要程序员进行register说明。
关键字extern和static说明静态存储变量和函数标识符。全局说明的标识符默认为extern。
如果这两个关键字用于说明变量,程序再开始执行的时候就分配和初始化存储空间,如果用于说明函数,表示从程序执行开始就存在这个函数名。
用static声明的局部变量只能在定义该变量的函数体中使用。与自动变量不同的是,static在第一次使用的时候进行了初始化,函数退出时,系统保持其存储空间和数值,下次调用这个函数是,static变量还是上次退出函数时的值。
#include<iostream>
using namespace std;
int func();
int main() {
cout << "a+b="<<func() << endl;
cout << "a+b=" << func() << endl;
system("pause");
}
int func() {
int a = 0;
static int b = 1;
a++;
b++;
cout << "auto a=" << a << endl;
cout << "static b=" << b << endl;
return a + b;
}
宏定义指令
宏定义指令 #define
用来指定正文替换程序中出现的标识符。在c语言中不带参数的#define常用于定义常量,带参数的#define用于定义简单函数。
例如:
#define PI 3.1415926535
#define area(r) PI*r*r
由于宏指令是在程序正式编译之前执行的,所以不能对替换内容进行语法检查。c++的const关键字定义常量和inline定义内联函数代替了#define定义常量和函数的作用。
条件编译
条件编译指令可以根据一个常量值作为判断条件,决定源程序中某一段代码是否参加编译,条件编译指令的结构和if选择结构非常相似。以下是常见的形式:
若常量表达式的值为真(非0),则程序文本参与编译。
#if 常量表达式
程序文本
#endif
若常量表达式为真(非0),则程序文本1参与编译,否则程序文本2参与编译。条件编译指令中的常量表达式必须在编译时(程序执行之前)就有确定值,不能再常量表达式中进行强制类型转换,或进行sizeof计算,也不能是枚举常量。
#if 常量表达式
程序文本1
#else
程序文本2
#endif
若标识符没有定义,则程序文本被编译,若标识符已经定义,则程序文本被忽略。
#ifndef 标识符
#define 标识符
程序文本
#endif
命名空间
命名空间可以帮助程序员在开发新的软件组件的时候不会与已有的软件组件产生命名冲突。命名空间是类、函数、对象、类型和其他名字生命的集合。std是c++标准的名空间。
定义命名空间
开发一个应用程序常常需要多个库,它们源自不同的文件,这些文件常常用include进来。方便在其他文件调用。 定义命名空间如下:
namespace 标识符
{语句序列}
这样就可以用using语句
using namespace 标识符;
将命名空间的元素纳入进来。
标识符::函数名/对象名
并可以使用如上的方法来对命名空间的类、函数等等进行调用。
预处理器指示符
(update Oct,11 2018)
#include 指示符读入指定文件的内容 它有两种格式
#include <some_file.h>
#include "my_file.h" 如果文件名用尖括号 < 和 > 括起来 表明这个文件是一个工程或标准头文件查找过程会检查预定义的目录我们可以通过设置搜索路径环境变量或命令行选项来修改这些目录。在不同的平台上这些方法大不相同 建议你请教同事或查阅编译器手册以获得更进一步的信息。如果文件名用一对引号括起来则表明该文件是用户提供的头文件查找该文件时将从当前文件目录开始。
虚函数与动态绑定
一个虚函数在基类中被声明为virtual,则编译器将建立一个可由运行环境解释的特殊结构,并由程序执行时而不是编译时由运行环境来执行这个函数的调用。
若虚函数如下定义:
virtual void draw()=0;
则这个函数被称为纯虚函数,如果一个类至少有一个纯虚函数,则该类是抽象类。一个抽象类必须作为基类被其他类集成,抽象类不能自己生成实例。继承抽象类的派生类必须自行定义自己的纯虚函数的实现。
pragma pack(n)
设定结构体、联合以及类成员变量以 n 字节方式对齐。其实之所以有内存字节对齐机制,就是为了最大限度的减少内存读取次数。我们知道CPU读取速度比内存读取速度快至少一个数量级,所以为了节省运算花费时间,只能以牺牲空间来换取时间了。
#pragma pack(push) // 保存对齐状态
#pragma pack(4) // 设定为 4 字节对齐
struct test
{
char m1;
double m4;
int m3;
};
#pragma pack(pop) // 恢复对齐状态
下面分析其存储情况,首先为 m1 分配空间,其偏移量 为 0,满足我们自己设定的对齐方式(4 字节对齐),m1 大小为 1 个字节。接着开始 为 m4 分配空间,这时其偏移量为 1,需要补足 3 个字节,这样使偏移量满足为 n=4 的倍数(因为 sizeof(double)大于 4),m4 占用 8 个字节。接着为 m3 分配空间,这时 其偏移量为 12,满足为 4 的倍数,m3 占用 4 个字节。这时已经为所有成员变量分配 了空间,共分配了 16 个字节,满足为 n 的倍数。如果把上面的#pragma pack(4)改为 #pragma pack(8),那么我们可以得到结构的大小为 24。
volatile
volatile int i = 10;
- volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
- volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
- const 可以是 volatile (如只读的状态寄存器)
- 指针可以是 volatile
extern “C”
- 被 extern 限定的函数或变量是 extern 类型的
- 被 extern “C” 修饰的变量和函数是按照 C 语言方式编译和连接的
extern “C” 的作用是让 C++ 编译器将 extern “C” 声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。
example:
#ifdef __cplusplus
extern "C" {
#endif
void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif
struct 和 typedef struct
C 中
// c
typedef struct Student {
int age;
} S;
等价于
// c
struct Student {
int age;
};
typedef struct Student S;
此时 S 等价于 struct Student,但两个标识符名称空间不相同。
另外还可以定义与 struct Student 不冲突的 void Student() {}。
C++ 中
由于编译器定位符号的规则(搜索规则)改变,导致不同于C语言。
一、如果在类标识符空间定义了 struct Student {…};,使用 Student me; 时,编译器将搜索全局标识符表,Student 未找到,则在类标识符内搜索。
即表现为可以使用 Student 也可以使用 struct Student,如下:
// cpp
struct Student {
int age;
};
void f( Student me ); // 正确,"struct" 关键字可省略
二、若定义了与 Student 同名函数之后,则 Student 只代表函数,不代表结构体,如下:
typedef struct Student {
int age;
} S;
void Student() {} // 正确,定义后 "Student" 只代表此函数
//void S() {} // 错误,符号 "S" 已经被定义为一个 "struct Student" 的别名
int main() {
Student();
struct Student me; // 或者 "S me";
return 0;
}
C++ 中 struct 和 class
总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
区别
最本质的一个区别就是默认的访问控制。
-
默认的继承访问权限。struct 是 public 的,class 是 private 的。
-
struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。
explicit(显式)关键字
- explicit 修饰构造函数时,可以防止隐式转换和复制初始化
-
explicit 修饰转换函数时,可以防止隐式转换,但按语境转换除外
struct A { A(int) { } operator bool() const { return true; } }; struct B { explicit B(int) {} explicit operator bool() const { return true; } }; void doA(A a) {} void doB(B b) {} int main() { A a1(1); // OK:直接初始化 A a2 = 1; // OK:复制初始化 A a3{ 1 }; // OK:直接列表初始化 A a4 = { 1 }; // OK:复制列表初始化 A a5 = (A)1; // OK:允许 static_cast 的显式转换 doA(1); // OK:允许从 int 到 A 的隐式转换 if (a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换 bool a6(a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换 bool a7 = a1; // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换 bool a8 = static_cast<bool>(a1); // OK :static_cast 进行直接初始化 B b1(1); // OK:直接初始化 B b2 = 1; // 错误:被 explicit 修饰构造函数的对象不可以复制初始化 B b3{ 1 }; // OK:直接列表初始化 B b4 = { 1 }; // 错误:被 explicit 修饰构造函数的对象不可以复制列表初始化 B b5 = (B)1; // OK:允许 static_cast 的显式转换 doB(1); // 错误:被 explicit 修饰构造函数的对象不可以从 int 到 B 的隐式转换 if (b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换 bool b6(b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换 bool b7 = b1; // 错误:被 explicit 修饰转换函数 B::operator bool() 的对象不可以隐式转换 bool b8 = static_cast<bool>(b1); // OK:static_cast 进行直接初始化 return 0; }
friend 友元类和友元函数
- 能访问私有成员
- 破坏封装性
- 友元关系不可传递
- 友元关系的单向性
- 友元声明的形式及数量不受限制
:: 范围解析运算符
分类
- 全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间
- 类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
-
命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的
int count = 0; // 全局(::)的 count class A { public: static int count; // 类 A 的 count(A::count) }; int main() { ::count = 1; // 设置全局的 count 的值为 1 A::count = 2; // 设置类 A 的 count 为 2 int count = 0; // 局部的 count count = 3; // 设置局部的 count 的值为 3 return 0; }
decltype
decltype 关键字用于检查实体的声明类型或表达式的类型及值分类。语法:
decltype ( expression )
// 尾置返回允许我们在参数列表之后声明返回类型
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
// 处理序列
return *beg; // 返回序列中一个元素的引用
}
// 为了使用模板参数成员,必须用 typename
template <typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
// 处理序列
return *beg; // 返回序列中一个元素的拷贝
}
成员初始化列表
好处
- 更高效:少了一次调用默认构造函数的过程。
-
有些场合必须要用初始化列表:
- 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
- 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
- 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。
initializer_list 列表初始化【C++11】
用花括号初始化器列表列表初始化一个对象,其中对应构造函数接受一个 std::initializer_list 参数.
#include <iostream>
#include <vector>
#include <initializer_list>
template <class T>
struct S {
std::vector<T> v;
S(std::initializer_list<T> l) : v(l) {
std::cout << "constructed with a " << l.size() << "-element list\n";
}
void append(std::initializer_list<T> l) {
v.insert(v.end(), l.begin(), l.end());
}
std::pair<const T*, std::size_t> c_arr() const {
return {&v[0], v.size()}; // 在 return 语句中复制列表初始化
// 这不使用 std::initializer_list
}
};
template <typename T>
void templated_fn(T) {}
int main()
{
S<int> s = {1, 2, 3, 4, 5}; // 复制初始化
s.append({6, 7, 8}); // 函数调用中的列表初始化
std::cout << "The vector size is now " << s.c_arr().second << " ints:\n";
for (auto n : s.v)
std::cout << n << ' ';
std::cout << '\n';
std::cout << "Range-for over brace-init-list: \n";
for (int x : {-1, -2, -3}) // auto 的规则令此带范围 for 工作
std::cout << x << ' ';
std::cout << '\n';
auto al = {10, 11, 12}; // auto 的特殊规则
std::cout << "The list bound to auto has size() = " << al.size() << '\n';
// templated_fn({1, 2, 3}); // 编译错误!“ {1, 2, 3} ”不是表达式,
// 它无类型,故 T 无法推导
templated_fn<std::initializer_list<int>>({1, 2, 3}); // OK
templated_fn<std::vector<int>>({1, 2, 3}); // 也 OK
}
多态
- 多态,即多种状态,在面向对象语言中,接口的多种不同的实现方式即为多态。
- C++ 多态有两种:静态多态(早绑定)、动态多态(晚绑定)。静态多态是通过函数重载实现的;动态多态是通过虚函数实现的。
- 多态是以封装和继承为基础的。
静态多态(早绑定)
函数重载
class A
{
public:
void do(int a);
void do(int a, int b);
};
动态多态(晚绑定)
- 虚函数:用 virtual 修饰成员函数,使其成为虚函数
- 普通函数(非类成员函数)不能是虚函数
- 静态函数(static)不能是虚函数
- 构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)
-
内联函数不能是表现多态性时的虚函数
class Shape // 形状类 { public: virtual double calcArea() { ... } virtual ~Shape(); }; class Circle : public Shape // 圆形类 { public: virtual double calcArea(); ... }; class Rect : public Shape // 矩形类 { public: virtual double calcArea(); ... }; int main() { Shape * shape1 = new Circle(4.0); Shape * shape2 = new Rect(5.0, 6.0); shape1->calcArea(); // 调用圆形类里面的方法 shape2->calcArea(); // 调用矩形类里面的方法 delete shape1; shape1 = nullptr; delete shape2; shape2 = nullptr; return 0; }
虚析构函数
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
class Shape
{
public:
Shape(); // 构造函数不能是虚函数
virtual double calcArea();
virtual ~Shape(); // 虚析构函数
};
class Circle : public Shape // 圆形类
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
shape1->calcArea();
delete shape1; // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。
shape1 = NULL;
return 0;
}
虚函数、纯虚函数
- 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样的话,这样编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
- 虚函数在子类里面也可以不重载的;但纯虚函数必须在子类去实现。
- 虚函数的类用于 “实作继承”,继承接口的同时也继承了父类的实现。当然大家也可以完成自己的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
- 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类和大家口头常说的虚基类还是有区别的,在 C# 中用 abstract 定义抽象类,而在 C++ 中有抽象类的概念,但是没有这个关键字。抽象类被继承后,子类可以继续是抽象类,也可以是普通类,而虚基类,是含有纯虚函数的类,它如果被继承,那么子类就必须实现虚基类里面的所有纯虚函数,其子类不能是抽象类。
虚函数指针、虚函数表
- 虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定。
- 虚函数表:在程序只读数据段(.rodata section,见:目标文件存储结构),存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。
虚继承
虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。
底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
虚继承、虚函数
相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
不同之处:
-
虚继承
- 虚基类依旧存在继承类中,只占用存储空间
- 虚基类表存储的是虚基类相对直接继承类的偏移
-
虚函数
- 虚函数不占用存储空间
- 虚函数表存储的是虚函数地址
模板类、成员模板、虚函数
- 模板类中可以使用虚函数
- 一个类(无论是普通类还是类模板)的成员模板(本身是模板的成员函数)不能是虚函数
内存分配和管理
malloc、calloc、realloc、alloca
- malloc:申请指定字节数的内存。申请到的内存中的初始值不确定。
- calloc:为指定长度的对象,分配能容纳其指定个数的内存。申请到的内存的每一位(bit)都初始化为 0。
- realloc:更改以前分配的内存长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定。
- alloca:在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca 不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca 不宜使用在必须广泛移植的程序中。C99 中支持变长数组 (VLA),可以用来替代 alloca。
malloc、free
//申请内存,确认是否申请成功
char *str = (char*) malloc(100);
assert(str != nullptr);
//释放内存后指针置空
free(p);
p = nullptr;
new、delete
- new / new[]:完成两件事,先底层调用 malloc 分了配内存,然后调用构造函数(创建对象)。
- delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用 free 释放空间。
-
new 在申请内存时会自动计算所需字节数,而 malloc 则需我们自己输入申请内存空间的字节数。
//申请内存,确认是否申请成功 int main() { T* t = new T(); // 先内存分配 ,再构造函数 delete t; // 先析构函数,再内存释放 return 0; }
delete this 合法吗?
合法,但:
- 必须保证 this 对象是通过 new(不是 new[]、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的
- 必须保证调用 delete this 的成员函数是最后一个调用 this 的成员函数
- 必须保证成员函数的 delete this 后面没有调用 this 了
- 必须保证 delete this 后没有人使用了
如何定义一个只能在堆上(栈上)生成对象的类?
只能在堆上
方法:将析构函数设置为私有
原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
只能在栈上
方法:将 new 和 delete 重载为私有
原因:在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。
智能指针
C++ 11
- shared_ptr
- unique_ptr
- weak_ptr
- auto_ptr(被 C++11 弃用)
Class shared_ptr 实现共享式拥有(shared ownership)概念。多个智能指针指向相同对象,该对象和其相关资源会在 “最后一个 reference 被销毁” 时被释放。为了在结构较复杂的情景中执行上述工作,标准库提供 weak_ptr、bad_weak_ptr 和 enable_shared_from_this 等辅助类。
Class unique_ptr 实现独占式拥有(exclusive ownership)或严格拥有(strict ownership)概念,保证同一时间内只有一个智能指针可以指向该对象。你可以移交拥有权。它对于避免内存泄漏(resource leak)——如 new 后忘记 delete ——特别有用。
shared_ptr
多个智能指针可以共享同一个对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的所有资源。
支持定制型删除器(custom deleter),可防范 Cross-DLL 问题(对象在动态链接库(DLL)中被 new 创建,却在另一个 DLL 内被 delete 销毁)、自动解除互斥锁
weak_ptr
weak_ptr 允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)。因此,在 default 和 copy 构造函数之外,weak_ptr 只提供 “接受一个 shared_ptr” 的构造函数。
可打破环状引用(cycles of references,两个其实已经没有被使用的对象彼此互指,使之看似还在 “被使用” 的状态)的问题
unique_ptr
unique_ptr 是 C++11 才开始提供的类型,是一种在异常时可以帮助避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。一旦拥有着被销毁或编程 empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。
unique_ptr 用于取代 auto_ptr
auto_ptr
被 c++11 弃用,原因是缺乏语言特性如 “针对构造和赋值” 的 std::move 语义,以及其他瑕疵。
auto_ptr 与 unique_ptr 比较
auto_ptr 可以赋值拷贝,复制拷贝后所有权转移;unqiue_ptr 无拷贝赋值语义,但实现了move 语义; auto_ptr 对象不能管理数组(析构调用 delete),unique_ptr 可以管理数组(析构调用 delete[] );