C++核心知识点全解析(二)

8. C++11中有哪些常用的新特性?

回答重点

C++11新特性几乎是面试必问的一个话题,可以主要回答以下几个特性:

  • auto类型推导

  • 智能指针

  • RAll lock

  • std::thread

  • 左值右值

  • std::function和lambda表达式

auto类型推导

auto可以让编译器在编译时就推导出变量的类型,看代码:

cpp 复制代码
auto a = 10; // 10是int型,可以自动推导出a是int

int i = 10;
auto b = i; // b是int型

auto d = 2.0; // d是double型
auto f = []() { // f是啥类型?直接用auto就行
    return std::string("d");
}

利用auto可以通过=右边的类型推导出变量的类型。什么时候使用auto呢?简单类型其实没必要使用auto,某些复杂类型就有必要使用auto,比如lambda表达式的类型,async函数的类型等,

例如:

cpp 复制代码
auto func = [&] {    
    cout << "xxx";
}; // 对于func你难道不使用auto吗,反正我是不关心lambda表达式究竟是什么类型。
auto asyncfunc = std::async(std::launch::async, func);

智能指针

C++11新特性中主要有两种智能指针 std::shared_ptr和 std::unique_ptr

那什么时候使用std::shared_ptr,

什么时候使用 std::unique_ptr呢?

  • 当所有权不明晰的情况,有可能多个对象共同管理同一块内存时,要使用std::shared_ptr

  • 而std::unique_ptr强调的是独占,同一时刻只能有一个对象占用这块内存,不支持多个对象共同管理同一块内存。

两类智能指针使用方式类似,拿std::unique_ptr举例:

cpp 复制代码
using namespace std;

struct A {
   ~A() {
       cout << "A delete" << endl;
   }
   void Print() {
       cout << "A" << endl;
   }
};

int main() {
   auto ptr = std::unique_ptr<A>(new A);
   auto tptr = std::make_unique<A>(); // error, c++11还不行,需要c++14
   std::unique_ptr<A> tem = ptr; // error, unique_ptr不允许移动,编译失败
   ptr->Print();
   return 0;
}

RAll lock

C++11提供了两种锁封装,通过RAIl方式可动态的释放锁资源,防止编码失误导致始终持有锁。这两种封装是std::lock_guard和 std::unique_lock,使用方式类似,看下面的代码:

cpp 复制代码
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>

using namespace std;
std::mutex mutex_;

int main() {
   auto func1 = [](int k) {
       // std::lock_guard<std::mutex> lock(mutex_);
       std::unique_lock<std::mutex> lock(mutex_);
       for (int i = 0; i < k; ++i) {
           cout << i << " ";
      }
       cout << endl;
  };
   std::thread threads[5];
   for (int i = 0; i < 5; ++i) {
       threads[i] = std::thread(func1, 200);
  }
   for (auto& th : threads) {
       th.join();
  }
   return 0;
}

普通情况下建议使用std::lock_guard,因为std::lock_guard更加轻量级,但如果用在条件变量的wait中环境中,必须使用std::unique_lock。

std::thread

什么是多线程这里就不过多介绍,新特性关于多线程最主要的就是std:.thread的使用,它的使用也很简单,看代码:

cpp 复制代码
#include <iostream>
#include <thread>

using namespace std;

int main() {
   auto func = []() {
       for (int i = 0; i < 10; ++i) {
           cout << i << " ";
      }
       cout << endl;
  };
   std::thread t(func);
   if (t.joinable()) {
       t.detach();
  }
   auto func1 = [](int k) {
       for (int i = 0; i < k; ++i) {
           cout << i << " ";
      }
       cout << endl;
  };
   std::thread tt(func1, 20);
   if (tt.joinable()) { // 检查线程可否被join
       tt.join();
  }
   return 0;
}

