Appearance
C++
更新: 5/12/2025 字数: 0 字 时长: 0 分钟
数据类型
基础数据类型
注意:以下数据类型的位数以64位操作系统为准。
基础数据类型 | Java | C# | C++ |
---|---|---|---|
布尔类型 | bool | ||
8位字符类型 | x | char(取值范围:[-128, 127]或[0, 255]) | |
8位有符号字符类型 | signed char(取值范围:[-128, 127]) | ||
8位无符号字符类型 | unsigned char(取值范围:[0, 255]) | ||
16位Unicode字符类型 | char | char16_t | |
32位Unicode字符类型 | x | char32_t | |
宽字符类型 | x | wchar_t(2或4字节,存储Unicode字符) | |
8位有符号整数 | byte | sbyte | byte |
16位有符号整数 | short | ||
32位有符号整数 | int | ||
64位有符号整数 | long | long [long] | |
32位单精度浮点类型 | float | ||
64位双精度浮点类型 | double | ||
扩展精度浮点类型 | x | long double(8、12或16字节) | |
精准浮点类型 | x | decimal | x |
8位无符号整数 | sbyte | unsigned byte | |
16位无符号整数 | ushort | unsigned short | |
32位无符号整数 | uint | unsigned int | |
64位无符号整数 | ulong | unsigned long [long] |
- Java没有有无符号之分,都是有符号的,取值范围从负数到正数。
- C++的
long
类型可能是4字节或8字节,具体取决于平台,但long long
一定是8字节。
复合数据类型
数组
数组名
表示数组首元素的地址,而&数组名
表示整个数组的首地址。虽然值相同,但含义不同。- 数组名可看做是一个指向不变的指针(相关内容见const关键字)。
c++
int arr1[3] = {1, 2, 3};
cout << "arr1 = " << arr1 << endl;
cout << "&arr1 = " << &arr1 << endl;
cout << "arr1 + 1 = " << arr1 + 1 << endl; // 第二个元素地址,相差4个字节(int类型)
cout << "&arr1 + 1 = " << &arr1 + 1 << endl; // 相差整个数组长度的字节数,即3*4字节
cout << "*(arr1 + 1) = " << *(arr1 + 1) << endl; // 第二个元素的值
定义数组的长度必须是编译时常量。常见的编译时常量:
- 字面量
- const常量
- constexpr常量
- 枚举常量
- 常量表达式
字符数组
字符数组是C风格的字符串。
- 字符数组末尾需要有结束标识符号'\0'。
- 字符数组是可以修改的。
c++
// 系统自动在末尾加上'\0',所以数组长度为4
char name1[] = "dog";
// {}形式初始化,系统不会在末尾加上'\0',需要自己加上。
char name2[] = { 'd', 'o', 'g', '\0'};
// 字符串长度小于数组长度,数组剩余空间全被初始化为'\0'
char name3[10] = "dog";
cout << name1 << " " << strlen(name1) << " " << sizeof(name1) << endl; // 3 3 4
cout << name2 << " " << strlen(name2) << " " << sizeof(name2) << endl; // 3 3 4
cout << name3 << " " << strlen(name3) << " " << sizeof(name3) << endl; // 3 3 10
name3[3] = 's';
name3[5] = 'x';
cout << "modified name3 = " << name3 << endl; // dogs
提示
在获取了字符串的长度后,切记长度+1
来初始化字符数组的长度!
指针
指针是一个变量,其存储的是值的地址,而不是值本身。
c++
int x = 10;
int* ptr = &x;
int** pptr = &ptr; // 指针是变量,所以它也有地址,pptr是二级指针,即指向指针的指针。
// &x表示x的地址,即ptr变量的值。
cout << "&x = " << x << ", ptr = " << ptr << endl;
// *运算符用于指针表示解除引用,可以获取指针所指向的地址空间的值。即x = *ptr。
cout << "x = " << x << ", *ptr = " << *ptr << endl;
指针多用于使用new在堆中动态分配内存,如:
c++
int* p1 = new int(10);
int* p2 = new int[10];
// do something;
delete p1; // 释放内存
delete[] p2; // 使用new[]为数组分配内存,则应使用delete[]释放内存。
- new分配的内存用delete来释放;new[]为数组分配的内存用delete[]来释放。
- 不要使用delete来释放不是new分配的内存
- 不要使用delete释放同一内存块两次。
- 对空指针应用delete是安全的。
常见指针问题
- 野指针:未被初始化或已经被释放的指针,其指向的内存地址是未知的。声明指针时要初始化为nullptr或有效的地址。
- 悬挂指针:指针指向的内存空间已经被释放,但指针仍然指向该内存空间。新手常见的错误是函数返回了局部变量(栈空间)的指针。c++
static int* buildX(int n=1) { int x = 10 * n; // 在栈空间上分配的内存,函数执行完之后将被释放。 cout << "&x=" << &x << endl; return &x; } static int* buildY(int n=1) { int* y = new int(10 * n); // 在堆空间上分配的内存,由程序员自己释放。 cout << "y=" << y << endl; return y; } int main() { int* xp = buildX(10); // xp是悬挂指针,所指向的内存空间已经被释放 cout << "xp=" << xp << ", *xp=" << *xp << endl; int* yp = buildY(10); cout << "yp=" << yp << ", *yp=" << *yp << endl; delete yp; // 释放指针所指向的内存空间,但指针本身的值还是指向该内存空间。 yp = nullptr; // 避免悬挂指针 }
指向函数的指针
最简形式:void (*p_func)()
较为复杂的函数指针数组:
c++
const double* (*pf[3])(const double* arr, int n) = { func1, func2, func3 };
const double* (*(*pf2)[3])(const double* arr, int n) = &pf;
解读
首先要知道运算符的优先级:() > [] > *
。
引用【左值引用】
引用是已定义的变量的别名,其主要用途是作为函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本,提高效率。
c++
int a = 10;
int &ra = a; // ra是a的引用
int* pa = &a; // pa是a的指针
提示
必须在声明引用变量时进行初始化,并且不能重新绑定到另一个变量。
枚举
C++11引入了增强型枚举,枚举值限定在类作用域内,避免了命名冲突。
c++
enum class State : int
{
Unknown = 0,
Success = 1,
Failed = 2,
Running = 3
};
auto state = State::Running;
尽量少用传统枚举类型,但有些场合挺好用,比如在类内定义编译时常量:
c++
class Stack
{
public:
enum { MAX_SIZE = 100 };
// static constexpr int MAX_SIZE = 100;
int arr[MAX_SIZE];
}
结构体
C++的结构体和类基本相同,唯一的区别是默认的访问权限:结构体成员和继承的默认访问权限都是public;而类成员和继承的默认访问权限都是private。
结构体主要用于对数据的简单封装,较为复杂的业务逻辑应该使用类。
c++
struct Point
{
float x;
float y;
void display() const
{
cout << "(" << x << ", " << y << ")" << endl;
}
};
void test_struct()
{
Point point = {1.0, 2.5};
point.display();
}
联合体
多个成员共享同一块内存。
标准库数据类型
string
string类是对字符数组的封装,支持自动扩容,提供了更加简单的API。
pair
std::pair<T1, T2>
是一个模板类,适用于需要将两个元素(可以不同类型)组合在一起的场景。
c++
#include <utility>
using namespace std;
std::pair<int, char> tuple(1, 'a');
cout << "first val:" << tuple.first << endl;
cout << "second val:" << tuple.second << endl;
tuple
元组是pair的泛化,支持任意数量的元素组合。用法比较奇怪,用的时候去查吧!
容器
容器是存储其它对象的对象,储存的对象类型必须是可复制构造的和可赋值的。
序列容器
基本容器 | 描述 | 内存结构 | 固定大小 | 使用场景 |
---|---|---|---|---|
array | 固定数组 | 连续存储 | 是 | 数组大小固定的随机索引访问 |
vector | 动态数组 | 连续存储 | 否 | 动态扩容且随机索引访问 |
list | 双向链表 | 非连续存储 | 否 | 双向遍历,随机插入或删除元素 |
forward_list | 单向链表 | 非连续存储 | 否 | 单向遍历,随机插入或删除元素 |
deque | 双端队列 | 分段连续内存 | 否 | 头尾两端频繁插入或删除元素 |
容器适配器是基于基本容器的简化接口,它们提供了一种特定的接口来访问底层容器的数据,其底层容器通常可以有多种选择。
容器适配器 | 描述 | 特点 | 默认底层容器 |
---|---|---|---|
stack | 栈 | 后进先出 | deque |
queue | 队列 | 先进先出 | deque |
priority_queue | 优先级队列 | 快速访问优先级最高的元素 | vector实现的最大堆 |
关联容器
基于键值对的容器,它们通过键来存储和访问元素。关联容器分为集合和映射,集合使用红黑树(对数级时间复杂度),映射使用哈希表(常数级时间复杂度)。
关联容器 | 描述 |
---|---|
set | 有序集合 |
unordered_set | 无序集合 |
multipset | 允许重复值的有序集合 |
unordered_multiset | 允许重复值的无序集合 |
map | 有序映射 |
unordered_map | 无序映射 |
multipmap | 同一个键可关联多个值的有序映射 |
unordered_multimap | 同一个键可关联多个值的无序映射 |
数据类型转换
类型转换 | 描述 |
---|---|
静态类型转换static_cast | 将一种数据类型的值强制转换为另一种近似的数据类型的值。 |
动态类型转换dynamic_cast | 通常用于将一个基类指针或引用转换为派生类指针或引用。指针类型转换失败返回空指针;引用类型转换失败会抛出std::bad_cast异常 |
常量转换const_cast | 将const类型的对象转换为非const类型的对象,不改变对象类型。 |
重新解释转换reinterpret_cast | 将一个数据类型的值重新解释为另一个数据类型的值,通常用于在不同的数据类型之间进行转换。 |
关键字
const
定义常量
- const修饰的变量不可修改,且必须初始化。
- const修饰的常量可以是编译时常量,也可以是运行时常量,具体取决于其初始化方式。
- 非const变量默认是extern。要使const变量能够在其他文件中访问,必须在文件中显式地指定它为extern。
c++
// 不指定extern,则只能在当前文件内访问
extern const int BUF_SIZE = 100; // 编译时常量
int getSize()
{
return sizeof(int);
}
int main() {
const int n = 10; // 编译时常量
cosnt int n1 = 5 * n; // 常量表达式也是编译时常量
int arr[n]{};
const int size = getSize(); // 运行时常量:需要运行时才能确定
}
const与指针
const位于*
左侧,修饰的是* 变量
,修饰的是值,即不可以通过指针来修改其指向的值,但可以修改指针的指向。
c++
const char* s1 = "Tom";
// *s1 = "Lucy"; // 错误,不能通过指针来修改其指向的值
s1 = "Lucy"; // 修改指针的指向
这里并不是修改了Tom,而是在新的内存中存放Lucy,把指针s1的指向由Tom的内存改为指向Lucy的内存。
const位于*
右侧,修饰的是指针变量,指针的值是不可以改变的,即指向不能变,必须初始化。可以通过指针来修改其指向的值。
c++
char* const s2 = "Tom";
// *s2 = "Lucy"; // 错误
// s2 = "Lucy"; // 错误,因为指向不可改变
为什么高亮的行也会错误呢?在C++中,字符串字面量的类型是const char[]
,是不可以修改的。事实上,这相当于上面两者的结合体,既不能修改指针指向,也不能通过指针修改其指向的值。
c++
const char* const s2 = "Tom";
如果要修改,应该使用string或字符数组。
const与函数
const与类
- const修饰成员变量,必须使用初始化列表初始化,不能在构造函数内初始化。
- const修饰成员函数,不能修改成员变量的值,且只能调用const成员函数。
- const修饰类的实例对象,该对象的所有成员变量都不能修改。
c++
class Person {
public:
const int age_ = 0;
Person(int age) : age_(age) {
// this->age_ = age;
}
void display() const;
}
const Person p1(10);
// p1.age = 20;
constexpr
- constexpr修饰的变量必须能在编译阶段确定或计算出,即编译时常量。
- constexpr修饰的函数的参数和返回值(返回值可以用非常量的变量存储)必须满足常量表达式的要求。
- constexpr结合模板使用可以实现复杂的编译时计算,提高代码的灵活性和性能。c++
template <typename T> T getA(T a) { // std::is_same 编译时检查两个类型是否相同 if constexpr (std::is_same<T, int>::value) return a * 10; else if constexpr (std::is_same<T, float>::value) return a * 1000; else return a; }
static
- 静态函数:静态函数的作用域为当前文件,即只能在当前文件内访问。普通函数默认是extern,可以在其他文件中访问。
- 静态变量:空间分配只初始化一次,存在于程序运行的整个生命周期。
- static与类:静态成员变量和静态成员方法都由类所拥有,内存空间只有一份,类的所有实例对象都共享类的静态成员变量和静态成员方法。
- 静态成员变量:不能使用构造函数初始化,除非静态成员变量是const整数类型或枚举型,否则不能在声明时初始化。
- 静态成员方法:只能访问静态成员变量和静态成员方法。
- 可直接通过类名访问public的静态成员变量和静态成员方法。
c++
static void test2() {
static int age = 1; // age变量在静态存储区,再次运行函数也不会重新初始化age
cout << "age = " << age << endl;
age++;
cout << "age = " << age << endl;
}
c++
class Person
{
public:
static int age;
static void addAge() {
age++;
}
}
int Person::age = 1;
int main() {
Person p1 = Person();
p1.addAge();
Person::addAge();
cout << "age = " << Person::age << endl; // 3
Person p2 = Person();
p2.addAge();
cout << "age = " << p2.age << endl; // 4
}
typedef
用于定义类型的别名,便于代码的可读性和维护性。
c++
typedef unsigned int Item; // 定义unsigned int类型的别名为Item
Item x = 10;
extern
- 用于声明全局变量。通常用于在多个源文件中共享全局变量。
- 用于解决 C++ 和 C 代码之间的兼容性问题。由于C++支持函数重载,C++编译器会将函数名与参数类型、返回值等信息一起编译到函数符号中,而C编译器不会这样做。因此,直接在C程序中调用C++函数会导致链接错误。为了解决这个问题,可以使用
extern "C"
关键字来指示编译器按照C语言的方式来处理特定的代码。c++//xx.h int add(...) //xx.c int add(){} //xx.cpp extern "C" { #include "xx.h" }
c++//xx.hpp #include <iostream> extern "C" { // 可同时声明多个函数 int add(int, int); void increment(); } //xx.cpp int add(int x, int y) { std::cout << "x=" << x << ", y=" <<y << std::endl; return x + y; } //xx.c 先声明函数,再调用 extern int add(int, int); // extern可省略,但推荐加上 int main() { add(1, 2); }
inline
- 在类中声明函数并定义,则该函数是隐式内联函数。在声明之后想要成为内联函数,必须在定义处加inline关键字。(推荐后者写法)
- 虚函数可以是内联函数,但当虚函数表现多态性的时候不会内联,因为虚函数的多态性在运行期,而编译器无法知道运行期调用哪块代码。编译器需要知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
内联函数仅仅省去了函数调用的开销,从而提高函数的执行效率,但每一处的内联函数都要复制代码,会导致程序代码量增加。以下情况不适合使用内联:
- 函数体内的代码较长,内联将导致内存消耗代价较高。
- 执行函数的时间比调用函数的开销大。
final
- 将类声明为final,将禁止此类被继承。
- 普通函数不能声明为final,只有虚函数声明为final,可防止派生类覆盖该虚函数。
override
显式声明派生类中的成员函数覆盖了基类中的虚函数。
- 增强可读性并进行编译器检查:如果派生类中的函数没有正确覆盖基类中的虚函数(例如函数签名不匹配),编译器会报错。
- 与final结合使用表示该函数覆写了基类的虚函数,且不允许进一步覆写。
volatile
- 变量可能被某些编译器未知因素更改,使用volatile修饰变量告知编译器不要对该变量进行优化。
- volatile声明的变量,每次访问都必须从内存中取值(没有被volatile修饰的变量可能由于编译器的优化,从CPU寄存器中取值)。
- const可以是volatile(如只读的状态寄存器)
- 指针可以是volatile
explicit
- 当类的构造函数只有一个输入参数时(其它参数有默认值也算只有一个输入),此构造函数可隐式将参数值转换为类对象。explicit修饰此构造函数后,将关闭此特性,但仍可以显式强制类型转换。
- 修饰转换函数时,可以防止隐式转换,但按语境(通常是在与布尔相关的条件判断或布尔逻辑运算符中的语境)转换除外。
c++
class Person
{
public:
// 构造函数
explicit Person(std::string name, int age=0): name_(name) {}
// 转换函数1:将Person对象转换为string类型
explicit operator std::string() const { return name_; }
// 转换函数2:将Person对象转换为int类型
operator int() const { return age_; }
explicit operator bool() const { return !name_.empty(); }
private:
std::string name_;
int age_ = 0;
};
int main() {
Person p1 = Person("Tom");
// Person p1 = "Tom"; // 隐式转换,构造函数使用explicit后会报错
Person p2 = (Person) "Tom"; // 显式强制转换
std::string name = (std::string) p2; // 强制类型转换
int age = p2; // 隐式调用转换函数2,转换函数使用explicit后会报错
if (p2) {}
}
提示
谨慎使用隐式转换函数。通常,最好选择仅在被显式地调用时才会执行的函数。
函数
函数参数
参数类型 | 函数不需要修改实参 | 函数需要修改实参 |
---|---|---|
基本数据类型 | 值传递 | 引用或指针 |
数组 | const指针 | 指针 |
结构体 | const指针或const引用 | 引用或指针 |
类对象 | const引用或const指针 | 引用或指针 |
以上只是指导建议,实际情况很可能会有不同的选择。如对于基本类型,cin使用引用,因此可以使用cin>>n
,而不是cin>>&n
。
函数返回对象的情况
返回对象
当函数返回的对象是局部变量时,则不应该按引用方式返回,因为函数执行完后局部变量将调用析构函数,引用指向的对象将不再存在。通常,被重载的运算符会返回对象。
这种情况下,存在调用复制构造函数来创建被返回的对象的开销,但这是不可避免的。
返回const对象
意义不大。
返回const引用
返回指向const对象引用的场景:为了提高效率,函数返回的是传参的对象的引用。如比较两个对象:
c++
const Person& Max(const Person& p1, const Person& p2)
{
if (p1.age > p2.age)
return p1;
else
return p2;
}
返回非const引用
两种常见的情形:
- 重载赋值运算符,旨在提高效率。一般来说,对象都是可修改的,所以返回不会加const。
- 重载与cout一起使用的
<<
运算符。这种情形只能返回非const引用ostream &
,如果返回类型是ostream
,将要求调用ostream类的复制构造函数,而ostream类没有公有的复制构造函数。
函数重载
同一作用域内,函数名相同,但函数参数的类型或个数或顺序不同。重载的函数可以有不同的返回类型, 但是仅仅函数返回类型不同不是重载。
函数模板
函数模板使用泛型来定义函数,通过将类型作为参数传递给模板,可使编译器生成该类型的函数。
如果要将同一算法用于不同形参类型的函数,应该使用函数模板。比如有个函数交换两个值,参数类型有整型、浮点型等,如果为每种类型定义一个函数,代码就会很冗余。
c++
// 隐式实例化:常规函数模板
template <typename T> void Swap(T &a, T &b);
// 显式实例化:根据上面的函数模板生成double类型的函数定义。
// 意义不是很大,本身传入double类型的参数时编译器就会生成double类型的函数定义。
template void Swap(double &a, double &b);
template <typename T>
void Swap(T &a, T &b) {
T temp;
temp = a;
a = b;
b = temp;
}
但并非所有的类型都使用相同的算法,这时可以使用显式具体化——对特定类型,不使用函数模板来生成函数定义,而使用专门为此类型显式地定义的函数定义。
c++
struct job {
char name[40];
double salary;
int floor;
};
// 显示具体化
template <> void Swap<job>(job& j1, job& j2); // <job>可以省略
template <> void Swap(job& j1, job& j2) {
double t1;
int t2;
t1 = j1.salary;
j1.salary = j2.salary;
j2.salary = t1;
t2 = j1.floor;
j1.floor = j2.floor;
j2.floor = t2;
}
如果函数模板的参数有变化,这时可以重载函数模板。
c++
// 重载函数模板
template <typename T> void Swap(T a[], T b[], int n);
template <typename T>
void Swap(T a[], T b[], int n) {
T temp;
for (int i = 0; i < n; i++) {
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
提示
重载是因为函数参数不同,显式具体化是因为算法不同。
面向对象
特殊成员函数
构造函数
- 当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。如果有定义构造函数,又想使用默认构造函数,则必须显式定义默认构造函数。
- 默认构造函数只能有一个,而构造函数可以有多个。
- 默认构造函数可以没有参数,如果有,则必须给所有参数都提供默认值。
构造函数的初始化方式
- 初始化列表
- 构造函数体内赋值
- 使用默认参数
- 委托构造
c++
class Person
{
public:
const int id_; // const成员变量只能使用初始化列表的方式
string name_;
short age_;
double height_;
Person(int id, const string& name, short age)
: id_(id), name_(name) { // 1. 初始化列表
age_ = age;
height_ = 0.0; // 2. 构造函数体内赋值
}
Person(int id, const string& name, double height, short age=0)//3. 使用默认参数
: Person(id, name, age) { // 4. 委托构造
height_ = height;
}
};
析构函数
- 如果没有定义,编译器提供默认析构函数。
复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。其原型如下:
c++
ClassName(const ClassName &); // 声明
ClassName::ClassName(const ClassName & cls) {} // 定义
如果没有定义,编译器会提供默认复制构造函数,其行为是逐个拷贝非静态成员变量(成员复制也称为浅复制),复制的是成员的值。
什么情况下会调用复制构造函数?
- 新建一个对象并将其初始化为同类型现有对象时。c++
// 假设Person类,已有实例对象p1 Person p2(p1); Person p2 = p1; Person p2 = Person(p1); Person* ptr = new Person(p1);
- 生成对象副本时,如函数按值传递对象或函数返回对象。
赋值运算符
通过重载赋值运算符实现类对象赋值,其原型如下:
c++
ClassName& operator=(const ClassName &); // 声明
ClassName& ClassName::operator=(const ClassName & cls) // 定义
{
if (this == &cls)
{
return *this;
}
...
return *this;
}
何时会调用赋值运算符?将已有对象赋给另一个已有对象时。
如果没有定义,编译器会提供默认赋值运算符,其行为同复制构造函数。
在构造函数中使用new时要特别小心
- 构造函数中使用new初始化指针成员,则析构函数中应该使用delete。
- 如果有多个构造函数,则必须以相同方式使用new,要么都带中括号,要么都不带。因为析构函数只有一个,
new
和delete
,new[]
和delete[]
要对应。 - 应该定义一个复制构造函数,通过深拷贝将一个对象初始化为另一个对象。
- 应该重载赋值运算符,通过深拷贝将一个对象复制给另一个对象。
封装
访问修饰符 | Java | C# | C++ |
---|---|---|---|
public | 都可见 | ||
protected | 同一包内的类本身和派生类可见 | 类本身和派生类可见 | |
private | 只对类本身可见 | ||
缺省 | 同一包内可见 | 类是internal,成员是private | private |
- 类的实例对象不属于类本身。
- C#有5个访问修饰符,可指定7种可访问性级别。
- C++的访问修饰符只能修饰成员,不能修饰类;而Java和C#的修饰符能修饰类和类的成员。
继承
访问控制
C++类继承时可以指定继承的访问级别。
基类成员/继承方式 | private | protected | public |
---|---|---|---|
private | ❌ | ❌ | ❌ |
protected | private | protected | protected |
public | private | protected | public |
- 基类private成员,不管何种方式继承,派生类都无法访问。
- 基类protected成员,派生类都可以访问。
- private继承:其基类的protected成员在派生类中是private成员。
- protected或public继承:其基类的protected成员在派生类中是protected成员。
- 基类public成员,派生类都可以访问
- private继承:其基类的public成员在派生类中是private成员。
- protected继承:其基类的public成员在派生类中是protected成员。
- public继承:其基类的public成员在派生类中是public成员。
基类方法
派生类不会继承基类的哪些方法:
- 构造函数:派生类需要定义自己的构造函数。创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。派生类通常使用成员初始化列表来调用基类构造函数。
- 析构函数:释放对象时,程序首先调用派生类的析构函数,然后调用基类的析构函数。基类的析构函数应当是虚的。
- 赋值运算符:重载函数的特征标不一样。
- 友元函数:友元函数不属于类成员,因此不能被继承。友元函数是全局的,所以派生类也能直接调用友元函数,但最好是将派生类(指针或引用)强制转换为基类来调用基类的友元函数。
派生类访问基类:
- 访问基类方法:使用作用域解析运算符
::
。 - 获取基类对象本身:cpp
const Person & Student::getPerson() const { return (const Person &) *this; }
- 访问基类友元函数:c++
ostream & operator<<(ostream & os, const Student & s) { os << (const Person &) s << " " << s.teacher << endl; return os; }
- 对于保护继承和私有继承,基类的公有方法在派生类中属于保护成员和私有成员。如何让基类的公有方法在派生类外也能访问呢?
- 在派生类定义一个公有方法调用基类的公有方法。
- 在public中使用
using
声明。c++class Student : private Person { public: // 注意声明只使用成员名——没有圆括号、函数特征标和返回类型。 using Person::show; }
提示
基类指针(或引用)可以在不进行显式类型转换的情况下指向(或引用)派生类对象,但只能调用基类方法。
多重继承
多重继承带来的问题:SingingWaiter实例对象的Singer对象和Waiter对象都有Worker对象,但实际只需要一个Worker对象。由此C++引入虚基类。
虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。
c++
class Singer : public virtual Worker {...};
class Waiter : virtual public Worker {...};
class SingingWaiter : public Singer, public Waiter {...};
新的构造函数规则
假设有以下构造函数:
c++
SingingWaiter(const Worker& wk, int s=0, int w=0)
: Singer(wk, s), Waiter(wk, w) {}
为避免当wk能通过两条途径传递给Worker对象,C++在基类是虚基类时,禁止信息通过中间类自动传递给基类。上述构造函数将初始化成员s和w,但wk参数中的信息不会传递给子对象Worker,而是使用Worker类的默认构造函数。
如果不希望使用虚基类的默认构造函数,则需显式调用虚基类的构造函数:
c++
SingingWaiter(const Worker& wk, int s=0, int w=0)
: Worker(wk), Singer(wk, s), Waiter(wk, w) {}
同名函数
在上面的类图中,假设在SingingWaiter中没有定义show方法,那将调用哪个父类的show方法呢?这就会产生歧义性。如何解决?
- 在SingingWaiter类中重新定义show方法。(推荐)
- 使用作用域解析运算符:
singingWaiter.Singer::show();
这将调用Singer类的show方法。
多态
同一个方法在不同的派生类中有不同的行为。
实现方式:
- 重新定义:在派生类中重新定义基类的方法,通过不同的派生类调用方法。
- 虚函数:
- 派生类必须对基类的虚函数进行重写,重写的函数可以继续声明为虚函数。
- 必须通过基类的指针(或引用)调用虚函数,但实际给的是派生类的指针(或引用)。
对于方式1,编译器对非虚函数使用静态联编,在编译阶段根据指针(或引用)的类型,调用类型对应的函数。
对于方式2,编译器对虚函数使用动态联编,在代码执行时根据指针(或引用)所指向的类型(注意不是指针的类型),调用对应的函数。
c++
class Animal
{
public:
virtual void speak(const string& s = "...")
{
cout << "animal speak " << s << endl;
}
}
class Dog : public Animal
{
public:
void speak(const string& s = "wang")
{
cout << "dog speak " << s << endl;
}
}
class Cat : public Animal
{
public:
void speak(const string& s="miao")
{
cout << "cat speak " << s << endl;
}
}
c++
void test1()
{
Dog* dog = new Dog();
dog->speak();
Cat* cat = new Cat();
cat->speak();
}
c++
void speak(const Animal& animal)
{
animal.speak();
}
void test2()
{
Animal* animal = new Dog();
// 通过基类调用虚函数,实际调用的是派生类的虚函数;
// 但虚函数的默认参数使用的是基类的默认参数
animal->speak();
// 更常见的方式
Dog* dog = new Dog();
Cat* cat = new Cat();
speak(*dog);
speak(*cat);
}
虚函数
- 默认参数是静态绑定的(编译阶段已经确定),而虚函数是动态绑定的。所以虚函数的默认参数取决于指针或引用的类型,而不是它们所指向的对象的类型。
- 给基类提供一个虚析构函数准没错。
- 构造函数、静态函数不能是虚函数。
- 友元不能是虚函数,因为友元不是类成员。
抽象类
C++使用纯虚函数提供未实现的函数。包含有纯虚函数的类即为抽象类,不能创建抽象类的实例对象。
c++
virtual void speak() const = 0; // =0 表示纯虚函数
- 一个类继承抽象类,它必须实现抽象类的所有纯虚函数,才能称为非抽象类。
- 抽象类可以有构造函数。
友元
友元类
我的朋友由我自己声明
- 友元类的所有方法都可以访问原始类的私有成员和保护成员。
- 友元声明可以位于公有、私有或保护部分,其所在位置无关紧要。
c++
class Tv
{
private:
int state;
int channel;
public:
enum { OFF, ON };
// 声明Remote类是友元类
friend class Remote;
Tv(int s = OFF) : state(s) {}
void onOff() { state = (state == ON) ? OFF : ON; }
}
class Remote
{
public:
void onOff(Tv &t) { t.onOff(); }
void setChannel(Tv &t, int c) { t.channel = c; }
}
友元成员函数
可选择仅让特定的类成员称为另一个类的友元,而不必让整个类称为友元,但这样做要非常小心各种声明和定义的顺序,要使用前向声明。上面的代码中只有setChannel
方法直接访问了Tv
类的成员,因此可以让此方法成为友元成员函数。
c++
class Tv; // 前向声明
class Remote
{
public:
void onOff(Tv &t); // 只能声明,因为此时还不清楚Tv类的成员方法
void setChannel(Tv &t, int c);
};
class Tv
{
private:
int state;
int channel;
public:
friend void Remote::setChannel(Tv &t, int c); // 声明友元的成员函数
Tv(int s = OFF);
void onOff();
}
// 此时编译器已经知道Tv类的成员,可以定义(或在实现文件中定义)
inline void Remote::setChannel(Tv &t, int c){t.channel = c;}
c++
#include "tv.hpp"
Tv::Tv(int s) : state(s) {}
void Tv::onOff() { state = (state == ON) ? OFF : ON; }
void Remote::onOff(Tv &t) { t.onOff(); }
可以相互声明自己是对方的友元类:
c++
class Tv
{
friend class Remote;
public:
void buzz(Remote &r); // 只能声明,因为此时编译器没有足够的信息
}
class Remote
{
friend class Tv;
public:
void onOff(Tv &t) { t.onOff(); } // 可以定义
}
inline void Tv::buzz(Remote &r) { }
友元重载运算符
很多运算符可以使用成员函数或非成员函数来实现运算符重载。一般来说,非成员函数应该是友元函数。
c++
Time operator+(const int hour)
{
Time res = *this;
res.hour = res.hour + hour;
return res;
}
c++
friend Time operator+(const int hour, const Time & t); // 声明友元函数
// 定义友元函数,注意不能带friend
Time operator+(const int hour, const Time & t)
{
Time res = t;
res.hour = t.hour + hour;
return res;
}
- 对于成员函数,语句
t2 = t1 + 3
将转换为t2 = t1.operator+(3)
; - 对于友元函数,语句
t2 = 3 + t1
将转换为t2 = operator+(3, t1)
。 对成员函数来说,一个操作数通过this指针隐式传递,另一个操作数作为函数参数显式传递;而对于友元函数来说,两个操作数都作为参数显式传递。
提示
如果将非类的项作为重载运算符函数的第一个操作数,应该使用友元函数。
重载<<
运算符时需要注意返回ostream &
,以便能继续输出后续的内容:
c++
friend ostream & operator<<(ostream & os, const Time & t);
ostream & operator<<(ostream & os, const Time & t)
{
os << t.hour << ": " << t.minute << ": " << t.second;
return os;
}
Time t1(3, 4, 5);
cout << t1 << " PM\n";
// cout << t1 返回的是 ostream &,所以能继续 cout << " PM"
类模板
模板形式
c++
// 定义模板
template <typename T>
class ClassName {
private:
T m_data;
public:
ClassName(T);
}
// 实现模板
template <typename T>
ClassName<T>::ClassName(T t)
{
m_data = t;
}
// 使用模板类
ClassName<int> cn(1);
c++
template <typename T, int n>
class ClassName2 {}
c++
// 参数可带默认类型
template <typename T1, typename T2 = int>
class ClassName3 {}
模板可以作为基类,也可用作组件类,还可用作其他模板的类型参数。
提示
通常将模板的声明和定义放到同一个头文件中。
具体化
同函数模板,类模板的具体化也分为隐式实例化、显式实例化和显式具体化。
c++
// 编译器在需要对象之前,不会生成类的隐式实例化
ClassName2<int, 10> * ptr; // 指针,还不需要对象,此步骤不会生成类
ptr = new ClassName2<int, 10>; // 编译器将生成类定义,并根据该定义创建对象
c++
// 虽没有创建或提及类对象,编译器也将生成类声明(包括方法定义)
template class ClassName2<string, 10>;
c++
// 显式具体化的类模板定义,对特定类型,类模板有不同的行为(与函数模板用意相同)。
template <> class ClassName<char*> {};
c++
// 假设有一个通用的类模板定义
template <typename T1, typename T2> class TestClass {};
// 指定部分类型参数的类型,如指定T2为int类型
template <typename T1> class TestClass<T1, int> {};
成员模板
模板可以作为结构体、类或模板类的成员。
c++
template <typename T>
class Beta
{
private:
template <typename V> // 嵌套模板作为模板类的成员
class Hold
{
private:
V val;
public:
Hold(V v=0): val(v) {}
void show() const { cout << val << endl; }
V value() const { return val; }
};
Hold<T> q;
Hold<int> n;
public:
Beta(T t, int i): q(t), n(i) {}
template<typename U> // 模板方法
U blab(U u, T t) { return (q.value() + n.value()) * u / t; }
void show() const { q.show(); n.show(); }
};
c++
template<typename T>
class Beta
{
private:
template <typename V>
class Hold;
Hold<T> q;
Hold<int> n;
public:
Beta(T t, int i) : q(t), n(i) {}
template<typename U>
U blab(U u, T t);
void show() const { q.show(); n.show(); }
};
template <typename T>
template <typename V>
class Beta<T>::Hold
{
private:
V val;
public:
Hold(V v = 0) : val(v) {}
void show() const { cout << val << endl; }
V value() const { return val; }
};
template <typename T>
template <typename U>
U Beta<T>::blab(U u, T t)
{
return (n.value() + q.value()) * u / t;
}
模板作为参数
c++
template <template <typename T> typename U, typename V>
class Crab {
private:
U<int> s1;
}
// ...
Crab<ClassName, int> c1;
U
作为模板的参数,其本身也是一个类模板,类U
模板的的声明是template <typename T>
,实例对象c1中的ClassName
模板类要符合此声明。U<int>
将被替换成ClassName<int>
。
模板类和友元
- 非模板友元:在模板类中将一个常规函数声明为友元。
- 约束模板友元:友元的类型取决于类被实例化时的类型,即类模板的参数类型作为友元函数模板参数类型的一部分。
- 非约束模板友元:友元的所有具体化都是类的每一个具体化的友元,即类模板的参数类型和友元函数模板的参数类型无关。
c++
template <typename T>
class HasFriend
{
public:
HasFriend(const T& t) : item(t) { ct++; }
~HasFriend() { ct--; }
friend void counts();
friend void reports(HasFriend<T>&);
private:
T item;
static int ct;
};
template <typename T>
int HasFriend<T>::ct = 0;
void counts()
{
cout << "int count: " << HasFriend<int>::ct << ";";
cout << "double count: " << HasFriend<double>::ct << endl;
}
void reports(HasFriend<int>& hf)
{
cout << "HasFriend<int>: " << hf.item << endl;
}
void reports(HasFriend<double>& hf)
{
cout << "HasFriend<double>: " << hf.item << endl;
}
int main()
{
cout << "No Objects declared: ";
counts();
HasFriend<int> hfi1(10);
cout << "After hfi1 declared: ";
counts();
HasFriend<int> hfi2(20);
cout << "After hfi2 declared: ";
counts();
HasFriend<double> hfdb(10.5);
cout << "After hfdb declared: ";
counts();
reports(hfi1);
reports(hfi2);
reports(hfdb);
}
c++
template <typename T> void counts();
template <typename T> void reports(T&);
template <typename TT>
class HasFriend
{
public:
HasFriend(const TT& t) : item(t) { ct++; }
~HasFriend() { ct--; }
friend void counts<TT>();
friend void reports<HasFriend<TT>>(HasFriend<TT>&);
private:
TT item;
static int ct;
};
template <typename T>
int HasFriend<T>::ct = 0;
template <typename T>
void counts()
{
cout << "template size: " << sizeof(HasFriend<T>) << endl;
cout << "template counts: " << HasFriend<T>::ct << endl;
}
template <typename T>
void reports(T& hf)
{
cout << hf.item << endl;
}
int main()
{
counts<int>();
HasFriend<int> hfi1(10);
HasFriend<int> hfi2(20);
HasFriend<double> hfdb(20.5);
reports(hfi1);
reports(hfi2);
reports(hfdb);
cout << "counts<int>() output: ";
counts<int>();
cout << "counts<double>() output: ";
counts<double>();
}
c++
template <typename T>
class ManyFriend
{
private:
T item;
public:
ManyFriend(const T& t): item(t) {}
template <typename C, typename D> friend void show(C&, D&);
};
template <typename C, typename D> void show(C& c, D& d)
{
cout << c.item << ", " << d.item << endl;
}
int main()
{
ManyFriend<int> hfi1(10);
ManyFriend<int> hfi2(20);
ManyFriend<double> hfdb(20.5);
show(hfi1, hfi2);
show(hfdb, hfi2);
}
C++新特性
右值引用
- 左值和右值的区别在于能否取地址。左值能通过
&
获取地址,而右值不能。 - 左值可以出现在
=
的左边或右边,但右值只能出现在=
的右边。 - 左值包括变量、数组元素、结构体成员等等。右值包括字面常量、诸如x+y等表达式、返回值不是引用的函数等。
右值引用可关联到右值,即可出现在赋值表达式右边但不能对其应用地址运算符的值。
c++
int a = 10; // a是左值,10是右值。
int b = a;
int&& rr1 = 100; // rr1是右值引用
int&& rr2 = a + b; // rr2是右值引用
引入右值引用的主要目的之一是实现移动语义。
移动语义
Lambda
表达形式:[]() mutable -> returntype {};
语法 | 描述 |
---|---|
[] | 捕获列表:捕获外部变量供函数体使用。不可省略。[=] 按值传递方式捕获函数体内使用的外部变量;[&] 按引用传递方式捕获函数体内使用的外部变量。 |
() | 参数列表:lambda函数的参数。 |
mutable | 修饰符:允许在函数体中修改捕获的变量。 因为引用传递方式就可修改外部变量,所以基本不用。 |
-> returntype | 返回类型:lambda函数的返回类型。 |
{} | 函数体:lambda的函数体。 不可省略 |
- 如果使用了
mutable
或-> returntype
,则不能省略()
,即使没有参数。 - 按值传递捕获的变量不受外部修改的影响,函数体内还是原值;按引用传递捕获的变量外部修改会影响函数体内的变量的值。
- 虽然返回值类型编译器可以推导,但最好还是注明返回值类型。
c++
auto l2 = [] {}; // 最简形式
auto add2 = [](int x, int y) -> int {return x + y;};
cout << add2(1, 2) << endl;
auto a(10), b(20);
auto add3 = [=, &b](int z) -> int {return a + b + z;};
a = 20;
b = 5;
cout << add3(10) << endl; // 25
智能指针
智能指针类型 | 描述 |
---|---|
auto_ptr | C++98定义的智能指针模板,C++11后被淘汰了。 |
unique_ptr | 独占所有权的智能指针,同一内存只能有一个 unique_ptr 指针。 |
shared_ptr | 共享所有权的智能指针,多个 shared_ptr 可以指向同一内存。 采用引用计数,当复制或拷贝时,计数+1;当析构时计数-1,计数为0则释放内存。 |
weak_ptr | 弱引用智能指针,用于与 shared_ptr 配合使用,避免循环引用导致的内存泄漏。 |
初始化
注:xxx_ptr
表示 unique_ptr
或 shared_ptr
。
c++
// 初始化一个可以指向T类型的空的智能指针
xxx_ptr<T> ptr1;
// 初始化一个指向T类型实例的智能指针
xxx_ptr<T> ptr2(new T());
// 初始化一个可以指向T类型数组的空的智能指针
xxx_ptr<T[]> ptr3;
// 初始化一个指向T类型数组实例的智能指针
xxx_ptr<T[]> ptr4(new T[]);
// 初始化一个可以指向T类型实例的智能指针,并使用自定义删除器来释放内存
unique_ptr<T, D> ptr5;
// 虽然编译器不会报错,但ptr6无法通过赋值或reset接管对象(运行时异常退出)。
// 要么直接用ptr8的方式,要么就用ptr1的方式并使用ptr1.reset(new T(), D());
shared_ptr<T> ptr6(nullptr, D());
// 初始化一个指向T类型实例的智能指针,并使用自定义删除器来释放内存
unique_ptr<T, D> ptr7(new T());
shared_ptr<T> ptr8(new T(), D());
// make_shared 会同时分配控制块(用于引用计数)和对象本身,
// 相比于 new 方式初始化,少一次内存分配次数,更加简洁高效(推荐)
shared_ptr<T> ptr9 = make_shared<T>(args);
unique_ptr
API | 描述 |
---|---|
get() | 获取接管对象的指针。 |
reset(...args) | 销毁当前对象(如果存在),并接管新的对象(如果提供)或重置为空指针。 |
release() | 仅释放接管对象的控制权,不会销毁对象。 |
- 不支持赋值和复制操作,需要使用
std::move()
进行移动操作。
shared_ptr
API | 描述 |
---|---|
get() | 获取接管对象的指针。 |
reset(...args) | 当前对象引用计数-1(如果存在),并接管新的对象(如果提供,新对象引用计数+1)或重置为空指针。可指定自定义deleter |
use_count() | 获取当前接管对象的引用计数。空指针调用返回为0 |
swap() | 交换接管的对象,原对象的引用计数不变 |
weak_ptr
std::weak_ptr
不能直接构造,必须从一个shared_ptr
或另一个weak_ptr
构造。它的构造和析构不会引起引用计数的增加或减少。
API | 描述 |
---|---|
lock() | 返回接管对象的shared_ptr (引用计数+1),如果接管对象已被释放,则返回空的shared_ptr |
reset() | 重置为空指针,注意引用计数不变 |
use_count() | 获取当前接管对象的shared_ptr 指针的引用计数。 |
swap() | 交换接管的对象,原对象的引用计数不变 |
expired() | 判断当前weak_ptr 是否还有托管的对象,有则返回false,无则返回true |
- 不支持
*
和->
对指针的访问
Example:
c++
#include <iostream>
#include <memory>
using namespace std;
class Person
{
private:
std::string _name;
public:
Person() {
std::cout << "调用Person的构造函数" << std::endl;
_name = "Unknown";
}
Person(string name) {
std::cout << "调用Person的带参构造函数: " << name << std::endl;
_name = name;
}
~Person() { std::cout << "调用Person的析构函数" << std::endl; }
void doSomething() const { std::cout << _name << " doSomething...\n"; }
};
class PersonDeleter
{
public:
void operator()(Person* pt) {
pt->doSomething();
delete pt;
}
};
class Girl;
class Boy
{
private:
weak_ptr<Girl> _girlFriend;
public:
Boy()
{
cout << "Boy构造函数\n";
}
virtual ~Boy()
{
cout << "~Boy析构函数\n";
}
void setGirlFriend(shared_ptr<Girl> girlFriend)
{
cout << "In setGirlFriend a: " << girlFriend.use_count() << endl; // 2
// 赋值给weak_ptr不会让引用计数+1
// 这里是因为girlFriend作为值传递方式传参而使计数+1
// 当函数执行完之后,girlFriend会销毁使计数-1
_girlFriend = girlFriend;
cout << "In setGirlFriend b: " << _girlFriend.use_count() << endl; // 2
// 获取共享指针
shared_ptr<Girl> spGril =_girlFriend.lock();
cout << "In setGirlFriend c: " << spGril.use_count() << endl; // 3
spGril = nullptr;
cout << "In setGirlFriend d: " << _girlFriend.use_count() << endl; // 2
}
void resetGirlFriend()
{
if (!_girlFriend.expired())
{
_girlFriend.reset();
cout << "分手成功!\n";
}
else
cout << "已经没有女朋友了!\n";
}
};
class Girl {
private:
shared_ptr<Boy> _boyFriend;
public:
Girl() {
cout << "Girl构造函数" << endl;
}
virtual ~Girl() {
cout << "~Girl析构函数" << endl;
}
void setBoyFriend(shared_ptr<Boy> boyFriend) {
_boyFriend = boyFriend;
}
};
c++
#include "smart_ptr.hpp"
void test_unique_ptr()
{
std::cout << "===================Test unique_ptr==========================\n";
std::unique_ptr<string> ptr_string(new string("zhangsan"));
std::unique_ptr<int> ptr_int(new int(100));
{
Person* person = new Person();
std::unique_ptr<Person> p2;
p2.reset(person); // 接管person实例的内存空间
std::unique_ptr<Person> p3;
p3 = std::move(p2); // 使用move把左值变为右值后就可以赋值了,p2被置为空
p3.reset(); // 等同于 p3 = nullptr;
// auto p = p3.release(); // 仅释放托管对象的控制权,不销毁对象。返回对象指针
// delete p; // 如果使用release(),则需要手动释放内存
}
std::cout << "\n*****测试自定义的 deleter ***** \n";
{
Person* person2 = new Person("Tom");
std::unique_ptr<Person, PersonDeleter> p4(person2);
}
}
c++
#include "smart_ptr.hpp"
void test_shared_ptr()
{
std::cout << "===================Test shared_ptr==========================\n";
std::shared_ptr<int> ptr_int = make_shared<int>(100);
{
std::shared_ptr<Person> p1 = make_shared<Person>("Tom");
std::shared_ptr<Person> p2;
std::cout << "p2.use_count() = " << p2.use_count() << std::endl;
p2 = p1;
std::cout << "count: " << p1.use_count() << " " << p2.use_count() << std::endl;
// 2 2
p2.reset(); // 当前接管对象的引用计数-1,并将p2置为空指针。等同于 p2 = nullptr;
std::cout << "count: " << p1.use_count() << " " << p2.use_count() << std::endl;
// 1 0
}
{
Person* person = new Person("Lucy");
std::shared_ptr<Person> p3(person, PersonDeleter());
std::shared_ptr<Person> p4(p3); // 等同于 p4 = p3;
std::cout << "count: " << p3.use_count() << " " << p4.use_count() << std::endl;
p4 = nullptr; // p4没有使用自定义deleter
std::cout << "count: " << p3.use_count() << " " << p4.use_count() << std::endl;
}
{
Person* person = new Person("Lucy2");
// 用下面的方式初始化p5,在运行p5.reset时会异常退出
//std::shared_ptr<Person> p5(nullptr, PersonDeleter());
std::shared_ptr<Person> p5;
p5.reset(person, PersonDeleter());
std::cout << "p5 count: " << p5.use_count() << std::endl;
p5.reset(); // 使用自定义deleter
std::cout << "p5 count: " << p5.use_count() << std::endl;
}
}
c++
#include "smart_ptr.hpp"
void test_weak_ptr()
{
cout << "===================Test weak_ptr==========================\n";
shared_ptr<Boy> spBoy(new Boy());
shared_ptr<Girl> spGirl(new Girl());
spGirl->setBoyFriend(spBoy);
cout << "Before setGirlFriend: " << spGirl.use_count() << endl; // 1
spBoy->setGirlFriend(spGirl);
cout << "After setGirlFriend: " << spGirl.use_count() << endl; // 1
spBoy->resetGirlFriend();
cout << spGirl.use_count() << endl; // 1
}
使用指南
- 如果程序需要使用多个指向同一个对象的指针,应该使用
shared_ptr
;如果有循环引用的情况,则使用weak_ptr
。 unique_ptr
可以转化为shared_ptr
,但反之则不行。- 不要把原生对象的指针托管给多个智能指针,这非常危险!c++
int* x = new int(10); shared_ptr<int> p1(x); // 或使用p1.reset(x)的形式; shared_ptr<int> p2(x); p1 = nullptr; // p1所接管的指针所指向的内存空间会被释放。 // p2 所接管的指针将变成悬挂指针。
- 如果使用了release(),记得手动释放返回的指针。
- 不要delete智能指针get()返回的指针。既然用上了智能指针,那就让代码“智能”一点。
- 智能指针作为函数参数或返回值的情况
函数 unique_ptr shared_ptr 返回值 所有权转让给接收返回值的unique_ptr 复制行为,引用计数+1,但函数执行完后会自动销毁,引用计数-1 参数:值传递 所有权转让给函数的局部变量参数,函数执行完后会销毁(谨慎) 同上 参数:引用传递 所有权仍归函数外的实参,函数执行完后不会销毁 计数不变 c++运行结果如下:static unique_ptr<Person> func1(string name) { return unique_ptr<Person>(new Person(name)); } static void func2(unique_ptr<Person> up) { up->doSomething(); } static void func3(unique_ptr<Person>& up) { up->doSomething(); } void test_func_unique_ptr() { auto p5 = func1("P5"); func3(p5); Person* person3 = new Person("P6"); unique_ptr<Person> p6(person3); func2(std::move(p6)); // 函数执行完后,person3的内存会被释放 cout << "After func2\n"; }