文章目录
- 一、内存管理模型
- 二、引用
- 三、区别
- C和C++的特点与区别?
- class和struct的区别?
- C/C++中指针和引用的区别
- 动态绑定和静态绑定的区别
- 重载和重写的区别
- new delete 与malloc free 区别
- 类的成员变量在构造函数赋值和在初始化列表的区别
- 四、面向对象三大特性
- 1、封装
- 2、继承
- 3、多态
- 五、关键字
- 1、#define 宏定义
- 2、const常量
- 3、static静态变量
- 4、inline内联函数
- 5、mutable关键字
- 6、volatile
- 7、extern
- 8、register
- 9、auto
- 10、explicit
- 六、智能指针
- shared_ptr
- unique_ptr
- weak_ptr
- 实现智能指针
- 七、类型转换
- static_cast
- reinterpret_cast
- dynamic_cast
- const_cast
- 八、C++11的新特性
- auto和decltype
- 移动语义move
- 返回值优化(RVO)
- 构造函数的列表初始化
- Lambda表达式
- 新特性的关键字
- nullptr
- default
- final和override
- const
- delete
- explicit
- thread_local
- 返回值可能产生的临时对象
- 构造函数为什么不能是虚函数
一、内存管理模型
1、代码区:存放函数体的二进制代码,由操作系统进行管理,特点是共享和只读。
2、全局区:存放已初始化的全局变量、static修饰的静态变量、const修饰的常量。
3、bss:存放未初始化的全局变量
3、栈区:存放局部变量和函数传递的参数,由编译器自动进行分配和释放。
4、堆区:存放动态分配的变量,需要程序员使用new,malloc分配和使用delete,free释放,若不释放一般情况下程序运行完由操作系统回收
内存不同区域的意义
赋予了变量的不同生命周期,提供给程序员灵活地编程。
二、引用
2.1、意义
给变量起别名
用法
数据类型 &别名 = 原变量名
2.2、注意事项
引用必须初始化,在初始化之后,不可以改变(赋值操作可以)
例子:
int a = 10;
int b = 20;
int &c;//错误,必须初始化
int &c = a;
int d = 30;
c = b;//赋值操作,没用更改引用
&c = d;//错误,引用初始化后不可以改变
2.3、引用做函数参数
作用:函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针修改实参,通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单
例子:
void fun01(int *a,int *b){
*a = 10;
*b = 20;
}
void fun01(int &a,int &b){
a = 10;
b = 20;
}
//俩者都能改变a和b的值
2.4、引用作为返回值
注意:局部变量的引用不能作为返回值
#include <iostream>
#include <stdio.h>
using namespace std;
int &test01(){
//int a = 10;局部变量的引用不能作为返回值
static int a = 10;//全局变量
return a;
}
int main(void){
int &ret = test01();
printf("ret = %d\n",ret);//输出10
test01() = 1000;//可以作为左值
printf("ret = %d\n",ret);//输出1000
return 0;
}
2.5 引用的本质
本质:引用的本质在c++内部实现是一个指针常量.指针常量是指指针的指向是不可以修改的,但是值可以修改,常量指针是指指向可以修改,但是值不可以修改的
三、区别
C和C++的特点与区别?
C语言的特点:
1、C语言是面向过程的结构化语言,易于调试和维护。
2、表现能力和处理能力极强,可以直接访问内存的物理地址。
3、C语言可以对硬件进行操作,也可以应用软件的开发。
4、C语言效率高、可移植性强等特点。
C++语言的特点:
1、在C语言的基础上进行扩充和完善,使得C++兼容了C语言的面向过程的特点,成为了C++程序设计语言。
2、可以使用抽象数据类型进行基于对象编程。
3、可以使用多继承、多态进行面向对象编程。
4、可以使用以模板为特征进行泛型化编程。
C语言和C++语言的本质区别:C语言是面向过程的结构化语言,而C++是面向对象语言,换句话说C++在C语言的基础上增加了面向对象的程序设计的新内容,使得C++成为软件开发的重要工具。
class和struct的区别?
在C++语言,class和struct都可以定义一个类。
1、默认继承权限:如果不指定权限,来自于class的继承默认为private,来自于struct继承默认public。
2、成员默认权限:如果成员不指定权限,class定义的类成员默认权限private,而struct类成员默认权限public。
C/C++中指针和引用的区别
1、指针是一个变量,有自己的一块内存,只不过这个变量存储的是内存的地址,它指向内存的一个存储单元,即指针是个实体,而引用只是个变量的别名。
2、使用sizeof获取大小不同,指针的sizeof在64位机中占有8个字节,在32位机中占有4个字节,而引用占用的是对象类型的大小。
3、指针可以初始化为NULL,而引用必须初始化且初始化的对象不能为NULL。
4、作为参数传递时,指针会被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用指向的对象。
5、指针可以改变指向的对象,而引用只能是一个对象的引用,不能修改。
6、指针可以有多级指针,引用只能一级。
7、指针和引用使用的++运算符意义不一样。引用++等于指向的对象++,而指针++表示指向的对象下一个内存地址。
8、函数的返回值如果是动态分配的内存,尽量使用指针,使用引用可能容易会导致内存泄漏,比如函数fun()内部string *str = new string(“hello”),返回一个str的引用,这个引用string s = fun(),程序员忘记对它delete释放内存,所以容易导致内存泄漏。
动态绑定和静态绑定的区别
也叫动态连编和静态连编。
如果父类中存在有虚函数,那么编译器便会为之生成虚表(属于类)与虚指针(属于某个对象),在程序运行时,根据虚指针的指向,来决定调用哪个虚函数,这称之与动态绑定,与之相对的是静态绑定,静态绑定在编译期就决定了。
重载和重写的区别
重载(overload):同一个函数名,但参数表不同,表现的形式不同,比如重载构造函数、重载运算符等。
重写(override):基类的虚函数或普通成员函数可能会被子类重写,基类的纯虚函数必须被子类重写。
new delete 与malloc free 区别
1、new delete和malloc free都是申请和释放的堆上的空间,都是成对存在的,否则将会造成内存泄露或二次释放。
2、不同的是,new delete是C++中定义的操作符,new除了分配空间外,还会调用类的构造函数来完成初始化工作,delete除了释放空间外还会调用类的析构函数。而malloc和free是C语言中定义的函数。
3、new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
4、new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。
5、new不仅分配一段内存,而且会调用构造函数,malloc不会。
6、new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
7、new是一个操作符可以重载,malloc是一个库函数。
8、malloc分配的内存不够的时候,可以用realloc扩容。new没用这样操作。
9、new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。
10、申请数组时:new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。
类的成员变量在构造函数赋值和在初始化列表的区别
从必要性和效率两个方面讲解区别:
1、必要性
第一种情况:
如果A类只有带参数的构造函数,则编译器不会生成默认的无参构造函数,并且A类是B类的成员变量,那么B类必须使用初始化列表方式。
#include <iostream>
using namespace std;
class A{
public:
A(int a){
}
};
class B{
public:
B();
public:
A a;
};
/*
B::B(){ //编译出错,因为a没有无参的构造函数
}
*/
B::B() : a(2){ //a显示使用带参数的构造函数
}
int main()
{
B b;
return 0;
}
第二种情况:
类含有const或引用的成员变量,必须使用初始化列表。因为成员变量在构造函数执行之前已经被无参构造函数初始化过了,那么在构造函数内使用赋值操作是错误的。
#include <iostream>
#include <string>
using namespace std;
class B{
public:
B();
public:
const string str;
};
//出错,因为在构造函数执行之前,类的成员变量已经使用无参构造函数初始化过了
//const 常量不能进行赋值操作
/*
B::B() {
str = "s545";
}
*/
B::B() : str("1215"){
}
int main()
{
B b;
return 0;
}
2、效率上
对于在构造函数内使用赋值操作,会在构造函数执行之前成员变量会调用一次默认的构造函数,在赋值操作又进行一次拷贝构造函数。
而对于使用初始化列表方式,只会执行一次构造函数。
四、面向对象三大特性
1、封装
定义:隐藏对象的属性和实现细节,仅仅对外提供接口和方法。
优点:
1)隔离变化;2)便于使用; 3)提高重用性; 4)提高安全性
缺点:
1)如果封装太多,影响效率; 2)使用者不能知道代码具体实现。
2、继承
定义:一个对象可以使用另一个对象的属性和方法。
优点:
1)减少重复的代码;2)继承是多态的前提;3)继承增加了类的耦合性
缺点:
1)继承在编译期间就定义了,无法在运行时改变父类的实现。
2)如果子类继承父类之后,父类的实现不适合解决子类的问题,必须重写或替换父类的实现,这种依赖关系限制了代码的灵活性、最终降低了代码复用。
3)如果父类定义部分的实现,父类的实现的改变会影响子类的实现。
3、多态
C++有两种多态方式,称为动多态、静多态。
静多态:主要通过模板template来实现的,宏也是实现静多态的一种途径。
动多态:C++是通过虚函数实现的,基类的声明一些方法(一般为纯虚函数),子类通过继承必须重写方法,而且基类的析构函数必须为虚函数,否则子类无法析构可能会导致内存泄漏。通过基类的指针指向子类的对象,运行时将会根据对象的实际类型来调用相应的函数就可以实现调用子类重写的方法。
用一句话概括多态:在基类的函数前加上virtual关键字,该函数为虚函数,运行时会根据对象的实际类型在调用函数,若对象是派生类,则调用派生类的方法,若对象是基类,则调用基类的方法。
特点:
1)虚函数 = 0 为纯虚函数,比如virtual void fun() = 0;派生类必须重写纯虚函数。
2)析构函数必须是虚函数,否则派生类无法调用析构可能会导致内存泄漏,。
3)存在虚函数的类都有一个一维的虚函数表(虚表),该类的对象都有一个虚函数表指针,该指针指向了虚函数表。虚表是跟类对应的,虚表指针是跟对象对应的。
4)多态通过虚函数实现,结合动态绑定。
5)抽象类必须包含一个纯虚函数。
虚函数
每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类。
B的虚函数表中存放着B::foo和B::bar两个函数指针。
D的虚函数表中存放的既有继承自B的虚函数B::foo,又有重写(override)了基类虚函数B::bar的D::bar,还有新增的虚函数D::quz。
虚函数表构造过程:
问题:
1、基类的析构函数为什么是虚函数?
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在运行前已经决定了调用哪个析构函数,当在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。若是声明为虚函数,编译器则实施动态绑定,当删除基类指针时,会先调用派生类的析构函数,而派生类的析构函数又会自动调用基类的析构函数。
在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,要将基类的析构函数声明为虚函数。
2、类作为基类时析构函数一定要是虚函数吗?
如果不需要基类对派生类及对象进行操作,则不能定义虚函数,因为这样会增加内存开销.当类里面有定义虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间.所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。
五、关键字
1、#define 宏定义
#define又称宏定义,标识符为所定义的宏名,简称宏。
用法:
#define 标识符 常量 //不能以;结束,它不是个语句
//常见的用法:
//数字
#define MAX 1024
//字符串
#define ERROR "RETURN CODE ERROR"
//字符
#define YES 'Y'
//表达式:注意下面两个表达式不一样
#define FUN1(x) (x*x)
#define FUN2(x) ((x)*(x))
//宏定义表达式例子
int x = 3;
int fun1 = FUN1(x + 3);
int fun2 = FUN2(x + 3);
//结果:fun1 = (x+3*x+3) = 24 fun2 = ((x+3)*(x+3)) = 36
#undef:是结束该宏定义的作用域,#define 宏定义的常量的作用域由定义开始到源程序的结束,如果使用#undef 这个宏则结束它的作用域。
特点:定义的标识符不占用内存,只是一个临时的符号,在经过预编译后就不存在了。
预编译(也称预处理):预编译不是编译,而是在执行编译之前,由操作系统自动完成,在预处理阶段时,执行的操作就是简单的“文本替换”,将源程序中所有宏定义的“标识符”替换成常量,称为”宏替换“或”宏展开”。替换完之后再进行编译,所以说单击“编译”一般执行两个操作,是先预编译,再编译。
注意:凡是以#开头的都是预处理指令,如#include<stdio.h>,在编译之前先将stdio.h的全部文本内容替换这一行,然后再进行正式的编译。
优点:易于程序的修改,易于维护。使用宏定义一个宏代替经常使用的常量,如果这个常量在项目的后期需要修改,就不需要对源程序的这个常量一个个的修改,直接修改这个宏定义的常量即可。
在《Effective++》中的第一条就是这样写到的:尽量用const和inline而不用#define。其实它实际就是说“尽量使用编译器并非预处理”。(为什么是const和online将在后面讲到)
2、const常量
关键字const也称限定符。使用const修饰的变量可以允许指定一个语义上的约束–不能修改该变量且由编译器实施。通过const可以通知编译器和程序员这个变量不能被改变。
用法:
//1、修饰普通变量
const int x = 10; // 这个变量x的值不能被改变
//2、修饰指针
int a = 10,b = 30;
//指针常量,本质是一个常量,不能够修改,故不能修改指针指向
int *const p = &a; // 指向字符串类型的指针常量,不能修改指针p的指向,可以修改指向的存储单元的内容
*p = &b; // 出错,不能修改指针的指向
*p = 66; // 每次,可以修改指向的存储单元内容
//常量指针,本身是一个指针,可以修改指针指向
const int* q = &a; // 指向常量字符串类型的指针,可以改变指针的指向,但指针的指向存储单元的内容不可变。
*q = 45;//出错,不能修改指向的存储单元内容
q = &b; //没错,可以修改指针的指向
const int *const k = &a; // 不能修改指针的指向,同时也不能修改指向的存储单元的内容
//3、修饰函数的形参
void fun(const int value) // 通知程序员不能修改value
void fun1(const T* p) const // 常成员函数,不能修改p对象的数据成员
3、static静态变量
关键字static的使用方式分为两部分。
1)面向过程的static
主要的用法:静态全局变量、静态局部变量、静态函数
静态全局变量:
全局变量被static修饰后就变成了静态全局变量,它的生命周期为定义开始到源程序的结束,它实现了整个文件内共享,但不能跨文件使用,所以其他文件可以有相同的static修饰的变量名,不会起冲突。
静态局部变量:
它和静态局部变量不同的是,它的作用域仅在某个函数内,生命周期也是整个源程序运行时间,当源程序运行到这个函数该变量处才会进行初始化,若未初始化,则自动初始化为0,一旦初始化了,再次调用就不会初始化,可以直接运算操作。
静态函数:
面向过程的函数被static修饰后,和普通的函数不同的是,它只对当前文件可见。
2)面向对象的static
对于面向对象的static主要是在类中使用,分为类的静态成员变量和类的静态成员函数。
类的静态成员变量
它和面向过程的静态变量不同的点是,它是属于类的,而不是属于类的实例化对象,故使用它方法不需要对象名,直接类名::变量名使用。
类的静态成员函数
它也是属于类的,在类的实例化对象初始化前,已经分配好了存储空间,所以也没有类对象的this指针。从而实现了所有的这个类的实例化对象共享该静态成员函数,信息传递,直接类名::函数名使用。
注意:类的静态成员函数不能调用类的非静态成员变量和函数,因为静态成员函数是在类的实例化对象初始化之前已经分配好了内存,是属于类的,而非静态成员函数或变量是属于某个类实例化对象的。
问答:
被static修饰后的变量或函数不能在其他文件中调用?
是的。static有以文件为单位隐藏名字的功能,但static修饰的变量或函数定义在头文件中,其他文件包含了该头文件,是可以使用的,对于其他普通函数比较,其他文件每次调用普通函数都会拷贝一份,而static修饰函数在内存中只占一份。
4、inline内联函数
内联函数是C++为提高程序运行速度所做的一项改进。常规函数和内联函数之间的主要区别不再于编写方式,而在于C++编译器如何将它们组合到程序中。
内联函数的编译代码与其他程序代码”内联”起来,即编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,然后再跳回来。因此,内联函数的运行速度比常规函数稍快。但代价是需要占用更多内存。
要使用这项特性,必须采取下述措施:在函数声明前加上inline关键字。
5、mutable关键字
关键字mutable一般是修饰对类的非静态成员变量,如果类中的非静态成员函数被const修饰,表明这个函数不能修改该类对象的成员变量,但需要改变这个成员变量的话,就需要使用mutable关键字对它修饰。
class mutabletest{
public:
int getvalue() const
{
return ++value; //被const修饰函数后,本来这个变量不能被修改,但它被mutable修饰了就可以了
}
public:
mutable int value; // 如果不使用mutable修饰,则出错
};
6、volatile
关键字volatile是防止优化编译器使变量从内存中装入寄存器中,如果变量被volatile修饰后,每次读取该变量都会直接从内存中读取,而不是从寄存器。假设两个线程都读取该变量,若变量没有被volatile修饰,则有可能一个线程从内存中读取,一个从寄存器读取,造成了程序出现错误。
7、extern
关键字extern的作用是声明变量和函数为外部链接,即该变量或函数名在其它文件中可见。在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。extern是声明不是定义,即不分配存储空间。
注意:声明是可以多次的,而定义只能一次。
//etn.h
extern int a;// 声明,不是定义,如果不加入extern关键字,则为定义
extern void fun(); // 声明,不加extern也行。因为函数的定义是有{}
//etn.c
#include "etn.h"
//定义
int a = 10;
void fun()
{
a++;
}
//main.c
#include <stdio.h>
#include "etn.h"
int main()
{
fun();
a++;
return ;
}
问题:防止C语言头文件被重复包含
例子:
//est.h
int a = 10;
上述的est.h文件被多个.c文件包含,会引起变量a的多次重定义。
解决方案:
方法一:使用条件编译#ifndef #define #endif
//est.h
#ifndef _EST_H //如果这个_EST_H宏没有被定义,则往下走
#define _EST_H //定义这个宏
int a = 10;
#endif
上述的头文件被多个.c包含,会将头文件的全部内容替换该#include "est.h"这一行,但有了预处理的宏定义之后,避免了重复引入同一个文件。缺点就是多个头文件可能会引起宏名重复。
方法二:使用微软的#pragma once,不会造成宏名冲突问题
同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件,如果某个头文件有多份拷贝,这个方法就不能保证他们不被重复包含。
8、register
用register声明的变量称着寄存器变量,在可能的情况下会直接存放在机器的寄存器中;但对32位编译器不起作用,当globaloptimizations(全局优化)开的时候,它会做出选择是否放在自己的寄存器中;不过其它与register关键字有关的其它符号都对32位编译器有效。
9、auto
关键字auto是自动推导类型。
10、explicit
针对单参数的构造函数而言,修饰构造函数防止隐式转化,多于2个以上参数不会隐式转化。explicit关键字用于取消构造函数的隐式转换,对有多个参数的构造函数使用explicit是个语法错误。
class A
{
public:
explicit A(int a) {}
};
int main()
{
A a(2), b(5);
a = b;
a = 5; // 出错
return 0;
}
问题:
1、const与#define相比有什么不同?
对于C++语言来说,两者都可以定义常量。但是前者比后者有更多的优点:
(1)const常量有数据类型,而宏定义没有数据类型。编译器可以对前者进行类型安全检查,而对后者只是进行字符替换,没有类型安全检查,并且在字符替换中可能产意料不到的错误(边际效应)。
(2)有些集成化的调试工具可以对const调试,但不能对宏常量进行调试。
对于#define的这种缺点,除了上面的const外,还可以用inline。内联函数不仅具有实现宏函数的效率,还加上了类型检查和可预计的行为这些优点。比如:
#definemax(a,b) ((a) > (b) ? (a) : (b))
int a = 5, b= 0;
max(++a,b); //此时a值增加了2次
max(++a,b+10); //此时a值增加了1次
而换成inline,我们就可以避免这种不可预见的情况,甚至还可以使用C++里的另一个功能——模板函数。比如换成以下的方式:
inlineint max(int a, int b) { return a > b ? a : b;}
或者
template
inline constT& max(const T& a, const T& b)
{ return a> b ? a : b; }
六、智能指针
在C++的标准库中,智能指针有shared_ptr、unique_ptr、weak_ptr。
shared_ptr
内部采用了计数引用,用来记录该类的实例化对象的个数,它作为一个智能指针的一种,有着类似普通指针的行为,比如*、->、拷贝构造、赋值等运算符重载。当计数引用为0时,该类的对象指向的内存空间会自动释放掉。
注意:
拷贝构造:被拷贝的对象使得计数引用+1
赋值操作符:左操作符的对象的计数引用-1,当计数引用为0时释放内存空间;右操作符的对象的计数引用+1
#include <iostream>
#include <memory> //智能指针的头文件
int main() {
{
int a = 10;
std::shared_ptr<int> sptr = std::make_shared<int>(a);
std::cout << sptr.use_count() << std::endl;// 1
std::shared_ptr<int> ptr1 = sptr;
std::cout << ptr1.use_count() << std::endl;// 2
std::cout << sptr.use_count() << std::endl;// 2
int *p = ptr1.get();
std::cout<<*p<<std::endl; // 10
int b = 30;
std::shared_ptr<int> sptr2 = std::make_shared<int>(b);
ptr1 = sptr2;
std::cout << sptr2.use_count() << std::endl; // 2
std::cout << sptr.use_count() << std::endl; // 1
}
// {
// std::unique_ptr<int> uptr(new int(10));
// std::unique_ptr<int> mptr = std::move(uptr);
//
// mptr.release();
// }
}
unique_ptr
unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
#include <iostream>
#include <memory>
int main() {
class test{
public:
test(int nums = 0){
num = nums;
}
public:
int num;
};
{
//自定义的类
std::unique_ptr<test> uptr(new test(10));
std::unique_ptr<test> mptr = std::move(uptr);
auto a = mptr->num;
std::cout << a << std::endl;
//释放
//mptr.release();
//move 转移使用权
std::unique_ptr<test> tptr = std::move(mptr);
std::cout << tptr->num << std::endl;
tptr.release();
std::unique_ptr<int> ptr(new int(20));
std::cout << *ptr << std::endl;
int b = 65;
//重置
ptr.reset(&b);
std::cout << *ptr << std::endl;
//释放
ptr.release();
}
}
weak_ptr
weak_ptr是为了配合shared_ptr的使用而引入一种弱智能指针,因为它不具有普通指针的行为,比如*、->等的重载运算符,它的最大作用就是协助于shared_ptr工作,像旁观者一样观测资源的使用情况。它可以从shared_ptr或weak_ptr的对象构造,获取资源的观测权,但weak_prt没有共享资源,不会引起计数引用+1操作,使用use_count()可以观测资源的计数引用,还有使用expired()判断智能指针的存在,等价于use_count() == 0。另外还可以使用lock()获取到shared_ptr的使用权,操作资源,当use_count() ==0时,lock()获取到的shared_ptr的空智能指针。
#include <iostream>
#include <memory>
int main()
{
{
std::shared_ptr<int> ptr = std::make_shared<int>(88);
std::weak_ptr<int> wptr = ptr;
std::cout << wptr.use_count() << std::endl; // 1
if(wptr.expired())
{
std::cout << "ptr not exist" << std::endl;
}
std::cout << ptr.use_count() << std::endl; // 1
std::cout << *wptr.lock() << std::endl; // 88
std::shared_ptr<int> cpt = wptr.lock();
std::cout << cpt.use_count() << std::endl; // 2
std::cout << ptr.use_count() << std::endl; // 2
}
}
实现智能指针
#include <iostream>
#include <memory>
template<class T>
class smartPointer{
public:
smartPointer(T* _ptr = nullptr) : ptr(_ptr){
count = ptr == nullptr ? new int(0) : new int(1);
}
smartPointer(const smartPointer& smptr){
if(&smptr != this){
//std::cout << " smartPointer(const smartPointer& smptr) " << std::endl;
this->ptr = smptr.ptr;
this->count = smptr.count;
(*this->count)++;
}
}
smartPointer& operator=(const smartPointer& smptr){
if(smptr.ptr == this->ptr) return *this;
//std::cout << " operator=(const smartPointer& smptr) " << std::endl;
if(this->ptr)
{
(*this->count)--;
if(*this->count == 0)
{
delete this->ptr;
this->ptr = nullptr;
delete this->count;
this->count = nullptr;
}
}
else
{
//注意:当使用赋值操作符时,若this->ptr == nullptr,则需要释放this->count指针指向的内存
//因为初始化的时候动态分配了this->count
delete this->count;
this->count = nullptr;
}
this->ptr = smptr.ptr;
this->count = smptr.count;
(*this->count)++;
return *this;
}
T& operator*(){
return *this->ptr;
}
T* operator->(){
return this->ptr;
}
~smartPointer(){
if(this->ptr)
{
(*this->count)--;
if((*this->count) == 0)
{
delete this->count;
this->count = nullptr;
delete this->ptr;
this->ptr = nullptr;
}
}
else
{
std::cout << "this->ptr is empty " << std::endl;
delete this->count;
this->count = nullptr;
}
}
int use_count(){
return *this->count;
}
private:
T* ptr;
int* count;
};
int main()
{
smartPointer<int> smptr(new int(83));
std::cout << smptr.use_count() << std::endl; // 1
std::cout << *smptr << std::endl; // 83
//拷贝构造
smartPointer<int> tmptr = smptr;
std::cout << tmptr.use_count() << std::endl; // 2
std::cout << smptr.use_count() << std::endl; // 2
smartPointer<int> ptr(new int(55));
std::cout << ptr.use_count() << std::endl; // 1
//operator=
ptr = tmptr;
std::cout << ptr.use_count() << std::endl; // 3
smartPointer<int> q;
}
七、类型转换
类型 | 解释 |
---|---|
static_cast | 静态类型转换。 |
reinterpreter_cast | 重新解释类型转换。 |
dynamic_cast | 子类和父类之间的多态类型转换。 |
const_cast | 去掉const属性转换 |
static_cast
static_cast : 静态类型转换
static_cast<目标类型>(标识符)
所谓的静态,即在编译期内即可决定其类型的转换,用的也是最多的一种。
#include <iostream>
using namespace std;
int main()
{
double dou = 3.1452;
int a = (int)dou; //c语言的风格
cout << "a = " << a <<endl;
int b = static_cast<int>(dou); // c++风格
cout << "b = " << b <<endl;
}
reinterpret_cast
reinterpret_cast<目标类型>(标识符)
数据的二进制重新解释,但是不改变其值。
#include <iostream>
using namespace std;
class Animal{
public:
void type(){
cout << "type is animal" << endl;
}
};
class Book{
public:
void type(){
cout << "type is book" << endl;
}
};
int main()
{
Animal* ani = new Animal();
ani->type();
Book* book = reinterpret_cast<Book*>(ani);
book->type();
//book 和 ani 的地址一样,指向相同的内存空间
cout << book << endl;
cout << ani << endl;
delete book;
book = nullptr;
}
dynamic_cast
dynamic_cast : 用于父子类的之间类型转换
#include <iostream>
using namespace std;
class Animal{
public:
virtual void eat(){
}
};
class cat : public Animal{
public:
void eat(){
cout << "eat fish" <<endl;
}
};
class dog : public Animal{
public:
void eat(){
cout << "eat shi" <<endl;
}
};
int main()
{
Animal* ani = nullptr;
ani = new cat();
dog* d = dynamic_cast<dog*>(ani); // 类型转换失败,因为基类指针动态分配的时猫,失败返回为nullptr
if(d != nullptr)
{
d->eat();
}
else cout << "dog fail" <<endl;
cat* c = dynamic_cast<cat*>(ani); // 类型转换成功
if(c != nullptr)
{
c->eat();
}
else cout << "cat fail" <<endl;
delete c;
c = nullptr;
}
const_cast
const_cast : 去掉const的属性
const_cast<目标类型>(标识符):目标类型只能是指针或者引用
#include <iostream>
using namespace std;
int main()
{
const int a = 55;
cout << "a = " << a <<endl;
//int a_1 = const_cast<int>(a); //失败,只能时引用或指针类型。
int& a_2 = const_cast<int&>(a);
a_2 = 44;
cout << "a_2 = " << a_2 <<endl;
int* a_3 = const_cast<int*>(&a);
*a_3 = 33;
cout << "a_3 = " << *a_3 << endl;
}
八、C++11的新特性
auto和decltype
auto 自动推导类型
1、auto推导变量必须初始化
2、auto在一行中推导多个变量时,不能有二义性,否则推导失败
3、auto推导不了数组类型,可以推导指针
4、auto不能作为函数参数类型推导
5、在类中不能作为非静态类型推导
#include <iostream>
using namespace std;
/*
void fun(auto a) //error
{}
*/
class test{
int a = 10;
//auto w = 50; //error
//static auto w = 50; //error
static const auto s = 5;
};
int main()
{
auto i = 10;
auto &b = i;
const auto a = i;
int arr[3] = {1,2,3};
//auto ve = arr; //error
auto *v =arr;
return 0;
}
decltype 表达式推导
#include <iostream>
using namespace std;
int sum(int a , int b)
{
return a+b;
}
int main()
{
int a = 10;
decltype(a) b = 50;
decltype(a + b) c = 56;
decltype(sum(a,b)) d = 89;
cout << d;
return 0;
}
移动语义move
左值:可以取地址又有名字的东西为左值
右值:不可以取地址的东西为右值
左值一般包括:
1、变量名和函数名
2、返回左值引用的函数调用
3、前置自增自减表达式++i、–i
4、由赋值表达式或赋值运算符连接的表达式(a=b, a += b等)
5、解引用表达式*p
6、字符串字面值"abcd"
右值一般包括:
纯右值:运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值。
将亡值:
将亡值是指C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move函数的返回值、转换为T&&类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。
class A {
xxx;
};
A a;
auto c = std::move(a); // c是将亡值
auto d = static_cast<A&&>(a); // d是将亡值
移动语言解决浅拷贝问题:
class A {
public:
A(int size) : size_(size) {
data_ = new int[size];
}
A(){}
//正常的深拷贝,开辟新的内存
A(const A& a) {
size_ = a.size_;
data_ = new int[size_];
cout << "copy " << endl;
}
//移动拷贝构造函数
A(A&& a) {
this->data_ = a.data_;
a.data_ = nullptr;
cout << "move " << endl;
}
~A() {
if (data_ != nullptr) {
delete[] data_;
}
}
int *data_;
int size_;
};
int main() {
A a(10);
A b = a;
A c = std::move(a); // 调用移动构造函数
return 0;
}
返回值优化(RVO)
RVO和NRVO的区别:
RVO:
函数内多个return 的是同一个对象
或只有一个return
或非命名优化(匿名对象)。
NRVO:
函数内多个return 不是同一个对象,或return 对象既有匿名对象也有命名对象,则不进行返回值优化。
构造函数的列表初始化
通常来讲,编译器会执行默认的无参构造函数之前进行初始化类的成员变量,那么当一个类中有const变量和引用变量的话,那么必须采用列表初始化方式。
因为const和引用要求变量必须是初始化,并之后不能改变。若不采用列表初始化的话而使用赋值操作的话,就会编译器报错。
优点:只会调用一次构造函数。
Lambda表达式
lambda表达式可以说是c++11引用的最重要的特性之一,它定义了一个匿名函数,可以捕获一定范围的变量在函数内部使用,一般有如下语法形式:
auto func = [capture] (params) opt -> ret { func_body; };
其中func是可以当作lambda表达式的名字,作为一个函数使用,capture是捕获列表,params是参数表,opt是函数选项(mutable之类), ret是返回值类型,func_body是函数体。
一个完整的lambda表达式:
auto func1 = [](int a) -> int { return a + 1; };
auto func2 = [](int a) { return a + 2; };
cout << func1(1) << " " << func2(2) << endl;
如上代码,很多时候lambda表达式返回值是很明显的,c++11允许省略表达式的返回值定义。
lambda表达式允许捕获一定范围内的变量:
1、[] 不捕获任何变量
2、[&] 引用捕获,捕获外部作用域所有变量,在函数体内当作引用使用
3、[=] 值捕获,捕获外部作用域所有变量,在函数内内有个副本使用
4、[=, &a] 值捕获外部作用域所有变量,按引用捕获a变量
5、[a] 只值捕获a变量,不捕获其它变量
6、[this] 捕获当前类中的this指针
lambda表达式示例代码:
int a = 0;
auto f1 = [=](){ return a; }; // 值捕获a
cout << f1() << endl;
auto f2 = [=]() { return a++; }; // 修改按值捕获的外部变量,error
auto f3 = [=]() mutable { return a++; };
代码中的f2是编译不过的,因为我们修改了按值捕获的外部变量,其实lambda表达式就相当于是一个仿函数,仿函数是一个有operator()成员函数的类对象,这个operator()默认是const的,所以不能修改成员变量,而加了mutable,就是去掉const属性。
新特性的关键字
nullptr
nullptr是一个空指针的常量值,NULL其实是一个int的0,如果表示空指针,建议使用nulllptr而不使用NULL。
default
如果一个类的自定义了构造函数,那么编译器不会隐式产生默认的无参构造函数。但如果给构造函数加上“=default”,那么会显式自动生成函数体。
final和override
final表示一个类不能作为基类,进行派生下去和重载虚函数。override表示子类重写基类的虚函数,如果加上该关键字的函数,在基类中没有找到,那么编译器会报错,给程序员在编码时候增加了警惕性。
const
delete
explicit
专用于修饰单个参数的构造函数,表示只能显式构造,不可以被隐式转换。
explicit的作用:
#include <iostream>
using namespace std;
class test{
public:
test(int x)
{
cout << " int 构造函数 " << endl;
}
test(char* str)
{
cout << " char* 构造函数 " <<endl;
}
};
int main()
{
test t = 'c';
//结果输出: int 构造函数
//而我们需要的是 char* 构造函数 ,无法做到
return 0;
}
加上“explicit"
#include <iostream>
using namespace std;
class test{
public:
explicit test(int x)
{
cout << " int 构造函数 " << endl;
}
};
int main()
{
test t(50);
//test t1 = 80; error
return 0;
}
thread_local
c++11引入thread_local,用thread_local修饰的变量具有thread周期,每一个线程都拥有并只拥有一个该变量的独立实例,一般用于需要保证线程安全的函数中。类似于static变量
注意:不能作为一个类的成员变量,它属于线程的,拥有线程生命周期。
#include <iostream>
#include <thread>
class A {
public:
A() {}
~A() {}
void test(const std::string &name) {
thread_local int count = 0;
++count;
std::cout << name << ": " << count << std::endl;
}
};
void func(const std::string &name) {
A a1;
a1.test(name);
a1.test(name);
A a2;
a2.test(name);
a2.test(name);
}
int main() {
std::thread(func, "thread1").join();
std::thread(func, "thread2").join();
return 0;
}
输出:
thread1: 1
thread1: 2
thread1: 3
thread1: 4
thread2: 1
thread2: 2
thread2: 3
thread2: 4
返回值可能产生的临时对象
当返回的对象不是局部变量的话,那么return 会调用拷贝构造函数产生临时对象,然后在执行operator=重载运算符。
#include <iostream>
using namespace std;
class test{
public:
test() {
cout << "test"<<endl;
}
~test(){
cout << "~test" <<endl;
}
test(const test& t)
{
cout << "copy colt" << endl;
this->a = t.a;
}
void operator=(const test& t)
{
cout << "copy = "<<endl;
this->a = t.a;
}
public:
int a;
};
test get(test& t)
{
cout << "------------" << endl;
return t;
}
int main()
{
test w ,s;
cout << "------------"<< endl;
test d;
//test temp(w); d = w;
d = get(w); // 等同于test temp(w); d = w;
//d = test();
cout << "------------" << endl;
return 0;
}
执行test temp(w); d = w; 结果如下: