【C++】《C++ Primer 5th》笔记-Chapter2-变量和基本类型

笔记:
一、基本内置类型
1、选择类型的一些准则:
①当明确知晓数值不可能为负时,选用无符号类型;
②使用int执行整数运算。在实际应用中,short常常显得太小而long一般和int有一样的尺寸。如果你的数值超过了int的表示范围,选用long long。
③在算术表达式中不要使用char或bool,只有在存放字符或布尔值时才使用它们。因为类型char在一些机器上是有符号的,而在另一些机器上又是无符号的,所以如果使用char进行运算特别容易出问题。如果你需要使用一个不大的整数,那么明确指定它的类型是signed char或者unsigned char。
④执行浮点数运算选用double,这是因为float通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。事实上,对于某些机器来说,双精度运算甚至比单精度还快。long double提供的精度在一般情况下是没有必要的,况且它带来的运行时消耗也不容忽视。

2、类型所能表示的值的范围决定了转换的过程:
①当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false,否则结果为true。
②当我们把一个布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1。
③当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。
④当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占空间超过了浮点类型的容量,精度可能有损失。
⑤当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char所得的结果是255。
⑥当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。

3、如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动转换成无符号数。

4、以0开头的整数代表八进制数,以0x或0X开头的代表十六进制数。

5、默认情况下,十进制字面值是带符号数的,八进制和十六进制字面值既可能是带符号的也可能是无符号的。

6、编译器在每个字符串的结尾处添加一个空字符('\0'),因此,字符串字面值的实际长度要比它的内容多1。

二、变量
1、初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。

2、C++语言定义了初始化的好几种不同的形式,以下的4条语句都可以做到这一点:
int a = 0;
int a = {0};
int a{0};
int a(0);

3、用花括号来初始化变量的初始化形式被称为列表初始化。当用于内置类型的变量时,这种初始化形式有一个重要的特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错。

4、默认初始化:如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。定义在函数体内部的内置类型变量将不被初始化。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误。绝大多数类都支持无须显式初始化而定义对象,这样的类提供了一个合适的默认值。建议初始化每一个内置类型的变量。

5、C++语言支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
为了支持分离式编译,C++语言将声明和定义区分开来。声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义负责创建与名字关联的实体。
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量:
extern int i;   // 声明i而非定义i
int j;            // 声明并定义j
任何包含了显式初始化的声明即成为定义。extern语句如果包含初始值就不再是声明,而变成了定义:
extern double pi = 3.1416;  // 定义
注意:在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。
变量能且只能被定义一次,但是可以被多次声明。
如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。

6、C++是一种静态类型语言,其含义是在编译阶段检查类型。

7、C++的标识符由字母、数字和下画线组成,其中必须以字母或下画线开头。标识符的长度没有限制,但是对大小写字母敏感。
用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。此外,定义在函数体外的标识符不能以下画线开头。

8、名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束。
作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字。
下面语句中,可以使用作用域操作符"::"来覆盖默认的作用域规则,因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域右侧名字对应的变量。
std::cout << ::a << std::endl;
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。

三、复合类型
1、C++有右值引用和左值引用。通常我们使用术语"引用"时,指的其实是"左值引用"。
引用为对象起了另外一个名字,引用类型引用另外一个类型。通常将声明符写成&d的形式来定义引用类型,其中d是声明的变量名。
注意:引用必须被初始化。一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始化对象一直绑定在一起。因为无法令引用重新绑定到另一个对象,因此引用必须初始化。

2、引用即别名。引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。
为引用赋值,实际上是把值赋给了与引用绑定的对象。获取引用的值,实际上是获取了与引用绑定的对象的值。以引用作为初始值,实际上是以与引用绑定的对象作为初始值。
注意:因为引用本身不是一个对象,所以不能定义引用的引用。
注意:引用只能绑定在对象上,而不能与字面值或某个表达式的结果绑定在一起。
注意:除特殊情况外,所有引用的类型都要和与之绑定的对象严格匹配。

3、指针与引用相比有很多不同点,其一是指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象;其二是指针无须在定义时赋初值。
和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
指针存放某个对象的地址,要想获取该地址,需要使用取地址符(操作符&)。
引用不是对象,没有实际地址,所以不能定义指向引用的指针。
注意:除特殊情况外,所有指针的类型都要和它所指向的对象严格匹配。因为在声明语句中指针的类型实际上被用于指定它所指向对象的类型,所以二者必须匹配。如果指针指向了一个其他类型的对象,对该对象的操作将发生错误。

4、指针的值(即地址)应属下列4种状态之一:
①指向一个对象;
②指向紧邻对象所占空间的下一个位置;
③空指针,意味着指针没有指向任何对象;
④无效指针,也就是上述情况之外的其他值。
注意:试图拷贝或以其他形式访问无效指针的值都将引发错误。

5、如果指针指向了一个对象,则允许使用解引用符(操作符*)来访问该对象。
对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值。
解引用操作仅适用于那些确实指向了某个对象的有效指针。

