cpp

c++学习日志

C++11

  1. 基于范围的for

    1
    for (type a:container) {}
  2. 自动类型推断auto

  3. Lambda

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // capture: 获取上文对应变量,[=]:捕获上文所有变量
    // params: 参数
    [capture](params){body}
    // 如:
    int a=1, b=2;
    auto aa=[a, &b](int c){
    b=1;
    return a+b+c;
    };
  4. 后置返回类型

    1
    2
    3
    auto sum(auto a, auto b) -> decltype(a+b) {
    return a+b;
    }
  5. final, override

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Parent final {
    public:
    virtual int sum(int a, int b) final {
    return a+b;
    }
    virtual int sum2(int a, int b) {
    return a+b;
    }
    virtual void run() = 0;
    };
    class PP: public Parent {
    public:
    virtual void run() override {
    return;
    }
    };
  6. nullptr

  7. long long

  8. 线程

  9. tuple

  10. 智能指针

  11. 条件变量

C++14

  1. Lambda:参数支持auto

new 和 malloc

  1. new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。特别的,new甚至可以不为对象分配内存!定位new的功能可以办到这一点

    • 定位new就是相当于给一个引用一个已有的内存空间,但是可以设置新的类型,共享同一块空间
    1
    2
    3
    4
    5
    6
    7
    new(address) type;
    new(address) type(initializers);
    new(address) type[size];
    new(address) type[size]{braced initializer list};

    int a;
    char *b=new(&a) char;
  2. new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void *, 需要通过强制类型转换将void*指针转换成我们需要的类型。

  3. new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。

  4. 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。

  5. 是否调用构造函数/析构函数

    • 使用new操作符来分配对象内存时会经历三个步骤:

      • 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
      • 第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
      • 第三部:对象构造完成后,返回一个指向该对象的指针。
    • 使用delete操作符来释放对象内存时会经历两个步骤:

      • 第一步:调用对象的析构函数。
      • 第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。
    • 总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会

  6. C++提供了new[] 与delete[]来专门处理数组类型;至于malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小

  7. operator new /operator delete的实现可以基于malloc,而malloc的实现不可以去调用new。

  8. opeartor new /operator delete可以被重载。标准库是定义了operator new函数和operator delete函数的8个重载版本

  9. new无法更改已分配内存的大小,而malloc申请的内存可以通过realloc调整

allocator

在g++中实现的allocator,是用new和相关函数进行封装的

SGI STL的allocator是自行实现的,内存分配分为2个阶段:第一是在8B-128B的16个链表中寻找合适的内存,如果找不到,就到内存池拿,如果内存池不够,就使用malloc申请内存,补充内存池;第二是大于128B的,直接使用malloc

虚函数

以下取自维基百科:
在面向对象程序设计中,派生类继承自基类。使用指针或引用访问派生类对象时,指针或引用本身所指向的类型可以是基类而不是派生类。如果派生类覆盖了基类中的方法,通过上述指针或引用调用该方法时,可以有两种结果:

  1. 调用到基类的方法:编译器根据指针或引用的类型决定,称作“早绑定”;
  2. 调用到派生类的方法:语言的运行时系统根据对象的实际类型决定,称作“迟绑定”。

虚函数的效果属于后者。如果问题中基类的函数是“虚”的,则调用到的都是最终派生类(英语:most-derived class)中的函数实现,与指针或引用的类型无关。反之,如果函数非“虚”,调用到的函数就在编译期根据指针或者引用所指向的类型决定。

有了虚函数,程序甚至能够调用编译期还不存在的函数。

在 C++ 中,在基类的成员函数声明前加上关键字 virtual 即可让该函数成为虚函数,派生类中对此函数的不同实现都会继承这一修饰符,允许后续派生类覆盖,达到迟绑定的效果。即便是基类中的成员函数调用虚函数,也会调用到派生类中的版本。

实现:虚函数表和虚表指针

虚函数表

