第二章 变量和基本类型 p31

第二章 变量和基本类型 p31√

2.1 基本内置类型p30

2.1.1 算数类型 p30

带符号类型和无符号类型

int、short、long 和long long 都是带符号的,在前面加上unsigned就可以得到无符号类型。

与其他整型不同,字符型被分为了三种,char、unsigned char、 signed char。char会表现为其中某一种,具体哪种由编译器决定。

unsigned int 可以缩写为unsigned。

2.1.2 类型转换

  • 非布尔类型的算数值赋给布尔类型时,初始值为0则结果为false,否则为true;

  • 当我们把一个布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1;

  • 一个浮点数赋值给整数类型时,进行了近似处理。结果值仅保留浮点数中小数点之前的部分;

  • 当我们把一个整数赋给浮点类型时,小数部分记为0。如果整数超过了浮点类型的容量,精度可能有损失。

  • 当赋给无符号类型一个超出它表示范围的值时,结果是初始值对 无符号类型可以表示的数值的总数 取模后的余数。

    例如:8比特的unsigned char可以表示0~255区间内的值,如果赋了一个区间外的值,则实际结果是该值对256取模后的所得的余数。

    #include <iostream>
    using namespace std;
    
    int main(){
        unsigned char b = 354; // 256 + 98 = 354
        cout << b << endl; // 输出b   354 % 256 = 98 是b对应的asc码值
        return 0;
    }
    
  • 当赋给带符号类型一个超出它表示范围的值时,结果是未定义的。会报错。

含有无符号类型的表达式

当一个算数表达式既有无符号数又有int值时,那个int值就会转换成无符号数

当从无符号数中减去一个值时,不管这个值是不是无符号数,都必须确保结果不能是负值。

切勿混用带符号类型和无符号类型!

2.1.3 字面值常量

一个形如42的值被称作字面值常量。

整型和浮点型字面值

十进制字面值不会是负数。如果我们使用了一个形如-42的负十进制字面值,那个负号并不在字面值之内。它的作用仅仅是对字面值取负号而已。

默认的,浮点型字面值是一个double类型。

字符和字符串字面值
'a'; // 字符字面值
"hello" // 字符串字面值
// 编译器会在每个字符串结尾处添加一个空字符'\0',因此,字符串字面值的实际长度要比它的内容多1。
转义序列p37
指定字面值类型
布尔字面值和指针字面值

true和false是布尔类型的字面值;

nullptr是指针字面值。

2.2 变量p38

变量提供一个具名的、可供程序操作的存储空间。

变量与对象一般可以互换使用。

2.2.1 变量定义p38

首先是类型说明符,随后紧跟一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。

初始值

当对象在创建时获得了一个特定的值,我们说这个对象被初始化了。

列表初始化

用花括号来初始化变量的形式叫做列表初始化。

int a = 0;
int b = {0};
int c{0};
int d(0);

都可以用于初始化。

默认初始化

如果定义变量时没有指定默认初值,则变量被默认初始化

如果内置类型的变量未被显示初始化,它的值由定义的位置决定。定义与任何函数体之外(结构体、类都是函数体之外!)的变量被初始化为0;

**定义在函数体内部的内置类型变量将不被初始化,一个未被初始化的内置类型变量的值是未定义的。**如果试图拷贝或以其他形式访问此类值将引发错误。

类的对象如果没有显示地初始化,则其值由类确定。

2.2.2 变量声明和定义的关系 p41

为了支持分离式编译,C++将声明和定义区分开来。声明使得名字为程序所知,而定义负责创建与名字关联的实体。

变量声明规定了变量的类型和名字。

但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。

如果想声明一个变量而非定义它,就在变量名前面添加关键字extern,而且不要显式的初始化变量:

extern int i; //声明i而非定义i
int j; //定义j

任何包含了显式初始化的声明即成为定义。我们也能给extern关键字标记的变量赋一个初始值,但这样就抵消了extern的作用。extern语句如果包含初始值就不再是声明,而变成定义了:

extern double pi = 3.1415; //定义

