分类 编程语言 下的文章

一、基本用法

画一个简单的二叉树:

digraph bin_tree {
    1->2;
    1->3;
}

图形:

二、设置形状

上面的1/2/3都是一个node,通常被称为节点,默认情况下节点是圆形的。可以通过shape属性来设置节点形状。

设置形状为长方形:

digraph bin_tree {
    node [shape="rectangle"];
    1->2;
    1->3;
}

设置形状为三角形:

digraph bin_tree {
    node [shape="triangle"];
    1->2;
    1->3;
}

graphviz提供了很多形状可以选择,具体的类型和样式可在Node Shapes找到。

三、设置线条

3.1 设置虚线

设置线条的属性要修改edge属性,线条不只是箭头,每个node的边也被edge属性控制。

例如设置节点的线条为虚线:

digraph bin_tree {
    node [shape="rectangle" style="dashed"];
    1->2;
    1->3;
}

dash-node

设置箭头的线条为虚线:

digraph bin_tree {
    node [shape="rectangle" style="dashed"];
    edge [style="dashed"];
    1->2;
    1->3;
}

3.2 设置箭头形状

当然,箭头的形状也是可以设置的,例如设置成不要箭头:

digraph bin_tree {
    node [shape="circle"];
    edge [arrowhead="none"];
    1->2;
    1->3 ;
}

箭头的形状可以在Arrow Shapes找到,还有很多线条的形状可以设置。

四、设置颜色

设置线条颜色为红色,节点填充色为灰色:

digraph bin_tree {
    node [shape="rectangle" style="dashed,filled" color="gray"];
    edge [style="dashed" color="red"];
    1->2;
    1->3;
}

对node/edge的属性配置是全局生效的,局部生效的方法:

digraph bin_tree {
    node [shape="rectangle" style="dashed,filled" color="gray"];
    edge [style="dashed" color="red"];
    1->2;
    1->3 [color="blue"];
}

设置rgb颜色

edge [style="dashed" color="#ff00ff"];

判断操作系统的方法:

# project variables
MESSAGE(STATUS "platform: ${CMAKE_SYSTEM_NAME}")
if (UNIX AND NOT APPLE)
    MESSAGE(STATUS "unix")
elseif (WIN32)
    MESSAGE(STATUS "windows")
elseif (APPLE)
    MESSAGE(STATUS "mac os")
else ()
    MESSAGE(STATUS "other platform")
endif ()

要注意的问题是APPLE也属于UNIX

一、概述

CMakeLists.txt是cmake编译系统构建器的构建文件,就像是make中的Makefile一样。

Makefile是通过固定的文件格式来构建目标,相对于Makefile而言,CMakeList并没有固定的语法格式,他是通过各种函数和指令来完成构建。

一个基本的CMakeList.txt内容结构为:

cmake_minimum_required(VERSION 3.4.1)

project("link_list")

# 设置变量
set(PROJ_ROOT_DIRECTORY /home/maqian/Desktop/code2019)
set(PROJ_ROOT_LIBRARY ${PROJ_ROOT_DIRECTORY}/lib)

# 添加头文件
include_directories(
        ../../common
)

# 添加编译选项
add_compile_options(-std=c++11)

# 添加宏定义
add_definitions(-D__LINKLIST_TEST__)

# 添加目标
add_executable(doubly_linklist doubly_linklist.cpp)

# 添加静态库
link_directories(${PROJ_ROOT_LIBRARY})
target_link_libraries(doubly_linklist ${PROJ_ROOT_LIBRARY}/libgtest.a)

二、常见用法

2.1 设置变量

设置变量的函数是set,格式为:

set(SOURCE_DIR /usr/src/)

表示设置一个SOURCE_DIR的变量,其值为/usr/src

2.2 添加头文件目录

添加上级目录下的inc目录作为头文件的搜索路径:

include_directories(
    ../inc/
)

2.3 添加编译选项

添加编译选项的函数为add_compile_options,例如指定编译标准为c++11

