C++11相关知识点

auto、decltype自动类型推导

auto:让编译器在编译期就推导出变量的类型。

auto 变量名 = 变量值;

int i = 0;
auto *a = &i; // a是int*
auto &b = i; // b是int&
auto c = b; // c是int,忽略了引用
 
const auto d = i; // d是const int
auto e = d; // e是int
 
const auto& f = e; // f是const int&
auto &g = f; // g是const int&

在不声明为引用或指针时,auto会忽略等号右边的引用类型和const、volatile关键字。

在声明为引用或者指针时,auto会保留等号右边的引用类型和const、volatile关键字。

不允许使用auto的场景:

1.不能作为函数参数使用

int func(auto a,auto b)//error
{
	cout << a << "" << endl;
}

2. 不能用于类的非静态成员初始化

class Test
{
	auto a1 = 0;//error
	static auto a2 = 0;//error 类的静态非常量成员不允许在类内直接初始化
	static int a;
	static const auto a3 = 0;//ok 
};
auto Test::a = 1;//error

3.不能使用auto关键字定义数组

int func()
{
	int array[] = { 1,2,3,4,5 };
	auto t1 = array;//ok t1被推导为int*类型
	auto t2[] = array;//error auto无法定义数组
	auto t3[] = { 1,2,3,4,5 };//error auto无法定义数组
}

4.无法用auto推出模板参数

template<typename T>
struct Test {T a};
int func()
{
	Test<double>t;
	Test<auto>t1 = t;//error 无法推出模板类型
}

总结一下auto的限制:

  • auto的使用必须马上初始化,否则无法推导出类型
  • auto在一行定义多个变量时,各个变量的推导不能产生二义性,否则编译失败
  • auto不能用作函数参数
  • 在类中auto不能用作非静态成员变量
  • auto不能定义数组,可以定义指针
  • auto无法推导出模板参数

相比于auto用于推导变量类型,decltype则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算。

decltype(表达式)

	const int& i = 1;
	decltype(i) b = 1;//b是const int&
	int a = 2;
	decltype(a + 3.14) c = 5.15;//c是double
int a = 0, b = 0;
decltype(a + b) c = 0; // c是int,因为(a+b)返回一个右值
decltype(a += b) d = c;// d是int&,因为(a+=b)返回一个左值
 
d = 20;
cout << "c " << c << endl; // 输出c 20

decltype推导规则

对于decltype(exp)有

  • exp是表达式,decltype(exp)和exp类型相同
  • exp是函数调用,decltype(exp)和函数返回值类型相同
  • 其它情况,若exp是左值,decltype(exp)是exp类型的左值引用

auto和decltype的配合使用

auto和decltype一般配合使用在推导函数返回值的类型问题上。

下面这段代码:

template<typename T, typename U>
return_value add(T t, U u) { // t和u类型不确定,无法推导出return_value类型
    return t + u;
}

上面代码由于t和u类型不确定,那如何推导出返回值类型呢,我们可能会想到这种:

template<typename T, typename U>
decltype(t + u) add(T t, U u) { // t和u尚未定义
    return t + u;
}

这段代码在C++11上是编译不过的,因为在decltype(t +u)推导时,t和u尚未定义,就会编译出错,所以有了下面的叫做返回值类型后置的配合使用方法: 

template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
    return t + u;
}

返回值类型后置语法就是为了解决函数返回值类型依赖于参数但却难以确定返回值类型的问题。 

增强for

for(declaration:expression)

{

        //循环体
}

declaration表示遍历声明,在遍历过程中,会将遍历到的元素拷贝到声明的变量中,expression是要遍历的对象,它可以是表达式、容器、数组、初始化列表等。

lambda表达式

概念

sort(vec.begin(),vec.end,[ ](int a,int b){return a>b;}); //作为匿名函数使用

表示形式

具体语法

解释:

举例:

能不能改变:int a=100;

  1. [ = ](){return a = 10; } //错误 常函数,a是值传递,不可以改变
  2. [ & ](){return a = 10;} //可以 a是引用传递,a会被改写成10
  3. [ = ]()mutable{return a = 10;} //可以 mutable 可变,副本a会被改写成10,外面a不变
  4. [ & ]()mutable{return a = 10:} //可以 a是引用传递,mutable 可变,a会被改写成10

#include<iostream>
using namespace std;

class AA
{
public:
    AA()
    {
        auto f0 = [this](){return this;};
        cout<<f0()<<endl;
    }
};

int val1 = 10;
int main()
{
    auto f1 = [](){return val1 = 100;}; //全局的变量,不用捕获,可以直接使用
    cout<<f1()<<endl; //100
    cout<<val1<<endl; //100

    int j = 5;
    auto f2 = [&j](){return j = 10;};
    auto f3 = [j](){return j;}; //先后执行会有不同
    cout<<f2()<<endl; //10
    cout<<j<<endl; //10

    //修改的是副本
    auto f4 = [j]()mutable{return j = 100;};
    auto f5 = [j]()mutable{return j = 200;};
    cout<<f4()<<endl; //100
    cout<<j<<endl; //10
    cout<<f5()<<endl; //200
    cout<<j<<endl; //10

    auto f6 = [&j]()mutable{return j = 300;};
    cout<<f6()<<endl; //300
    cout<<j<<endl; //300

    AA a;

    return 0;
}