在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误(还不清楚是为什么)!!!

#include <iostream>
using namespace std;
extern int b = 6; // 没事
int main(){
    extern int a = 5; // 报错,不允许对外部变量的局部声明使用初始值设定项C/C++(2442)
}

2.2.3 标识符p42

2.4.4 名字的作用域p43

作用域是程序的一部分,C++语言中大多数作用域以花括号分隔(for()也是作用域)。

::默认访问全局作用域

2.3 复合类型p45

复合类型是指基于其他类型定义的类型。C++有几种复合类型,这里先说两种,引用和指针。

2.3.1 引用p45

一条简单的声明语句是由一个基本数据类型和紧随其后的一个声明符列表组成。

引用即别名

引用为对象起了另外一个名字,引用类型引用(refers to)了另外一种类型(int& 引用了int类型)。通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名:

int ival = 1024;
int& refVal = ival; //refVal指向ival(是ival的另一个名字)
int& refVal2; //报错,引用必须初始化
引用的定义

定义引用时,引用就一直和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化

引用即别名,它只是为一个已经存在的对象所起的另外一个名字。

double dval = 3.14;
int refVal2 = dval;//错误,此处引用类型的初始值必须是int型对象
int i = 1, i2 = 2; //都是int
int &r = i, r2 = i2;  //r是一个引用, r2是int类型
int &r3 = i, &r4 = i; //r3, r4都是引用类型

引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。

2.3.2 指针p47

对象是指一块能存储数据并具有某种类型的内存空间。

指针与引用的不同点:

1.指针本身也是一个对象,允许对指针进行赋值和拷贝,同时可以在生命周期内先后指向几个不同的对象;

2.指针与其他内置类型一样,无需在定义时赋初值。在块作用域内定义指针如果没有被初始化,也将拥有一个不确定的值。

定义指针:

int *p1, *p2; //都是指针
int *p3 , p4; //p3是指针,p4是int类型对象
获取对象的地址
int ival = 42;
int *p = &ival; //p存放变量ival的地址,或者说p是指向变量ival的指针

因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。

利用指针访问对象

int ival = 42;
int *p = &ival;
cout<< *p<<endl; //42
空指针

nullptr是C++11新标准引入的一种办法,它是一种特殊的字面值,它可以被转换成任意其它的指针类型。

NULL是一个预处理变量,用来给指针赋值,它在头文件cstdlib中定义,值是0。

int *p1 = nullptr; //等价于 int *p1 = 0;
int *p2 = 0; //直接将p2初始化为字面值常量0;
// 需要首先#include "cstdlib"
int *p3 = NULL; //等价于int *p3 = 0;

把int变量直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行。

void* 指针

void指针是一种特殊的指针类型,可用于存放任意对象的地址。

利用void*指针能做的事儿比较有限:比较,作为函数的输入或输出,或赋给另外的void指针。不能直接操作void指针所指的对象,因为不知道该对象的类型。

2.3.3 理解复合类型的声明p51

int* p1, p2; //p1是指向int的指针,p2是int
指向指针的引用

引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用。

int i = 42;
int *p;
int *&r = p; //r是一个对指针p的引用
r = &i; //r引用了一个指针,因此给r赋值&i就是令p指向i
*r = 0; //解引用r得到i,也就是p指向的对象,将i的值改为0

**要理解r的类型到底是什么,最简单的办法就是从右向左阅读r的定义。**离变量名最近的符号对变量的类型有最直接的影响,因此r是一个引用。

2.4 const限定符p53

因为const对象一旦创建后其值不能再改变,所以const对象必须初始化。

默认情况下,const对象仅在文件内有效

编译器将在编译过程中把用到const对象的地方都替换成对应的值。为了执行该替换,编译器必须知道变量的初始值。如果程序包含多个文件。则每个用了const对象的文件都必须得访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义。为了支持这一用法,同时避免对同一变量的重复定义默认情况下,const对象被设定为仅在文件内有效。当多个文件出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。(人话:因为编译的时候是每个源文件分别编译,然后会把用到const对象的地方都替换成对应的值。也就是说每个源文件都必须访问到const的初始值,好,如果两个文件都定义了一个同名的const对象,那么链接时就会报重定义的错误,因此规定定义在不同文件中同名的const对象是不同的对象)