虚函数表,每个含有虚函数的类都含有一份,同一类的实例共享同一份虚函数表,虚函数表位于类的最前面.虚函数表按声明顺序保存着类的所有虚函数指针.多重继承中,虚函数表的顺序按继承顺序保存
对象的大小:取决于继承了几个类。无虚函数的空类,大小是1。含有虚函数,大小是一个指针。继承了含有虚函数的父类,大小是继承数量*指针大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
public:
virtual void a() {
printf("a()\n");
}
virtual void b() {
printf("b()\n");
}
};
typedef void(*Func)(void);
typedef long long ll;
int main() {
A aa;
ll** tab=(ll**)&aa;
Func fa = (Func)tab[0][0];
Func fb = (Func)tab[0][1];
fa(); fb();
return 0;
}

纯虚函数:virtual int say() = 0;相当于抽象函数

虚继承:多重继承中,共用共同父类

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
28
29
30
31
32
33
34
35
36
37
38
39
40
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
class an {
public:
void say1() {
cout << 111 << endl;
}
virtual void say2() {
cout << 222 << endl;
}
};
class bn : an {
public:
void say1() {
cout << 11111 << endl;
}
void say2() {
cout << 22222 << endl;
}
};
int main() {
#ifdef LOCAL
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
#endif
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
bn *bb = new bn();
an *aa = (an*)bb;
bb->say1();
bb->say2();
aa->say1();
aa->say2();
}
// 输出为
// 11111
// 22222
// 111 非虚函数,调用基类方法
// 22222 虚函数,调用派生类方法

智能指针

C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存

unique_ptrauto_ptr类似,但是auto_ptr在c++11中被替换为unique_ptr,且unique_ptr不允许在局部赋值
unique_ptr在任何时候只能有一个指向内存,可以使用move转移所有权
share_ptr可以同时有多个智能指针指向内存
weak_ptr是弱引用,在使用前需要申请转为share_ptr,
share_ptr允许多个引用

shared_ref_cnt 被减为0时,自动释放 ptr 指针所指向的对象。当 shared_ref_cntweak_ref_cnt 都变成0时,才释放 ptr_manage 对象。
如此以来,只要有相关联的 shared_ptr 存在,对象就存在。weak_ptr 不影响对象的生命周期。当用 weak_ptr 访问对象时,对象有可能已被释放了,要先 lock()

epoll,select,poll

阻塞方式:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 申请socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock==-1) {
printf("create socket failed\n");
return 0;
}
sockaddr_in ser;
memset(&ser, 0, sizeof(ser));
ser.sin_port = htons(55555);
ser.sin_family=AF_INET;
ser.sin_addr.s_addr = htonl(INADDR_ANY);
// 绑定端口
if (bind(sock, (sockaddr*)&ser, sizeof(ser))==-1) {
printf("bind failed\n");
return 0;
}
// 开启监听
if (listen(sock, 1)==-1) {
printf("listen failed");
}
char buff[1024];
int conn;
sockaddr_in client;
while (true) {
// 开始接受请求
if ((conn=accept(sock, (sockaddr*)&client, &len))==-1) {
printf("accept failed");
return 0;
}
int n;
// 接收数据
while ((n = recv(conn, buff, 1024, 0))!=-1) {
buff[n]=0;
printf("recv from %d,%d,len:%d: %s", (int)client.sin_addr.s_addr, (int)client.sin_port, n, buff);
// 发送数据
send(conn, buff, n, 0);
if (!strcmp("exit\r\n", buff)) {
break;
}
}
close(conn);
}
close(sock);

select:

  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  3. select支持的文件描述符数量太小了,默认是1024
  4. 返回值
    • <0 select错误
    • 0 等待超时
    • >0 有可操作文件描述符
1
2
3
4
5
6
7
8
9
10
11
12
int s = socket(AF_INET, SOCK_STREAM, 0);  
bind(s, ...);
listen(s, ...);
int fds[] = 存放需要监听的socket;
while(1){
int n = select(..., fds, ...)
for(int i=0; i < fds.count; i++){
if(FD_ISSET(fds[i], ...)){
//fds[i]的数据处理
}
}
}