这里记住,std::thread在其对象生命周期结束时必须要调用join()或者 detach(),否则程序会terminate(),这个问题在C++20中的std::jthread得到解决,但是C++20现在多数编译器还没有完全支持所有特性,先暂时了解下即可,项目中没必要着急使用。

左值右值

关于左值和右值,有两种方式理解:

概念1:

左值:可以放到等号左边的东西叫左值。

右值:不可以放到等号左边的东西就叫右值。

概念2:

左值:可以取地址并且有名字的东西就是左值。

右值:不能取地址的没有名字的东西就是右值。

std::function 和 lambda 表达式

这两个可以说是很常用的特性,使用它们会让函数的调用相当方便。使用std::function可以完全替代以前那种繁琐的函数指针形式。

还可以结合std::bind一起使用,直接看一段示例代码:

cpp 复制代码
std::function<void(int)> f; // 这里表示function的对象f的参数是int,返回值是void
#include <functional>
#include <iostream>

struct Foo {
   Foo(int num) : num_(num) {}
   void print_add(int i) const { std::cout << num_ + i << '\n'; }
   int num_;
};

void print_num(int i) { std::cout << i << '\n'; }

struct PrintNum {
   void operator()(int i) const { std::cout << i << '\n'; }
};

int main() {
   // 存储自由函数
   std::function<void(int)> f_display = print_num;
   f_display(-9);

   // 存储 lambda
   std::function<void()> f_display_42 = []() { print_num(42); };
   f_display_42();

   // 存储到 std::bind 调用的结果
   std::function<void()> f_display_31337 = std::bind(print_num, 31337);
   f_display_31337();

   // 存储到成员函数的调用
   std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
   const Foo foo(314159);
   f_add_display(foo, 1);
   f_add_display(314159, 1);

   // 存储到数据成员访问器的调用
   std::function<int(Foo const&)> f_num = &Foo::num_;
   std::cout << "num_: " << f_num(foo) << '\n';

   // 存储到成员函数及对象的调用
   using std::placeholders::_1;
   std::function<void(int)> f_add_display2 = std::bind(&Foo::print_add, foo, _1);
   f_add_display2(2);

   // 存储到成员函数和对象指针的调用
   std::function<void(int)> f_add_display3 = std::bind(&Foo::print_add, &foo, _1);
   f_add_display3(3);

   // 存储到函数对象的调用
   std::function<void(int)> f_display_obj = PrintNum();
   f_display_obj(18);
}

从上面可以看到std::function的使用方法,当给std::function填入合适的参数表和返回值后,它就变成了可以容纳所有这一类调用方式的函数封装器。std::function还可以用作回调函数,或者在C++里如果需要使用回调那就一定要使用 std::function,特别方便。

lambda表达式可以说是C++11引入的最重要的特性之一,它定义了一个匿名函数,可以捕获一定范围的变量在函数内部使用,一般有如下语法形式:

cpp 复制代码
auto func = [capture] (params) opt -> ret { func_body; };

其中func是可以当作lambda表达式的名字,作为一个函数使用,capture是捕获列表,params是参数表,opt是函数选项(mutable之类),ret是返回值类型,func_body是函数体。

看下面这段使用lambda表达式的示例:

cpp 复制代码
auto func1 = [](int a) -> int { return a + 1; }; 
auto func2 = [](int a) { return a + 2; }; 
cout << func1(1) << " " << func2(2) << endl;

std::functionstd::bind使得我们平时编程过程中封装函数更加的方便,而lambda表达式将这种方便发挥到了极致,可以在需要的时间就地定义匿名函数,不再需要定义类或者函数等,在自定义STL规则时候也非常方便,让代码更简洁,更灵活,提高开发效率。

扩展知识

std::chrono

chrono很强大,平时的打印函数耗时,休眠某段时间等,都可使用chrono.

C++11 中入了duration, time_pointclocks,在c++20中还进一步支持了日期和时区。这里简要介绍下c++11中的这几个新特性。

duration

std::chrono::duration表示一段时间,常见的单位有s、ms等,示例代码:

