c++primier第十二章类和动态内存

本章内容包括:

  • 对类成员使用动态内存分配
  • 隐式和显式地复制构造函数
  • 隐式和显式地重载赋值操作符
  • 在构造函数中使用new所必须完成的工作
  • 使用静态类成员

  • 将布局new操作符用于对象
  • 使用指向对象的指针
  • 实现队列抽象数据类型(ADT)

 动态内存和类

复习范例和静态类成员

首先设计一个 StringBad类,然后设计一个功能稍强的 String类。

StringBad 和 String 类对象将包含一个字符串指针和一个表示字符串长度的值。这里使用 StingBad 和String 类,主要是为了深入了解 new、delete 和静态类成员的工作原理。因此,构造函数和析构函数调用时将显示一些消息,以便读者能够按照提示来完成操作。

对这个声明,需要注意的有两点。首先,它使用char指针(而不是char 数组)来表示姓名。这意味着类声明没有为字符串本身分配存储空间,而是在构造函数中使用new来为字符串分配空间。这避免了在类声明中预先定义字符串的长度。

strngbad.h

#include <iostream>
#ifndef STRNGBAD_H_
#define STRNGBAD_H_class StringBad
{
private:char *str;int len;static int num_strings;public:StringBad(const char *s);StringBad();~StringBad(); // destructor// friend functionfriend std::ostream &operator<<(std::ostream &os, const StringBad &st);
};
#endif

strngbad.cpp

#include <cstring>
#include "strngbad.h"
using std::cout;// initialjzing static class member
int StringBad::num_strings = 0;
// class methods
// construct StringBad fromC string
StringBad::StringBad(const char *s)
{len = std::strlen(s);str = new char[len + 1];std::strcpy(str, s);num_strings++;cout << num_strings << ": \" " << str << "\" object created\n";
}StringBad::StringBad()
{len = 4;str = new char[4];std::strcpy(str, "C++");num_strings++;cout << num_strings << ": \"" << str << "\"default object created\n";
}
StringBad::~StringBad()
{cout << "\"" << str << "\" object deleted, ";--num_strings;cout << num_strings << "left\n";delete[] str;
}
std::ostream &operator<<(std::ostream &os, const StringBad &st)
{os << st.str;return os;
}

这条语句将静态成员num stings的值初始化为0。请注意,不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。

初始化是在方法文件中,而不是在类声明文件中进行的,这是因为类声明位于头文件中,程序可能将头文件包括在其他几个文件中。如果在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误。

析构函数首先指出自己何时被调用。这部分包含了丰富的信息,但并不是必不可少的。不过,delete语句却是至关重要的。str 成员指向new 分配的内存。当StringBad 对象过期时,str 指针也将过期。但 str指向的内存仍被分配,除非使用deete 将其释放。删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存。因此,必须使用析构函数。在析构函数中使用delete 语句可确保对象过期时,由构造函数使用 new分配的内存被释放。

vegnews.cpp

#include <iostream>
using std::cout;
#include "strngbad.h"void callme1(StringBad &); // pass by reference
void callme2(StringBad);   // pass by value
int main()
{using std::endl;StringBad headlinel("Celery stalks at Midnight");StringBad headline2("Lettuce Prey");StringBad sports("Spinach Leaves Bowl for Dollars");cout << "headlinel:" << headlinel << endl;cout << "headline2:" << headline2 << endl;cout << "sports:" << sports << endl;callme1(headlinel);cout << "headlinel:" << headlinel << endl;callme2(headline2);cout << "headline2:" << headline2 << endl;cout << "Initialize one object to another:\n";StringBad sailor = sports;cout << "sailor:" << sailor << endl;cout << "Assign one object to another:\n";StringBad knot;knot = headlinel;cout << "knot: " << knot << endl;cout << " End of main()\n ";return 0;
}void callme1(StringBad &rsb)
{cout << "String passed by reference: \n";cout << "    \"" << rsb << "\"\n";
}void callme2(StringBad sb)
{cout << "String passed by value: \n";cout << sb << "\"\n";
}

提前中断

程序说明

callme1()按引用传递没有发生问题