6、空指针不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。
以下列出几个生成空指针的方法:
int *p1 = nullptr;   // 等价于int *p1 = 0;
int *p2 = 0;         // 直接将p2初始化为字面常量0
int *p3 = NULL;         // 等价于int *p3 = 0; //需要首先#include cstdlib
NULL是一个预处理变量,其值就是0.
当用到一个预处理变量时,预处理器(预处理器是运行于编译过程之前的一段程序)会自动地将它替换为实际值,因此用NULL初始化指针和用0初始化指针是一样的。现在的C++程序最好使用nullptr,同时尽量避免使用NULL。
建议初始化所有的指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针,否则就把它初始化为nullptr或者0.

7、任何非0指针对应的条件值都是true。
对于两个类型相同的合法指针,可以比较相等或者不相等。如果两个指针存放的地址相同,则它们相等,这里两个指针存放的地址值相同(两个指针相等)有三种可能:它们都为空、都指向同一个对象、或者都指向了同一个对象的下一个地址。
注意:一个指针指向某对象,同时另一个指针指向另外对象的下一地址,此时也有可能出现这两个指针值相同的情况,即指针相等。

8、void*是一种特殊的指针类型,可用于存放任意对象的地址。
利用void*指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入和输出、赋给另外一个void*指针。
不能直接操作void*指针所指向的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。

9、int* p1, p2;  // 容易误导。其实p1是指向int的指针,p2是int。

10、引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用。
int *p;        // p是一个int型指针
int *&r = p;   // r是一个对指针p的引用
要理解r的类型到底是什么,最简单的方法是从右往左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用是一个int指针。

四、const限定符
1、const int bufSize = 512; // bufSize是一个常量
任何试图为bufSize赋值的行为都将引发错误。因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化。
2、ci的常量特征仅仅在执行改变ci的操作时才会发挥作用。
int i = 42;
const int ci = i;   // 正确

3、当以编译时初始化的方式定义一个const对象时,编译器将在编译过程中把用到该变量的地方都替换成对应的值。
4、默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
某些情况下,const变量的初始值不是一个常量表达式,但又确实需要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量,我们需要只在一个文件中定义const,而在其他多个文件中声明并使用它。解决的办法是,对于const常量不管是声明还是定义都添加extern关键字,这样只需定义一次就行了:
extern const int bufSize = func();  // file1.cc定义并初始化了一个常量,该常量能被其他文件访问(因为bufSize是一个常量,必须用extern加以限制使其被其他文件使用)
extern const int bufSize;            // file1.h定义的bufSize和与file1.cc定义的是同一个(用extern限定是为了指明bufSize并非本文件所独有,它的定义将在别处出现)
注:如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。

5、与普通的引用不同的是,对常量的引用不能被用作修改它所绑定的对象。
试图让一个非常量引用指向一个常量对象是错误的。
引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。

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

7、在这种情况下,为了确保让ri绑定一个整数,编译器把下面语句变成"const int temp = dval; const int &ri = temp;",ri绑定了一个临时量对象。所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。注意当ri不是常量时,此时绑定的对象是一个临时量而非dval,这并非大家所希望看到的把引用绑定到临时量上。
double dval = 3.14;
const int &ri = dval;

8、常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值:
int i = 42;
int &r1 = i;        // 引用ri绑定对象i
const int &r2 = i;    // r2也绑定对象i,但是不允许通过r2修改i的值

9、指向常量的指针不能用于改变其所指对象的值。
要想存放常量对象的地址,只能使用指向常量的指针。
const double pi = 3.14;
double *ptr = &pi;            // 错误:ptr是一个普通指针
const double *cptr = &pi;    // 正确:cptr可以指向一个双精度常量

10、上面提过,指针的类型必须与其所指对象的类型一致,但是有两个例外,第一种例外的情况是允许令一个指向常量的指针指向一个非常量对象。
double dval = 3.14;
const double *cptr = &dval; // 正确:但是不能通过cptr改变dval的值

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

12、const指针(常量指针)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。
把*放在const关键字之前用以说明指针是一个常量,意味着不变的是指针本身的值而非指向的那个值:
int *const curErr = &errNum; // curErr将一直指向errNum。

13、从右向做阅读,*const表明pip是一个常量指针,它指向的对象是一个双精度浮点数常量。
const double pi = 3.14159;
const double *const pip = &pi; // pip是一个指向常量对象的常量指针

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

15、指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题,用名词顶层const便是指针本身就是个常量,而用名词底层const表示指针所指的对象是一个常量。
int i = 0;
int *const p1 = &i;          // 不能改变p1的值,这是一个顶层const
const int ci = 42;            // 不能改变c1的值,这是一个顶层const
const int *p2 = &ci;        // 允许改变p2的值,这是一个底层const
const int *const p3 = p2;    // 靠右的const是顶层const,靠左的是底层const
const int &r = ci;            // 用于声明引用的const都是底层const

16、当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响。
但底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换为常量,反之则不行。

17、常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定。C++11允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。一般来说,如果你认定变量是一个常量表达式,那就把它声明成constexpr类型。

18、尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。
函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。

