《C++程序设计原理与实践》笔记 第17章 向量和自由空间

news2024/10/5 21:17:54

本章和后面四章介绍C++标准库的容器和算法部分(通常称为STL)。本章和后面两张的重点是最常用、最有用的STL容器——向量的设计和实现。

17.1 引言

C++标准库中最有用的容器是vectorvector提供了给定类型的元素序列。标准库vector是一个方便、灵活、(时间和空间上)高效、静态类型安全的元素容器。

在本章和后面两章中,我们将展示如何用基本语言功能来构建vector,从而说明有用的概念和编程技术,并通过C++语言特性表达出来。我们在实现vector中遇到的语言功能和编程技术是通用并且被广泛使用的。

我们通过一系列逐渐复杂的vector实现来接近标准库vector

17.2 vector基础

首先考虑vector的一个非常简单的用法:

vector<double> age(4); // a vector with 4 elements of type double
age[0]=0.33;
age[1]=22.0;
age[2]=27.2;
age[3]=54.2;

这段代码创建了一个有四个double类型元素的向量,并给四个元素分别赋值为0.33、22.0、27.2和54.2。这四个元素编号为0、1、2、3。C++标准库容器中的元素编号始终从0开始。 向量中的元素个数称为大小(size),元素编号(索引)从0到size-1。可以用下图来表示age

向量示意图

如何在计算机内存中实现这种“图形化设计”?显然,我们需要定义一个类vector,它需要一个存储大小的数据成员和一个存储元素的数据成员。但是如何表示一组数量可变的元素?我们需要第一个元素的内存地址。在C++中,可以存储地址的数据类型称为指针(pointer)。

于是,可以定义我们自己的第一个版本的vector类:

// a very simplified vector of doubles
class vector {
    int sz;        // the size
    double* elem;  // pointer to the first element (of type double)
public:
    vector(int s); // constructor: allocate s doubles,
                   // let elem point to them
                   // store s in sz
    int size() const { return sz; }  // the current size
};

在进行vector的设计之前,首先学习“指针”和与它紧密相关的“数组”的概念——这是C++“内存”概念的关键。

17.3 内存、地址和指针

计算机内存是一个字节序列,这些字节从0开始编号。用于指示内存位置的编号称为地址(address)。地址本质上就是一个整数。例如,可以将1 MB (= 210 KB = 220 B)内存可视化为以下图形:

内存示意图

其中每个方格表示一个字节(内存单元),这些字节的编号(地址)依次为0, 1, 2, …, 220-1。

内存中的任何对象都有一个地址。例如:

int var = 17;

该语句为var分配了一块“int大小”(通常是4字节)的内存空间,并将值17放入这块内存。保存地址的对象称为指针(pointer),在语法上T*是“指向T的指针”。例如:

int* ptr = &var;  // ptr holds the address of var

取地址(address of)运算符&用于获得对象的地址。在这里ptr是指向var的指针,其值是var的地址。假设var的地址是4096,则ptr的值为4096:

指针示意图

注:假设一个int占4字节,则var实际上占用了4096~4099连续的4个字节,对象的地址是首字节的地址。

解引用(dereference)运算符*用于访问指针指向的对象。例如:

cout << "pi==" << pi << "; contents of pi==" << *pi << "\n";

可能的输出为

pi==0x649ff8b4; contents of pi==17

pi的值取决于编译器在内存中为变量var分配的地址(每次运行程序都会变化)。指针的值(地址)通常用十六进制表示。

解引用运算符也可以用于赋值运算符的左侧(pi是指针本身,*pi是指针指向的对象):

*pi = 27;  // OK: you can assign 27 to the int pointed to by pi

注意,即使指针的值可以被打印为一个整数,但指针不是int,二者不能混淆:

int i = pi;  // error: can't assign an int* to an int
pi = 7;     // error: can't assign an int to an int*

不同类型的指针也不能混用。例如,指向char的指针不能指向int

char* pc = pi;  // error: can't assign an int* to a char*
pi = pc;        // error: can't assign a char* to an int*

另见:《C程序设计语言》笔记 第5章 指针与数组

17.3.1 sizeof运算符

运算符sizeof用于获取对象或类型的大小(占用的内存字节数):

