0%

配置文件

redis的配置文件位于Redis的安装目录下,文件名为redis.conf,windows下为redis.windows.conf,这个文件是一个文本文件。

配置命名

在redis命令行下,你可以使用CONFIG命令获取或者设置相关配置

例如:

1
CONFIG GET loglevel
1
CONFIG GET * # 用于获取所有配置
1
CONFIG SET loglevel "notice"

Redis参数说明

详见

Docker 镜像与容器

Dockerfile

Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一系列构建镜像所需的指令和说明。

Dockerfile字段介绍:

  • FROM: 定制的镜像所基于的基础的镜像,后续的操作都将基于这个基础镜像。

    • FROM的基础镜像是必须指定的,如果确实没有基于的其他镜像,可以指定为一个特殊的空镜像叫scratch。
  • RUN: 后面接命令行命令,它有两种形式:

    • RUN 直接跟要执行的命令行命令
    • RUN [“可执行文件”, “参数1”, “参数2”, …]

    Dockerfile 的指令每执行一次都会在 docker 上新建一层。所以过多无意义的层,会造成镜像膨胀过大。如果命令太多,可以使用&&符号连接命令,例如

    1
    2
    3
    RUN yum install wget \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && tar -xvf redis.tar.gz
  • COPY: 复制指令,从上下文目录中复制文件或者目录到容器里指定路径:

    • ```shell
      COPY [—chown=:] <源路径1>… <目标路径>
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
        
      * 源文件或者源目录,这里可以是通配符表达式,其通配符规则要满足 Go 的 filepath.Match 规则,如COPY hom* /mydir/

      * 目标路径: 容器内的指定路径

      * CMD: 类似于RUN指令,但是CMD的命令是在容器开始运行时会被运行,RUN命令是在构建镜像时被运行。CMD命令为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束。CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖。**如果Dockerfile有多个CMD命令,仅最后一个生效**

      * EVN: 设置环境变量,定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。EVN命令使用格式如下:

      * ```shell
      ENV <key> <value>
      ENV <key1>=<value1> <key2>=<value2>...
  • WORKDIR: 指定工作目录。用 WORKDIR 指定的工作目录,会在构建镜像的每一层中都存在。(WORKDIR 指定的工作目录,必须是提前创建好的)。docker build 构建镜像过程中的,每一个 RUN 命令都是新建的一层。只有通过 WORKDIR 创建的目录才会一直存在。

  • 其他更多命令见这里

Docker 常用命令及介绍

Registry相关

1、切换Registry

镜像相关

获取镜像
1
docker pull <image-name>
查找镜像
1
docker search <image-name>
列出本地镜像
1
2
3
docker images
or
docker image ls

列出来表格有以下几个字端

  • REPOSITORY: 镜像名称
  • TAG: 镜像的标签
  • IMAGE ID: 镜像ID
  • CREATED: 镜像创建时间
  • SIZE: 镜像大小

同一仓库源可以有多个 TAG,代表这个仓库源的不同个版本,如 ubuntu 仓库源里,有 15.10、14.04 等多个不同的版本,我们使用 REPOSITORY:TAG 来定义不同的镜像。如果不指定TAG,默认会使用为TAG被标记为latest的那个。

删除镜像
1
docker rmi [-f] <image-id>
  • -f 表示force
使用Dockerfile构建镜像
1
docker build -t <image name>:<tag name> .

-t 参数是标签,如果不给tag name,会自动将tag name设置为latest,如果该镜像之前已经有被标记为latest tag 的镜像,那个这个之前的镜像的tag会被标记为none。所以最好为每个镜像给一个tag。

“.”表示上下文路径,是指 docker 在构建镜像,有时候想要使用到本机的文件(比如复制),docker build 命令得知这个路径后,会将路径下的所有内容打包。可以类比cmake命令的”cmake .”

解析:由于 docker 的运行模式是 C/S。我们本机是 C,docker 引擎是 S。实际的构建过程是在 docker 引擎下完成的,所以这个时候无法用到我们本机的文件。这就需要把我们本机的指定目录下的文件一起打包提供给 docker 引擎使用。

注意:上下文路径下不要放无用的文件,因为会一起打包发送给 docker 引擎,如果文件过多会造成过程缓慢。