cpp 复制代码
// 拿休眠一段时间举例,这里表示休眠100ms
std::this_thread::sleep_for(std::chrono::milliseconds(100));

表示一段时间,实际是这样:sleep_for里面其实就是 std::chrono::duration

cpp 复制代码
typedef duration<int64_t, milli> milliseconds;
typedef duration<int64_t> seconds;

duration具体模版如下:

cpp 复制代码
template <class Rep, class Period = ratio<1> > class duration;

Rep表示一种数值类型,用来表示Period的数量,比如int、float、double,Period是ratio类型,用来表示

【用秒表示的时间单位】比如second,常用的duration已经定义好了,在

std::chrono::duration下 :

  • ratio<3600,1>:hours

  • ratio<60, 1>: minutes

  • ratio<1,1>:seconds

  • ratio<1, 1000>: microseconds

  • ratio<1, 1000000>:microseconds

  • ratio<1,1000000000>:nanosecons

ratio的具体模板如下:

cpp 复制代码
template <intmax_t N, intmax_t D = 1> class ratio;

N代表分子,D代表分母,所以ratio表示一个分数,我们可以自定义Period,比如ratio<2,1>表示单位时间是2秒。

time_point

表示一个具体时间点,如2020年5月10日10点10分10秒,拿获取当前时间举例:

cpp 复制代码
std::chrono::time_point<std::chrono::high_resolution_clock> Now() {
   return std::chrono::high_resolution_clock::now();
}
// std::chrono::high_resolution_clock为高精度时钟,下面会提到

clocks

时钟,chrono里面提供了三种时钟:

1. steady clock

稳定的时间间隔,表示相对时间,相对于系统开机启动的时间,无论系统时间如何被更改,后一次调用nowO)肯定比前一次调用now)的数值大,可用于计时。

2. system_clock

表示当前的系统时钟,可以用于获取当前时间:

cpp 复制代码
int main() {
   using std::chrono::system_clock;
   system_clock::time_point today = system_clock::now();
   std::time_t tt = system_clock::to_time_t(today);
   std::cout << "today is: " << ctime(&tt);
   return 0;
}
// today is: Sun May 10 09:48:36 2020

3. high resolution_clock

high_resolution_clock 表示系统可用的最高精度的时钟,实际上就是system_clock 或者steady_clock其中一种的定义,官方没有说明具体是哪个,不同系统可能不一样,之前看gccchrono源码中 high_resolution_clock steady_clock 的typedef。

条件变量

条件变量是C++11引入的一种同步机制,它可以阻塞一个线程或多个线程,直到有线程通知或者超时才会唤醒正在阻塞的线程,条件变量需要和锁配合使用,这里的锁就是上面介绍的std::unique_lock

这里使用条件变量实现一个countDownLatch:

cpp 复制代码
class CountDownLatch {
   public:
    explicit CountDownLatch(uint32_t count) : count_(count);

    void CountDown() {
        std::unique_lock<std::mutex> lock(mutex_);
        --count_;
        if (count_ == 0) {
            cv_.notify_all();
        }
    }

    void Await(uint32_t time_ms = 0) {
        std::unique_lock<std::mutex> lock(mutex_);
        while (count_ > 0) {
            if (time_ms > 0) {
                cv_.wait_for(lock, std::chrono::milliseconds(time_ms));
            } else {
                cv_.wait(lock);
            }
        }
    }

    uint32_t GetCount() const {
        std::unique_lock<std::mutex> lock(mutex_);
      return count_;
    }

   private:
    std::condition_variable cv_;
    mutable std::mutex mutex_;
    uint32_t count_ = 0;
};
class CountDownLatch {
   public:
    explicit CountDownLatch(uint32_t count) : count_(count);

    void CountDown() {
        std::unique_lock<std::mutex> lock(mutex_);
        --count_;
        if (count_ == 0) {
            cv_.notify_all();
        }
    }