void sizes(char ch, int i, int* pi) {
    cout << "the size of char is " << sizeof(char) << ' ' << sizeof(ch) << '\n';
    cout << "the size of int is " << sizeof(int) << ' ' << sizeof(i) << '\n';
    cout << "the size of int* is " << sizeof(int*) << ' ' << sizeof(pi) << '\n';
}

sizeof可用于类型名或表达式。sizeof的结果是一个正整数,单位是sizeof(char)(定义为1)。

试一试

C++基本类型的长度如下表所示:

类型长度指针类型指针长度
bool1bool*4或8
char1char*4或8
short2short*4或8
int2或4int*4或8
long4或8long*4或8
long long8long long*4或8
float4float*4或8
double8double*4或8

注:

  • C++标准规定int类型的大小至少是2字节,在不同的机器上可能是2字节或4字节;类似地,long类型的长度可能是4字节或8字节。详见Fundamental types “Integer types” 一节。
  • 标准库头文件<cstdint>定义了一组固定宽度整数类型(别名),例如int32_tint64_t等。
  • 对于指针类型,在32位系统上是4字节,在64位系统上是8字节(指针的大小只与内存地址空间的大小有关,与实际类型无关)。

一个vector占用多少内存?尝试

cout << "the size of vector<int>(10) is " << sizeof(vector<int>(10)) << '\n';
cout << "the size of vector<int>(100) is " << sizeof(vector<int>(100)) << '\n';
cout << "the size of vector<int>(1000) is " << sizeof(vector<int>(1000)) << '\n';

输出为

the size of vector<int>(10) is 24
the size of vector<int>(100) is 24
the size of vector<int>(1000) is 24

看过本章和下一章后,这个解释将会变得很明显,但显然sizeof不计入向量元素。

注:vector对象本身只包含大小和指向元素的指针等成员,元素不属于vector对象的一部分,否则vector对象的大小将是不确定的。

17.4 自由存储和指针

C++程序的内存布局(memory layout)如下图所示:

内存布局

整个内存空间被划分为四部分:

  • 代码段(code segment):也称为文本段(text segment),包含由代码编译生成的机器指令。
  • 数据段(data segment):也称为静态存储(static storage),包含全局变量和静态变量。
  • (stack):也称为自动存储(automatic storage),包含局部变量和函数参数。
  • (heap):也称为自由存储(free storage),可以通过new运算符动态分配的内存。

参考:Memory Layout of C Programs

注:栈内存中的变量离开作用域时自动释放内存,而堆内存必须手动释放,否则将导致内存泄露。

例如:

double* p = new double[4];  // allocate 4 doubles on the free store

该语句在自由存储中分配4个double大小(32字节)的内存空间,并返回指向首地址的指针(假设为850),如下图所示:

自由存储分配

17.4.1 自由存储分配

new运算符从自由存储中分配内存,返回首字节的地址(指向它创建的对象或数组首元素的指针)。如果对象的类型是T,则返回的指针类型是T*

new运算符可以为单个对象或数组分配内存。例如:

int* pi = new int            // allocate one int
int* qi = new int[4];        // allocate 4 ints (an array of 4 ints)

double* pd = new double;     // allocate one double
double* qd = new double[n];  // allocate n doubles (an array of n doubles)

注意分配的元素个数可以是变量,这允许我们在运行时指定分配多少个对象。

17.4.2 通过指针访问

除了在指针上使用解引用运算符*,还可以使用下标运算符[],这两个运算符既可用于读也可用于写。例如:

double* p = new double[4];  // allocate 4 doubles on the free store
double x = *p;              // read the (first) object pointed to by p
double y = p[2];            // read the 3rd object pointed to by p

*p = 7.7;                   // write to the (first) object pointed to by p
p[2] = 9.9;                 // write to the 3rd object pointed to by p

当用于指针时,[]运算符将内存视为一系列对象(类型由指针声明指定)。p[i]等价于*(p+i)

注:只有当指针指向数组中的元素时,使用[]运算符才有意义。

17.4.3 范围

指针的主要问题是指针并不“知道”它指向多少个元素(甚至不知道它指向单个对象还是数组中的某个元素)。例如:

