c++ 基础14、c++ 基础烹饪

目录

1、隐式转换

2、非布尔类型的布尔意义

3、解指针类型

4、后置自增/自减

5、共合体

6、const 

7、左值和右值

8、左值引用、右值引用与移动语义

9、 std::move 和  std::forward 

10、static


1、隐式转换

  通常是编译器对c++ 的一个优化,和返回值一样,返回值需要经过值的拷贝构造,重新构造一个变量作为临时值,这种称之为优化,隐式类型转换在一些场景下会让程序更加简洁,降低代码编写难度。比如说下面这些场景,不同类型见的一个赋值优化。c++ 中赋值语句的返回值返回的是左值引用,如 a1 = a2 返回的是a1 的隐用,在隐式转换成 bool 类型。

int a = 'c'-'b';   ->  char ->int
double b = 10   ->  int ->double

int a1, a2
bool c = a1 =a2  ->int ->bool

对于赋值,在类中是这么体现的,常见的有拷贝构造函数、移动赋值函数等。

class  test {

public:
    test & operator (const test &)  // -> 拷贝构造函数
    test & operator (const test && ) // -> 移动拷贝构造函数
    test & operator  = (const test &)  // -> 赋值函数
    test & operator  = (const test &&) // ->移动赋值函数 

}

2、非布尔类型的布尔意义

bool 类型,在c 语言中,我们会使用 int 来表示一个 bool 类型 如下代码。通常我们会将 int 类型作为一个非bool 类型的判断方式,但是但遇到 true 时,就会适得其反,a == 10 ?? 

int a =10;
if(a){
    
}
if(a == true){
}
else{

}

3、解指针类型

c/c++ 是强语言类型还是弱语言类型,我们不得而知,有人认为自变量定义之后,类型就已经固定了,但是还是可以修改的,如下。reinterpret_cast 可以进行任意类型的数据转换,int 转 unit8_t  *,

原本int类型的变量,我们只要把它的首地址保存下来,然后按照另一种类型来解,那么就可以做到“改变 a 的类型”的目的,这也就意味着,指针类型是不安全的,因为你不一定能保证现在解指针的类型和指针指向数据的真实类型是匹配的.

int main()
{
    int a =10;
    unit8_t *b = reinterpret_cast<unit8_t *>(&a);
    *b = 1;
}

4、后置自增/自减

后自增意思是在表达式基础前返回原始值,表达式结束后在进行后增后减,对于后置类型返回值是一个原始变量的临时值,++i 返回的是引用,i++ 返回的是临时值,如下。

class elmet {

public :
   elmet ()=default;
   ~elmet () =default;
   // 前置
   elmet & operator  ++(){
      els ++;
      return *this;
   }
   // 后置
   elmet operator ++ (int i)
{
      elmet p = *this;
      els ++;
      return p
}
private:
   int els;
}

所以在for 循环中 ,for(int i=10;i<100;i++),i++由于是在使用当前值之后再+1,所以需要一个临时的变量来转存。而++i则是在直接+1,省去了对内存的操作的环节,相对而言能够提高性能。

5、共合体

共合体也叫联合体,共和体的所有成员共用内存空间,也就是说它们的首地址是一样的。对于解释就是,同一种数据的不同种解类型,二选1,。

union test{
 int a;
 char b[10];
}

//作为参数
int sun(const test & a){

}

int main()
{
    test t;
    t.b = 100;
    std::cout << t.a <<std:: endl;
    
/// 等价于  
    t.a = 100
    
}

6、const 

对于const 我们是老生长谈了,在翻译看来,意为常量,但是常量的定义是 比如 1,‘a’ 这些恒定不变的值,但是在 计算机语言中,int a; 申请了一个4个字节的内存,但是内存我们不怎么好表示和记忆,于是给这个内存起了一个名字,称之为变量a,意为这个内存的名字是a,其中的数据现在是随机制,还没有初始化,就是说这个名字我们是改变不了的,但是这个名字对应的内存存放数据,我们是可以修改的,但是如果 定义一个 const int a =100,意思是,对于a 这个内存数据,值是100,const 进行修饰这个a 的值是不可以修改的,称之为只读的变量a。

7、左值和右值

二者的区别在于是否是 一个临时的对象,左值是非临时对象,右值是一个临时对象,一个临时对象即在函数语句结束之后直接释放,生命周期非常短。左值的符合是 & ,右值是 && .从下示例

如果将二者传递参数互相调换,就会发送错误,因为右值引用需要绑定一个右值,不能是左值。

void processValue(int &i,int && j)
{
    printf("%d %d\n", i,j);
}
void main(){
    int a = 2;
    processValue(a, 233);
}

但是存在一种情况,如果将右值引用的临时对象作为函数传参,那么这个临时对象将成为一个左值定义。于是乎就需要std::forward 可以保证参数的语值定义,即左值一直是左值,右值一直是右值,后面会介绍到。

void processValue(int & j)
{
    printf("L%d", j);
}
void processValue(int && j)
{
    printf("R%d", j);
}
void processTest(int &&i)
{
    processValue(i);
}

void FunctionView(){
    int a = 2;
    processTest(2);
}

