开发喵星球

C++ 有多难?

马上就大三了,网络工程专业,以前的时间都被玩掉了,对 C++ 唯一的了解只有大一老师教的和我买的谭浩强的 C++ 书。人家都说要真正学好 C++ 很难,我现在的想法就是想靠这个混饭吃,所以我想问如果从现在开始我拼命学 C++,到大学毕业时能学到什么程度。

我也不是它太难我就不会去努力了,我不怕吃苦,因为现在感觉大学什么都不会,感到毕业前途迷茫,就想向各位大神咨询咨询,如果我现在玩命地学,能搞定它吗?

前言:

目前总计75题,所有的题目都在视频有讲述,我可能会提起让你看哪个博客或视频更加详细

一些提问sizeof的结果,我们只是取一般的x64情况,因为硬要扯,显然可能性太多了,没法假设int的size,假设空类的size,不用在意,我们就讲一般实现下sizeof的结果

有些涉及多个编译器不一样的问题自行考虑,还有不同版本的相同编译器结果不同的,对有些标准规定的遵守问题,都参见ISO


lambda

lambda这块有任何疑问参见博客,C++lambda剖析

第一题

auto p = +[] {return 6; };

p的类型是什么?

答案:函数指针

解释:这是一个非捕获的lambda,自然可以生成对应转换函数转换为函数指针,这里的一元+是为了创造一个合适的语境,一元+表"正"

第二题

int main() {
    static int a = 42;
    auto p = [=] { ++a; } ;
    std::cout << sizeof p << '\n';
    p();
    return a;
}

提问,打印p是多少?return a是多少?

答案: 1 43

解释:

  1. 如果变量满足下列条件,那么 lambda 表达式在使用它前不需要先捕获:该变量是非局部变量,或具有静态或线程局部存储期(此时无法捕获该变量),或者该变量是以常量表达式初始化的引用。
  2. 这里的捕获是[=],但是其实写不写都无所谓,反正这个作用域就一个静态局部变量a,你也无法捕获到这个变量。那么按照空类,p的大小一般来说自然也就是1了。

第三题

int main() {
    float x;
    float& r = x;
    auto p = [=] {};
    std::cout << sizeof p << '\n';
}

请问打印多少?

答案:1

解释:

如果捕获符列表具有默认捕获符,且未显式(以 this 或 this)捕获它的外围对象,或任何在 lambda 体内可 ODR 使用的自动变量,或对应变量拥有自动存储期的结构化绑定 (C++20 起),那么在以下情况下,它隐式捕获之:

或者,该实体在取决于某个泛型 lambda 形参的 (C++17 前)表达式内的潜在求值表达式中被指名(包括在使用非静态类成员的前添加隐含的 this->)。就此目的而言,始终认为 typeid 的操作数被潜在求值。即使实体仅在舍弃语句中被指名,它也可能会被隐式捕获。 (C++17 起)

什么是ODR使用?这就问你自己了,我们并不打算解答这个,起码你std::cout了这个值肯定是ODR使用

第四题

int main() {
    const int N = 10;
    auto p =[=] {
        int arr[N]{};
    };
    std::cout << sizeof p << '\n';
}

请问打印多少?

答案:1

解释:

  1. 如果变量满足下列条件,那么 lambda 表达式在读取它的值前不需要先捕获: 该变量具有 const 而非 volatile 的整型或枚举类型,并已经用**常量表达式初始化**,或者 该变量是 constexpr 的且没有 mutable 成员。(这是表示即使不使用=捕获在这里的语境下直接用也没问题)
  2. N没有被ODR使用,没有被捕获

第五题

int main() {
    const int N = 10;
    auto p =[=] {
        int p = N;
    };
    std::cout << sizeof p << '\n';
}

请问打印多少?

答案:msvc :4 gcc:1 clang:1

解释:我最终是认为两点想法,当然,这是可以讨论的

  1. 这属于编译器优化的行为,因为就算不带上**[=]**,也能正常访问(参考上一条的解释),在这种情况msvc也是打印1
  2. 读取编译时常量的值也不是ODR使用。

第六题

int main() {
    const float a = 6;
    [] {
        std::cout << a << '\n';
    }();
}

能否正常编译?打印多少?

答案:不能正常编译

解释:

  1. 如果变量满足下列条件,那么 lambda 表达式在读取它的值前不需要先捕获: 该变量具有 const 而非 volatile 的整型或枚举类型,并已经用常量表达式初始化,或者 该变量是 constexpr 的且没有 mutable 成员。
  2. 显然float不是整形

第七题

int main() {
    const int a = 6;
    [] {
        std::cout << &a << '\n';
    }();
}

能否正常编译?打印多少?

答案:不能正常编译

解释:与第六第七题的概念不同

第八题

lambda表达式的类型是什么?请写一段代码表示(不要超脱语言层面)