应用场景

1. 需要快速定义简单的函数对象

Lambda 表达式让你能够在不需要定义一个单独的函数或类的情况下定义简单的函数对象。例如:

auto add = [](int a, int b) { return a + b; };
std::cout << add(2, 3);  // 输出 5

2. 作为 STL 算法的参数

C++ STL 中许多算法(如 std::sort, std::for_each, std::find_if)接受函数对象作为参数。使用 lambda 可以让这些操作更加简洁,而不需要单独创建函数或仿函数。例如:

std::vector<int> vec = {1, 2, 3, 4, 5};
std::sort(vec.begin(), vec.end(), [](int a, int b) { return a > b; });

3. 作为回调函数

如果某个函数需要一个回调函数,lambda 可以提供内联定义的方式,而不需要为回调函数创建单独的类或函数。例如,在多线程编程或事件处理系统中经常使用回调函数:

auto callback = [](int x) { std::cout << "Callback received: " << x << std::endl; };
some_function_that_accepts_callback(callback);

4. 封装状态的函数对象

Lambda 可以捕获外部变量,因此它们能够访问并修改外部作用域的状态。这使得 lambda 成为一种方便的封装状态的函数对象。例如:

int factor = 2;
auto multiply = [factor](int x) { return x * factor; };
std::cout << multiply(5);  // 输出 10

5. 线程与并发编程

在并发编程中,lambda 表达式非常有用,特别是在传递给线程构造函数时。你可以将线程的执行逻辑直接以 lambda 的形式传递,无需为线程逻辑定义一个单独的函数:

std::thread t([]() {
    std::cout << "Hello from a thread!" << std::endl;
});
t.join();

常见问题

lambda 表达式引用悬空问题

引用悬空问题是指当 Lambda 表达式捕获了一个变量的引用,但该变量在 Lambda 被调用之前已经超出了作用域或被销毁。这使得 Lambda 访问的引用不再有效,造成未定义行为。

#include <iostream>
#include <functional>

std::function<void()> createLambda() {
    int x = 10;
    return [&]() {
        std::cout << "Value of x: " << x << std::endl; // 捕获 x 的引用
    };
}

int main() {
    auto lambda = createLambda(); // 创建 Lambda
    // 一旦 createLambda() 返回,x 的生命周期结束
    lambda(); // 此时,x 的引用已悬空
    return 0; // 可能导致未定义行为
}

在这个例子中,x 是在 createLambda() 内部定义的局部变量。当 createLambda() 返回后,x 的生命周期结束,lambda() 的调用将会访问一个已经被销毁的变量,导致引用悬空。

如何解决引用悬空问题

1. 使用值捕获

将引用捕获改为值捕获,这样 Lambda 表达式会在创建时复制一份外部变量的值,而不是引用。即使外部变量在 Lambda 表达式执行之前被销毁,Lambda 表达式内部仍然持有变量的副本。

std::function<void()> createLambda() {
    int x = 10;
    return [x]() { // 捕获 x 的值
        std::cout << "Value of x: " << x << std::endl; // 这里是安全的
    };
}

2.确保变量的生命周期足够长

确保 Lambda 捕获的变量在 Lambda 的使用期间是有效的。例如,将变量提升到较大作用域(或者是全局变量)。

int main() {
    int x = 10;
    auto lambda = [&]() {
        std::cout << "Value of x: " << x << std::endl;
    };
    lambda(); // 这里是安全的,因为 x 的生命周期在整个 main 函数中
    return 0;
}

3.使用智能指针来管理对象的生命周期

#include <iostream>
#include <memory>

std::function<void()> createLambda() {
    auto x = std::make_shared<int>(10);
    return [x]() {
        std::cout << "Value of x: " << *x << std::endl; // 使用智能指针
    };
}

int main() {
    auto lambda = createLambda();
    lambda(); // 安全访问
    return 0;
}

智能指针

头文件:#include<memory>

auto_ptr(已在C++11中弃用)

创建方式:

  1. 有参构造
  2. reset()方法
  3. 赋值

存在的问题:指针的所有权交出的太随意,交出后,原指针为空,后续通过该指针调用会出问题。

unique_ptr

唯一所有权智能指针,在auto_ptr的基础上禁止拷贝构造与赋值,通过move()对使用权进行转移,避免auto_ptr在用户无意识情况下将指针的所有权交出。

