自由存储
单个对象的分配和释放
int* ptr = new int; // 分配一个int类型的内存空间,并初始化为0
*ptr = 10; // 赋值
delete ptr; // 释放内存
数组的分配和释放
int* arr = new int[10]; // 分配一个int类型数组,包含10个元素
for (int i = 0; i < 10; ++i) {
arr[i] = i;
}
delete[] arr; // 释放数组内存
使用 new
初始化对象
std::string* str = new std::string("Hello, World!"); // 分配并初始化一个std::string对象
delete str; // 释放内存
禁止抛出异常
int* ptr = new (std::nothrow) int; // 使用nothrow选项,如果内存不足,不会抛出异常,而是返回nullptr
if (ptr == nullptr) {
std::cerr << "Memory allocation failed" << std::endl;
} else {
delete ptr; // 释放内存
}
放置在特定空间
char* buffer = new char[sizeof(MyClass)]; // 分配内存
MyClass* obj = new (buffer) MyClass(42); // 在已分配的内存上构造对象
// 使用obj...
obj->~MyClass(); // 显式调用析构函数
delete[] buffer; // 释放内存
重载 new 和 delete
#include <iostream>
#include <cstdlib>
// 全局变量用于跟踪内存分配
int allocations = 0;
// 重载 new
void* operator new(std::size_t size) {
std::cout << "Custom new called, requesting " << size << " bytes.\n";
allocations++;
return std::malloc(size);
}
// 重载 delete
void operator delete(void* ptr) noexcept {
std::cout << "Custom delete called.\n";
allocations--;
std::free(ptr);
}
// 重载 delete
void operator delete(void* ptr, std::size_t size) noexcept {
std::cout << "Custom delete called with size " << size << ".\n";
allocations--;
std::free(ptr);
}
// 重载 new[]
void* operator new[](std::size_t size) {
std::cout << "Custom new[] called, requesting " << size << " bytes.\n";
allocations++;
return std::malloc(size);
}
// 重载 delete[]
void operator delete[](void* ptr) noexcept {
std::cout << "Custom delete[] called.\n";
allocations--;
std::free(ptr);
}
// 重载 delete[]
void operator delete[](void* ptr, std::size_t size) noexcept {
std::cout << "Custom delete[] called with size " << size << ".\n";
allocations--;
std::free(ptr);
}
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called.\n";
}
~MyClass() {
std::cout << "MyClass destructor called.\n";
}
};
int main() {
std::cout << "Before allocation: " << allocations << " allocations.\n";
MyClass* obj = new MyClass(); // 调用重载的 new
std::cout << "After single object allocation: " << allocations << " allocations.\n";
MyClass* arr = new MyClass[5]; // 调用重载的 new[]
std::cout << "After array allocation: " << allocations << " allocations.\n";
delete obj; // 调用重载的 delete
std::cout << "After single object deletion: " << allocations << " allocations.\n";
delete[] arr; // 调用重载的 delete[]
std::cout << "After array deletion: " << allocations << " allocations.\n";
return 0;
}
重载了两个 delete 版本,这样才不会报警告,可在在线编译器尝试
列表
我们能用 {}
列表初始化命名变量,此外,在很多(但并非所有)地方 {}
列表还能作为表达式出现。它们的表现形式有两种:
- 限定为某种类型,形如
T{......}
,意思是“创建一个T
类型的对象并用T{......}
初始化它” - 未限定的
{......}
,其类型根据上下文确定
实现模型
{}
列表的实现模型由三部分组成:
-
如果
{}
列表被用作构造函数的实参,则其实现过程与使用()
列表类似。除非列表的元素以传值的方式传给构造函数,否则我们不会拷贝列表的元素。 -
如果
{}
列表被用于初始化一个聚合体(一个数组或者一个未提供构造函数的类)的元素,则列表的每个元素分别初始化聚合体中的一个元素。除非列表的元素以传值的方式传给聚合体元素的构造函数,否则我们不会拷贝列表的元素。 -
如果
{}
列表被用于构建一个initializer_list
对象,则列表的每个元素分别初始化initializer_list
的底层数组(underlying array)的一个元素。通常情况下,我们把元素从initializer_list
拷贝到实际使用它们的地方。#include <initializer_list> class Example { public: Example(std::initializer_list<int> list) { for (int value : list) { // 处理列表中的每个元素 } } }; int main() { Example obj{ 1, 2, 3 }; // 使用{}列表初始化对象 return 0; }
initializer_list
的底层数组是不可修改的,这意味着接受列表元素的容器必须使用拷贝操作,不能使用移动操作。
std::initializer_list<int> lst {1, 2, 3, 4, 5};
std::cout << *lst.begin() << std::endl;
*lst.begin() = 10; // 不OK,不能修改初始化列表中的元素
std::cout << *lst.begin() << std::endl;
限定列表
struct S {
int a;
int b;
};
S v{ 7,8 }; // 直接初始化一个变量
v = S{ 7,8 }; // 用限定列表进行赋值
S* p = new S{ 7,8 }; // 使用限定列表在自由存储上构建对象
如果某个限定列表中只含有一个元素,则其含义基本上等同于把该元素转换成另外一种类型。
未限定列表
当我们明确知道所用类型时,可以使用未限定列表。它只能被用作一条表达式,并且仅限于以下场景:
- 函数实参
- 返回值
- 赋值运算符
=
的右侧运算对象 - 下标
#include <utility>
void g(double d) {}
struct Matrix {
int operator[](std::pair<int, int> p) const { return 0; }
};
int f(double d, Matrix& m)
{
int v{ 7 }; // 初始化器(直接初始化)
int v2 = { 7 }; // 初始化器(拷贝初始化)
int v3 = m[{2, 3}]; // 假设 m接受一个值对作为其下标
v = { 8 }; // 赋值运算的右侧运算对象
v += { 9 }; // 错误:不能参数与算数运算
v = { 9 } + 10; // 错误:不能参数与算数运算
{ v } = 9; // 错误:不能作为赋值运算的左侧运算对象
g({ 10.0 }); // 函数实
return{ 11 }; // 返回值
}
标准库类型 initializer_list<T>
用于处理长度可变的 {}
列表。我们常把它用于用户自定义容器的初始化器列表,但是除此之外也可以直接使用它。
{}
列表是处理同质、变长列表的最简单的方法,但是注意 0 个元素的情况是个例外。此时,我们应该使用默认的构造函数。
只有当 {}
列表的所有元素类型相同时,我们才能推断该列表的类型。
#include<initializer_list>
int main() {
auto x0 = {}; //错误(缺少元素类型)
auto x1 = { 1 }; // initializer_list<int>
auto x2 = { 1,2 }; // initializer_list<int>
auto x3 = { 1,2,3 }; // initializer_list<int>
auto x4 = { 1,2.0 }; //错误:元素类型不相同
}
我们无法通过推断未限定列表的类型使其作为普通模板的实参。
template<typename T>
void f(T t){}
int main() {
f({}); //错误:初始化器的类型未知
f({ 1 }); //错误:未限定的列表与“普通的T”不匹配
f({ 1,2 }); //错误:未限定的列表与“普通的T”不匹配
f({ 1,2,3 }); //错误:未限定的列表与“普通的T”不匹配
}
lambda 表达式
lambda 表达式(lambda expression)有时也称为 lambda 函数(lambda function),它是定义和使用匿名函数对象的一种简便的方式。
一条 lambda 表达式包含以下组成要件:
- 一个可能为空的捕获列表(capture list),指明定义环境中的哪些名字能被用在 lambda 表达式内,以及这些名字的访问方式是拷贝还是引用。捕获列表位于
[]
内。 - 一个可选的参数列表(parameter list),指明 lambda 表达式所需的参数。参数列表位于
()
内。 - 一个可选的
mutable
修饰符,指明该 lambda 表达式可能会修改它自身的状态(即,改变通过值捕获的变量的副本)。 - 一个可选的
noexcept
修饰符。 - 一个可选的
->
形式的返回类型声明。 - 一个表达式体(body),指明要执行的代码,表达式体位于
{}
内。
在 lambda 的概念中,传参、返回结果以及定义表达式体等环节都与函数的相应概念是一致的。区别在于函数没有提供局部变量“捕获”的功能,这意味着 lambda 可以作为局部函数使用,而普通函数不能。
实现模型
Lambda 表达式是一种在现代编程语言中定义匿名函数的方式,它们可以被视为定义并使用函数对象的便捷方法。这种表达式允许在不显式定义函数的情况下创建函数对象,从而简化代码。
考虑一个 print_modulo
函数,其功能是检查一个整数向量 v
中的每个元素是否能被整数 m
整除,如果能,则将该元素输出到输出流 os
。以下是使用 lambda 表达式实现的代码:
void print_modulo(const vector<int>& v, ostream& os, int m) {
for_each(begin(v), end(v), [&](int x) {
if (x % m == 0) os << x << '\n';
});
}
在这个例子中,for_each
算法应用于向量 v
,对每个元素执行一个 lambda 表达式。如果元素 x
能被 m
整除,则将其输出到 os
。
为了更好地理解 lambda 表达式的工作原理,我们可以定义一个等价的函数对象 Modulo_print
:
class Modulo_print {
ostream& os; // 引用,用于输出
int m; // 值拷贝,用于整除检查
public:
Modulo_print(ostream& s, int mm) : os(s), m(mm) {} // 构造函数
void operator()(int x) const { // 函数调用运算符
if (x % m == 0) os << x << '\n';
}
};
这个类有两个成员变量 os
和 m
,分别对应于 lambda 表达式中的捕获列表 [&os, m]
。os
前的 &
表示它是引用,而 m
前的没有 &
表示它是值拷贝。
由 lambda 表达式生成的类的对象称为闭包对象(closure object) 。如果 lambda 通过引用捕获其局部变量,则闭包对象可以优化为仅包含一个指向外层栈框架的指针。
使用 Modulo_print
类,我们可以重写 print_modulo
函数如下:
void print_modulo(const vector<int>& v, ostream& os, int m) {
for_each(begin(v), end(v), Modulo_print{os, m});
}
在这个重写版本中,我们创建了一个 Modulo_print
对象,并将其传递给 for_each
算法,该对象将对向量 v
中的每个元素执行相同的操作。
捕获
Lambda 表达式的第一个字符总是 [
,后面跟着的是捕获列表,它定义了 Lambda 如何捕获外部作用域中的变量。以下是几种常见的捕获列表:
-
空捕获列表(
[]
):不捕获任何外部变量,Lambda 内部无法使用其外层上下文中的任何局部名字。sort(v.begin(), v.end(), [](int x, int y) { return abs(x) < abs(y); });
-
通过引用隐式捕获(
[&]
):所有局部变量都通过引用访问。int a = 10; auto lambda = [&]() { cout << a; };
-
通过值隐式捕获(
[=]
):所有局部变量都通过值访问,即捕获局部变量的副本。int a = 10; auto lambda = [&]() { cout << a; }; // a的副本被捕获
-
显式捕获(
[捕获列表]
):通过值或引用的方式捕获指定的局部变量。int a = 10, b = 20; auto lambda = [a, &b] { cout << a << " " << b; }; // a通过值捕获,b通过引用捕获
-
[&, 捕获列表]
:对于未在捕获列表中出现的局部变量,通过引用隐式捕获;列表中的变量通过值捕获,列出的名字不能以&
为前缀。int a = 10, b = 20; auto lambda = [&, a] { cout << a << " " << b; }; // a通过值捕获,b通过引用捕获
-
[=, 捕获列表]
:对于未在捕获列表中出现的局部变量,通过值隐式捕获;列表中的变量通过引用捕获,列出的名字必须以&
为前缀。int a = 10, b = 20; auto lambda = [=, &b] { cout << a << " " << b; }; // a通过值捕获,b通过引用捕获
捕获的考虑因素:
- 值捕获:适用于小对象或不需要修改外部变量的情况。
- 引用捕获:适用于需要修改外部变量或对象较大的情况。
- 线程安全:当 Lambda 被传递到其他线程时,通过值捕获(
[=]
)通常更安全,因为通过引用或指针访问其他线程的栈内存是不安全的。
Lambda 与生命周期
Lambda 表达式的生命周期可能比其调用者的生命周期更长,这可能导致潜在的问题。当 Lambda 表达式被传递给另一个线程或者被调用者存储起来以供后续使用时,这种情况尤其明显。
void setup(Menu& m) {
Point p1, p2, p3;
// ...计算p1, p2和p3的位置...
m.add("draw triangle", [&]{ m.draw(p1, p2, p3); });
}
在这个例子中,setup
函数计算三个点 p1
, p2
, p3
的位置,并将一个 Lambda 表达式添加到菜单 m
中。这个 Lambda 表达式通过引用捕获了局部变量 p1
, p2
, p3
。问题在于,当用户在几分钟后点击“draw triangle”按钮时,Lambda 表达式可能会试图访问这些局部变量,而它们可能已经超出了作用域,导致未定义行为。
为了避免这种情况,我们需要确保 Lambda 表达式中使用的局部变量的生命周期至少与 Lambda 表达式一样长。这可以通过值捕获([=]
)来实现,这样 Lambda 表达式就会复制这些变量,而不是引用它们。修改后的代码如下:
void setup(Menu& m) {
Point p1, p2, p3;
// ...计算p1, p2和p3的位置...
m.add("draw triangle", [=]{ m.draw(p1, p2, p3); });
}
名字空间名字
当在 Lambda 表达式中使用命名空间变量或全局变量时,由于它们总是可访问的,我们不需要在 Lambda 的捕获列表中显式捕获它们。
#include <iostream>
#include <map>
#include <utility>
#include <algorithm>
// 重载输出运算符,以便输出pair对象
template<typename U, typename V>
std::ostream& operator<<(std::ostream& os, const std::pair<U, V>& p) {
return os << '{' << p.first << ',' << p.second << '}';
}
// 打印map对象的所有元素
void print_all(const std::map<std::string, int>& m, const std::string& label) {
std::cout << label << ":\n{\n";
std::for_each(m.begin(), m.end(), [](const std::pair<std::string, int>& p) {
std::cout << p << '\n';
});
std::cout << "}\n";
}
int main() {
std::map<std::string, int> m;
m["one"] = 1;
m["two"] = 2;
m["three"] = 3;
print_all(m, "Initial map");
}
lambda 与 this
在类的成员函数中使用 Lambda 表达式时,可以通过将 this
添加到捕获列表中来访问类的成员。这种方式允许 Lambda 直接通过 this
访问和操作类的成员,而无需复制。
然而,需注意 [this]
和 [=]
互不兼容,此稍有不慎就可能在多线程程序中产生竞争条件。
#include <functional>
#include <map>
#include <string>
class Request {
using function = std::function<std::map<std::string, std::string>(const std::map<std::string, std::string>&)>; // 操作函数类型定义
std::map<std::string, std::string> values; // 参数
std::map<std::string, std::string> results; // 目标结果
function oper; // 操作
public:
Request(const std::string& s); // 解析并保存请求
void execute() {
// 使用this捕获列表
[this]() { results = oper(values); }(); // 根据结果执行相应的操作
}
};
mutable
的 lambda
通常情况下,人们不希望修改函数对象(闭包)的状态,因此默认设置为不可修改。换句话说,生成的函数对象的 operator()()
是一个 const
成员函数。只有在极少数情况下,如果我们确实希望修改状态(注意,不是修改通过引用捕获的变量的状态),则可以把 lambda
声明成 mutable
的。
#include <vector>
#include <algorithm>
#include <iostream>
void algo(std::vector<int>& v) {
int count = v.size();
// 使用mutable关键字允许修改闭包状态
std::generate(v.begin(), v.end(), [count]() mutable {
return --count;
});
}
int main() {
std::vector<int> vec(10);
algo(vec);
// 输出vec中的元素
for (int num : vec) {
std::cout << num << " ";
}
return 0;
}
每次调用 Lambda 表达式时,--count
都会递减 count
的值,并将这个递减后的值作为结果返回,从而生成一个递减的整数序列填充到向量中。
可以理解为,lambda 表达式是一个函数对象,这是在捕获的值是其成员变量。
默认情况下不允许修改按值传递进来的成员变量,可通过声明为
mutable
来绕开限制。这里相当于修改这个对象的成员变量,并且下次在调用这个对象时,其内部成员变量是上次调用后修改的结果。
所谓的闭包对象就是指的这个函数对象,闭包就是指把变量捕获进去(封装进去)了。
调用与返回
Lambda 表达式大多数规则是从函数和类借鉴而来,有两点需要注意:
-
参数列表的省略:如果 Lambda 表达式不接受任何参数,其参数列表可以省略。Lambda 表达式的最简形式是
[]{}
。 -
返回类型的推断:Lambda 表达式的返回类型可以由表达式本身推断得到,而普通函数则不能。
-
如果 Lambda 的主体部分不包含
return
语句,则返回类型为void
。auto g = [&](){ f(y); };
-
如果 Lambda 的主体部分有
return
语句,且类型相同,则返回类型为该return
表达式的类型。auto z1 = [y](int x) { if (y) return x; else return 2; };
-
在其他情况下,需要显式提供一个返回类型。
auto z2 = [y]() { if (y) return 1.0; else return 2; }; // 后面推断为int,前面推断是double auto z3 = [y]()->int { if (y) return 1.0; else return 2; };
-
lambda 的类型
Lambda 表达式是一种特殊的函数对象,其类型被称为闭包类型(closure type)。每个 lambda 表达式都有一个唯一的类型,这意味着任意两个 lambda 表达式都不会有相同的类型。这种类型的唯一性对于模板实例化机制来说非常重要,因为它允许区分不同的 lambda 表达式。
Lambda 表达式可以作为参数传递,也可以用于初始化声明为 auto
或者 std::function<R(AL)>
类型的变量。这里 R
代表 lambda 的返回类型,AL
代表它的参数类型列表。
递归使用的 Lambda
以下是一个尝试递归反转 C 风格字符串中字符的 lambda 表达式的例子:
#include <iostream>
#include <string>
#include <functional>
void f1() {
// 错误:rev在推断出类型之前被使用
auto rev = [&rev](char* b, char* e) {
if (1 < e - b) {
std::swap(*b, *--e);
rev(++b, e);
}
};
}
void f2() {
std::function<void(char* b, char* e)> rev = [&](char* b, char* e) {
if (1 < e - b) {
std::swap(*b, *--e);
rev(++b, e);
}
};
rev(&s1[0], &s1[0] + s1.size());
rev(&s2[0], &s2[0] + s2.size());
}
由于 auto
类型变量的类型在它被使用之前无法被推断出来,所以 f1
错误。
为了解决这个问题,我们需要先给 lambda 表达式一个名字,然后再使用它,如 f2
所示。
不捕获外部变量的 lambda
如果一个 lambda 表达式不捕获任何外部变量,我们可以将它赋值给一个指向正确类型函数的指针:
double (*p1)(double) = [](double a) { return std::sqrt(a); };
double (*p2)(double) = [&](double a) { return std::sqrt(a); }; // 错误:lambda捕获了内容
double (*p3)(double) = [](int a) { return std::sqrt(a); }; // 错误:参数类型不匹配
显式类型转换
C++ 中主要的显式类型转换方法及其特点:
-
构造:使用花括号
{}
提供对新值类型安全的构造。-
执行非窄化转换:
-
int
->double
:不允许 -
double
->int
:不允许 -
short
->int
:允许
-
-
-
命名的转换:提供不同等级的类型转换。
-
const_cast
:对某些声明为const
的对象获得写入的权利。- 仅在 const 修饰符和 volatile 修饰符上有所区别的类型转换。
-
static_cast
:反转一个定义良好的隐式类型转换。- 执行关联类型之间的转换,如指针类型在同一类层次中的转换,整数到枚举类型的转换,或浮点类型到整数类型的转换。
- 它还能执行由构造函数和转换运算符定义的类型转换。
-
reinterpret_cast
:改变位模式的含义。-
通常情况下它产生的新类型的值与它的实参具有相同的位模式
-
用于非关联类型之间的转换,如整数到指针的转换。这种转换非常危险,因为它不进行类型检查,完全依赖于程序员的判断。
IO_device* d1 = reinterpret_cast<IO_device*>(0Xff00); // 0Xff00处的设备
-
-
dynamic_cast
:动态地检查类层次关系。- 执行指针或引用向类层次体系的类型转换,并进行运行时检查。
-
char x = 'a'; int* p1 = &x; // 错误:不存在char*向int*的隐式类型转换 int* p2 = static_cast<int*>(&x); // 错误:不存在char*向int*的隐式类型转换 int* p3 = reinterpret_cast<int*>(&x); // OK:责任自负 struct B { /*...*/ }; struct D : B { /*...*/ }; // D 是 B 的公有派生类 B* pb = new D; // OK: D* 向 B* 的隐式类型转换 D* pd = pb; // 错误:不存在 B* 到 D* 的隐式类型转换 D* pd2 = static_cast<D*>(pb); // OK
-
-
C 风格的转换:提供命名的类型转换或其组合。
-
C++ 继承了 C 语言中的 C 风格类型转换,这种转换使用符号
(T)e
来实现,其中T
是目标类型,e
是表达式。C 风格类型转换可以执行static_cast
、reinterpret_cast
和const_cast
任意组合之后得到的类型转换,其目的是从表达式e
得到类型为T
的值。基本的 C 风格类型转换:
int i = 10; double d = (double)i; // 将int转换为double
指针类型转换
void* voidPtr = &i; int* intPtr = (int*)voidPtr; // 将void*转换为int*
移除 const 修饰符(不推荐,因为这违反了 const 的正确使用):
const int* ci = &i; int* modifiablePtr = (int*)ci; // 错误地移除了const修饰符
-
C 风格类型转换比 C++ 的命名转换运算符更危险,因为它难以在大型程序中定位,且不易理解程序员的真实意图。
-
C 风格类型转换允许将类的指针转换为指向该类私有基类的指针,这是不安全的,应该避免这样做。
class Base {}; class Derived : private Base {}; Derived d; Base* basePtr = (Base*)&d; // 错误:将Derived*转换为Base*,但Derived是私有继承自Base
-
-
函数化符号:提供 C 风格转换的另一种形式。
- C++ 中的函数形式的转换(function-style cast)使用
T(e)
的语法,其中T
是目标类型,e
是要转换的值。这种转换方式有时被称为函数形式的转换,它提供了一种将值e
转换为类型T
的简便方法。 - 对于内置类型,
T(e)
等价于 C 风格的转换(T)e
,这意味着它并不比 C 风格的转换更安全。
- C++ 中的函数形式的转换(function-style cast)使用
建议:在处理行为良好的构造时使用花括号初始化 T{v}
,而在其他情况下使用命名的转换(如 static_cast
)。