add_compile_options(-std=c++11)
设置编译标准还可以使用:set(CMAKE_CXX_STANDARD 11)。

2.4 添加宏定义

添加一个__XTEST__的宏定义:

add_definitions(-D__XTEST__)

2.5 添加静态库

添加静态库依赖两个函数link_directoriestarget_link_libraries,用法如下:

set(PROJ_ROOT_LIBRARY /usr/lib/)

link_directories(${PROJ_ROOT_LIBRARY})
target_link_libraries(doubly_linklist ${PROJ_ROOT_LIBRARY}/libgtest.a)

要注意的是PROJ_ROOT_LIBRARY设置的路径是全路径,并且target_link_libraries要在add_executable的后面声明。

一、安装googletest

单测对程序员而言是提升代码质量最重要、最有效的一个措施,对程序员来说,要想写一个好的程序,那么必定少不了好的单元测试。googletest(gtest)是google开发出来的一个开源的、跨平台的测试框架,是C++中最出名的测试框架。

gtest支持linux、windows以及mac系统,安装它依赖下面几项:

  1. gtest源码:gtest属于开源项目,代码仓库https://github.com/google/googletest
  2. cmake:gtest使用cmake构建项目,编译需要cmake环境,cmake下载地址
  3. 编译器:linux环境下可使用g++编译,windows环境下使用vs或者clion等工具编译,mac环境使用xcode或clion等工具编译。

这里的测试环境为mac+clion(付费),clion下载地址,选择clion是因为clion跨平台。

windows环境除了配置clion编译环境以外,其他步骤和mac系统一致。

1.1 环境配置

第一步,先使用git克隆代码到本地,注意最好不要放到中文路径了。

git clone https://github.com/google/googletest.git

第二步,安装cmake,不同系统的的安装方式不一样,windows在上面的页面下载一直下一步安装就行了,其他系统可以直接使用对应平台的包管理工具安装。

# mac
brew install cmake
# centos 
yum install cmake
# ubuntu
apt-get install cmake

第三步,安装clion,linux和mac环境下安装clion和gcc环境就可以使用了,windows配置clion编译环境可参考Window10上CLion极简配置教程

1.2 编译gtest库

配置好环境后,使用clion打开代码目录,然后载入代码目录,选择gtest项目编译生成:

编译成功后生成libgtestd.a文件到cmake编译路径的lib路径下:

生成的libgtestd.a即为gtest的库文件,项目中引用这个库文件就能使用gtest了。

二、使用googletest

2.1 引入库

将libgtestd.a文件拷贝到代码根路径的lib路径下,在CMakeList.txt中加上以下内容:

# 添加上库文件的路径,注意相对路径
link_directories(lib/)
# 添加可执行文件
add_executable(demo demo.cpp)
# 链接gtest库文件
target_link_libraries(demo libgtestd.a)

2.2 引入头文件

拷贝googletest/include目录下的gtest目录到当前目录下,然后在CMakeList.txt中添加上对应的调用:

include_directories(
    include/
)

然后在代码中添加头文件gtest/gtest.h就可以使用了。

2.3 测试

添加代码add.cpp

#include "gtest/gtest.h"

int add(int a, int b) {
    return a + b;
}

TEST(add, zero) {
    EXPECT_EQ(0, add(0, 0));
}

TEST(add, positive_number) {
    EXPECT_EQ(3, add(1, 2));
}

TEST(add, negative_number) {
    EXPECT_EQ(-3, add(-1, -2));
}

int main() {
    ::testing::InitGoogleTest();
    return RUN_ALL_TESTS();
}

执行结果:

 title=

三、gtest的使用教程

参考文档:Googletest Primer,google官方出品。

3.1 基本用法

gtest最基本的用法就是断言,它内部提供了很多种断言方式,例如:

ASSERT_EQ()
ASSERT_TURE()
EXPECT_EQ()
EXPECT_TRUE()
// ...

其中ASSERT_*的断言,在条件不满足后会终止,而EXPECT_*不会终止。