使用场景:单例模式(在我之前的文章:C++之设计模式-优快云博客

shared_ptr

#include <iostream>
#include<vector>
#include<string>
#include<memory>
using namespace std;

class AAAA{
private:
    string s;
public:
    AAAA( const string str)
    {
        s = str;
        cout<<"AAAA:"<< s << endl;
    }
    ~AAAA()
    {
        cout<<"~AAAA" << endl;
    }
    void say()
    {
        cout<<"say:"<< s << endl;
    }
};

int main() {
    shared_ptr<AAAA> sp1( new AAAA("shared_ptr1"));
    shared_ptr<AAAA> sp2;
    sp2.reset( new AAAA("shared_ptr2"));

    shared_ptr<AAAA> sp3;
    sp3 = make_shared <AAAA>("share_ptr3");
    {
        shared_ptr<AAAA>sp4;
        sp4=sp3; //指向同一个 共享
        cout<< sp4.use_count()<< endl;//2
    }

    sp3.get()->say();//say:share_ptr3
    sp3->say();//say:share_ptr3
    
    cout<<sp3.use_count()<<endl;//查看引用计数 1
    sp3.reset();//主动回收空间
    return 0;
}

存在的问题: 

1、循环引用

什么是循环引用?举例:

解释:此时spc.use_count()为2,spb.use_count()也为2,走到{ }的结尾处,对pc,pb回收,此时引用计数都变为1,空间无法回收,这就是循环引用。

2、多线程下的线程安全问题

为了保证空间正确回收,引用计数+1或-1是线程安全的,但关于指针指向空间的使用,是非线程安全,所以在多线程下使用这个空间要加锁。

weak_ptr

同时在weak_ptr销毁时,对应空间的引用计数也不会减1。

weak_ptr,弱引用计数智能指针,它的提出主要是解决shared_ptr中循环引用的问题。

怎么解决?举例:

解释:将spb或spc由shared_ptr改成weak_ptr,这里我们改spb。此时spc.use_count()为1,spb.use_count()为2,回收pc、pb后,spc.use_count()为0,回收其空间,当该空间回收后,该空间的spc对象也就不存在了,这时spb.use_count()也由1变为0,对应的空间也得到了回收。

  1. 可以使用 lock()方法获取与weak_ptr捆绑的shared_ptr。lock() 方法会尝试升级 weak_ptr 到一个 shared_ptr。如果 weak_ptr 所指向的对象仍然存在(即没有被销毁),lock() 方法会成功并返回一个 shared_ptr;如果对象已经被销毁,它会返回一个空的 shared_ptr。
  2. weak_ptr不能直接调用函数,如果想调用,需要先获得与weak_ptr捆绑的shared_ptr,再通过shared_ptr进行调用。
  3. expire()方法用于检查 weak_ptr 是否指向一个有效的 shared_ptr。如果 weak_ptr 所指向的对象已经被销毁(即所有与之关联的 shared_ptr 都已被释放),expire() 将返回 true,表示 weak_ptr 失效。

举例:

#include <iostream>
#include <memory>

class MyClass {
public:
    void doSomething() {
        std::cout << "Doing something" << std::endl;
    }
};

int main() {
    std::shared_ptr<MyClass> sp(new MyClass());
    std::weak_ptr<MyClass> wp(sp); // 从 shared_ptr 创建 weak_ptr
    //std::weak_ptr<MyClass> wp = sp;

    // 模拟 shared_ptr 被销毁的情况
    sp.reset();

    if(wp.expired())
    {
        std::cout<<"weak_ptr失效"<<std::endl;//weak_ptr失效
    }

    // 尝试通过 weak_ptr 获取 shared_ptr
    std::shared_ptr<MyClass> lockedSp = wp.lock();

    if (lockedSp) {
        lockedSp->doSomething(); // 如果 shared_ptr 不为空,调用成员函数
    } else {
        std::cout << "Object has been destroyed." << std::endl;//Object has been destroyed.
    }

    return 0;
}

总结:

智能指针是线程安全的吗

使用智能指针注意事项

如果想用智能指针管理数组,要注意避免内存泄漏问题:

unique_ptr:unique_ptr 对数组有很好的支持。unique_ptr 提供了专门用于管理数组的特化版本 unique_ptr<T[]>,它会自动使用 delete[] 操作符来释放内存,不需要额外指定自定义删除器。

#include <memory>

int main() {
    std::unique_ptr<int[]> arr(new int[10]); // 管理一个长度为10的整型数组
    arr[0] = 1; // 使用数组
    arr[1] = 2;
    // 数组会在arr超出作用域时自动释放
    return 0;
}

shared_ptr:不支持类似unique_ptr<T[]>,需要显式指定自定义删除器。

#include <memory>

int main() {
    std::shared_ptr<int> arr(new int[10], std::default_delete<int[]>());//方法一
    //std::shared_ptr<int> arr(new int[10], [](int* p) { delete[] p; });//方法二
    arr.get()[0] = 1; // 使用数组
    arr.get()[1] = 2;
    // 数组会在最后一个引用离开作用域时自动释放
    return 0;
}

使用智能指针一定能防止内存泄漏吗

智能指针是 C++ 中用于管理动态分配内存的工具,它利用对象的生命周期来自动管理内存。当智能指针对象超出其作用域时,会自动调用析构函数,在析构函数中释放其所管理的内存,从而避免手动管理内存时可能出现的忘记释放内存的问题。但并不能完全防止内存泄漏,举例如下:

1. 循环引用

循环引用指的是两个或多个 shared_ptr 对象相互引用,使得它们的引用计数永远不会变为 0,从而无法释放所管理的对象。

2.自定义删除器的实现不正确

例如,在使用 shared_ptr 管理数组时,如果没有正确实现自定义删除器,会导致内存泄漏。

3.智能指针使用不当

如果在使用智能指针时,手动释放了其所管理的内存,或者将智能指针与原始指针混用不当,也可能导致内存泄漏。

右值引用

概念

引用分为左值引用,右值引用,常引用。

左值一般有:

  • 函数名和变量名
  • 返回左值引用的函数调用
  • 前置自增自减表达式++i、--i
  • 由赋值表达式或赋值运算符连接的表达式(a=b, a += b等)
  • 解引用表达式*p
  • 字符串字面值"abcd"

返回值优化

  • 当函数需要返回一个对象实例的时候,就会创建一个临时对象并通过拷贝构造函数将目标对象复制到临时对象,这里有拷贝构造函数和析构函数会被多余的调用到,有代价。而通过返回值优化,C++标准允许省略调用这些拷贝构造函数。

举例:

左值引用与右值引用举例:

int main()
{
    //以下的都是左值
    int* p = new int(0);
    int a = 1;
    const int b = 2;
    //以下是对上面左值的左值引用
    int*& rp = p;
    int& ra = a;
    const int& rb = b;
    int& pvalue = *p;
 
    return 0;
}
int main()
{

    double x = 1.1, y = 2.2;
    //以下是常见的右值
    10;
    x + y;
    fmin(x, y);

    //以下是对右值的右值引用
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);

    //这里编译会报错:“error C2106: “=”: 左操作数必须为左值”
    10 = 1;
    x + y = 1;
    fmin(x, y) = 1;

    return 0;
}

