C++ 学习

1. 导入导出函数

在项目中看到一个这样的用法,

class BASE_EXPORT TimeDelta {};

甚是好奇,继续深挖下去,看到了BASE_EXPORT的定义:

BASE_EXPORT int64_t SaturatedSub(TimeDelta delta, int64_t value);

依然不懂继续找,结果没找到,搜索发现这个好像是c++导入导出库函数的宏定义

#define BASE_EXPORT __declspec(dllexport)
#else
#define BASE_EXPORT __declspec(dllimport)
#endif  // defined(BASE_IMPLEMENTATION)
#else
#define BASE_EXPORT __attribute__((visibility("default")))
#endif  // defined(WIN32)
#else
#define BASE_EXPORT
#endif  // defined(COMPONENT_BUILD)

本来导入导出函数的定义

//C++导出方式
extern "C" __declspec (dllexport)  int Dll_Add(int nLeft, int nRight)
{
	return (nLeft + nRight);
}
 
 
//C的导出方式
extern "C"__declspec (dllexport)  int Dll_Sub(int nLeft, int nRight)
{
	return (nLeft - nRight);
}
 
//DEF导出方式
 
int Dll_Mul(int nLeft, int nRight)
{
	return (nLeft * nRight);
}

2. unique智能指针 c++11

