C++新特性系列一:基础知识

一、auto

  1. auto只能推断出类型,引用不是类型,所以auto无法推断出引用,要使用引用只能自己加引用符号。

    #include <iostream>

    int main()
    {
    int i = 100;
    auto i2 = i; // i2为int类型
    auto &i2 = i; // i2为int&类型
    return 0;
    }
  2. auto关键字在推断引用的类型时:会直接将引用替换为引用指向的对象。其实引用一直是这样的,引用不是对象,任何使用引用的地方都可以直接替换成引用指向的对象。

    #include <iostream>

    int main()
    {
    int i = 100;
    const int &i2 = i;//此处i2类型为const int &
    auto i3 = i2;//i3类似是int,此处是:auto关键字在推断引用的类型时:会直接将引用替换为引用指向的对象。由于i2为引用类型,因此在auto推断时候,把i2替换为了该引用指向的对象
    return 0;
    }
  3. auto关键字在推断类型时,如果没有引用符号,会忽略值类型的const修饰,而保留修饰指向对象的const,典型的就是指针。可能有些不好理解,看看代码就好说了。3和4的主要作用对象就是指针。

    #include <iostream>

    int main()
    {
    // example 1
    int i = 100;
    const int *const pi = &i; //第一个const修饰指针指向的值,第二个const指向自己pi
    // 3.没有引用符号,会忽略值类型的const修饰,而保留修饰指向对象的const
    //因此把第二个const忽略了,保留了第一个const
    auto pi2 = pi; // pi2为const int *

    // example 2
    const int i = 100; //由于此处的const修饰值,所以会被忽略
    auto i2 = i; //因此i2类型是int
    return 0;
    }
  4. auto关键字在推断类型时,如果有了引用符号,那么值类型的const和修饰指向对象的const都会保留。

    #include <iostream>

    int main()
    {
    // example 1
    int i = 100;
    const int *const pi = &i;
    auto i2 = pi; // i2类型为const int * 没有引用类型,忽略值类型修饰的const,保留修饰对象的const
    auto &i2 = pi; // i2类型为const int * const //有引用符号,值类型的const和修饰指向对象的const都会保留
    // example 2
    const int i = 100;
    auto &i2 = i; // i2类型为const int & //有引用符号,值类型的const和修饰指向对象的const都会保留
    return 0;
    }

其实3,4为什么会出现这种情况,因为在传递值时,修改这个值并不会对原有的值造成影响。而传递引用时,修改这个值会直接对原有的值造成影响。

  1. 当然,我们可以在前面加上const,这样永远都有const的含义。

    #include <iostream>

    int main()
    {
    // example 1
    int i = 100;
    const auto i2 = i; //i2类型为const int
    return 0;
    }
  2. auto不会影响编译速度,甚至会加快编译速度。因为编译器在处理XX a = b时,当XX是传统类型时,编译期需要检查b的类型是否可以转化为XX。当XX为auto时,编译期可以按照b的类型直接给定变量a的类型,所以效率相差不大,甚至反而还有提升。

  3. (*)最重要的一点,就是auto不要滥用,对于一些自己不明确的地方不要乱用auto,否则很可能出现事与愿违的结果,使用类型应该安全为先。
  4. (*)auto主要用在与模板相关的代码中,一些简单的变量使用模板常常导致可读性下降,经验不足还会导致安全性问题。

二、静态变量,指针和引用

变量的存储位置有三种,分别是静态变量区,栈区,堆区。
静态变量区在编译时就已经确定地址,存储全局变量与静态变量。

#include <iostream>
unsigned g_i = 0; //全局变量在程序编译时已经初始化了
unsigned test()
{
static unsigned callCount = 0;//程序编译时候,已经被赋值为0了,在程序执行时,直接执行了下一行
return ++callCount;
}
int main()
{
++g_i; //程序运行时才执行,所以没问题
test();
test();
test();
test();
test();
unsigned testFuncCallNum = test();
std::cout << testFuncCallNum; //6
return 0;
}

指针都是存储在栈上或堆上,不管在栈上还是堆上,都一定有一个地址。

本质上说,指针和普通变量没有区别。

在32位系统中,int变量和指针都是32位。指针必须和“&”,“*”这两个符号一起使用才有意义。

&a代表的a这个变量的地址,a代表的a对应地址存储的值,*a代表对应地址存储的值作为地址对应的值。

