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

posted @ 2023-03-19 18:35  CodeMagicianT  阅读(36)  评论(0编辑  收藏  举报