callme2()按值传递发生问题

首先,将 headline2作为函数参数来传递从而导致析构函数被调用。其次,虽然按值传递可以防止原始参数被修改,但实际上函数已使原始字符串无法识别,导致显示一些非标准字符(显示的具体内存取决于内存中包含的内容)。

当您使用一个对象来初始化另一个对象时,编译器将自动生成上述构造函数(称为复制构造函数,因为它创建对象的一个副本)。自动生成的构造函数不知道需要更新静态变量num_strings,因此会将计数方案搞乱。实际上,这个例子说明的所有问题都是由编译器自动生成的成员函数引起的,下面介绍这主题。

除去所有赋值操作,结果显示正常。

隐式成员函数

StringBad 类中的问题是由自动定义的隐式成员函数引起的,这种函数的行为与类设计不符。

具体来说C++自动提供了下面这些成员函数:

  • 默认构造函数,如果没有定义构造函数
  • 复制构造函数,如果没有定义
  • 赋值操作符,如果没有定义
  • 默认析构函数,如果没有定义
  • 地址操作符,如果没有定义

 更准确地说,编译器将生成上述最后4个函数的定义---如果程序使用对象的方式要求这样做。例如,如果您将:个对象赋给另一个对象,编译器将提供赋值操作符的定义。

结果表明,StringBad 类中的问题是由隐式复制构造函数和隐式赋值操作符引起的。

默认构造函数

复制构造函数

复制构造函数的问题

注意:

 使用显式复制构造函数

解决类设计中这种问题的方法是进行深度复制(deepcopy)。

复制构造函数应当复制字符串并将副本的地址赋给s成员,而不仅仅是复制字符串地址。这样每个对象都有自己的字符串,而不是引用另一个对象的字符串。

调用析构函数时都将释放不同的字符串,而不会试图去释放已经被释放的字符串。

可以这样编写Sting的复制构造函数:

必须定义复制构造函数的原因在于,
一些类成员是使用new初始化的、指向数据的指针,而不是数据本身。

赋值操作符

C++允许类对象赋值,这是通过自动为类重载赋值操作符实现的。
这种操作符的原型如下:

赋值的问题 

解决赋值的问题

对于由于默认赋值操作符不合适而导致的问题,解决办法是提供赋值操作符(进行深度复制)定义其实现与复制构造函数相似,但也有一些差别

 代码首先检查自我复制,这是通过查看赋值操作符右边的地址(&s)是否与接收对象(this)的地址相同来完成的。如果相同,程序将返回*this,然后结束。

如果地址不同,函数将释放 str指向的内存,这是因为稍后将把一个新字符串的地址赋给 str。如果不首先使用 delete操作符,则上述字符串将保留在内存中。由于程序中不再包含指向该字符串的指针,因此这些内存被浪费掉。

改进后的新String类

头文件

// string1.h -- fixed and augmented string class definition
#include <iostream>
using std::istream;
using std::ostream;
#ifndef STRINGl_H_
#define STRINGI_H_
class String
{
private:char *str;int len;// pointer to string// length of stringstatic int num_strings;       // number of objectsstatic const int CINLIM = 80; // cin input limit
public:// constructors and other methodsString(const char *s);  // constructorString();               //.default constructorString(const String &); // copy constructor~String();// destructorint length() const { return len; }// overloaded operator methodsString &operator=(const String &);String &operator=(const char *);char &operator[](int i);const char &operator[](int i) const;// overloaded operator friendsfriend bool operator<(const String &st, const String &st2);friend bool operator>(const String &stl, const String &st2);friend bool operator==(const String &st, const String &st2);friend ostream &operator<<(ostream &os, const String &st);friend istream &operator>>(istream &is, String &st); // static functionstatic int HowMany();
};#endif

方法文件

string1.cpp

