magnet:?xt=urn:btih:11ec36a15221c8df473d62ff15f0ed7d797d3da3
magnet
🐱‍🚀02_变量和基本类型 - 图1

基本内置类型

算术类型


算术类型分为两类:整型( integral type,包括字符和布尔类型在内)和浮点型

  • 算数类型的尺寸在不同的计算机上有差别、
  • 整数类型包含带符号类型和不带符号类型,混用会出现问题
  • C++支持从一种类型转换成另一种类型,但是这种转换会带来一定问题

:::tips 当一个算数表达式中既有无符号数又有有符号数时,int值会变成无符号数 :::

  1. usigned u=10;
  2. int i=-42;
  3. cout<< u+i<<endl;//int占位32位,输出4294967264;

字面值

比如直接输入42,42即为字面值常量,字面值常量的类型取决与其形式和值

  • 算术类型:整型和浮点型
  • 字符类型:字符串和字符
  • 转义序列:参考ASIIC :::info 此外可以通过加前缀或者后缀指定字面值的位宽等; :::

    择类型的一些经验准则

  • 当明确知晓数值不可能为负时,选用无符号类型。

  • 使用int执行整数运算。在实际应用中,short常常显得太小而long一般和int有一样的尺寸。如果你的数值超过了int的表示范围,选用long long

    变量


    变量提供一个具名的、可供程序操作的存储空间。 C+ + 中的每个变掀都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式、该空间能存储的值的范围,以及变品能参与的运算。对C+ + 程序员来说,“变量(varible) “和“对象(object) “一般可以互换使用。

    变量定义的基本形式

    1. int sum=0,value,
    2. units_sold=0;
    :::info 同一类型的可以在同一个数据类型声明后一起定义,多个变量用逗号隔开 :::

    初始化

    1. double price =199.99, discount=price*0.16;
    当一次定义了两个或多个变量时,对象的名字随着定义也就马上可以使用了。因此在同一条定义语句中,可以用先定义的变员值去初始化后定义的其他变址。 :::tips 但是应该记住需要先定义在使用,例如 :::
    1. double salary=wage=999.99;// wrong
    2. double wage;
    3. double salary=wage=999.99;//right
    :::info 初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。 :::

初始化的好几种不同形式

  1. int units_sold=0; // 普通的方式
  2. int units_sold={0}; //列表初始化
  3. int units_sold{0}; //列表初始化
  4. int units_sold(0) //普通初始化

列表初始化

作为C++11新标准的一部分,用花括号来初始化变属得到了全面应用
这种初始化的形式被称为列表初始化(list initialization)。现在,无论是初始化对象还是某些时候为对象赋新值,都可以使用这样一组由花括号括起米的初始值了

  1. long double ld=3.1415926536;
  2. int a{ld}, b={ld};// wrong
  3. int c(ld),d=ld;// right, converted!

:::tips 当用于内置类型的变址时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错 :::

默认初始化


如果定义变量时没有指定初值,则变址被默认初始化( default initialized),此时变属被赋予了“默认值"。默认值到底是什么由变最类型决定,同时定义变量的位置也会对此有影响。

如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。

  • 定义在外部的变量会被初始化为0
  • 定义在函数体内部的内置类型变量将不被初始化(uninitialized)。
  • 类若定义了默认构造函数,则会执行默认初始化

变量声明和定义


为了支持分离式编译,C++语言将声明和定义区分开来。声明( declaration) 使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义( definition)负责创建与名字关联的实体。 :::tips

  • 声明变量会指出变量的名字和类型,定义变量会在此之外还申请存储空间
  • 如果想声明一个变量而非定义它,就在变量名前添加关键字extern ,而且不要显式地初始化变量 :::
    1. extern int i;//声明而非定义
    2. int j;//声明并定义
    变量可以被声明多次,但是定义只能是一次。 此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。

作用域( scope)