表示形式

举例:

总结:左值引用不可引用右值,右值引用也不可引用左值,但可以引用移动的左值。

作用

实现移动语义完美转发

移动语义可以将资源(堆,系统对象等)从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高C++应用程序的性能。

move:这是一个用于将对象转换为右值引用的工具。它实际上并不执行任何移动操作,而是标记一个对象可以被移动。通过 move,我们可以触发移动构造函数或移动赋值运算符,从而实现资源的转移。

用法:

#include <utility> // 需要引用此头文件

class MyClass {
public:
    MyClass(const MyClass&) = delete; // 删除复制构造函数
    MyClass(MyClass&& other) noexcept { /* 移动构造 */ }
    MyClass& operator=(MyClass&& other) noexcept { /* 移动赋值 */ }
};

void example() {
    MyClass obj1;
    MyClass obj2 = std::move(obj1); // 将 obj1 转换为右值引用,触发移动构造
}

举例:

#include<iostream>
using namespace std;

#include<vector>

vector<int> getVec(vector<int>& vec)
{
    cout<<"fun::vec addr:"<<&vec.front()<<endl; //fun::vec addr:0x1e1275d0
    return vec;
}

vector<int>&& getVec2(vector<int>& vec)
{
    cout<<"fun::vec2 addr:"<<&vec.front()<<endl; //fun::vec2 addr:0x1e1275d0
    return move(vec);
}

int main() {
    //转移语义
    vector<int> vec = {1,2,3,4,5,6};
    cout<<"vec addr:"<<&vec.front()<<endl; //vec addr:0x1e1275d0

    vector<int> vec1 = getVec(vec);
    cout<<"vec1 addr:"<<&vec1.front()<<endl; //vec1 addr:0x1e1285f8

    vector<int> vec2 = getVec2(vec);
    cout<<"vec2 addr:"<<&vec2.front()<<endl; //vec2 addr:0x1e1275d0

    cout<<"vec items:"<<endl;
    for(auto& num : vec)
    {
        cout<<num<<" "; //空
    }
    cout<<endl;

    for(auto& num : vec2)
    {
        cout<<num<<" "; //1 2 3 4 5 6 
    }
    cout<<endl;

    return 0;
}

右值引用可以实现精确传参

  • 精确传参适用场景:需要将一组参数原封不动的传递给另一个函数。
  • 精确传参就是在参数传递过程中,所有属性和参数值都不能改变。
  • 在泛型函数中,这样的需求非常普遍。 

forward:允许我们在模板中转发函数参数,同时保持参数的值类别(左值或右值)不变。

用法:

#include <iostream>
#include <utility>

// 要转发的函数
void process(int& x) {
    std::cout << "Lvalue reference called with: " << x << std::endl;
}

void process(int&& x) {
    std::cout << "Rvalue reference called with: " << x << std::endl;
}

// 完美转发的包装器函数
template<typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg)); // 完美转发参数
}