以上面的代码为例,代码编写了一个add函数,返回两个传参的和:

int add(int a, int b) {
    return a + b;
}

然后引入gtest并写了三个测试用例:

#include "gtest/gtest.h"

TEST(add, zero) {
    EXPECT_EQ(0, add(0, 0));
}

TEST(add, positive_number) {
    EXPECT_EQ(3, add(1, 2));
}

TEST(add, negative_number) {
    EXPECT_EQ(-3, add(-1, -2));
}

三个用例分别表示:

  • 测试零值相加
  • 测试正数相加
  • 测试负数相加

主函数中添加启动gtest的入口:

::testing::InitGoogleTest();
RUN_ALL_TESTS();

运行程序,系统就会自动调用三个测试用例的函数来测试,并输出测试报告:

...
[ RUN      ] add.zero
[       OK ] add.zero (0 ms)
[ RUN      ] add.positive_number
[       OK ] add.positive_number (0 ms)
[ RUN      ] add.negative_number
[       OK ] add.negative_number (0 ms)
...

如果中间有断言失败的地方,报告也会表达出来。例如修改上面测试中的负数相加函数来制造错误场景:

TEST(add, negative_number) {
    EXPECT_EQ(-3, add(-1, 2)); // 把-2改成2,制造错误场景。
}

再执行测试,报告中就会把不通过的案例展示出来,并且会定位到对应的行,打印出失败的详细原因:

当案例执行失败后,我们也可以打印出一些我们自己的数据以供调试使用,例如:

TEST(add, negative_number) {
    EXPECT_EQ(-3, add(-1, 2)) << "this is a incorrect test";
}

案例失败后,不仅会打印出失败原因,还会打印出我们自己添加的语句:

3.2 常用断言

基本断言

致命断言非致命断言验证
ASSERT_TRUE(condition);EXPECT_TRUE(condition);条件condition为真
ASSERT_FALSE(condition);EXPECT_FALSE(condition);条件condition为假

二进制比较

致命断言非致命断言验证
ASSERT_EQ(val1, val2);EXPECT_EQ(val1, val2);val1 == val2
ASSERT_NE(val1, val2);EXPECT_NE(val1, val2);val1 != val2
ASSERT_LT(val1, val2);EXPECT_LT(val1, val2);val1 < val2
ASSERT_LE(val1, val2);EXPECT_LE(val1, val2);val1 <= val2
ASSERT_GT(val1, val2);EXPECT_GT(val1, val2);val1 > val2
ASSERT_GE(val1, val2);EXPECT_GE(val1, val2);val1 >= val2

字符串比较

致命断言非致命断言验证
ASSERT_STREQ(str1,str2);EXPECT_STREQ(str1,str2);两个c字符串内容相同
ASSERT_STRNE(str1,str2);EXPECT_STRNE(str1,str2);两个c字符串内容不同
ASSERT_STRCASEEQ(str1,str2);EXPECT_STRCASEEQ(str1,str2);两个c字符串内容相同(忽略大小写)
ASSERT_STRCASENE(str1,str2);EXPECT_STRCASENE(str1,str2);两个c字符串内容不同(忽略大小写)

四、使用Test Fixtures

Test Fixtures使用场景:测试案例需要初始化数据或者多个测试案例使用相同的测试数据。

例如在对一个的做单元测试时,测试pop功能,按照上面的测试方法,测试案例得这么写:

TEST(stack, pop) {
  my_stack s;
  // 先推入3个元素
  s.push(1); s.push(2); s.push(3);
  // 测试pop
  s.pop();
  EXPECT_EQ(s.size(), 2);
}

测试过程可以描述为:

  1. 创建一个栈对象的实例s。
  2. 推入3个元素,以便后面pop使用。
  3. 开始测试pop。

从直观上来看,所有和pop相关的测试案例都要这么来写,要先推入元素,再弹出。而实际上,步骤1和步骤2是和本轮测试无关,它只起到了初始化数据的作用,它是多余的,但是所有的测试案例又不得不做这一步操作。那么有没有办法解决这个问题呢?有!Test Fixtures的就是解决这种问题的,它可以在测试案例开始前自动生成好需要的数据。