答案:lambda 表达式是纯右值表达式,它的类型是独有的无名非联合非聚合类类型,被称为闭包类型(closure type)

template<class... Ts>
struct overloaded : Ts...
{
    using Ts::operator()...;
};
//使用
int main() {
    auto c = overloaded{ [](int arg) { std::cout << arg << ' '; },
                         [](double arg) { std::cout << arg << ' '; },
                          [](auto arg) { std::cout << arg << ' '; }
    };
    c(10);
    c(15.1);
    c("傻子傻子");
}

第九题

auto p = +[]()noexcept { };

C++17p的类型是什么?

答案:一般来说void (*)(void) noexcept

解释:

  1. 关于+第一题已经讲过,至于类型的话,如果是msvc用typeid,还会扯到调用约定,我们自己知道这回事就好了
  2. noexcept 说明是函数类型的一部分,可以作为任何函数声明符的一部分出现。(C++17起)

第十题

constexpr int a = [] {return 6; }();

上面代码在c++17是否合法?

答案:合法

解释: constexpr :显式指定函数调用运算符或运算符模板的任意特化为 constexpr 函数。如果没有此说明符但函数调用运算符或任意给定的运算符模板特化恰好满足针对 constexpr 函数的所有要求,那么它也会是 constexpr

第十一题

template<class... Args>
void f(Args... args) {
    auto lm = [&args...] {  };
    auto lm2 = [&] {  };
    std::cout << sizeof lm << '\n';
    std::cout << sizeof lm2 << '\n';
}
int main() {
    f(1, 1.0, 1.f);
}

请问打印多少?

答案:24 1

解释:

  1. 实际上是一个很普通的形参包展开,显式的捕获,自然就24
  2. 不符合隐式捕获的规则,参见第三题

第十二题

struct X {
    X() { puts("X"); }
    ~X() { puts("~X"); }
    X(X&&)noexcept { puts("X&&"); }
    X(const X&) { puts("const X&"); }
};

template<class...Args>
void g(Args&&...args) {  }

template<class... Args>
void f(Args&&... args) {
    [args...] { g(args...); }();
}

template<class... Args>
void f_(Args&&... args) {
    [... args = std::forward<Args>(args)] {g(args...); }();
}

int main() {
    X x_;
    f_(std::move(x_));
}

给出以上代码,打印多少?

答案:

X const X& ~X X X&& ~X ~X ~X

解释:f是普通的变参捕获,有拷贝开销,f_里面的lambda使用了完美转发,关于这个语法,需要使用c++20,形式如下:

...标识符 初始化器

第十三题

int main() {
    auto p = +[](...) {};
    auto p2 = [](auto...args) {
        ((std::cout << args << ' '), ...);
    };
    p(1, "*", 5.6);
    p2(1, "*", 5.6);
}

能否通过编译?打印多少?

答案:1 * 5.6

解释:C++14泛型lambda

第十四题

template <typename F, typename ...Ts>
    auto concat(F t, Ts ...ts)
    {
        if constexpr (sizeof...(ts) > 0) {
            return [=](auto ...parameters) {
                return t(concat(ts...)(parameters...));
            };
        }
        else {
            return [=](auto ...parameters) {
                return t(parameters...);
            };
        }
    }

给出以上代码,思考是否看的懂

解释:参见视频

第十五题

new一个带捕获lambda,以下代码是否正确?

int main() {
    auto p = new auto([x = 0](int c) {std::cout << c << std::endl; });
    (*p)(10);
    delete p;
}

返回的指针是指向了lambda类的对象,自然要先*然后调用operator(),如果你对new auto()这个组合有疑问

auto p = new auto(5.6);

默认实参

所有内容均可参见博客视频

第一题

void f(int, int, int = 10);
void f(int, int=6, int);
void f(int = 4,int,int);
void f(int a, int b, int c) { std::cout << a << ' ' << b << ' ' << c << '\n'; }
int main(){
    f();
}

给出以上代码,是否正确?打印多少?

答案:打印4 6 10

解释:在函数声明中,所有在拥有默认实参的形参之后的形参必须拥有在这个或同一作用域中先前的声明中所提供的默认实参

第二题

template<class...Args>
void f_(int n = 6, Args...args) {

}

给出以上代码,是否正确?

答案:正确

解释: 有例外情况是.除非该形参是从某个形参包展开得到的或是函数形参包

第三题

class C{
    void f(int i = 3);
    void g(int i, int j = 99);
    C(int arg); // 非默认构造函数
};

void C::f(int i = 3) {}        
void C::g(int i = 88, int j) {} 
C::C(int arg = 1) {}

给出以上代码,是否正确?

答案:错误

解释:

  1. 对于非模板类的成员函数类外的定义中允许出现默认实参并与类体内的声明所提供的默认实参组合。如果类外的默认实参会使成员函数变成默认构造函数或复制/移动(C++11 起)构造函数/赋值运算符,那么程序非良构。对于类模板的成员函数,所有默认实参必须在成员函数的初始声明处提供。
  2. C::fC::C都错误,前者一看也知道重定义默认实参了,后者添加默认实参是让它变成了默认构造函数C::g 正常组合,正确