epoll

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/* epoll_event.events:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT:表示对应的文件描述符可以写
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR:表示对应的文件描述符发生错误
EPOLLHUP:表示对应的文件描述符被挂断
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里,LT模式是默认模式
*/

int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock==-1) {
printf("create socket failed\n");
return 0;
}
sockaddr_in ser;
memset(&ser, 0, sizeof(ser));
ser.sin_port = htons(55555);
ser.sin_family=AF_INET;
ser.sin_addr.s_addr = htonl(INADDR_ANY);
int flag = fcntl(sock, F_GETFL, 0);
// 设置非阻塞
fcntl(sock, F_SETFL, flag | O_NONBLOCK);
if (bind(sock, (sockaddr*)&ser, sizeof(ser))==-1) {
printf("bind failed\n");
return 0;
}
if (listen(sock, maxCnt)==-1) {
printf("listen failed");
}
// 创建epoll
int ep = epoll_create(maxCnt);
if (ep==-1) {
printf("create epool failed\n");
return 0;
}
epoll_event eeve;
eeve.data.fd = sock;
eeve.events = EPOLLIN;
int clRet = epoll_ctl(ep, EPOLL_CTL_ADD, sock, &eeve);
if (clRet==-1) {
printf("epoll_ctl failed\n");
return 0;
}
char buff[1024];
auto events = new epoll_event[maxCnt];
while (true) {
clRet = epoll_wait(ep, events, maxCnt, -1);
for (int i=0; i<clRet; i++) {
auto nw = events[i];
if (sock==nw.data.fd && (EPOLLIN==(nw.events&EPOLLIN))) {
int newConn = accept(sock, NULL, NULL);
if (newConn==-1) {
printf("accept new connection failed\n");
continue;
}
epoll_event *newEv=new epoll_event();
newEv->events = EPOLLIN;
newEv->data.fd = newConn;
int res = epoll_ctl(ep, EPOLL_CTL_ADD, newConn, newEv);
if (res==-1) {
delete newEv;
printf("add new connection to epoll failed\n");
close(newConn);
}
} else if (nw.events==(nw.events|EPOLLIN)) {
int len=recv(nw.data.fd, buff, 1024, 0);
if (len<=0) {
int tmpFd = nw.data.fd;
epoll_ctl(ep, EPOLL_CTL_DEL, tmpFd, &nw);
close(tmpFd);
} else {
buff[len]=0;
printf("recv from:%s", buff);
send(nw.data.fd, buff, len, 0);
if (strcmp("exit\r\n", buff)==0) {
epoll_ctl(ep, EPOLL_CTL_DEL, nw.data.fd, &nw);
close(nw.data.fd);
}
}
} else if (nw.events==(nw.events|EPOLLERR)) {
printf("found error\n");
epoll_ctl(ep, EPOLL_CTL_DEL, nw.data.fd, &nw);
close(nw.data.fd);
}
}
}
close(sock);
close(ep);
return 0;

pthread.h

pthread_create(pthread_t *pId, pthread_attr_t *attr, void *(*func)(void *), void *args)
创建线程并运行
pId: 线程唯一id
attr:线程相关属性
func:线程入口
args: 线程入口参数

pthread_exit(void* rs)
在线程内使用,退出线程
rs: 线程返回数据

pthread_join(pthread_t pId, void** rs)
等待线程
pId:线程id
rs:pthread_exit(rs),捕获返回值

pthread_attr_t attr;//声明一个参数
pthread_attr_init(&attr);//对参数进行初始化
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);//设置线程为可连接的
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);//设置线程为可分离的
pthread_attr_destroy(&attr)//销毁属性,防止内存泄漏

int pthread_detach(pthread_t tid);
detached的线程在结束的时候会自动释放线程所占用的资源

fork