int main() {
    int a = 10;
    wrapper(a);                     // 传入左值
    wrapper(20);                    // 传入右值

    return 0;
}

举例:

#include<iostream>
using namespace std;

template <typename T>
void forward_value(const T& val)
{
    cout<<"forward_value(const T& val)"<<endl;
}

template <typename T>
void forward_value(T& val)
{
    cout<<"forward_value(T& val)"<<endl;
}

int main() {
    //精准传参
    int a = 0;
    const int &b = 1;

    forward_value(a); //int&
    forward_value(b); //const int&
    forward_value(2); //const int&
    return 0;
}

由上述代码可知,常引用以及常量匹配的都是const int&,那么我们再加上右值引用,会发生什么呢?

#include<iostream>
using namespace std;

template <typename T>
void forward_value(const T& val)
{
    cout<<"forward_value(const T& val)"<<endl;
}

template <typename T>
void forward_value(T& val)
{
    cout<<"forward_value(T& val)"<<endl;
}

template <typename T>
void forward_value(T&& val)
{
   cout<<"forward_value(T&& val)"<<endl;
}

int main() {
    //精准传参
    int a = 0;
    const int &b = 1;

    forward_value(a); //int&
    forward_value(b); //const int&
    forward_value(2); //int&&
    return 0;
}

加上右值引用后,结果发生了变化,2是一个右值,正确匹配,可见右值引用可以实现精准传参。

问题: 

四种类型转换

C语言里面有强制类型转换,为什么不继续沿用,而存在这四种类型转换?

  • C++在设计时,强调类型安全,但是C中的类型转换可谓无所不能,沿用的话,会破坏C++的类型系统,还会留下很多安全隐患。
  • 此外,四种类型转换还有他们使用的不同场合和功能,组合使用较C中的强制类型转换更强大。

static_cast 静态转换

编译时确定的

使用场景:

  • 相关内容的转换 例:数与数之间、int short char 之间、int*与 void*之间
  • 继承中对象的向上和向下转换(向下转换,没有检查,可能会有问题)

举例:

父类A 子类 A1(fun1) A2(fun2) A3(fun3)
A* pa= new A1;
//向下转换
A2 *pa2 = static_cast<A2*>(pa); // 转换可以,但是后面使用就出问题了
pa2 -> fun2();

注:

  • A是B的父类,B->A 继承中对象的向上转换,A->B 向下转换
  • 不相关的不能转换:student*与int*之间不能转换,char*与int*之间也不能(因为不安全),int -> int* 不可以,int*->int 也不可以

相当于限制版的强制类型转换

代码举例:

#include<iostream>
using namespace std;

class student{};
class A1
{
public:
    virtual void fun(){}
};
class A2:public A1{};
class A3:public A1{};
class A4:public A2{};

int main() {
    int a = 0;
    int *p = &a;
    char c = 0;
    char* pc = &c;
    student st;

    {
        //---------------static_cast-----------------
        //相关内容转换
        //p = static_cast<int*>(pc);//char*->int* 不允许
        void* lpvoid = static_cast<void*>(pc);
        a = static_cast<int>(c);
        //student* ps = static_cast<student*>(p);//int*->student* 不允许
        //p = static_cast<int*>(&st);//student*->int* 不允许
        //a = static_cast<int>(p);//int*->int 不允许
        //p = static_cast<int*>(a);//int->int* 不允许

        A2* pa2 = (A2*)(new A1);//子类指针默认无法指向父类对象,除非强转
        //继承关系中对象的向上转换
        A1* pa1 = static_cast<A1*>(pa2);
        pa1 = new A2;
        //继承关系中对象的向下转换
        pa2 = static_cast<A2*>(pa1);
        A3* pa3 = static_cast<A3*>(pa1);
        if(pa3)
        {
            cout<<"pa3 is valid"<<endl;//pa3会取到值,但后续如果执行A3里面的函数会有问题
        }
        
        //C语言强制类型转换
        student* ps2 = (student*)(p);
        p = (int*)&st;
    }
    return 0;
}

dynamic_cast 动态转换

  1. 只适用于继承上的两个类之间转换
  2. 继承的向上转换效果等同于static_cast
  3. 继承的向下转换会进行检验
  4. 向下转换安全(即父类指针要转换为子类指针,恰巧这个父类指针指向这个子类对象,可以得到对应结果)
  5. 向下转换不安全(即父类指针要转换为子类指针,恰巧这个父类指针不指向这个子类对象,返回结果为空)
  6. 向下转换,父类中必须包含虚函数(static_cast 没有这个要求)
  7. 特殊情况
  • 向下转换安全(即父类指针要转换为子类指针,恰巧这个父类指针指向这个子类的子类),通俗来说就是爷爷指向孙子,此时爷爷想转化为父亲。

代码举例:

A1、A2、A3、A4之间关系图:

#include<iostream>
using namespace std;

class student{};
class A1
{
public:
    virtual void fun(){}
};
class A2:public A1{};
class A3:public A1{};
class A4:public A2{};