第四题

struct Base{
    virtual void f(int a = 7) { std::cout << "Base " << a << std::endl; }
};
struct Derived : Base{
    void f(int a) override { std::cout << "Derived " << a << std::endl; }
};

int main(){
    std::unique_ptr<Base>ptr{ new Derived };
    ptr->f();
}

请问打印多少?

答案:Derived 7

解释:虚函数的覆盖函数不会从基类定义获得默认实参,而在进行虚函数调用时,默认实参根据对象的静态类型确定

如果你不知道什么是静态类型,我们可以介绍一下

静态类型

对程序进行编译时分析所得到的表达式的类型被称为表达式的静态类型程序执行时静态类型不会更改

动态类型

如果某个泛左值表达式指代某个多态对象那么它的最终派生对象的类型被称为它的动态类型。

// 给定
struct B { virtual ~B() {} }; // 多态类型
struct D: B {};               // 多态类型

D d; // 最终派生对象
B* ptr = &d;

// (*ptr) 的静态类型是 B
// (*ptr) 的动态类型是 D

对于纯右值表达式,动态类型始终与静态类型相同。

第五题

int main(){
    int f=0;
    void f2(int n = sizeof f);
    f2();
}

void f2(int n) {
    std::cout << n << '\n';
}

以上代码是否正确?打印多少?

答案:打印8

解释:默认实参中能在不求值语境使用局部变量,sizeof显然是不求值的,没有任何问题,但是msvc不行,以及clang有些版本。


列表初始化

所有内容均可参见博客视频

第一题

templatevoid f(T);表达式f({1, 2, 3})良构吗?

decltype({1,2,3})良构吗?

{}是表达式吗?它有类型吗?

答案:非良构,非良构,不是,没有

解释:规定

第二题

std::vector<int> V(std::istream_iterator<int>(std::cin), {});
    for (const auto i : V) {
        std::cout << i << ' ';
    }

那么这里的std::vector的构造器第二个参数传一个空{}是否正确?

答案:正确

解释:迭代器类型从首个实参推导,但也被用于第二形参位置

第三题

template<typename S>
struct Test {
    Test(S a ,S b)noexcept {
        std::cout << a << ' ' << b << '\n';
    }
};

int main() {
    Test t{ 1,{} };
}

给出以上代码,是否正确?

答案:正确

解释:参见第二题

第四题

auto p = { 1,2,3,4,5,6 };

p使用的是什么初始化,它的类型是什么?

答案:std::initializer_list<int>

解释:。对于使用关键词 auto 的类型推导中有一个例外,在复制列表初始化中将任何 花括号初始化器列表 均推导为 std::initializer_list

第五题

struct X{
    explicit  X(int a, int b) :a(a), b(b) { std::cout << "X(int a,int b)\n"; }

    int a{};
    int b{};
};

int main() {
    X x{ 1,2 };
    X x2( 1,2 );
    X x3 = { 1,2 };
}

给出以上代码,是否正确?

答案:错误

解释: 复制列表初始化(考虑 explicit 和非 explicit 构造函数,但只能调用非 explicit 构造函数)

第六题

struct X {
    explicit  X(int a, int b) :a(a), b(b) { std::cout << "X(int a,int b)\n"; }

    int a{};
    int b{};
};

X f() {
    return { 1,2 };
}

int main() {
    X x{ 1,2 };
    X x2(1, 2);
    auto ret = f();
}

给出以上代码,是否正确?

答案:错误

解释:return{1,2}是复制列表初始化,复制列表初始化(考虑 explicit 和非 explicit 构造函数,但只能调用非 explicit 构造函数)

第七题

std::array的构造函数是用std::initializer_list定义的吗?

答案:不是

解释:遵循聚合初始化的规则初始化 arraystd::array是聚合体


#define

第一题

#include<iostream>
struct S {
    int a, b;
};

#define SDEF(sname, ...) S sname __VA_OPT__(= { __VA_ARGS__ })
int main() {
    SDEF(bar, 1, 2); 
}

SDEF(bar, 1, 2);替换成了什么?

答案:S bar = {1, 2};


用户定义字面量

第一题

自定义一个字面量,做到如下功能:

"乐 :{} *\n"_f(5);
"乐 :{0} {0} *\n"_f(5);
"乐 :{:b} *\n"_f(0b01010101);

答案:

struct A {
    constexpr A(const char* s)noexcept :str(s) {}
    const char* str;
};

template<A a>
constexpr auto operator""_f() {
    return[=]<typename... T>(T... Args) { return std::format(a.str, Args...); };
}

名字查找

也牵扯待决名

参见视频

第一题

namespace X {
    inline namespace std{
    }
}

using namespace X;