容器相关

  • docker ps -a
    • 查看目前创建的所有容器的状态
  • docker stop <container id>
    • 暂停运行某个容器
    • Cotainer id 可以通过docker ps命令查看
  • docker restart <container id>
    • 重启某个容器
  • docker rm <container id>
    • 删除某个容器
  • docker rm $(docker ps -aq)
    • 删除所有容器
  • docker run -it <image_name>:<image_tag> /bin/bash
    • 使用某个镜像启动某个容器,并以命令行形式进入该容器
    • -i 参数:交互式操作
    • -t参数: 为容器重新分配一个伪输入终端
    • -i、-t一般同时使用
    • /bin/bash:使用的shell
    • 通过该命令如果在shell中执行exit命令后,容器将会停止运行
  • docker run -d <image_name>:<image_tag> [--name <custom_container_name>] [-p <outter_port>:<inner_port>]
    • 以守护进程模式在后台启动一个容器
    • -d参数:表示以守护进程形式在后台启动
    • —name参数:容器的名称,如果不定义则会是一个随机的名称
    • -p参数:端口映射,docker容器内部默认是不开放端口的,需要手动指定哪些端口开放,并且外部从哪些端口访问对应容器内的端口
  • docker exec -it <container id> /bin/bash
    • 以命令行的形式进入某个容器
    • docker exec 用于在一个运行的container中运行一个命令,更多docker exec的使用可以用docker exec —help查看
  • docker export <container id> > <filename>
    • 创建容器快照
    • 示例:docker export 1e560fca3906 > ubuntu.tar
  • docker import <filePath | url> <imageName:tag>
    • 将容器快照文件再导入为镜像

内联函数

inline function在编译后实质上是函数代码块的替换,编译器不一定会实现inline function,如果编译器认为inline function太复杂或者有递归调用。inline function的链接性为内部链接,所以inline function的定义和声明不能放在不同的文件中。

引用类型作函数参数

如果函数的参数为引用类型,如果传递的参数类型正确但不是左值或者传递的参数类型不正确,在其可以转换为正确的类型的情况下,编译将会创建一个临时变量,而将引用参数引用到该临时变量上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
double refcube(const double &ra){
return re * ra * ra;
}

double side = 3.0;
double *pd = &side;
double &rd = side;

long edge = 5;
double lens[4] = {2.0, 5.0, 10.0, 12.0}

double c1 = refcube(side);//won't create temporary variable
double c2 = refcube(lens[2]);//won't create temporary variable
double c3 = refcube(rd);//won't create temporary variable
double c4 = refcube(*pd);//won't create temporary variable
double c5 = refcube(edge);//will create temporary variable
double c6 = refcube(7.0);//will create temporary variable
double c7 = refcube(side + 10.0);//will create temporary variable

带默认参数的函数

  • 如果要用为某个参数设置默认值,则必须为它右边的所有参数提供默认值
  • 函数定义中有默认参数,在函数实现中的函数头中可以不再写默认参数的值。

可变参数函数

1
int sum(int n, ...);

...代表可以接受任意类型的数据任意多个。

获取到可变参数列表的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int sum(int n, ...) {
va_list ap;
int sum = 0;
va_start(ap, n);
while (n > 0) {
int temp = va_arg(ap, int);
sum += temp;
--n;
}
va_end(ap);
return sum;
}

sum(3, 1, 2, 3);

1、va_list args; //定义一个可变参数列表
2va_start(args,n); //初始化args指向强制参数n的`下一个参数`
3va_arg(args,type); //`获取当前参数内容`并将args指向下一个参数
4va_end(args); //释放args

在使用va_arg宏时,要求type是POD(Plain Old Data)类型,能用 C 的 memcpy() 等函数进行操作的类、结构体就是 POD 类型的数据,所有基本数据类型都是POD。并且如果type和对应的参数不对的话会产生强制类型转换。如果类型转换失败就会将数据设置为0或者其他初始化的值。

函数重载

  • 函数的参数列表也称为函数特征标

  • 函数重载要求函数的参数数目或参数类型或参数的顺序不同,而参数名是无关紧要,且编译器在检查时不一定会将引用类型和本身类型视为同一特征标,但是如果调用重载函数存在二义性则编译器会报错。

  • 对于非引用传参,形参是否const是等价的。但是当使用引用传参时,有无const是不同的。使用指针传参时,指向const对象的指针和指向非const对象的指针做形参的函数是不同的

  • 只是函数返回值不同不能构成函数重载

  • 在类中,C++将区分常量和非常量函数(即函数本身是否为const)的特征标

  • 函数重载匹配原则

    • 最匹配原则
    • 如果没有匹配的将会进行类型强转后进行匹配,但是进行类型强转后进行匹配后有多个匹配的会产生报错;

函数模版