某些时候有这样一种const变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下我们不希望编译器为每个文件分别生成独立的变量。也就是说,我们想要只在一个文件中定义const,而在其他多个文件中声明并使用它。

解决问题的办法是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了:

//file_1.cpp
extern const int bufSize = 512;
//file_2.cpp
extern const int bufSize;

file_1.cpp定义并初始化了bufSize,因为这条语句包含了初始值,所以显然这是一次定义。然而,因为bufSize是一个常量,必须用extern加以限定使其被其他文件使用。file_2.cpp中声明了bufSize。

Note:如果想要在多个文件中共享const对象,必须在变量的定义之前添加extern关键字。

2.4.1 const的引用p55

对const对象的引用,我们称之为对常量的引用。与普通引用不同的是,对常量的引用不能用作修改它所绑定的对象。

const int ci = 1024;
const int &r1 = ci; // 正确:引用及其对应的对象都是常量
r1 = 42; // 错误:r1是对常量的引用
int &r2 = ci; // 错误:非常量引用不能指向一个常量对象
初始化和对const的引用

之前提到过,引用的类型必须与其所用的引用对象的类型一致但是有两个例外第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许一个常量引用绑定非常量的对象、字面值,甚至是一个表达式,举例:

int i = 42;
const int &r1 = i; // 正确:允许将const int& 绑定到一个普通的int对象上
const int &r2 = 42; //正确:标准引用
const int &r3 = r1 * 2; //正确:标准引用
int &r4 = r1 * 2; //错误:非常量引用的初始值必须为左值,也就是说r1引用的对象是一个右值

为什么可以这样呢?其实是以下过程:

double dval = 3.14;
const int &ri = dval;
//实际过程
const int temp = dval; // 由双精度浮点数生成一个临时的整型常量(临时量)
const int &ri = temp;
// !!!只有当dval和ri的类型不同时,才会有临时量,就像上述代码,如果dval改变,ri引用的变量是不会改变的,因为它绑定了一个临时变量;
//而下述代码,如果dval改变,ri也会变,因为const引用只是不允许通过引用去改变变量,变量自己可以改变自己。
int a = 3;
const int & con_b = a;
cout << con_b << endl; // 3
a = 6;
cout << con_b << endl; // 6
对const的引用可能引用一个并非const的对象

必须认识到,常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是常量未做限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值。

2.4.2 指针和const p56

指向常量的指针(pointer to const)不能用于改变其所指对象的值。想要存放常量对象的地址,只能使用指向常量的指针:

const double pi = 3.14; 
double *ptr = &pi; // 错误:ptr是一个普通指针
const double *cptr = &pi; //正确
*cptr = 42; //错误:不能给*ptr赋值

指针的类型必须与其所指的对象的类型一致,但是有两种情况例外。第一种例外情况是允许一个指向常量的指针指向一个非常量对象:

double dval = 3.14; //值可以改变
cptr = &dval; //正确:但是不能通过cptr改变dval的值
//!!!!!!如果此时改变了dval的值,*cptr是会跟着变得,常量指针只是不允许通过指针去改变值

和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变

所谓指向常量的指针或引用,不过是指针自以为是的罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值。

const指针

常量指针必须初始化,而且一旦初始化完成,它的值就不能在改变了。即不变的是指针本身而非指向的那个值。例如:

int errNumb = 0;
int* const curErr = &errNumb; //curErr将一直指向errNumb
const double pi = 3.14;
const double* const pip = &pi; //pip是一个指向常量对象的常量指针
// ! 从右往左读,首先pip是一个const对象,其次它是一个指向const double类型的指针

2.4.3 顶层const p57 √

顶层const表示指针本身是个常量

底层const表示指针所指的对象是一个常量

更一般的,顶层const可以表示任意的对象是常量,底层const则与指针和引用等符合类型的基本类型部分有关。