double* p = new double;        // allocate a double
double* q = new double[1000];  // allocate 1000 doubles
q[700] = 7.7;       // fine
q = p;              // let q point to the same as p
double d = q[700];  // out-of-range access!

其中第二个q[700](也就是p[700])是越界(out-of-range)访问,p[700]这个内存位置可能是其他对象的一部分。越界访问是一个典型的灾难性错误:“灾难性”表现为“程序神秘地崩溃”或者“程序给出错误的输出”。越界访问是特别令人讨厌的。越界读取会得到一个“随机”值,这取决于某些完全无关的对象;而越界写入可能会给某些对象意外的错误值。

我们必须保证这种越界访问不会发生。 我们使用vector而不是直接使用new分配的内存的原因之一是vector知道它的大小,因此可以很容易地避免越界访问。

17.4.4 初始化

我们希望确保指针及其指向的对象都是被初始化的。考虑:

double* p0;                    // uninitialized: likely trouble
double* p1 = new double;       // get (allocate) an uninitialized double
double* p2 = new double(5.5);  // get a double initialized to 5.5
double* p3 = new double[5];    // get (allocate) 5 uninitialized doubles

未初始化的指针是野指针(wild pointer),例如上面的p0。由于其值是随机的,使用未初始化的指针必定导致越界访问。

对于内置类型,new分配的内存是未初始化的,例如p1。可以使用(){}来初始化,例如p2

new创建的数组默认也是未初始化的。可以指定初始化列表(此时可以省略元素个数):

double* p4 = new double[5]{0, 1, 2, 3, 4};
double* p5 = new double[]{0, 1, 2, 3, 4};

注:如果指定了元素个数,但初始化列表中初始值个数少于元素个数,则剩余元素将被默认初始化。

对于自定义类型可以更好地控制初始化。如果类型X有默认构造函数,则未指定初始值时将使用默认构造:

X* px1 = new X;      // one default-initialized X
X* px2 = new X[17];  // 17 default-initialized Xs

如果类型Y有构造函数,但没有默认构造函数,则必须显式地初始化:

Y* py1 = new Y;      // error: no default constructor
Y* py2 = new Y(13);  // OK: initialized to Y(13)
Y* py3 = new Y[17];  // error: no default constructor
Y* py4 = new Y[17]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};

当数组只有几个元素时,初始值列表将会很方便。

17.4.5 空指针

如果没有可用的值来初始化指针,则使用空指针(null pointer),nullptr

double* p = nullptr;  // the null pointer

空指针的值为0,通常用于测试一个指针是否有效(例如是否指向某个对象),例如if (p != nullptr),等价于if (p)。注意,对于未初始化的指针,这个测试是没有意义的。

注:

  • 空指针可用于函数返回值表示异常情况,例如“未找到”或“发生了错误”。
  • nullptr是C++11引入的关键字,可以隐式转换为任意类型的指针。旧的代码使用0NULL表示空指针。

17.4.6 自由存储释放

由于一台计算机的内存是有限的,因此在使用结束后应当将内存释放回自由存储,从而自由存储可以将这些内存重新用于新的分配。对于大型程序和长时间运行的程序(例如操作系统、嵌入式系统)来说,这种释放内存并重新使用是很重要的。

如果使用new分配的内存没有被释放,并且离开了保存其地址的指针的作用域,则会发生内存泄露(memory leak)(因为之后再也没有机会去释放)。例如:

double* calc(int res_size, int max) {
    double* p = new double[max];  // leaks memory
    double* res = new double[res_size];
    // use p to calculate results to be put in res
    return res;
}

double* r = calc(100,1000);

每次调用calc()会“泄露”分配给pdouble数组所占用的内存(如果调用者不释放r,同样会造成内存泄露)。

将内存释放回自由存储的运算符是delete,用于new返回的指针。有两种形式的delete

  • delete p释放由new分配的单个对象的内存。
  • delete[] p释放由new分配的数组的内存。

上面的例子修正后变为

double* calc(int res_size, int max) {
    // the caller is responsible for the memory allocated for res
    double* p = new double[max];
    double* res = new double[res_size];
    // use p to calculate results to be put in res
    delete[] p;  // we don't need that memory anymore: free it
    return res;
}