    void Await(uint32_t time_ms = 0) {
        std::unique_lock<std::mutex> lock(mutex_);
        while (count_ > 0) {
            if (time_ms > 0) {
                cv_.wait_for(lock, std::chrono::milliseconds(time_ms));
            } else {
                cv_.wait(lock);
            }
        }
    }

    uint32_t GetCount() const {
        std::unique_lock<std::mutex> lock(mutex_);
      return count_;
    }

   private:
    std::condition_variable cv_;
    mutable std::mutex mutex_;
    uint32_t count_ = 0;
};

9. C++ 中 static 的作用?什么场景下用到 static?

回答重点

谈到C++的static,可以重点回答以下几个方面:

  1. 修饰局部变量: 当static用于修饰局部变量时,这个变量的存储位置会在程序执行期间保持不变,且只在程序执行到该变量的声明处时初始化一次。

即使函数被多次调用,static局部变量也只在第一次调用时初始化,之后的调用将不会重新初始化它。

  1. 修饰全局变量或函数:当static用于修饰全局变量或函数时,限制了这些变量或函数的作用域,它们只能在定义它们的文件内部访问。有助于避免在不同文件之间的命名冲突。

  2. 修饰类的成员变量或函数:在类内部,static成员变量或函数属于类本身,而不是类的任何特定对象。这意味着所有对象共享同一个static成员变量,无需每个对象都存储一份拷贝。static成员函数可以在没有类实例的情况下调用。


10. C++ 中 const 的作用?谈谈你对const的理解?

回答重点

const最主要的作用就是声明一个变量为常量,即这个变量的值在初始化之后就不能被修改。

const不仅可以用作普通常量,还可以用于指针、引用、成员函数、成员变量等。具体作用如下:

  1. 定义普通常量:当修饰基本数据类型的变量时,表示常量含义,对应的值不能被修改。
cpp 复制代码
const int MAX_SIZE = 100; // MAX_SIZE是一个常量,其值不能被修改
  1. 修饰指针:这里分多种情况,比如指针本身是常量,指针指向的数据是常量,或者指针本身和其

指向的数据都是常量。

  1. 修饰引用:const修饰引用时,一般用作函数参数,表示函数不会修改传递的参数值。
复制代码
cpp 复制代码
void func(const int& a) { // a是一个对常量的引用,不能通过a修改其值  
    // ...  
}
  1. 修饰类成员函数: const修饰成员函数,表示函数不会修改类的任何成员变量,除非这些成员变量被声明为mutable
cpp 复制代码
class MyClass {  
public:  
    void myFunc() const { // myFunc是一个const成员函数,它不会修改类的任何成员变量  
        // ...  
    }  
};
  1. 修饰类成员变量:const修饰成员函数,表示生命周期内不可改动此值。
cpp 复制代码
class MyClass {  
public:  
    const int a = 5;
};
相关推荐
载数而行5202 小时前
算法系列2之最短路径
c语言·数据结构·c++·算法·贪心算法
weixin_440401692 小时前
Python数据分析(空值、重复值检测删除与设置)
开发语言·python·数据分析
消失的旧时光-19432 小时前
C++ 多线程与并发系统取向(五)—— std::atomic:原子操作与状态一致性(类比 Java Atomic)
开发语言·jvm·c++·并发
资深web全栈开发2 小时前
CoI - 组合优于继承:解耦的艺术
android·java·开发语言
低频电磁之道2 小时前
C++中预定义宏
开发语言·c++
工程师0072 小时前
MQTT 概念详解与 C# 实战
开发语言·c#·mqtt通信
fpcc2 小时前
并行编程实战——CUDA编程的Warp Vote
c++·cuda
fpcc2 小时前
并行编程实战——CUDA编程的Warp Shuffle
c++·cuda
代码改善世界3 小时前
栈和队列的实现与详解(C语言版):从底层原理到代码实战
c语言·开发语言