15、基础 | C++ 新特性(2)
@[toc]
auto & decltype
关于C++11新特性,最先提到的肯定是类型推导,C++11引入了auto和decltype关键字,使用他们可以在编译期就推导出变量或者表达式的类型,方便开发者编码也简化了代码。
- auto:让编译器在编译器就推导出变量的类型,可以通过=右边的类型推导出变量的类型。
auto a = 10; // 10是int型,可以自动推导出a是int
- decltype:相对于auto用于推导变量类型,而decltype则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算。
cont int &i = 1;int a = 2;decltype(i) b = 2; // b是const int&
左值右值
众所周知C++11新增了右值引用,这里涉及到很多概念:
-
左值:可以取地址并且有名字的东西就是左值。
-
右值:不能取地址的没有名字的东西就是右值。
-
纯右值:运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值。
-
将亡值:可以理解为即将要销毁的值。
-
左值引用:对左值进行引用的类型。
-
右值引用:对右值进行引用的类型。
-
移动语义:转移资源所有权,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用。
-
完美转发:可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参。
-
返回值优化:当函数需要返回一个对象实例时候,就会创建一个临时对象并通过复制构造函数将目标对象复制到临时对象,这里有复制构造函数和析构函数会被多余的调用到,有代价,而通过返回值优化,C++标准允许省略调用这些复制构造函数。
列表初始化
在C++11中可以直接在变量名后面加上初始化列表来进行对象的初始化。
std::function & std::bind & lambda表达式
c++11新增了std::function、std::bind、lambda表达式等封装使函数调用更加方便。
模板的改进
C++11关于模板有一些细节的改进:
-
模板的右尖括号
-
模板的别名
-
函数模板的默认模板参数
并发
c++11关于并发引入了好多好东西,有:
-
std::thread相关
-
std::mutex相关
-
std::lock相关
-
std::atomic相关
-
std::call_once相关
-
volatile相关
-
std::condition_variable相关
-
std::future相关
-
async相关
智能指针
很多人谈到c++,说它特别难,可能有一部分就是因为c++的内存管理,不像java那样有虚拟机动态的管理内存,在程序运行过程中可能就会出现内存泄漏,然而这种问题其实都可以通过c++11引入的智能指针来解决,这种内存管理还是c++语言的优势,因为尽在掌握。
c++11引入了三种智能指针:
-
std::shared_ptr
-
std::weak_ptr
-
std::unique_ptr
基于范围的for循环
看代码
vector<int> vec;
for (auto iter = vec.begin(); iter != vec.end(); iter++) { // before c++11
cout << *iter << endl;
}
for (int i : vec) { // c++11基于范围的for循环
cout << "i" << endl;
}
委托构造函数
委托构造函数允许在同一个类中一个构造函数调用另外一个构造函数,可以在变量初始化时简化操作,通过代码来感受下委托构造函数的妙处:
不使用委托构造函数:
struct A {
A(){}
A(int a) { a_ = a; }
A(int a, int b) { // 好麻烦
a_ = a;
b_ = b;
}
A(int a, int b, int c) { // 好麻烦
a_ = a;
b_ = b;
c_ = c;
}
int a_;
int b_;
int c_;
};
使用委托构造函数:
struct A {
A(){}
A(int a) { a_ = a; }
A(int a, int b) : A(a) { b_ = b; }
A(int a, int b, int c) : A(a, b) { c_ = c; }
int a_;
int b_;
int c_;
};
初始化变量是不是方便了许多。
继承构造函数
继承构造函数可以让派生类直接使用基类的构造函数,如果有一个派生类,希望派生类采用和基类一样的构造方式,可以直接使用基类的构造函数,而不是再重新写一遍构造函数,老规矩,看代码:
不使用继承构造函数:
struct Base {
Base() {}
Base(int a) { a_ = a; }
Base(int a, int b) : Base(a) { b_ = b; }
Base(int a, int b, int c) : Base(a, b) { c_ = c; }
int a_;
int b_;
int c_;
};
struct Derived : Base {
Derived() {}
Derived(int a) : Base(a) {} // 好麻烦
Derived(int a, int b) : Base(a, b) {} // 好麻烦
Derived(int a, int b, int c) : Base(a, b, c) {} // 好麻烦
};
int main() {
Derived a(1, 2, 3);
return 0;
}
使用继承构造函数:
struct Base {
Base() {}
Base(int a) { a_ = a; }
Base(int a, int b) : Base(a) { b_ = b; }
Base(int a, int b, int c) : Base(a, b) { c_ = c; }
int a_;
int b_;
int c_;
};
struct Derived : Base {
using Base::Base;
};
int main() {
Derived a(1, 2, 3);
return 0;
}
只需要使用using Base::Base继承构造函数,就免去了很多重写代码的麻烦。
nullptr
nullptr是c++11用来表示空指针新引入的常量值,在c++中如果表示空指针语义时建议使用nullptr而不要使用NULL,因为NULL本质上是个int型的0,其实不是个指针。举例:
void func(void *ptr) {
cout << "func ptr" << endl;
}
void func(int i) {
cout << "func i" << endl;
}
int main() {
func(NULL); // 编译失败,会产生二义性
func(nullptr); // 输出func ptr
return 0;
}
final & override
c++11关于继承新增了两个关键字,final用于修饰一个类,表示禁止该类进一步派生和虚函数的进一步重载,override用于修饰派生类中的成员函数,标明该函数重写了基类函数,如果一个函数声明了override但父类却没有这个虚函数,编译报错,使用override关键字可以避免开发者在重写基类函数时无意产生的错误。
示例代码1:
struct Base {
virtual void func() {
cout << "base" << endl;
}
};
struct Derived : public Base{
void func() override { // 确保func被重写
cout << "derived" << endl;
}
void fu() override { // error,基类没有fu(),不可以被重写
}
};
示例代码2:
struct Base final {
virtual void func() {
cout << "base" << endl;
}
};
struct Derived : public Base{ // 编译失败,final修饰的类不可以被继承
void func() override {
cout << "derived" << endl;
}
};
default
c++11引入default特性,多数时候用于声明构造函数为默认构造函数,如果类中有了自定义的构造函数,编译器就不会隐式生成默认构造函数,如下代码:
struct A {
int a;
A(int i) { a = i; }
};
int main() {
A a; // 编译出错
return 0;
}
上面代码编译出错,因为没有匹配的构造函数,因为编译器没有生成默认构造函数,而通过default,程序员只需在函数声明后加上“=default;”,就可将该函数声明为 defaulted 函数,编译器将为显式声明的 defaulted 函数自动生成函数体,如下:
struct A {
A() = default;
int a;
A(int i) { a = i; }
};
int main() {
A a;
return 0;
}
编译通过。
delete
c++中,如果开发人员没有定义特殊成员函数,那么编译器在需要特殊成员函数时候会隐式自动生成一个默认的特殊成员函数,例如拷贝构造函数或者拷贝赋值操作符,如下代码:
struct A {
A() = default;
int a;
A(int i) { a = i; }
};
int main() {
A a1;
A a2 = a1; // 正确,调用编译器隐式生成的默认拷贝构造函数
A a3;
a3 = a1; // 正确,调用编译器隐式生成的默认拷贝赋值操作符
}
有时候想禁止对象的拷贝与赋值,可以使用delete修饰,如下:
struct A {
A() = default;
A(const A&) = delete;
A& operator=(const A&) = delete;
int a;
A(int i) { a = i; }
};
int main() {
A a1;
A a2 = a1; // 错误,拷贝构造函数被禁用
A a3;
a3 = a1; // 错误,拷贝赋值操作符被禁用
}
delele函数在c++11中很常用,std::unique_ptr就是通过delete修饰来禁止对象的拷贝的。
explicit
explicit专用于修饰构造函数,表示只能显式构造,不可以被隐式转换,根据代码看explicit的作用:
不用explicit:
struct A {
A(int value) { // 没有explicit关键字
cout << "value" << endl;
}
};
int main() {
A a = 1; // 可以隐式转换
return 0;
}
使用explicit:
struct A {
explicit A(int value) {
cout << "value" << endl;
}
};
int main() {
A a = 1; // error,不可以隐式转换
A aa(2); // ok
return 0;
}
const
因为要讲后面的constexpr,所以这里简单介绍下const。
const字面意思为只读,可用于定义变量,表示变量是只读的,不可以更改,如果更改,编译期间就会报错。
主要用法如下:
- 用于定义常量,const的修饰的变量不可更改。
const int value = 5;
- 指针也可以使用const,这里有个小技巧,从右向左读,即可知道const究竟修饰的是指针还是指针所指向的内容。
char *const ptr; // 指针本身是常量
const char* ptr; // 指针指向的变量为常量
- 在函数参数中使用const,一般会传递类对象时会传递一个const的引用或者指针,这样可以避免对象的拷贝,也可以防止对象被修改。
class A{};
void func(const A& a);
- const修饰类的成员变量,表示是成员常量,不能被修改,可以在初始化列表中被赋值。
class A {
const int value = 5;
};
class B {
const int value;
B(int v) : value(v){}
};
- 修饰类成员函数,表示在该函数内不可以修改该类的成员变量。
class A{
void func() const;
};
- 修饰类对象,类对象只能调用该对象的const成员函数。
class A {
void func() const;
};
const A a;
a.func();
constexpr
constexpr是c++11新引入的关键字,用于编译时的常量和常量函数,这里直接介绍constexpr和const的区别:
两者都代表可读,const只表示read only的语义,只保证了运行时不可以被修改,但它修饰的仍然有可能是个动态变量,而constexpr修饰的才是真正的常量,它会在编译期间就会被计算出来,整个运行过程中都不可以被改变,constexpr可以用于修饰函数,这个函数的返回值会尽可能在编译期间被计算出来当作一个常量,但是如
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
【C/C++面试必考必会】专栏,直击面试核心,精选C/C++及相关技术栈中面试官最爱的必考点!从基础语法到高级特性,从内存管理到多线程编程,再到算法与数据结构深度剖析,一网打尽。助你快速构建知识体系,轻松应对技术挑战。希望专栏能让你在面试中脱颖而出,成为技术岗的抢手人才。