C++ 语言特性


本章内容概述

本文用于笔者学习 C++ 部分语言特性时记录笔记,主要内容包含左值和右值、指针、类型转换、模板等新特性,是十分重要的知识点,也是完善 C++ 知识板块的必经之路。因为是新特性,所以难免有难以理解之处,对于难以理解的地方,笔者的方法是思考这项特性产生的原因,是什么需求促使这项特性的产生,以及这样的特性体现在哪些地方,带来了怎样的好处。思考着几个问题,相信会对理解特性有所帮助,也会更加深刻。


一、左值和右值

左值和右值无疑是新特性中尤为重要的一个知识点,对他的理解和使用无必要深刻和熟练。

左值,即传统意义上的变量或对象,存放在具体的地址中,并可以通过取地址运算符查看在内存中的地址,并且可以被赋值,即可以在 “=” 左侧,即便赋值结束后,该变量依然存在。

右值,则实行提出的概念,专指“一闪而过”的变量,不可取地址,因为他们只短暂的存在一瞬间,在完成表达式赋值以后,就会被立刻销毁,因此右值只能出现在 “=” 右侧,可以通过代码初步体会:

// x 是左值,666 为右值
int x = 666;   	// ok 
int *y = x; 	// ok
int *z = &666; 	// error
666 = x; 		// error
int a = 9; 		// a 为左值
int b = 4; 		// b 为左值
int c = a + b; 	// c 为左值 , a + b 为右值
a + b = 42; 	// error

对右值有了简单认识后,再次进行测试:

int gi = 10;
int setgi() { return gi; }
int& Setgi() { return gi; }

//setgi() = 10; 表达式左侧必须是可修改的左值
Setgi() = 20;

可以得出结论,函数返回值也有左值和右值的区分,如果仅按值传递,那么返回的就是右值,仅当按引用传递或地址传递时,才会返回左值。

1.左值引用

左值引用可以区分为非常量左值引用和常量左值引用,其中,常量左值引用可以绑定到非常量左值、常量左值和右值,但是非常量左值引用只能绑定到非常量左值,不能绑定到常量左值和右值。

这一点,笔者的理解是,对于非常量左值引用,理所应当可以绑定到非常量左值,但是不能绑定到常量左值,因为常量左值不能被修改,而之所以不能被绑定到右值,则是因为非常量左值引用绑定的值随时可能被修改,但右值作为随时被销毁的值,如果是在销毁后又被修改,则会出现非法访问内存,因此非常量左值引用只能绑定到非常量左值。

那么,为什么常量左值引用可以绑定到任意值呢?首先,理所应当可以绑定非常量左值和常量左值,容易理解;对于右值,在绑定到常量左值引用后,虽然右值随时可能被销毁,但是常量左值引用只允许读,不会被修改,即便右值被销毁,也不会出现非法访问未知内存,这样理解会更容易接受。

但是事实上,如果一个右值被绑定在常量左值引用后,那么这个右值的生命周期就会被延长,直到引用被销毁,从而不会因此产生悬空的引用,代码如下:

class A
{ public: A() { cout << "A construct" << endl; }
	      ~A() { cout << "A destruct" << endl; }
};

produce();
//const A& a = produce();
cout << 1 << endl;

//输出结果为
//A construct
//A destruct
//1

但是当绑定常量引用后:

//produce();
const A& a = produce();
cout << 1 << endl;

//输出结果为
//A construct
//1
//A destruct

可以看出,右值的生命周期延长到了程序运行结束。

2.右值引用

相比于左值引用,右值引用更为简单一些,只能绑定到右值上,它的声明主要是告知编译器传递的参数是一个即将被销毁的值,如果利用该值拷贝的话,可以自由移动它的资源,无需额外拷贝,从而减少资源浪费。

简而言之,右值引用消除了两个对象交互时不必要的拷贝带来的额外资源浪费,同时可以更简洁明确的定义泛型函数,代码如下:

void produce(A a) { }

A a;
produce(a);

//输出结果
//A construct
//A copy construct
//A destruct
//A destruct

如果使用右值引用的话,代码如下:

void produce(A a) { }

produce(A());

//输出结果
//A construct
//A destruct

原地生成一个临时对象后,并不调用拷贝构造,而是直接传递,完成后直接析构,节约资源。

3.左值与右值的转化