int main() {
    ::std::vector v{ 1,2 };
    std::vector v2{ 1,2 };
}

给出以上代码,是否正确?

答案:不正确

解释:如果 :: 左边为空,那么查找过程只会考虑全局命名空间作用域中作出(或通过 using 声明引入到全局命名空间中,如using std::cout;,不要将using声明和指令混淆)的声明,所以main中第一行,在进行有限定查找,全局查找到std没问题,第二行则有歧义

第二题

int main() {
    struct std{};
    ::std::vector v{ 1,2 };
    std::vector v2{ 1,2 };
}

给出以上代码,是否正确?

答案:不正确

解释:参见第一题

第三题

namespace X {
    struct Y{};
    void f(Y){}
}

int main() {
    f(X::Y());
}

给出以上代码,是否正确?

答案:正确

解释:参见无限定名字查找中实参依赖查找,又称 ADL 或 Koenig 查找 [1],是一组对函数调用表达式(包括对重载运算符的隐式函数调用)中的无限定的函数名进行查找的规则。在通常无限定名字查找所考虑的作用域和命名空间之外,还会在它的各个实参的命名空间中查找这些函数。

第四题

struct Base{
    virtual void f()
    {
        std::cout << "基\n";
    }
};
struct Derived : Base{
    void f() override 
    {
        std::cout << "派生\n";
    }
};
int main() {
    std::unique_ptr<Base>p{ new Derived };
    p->f();//派生
    p->Base::f();//基
}

请问以上代码打印什么?

答案:派生 基

解释:虚函数调用在使用有限定名字查找时被抑制

第五题

template<class T>
struct X {
    void f() { std::cout << "X\n"; }
};

void f() { std::cout << "全局\n"; }

template<class T>
struct Y : X<T> {
    void t() {
        this->f();
    }
    void t2() {
        f();
    }
};

int main() {
    Y<void>y;
    y.t();
    y.t2();
}

请问打印什么?

答案:X 全局

解释:

  1. 对于在模板的定义中所使用的非待决名,当检查该模板的定义时将进行无限定的名字查找。
  2. 在这个位置与声明之间的绑定并不会受到在实例化点可见的声明的影响。而对于在模板定义中所使用的待决名,它的查找会推迟到得知它的模板实参之时。
  3. 写下**f()**的时候,它被判定为非待决名,于是在检查该模板定义的时候,也就是没实例化的时候就查找名字,没找到的话就报错未定义,但是全局有一个f,就找到那个了。
  4. 如果写上**this->f()**构成待决名,它的查找推迟到知道模板实参,也就是实例化的时候。

第六题

template<class T>
struct X{
    using type = const T::type;
};

struct Y {
    using type = int;
};

int main() {
    X<Y>::type a{};
}

给出以上代码,请问是否正确?

答案:错误

解释:在模板(包括别名模版)的声明或定义中,不是当前实例化的成员且取决于某个模板形参的名字不会被认为是类型,除非使用关键词 typename 或它已经被设立为类型名


重载决议

我非常喜欢考{},因为错误言论实在太多,那么这里就多写点

第一题

void f(const int(&)[]) { puts("const int(&)[]"); }
void f(const int(&)[2]) { puts("const int(&)[2]"); }

int main() {
    f({ 1,2,3 });
    f({ 1,2 });
}

请问打印多少?

答案:

const int(&)[] const int(&)[2]

解释:

第二题

void f(const int(&)[]) { puts("const int(&)[]"); }
void f(const int(&)[2]) { puts("const int(&)[2]"); }
void f(int(&&)[]) { puts("const int(&&)[]"); }

int main() {
    f({ 1,2,3 });
    f({ 1,2 });
}

请问打印多少?

答案:

const int(&&)[] const int(&&)[]

解释:当前语境右值引用优于const T&,不需要进行值类别的隐式转换,具体参见重载决议列表初始化中的隐式转换序列

第三题

struct X { int x, y; };

struct Y {
    Y(std::initializer_list<int>){}
};

void f(const int(&)[]) { puts("const int(&)[]"); }
void f(const int(&)[2]) { puts("const int(&)[2]"); }
void f(int(&&)[]) { puts("int(&&)[]"); }
void f(X) { puts("X"); }
void f(Y) { puts("Y"); }


int main() {
    f({ 1,2,3 });
    f({ 1,2 });
    f({ .x=1,.y=2 });
}

请问打印多少?

答案:

int(&&)[] int(&&)[] X

解释:参见重载决议列表初始化中的隐式转换序列


值类别

判断题

第一题

字面量都是纯右值表达式

答案:错误

第二题

返回类型是非引用的函数调用或重载运算符表达式,例如 str.substr(1, 2)、str1 + str2 或 it++都是纯右值表达式

答案:正确

第三题

std::move(x)亡值表达式

答案:正确

第四题

void f(int&&){}
int main() {
    int n = 6;
    int&& p = std::move(n);
    f(p);
}