定义了一个简单的的类:

class my_stack {
public:
    void push(int a) {
        s.push(a);
    }
    void pop() {
        s.pop();
    }
    int size() {
        return s.size();
    }
private:
    stack<int> s;
};

再定义一个测试类stack_test

class stack_test : public ::testing::Test {
protected:
    void SetUp() override {
        stack1.push(1);
        stack1.push(2);
    }

    void TearDown() override {
    }

    my_stack stack1;
    my_stack stack2;
};

它要公有继承于::testing::Test,其中的SetUpTearDown函数分别是初始化和清理函数,也就是类生成前和使用后要做的工作。

此时使用TEST_F宏定义来测试:

TEST_F(stack_test, push) {
    EXPECT_EQ(stack1.size(), 2);
    EXPECT_EQ(stack2.size(), 0);
    stack2.push(1);
    EXPECT_EQ(stack2.size(), 1);
}

TEST_F(stack_test, pop) {
    EXPECT_EQ(stack1.size(), 2);
    EXPECT_EQ(stack2.size(), 0);
    stack1.pop();
    EXPECT_EQ(stack1.size(), 1);
}

在执行TEST_F之前,gtest会自动构建一个stack_test的实例,并执行SetUp函数。也就是说,当真正执行到我们的测试代码的时候,就已经存在一个初始化好的测试环境了。这个时候可以直接访问stack_test的内部成员,通过成员变量来做单元测试。

原理来看其实很简单,就是把初始化的过程交给了gtest来完成,它来帮我们实例对象,进行初始化,我们直接用就行。

测试结果:

五、其他

5.1 clion环境跨平台使用gtest

如何在不改变CMakeList.txt的情况下跨平台使用gtest?配置CMakeList,根据不同平台读取不同的库:

# 根据不同平台设置库的目录
if (UNIX AND NOT APPLE) # unix非苹果系统
    set(PROJ_LIBRARY_PATH /home/maqian/code/lib)
    set(GTEST_LIBRARY_NAME libgtestd.a)
elseif (WIN32) # windwos系统
    set(PROJ_LIBRARY_PATH d:/mingw64/lib)
    set(GTEST_LIBRARY_NAME libgtestd.a)
elseif (APPLE) # mac系统
    set(PROJ_LIBRARY_PATH /Users/maqian/code/lib)
    set(GTEST_LIBRARY_NAME libgtestd.a)
else () # 未知系统
    MESSAGE(STATUS "other platform: ${CMAKE_SYSTEM_NAME}")
endif ()
# 引入链接库目录
link_directories(${PROJ_LIBRARY_PATH})
# 链接库到目标
target_link_libraries(xxxx ${GTEST_LIBRARY_NAME})

六、参考

Google Test support

Googletest Primer

往vector中添加元素时,如果空间不够将会导致扩容。vector有两个属性:size和capacity。size表示已经使用的数据容量,capacity表示数组的实际容量,包含已使用的和未使用的。

vector扩容规则:

  1. 当数组大小不够容纳新增元素时,开辟更大的内存空间,把旧空间上的数据复制过来,然后在新空间中继续增加。
  2. 新的更大的内存空间,一般是当前空间的1.5倍或者2倍,这个1.5或者2被称为扩容因子,不同系统实现扩容因子也不同。
注意,扩容会导致迭代器失效。

在VS2017中,vector的扩容因子是1.5。可以追踪push_back的实现:

decltype(auto) emplace_back(_Valty&&... _Val)
{    // insert by perfectly forwarding into element at end, provide strong guarantee
    if (_Has_unused_capacity())
    {
        return (_Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...));
    }

    _Ty& _Result = *_Emplace_reallocate(this->_Mylast(), _STD forward<_Valty>(_Val)...);
    // ...
}