int main() {
    int a = 0;
    int *p = &a;
    char c = 0;
    char* pc = &c;
    student st;
    
    {
        A2* pa2 = (A2*)(new A1);
        //继承关系中对象的向上转换
        A1* pa1 = dynamic_cast<A1*>(pa2);
        pa1 = new A2;
        //继承关系中对象的向下转换
        pa2 = dynamic_cast<A2*>(pa1);
        A3* pa3 = dynamic_cast<A3*>(pa1);
        if(pa3)
        {
            cout<<"pa3 is valid"<<endl;
        }
        else
        {
            cout<<"pa3 is invalid"<<endl;//pa3 is invalid
        }
        //特殊情况,向下转换安全:父类指针要转换为子类指针,恰巧这个父类指针指向这个子类的子类
        pa1 = new A4;
        pa2 = dynamic_cast<A2*>(pa1);
        if(pa2)
        {
            cout<<"pa2 is valid"<<endl;//pa2 is valid
        }
        else
        {
            cout<<"pa2 is invalid"<<endl;
        }

        pa1 = new A2;
        A4* pa4 = dynamic_cast<A4*>(pa1);
        if(pa4)
        {
            cout<<"pa4 is valid"<<endl;
        }
        else
        {
            cout<<"pa4 is invalid"<<endl;//pa4 is invalid
        }

        //交叉转换
        //pa3 = static_cast<A3*>(pa2); //相当于A1转A2,A2又转A3,静态转换不可以
        pa3 = dynamic_cast<A3*>(pa2); //动态转换可以
    }
    return 0;
}

为什么要用dynamic_cast?

对于动态转换中继承的向下转换,怎么进行安全检验的? 

根据虚函数的入口地址进行检验,如果虚函数的入口地址相同,那么是安全的。

static_cast与dynamic_cast区别?

  1. static_cast 支持相关数据类型转化,dynamic_cast不支持
  2. 对于继承关系中的转换,static_cast可以向上和向下转换,dynamic_cast向上转换与static_cast一致,向下转换,父类要有虚函数,并且存在检验机制
  3. static_cast不支持交叉转换,只能是向上下,dynamic_cast可以交叉转换

const_cast 去常转换

int* p=const_cast<int*>( const_int_lp);
const->非const
volatile->非volatile

例: const A*-> A* 或者 const A ->A
作用:

  • 由于常对象只能调用常函数,并且对象的属性不能修改,去常转换后,可以调用任何类成员函数,并且可以修改属性值

volatile

  • 防止编译器优化
  • 避免变量在CPU缓存和在内存不一致,每次读取都是先读内存,确保读取的是最新值。

reinterpret_cast 重解释类型转换

  • 是C++里的强制类型转换符
  • 相当于(type),无所不能
  • 有安全隐患,一般编译实在不过的时候使用

委托构造函数&继承构造函数

什么是委托构造函数?

允许在构造函数中调用同一个类中另外一个构造函数 ,实现代码的简化。但要注意不可出现闭环,下面的例子是链式调用,这是可以的,如果3调用1的同时,1也调用3,这是不可以的。同时构造函数的调用应写在初始化列表中,不应写在函数体内部,否则可能会提示形参重定义。

class Test
{
private:
	int m_max;
	int m_min;
	int m_mid;
public:
	Test() {};
	Test(int max)
	{
		this->m_max = max > 0 ? max : 100;
	}
	Test(int max, int min):Test(max)//调用Test(max)
	{
		this->m_min = min > 0 && min < max ? min : 1;
	}
	Test(int max, int min, int mid):Test(max, min)//调用Test(max, min)
	{
		this->m_mid = mid<max && mid>min ? min : 50;
	}
};

什么是继承构造函数? 

允许派生类继承基类的构造函数,从而减少代码的重复。在没有继承构造函数之前,派生类需要显示地定义构造函数,并在构造函数中调用基类的构造函数来初始化基类的部分,有了继承构造函数以后,就可以让派生类自动继承并使用基类的构造函数,这样就避免了显示地调用。

class Base {
public:
    Base(int i) :m_i(i) {}
    Base(int i, double j) :m_i(i),m_j(j) {}
    Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}
    int m_i;
    double m_j;
    string m_k;
};
class Child :public Base
{
public:
    Child(int i) :Base(i) {}
    Child(int i, double j) :Base(i, j) {}
    Child(int i, double j, string k) :Base(i, j, k) {}
};

int main() {
    Child c(1, 1.2, "Bob");
    cout << c.m_i << " " << c.m_j << " " << c.m_k << endl;//1 1.2 Bob
    return 0;
}

引入后:

class Base {
public:
    Base(int i) :m_i(i) {}
    Base(int i, double j) :m_i(i),m_j(j) {}
    Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}
    int m_i;
    double m_j;
    string m_k;
};
class Child :public Base
{
public:
    using Base::Base;
};

