c++学习日志
C++11
基于范围的
for
1
for (type a:container) {}
自动类型推断auto
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;
};后置返回类型
1
2
3auto sum(auto a, auto b) -> decltype(a+b) {
return a+b;
}final
,override
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class 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;
}
};nullptr
long long
线程
tuple
智能指针
条件变量
C++14
- Lambda:参数支持auto
new 和 malloc
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。特别的,new甚至可以不为对象分配内存!定位new的功能可以办到这一点
- 定位new就是相当于给一个引用一个已有的内存空间,但是可以设置新的类型,共享同一块空间
1
2
3
4
5
6
7new(address) type;
new(address) type(initializers);
new(address) type[size];
new(address) type[size]{braced initializer list};
int a;
char *b=new(&a) char;new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void *, 需要通过强制类型转换将void*指针转换成我们需要的类型。
new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
是否调用构造函数/析构函数
使用new操作符来分配对象内存时会经历三个步骤:
- 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
- 第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
- 第三部:对象构造完成后,返回一个指向该对象的指针。
使用delete操作符来释放对象内存时会经历两个步骤:
- 第一步:调用对象的析构函数。
- 第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。
总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会
C++提供了new[] 与delete[]来专门处理数组类型;至于malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小
operator new /operator delete的实现可以基于malloc,而malloc的实现不可以去调用new。
opeartor new /operator delete可以被重载。标准库是定义了operator new函数和operator delete函数的8个重载版本
new无法更改已分配内存的大小,而malloc申请的内存可以通过realloc调整
allocator
在g++中实现的allocator,是用new和相关函数进行封装的
SGI STL的allocator是自行实现的,内存分配分为2个阶段:第一是在8B-128B的16个链表中寻找合适的内存,如果找不到,就到内存池拿,如果内存池不够,就使用malloc申请内存,补充内存池;第二是大于128B的,直接使用malloc
虚函数
以下取自维基百科:
在面向对象程序设计中,派生类继承自基类。使用指针或引用访问派生类对象时,指针或引用本身所指向的类型可以是基类而不是派生类。如果派生类覆盖了基类中的方法,通过上述指针或引用调用该方法时,可以有两种结果:
- 调用到基类的方法:编译器根据指针或引用的类型决定,称作“早绑定”;
- 调用到派生类的方法:语言的运行时系统根据对象的实际类型决定,称作“迟绑定”。
虚函数的效果属于后者。如果问题中基类的函数是“虚”的,则调用到的都是最终派生类(英语:most-derived class)中的函数实现,与指针或引用的类型无关。反之,如果函数非“虚”,调用到的函数就在编译期根据指针或者引用所指向的类型决定。
有了虚函数,程序甚至能够调用编译期还不存在的函数。
在 C++ 中,在基类的成员函数声明前加上关键字 virtual
即可让该函数成为虚函数,派生类中对此函数的不同实现都会继承这一修饰符,允许后续派生类覆盖,达到迟绑定的效果。即便是基类中的成员函数调用虚函数,也会调用到派生类中的版本。
实现:虚函数表和虚表指针
虚函数表
虚函数表,每个含有虚函数的类都含有一份,同一类的实例共享同一份虚函数表,虚函数表位于类的最前面.虚函数表按声明顺序保存着类的所有虚函数指针.多重继承中,虚函数表的顺序按继承顺序保存。
对象的大小:取决于继承了几个类。无虚函数的空类,大小是1。含有虚函数,大小是一个指针。继承了含有虚函数的父类,大小是继承数量*指针大小
1 | class A { |
纯虚函数:virtual int say() = 0;相当于抽象函数
虚继承:多重继承中,共用共同父类
1 |
|
智能指针
C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存
unique_ptr
和auto_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_cnt
与 weak_ref_cnt
都变成0时,才释放 ptr_manage
对象。
如此以来,只要有相关联的 shared_ptr
存在,对象就存在。weak_ptr
不影响对象的生命周期。当用 weak_ptr
访问对象时,对象有可能已被释放了,要先 lock()
。
epoll,select,poll
阻塞方式:
1 | // 申请socket |
select:
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小了,默认是1024
- 返回值
- <0 select错误
- 0 等待超时
- >0 有可操作文件描述符
1 | int s = socket(AF_INET, SOCK_STREAM, 0); |
epoll
1 | /* epoll_event.events: |
pthread.h
pthread_create(pthread_t *pId, pthread_attr_t *attr, void *(*func)(void *), void *args)
创建线程并运行pId
: 线程唯一idattr
:线程相关属性func
:线程入口args
: 线程入口参数
pthread_exit(void* rs)
在线程内使用,退出线程rs
: 线程返回数据
pthread_join(pthread_t pId, void** rs)
等待线程pId
:线程idrs
: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 | struct node { |
explicit
explicit
修饰构造函数时,可以防止隐式转换和复制初始化explicit
修饰转换函数时,可以防止隐式转换,但 按语境转换 除外
类型推断和template
1 | template<typename T1> |
存储方式&访问性
存储描述 | 持续性 | 作用域 | 链接性 | 声明 |
---|---|---|---|---|
自动 | 自动 | 代码块 | 无 | 在代码块里 |
寄存器 | 自动 | 代码块 | 无 | 在代码块里,使用register |
静态,无链接性 | 静态 | 代码块 | 无 | 代码块里,使用static |
静态,外部链接性 | 静态 | 文件 | 外部 | 不在任何函数内 |
静态,内部链接性 | 静态 | 文件 | 内部 | 不在任何函数内,使用static |
外部/内部链接性区别在于能/不能被其他源文件访问
使用
extern
声明变量并不赋值,则表示此变量在别的文件里,不分配内存位于文件内,代码块外的
const
常量,默认是内部链接性的,如果要具有外部链接性,需要extern const
定义,并在需要引用的文件中extern const
中声明函数默认是静态外部链接的,加
static
可以变成内部链接语言链接性: 编译器可能会在编译时将函数名翻译为另外一种格式,这就可能导致链接错误
1
2extern "C" void func();
extern "C++" void func();
attribute
attribute可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)
packed: 变量或结构域以最小对齐单位对齐,如变量以字节对齐,结构域以位对齐
预处理
1 |
|
拷贝构造函数和赋值运算符
1 | // 拷贝构造函数,被赋值对象是新建的 |
std::move()转移所有权,被move的元素,会失去对原来值的所有权.(如容器类,则会被清空)
std::forward()直接转发,不修改值的左右值属性
右值引用和转移语义
消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
能够更简洁明确地定义泛型函数
条件变量
条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
1 | int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr); |