以上代码合法

答案:错误

解释:右值引用是左值表达式

思考题

第一题

struct X {
    X()noexcept { puts("默认构造"); }
    X(const X&) { puts("复制构造\n"); }
    X(X&&)noexcept { puts("移动构造"); }
};
X f() {
    X x;
    return x;
}
int main() {
    X x = X();
    X x2 = f();
}

确保在C++17的环境下,请问打印什么?

答案:两个默认构造

解释:针对纯右值临时量的 C++17 核心语言规定在本质上不同于之前的 C++ 版本:不再有临时量用于复制/移动。返回值优化是强制要求的,而不再被当做复制消除;

第二题

int main() {
    int a = 1, b = 2;
    using T = decltype(a + b);
    using T2 = decltype(std::move(a));
}

T和T2的类型是什么?

答案:int int&&

解释:纯右值表达式和亡值表达式


数组

第一题

int main() {
    const char array[10]{};
    using T = decltype(array[0]);
}

T的类型是什么?

答案:const char&

解释:对数组类型(通过 typedef 或模板操作)应用cv 限定符会将限定符应用到它的元素类型,但元素是有 cv 限定类型的任何数组类型都会被认为拥有相同的 cv 限定性。如果在C语言,直到C23在明确这一概念。

第二题

int main() {
    using T = decltype(("***"));
}

T的类型是什么?

答案:字符串字面量的类型是const char[4],T的类型是const char(&)[4]。

解释:这两个看似是一个问题,其实不对,这里其实还考察了你对decltype这个关键字的了解。

字符串字面量的类型标准早有规定是const char[N],N表示大小,并且字符串字面量属于左值,那么按照decltype的规定,自然也推导出左值引用了。

C语言C99数组

c语言的这些数组并不保证c++兼容他们,看具体编译器

第一题

struct test
{
    int a;
    double b;
    char c[];
};
int main() {
    auto t = (test*)malloc(sizeof(test) + 27 * sizeof(char));
    memset(t->c, 0, 27);
    std::cout << sizeof * t << std::endl;
    for (int i = 0; i < 26; i++) {
        t->c[i] = 'A' + i;
    }
    std::cout << t->c << std::endl;
    free(t);
}

打印多少?

答案:16 ABCDEFGHIJKLMNOPQRSTUVWXYZ

解释:可能这里有很多人会有疑问,C/C++明确规定不允许定义长度为0的数组,为什么这里却可以?

柔性数组这个东西是c99添加到c语言标准的,它实际上不算什么扩展,是结构体最后成员拥有不完整的数组类型,我们上面写的是大小为0的,所以我说那种是非标准扩展,并不是说柔性数组是非标准的

柔性数组成员允许结构中包含一个大小可变的数组。柔性数组成员只作为一个符号地址存在,而且必须是结构体的最后一个成员,sizeof 返回的这种结构大小不包括柔性数组的内存。

柔性数组成员不仅可以用于字符数组,还可以是元素为其它类型的数组。包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

第二题

void foo(size_t x, int a[*]);
void foo(size_t x, int a[x])
{
    printf("%zu\n", sizeof(a)); 
}
int main(){
    size_t n=10;
    int array[n];
    foo(n,array);
}

代码是否正确?

答案:正确

解释:若大小是**,则声明是对于未指定大小的* VLA 的。这种声明只能出现于函数原型作用域,并声明一个完整类型的数组。其实,所有函数原型作用域中的 VLA 声明器都被处理成如同用***替换表达式。友情提示,这种形式只能是C,而不是C++,经测试在gcc12.2的c++环境下不行,得c。

第三题

extern int n;
int A[n];            
extern int (*p2)[n]; 
int B[100];          
void fvla(int m, int C[m][m]);

下面声明的数组,哪些错误,哪些正确

答案:第一第二错误,第三第四对

解释:非常量长度数组与从它们派生的类型(指向它们的指针,等等)被通称为“可变修改类型”( VM )。任何可变修改类型的对象只能声明于块作用域或函数原型作用域中

第四题

int main(){
    int n=10;
    static int array[n];
    extern int array_[n];
    int array__[n];
}

下面声明的数组,哪些错误,哪些正确

答案:第一第二错误,第三个正确

解释:VLA 必须拥有自动或分配存储期。指向 VLA 的指针,但不是 VLA 自身亦可拥有静态存储期。 VM 类型不能拥有链接

第五题

//test.cpp
int array[6]{ 1,2,3,4,5,6 };

//main.cpp
extern int array[];
int main() {
    for (size_t i = 0; i < 6; i++)
        std::cout << array[i] << ' ';
}

以上代码是否正确?

答案:正确

解释:若忽略数组声明器中的表达式,则它声明一个未知大小数组。除了函数参数列表中的情况(这种数组被转换成指针),而且当初始化器可用时,这种类型是一个不完整类型

第六题

int* p = new int[0]; 
delete[] p;

