东风草堂博客

公众号:开发者来风

连接过程

总的来说,基于 UDS 的连接过程比 inet 的 socket 连接过程要简单多了。客户端先创建一个自己用的 socket,然后调用 connect 来和服务器建立连接。

在 connect 的时候,会申请一个新 socket 给 server 端将来使用,和自己的 socket 建立好连接关系以后,就放到服务器正在监听的 socket 的接收队列中。 这个时候,服务器端通过 accept 就能获取到和客户端配好对的新 socket 了。

总的 UDS 的连接建立流程如下图。

阅读全文 »

拷贝构造函数和赋值运算符

在默认情况下(用户没有定义,但是也没有显式的删除),编译器会自动的隐式生成一个拷贝构造函数和赋值运算符。但用户可以使用delete来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算。
拷贝构造函数必须以引用的方式传递参数。这是因为,在值传递的方式传递给一个函数的时候,会调用拷贝构造函数生成函数的实参。如果拷贝构造函数的参数仍然是以值的方式,就会无限循环的调用下去,直到函数的栈溢出。
拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例。拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例;赋值运算符是执行某种运算,将一个对象的值复制给另一个对象(已经存在的)。调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。

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
class Person
{
public:
Person() {}
Person(const Person& p) {
cout << "Copy Constructor" << endl;
}

Person& operator=(const Person& p) {
cout << "Assign" << endl;
return *this;
}

private:
int age;
string name;
};

void f(Person p)
{
return;
}

Person f1()
{
Person p;
return p;
}

int main()
{
Person p;
Person p1 = p; // 这是虽然使用了"=",但是实际上使用对象p来创建一个新的对象p1。也就是产生了新的对象,所以调用的是拷贝构造函数。
Person p2;
p2 = p; // 首先声明一个对象p2,然后使用赋值运算符"=",将p的值复制给p2,显然是调用赋值运算符,为一个已经存在的对象赋值 。
f(p2); // 以值传递的方式将对象p2传入函数f内,调用拷贝构造函数构建一个函数f可用的实参。
p2 = f1(); // 这条语句拷贝构造函数和赋值运算符都调用了。函数f1以值的方式返回一个Person对象,在返回时会调用拷贝构造函数创建一个临时对象tmp作为返回值;返回后调用赋值运算符将临时对象tmp赋值给p2
Person p3 = f1(); // 按照上一条的解释,应该是首先调用拷贝构造函数创建临时对象;然后再调用拷贝构造函数使用刚才创建的临时对象创建新的对象p3,也就是会调用两次拷贝构造函数。不过,编译器也没有那么傻,应该是直接调用拷贝构造函数使用返回值创建了对象p3。

getchar();
return 0;
}

C++ std::string写时复制与深浅拷贝,默认是浅拷贝,写时进行深拷贝!如果要强制使用深拷贝,使用地址c_str()进行构造。

1
2
3
4
5
6
7
8
std::string a("test");
std::string b = a;
printf("%p: %s\n", a.c_str(), a.c_str());
printf("%p: %s\n", b.c_str(), b.c_str());
std::cout << "---after---" << std::endl;
b[0] = 'T';
printf("%p: %s\n", a.c_str(), a.c_str());
printf("%p: %s\n", b.c_str(), b.c_str());
阅读全文 »

  • 基类与派生类的关系:

  • 派生类对象可以使用基类的方法,条件是方法不是私有的。

  • 基类指针可以在不进行显示类型转换的情况下指向派生类对象。

  • 基类引用可以在不进行显示类型转换的情况下引用派生类对象。

  • C++有三种继承关系:公有继承、保护继承和私有继承。

  • 公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行任何操作,也可以对派生类对象执行。注意:is-a关系通常是不可逆的,也就是说,水果不是香蕉。公有继承不能建立has-ais-like-ais-implemented-as-auses-a关系。

  • 多态公有继承:在派生类中重新定义基类方法。使用虚方法virtual,该关键字只出现在方法原型中。
    对于虚函数,程序将根据对象类型来确定使用哪个版本。对于两个对象中行为相同的方法,只在基类中声明。如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。
    方法在基类中声明为虚拟的后,它在派生类中将自动成为虚方法,一般也都在派生类中指出。为基类声明一个虚拟析构函数也是一种惯例,可以确保正确的析构函数被调用。一般先调用派生类的析构函数,再调用基类的析构函数。

    友元不能是虚函数,只有成员才能是虚函数,但可以让友元函数使用虚拟成员函数。 可以使用数组来表示多种类型的对象,这就是多态性。
  • 访问控制protected:关键字protected与private相似,在类外只能用公有类成员来访问protected部分中的类成员。protected与private的区别只有在基类派生的类中才能表现出来。派生类的成员可以直接访问基类的保护成员,但不能访问基类的私有成员。

     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class TheOnlyInstance
    {
    public:
    static TheOnlyInstance * GetTheOnlyInstance();
    protected:
    TheOnlyInstance() {}
    }

    TheOnlyInstance* TheOnlyInstance::GetTheOnlyInstance()
    {
    static TheOnlyInstance objTheOnlyInstance;
    return &objTheOnlyInstance;
    }

    int main()
    {
    TheOnlyInstance noCanDo; //not allowed
    TheOnlyInstance * pTheOnlyInstance = TheOnlyInstance::GetTheOnlyInstance(); //以后调用,将返回同一个静态地址
    }
  • 使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生类公有接口的一部分,但可以在派生类的成员函数中使用它们。这种不完全继承是has-a关系的一部分,其特性与包含相同。
    包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员,这是两种方法的主要区别。
    成员初始化列表使用std::string(str),而不是name(str),这里string是基类。这是包含和私有继承之间的第二个主要区别。
    使用作用域解析操作符可以访问基类方法,但如果要使用基类对象本身,可以使用强制类型转换:

    1
    2
    3
    4
    const string & Student::Name() const
    {
    return (const string &) *this;
    }