左值和右值就涉及到了赋值表达式,“=“”;赋值表达式的左边表示即将改变的变量,右边表示从什么地方获取这个值。因此,很自然地,右值不会改变,而左值会改变。那么在这个定义下,“常量”自然是只能做右值,因为常量仅仅有“值”,并没有“存储”或者“地址”的概念。而对于变量而言,“只读变量”也只能做右值,原因很简单,因为它是“只读”的。

虽然常量和只读变量是不同的含义,但是都是用来读取值的,也就是可以用来作为右值,const 引用可能是引用,也可能只是个普通变量。也就是说,当用一个 const 引用来接收一个变量的时候,这时的引用是真正的引用,const 引用还可以接收常量,这时,由于常量根本不是变量,自然也不会有内存地址,是只读变量。

int main()
{ 
   const int a=10;    // 普通变量
   const int & r =a ;  // &r 是a 的引用,a
   const int &r2 = 8; // 8是一个常量,因此r2并不是引用,而是一个只读变量
}

8、左值引用、右值引用与移动语义

左值引用,顾名思义,是一个左值的引用,通过&获得左值引用,左值引用只能绑定左值

	int intValue1 = 10;
	//将intValue1绑定到intValue2和intValue3
	int &intValue2 = intValue1, &intValue3 = intValue2;
 
	intValue2 = 100;
	std::cout << intValue1 << std::endl;//100
	std::cout << intValue2 << std::endl;//100
	std::cout << intValue3 << std::endl;//100

通过&&获得右值引用,右值引用只能绑定右值,右值引用的好处是减少右值作为参数传递时的复制开销。

    int intValue = 10;
	int &&intValue2 = 10;//正确
	int &&intValue3 = intValue;//错误


	int intValue = 10;
	int &&intValue3 = std::move(intValue);

C++11 的右值引用语法的引入,其实也完全是针对于底层实现的,而不是针对于上层的语义友好。换句话说,右值引用是为了优化性能的,而并不是让程序变得更易读的。右值引用,那么它一定是对一个右值进行引用,右值可以是一个变量,也可以是一个常量,于是存在2种情况,右值引用 int && r = 5; 当用常量修饰时,右值引用相当于定义一个普通变量,那么当一个变量 如 int a =10,int && r = a;是会报错的,因为右值意为着该变量只能只读,不可以被修改,没有地址和存储的概念,因此 如果使用 const int a =10;int &&r =  a是 a是一个只读变量,是正常的。

在函数的使用过程中,如 int a = fun(); fun() 返回的是一个临时值,在赋值完成之后就会释放掉,int && a = func();返回的是一个右值引用,其延长了对象的生命周期和 a一样长,在函数结束之后才会进行释放,来延长右值的生命周期,进而避免无谓的拷贝和析构过程。右值移动实现移动语义,移动语义用来减少拷贝过程中带来的数据负担,对于深拷贝而言,不仅需要拷贝成员变量的数据,还要把成员指针指向的资源一并给拷贝过来。采用移动语义,是将该资源的所有权直接转移过来,并不会发生内存拷贝。其是由移动构造函数实现的,C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数。

拷贝构造函数: 用一个已存在的对象去创建 创建一个新对象

Holder h1(10000); // regular constructor
Holder h2 = h1;   // copy constructor
Holder h3(h1);    // copy constructor (alternate syntax)


Holder(const Holder& other)
{
  m_data = new int[other.m_size];  // (1)
  std::copy(other.m_data, other.m_data + other.m_size, m_data);  // (2)
  m_size = other.m_size;
}

拷贝赋值函数,已存在的对象替换另一个已存在的对象;

Holder h1(10000);  // regular constructor
Holder h2(60000);  // regular constructor
h1 = h2;           // assignment operator

Holder& operator=(const Holder& other) 
{
  if(this == &other) return *this;  // (1)
  delete[] m_data;  // (2)
  m_data = new int[other.m_size];
  std::copy(other.m_data, other.m_data + other.m_size, m_data);
  m_size = other.m_size;
  return *this;  // (3)
}

9、 std::move 和  std::forward 

标准库的std::move 是将一个左值转换成一个右值,可以减少对象的不必要拷贝,所以的标准库容器都支持移动语义,即将一个对象的所有权转移给另外一个对象,木有发送内存拷贝,这样就可以提升程序与运行的性能,和 for循环里面的 i++;后置 i++ 会产生一个临时对象。一个对象经过 std::move之后内部数据就被掏空了

void processValueL(int & j)
{
    printf("L%d", j);
}
void processValueR(int && j)
{
    printf("R%d", j);
}

void main(){
    int a = 2;
    processValueL(a);
    processValueR(std::move(a));

}