// stringl.cppString class methods
#include <cstring>
#include "string1.h" //includes <iostream>
using std::cin;
using std::cout;int String::num_strings = 0;
// static method
int String::HowMany()
{return num_strings;
}
// class methods
String::String(const char *s) // construct String fromC string
{len = std::strlen(s);// set size// allot storagestr = new char[len + 1];std::strcpy(str, s);num_strings++;
}String::String()
{len = 4;str = new char[1];str[0] = '\0';// default stringnum_strings++;
}String::String(const String &st)
{num_strings++;len = st.len;str = new char[len + 1];std::strcpy(str, st.str);
}String::~String()
{--num_strings;delete[] str;
}String &String::operator=(const String &st)
{if (this == &st)return *this;delete[] str;len = st.len;str = new char[len + 1];std::strcpy(str, st.str);return *this;
}
// assign aC string to a string
String &String::operator=(const char *s)
{delete[] str;len = std::strlen(s);str = new char[len + 1];std::strcpy(str, s);return *this;
}char &String::operator[](int i)
{return str[i];
}// read-only char access for const String
const char &String::operator[](int i) const
{return str[i];
}
// overloaded operator friends
bool operator<(const String &st1, const String &st2)
{return (std::strcmp(st1.str, st2.str) < 0);
}
bool operator>(const String &stl, const String &st2)
{return st2.str < stl.str;
}
bool operator==(const String &stl, const String &st2)
{return (std::strcmp(stl.str, st2.str) == 0);
}// simple String output
ostream &operator<<(ostream &os, const String &st)
{os << st.str;return os;
}// quick and dirty string input
istream &operator>>(istream &is, String &st)
{char temp[String::CINLIM];is.get(temp, String::CINLIM);if (is)st = temp;while (is && is.get() != '\n')continue;return is;
}

 程序文件

#include <iostream>
#include "string1.h"
const int ArSize = 10;
const int MaxLen = 81;
int main()
{using std::cin;using std::cout;using std::endl;String name;cout << "Hi,what's your name?\n>>";cin >> name;cout << name << ",please enter upto " << ArSize<< "short sayings <empty line to quit>:\n";String sayings[ArSize]; // array of objectschar temp[MaxLen];      // temporary string storageint i;for (i = 0; i < ArSize; i++){cout << i + 1 << ": ";cin.get(temp, MaxLen);while (cin && cin.get() != '\n')continue;if (!cin || temp[0] == '\0') // empty linebreak;                   // i not incrementedelsesayings[i] = temp; // overloaded assignment}int total = i;// total # of lines readcout << "Here are your sayings:\n";for (i = 0; i < total; i++)cout << sayings[i][0] << ":" << sayings[i] << endl;int shortest = 0;int first = 0;for (i = 1; i < total; i++){if (sayings[i].length() < sayings[shortest].length())shortest = i;if (sayings[i] < sayings[first])first = i;}cout << "shortest saying:\n"<< sayings[shortest] << endl;cout << "First alphabetically:\n"<< sayings[first] << endl;cout << "This program used " << String::HowMany()<< "String objects. Bye.\n";return 0;
}

运行结果

在构造函数中使用new时应注意的事项

有关返回对象的说明


返回指向const对象的引用

使用const引用的常见原因是旨在提高效率,但对于何时可以采用这种方式存在一些限制。

如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过传递引用来提高方法的效率。例如,假设要编写函数 Max(),它返回两个 Vector 对象中较大的一个返回对象将调用复制构造函数,而返回引用不会。

引用指向的对象应该在调用函数执行时存在。

第三,v1和v2都被声明为 const引用,因此返回类型必须为const,这样才匹配。

返回非const对象的引用

cout 能连续输出;

返回对象

返回指向对象的指针

加入指针的程序

