Skip to content

Sunday, 26 May 2024

Journey to Vietnam

Embarking on my journey to FOSSASIA 2024, I felt a mix of excitement and anticipation. As I boarded my flight, the reality of the adventure ahead began to sink in. I arrived early in the morning on April 7th, filled with enthusiasm and a bit of jet lag, ready to dive into the vibrant culture of Vietnam and the exhilarating events planned for the conference.

Arrival in Hanoi

Landing in Hanoi, I was greeted by the warm, humid air and a bustling airport scene. I quickly hopped onto Bus Express 86, which conveniently took me from the airport to the heart of the city. The ride itself was a mini-tour, offering glimpses of Hanoi’s unique blend of traditional and modern architecture. My destination was Hotel LakeSide, where I was warmly welcomed and offered an early check-in at no extra cost—a gesture that felt like a blessing after a long flight. The hotel staff’s generosity allowed me to freshen up and catch a few hours of much-needed sleep before the scheduled Hanoi City Tour organized by FOSSASIA.

A special shout-out goes to Lily’s Travel Agency for arranging an amazing stay throughout the conference days. For transportation, I relied on Grab, a widely used cab application in Vietnam similar to Uber. Despite the language barrier, the local drivers were exceptionally supportive and always willing to go the extra mile, which made commuting around the city a breeze.

Exploring Hanoi

By 2 pm, I was ready for the city tour, excited to explore Hanoi’s rich history and culture. Our itinerary included visits to the One Pillar Pagoda, the Temple of Literature & National University, and the Imperial Citadel of Thang Long. Each site was more breathtaking than the last, steeped in history and surrounded by lush greenery. It was a day filled with fun, laughter, and new friendships. We shared stories, took countless photos, and even indulged in some silly antics that added a touch of whimsy to the day (try to find it in the photos!).

After the tour concluded around 7 pm, we split into groups to find dinner. I joined a few new friends for a delicious meal at a local restaurant, savoring the flavors of authentic Vietnamese cuisine. Later, I met Phu Nguyen, a KDE contributor from Hanoi, to collect a monitor for our booth. Phu, who works in Germany, couldn’t attend the event but was incredibly helpful in providing the display. With the monitor in hand, I returned to my hotel, reflecting on a day well spent and eagerly anticipating the start of the conference the next day.

Conference Kickoff

Each morning began with a hearty breakfast at the hotel, overlooking the serene Giang Vo Lake. Armed with hardware, promotional materials, and stickers, I set off for the Posts and Telecommunications Institute of Technology, where FOSSASIA 2024 was held. The venue buzzed with excitement, as hundreds of students and tech enthusiasts gathered around, curious about the event.

Our Booth: The Focal Point

Our booth, strategically placed next to FSFE, COSCUP, and CalyxOS, quickly became the busiest spot at the venue. With the invaluable help of Aniqa and Paul from the KDE promo team, we had an impressive setup that drew in crowds continuously. Aniqa and Paul were instrumental in organizing the booth, ensuring we had everything we needed, and providing ongoing support throughout the event. Tomaz joined us on the first day, bringing a reMarkable tablet and his personal laptop to enhance our setup. Our booth was a vibrant hub of activity, attracting attendees with emulations of Nintendo games and Tomaz’s impressive origami skills. He exchanged his intricate origami creations for discussions about KDE, engaging many curious students.

Engaging with the Community

The conference was a vibrant hub of activity, featuring organizations such as FreeCAD, AlmaLinux, and fossunited. It was inspiring to see the diversity of projects and the passion driving each community. Our booth quickly became the most popular, with attendees lining up to learn about KDE’s latest projects and initiatives. Despite most attendees being college students with limited funds, their enthusiasm for KDE was overwhelming. Tomaz’s origami talent truly stood out, drawing significant attention and sparking numerous conversations about open-source software and community contributions.

Walking through the venue, I marveled at the variety of booths and the innovative projects on display. The FreeCAD team showcased their latest developments in open-source CAD software, while AlmaLinux representatives engaged attendees with their enterprise-grade Linux distribution. fossunited’s booth was a hive of activity, emphasizing their mission to promote and support open-source projects in India. Each interaction was a learning opportunity, and the camaraderie among the open-source communities was palpable.

Evening Get-Togethers

Evenings were spent exploring the local culture, including a memorable Evening Get Together at Ta Hien Beer and Food Street. The lively atmosphere, coupled with delicious street food and refreshing drinks, made for perfect networking opportunities and deepened the bonds formed during the day. The conference days were a whirlwind of activity, leaving us with lasting impressions and numerous connections.

Sapa Valley Trek and Farewell

After the conference, I ventured to Sapa Valley for a three-day trek. The journey was a stark contrast to the bustling city, offering a tranquil escape into nature. Walking among the rice paddies, often alongside local farmers, and soaking in the serene landscape was a rejuvenating experience. The trek left me with fond memories of the picturesque valley, the warmth of the local people, and the breathtaking beauty of Vietnam’s countryside.

I returned on April 15th, with a heart full of memories and a mind brimming with inspiration from FOSSASIA 2024. The conference not only highlighted the incredible work being done in the open-source community but also showcased the rich culture and hospitality of Vietnam. Reflecting on the experience, I felt a deep sense of gratitude and excitement for the future.

I want to extend my heartfelt thanks to KDE e.V. for sponsoring my attendance at this event. Their support made this enriching experience possible, and I am profoundly grateful for the opportunity to represent the KDE Community at FOSSASIA 2024. Until next time, FOSSASIA!

Gallery

Saturday, 25 May 2024

Hindi Translation of Merkuro - Season of KDE 2024 In this blog, I will discuss my experience with season of KDE 2024. I came to know about this program through a youtube video. I was given some pre tasks to complete by the mentors of the project which can be found at https://github.com/officialasishkumar/translation-kde-applications. After completing the pre-tasks, I applied for the project and eventually got selected. During my SoK period, I have to translate Merkuro, KDE connect, KTorrent and Cantor in Hindi.

Friday, 24 May 2024

Plasma Wayland Protocls 1.13.0 is now available for packaging.

This adds features needed for the Plasma 6.1 beta.

URL: https://download.kde.org/stable/plasma-wayland-protocols/
SHA256: dd477e352f5ff6e6ac686286c4b22b19bf5a4921b85ee5a7da02bb7aa115d57e
Signed by: E0A3EB202F8E57528E13E72FD7574483BB57B18D Jonathan Esk-Riddell jr@jriddell.org

Full changelog:

  • plasma-window-management: add a stacking order object
  • output device, output management: add brightness setting
  • outputdevice,outputconfiguration: add a way to use the EDID-provided color profile
  • Enforce passing tests
  • output device, management: change the descriptions for sdr gamut wideness

Wednesday, 22 May 2024

gcompris 4.1

Today we are releasing GCompris version 4.1.

It contains bug fixes and graphics improvements on multiple activities.

It is fully translated in the following languages:

  • Arabic
  • Bulgarian
  • Breton
  • Catalan
  • Catalan (Valencian)
  • Greek
  • Spanish
  • Basque
  • French
  • Galician
  • Croatian
  • Hungarian
  • Italian
  • Lithuanian
  • Malayalam
  • Dutch
  • Norwegian Nynorsk
  • Polish
  • Brazilian Portuguese
  • Romanian
  • Russian
  • Slovenian
  • Swedish
  • Turkish
  • Ukrainian

It is also partially translated in the following languages:

  • Azerbaijani (97%)
  • Belarusian (86%)
  • Czech (95%)
  • German (95%)
  • UK English (95%)
  • Esperanto (99%)
  • Estonian (95%)
  • Finnish (94%)
  • Hebrew (95%)
  • Indonesian (99%)
  • Macedonian (90%)
  • Portuguese (95%)
  • Slovak (83%)
  • Albanian (99%)
  • Swahili (99%)
  • Chinese Traditional (95%)

You can find packages of this new version for GNU/Linux, Windows, Android, Raspberry Pi and macOS on the download page. This update will also be available soon in the Android Play store, the F-Droid repository and the Windows store.

Thank you all,
Timothée & Johnny

Friday, 17 May 2024

强制类型转换