以上代码是否正确?

答案:正确

解释:虽然之前说过C/C++标准规定数组大小不能为0,但是在用于new[] 表达式时,数组的大小可以为零;这种数组没有元素。你可能会疑问为什么还要delete?有的时候规定就是如此,你就算是new int[0]也一样调用了operator new等,返回了地址,p也指向了某个玩意,你不用在意,反正正常人不会这么写。

第七题

int main() {
    int array[6]{};
    using T = decltype( + array);
}

T的类型是什么?

答案:T是int*类型,表达式是纯右值表达式。

解释:

存在从数组类型的左值和右值到指针类型的右值的隐式转换:它构造一个指向数组首元素的指针。凡在数组出现于不期待数组而期待指针的语境中时,均使用这个转换。

你可能会觉得这个+很奇怪,它只是为了创造期待指针的语境从而让数组转换为指针仅此而已(“TN 元素数组”或“T 的未知边界数组”类型的左值右值,可隐式转换成“指向 T 的指针”类型的纯右值),类似在对lambda也有效。

第八题

int f(char s[3]);
int f(char[]);
int f(char* s);

以上代码声明了几个函数?

答案:一个

解释:

  1. 如果类型是“T 的数组”或“T 的未知边界数组”,那么它被替换成类型“T 的指针”
  2. 如果类型是函数类型 F,那么它被替换成类型“F 的指针”
  3. 从形参类型中丢弃顶层 cv 限定符(此调整只影响函数类型,但不改动形参的性质

第九题

void f(int a[0]){}

以上代码是否正确

答案:你猜?

解释:看编译器,按道理是不行滴,实际上,谁知道呢、、、、不用在意


函数

第一题

void f3(int(void));
void f3(int());
void f3(int(*)());

以上代码声明了几个函数

答案:一个

解释:如果类型是函数类型 F,那么它被替换成类型“F 的指针”,这里F指任意函数对象,如int(),int(int,double)

第二题

struct X {
    auto operator()() {
    }
};
int main() {
    std::thread t(X());
}

t是什么?

答案:函数声明

解释:参见第一题,X()被当做了函数类型,指代返回X的函数指针


形参包展开

我们前面的题目已经使用了很多包展开了,所以我们这里只写两个例子

第一题

template<class F,class...Args>
auto f(F func,Args...args) {
    int _[] = { (func(args),0)... };
}
int main() {
    f([](auto t) {std::cout << t << ' '; }, 1, "*");
}

请问打印多少?

答案:1 *

解释:基本的形参包展开我们也就不再强调了,且如果你用的上C++17,使用折叠表达式就不需要这么丑陋的写法了,这个形参包展开后,是下面这样

int _[] = { (func(args0),0),(func(args1),0) },这里的args0指代形参包第一个元素,展开是根据前面表达式展开的,内部的小括号,按照逗号运算符就会从左到右执行,自然也就调用了我们的成员函数,这其实是为了处理函数void,得让这行代码良构,如果传递的函数不是返回void的话,那么就不需要这样写,参见下面例子

template<class F, class...Args>
auto f(F func, Args...args) {
    int _[] = { func(args)... };
}
int main() {
    f([](auto t) {std::cout << t << ' '; return 0; }, 1, "*");
}

第二题

template<class...Args>
std::initializer_list<int> f(Args...args) {
    static auto list = { args * args + args... };
    return list;
}

int main() {
    for (auto ret = f(1,2,3,4,5); const auto & i : ret) {
        std::cout << i << ' ';
    }
}

请问打印多少

答案:2 6 12 20 30

解释:已经说过很多遍了,无非就是一个包展开,根据前面的表达式进行一个展开,我们这里就是args * args + args是一个表达式,每次替换为形参包里的对象进行操作,展开之后类似下面这样

{(__args0 * __args0) + __args0, (__args1 * __args1) + __args1, (__args2 * __args2) + __args2, (__args3 * __args3) + __args3, (__args4 * __args4) + __args4})

并发支持库

第一题

实现std::lock_guard

答案:你要嘛直接看标准库抄一遍,要嘛看了视频自己写,up习惯很不好,写过的代码都直接删,那就复制标准库了

template <class _Mutex>
class _NODISCARD_LOCK lock_guard { // class with destructor that unlocks a mutex
public:
    using mutex_type = _Mutex;

    explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
        _MyMutex.lock();
    }

    lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // construct but don't lock

    ~lock_guard() noexcept {
        _MyMutex.unlock();
    }

    lock_guard(const lock_guard&)            = delete;
    lock_guard& operator=(const lock_guard&) = delete;

private:
    _Mutex& _MyMutex;
};

第二题

实现std::scoped_lock

答案:你嘛吗直接看标准库抄一遍,要嘛看了视频自己写,up不保存以前的代码,直接复制

