69.处理类型
1.类型别名
随着程序越来越复杂,程序中用到的类型也越来越复杂,这种复杂性体现在两个方面。一是一些类型难于“ 拼写 “,它们的名字既难记又容易写错,还无法明确体现其真实目的和含义。 二是有时候根本搞不清到底需要的类型是什么,程序员不得不回过头去从程序的上下文中寻求帮助。
1.1类型别名
类型别名(type alias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。
有两种方法可用于定义类型别名。传统的方法是使用关键字typedef:
typedef double wages; / /wages是double的同义词
typedef wages base, *p; //base是double的同义词,p是double*的同义词
新标准规定了一种新的方法, 使用别名声明(alias declaration)来定义类型的别名:
using SI = Sales_Item;//SI是Sales_item的同义词
指针、常量和类型别名
如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。例如下面的声明语句用到了类型pstring,它实际上是类型char*的别名:
typedef char *pstring;
const pstring cstr = O;//cstr是指向char的常量指针
const pstring *ps;//ps是一个指针,它的对象是指向char的常量指针
const char *cstr = 0;//是对const pstring cstr的错误理解
这种理解是错误的。声明语句中用到pstring时,其基本数据类型是指针。可是用char*重写了声明语句后,数据类型就变成了char,*成为了声明符的一部分。这样改写的结果是,const char成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
typedef char* pstring;
int main()
{
char ch = 'A';
const pstring cstr = &ch;
cout << "*cstr = " << *cstr << endl;
*cstr = 'B';
cout << "*cstr = " << *cstr << endl;
char st = 'X';
//cstr = &st;//错误 cstr应该是常量
return 0;
}
char *<=> pstring
const pstring cstr = 0;
cstr是指针没有问题,但是这个指针只能指向一个对象,却可以通过指针修改指向的对象
总结:cstr应该是一个指向变量的指针常量。
const char *cstr = 0; -->指向常量的指针变量
1.2auto类型说明符
C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应一种特定类型的说明符(比如double)不同,auto让编译器通过初始值来推算变量的类型。显然,auto定义的变量必须有初始值:
//由vall和val2相加的结果可以推断出item的类型
auto item= val1 + val2;//item初始化为val1和val2相加的结果
此处编译器将根据val1和val2相加的结果来推断item的类型。
使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto i = 0, *p = &i;//正确:i是整数
auto sz = 0, pi= 3.14;//错误:sz和pi的类型不一致
复合类型、常量和auto
编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。
首先,正如我们所熟知的,使用引用其实是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为auto的类型:
int i= 0,&r = i;
auto a = r;//a是个整数(r是i的别名,而i是一个整数)
其次,auto一般会忽略掉顶层const(参见C++ Primer2.4.3节,第57页), 同时底层const则会保留下来, 比如当初始值是一个指向常量的指针时:
const int ci = i, &cr = ci;
auto b = ci;//b是一个整数(ci的顶层const特性被忽略掉了)
auto c = cr;//c是一个整数(er是ci的别名,ci本身是一个顶层const)
auto d = &i;//d是一个整型指针(整数的地址就是指向整数的指针)
auto e = &ci;//e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f = ci; //ci的推演类型是int, f是const int
还可以将引用的类型设为auto,此时原来的初始化规则仍然适用:
auto &g = ci; //g是一个整型常量引用,绑定到ci
auto &h = 42; //错误:不能为非常量引用绑定字面值
const auto &j = 42; //正确:可以为常量引用绑定字面值
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了。
要在一条语句中定义多个变量,切记,符号&和*只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须是同种类型:
auto k = ci, &l = i; //k是整数,1是整型引用
auto &m = ci, *p = &ci;//m是对整型常量的引用,p是指向整型常量的指针
//错误:i的类型是int而&m的类型是const int
auto &n = i, *p2 = &ci;
2.decltype
2.1什么是decltype
decltype是C++11新增的一个关键字,和auto的功能一样,用来在编译时期进行自动类型推导。引入decltype是因为auto并不适用于所有的自动类型推导场景,在某些特殊情况下auto用起来很不方便,甚至压根无法使用。
auto varName=value;
decltype(exp) varName=value;
● auto根据=右边的初始值推导出变量的类型,decltype根据exp表达式推导出变量的类型,跟=右边的value没有关系
●auto要求变量必须初始化,这是因为auto根据变量的初始值来推导变量类型的,如果不初始化,变量的类型也就无法推导
●而decltype不要求,因此可以写成如下形式
decltype(exp) varName;
原则上讲,exp只是一个普通的表达式,它可以是任意复杂的形式,但必须保证exp的结果是有类型的,不能是void;如exp为一个返回值为void的函数时,exp的结果也是void类型,此时会导致编译错误。
在此过程中, 编译器分析表达式并得到它的类型, 却不实际计算表达式的值:
decltype(f()) sum = x; //sum的类型就是函数f的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。换句话说,编译器为sum指定的类型是什么呢?就是假如f被调用的话将会返回的那个类型。
decltype处理顶层const和引用的方式与auto有些许不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):
const int ci= 0, &cj = Ci;
decltype(ci) x = O; //x的类型是const int
decltype(cj) y = x;// y的类型是const int &, y绑定到变量x
decltype(cj) z;//错误:z是个引用,必须初始化
因为cj是一个引用,decltype(cj)的结果就是引用类型,因此作为引用的z必须被初始化。
需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在decltype处是个例外。
————————————————
版权声明:本文为CSDN博主「qq_38196982」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_38196982/article/details/118578967
2.2decltype的推导规则
decltype(exp) varname = value;
●如果表达式exp是一个不被括号()包围的表达式,或者是个类成员访问表达式,或者是一个单独的变量,那么decltype(exp)的类型就是和exp一致的;
●如果exp是函数调用,那么decltype(exp)的类型就和函数的返回值类型一致,但是注意一点就是函数的返回值不能是void;
●如果exp是一个左值,或者被括号()包围,那么decltype(exp)的类型就是exp的引用,假如exp的类型为T,那么decltype(exp)的类型就是T&。
2.2.1.表达式为单独变量
#include <QCoreApplication>
#include <iostream>
#include<string>
using namespace std;
class SERVEN_PAR
{
public:
static int ser1; // 类静态成员变量在类中不能初始化(类静态成员变量类内声明类外初始化)
string ser2;
int ser3;
float ser4;
};
int SERVEN_PAR::ser1 = 1; // 初始化类的静态变量
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
int n = 0;
const int& r = n;
SERVEN_PAR SERVEN;
decltype(n) b = n; // n为int类型,b被推导为int类型
decltype(r) c = n; // r为const int&类型,c被推导为const int类型
decltype(SERVEN_PAR::ser1) d = 0; // ser1位类的一个int类型成员,那么d被推导为int类型
decltype(SERVEN.ser2) e = "https://www.baidu.com"; // ser2位类的一个string类型,那么e被推导为string类型
return a.exec();
}
2.2.2表达式为函数调用
#include <QCoreApplication>
#include <iostream>
#include<string>
using namespace std;
int& func_int_first(int, char); // 返回值为int&
int&& func_int_second(void); // 返回值为int&&
int func_int(double); // 返回值为int
const int& func_cint_first(int,int,int); // 返回值为const int&
const int&& func_cint_second(void); // 返回值为const int&&
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
int n = 10;
decltype(func_int_first(100,'S')) b = n; // b的类型为int&, 这里要赋初始值,因为int&引用需要初始化的,但是这里不能直接赋数值,因为这是左值引用
decltype(func_int_second()) c = 0; // c的类型为int&&,这里需要赋初始值,这是右值引用,所以可以直接赋数值
decltype(func_int(10.5)) d = 0; // d的类型为 int
decltype(func_cint_first(1,2,3)) x = n; // x 的类型为 const int &
decltype(func_cint_second()) y = 0; // y 的类型为 const int&&
return a.exec();
}
值得注意的是:这里的表达式只使用了函数的返回类型,不会执行函数里面的函数体内容的。代码中decltype(func_int_first(100,'S')) b = n;b的类型为int&, 这里要赋初始值,因为int&引用需要初始化的,但是这里不能直接赋数值,因为这是左值引用。而decltype(func_int_second()) c = 0;c的类型为int&&,这里需要赋初始值,这是右值引用,所以可以直接赋数值。
2.2.3表达式为左值,或者被()包围
#include <QCoreApplication>
#include <iostream>
#include<string>
using namespace std;
class SERVEN_PAR
{
public:
int x;
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
SERVEN_PAR SERVEN;
/* 带有括号的表达式 */
decltype(SERVEN.x) b;
decltype((SERVEN.x)) c = b;
/* 加法表达式 */
int n = 0, m = 0;
decltype(n+m) d = 0; // n+m得到一个右值,复合推导规则一,推导结果为int
decltype(n = n+m) e = c; // n=n+m得到一个左值,复合推导规则三,推导结果为int
return a.exec();
}
上面的代码中包括了带有括号的表达式,和加法表达式两种,其中加法表达式里面分为了左值和右值,左值是指那些在表达式执行结束后依然存在的数据,也就是持久性的数据;右值是指那些在表达式执行结束后不再存在的数据,也就是临时性的数据,对表达式取地址操作,如果编译器报错了那就是右值,如果编译器不报错那就是左值。
3.decltype的应用
我们知道了auto的使用比较简单,所以在一般的类型推导过程中,使用auto会比decltype更加方便。但是auto只能用于类的静态成员,不能用于类的非静态成员(普通成员),如果我们想推导非静态成员的类型的话,那么就得使用decltype关键字了,下面是一个模板的定义:
#include <vector>
using namespace std;
template <typename T>
class SERVEN_PAR {
public:
void func(T& container) {
m_it = container.begin();
}
private:
typename T::iterator m_it; //注意这里
};
int main(){
const vector<int> v;
SERVEN_PAR<const vector<int>> obj;
obj.func(v);
return 0;
}
单独看SERVEN_PAR类中的m_it成员的定义,很难看出有什么错误,但是在使用SERVEN_PAR类的时候,如果传入一个const类型的容器,编译器马上就会弹出一大堆错误。原因就在于T::iterator并不能包括所有的迭代器类型,当T是一个const容器时,应用使用const_iterator。要想解决这个问题,在之前的 C++98/03 版本下只能想办法把 const 类型的容器用模板特化单独处理,增加了不少工作量,看起来也非常晦涩。但是有了 C++11 的 decltype 关键字,就可以直接这样写:
template <typename T>
class SERVEN_PAR
{
public:
void func(T& container) {
m_it = container.begin();
}
private:
decltype(T().begin()) m_it; //注意这里
};
————————————————
版权声明:本文为CSDN博主「三贝勒文子」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43340455/article/details/124868554