#include <iostream>
#include "string1.h"
#include <ctime>
const int ArSize = 10;
const int MaxLen = 81;
int main()
{using namespace std;String name;cout << "Hi,what's your name?\n>>";cin >> name;cout << name << ",please enter upto " << ArSize<< "short sayings <empty line to quit>:\n";String sayings[ArSize]; // array of objectschar temp[MaxLen];      // temporary string storageint i;for (i = 0; i < ArSize; i++){cout << i + 1 << ": ";cin.get(temp, MaxLen);while (cin && cin.get() != '\n')continue;if (!cin || temp[0] == '\0') // empty linebreak;                   // i not incrementedelsesayings[i] = temp; // overloaded assignment}int total = i;// total # of lines readif (total > 0){cout << "Here are your sayings:\n";for (i = 0; i < total; i++)cout << sayings[i] << endl;String *shortest = &sayings[0];String *first = &sayings[0];for (i = 1; i < total; i++){if (sayings[i].length() < shortest->length())shortest = &sayings[i];if (sayings[i] < *first)first = &sayings[i];}cout << "shortest saying:\n"<< *shortest << endl;cout << "First alphabetically:\n"<< *first << endl;srand(time(0));int choice = rand() % total;String *favorite = new String(sayings[choice]);cout << "My favorite saying: \n " << *favorite << endl;delete favorite;}elsecout << "No much to say, eh?\n";cout << "Bye.\n";return 0;
}

析构函数的调用

  • 如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。
  • 如果对象是静态变量(外部、静态、静态外部或来自名称空间),则在程序结束时将调用对象的析构函数。
  • 如果对象是用new 创建的,则仅当您显式使用 delete 删除对象时,其析构函数才会被调用。

 指针和对象小结

再谈布局new操作符

#include <iostream>
#include <string>
#include <new>
using namespace std;
const int BUF = 512;
class JustTesting
{
private:string words;int number;public:JustTesting(const string &s = "Just Testing", int n = 0){words = s;number = n;cout << words << "constructed\n";}~JustTesting() { cout << words << " destroyed\n"; }void Show() const{cout << words << ", " << number << endl;}
};int main()
{char *buffer = new char[BUF];// get a block of memoryJustTesting *pc1, *pc2;// place object in bufferpc1 = new (buffer) JustTesting;pc2 = new JustTesting("Heap1", 20); // place object on heapcout << "Memory block addresses:\n"<< "buffer: " << (void *)buffer << "    heap : " << pc2 << endl;cout << " Memory contents :\n ";cout << pc1 << " : ";pc1->Show();cout << pc2 << " : ";pc2->Show();JustTesting *pc3, *pc4;pc3 = new (buffer) JustTesting("Bad Idea", 6);pc4 = new JustTesting("Heap2", 10);cout << "Memory contents:\n";cout << pc3 << ": ";pc3->Show();cout << pc4 << ": ";pc4->Show();delete pc2;delete pc4;delete[] buffer;cout << "Done\n";return 0;
}

在使用布局new操作符时存在两个问题。首先,在创建第二个对象时,布局new操作符使用一-个新对象来覆盖用于第一个对象的内存单元。显然,如果类动态地为其成员分配内存,这将引发问题。
其次,将 delete用于 pc2 和 pc4 时,将自动调用为 pc2 和 pc4 指向的对象调用析构函数;然而,将 delete[]用于 bufer 时,不会为使用布局 new 操作符创建的对象调用析构函数。

解决方法


 

原因在于 delete 可与常规 new 操作符配合使用,但不能与布局new操作符配合使用。

例如,指针 pc3没有收到 new 操作符返回的地址,因此 delete pc3 将导致运行阶段错误。在另一方面,指针 pc1指向的地址与 buffer 相同,但 buffer 是使用new[]初始化的,因此必须使用 delete[]而不是 delete 来释放。

即使 buffer是使用 new而不是new[]初始化的,delete pc1也将释放buffer,而不是 pc1。这是因为 new/delete 系统知道已分配的 512 字节块 buffer,但对布局new 操作符对该内存块做了何种处理一无所知。

#include <iostream>
#include <string>
#include <new>
using namespace std;
const int BUF = 512;
class JustTesting
{
private:string words;int number;public:JustTesting(const string &s = "Just Testing", int n = 0){words = s;number = n;cout << words << "constructed\n";}~JustTesting() { cout << words << " destroyed\n"; }void Show() const{cout << words << ", " << number << endl;}
};int main()
{char *buffer = new char[BUF];// get a block of memoryJustTesting *pc1, *pc2;// place object in bufferpc1 = new (buffer) JustTesting;pc2 = new JustTesting("Heap1", 20); // place object on heapcout << "Memory block addresses:\n"<< "buffer: " << (void *)buffer << "    heap : " << pc2 << endl;cout << " Memory contents :\n ";cout << pc1 << " : ";pc1->Show();cout << pc2 << " : ";pc2->Show();JustTesting *pc3, *pc4;pc3 = new (buffer + sizeof(JustTesting)) JustTesting("Bad Idea", 6);pc4 = new JustTesting("Heap2", 10);cout << "Memory contents:\n";cout << pc3 << ": ";pc3->Show();cout << pc4 << ": ";pc4->Show();delete pc2;delete pc4;pc3->~JustTesting();pc1->~JustTesting();delete[] buffer;cout << "Done\n";return 0;
}

 