作用域是程序的一部分,在其中名字有其特定的含义。C ++语言中入多数作用域都以花括号分隔。
同一命名可以在不同作用域中指代不同的实体,区分方式是看声明在哪个作用域。

作用域的嵌套


作用域能彼此包含,被包含(或者说被嵌套)的作用域称为内层作用域(inner scope) , 包含着别的作用域的作用域称为外层作用域(outer scope).
edge.PNG
输出#3 使用作用域操作符来覆盖默认的作用域规则,因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量。

复合类型(compound type)


复合类型(compound type) 是指基于其他类型定义的类型。

声明

  • 一条声明语句由一个基本数据类型( base type)和紧随其后的一个声明符( declarator)列表组成。每个声明符命名了一个变量并指定该变品为与基本数据类型有关的某种类型。
  • 声明符其实就是变量名,此时变量的类型也就是声明的基本数据类型。其实还可能有更复杂的声明符,它基于基本数据类型得到更复杂的类型,并把它指定给变量。

    引用


    引用( reference) 为对象起了另外一个名字,引用类型引用( refers to) 另外一种类型。

    1. int i=1024,i2=2048;
    2. int &r=i,r2=i2; //r是引用,r2是int
  • 定义引用时,程序把引用和它的初始值绑定( bind)在一起,而不是将初始值拷贝给引用。

  • 引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字
  • 一个语句中可以定义多个引用,但是每个引用表示符都需要用&开头
  • 引用的对象必须是一个变量,除非使用const限定符。
  • 引用必须初始化!

指针


与引用类似,指针也实现了对其他对象的间接访问。

:::tips C.F.
其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。 :::

取地址

指针存放某个对象的地址,要想获取该地址,需要使用取地址符(操作符&)

  1. int ival=42;
  2. int *p=&ival;


如果指针指向了一个其他类型的对象,对该对象的操作将发生错误

  1. double dval;
  2. double *pd=dval;//right
  3. int *pi=pd; //wrong
  4. pi=&dval; // wrong
  5. const int* cp;
  6. int * p=cp; // wrong

指针的值


指针的值(即地址)应属下列4 种状态之一 :::tips

  • 指向一个对象。
  • 指向紧邻对象所占空间的下一个位置。
  • 空指针,意味着指针没有指向任何对象。
  • 无效指针,也就是上述情况之外的其他值。 :::

解引用符

  1. int ival=42, *p=&ival;
  2. cout<< *p;


如果指针指向了一个对象,则允许使用解引用符(操作符*)来访问该对象:
某些符号有多重含义
edge.PNG
但是由于含义截然不同,所以我们完全可以把它当作不同的符号来看待。

空指针

得到空指针最直接的办法就是用字面值nullptr来初始化指针,这也是C++11新标准刚刚引入的一种方法。nullptr是一种特殊类型的字面值
int变量直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行

:::tips 建议初始化所有的指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0。这样程序就能检测并知道它没有指向任何具体的对象了。 :::

赋值和指针


引用本身并非一个对象。一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。
指针和它存放的地址之间就没有这种限制了。和其他任何变量(只要不是引用)一样,给指针赋值就是令它存放一个新的地址,从而指向一个新的对象
有时不好区分赋值对象,需谨记赋值永远改变的是等号左侧的对象

void*指针


void*是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解

  • void*指针可以:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针
  • 但是不能直接操作void*指针指向的对象,因为不知道器类型。

概括说来,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象,也就是说既不能访问,也不能修改,只能传递给其他指针。

复合类型声明


变量的定义是包括一个基本数据类型和一组声明符,在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同。也就是说,一条定义语句可能定义出不同类型的变量。

定义多个变量

类型修饰符*&并不是作用与本次定义的全部变量的。修饰符只是修饰一个变量

  1. int *p1,p2;//p1是指向int的指针,p2是int

指针的指针