int main() {
    Child c(1, 1.2, "Bob");
    cout << c.m_i << " " << c.m_j << " " << c.m_k << endl;//1 1.2 Bob
    return 0;
}

constexpr

用于修饰函数和变量,在编译期进行运算,从而提高程序的性能。适用于常量表达式,要求值或函数能够在编译时确定。

#include <iostream>

// constexpr 函数
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    constexpr int fact5 = factorial(5);  // 编译期求值
    std::cout << "Factorial of 5 is: " << fact5 << std::endl;

    // 运行时求值
    int n = 5;
    int fact = factorial(n);  // 在运行时计算
    std::cout << "Factorial of 5 is: " << fact << std::endl;

    return 0;
}

std::function与std::bind

std::function介绍

std::function是一个函数封装器,它可以存储、调用和传递任何可调用对象。

满足以下条件之一就可称为可调用对象: 

  • 函数指针
  • 类成员(函数)指针
  • lambda表达式
  • bind表达式或其它函数对象
  • 具有operator()成员函数的类对象(传说中的仿函数)
  • 可被转换为函数指针的类对象

std::bind介绍

std::bind可以将可调用对象和参数一起绑定,绑定后的结果使用std::function进行保存,并延迟调用到任何我们需要的时候。通过std::bind,可以在调用函数时提前绑定部分参数,从而简化函数调用的过程,这种方式也被称为“柯里化”(currying),有助于提高代码的可读性和灵活性。

std::bind通常有两大作用

  • 将可调用对象与参数一起绑定,存储在std:.function供调用
  • 提前绑定部分参数,简化函数调用

function与bind使用代码

#include<iostream>
using namespace std;

int add(int a,int b)
{
    return a + b;
}
//&add 可调用对象
//function 函数封装器,存储可调用对象
//bind 将可调用对象和部分参数绑定在一起
#include<functional>

int main()
{
    std::function<int(int,int)>f = &add;
    cout<<"res:"<<f(3,4)<<endl;// res:7

    //在调用之前 将参数与函数指针捆绑 然后使用function存储
    //因为已经给定参数 所以类型参数列表int()中为空
    //因为捆绑的时候已经传完参数 所以调用时不需要传参
    std::function<int()>f1 = std::bind(&add,3,4);
    cout<<"res:"<<f1()<<endl;// res:7
    
    //当捆绑时不知道需要传递的参数时,可以使用std::placeholders::_1 进行参数占位
    std::function<int(int ,int)>f2 = std::bind(&add,std::placeholders::_1,std::placeholders::_2);
    cout<<"res:"<<f2(3,4)<<endl;// res:7
    
    return 0;
}

捆绑普通函数

#include<iostream>
using namespace std;

int add(int a,int b)
{
    return a + b;
}

#include<functional>

//加减乘除都可以实现计算
int getResult(std::function<int(int,int)>f)
{
    int a = 0,b = 0;
    cin >> a >> b;
    return f(a,b);
}
//getResult() 是回调函数,先将函数指针传入(可能包含参数),在需要的时候执行

int main()
{
    std::function<int(int ,int)>f = 
            std::bind(&add,std::placeholders::_1,std::placeholders::_2);
    int res = getResult(f);
    cout<<"res:"<<res<<endl;
    
    return 0;
}

捆绑类成员函数

#include<iostream>
using namespace std;


class AA
{
public:
    int multi(int a,int b)
    {
        return a * b;
    }
};

#include<functional>

//加减乘除都可以实现计算
int getResult(std::function<int(int,int)>f)
{
    int a = 0,b = 0;
    cin >> a >> b;
    return f(a,b);
}

int main()
{
    AA aa;
    //将类成员函数指针与对象的地址捆绑(类中非静态成员函数第一个参数默认为this指针)
    //然后multi还有两个参数 int int
    std::function<int(int ,int)>f = 
            std::bind(&AA::multi,&aa,std::placeholders::_1,std::placeholders::_2);
    int res = getResult(f);
    cout<<"res:"<<res<<endl;
    
    return 0;
}

捆绑静态成员函数

#include<iostream>
using namespace std;


class AA
{
public:
    int multi(int a,int b)
    {
        return a * b;
    }
    static int div(int a,int b)
    {
        return a / b;
    }
};

#include<functional>

//加减乘除都可以实现计算
int getResult(std::function<int(int,int)>f)
{
    int a = 0,b = 0;
    cin >> a >> b;
    return f(a,b);
}

int main()
{
    std::function<int(int ,int)>f =
            std::bind(&AA::div,std::placeholders::_1,std::placeholders::_2);
    int res = getResult(f);
    cout<<"res:"<<res<<endl;

    return 0;
}

使用function与bind有什么好处?

可以在使用类成员函数时不依赖对象,提前将对象地址绑定到函数指针上

chrono时间库

C++11有了chrono时间库,可以在不同系统中很容易的实现定时功能。

要使用chrono库,需要#include<chrono>,其所有实现均在std::chrono

chrono是一个模版库,使用简单,功能强大,只需要理解三个概念:duration、time_point、clock

