四、其他一些用法和概念
4.1 内联函数
参考:C/C++编程笔记:inline函数的总结!教你正确使用inline,值得收藏! - 知乎 (zhihu.com)
【C++面试100问】第二十一问:内联函数是怎么实现的,有什么优缺点?_哔哩哔哩_bilibili
4.1.1 使用目的
为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。
形式类似于宏定义,但比宏多了类型检验,且可以访问类的成员变量。
4.1.2 案例
inline char& dbtest(int a){
return (i % 2 > 0) ? "奇" : "偶" ;
}
int main()
{
int i = 0;
for (i = 1; i < 100; i++){
cout << i << "的奇偶性: " << dbtest(i) << endl;
}
}
上面的例子就是标准的内联函数的用法,使用inline修饰带来的好处我们表面看不出来,其实,在内部的工作就是在每个for循环的内部任何调用dbtest(i)的地方都换成了(i%2>0)?”奇”:”偶”,这样就避免了频繁调用函数对栈内存重复开辟所带来的消耗。
4.1.3 注意事项
1、inline的使用时有所限制的,inline只适合函数体内部代码简单的函数使用,不能包含复杂的结构控制语句例如while、switch,并且不能内联函数本身不能是直接递归函数(即,自己内部还调用自己的函数)。
2、inline函数的定义放在头文件中。因为内联函数要在调用点展开,所以编译器必须随处可见内联函数的定义,要不然就成了非内联函数的调用了。所以,这要求每个调用了内联函数的文件都出现了该内联函数的定义,并且在每个文件里都保证定义的一致性。
3、关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。
4.2 强制类型转换
参考:C++ 四种强制类型转换 - 静悟生慧 - 博客园 (cnblogs.com)
在C++语言中新增了四个关键字static_cast、const_cast、reinterpret_cast和dynamic_cast。这四个关键字都是用于强制类型转换的。
基本数据类型的转换使用 static_cast <类型说明符> (变量或表达式)。
另外三类转换待学习
4.3 智能指针
传统指针管理的困境:1.资源释放了,指针没有置空;2.没有释放资源,产生内存泄漏;3.重复释放资源,引发coredump。
智能指针解决思想:RAII(Resource Acquisition Is Initialization,资源获取即初始化),它充分的利用了C++语言局部对象自动销毁的特性来控制资源的生命周期。
智能指针需要#include <memory>
4.3.0 new/delete 裸指针
分配在堆区,手动开辟和释放。
void func()
{
int* p = new int[100];
delete[] p;
}
4.3.1 unique_ptr 独享指针
unique_ptr<int> up {make_unique<int>(100)};
4.3.2 shared_ptr 共享指针
当指向某个对象的shared_ptr个数降为0时,系统会自动释放。
4.3.3 weak_ptr 弱指针
伴随着shared_ptr存在,主要用于解决shared_ptr环形引用的问题。
4.4 命名空间 namespace
开发一个大型工程涉及多开发人员的参与,可能会引入同名函数和类型,造成编译冲突;为了缓解对开发的影响,我们可以使用命名空间进行区分。
4.4.1 内联命名空间
内联命名空间能够把空间内的函数和类型导出到父命名空间中,这样及时不指定自命名空间也可以使用期空间内的函数和类型了。案例:
namespace Parent {
namespace Child1
{
void foo() {std::cout << "Child1::foo" << std::ednl};
}
inline namespace Child2
{
void foo() {std::cout << "Child2::foo" << std::ednl};
}
}
int main()
{
Parent::Child1::foo();
Parent::foo(); //等价于Parent::Child2::foo();
}
使用场景:有助于库代码的无缝升级,让客户无需修改代码也能自由使用新老库。如inline namespace V1、inline namespace V2。
4.5 枚举类型 enum
4.6 auto占位符
系统可以自动识别数据类型,需要在定义时同时进行初始化,主要用在两种情况下:
1)一眼就能看出声明变量的初始化类型时,最常见的是迭代器;
2)对于复杂类型,例如lambda表达式、bind等可以直接使用auto。
4.6.1 推导规则
1)在使用auto声明变量时,如果既没有使用引用,也没有使用指针,name编译器在推导的时候会忽略const和volatile限定符。
2)在使用auto声明变量初始化时,目标对象如果是引用,则引用属性会被忽略。
int x = 1;
const int y = 1;
auto m = x; // auto推导类型为int, m推导类型为int
auto n = y; // auto推导类型为int, n推导类型为int
auto m = &x; // auto推导类型为int*, m推导类型为int*
auto n = &y; // auto推导类型为const int*, n推导类型为int*
auto *m = &x; // auto推导类型为int, m推导类型为int*
auto *n = &y; // auto推导类型为const int, n推导类型为int*
int &z = x;
auto m = z; // auto推导类型为int,而非int&
4.6.2 auto和const auto&的对比
1.auto即for(auto x:range) 会拷贝一份range元素,不会改变range中的元素;
2.只读取range中的元素,使用const auto&,如:for(const auto&x:range),它不会进行拷贝,也不会修改range,效率会比用auto快一点。
想要拷贝元素:for(auto x:range)
想要修改元素:for(auto &&x:range)
只读元素:for(const auto& x:range)
4.7 decltype说明符
其作用是返回变量的类型(declare type)。
int a = 1;
decltype(a) b = 2;
4.8 函数返回类型后置
在定义函数时,可以用auto对函数返回类型占位,在函数参数后面再定义返回类型;其优势在于可以利用函数形参来定义返回值类型。
// 作用1:可以在后置中使用参数
auto addFunc(T a, T b) -> decltype(a + b)
{
return (a + b)
}
// 作用2:函数指针只能后置
int bar_impl(int x)
{
return x;
}
auto foo() -> int(*)(int)
{
return bar_impl;
}
4.9 static静态变量及函数的使用
定义在类中的static函数,无需实例化即可在类外调用,采用的方式为类名::函数名,但与此同时,函数内部对于成员变量的使用也只能使用static成员变量。这是因为static类型在编译时即进行初始化,因此需要事先定义以分配内存。
static静态变量不能在类中初始化,一般在类外和main()函数之前初始化,缺省时初始化为0。
static静态常量如果是基本数据类型(字面常量-literal constant,包括整形、浮点型、字符串型、字符型),可以在类内定义时同时初始化,但还是推荐在类外初始化。
4.10 全局变量的定义和使用
定义在h文件中,为了避免多个cpp文件包含该h文件在编译时出现重复定义的问题,需要声明外部变量:
constexpr double STEP_LENGTH = 2.0;
// 或者是:
extern const double STEP_LENGTH = 2.0;
4.11 关键字 override和final
C++ 多态行为的基础:基类声明虚函数,派生类声明一个函数覆盖该虚函数。
虚函数的两个常见错误:无意中使用了同名函数重写、虚函数签名不匹配(由于函数名,输入参数不同导致创建了新函数,而不是重写)。
为了避免上述问题,使用关键字方法如下:
class Base {
public:
virtual void Show(int x); // 虚函数
};
class Derived : public Base {
public:
virtual void Show(int x) const override; // const 属性不一样,新的虚函数
};
override:派生类中使用,保证派生类中声明的重载函数,与基类的虚函数有相同的签名;
class Base {
public:
virtual void Show(int x) final; // 虚函数
};
class Derived : public Base {
public:
virtual void Show(int x) override; // 重写提示错误
};
final:父类中使用,阻止类的进一步派生 和 虚函数的进一步重写。
参考:C++11关键字:override 和 final - 知乎 (zhihu.com)
4.12 左值与右值
左值一般是指一个指向特定内存的具有名称的值(具名对象),它有一个相对稳定的内存地址,并且有一段较长的生命周期,可以放在等号左边(也可以在右边, 比如int a = 1; int b = a;中a和b都是左值),常见的包括具体的变量名、返回左值引用的函数调用、前置自增/自减等;
右值则是不指向稳定内存地址的匿名值(不具名对象),它的生命周期很短,通常是暂时性的,只能放在等号的右边,可以分为纯右值(如字面值1、非引用类型的函数调用、后置自增/自减等)和将亡值(用于移动语义)。
基于这一特征,我们可以用取地址符&来判断左值和右值,能取到内存地址的是左值,否则为右值(字符串常量除外,可以取地址但是右值)。
4.12.1 左值引用与右值引用(C++11引入)
左值引用是对左值的引用,目的主要是为了避免对象的拷贝,例如函数传参、函数返回值等;const左值引用可以指向右值,但局限是不能修改这个值。
右值引用是对右值的引用,功能主要是实现移动语义和完美转发;右值引用可以通过std::move()指向左值。
4.12.2 复制构造与移动构造
复制构造的形参数是一个左值引用,往往进行的是深复制,即在不能破坏实参对象的前提下复制目标对象;
而移动构造恰恰相反,它接受的是一个右值,其核心思想是通过转移实参对象数据以达成构造目标对象的目的,也就是说实参对象是会被修改的。
在移动构造函数中减少了重复的创建、拷贝、销毁等操作,提高了运行效率,特别是那些只使用一次的分配在堆上的变量(比如字符串、自定义类型)。
4.12.3 万能引用与完美转发
完美转发是利用函数模板将自己的参数完美地(保持转发数据值和类型不变、左右值属性不变)转发给内部调用的其它函数;
实现方式是通过万能引用T &&t来保证左右值属性不变,其背后原理是1.引用折叠规则(遇左则左:传入参数为左值或者左值引用,T&&将转化为int &,否则将转化为int &&);2.std::forward<T>(t)将左值/右值引用进行解引用,转为左值/右值。
参考:【大厂面试c++】2.左值引用与右值引用的区别?_哔哩哔哩_bilibili
4.13 lambda表达式
捕获列表只包含非静态的局部变量,对于全局变量和静态变量直接用就可以了;这是因为lambda就相当于一个函数,在别的地方使用可能会造成