函数模版并没有创建一个函数,它只是告诉编译器如何创建如何创建一个函数,编译器在遇到使用模版函数的地方会将模版函数展开成具体的函数。

模版函数匹配原则:越具体越适配原则

1
2
3
4
5
6
template<typename T>
void func(T t);//函数1
void func(T *t);//函数2

int a = 0;
func(&a);//函数2更适配

decltype(x) y

decltype(x) y,用于说明y的类型和x的类型一样,decltype的运用场景大多是模版

1
2
template<typename T1, typename T2>
auto gt(T1 x, T2 y) -> decltype(x + y);

函数指针

函数指针的声明方式:

1
return_type (*pointer_name)(parameter);
  • *和pointer_name要用括号括起来,否则就变成了返回指针的函数。

  • 不能声明一个指向模版函数的指针,因为模版函数并没有实现真的函数,而只是说明如何声明函数

宏定义形式的函数

1
#define MAX(a, b) (a) > (b) ? (a) : (b)
注意要将参数用括号扩起来

有些时候用宏定义的代码被do { } while(0);包含了起来,例如:

1
2
3
#define MAX(a, b) do {    \
(a) > (b) ? (a) : (b); \
} while(0)

这是因为宏的处理是文本替换,有时候将宏用文本替换后代码就产生了语法错误,而使用do {} while(0)是为了避免由于文本替换后带来的语法错误,并保证宏函数无论如何都会执行一次。

确定对象被使用前已被初始化

  • 对内置型数据(char, int, float, char * 等)进行手工初始化,因为C++不保证初始化它们
  • C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前
  • 对象成员变量的初始化使用成员初始化列表的方式(如果成员变量是const或者reference就一定要使用初始化列表的方式)
  • 如果class内存在许多成员变量或者是从多个基类继承来的,而且这个class有多个构造方法,导致成员初始化列表存在许多重复,这种情况下可以合理地在初始列中挑出那些“赋值操作的性能和初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常为private),供所有的构造函数调用
  • C++有十分固定的成员初始化次序,base class总是早于derived class被初始化,class的成员变量总是以其声明次序被初始化,即使它们在成员初始化列表中以不同的次序出现,所以当你在成员初始值列中列出各个成员时,最好总是与其声明次序一致

不同编译单元内定义的non-local static对象的初始化次序

函数内的static对象称为local static对象,其他static对象成为non-local static对象。

所谓编译单元是指产出单一目标文件(single object file)的那些源码,基本上它是单一源码文件加上其所含入的头文件。

现在问题涉及至少两个源码文件,每一个内含至少一个non-local static对象,如果某个编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。

例子:

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
//in a.h
class FileSystem {
public:
std::size_t numDisks() const;
};

extern FileSystem tfs;//a non-local static object

//in b.h
class Directory {
public:
Directory( params );
};
//in b.cpp
#include "b.h"
#include "a.h"
Directory::Directory( params ) {
std::size_t disks = tfs.numDisks();//use tfs
}

//in main.cpp
Directory tmpDirectory( params );//tempDirectory is also a non-local static object

int main() {
...;
return 0;
}

在初始化tmpDirectory时需要用到tfs,这时候tfs可能还没有被初始化,这就会出现问题。

解决办法:将每个non-local static对象放到一个属于它自己的专属函数内,成为一个local static对象,这些函数返回一个reference指向它所含的对象。

接上例:

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
//in a.h
class FileSystem{ ... };
FileSystem& tfs() {
static FileSystem fs;
return fs;
}

//in b.h
class Directory{ ... };

//in b.cpp
Directory::Directory( params ) {
std::size_t disks = tfs().numDisks();
}

//main.cpp
Directory& tmpDir() {
static Directory td( params );
return td;
}

int main() {
...;
temDir().doSomething();
return 0;
}

这种方法的基础在于:C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象的定义式”时被初始化。这种方式也是Singleton模式的一个常见实现方法。

变量作用范围

存储持续性

  • 自动存储持续性:在程序开始执行其所属的函数或者代码块(花括号括起来的部分)时被创建,执行完函数或者代码块后,其内存空间被释放。
  • 静态存储持续性:在程序的整个运行过程中都存在
  • 线程存储持续性(c++11)使用thread_local声明,其生命周期与所属线程一样长
  • 动态存储持续性:使用new和delete来动态管理

连接性

  • 外部连接性:可在其他文件中访问
  • 内部链接性:只能在当前文件中访问
  • 无链接性:只能在当前函数或代码块中访问