复习各种技术

重载<<操作符

重新定义<<操作符,定义下面友元操作函数:

转换函数

其构造函数使用new的类

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

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

相关文章

《动手学深度学习》笔记2.2——神经网络从基础→进阶 (参数管理-每层的权重/偏置)

目录 0. 前言 正文&#xff1a;参数管理 1. 参数访问 1.1 [目标参数] 1.2 [一次性访问所有参数] 1.3 [从嵌套块收集参数] 2. 参数初始化 2.1 [内置初始化] 2.2 [自定义初始化] 2.3 [参数绑定-共享参数] 3. 小结&#xff08;第2节&#xff09; 4. 延后初始化 (原书第…

AR 眼镜之-蓝牙电话-来电铃声与系统音效

目录 &#x1f4c2; 前言 AR 眼镜系统版本 蓝牙电话 来电铃声 系统音效 1. &#x1f531; Android9 原生的来电铃声&#xff0c;走的哪个通道&#xff1f; 2. &#x1f4a0; Android9 原生的来电铃声&#xff0c;使用什么播放&#xff1f; 2.1 来电铃声创建准备 2.2 来…

国庆普及模拟2总结

目录 题目链接&#xff1a; 官方题解&#xff1a; 概述&#xff1a; 总结反思&#xff1a; 题目 T1: 题目分析&#xff1a; 错误代码&#xff1a; 错因&#xff1a; &#xff21;&#xff23;代码&#xff1a; T2&#xff1a; 题目分析&#xff1a; 赛时代码&#xf…

LeetCode[中等] 55.跳跃游戏

给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标&#xff0c;如果可以&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 思路 贪心算法 可达位置…

CSS中字体图标的使用

引言&#xff1a; 在网页设计当中&#xff0c;会有很多很简洁的图标&#xff0c;比如箭头&#xff0c;照相机&#xff0c;放大镜等 这些大概率都是使用字体图标来完成的&#xff0c;因为字体图标比较简洁高效&#xff0c;不会像图片一样需要向浏览器请求数据。那么字体图标该…

记一次vue路由跳转登陆之前的页面,参数丢失问题

一、背景 vue3.0,项目登陆之前访问某个可访问的页面,当跳转到需要登陆才能访问的页面时,跳转到登陆页面,登陆后再跳转到登陆之前需要登陆才能访问的页面,跳转时发现参数丢失了。 A页面(无需登陆)===> B页面(需要登陆)====> 如果未登陆跳转到C登陆页面 ===>…

什么是文件完整性监控(FIM)

组织经常使用基于文件的系统来组织、存储和管理信息。文件完整性监控&#xff08;FIM&#xff09;是一种用于监控和验证文件和系统完整性的技术&#xff0c;识别用户并提醒用户对文件、文件夹和配置进行未经授权或意外的变更是 FIM 的主要目标&#xff0c;有助于保护关键数据和…

《NoSQL》非关系型数据库MongoDB 学习笔记!

Mongo基础&#xff1a; 使用数据库&#xff1a; 使用use 命令 后面跟着要使用的数据库名字即可&#xff0c; 例如&#xff1a;use cities, 值得注意的是&#xff0c; mongo中不像mysql&#xff0c; 还需要先创建数据库&#xff0c;后访问&#xff0c; mongo中&#xff0c;你无…

数据库管理-第246期 为啥有些老板瞧不上技术(20241002)