所以指针才可以灵活的操作内存,但这也带来了严重的副作用,比如指针加加减减就可以操作内存,所以引用被发明了,引用就是作用阉割的指针(可以视为“类型*const”,所以引用必须上来就赋初值,不能设置为空),编译器不将其视作对象,操作引用相当于操作引用指向的对象。也就从根本是杜绝了引用篡改内存的能力。

#include <iostream>

int main()
{
int i = 100;
int &i2 = i;
int const *i3 = &i; //以上两行本质上一样
return 0;
}

新手如果不懂内存,就直接将引用视为指向对象的别名就可以了。

三、左值、右值、左值引用、右值引用

1.左右值

C++任何一个对象要么是左值,要么是右值。

比如int i = 10,i和10都是对象

左值: 拥有地址属性的对象就叫左值,左值来源于c语言的说法,能放在“=”左面的就是左值,注意,左值也可以放在“=”右面。

右值: 不是左值的对象就是右值。或者说无法操作地址的对象就叫做右值。一般来说,判断一个对象是否为右值,就看它是不是左值,有没有地址属性,不是左值,那就是右值。

比如临时对象,就都是右值,临时对象的地址属性无法使用。

注意:左值也可以放在“=”右面,但右值绝对不可以放在等号左面

#include <iostream>

int main()
{
int i = 100;//i是左值
int i2=i;// i2、i都是左值

int i3=(i+1);//(i+1)是临时对象,有地址,但是无法使用,因此是右值

++i=200;//左值,先给i+1,返回i,有地址属性,可以操作地址
// i++=200;//错误,返回的值是(i+1)此处的i是临时变量,无法使用地址属性
return 0;
}
  1. 引用的分类

(1) 普通左值引用:就是一个对象的别名,只能绑定左值,无法绑定常量对象。

#include <iostream>

int main()
{
int i = 100;//i是左值
int &ref=i;//普通左值引用

const int i = 100;//i是左值
int &ref=i;//报错,error
return 0;
}

(2) const左值引用:可以对常量起别名,可以绑定左值和右值。

#include <iostream>

int main()
{
const int i = 100; // i是左值

const int &ref = i; //const左值引用,可以对常量起别名,可以绑定左值或者右值
std::cout<<ref<<std::endl;
const int &ref3 = (i+1);
std::cout<<ref3<<std::endl;
return 0;
}

(3) 右值引用:只能绑定右值的引用。

#include <iostream>

int main()
{
int i = 100; // i是左值
int &&rrefI = 200;
int &&rrefI = (i + 1);
int &&rrefI = i++;
return 0;
}

(4) 万能引用。

四、move函数,临时对象

  1. move函数:

(1) 右值看重对象的值而不考虑地址,move函数可以对一个左值使用,使操作系统不再在意其地址属性,将其完全视作一个右值。

#include <iostream>

int main()
{
int i = 100; // i是左值
int &&rrefI = i; //错误
int &&rrefI = std::move(i); //正确
return 0;
}

(2) move函数让操作的对象失去了地址属性,所以我们有义务保证以后不再使用该变量的地址属性,简单来说就是不再使用该变量,因为左值对象的地址是其使用时无法绕过的属性。

  1. 临时对象:

右值都是不体现地址的对象。那么,还有什么能比临时对象更加没有地址属性呢?右值引用主要负责处理的就是临时对象。

程序执行时生成的中间对象就是临时对象,注意,所有的临时对象都是右值对象,因为临时对象产生后很快就可能被销毁,使用的是它的值属性。

#include <iostream>

int get_i()
{
return 0;
}

int main()
{
int i = 100; // i是左值
int &&rrefI = i + 1; //临时对象->右值
int &&rreg = get_i(); //临时对象->右值
return 0;
}

五、可调用对象

如果一个对象可以使用调用运算符“()”,()里面可以放参数,这个对象就是可调用对象。
(*)注意:可调用对象的概念新手只要记住就可以了,后面会反复用到,这个概念很重要。
可调用对象的分类:

  1. 函数:

函数自然可以调用()运算符,是最典型的可调用对象。

#include <iostream>

void get_i()
{
std::cout<<"123"<<std::endl;
}

int main()
{
get_i();//get_i就是可调用对象,()为运算符
return 0;
}
#include <iostream>

void get_i(int i)
{
std::cout << i << std::endl;
std::cout << "123" << std::endl;
}