特殊的是,指针类型既可以是底层const也可以是顶层const。

int i = 0;
int *const p1 = &i; //不能改变p1的值,这是一个顶层const
const int ci = 42; //不能改变ci的值,这是一个顶层const
const int *p2 = &ci; //允许改变p2的值,这是一个底层const
const int *const p3 = p2;//靠右的const是顶层const,靠左的是底层const
const int &r = ci; //用于声明引用的const都是底层const

底层const的限制不容忽视,当执行对象拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格!一般来说,非常量可以转换成常量,反之则不行。

int *p = p3; // 错误:p3包含底层const含义,而p没有
p2 = p3; // 正确:p2,p3都是底层const
p2 = &i; // 正确,int* 能转换成const int*
int &r = ci; // 错误,普通的int&不能绑定到一个const int上
const int& r2 = i; // 正确:const int&可以绑定到一个普通的int上

2.4.4 constexpr和常量表达式 p58 √

**常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。**显然,字面值属于常量表达式。用常量表达式初始化的const对象也是常量表达式。

constexpr变量

C++11新规定,允许变量声明为constexpr类型以便编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是个常量,而且必须用常量表达式初始化。

constexpr int mf = 20; //20是常量表达式, mf不能再修改了
constexpr int limit = mf + 1; // mf + 1是常量表达式
const int sz = size(); //只有当size()是一个constexpr函数时才是一条正确的声明语句

字面值类型

尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或是存储于某个固定地址中的对象。

函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr不能指向这样的变量。相反的,定义与所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。

指针和constexpr

constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关;

const int *p = nullptr; // p是一个指向整型常量的指针
constexpr int *q = nullptr; //q是一个指向整数的常量指针

与其他常量指针类似,constexpr指针既可以指向常量也可以指向一个非常量:

constexpr int *np = nullptr; //np是一个指向整数的常量指针,其值为空
int j = 0;
// i, j 必须在函数体之外
constexpr int i = 42; 
constexpr const int *p = &i; // p是常量指针,指向整型常量i
const int *p1 = &j;//p1是常量指针,指向整数j
const和constexpr的区别?

constexpr 是 C++ 11 标准新添加的关键字,在此之前(C++ 98/03标准)只有 const 关键字,在实际使用中经常会表现出两种不同的语义。举个例子:

#include <iostream>
#include <array>
using namespace std;

void func1(const int x){
    // 错误,因为x是只读的变量,并不是一个常量。
    array <int, x> myarr{1,2,3,4,5};
    cout << myarr[1] << endl;
}

void func2(){
    const int x = 5; // 这里定义的常量
    array <int, x> myarr{1,2,3,4,5};
    cout << myarr[1] << endl;
}

int main()
{
   dis_1(5);
   dis_2();
}

可以看到,func1() 和 func2() 函数中都包含一个 const int x,但 func1() 函数中的 x 无法完成初始化 array 容器的任务,而 func1() 函数中的 x 却可以。

这是因为,func1() 函数中的“const int x”只是想强调 x 是一个只读(不能修改!)的变量,其本质仍为变量,无法用来初始化 array 容器;而 func2() 函数中的“const int x”,表明 x 是一个只读变量的同时,x 还是一个值为 5 的常量,所以可以用来初始化 array 容器。

C++ 11标准中,为了解决 const 关键字的双重语义问题,保留了 const 表示“只读”的语义,而将“常量”的语义划分给了新添加的 constexpr 关键字。因此 C++11 标准中,建议将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。

在上面的实例程序中,func2() 函数中使用 const int x 是不规范的,应使用 constexpr 关键字。

但有人可能好奇,“只读”的变量不就意味着其是一个常量吗?并不是的,“只读”的变量和常量之间并没有必然的联系,举个例子:

#include <iostream>
using namespace std;

int main()
{
    int a = 10;
    const int& rb = a; // rb是一个常量引用,这个const只是限制我们的权限,使我们不能通过rb去修改a这个变量的内容,但是a可以通过其他方式去修改
    cout << rb << endl;

    a = 20;
    cout << rb << endl;
}

