explorer

万丈高楼平地起,勿在浮沙筑高台

0%

学习书籍:Mastering Embedded Linux Programming: Create fast and reliable embedded solutions with Linux 5.4 and the Yocto Project 3.1 (Dunfell), 3rd Edition

通过阅读这部书,将整个嵌入式 Linux 的开发知识串联起来,以整理这些年来所学的杂乱知识。

  • 开发主机:ubuntu 20.04 LTS
  • 开发板:myc-c8mmx-c imx8mm(4核 A53 + M4)
  • 系统:Linux 5.4
  • yocto:3.1

工具链的选择一般放在最开始,工具链选好以后最好就不要变动了。

或者说要变动的话,也是所有变动,所有的源码都要重新编译一次。

阅读全文 »

要养成好的习惯:永远在使用对象之前先将它初始化。

  • 对于内置类型,在定义时就初始化
  • 对于类类型,在构造函数初始值列表中初始化
    • 类类型中的私有变量是内置类型时,也可以在声明时初始化,这样可以避免初始值列表过长。
阅读全文 »

编译器默认会为一个类提供:

  • 默认构造函数:如果类编写了构造函数,则编译器就不会自动提供默认构造函数了
  • 拷贝构造函数:单纯地将每一个数据成员进行拷贝
    • 如果数据成员中的对象具有它自己的拷贝构造函数,则也会调用它
  • 拷贝赋值函数:单纯地将每一个数据成员进行拷贝
    • 如果数据成员中的对象具有它自己的拷贝赋值函数,则也会调用它
  • 析构函数

对于 modern cpp 其实还有移动构造和移动赋值

阅读全文 »

需要为基类的虚构函数加上virtual是基本常识:

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
#include <iostream>
#include <string>

class BasicClass{
public:
virtual ~BasicClass(){
std::cout << "BasicClass destructor!\n";
}
};

class DerivedClass: public BasicClass{
public:
~DerivedClass(){
std::cout << "DerivedClass destructor!\n";
}
};

int main(void){

class BasicClass* p_class = new DerivedClass();

// 如果 BasicClass 的析构函数没有 virtual 关键字
// 那么执行 delete 后只会执行 BasicClass 的析构函数
// 也就是说,只释放了基类内存,而派生类的内存没有被释放掉
delete p_class;


return 0;
}
阅读全文 »

operator=类重载返回 *this 引用

为了满足连锁赋值的需求,赋值重载方法需要返回对象。

而为了能够提高效率,返回它的引用是个好习惯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Widget{
public:
// ...
// +=, -=, *= 这类函数重载都要满足连续赋值要求
Widget& operator+=(const Widget& rhs){
// ...
return *this;
}
Widget& operator=(const Widget& rhs){
// ...
return *this;
}
// 即使参数类型不同,也需要满足连续赋值
Widget& operator=(int rhs){
// ...
return *this;
}
// ...
};

operator=中处理自我赋值

在实现拷贝赋值的过程中,需要处理自我赋值(自己给自己赋值)这种特殊情况。

当存在别名这种情况下,自我赋值还是会比较容易发生的。

比如,当向一个含有指针的类进行赋值:

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 <string>
#include <cstring>

class MyString{
public:
// ...
MyString& operator=(const MyString& str){
if(this == &str){
return *this;
}
// 如果没有上面判断,那么当自我赋值时,便会删除自己指向的内存
// 接下来的操作结果便是未定义的
delete[] str_;

str_ = new char[str.len_];
std::memcpy(str_, str.str_, str.len_);
len_ = str.len_;

return *this;
}
private:
int len_ = 0;
char *str_ = nullptr;
};

但是上面的处理方式仍然有不足之处:

  • 如果new操作无法申请需求的内存而抛出异常,那么这个类的str_所指向的内存就会被保持被删除的状态,接下来的其他操作将会出现奇怪的行为。

使用下面的方式便可以处理异常:

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
#include <iostream>
#include <string>
#include <cstring>

class MyString{
public:
// ...
MyString(const char* str){
int str_len = std::strlen(str);
if(str_len == 0){
len_ = 1;
str_ = new char[len_];
str_[0] = '\0';
}else{
len_ = str_len + 1;
str_ = new char[len_];
std::memcpy(str_, str, len_);
}
}
// ...
MyString& operator=(const MyString& str){
// 先使用一个副本指向当前指针指向的内存
char* str_tmp = str_;

// 这里使用构造函数
// 如果此处 new 抛出异常,则不会对当前对象有任何影响
str_ = new MyString(str.str_);
// 删除副本的内存
delete[] str_tmp;

len_ = str.len_;

return *this;
}
private:
int len_ = 0;
char *str_ = nullptr;
};

如上代码所示,即避免了自我赋值的问题,也避免了抛出异常的问题。

注意要复制对象的每个成分

  • 拷贝函数应该确保复制对象内的所有成员变量,以及所有父类的成分
  • 通常拷贝构造和拷贝赋值函数有一部分重复代码,这个时候增加一个私有的函数是个好办法

对于第一点的示例如下:

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
class Customer{
public:
// ...
Customer(const Customer& rhs): name(rhs.name){

}
Customer& operator=(const Customer& rhs){
name = rhs.name;

return *this;
}
private:
// ...
std::string name;
};

class PriorityCustomer: public Customer{
public:
// ...
PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), // 调用基类的拷贝构造函数
priority(rhs.priority){

}

PriorityCustomer& PriorityCustomer(const PriorityCustomer& rhs){
Customer::operator=(rhs); //调用基类的拷贝赋值函数
priority = rhs.priority;

return *this;
}
private:
int priority;
};