静态初始化和动态初始化

  • 静态初始化:在编译器处理文件时对变量进行初始化

  • 动态初始化:编译后初始化

静态持续变量

编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。并且,如果没有显示地初始化静态变量,编译器将把它设置为0。在默认情况下,静态数组和结构将每个元素或成员的所有位(bit)都设置为0。

声明三种链接性的静态持续变量的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int global = 1000;//静态持续,外部链接

static int one_file = 50;//静态持续,内部链接

int main(){

...;

}

void func(){

static int count = 0;//静态持续,无链接

}

静态外部变量

静态持续性外部链接性的变量,定义有两种方式:

1
2
3
4
5
double up = 0.0;

extern double up = 0.0;

//即定义静态外部变量的时候可以省略extern关键字

声明外部变量的方式:

1
extern double up;//up 在其他文件定义

静态内部变量

使用static定义并同时初始化声明的变量就是静态内部变量

static声明的静态内部变量将会隐藏常规同名外部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//in file1

int errors = 20;



//in file2

static int errors = 5;

void func(){

cout << errors;//使用在file2中定义的errors

}

同名变量覆盖规则:外部可见变量被本文件可见变量覆盖,局部变量覆盖全局变量

const定义的全局变量

默认情况下全局变量的链接性为外部的,const全局变量的链接性为内部的。也就是全局const定义就像使用了static说明符一样。

1
const int value = 10;//same as static const int value = 10;

但是const可以和extern连用,声明链接性为外部的常量:

1
extern const int value = 10;

函数的持续性和链接性

  • 函数默认情况下是静态的,即在整个程序执行期间都一直存在,默认情况下,函数的链接性为外部的,即多个文件可见

  • 可以使用static关键字将函数的链接性设置为内部,且静态定义将覆盖外部定义,且必须在函数的定义和实现中都使用static关键字

  • 内联函数不受这项规则的约束,内联函数的链接性为内部的,但是c++要求同一个函数的所有内联定义都必须相同

如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本

语言链接性

链接程序要求每个不同的函数都有不同的符号名,在C语言中,一个名称只对应一个函数。例如c编译器可能将spiff这样的函数翻译为_spiff。

但在c++中,由于函数重载,一个名称可能对应多个函数,必须将这些函数翻译为不同的符号名称。例如可能将spiff(int)翻译为spiff_i,将spiff(double, double)翻译成spiff_d_d。

如果要在c++程序中使用c库中预编译的函数,由于有c编译器预编译的函数符号和c++要寻找的函数符号不相同,所有可能c++编译器会找不到这些函数。为了解决这个问题,可以用函数原型来指出要使用的约定:

1
2
3
extern "C" void spiff(int);//use C protocol for name look-up
extern void spoff(int);//use C++ protocol for name look-up
extern "C++" void spaff(int);//use C++ protocol for name look-up

另外的形式:

1
2
3
4
5
6
7
8
#ifdef __cplusplus
extern "C" {
#endif
void some_func();

#ifdef __cplusplus
}
#endif

注意由于extern "C"之后将按照C的方式翻译函数的名称,所以此时将不能在extern "C" 中进行函数重载

new、new[]和定位new

new、new[]的调用实质上是分别调用:

1
2
void * operator new(std::size_t);
void * operator new[](std::size_t);

要使用定位new特性,首先需要包含头文件new。定位new的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct chaff {
char *dross;
int slag;
chaff(const char *str = "", int slag = 0) : slag(slag) {
dross = new char[strlen(str) + 1];
strcpy(dross, str, strlen(str));
}
}

char buffer1[50];
char buffer2[500];

chaff *p1 = new (buffer1) chaff("1");
chaff *p2 = new (buffer2) int[20];
chaff *p3 = new (buffer1) chaff("2");//将会覆盖p1中的内容
chaff *p4 = new (buffer1 + sizeof(chaff)) chaff("3");//不会覆盖p3指向的内容

delete和delete[]只能用于指向常规new运算符分配的堆内存,所以delete不一定能作用于定位new运算符所指向的区域,所以最好不要delete定位new运算符返回的指针

new 和 delete、new[] 和 delete[]要配套使用,delete和delete[]可以作用于空指针,delete和delete[]不能同时释放同一内存空间两次,除非是空指针,否则将会出现runtime error。

说明符和限定符

cv-限定符

  • const
  • volatile

假设编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在这两次使用之间不会发生变化。如果不将变量声明为volatile,则编译器讲进行这种优化;将变量声明为volatile则相当于告诉编译器,不要进行这种优化。volatile的使用一般在多进程或多线程的情况下,可能其他进程或线程或改变某个内存空间的值,如果不两次获取的话,程序就会出错。