在大部分实际场景中,const 和 constexpr 是可以混用的,例如:

const int a = 5 + 4;
constexpr int a = 5 + 4;

它们是完全等价的,都可以在程序的编译阶段计算出结果。但在某些场景中,必须明确使用 constexpr,例如:

#include <iostream>
#include <array>
using namespace std;

constexpr int sqr1(int arg){
    return arg * arg;
}

const int sqr2(int arg){
    return arg * arg;
}

int main()
{
    array<int,sqr1(10)> mylist1;//可以,因为sqr1时constexpr函数
    array<int,sqr2(10)> mylist1;//不可以,因为sqr2不是constexpr函数
    return 0;
}

其中,因为 sqr2() 函数的返回值仅有 const 修饰,而没有用更明确的 constexpr 修饰,导致其无法用于初始化 array 容器(只有常量才能初始化 array 容器)。

总的来说在 C++ 11 标准中,const 用于为修饰的变量添加“只读”属性;而 constexpr 关键字则用于指明其后是一个常量(或者常量表达式),编译器在编译程序时可以顺带将其结果计算出来,而无需等到程序运行阶段,这样的优化极大地提高了程序的执行效率(解释一下为什么常量表达式可以提高程序的执行效率,假如说两个变量相加,它们一开始肯定都是在内存中的,因此都会经历一个访问内存的过程,但如果是常量表达式相加,也就是立即数,是不需要访存的,减少了指令执行的次数,个人理解)。

2.5 处理类型 p60

2.5.1 类型别名 p60

类型别名的好处是它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。

**传统方法:**typedef

typedef double wages; //wages是double的同义词
typedef wages base, *p; //base是double的同义词, p是double*的同义词

新方法:

using SI = Sales_item; //SI是Sales_item的同义词
指针、常量和类型别名
typedef char* pstring;
const pstring cstr = 0; //cstr是指向char的常量指针
const pstring *ps; //ps是一个 指针,它的对象是一个指向char的常量指针

但有一点需要注意,当我们遇到使用类型别名的声明语句时,不应该把类型别名替换成它本来的样子,比如一下两个语句是不一样的:

const pstring cstr; // 1
const char* ptr; // 2

在1中,pstring的基本数据类型是指针,经过const修饰后,cstr就是一个指向char类型的常量指针;

在2中,基本数据类型是char,*成为了声明符的一部分,经过const修饰后,ptr就是一个指向const char类型的常量指针。

也就是说,在1中const是顶层const,在2中是底层const。

2.5.2 auto类型说明符 p61????

auto定义的变量必须有初始值。

使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样。

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

使用引用其实就是使用引用的对象。

int i = 0, &r = i;
auto a = r; // a是一个整数

其次,auto一般会忽略掉顶层const,同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:

const int ci = i, &cr = ci;
auto b  = ci; //b是一个整数,(ci的顶层const特性被忽略掉了)
auto c = cr; //c是一个整数,(cr是ci的别名,ci本身就是一个顶层const)
auto d = &i; //d是一个整形指针(整数的地址就是指向整数的指针)
auto e = &ci; //e是一个指向整数常量的指针(对常量取地址是一种底层const)
int i = 1;
const int m = 4;
int* r1 = &i;
const int * const r2 = &i;
auto r3 = r2;
r3 = &m;
*r3 = 22;//报错,不会忽略底层const
const int i = 1; 
int m = 4;
const auto r3 = &i; //只要推断出表达式是指针,那么第一行的const就是底层const,不会忽略
r3 = &m; //报错,此时r3同时具有顶层const和底层const
*r3 = 22;//报错,不会忽略底层const,r3指向了m但是他依然具有底层const

const int i = 1;
int m = 4;
auto r3 = i;
r3 = m;//如果是这样,不会报错,i的顶层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的引用时,初始值中的顶层const仍然保留,引用只是一个别名。

const int i = 1;
int m = 4;
auto &r3 = i;
r3 = m;//错误:r3是一个 const int&