C++提供了四个强制类型转换的关键字:

  • static_cast
  • const_cast
  • reinterpret_cast
  • ``dynamic_cast`

static_cast

1
static_cast<目标类型>(表达式)
1
2
int num = 2;
double result =static_cast<double>(num);

该运算符将表达式转换为目标类型。但没有进行运行时类型检查来保证转换的安全性

主要用法

  1. 用于类层次结构中父类和子类之间指针或引用的转换.进行上行转换是安全的(即将子类的指针或引用转换成父类是正确的);进行下行转换的时候,由于没有动态类型检查,所以是不安全的。继承必须为public
  2. 用于基本类型之间的转换,如intchar安全性也需要程序员来保证
  3. 把空指针转换为目标类型的空指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Person
{
public:
void print()
{
cout << "Person " << endl;
}
};

class Son : public Person
{
public:
void print()
{
cout << "Son " << endl;
}
};

void print1(Person *p)
{
p->print();
}

int main()
{
Son s;
print1(static_cast<Person *>(&s));
return 0;
}

const_cast

const_cast是c++中专用于处理与const相关的强制类型转换的关键字

其功能为:为一个变量重新设定其const描述.

即:const_cast可以为一个变量强行增加或删除其const限定.

需要明确的是,即使用户通过const_cast强行去除了const属性,也不代表当前变量从不可变变为了可变。const_cast只是使得用户接管了编译器对于const限定的管理权,故用户必须遵守“不修改变量”的承诺。如果违反此承诺,编译器也不会因此而引发编译时错误,但可能引发运行时错误。

  1. const_cast可用于更改const成员函数内的非const类成员。
  2. const_cast可用于将const数据传递给不接收const的函数。
  3. const_cast<>里边的内容必须是引用或者指针。
  4. const_cast也可以用来抛弃volatile__unaligned属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Student
{
private:
int roll;
public:
Student(int r) :roll(r) {}
void fun() const
{
(const_cast<Student*> (this))->roll = 5;
}
int getRoll() { return roll; }
};

int main() {
Student student(3);
std::cout << "Old roll number: " << student.getRoll() << std::endl;
student.fun();
std::cout << "New roll number: " << student.getRoll() << std::endl;

// const_cast只能调节类型限定符,不能更改基础类型
int a1 = 40;
//const int* b1 = &a1;
//char* c1 = const_cast <char*> (b1); // 编译程序时出错

const volatile int* d1 = &a1;
std::cout << "typeid of d1 " << typeid(d1).name() << '\n'; // int const volatile *
int* e1 = const_cast <int*> (d1);
std::cout << "typeid of e1 " << typeid(e1).name() << '\n'; // int *

return 0;
}

在const成员函数fun()中,编译器将“this”视为“ const student const this”,即“this”是指向常量对象的常量指针,因此编译器不允许通过以下方式更改数据成员“这个”指针。const_cast将“this”指针的类型更改为“student const this”

const_cast比简单类型转换更安全。从某种意义上讲,如果强制类型与原始对象不相同,则强制转换不会发生,这是比较安全的。

1
2
3
int a=20;
const int *p=&a;
char *c1=const_cast<char*> (p);//编译时程序出错

reinterpret_cast

reinterpret,即重新解释.

该强制类型转换的作用是提供某个变量在底层数据上的重新解释.

当我们对一个变量使用reinterpret_cast后,编译器将无视任何不合理行为,强行将被转换变量的内存数据重解释为某个新的类型。用于进行各种不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换。转换时,执行的是逐个比特复制的操作。 它不检查指针类型和指针所指向的数据是否相同。

需要注意的是,reinterpret_cast要求转换的两个数据所占用的内存大小一致,否则会引发编译时错误.

1
data_type *var_name = reinterpret_cast <data_type *>(pointer_variable);

使用 reinterpret_cast 的目的:

  1. reinterpret_cast是一种非常特殊且危险的类型转换操作符。并且建议使用适当的数据类型使用它,即(指针数据类型应与原始数据类型相同)。
  2. 它可以将任何指针类型转换为任何其他数据类型。
  3. 当我们要使用位时使用它。
  4. 它仅用于将任何指针转换为原始类型。
  5. 布尔值将转换为整数值,即0表示false,1表示true。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class A {
public:
int a;
A(int i) :a(i) {}
void fun_a()
{
std::cout << "In class A\n";
}
};

class B {
public:
int b;
B(int i) :b(i) {}
void fun_b()
{
std::cout << "In class B\n";
}
};

void testReinterpretCast() {
B *x = new B(5);
A* y = reinterpret_cast<A*>(x);
y->fun_a(); // In class A
std::cout << y->a << std::endl; // 5
}

dynamic_cast

dynamic用于在运行时实现向下类型转换。

1
dynamic_cast <type-id> (expression)

expression转换为type-id类型,type-id必须是类的指针,类的引用或者是void,

如果type-id是一个指针,那么expression也是一个指针,是引用的话同为引用

特点如下:

  1. 它是在运行是进行处理的,其余三个都是在编译时完成. 运行时进行类型检查
  2. 不能用于内置的基本数据类型之间的强制转换
  3. dynamic_cast 要求 <> 内所描述的目标类型必须为指针或引用。dynamic_cast 转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回 nullptr
  4. 在类的转换时,在类层次上进行向上转换(子类指针指向父类指针),与static_cast的效果是一样的。在进行父类指针向子类指针的转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
  5. 向下转换的成功与否还与将要转换的类型有关,即要转换的指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败。在C++中,编译期的类型转换有可能会在运行时出现错误,特别是涉及到类对象的指针或引用操作时,更容易产生错误。dynamic_cast操作符则可以在运行期对可能产生问题的类型转换进行测试。
  6. 使用 dynamic_cast 进行转换的,基类中一定要有虚函数,否则编译不通过(类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义)。这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表中,只有定义了虚函数的类才有虚函数表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class AA {
public:
virtual void print() {
cout << "in class AA" << endl;
};
};

class BB :public AA {
public:
void print() {
cout << "in class BB" << endl;
};
};

void testDynamicCast() {
AA* a1 = new BB; // a1是A类型的指针指向一个B类型的对象
AA* a2 = new AA; // a2是A类型的指针指向一个A类型的对象

BB* b1, * b2, * b3, * b4;

b1 = dynamic_cast<BB*>(a1);// not null,向下转换成功,a1 之前指向的就是 B 类型的对象,所以可以转换成 B 类型的指针。
if (b1 == nullptr)
cout << "b1 is null" << endl;
else
cout << "b1 is not null" << endl;

b2 = dynamic_cast<BB*>(a2);// null,向下转换失败
if (b2 == nullptr)
cout << "b2 is null" << endl;
else
cout << "b2 is not null" << endl;

// 用 static_cast,Resharper C++ 会提示修改为 dynamic_cast
b3 = static_cast<BB*>(a1);// not null
if (b3 == nullptr)
cout << "b3 is null" << endl;
else
cout << "b3 is not null" << endl;

b4 = static_cast<BB*>(a2);// not null
if (b4 == nullptr)
cout << "b4 is null" << endl;
else
cout << "b4 is not null" << endl;

a1->print();// in class BB
a2->print();// in class AA

b1->print();// in class BB
//b2->print(); // null 引发异常
b3->print();// in class BB
b4->print();// in class AA
}

详细

Friday, 10 May 2024

Lambda表达式

匿名函数是很多高级语言都支持的概念,如lisp语言在1958年首先采用匿名函数。匿名函数有函数体,但没有函数名。C++11中引入了lambda表达式。利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,并且使代码更可读。但是从本质上来讲,lambda表达式只是一种语法糖,因为所有其能完成的工作都可以用其它稍微复杂的代码来实现。但是它简便的语法却给C++带来了深远的影响。如果从广义上说,lamdba表达式产生的是函数对象。

相同类似功能我们也可以使用函数对象或者函数指针实现:函数对象能维护状态,但语法开销大,而函数指针语法开销小,却没法保存范围内的状态。lambda表达式正是结合了两者的优点。

声明Lambda表达式

1
2
3
4
5
6
7
// 完整语法
[capture list] (params list) mutable(optional) constexpr(optional)(c++17) exception attribute -> return type { function body };

// 可选的简化语法
[capture list] (params list) -> return type {function body}; //1
[capture list] (params list) {function body};//2
[capture list] {function body};//3
  • capture list:捕获外部变量列表,不能省略;
  • params list:形参列表,可以省略(但是后面必须紧跟函数体);
  • mutable指示符: 可选,将lambda表达式标记为mutable后,函数体就可以修改传值方式捕获的变量;
  • constexpr:可选,C++17,可以指定lambda表达式是一个常量函数;
  • exception:异常设定, 可选,指定lambda表达式可以抛出的异常;
  • attribute:可选,指定lambda表达式的特性;
  • return type:返回类型
  • function body:函数体

标号1. 函数声明了一个const类型的表达式,此声明不可改变capture list中的捕获的值。

标号2. 函数省略了返回值,此时如果function body内含有return语句,则按return语句返回类型决定返回值类型,若无则返回值为void类型。

标号3. 函数无参数列表,意味无参函数。

1
2
3
vector<int> vec{1,0,9,5,3,3,7,8,2};

sort(lbvec.begin(), lbvec.end(), [](int a, int b) -> bool { return a < b; });

捕获外部变量

lambda表达式最前面的方括号的意义何在?其实这是lambda表达式一个很Hong要的功能,就是闭包。这里我们先讲一下lambda表达式的大致原理:每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。

Lambda表达式可以捕获外面变量,但需要我们提供一个谓词函数([capture list]在声明表达式最前)。类似参数传递方式:值传递、引入传递、指针传递。在Lambda表达式中,外部变量捕获方式也类似:值捕获、引用捕获、隐式捕获

值捕获

1
2
3
4
5
int a = 123;
auto f = [a] { cout << a << endl; };
f(); // 输出:123
a = 321;
f(); // 输出:123

值捕获和参数传递中的值传递类似,被捕获的值在Lambda表达式创建时通过值拷贝的方式传入,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。因此Lambda表达式函数体中不能修改该外部变量的值; 因为函数调用运算符的重载方法是const属性的。同样,函数体外对于值的修改也不会改变被捕获的值。 想改动传值方式捕获的值,那么就要使用mutable

1
2
3
auto add_x = [x](int a) mutable { x *= 2; return a + x; };  // 复制捕捉x

cout << add_x(10) << endl; // 输出:30

因为一旦将lambda表达式标记为mutable,那么实现的函数调用运算符是非const属性的。

引用捕获

1
2
3
4
int a = 123;
auto f = [&a] { cout << a << endl; };
a = 321;
f(); // 输出:321

引用捕获的变量使用的实际上就是该引用所绑定的对象,因此引用对象的改变会改变函数体内对该对象的引用的值。 对于引用捕获方式,无论是否标记mutable,都可以在lambda表达式中修改捕获的值。

隐式捕获

隐式捕获有两种方式,分别是 [=]:以值补获的方式捕获外部所有变量 [&]:表示以引用捕获的方式捕获外部所有变量

1
2
3
int a = 123, b=321;
auto df = [=] { cout << a << b << endl; }; // 值捕获
auto rf = [&] { cout << a << b << endl; }; // 引用捕获

其他

捕获外部变量形式
[ ]不捕获任何变量(无参函数)
[变量1,&变量2, …]值(引用)形式捕获指定的多个外部变量
[this]值捕获this指针
[=, &x]变量x以引用形式捕获,其余变量以传值形式捕获
[*this]通过传值方式捕获当前对象
[&, x]默认以引用捕获所有变量,但是x是例外,通过值捕获

既然只使用一次,那直接写全代码不就行了,为啥要函数呢?——因为lambda可以捕获局部变量

在上面的捕获方式中,注意最好不要使用[=][&]默认捕获所有变量。首先说默认引用捕获所有变量,你有很大可能会出现悬挂引用(Dangling references),因为引用捕获不会延长引用的变量的声明周期:

1
2
3
4
std::function<int(int)> add_x(int x)
{
return [&](int a) {return x+a;};
}

因为参数x仅是一个临时变量,函数调用后就被销毁,但是返回的lambda表达式却引用了该变量,但调用这个表达式时,引用的是一个垃圾值,所以会产生没有意义的结果。如果通过传值的方式来解决上面的问题:

1
2
3
4
std::function<int(int)> add_x(int x)
{
return [=](int a) { return x + a; };
}

使用默认传值方式可以便面悬挂引用问题.但是采用默认值捕获所有变量仍然有风险。例如当在类中捕获私有变量,当返回值为lambda表达式时,无法捕获到私有变量,但当指定为[=]时,会捕获到this指针的副本,当类已经调用析构函数,使用该指针仍然不安全.

参数

  • 参数列表中不能有默认参数
  • 不支持可变参数
  • 所有参数必须要参数名(相当于不可以有占位参数)

返回类型

单一的return语句可以推断返回类型;多语句则默认返回void,否则报错,应指定返回类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 正确,单一return语句
transform(vi.begin(),vi.end(),vi.begin(),
[] (int i) {
return i<0? -i; i;
}
);
// 错误。不能推断返回类型
transform(vi.begin(),vi.end(),vi.begin(),
[] (int i) {
if (I<0) return -i;
else return i;
}
);
// 正确,尾置返回类型
transform(vi.begin(),vi.end(),vi.begin(),
[] (int i) ->int{
if (I<0)
return -i;
else
return i;
}
);

赋值

auto和function可接受lambda表达式的返回:

1
2
3
int x =8,y=9;
auto add = [](int a,int b){return a+b;};
std::function<int(int,int)> Add = [=] (int a,int b){return a+b};

lambda表达式产生的类不含有默认构造函数、赋值运算符、默认析构函数。至于闭包类中是否有对应成员,C++标准中给出的答案是:不清楚的,看来与具体实现有关。还有一点要注意:lambda表达式是不能被赋值的:

1
2
3
4
5
auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };

a = b; // 非法,lambda无法赋值
auto c = a; // 合法,生成一个副本

因为禁用了赋值操作符:

1
ClosureType& operator=(const ClosureType&) = delete;

但是没有禁用复制构造函数,所以可以用一个lambda表达式去初始化另外一个lambda表达式而产生副本。并且lambda表达式也可以赋值给相对应的函数指针,这也使得你完全可以把lambda表达式看成对应函数类型的指针

新特性

C++14中,lambda又得到了增强,一个是泛型lambda表达式,一个是lambda可以捕捉表达式。

lambda捕捉表达式

lambda表达式可以按复制或者引用捕获在其作用域范围内的变量。而有时候,我们希望捕捉不在其作用域范围内的变量,而且最重要的是我们希望捕捉右值。所以C++14中引入了表达式捕捉,其允许用任何类型的表达式初始化捕捉的变量:

1
2
3
4
5
6
7
8
// 利用表达式捕获,可以更灵活地处理作用域内的变量
int x = 4;
auto y = [&r = x, x = x + 1] { r += 2; return x * x; }();
// 此时 x 更新为6,y 为25

// 直接用字面值初始化变量
auto z = [str = "string"]{ return str; }();
// 此时z是const char* 类型,存储字符串 string

可以看到捕捉表达式扩大了lambda表达式的捕捉能力,有时候你可以用std::move初始化变量。这对不能复制只能移动的对象很重要,比如std::unique_ptr,因为其不支持复制操作,你无法以值方式捕捉到它。但是利用lambda捕捉表达式,可以通过移动来捕捉它:

1
2
3
4
auto myPi = std::make_unique<double>(3.1415);

auto circle_area = [pi = std::move(myPi)](double r) { return *pi * r * r; };
cout << circle_area(1.0) << endl; // 3.1415

原文章

Friday, 3 May 2024

可变参数模板

可变参数模板是C++11引入的一个特性,它允许定义一个接受任意数量参数的模板.

在函数模板或类模板中,我们可以使用可变参数模板来定义接受任意数量参数的函数或类。可变参数模板使用了模板参数包(template parameter pack),它允许我们将零个或多个模板参数打包成一个参数集合。

1
2
3
4
5
6
7
8
9
template <typename T>
T sum(T arg){
return arg;
}

template<typename T,typename...Args>
T sum(T first ,Args...res){
return first+sum(rest...);
}

尾部类型推导

当需要使用尾部类型推导的时候,通常是因为函数的返回类型依赖于函数参数或其他的上下文.

1
2
3
4
5
6
7
8
9
10
11
12
auto add=[](int x,int y)->int {
return x+y;
}
auto add=[](int x,int y)->auto
{
return x+y;
}
//函数模板
template<typename T, typename U>
auto multiply(T x, U y) -> decltype(x * y) {
return x * y;
}

std::accumulate

std::accumulate 是 C++ 标准库 <numeric> 头文件中提供的一个算法函数,用于计算给定范围内的元素的总和。它接受一个起始迭代器和一个终止迭代器,以及一个初始值,然后对范围内的元素进行累加,并返回结果。

它的基本语法如下:

1
2
cppCopy codetemplate< class InputIt, class T >
T accumulate( InputIt first, InputIt last, T init );

其中:

  • InputIt 是起始迭代器和终止迭代器的类型。
  • first 是范围的起始迭代器。
  • last 是范围的终止迭代器(不包括在范围内)。
  • init 是初始值,用于累加元素。

decltype

decltype 是 C++11 引入的一个关键字,用于获取表达式的类型。它可以用于声明变量、函数返回类型、模板参数等地方。

1
decltype(expression)

std::bind

链接

std::bind的头文件是

解释

std::result_of

std::result_of 是一个 C++11 标准库中的模板工具,用于获取函数对象的返回类型。它可以用于在编译时确定函数对象的返回类型,而不需要实际调用该函数对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <type_traits>

int add(int a, int b) {
return a + b;
}

int main() {
// 获取 add 函数的返回类型
typedef std::result_of<decltype(add)&(int, int)>::type result_type;

// 输出返回类型
std::cout << "Result type: " << typeid(result_type).name() << std::endl;

return 0;
}

std::result_ofdecltype 都是用于获取表达式的类型,但它们之间有一些区别:

  1. 用法
    • std::result_of 用于获取函数对象的返回类型,而不需要实际调用该函数对象。它是一个模板类,需要指定函数对象的类型和参数类型。
    • decltype 用于获取表达式的类型,可以是任意的表达式,不仅限于函数调用。它是一个关键字,可以直接用于变量声明、返回类型推断等地方。
  2. 适用范围
    • std::result_of 主要用于获取函数对象的返回类型,因此在涉及函数对象的场景下更为常见。
    • decltype 不仅可以用于函数调用,还可以用于变量声明、表达式的类型推断等场景,更为灵活。
  3. 语法
    • std::result_of 的语法是模板类模板参数,需要传递函数类型和参数类型。
    • decltype 的语法更为灵活,可以直接对任意表达式使用。

std::filesvstem

cppreference

csdn

Tuesday, 9 April 2024

C++11线程特性

std::thread

标准库

C++11引入了std::thread来创建线程,支持对线程join或者detach.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <thread>

using namespace std;

int main() {
auto func = []() {
for (int i = 0; i < 10; ++i) {
cout << i << " ";
}
cout << endl;
};
std::thread t(func);
if (t.joinable()) {
t.detach();
}
auto func1 = [](int k) {
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
};
std::thread tt(func1, 20);
if (tt.joinable()) { // 检查线程可否被join
tt.join();
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class ThreadGuard
{
public:
enum class DeAction{join,detach};
ThreadGuard(std::thread&& t,DeAction action): m_t(move(t)) , m_action(action){};
~ThreadGuard()
{
if(m_t.joinable())
{
if(m_action==DeAction::join)
{
m_t.join();
}
else
{
m_t.detach();
}
}
}
ThreadGuard(ThreadGuard&&)=default;
ThreadGuard& operator=(ThreadGuard&&)=default;
private:
std::thread m_t;
DeAction m_action;
};

c++11还提供了获取线程id,或者系统cpu个数,获取thread native_handle,使得线程休眠等功能

1
2
3
4
5
std::thread t(func);
cout << "当前线程ID " << t.get_id() << endl;
cout << "当前cpu个数 " << std::thread::hardware_concurrency() << endl;
auto handle = t.native_handle();// handle可用于pthread相关操作
std::this_thread::sleep_for(std::chrono::seconds(1));

std::mutex

std::mutex是一种线程同步的手段,用于保存多线程同时操作的共享数据。

文库

mutex分为四种:

  • std::mutex:独占的互斥量,不能递归使用,不带超时功能

  • std::recursive_mutex:递归互斥量,可重入,不带超时功能

  • std::timed_mutex:带超时的互斥量,不能递归

  • std::recursive_timed_mutex:带超时的互斥量,可以递归使用

    std::mutex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <mutex>
#include <thread>

using namespace std;
std::mutex mutex_;

int main() {
auto func1 = [](int k) {
mutex_.lock();
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
mutex_.unlock();
};
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(func1, 200);
}
for (auto& th : threads) {
th.join();
}
return 0;
}

std::timed_mutex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>

using namespace std;
std::timed_mutex timed_mutex_;

int main() {
auto func1 = [](int k) {
timed_mutex_.try_lock_for(std::chrono::milliseconds(200));
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
timed_mutex_.unlock();
};
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(func1, 200);
}
for (auto& th : threads) {
th.join();
}
return 0;
}

std::lock相关

这里主要介绍两种RAII方式的锁封装,可以动态的释放锁资源,防止线程由于编码失误导致一直持有锁。

c++11主要有std::lock_guard和std::unique_lock两种方式,使用方式都类似,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>

using namespace std;
std::mutex mutex_;

int main() {
auto func1 = [](int k) {
// std::lock_guard<std::mutex> lock(mutex_);
std::unique_lock<std::mutex> lock(mutex_);
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
};
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(func1, 200);
}
for (auto& th : threads) {
th.join();
}
return 0;
}

std::lock_guard

1
2
3
4
#include <mutex>

template <class Mutex>
class lock_guard;

Mutex 是互斥量的类型。

使用 std::lock_guard 时,只需在需要保护的代码块中创建一个 std::lock_guard 对象,并将需要保护的互斥量传递给它的构造函数。当 std::lock_guard 对象创建时,会自动锁定互斥量,当对象销毁时,会自动解锁互斥量。

std::unique_lock

1
2
3
4
#include <mutex>

template <class Mutex>
class unique_lock;

Mutex 是互斥量的类型。

std::lock_guard 不同,std::unique_lock 对象可以在构造时不锁定互斥量,并且可以在后续的代码中手动锁定或解锁。此外,std::unique_lock 还提供了更多的功能,如可延迟锁定、条件变量的支持等。

std::atomic

c++11提供了原子类型std::atomic,理论上这个T可以是任意类型,但是我平时只存放整形,别的还真的没用过,整形有这种原子变量已经足够方便,就不需要使用std::mutex来保护该变量啦。看一个计数器的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct OriginCounter { // 普通的计数器
int count;
std::mutex mutex_;
void add() {
std::lock_guard<std::mutex> lock(mutex_);
++count;
}

void sub() {
std::lock_guard<std::mutex> lock(mutex_);
--count;
}

int get() {
std::lock_guard<std::mutex> lock(mutex_);
return count;
}
};

struct NewCounter { // 使用原子变量的计数器
std::atomic<int> count;
void add() {
++count;
// count.store(++count);这种方式也可以
}

void sub() {
--count;
// count.store(--count);
}

int get() {
return count.load();
}
};

std::call_once

c++11提供了std::call_once来保证某一函数在多线程环境中只调用一次,它需要配合std::once_flag使用,直接看使用代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
std::once_flag onceflag;

void CallOnce() {
std::call_once(onceflag, []() {
cout << "call once" << endl;
});
}

int main() {
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(CallOnce);
}
for (auto& th : threads) {
th.join();
}
return 0;
}

volatile

volatile通常用来建立内存屏障,volatile修饰的变量,编译器对访问该变量的代码通常不再进行优化,看下面代码:

1
2
3
int *p = xxx;
int a = *p;
int b = *p;

a和b都等于p指向的值,一般编译器会对此做优化,把*p的放入寄存器,之后a,b都等于寄存器的值,但是如果把中间p地址的值改变,内存值改变了,但a,b还是从寄存器中取的值(不一定,看编译器的优化结果),这不符合需求,所以对p加上volatile修饰可以避免此类优化.

注意:volatile不能解决多线程安全问题,针对特种内存才需要使用volatile,它和atomic的特点如下:

  • std::atomic用于多线程访问的数据,且不用互斥量,用于并发编程中
  • volatile用于读写操作不可以被优化掉的内存,用于特种内存中

std::condition_variable

条件变量是c++11引入的一种同步机制,它可以阻塞一个线程或者个线程,直到有线程通知或者超时才会唤醒正在阻塞的线程,条件变量需要和锁配合使用,这里的锁就是上面介绍的std::unique_lock。

成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class CountDownLatch {
public:
explicit CountDownLatch(uint32_t count) : count_(count);

void CountDown() {
std::unique_lock<std::mutex> lock(mutex_);
--count_;
if (count_ == 0) {
cv_.notify_all();
}
}

void Await(uint32_t time_ms = 0) {
std::unique_lock<std::mutex> lock(mutex_);
while (count_ > 0) {
if (time_ms > 0) {
cv_.wait_for(lock, std::chrono::milliseconds(time_ms));
} else {
cv_.wait(lock);
}
}
}

uint32_t GetCount() const {
std::unique_lock<std::mutex> lock(mutex_);
return count_;
}

private:
std::condition_variable cv_;
mutable std::mutex mutex_;
uint32_t count_ = 0;
};
  • notify_one

    • 通知一个线程等待
  • notify_all

    • 通知所有线程等待
  • wait

    • 阻塞当前线程,直到条件变量被唤醒
  • wait_for

    • 阻塞当前线程,知道条件变量被唤醒或指定的超时时间后
  • wait_until

    • 阻塞当前线程,直到条件变量被唤醒或到达指定时间点

std::future

c++11关于异步操作提供了future相关的类,主要有std::future、std::promise和std::packaged_task,std::future比std::thread高级些,std::future作为异步结果的传输通道,通过get()可以很方便的获取线程函数的返回值,std::promise用来包装一个值,将数据和future绑定起来,而std::packaged_task则用来包装一个调用对象,将函数和future绑定起来,方便异步调用。而std::future是不可以复制的,如果需要复制放到容器中可以使用std::shared_future。

std::promise与std::future配合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <functional>
#include <future>
#include <iostream>
#include <thread>

using namespace std;

void func(std::future<int>& fut) {
int x = fut.get();
cout << "value: " << x << endl;
}

int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(func, std::ref(fut));
prom.set_value(144);
t.join();
return 0;
}

std::packaged_task与std::future配合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <functional>
#include <future>
#include <iostream>
#include <thread>

using namespace std;

int func(int in) {
return in + 1;
}

int main() {
std::packaged_task<int(int)> task(func);
std::future<int> fut = task.get_future();
std::thread(std::move(task), 5).detach();
cout << "result " << fut.get() << endl;
return 0;
}

std::future用于访问异步操作的结果,而std::promise和std::packaged_task在future高一层,它们内部都有一个future,promise包装的是一个值,packaged_task包装的是一个函数,当需要获取线程中的某个值,可以使用std::promise,当需要获取线程函数返回值,可以使用std::packaged_task。

async

async是比future,packaged_task,promise更高级的东西,它是基于任务的异步操作,通过async可以直接创建异步的任务,返回的结果会保存在future中,不需要像packaged_task和promise那么麻烦,关于线程操作应该优先使用async,看一段使用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <functional>
#include <future>
#include <iostream>
#include <thread>

using namespace std;

int func(int in) { return in + 1; }

int main() {
auto res = std::async(func, 5);
// res.wait();
cout << res.get() << endl; // 阻塞直到函数返回
return 0;
}

async具体语法如下:

1
async(std::launch::async | std::launch::deferred, func, args...);

第一个参数是创建策略:

  • std::launch::async表示任务执行在另一线程
  • std::launch::deferred表示延迟执行任务,调用get或者wait时才会执行,不会创建线程,惰性执行在当前线程。

如果不明确指定创建策略,以上两个都不是async的默认策略,而是未定义,它是一个基于任务的程序设计,内部有一个调度器(线程池),会根据实际情况决定采用哪种策略。

若从 std::async 获得的 std::future 未被移动或绑定到引用,则在完整表达式结尾, std::future的析构函数将阻塞直至异步计算完成,实际上相当于同步操作:

1
2
std::async(std::launch::async, []{ f(); }); // 临时量的析构函数等待 f()
std::async(std::launch::async, []{ g(); }); // f() 完成前不开始

注意:关于async启动策略这里网上和各种书籍介绍的五花八门,这里会以cppreference为主。

有时候我们如果想真正执行异步操作可以对async进行封装,强制使用std::launch::async策略来调用async。

1
2
3
4
template <typename F, typename... Args>
inline auto ReallyAsync(F&& f, Args&&... params) {
return std::async(std::launch::async, std::forward<F>(f), std::forward<Args>(params)...);
}

原文章

补充

Friday, 29 March 2024

shell

主函数

  • 先创建一个Shell类型的类
  • 然后屏蔽信号ctrl+c与ctrl+D
  • 接着打印提示符,接收命令的输入
  • 判断该次命令是否含有exit clear !等信息,如果有,执行相应操作
  • 通过调用解析函数解析本次输入的命令

屏蔽信号

  • ctrl + C

    1
    signal(SIGINT, SIG_IGN);//将该信号的处理方式设置为忽略
  • ctrl + D

    1
    2
    3
    4
    struct termios term;
    tcgetattr(STDIN_FILENO, &term);
    term.c_cc[VEOF] = _POSIX_VDISABLE;
    tcsetattr(STDIN_FILENO, TCSANOW, &term);

首先定义一个termios类型的结构体,用来存储终端相关的属性信息

然后通过调用tcgetattr函数将获取终端的相关属性,并将这些属性保存到 **term **结构体中,第一个参数表示标准输入,是一个预定的文件描述符常量

将结构体中的c_cc数组成员的VEOF设置为禁用. c_cc数组是包含终端特殊字符的数组,VEOF是其中的一个索引,第二个参数是一个特殊的宏,用于禁用该特殊字符

最后,tcsetattr(STDIN_FILENO, TCSANOW, &term);调用tcsetattr()函数,它用于设置终端的属性。STDIN_FILENO表示标准输入文件描述符,TCSANOW表示立即生效的选项,&term是要设置的终端属性结构体。

tcgetattr

1
int tcgetattr(int fd, struct termios *termios_p);

参数说明:

  • fd 是一个打开的终端设备文件描述符。

  • termios_p 是一个指向 termios 结构体的指针,用于存储获取

    到的终端属性。

tcgetattr 函数返回一个整数值,表示函数调用的成功与否。如果函数成功执行,返回值为0;如果出现错误,返回值为-1,并设置相应的错误码。在调用该函数时,你需要检查返回值以确保函数调用是否成功。

tcsetattr

1
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);

参数说明:

  • fd 是一个打开的终端设备文件描述符。
  • optional_actions 是一个表示操作选项的整数值,用于指定何时应用属性的更改。
  • termios_p 是一个指向 termios 结构体的指针,其中包含了要应用的终端属性。

optional_actions 参数用于指定何时应用属性的更改,它可以取以下三个值之一:

  • TCSANOW:立即更改属性。
  • TCSADRAIN:在所有输出都被传输后更改属性。
  • TCSAFLUSH:在所有输出都被传输后更改属性,并丢弃所有未读的输入。

print

getlogin

1
2
char* getlogin(void)
//返回一个指向当前登陆用户的用户名的字符串指针

strcspn

1
2
3
4
size_t strscpn(const char *str1,const char *str2);
//str1:要搜索的字符串
//str2:要查找的字符集合
//计算字符串中连续不包含指定字符集合的最大前缀长度。

strftime

1
size_t strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr);

参数说明:

  • str 是一个指向字符数组的指针,用于存储格式化后的时间字符串。
  • maxsizestr 所指向的字符数组的最大容量。
  • format 是一个格式字符串,用于定义输出的时间格式。
  • timeptr 是一个指向 struct tm 结构体的指针,其中包含要格式化的时间和日期。
1
2
3
4
chrono::system_clock::now();
//是c++<chrono>头文件中的一个函数调用,用于获取当前的系统时间点
chrono::system_clock::to_time_t
//是一个函数模板,将time_point对象转换为time_t的类型

mycd

getenv

1
char* getenv(const char* name);

参数说明:

  • name是一个指向以null结尾的字符串,表示要获取的环境变量的名称.

根据指定的环境变量的名称,返回对应的环境变量的值。找到则返回一个null结尾的字符串,表示该环境变量的值.否则返回null.

1
2
const char* path = s[1].c_str();
//将string对象转换为c风格的字符串.

需要注意的是c_str()返回的指针指向的仍为string内部存储的字符数组,因此需要确保在使用path指针时,string对象仍然有效

get_current_dir_name

是一个标准的POSIX函数,用于获取当前工作目录的路径

由于get_current_dir_name( )分配了动态内存,因此使用完毕后需要调用free( )。

setenv

1
int setenv(const char* name, const char* value, int overwrite);

参数说明:

  • name 是一个指向以 null 结尾的字符串,表示要设置或新建的环境变量的名称。
  • value 是一个指向以 null 结尾的字符串,表示要设置的环境变量的值。
  • overwrite 是一个整数值,指示在环境变量已经存在时是否进行覆盖。如果 overwrite 为非零值,则会覆盖现有环境变量;如果 overwrite 为零,则不进行覆盖。

parse

istringstream是C++标准库中的一个类,他是用于从字符串中进行输入操作的输入流类.

getline()

1
std::istream& getline(std::istream& is, std::string& str, char delim);

参数说明:

  • is 是一个输入流对象,表示要从中读取文本的输入流,通常是 std::cin(标准输入)或文件流对象。
  • str 是一个字符串对象的引用,表示读取的文本将存储在其中。
  • delim 是一个字符(默认为换行符 \n),表示行的结束符。

pipe

“pipe”(管道)是一种在进程间进行通信的机制。它允许一个进程的输出直接成为另一个进程的输入,从而实现进程间的数据传输。

  1. 匿名管道:
    • 匿名管道是最常见的管道类型。它是一种单向通信管道,只能在具有父子关系的进程之间使用。父进程创建管道后,可以通过文件描述符进行读取或写入。子进程继承了父进程的文件描述符,从而可以使用相反的操作进行读取或写入。
    • 匿名管道是一种半双工通信方式,即数据只能在一个方向上流动。如果需要双向通信,需要创建两个匿名管道。
    • 使用匿名管道的典型流程是:父进程创建管道,然后调用 fork() 创建子进程。父进程关闭不需要的管道端口,子进程关闭另一个不需要的端口。然后父进程可以向管道写入数据,子进程可以从管道读取数据。
  2. 命名管道:
    • 命名管道也称为FIFO(First-In-First-Out),它提供了一种无关进程关系的进程间通信方式。命名管道在文件系统中有一个与之相关联的路径名,多个进程可以通过这个路径名来进行通信。
    • 命名管道是一种半双工通信方式,类似于匿名管道,只能在一个方向上流动数据。
    • 命名管道可以使用 mkfifo 函数创建,也可以使用命令行工具 mkfifo 创建。

管道的使用可以通过系统调用函数来实现,例如 POSIX 标准中的 pipe() 函数。在使用管道时,重要的是要了解管道的读取和写入端口以及数据的流动方向,以确保正确的通信。

1
int pipe(int pipefd[2]);

pipe() 函数接受一个整型数组 pipefd[2],用于存储管道的文件描述符。pipefd[0] 表示管道的读取端口,pipefd[1] 表示管道的写入端口。该函数返回值为 0 表示成功,-1 表示失败。

以下是 pipe() 函数的详细解释:

  • pipefd 参数是一个包含两个元素的整型数组,用于存储管道的文件描述符。pipefd[0] 是管道的读取端口,用于从管道中读取数据;pipefd[1] 是管道的写入端口,用于向管道中写入数据。
  • pipe() 函数创建一个匿名管道,并将管道的读取端口和写入端口的文件描述符填充到 pipefd 数组中。
  • 匿名管道是一种半双工通信方式,数据只能在一个方向上流动。如果需要双向通信,需要创建两个匿名管道。
  • 管道是内核中的一个缓冲区,用于在相关进程之间传输数据。数据写入管道后,可以从管道的读取端口读取。
  • 管道的大小是有限的,一旦管道的缓冲区满了,进程写入数据时可能会被阻塞,直到有足够的空间来容纳数据。同样,如果管道为空,从管道读取数据时可能会被阻塞,直到有数据可读。
  • 如果一个进程关闭了管道的读取端口,而另一个进程关闭了管道的写入端口,这两个进程之间的管道通信将结束。

dup2

1
2
3
#include <unistd.h>

int dup2(int oldfd, int newfd);
  • oldfd 参数是需要复制的文件描述符。它可以是任何有效的文件描述符,包括标准输入(0)、标准输出(1)和标准错误(2)。
  • newfd 参数是要复制到的新文件描述符。如果 newfd 已经打开,则会先关闭它,然后将 oldfd 复制到 newfd
  • 复制后,newfd 将和 oldfd 指向相同的文件表项,它们共享同一个文件偏移量和文件状态标志。
  • 如果 newfd 等于 oldfd,则 dup2() 函数不进行任何操作,直接返回 newfd
  • dup2() 函数的一个常见用途是重定向标准输入、标准输出和标准错误。例如,可以将标准输出重定向到一个文件,或者将标准错误重定向到一个套接字。

executeCommand

open

  • command[i + 1].c_str()command是一个字符串向量,command[i + 1]表示该向量中的第i + 1个元素。.c_str()将该元素转换为C风格的字符串,以便与open()函数的参数类型匹配。这个参数是要打开的文件的路径。
  • O_WRONLY:是open()函数的一个打开模式,表示以只写方式打开文件。这意味着你可以向文件写入数据,但不能读取文件的内容。
  • O_CREAT:是open()函数的一个打开模式,表示如果文件不存在,则创建该文件。如果文件已经存在,open()函数仍然会成功打开文件。
  • O_TRUNC:是open()函数的一个打开模式,表示如果文件已经存在且以只写方式打开,那么在打开文件时将截断文件的长度为0。这意味着打开文件后,文件中已经存在的内容将被清空。
  • 0644:表示文件的权限。在这里,0644表示文件所有者具有读和写的权限,而其他用户只有读的权限。它是一个八进制数,其中第一个数字0表示这是一个八进制数,后面的三个数字分别表示文件所有者、文件所属组和其他用户的权限。

代码

shepp.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
using namespace std;
#include <string>
#include <vector>
#include <sstream>
#include <chrono>
#include <unistd.h>
#include <csignal>
#include <algorithm>
#include <termios.h>
#include <cstdlib>
#include <wait.h>
#include <fstream>
#include <sys/stat.h>
#include <fcntl.h>

class Shell
{
public:
void print();
void setlinebuf();
string retlinebuf();
void setsignal();
bool isempty();
void addHistory(const string& line);
void setEOF();
void printHistory();
void searchAndReplay(const string &keyword);
void mycd(vector<string> &s);
void executeCommand(const vector<string> &command);
void parse(string &s);
private:
string linebuf;
vector<string> history;
};

func.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
#include "shell.hpp"

void Shell::print()
{ cout << "----------------------------------------------------------------------------" << endl;
cout << " ___ ___" << endl;
cout << " ______ / /___ ______ _____ ___ ___ ____ _____/ /___" << endl;
cout << " / __ \\/ __ \\ / ___ - ___ \\/ / / / /_ / / __/ __ \\" << endl;
cout << "/ /_/ / / / / / / / / / / /__/ / / /_(__ ) / / /" << endl;
cout << "\\______/__/ /__/ /__/ /__/ /__/\\____, / /____/____/__/ /__/" << endl;
cout << " /_______/" << endl;
cout << "----------------------------------------------------------------------------" << endl;
char cwd [1024];
auto now = chrono::system_clock::now();//返回当前的时间
time_t time = chrono::system_clock::to_time_t(now);//将time_t point转换为time_t类型
tm* timeinfo = std::localtime(&time);//将time_t转换为本地时间年月日,并存储在timeinfo中
char timeStr[9];
strftime(timeStr, sizeof(timeStr), "%H:%M:%S", timeinfo);
//std::strftime()将std::tm结构的时间信息格式化为字符串,并将结果存储在指定的字符数组中。这里使用"%H:%M:%S"作为格式字符串
//,它表示小时、分钟和秒的格式。timeStr是目标字符数组,sizeof(timeStr)用于指定数组的大小,
//timeinfo是包含时间信息的std::tm结构的指针。
cout << "# future @ future-arch in " << getcwd(cwd,1024) << " [" << timeStr << "]" << endl;
cout << "$ " ;
fflush(stdout);
}

void Shell ::setlinebuf()
{
getline(cin,this->linebuf);
}

string Shell::retlinebuf ()
{
return this->linebuf;
}

void Shell:: setsignal()
{
signal(SIGINT, SIG_IGN);//忽略ctrl+c的操作
}

bool Shell ::isempty()
{
if(this->linebuf.empty())
{
return true;
}
return false;
}

void Shell::addHistory(const string& line)
{
for(vector<string>::iterator it = history.begin(); it != history.end();it++)
{
if((*it).compare(line) == 0)
return ;
}
history.push_back(line);
}

void Shell ::setEOF()
{
struct termios term;//存储终端的相关信息
tcgetattr(STDIN_FILENO, &term);//获取相关信息,并存入结构体
term.c_cc[VEOF] = _POSIX_VDISABLE;//禁止使用EOF
tcsetattr(STDIN_FILENO, TCSANOW, &term);//将设置存入终端
}

void Shell::printHistory()
{
for(vector<string>::iterator it = this->history.begin(); it != this->history.end();it++)
{
cout << *it << endl;
}
cout << "~~~~~~~~~~~~~~~~~~~~~~~~~~~"<< endl;
}

void Shell::searchAndReplay(const string& keyword)
{
for (const string& line : history)
{
if (line.find(keyword) != string::npos)
{
cout << "Replaying: " << line << endl;
linebuf = line;
break;
}
}
}

void Shell::parse(string& s)
{
vector<vector<string>> tokens;
istringstream iss(s);//可方便的从字符串中读取数据
string root;
while (getline(iss, root, '|'))//将s以|分割开
{
istringstream root_iss(root);
vector<string> root_tokens;
string token;
while (root_iss >> token)//将单个命令以空格分隔开
{
root_tokens.push_back(token);
}

tokens.push_back(root_tokens);
}
if (tokens.size() == 1)
{
vector<string>& command = tokens[0];
if (command[0] == "cd")
{
mycd(command);
}
else
{
executeCommand(command);
}
}
else if (tokens.size() > 1)
{
int numPipes = tokens.size() - 1;
vector<int> pipefds(2 * numPipes, 0); // 用于存储管道的文件描述符
for (int i = 0; i < numPipes; ++i)
{
if (pipe(pipefds.data() + i * 2) < 0)
{
perror("pipe()");
exit(1);
}
}
int commandIndex = 0;
for (auto& command : tokens)
{
pid_t pid = fork();
if (pid < 0)
{
perror("fork()");
exit(1);
}
if (pid == 0)
{
// 子进程
if (commandIndex != 0) // 不是第一个命令,读取前一个管道的输出
{
if (dup2(pipefds[(commandIndex - 1) * 2], STDIN_FILENO) < 0)
{
perror("dup2()");
exit(1);
}
}
if (commandIndex != numPipes) // 不是最后一个命令,将输出写入下一个管道
{
if (dup2(pipefds[commandIndex * 2 + 1], STDOUT_FILENO) < 0)
{
perror("dup2()");
exit(1);
}
}
// 关闭所有管道的文件描述符
for (int i = 0; i < 2 * numPipes; ++i)
{
close(pipefds[i]);
}
if (command[0] == "cd")
{
mycd(command);
}
else
{
executeCommand(command);
}

exit(0);
}
else
{
// 父进程
++commandIndex;
}
}
// 关闭所有管道的文件描述符
for (int i = 0; i < 2 * numPipes; ++i)
{
close(pipefds[i]);
}

// 等待所有子进程结束
for (size_t i = 0; i < tokens.size(); ++i)
{
wait(NULL);
}
}
}

void Shell::mycd(vector<string>& s)
{
if (s.size() > 2)
{
cout << "cd: 函数调用参数过多" << endl;
return;
}
else if (s.size() == 1 || s[1].compare("~") == 0)
{
const char* homeDir = getenv("HOME");
if (homeDir)
{
if (chdir(homeDir) == -1)
{
perror("chdir()");
}
}
else
{
cout << "cd: 无法获取家目录路径" << endl;
return;
}
}
else if (s[1].compare("-") == 0)
{
const char* lastDir = getenv("OLDPWD");
if (lastDir)
{
if (chdir(lastDir) == -1)
{
perror("chdir()");
}
}
else
{
cout << "cd: 无法找到上一个路径" << endl;
}
}
else
{
const char* path = s[1].c_str();
if (chdir(path) == -1)
{
perror("chdir()");
}
}
// 更新环境变量 OLDPWD
char* currentDir = get_current_dir_name();//获取当前工作目录的路径
if (currentDir)
{
if (setenv("OLDPWD", currentDir, 1) == -1)//设置环境变量
{
perror("setenv()");
}
free(currentDir);
}
else
{
perror("get_current_dir_name()");
}
}

void Shell::executeCommand(const vector<string>& command)
{
pid_t pid;
fflush(NULL);
pid = fork();
if (pid < 0)
{
perror("fork()");
}
if (pid == 0)
{
vector<char*> args;
vector<int> outputFds; // 存储输出文件描述符的容器

for (size_t i = 0; i < command.size(); ++i)
{
if (command[i] == ">") // 输出重定向:覆盖写入文件
{
int fd = open(command[i + 1].c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1)
{
perror("open()");
exit(1);
}
outputFds.push_back(fd); // 将新的文件描述符添加到容器中
dup2(fd, STDOUT_FILENO);
close(fd);
i++;
}
else if (command[i] == ">>") // 输出重定向:追加写入文件
{
int fd = open(command[i + 1].c_str(), O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd == -1)
{
perror("open()");
exit(1);
}
outputFds.push_back(fd); // 将新的文件描述符添加到容器中
dup2(fd, STDOUT_FILENO);
close(fd);
i++;
}
else if (command[i] == "<") // 输入重定向
{
int fd = open(command[i + 1].c_str(), O_RDONLY);
if (fd == -1)
{
perror("open()");
exit(1);
}
dup2(fd, STDIN_FILENO);
close(fd);
i++;
}
else if (command[i] == "<<") // Here文档
{
string delimiter = command[i + 1];
string inputText;

while (getline(cin, inputText))
{
if (inputText == delimiter)
break;
inputText += "\n";
write(STDIN_FILENO, inputText.c_str(), inputText.size());
}

i++;
}
else
{
args.push_back(const_cast<char*>(command[i].c_str()));
}
}

args.push_back(nullptr); // execvp 需要以 nullptr 结尾的参数数组
execvp(args[0], args.data());

// execvp 执行失败时才会执行到这里
perror("execvp()");
exit(1);
}
else
{
wait(NULL);
}
}

shell.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include "func.cpp"

int main()
{
int num = 1;
Shell mysh;
mysh.setsignal();
mysh.setEOF();
while(num)
{
mysh.print();
mysh.setlinebuf();
string t = mysh.retlinebuf();
if(t.empty())
{
cout << "No input" << endl;
}
else if(t.rfind("exit")!=string::npos)
{
break;
}
else if(t.find("!")==0)
{
string keyword = t.substr(1); //获取去除感叹号的关键字
mysh.searchAndReplay(keyword);
}
else if(t.find("clear")==0)
{
mysh.addHistory(t);
// cout << "\033[2J\033[1;1H";//通过转义序列清除终端,但只能清楚当次的命令
system("clear");
num++;
}
else
{
mysh.addHistory(t);
num++;
}
mysh.parse(t);
if(num%3==0)
{
cout << "~~~~~~~~~~~~~~~~~~~~~~~~~~~"<< endl;
cout << " History Order :" << endl;
mysh.printHistory();
}
}
return 0;
}

后续问题

  1. 在输入命令全为空格的时候,会直接推出程序,造成程序崩溃.

    1. 是在parse中解析命令的时候,由于全为空格,所以会将一个空字符串token放入命令数组tokens,导致tokens.size()不为0造成exec函数的执行出现错误

    2. 所以在开始解析命令的时候加上全为空格的判断.

      1
      2
      3
      4
      if(std::all_of(s.begin(),s.end(),::isspace))
      {
      return {};
      }
    3. 在全为空格的时候直接返回,这样就可以返会空的二维数组.

  2. 缺乏对乱码命令的提示:例如输入”djsakldjalkjsduj”,等命令没有类似zsh : command not found : jdklasjdlak的提示,而是由于exec函数的无法执行出现的提示.

  3. 对于主题打印的提示,可以将Oh my zsh的打印只放在初始的时候,而不是每一次都打印.

  4. 另外在输出主机名称时,输出的是固定的作者的主机名称,这里为了偷懒,未使用gethostname()来获取主机名.

  5. 可以考虑对命令的存储进行改进,运用其他的结构存储,而不是二维数组.

  6. 增加类与类之间的联系,而不是简单的创造一个类,这样的话就只是相当于类似头文件的作用,并没有过多的用到一些c++的特性之类的东西.

Sunday, 17 March 2024

利用条件表达式完成下面的命题:学习成绩成绩>=90分的同学用A表示,学习成绩在60~89分的同学用B表示哦=,学习成绩在60分以下的同学用C表示.

int gra=0;

class

class 是一种用于创建用户定义的数据类型的关键字。它是面向对象编程(OOP)的基本概念之一。通过使用 class,您可以将数据成员(属性)和成员函数(方法)封装在一个单一的实体中,从而使代码更有组织性和可维护性。

语法: class 类名 {访问权限 : 属性\行为};

class 的功能与struct类似,但提供了更为丰富的功能.

class 可以在设计的时,可以把属性和行为放在不同的权限下,加以控制.

权限的分类

  • public 公共权限
  • protected 保护权限
  • private 私有权限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//公共权限  public
//保护权限 protected
//私有权限 private
//私有权限与保护权限在类内可以访问,在类外不可以访问
class Num
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
public:
void func()
{
m_A=10;
m_B=20;
m_C=30;
}
}

int main()
{
Num m;
m.m_A=10;
//m.m_B=20;//类外不可以访问
//m.m_C=30;
}

class与struct的区别

  • struct 默认权限为公共
  • class 默认权限为私有

对象的初始化清理

对象的初始化和清理是两个非常重要的安全问题.

一个对象或者变量没有初始状态,对其使用后果未知

同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

c++利用了构造函数析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会给我们提供空实现

构造函数

构造函数语法: 类名(){}

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造,无需手动调用,而且只会调用一次
1
2
3
4
5
6
7
8
9
class Num
{
public:
Num(int a): m_A(a)
{
cout << "gz" << endl;
}
int m_A;
};

构造函数的分类:

  • 按参数
    • 有参构造
    • 无参构造
  • 按类型
    • 普通构造
    • 拷贝构造

构造函数的调用方法

  • 括号法

    1
    2
    3
    Person P;//默认构造时后面不要加括号
    Person p1(10);
    Person p2(p1);
  • 显示法

    1
    2
    3
    Person p;
    Person p1=Person(10);
    Person P2=Person(p2);
  • 隐式转换法

    1
    2
    Person p4=10;
    Person p5=p4;//相当于Person p5=Person(10);

explcit

在 C++ 中, explicit 关键字用于防止编译器进行隐式类型转换。当您将构造函数或转换函数声明为 explicit 时,这意味着该构造函数或转换函数只能由程序员显式调用,而不能由编译器隐式调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class P
{
public:
P()
{

}
P(int a)
{
m_A = a;
}
explicit P(P& a)
{
m_A = a.m_A;
}
int m_A;
};

int main()
{
P p;
p.m_A = 1;
//P p1 = p;//会报错,因为声明不可以隐式类型转换
P p1=P(1);
cout << p1.m_A << endl;
return 0;
}

拷贝构造函数调用时机

c++拷贝构造函数调用时机

  • 使用一个创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值(做参数)
  • 以值的方式返回局部对象(做返回值)

构造函数调用规则

默认情况下,c++编译器至少给一个类添加3个函数

1.默认构造参数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝构造函数,对属性值进行拷贝

规则如下:

  • 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,c++不会在提供其他的构造函数

深浅拷贝

浅拷贝:简单的赋值操作

深拷贝:在堆区重新申请空间,进行拷贝操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Num
{
public:
Num()
{
cout << "gz" << endl;
}
Num(const Num &other)
{
m_A=other.m_A;
m_B=new int (*other.m_B);
cout << "kb" << endl;
}
~Num()
{
if(this->m_B)
{
delete this->m_B;
this->m_B=nullptr;
}
}
int m_A;
int *m_B;
};

int main()
{
Num a;
a.m_A=1;
a.m_B=new int(20);
Num b(a);
cout << b.m_A << endl;
cout << *b.m_B << endl;
cout << a.m_B << endl;
return 0;
}

class 与 struct

C++中的 struct 和 class 基本是通用的,唯有几个细节不同:

  • 使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。
  • class 继承默认是 private 继承,而 struct 继承默认是 public 继承。
  • class 可以使用模板,而 struct 不能。

引用

作用:给变量起别名

语法:数据类型 &别名 =原名

注意事项

  • 引用必须要初始化
  • 引用一旦初始化后,就不可以更改

引用可以简化指针的用法.

引用做函数参数

作用:函数传参时,可以利用引用的技术让形参修饰实参

优点:可以简化指针修改实参

通过引用参数产生的效果按地址传递是一样的。引用的语法更加的简单。

引用是可以作为函数的返回值存在的.

不返回局部变量的引用.

函数的调用可以作为左值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int sum1 = 0;
int& sum(int &a,int &b)
{
sum1 = a + b;
return sum1;
}

int main()
{
int a=10;
int b=10;
sum(a,b)=30;
cout << sum1 << endl;
return 0;
}

本质:引用的本质在c++内部实现是一个指针常量.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void func(int &ref)
{
ref=100;
}

int main()
{
int a=10;
//自动转换为int *const ref = &a;指针常量是指针指向不可改,也说明为什么引用不可以改
int &ref=a;
ref=20;

func(a);
return 0;
}

左值 & 右值

C++中所有的值都必然属于左值、右值二者之一.

左值是指表达式结束之后依然存在的持久化对象

右值是之表达式结束时就不再存在的临时对象.

所有的具有变量或者对象都是左值,而右值不具有名称.很难得到左值和右值的真正定义,但是有一个可以区分的便捷方法:看能否对表达式取地址,如果可以,则为左值,否则为右值。

右值又分为 将亡值纯右值

纯右值是C++98标准中右值的概念,如非引用返回的函数返回的临时变量值;

而将亡值则是c++新增的和右值引用相关的表达式,这样的表达式通常是将要移动的对象。&&函数返回值、std::move()函数的返回值等.

categories来说,c++11前,只有左值(lvalue)和右值(rvalue);c++11后,任何value categories(值类型)都是下面的三种之一:lvaluexvalueprvaluegvalue为广义左值,包括lvaluexvaluervalue为右值,包括xvalueprvalue,可见xvalue可以是左值也可以是右值。

纯右值(prvalue):纯右值是传统右值的一部分,纯右值是表达式产生的中间值,不能取地址。 纯右值一定会在表达式结束后被销毁。

一般有以下的条件:

  1. 本身就是纯粹的字面值,如3、false.
  2. 求值结果相当于字面值或是一个不具名的临时变量.

消亡值(xvalue)是c++11的不具名的右值引用引入的。 以下情况的表达式求值结果为xvalue,准确的说叫不具名右值引用,这种属于新的”右值”,由右值引用带来,通常用来完成移动构造移动赋值的特殊任务。事实上,将亡值不过是C++11提出的一块晦涩的语法糖。它与纯右值在功能上及其相似,如都不能做操作符的左操作数,都可以使用移动构造函数和移动赋值运算符。当一个纯右值来完成移动构造或移动赋值任务时,其实它也具有“将亡”的特点。一般,我们不必刻意区分一个右值到底是纯右值还是将亡值。

左值引用 & 右值引用

c++98中的引用很常见了,就是给变量取了个别名,在c++11中,因为增加了右值引用(rvalue reference)的概念,所以c++98中的引用都称为了左值引用(lvalue reference**)**。 c++11中的右值引用使用的符号是&&

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A{
public:
int a;
};

A getTemp()
{
return A();
}

int main()
{
int a=10;
int &refA = a;//左值引用
//int &ref2 = 2; //编译错误
int &&ref1=1;//右值引用
int b =5;
//int && refB =b;//编译错误,不能将一个左值赋值给一个右值引用
A&& refIns = getTemp();//函数返回值是 右值
return 0;
}

getTemp()返回的右值本来在表达式语句结束后,其生命也就该终结了(因为是临时变量),而通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量refIns的生命期一样,只要refIns还活着,该右值临时变量将会一直存活下去。实际上就是给那个临时变量取了个名字。

注意:这里refIns类型是右值引用类型(int &&),但是如果从左值和右值的角度区分它,它实际上是个左值。因为可以对它取地址,而且它还有名字,是一个已经命名的右值。

常量左值引用

左值引用只能绑定左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。但是,常量左值引用却是个奇葩,它可以算是一个“万能”的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。

1
const A &a =getTmp();//不会报错
1
2
3
4
5
6
7
8
9
10
11
12
int max(const int &a,const int &b)
{
return a+b;
}

int main()
{
int a=1;
int b=2;
cout << max(1,2) << endl;
return 0;
}

总结

T是一个数据类型

  1. 左值引用,使用T&只能绑定左值
  2. 右值引用,使用T&&只能绑定右值
  3. 常量左值,使用const T&,既可以绑定左值也可以绑定右值
  4. 已命名的右值引用,编译器会认为是个左值
  5. 编译器有返回值优化,但不要过于依赖(-fno-elide-constructors)

移动语义

移动构造函数 & 移动赋值函数

用c++实现一个字符串类MiniStringMiniString内部管理一个C语言的char *数组,这个时候一般都需要实现拷贝构造函数和拷贝赋值函数,因为默认的拷贝是浅拷贝,而指针这种资源不能共享。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class MiniString
{
public:
static size_t kbsize;
MiniString(const char *str)
{
if(str)
{
m_data = new char[strlen(str) + 1];
strcpy(m_data, str);
}
else
{
m_data = new char[1];
*m_data = '\0';
}
}
MiniString(const MiniString &other)
{
kbsize ++;
m_data=new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
//cout << "kb" << kbsize << endl;
}
MiniString &operator=(const MiniString &other)
{
if(this==&other)
{
return *this;
}
delete[] m_data;
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
return *this;
}
~MiniString()
{
if(m_data)
{
delete[] m_data;
}
}
private:
char *m_data;
};

size_t MiniString::kbsize =0;

int main()
{
vector<MiniString> v;
v.reserve(1000);
for(int i=0; i<1000; i++)
{
v.push_back(MiniString("hello world!"));
}
cout << MiniString::kbsize << endl;

return 0;
}

代码执行了1000次拷贝构造函数,如果MiniString("hello")构造出来的字符串本来就很长,构造一遍就很耗时了,最后却还要拷贝一遍,而MiniString("hello")只是临时对象,拷贝完就没什么用了,这就造成了没有意义的资源申请和释放操作,如果能够直接使用临时对象已经申请的资源,既能节省资源,又能节省资源申请和释放的时间。而C++11新增加的移动语义就能够做到这一点。

移动语义避免了移动原始数据,而只是修改了记录。要实现移动语义就必须增加两个函数:移动构造函数和移动赋值构造函数。必须让编译器知道什么时候需要复制,什么时候不需要复制。这就是右值引用发挥最大作用的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <iostream>
using namespace std;
#include <cstring>
#include <vector>

class MiniString
{
public:
static size_t kbsize;
static size_t movesize;
static size_t Fkbsize;
static size_t Fmovesize;
MiniString(const char *str)
{
if(str)
{
m_data = new char[strlen(str) + 1];
strcpy(m_data, str);
}
else
{
m_data = new char[1];
*m_data = '\0';
}
}
MiniString(const MiniString &other)
{
kbsize ++;
m_data=new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
// cout << "kb" << kbsize << endl;
}
MiniString(MiniString &&other)
{
m_data=other.m_data;
movesize ++;
other.m_data=NULL;
}
MiniString &operator=(const MiniString &other)
{
Fkbsize ++;
if(this==&other)
{
return *this;
}
delete[] m_data;
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
return *this;
}
MiniString &operator=(MiniString && other)
{
Fmovesize++;
if(this==&other)
{
return *this;
}
delete[] m_data;
m_data=other.m_data;
other.m_data=NULL;
return *this;
}
~MiniString()
{
delete[] m_data;
}
private:
char *m_data;
};

size_t MiniString::kbsize =0;
size_t MiniString::movesize=0;
size_t MiniString::Fkbsize=0;
size_t MiniString::Fmovesize;

int main()
{
vector<MiniString> v;
v.reserve(1000);
for(int i=0; i<1000; i++)
{
v.push_back(MiniString("hello world!"));
}
cout <<"kbsize\t"<< MiniString::kbsize << endl;
cout <<"Fkbsize\t"<< MiniString::Fkbsize << endl;
cout <<"movesize\t"<< MiniString::movesize << endl;
cout <<"Fmovesize\t"<< MiniString::Fmovesize << endl;

return 0;
}

移动构造函数与拷贝构造函数的区别是,拷贝构造的参数是const MiniString& str,是常量左值引用,而移动构造的参数是MiniString&& str,是右值引用,而MiniString("hello")是个临时对象,是个右值,优先进入移动构造函数而不是拷贝构造函数。而移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间,将要拷贝的对象复制过来,而是”偷”了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,”偷”也白偷了。

不用奇怪为什么可以抢别人的资源,临时对象的资源不好好利用也是浪费,因为生命周期本来就是很短,在你执行完这个表达式之后,它就毁灭了,充分利用资源,才能很高效。

当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。

std::move()

对于一个左值,肯定是调用拷贝构造函数了,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。其实就是告诉编译器,虽然我是一个左值,但是不要对我用拷贝构造函数,而是用移动构造函数。std::move 的源码实现:

1
2
3
4
5
6
7
8
9
// FUNCTION TEMPLATE move
template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&& >(_Arg);
}
//_NODISCARD 这是一种标记,用于指示编译器在忽略函数返回值时发出警告。
//constexpr:表示这个函数是在编译时计算的
//noexcept:表示这个函数不会抛出异常
//remove_reference_t 是 C++ 标准库 <type_traits> 中的一个模板元函数,用于移除类型的引用修饰符(& 或 &&)。

static_cast:

用于显示类型转换.在编译时进行类型检查,用于在不同但相关的类型之间检查.

其他类型转换的函数

  • dynamic_cast:
    • 用于在运行时进行安全的向下转型,主要用于处理继承关系中的多态类型
    • 仅在具有虚函数的类层次结构中才有效
    • 如果转型合法,则返回目标类型的指针或引用,否则返回nullptr
  • reinterpret_case
    • 执行底层的二进制位级别的转换
    • 非常强大,但潜在的不安全,因为它可以忽略类型之间的语义差异

可以看到 std::move 是一个模板函数,通过remove_reference_t获得模板参数的原本类型,然后把值转换为该类型的右值。用C++大师 Scott Meyers 的在《Effective Modern C++》中的话说, std::move 是个cast ,not a move.

注意: 使用 move 意味着,把一个左值转换为右值,原先的值不应该继续再使用(承诺即将废弃)。

1
2
3
4
5
6
7
8
9
10
11
MiniString str1("hello"); // 调用构造函数
MiniString str2("world"); // 调用构造函数
MiniString str3(str1); // 调用拷贝构造函数
MiniString str4(std::move(str1)); // 调用移动构造函数
// cout << str1.get_c_str() << endl; // 此时str1的内部指针已经失效了!不要再使用
// 虽然str1中的m_dat已经称为了空,但是str1这个对象还活着,知道出了它的作用域才会析构,
// 而不是move完了立刻析构
MiniString str5;
str5 = str2; // 调用拷贝赋值函数
MiniString str6;
str6 = std::move(str2); // 调用移动赋值函数,str2的内容也失效了,不要再使用

要注意一下几点:

  1. str6 = std::move(str2),虽然将str2的资源给了str6,但是str2并没有立刻析构,只有在str2离开了自己的作用域的时候才会析构,所以,如果继续使用str2m_data变量,可能会发生意想不到的错误。
  2. 如果我们没有提供移动构造函数,只提供了拷贝构造函数,std::move()会失效但是不会发生错误,因为编译器找不到移动构造函数就去寻找拷贝构造函数,也这是拷贝构造函数的参数是const T&常量左值引用的原因!
  3. c++11中的所有容器都实现了move语义,move只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝。move对于拥有如内存、文件句柄等资源的成员的对象有效,如果是一些基本类型,如intchar[10]数组等,如果使用move,仍会发生拷贝(因为没有对应的移动构造函数),所以说move对含有资源的对象说更有意义。

通用引用

当右值引用和模板结合的时候,就复杂了。T&&并不一定表示右值引用,它可能是个左值引用又可能是个右值引用。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <class T>
void func(T&&a,T&&b)
{
cout << a+b << endl;
}

int main()
{
int a=10;
int b=20;
func(a,b);
func(200,300);
return 0;
}

如果上面的函数模板表示的是右值引用的话,肯定是不能传递左值的,但是事实却是可以。这里的&&是一个未定义的引用类型,称为universal references,它必须被初始化,它是左值引用还是右值引用却决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。

注意:只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个universal references

1
2
template <typename T>
void func(T&&a);//这里T需要推导,所以&&是universal references
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <class T>
class Person
{
public:
Person(T age)
{
m_age = age;
}
T m_age;
};

int main()
{
Person<int> p(1);
cout <<p.m_age << endl;
return 0;
}

template<typename T>
class Test {
Test( Test&& rhs ); // Test是一个特定的类型,不需要类型推导,所以&&表示 右值引用
};
1
2
template <typename T>
void func(std::vector<T>&& data);//在往vector添加元素的时候已经将T推断出来,所以是右值引用

所以最终还是要看T被推导成什么类型,如果T被推导成了string,那么T&&就是string&&,是个右值引用,如果T被推导为string&,就会发生类似string& &&的情况,对于这种情况,c++11增加了引用折叠的规则,总结如下:

  1. 所有的右值引用叠加到右值引用上仍然使一个右值引用。
  2. 所有的其他引用类型之间的叠加都将变成左值引用。

完美转发

所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
using namespace std;

void process(int &i)
{
cout << "process(int &)" << i << endl;
}

void process(int &&i)
{
cout << "process(int &&)" << i << endl;
}

void myforward(int &&i)
{
cout <<"myforward(int &&)" << i << endl;
//process(i);
process(forward<int>(i));
}

int main()
{
int a =0;
process(a);
process(1);
process(move(a));
myforward(1);
myforward(move(a));
return 0;
}

上面的例子就是不完美转发,而c++中提供了一个std::forward()模板函数解决这个问题。将上面的myforward()函数简单改写一下:

上面修改过后还是不完美转发,myforward()函数能够将右值转发过去,但是并不能够转发左值,解决办法就是借助universal references通用引用类型和std::forward()模板函数共同实现完美转发。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void RunCode(int &&m) {
cout << "rvalue ref" << endl;
}
void RunCode(int &m) {
cout << "lvalue ref" << endl;
}
void RunCode(const int &&m) {
cout << "const rvalue ref" << endl;
}
void RunCode(const int &m) {
cout << "const lvalue ref" << endl;
}

// 这里利用了universal references,如果写T&,就不支持传入右值,而写T&&,既能支持左值,又能支持右值
template<typename T>
void perfectForward(T && t) {
RunCode(forward<T> (t));
}

template<typename T>
void notPerfectForward(T && t) {
RunCode(t);
}

int main()
{
int a = 0;
int b = 0;
const int c = 0;
const int d = 0;

notPerfectForward(a); // lvalue ref
notPerfectForward(move(b)); // lvalue ref
notPerfectForward(c); // const lvalue ref
notPerfectForward(move(d)); // const lvalue ref

cout << endl;
perfectForward(a); // lvalue ref
perfectForward(move(b)); // rvalue ref
perfectForward(c); // const lvalue ref
perfectForward(move(d)); // const rvalue ref
}

原文链接