用类名显示地限定函数名不适合于友元函数,这是因为友元不属于类。不过可以显式的转换为基类来调用正确的函数。另一方面,如果不使用类型转换,由于使用的多重继承,编译器将无法确定转换成哪个基类。<例子>

	 
1
2
3
4
5
ostream & operator(ostream & os, const Student & stu)
{
os << "Scores for " << (const string &) stu << ":\n";
...
}

私有继承所提供的特性比包含多,但会引发许多问题。
私有继承可以重新定义虚函数,但也只能在类中使用。

  • 保护继承是私有继承的变体。基类的公有成员和保护成员都将成为派生类的保护成员,与私有不同,第三代的派生类能使用保护成员。

  • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。
    在C++中,动态联编与指针和引用调用的方法相关,从某中程度上说,这是由继承控制的。编译器对非虚函数采用静态编联。也就是说,当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。
    派生类的虚函数的返回类型形参类型必须与基类函数匹配,否则会隐藏同名的基类方法。只有一个例外,当类的虚函数的返回类型是类本身的指针或引用时,这称为返回类型协变。
    如果基类声明被重载了,则应在派生类中重新定义所有的基类版本,否则其他的版本都将被隐藏。
    将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这时公有继承不需要进行显式类型转换,这种转换是可以传递的。
    仅将那些预期将被重新定义的方法声明为虚拟的。构造函数不能是虚函数,析构函数应当是虚函数。

  • 编译器处理虚函数的方法:给每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,这种数组称为虚函数表。
    虚函数的实现原理:
    虚函数的实现需要利用虚表(Virtual Table)和虚表指针(Virtual Table Pointer)。虚表是一个指针数组,它存储了类的虚函数的地址,虚表指针是一个指针,指向该对象所属的类的虚表首地址。在调用虚函数时,程序通过虚表指针找到虚表,再根据函数的索引(函数在虚表中的下标)找到对应的虚函数并执行。当一个类被继承时,派生类会继承父类的虚表地址,并在其中添加新的虚函数。如果派生类需要覆盖父类的虚函数,那么它会把新的虚函数地址写入虚表中对应的位置。这样就实现了运行时的多态性。
    虚函数表的创建:虚函数表在编译时由C++编译器自动生成。对于每个具有虚函数的类,编译器会生成一个虚函数表,其中包含了该类的虚函数指针。这个虚函数表通常位于类的内部,它是静态的,一旦创建就不会更改。
    虚函数表的指针:对于每个包含虚函数的类,编译器会在类的内部添加一个指向虚函数表的指针,通常位于对象的内存布局的最前面。这个指针被称为虚函数表指针(vptr)。
    运行时动态绑定:当你通过基类指针或引用调用虚函数时,实际执行的是派生类中的版本(如果派生类重新定义了虚函数)。这是因为虚函数表指针(vptr)指向了正确的虚函数表,允许在运行时根据对象的类型进行动态绑定。

    1
    2
    Base* ptr = new Derived;
    ptr->foo(); // 调用Derived类中的foo()
  • 虚函数指针会不会变,什么时候初始化,在析构里会不会变,析构函数能访问虚函数吗?
    初始化时创建:虚函数表指针在对象创建时就被初始化。当你创建一个类的实例(对象)时,其中包括一个指向该类虚函数表的虚函数表指针。这个指针是在对象构造过程中初始化的。
    不会在析构函数中改变:虚函数表指针通常在对象的整个生命周期内保持不变。这意味着即使在对象的析构函数中,虚函数表指针也不会改变。析构函数是用于对象销毁的,不负责改变虚函数表指针。
    虚函数的调用:虚函数表指针的主要作用是支持运行时多态性。通过这个指针,程序可以在运行时查找并调用正确的虚函数版本。在析构函数中,你可以调用虚函数,但需要注意的是析构函数本身不会改变虚函数表指针。在析构函数中调用虚函数时,通常执行的是对象的类型,而不是基类的类型。
    注意虚函数表指针变化的情况:在一些特殊情况下,虚函数表指针可能会变化。例如,当一个对象通过复制构造函数复制时,复制的对象将有自己的虚函数表指针。这通常发生在基类和派生类之间的复制。另外,如果你使用虚继承,虚函数表指针也可能会更改。

  • 抽象基类:当类声明中包含纯虚函数时,则不能创建该类的对象。要真正的成为抽象基类,则至少应包含一个纯虚函数。原型中的=0使虚函数成为纯虚函数。C++允许纯虚函数有定义,也可以不定义。纯虚方法是定义派生类的通用接口。

  • 如果基类派生类都使用动态内存分配,则必须为派生类定义显式析构函数、复制构造函数和赋值操作符,也就是说,必须使用相应的基类方法处理基类元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class hasDMA : public baseDMA
    {
    private:
    char * style;
    public:
    ...
    };

    hasDMA & hasDMA::operator =(const hasDMA & hs)
    {
    if (this == &hs)
    return *this;
    baseDMA::operator =(hs); // 重点!
    style = new char[std::strlen(hs.style) + 1];
    std::strcpy(style, hs.style);
    return *this;
    }
  • 派生类对象的友元函数可以通过基类的友元函数访问基类的成员,由于友元不是成员函数,友元函数不能继承,不能使用作用预解析符,所以可以相应类强制类型转换选择正确的函数。<例子>

    1
    2
    3
    4
    5
    6
    7
    std::ostream & operator <<(std::ostream & os, const hasDMA & hs)
    {
    os << (const baseDMA &)hs; // 相应类强制类型转换
    //也可以:os << dynamic_cast<const baseDMA &> (hs)
    os << "Style: " << hs.style << std::endl;
    return os;
    }
  • 通常,包含、私有继承和保护继承用于实现has-a关系,即新的类将包含另一个类的对象。

  • 包含对象成员的类:使用公有继承时,类可以继承接口,可能还有实现。获得接口是is-a关系的组成部分,而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分。

    对比私有继承:

    对于has-a关系来说,类对象不能自动获得被包含对象的接口是一件好事。例如,string类将+操作符重载为将两个字符串连接起来;但从概念上说,将两个Student对象串接起来是没有意义的。
    被包含对象的接口不是公有的,但可以在类方法中使用它。<例子>

    1
    2
    3
    4
    5
    6
    7
    double Student::Average() const
    {
    if (scores.size() > 0)
    return scores.sum() / scores.size();
    else
    return 0;
    }

    私有辅助方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #=> 位于private
    ostream & Student::arr_out(ostream & os) const
    {
    int i;
    int lim = scores.size();
    if (lim > 0)
    {
    for (i = 0; i < lim; i++)
    {
    os << scores[i] << " ";
    if (i % 5 != 0)
    os << endl;
    }
    }
    else
    os << " empty array ";
    return os;
    }
  • 使用using重新定义访问权限:

  • 方法一是定义一个使用该基类方法的派生类方法。

      
    1
    2
    3
    4
    double Student::sum() const
    {
    return std::valarray<double>::sum();
    }
  • 另一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明(将像名称空间那样)来指出派生类可以使用的特定的基类成员,即使使用的是私有派生。注意:using 声明只使用成员名——没有圆括号、函数特征标和返回类型。using声明只适合继承,而不适用于包含。

      
    1
    2
    3
    4
    5
    6
    7
    8
    class Student : private std::string, private std::valarray<double>
    {
    ...
    public:
    using std::valarray<double>::min;
    using std::valarray<double>::max;
    ...
    }
  • 虚基类:虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如:通过在类声明中使用关键字virtual, 可以使Worker被用作Singer和Waiter的虚基类

    1
    2
    class Singer : virtual public Worker { ... };
    class Waiter : public virtual Worker { ... };

    可以将SingingWaiter类定义为

    1
    class SingingWaiter : pulic Singer, public Waiter { ... };
  • 友元类:

  • 当一个类B成为了另外一个类A的“朋友”时,那么类A的私有和保护的数据成员就可以被类B访问。我们就把类B叫做类A的友元。

  • 友元类可以通过自己的方法来访问把它当做朋友的那个类的所有成员。但是我们应该注意的是,我们把类B设置成了类A的友元类,但是这并不会是类A成为类B的友元。说白了就是:甲愿意把甲的秘密告诉乙,但是乙不见得愿意把乙自己的秘密告诉甲。

  • 声明友元类的方法其实很简单,只要我们在类A的成员列表中写下语句:friend class B;这样一来,类B就被声明成了类A的友元。注意,类B虽然是类A的友元,但是两者之间不存在继承关系。这也就是说,友元类和原来那个类之间并没有什么继承关系,也不存在包含或者是被包含的关系。