2.5.3 decltype类型指示符 p62 !!!??

有时我们会想从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。

所以C++11新标准引入了第二种类型说明符decltype(类型说明符还有const,auto),它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:

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 = 0; //x的类型是const int
decltype(cj) y = x; //y的类型是 const int&, y绑定到变量x
decltype(cj) z; //错误:z是一个引用,必须初始化
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)的结果是引用类型。如果想让结果类型是r所指的类型可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体的值而非引用

!!另一方面,如果表达式的内容是解引用操作,则decltype将达到引用类型。*因为解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(p)的结果类型就是int&,而非int。

decltype和auto的另一重要区别是,decltype的结果与表达式密切相关。有一种情况需要特别注意:

对于decltype所用的表达式来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。

如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;

如果给变量加上了一层或多层括号,编译器就会把它当成一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型:

int i = 42;
decltype((i)) d; //错误:d是int&, 必须初始化
decltype(i) e;//正确:e是一个(为初始化)的int

切记:decltype((variable))的结果永远是引用,而decltype(variable) 结果只有当variable本身就是一个引用时才是引用。

https://blog.csdn.net/u014609638/article/details/106987131/

2.6 自定义数据结构 p64

2.6.1 定义Sales_data类型

用户能直接访问其中的数据元素。

struct Sales_data{
    std::string bookNo; //ISBN
    unsigned units_sold = 0; //销售数量
    double revenue = 0.0; //收入
};

类数据成员

C++11新标准规定,可以为数据成员提供一个类内初始值。创建对象时,类内初始值将用于初始化数据成员。没有初始化的成员将被默认初始化。

2.6.2 使用Sales_data类

实现求两次交易相加结果的功能。

0-201-78345-X 3 20.00

0-201-78345-X 2 25.00

添加两个Sales_data对象

#include "iostream"
#include "string"
#include "Sales_data.h"
int main(){
    Sales_data data1, data2;
    //读入data1和data2的代码
    //检查data1和data2的ISBN是否相同的代码
    //如果相同,求data1和data2的总和
}

Sales_data对象读入数据

double price = 0; //书的单价、用于计算销售输入
//读入第一笔交易:ISBN、销售数量、单价
std::cin>>data1.bookNo>>data1.units_sold>>price;
data1.revenue = data1.units_sold * price;
//读入第二笔交易:ISBN、销售数量、单价
std::cin>>data2.bookNo>>data2.units_sold>>price;
data2.revenue = data2.units_sold * price;

输出两个Sales_data对象的和

2.6.3 编写自己的头文件

预处理器概述

**确保头文件多次包含仍能安全工作的常用技术是预处理器。预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。#include就是一项预处理功能。**当预处理器看到#include标记时就会用指定的头文件的内容代替#include。

C++程序还会用到的一项预处理功能是头文件保护符,头文件保护符依赖于预处理变量。

预处理变量有两种状态:已定义和未定义。#define指令把一个名字设为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:

#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到**#endif**指令为止。

使用这些功能就能有效地防止重复包含的发生:

#ifndef SALES_DATA_H //如果没有定义SALES_DATA_H预处理变量
#define SALES_DATA_H //定义预处理变量SALES_DATA_H
#include <string>
struct Sales_data{
    std::string bookNo; //ISBN
    unsigned units_sold = 0; //销售数量
    double revenue = 0.0; //收入
};
#endif

第一次包含Sales_data.h时,#ifndef的检查结果为真,预处理器顺序执行后面的操作直至遇到#endif为止。**此时预处理变量SALES_DATA_H的值变为已定义,而且Sales_data.h也会被拷贝到我们程序中来。**后面如果再一次包含Sales_data.h,则#ifndef的检查结果将为假,编译器将忽略#ifndef到#endif之间的部分。

预处理变量无视C++语言中关于作用域的规则。

整个程序中的预处理变量包括头文件保护符必须唯一,通常是基于头文件中类的名字来构建保护符的名字,以确保其唯一性。为了避免与程序中其他实体名字发生冲突,一般把预处理变量的名字全部大写。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值