#include<iostream>
#include<chrono>
#include<thread>
using namespace std;

int main()
{
    //时间间隔举例
    std::this_thread::sleep_for(std::chrono::milliseconds(2000));
    //std::this_thread::sleep_for(std::chrono::seconds(2));
    cout<<"sleep finish"<<endl;

    //this_thread代表当前线程 sleep_for休眠 参数传的是chrono的时间间隔
    //milliseconds 毫秒 seconds 秒
    /*
        chrono::milliseconds
        milliseconds 类型别名
        typedef duration<int64_t, milli> milliseconds;

        typedef ratio<1,1000> milli;
        ratio<1,1000> 分数 1/1000 因为要使用ms 所以是1s的1/1000

        chrono::duration 结构体 //模版参数 第一个是数值 第二个是单位
        std::chrono::milliseconds(2000)
        std::chrono::duration<int64_t, milli> 2000是int64_t类型的数值
    */

    //时间点 打印当前时间
    std::chrono::system_clock::time_point tp =
            std::chrono::system_clock::now(); // 获取当前时间点
    time_t t_t = std::chrono::system_clock::to_time_t(tp);
    tm* t = std::localtime(&t_t);

//    string ti = ctime(&t_t);
//    cout << ti << endl;

    //获取毫秒
    auto du = tp.time_since_epoch();//获取Unix时间戳(时间间隔) 单位是秒
    //duration 时间间隔转换 duration_cast<转换的目标类型>(原值)
    int ms = chrono::duration_cast<chrono::microseconds>(du).count();
    ms = ms % 1000;

    //Unix时间戳 1970 01 01 00:00:00 起始时间(纪元时间)
    //1970 01 01 00:00:00的格林威治时间 是纪元时间 epoch
    //例:格林威治时间 00:00:00 对应北京时间 08:00:00  那么此时北京时间为 1970 01 01 08:00:00
    //当前时间距离纪元时间有多少秒表示时间戳

    /*
    时间戳的应用:例如,网站向服务器发送请求,某些服务器为避免重复指令一直发送,服务器通常会进行过滤,
    即服务器在短时间内如果收到多次重复命令,就进行过滤。但某些情况下,我们就想要获取服务器响应,
    那我们就加时间戳,让服务器直到我们想获得当前时效的内容,这时候服务器就进行处理。
    */

    //打印当前时间
    printf("%d-%02d-%02d %02d:%02d:%02d %3d\n"
           ,t->tm_year +1900/*年份从1900开始*/
           ,t->tm_mon +1/*月份从0开始*/
           ,t->tm_mday
           ,t->tm_hour
           ,t->tm_min
           ,t->tm_sec
           ,ms);

    //计算一段时间间隔 使用稳定时钟
    std::chrono::steady_clock::time_point begin =
            chrono::steady_clock::now();

    std::this_thread::sleep_for(chrono::seconds(1));

    std::chrono::steady_clock::time_point end =
            chrono::steady_clock::now();
    //两个时间点的差 即为时间间隔
    auto d = end - begin;
    //转换为以ms为单位
    int diff = chrono::duration_cast<chrono::milliseconds>(d).count();
    cout<<"diff:"<<diff<<endl;

    return 0;

}

时间间隔 duration

        chrono::milliseconds
        milliseconds 类型别名
        typedef duration<int64_t, milli> milliseconds;
     
        typedef ratio<1,1000> milli;
        ratio<1,1000> 分数 1/1000 因为要使用ms 所以是1s的1/1000
    
        chrono::duration 结构体 //模版参数 第一个是数值 第二个是单位
        std::chrono::milliseconds(2000)
        std::chrono::duration<int64_t, milli> 2000是int64_t类型的数值

  • 由于各种时间段(duration)表示不同,chrono库提供了duration_cast类型转换函数,用于将duration转换成另一个类型的duration。(例:s转换成ms)
  • duration还有一个成员函数count(),用来表示这一段时间的长度

时间点 time_point

获取当前时间点

std::chrono::system_clock::time_point tp = std::chrono::system_clock::now(); 

注:一个time_point必须有一个clock计时

时钟

system_clock

  • 系统时钟,用于获取系统时间的时钟,由于系统时间可以改变,这个时钟也会随之改变
  • 不是连续的,一般不用系统时钟计算时间间隔

steady_clock

  • 稳定时钟,不受系统时间的影响,是单调递增的,一般用于计算时间间隔

high_resolution_clock

  • 高分辨率时钟,system_clock 或 steady_clock 的别名,到底是哪个受系统影响,提供高分辨率的时间计算

三种时钟总结:

  • 这三种时钟类都提供了一个静态成员函数now()用于获取当前时间,该函数的返回值是一个time_point类型。
  • system_clock除了now()函数外,还提供了to_time_t()静态成员函数。用于将系统时间转换成熟悉的std::time_t类型,得到了time_t类型的值,再使用ctime()函数将时间转换成字符串格式,就可以很方便地打印当前时间了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值