using pf_type = void (*)(int); //函数指针

void myFunc(pf_type pf, int i)
{
pf(i);
}

int main()
{
myFunc(get_i, 200); //
return 0;
}
  1. 仿函数:

具有operator()函数的类对象,此时类对象可以当做函数使用,因此称为仿函数。

#include <iostream>

class Test
{
public:
void operator()(int i)
{
std::cout << i << std::endl;
std::cout << " void operator()(int i)" << std::endl;
}
};

int main()
{
Test t;
t(20); //可调用对象
return 0;
}
  1. lambda表达式:

就是匿名函数,普通的函数在使用前需要找个地方将这个函数定义,于是C++提供了lambda表达式,需要函数时直接在需要的地方写一个lambda表达式,省去了定义函数的过程,增加开发效率。

注意:lambda表达式很重要,现代C++程序中,lambda表达式是大量使用的。

lambda表达式的格式:最少是“[] {}”,完整的格式为“[] () ->ret {}”。

int main()
{
[]
{ std::cout << " void operator()(int i)" << std::endl; }();
return 0;
}

lambda各个组件介绍

  1. []代表捕获列表:表示lambda表达式可以访问前文的哪些变量。
  • []表示不捕获任何变量。
  • [=]:表示按值捕获所有变量。
  • [&]:表示按照引用捕获所有变量。
    =,&也可以混合使用,比如
  • [=, &i]:表示变量i用引用传递,除i的所有变量用值传递。
  • [&, i]:表示变量i用值传递,除i的所有变量用引用传递。
    当然,也可以捕获单独的变量
  • [i]:表示以值传递的形式捕获i
  • [&i]:表示以引用传递的方式捕获i
#include <iostream>


int main()
{
int i = 10;
//[]为空表示不捕获,[=]表示捕获所有变量
[=]
{
std::cout<<i<<std::endl;
std::cout << " void operator()(int i)" << std::endl; }();
return 0;
}
  1. ()代表lambda表达式的参数,函数有参数,lambda自然也有。
#include <iostream>

int main()
{
int i = 10;
//[]为空表示不捕获,[=]表示捕获所有变量
[=](int element) // element函数参数
{
std::cout << element << std::endl;
std::cout << i << std::endl;
std::cout << "void operator()(int i)" << std::endl;
}(200); //给函数参数element的值
return 0;
}
  1. ->ret表示指定lambda的返回值,如果不指定,lambda表达式也会推断出一个返回值的。
#include <iostream>

int main()
{
int i = 10;
//[]为空表示不捕获,[=]表示捕获所有变量
// element函数参数
auto ret = [=](int element) -> int
{
std::cout << element << std::endl;
std::cout << i << std::endl;
std::cout << "void operator()(int i)" << std::endl;
return i;
}(200); //给函数参数element的值
return 0;
}
  1. {}就是函数体了,和普通函数的函数体功能完全相同。

lambda表达式最常见用法 为 给普通函数 作参数。

#include <iostream>

using pf_type = void (*)(int); //函数指针

void myFunc(pf_type pf, int i)
{
pf(i);
}

int main()
{
myFunc([](int i)
{
std::cout << i << std::endl;
std::cout << "void operator()(int i)" << std::endl; },
200);
return 0;
}

如果需要捕获 lambda中的参数,需要以下方法修改:

#include <iostream>
#include <functional>

using pf_type = void (*)(int); //函数指针
using func_type = std::function<void(int)>;

void myFunc(pf_type pf, int i)
{
pf(i);
}

void myFunc1(func_type pf, int i)
{
pf(i);
}

int main()
{
int i1 = 100;
//以下报错,函数指针的缺陷,无法捕获
myFunc([i1](int i)
{
std::cout << i << std::endl;
std::cout << i1 << std::endl;
std::cout << "void operator()(int i)" << std::endl; },
200);
//c++11 提供了functional,解决以上问题
myFunc1([i1](int i)
{
std::cout << i << std::endl;
std::cout << i1 << std::endl;
std::cout << "void operator()(int i)" << std::endl; },
200);
return 0;
}

C++的可调用对象主要就这三个,当然,这三个也可以衍生出很多写法。

最常见的就是函数指针,函数指针的本质就是利用指针调用函数,本质还是函数。

Author: CinKate
Link: http://renxingkai.github.io/2022/08/17/cpp-base/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.