通过*的个数可以区分指针的级别。也就是说,**表示指向指针的指针,***表示指向指针的指针的指针,以此类推
edge.PNG :::tips int **可以这样读吗?int* *p即数据类型是int*(指向int的指针),声明符为*。因此为指向指针的指针,被指向的指针指向一个int变量(好拗口) :::

对指针的引用


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

edge.PNG
最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响 :::tips 这样理解:int*&r,其中数据类型是int*,声明符为&,因此表示了对指针的引用。 :::

const限定符

若是希望定义变量的值不被改变,则可以用const限定符修饰他 :::tips

  • 因为const对象一旦创建后其值就不能再改变,所以**const**对象必须初始化
  • 一如既往,初始值可以是任意复杂的表达式并且,只能在const类型的对象上执行不改变其内容的操作 :::

const作用域

在默认的情况下,const对象仅仅在文件内有效。当多个文件内出现了同名的const变量时,这些变量并不是同一个,而是独立的变量。

某些时候有这样一种const变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。
解决的办法是,对const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了

  1. //file_1.cc定义并且初始化一个常量,该常量能被其他文件访问
  2. extern const int bufSize=fcn();
  3. // file_1.h
  4. extern const int bufSize;//与file_1.cc中定义的bufSize是同一个

const的引用


可以把引用绑定到const对象上,就像绑定到其他对象上一样,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。
tips:对常量的引用需要将引用声明为常量

  1. const int c=1024
  2. const int &r1=c; //正确
  3. int &r2=c; //错误

初始化和对const的引用

之前提到,对于引用来说类型必须与引用的对象一致,但是有两个例外:

  • 第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值

    1. int i=42;
    2. const int &r1=i;//合法,允许用常量引用引用一个普通的常量
    3. int &r4=r1*2 //不合法,不能用非常量引用引用一个常量;

    常量引用被绑定时究竟发生了什么:

    1. double dval =3.14;
    2. const int &ri=dval;

    ri医用的是一个int 型的常量,但是dval是一个double类型的数据,因此会进行数据转换:

    1. const int temp=dval;
    2. const int &ri=temp;

    编译器会先生成一个临时变量temp,然后将dval转换成int型,然后让ri绑定此临时变量;
    因此const引用可以用任意表达式作为初值,因为他并不会改变这个值,但是非const就不可以了 :::info 也就是说如果ri不是一个常量引用,那么ri会允许对于引用对象进行改变,而实际改变的可能是这个临时变量,而这样必然会引起错误;而对于常量引用而言,因为不会更改引用对象,所以允许这样。 :::

  • 对const的引用可能引用一个井非const的对象

因为常量引用的对象可能是一个非常量,那么这个值也可以通过其他方式进行修改
edge.PNG
r2绑定(非常量)整数l是合法的行为。然而,不允许通过r2修改l的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过像r1一样绑定到l的其他引用来修改。

:::tips 简单来说,常量引用可以以任何变量作为对象;并且常量只能用常量引用; :::

指向常量的指针pointer to const


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

  1. const double pi=3.14;
  2. const double *ptr=&pi; //合法
  3. double dval=3.14;
  4. ptr=&dval; //合法

:::tips 和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,因此和常量引用一样,也可以指向任意的对象; :::

常量指针_const pointer


允许把指针本身定为常量。常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值

  1. int errNumb=0;
  2. int *const curErr=&errNumb; //curErr是常量指针,将一直指向errNumb;
  3. const double pi=3.14;
  4. const double *const pip=&pi; //pip是指向常量的常量指针,本身不能被修改,指向的对象也不能;

指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型。

顶层常量


例如指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题; :::tips 用名词顶层const(top-level const)表示本身是个常量,而用名词底层const( low-level const)表示指针所指、所引用的对象是一个常量。 :::

常量表达式


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

constepr变量


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

  1. constexpr int mf=20; //right
  2. constexpr int limit =mf=1;//right;
  3. constexpr int sz=size(); //除非是constexpr函数

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

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

  1. const int *p=nullptr; //指向常量的指针
  2. constexpr int *q=nullptr; //常量指针

