Appearance
C++入门指南
更新: 4/19/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; // 第二个元素的值
字符数组
字符数组是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++的结构体和类基本相同,唯一的区别是默认的访问权限:结构体成员和继承的默认访问权限都是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变量默认是extern。要使const变量能够在其他文件中访问,必须在文件中显式地指定它为extern。
c++
// 不指定extern,则只能在当前文件内访问
extern const int BUF_SIZE = 100;
int main() {
const int n = 10; // const变量,不能修改,且必须初始化
}
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与函数
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
}
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
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; }
explicit
- 修饰构造函数时,可以防止隐式转换和复制初始化。
- 修饰转换函数时,可以防止隐式转换,但按语境转换除外。
函数
函数参数
参数类型 | 函数不需要修改实参 | 函数需要修改实参 |
---|---|---|
基本数据类型 | 值传递 | 指针(推荐)或引用 |
数组 | const指针 | 指针 |
结构体 | const指针或const引用 | 引用或指针 |
类对象 | const引用(推荐)或const指针 | 引用(推荐)或指针 |
以上只是指导建议,实际情况很可能会有不同的选择。如对于基本类型,cin使用引用,因此可以使用cin>>n
,而不是cin>>&n
。
函数重载
同一作用域内,函数名相同,但函数参数的类型或个数或顺序不同。重载的函数可以有不同的返回类型, 但是仅仅函数返回类型不同不是重载。
函数模板
函数模板使用泛型来定义函数,通过将类型作为参数传递给模板,可使编译器生成该类型的函数。
如果要将同一算法用于不同形参类型的函数,应该使用函数模板。比如有个函数交换两个值,参数类型有整型、浮点型等,如果为每种类型定义一个函数,代码就会很冗余。
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++
ClassName(const ClassName &);
如果没有定义,编译器会提供默认复制构造函数,其行为是逐个拷贝非静态成员变量(成员复制也称为浅复制),复制的是成员的值。
什么情况下会调用复制构造函数?
- 新建一个对象并将其初始化为同类型现有对象时。c++
// 假设Person类,已有实例对象p1 Person p2(p1); Person p2 = p1; Person p2 = Person(p1); Person* ptr = new Person(p1);
- 生成对象副本时,如函数按值传递对象或函数返回对象。
赋值运算符
通过重载赋值运算符实现类对象赋值,其原型如下:
c++
ClassName & ClassName::operator=(const ClassName &);
何时会调用赋值运算符?将已有对象赋给另一个对象时。
如果没有定义,编译器会提供默认赋值运算符,其行为同复制构造函数。
在构造函数中使用new时要特别小心
- 构造函数中使用new初始化指针成员,则析构函数中应该使用delete。
- 如果有多个构造函数,则必须以相同方式使用new,要么都带中括号,要么都不带。因为析构函数只有一个,
new
和delete
,new[]
和delete[]
要对应。 - 应该定义一个复制构造函数,通过深拷贝将一个对象初始化为另一个对象。
- 应该重载赋值运算符,通过深拷贝将一个对象复制给另一个对象。
函数返回对象的情况
返回对象
当函数返回的对象是局部变量时,则不应该按引用方式返回,因为函数执行完后局部变量将调用析构函数,引用指向的对象将不再存在。通常,被重载的运算符会返回对象。
这种情况下,存在调用复制构造函数来创建被返回的对象的开销,但这是不可避免的。
返回const对象
意义不大。
返回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类没有公有的复制构造函数。
封装
访问修饰符 | 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成员。
基类方法
- 派生类继承了所有的基类方法,但以下除外:
- 基类的构造函数、析构函数、复制构造函数、赋值运算符
- 基类的重载运算符
- 基类的友元函数
- 派生类需要定义自己的构造函数。创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。
- 基类指针(或引用)可以在不进行显式类型转换的情况下指向(或引用)派生类对象,但只能调用基类方法。
多态
同一个方法在不同的派生类中有不同的行为。
实现方式:
- 在派生类中重新定义基类的方法,通过不同的派生类调用方法。
- 使用虚函数,但有前提条件:
- 派生类必须对基类的虚函数进行重写,重写的函数可以继续声明为虚函数。
- 必须通过基类的指针(或引用)调用虚函数,但实际给的是派生类的指针(或引用)。
对于方式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);
}
虚函数
- 默认参数是静态绑定的(编译阶段已经确定),而虚函数是动态绑定的。所以虚函数的默认参数取决于指针或引用的类型,而不是它们所指向的对象的类型。
- 给基类提供一个虚析构函数准没错。
- 构造函数、静态函数不能是虚函数。
- 友元不能是虚函数,因为友元不是类成员。
提示
对于Java和C#,它们没有虚函数的概念,通过重写实现多态。
抽象基类
C++使用纯虚函数提供未实现的函数。包含有纯虚函数的类即为抽象基类,不能创建抽象基类的实例对象。
c++
virtual void speak() const = 0; // =0 表示纯虚函数
- 一个类继承抽象基类,它必须实现抽象基类的所有纯虚函数,才能称为非抽象类。
- 抽象基类可以有构造函数。
类模板
友元
接口
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"; }