文章目录
- 一 auto
- 二 decltype
- 三 变量初始化
- 四 增强for循环
- 五 静态断言
- 六 noexcept
- 七 nullptr
- 八 强类型枚举
- 九 常量表达式
- 十 自定义字面量
- 十一 原生字符串字面值
- 十二 继承构造函数
- 十三 委托构造函数
- 十四 final
- 十五 override
- 十六 default
- 十七 delete
- 十八 模板实例化
- 十九 用using起别名
- 二十 支持函数模板的默认参数
- 二十一 左值引用和右值引用
- 二十二 返回值优化技术
- 二十三 移动构造函数
- 二十四 转移赋值函数
- 二十五 标准库函数 std::move
- 二十六 完美转发 std::forward
- 二十七 智能指针 unique_ptr
- 二十八 智能指针 shared_ptr
- 二十九 智能指针 weak_ptr
- 三十 闭包
- 三十一 lambda表达式
一 auto
用法
C++11之前:表示自动在栈区分配内存,可隐藏
// 二者等价
int a = 1;
auto int a = 1;
C++11之后:表示自动推导类型
auto a = 1;
注意点
- auto变量必须初始化,否则无法推导类型
- visual studio不支持函数参数是auto类型,clion也不支持函数参数为auto类型,qt creator支持
- 数组不能使用auto
- 模板类型不能是auto
二 decltype
用法
获取变量类型,定义变量
int main(){
int i;
// decltype(i)相当于int,可以直接用于变量定义
decltype(i) j = 0;
}
配合auto可以自动获取返回类型
auto func(int a, couble b) -> decltype(a + b){
return a + b;
}
配合模板,可以智能的获取返回类型
template<class T1, class T2>
auto mul(T1 &t1, T2 &t2) -> decltype(t1 * t2){
return t1 * t2;
}
int main(){
int a = 10;
double b = 11.1;
auto c = mul(a, b);
cou << "c = " << c << endl;
}
三 变量初始化
可以通过构造函数参数列表初始化类变量
class A{
public:
int a;
A(int i) : a(i){
}
}
可以通过{}进行类变量初始化
class B{
public:
int a = {1};
int b = 2;
A temp = {3};
string str = {"mike"};
}
可以通过{}列表初始化
int a = 1;
int b = {2};
int c{3};
int arr[] = {1, 2, 3};
int arr2[]{1, 2, 3};
在vs和clion中通过列表初始化可以避免类型收窄。类型收窄就是因为自动类型转换导致的精度丢失。Java是默认防止类型收窄的。qt可以不会防止类型收窄。
四 增强for循环
纯指针无法使用增强for循环,没有定义大小的数组也无法使用增强for循环。
int* arr;
/*
* 错误
for(auto i : arr){
}
*/
五 静态断言
在编译时就检查条件,静态断言使用的必须是常量,编译阶段就可以确定值
int main(){
bool flag = false;
// 非静态断言
assert(flag == true);
// 静态断言, 编译阶段就报错
static_assert(sizeof(flag) == 2, "断言不通过");
cout << "断言通过" << endl;
return 0;
}
六 noexcept
表示不能抛出任何异常
c++ 11 之前可以这么写
// 不能抛出任何异常
void func() throw(){
}
c++ 11 之后可以这么写
void func() noexcept{
}
七 nullptr
消除NULL带来的二义性
传参的时候NULL会被当做0来处理
int main() {
// 成功
int p = NULL;
// 失败
int q = nullptr;
return 0;
}
八 强类型枚举
int main(){
// 编译报错,main::ok重定义,以前的定义是枚举数
enum status { ok, error };
enum status2 {ok, error};
}
int main(){
enum class status { ok, error };
enum class status2 { ok, error };
// 必须要加作用域
status flag = status::ok;
cout << sizeof(flag) << endl;
// 可以指定里面数据的具体类型
enum class status3:char { ok, error };
enum class status4:long long { ok, error };
}
九 常量表达式
发生在编译阶段
constexpr int getNum(){
return 3;
}
int getNum2(){
return 3;
}
int main(){
// getNum()可以当做常量,在编译阶段就确定
enum {e1 = getNum(), e2};
// 错误,初始化类型必须是整型常量
enum {e1 = getNum2(), e2};
}
constexpr的局限性
int main(){
constexpr int func();
// 错误,在使用时必须有定义
int a = func();
}
constexpr int func(){
// constexpr int a = 1 可以作为return语句,与return 1相同
constexpr int a = 1;
}
int a = 1;
constexpr int func2(){
// 错误,不可以返回全局变量
return a;
}
constexpr int func3() {
// 错误,返回值必须是常量或常量表达式,不能是运行时数据
return rand();
}
类中成员函数的表达式
class Date{
public:
// 常量表达式构造函数的函数体必须是空的
constexpr Date(int year):year(year){
}
constexpr int getYear(){
return year;
}
private:
int year;
};
int main(){
constexpr Date d(3);
// 常量表达式可以配合静态断言
static_assert(d.getYear() == 3, "年不为2");
Date d2(4);
// 错误! d2是变量,无法使用静态断言
static_assert(d2.getYear() == 4, "年不为2");
cout << d.getYear() << endl;
}
十 自定义字面量
自定义字面量,名字要求 operate_“” xxx*(T1, size_t)
T1只能是如下七种类型之一;size_t是可选的,当T1是字符串类型时,size_t存在,但传参时无需赋值,编译器自动推导出T1的长度传给size_t。
char const * // 传参时,不是传的字符串常量
unsigned long long
long double
char const *, size_t
wchar_t const *, size_t
char16_t const *, size_t
char32_t const *, size_t
十一 原生字符串字面值
可以规避字符串中的一些转义字符,保留换行后的Tab
十二 继承构造函数
在子类中,使用using 父类::父类构造函数可以直接声明子类构造函数沿用父类的构造函数。
但是继承构造函数不能使用默认构造函数
继承构造函数只能初始化父类中的成员变量
一旦使用继承构造函数,编译器就不会提供默认构造函数
class A{
public:
A(int a, int b){
this->a = a;
this->b = b;
}
protected:
int a;
int b;
};
class B : public A{
public:
using A::A;
void display(){
cout << "a = " << a << "b = " << b << endl;
}
};
十三 委托构造函数
一个构造函数调用其他构造函数
class Test{
public:
Test():Test(1, 'a'){
}
Test(int x): Test(x, 'b'){
}
Test(int x, int b): a(x), b(y){
}
private:
int a;
int b;
}
十四 final
作用基本上和Java中的一致,只是写的位置不一样。final修饰函数时,只能修饰虚函数
class A final{
private:
int a;
}
// 报错!A类不可被继承
class B : public A{
}
class A{
public:
virtual void func() final{
cout << "这是虚函数" << endl;
}
}
class B{
public:
// 报错!final虚函数不可被重写
virtual void func(){
}
}
十五 override
用于显式的标记某个方法是重写父类的同名方法
在C++中,重写与规则与Java不同。如果子类重写了父类的某个方法,则会覆盖掉所用同名的重载方法。Java则不会覆盖。
public:
virtual void func(){
cout << "这是虚函数" << endl;
}
virtual void func(int a){
cout << "这是带参数的虚函数" << endl;
}
};
class B : public A{
public:
// 报错!final虚函数不可被重写
virtual void func(int a) override{
}
};
int main(){
B b;
// 报错!A中的无参func()已经被覆盖
b.func();
}
十六 default
default只能修饰类中默认提供的成员函数:无参构造、拷贝构造、赋值运算符重载、析构函数。表示使用系统提供的相关函数,效率比程序员自己提供要高。
class X{
public:
x() = default;
X(int i){
a - i;
}
int a;
}
十七 delete
显式的声明某个函数被禁用,无法调用。可以用于自定的函数。
class Person{
public:
Person(){}
Person (const Person & p){
this->name = p.name;
} // 拷贝构造
Person& operator=(const Person& p){
this->name = p.name;
return *this;
}
void* operator new(size_t) = delete; // 禁用new操作符,无法new对象
private:
int name;
};
禁用拷贝构造和等号赋值
class Person{
public:
Person(){}
Person (const Person & p) = delete; // 禁用拷贝构造
Person& operator=(const Person& p) = delete; // 禁用等号赋值
private:
int name;
};
十八 模板实例化
在使用嵌套模板的时候,会存在X<<Z>>的情况,在C++11之前,>>被强制解析为右移,因此在嵌套模板中,必须加空格,写成X<Y<Z> >,比较麻烦。在C++11之后,改进了这一点,不需要在加空格,编译器会自动解析是模板实例化还是右移操作符。
十九 用using起别名
C++11之前,通过typedef来给一个类型起别名
typedef int int_32;
在C++11之后,可以使用using来定义别名
using int_32 = int;
二十 支持函数模板的默认参数
类模板默认参数是在C++11之前就支持,但函数模板的默认参数在C++11之后才支持。
类模板默认参数必须从右到左定义。函数模板的默认参数则没有这个限制。
二十一 左值引用和右值引用
左值:有明确定义的变量,引用返回类型的函数等
右值:字面量,非引用返回类型的函数
右值引用:使用双&&
int main(){
int i = 1;
int& ref1 = i; // 正确
int& ref2 = 10; // 错误,左值引用无法引用右值
int&& ref3 = 10; // 正确
int&& ref4 = i; // 错误,右值引用无法引用左值
}
void func(int&& i){
cout << i << endl;
}
void func(int& i){
cout << i << endl;
}
int main(){
func(1); // 输出1
int a = 2;
func(a); // 输出2
}
二十二 返回值优化技术
在vs、qt creator和clion中,会对值返回类型的函数做不同程度的优化。以如下代码举例
class MyString {
public:
MyString(char* tmp) {
len = (int)strlen(tmp);
str = new char[len + 1];
strcpy(str, tmp);
cout << "调用普通构造 " << "str = " << str << endl;
}
MyString(const MyString& tmp) {
len = tmp.len;
str = new char[len + 1];
strcpy(str, tmp.str);
cout << "调用拷贝构造 " << "str = " << str << endl;
}
MyString& operator=(const MyString& tmp) {
if (&tmp == this) {
return *this;
}
len = 0;
delete[] str;
len = tmp.len;
str = new char[len + 1];
strcpy(str, tmp.str);
cout << "调用赋值运算符" << endl;
return *this;
}
~MyString() {
if (str != NULL) {
cout << "调用析构函数 " << "str = " << str << endl;
delete[] str;
str = NULL;
len = 0;
}
}
private:
int len;
char* str;
};
MyString getString() {
char ch[] = "abcd";
char* str = ch;
MyString myString(str);
return myString;
}
int main() {
MyString temp = getString();
return 0;
}
在vs 2013中,如果返回的是值,会生成临时对象返回,再将这个临时对象过度
给接收的变量,此时过度不会调用拷贝构造,因为vs 2013进行了优化。
在vs 2022、qt creator和clion中,省略临时变量,将返回对象赋值给接收的变量。
如果使用-std=c++11 -fno-elide-constructors
禁用返回值优化技术,就会多出许多两次拷贝构造函数和析构函数。
c++11新特性对于临时变量的拷贝,给出了新的优化–使用移动构造函数
二十三 移动构造函数
移动构造函数是针对拷贝构造函数进行的优化。使用右值引用来定义
class MyString{
public:
...
MyString(MyString && tmp){
str = tmp.str;
len = tmp.len;
t.str = NULL; //非常重要!否则会发生重复释放内存问题导致程序崩溃。
}
}
二十四 转移赋值函数
是针对赋值运算符重载函数的优化。同样是使用右值引用来进行定义的。
二十五 标准库函数 std::move
作用:将一个左值转化为右值
左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体;
右值当在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。
一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。
左值:指表达式结束后依然存在的持久对象,可以取地址,具名变量或对象 。
右值:表达式结束后就不再存在的临时对象,不可以取地址,没有名字。
std::move是为了性能而生。通过它可以尽可能的释放不再需要使用的左值,仅使用右值。
二十六 完美转发 std::forward
完美转发适用于这样的场景:需要将一组参数原封不动的传递给另一个函数
原封不动不仅仅是参数的值不变,在C++中,除了参数值之外,还有以下两组属性:左值/右值和const/non-const。完美转发就是在参数传递的过程中,这些属性和参数值都不发生改变,同时,不产生额外的开销,就好像转发者不存在一样。在泛型编程中,这样的需求非常普遍。
以下面的代码为例:
template <class T> void func(const T& t){
}
template <class T> void func(T& t){
}
template <class T> void forward_val(const T& tmp){
func(tmp);
}
template <class T> void forward_val(T& tmp){
func(tmp);
}
对于forward_val函数来说,有多少种不同的参数就要重载不同的次数。而作为中间层的forward_val函数来说,重载这么多次是没有实际效果的,因为最终都是调用的func的方法。因此能不能只进行一次forward_val函数的定义?可以通过完美转发来实现。
C++11是如何解决完美转发的问题的?实际上,C++11是通过引入一条所谓“折叠引用
”(reference collapsing)的新语言规则,并结合新的模板推导规则来完美转发。
折叠引用:将复杂的未知表达式折叠为已知的简单表达式。
例如
typedef const int T;
typedef T& TR; // 实际上 TR 就是 const int &
TR & v = 1; // ??? v 不成了 const int && 的类型?事实上C++编译器会进行折叠引用
C++11中的引用折叠规则:
TR类型 | 声明的变量v的类型 | v的实际类型 |
---|---|---|
T& | TR | T& |
T& | TR& | T& |
T& | TR&& | T& |
T&& | TR | T&& |
T&& | TR& | T& |
T&& | TR&& | T&& |
总结:只要出现左值引用(即只有一个&),那折叠的结果就是左值引用,出现右值引用(即有两个&&)而没有左值引用,则结果为右值引用。
配合std::forward,就可以将参数的实际类型传递给目标函数。事实上,折叠引用更像是一个mask。
template <class T>
void forward_val(T&& tmp){
// 保持tmp的左值、右值、const、non-const属性
func(std::forward<>(tmp));
}
二十七 智能指针 unique_ptr
在指针离开其作用域时,可以自动释放指针所指向的空间。程序员只需要关心申请堆内存,而不需要关心是否要释放堆内存。
unique_ptr内存禁用了拷贝构造函数。因为是unique的。
unique_ptr<int> up1(new int(11));
// 错误,禁止使用拷贝构造函数
unique_ptr<int> up2 = up1;
但是可以使用std::move()
unique_ptr<int> up1(new int(11));
// 可以,把up1的使用权转移给up2,up1将不能再操作堆空间
unique_ptr<int> up2 = std::move(up1);
使用reset()函数可以显式释放堆区内容
使用带参的reset(unique_ptr),会先释放原来堆区内容,重新给up1绑定一个新的堆区内容
int main(){
unique_ptr<int> up1(new int(111));
// 释放控制权,但不释放堆内存空间,让给指针p
int* p = up1.release();
unique_ptr<int> up2(new int(111));
// 释放指针,释放堆内存
up2.reset();
}
二十八 智能指针 shared_ptr
多个指针指向同一个堆内存对象。每个shared_ptr都会用一个计数器字段use_count记录当前指向的对象总共有多少个shared_ptr。每次reset一个shared_ptr都会将计数器减1。若计数器为1,再释放当前shared_ptr,此时计数器将变为0,就会释放堆内存。
二十九 智能指针 weak_ptr
weak_ptr不直接绑定堆内存,而是间接的通过shared_ptr指向堆内存。直接用shared_ptr赋值给weak_ptr不会增加use_count()的大小。只有通过weak_ptr的lock()方法获取shared_ptr之后,use_count才会增加。
三十 闭包
什么是闭包?一种说法是有状态的函数。什么叫有状态的函数?就是这个函数有了自己的局部变量,在运行的时候,这个函数会发生值的改变,即状态发生了改变。
闭包的实现
- 仿函数(函数对象):不是C++11新特性
- std::bind
- lambda
std::bind绑定器:类似于函数指针,可间接调用某个函数
// std::bind(func, 值或占位符)(占位符值)
void func(int x, int y){
cout << x << " " << y << endl;
}
int main(){
bind(func, 11, 22);
// 使用占位符
bind(func, std::placeholders::_1, std::placeholders::_2)(11, 22, 33);
// 使用命名空间 std::placeholders
using namespace std::placeholders;
// 报错!占位符绑定第二个参数,而参数只有1个
bind(func, 11, _2)(11);
bind(func, 11, _2)(11, 22);
}
三十一 lambda表达式
lambda表达式是一个匿名函数,本质上是一个函数
语法:[](){}
[]:外部变量捕获列表,捕获的变量可以在lambda内部使用
():方法参数列表,没有参数的话可以省略
{}:方法体
int main(){
int a = 1;
int b = 2;
int c = 3;
auto f1 = [](){};
auto f2 = [a, b]{cout << a << " " << b << endl;};
auto f3 = [a, b](int x, int y){
cout << a << " " << b << endl;
cout << "x = " << x << ", b = " << b << endl;
};
// 调用f3
f3(10, 20);
auto f4 = [=]{
cout << a << " " << b << " " << c << endl;
};
f4();
// 捕获全部外部变量,以引用传递的方式
auto f5 = [&]{
cout << a << " " << b << " " << c << endl;
};
// 捕获全部外部变量,a以值方式捕获,其余以引用方式捕获
auto f6 = [&, a]{
cout << a << " " << b << " " << c << endl;
};
auto f7 = [&, a]{
// 报错!无法改变外部变量的值
a++;
};
// 默认lambda表达式是const修饰的
// 若想修改外部变量的值,要添加mutable关键字,且()不能省略
auto f7 = [&, a]() mutable{
a++;
};
}
int i = 0;
class Demo{
public:
void func(){
auto f1 = [](){
// 默认可以捕获全局变量
cout << i << endl;
// 报错,没有捕获变量a
cout << a << endl;
};
// 捕获this即可捕获成员变量
auto f2 = [this](){
cout << i << endl;
cout << a << endl;
};
}
private:
int a = 1;
};
值传递和引用传递的区别
int main(){
int a = 1;
auto f1 = [=]() mutable{
a = 10;
// 输出10
cout << "a = " << a << endl;
};
f1();
// 依然输出1
cout << a << endl;
a = 2;
// 依然输出10
f1();
auto f2 = [&]() mutable{
a = 10;
};
f2();
// 输出10
cout << a << endl;
}
- 值传递:lambda内部的变量a和外部的变量a不是同一个变量,内部a由外部a拷贝赋值
- 引用传递:内外部a是同一个变量
事实上,lambda表达式的底层实现就是仿函数