mutable限定符

mutable用来指出即使结构或类变量为const,其某个成员也可以被修改。

1
2
3
4
5
6
7
struct data {
char name[30];
mutable int access;
}

const data veep{"tom", 0};
veep.access++;//allowed

命名空间

“::”放在变量前面,该运算符表示使用变量的全局版本

1
cout << ::value << "\n"

使用namespace创建名称空间

1
2
3
4
5
6
7
namespace Tom {
int pal;
}

namespace Jack {
int pal;
}

名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。

名称空间是开放的,即可以把名称加入到已有的名称空间中:

1
2
3
4
5
6
namespace Tom {
void func() {
...
}
//将会扩展上面的Tom命名空间
}

using声明和using编译指令

using声明使特定的标示符可用,using编译指令使整个名称空间可用

1
2
using Tom::func;//using声明
using namespace Tom;//using 编译指令

using编译指令和using声明的区别

如果某个名称已经在函数中声明了,则不能用using声明导入相同的名称。然而,使用了using编译指令(using namespace)时,将进行名称解析。如果使用using编译指令导入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace Jill{
double bucket(double n){ ... }
double fetch;
struct Hill{ ... };
}
char fetch;//全局fetch
int main() {
using namespace Jill;
Hill Thrill;
double water = bucket(2);
double fetch;//将会隐藏Jill::fetch
cin >> fetch;//写局部fetch
cin >> ::fectch;//写全局的fetch
cin >> Jill::fetch;//写Jill::fetch
...
}

int foom() {
int fetch;//将会隐藏全局fetch
using Jill::fetch;//错误
Hill top;//错误
Jill::Hill hill;//正确
}

关于命名空间的一些小tip

  • 可以在名称空间中使用using编译指令和using声明:

    • ```cpp
      namespace myth{
      using Jill::fetch;
      using namespace std;
      
      }
      //这时候访问fetch的两种方式:
      cout << myth::fetch;
      cout << Jill::fetch
      1
      2
      3
      4
      5
      6

      * 可以为名称空间创建别名:

      * ```cpp
      namespace my_very_favorite_things{ ... };
      namespace mvft = my_very_favorite_things;
  • 未命名的名称空间:

    • namespace {
          int ice;
          int bandycoot;
      }
      
    • 不能在未命名的名称空间所属文件之外的其他文件中,使用该未命名名称空间中的名称。未命名的名称空间中的变量相当于声明了链接性为内部的静态变量。

尽量以const,enum,inline替换#define

具体描述

使用#define定义的常量,尽量使用const 来定义,使用#define定义的函数,尽量使用inline函数来定义

原因

看下面的例子:

1
2
3
4
5
6
#define MAX(a, b) (a) > (b) ? (a) : (b)

int a = 5;
int b = 0;
cout << MAX(++a, b);//a被累加两次
cout << MAX(++a, b+10);//a被累加一次

由于使用#define定义的函数或者常量都是本文替换,所以文本替换后其语意会产生偏差。

将成员变量声明为private

将成员函数声明为private,为成员函数提供get和set方法,

必须返回对象时,别妄想返回其reference

返回对象时,如果你返回一个指向局部对象的reference的话,会产生运行时错误,如果你返回一个指向new出来的对象的reference的话,这是一个不好的设计,前面的条款说过要用资源管理对象来管理动态数据,而资源管理对象一定是非动态对象。

宁以pass-by-reference-to-const替换pass-by-value

copy构造函数定义了pass-by-value的行为

对于某些pass-by-value的代价并不昂贵的数据(比如说基本数据类型和STL中的迭代器和函数对象),其他情况下对于pass-by-value代价较高的情况下使用pass-by-reference替代。

设计class犹如设计基本数据类型

在设计一个class时,可以问一下以下几个问题:

  • 对象应该如何被创建和销毁
  • 对象的初始化和对象的赋值该有什么样的差别
  • 对象如果被passed by value,意味着什么?copy构造函数用来定义一个class的pass-by-value的行为
  • 对象数据成员的合法值
  • class的继承关系
  • class需要什么样的转换(显示转换和隐式转换)
  • class需要用到操作符函数嘛?应该使用成员函数版还是非成员函数版?
  • class成员的访问属性
  • 是要设计成一个class还是设计成一个template class

让接口容易被正确使用,不易被误用

尽量将接口设计得让别人使用的时候不会出现错用接口的情况。