数据库管理246期 2024-10-02 数据库管理-第246期 为啥有些老板瞧不上技术&#xff08;202401002&#xff09;1 背景2 割裂3 感触总结 数据库管理-第246期 为啥有些老板瞧不上技术&#xff08;202401002&#xff09; 作者&#xff1a;胖头鱼的鱼缸&#xff08;尹海文&#xff09…

leetcode:380. O(1) 时间插入、删除和获取随机元素

实现RandomizedSet 类&#xff1a; RandomizedSet() 初始化 RandomizedSet 对象bool insert(int val) 当元素 val 不存在时&#xff0c;向集合中插入该项&#xff0c;并返回 true &#xff1b;否则&#xff0c;返回 false 。bool remove(int val) 当元素 val 存在时&#xff0…

数据仓库简介(一)

数据仓库概述 1. 什么是数据仓库&#xff1f; 数据仓库&#xff08;Data Warehouse&#xff0c;简称 DW&#xff09;是由 Bill Inmon 于 1990 年提出的一种用于数据分析和挖掘的系统。它的主要目标是通过分析和挖掘数据&#xff0c;为不同层级的决策提供支持&#xff0c;构成…

计算机毕业设计 基于Python的广东旅游数据分析系统的设计与实现 Python+Django+Vue Python爬虫 附源码 讲解 文档

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

Android AMS介绍

注&#xff1a;本文为作者学习笔记&#xff0c;如有误&#xff0c;请各位大佬指点 系统进程运行环境的初始化 Context是一个抽象类&#xff0c;它可以访问application环境的全局信息和各种资源信息和类 context功能&#xff1a; 对Activity、Service生命周期的管理通过Intent发…

LabVIEW自动生成NI-DAQmx代码

在现代数据采集和控制系统中&#xff0c;LabVIEW被广泛应用于各种工业和科研领域。其中&#xff0c;NI-DAQmx是一个强大的驱动程序&#xff0c;可以帮助用户高效地管理和配置数据采集任务。本文将介绍如何在LabVIEW中通过DAQ Assistant Express VI和任务常量自动生成NI-DAQmx代…

Python编码系列—Python状态模式:轻松管理对象状态的变化

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

架构演化--将MVC代码重构成DDD

什么是好的代码架构 在当前的工作中我们所面临的主要矛盾是“越来越多的多场景化复杂业务需求与有限的研发人力之间的矛盾”。而要解决这一矛盾&#xff0c;就要求我们的系统能做到&#xff1a;设计易拓展、代码易复用、逻辑易传承、运行更稳定。 设计易拓展 一个好的架构应该…

Ceph RocksDB 深度调优

介绍 调优 Ceph 可能是一项艰巨的挑战。在 Ceph、RocksDB 和 Linux 内核之间&#xff0c;实际上有数以千计的选项可以进行调整以提高存储性能和效率。由于涉及的复杂性&#xff0c;比较优的配置通常分散在博客文章或邮件列表中&#xff0c;但是往往都没有说明这些设置的实际作…

如果您忘记了 Apple ID 和密码,按照指南可重新进入您的设备

即使您的 iPhone 或 iPad 由于各种原因被锁定或禁用&#xff0c;也可以使用 iTunes、“查找我的”、Apple 支持和 iCloud 解锁您的设备。但是&#xff0c;此过程需要您的 Apple ID 和密码来验证所有权并移除激活锁。如果您忘记了 Apple ID 和密码&#xff0c;请按照我们的指南重…

G502 鼠标自定义(配合 karabiner)

朋友送了我一个 G502 多功能鼠标&#xff0c;除了鼠标正常的左键、右键和滑轮外&#xff0c;额外提供了 6 个按键&#xff0c;并且滑轮可以向左、向右、向下按下&#xff0c;共计 9 个自定义的按键。 虽然是 karabiner 的老用户&#xff0c;但一直在使用 TrackPad&#xff0c;所…

SpringGateway(网关)微服务

一.启动nacos 1.查看linux的nacos是否启动 docker ps2.查看是否安装了nacos 前面是你的版本&#xff0c;后面的names是你自己的&#xff0c;我们下面要启动的就是这里的名字。 docker ps -a3.启动nacos并查看是否启动成功 二.创建网关项目 1.创建idea的maven项目 2.向pom.x…