std::move 函数,支持将一个左值转化成右值,继而方便使用移动语义,完成资源转移,代码如下:

void fun(int& i)  { cout << "fun lv ref" << endl; }
void fun(int&& i) { cout << "fun rv ref" << endl; }

int a = 10;

fun(10);	//fun rv ref
fun(a);		//fun lv ref

fun((int&&)a);		//fun rv ref
fun(move(a));		//fun rv ref
fun(static_cast<int&&>(a));		//fun rv ref
fun(forward<int&&>(a));			//fun rv ref

可以看到,有四种方法可以完成转换:C 风格的类型转换,move 函数转换,static_cast 强制类型转换,forward <T&&> 转换。

4.引用折叠

当出现多重引用折叠的情况时,除了右值引用和右值引用重合仍为右值引用,其余叠加情况全部叠加为左值引用。

5.万能引用类型

在模板中,T&& t 作为未定义引用类型,会发生自动类型推断,它既可以接受左值,也可以接收右值,取决于初始化的值的类型,并与之保持一致。

使用模板类型右值引用定义,但是却极有可能是左值,也有可能是右值,利用这一点,可以实现移动语义和完美转发。

二、指针

1.基本使用

指针的定义和基本使用属于基本内容,此处不在赘述。

2.野指针和悬空指针

2.1悬空指针

一个指针指向一处内存空间,当这块内存空间存储的对象被释放后,该指针仍然指向这处空间,如果此时再次利用该指针对指向内存进行操作,则会出现意想不到的错误。这样的指针被称为悬空指针。

因此,当释放掉一处内存空间安后,应当立刻将其置空,避免非法访问。

2.2野指针

野指针,是指未经过初始化的指针,指向内存完全随机,极易导致非法访问,因此指针被定义后应当尽快初始化,使用完毕后也应当立刻置空。

3.C++11 nullptr

相比于 NULL,nullptr 具备一定的优势:NULL 作为一个预处理变量,是一个宏定义,值为0,定义在 对应头文件中,即 #define NULL 0。而 nullptr 作为关键字,是一种特殊类型的字面值,本身具有类型,可以转化为任意类型,更为严谨,可以避免特殊情况下的函数重载因 NULL 产生意外匹配的情况。

4.指针和引用

指针是一个变量,引用是一个别名。

指针在内存中有具体的存储地址,但引用没有;指针定义后可以修改指向,但引用一旦绑定无法修改。

5.函数指针

函数指针,即指向函数的指针,可以利用函数指针完成对函数的调用,代码如下:

int mul(int x,int y) { return x*y; }
int div(int x,int y) { return x/y; }

//函数指针定义
//returnvalue (*ptrname) (paramlist)
int (*fun)(int x,int y); 

fun=mul;
cout<<fun(15,5)<<endl;	//75
fun=div;
cout<<fun(15,5)<<endl;	//3

需要注意的是,函数指针在初始化时有两种方式,代码如下:

fun=mul;
fun=&mul;

在第一种方式中,mul 作为函数首地址,被赋值给 fun;在第二种方式中,mul 作为函数对象,&mul 作为函数对象指针,被赋值给 fun 。

三、强制类型转换

C++ 相比于 C ,对类型转换的要求十分严格,甚至禁止了某些类型转换,但也提供了强制类型转换的方法,分别应用于不同情况。

1.static_cast

static_cast,意为“静态转换”,即在编译期间转换,如果转换失败则会抛出错误,适用情况有:

基本数据类型的转换和数据强制类型转换:将一种数据类型转换为另一种数据类型,但是指针不可以,代码如下:

int a = static_cast<int>(10.7);

//int* p;
//double* pd = static_cast<double*>(p); 不可以进行基本数据类型之间的转换

类层次之间的上行转换:将子类(引用或指针)转换为父类(引用或指针),但是需要注意的是,只能做类之间的上行转换,不能进行下行转换,因为没有动态类型检查,是不安全的,代码如下:

base b = static_cast<base>(derive());
base* pb = static_cast<base*>(&derive());

//derive d = static_cast<derive>(base()); 不可以下行转换

指针与空指针转换:可以将空指针转换为目标类型的空指针,代码如下:

void* pN;
base* b = static_cast<base*>(pN);

表达式类型转换:可以将任何类型的表达式转换为 void 类型。

2.const_cast

