用这篇小型八股文纪念一下失败的半年,基本都是最近记录的C++相关内容。
常见关键词
const
修改 const
变量会在编译期出错,试图间接地修改常量是 UB(例如通过引用或者指针去修改)。
全局常量对象存储在 .rodata
中,局部常量对象则存储在栈上。
下面通过代码来对这一点进行验证。
#include <iostream>
const int a = 15;
const char *s = "Hello world";
int main() {
std::cout << a << s << std::endl;
return 0;
}
使用 -O0
编译时,a
和 s
都是存储在 .rodata
中的,通过 readelf -x .rodata
可以看到 0f000000
以及 s
对应的字符串。
test: file format elf64-x86-64
Contents of section .rodata:
2000 01000200 0f000000 48656c6c 6f20776f ........Hello wo
2010 726c6400 010101 rld....
但是如果用 -O2
编译的话,a
就被优化成了立即数,通过 g++ -S
获得汇编代码可以确认这一点:
10 main:
11 .LFB1996:
12 .cfi_startproc
13 pushq %rbp
14 .cfi_def_cfa_offset 16
15 .cfi_offset 6, -16
16 movl $15, %esi # a变成了立即数
当然字符串常量 s
依然存储在 .rodata
中:
test: file format elf64-x86-64
Contents of section .rodata:
2000 01000200 48656c6c 6f20776f 726c6400 ....Hello world.
static
static
变量的特性:
static
变量在编译阶段就已经分配内存空间了,程序中只有一份。- 如果
static
局部变量不初始化,那么它默认为0。 - 未初始化的
static
变量放在.bss
段,初始化的变量则放在.data
段。(和全局变量一样)
同样通过代码来进行验证。
#include <stdio.h>
static int y = 2;
static int z;
void func() {
static int x = 1;
static int w;
x++;
y++;
w = 1;
z = 1;
}
int main() {
func();
return 0;
}
通过 objdump
可以找到每个变量的地址:(Update:直接 readelf
应该也能看到)
111d: 8b 05 f1 2e 00 00 mov 0x2ef1(%rip),%eax # 4014 <x.1>
1123: 83 c0 01 add $0x1,%eax
1126: 89 05 e8 2e 00 00 mov %eax,0x2ee8(%rip) # 4014 <x.1>
112c: 8b 05 de 2e 00 00 mov 0x2ede(%rip),%eax # 4010 <y>
1132: 83 c0 01 add $0x1,%eax
1135: 89 05 d5 2e 00 00 mov %eax,0x2ed5(%rip) # 4010 <y>
113b: c7 05 db 2e 00 00 01 movl $0x1,0x2edb(%rip) # 4020 <w.0>
1142: 00 00 00
1145: c7 05 cd 2e 00 00 01 movl $0x1,0x2ecd(%rip) # 401c <z>
结合 readelf
得到的段信息,可以看到 x
和 y
是在 .data
中的,w
和 z
是在 .bss
中的
[22] .data PROGBITS 0000000000004000 00003000
0000000000000018 0000000000000000 WA 0 0 8
[23] .bss NOBITS 0000000000004018 00003018
0000000000000010 0000000000000000 WA 0 0 4
静态全局变量只能在当前定义的文件内使用,而普通全局变量在所有文件都能使用。
通过 readelf -a
查看 .o
文件就可以看到静态全局变量被标记为了 LOCAL
# test.c
int x = 0;
static int y = 0;
# gcc -c main.c && readelf -a main.o
Symbol table '.symtab' contains 4 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 y
3: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 x
[22] .data PROGBITS 0000000000004000 00003000
0000000000000014 0000000000000000 WA 0 0 8
[23] .bss NOBITS 0000000000004014 00003014
000000000000000c 0000000000000000 WA 0 0 4
因此不同的 TU 内可以有同名的 static 变量,例如下面的例子
// fun1.cpp
#include <iostream>
static int a = 1;
void fun1() {
std::cout << "func1 " << a << std::endl;
}
// fun1.cpp
#include <iostream>
static int a = 2;
void fun2() {
std::cout << "func2 " << a << std::endl;
}
// main.cpp
void fun1();
void fun2();
int main() {
fun1();
fun2();
}
g++ fun1.cpp fun2.cpp main.cpp -o main
extern
通过 readelf
可以看到,extern
变量会被标记为未定义符号(NOTYPE)
- 4: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND x # extern int x;
+ 3: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 x # int x;
goto
在C语言中,goto
可以在 Error Handling 的场合下使用,例如在函数中处理异常退出流程的资源释放、环境清理等功能,可以通过 goto
统一跳转到一处执行。
decltype
decltype(e)
的语法规则主要有以下四条:
- 如果
e
是一个没有用小括号括起来的标识符表达式或类成员存取表达式,那么decltype(e)
的结果类型为该表达式中标识符的声明类型。 - 如果
e
是T
类型的 xvalue,那么decltype(e)
的结果类型为T&&
。 - 如果
e
是T
类型的左值,那么decltype(e)
的结果类型为T&
。 - 如果
e
是T
类型的纯右值,那么decltype(e)
的结果类型为T
。- 在 C++17 之前,如果该纯右值是一个函数调用的返回,不会为其创建临时对象。
- 在 C++17 以后,纯右值是没有空间的,所以该值不会被物化(Materialization)。
- 这里只是想要说明不会真正创建出对象,说法上的区别是由于 C++17 对 value catalog 修改的造成的。
- 如果
e
被括号括起来,那么它会被视为一个左值表达式。
问题:
cppreference 中说,C++17 后,如果
e
是一个 id-expression naming a structured binding,那么获取到的类型是引用,但是我没有复现出来auto p = std::make_pair(1, 2); const auto& [v1, v2] = p; std::cout << (std::is_reference_v
) << " " << (std::is_const_v ) << std::endl; // 输出 0 1
auto
在表达式中,类型的推导是和模板类型推导类似的。
例如 const auto& i = expr
,这里可以想象有一个如下的模板,并且调用了 f(expr)
来推断 U
的类型:
template<class U> void f(const U& u);
(C++14以后) 表达式 auto& f()
的返回类型是通过 return statement 进行推断的。
decltype(auto)
decltype(auto)
推断出的类型为 decltype(expr)
, 其中 expr
就是初始化表达式。
在 C++14 后,decltype(auto)
不能添加其他修饰。
lambda
表达式
lambda
表达式是一种纯右值表达式,其类型是唯一的、未命名的、非联合体和非聚合体的类类型,称为闭包类型(closure type)。编译器会为 lambda
表达式自动生成一个对应的lambda匿名类。
lambda
表达式对应的类通常有以下几个成员函数和对象:
operator()
重载函数 是调用的入口。如果参数列表中有 auto
或者模板类型(C++20之后),生成的函数也会带模板。
ret operator()(params) { body }
template<template-params>
ret operator()(params) { body }
// generic lambda, operator() is a template with two parameters
auto glambda = []<class T>(T a, auto&& b) { return a < b; };
如果捕获列表为空,会定义一个类型转换函数,可以用于赋值
using F = ret(*)(params);
operator F() const noexcept;
constexpr operator F() const noexcept; # since C++17
#include <iostream>
void foo(int (*func)(int)) {
std::cout << func(5) << std::endl;
}
int main() {
int var = 10;
auto l1 = [](auto a) {return a;};
auto l2 = [&](auto a) {return a + var;};
foo(l1); // OK
foo(l2); // Error
}
在 C++20 之前,不会生成默认构造/operator=
函数,C++20 之后在捕获列表为空的情况下会生成一个默认构造函数/operator=
函数
那些通过拷贝捕获的对象([=]
或 [a]
),会被存储在成员对象中,而引用对象是否被存储在其中是 unspecified 的。
结构化绑定
在 C++17 之前,我们有两种方式来接收一个 std::pair
对象:
第一种方式直接获取对象,并使用 first
和 second
来对对象进行访问,该种方式可读性较差。
例如下面的例子中使用 result
接收 map::insert
的返回值,并用 second
来获取插入结果:
auto result = mp.insert({1, 1});
bool success = result.second;
第二种方法则是使用 std::tie
对每个对象进行绑定(C++11 后)
#include <iostream>
#include <map>
#include <tuple>
int main() {
std::map<int, int> mp;
bool inserted;
std::tie(std::ignore, inserted) = mp.insert({1, 1});
return 0;
}
在上述例子中,std::pair
的两个元素被分别绑定在了 std::ignore
和 inserted
变量上,其中前者表示忽略赋值。此种方式相比第一种代码可读性更高,不过 std::tie
也有明显的缺点:
- 绑定的变量必须提前声明,且其类型必须提前明确,不能自动推导。
- 由于绑定的变量需要提前声明和定义,变量需要调用一次构造函数,然后才被绑定赋值为新的数值,这种冗余操作对于复杂对象可能有性能上的损耗。
结构化绑定语法
结构化绑定(Structure Bindings) 可以对数组 array
、元组 tuple
、结构体 struct
等类型的成员变量进行绑定,语法上非常方便。
例如上面的例子可以简化为:
auto& [itr, inserted] = map.insert({ 1, 2 });
结构化绑定的基本格式以及各部分的作用如下:
attr(optional) cv-auto ref-qualifier(optional) [ identifier-list ] = expression; (1)
attr(optional) cv-auto ref-qualifier(optional) [ identifier-list ] { expression }; (2)
attr(optional) cv-auto ref-qualifier(optional) [ identifier-list ] ( expression ); (3)
attr
:可选的属性序列(例如[[gnu::unused]]]
)cv-auto
:cv-qualified 的auto
类型(C++20 后也可以包含static
、thread_local
)ref-qualifier
:&
或&&
identifier-list
:用逗号分隔的标识符名称的列表expression
:必须是数组或 non-union 类型
内部实现
一个结构化绑定的声明首先会引入一个新变量 e(保证不会与其他变量重名)来存储初始化的值:
- 情况1:如果
expression
是A
类型的数组,且不存在ref-qualifier
,那么 e 的类型是cv-A
,并且每个元素是从expression
对应元素通过拷贝构造(1)或直接构造(2)(3)得到的。 - 情况2:否则,e 是通过将
[identifier-list]
进行替换来定义的,即attr cv-auto ref-qualifier e initializer
同时将 e 的类型记为 E,在情况2下,这部操作等同于如下代码:
attr cv-auto ref-qualifier _e initializer;
using E = std::remove_reference_t<decltype((_e))>。
在引入了 e 之后,就会进行真正的绑定过程,根据 E 的类型分为三种绑定方式:
-
Case 1:如果 E 是数组类型,并且列表中的每个名字都会绑定到一个元素上。
-
Case 2:如果 E 不是 union 类,并且
std::tuple_size<E>
是一个包含了value
成员变量的完整类型,此时会使用 "tuple-like" binding protocol。 -
Case 3: 如果 E 不是 union 类,但是
std::tuple_size<E>
不是完整类型,identifier-list
会被绑定到 E 的每个可访问的成员变量上。
Case 1: 绑定数组
identifier-list
的每个标识符都会成为 e 中一个元素的左值引用。
其 referenced type 是数组中每个元素的类型,如果 E 是 cv-qualified 的,那么每个元素的类型也是 cv-qualified 的。
int a[2] = {1, 2};
auto [x, y] = a; // creates e[2], copies a into e,
// then x refers to e[0], y refers to e[1]
auto& [xr, yr] = a; // xr refers to a[0], yr refers to a[1]
Case 2: tuple-like binding protocol
这里先再重复一下:e 就是通过将
[identifier-list]
进行替换来定义的,using E = std::remove_reference_t<decltype((_e))>
tuple-like 是实现了
std::tuple_size
和std::tuple_element
的对象,具体可以看参考
如果 std::tuple_size<E>::value
被定义了,并且 std::tuple_size<E>::value
等于列表长度,就可以进行绑定。
对于每个标识符,会引入一个变量,其类型为 "std::tuple_element<i, E>::type
的引用",根据 initializer 是否是左值设为左值/右值引用。之后按如下方式选择 initializer:
- 如果能找到
E
中形如E::get<typename>()
的类成员函数,其第一个模板参数是非类型参数,那么调用e.get<i>()
。 - 否则调用
std::get<i>(e)
在这些初始化中,如果 e 是左值引用(ref-qualifier=&
或 ref-qualifier=&&
并且 expression
是左值),那么 e
是左值,否则 e
是将亡值。
最终标识符就成为一个绑定到上述变量的名字,其 referenced type 就是 std::tuple_element<i, E>::type
。
float x{};
char y{};
int z{};
std::tuple<float&, char&&, int> tpl(x, std::move(y), z);
const auto& [a, b, c] = tpl;
// using Tpl = const std::tuple<float&, char&&, int>;
// a names a structured binding that refers to x (initialized from get<0>(tpl))
// decltype(a) is std::tuple_element<0, Tpl>::type, i.e. float&
// b names a structured binding that refers to y (initialized from get<1>(tpl))
// decltype(b) is std::tuple_element<1, Tpl>::type, i.e. char&&
// c names a structured binding that refers to the third component of tpl, get<2>(tpl)
// decltype(c) is std::tuple_element<2, Tpl>::type, i.e. const int
注意事项
若 expression
为纯右值,则结构化绑定的修饰符只能用 const auto&
或 auto&&
,auto&
不可绑定右值:
int a = 1;
const auto& [x] = std::make_tuple(a); //ok
auto&& [z] = std::make_tuple(a); //ok
auto& [y] = std::make_tuple(a); //error
一个小问题
就以上述的代码为例,下面的 static_assert
能通过吗?
auto& [itr, inserted] = map.insert({ 1, 2 });
static_assert(std::is_reference_v<decltype(inserted)>);
答案是否定的,当 x
是结构化绑定的对象时,decltype(x)
被称为 referenced type,这个词在前文也出现过。
例如下面的代码中,输出的值就是 0 1
:
int main() {
std::pair p1{1.1f, 2};
auto& [a, b] = p1;
int i1 = 1;
std::pair<int&, double> p2{i1, 2.2};
auto& [c, d] = p2;
std::cout << std::is_reference_v<decltype(a)> << " " << std::is_reference_v<decltype(c)> << std::endl;
return 0;
}
当然我不太明白这个设计的逻辑是什么,原文如下:
decltype(x)
, where x
denotes a structured binding, names the referenced type of that structured binding. In the tuple-like case, this is the type returned by std::tuple_element, which may not be a reference even though a hidden reference is always introduced in this case. This effectively emulates the behavior of binding to a struct whose non-static data members have the types returned by tuple_element
, with the referenceness of the binding itself being a mere implementation detail.
参考
- C++17结构化绑定
- C++ 17 結構化綁定
- What are the types of identifiers introduced by structured bindings in C++17?
字面值类型 (Literal Type)
C++中可以把类型分为两类:Literal 和 Non-literal Type
其中 Literal Type 包含了如下类型:
此外,满足如下条件的类也可以是 Literal Type:
- 析构函数必须是 trivial(compiler-provided) 的 (C++20之前) /
constexpr
修饰的 (C++20之后) - 是如下一种类型
如下就是一个聚合类
struct Point2 {
int x;
int y;
};
此外还有一个字面值常量类的例子:
class conststr {
const char* p;
size_t sz;
public:
template<std::size_t N>
constexpr conststr(const char(&a)[N]) : p(a), sz(N - 1) {}
constexpr char operator[](size_t n) const {
return n < sz ? p[n] : throw std::out_of_range("");
}
constexpr size_t size() const { return sz; }
// g++ -std=c++20
constexpr ~conststr() {};
};
constexpr
这一节的介绍并不完整,因为完整内容太复杂了...所以就挑了一点进行说明,完整版请翻阅 cppreference
constexpr
变量
constexpr
变量的类型必须为 Literal Type,同时其必须被初始化,也就是其初始化语句(包括所有的隐式转换和构造函数调用)必须都是常量表达式
结合 Literal Type 的定义,我们就可以定义如下的变量:
constexpr conststr s("Hello World");
constexpr Point pt = {10, 10};
constexpr int sum = pt.x + pt.y;
constexpr
函数
constexpr
也可以修饰函数,不过对于函数有如下限制条件:
- 函数本身不能是虚函数,且不能包含 try catch(C++20之前)
- 函数体内不能包含非 Literal Type 的变量定义
- 不能使用
std::unique_ptr
/std::shared_ptr
- 返回类型和每个参数类型都必须是 Literal Type
- 对于
constexpr
构造函数来说:类本身不能有虚基类,每个成员对象都必须被初始化- C++20 对此进行了扩展,支持
constexpr
析构函数
- C++20 对此进行了扩展,支持
constexpr
函数和 constexpr
变量不一样,并不一定要求 [编译时求值],它只表达了[函数具备这个能力]。
只有所有参数都是常量表达式,并且返回的结果被用于常量表达式(比如用于初始化 constexpr
数据),才会在编译期进行求值。
结合之前的内容就可以实现各种编译期的功能,例如字符串统计:
constexpr size_t count_lower(conststr s) {
size_t c{};
for (size_t n{}; n != s.size(); ++n) {
if ('a' <= s[n] && s[n] <= 'z') {
++c;
}
}
return c;
}
// An output function that requires a compile-time constant N, for testing
template<int N>
struct constN {
constN() { std::cout << N << '\n'; }
};
int main() {
std::cout << "the number of lowercase letters in \"Hello, world!\" is ";
constN<count_lower("Hello, world!")>();
}
constexpr
构造函数
对于函数体不是 =delete
的 constexpr
构造函数来说,必须满足如下条件:
- 对于 union 来说,只能初始化恰好一个非静态成员。
- 对于类或结构体的构造函数,必须初始化每个基类子对象和每个非静态数据成员(不包含 union)。如果还包含匿名联合体,需要按照条件1初始化。
- 在初始化非静态数据成员和基类时,所选择的每个构造函数也必须是
constexpr
构造函数。
constexpr
析构函数
在 C++20 前,析构函数不能被 constexpr
修饰,只能使用默认析构函数。
C++20 之后,满足如下条件的析构函数也可以被 constexpr
修饰:
- 每个非静态数据成员和基类所使用的析构函数也必须是
constexpr
析构函数。
C++17 if constexpr
C++17 增加了 if constexpr
特性,可以实现条件编译功能,例如实现一个编译期的斐波那契求值(如果在 C++17 前还需要 N=0/1
的模板进行特化):
template<long N>
constexpr long fibonacci() {
if constexpr (N >= 2) {
return fibonacci<N-1>() + fibonacci<N-2>();
} else {
return N;
}
}
int main() {
static_assert(fibonacci<2>() == 1);
static_assert(fibonacci<3>() == 2);
}
在模板类中也可以利用这个特性,比如下面的例子想要实现一个字符串转换函数:
template<typename T>
std::string toStr(T t) {
if (std::is_same_v<T, std::string>)
return t;
else
return std::to_string(t);
}
由于在使用 std::string
实例化模板后会发现 std::to_string(t)
函数不存在,即使我们没用到该分支,因此会编译失败。
toStr(std::string{"abc"}); // Error! 编译失败
在 C++14 中可以使用 std::enable_if
来解决这个问题,该方法为不同的类型生成了不同的模板:
template<typename T>
std::enable_if_t<std::is_same_v<T, std::string>, std::string> toStr(T t) {
return t;
}
template<typename T>
std::enable_if_t<!std::is_same_v<T, std::string>, std::string> toStr(T t) {
return std::to_string(t);
}
而在 C++17 之后,直接使用 if constexpr
就可以了
template<typename T>
std::string toStr(T t) {
if constexpr (std::is_same_v<T, std::string>)
return t;
else
return std::to_string(t);
}
C++17 lambda constexpr
另外 C++17 中 lambda 表达式也可以是 constexpr
函数,例如下面定义了一个 lambda constexpr
的 add5
函数,可以用于模板实例化。
#include <iostream>
template <typename T>
constexpr auto addTo(T i) {
return [i](auto j) {return i + j;};
}
constexpr auto add5 = addTo(5);
template <unsigned N>
class SomeClass{
public:
const unsigned value = N;
};
int main() {
SomeClass<add5(22)> someClass27;
std::cout << someClass27.value << std::endl;
}
C++20 改进
C++20 对 constexpr
函数做出了很大的改进,可以进行有限制的动态内存分配和使用 std::vector
/std::string
。
下面是一个例子,如果想在 C++20 之前实现这样的编译期求和是比较麻烦的:
constexpr int sum(int n) {
auto p = new int[n];
std::iota(p, p + n, 1);
auto t = std::accumulate(p, p + n, 0);
delete [] p;
return t;
}
static_assert(sum(10) == 55);
当然对这一点还是会有限制:
constexpr
函数中不能使用std::unique_ptr
/std::shared_ptr
- 动态内存的生命周期必须在
constexpr
函数的上下文中,即不能返回动态内存分配的指针 - 不能返回
std::vector
/std::string
对象
编译期多态
此外在 C++20 中还能实现编译期多态(已经越来越看不懂了)
struct Box {
double width{0.0};
double height{0.0};
double length{0.0};
};
struct Product {
constexpr virtual ~Product() = default;
constexpr virtual Box getBox() const noexcept = 0;
};
struct Notebook : public Product {
constexpr ~Notebook() noexcept {};
constexpr Box getBox() const noexcept override {
return {.width = 30.0, .height = 2.0, .length = 30.0};
}
};
struct Flower : public Product {
constexpr Box getBox() const noexcept override {
return {.width = 10.0, .height = 20.0, .length = 10.0};
}
};
constexpr bool canFit(const Product &prod, const Box &minBox) {
const auto box = prod.getBox();
return box.width < minBox.width && box.height < minBox.height && box.length < minBox.length;
}
int main() {
constexpr Notebook nb;
constexpr Box minBox{100.0, 100.0, 100.0};
static_assert(canFit(nb, minBox));
}
函数调用流程
我们的 main
函数如下:
class Foo {
public:
Foo(int t_ = 0) : t1(t_), t2(t_) {}
static void static_func() {
int a = t3;
}
void member_func() {
t2 += 1;
}
private:
int t1 = 0;
int t2 = 0;
static int t3;
};
int Foo::t3 = 1;
int main() {
Foo foo(10);
Foo::static_func();
foo.member_func();
}
这里来看一下函数调用流程
0000000000001135 <main>:
1135: 55 push %rbp
1136: 48 89 e5 mov %rsp,%rbp
1139: 48 83 ec 10 sub $0x10,%rsp
113d: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
1144: 00 00
1146: 48 89 45 f8 mov %rax,-0x8(%rbp)
114a: 31 c0 xor %eax,%eax
114c: 48 8d 45 f0 lea -0x10(%rbp),%rax
1150: be 0a 00 00 00 mov $0xa,%esi
1155: 48 89 c7 mov %rax,%rdi
1158: e8 2d 00 00 00 call 118a <_ZN3FooC1Ei>
115d: e8 49 00 00 00 call 11ab <_ZN3Foo11static_funcEv>
1162: 48 8d 45 f0 lea -0x10(%rbp),%rax
1166: 48 89 c7 mov %rax,%rdi
1169: e8 4e 00 00 00 call 11bc <_ZN3Foo11member_funcEv>
116e: b8 00 00 00 00 mov $0x0,%eax
1173: 48 8b 55 f8 mov -0x8(%rbp),%rdx
1177: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
117e: 00 00
1180: 74 05 je 1187 <main+0x52>
1182: e8 a9 fe ff ff call 1030 <__stack_chk_fail@plt>
1187: c9 leave
1188: c3 ret
1189: 90 nop
首先来看 member_func
的调用:
00000000000011bc <_ZN3Foo11member_funcEv>:
11bc: 55 push %rbp
11bd: 48 89 e5 mov %rsp,%rbp
11c0: 48 89 7d f8 mov %rdi,-0x8(%rbp)
11c4: 48 8b 45 f8 mov -0x8(%rbp),%rax
11c8: 8b 40 04 mov 0x4(%rax),%eax
11cb: 8d 50 01 lea 0x1(%rax),%edx
11ce: 48 8b 45 f8 mov -0x8(%rbp),%rax
11d2: 89 50 04 mov %edx,0x4(%rax)
11d5: 90 nop
11d6: 5d pop %rbp
11d7: c3 ret
11d8: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
11df: 00
- 在函数外通过,
lea -0x10(%rbp),%rax
以及mov %rax,%rdi
将 this 指针作为参数存储在rdi
里 push %rbp
:首先将当前的rbp
压入栈帧用于恢复mov %rsp,%rbp
:将当前的rsp
作为新的栈帧- 由于这里没有局部变量,也没有函数调用,所以不需要通过
rsp
显式分配栈帧(与之相反的是main
函数里的sub $0x10,%rsp
) mov %rdi,-0x8(%rbp)
:把this指针放在rbp-8
的位置(栈是高地址向低地址生长)mov -0x8(%rbp),%rax
:把this指针再存到rax
中mov 0x4(%rax),%eax
:间接寻址找到t2
的值,后面的几行就是计算t2 += 1
的过程(对象内部的布局还是从低地址到高地址)- 最后通过
pop %rbp
和ret
返回
再来看下 static_func
的调用:
00000000000011ab <_ZN3Foo11static_funcEv>:
11ab: 55 push %rbp
11ac: 48 89 e5 mov %rsp,%rbp
11af: 8b 05 5b 2e 00 00 mov 0x2e5b(%rip),%eax # 4010 <_ZN3Foo2t3E>
11b5: 89 45 fc mov %eax,-0x4(%rbp)
11b8: 90 nop
11b9: 5d pop %rbp
11ba: c3 ret
11bb: 90 nop
- 静态成员函数可以直接调用,所以不需要传入 this 指针,前两行忽略
mov 0x2e5b(%rip),%eax
:获取静态成员变量的值(在.data
,地址是 4010)- 后面的流程就差不多了
这里就看出了函数调用最主要的工作是:设置入参 & 设置栈帧
类型转换
static_cast
:相当于C语言里的强制转换,不能转换指针类型。dynamic_cast<type*>(expression)
:expression
必须是type
的基类或父类。- 如果转换目标是引用,即
dynamic_cast<type&>
,则转换失败时会抛出std::bad_cast
异常。 - 指针类型转换失败会返回空指针。
- 如果转换目标是引用,即
const_cast
:用于修改类型的const
或volatile
属性,可以去修改那些原本不是const
,但是经过了一些原因被变换成const
的数据。reinterpret_cast
:用来处理无关类型之间的转换,几乎算是万能的。