分类 编程语言 下的文章

一、关于glog库

golang中的glog库是google著名开源C++日志库glog的golang版本,在golang默认日志库的基础上做了更进一层的封装使得该库能更贴近日常使用。项目首页为golang/glog,当前版本的glog有以下几个特点:

  1. 支持四种日志级别:INFO < WARNING < ERROR < FATAL,支持不同级别的日志打印到不同文件中。
  2. 默认情况下日志不是打印到标准输出或标准错误,需要手动添加参数才能把日志打到标准错误中。
  3. 支持根据文件大小来切割文件,但是不支持根据日期切割。
  4. 日志输出的格式固定且不可修改:Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg,其中L表示日志级别首字符,如Info日志,L打印出来是ID`。
  5. 源码实现简单,方便自主修改。

安装glog:

go get github.com/golang/glog

1.1 基本用法

glog依赖命令行参数输入,默认是打印到文件的,在使用该库前必须先使用flag包解析命令行参数:

flag.Parse()

因为glog底层实现是先把日志输入到缓冲区buffer中,然后定时写入文件,所以在执行完日志输出后要手动执行glog.Flush()确保日志刷新到文件。一般在函数开始处执行:

defer glog.Flush()

输出日志时,需要加上-log_dir参数以指定目录写入日志文件,如以下代码:

package main

import (
    "flag"
    "github.com/golang/glog"
)

func main() {
    flag.Parse()
    defer glog.Flush()

    glog.Info("HelloWorld")
}

编译后执行:

./glog -log_dir=log

在log目录下会生成相应的日志文件,查看对应的日志:

打印到标准输出

默认情况下,日志不是打印到标准输出中,如需打印到标准输出可以使用以下两个参数:

  1. logtostderr:打印到标准错误而不是文件。
  2. alsologtostderr:同时打印到标准错误。

这两个参数都会把日志打印到标准错误中(在linux终端环境下,前台显示的标准错误和标准输出可以认为是同一个输出),调试日志时可以加上这两个参数中的任一:

二、日志级别

v levelv module功能是最常用到的功能,适用于给同一套代码在不同环境下设置不同日志级别的功能。如在调试环境下打印出更多级别的日志,生产环境下打印更少的日志。

2.1 打印特定级别的日志

使用方法:

glog.V(n).Info("Log message")

其中n表示的是日志级别,当n小于等于设定日志级别时将会被打印。默认情况下日志级别时0,启动时通过-v=x参数设定日志级别。如在代码中分别以级别3和级别5打印两条日志:

func main() {
    flag.Parse()
    defer glog.Flush()

    glog.V(3).Info("Level 3 log")
    glog.V(5).Info("Level 5 log")
}

执行时设定日志级别为4,日志将不会打印出第二条级别为5的日志:

./glog -v=4 -log_dir=log

日志:

Log file created at: 2019/09/08 18:04:55
Running on machine: maqianos
Binary: Built with gc go1.10.4 for linux/amd64
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
I0908 18:04:55.924127     546 main.go:12] Level 3 log

2.2 vmodule选项

vmodule选项可以在上面设定v level之后,单独再给某个文件设置日志级别,适用于以下场景:

  • 系统默认日志级别为3,但是希望main.go中的日志级别为5

执行:./glog -v=4 -log_dir=log -vmodule=main=5,日志中将会打印出级别为5的日志:

Log file created at: 2019/09/08 18:11:36
Running on machine: maqianos
Binary: Built with gc go1.10.4 for linux/amd64
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
I0908 18:11:36.096301     557 main.go:12] Level 3 log
I0908 18:11:36.100119     557 main.go:13] Level 5 log

三、自定义修改

3.1 修改日志WARN为DEBUG

默认是没有DEBUG日志的,WARN日志用得少可以把WARN日志改成DEBUG。在glog.go源码文件中,修改以下内容:

const severityChar = "IWEF" // 改为"IDEF"

var severityName = []string{
    infoLog:  "INFO",
    warningLog: "WARN" // 改为"DEBUG",
    errorLog: "ERROR",
    fatalLog: "FATAL",
}

这里实现简单,改起来也简单。

3.2 默认打印到标准错误

// 搜索函数init
func init() {
    // 注释掉下面两行
    //flag.BoolVar(&logging.toStderr, "logtostderr", false, "log to standard error instead of files")
    //flag.BoolVar(&logging.alsoToStderr, "alsologtostderr", false, "log to standard error as well as files")
    
    // 添加下面两行
    logging.alsoToStderr = false
    logging.toStderr = true

    // Default stderrThreshold is ERROR.
    logging.stderrThreshold = errorLog

    logging.setVState(0, nil, false)
    go logging.flushDaemon()
}

一、strcpy

strcpy是字符串拷贝函数,将一个字符串拷贝到另一个字符串。

代码:

char *strcpy(char *dst, const char *src) { // [1]
    char *p = dst; // [2]

    if (src == NULL || dst == NULL) { // [3]
        return NULL;
    }

    while ((*dst++ = *src++) != '\0'); // [4]

    return p; // [5]
}

实现该函数的注意点:

  1. 传入参数src使用const修饰,避免函数内部修改数据。
  2. 使用p备份dst指针,在拷贝完成之后返回该值。
  3. 需要判断传入参数的合法性,避免出现不合理输入。
  4. 注意判断拷贝完成的标准是src[i] == '\0','\0'也要拷贝到dst字符串中。
  5. 返回拷贝完成的值,更好的支持链式表达式。

高级:

上面的实现是没有区分内存重叠的场景,假设源目地址存在重叠,使用上面的函数实现是有问题的。如图所示:

image.png

dst和src都是指向同一个数组,当把src的第三个元素赋值到dst后,src原有的'\0'被覆盖了,后续继续拷贝就出现问题。

解决这个问题其实很简单,只要从后往前复制src即可解决这个问题。可参考下面memcpy的实现。

二、memcpy

memcpy的作用是把一块内存区域拷贝到另外一个内存地址上面去。

实现:

void *memcpy(void *dst, const void *src, unsigned int count) { // [1]
    char *pdst, *psrc;

    if (dst == NULL || src == NULL || count == 0) { // [2]
        return NULL;
    }

    if (dst == src) { // [3]
        return dst;
    }

    if (dst > src) { // [4]
        pdst = (char *)dst + count - 1;
        psrc = (char *)src + count - 1;

        while (count--) {
            *pdst-- = *psrc--;
        }
    } else { // [5]
        pdst = (char *)dst;
        psrc = (char *)src;

        while (count--) {
            *pdst++ = *psrc++;
        }
    }

    return dst; // [6]
}

需要注意的点:

  1. src使用const修饰,count设置成无符号类型。
  2. 判断传入参数的有效性,避免无效输入。
  3. 判断源目地址相等的情况。
  4. 目的地址大于源地址,从高位向地位复制,避免出现内存重叠的问题。
  5. 目的地址小于源地址,从低位向高位复制。
  6. 返回复制好的目的地址指针以支持链式表达式。

三、memmove

memmove的作用是把一块内存空间的内容移动到另一个内存空间,实现方法和上面的memcpy一致。

一、vector迭代器失效

vector是先行存储的,大部分时候的插入删除操作都有可能导致迭代器失效。失效场景:

  • 执行插入操作时,end指针失效,如果此时重新分配内存了,所有的迭代器失效,否则其他迭代器可能不会失效。
  • 执行删除操作时,所有的迭代器都会失效

示例代码:

vector<int> data;
vector<int>::iterator it;
data.push_back(100);
data.push_back(200);
data.push_back(300);
data.push_back(400);

for (it = data.begin(); it != data.end(); it++) {
    cout << *it << endl;
    if (*it == 300)
        data.erase(it); // 删除之后将会失效
}

二、list迭代器失效

失效规则:

  • 插入操作(insert)和接合操作(splice)不会造成原有的list迭代器失效
  • 删除操作(erase)只有指向被删除元素的那个迭代器失效,其他迭代器不受影响

list不是线性存储的,链式存储的好处就是方便增加和删除,不会影响原有的内存空间。

三、deque迭代器失效

失效规则:

  • 在deque容器首部或者尾部插入元素不会使得任何迭代器失效,vs环境下测试会失效,但是g++没有失效
  • 在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效
  • 在deque容器的任何其他位置的插入和删除操作将使指向该容器元素的所有迭代器失效
// 首尾添加元素,在vs环境下测试失效,g++正常
void deque_f() {
    deque<int> q;
    deque<int>::iterator it;

    q.push_back(100);
    q.push_back(200);
    q.push_back(300);
    q.push_back(400);

    for (it = q.begin(); it != q.end(); it++) {
        cout << *it << endl;
        if (*it == 200) // 不会失效
            q.push_front(50);
        if (*it == 300) // 不会失效,且最后会输出500
            q.push_back(500);
    }

}

四、map和set

map和set底层都是红黑树,不是线性存储,它们的失效规则:

  • 执行插入操作时,原有的迭代器不会失效
  • 删除操作时,其他的迭代器不会失效,被删除元素的迭代器失效

一、普通多继承时子类的内存布局

class CTestA {
    int m_data;
};

class CTestB : virtual public CTestA {
};

class CTestC : virtual public CTestA {
};

class CTestD : public CTestB, public CTestC {
};

内存布局:

1>class CTestD    size(8):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | +--- (base class CTestA)
1> 0    | | | m_data
1>    | | +---
1>    | +---
1> 4    | +--- (base class CTestC)
1> 4    | | +--- (base class CTestA)
1> 4    | | | m_data
1>    | | +---
1>    | +---
1>    +---

CTestD的大小是8,分别包含了一份CTestA和CTestB,他们分别是4个字节,总共8字节。

1.2 在每个类中添加私有变量

class CTestA {
    int m_data;
};

class CTestB : public CTestA {
    int m_data_b;
};

class CTestC : public CTestA {
    int m_data_c;
};

class CTestD : public CTestB, public CTestC {
    int m_data_d;
};

CTestD的大小为20,其中包含CTestA的8个字节和CTestB的8个字节以及自定义的m_data_d的四个字节:

1>class CTestD    size(20):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | +--- (base class CTestA)
1> 0    | | | m_data
1>    | | +---
1> 4    | | m_data_b
1>    | +---
1> 8    | +--- (base class CTestC)
1> 8    | | +--- (base class CTestA)
1> 8    | | | m_data
1>    | | +---
1>12    | | m_data_c
1>    | +---
1>16    | m_data_d
1>    +---

1.3 总结

多继承,如果没有使用virtual关键字,子类的大小为每个继承的父类的大小之和。其内存分布在变量的最开始部分,先排布父类的内存。

二、使用虚继承

2.1 单个父类虚继承

class CTestA {
    int m_data;
};

class CTestB : virtual public CTestA {
    int m_data_b;
};

class CTestC : public CTestA {
    int m_data_c;
};

class CTestD : public CTestB, public CTestC {
    int m_data_d;
};

内存排布:

1>class CTestD    size(24):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | {vbptr}
1> 4    | | m_data_b
1>    | +---
1> 8    | +--- (base class CTestC)
1> 8    | | +--- (base class CTestA)
1> 8    | | | m_data
1>    | | +---
1>12    | | m_data_c
1>    | +---
1>16    | m_data_d
1>    +---
1>    +--- (virtual base CTestA)
1>20    | m_data
1>    +---
1>

此时CTestD包含了以下几个部分:

  • CTestB: B采用了虚继承,其父类A的m_data不会占用D中的空间,但它内部多了一个vbptr指针,总共占用8字节
  • CTestC: C没有采用虚继承,其内部的m_data也还被D继承,没有vbptr指针,算上C自己的m_data_c共占用8个字节
  • m_data_d: D本身自己的成员变量,占用4字节
  • vitrual base CTestA: 虚继承于CTestA,包含CTestA的数据m_data,占用4个字节

CTestD共占用24个字节。

2.2 父类都使用虚继承

将CTestC也改成虚继承的方式:

class CTestC : virtual public CTestA {
    int m_data_c;
};

此时CTestD的内存分布情况为:

1>class CTestD    size(24):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | {vbptr}
1> 4    | | m_data_b
1>    | +---
1> 8    | +--- (base class CTestC)
1> 8    | | {vbptr}
1>12    | | m_data_c
1>    | +---
1>16    | m_data_d
1>    +---
1>    +--- (virtual base CTestA)
1>20    | m_data
1>    +---
1>

CTestD依旧占用24个字节的内存空间,不过和上面不同的是,D中属于C的8个字节空间的内容变了,其中原本属于A的m_data内存变成了vbptr指针。

2.3 总结

当子类(D)的多个父类(A,B)都继承于同一个基类(A)时,且继承时都添加了virtual关键字(属于虚继承时),子类(D)只会保存一份来自基类(A)的内存。每个父类(A,B)中会添加一个vbptr指针指向公共的基类。

三、vbptr指针

3.1 vbptr的内容

当使用虚继承时,类中会生成vbptr指针,指向公共基类的位置。vbptr中包含两个偏移量,第一个是vbptr指针在当前类中的偏移量,第二个是公共的基类在当前类中的位置。例如上面的CTestD类,其内存布局为:

两个vbtr内容的解析:

  1. CTestB.vbptr: 第一个偏移量=CTestB.vbptr - BTestB = 0,第二个偏移量=CTestA - CTestB = 22。
  2. CTestC.vbptr: 第一个偏移量=CTestC.vbptr - CTestC = 0,第二个偏移量=CTestA - CTestC = 16。

2.2 第一个偏移量的细节

上面的例子中,CTestB和CTestC的vbptr指针的第一个偏移量(以vbptr[0]表示)都是0,它表示的是这个偏移量对于当前类的偏移。因为类B和类C目前只有vbptr的指针,所有的类中,vbptr是在成员变量之前的,所以他们都是0。但是如果在类中加入一个虚函数,使类中产生虚指针,那么这个偏移量就不是0了。

修改类B:

class CTestB : virtual public CTestA {
    int m_data_b;
public:
    CTestB() {
        m_data_b = 2;
    }
    void print_data() {
        cout << m_data_b << endl;
    }
    virtual void hello() {
    }
};

打印出C的内存分布:

1>class CTestD    size(28):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | {vfptr}
1> 4    | | {vbptr}
1> 8    | | m_data_b
1>    | +---
1>12    | +--- (base class CTestC)
1>12    | | {vbptr}
1>16    | | m_data_c
1>    | +---
1>20    | m_data_d
1>    +---
1>    +--- (virtual base CTestA)
1>24    | m_data
1>    +---
1>

可以看到,类B中多了一个vfptr,它指向的是虚函数表的地址,此时vbptr放在第二位,CTestB.vbptr[1]的值就是-4。类B和类C的vbptr的细节:

1>CTestD::$vbtable@CTestB@:
1> 0    | -4
1> 1    | 20 (CTestDd(CTestB+4)CTestA)
1>
1>CTestD::$vbtable@CTestC@:
1> 0    | 0
1> 1    | 12 (CTestDd(CTestC+0)CTestA)

B的第1个偏移变成了-4,而C的没有变化。

一、匿名管道

pipe的基本描述和用法

匿名管道是linux中的一种通信方式,为有血缘关系的进程提供数据通信。相关的api函数为:

#include <unistd.h>
int pipe(int pipefd[2]);

pipe函数传入一个长度为2的int数组,执行成功将在内核区域分配一块内存区域,并设置pipefd为管道的文件描述符。

创建子进程后,子进程也会继承这两个描述符,对于同一个文件描述符来说,父端写,子端就可以读,字段写,父端也可以读。

工作流程可以描述为:

但是由于管道内部实现是用的环形队列,因此父子不能同时对一个管道进行读操作或者写操作,队列是单向的,两端写就有问题。

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>

#define SEND_BUFF "HelloWorld"

#define CLOSE_FD(fd) \
    do {    \
        if (fd != -1) { \
            close(fd);  \
            fd = -1;    \
        }   \
    } while (0)

int main(){
    int fd[2], ret, n;
    char buf[1025];

    ret = pipe(fd);
    if (ret == -1)
        goto _err;

    memset(buf, 0, 1025);
    pid_t pid = fork();

    if (pid == -1) {
        goto _err;
    } else if (pid == 0) {
        // 子端关闭fd1,打开fd0读
        CLOSE_FD(fd[1]);
        n = read(fd[0], buf, 1024);
        if (-1 == n) goto _err;
        printf("child read %d byte data: %s\n", n, buf);
        CLOSE_FD(fd[0]);
    } else {
        // 父端关闭fd0,打开fd1写
        CLOSE_FD(fd[0]);
        n = write(fd[1], SEND_BUFF, sizeof(SEND_BUFF));
        if (n == -1) goto _err;
        printf("parent send %d bytes data: %s\n", 
                sizeof(SEND_BUFF), SEND_BUFF);
        // 等待子进程退出
        wait(0);
        CLOSE_FD(fd[1]);
    }
    return 0;

_err:
    perror("Error");
    CLOSE_FD(fd[0]);
    CLOSE_FD(fd[1]);
    return -1;
}

1.2 非阻塞pipe

管道还有一个函数pipe2提供了更高级的选项,可以使得管道成为非阻塞读:

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h>              /* Obtain O_* constant definitions */
#include <unistd.h>

int pipe2(int pipefd[2], int flags);

其中,当flags置为O_NONBLOCK时,管道为非阻塞,从非阻塞的管道读取数据时,如果此时没有数据可读,将返回-1,并设置errno为EAGAIN

修改子进程部分代码为:

if (pid == 0){
    CLOSE_FD(fd[1]);
    while (1) {
        n = read(fd[0], buf, 1024);
        if (-1 == n) {
            if (errno == EAGAIN) {
                printf("READ AGAIN\n");
                // 没有数据可读时,休眠一秒,继续读
                sleep(1);
                continue;
            }
            goto _err;
        }
        printf("child read %d byte data: %s\n", n, buf);
        break;
    }
    CLOSE_FD(fd[0]);
}

父进程在写数据前先睡眠5秒:

else {
    CLOSE_FD(fd[0]);
    sleep(5);
    n = write(fd[1], SEND_BUFF, sizeof(SEND_BUFF));
    // ...
}

运行结果:

1.3 管道数据读取的原则

  1. 如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
  2. 如果有指向管道写端的文件描述符没关闭,而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
  3. 如果所有指向管道读端的文件描述符都关闭,这时有进程指向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
  4. 如果有指向管道读端的文件描述符没关闭,而持有管道写端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再write会阻塞,直到管道中有空位置了才写入数据并返回。

二、有名管道fifo

2.1 fifo的基本用法

fifo管道是linux文件系统中的一种文件类型,创建在磁盘上,任何进程都可以打开这个文件进行读写。

它可以解决无血缘关系进程的通信问题,创建管道的函数为:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

当管道文件被创建之后,就可像文件一样打开它进行读写:

// read.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>
#include <errno.h>
#include<unistd.h>

#define FIFO_FILE "fifo"
#define BUFF_SIZE 1024

int main() {
    int ret, fd = -1;
    char buf[BUFF_SIZE] = { 0 };

    ret = mkfifo(FIFO_FILE, 0755);
    if (ret == -1 && errno != EEXIST)
        goto _err;

    fd = open(FIFO_FILE, O_RDONLY);
    if (fd == -1)
        goto _err;

    bzero(buf, BUFF_SIZE);
    ret = read(fd, buf, sizeof(buf) - 1);
    if (ret == -1)
        goto _err;

    printf("read: %s\n", buf);
    close(fd);
    fd = -1;
    
    return 0;
_err:
    perror("Error");
    if (fd > 0) {
        close(fd);
        fd = -1;
    }
    return -1;
}

写管道程序:

// write.c
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>

#define FIFO_FILE "fifo"

int main(){
    int ret, fd;

    fd = open(FIFO_FILE, O_WRONLY);
    if (fd == -1)
        goto _err;
        
    unlink(FIFO_FILE);

    ret = write(fd, "HelloWorld", 11);
    if (ret == -1)
        goto _err;

    close(fd);
    fd = -1;

    return 0;

_err:
    perror("Error");
    if (fd > 0) {
        close(fd);
        fd = -1;
    }
    return -1;
}

此时运行./read,会创建管道文件,新开一个窗口就可以看到,管道的颜色和普通文件不一样,并且ls的第一个字符是p

默认情况下,读是阻塞的(类似于读文件),直到有数据过来为止,所以./read执行后,会阻塞,直到运行./write之后才会收到数据。

read: HelloWorld

2.2 非阻塞pipe

非阻塞pipe有一个注意事项:当打开的管道没有其他进程也打开的话(即只有当前进程在读,没有其他进程了),直接返回0,不会返回EAGAGIN

// 加上O_NONBLOCK标记
fd = open(FIFO_FILE, O_RDONLY | O_NONBLOCK);
if (fd == -1)
    goto _err;

bzero(buf, BUFF_SIZE);
printf("fd = %d\n", fd);
while (1) {
    ret = read(fd, buf, sizeof(buf) - 1);
    if (ret == -1) {
        if (errno == EAGAIN) {
            printf("READ AGAIN\n");
            sleep(1);
            continue;
        }
        goto _err;
    } else if (ret == 0) {
        printf("read end\n");
        close(fd);
        fd = -1;
        break;
    } else {
        printf("read: %s\n", buf);
    }
}

编译后如果直接执行,程序会直接退出,因为此刻没有其他进程在写,不会返回EAGAIN

一、线程同步的几种方法

多线程主要有以下几种同步方法:

  1. 互斥量
  2. 读写锁
  3. 屏障
  4. 条件变量
  5. 信号量
  6. 自旋锁

二、几种同步方式的比较

同步方式优缺点和适用范围
互斥量最简单的锁,使用临界区的方式锁住代码块,最基础的锁,适用于多线程互斥
读写锁有更高的并行性,只会锁住写锁,读锁共享,适用于读远大于写的场景
屏障适用于线程等待,直到多个线程到达同一点时再继续运行,主要用于同步
条件变量允许线程以无竞争的方式等待特定条件的发生,适用于生产者和消费者场景
信号量通过PV变量的方式,多用于生产者和消费者场景,和条件变量类似
自旋锁被锁住时线程处于忙等,适用于锁被持有的时间短,不希望线程把时间花在重新调度上

三、信号量和条件变量的区别

  1. 条件变量需要用到互斥锁,信号量不用
  2. 信号量是基于PV操作,需要多个信号量配合使用
  3. 信号量可以用于线程和进程,条件变量只能用于线程

一、结构体描述

结构体对齐是C/C++优化结构体内存排布的一种机制,它的出现是为了解决跨总线寻址的问题。

例如对于以下结构:

struct stu {
    char a;
    int b;
};

如果不执行结构体对齐,它在内存中的排布应该是这样的:

对于字段b而言,它占四个字节,跨了两个总线地址,如果要取出b的值就要寻址两次。而如果把结构设计成以下形式:

对b的访问就只用寻址一次了,所以,为了减少cpu消耗,编译器会对结构进行优化,会对内存进行对齐。

二、对齐原则

结构体对齐遵循的原则:

  1. 第一个成员的首地址为0,每个成员的首地址是自身大小的整数倍,不够时填充空字节
  2. 结构体总大小是结构体中最大元素的整数倍,存在结构体嵌套时,最大元素是内部的结构体成员而不是这个结构体

2.1 规则一

struct Stu {
    char name[7];
    int age;
    char sex;
    short id;
    int friends;
};
  1. name成员放在Stu成员的最开始,它占了7个字节,位于0-6
  2. age占四个字节,它本应该从第7个字节存放,但是7不能整除age的大小,它被放在第8个字节
  3. sex占一个字节,放在第12位
  4. id占两个字节,本应放在第13字节,但13无法整除2,此时会在id前面填充一个字节对齐
  5. friends放在第16个字节开始

使用vs查看内存分布可以看到,name和sex后面被填充了一个字节:

1>class Stu    size(20):
1>    +---
1> 0    | name
1>      | <alignment member> (size=1)
1> 8    | age
1>12    | sex
1>      | <alignment member> (size=1)
1>14    | id
1>16    | friends
1>    +---

2.2 规则二

struct Nginx {
    struct Stu stu;
    char c;
};

当结构体中包含结构体时,此时最大的元素是name,它对齐后占8个字节,为了维持对齐的特性,会在Nginx成员后面继续填充:

1>class Nginx    size(24):
1>    +---
1> 0    | Stu stu
1>20    | c
1>      | <alignment member> (size=3)
1>    +---

可以看到c后面填充了八个字节。

STL中的mapset默认时不支持存结构体的,如果要添加结构体的支持,必须手动重载<运算符。

原因:map和set底层都是通过红黑树实现的,红黑树搜索树的一种,插入数据时要比较大小,所以结构体必须重载小于号

示例:

#include <iostream>
#include <string>
#include <set>

using namespace std;

typedef struct stu_st {
    string name;
    int age;
}stu_t;

int main() {
    set<stu_t> stu_infos;

    stu_t a, b;
    a.name = "xiaoming";
    a.age = 20;

    b.name = "xiaohua";
    b.age = 21;

    stu_infos.insert(a);
    stu_infos.insert(b);

    cout << stu_infos.size() << endl;

    return 0;
}

以上代码在vs下编译报错:

问题很明确,没有重载<符号,添加上以下代码即可:

bool operator<(const stu_t& a, const stu_t& b) {
    return a.name < b.name;
}

一、申请方式

  • 栈是系统自动申请,自动释放。
  • 堆需要手动申请,手动释放。

二、增长方向

  • 栈是从高地址向地地址增长
  • 堆从地地址到高地址增长

三、存储位置

  • 栈的内存空间在用户空间的最顶端,3G以下
  • 堆位于全局静态区,在栈的下面

四、大小限制

  • 栈可分配的内存大小较小
  • 堆中可分配的内存较大

五、申请效率

  • 栈内存申请较快,不会产生碎片
  • 堆内存申请较慢,会产生碎片