成功时返回两个值,子进程返回0,父进程返回子进程标记
个进程通过调用fork函数,创建一个新进程。新进程成为子进程(child process)
通过判断fork返回值,是0还是非0(非-1),即可控制让父子进程做不同的事情
fork会复制所有资源(写时复制)
vfork不会复制资源,回合父进程共享,像线程,但是会先于父进程运行,并且导致父进程阻塞,直至子进程exit()(不可以使用return,否则会重复进入vfork)
clone可以精细控制子进程和父进程的资源共享

atomic

生成一个只能原子性访问的变量

1
atomic<int> cnt(0);

volatile

告知编译器不要从寄存器获取变量的值,而是每次都从内存中载入,避免出现问题

1
int volatile a;

restrict

告知编译器,所有修改该指针指向的值必须通过该指针操作,帮助编译器更好的优化代码

1
int *restrict a=(int *)malloc(10*sizeof(int));

mutable

即使结构或类被设置为const,其中某个被mutable修饰的成员也可以被修改

1
2
3
4
5
6
7
struct node {
int a;
mutable int b;
};
const node c;
c.a++; // 不允许
c.b++; // 允许

explicit

  • explicit 修饰构造函数时,可以防止隐式转换和复制初始化
  • explicit 修饰转换函数时,可以防止隐式转换,但 按语境转换 除外

类型推断和template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T1>
T1 sum(T1 a, T1 b) {
return a+b;
}
// 显式具体化
template<> long sum(long a, long b) {
return a+b;
}
// 显式实例化
cout << sum<long long>(1, 2) << endl;
// 隐式实例化
cout << sum(1, 2) << endl;
// 定义一个和x相同类型的变量y
decltype(x) y;
// 返回值类型推断
auto func(type a) -> decltype(a);

存储方式&访问性

存储描述 持续性 作用域 链接性 声明
自动 自动 代码块 在代码块里
寄存器 自动 代码块 在代码块里,使用register
静态,无链接性 静态 代码块 代码块里,使用static
静态,外部链接性 静态 文件 外部 不在任何函数内
静态,内部链接性 静态 文件 内部 不在任何函数内,使用static
  • 外部/内部链接性区别在于能/不能被其他源文件访问

  • 使用extern 声明变量并不赋值,则表示此变量在别的文件里,不分配内存

  • 位于文件内,代码块外的const常量,默认是内部链接性的,如果要具有外部链接性,需要extern const定义,并在需要引用的文件中extern const中声明

  • 函数默认是静态外部链接的,加static可以变成内部链接

  • 语言链接性: 编译器可能会在编译时将函数名翻译为另外一种格式,这就可能导致链接错误

    1
    2
    extern "C" void func();
    extern "C++" void func();

attribute

attribute可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)

packed: 变量或结构域以最小对齐单位对齐,如变量以字节对齐,结构域以位对齐

预处理

1
2
3
4
5
6
7
8
9
10
#define fun(x) #x // 如果调用fun(abc),则会转为"abc",即参数的变量名

#define fun(x) ahh#x //fun(abc),abc=1的话,展开为变量:ahh1

#pragma pack(push) // 保存对齐状态
#pragma pack(4) // 设定为 4 字节对齐
struct node {

};
#pragma pack(pop)

拷贝构造函数和赋值运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 拷贝构造函数,被赋值对象是新建的
// 在Person a=b;时调用
// 可以写为Person a(b);
Person(const Person& p);
// 赋值运算符,被赋值的对象是已有的
// 在 Person a; a=b; 时调用
Person& operator=(const Person& p);

// 移动构造函数
// Person b(std::move(a)), b=std::move(a);
Person(Person&& p);
// 移动赋值运算符
// Person a,b; a=std::move(b);
Person& operator=(Person&& p);

std::move()转移所有权,被move的元素,会失去对原来值的所有权.(如容器类,则会被清空)

std::forward()直接转发,不修改值的左右值属性

右值引用和转移语义

消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
能够更简洁明确地定义泛型函数

条件变量

条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。

1
2
3
4
5
6
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond); //解除所有线程的阻塞