19、必须明确一点,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:
const int *p = nullptr;        // p是一个指向整型常量的指针
constexpr int *q = nullptr;    // q是一个指向整数的常量指针,因为constexpr把它所定义的对象置为了顶层const

20、和其他常量指针类似,constexpr指针既可以指向常量,也可以指向一个非常量。

五、处理类型
1、类型别名是一个名字,它是某种类型的同义词。有两种方法可用于定义类型别名。
①传统的方法是使用关键字typedef;
typedef double wages;
typedef wages base, *p;    // base是double的同义词,p是double*的同义词
②使用别名声明来定义类型的别名;
using SI = Sales_item;    // SI是Sales_item的同义词

2、特别注意以下例子:
typedef char *pstring;
const pstring cstr = 0;    // cstr是指向char的常量指针
const pstring *ps;        // ps是一个指针,它的对象是指向char的常量指针
pstring实际上是指向char的指针,因此const pstring就是指向char的常量指针,而非指向常量字符的指针。
注意:不能直接把类型别名替换成它本来的样子来理解该语句:
const char *cstr = 0;    // 是对const pstring cstr的错误理解   
声明语句中用到pstring时,其基本类型是指针。可是用char*重写了声明语句后,数据类型就变成了char,*成为了声明符的一部分。这样改写的结果是,const char成立基本数据类型。前后两种声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。

3、用auto类型说明符就能让编译器替我们去分析表达式所属的类型。
auto让编译器通过初始值来推算变量的类型,所以,auto定义的变量必须有初始值。
使用auto也能在一条语句中声明多个变量。因为一条声明语句只能由一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样。

4、auto一般会忽略掉顶层const,同时底层const则会保留下来。
如果希望推断出的auto类型是一个顶层的const,需要明确指出:
int i = 0;
const int ci = i;
auto b = ci;        // b是一个整数(ci的顶层const特性被忽略掉了)
const auto f = ci;  // ci的推演类型是int,f是const int
还可以将引用的类型设为auto,此时原来的初始化规则仍然适用:
auto &g = ci;        // g是一个整形常量引用,绑定到ci
auto &h = 42;        // 错误:不能为非常量引用绑定字面值
const auto &j = 42;    // 正确:可以为常量引用绑定字面值
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了。

5、类型说明符decltype的作用是选择并返回操作数的数据类型。
在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:
decltype(f()) sum = x;    // sum的类型就是函数f的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。

6、decltype处理顶层const和引用的方式与auto有些许不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)。
注意:引用从来都作为其所指对象的同义词出现,只有用在decltype处是个例外。

7、r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0。另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。解引用指针可以得到指针所指的对象,而且还能给这个对象赋值,因此decltype(*p)的结果类型就是int&,而非int。
int i = 42, *p = &i, &r = i;
decltype(r+0) b;    // 正确:加法的结果是int,因此b是一个(未初始化)int
decltype(*p) c;        // 错误:c是int&,必须初始化

8、如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型:
decltype((i)) d;        // 错误:d是int&,必须初始化
decltype(i) e;            // 正确:e是一个(未初始化)int
切记:decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。

六、自定义数据结构
1、一般来说,最好不要把对象的定义和类的定义放在一起。这么做无异于把两种不同实体的定义混在了一条语句里,一会儿定义类,一会儿又定义变量,显然这是一种不被建议的行为。

2、对类内初始值的限制:或者放在花括号里,或者放在等号右边,记住不能使用圆括号。

3、类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。

4、头文件通常包含那些只能被定义一次的实体,如类、const和constexpr变量等。
有必要在书写头文件时做适当处理,使其遇到多次包含的情况也能安全和正常地工作。
头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。

5、确保头文件多次包含仍能安全工作的常用技术是预处理器。预处理是在编译之前执行的一段程序,可以部分地改变我们所写的程序。
C++程序还会用到的一项预处理功能是头文件保护符,头文件保护符依赖于预处理变量。预处理变量有两张状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。使用这些功能就能有效地防止重复包含的发生。

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

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


一些术语:
1、const是一种类型修饰符,用于说明永不改变的对象。const对象一旦定义就无法再赋新值,所以必须初始化。
2、常量表达式:能在编译时计算并获取结果的表达式。
3、constexpr是一种函数,用于代表一条常量表达式。
4、声明:声称存在一个变量、函数或是别处定义的类型。名字必须在定义或者声明之后才能使用。
5、全局作用域的内置类型对象初始化为0;局部作用域的对象未被初始化即拥有未定义的值。
6、定义:为某一特定类型的变量申请存储空间,可以选择初始化该变量。名字必须在定义或者声明之后才能使用。
7、类内初始值:在声明类的数据成员时同时提供的初始值,必须置于等号右侧或花括号内。
8、被初始化:变量在定义的同时被赋予初始值,变量一般都应该被初始化。
9、预处理变量:由预处理器管理的变量。在程序编译之前,预处理器赋值将程序中的预处理变量替换成它的真实值。
10、分离式编译:把程序分割为多个独立文件的能力。
11、顶层const是一个const,规定某对象的值不能改变。
12、void*:可以指向任意非常量的指针类型,不能指向解引用操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值