double* r = calc(100,1000);
// use r
delete[] r;  // we don't need that memory anymore: free it

这个例子说明了使用自由存储的一个主要原因:我们可以在一个函数中创建对象,并将其返回给调用者(来避免值拷贝)。

注:

  • 如果函数返回了由new返回的指针,则调用者负责释放该内存,否则会发生内存泄露。
  • C++11引入了智能指针shared_ptrunique_ptr,能够自动释放内存,定义在头文件<memory>中。
  • C++17引入了拷贝消除(copy elision)/返回值优化(return value optimization, RVO)特性:在特定情况下,即使函数返回值是值类型也不会发生值拷贝。

删除一个对象两次是一个严重的错误。 例如:

int* p = new int(5);
delete p;  // fine: p points to an object created by new
// ... no use of p here ...
delete p;  // error: p points to memory owned by the free-store manager

第二次delete p时,你已不再拥有p指向的内存,自由存储管理器可能已经将其回收并分配给其他对象,删除该对象(由程序的其他部分拥有)会引起程序错误。

删除空指针不会做任何事,因此删除空指针是无害的。

实际上,编译器可以知道我们什么时候不再需要一块内存,并在没有人为干预的情况下回收它。这叫做自动垃圾收集(garbage collection, GC)。然而自动垃圾收集并不是无代价的,也不是对所有应用都是理想的。在C++中,(通过new使用自由存储时)程序员必须自己处理“垃圾”。

注:

  • Java、Python等语言内置了垃圾收集器,能够自动管理内存。
  • 在C++中可以通过标准库容器、智能指针等工具来简化垃圾收集工作。

17.5 析构函数

现在可以用自由存储分配实现17.2节中vector的构造函数:

vector(int s) :sz(s), elem(new double[s]{0}) {}

构造函数分配了sdouble,并全部初始化为0。

然而,这个vector会泄露内存,因为在构造函数中使用new分配的内存没有被释放。考虑:

void f(int n) {
    vector v(n);  // allocate n doubles
    // ... use v ...
}

当函数f()返回时,v在自由存储中分配的内存没有被释放。我们可以为vector定义一个clean_up()并调用它:

void f2(int n) {
    vector v(n);   // allocate n doubles
    // ... use v ...
    v.clean_up();  // clean_up() deletes elem
}

这是可以的。但是,关于自由存储的一个最常见的问题是人们忘记deleteclean_up()也有同样的问题:人们可能忘记调用它。更好的做法是使编译器知道一个与构造函数具有相反功能的函数——析构函数(destructor)(如9.4.2节所述,使用构造函数来避免忘记调用初始化函数)。当一个类的对象被创建时构造函数将被隐式调用,同样,当一个对象离开作用域时析构函数将被隐式调用。构造函数确保对象被正确地创建和初始化,相反,析构函数确保对象被正确地销毁(释放了该释放的“资源”)。

T的析构函数名为~T。例如:

// destructor: free memory
~vector() {
    delete[] elem;
}

这样就可以写出:

void f3(int n) {
    double* p = new double[n];  // allocate n doubles
    vector v(n);  // the vector allocates n doubles
    // ... use p and v ...
    delete[ ] p;  // deallocate p's doubles
}  // vector automatically cleans up after v

显然,delete[]相当繁琐并且容易出错。有了vector,就没有必要再使用new分配内存,并记得用delete释放。vector已经做了,而且做得更好。

注:对于new创建的单个对象,可以用智能指针来代替。

每个拥有资源(自由存储、文件、缓冲区、线程、锁等)的类都需要析构函数,在构造函数中获取资源,在析构函数中释放资源。vector的构造函数/析构函数处理自由存储内存就是一个典型的例子。

17.5.1 生成的析构函数 析构顺序

如果一个类没有定义析构函数,则编译器会自动生成一个析构函数(相当于函数体为空)。

无论是自定义的还是生成的析构函数,在执行完函数体之后,编译器还会调用数据成员的析构函数和基类的析构函数。“析构函数应该被调用”这一保证就是这样实现的。

注:这一规则保证了析构顺序和构造顺序是相反的

  • 构造顺序:基类 → 成员(按照声明顺序) → 自身
  • 析构顺序:自身 → 成员(按照声明相反顺序) → 基类