unique_ptr 不共享它的指针。它无法复制到其他 unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL) 算法。只能移动unique_ptr。这意味着,内存资源所有权将转移到另一 unique_ptr,并且原始 unique_ptr 不再拥有此资源。我们建议你将对象限制为由一个所有者所有,因为多个所有权会使程序逻辑变得复杂。因此,当需要智能指针用于纯 C++ 对象时,可使用 unique_ptr,而当构造 unique_ptr 时,可使用[make_unique](https://msdn.microsoft.com/zh-cn/library/dn439780.aspx) Helper 函数。
std::unique_ptr实现了独享所有权的语义。一个非空的std::unique_ptr总是拥有它所指向的资源。转移一个std::unique_ptr将会把所有权也从源指针转移给目标指针(源指针被置空)。拷贝一个std::unique_ptr将不被允许,因为如果你拷贝一个std::unique_ptr,那么拷贝结束后,这两个std::unique_ptr都会指向相同的资源,它们都认为自己拥有这块资源(所以都会企图释放)。因此std::unique_ptr是一个仅能移动(move_only)的类型。当指针析构时,它所拥有的资源也被销毁。默认情况下,资源的析构是伴随着调用std::unique_ptr内部的原始指针的delete操作的。
**有几个特点:**

1. 创建unique
需要将一个new操作符返回的指针传递给unique_ptr的构造函数。
unique_ptr<int> pInt(new int(5));
unique_ptr<T> make_unique(size_t size);

2. 无法进行复制构造和赋值操作
unique_ptr没有copy构造函数,不支持普通的拷贝和赋值操作。
int main()
{
// 创建一个unique_ptr实例
unique_ptr pInt(new int(5));
unique_ptr pInt2(pInt); // 报错
unique_ptr pInt3 = pInt; // 报错
}

3. 可以进行移动构造和移动赋值操作
unique_ptr虽然没有支持普通的拷贝和赋值操作,但却提供了一种移动机制来将指针的所有权从一个unique_ptr转移给另一个unique_ptr。如果需要转移所有权,可以使用std::move()函数。
实例:
int main()
{
unique_ptr pInt(new int(5));
unique_ptr pInt2 = std::move(pInt); // 转移所有权
//cout << *pInt << endl; // 出错,pInt为空
cout << *pInt2 << endl;
unique_ptr pInt3(std::move(pInt2));
}

4. 可以返回unique_ptr
unique_ptr不支持拷贝操作,但却有一个例外:可以从函数中返回一个unique_ptr。
实例:
unique_ptr clone(int p)
{
unique_ptr pInt(new int§);
return pInt; // 返回unique_ptr
}
int main() {
int p = 5;
unique_ptr ret = clone§;
cout << *ret << endl;
}

3. C++ mutable关键字

类中的mutable
mutable从字面意思来说就是可变的之意,C++有一个特别有意思的约定——凡事都没有绝对(来自某老师),所以当常量用mutable修饰的时候,某些程度上,可以修改其值。他将const分成了两个概念:

二进制层面的 const,也就是「绝对的」常量,在任何情况下都不可修改(除非用 const_cast)。
引入 mutable 之后,C++ 可以有逻辑层面的 const,也就是对一个常量实例来说,从外部观察,它是常量而不可修改;但是内部可以有非常量的状态。
mutable 只能用来修饰类的数据成员;而被 mutable 修饰的数据成员,可以在 const 成员函数中修改。

Lambda 表达式中的 mutable
C++11 引入了 Lambda 表达式,程序员可以凭此创建匿名函数。在 Lambda 表达式的设计中,捕获变量有几种方式;其中按值捕获(Caputre by Value)的方式不允许程序员在 Lambda 函数的函数体中修改捕获的变量。而以 mutable 修饰 Lambda 函数,则可以打破这种限制。

int x{0};
auto f1 = [=]() mutable {x = 42;};  // okay, 创建了一个函数类型的实例
auto f2 = [=]()         {x = 42;};  // error, 不允许修改按值捕获的外部变量的值

需要注意的是,上述 f1 的函数体中,虽然我们给 x 做了赋值操作,但是这一操作仅只在函数内部生效——即,实际是给拷贝至函数内部的 x 进行赋值——而外部的 x 的值依旧是 0。

4.C++ explicit的作用

在C++中,explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。
explicit使用注意事项:

  • explicit 关键字只能用于类内部的构造函数声明上。
  • explicit 关键字作用于单个参数的构造函数。
  • 在C++中,explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换
    实例分析:
class Circle  
{  
   public:  
       Circle(double r) : R(r) {}  
       Circle(int x, int y = 0) : X(x), Y(y) {}  
       Circle(const Circle& c) : R(c.R), X(c.X), Y(c.Y) {}  
   private:  
       double R;  
       int    X;  
       int    Y;  
};

 
//发生隐式类型转换  
//编译器会将它变成如下代码  
//tmp = Circle(1.23)  
//Circle A(tmp);  
//tmp.~Circle(); 
Circle A = 1.23;

//注意是int型的,调用的是Circle(int x, int y = 0)  
//它虽然有2个参数,但后一个有默认值,任然能发生隐式转换  
Circle B = 123;  
//这个算隐式调用了拷贝构造函数  
Circle C = A; 

加上explicit关键字之后,可以防止隐式类型转换

class Circle  
{  
   public:  
       explicit Circle(double r) : R(r) {}  
       explicit Circle(int x, int y = 0) : X(x), Y(y) {}  
       explicit Circle(const Circle& c) : R(c.R), X(c.X), Y(c.Y) {}  
   private:  
       double R;  
       int    X;  
       int    Y;  
};

//以下三句会报错
//Circle A = 1.23;   
//Circle B = 123;  
//Circle C = A;
 
//只能用显示的方式调用了  
//未给拷贝构造函数加explicit之前可以这样  
Circle A = Circle(1.23);  
Circle B = Circle(123);  
Circle C = A;  
 
//给拷贝构造函数加了explicit后只能这样了  
Circle A(1.23);  
Circle B(123);  
Circle C(A);

5. 闭包的理解

一种说法是,闭包是带有上下文的函数。说白了,就是有状态的函数。
一个函数, 带上了一个状态, 就变成了闭包了。什么叫 “带上状态” 呢? 意思是这个闭包有属于自己的变量,这些个变量的值是创建闭包的时候设置的,并在调用闭包的时候,可以访问这些变量。

函数是代码,状态是一组变量,将代码和一组变量捆绑,就形成了闭包,内部包含 static 变量的函数不是闭包,因为这个 static 变量不能捆绑。闭包的状态捆绑,必须发生在运行时。
实现详见闭包理解

6. C++ bind函数

std::bind函数是用来绑定函数调用的某些参数的。std::bind它可以预先把指定可调用实体的某些参数绑定到已有的变量,产生一个新的可调用实体。它绑定的参数的个数不受限制,绑定的具体哪些参数也不受限制,由用户指定。

std::bind:(1)、将函数、成员函数和闭包转成function函数对象;(2)、将多元(n>1)函数转成一元函数或者(n-1)元函数。

7. lambda表达式

声明Lambda表达式
Lambda表达式完整的声明格式如下:

[capture list] (params list) mutable exception-> return type { function body }

各项具体含义如下
capture list:捕获外部变量列表
params list:形参列表
mutable指示符:用来说用是否可以修改捕获的变量
exception:异常设定
return type:返回类型
function body:函数体

举例

auto f = [函数对象参数](函数参数列表) mutable throw(类型)->返回值类型 {函数语句};

Lambda表达式的定义结构分为六个部分:

函数对象参数部分:Lambda表达式的引入部分,用于初始化Lambda表达式,其中[]中可以填入一些标识来指示Lambda表达式如何捕获可以访问的变量。Lambda表达式可以捕获的变量只限于在Lambda表达式定义位置之前出现的变量。标识的放置有以下几种方式:

  • 空白表示不使用任何参数,仅可使用形参列表中的参数。
  • =(等号),表示使用的变量以值传递的方式捕获,可以直接使用this指针,不再需要显式列举this。
  • &(与号),表示使用的变量以引用传递的方式捕获,可以直接使用this指针,不在需要显示列举this。
  • this,表示Lambda表达式可以使用所在类中的成员变量。
    变量名,表示在Lambda表达式中,该变量使用以值传递的方式捕获。
  • &变量名,表示在Lambda表达式中,该变量使用以引用传递的方式捕获。
  • 组合方式,以逗号分隔各个捕获标识,特殊标识的变量按照特殊方式捕获,其余按照默认的标识进行捕获,例如[=, &a, &b]表示a与b两个变量按引用捕获,其余变量按值捕获。

函数参数列表与常规函数定义中的形参列表相同。
mutable关键字,表示可以修改按值传入的变量的副本(不是值本身),类似于不带const关键字的形参。使用mutable关键字后对按值传入的变量进行的修改,不会将改变传递到Lambda表达式之外。
throw(类型)表达式,表示Lambda表达式可以抛出指定类型的异常。
->返回值类型,指示Lambda表达式定义的匿名函数的返回值类型。
函数语句,跟常规函数的函数语句相同,如果指定了函数的返回值类型,函数实现语句中一定需要return来返回相应的类型的值。

捕获局部变量
类似参数传递方式(值传递、引入传递、指针传递),在Lambda表达式中,外部变量的捕获方式也有值捕获、引用捕获、隐式捕获。
1、值捕获
值捕获和参数传递中的值传递类似,被捕获的变量的值在Lambda表达式创建时通过值拷贝的方式传入,因此随后对该变量的修改不会影响影响Lambda表达式中的值。
示例如下:

int main()
{
    int a = 123;
    auto f = [a] { cout << a << endl; }; 
    a = 321;
    f(); // 输出:123
}

这里需要注意的是,如果以传值方式捕获外部变量,则在Lambda表达式函数体中不能修改该外部变量的值。

2、引用捕获
使用引用捕获一个外部变量,只需要在捕获列表变量前面加上一个引用说明符&。如下:

int main()
{
    int a = 123;
    auto f = [&a] { cout << a << endl; }; 
    a = 321;
    f(); // 输出:321
}

从示例中可以看出,引用捕获的变量使用的实际上就是该引用所绑定的对象。

3、隐式捕获

上面的值捕获和引用捕获都需要我们在捕获列表中显示列出Lambda表达式中使用的外部变量。除此之外,我们还可以让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获。隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。

//隐式值捕获示例:
int main()
{
    int a = 123;
    auto f = [=] { cout << a << endl; };    // 值捕获
    f(); // 输出:123
}

//隐式引用捕获示例:
int main()
{
    int a = 123;
    auto f = [&] { cout << a << endl; };    // 引用捕获
    a = 321;
    f(); // 输出:321
}

4、混合方式
Lambda表达式还支持混合的方式捕获外部变量,这种方式主要是以上几种捕获方式的组合使用。
C++11中的Lambda表达式捕获外部变量主要有以下形式:

捕获形式说明
[]不捕获任何外部变量
[变量名, …]默认以值得形式捕获指定的多个外部变量(用逗号分隔),如果引用捕获,需要显示声明(使用&说明符)
[this]以值的形式捕获this指针
[=]以值的形式捕获所有外部变量
[&]以引用形式捕获所有外部变量
[=, &x]变量x以引用形式捕获,其余变量以传值形式捕获
[&, x]变量x以值的形式捕获,其余变量以引用形式捕获

修改捕获变量
前面我们提到过,在Lambda表达式中,如果以传值方式捕获外部变量,则函数体中不能修改该外部变量,否则会引发编译错误。那么有没有办法可以修改值捕获的外部变量呢?这是就需要使用mutable关键字,该关键字用以说明表达式体内的代码可以修改值捕获的变量,示例:

int main()
{
    int a = 123;
    auto f = [a]()mutable { cout << ++a; }; // 不会报错
    cout << a << endl; // 输出:123
    f(); // 输出:124
}

Lambda表达式的参数
Lambda表达式的参数和普通函数的参数类似,那么这里为什么还要拿出来说一下呢?原因是在Lambda表达式中传递参数还有一些限制,主要有以下几点:

  • 参数列表中不能有默认参数
  • 不支持可变参数
  • 所有参数必须有参数名

8. 序列化执行&promise序列化执行

序列化执行就是使用单线程的方式,确保任务一个个执行,设计原则是利用c++对象可以自动析构,封装一个task,然后放入task_queue中,然后模拟其顺序执行。每次析构当前这个任务对象的时候就会自动执行下一个Task。
代码主要体现这个思想,大致是这个意思。

class Task{
	Task(){...};

	~Task(){
	...
	postTask();  //post之后executeNextTask
	}
}

class TaskManager{

	pushTask();

	queue<> task_queue;
}

int main()
{
	TaskManager tm;
	tm.pushTask(new Task());
}

9. string的一段巧妙用法

	int a=10;
	
    string s((const char *)&a, sizeof(int));
    //cout<<&a<<endl;
    cout<<s<<endl;

    cout<<s.size()<<endl;   //输出4

    printf("%s", s.c_str());

    int aa=*(int*)s.c_str();

    cout<<aa<<endl;   //输出10

4
10

这段代码只输出两个值,第一个4代表int *类型地址的大小,第二个10代表变量a原本的值。
他的转化过程是这样的:

  1. string 类型变量通过string s((const char *)&a, sizeof(int));这个操作存储的是变量a 的地址;
  2. 然后可以通过int aa=*(int*)s.c_str();,先将string类型转化成char * ,然后转化成int * ,然后解引用拿到原来变量a的值
    这真是一段有趣的代码。。。

10. C++ std::function<> 类模板

std::function 简介
类模板std :: function是一个通用的多态函数包装器。 std :: function的实例可以存储,复制和调用任何可调用的目标 :包括函数,lambda表达式,绑定表达式或其他函数对象,以及指向成员函数和指向数据成员的指针。当std::function对象未包裹任何实际的可调用元素,调用该std::function对象将抛出std::bad_function_call异常。
详细介绍,点此。
实例
例如:function<int(int,int)> func;
则 function类的实例func可以指向 返回值为int型,有两个形参都为int型的函数。

#include <functional>
#include <iostream>
int f(int a, int b)
{
  return a+b;
}
int main()
{
	std::function<int(int, int)>func = f;
	cout<<f(1, 2)<<endl;      // 3
	system("pause");
	return 0;

从上面我们可以看出,使用C++11的function类调用函数方便多了。
虽然是function是类模板,但其只有成员函数,无数据成员。

function的成员函数

成员函数声明说明
constructor构造函数:constructs a new std::function instance
destructor析构函数: destroys a std::function instance
operator=给定义的function对象赋值
operator bool检查定义的function对象是否包含一个有效的对象
operator()调用一个对象

11.#define和#pragma once

#define 的方式依赖于宏名不能冲突。所以不仅可以保证同一个文件不被包含多次,也可以保证内容完全相同的两个文件不会被重复包含。命名中要求包含全路径的原因是避免不同头文件因宏名冲突而导致问题难定位。非全路径方式发生宏名冲突时会导致头文件存在,编译器却报找不到声明的错误,这种问题难以跟进和解决,相对而言宏名冲突更容易定位和排查。

#pragma once 已被编译器普遍支持,其是由编译器提供保证,同一个文件不会被编译多次。此处所说的“同一个文件”是指绝对路径相同的文件,当同一份文件放置在多个绝对路径下时,会提示宏名冲突。

12. 内联函数 inline

  • inline只是对编译器的建议,内联函数不一定会被展开,最终由编译器决定;
  • 内联函数和宏的区别是:

宏是在预处理阶段,内联函数是在编译阶段
宏没有类型检查,内联函数有类型检查
内联函数不一定会被展开,宏一定,两者都会让代码体积变大
内联函数再返回行号的时候是定义的地方,不是调用的地方,宏是被插入的地方

  • 内联函数要很短1-5行,内联函数不能包括for循环,switch语句,递归调用语句
    详细见此

13. 尾递归

作者:贾贾贾
链接:https://www.zhihu.com/question/20761771/answer/1094636184
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

简单来讲,尾递归是指在一个方法内部,递归调用后直接return,没有任何多余的指令了。比如,一个递归实现的累加函数

public static int acc(int n){
        if(n == 1){
            return 1;
        }
        return n + acc(n - 1);
    }

请问这个是尾递归么?答案是否定的。可能有的人会说,明明最后一个步骤就是调用acc,为啥不是尾递归?实际上,你看到的最后一个步骤不代表从指令层面来讲的最后一步。这个方法的return先拿到acc(n-1)的值,然后再将n与其相加,所以求acc(n-1)并不是最后一步,因为最后还有一个add操作。把上面的代码做个等价逻辑转换就很清晰了。

public static int acc(int n){
       if(n == 1){
           return 1;
       }
       int r = acc(n - 1);
       return n + r;
   }

看,是不是还隐含一个add操作?累加的尾递归写法是下面这样子的:

public static int accTail(int n, int sum){
        if(n == 1){
            return sum + n;
        }
        return accTail(n - 1,sum + n);
    }

递归调用后就直接返回了,这是真正的尾递归。

接着讲一下为啥尾递归容易优化。递归调用的缺点是方法嵌套比较多,每个内部的递归都需要对应的生成一个独立的栈帧,然后将栈帧都入栈后再调用返回,这样相当于浪费了很多的栈空间.比如acc(3),执行过程如下图:递归图
如上图可见,这个递归操作要同时三个栈帧存在于栈上才能收尾。尾递归要避免的是,嵌套调用的展开导致的多个栈帧并存的情况。那为啥尾递归就能避免这种情况呢。acc这种递归相当于外层调用依赖内层调用的结果,然后再做进一步的操作,最终由最外层的方法收口操作,返回最终结果。但尾递归由于将外层方法的结果传递给了内层方法,那外层方法实际上没有任何利用价值了,直接从栈里踢出去就行了,所以可以保证同时只有一个栈帧在栈里存活,节省了大量栈空间。
整个逻辑上来讲是这样的:

main方法将accTail(3,0)入栈accTail(3,0)执行n–,sum+=3,将两个结果传递给下个accTail,即准备执行accTail(2,3)。这时accTail(3,0)生命周期结束,出栈。以此类推,acc(2,3)入栈出栈,acc(1,5)入栈出栈,最后得到最终结果6。

通过上面的逻辑分析,方法内部只是执行一系列操作,将操作结果传递给下一个递归调用,所以完全可以将调用递归方法之前的逻辑单独抽出来一个没有递归调用的方法,至于递归的逻辑改为由while循环控制调用流程,啥时候结束、啥时候进行下一次调用。因为刚才讲了,尾递归下次操作之前能够将上次栈帧释放,可以保证只有一个相关的栈帧在栈里,因此改用循环控制足够了用循环具体代码可以自己试一下。

14. 回调

回调分为同步回调和异步回调;
同步回调:就是在另一个函数中调用,以参数形式传进去的回调函数;
异步回调:也是以参数形式传进去的函数,但这个函数什么时候触发执行,有一定的条件;比如UI界面的事件回调,就是将待执行的事件函数的地址保存下来,当事件触发的时候,事件被加入到一个event_queue,loop会一直从这个队列里面取出事件处理,然后对应的调用其事件的函数,因为这个函数是上层传入的,所以叫回调函数(回过去掉了上层传来的函数)。

15 Initializer list

initializer_list
initializer_list 其实只是指向一个c++ array第一个元素的对象
initializer_list 复制的时候只是浅copy

16 Array

TR1版本c++ array实现
array其实只是一个对c数组的封装,使其支持c++ STL

后期版本的c++ array实现,变复杂了

17 引用Reference

在这里插入图片描述

p指向一个指针,r代表int刑变量x,(r不是一个指针,但是编译器实现的时候都是通过指针实现的)
r占用内存和x一样,r和x的地址也一样(都是编译器造成的假象)

18 程序二异性 Ambiguity

程序二异性是不被允许的,比如下面的两个例子

宏的高级用法

参考:
C++ 11 创建和使用 unique_ptr
C++ 中的 mutable 关键字
C++ explicit的作用
C++闭包的理解
C++11中bind的使用
C++ 11 Lambda表达式
关于lamda 表达式以及mutable 参数
C++11 function类模板
C++内联函数机制全面解析
#define宏的高级用法
尾递归
回调函数的原理
回调函数是什么

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值