阅读全文 »

在 Python 中,yield 是一个关键字,用于定义生成器函数。生成器函数可以像正常的函数一样定义,带有参数和代码块,在调用时返回一个迭代器。但与普通函数不同的是,当函数遇到 yield 关键字时,它会暂停执行并返回一个值给外部的调用者。然后,当下一次调用 .next() 或者 next() 方法时,函数会从上次暂停的位置继续执行。

以下是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b

# 调用生成器函数
f = fibonacci()

# 使用 next() 函数获取下一个值
print(next(f)) # 输出:0
print(next(f)) # 输出:1
print(next(f)) # 输出:1
print(next(f)) # 输出:2
print(next(f)) # 输出:3

在上面的例子中,我们定义了一个名为 fibonacci 的生成器函数,它用来生成斐波那契数列。每次调用 next(f) 时,它都会返回下一个斐波那契数,并且函数会在 yield 语句处暂停,等待下一次调用。这种方式可以让我们在需要大量数据处理时,将其分成小块进行处理,降低内存压力,提高程序效率。

说明

etc在linux系统中是配置文件目录名;etcd就是配置服务。诞生于CoreOS公司,最初用于解决集群管理系统中os升级时的分布式并发控制、配置文件的存储与分发等问题,基于此,etcd设计为提供高可用、强一致性的小型kv数据存储服务。
etcd基于go语言实现,主要用于共享配置、服务发现、集群监控、leader选举、分布式锁等场景,用于存储少量重要的数据。

当客户端需要调用某个服务时,它可以向服务注册中心发送查询请求,以获取特定服务的可用节点列表。然后,客户端可以选择其中一个节点进行调用。

架构


1
2
3
4
5
etcdctl get key -w json
{"header": {"cluster_id":1481639068965178418, "member_id":10276657743932975437, "revision": 47, "raft_term":8}, ...}
etcdctl get key --rev=47
etcdctl lease grant 30
etcdctl put key test --lease=33435
阅读全文 »

工具安装

https://studygolang.com/dl下载最新版本。

1
2
3
4
5
6
7
8
9
10
11
12
yum install epel-release
yum update
yum install golang

sudo tar -C /usr/local -xzf go1.13.linux-amd64.tar.gz
ls /usr/local/
bin/ etc/ games/ go/ include/ lib/ man@ sbin/ share/ src/
sudo vim /etc/profile
# 末尾添加
export PATH=$PATH:/usr/local/go/bin
source /etc/profile
go version

项目实战

jupiter

阅读全文 »

基础

npx的全称是Node Package Runner,是一个命令行工具,它可以在不安装包的情况下直接运行Node.js包中的命令。npx的作用类似于npm全局安装包,然后在命令行中执行安装包里的命令,但npx不需要提前安装,它会自动下载所需的包并执行命令。因此,npx被称为“npm 5.2.0+附带的一个功能”。
在 npx 命令后加上 ts-node 命令 tsx,表示使用 ts-node 解析并运行 TypeScript 文件。这种方式可以避免在本地全局安装 ts-node,而且方便在多个项目中使用不同版本的 ts-node。

在 TypeScript 中,?. 表示可选链操作符,它用于访问可能不存在的属性或方法。
在旧版的 JavaScript 中,当试图访问一个不存在的属性或方法时,程序会抛出一个类型为“undefined”的错误。在 TypeScript 中,可以使用可选链操作符?. 来避免这种错误,当访问的对象或属性不存在时,表达式会直接返回 undefined,而不会抛出错误。

异步js

XHR 有 5 种状态:XMLHttpRequest.readyState

阅读全文 »
0%