std::forward  参数的完美转发

  /**
   *  @brief  Forward an lvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

  /**
   *  @brief  Forward an rvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
            " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }

有两个函数,其中一个参数是左值引用,另外一个参数是右值引用,如果传递的是左值,Tp推断为string&,则返回变成static_cast<string& &&>,也就是static_cast<string&>,所以返回的是左值引用,如果传递的是右值,Tp推断为string或string&&,则返回变成static_cast<string&&>,所以返回的是右值引用。这里有必要了解一下引用折叠,当函数模板传入的类型不同的话,对于T的也是存在区别,编译器可以正常编译,满足 以下条件:只有当两个右值引用相加时才会产生右值。

& + & -> &
& + && -> &
&& + & -> &
&& + && -> &&

template <typename T>
void f(T &t) {
}


void Demo() {
  int a = 3;

  f<int>(a);
  f<int &>(a);
  f<int &&>(a);
}

10、static

static 真的是非常的不陌生,且非常讨厌,主要是因为一次多用,贼麻烦搞得,有四种用法。

1、在局部变量前的static,限定的是变量的生命周期          

2、在全局变量/函数前的static,限定的变量/函数的作用域 

 3、在成员变量前的static,限定的是成员变量的生命周期

4、在成员函数前的static,限定的是成员函数的调用方(或隐藏参数)

当作为一个局部静态变量的时候,它的生命周期就是主程序生命周期,只初始化一次,count 函数结束就释放掉。

void f() {
  static int count = 0;
  count++;
}

static修饰全局变量或函数时,用于限定其作用域为“当前文件内”。同理,由于已经是“全局”变量了,生命周期一定是符合全局的,也就是“主函数执行前构造,主函数执行结束后释放”。至于全局函数就不用说了,函数都是全局生命周期的。

因此,这时候的static不会再对生命周期有影响,而是限定了其作用域。与之对应的是extern。用extern修饰的全局变量/函数作用于整个程序内,换句话说,就是可以跨文件。

int g_val = 4; // 定义全局变量
// a2.cc
extern int g_val; // 声明全局变量
void Demo() {
  std::cout << g_val << std::endl; // 使用了在另一个文件中定义的全局变量
}

而用static修饰的全局变量/函数则只能在当前文件中使用,不同文件间的static全局变量/函数可以同名,并且互相独立。

/ a1.cc
static int s_val1 = 1; // 定义内部全局变量
static int s_val2 = 2; // 定义内部全局变量
static void f1() {} // 定义内部函数
// a2.cc
static int s_val1 = 6; // 定义内部全局变量,与a1.cc中的互不影响
static int s_val2; // 这里会视为定义了新的内部全局变量,而不会视为“声明”
static void f1(); // 声明了一个内部函数
void Demo() {
  std::cout << s_val1 << std::endl; // 输出6,与a1.cc中的s_val1没有关系
  std::cout << s_val2 << std::endl; // 输出0,同样不会访问到a1.cc中的s_val2
  f1(); // ERR,这里链接会报错,因为在a2.cc中没有找到f1的定义,并不会链接到a1.cc中的f1
}

静态成员变量指的是用static修饰的成员变量。普通的成员变量其生命周期是跟其所属对象绑定的。构造对象时构造成员变量,析构对象时释放成员变量。而用static修饰后,其声明周期变为全局,也就是“主函数执行前构造,主函数执行结束后释放”,并且不再跟随对象,而是全局一份,

truct Test {
  int a; // 普通成员变量
};

int main(int argc, const char *argv[]) {
  Test t; // 同时构造t.a
  auto t2 = new Test; // 同时构造t2->a
  delete t2; // t2所指对象析构,同时释放t2->a
} // t析构,同时释放t.a

struct Test {
  static int a; // 静态成员变量(基本等同于声明全局变量)
};

int Test::a = 5; // 初始化静态成员变量(主函数前执行,基本等同于初始化全局变量)
int main(int argc, const char *argv[]) {
  std::cout << Test::a << std::endl; // 直接访问静态成员变量
  Test t;
  std::cout << t.a << std::endl; // 通过任意对象实例访问静态成员变量
} // 主函数结束时释放Test::a

static关键字修饰在成员函数前面,称为“静态成员函数”。我们知道普通的成员函数要以对象为主调方,对象本身其实是函数的一个隐藏参数(this 指针):

struct Test {
  int a;
  void f(); // 非静态成员函数
};

void Test::f() {
  std::cout << this->a << std::endl;
}

void Demo() {
  Test t;
  t.f(); // 用对象主调成员函数
}

回到 C++的静态成员函数这里来。用static修饰的成员函数表示“不需要对象作为主调方”,也就是说没有那个隐藏的this参数。

struct Test {
  int a;
  static void f(); // 静态成员函数
};

void Test::f() {
  // 没有this,没有对象,只能做对象无关操作
  // 也可以操作静态成员变量和其他静态成员函数
}

可以看出,这时的静态成员函数,其实就相当于一个普通函数而已。这时的类同样相当于一个命名空间,而区别在于,如果这个函数传入了同类型的参数时,可以访问私有成员,例如:

class Test {
 public:
   static void f(const Test &t1, const Test &t2); // 静态成员函数
 private:
   int a; // 私有成员
};

void Test::f(const Test &t1, const Test &t2) {
  // t1和t2是通过参数传进来的,但因为是Test类型,因此可以访问其私有成员
  std::cout << t1.a + t2.a << std::endl;
}

先到这里吧,由于本人经验有限,如有错误,欢迎修正!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值