:::tips 限定符constexpr仅 对指针有效,与指针所指的对象无关;
constexpr把它所定义的对象置为了顶层const :::

处理类型

类型别名


类型别名( type alias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易千理解和使用,有两种方法可以定义类型别名:

  • typedef
  • 别名声明

含有typedef 的声明语句定义的不再是变量而是类型别名。和以前的声明语句一样,这里的声明符也可以包含类型修饰,从而也能由基本数据类型构造出复合类型来。

  1. typedef double wage;
  2. typedef wage base, *p;
  3. using SI=sales_item; //别名声明

指针、常量和类型别名

  1. typedef char *pstring;
  2. const pstring cstr=0;//指向char的常量指针
  3. const pstring *ps; //ps是一个指针,对象是指向char的常量指针

和过去一样,const是对给定类型的修饰。pstring实际上是指向chra的指针,因此,const pstring就是指向char的常量指针,而非指向常量字符的指针。

auto类型说明符


C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应一种特定类型的说明符(比如double)不同,auto让编译器通过初始值来推算变量的类型。显然,auto定义的变量必须有初始值。

  1. auto itm =val1+val2;
  2. int i=0,&r=i;
  3. auto a=r; //a是int
  4. const int ci=i;
  5. auto e=&ci //e是指向int常量的指针(对常量对象取地址是一种底层const)

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

  • auto一般会忽略掉顶层const,同时底层const则会保留下来,比如当初始值是一个指向常量的指针时。
  • 若是希望auto类型的变量是顶层const 那么可以显式的声明const auto p=ci,此时推断的结果不变,但是因为显式地加上了const,所以便是顶层const :::

decltype类型指示符


希望从表达式的类型推断出要定义的变扯的类型,但是不想用该表达式的值初始化变量。为了满足这一要求,C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。

  1. decltype(f()) sum=x;
  2. const int ci=0,&cj=ci;
  3. decltype(ci) x=0; //const int
  4. decltype(cj) y=x; //const int &

decltype处理顶层const和引用的方式与auto有些许不同。 :::info

  • 如果decltype使用的参数是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)
  • 如果表达式的内容是表达式(解引用操作),则decltype将得到引用类型。 ::: 如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型就是int&,而非int
    decltype和auto的另一处重要区别是,decltype的结果类型与表达式形式密切相关。有一种情况要特别注意:对于decltype所用的表达式来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。 :::tips

  • 加上括号会把变量当作是表达式,变量本身是可以作为赋值语句左值的特殊表达式,因此会得出引用类型

  • decltype并不会计算表达式的值。只会根据表达式来得到类型,因此作为参数的式子并不会执行? :::

    1. decltype((i)) d;//得到的是引用,必须初始化
    2. decltype(i) e;//right

    小结

  • auto返回值是编译器优化后的结果,引用、顶层const可能被忽略

  • decltype会直接返回变量的类型,包括顶层const和引用
  • auto 类型说明符用编译器计算变量的初始值来推断其类型,而decltype虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值。

    自定义数据结构


    从最基本的层面理解,数据结构是把一组相关的数据元素组织起来然后使用它们的策略和方法。

    头文件

    为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。
    有必要在书写头文件时做适当处理,使其遇到多次包含的情况也能安全和正常地工作。

    预处理器


    确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor) ,它由C++语言从C语言继承而来。预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。之前已经用到了一项预处理功能#include ,当预处理器看到#include标记时就会用指定的头文件的内容代替#include

头文件保护符(header guard) ,头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变益,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变旦未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。

  1. #ifndef SALES_DATA_H
  2. #define SALES_DATA_H
  3. #include<string>
  4. struct Sales_data{
  5. ---
  6. };
  7. #endif

:::tips 头文件保护是一种习惯性做法 :::

小结