函数中先通过_Has_unused_capacity函数判断是否有还有未使用的空间,如果有未使用的空间,直接在未使用的空间上新增。如果没有未使用的空间了,就需要执行_Emplace_reallocate重新扩容:

pointer _Emplace_reallocate(const pointer _Whereptr, _Valty&&... _Val)
{    
    // ..
    const size_type _Newsize = _Oldsize + 1;
    const size_type _Newcapacity = _Calculate_growth(_Newsize);
    // ..
}

函数中先判断新长度,然后继续调用_Calculate_growth扩容,这个函数才是真正用来扩容的函数。

忽略过其他判断逻辑,_Calculate_growth的实现为:

size_type _Calculate_growth(const size_type _Newsize) const
{    
    // ...
    const size_type _Geometric = _Oldcapacity + _Oldcapacity / 2;
    // ...
}

新的扩容大小等于当前容量再加上当前容量的一半,即按照1.5倍扩容。

在GCC的实现中,vector扩容是2倍扩容的。

1.5倍扩容和2倍扩容的区别

  1. 扩容因子越大,需要分配的新内存空间越多,越耗时。空闲空间较多,内存利用率低。
  2. 扩容因子越小,需要再分配的可能性就更高,多次扩容耗时。空闲空间较少,内存利用率高。

因此,小规模数组,添加元素不频繁的,建议使用扩容因子更小的。当数据规模越大,插入更频繁,大扩容因子更适合。

示例代码

#include <iostream>
#include <vector>

using namespace std;

int main() {
    int i;
    vector<int> v;

    cout << v.capacity() << " ";
    for (i = 0; i < 10; i++) {
        v.push_back(1);
        cout << v.capacity() << " ";
    }

    return 0;
}

以上代码通过循环插入了十个元素,并打印出每次插入后vector的容量:

0 1 2 3 4 6 6 9 9 9 13

能看出的增长规律为:0 -> 1 -> 2 -> 3 -> 4 -> 6 -> 9 -> 13,而同样的代码通过G++编译后的输出结果为:

0 1 2 4 4 8 8 8 8 16 16

一、迭代器失效

向容器添加或者删除元素可能会导致指向容器的指针、引用或者迭代器失效。使用已经失效的指针、引用或者迭代器将会导致程序出现异常,编码过程中一定要时刻注意迭代器失效的场景。

例如,以vector为例:

int main() {
    vector<int> v{1, 2};
    vector<int>::iterator it;

    for (it = v.begin(); it != v.end(); it++) {
        v.push_back(*it);
    }
    
    return 0;
}

执行以上代码会导致段错误:

原因:在循环中新增了元素,并且重新分配了内存空间,导致迭代器失效。使用已经失效的迭代器会导致程序出现段错误。

迭代器失效,主要有两个层面的意思:

  1. 无法通过迭代器++或--操作遍历整个stl容器,记作第一层失效
  2. 无法通过迭代器存取迭代器所指向的内存,记作第二层失效

二、失效场景

vector和string

如果增加或删除元素导致内存空间重新分配了,那么指向容器的迭代器都会失效(第二层失效)。如果存储空间未重新分配,指向删除元素之前的所有迭代器还有效(第一层失效),但是删除元素之后的所有迭代器都无效了(第二层失效)。

deque

插入到除首尾元素之外任何位置都会导致迭代器失效(第二层失效)。如果插入到首尾元素,迭代器会失效,但是指向已存在元素的指针和引用不失效(第一层失效)。

删除除首尾元素之外的元素,所有迭代器失效(第二层失效)。如果删除的是首尾元素,首前和尾后迭代器失效,其他元素的引用、指针和迭代器不会失效。

map和set

删除和添加元素会导致内部结构调整,迭代器失效,但是引用和指针任然有效(第一层失效)。

list

添加元素不会导致迭代器失效,但是删除元素会导致删除元素后面的所有迭代器失效(第一层失效)。list删除元素永远都会导致尾后迭代器失效(第二层失效)。

三、避免迭代器失效

