C++小型八股文

用这篇小型八股文纪念一下失败的半年,基本都是最近记录的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 编译时,as 都是存储在 .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 得到的段信息,可以看到 xy 是在 .data 中的,wz 是在 .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) 的结果类型为该表达式中标识符的声明类型。
  • 如果 eT 类型的 xvalue,那么 decltype(e) 的结果类型为 T&&
  • 如果 eT 类型的左值,那么 decltype(e) 的结果类型为 T&
  • 如果 eT 类型的纯右值,那么 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 对象:

第一种方式直接获取对象,并使用 firstsecond 来对对象进行访问,该种方式可读性较差。

例如下面的例子中使用 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::ignoreinserted变量上,其中前者表示忽略赋值。此种方式相比第一种代码可读性更高,不过 std::tie也有明显的缺点:

  1. 绑定的变量必须提前声明,且其类型必须提前明确,不能自动推导。
  2. 由于绑定的变量需要提前声明和定义,变量需要调用一次构造函数,然后才被绑定赋值为新的数值,这种冗余操作对于复杂对象可能有性能上的损耗。

结构化绑定语法

结构化绑定(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 后也可以包含 staticthread_local
  • ref-qualifier&&&
  • identifier-list:用逗号分隔的标识符名称的列表
  • expression:必须是数组或 non-union 类型

内部实现

一个结构化绑定的声明首先会引入一个新变量 e(保证不会与其他变量重名)来存储初始化的值:

  1. 情况1:如果 expressionA 类型的数组,且不存在 ref-qualifier,那么 e 的类型是 cv-A,并且每个元素是从 expression 对应元素通过拷贝构造(1)或直接构造(2)(3)得到的。
  2. 情况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_sizestd::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.

参考

字面值类型 (Literal Type)

C++中可以把类型分为两类:Literal 和 Non-literal Type

其中 Literal Type 包含了如下类型:

此外,满足如下条件的类也可以是 Literal Type:

  • 析构函数必须是 trivial(compiler-provided) 的 (C++20之前) / constexpr 修饰的 (C++20之后)
  • 是如下一种类型
    • closure type,就是 lambda 表达式 (C++17之后)
    • 聚合类/非聚合类 balabala....(没搞明白原文的意思)
    • 至少有一个 constexpr 构造函数(不能是拷贝或移动构造函数)

如下就是一个聚合类

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 析构函数

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 构造函数

对于函数体不是 =deleteconstexpr 构造函数来说,必须满足如下条件:

  1. 对于 union 来说,只能初始化恰好一个非静态成员。
  2. 对于类或结构体的构造函数,必须初始化每个基类子对象和每个非静态数据成员(不包含 union)。如果还包含匿名联合体,需要按照条件1初始化。
  3. 在初始化非静态数据成员和基类时,所选择的每个构造函数也必须是 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 constexpradd5 函数,可以用于模板实例化。

#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 %rbpret 返回

再来看下 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:用于修改类型的 constvolatile 属性,可以去修改那些原本不是 const,但是经过了一些原因被变换成 const 的数据。
  • reinterpret_cast:用来处理无关类型之间的转换,几乎算是万能的。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