template <class... _Mutexes>
class _NODISCARD_LOCK scoped_lock { // class with destructor that unlocks mutexes
public:
    explicit scoped_lock(_Mutexes&... _Mtxes) : _MyMutexes(_Mtxes...) { // construct and lock
        _STD lock(_Mtxes...);
    }

    explicit scoped_lock(adopt_lock_t, _Mutexes&... _Mtxes) : _MyMutexes(_Mtxes...) {} // construct but don't lock

    ~scoped_lock() noexcept {
        _STD apply([](_Mutexes&... _Mtxes) { (..., (void) _Mtxes.unlock()); }, _MyMutexes);
    }

    scoped_lock(const scoped_lock&)            = delete;
    scoped_lock& operator=(const scoped_lock&) = delete;

private:
    tuple<_Mutexes&...> _MyMutexes;
};

template <class _Mutex>
class _NODISCARD_LOCK scoped_lock<_Mutex> {
public:
    using mutex_type = _Mutex;

    explicit scoped_lock(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
        _MyMutex.lock();
    }

    explicit scoped_lock(adopt_lock_t, _Mutex& _Mtx) : _MyMutex(_Mtx) {} // construct but don't lock

    ~scoped_lock() noexcept {
        _MyMutex.unlock();
    }

    scoped_lock(const scoped_lock&)            = delete;
    scoped_lock& operator=(const scoped_lock&) = delete;

private:
    _Mutex& _MyMutex;
};

template <>
class scoped_lock<> {
public:
    explicit scoped_lock() {}
    explicit scoped_lock(adopt_lock_t) {}
    ~scoped_lock() noexcept {}

    scoped_lock(const scoped_lock&)            = delete;
    scoped_lock& operator=(const scoped_lock&) = delete;
};

第三题

int main() {
    std::promise<void>read;
    std::future<void>fu = read.get_future();
    std::thread t{ [&] {
        fu.wait();//阻塞
        std::cout << "乐\n";
    } };
    std::cout << "main\n";
    read.set_value();//必须这行执行之后,wait()才能执行
    t.join();
}

以上代码打印什么?

答案: main 乐

解释:参见注释或视频


成员函数

成员函数

第一题

struct X {
    void f()const { std::cout << "const\n"; }
};
int main() {
    X x;
    x.f();
    std::move(x).f();
}

以上 是否正确,如果正确,打印什么?

答案:打印两个const,当然正确,没有任何理由不正确,如果有,你最好给我详细的写出你的错误观点!!!!

第二题

struct X {
    void f()const& { std::cout << "const\n"; }
};

int main() {
    X x;
    x.f();
    std::move(x).f();
}

以上 是否正确,如果正确,打印什么?

答案:正确,打印两个const

解释:你可以理解为成员函数第一个参数是有一个隐式的参数,它的类型默认是T&,给成员函数const的话,实际上也就是给第一个形参,const T&这种形式,自然可以接收左值右值,你可以理解为调用成员函数是需要隐式传参的,就是调用者。

当然,我知道有人会说修饰this指针什么的,其实我是一个意思,的确是修饰this,只是我把this当引用说了,而不是指针

第三题

struct S{
    int n{};
    void f()const& { std::cout << "const&\n"; }
    void f()volatile& { std::cout << "volatile&\n"; }
    void f()const volatile& { std::cout << "const volatile&\n"; }
    void f()& { std::cout << "&\n"; }
    void f()&& { std::cout << "&&\n"; }
    void f()const&& { std::cout << "const &&\n"; }
    void f()volatile&& { std::cout << "volatile &&\n"; }
    void f()const volatile&& { std::cout << "const volatile &&\n"; }
};
int main(){
    S s;
    s.f();
    std::move(s).f();
    S().f();
}

以上代码是否正确?如果正确,打印什么?

答案:& && &&

C++23显式对象形参

视频都有讲

第一题

struct X {
    void f(this X x) {
        std::cout << "f\n";
    }
    void f2(this X& x) {
        std::cout << "f2\n";
    }
};
int main() {
    const X x;
    x.f();
    x.f2();
}

以上代码是否正确?如果正确,打印什么?

答案:x.f2()不正确

解释:

  1. 以前使用隐式对象实参,就是普通的那种成员函数调用,默认都是T&这种,const的对象显然是没办法调用的,就如同void(int&)函数类型不能传入const int类型的对象一样。
  2. 但是显示对象形参之后,我们更加自由了,就比如上面的f(),它的调用之所以正确,其实也很简单,如同void(int){}函数类型可以传入const int类型的对象一样
  3. f2这个显式形参类型是类似于加上const修饰成员函数的隐式形参的,所以错误原因也和第一点的解释一样

第二题

struct foo {
    template<class Self>
    void bar(this Self&& self) {
        std::cout << "bar\n";
    }

    template<>
    void bar(this foo& self) {
        std::cout << "bar &\n";
    }
    template<>
    void bar(this const foo& self) {
        std::cout << "const bar &\n";
    }
    template<>
    void bar(this foo&& self) {
        std::cout << "bar &&\n";
    }
    template<>
    void bar(this const foo&& self) {
        std::cout << "const bar &&\n";
    }
};