避免迭代器失效的几种方法:

  1. 减小迭代器的使用范围,不保存迭代器的值。
  2. 避免在遍历迭代器的过程中修改容器。
  3. 不要保存首前和尾后指针。

vector避免删除失效

在遍历vector的过程中删除元素,会导致后面的迭代器失效。如果希望删除后还能继续使用迭代器,要使用erase方法,并接收返回值作为下一个迭代器使用。

正确的使用方式:

int main() {
    vector<int> v{1, 2, 3, 4, 5};
    vector<int>::iterator it;

    for (it = v.begin(); it != v.end();) {
        if (*it == 2 || *it == 4) {
            // 接收返回值作为下一个迭代器
            it = v.erase(it);
            continue;
        }
        cout << *it << endl;
        it++;
    }
}

set/map避免迭代器失效

set和map也和vector一样:

int main() {
        set<int> s{1, 2, 3, 4, 5};
    set<int>::iterator it;

    for (it = s.begin(); it != s.end();) {
        if (*it == 2 || *it == 4) {
            it = s.erase(it);
            continue;
        }
        cout << *it << endl;
        it++;
    }
    return 0;
}

linux内核中的container_of函数

一、container_of的用途

众所周知,linux内核是用C写的,并且内核中是存在许多数据结构的,栈、链表、哈希表以及红黑树等等。但是C语言中一个致命的缺点就是没有泛型,没有泛型的话所有的数据结构就无法通过一套代码来实现。那有没有办法可以使得这些数据结构成为通用的呢?答案肯定是有的,不然如果每个结构体都要实现一套自己的链表,内核将会变得臃肿、复杂且不好维护。要知道C语言虽然没有泛型,但是它有指针,实现内核的大佬们就通过指针来实现了属于C的泛型

以链表为例,首先定义一个全局通用的链表节点:

struct list_node {
    struct list_node *prev, *next;
}

节点中只包含了prevnext两个成员,不含有任何数据内容。所有的数据内容都要另外再定义结构体,把这个链表节点包含进来,再通过这个链表节点实现链表移动。例如实现一个lru缓存节点的链表:

struct lru_cache {
    int page_addr;
    struct list_node ln;
}
数据节点说的是lru_cache结构类型的节点,链表节点是lru_cache中ln成员节点。

它通过ln元素串起来的数据形式为:

图片看起来很好理解,关键的问题就在于如何通过这个节点来组成链表,以及如何通过这个链表节点找到本身的数据节点呢?这便是container_of发挥作用的时候了。

container_of的作用就是给定结构体类型和数据成员名返回结构体本身的地址,它需要三个参数:

  1. 数据节点指针,表示当前某个数据节点中的链表节点地址,即某个lru_cache节点中ln的地址。
  2. 当前数据节点的数据类型,即struct lru_cache这个结构体。
  3. 链表节点在数据节点中的名字,即ln

假设ptr是某个节点中ln元素的地址,那么通过container_of(ptr, struct lru_cache, ln)就能得到这个节点的地址了。通过它来完成一次节点遍历的过程可以描述为:

struct list_node *p = head;
struct lru_cache *lru_node;
while (p) {
    lur_node = container_of(ptr, struct lru_cache, ln);
    // 处理节点
    // print(lru_node);
    // ...
    
    p = p->next == head ? NULL : p->next;
}

二、container_of的实现

container_of的宏定义:

#define container_of(ptr, type, member) ({          \  
    const typeof( ((type *)0)->member ) *__mptr = (ptr); \  
    (type *)( (char *)__mptr - offsetof(type,member) );})

宏定义有三个变量,展开后一共有两行语句:

  1. const typeof( ((type *)0)->member ) *__mptr = (ptr);
  2. (type *)( (char *)__mptr - offsetof(type,member) );}

这两行语句的解析:

先通过传入的type生成一个该类型的指针,(type *)0表示指向NULL的type类型的指针,假设这个指针为p,语句就变成了:

const typeof( p->member ) *__mptr = (ptr);

