《C++ Primer》第2章 变量和基本类型(下)

2.5 处理类型

随着程序愈加复杂,用的类型也愈加复杂:一是类型名愈加复杂且无法体现其真实含义,二是根本搞不清到底需要的类型是什么。程序员只能回头看上下文寻求帮助。

2.5.1 类型别名

类型别名是一个名字,它是某种类型的同义词

两种方法定义类型别名:

  1. 传统方法:使用关键字typedef:

    typedef double wages;  //wages是double的同义词
    typedef wages base, *p;  //base是double的同义词,p是double*的同义词
    
  2. 新方法:使用关键字using:

    using SI = Sales_item; //SI是Sales_item的同义词
    

类型别名与类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名。

wages hourly, weekly; //等价于double hourly, weekly;
SI item;              //等价于Sales_item item;

指针、常量和类型别名

当某个类型别名指代的是复合类型或常量时,不要试图把类型别名替换成它本来的样子去理解,这样是错误的。

typedef char *pstring; //pstring是char*的别名
const pstring cstr = 0;//cstr是指向char的常量指针
const pstring *ps;     //ps是一个指针,它的对象是指向char的常量指针

上述两条声明语句的基本数据类型都是const pstring,和过去一样,const是对给定类型的修饰。pstring实际上是指向char的指针,也就是说pstring是指针类型,因此,const pstring就是指向char常量指针

如果把它替换成本来的样子去理解

const char *cstr = 0; //这是一个指向常量char的普通指针。所以这种理解方法是错误的。

2.5.2 auto类型说明符

C++11新标准引入了**auto类型说明符**,用它可以让编译器替我们去分析表达式所属的类型

auto让编译器通过初始值推断变量类型,所以auto定义的变量必须有初始值。

//由val1+val2的结果可以推断出item的类型
auto item = val1 + val2; //item初始化为val1+val2的结果

使用auto也能在一条语句中声明多个变量,但该语句中的初始基本数据类型必须一致:

auto i = 0, *p = &i; //正确:i是整数,p是整型指针
auto sz = 0, pi = 3.14; //错误:sz和pi的类型不一致

复合类型、常量和auto

编译器推断出来的auto类型有时候和初始值的类型不完全一致,编译器会适当改变结果类型使其更加符合初始化规则。

  1. 当引用被用作初始值时,编译器以引用对象的类型作为auto的类型。

    int i = 0, &r = i;  
    auto a = r;  //a是一个整数(r是i的别名,而i是一个整数)
    
  2. auto一般会忽略掉顶层const,而保留底层const。

    const int ci = i, &cr = ci; //ci是一个整型常量,cr是对整型常量ci的引用
    auto b = ci;   //b是int(因为ci的顶层const被忽略了,只剩int)
    auto c = cr;   //c是int(因为cr是ci的别名,而ci的顶层const被忽略,只剩int)
    auto d = &i;   //d是整型指针(整数的地址就是指向整数的指针)
    auto e = &ci;  //e是指向整数常量的指针(对常量对象取地址是一种底层const)
    

    如果希望推断出的auto类型是一个顶层const,需要明确指出:

    const auto f = ci; //ci的推演类型是int,f是const int
    
  3. 将引用的类型设为auto时,初始值的顶层const属性仍然保留

    auto &g = ci;    //g是常量引用
    auto &h = 42;    //错误:非常量引用不能绑定到字面值
    const auto &j = 42; //j是常量引用,常量引用可以绑定到字面值
    

要在一条语句定义多个变量,切记,符号&和*只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须为同一类型:

auto k = ci, &l = i;    //k是int(顶层const被忽略),l是整型引用
auto &m = ci, *p = &ci; //m是对整型常量的引用(ci的顶层const被保留),p是指向整型常量的指针(整数的地址就是指向整数的指针,对常量对象取址是一种底层const)
auto &n = i, *p2 = &ci; //错误:i的类型是int,而&ci的类型是const int

2.5.3 decltype类型指示符

decltype:选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到其类型,却不实际计算表达式的值。

decltype(f()) sum = x; //sum的类型就是函数f的返回类型

注:编译器并不实际调用函数 f

如果decltype使用的表达式是一个变量,则decltype会返回该变量的类型(包括顶层const和引用在内):

const int ci = 0, &cj = ci;  //ci是整型常量;cj是整型常量引用,绑定到ci
decltype(ci) x = 0;          //x的类型是const int (ci的顶层cont也被保留了)
decltype(cj) y = x;          //y的类型是const int&,y绑定到x
decltype(cj) z;              //错误:z是引用,必须初始化。