常量转换,主要适用于 const 和非 const、volatile 和非 volatile 之间的转换,可以强制去除常量属性,但是只能用于去除常量指针和常量引用的常量属性,不可以去除常变量的常量属性。或许会产生疑惑,既然将它声明为常量引用,就是不希望修改它,为什么又要去除引用的常量属性呢?这是因为在某些情况下,必须将常量指针传入参数列表中声明为普通指针的函数中,可以保证在函数中不会对其进行修改,从而人为保证安全性,以通过编译。代码如下:

const int ci = 10;
const int* pci = &ci;
int* fpci = const_cast<int*>(pci);

cout << ci << endl;		//10
cout << *pci << endl;	//10
cout << *fpci << endl;	//10

cout << pci << endl;	//009CF744
cout << &ci << endl;	//009CF744
cout << fpci << endl;	//009CF744

既然常量指针被去除了常量属性,那么对其进行修改会如何呢?代码如下:

const int ci = 10;
const int* pci = &ci;
int* fpci = const_cast<int*>(pci);

*fpci = 20;

cout << ci << endl;		//10
cout << *pci << endl;	//20
cout << *fpci << endl;	//20

cout << pci << endl;	//009CF744
cout << &ci << endl;	//009CF744
cout << fpci << endl;	//009CF744

可以观察到很有趣的现象,指针指向的值发生了改变,但原变量的值却并未改变,而且它们三者的地址竟然仍保持一致。事实上,这种赋值行为属于未定义行为,是十分不建议的,去除常量属性的初衷是在人为保证安全性的情况下通过编译,绝不是为了修改内容,因此对去除常量属性后的指针做修改已经损害了安全性,是不被建议的行为。

3.reinterpret_cast

重解释转换,可以用来处理无关类型之间的转换:产生一个新的值,这个值会有与原始参数有完全相同的比特位,执行时按照逐个比特复制,从而完成指针类型、指针到整型、整型到指针的转换,一般不建议使用。

4.dynamic_cast

动态类型转换,可以动态实现父类与子类指针之间的转换,会检查指针指向的对象类型和转化后的类型,相同时才会安全,否则置空,只能用于父类含有虚函数,因此更为安全。

四、模板

1.基本定义

基本的使用方法属于基础内容,此处不再赘述。

2.函数模板和类模板

函数模板和类模板有所不同:

实例化方式不同,函数模板实例化由编译器处理函数调用时自动实例化,但是类模板必须显式实例化后才可使用。

默认参数,函数模板不支持默认参数,类模板模板参数列表中可以定义默认参数。

特化,函数模板只能全特化,类模板可以偏特化。

调用方式,函数模板可以显式调用,也可以隐式调用,类模板只能显式调用。

3.可变参数模板

数量不定的模板参数,使用时无需固定参数数量,可以以“包”的形式传入,在内部进行处理,代码如下:

void aprint() { }

template<typename T,typename...Types>
void aprint(const T& f, const Types&...args)
{
	cout << f << endl;
	aprint(args...);
}

递归调用,逐步处理内部数据。

4.模板特化

针对某些特定类型的初始化方式,类模板或函数模板可以有独特的处理方式,比如确定类型为指针,需要专门对指针进行某些与其他普通数据类型不同的操作,模板可以进行特化。

全特化,将全部待确定类型变量进行指定,全部特例化。

偏特化,只对部分待确定类型参数指定类型,剩余部分需要编译器在编译阶段确定。

5.迭代器

迭代器作为访问容器内部元素的工具,是一种概念上的抽象,它使得可以访问容器内部元素而无需暴露容器内部的表达方式。迭代器基本分为五种:

输入迭代器:只能向前单步迭代元素,不允许修改;

输出迭代器:只能向前单步迭代,只有写权限;

向前迭代器:可以在区间内进行读写操作,可以向前单步迭代;

双向迭代器:可以在区间内向前向后迭代,可进行读写,可单步迭代;

随机迭代器:具备以上四种迭代器全部功能,且可进行加减。

6.泛型编程

在模板、容器和迭代器的支持下,便可以进行泛型编程。泛型编程是一种思想,可以一定程度上扩展代码的可复用性,并且效率较高,编译器简便可确定静态类型信息,同时类型检查严格,容易发现错误。


本章总结

本章首先探讨了

最后,我是Alkaid#3529,一个追求不断进步的学生,期待你的关注!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alkaid3529

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值