然后定义一个链表节点类型的指针__mptr指向ptr,因为不知道ptr的数据类型,所以要通过typeof (p->member)得到数据类型。此时__mptr的指向是:

因此,到这里想要得到lru_cache的地址,只要把__mptr的地址减去ln成员在结构体中的偏移就行了。

这也正是第二个语句的作用:先通过offset_of获取到偏移,再通过(char *)强制转换__mptr的数据类型,使得它的步长是1。最后减去偏移就得到数据节点的地址。

一、友元

友元可以允许其他类或者函数访问自己的非共有成员,如果类想把它的函数作为友元,只需要增加一条以friend开头的函数声明即可。

1.1 添加外部函数作为友元

以下一个学生类,类中保存了学生的年龄、名字以及性别信息:

class stu_st {
private:
    int age;
    string name;
    char sex;
};

现在希望在类外面以函数的形式来计算两个学生的年龄之和,因为age成员是私有的,所以目前类外部的函数是无法获取到学生年龄,这个想法无法完成。但是有了友元之后,这个想法就能实现了。只要在类中添加友元定义,外部再实现函数就可以了:

class stu_st {
    friend int figure_age(const stu_st &a, const stu_st &b);
    // ...
};

// 实现计算年龄函数
int figure_age(const stu_st &a, const stu_st &b) {
    return a.age + b.age;
}
友元是不区分共有和私有的,以友元修饰的函数即使声明在private域,外部也是能访问的。

1.2 以外部类作为友元

新增一个老师类,老师能获取学生的年龄:

class teacher_st;
class stu_st {
    friend class teacher_st;
    // ...
};

class teacher_st {
public:
    unsigned int get_stu_age(const stu_st &stu) {
        return stu.age;
    }
};

1.3 静态成员变量

当类中存在静态变量时,友元类和函数也是能直接访问这个变量的。

以下代码声明了一个teacher_st作为老师类,声明了一个stu_st作为学生类,学生类中有一个静态变量total_count表示学生的总数,老师作为友元类来获取这个数量:

#include <iostream>

using namespace std;

class teacher_st;
class stu_st {
    friend class teacher_st;
private:
    static unsigned int total_count;
};

class teacher_st {
public:
    unsigned int get_stu_count() {
        return stu_st::total_count;
    }
};

unsigned int stu_st::total_count = 10;

int main() {
    teacher_st t;
    cout << t.get_stu_count() << endl;
    return 0;
}

运行结果:

二、运算符重载

2.1 运算符重载语法

运算符重载给类提供了大大的便利性,使得自定义类型也能和内置类型一样使用系统操作符。

运算符重载的语法:

void operator+(const stu_st &s);

各元素说明:

  • void:返回值类型
  • operator+:表示重载运算符+
  • s:运算符的参数
运算符重载有几种不同的写法,可以写在类中,也可以写在类外面。

在类中声明

以学生类为例,重载小于符号<使得类可以直接通过年龄大小作为对比:

class stu_st {
private:
    unsigned int age;
    string name;
public:
    stu_st(int age, string name) : age(age), name(name) {
    }
    // 重载小于符号
    bool operator<(const stu_st &x) const {
        return this->age < x.age;
    }
};

在类外面声明

因为类外面的函数无法直接访问类内部数据,因此,类外面的函数需要被声明为类的友元函数。

class stu_st {
private:
    unsigned int age;
    string name;
public:
    // 声明重载操作符>
    friend bool operator>(const stu_st &, const stu_st &);
};

bool operator>(const stu_st &a, const stu_st &b) {
    return a.age > b.age;
}

注意

在类内部重载操作符,编译器默认会把this作为第一个参数传入,因此,重载时无需再传入当前类对象本身。例如:

bool operator<(const stu_st &x) const

这就表示用当前对象和x对象作对比。而外部声明的函数,因为没有封装在类中,不能传入this指针,因此声明时需要传入对象本身。即:

friend bool operator>(const stu_st &, const stu_st &);

2.3 重载输入输出运算符