注:引用从来都作为所指对象的同义词出现,只有在decltype处是一个例外。

decltype和引用

如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。

//decltype的结果可以是引用类型
int i =42, *p = &i, &r = i;  
decltype(r+0) b;  //正确:加法的结果是int,因此b是一个未初始化的int
decltype(*p) c;   //错误:c是int&,必须初始化

因为r是一个引用,如果直接decltype(r)则返回的类型是引用类型,引用类型必须初始化。而如果是decltype(r+0)就不一样了,在表达式r+0中,r作为i的别名出现,i+0的返回值是int,所以bint,可以不用初始化。

如果表达式的内容是解引用操作,则decltype将得到引用类型。所以decltype(*p)返回的是int&

decltypeauto的另一处重要区别:decltype的结果类型与表达式形式密切相关。

值得注意的是:如果decltype使用的是一个不加括号的变量,则结果就是该变量的类型

​ 如果给该变量加上一层或多层括号,则会得到引用类型

//decltype的表达式如果是加了括号的变量,结果将是引用
decltype((i)) d;  //错误:d是int&,必须初始化
decltype(i) e;    //正确:e是未初始化的int

切记:decltype((var))的结果永远是引用

​ 而decltype(var)只有当var本身是一个引用时才是引用。

decltype和auto的区别

  1. 对引用变量的不同

    auto将引用变量赋给变量后,变量的类型为引用变量所绑定的变量的类型。而decltype则是为引用类型。即引用在decltype处不作为所绑定对象的同义词出现。

  2. 处理顶层const的方式不同

    auto一般会忽略掉顶层的const,同时底层的const会被保留下来。而decltype 则会返回该变量的完整类型(包括顶层const和引用在内)。

2.6 自定义数据结构

2.6.1 定义Sales_data类

//初步定义Sales_data类
struct Sales_data {
   
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};

以关键字struct开始,紧跟着类名类体(类体部分可为空),类体由花括号包围形成一个新的作用域。

类内部定义的名字必须唯一,但是可以与类外部定义的名字重复。

类体右侧的表示结束的花括号后必须写一个分号。这是因为类体后面可以紧跟变量名以示对该类型对象的定义,所以分号必不可少。

struct Sales_data {
   /*...*/} accum, trans, *salesptr;
//与上一语句等价,但可能更好些
struct Sales_data {
   /*...*/};
Sales_data accum, trans, *salesptr;

类数据成员

  • 每个对象有自己的一份数据成员拷贝。修改一个对象的数据成员,不会影响其他Sales_data的对象。

  • 可以为数据成员提供一个类内初始值。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化,如bookNo将初始化为空字符串。

  • 类内初始值或者放在花括号里,或者放在赋值运算符右边。记住不能使用圆括号

    不能使用圆括号的原因

    因为无法避免这样的情况

    class Widget {
          
    private: 
    typedef int x;
    int z(x);
    };
    

    这样,z的声明成了函数声明。

2.6.3 编写自己的头文件

  • 类通常定义在头文件中,且类所在的头文件的名字与类的名字一样
  • 头文件通常包含那些只能被定义一次的实体,如类、const和constexpr变量等。

预处理器概述

经常出现头文件多次包含的情况,所以有必要进行适当处理。

预处理器可以用来确保头文件多次包含仍能安全工作。

**预处理器是在编译之前执行的一段程序。**如#include

头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。

#define指令把一个名字设定为预处理变量。

#ifdef当且仅当变量已定义时为真。

#ifndef当且仅当变量未定义时为真。

一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。

使用这些功能可以有效地防止重复包含的发生。

#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data {
   
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};
#endif

第一次包含Sales_data.h时,#ifndef检查结果为真,预处理器将顺序执行后续操作直至遇到#endif为止。此时,预处理变量SALES_DATA_H的值将变为已定义,后边如果再一次包含Sales_data.h,则#ifndef检查结果将为假,编译器将忽略#ifndef#endif之间的部分。

  • 预处理变量无视C++语言中关于作用域的规则
  • 整个程序中的预处理变量包括头文件保护符必须唯一,一般把预处理变量的名字全部大写
  • 要习惯性地加上头文件保护符,不管程序需不需要。
全部评论

相关推荐

10-09 22:05
666 C++
找到工作就狠狠玩CSGO:报联合国演讲,报电子烟设计与制造
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务