C++ 学习笔记五 —— 其它用法&C++11+新特性(持续更新中)

四、其他一些用法和概念

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 IInitialization,资源获取即初始化),它充分的利用了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就相当于一个函数,在别的地方使用可能会造成

  • 51
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值