重载输入和输出运算符需要注意的一个问题是:与iostream标准库相关的运算符重载,必须是非成员函数。

#include <iostream>
#include <ostream>
#include <string>

using namespace std;

class stu_st {
private:
    unsigned int age;
    string name;
public:
    stu_st() {};

    friend istream &operator>>(istream &, stu_st &);
    friend ostream &operator<<(ostream &os, const stu_st &stu);
};

// 重载输出运算符
ostream &operator<<(ostream &os, const stu_st &stu) {
    os << "Name: " << stu.name << "\tAge: " << stu.age;
    return os;
}

// 重载输入运算符
istream &operator>>(istream &is, stu_st &stu) {
    is >> stu.name >> stu.age;
    return is;
}

int main() {
    stu_st stu;
    cin >> stu;
    cout << stu;
}

测试效果:

一、简介

go module公共代理仓库,代理并缓存go模块。你可以利用该代理来避免DNS污染导致的模块拉取缓慢或失败的问题,加速你的构建。

简单来说就是国内访问被墙,go get无法在线获取到仓库,代理仓库就是帮我们解决这个问题的。

二、使用方法

  1. 使用go1.11以上版本并开启go module机制
  2. 导出GOPROXY环境变量
export GOPROXY=https://xxxx.xx

三、国内优质代理仓库

3.1 七牛

export GOPROXY=https://goproxy.cn

3.2 阿里云

export GOPROXY=https://mirrors.aliyun.com/goproxy/

一、缓存

缓存的作用一般是为了减轻数据库压力设计的,因为数据都是读写磁盘,当并发量大的时候,磁盘IO可能跟不上并发量。而缓存一般设计都是放在内存中的,最常见的例如redis和memcached,都是把数据都缓存在内存中。读写内存的速度比磁盘快很多,因此把常用的数据都放到内存中做缓存可以给数据库减轻很大的压力。

一个正常的缓存处理流程为:

  1. 先判断缓存中是否存在数据,如果缓存存在数据,直接读缓存中的数据。
  2. 如果缓存不存在,从数据库读数据。
  3. 如果数据库中存在数据,返回数据库中的数据并写到缓存。
  4. 如果数据库中也不存在数据,直接返回。

image2db4e44cb2632283.png

而作为缓存系统,需要考虑的几个问题是:缓存穿透、缓存击穿和缓存雪崩。

二、缓存穿透

缓存穿透:查询一个数据库不存在的数据,因为数据都不存在,所以缓存中也不存在,请求会直接去数据库中查询,这种情况就产生了缓存穿透。

例如对一个只含非负整数的表中查询id = -1的记录,缓存中不存在数据,所以每次都会请求数据库。当有攻击者恶意使用这种方式频繁请求数据时,会给后端数据库造成极大的压力。

解决方案:

  1. 增加数据校验,直接过滤掉明显不存在的数据。这里一般采用布隆过滤器,预先生成所有可能的数据的bitmap,请求数据库前先在过滤器中查找,过滤掉不可能的数据。
  2. 对不存在的值也设置缓存,在数据库中查询到不存在的数据之后在缓存中添加一个key=null的值。

三、缓存击穿

缓存击穿:某个key缓存在某一时刻失效,由于请求量特别大,导致这一时刻的所有请求同时都直接请求数据库,给数据库带来巨大压力。

解决方案:使用互斥锁或者其它方法锁住这个key(单例模式),当有请求已经在查询这个key时,等待请求,这个请求完成后数据写入缓存再从缓存中读取数据。

四、缓存雪崩

缓存雪崩:给一些键值设置了过期时间,同一时刻这些数据同时失效,请求直接到达数据库。

解决方案:

  1. 和缓存击穿一样,使请求单例执行。
  2. 不要给缓存的过期时间都设置成一样的,设置超时事件的时候加上一个随机时间作为时间。

缓存雪崩和击穿其实一样,缓存击穿时缓存雪崩的一个特例。

两者的区别:缓存雪崩是指多个缓存数据同时失效,而缓存击穿特指某一个数据失效。