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::function和std::bind使得我们平时编程过程中封装函数更加的方便,而lambda表达式将这种方便发挥到了极致,可以在需要的时间就地定义匿名函数,不再需要定义类或者函数等,在自定义STL规则时候也非常方便,让代码更简洁,更灵活,提高开发效率。
扩展知识
std::chrono
chrono很强大,平时的打印函数耗时,休眠某段时间等,都可使用chrono.
在 C++11 中入了duration, time_point和clocks,在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,可以重点回答以下几个方面:
- 修饰局部变量: 当
static用于修饰局部变量时,这个变量的存储位置会在程序执行期间保持不变,且只在程序执行到该变量的声明处时初始化一次。
即使函数被多次调用,static局部变量也只在第一次调用时初始化,之后的调用将不会重新初始化它。
-
修饰全局变量或函数:当
static用于修饰全局变量或函数时,限制了这些变量或函数的作用域,它们只能在定义它们的文件内部访问。有助于避免在不同文件之间的命名冲突。 -
修饰类的成员变量或函数:在类内部,
static成员变量或函数属于类本身,而不是类的任何特定对象。这意味着所有对象共享同一个static成员变量,无需每个对象都存储一份拷贝。static成员函数可以在没有类实例的情况下调用。
10. C++ 中 const 的作用?谈谈你对const的理解?
回答重点
const最主要的作用就是声明一个变量为常量,即这个变量的值在初始化之后就不能被修改。
但const不仅可以用作普通常量,还可以用于指针、引用、成员函数、成员变量等。具体作用如下:
- 定义普通常量:当修饰基本数据类型的变量时,表示常量含义,对应的值不能被修改。
cpp
const int MAX_SIZE = 100; // MAX_SIZE是一个常量,其值不能被修改
- 修饰指针:这里分多种情况,比如指针本身是常量,指针指向的数据是常量,或者指针本身和其
指向的数据都是常量。
- 修饰引用:const修饰引用时,一般用作函数参数,表示函数不会修改传递的参数值。
cpp
void func(const int& a) { // a是一个对常量的引用,不能通过a修改其值
// ...
}
- 修饰类成员函数:
const修饰成员函数,表示函数不会修改类的任何成员变量,除非这些成员变量被声明为mutable
cpp
class MyClass {
public:
void myFunc() const { // myFunc是一个const成员函数,它不会修改类的任何成员变量
// ...
}
};
- 修饰类成员变量:
const修饰成员函数,表示生命周期内不可改动此值。
cpp
class MyClass {
public:
const int a = 5;
};