int main() {
    foo a;
    a.bar();
    std::move(a).bar();

    const foo b;
    b.bar();
    std::move(b).bar();
}

以上代码是否正确?如果正确,打印什么?

答案:

bar & bar && const bar & const bar &&

解释:对于成员函数模板,显式对象形参的类型和值类别可以被推导,因此该语言特性也被称为“推导 this”

第三题

struct X {
    int n{};
    void plus(this X x) {
        x.n++;
    }
};
int main() {
    X x;
    x.plus();
    auto p = &X::plus;
    p(x);
    std::cout << x.n << '\n';
}

以上代码是否正确?如果正确,打印什么?

答案:0

解释:就很单纯的,按照正常函数调用理解就好,这是按值传递的,自然修改也不会修改到原来的对象,除非是引用,我知道这并不符合大家的一般直觉,因为在C++23前,隐式对象实参一直是引用的

第四题

int main() {
    auto p = [n = 0](this auto self, auto f, auto x) {
        f(x);
        self.n++;
        std::cout << self.n << '\n';
    };
    p([](auto x) {std::cout << x << '\n'; }, 10);
    p([](auto x) {std::cout << x << '\n'; }, "*");
}

以上代码是否正确?如果正确,打印什么?

答案:

10 1 * 1

解释:lambda是类,这个()实际是它的operator(),使用显式对象形参当然没问题,关于显式对象形参的类型,我们只能用auto占位,这很正常,关于结果,10*都是传递的lambda打印结果,没什么好说的,但是为什么是两个都是1呢?第三题已经说过了,显式对象形参不是引用,除非写成this auto& self的形式

杂项

第一题

const std::string& f(const std::string& str) {
    return str;
}
int main() {
    auto& ret = f("哈哈");
    std::cout << ret << '\n';
}

以上代码是否正确?

答案:实际上运行是有问题的,我们没办法打印出哈哈

解释:这个代码看起来没问题,实际上问题大了,首先我们传递这个字符串字面量作为参数,实际上会先在当前作用域构造出一个纯右值的临时对象,然后再传递,函数形参是const std::string& str接纯右值表达式是没问题。但是它最后还想返回这个对象的引用,就不对了,return 语句中绑定到函数返回值的临时量不会被延续:它在返回表达式的末尾立即销毁。这种 return 语句始终返回悬垂引用。

第二题

void f(int n=1)try
{
    int n{ 6 };
}catch(...){}

int main() {
    f();
}

以上代码是否正确?

答案:正确

解释:函数形参(包括 lambda 表达式的形参)或函数局部预定义变量的潜在作用域开始于其声明点。

第三题

struct X {
    void* operator new (size_t size) = delete;
};
int main() {
    X* x = ::new X;
}

以上代码是否正确?

答案:正确

解释:使用了::有限定查找,查找到全局的operator new,而不是成员里显式删除的

第四题

void t(int){}
void t(double) = delete;

int main() {
    t(1.);
}

以上代码是否正确?

答案:不正确

解释:形参为double的版本被显式的删除了,不能够隐式转换,很常见的写法

第五题

int main() {
    std::vector v{ 1,2,3,4,5 };
    std::function f([](int& i) { i = i * i; });
    std::function f2([](int& i) {i = i + i; });
    std::function f3([](int i) {std::cout << i << ' '; });
    v | f | f2 | f3;
    std::cout << '\n';

    v | [](int& i) { i = i * i; } | [](int i) {std::cout << i << ' '; };

    std::cout << '\n';
    for (auto i : v | [](int& i) {i = i / 10; }) {
        std::cout << i << ' ';
    }
}

根据以上使用代码,实现一个管道运算符

答案:

template<typename U, typename F>
    requires std::regular_invocable<F, U&>
std::vector<U>& operator | (std::vector<U>& vl, F f) {
    for (auto& i : vl) {
        f(i);
    }
    return vl;
}

第六题

template<class Ty,size_t size>
struct array {
    Ty* begin() { return arr; };
    Ty* end() { return arr + size; };
    Ty arr[size];
};
int main() {
    ::array arr{1, 2, 3, 4, 5};
    for (const auto& i : arr) {
        std::cout << i << ' ';
    }
}

给出以上代码,请为模板类array添加推导指引,让main中代码合法

答案:

template<typename Tp, typename... Up>
array(Tp, Up...)->array<std::enable_if_t<(std::is_same_v<Tp, Up>&& ...), Tp>, 1 + sizeof...(Up)>;

总结

如果你对答案有什么高明的见解,或者发现一些错误,欢迎评论或修改讨论

   
分类:C/C++ 作者:开发喵 发表于:2023-09-12 14:30:41 阅读量:114
<<   >>


powered by kaifamiao