例如:

struct Customer {
    string name;
    vector<string> addresses;
};

void some_fct() {
    Customer fred;
    // initialize fred
    // use fred
}

当退出some_fct()后,fred将被销毁,nameaddresses的析构函数将被调用。

所有的规则可以总结为:当对象被销毁时(离开作用域、delete等),析构函数被调用。

17.5.2 析构函数和自由存储 虚析构函数

考虑一个自由存储和类层次结构结合使用的例子:

Shape* fct() {
    Text tt(Point(200, 200), "Annemarie");
    // ...
    Shape* p = new Text(Point(100, 100), "Nicholas");
    return p;
}

void f() {
    Shape* q = fct();
    // ...
    delete q;
}

在函数fct()中,Text对象tt在离开fct()时被销毁,tt及其成员和基类Shape的析构函数将被调用。但是,对于从fct()返回的由new创建的Text对象,调用函数f()不知道q指向一个Text,那么delete q是如何调用Text的析构函数的?

在14.2.1节,我们略过了一个事实:Shape有一个虚析构函数,这正是问题的关键。当执行delete q时,delete会查看q的类型以确定是否需要调用析构函数,如果是则调用。因此delete q调用Shape的析构函数~Shape()。而~Shape()是虚函数,根据虚函数调用机制,实际会调用派生类的析构函数,在这里是~Text()(之后又自动调用了~Shape())。如果~Shape()不是虚函数,则~Text()将不会被调用,Textstring成员将不会被正确销毁。

经验法则:如果一个类作为基类,则需要虚析构函数。 否则当通过基类指针delete一个由new创建的派生类对象时,派生类的析构函数不会被调用,从而派生类对象不会被正确销毁。

试一试

17.6 访问元素

为了读写向量元素,我们提供简单的get()set()成员函数:

double get(int n) { return elem[n]; }       // access: read
void set(int n, double v) { elem[n] = v; }  // access: write

这里在elem指针上使用[]运算符访问元素,如17.4.2节所述。

现在可以这样使用vector

vector v(5);
for (int i = 0; i < v.size(); ++i) {
    v.set(i, 1.1*i);
    cout << "v[" << i << "]==" << v.get(i) << '\n';
}

与常用的下标符号相比,使用get()set()的代码很丑陋。18.4节将为vector定义[]运算符。

当前版本的vector:简单向量v1

17.7 指向类对象的指针

“指针”的概念是通用的,可以指向内存中的任何对象。例如,可以使用vector的指针:

vector* f(int s) {
    vector* p = new vector(s);  // allocate a vector on free store
    // fill *p
    return p;
}

void ff() {
    vector* q = f(4);
    // use *q
    delete q;  // free vector on free store
}

请记住:位于构造函数之外的new会带来忘记delete它所创建的对象的机会。除非你有一个好的删除对象的策略(例如Vector_ref),否则尽量将new放在构造函数中、delete放在析构函数中。

给定一个对象名,可以使用.运算符访问成员:

vector v(4);
int x = v.size();
double d = v.get(3);

类似地,给定一个对象指针,可以使用->运算符访问成员:

vector* p = new vector(4);
int x = p–>size();
double d = p–>get(3);

p->m等价于(*p).m

.->通常称为成员访问运算符(member access operators)。

17.8 类型混用:void*和类型转换

偶尔,我们不得不放弃类型系统的保护(例如,与不知道C++类型的其他语言交互,与没有按照静态类型安全设计的旧代码交互)。在这种情况下,我们需要两样东西:

  • 一种指针,指向不知道保存的是何种对象的内存
  • 一种操作,告诉编译器这种指针指向的是哪种类型(未经证实)

类型void*的含义是“指向编译器不知道类型的内存的指针”。当我们想在代码之间传递不知道实际类型的地址时,就需要使用void*。例如回调函数的“地址”参数(16.3.1节),以及最底层的内存分配器(例如new运算符的实现)。

void用于表示“没有返回值”,不存在void类型的对象,因此void*指针不能被解引用。任何类型的指针都可以赋值给void*指针,反之则必须用static_cast进行显式类型转换(explicit type conversion)或者称为转换(cast)。例如:

void* pv = new int;  // OK: int* converts to void*
void* pv2 = pv;      // copying is OK (copying is what void*s are for)
double* pd = pv;     // error: cannot convert void* to double*
*pv = 7;             // error: cannot dereference a void* (we don't know what type of object it points to)
pv[2] = 9;           // error: cannot subscript a void*
int* pi = static_cast<int*>(pv);  // OK: explicit conversion

C++提供的显式类型转换运算符:

  • static_cast:将一种类型转换为另一种相关的类型(例如intdoublevoid*double*)。
  • dynamic_cast:将基类指针/引用转换为派生类指针/引用(反之不需要转换)。如果转换失败,对于指针类型返回空指针,对于引用类型抛出std::bad_cast异常。
  • const_cast:移除const属性。
  • reinterpret_cast:在两个不相关的类型之间转换,例如intdouble*

只有在完全有必要时才使用static_cast,而const_castreinterpret_cast是两个更危险的转换。不要期望使用reinterpret_cast的代码可以移至。

17.9 指针和引用

引用可以看作是一种自动解引用的、不可变的指针,或者是对象的别名。指针和引用的区别:

  • 指针可以不初始化(野指针),也可以不指向任何对象(空指针),而引用必须初始化
  • 给指针赋值会改变指针本身的值,使其指向其他对象;而引用在初始化之后不能引用其他对象,给引用赋值改变的是被引用的对象的值。
  • 指针通过new&获取,使用*[]解引用;引用通过对象名获取,不需要解引用。

注:

  • 指针的const属性分为指针本身和被指向的对象:
类型p可变*p可变
T*
const T* (等价于T const*
T* const
const T* const
  • const属性方面,T&相当于T* constconst T&相当于const T* const

例如:

int x = 10;
int* p = &x;    // you need & to get a pointer
*p = 7;         // use * to assign to x through p
int x2 = *p;    // read x through p
int* p2 = &x2;  // get a pointer to another int
p2 = p;         // p2 and p both point to x
p = &x2;        // make p point to another object

对应的引用的例子如下:

int y = 10;
int& r = y;    // the & is in the type, not in the initializer
r = 7;         // assign to y through r (no * needed)
int y2 = r;    // read y through r (no * needed)
int& r2 = y2;  // get a reference to another int
r2 = r;        // the value of y is assigned to y2
r = &y2;       // error: you can't change the value of a reference
               // (no assignment of an int* to an int&)

注意:

  • r2 = r相当于*p2 = *p,而不是p2 = p
  • r = &y2是类型错误,不存在给引用本身赋值使其引用另一个对象的操作。

指针和引用都是使用内存地址实现的,只是在用法上稍有不同。

17.9.1 指针参数和引用参数

当你希望将一个变量的值改为由函数计算的结果时,有三种选择:返回值、指针参数和引用参数。例如:

int incr_v(int x) { return x+1; }  // compute a new value and return it
void incr_p(int* p) { ++*p; }      // pass a pointer (dereference it and increment the result)
void incr_r(int& r) { ++r; }       // pass a reference

每种方法各有利弊,需要根据具体的函数及其用法决定。使用指针参数会提醒程序员参数可能被修改,但需要判断空指针;相反,引用参数“看起来很无辜”(看不出参数会被修改),但可以确保引用了一个对象。

因此,答案是“取决于函数的性质”:

  • 对于小的对象,使用传值。
  • 对于“没有对象”(用空指针表示)是合法参数的函数,使用指针参数(记得判断空指针)。
  • 否则,使用引用参数。

另见8.5.6节。

17.9.2 指针、引用和继承

如14.4节所述,派生类可以被用在需要其公有基类的地方(接口继承)。对于指针或引用,这一思想表达为:如果BD的公有基类,则D*可以被隐式转换为B*D&可以被隐式转换为B&。例如:

void rotate(Shape* s, int n);  // rotate *s n degrees

Shape* p = new Circle(Point(100, 100), 40);
Circle c(Point(200, 200), 50);
rotate(p, 35);
rotate(&c, 45);

对于引用是类似的:

void rotate(Shape& s, int n);  // rotate s n degrees

Shape& r = c;
rotate(r, 55);
rotate(*p, 65);
rotate(c, 75);

这对于大多数面向对象编程技术来说至关重要。

17.9.3 示例:链表

17.9.4 链表操作

17.9.5 链表的使用

17.10 this指针

17.10.1 更多用法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/449551.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis从零上手

文章目录 NoSQL概述互联网发展史单机MySQL的年代&#xff08;90年代&#xff09;缓存时代: Memcached MySQL 垂直拆分(读写分离)分库分表 水平拆分(MySQL集群)如今最近的年代 为什么要用NoSQL什么是NoSQLNoSQL特点阿里巴巴演进分析阿里巴巴中文站架构发展历程简单分析 NoSQL…

MySQL架构设计其实很简单

一、概述 我们很多互联网项目首选的数据库就是MySql5.7&#xff0c;因为免费&#xff0c;支持数据库主从复制&#xff0c;读写分离&#xff0c;且语法简单&#xff0c;容易上手&#xff0c;虽然对sql的函数、触发器、存储过程有一定的了解&#xff0c;也能说出一些优化sql语句…

基于SpringBoot+Vue的前后端分离的博客管理系统

菜单 首页写文章内容管理 博客管理相册管理归档登录、注册(项目整合了shiro+jwt+redis,用户未登录不能发布博客、内容管理、相册、归档等功能) 文章​​​​​​​末尾赋前后端代码以及数据库文件链接!!! 1 首页 首页包括菜单、博客、登录用户、推荐博客排行、博客分类…

Autosar-Interface接口

文章目录 一、Autosar接口1、AUTOSAR Interface2、Standardized Interface3、Standardized AUTOSAR Interface二、AUTOSAR接口的通用规则三、示例-存储四、总结一、Autosar接口 在Autosar里规定了三类接口,分别是AUTOSAR接口,标准AUTOSAR接口和标准接口。 下图将所有的接口…

【Linux】I/O复用

概述 I/O复用使得程序能同时监听多个文件描述符&#xff0c;看文件上是否有用户感兴趣的事件发生&#xff0c;提高程序性能。 使用I/O复用技术的情况&#xff1a; 客户端同时处理多个套接字 客户端同时处理用户输入和网络连接 同时监听多个端口 同时处理TCP和UDP请求 TCP服务器…

【JavaEE】文件操作(InputStream、OutputStream)

博主简介&#xff1a;想进大厂的打工人博主主页&#xff1a;xyk:所属专栏: JavaEE初阶 在Java中总会用到文件操作&#xff0c;比如从盘符读写文件&#xff0c;按字节和字符进行数据读写&#xff0c;那么你真的认识什么是文件路径吗&#xff1f;平时谈到的“文件”&#xff0c;…

mybatis实现CRUD详解(使用mapper映射文件实现增删改查)

mybatis实现CRUD详解(使用mapper映射文件实现增删改查) 创建maven项目&#xff1a;项目结构如图所示 准备数据库表&#xff1a; 准备pom.xml所需的依赖&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.…

快速弄懂RPC

快速弄懂RPC 常见的远程通信方式远程调用RPC协议RPC的运用场景和优势 常见的远程通信方式 基于REST架构的HTTP协议以及基于RPC协议的RPC框架。 远程调用 是指跨进程的功能调用。 跨进程可以理解为一个计算机节点的多个进程或者多个计算机节点的多个进程。 RPC协议 远程过…

C++string类

&#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;C &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 本博客主要内容讲解了C中的string类以及类内的一些常用函数 Cstring类 文章…

k8s_3节点集群部署

背景 近期想在自己电脑上部署一套3节点K8s 作为自己平时的学习测试环境。 本来想看一下有没有比较便捷的部署方式如&#xff1a; rancherdesktop: https://docs.rancherdesktop.io/zh/next/getting-started/installation/ sealos: https://www.sealos.io/docs/getting-started…

【超算/先进计算学习】日报3

目录 今日已完成任务列表遇到的问题及解决方案任务完成详细笔记作业管理系统 slurm作业管理系统及常用用户命令节点状态查询/作业提交命令作业队列查询/作业取消命令任务练习 调试器 GDB实时调试core 文件调试 对自己的表现是否满意简述下次计划其他反馈 今日已完成任务列表 4-…

Spring的体系结构

Spring的体系结构 一、Spring简介1. Spring介绍1.1 为什么要学1.2 学什么1.3 怎么学 2. 初识Spring2.1 Spring家族2.2 Spring发展史 3. Spring体系结构3.1 Spring Framework系统架构图3.2 Spring Framework课程学习路线 4. Spring核心概念4.1 目前我们代码存在的问题4.2 核心概…

『Linux从入门到精通』第 ⑫ 期 -深入了解冯诺依曼体系结构与操作系统(Operator System)

文章目录 &#x1f490;专栏导读&#x1f490;文章导读&#x1f337;计算机之父——冯诺依曼&#x1f337;冯诺依曼体系结构&#x1f33a;木桶效应 &#x1f337;操作系统(Operator System)&#x1f33a;如何理解操作系统&#x1f33a;系统调用和库函数概念 &#x1f490;专栏导…

论文结构商讨

论文查重 pass软件 提出问题 1、由于选取的算法太多了&#xff0c;不知道是简单介绍&#xff0c;还是深入介绍 希望找到一个具体的模板 2、数据库要是用一个就在前面介绍&#xff0c;用不一样的就在每章里面分别介绍 4、数据集介绍放在那个具体位置&#xff1f; 如果可以画出…

蓝精灵事件来袭: 深受喜爱的蓝色角色来 Web3 啦!

起源于漫画并成为国际知名的卡通和电影明星的蓝色x小家伙正在进入 NFT 舞台。 作者&#xff1a;Coindesk——Toby Bochan 因漫画、卡通片和电影而闻名的热门动画系列“蓝精灵”正在展开新的冒险&#xff0c;由蓝精灵协会推出一系列新的非同质化通证&#xff08;NFT&#xff09;…

汽车ECU的内部构成与功能模块

摘要&#xff1a; ECU作为与外部通信的接口&#xff0c;为了通过微控制器运算实现符合实际状态的最佳控制&#xff0c;需要检测外部状态的输入手段及实际控制驱动的输出手段。 1. 常规功能模块 ① 电源&#xff1a;向ECU内的各模块提供稳定的电压&#xff08;5V、3V等&#xf…

sequelize + Nodejs + MySQL 的简单用法

How to Use Sequelize ORM in NodeJS - Tutorial 1 Sequlize 简介 Sequelize 是最流行的可以与 Nodejs 一起使用的一种关系数据库 ORM (Object-relational mapping 对象关系映射)&#xff0c;Mongoose 是 MongoDB 的 ORM. Sequelize 的作用&#xff0c;简单地说&#xff0c;就…

Netty编解码器,Netty自定义编解码器解决粘包拆包问题,Netty编解码器的执行过程详解

文章目录 一、编解码器概述1、编解码器概述2、编码器类关系图3、解码器类关系图 二、以编解码器为例理解入站出站1、Server端2、Client端3、编解码器3、执行查看结果4、注意事项 三、Netty其他内置编解码器1、ReplayingDecoder2、其他编码器3、内置编解码器处理粘包拆包问题 四…

[LeetCode复盘] LCCUP‘23春季赛 20230422

[LeetCode复盘] LCCUP23春季赛 20230422 一、总结二、 1. 补给马车1. 题目描述2. 思路分析3. 代码实现 三、2. 探险营地1. 题目描述2. 思路分析3. 代码实现 四、 3. 最强祝福力场1. 题目描述2. 思路分析3. 代码实现 五、 4. 传送卷轴1. 题目描述2. 思路分析3. 代码实现 六、 5…

REDIS03_AOF概述、工作流程、写回策略、正常异常流程、重写机制、配置文件详解

文章目录 ①. AOF - 概述作用②. AOF - 工作流程③. 缓冲区 - 写回策略④. 配置文件说明(6 VS 7)⑤. 正常、异常恢复⑥. AOF - 优劣势⑦. AOF - 重写机制⑧. AOF优化配置项详解⑨. RBD和AOF共存模式 ①. AOF - 概述作用 ①. 官网介绍 ②. 以日志的形式来记录每个写操作,将Red…