马上就大三了,网络工程专业,以前的时间都被玩掉了,对 C++ 唯一的了解只有大一老师教的和我买的谭浩强的 C++ 书。人家都说要真正学好 C++ 很难,我现在的想法就是想靠这个混饭吃,所以我想问如果从现在开始我拼命学 C++,到大学毕业时能学到什么程度。
我也不是它太难我就不会去努力了,我不怕吃苦,因为现在感觉大学什么都不会,感到毕业前途迷茫,就想向各位大神咨询咨询,如果我现在玩命地学,能搞定它吗?
目前总计75题,所有的题目都在视频有讲述,我可能会提起让你看哪个博客或视频更加详细
一些提问sizeof的结果,我们只是取一般的x64情况,因为硬要扯,显然可能性太多了,没法假设int的size,假设空类的size,不用在意,我们就讲一般实现下sizeof的结果
有些涉及多个编译器不一样的问题自行考虑,还有不同版本的相同编译器结果不同的,对有些标准规定的遵守问题,都参见ISO
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
解释:
[=]
,但是其实写不写都无所谓,反正这个作用域就一个静态局部变量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
解释:
int main() {
const int N = 10;
auto p =[=] {
int p = N;
};
std::cout << sizeof p << '\n';
}
请问打印多少?
答案:msvc :4 gcc:1 clang:1
解释:我最终是认为两点想法,当然,这是可以讨论的
int main() {
const float a = 6;
[] {
std::cout << a << '\n';
}();
}
能否正常编译?打印多少?
答案:不能正常编译
解释:
constexpr
的且没有 mutable 成员。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++17
p的类型是什么?
答案:一般来说void (*)(void) noexcept
解释:
+
第一题已经讲过,至于类型的话,如果是msvc用typeid
,还会扯到调用约定,我们自己知道这回事就好了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
解释:
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) {}
给出以上代码,是否正确?
答案:错误
解释:
C::f
和C::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
定义的吗?
答案:不是
解释:遵循聚合初始化的规则初始化 array
,std::array
是聚合体
#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 全局
解释:
f()
**的时候,它被判定为非待决名,于是在检查该模板定义的时候,也就是没实例化的时候就查找名字,没找到的话就报错未定义,但是全局有一个f,就找到那个了。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]
解释:
T
所需的最坏隐式转换序列。T
所需的最坏隐式转换序列。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的规定,自然也推导出左值引用了。
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*
类型,表达式是纯右值表达式。
解释:
存在从数组类型的左值和右值到指针类型的右值的隐式转换:它构造一个指向数组首元素的指针。凡在数组出现于不期待数组而期待指针的语境中时,均使用这个转换。
你可能会觉得这个+
很奇怪,它只是为了创造期待指针的语境从而让数组转换为指针仅此而已(“T
的 N
元素数组”或“T
的未知边界数组”类型的左值或右值,可隐式转换成“指向 T
的指针”类型的纯右值),类似在对lambda也有效。
int f(char s[3]);
int f(char[]);
int f(char* s);
以上代码声明了几个函数?
答案:一个
解释:
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();
}
以上代码是否正确?如果正确,打印什么?
答案:&
&&
&&
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()
不正确
解释:
T&
这种,const的对象显然是没办法调用的,就如同void(int&)
函数类型不能传入const int
类型的对象一样。f()
,它的调用之所以正确,其实也很简单,如同void(int){}
函数类型可以传入const int
类型的对象一样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)>;
如果你对答案有什么高明的见解,或者发现一些错误,欢迎评论或修改讨论
powered by kaifamiao